LLM 工具调用与 MCP 机制
主题LLM 如何被提供可用工具、工具调用协议、MCP 工具发现机制、以及多模型参数差异的抹平方式。一、工具调用整体流程工具调用Function Calling / Tool Use本质是一套「声明 → 决策 → 执行 → 回填」的循环在请求里声明「有哪些工具可用」(tools 定义)LLM 根据用户问题决定「是否调用 / 调用哪个 / 传什么参数」应用程序真正执行工具拿到结果把结果回填给 LLMLLM 生成最终自然语言回答关键认知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_choiceauto模型自行决定是否调用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: tooltool_call_id{role:tool,tool_call_id:call_abc123,content:{\temp\: 28, \desc\: \晴\}}Anthropic —— 用role: user里的tool_resulttool_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 的完整 messagesschema 越清晰越准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还能暴露 resourcesresources/list和 promptsprompts/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 格式内部转译成各厂商格式。具体抹平了哪些差异以工具调用为例差异点OpenAIAnthropicGemini抹平方式工具定义键名function.parametersinput_schemafunctionDeclarations.parametersadapter 改字段名schema 本体同构调用意图tool_calls[]content[].tool_usefunctionCall统一解析成内部 ToolCall参数载荷argumentsJSON 字符串input对象args对象adapter 统一 parse 成对象结果回填role:“tool” tool_call_idrole:“user” 里 tool_result tool_use_idfunctionResponse统一封装成 ToolResult 按厂商拼回system 提示messages 里 role:“system”顶层独立 system 字段systemInstructionadapter 搬运到对应位置强制调用tool_choicetool_choicetoolConfig.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 位置参数是字符串还是对象工具定义、调用、回填的封装形式流式事件格式抹不平能力/行为层是否支持并行工具调用是否支持工具调用本身老模型/小模型可能不支持只能降级为提示词模拟 ReActJSON Schema 支持程度enum、嵌套对象、$ref 遵守度不同指令遵循度 / 幻觉率上下文窗口、token 计费、stop_reason 语义差异多模态、思维链thinking等特有能力对能力差异框架只能做「能力探测 降级策略」而非真正抹平。结论格式差异在 Agent / 框架 / 网关的适配层根据 provider 自动抹平业务代码通常无感。能力差异抹不平只能靠能力标记 降级 / 兼容策略处理。这也是很多框架有「provider」或「model capability」概念的原因——既做格式翻译也记录每个模型支持什么运行时决定走原生工具协议还是模拟方案。

相关新闻