让 LLM 调用工具,听起来简单——告诉它「这是工具,你用吧」——但落地有一大堆细节:怎么描述工具、LLM 怎么表达调用意图、参数怎么填、调用结果怎么回传。这些问题经历了从「Prompt 约定」到「原生结构化」再到「协议标准化」的三段演进。
让 LLM 调用一个工具,本质上要回答四个问题:
| 问题 | 含义 |
|---|---|
| 有哪些工具 | 工具清单怎么告诉 LLM |
| 什么时候调 | LLM 怎么判断「现在该调工具了」 |
| 怎么调 | 工具名 + 参数怎么表达 |
| 结果怎么用 | 工具返回怎么回传给 LLM |
早期 ReAct 用「Prompt 约定」回答这四个问题;现代 LLM 用「原生结构化调用」;最新的 MCP 协议试图「标准化」。下面分别看。
早期 LLM 没有原生工具调用能力,靠 Prompt 约定——告诉 LLM 「按 Action: tool[args] 格式输出」。
你可以使用以下工具: 1. search(query): 在网上搜索查询词,返回结果摘要 2. calculator(expression): 计算数学表达式 3. get_stock(ticker): 查询股票实时价格 当需要使用工具时,按以下格式输出: Thought: <你的推理> Action: <工具名>[<参数>] 示例: Thought: 我需要查苹果股价。 Action: get_stock[AAPL]
| 问题 | 表现 |
|---|---|
| 格式不稳 | LLM 偶尔不按格式输出 |
| 解析脆弱 | 正则解析易崩 |
| 参数难表达 | 复杂参数(嵌套对象、数组)很难用文本 |
| 工具数量受限 | 工具多了 Prompt 太长 |
| 校验靠运气 | LLM 可能调不存在的工具或给错参数 |
⚠️ Prompt 约定的最大风险是「调了不存在的工具」。LLM 可能脑补一个听起来合理的工具名(如
send_email_via_gmail),而实际注册表里没有。没有兜底校验的 Prompt 约定,迟早出生产事故。
2023 年 6 月,OpenAI 率先在 API 层引入原生 Function Calling——LLM 直接输出结构化的工具调用 JSON,而不是文本。Anthropic、Google、阿里等很快跟进。
用 JSON Schema 描述工具,比自然语言严谨得多:
{ "name": "get_stock_price", "description": "查询股票实时价格", "parameters": { "type": "object", "properties": { "ticker": { "type": "string", "description": "股票代码,如 AAPL、GOOGL" }, "exchange": { "type": "string", "enum": ["NASDAQ", "NYSE", "HKEX"], "description": "交易所,可选" } }, "required": ["ticker"] } }
LLM 决定调工具时,输出结构化对象(而非文本):
{ "tool": "get_stock_price", "parameters": { "ticker": "AAPL", "exchange": "NASDAQ" } }
这个输出可被严格解析、校验,告别正则脆弱性。
工具执行后,结果以「tool message」形式回传:
[tool result] { "price": 187.5, "currency": "USD", "timestamp": "2026-07-20T09:30Z" }
| 维度 | Prompt 约定 | 原生调用 |
|---|---|---|
| 输出格式 | 自由文本 | 结构化 JSON |
| 解析 | 正则(脆) | JSON 解析(稳) |
| 参数表达 | 简单值尚可 | 复杂嵌套无压力 |
| 校验 | 弱 | 强(Schema 约束) |
| 工具数量 | 少 | 中(仍受 Prompt 上限) |
| 模型支持 | 任何 LLM | 需模型支持 |
💡 原生调用是工程上的巨大进步:它把「让 LLM 调工具」从「靠 Prompt 巧妙引导」变成「API 原生支持」,稳定性数量级提升。今天所有主流 LLM 都支持 Function Calling,生产 Agent 几乎都用原生调用而非 Prompt 约定。
原生调用支持两个高级能力:多工具调用与并行调用。
一次让 LLM 输出多个工具调用,按顺序执行:
[ { "tool": "get_stock_price", "parameters": { "ticker": "AAPL" } }, { "tool": "get_stock_price", "parameters": { "ticker": "MSFT" } }, { "tool": "get_stock_price", "parameters": { "ticker": "GOOGL" } } ]
OpenAI 等模型支持并行工具调用——一次输出多个无依赖的工具调用,框架并行执行,显著降低延迟。这正是 3.4 节 ReWOO 思想的官方实现。
JSON Schema 描述工具看似简单,写得好坏差很多。几条经验:
❌ "description": "查股票" ✅ "description": "查询指定股票代码的实时价格,支持美股(AAPL)、港股(0700.HK)、A股(600519.SH)。返回最新成交价、涨跌幅、成交量。"
LLM 选工具主要靠 description 匹配。描述越具体,选错率越低。
❌ "ticker": { "type": "string" } ✅ "ticker": { "type": "string", "pattern": "^[A-Z]{1,5}(\\.[A-Z]{2,4})?$", "description": "..." }
带 enum、pattern、范围约束,能在 LLM 输出阶段就拦截大量错误。
"description": "查询股票实时价格。 仅当用户明确询问股价时使用; 历史股价请用 get_historical_stock; 财报数据请用 get_financial_report。"
明确「何时用、何时不用」,能有效减少误调用。
⚠️ 工具描述是 Prompt Engineering 的延伸:很多人以为原生调用就不用写 Prompt 了,其实 JSON Schema 里的 description 字段就是 Prompt 的一部分。描述写得差,再好的模型也会调错工具。
每个 LLM 厂商都有一套 Function Calling 格式,工具开发者要为每家适配一遍。2024 年 Anthropic 提出 MCP(Model Context Protocol,模型上下文协议),试图标准化。
| 问题 | MCP 的解法 |
|---|---|
| 工具格式各厂不同 | 统一协议描述工具 |
| 工具与模型耦合 | 工具与模型解耦,一次开发多模型可用 |
| 工具散落各处 | 统一注册与发现 |
| 工具状态难管 | 协议内建资源、提示、工具三类原语 |
| 原语 | 含义 | 类比 |
|---|---|---|
| Tools | 可调用的函数 | Function Calling |
| Resources | 可读取的数据源 | 文件、数据库 |
| Prompts | 可复用的提示模板 | Prompt 库 |
MCP 的核心思想:把「工具」做成独立的 MCP Server,Host(如 Claude Desktop、IDE、Agent 框架)通过统一协议连接任意 Server。写一次工具,所有支持 MCP 的客户端都能用。
💡 MCP 的意义:它在做工具调用领域的「USB 接口」——一个标准化的连接协议,让工具生态不再被单一模型厂商锁定。MCP 是否能成为事实标准仍在演化中,但它代表了工具调用标准化的方向。第 8 章会进一步讨论 MCP 的工程影响。
即便原生调用,也有失败模式:
| 失败 | 表现 | 兜底 |
|---|---|---|
| 调不存在的工具 | LLM 脑补工具名 | 注册表校验,拒绝并提示 |
| 参数类型错 | 给 string 传 number | Schema 校验 |
| 参数缺 | 漏 required | Schema 校验 + 回喂 LLM |
| 参数值错 | ticker 拼错 | 工具内校验 + 错误回喂 |
| 调用时机错 | 不该调时调 | description 写清「何时用」 |
| 死循环调用 | 反复调同一工具 | 重复检测 |
工具调用失败时,不能吞掉错误,要把错误信息格式化后回喂 LLM,让它自主决定下一步(第 2.4 节原则):
[Thought] 我要查苹果股价。 [Action] get_stock_price(ticker="APPL") ← 拼错 [Observation] Error: ticker "APPL" not found. Did you mean "AAPL"? [Thought] 我拼错了,应该是 AAPL。 [Action] get_stock_price(ticker="AAPL") ← 改对 [Observation] { price: 187.5, ... }
⚠️ 错误回喂是 Function Calling 工程化的命脉。一个把错误吞掉的 Agent,会在错误方向上狂奔;一个把错误回喂的 Agent,能像人类一样「错了就改」。这是 ReAct 范式(第 4 章)的核心循环在 Action 层的具体体现。
本章还会讲「代码解释器」(6.3 节)。先做个区分:
| 维度 | 函数调用 | 代码解释器 |
|---|---|---|
| 调用什么 | 预定义的函数 | LLM 自己写的代码 |
| 灵活性 | 低(受工具集限制) | 高(图灵完备) |
| 安全性 | 高(函数可控) | 低(代码能干任何事) |
| 适用 | 已知能力(查询、操作) | 未知能力(数据分析、计算) |
函数调用是「让 LLM 用我们准备好的工具」;代码解释器是「让 LLM 自己造工具」。两者互补——已知能力用函数调用,未知能力用代码解释器。
| 反模式 | 表现 | 后果 | 正确做法 |
|---|---|---|---|
| 工具描述太简 | description 一句话 | 选错工具 | 详细描述+何时用 |
| 参数无约束 | 没有 enum/pattern | 参数错乱 | Schema 严格约束 |
| 吞错误 | 失败返回空 | Agent 困惑 | 错误回喂 |
| 工具过多全暴露 | 上百个全塞 Prompt | 注意力稀释 | 工具检索(6.2 节) |
| 不做注册表校验 | LLM 调啥就执行啥 | 调不存在工具 | 强制校验 |
| 不限制并行 | 无依赖检测就并行 | 数据不一致 | 依赖分析(6.2 节) |
Action: tool[args]),简单但脆弱——格式不稳、解析易崩、参数难表达、易调不存在的工具。下一节《6.2 工具集成与 API 编排》将讨论多工具管理——工具注册表、工具检索、并行/串行编排。