如果你还在用 JSON 定义 AI 工具调用可能已经落后了。最近一项来自 Google DeepMind 的研究揭示了一个被多数开发者忽略的趋势在 14 个主流大语言模型中有 11 个在“代码优先”的工具调用方式上表现显著优于传统的“JSON 模式”。这不仅仅是几个百分点的性能提升它指向了一个更深层的范式转变——AI 正在从被动解析结构化指令转向主动理解和执行代码逻辑。对于开发者而言这意味着什么过去我们习惯于将工具能力封装成一份详尽的 JSON Schema描述函数名、参数类型和说明然后交给模型去“填空”。这种方式看似规范却无形中为模型套上了一层枷锁增加了格式解析的负担和出错的概率。而“代码优先”则反其道而行之直接给模型看一段代码比如一个 Python 函数让它理解这段代码能做什么然后生成相应的调用。模型的表现反而更好了。本文将深入剖析这一转变背后的技术逻辑并通过一个完整的 Python 示例带你亲手实践从“JSON 模式”到“代码优先”的迁移。你会发现这不仅关乎 API 调用格式的选择更是一种更符合大语言模型“思考”方式的工程实践。它能直接提升你构建的 AI Agent 或智能助手的可靠性、灵活性和开发效率。1. 工具调用的演进从 JSON 规范到代码理解要理解“代码优先”为何有效我们得先看看传统的工具调用是如何工作的。1.1 传统 JSON 模式清晰的规范沉重的负担在 AI 应用开发中为了让大语言模型能够使用外部工具如查询数据库、调用 API、执行计算我们需要将工具“描述”给模型。最主流的方式就是提供一份 JSON 格式的工具定义列表。例如一个查询天气的工具可能被这样定义{ tools: [ { type: function, function: { name: get_current_weather, description: 获取指定城市的当前天气, parameters: { type: object, properties: { location: { type: string, description: 城市名称例如北京上海 }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位 } }, required: [location] } } } ] }开发者需要将这份定义连同用户问题如“北京天气怎么样”一起发送给模型。模型的任务是理解用户意图严格遵循 JSON Schema 的格式生成一个合法的函数调用请求例如{name: get_current_weather, arguments: {\location\: \北京\, \unit\: \celsius\}}。这种方式的优点很明显格式统一、机器可读、便于验证。但它的缺点在复杂场景下被放大解析负担模型需要额外学习 JSON Schema 的语法和约束规则。灵活性差任何工具定义的修改如增加可选参数都需要更新 Schema 并重新让模型适应。容错率低生成的 JSON 必须格式完美一个缺少的引号或错误的括号都可能导致调用失败。1.2 代码优先模式让模型“读代码”“代码优先”模式采取了截然不同的思路。它不要求开发者编写冗长的 JSON 描述而是直接提供工具的实现代码或函数签名作为上下文。同样以查询天气为例你提供给模型的可能是一段 Python 代码# 工具函数定义 def get_current_weather(location: str, unit: str “celsius”) - str: “”” 获取指定城市的当前天气信息。 参数: location: 城市名称例如“北京”、“上海”。 unit: 温度单位可选“celsius”摄氏度或“fahrenheit”华氏度默认为“celsius”。 返回: 描述天气的字符串。 “”” # 这里是实际的实现逻辑可能是调用某个天气API # 为示例简化返回模拟数据 return f”{location}的天气是晴朗22度。”然后你直接问模型“请帮我查询北京的天气。” 模型在阅读了上面的函数代码包括函数名、参数、类型注解和文档字符串后它需要生成的是调用这段代码的语句而不是一个 JSON 对象。它可能会生成get_current_weather(“北京”)或weather_info get_current_weather(location”北京”, unit”celsius”)。为什么这样更优更自然的上下文大语言模型在预训练时“阅读”了海量的代码对函数定义、参数传递的理解能力极强。提供代码作为上下文更贴近其原始训练数据分布。信息密度更高一段带有类型注解和文档字符串的代码所包含的语义信息这个工具是干什么的、怎么用通常比等价的 JSON Schema 更丰富、更直接。减少格式约束模型无需精确匹配一个外部的 JSON 结构只需生成符合编程语言语法的调用语句容错性更高。便于迭代当工具函数更新时只需更新代码片段模型能基于新的代码自然适应无需重新学习一套描述规则。Google DeepMind 的研究量化了这种优势。在他们的测试中包括 GPT-4、Claude 3、Gemini 等在内的 11 个模型在代码优先提示下工具调用的准确率和鲁棒性均有显著提升。这对于追求稳定性和效率的生产级 AI 应用来说是一个至关重要的信号。2. 环境准备构建你的代码优先工具调用实验场理论需要实践验证。接下来我们将搭建一个简单的环境对比 JSON 模式和代码优先模式在实际调用中的差异。我们将使用 OpenAI 兼容的 API这里以开源模型为例避免具体 API 密钥问题和 Python 语言。2.1 基础环境配置首先确保你的 Python 环境版本在 3.8 以上。我们将使用openai这个通用客户端库它也能兼容许多提供 OpenAI 兼容接口的服务。# 创建并进入项目目录 mkdir code_first_tools cd code_first_tools # 创建虚拟环境推荐 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate # 安装必要库 pip install openai如果你的后端使用的是本地部署的模型如通过 Ollama、vLLM 等部署的 Llama、Qwen 等你需要确保其 API 端点支持工具调用功能。本文示例将使用一个假设的本地端点http://localhost:11434/v1模拟 Ollama 的 OpenAI 兼容模式。2.2 准备一个模拟工具服务器为了测试工具调用我们需要一个简单的“工具”后端来响应调用。这里我们用 FastAPI 快速创建一个。pip install fastapi uvicorn创建一个名为tool_server.py的文件# tool_server.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import Optional app FastAPI() # 定义请求模型 class WeatherRequest(BaseModel): location: str unit: Optional[str] “celsius” class CalculatorRequest(BaseModel): a: float b: float operation: str # ‘add‘, ’subtract‘, ’multiply‘, ’divide‘ # 工具1获取天气模拟 app.post(“/tools/weather”) async def get_weather(req: WeatherRequest): # 模拟业务逻辑 if req.unit not in [“celsius”, “fahrenheit”]: raise HTTPException(status_code400, detail“单位必须是 celsius 或 fahrenheit”) return { “location”: req.location, “temperature”: 22 if req.unit “celsius” else 71.6, “unit”: req.unit, “condition”: “晴朗” } # 工具2简单计算器 app.post(“/tools/calculator”) async def calculate(req: CalculatorRequest): ops { “add”: lambda x, y: x y, “subtract”: lambda x, y: x - y, “multiply”: lambda x, y: x * y, “divide”: lambda x, y: x / y if y ! 0 else None } if req.operation not in ops: raise HTTPException(status_code400, detail“不支持的操作”) result ops[req.operation](req.a, req.b) if result is None: raise HTTPException(status_code400, detail“除数不能为零”) return {“a”: req.a, “b”: req.b, “operation”: req.operation, “result”: result} if __name__ “__main__”: import uvicorn uvicorn.run(app, host“0.0.0.0”, port8000)运行这个服务器python tool_server.py服务器将在http://localhost:8000启动提供了两个工具端点。3. 传统 JSON 模式工具调用实现现在我们首先用传统 JSON 模式的方式让 AI 模型来调用我们的工具。创建一个json_mode_client.py文件# json_mode_client.py import json import requests from openai import OpenAI # 配置客户端指向你的模型服务此处以本地Ollama为例 client OpenAI( base_url“http://localhost:11434/v1”, # 请替换为你的实际API端点 api_key“ollama”, # 本地模型可能不需要有效的key但需占位 ) # 1. 定义工具列表JSON Schema格式 tools [ { “type”: “function”, “function”: { “name”: “get_weather”, “description”: “获取指定城市的当前天气信息。”, “parameters”: { “type”: “object”, “properties”: { “location”: { “type”: “string”, “description”: “城市名称例如‘北京’、‘上海’。” }, “unit”: { “type”: “string”, “enum”: [“celsius”, “fahrenheit”], “description”: “温度单位默认为‘celsius’。” } }, “required”: [“location”] } } }, { “type”: “function”, “function”: { “name”: “calculate”, “description”: “执行简单的二元算术运算。”, “parameters”: { “type”: “object”, “properties”: { “a”: {“type”: “number”, “description”: “第一个数字。”}, “b”: {“type”: “number”, “description”: “第二个数字。”}, “operation”: { “type”: “string”, “enum”: [“add”, “subtract”, “multiply”, “divide”], “description”: “运算类型加(add)、减(subtract)、乘(multiply)、除(divide)。” } }, “required”: [“a”, “b”, “operation”] } } } ] # 2. 模拟工具执行函数实际会调用我们的tool_server def execute_tool(tool_name: str, arguments: dict): “””根据工具名和参数调用对应的本地API。“”” base_url “http://localhost:8000” if tool_name “get_weather”: response requests.post(f“{base_url}/tools/weather”, jsonarguments) elif tool_name “calculate”: response requests.post(f“{base_url}/tools/calculator”, jsonarguments) else: return {“error”: f“未知工具: {tool_name}”} if response.status_code 200: return response.json() else: return {“error”: response.text} # 3. 与模型对话处理工具调用 def chat_with_ai_json_mode(user_query: str): messages [{“role”: “user”, “content”: user_query}] # 第一步获取模型的回复其中可能包含工具调用请求 response client.chat.completions.create( model“llama3.1”, # 替换为你的实际模型名称 messagesmessages, toolstools, tool_choice“auto”, # 让模型决定是否调用工具 ) message response.choices[0].message messages.append(message) # 将模型的回复加入对话历史 # 检查回复中是否包含工具调用 if message.tool_calls: print(f“模型请求调用工具...”) for tool_call in message.tool_calls: tool_name tool_call.function.name tool_args json.loads(tool_call.function.arguments) print(f“ 调用工具 ‘{tool_name}‘参数: {tool_args}”) # 执行工具 result execute_tool(tool_name, tool_args) print(f“ 工具返回: {result}”) # 将工具执行结果作为新消息送回给模型 messages.append({ “role”: “tool”, “tool_call_id”: tool_call.id, “content”: json.dumps(result) }) # 获取模型基于工具结果的最终回答 second_response client.chat.completions.create( model“llama3.1”, messagesmessages, ) final_message second_response.choices[0].message print(f“\n最终回答: {final_message.content}”) return final_message.content else: print(f“模型直接回答: {message.content}”) return message.content if __name__ “__main__”: # 测试查询 query “北京现在的温度是多少用摄氏度表示。” print(f“用户问题: {query}”) chat_with_ai_json_mode(query)关键点解析tools列表这就是我们提供给模型的“工具说明书”完全是 JSON Schema 格式。tool_choice“auto”让模型自主决定是否需要以及调用哪个工具。模型返回的tool_calls包含了它希望调用的函数名和一个 JSON 格式的参数字符串。我们解析这个 JSON 字符串调用本地工具服务器再将结果以 JSON 字符串形式传回给模型。运行这个客户端确保tool_server.py在运行并正确配置了 OpenAI 客户端指向你的模型服务你会看到模型成功解析了用户问题生成了符合 JSON Schema 的调用参数并得到了结果。4. 代码优先模式工具调用实现接下来我们实现代码优先模式。核心思想是不提供 JSON Schema而是在系统提示词System Prompt或上下文里直接给出工具的函数定义代码。创建一个code_first_client.py文件# code_first_client.py import json import requests from openai import OpenAI client OpenAI( base_url“http://localhost:11434/v1”, api_key“ollama”, ) # 代码优先的关键将工具定义写成代码字符串 TOOLS_CODE_CONTEXT “”” 你是一个智能助手可以调用以下 Python 工具函数来帮助用户。 请根据用户的问题决定是否需要调用工具并生成对应的 Python 调用代码。 可用的工具函数 1. 获取天气信息 python def get_weather(location: str, unit: str “celsius”) - dict: “”” 获取指定城市的当前天气信息。 参数: location (str): 城市名称例如“北京”、“上海”。 unit (str): 温度单位可选“celsius”摄氏度或“fahrenheit”华氏度。默认为“celsius”。 返回: dict: 包含天气信息的字典例如 {‘location‘: ’北京‘, ’temperature‘: 22, ’unit‘: ’celsius‘, ’condition‘: ’晴朗‘}。 “”” # 实际实现会调用内部API此处为示意。 import requests response requests.post(“http://localhost:8000/tools/weather”, json{“location”: location, “unit”: unit}) return response.json()执行计算def calculate(a: float, b: float, operation: str) - dict: “”” 执行简单的二元算术运算。 参数: a (float): 第一个数字。 b (float): 第二个数字。 operation (str): 运算类型必须是 ‘add‘, ’subtract‘, ’multiply‘, ’divide‘ 之一。 返回: dict: 包含计算结果的字典例如 {‘a‘: 5, ’b‘: 3, ’operation‘: ’add‘, ’result‘: 8}。 “”” # 实际实现会调用内部API此处为示意。 import requests response requests.post(“http://localhost:8000/tools/calculator”, json{“a”: a, “b”: b, “operation”: operation}) return response.json()调用规则如果你决定调用工具请在回复中只输出你要执行的 Python 调用代码。代码必须是有效的、可执行的单个语句或一个简短的代码块。不要输出任何解释性文字。如果你不需要调用工具请直接给出自然语言回答。 “””def execute_generated_code(code_str: str): “””在一个安全的沙箱环境中执行模型生成的代码并捕获返回结果。“”” # 注意在生产环境中执行任意生成的代码是极度危险的 # 这里仅为演示我们做极度简化的“执行”解析出函数调用然后用我们的 execute_tool 去执行。 # 更安全的做法是使用严格的解析器如AST提取函数名和参数或者引导模型生成一个可解析的中间格式。 print(f“尝试解析并执行代码: {code_str}”) # 这是一个非常简陋的解析示例仅用于演示概念。 # 假设模型生成的是类似get_weather(“北京”)或result calculate(5, 3, ‘add’)的代码。 # 在实际应用中你需要更鲁棒的代码解析和映射逻辑。 # 此处我们直接信任并eval仅用于封闭的演示环境切勿在生产中使用 try: # 限制可用的命名空间 local_vars {“get_weather”: None, “calculate”: None} # 占位实际不会在这里执行 # 真正的工具执行函数 def local_get_weather(location, unit“celsius”): response requests.post(“http://localhost:8000/tools/weather”, json{“location”: location, “unit”: unit}) return response.json() def local_calculate(a, b, operation): response requests.post(“http://localhost:8000/tools/calculator”, json{“a”: a, “b”: b, “operation”: operation}) return response.json() # 将安全的执行函数放入命名空间 safe_dict {“get_weather”: local_get_weather, “calculate”: local_calculate, “requests”: requests} #警告eval 有安全风险此处仅用于演示。生产环境必须使用AST解析等安全方式。result eval(code_str, {“builtins”: {}}, safe_dict) return result except Exception as e: return {“error”: f“执行生成的代码时出错: {e}”}def chat_with_ai_code_first(user_query: str): messages [ {“role”: “system”, “content”: TOOLS_CODE_CONTEXT}, {“role”: “user”, “content”: user_query} ]response client.chat.completions.create( model“llama3.1”, # 替换为你的实际模型名称 messagesmessages, temperature0.1, # 降低随机性使输出更稳定 ) ai_response response.choices[0].message.content print(f“模型原始回复:\n{ai_response}”) # 判断回复是代码还是自然语言 if ai_response.strip().startswith(“def “) or “get_weather” in ai_response or “calculate” in ai_response: # 看起来模型生成了代码调用 print(“\n检测到代码调用尝试执行...”) tool_result execute_generated_code(ai_response.strip()) print(f“工具执行结果: {tool_result}”) # 将结果反馈给模型让其生成最终回答 follow_up_messages messages [ {“role”: “assistant”, “content”: ai_response}, {“role”: “user”, “content”: f“工具执行的结果是: {tool_result}。请根据这个结果回答我最初的问题。”} ] second_response client.chat.completions.create( model“llama3.1”, messagesfollow_up_messages, ) final_answer second_response.choices[0].message.content print(f“\n最终回答: {final_answer}”) return final_answer else: # 模型直接给出了自然语言回答 print(f“\n模型直接回答: {ai_response}”) return ai_responseifname “main”: query “计算一下15乘以7等于多少” print(f“用户问题: {query}”) chat_with_ai_code_first(query)**模式对比与核心转变** 1. **提示词变革**系统提示词 (TOOLS_CODE_CONTEXT) 不再是冰冷的 JSON 列表而是一段包含完整函数定义包括文档字符串和类型提示的“代码文档”。这更接近人类程序员阅读 API 文档的方式。 2. **输出格式**我们要求模型在需要调用工具时直接输出 Python 调用代码。这解放了模型让它用更擅长的方式代码生成来回应。 3. **执行引擎**我们需要一个“执行器”来运行模型生成的代码。示例中简陋的 eval **仅用于演示概念**。在生产环境中你必须使用更安全的方式例如 * 解析生成的代码的抽象语法树 (AST)只允许特定的函数调用。 * 使用沙箱环境如 restrictedpython。 * 或者引导模型输出一个结构化的中间表示但这又可能走回老路。更优雅的方式是模型服务本身支持代码优先作为原生特性。 ## 5. 运行对比与效果分析 分别运行两个客户端处理相同的问题观察其过程和结果。 **测试用例1**“北京现在的温度是多少用摄氏度表示。” * **JSON 模式**模型需要理解 location 和 unit 参数并生成 {name: get_weather, arguments: {\location\: \北京\, \unit\: \celsius\}}。任何格式错误都会导致失败。 * **代码优先模式**模型阅读了 get_weather 函数的定义它可能生成 get_weather(北京) 或 get_weather(location北京, unitcelsius)。后者利用了关键字参数可读性更好也更符合编程习惯。 **测试用例2**“计算一下15乘以7等于多少” * **JSON 模式**模型需要匹配 calculate 函数的 Schema理解 operation 枚举中 multiply 对应乘法生成 {name: calculate, arguments: {\a\: 15, \b\: 7, \operation\: \multiply\}}。 * **代码优先模式**模型看到 calculate(a, b, operation) 的定义可能直接生成 calculate(15, 7, multiply)。对于模型而言将“乘以”映射到字符串 multiply 比映射到一个复杂的 JSON 路径更直观。 **潜在优势体现** * **复杂参数**如果工具函数有一个参数是复杂对象如 Dict[str, List[int]]在 JSON 模式中描述其 Schema 会非常繁琐且容易出错。而在代码优先中只需在 Python 函数签名中使用 param: Dict[str, List[int]] 即可模型对类型注解的理解能力很强。 * **默认参数**代码中的默认参数如 unit: str celsius能被模型自然理解并利用而在 JSON Schema 中描述默认值需要额外字段。 * **错误反馈**如果模型生成的调用代码有语法错误Python 解释器会给出清晰的错误信息这比解析一个格式错误的 JSON 字符串更容易诊断。 ## 6. 代码优先模式的工程化挑战与最佳实践 虽然代码优先模式在理论上更优但将其投入生产环境需要考虑几个关键问题。 ### 6.1 安全性绝对禁止直接执行任意代码 上面的示例使用了 eval这是**极其危险**的做法绝不能在真实项目中使用。模型可能被诱导生成恶意代码。 **安全实践建议** 1. **使用 AST 解析**解析模型生成的代码只提取函数调用名和参数值然后映射到内部的安全执行函数。 python import ast def safe_extract_call(code_str: str): try: tree ast.parse(code_str, mode‘eval’) if isinstance(tree.body, ast.Call): func_name tree.body.func.id args [ast.literal_eval(arg) for arg in tree.body.args] kwargs {kw.arg: ast.literal_eval(kw.value) for kw in tree.body.keywords} return func_name, args, kwargs except (SyntaxError, ValueError): pass return None, [], {} 2. **严格的白名单机制**只允许调用预先注册在安全列表中的函数。 3. **沙箱环境**对于必须执行代码的场景使用如 Docker 容器或专门的代码沙箱库进行隔离并严格限制资源CPU、内存、网络、文件系统。 ### 6.2 提示词工程引导模型生成规范的代码 你需要精心设计系统提示词明确告诉模型输出格式。例如 * “请只输出 Python 调用代码不要包含任何解释。” * “如果需要调用多个工具请将每个调用放在单独的一行。” * “如果工具调用结果需要赋值给变量请使用 result_x 的形式。” ### 6.3 错误处理与重试 模型生成的代码可能不完美如变量名拼写错误、参数顺序不对。你的系统需要具备 * **语法检查**在尝试执行前进行基本的语法验证。 * **语义验证**检查参数类型、数量是否符合工具函数要求。 * **优雅降级**如果代码执行失败可以将错误信息反馈给模型要求它修正或改用自然语言回答。 ### 6.4 与现有框架集成 目前像 LangChain 或 LlamaIndex 这样的主流 AI 应用框架其内置的“工具”抽象大多仍围绕 JSON Schema 设计。要采用代码优先模式你可能需要 1. 自定义 Tool 类使其接受代码字符串作为定义。 2. 重写代理Agent的推理逻辑使其能够处理代码形式的工具调用输出。 3. 或者等待框架原生支持这一模式。研究论文的发布通常会推动开源社区的快速跟进。 ## 7. 总结面向开发者的范式迁移 “工具调用转向代码优先”并非一个微小的技术调整而是一个思维方式的转变。它要求开发者从“为机器定义协议”转向“为模型提供文档”。 * **对模型而言**阅读代码比解析 JSON Schema 更自然这利用了其预训练阶段获得的大量代码理解能力从而表现出更高的准确性和鲁棒性。 * **对开发者而言**维护一套代码化的工具定义可能比维护一份等价的 JSON Schema 更简单、更直观尤其当工具接口发生变化时。 * **对应用架构而言**这意味着工具层与模型层的耦合方式发生了变化从严格的格式契约变为更灵活的语义理解。这为构建更复杂、动态的工具组合提供了新的可能性。 当前这一模式尚未在所有模型和平台中普及但 DeepMind 的研究已经指明了清晰的效率提升路径。作为开发者现在正是开始实验和储备相关知识的好时机。你可以从内部工具、非关键性任务开始尝试代码优先模式评估它在你的技术栈和用例中的实际效果为即将到来的范式迁移做好准备。 **下一步你可以** 1. 在你常用的模型如 GPT-4、Claude 3、DeepSeek上用同一组工具分别测试 JSON 和代码优先模式记录准确率差异。 2. 尝试用 AST 解析等安全方式构建一个生产可用的代码优先工具调用执行引擎。 3. 关注 LangChain、LlamaIndex 等框架的更新看它们是否会引入对代码优先模式的原生支持。 技术的进化往往源于对底层约束的重新思考。当我们将工具调用从“格式匹配”问题重构为“代码理解”问题我们或许正在打开一扇通往更强大、更灵活 AI 应用的大门。