主题:LLM 如何被提供可用工具、工具调用协议、MCP 工具发现机制、以及多模型参数差异的抹平方式。
一、工具调用整体流程
工具调用(Function Calling / Tool Use)本质是一套「声明 → 决策 → 执行 → 回填」的循环:
- 在请求里声明「有哪些工具可用」(tools 定义)
- LLM 根据用户问题决定「是否调用 / 调用哪个 / 传什么参数」
- 应用程序真正执行工具,拿到结果
- 把结果回填给 LLM,LLM 生成最终自然语言回答
关键认知:LLM 本身不执行工具,它只输出「我想调用某工具 + 参数」的结构化意图,真正的执行由应用代码负责。
二、如何提供可用工具(工具声明)
主流做法:在 API 请求中传入一个tools数组,每个工具用 JSON Schema 描述。
OpenAI 格式
{"model":"gpt-4","messages":[],"tools":[{"type":"function","function":{"name":"get_weather","description":"查询指定城市的实时天气","parameters":{"type":"object","properties":{"city":{"type":"string","description":"城市名,如 杭州"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["city"]}}}],"tool_choice":"auto"}Anthropic (Claude) 格式
{"model":"claude-sonnet-4","messages":[],"tools":[{"name":"get_weather","description":"查询指定城市的实时天气","input_schema":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}]}关键点:
name:工具唯一标识description:最重要,模型靠它判断何时调用,要写清楚用途/触发条件parameters/input_schema:用 JSON Schema 约束参数类型、枚举、必填项
三、工具调用协议(关键要素)
1. 调用控制字段 tool_choice
auto:模型自行决定是否调用none:禁止调用required/any:强制必须调用某工具- 指定具体工具名:强制调用该工具
2. 模型返回工具调用意图
OpenAI:
{"role":"assistant","tool_calls":[{"id":"call_abc123","type":"function","function":{"name":"get_weather","arguments":"{\"city\": \"杭州\"}"}}]}Anthropic:
{"role":"assistant","content":[{"type":"tool_use","id":"toolu_abc123","name":"get_weather","input":{"city":"杭州"}}],"stop_reason":"tool_use"}3. 回填执行结果(协议核心)
执行完工具后,必须用特定角色/类型把结果塞回对话历史,并通过 id 关联:
OpenAI —— 用role: "tool"+tool_call_id:
{"role":"tool","tool_call_id":"call_abc123","content":"{\"temp\": 28, \"desc\": \"晴\"}"}Anthropic —— 用role: "user"里的tool_result+tool_use_id:
{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_abc123","content":"{\"temp\": 28, \"desc\": \"晴\"}"}]}4. 多轮循环
回填后再次请求模型,模型可能继续调用下一个工具(多步/链式调用),或生成最终回答(stop_reason: end_turn/finish_reason: stop)。
5. 并行工具调用
现代模型支持一次返回多个 tool_call。协议要求:
- 每个 tool_call 有独立 id
- 回填时每个结果按 id 一一对应返回,缺一不可
- 应用可并行执行这些工具以提速
6. 常见协议注意事项
| 事项 | 说明 |
|---|---|
| arguments 是字符串 | OpenAI 的 arguments 是 JSON 字符串需二次解析;Anthropic 的 input 已是对象 |
| id 必须匹配 | 回填结果 id 与调用 id 不匹配会报错 |
| 完整对话历史 | 每轮请求需带上包含 tool_call + tool_result 的完整 messages |
| schema 越清晰越准 | description 和 enum 约束能显著降低幻觉/传错参 |
| 错误也要回填 | 工具执行失败时,把错误信息作为 tool_result 返回,让模型决定重试或换方案 |
四、MCP:支持哪些工具是怎么告诉 LLM 的
核心结论:LLM 本身并不直接"知道"有哪些 MCP,它只认标准的 tools 定义。中间的 MCP Client(宿主程序)负责去各 MCP Server 发现工具,再翻译成 LLM 的工具协议喂给它。
整体链路
MCP Server(提供工具) ↑ MCP 协议 (tools/list, tools/call) MCP Client / Host(如 Claude Desktop、IDE、Agent 框架) ↑ 把 MCP 工具翻译成 LLM 的 tools 定义 LLM API 请求 (tools: [...])第一步:宿主如何知道"支持哪些 MCP"—— 靠配置
{"mcpServers":{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/path"]},"github":{"command":"npx","args":["-y","@modelcontextprotocol/server-github"],"env":{"GITHUB_TOKEN":"xxx"}},"my-remote":{"url":"https://example.com/mcp","transport":"sse"}}}宿主启动时按配置逐个连接(本地用 stdio 启子进程,远程用 SSE / HTTP)。
第二步:MCP 协议的"工具发现"(底层 JSON-RPC 2.0)
握手初始化:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{}}}列出工具 tools/list:
// Client → Server{"jsonrpc":"2.0","id":2,"method":"tools/list"}// Server → Client{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"read_file","description":"读取指定路径的文件内容","inputSchema":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}}]}}MCP Server 除了 tools,还能暴露 resources(resources/list)和 prompts(prompts/list),发现机制类似。
第三步:翻译成 LLM 的 tools 定义
宿主把所有 MCP Server 返回的工具聚合,转换成 LLM 工具格式注入 API 请求。MCP 的 inputSchema 与 LLM 的 input_schema/parameters 几乎同构(都是 JSON Schema)。
命名冲突处理:多 Server 可能有同名工具,宿主通常加前缀区分,例如filesystem__read_file、github__create_issue。
第四步:调用回路
1. LLM 返回 tool_use: { name: "filesystem__read_file", input: {...} } 2. 宿主识别前缀,路由到对应 MCP Server 3. 宿主向该 Server 发 tools/call 4. Server 执行,返回 result 5. 宿主把 result 作为 tool_result 回填给 LLMtools/call 示例:
// Client → Server{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/a.txt"}}}// Server → Client{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"文件内容..."}]}}动态更新:工具列表变化通知
{"jsonrpc":"2.0","method":"notifications/tools/list_changed"}宿主收到后重新 tools/list,并在下一轮请求里更新给 LLM 的 tools 定义。
完整图景
配置文件 → 宿主知道「连哪些 MCP Server」 tools/list → 宿主知道「每个 Server 有哪些工具」 Schema 翻译+聚合 → 拼成 LLM 的 tools 定义 API 请求 → 这一刻 LLM 才「知道」有哪些工具可用 tool_use → LLM 决定调用 tools/call → 宿主路由回对应 Server 执行 tool_result 回填 → LLM 生成最终回答一句话总结:支持哪些 MCP 由宿主的配置决定;宿主通过 MCP 的 tools/list 发现工具;再翻译聚合成标准 tools 定义,在每次 API 请求里告诉 LLM。LLM 全程只跟标准工具协议打交道,对 MCP 本身无感知。
五、不同模型参数格式差异的抹平
核心结论:差异主要在「客户端 / Agent 框架层」通过适配器(Adapter / Provider 抽象)抹平。抹平的是「协议格式」,抹不平的是「模型能力和行为」。
抹平发生在哪一层
业务代码 ↓ 统一接口(抹平层) ┌─────────────────────────────┐ │ Provider / Adapter 抽象层 │ │ OpenAIAdapter / ClaudeAdapter / GeminiAdapter │ └─────────────────────────────┘ ↓ 各自原生 API 格式 OpenAI API Claude API Gemini API两种常见实现方式:
- Agent / SDK 框架内置适配:LangChain、LlamaIndex、Vercel AI SDK、Spring AI 等,定义统一的 Tool / Message 抽象,内部为每个厂商写 adapter。
- 网关 / 代理服务:如 LiteLLM、OneAPI,对外统一暴露 OpenAI 格式,内部转译成各厂商格式。
具体抹平了哪些差异(以工具调用为例)
| 差异点 | OpenAI | Anthropic | Gemini | 抹平方式 |
|---|---|---|---|---|
| 工具定义键名 | function.parameters | input_schema | functionDeclarations.parameters | adapter 改字段名,schema 本体同构 |
| 调用意图 | tool_calls[] | content[].tool_use | functionCall | 统一解析成内部 ToolCall |
| 参数载荷 | arguments(JSON 字符串) | input(对象) | args(对象) | adapter 统一 parse 成对象 |
| 结果回填 | role:“tool” + tool_call_id | role:“user” 里 tool_result + tool_use_id | functionResponse | 统一封装成 ToolResult 按厂商拼回 |
| system 提示 | messages 里 role:“system” | 顶层独立 system 字段 | systemInstruction | adapter 搬运到对应位置 |
| 强制调用 | tool_choice | tool_choice | toolConfig.mode | 统一枚举映射 |
本质:一套内部中间表示(IR)
业务定义统一 Tool │ ▼ 内部 IR(中立表示:Message / Tool / ToolCall / ToolResult) │ serialize(按 provider 出站翻译) ▼ 各厂商原生请求格式 │ 调用 API ▼ 各厂商原生响应 │ parse(入站翻译回 IR) ▼ 内部 IR → 业务统一处理伪代码:
classToolCall:# 中立表示id:strname:strargs:dict# 统一是对象,不管厂商用字符串还是对象classOpenAIAdapter:defto_request(self,tools,messages):...defparse_response(self,resp)->list[ToolCall]:# OpenAI 的 arguments 是字符串,这里 json.loads 抹平成 dict...classClaudeAdapter:defto_request(self,tools,messages):...defparse_response(self,resp)->list[ToolCall]:...能抹平 vs 抹不平
能抹平(格式/协议层):
- 字段名、消息结构、system 位置
- 参数是字符串还是对象
- 工具定义、调用、回填的封装形式
- 流式事件格式
抹不平(能力/行为层):
- 是否支持并行工具调用
- 是否支持工具调用本身(老模型/小模型可能不支持,只能降级为提示词模拟 ReAct)
- JSON Schema 支持程度(enum、嵌套对象、$ref 遵守度不同)
- 指令遵循度 / 幻觉率
- 上下文窗口、token 计费、stop_reason 语义差异
- 多模态、思维链(thinking)等特有能力
对能力差异,框架只能做「能力探测 + 降级策略」,而非真正抹平。
结论
- 格式差异:在 Agent / 框架 / 网关的适配层根据 provider 自动抹平,业务代码通常无感。
- 能力差异:抹不平,只能靠能力标记 + 降级 / 兼容策略处理。
- 这也是很多框架有「provider」或「model capability」概念的原因——既做格式翻译,也记录每个模型支持什么,运行时决定走原生工具协议还是模拟方案。