大模型能写诗、能推理、能翻译,但你问它「SKU-8821 现在还有多少库存」,它只能编一个数字。训练数据里没有你的实时业务数据,模型的「知识」停留在训练截止那天。要让 agent 真正干活——查数据库、调 API、读文件、下订单——靠的不是更大的上下文窗口,而是一套让模型「会调工具」的机制:function calling(OpenAI 叫法)或 tool use(Anthropic 叫法)。
这篇 SOP 给你一个可复制的实现模板:定义工具 schema → 模型决策 → 执行 → 回填结果,四步循环。代码基于官方文档核实,截至 2026-08-09,API 以官方为准。与同站的 MCP Server 开发实战 SOP(搭标准化工具层)和 MCP 客户端横评(选哪个客户端)互补:那两篇解决「工具怎么被标准化暴露」,这篇解决「模型侧怎么决策调用」。
一、工具调用是什么:四步循环
工具调用的本质,是让大模型学会「我不懂,但我找懂的人来办」。整套机制拆成四步:
- 定义工具 schema:你告诉模型有哪些工具可用、每个工具吃什么参数(用 JSON Schema 描述)。
- 模型决策:用户发指令,模型结合指令和工具列表,决定是否调用工具、调哪个、传什么参数。注意模型只生成「调用意图」,不真正执行。
- 执行:你的代码拿到模型返回的调用意图,真正去查数据库、调 API、读文件,拿到真实结果。
- 回填结果:把执行结果塞回对话,模型基于结果生成最终回答(或决定再调一次工具)。
关键认知:模型不执行代码,它只产出结构化的「调用意图」。真正执行的是你的应用代码。这个分工是工具调用安全的基石——模型不会偷偷 rm 你的数据库,除非你写了这样的工具还告诉它可以用。
二、定义工具 schema:JSON Schema 是通用语言
不管 OpenAI 还是 Anthropic,工具定义的内核都是 JSON Schema。以一个「查库存」工具为例:
{
"name": "check_inventory",
"description": "查询某 SKU 的当前库存数量与所在仓库。当用户询问商品库存、是否缺货、发货地时调用。",
"parameters": {
"type": "object",
"properties": {
"sku": { "type": "string", "description": "商品 SKU 编码,如 SKU-8821" },
"warehouse": {
"type": "string",
"enum": ["beijing", "shanghai", "guangzhou"],
"description": "仓库代号,未指定时查全部仓库合计"
}
},
"required": ["sku"]
}
}三条写 schema 的纪律:
- description 写清「什么时候调」:模型靠 description 决策,不是靠 name。写「查询库存」太弱,写「当用户询问商品库存、是否缺货、发货地时调用」模型才知道触发时机。description 还要写清「不调」的边界——比如「不要用此工具查历史库存,只查当前」——防止模型在不该调时硬调。
- required 明确:哪些参数必填、哪些可选,不要全标 required 也不要全不标。可选参数在 description 里说明缺省行为(如「未指定 warehouse 时查全部仓库合计」)。
- enum 限定枚举值:仓库代号用 enum 限定,比让模型自由填字符串安全得多——模型可能编出
wuhan而你的系统没这个仓。同理,布尔型参数用 enum["true","false"]比裸 string 稳。
schema 写得越严谨,模型参数幻觉越少。第六节会展开。两个进阶技巧:一是加 additionalProperties: false 锁死参数集合,防止模型塞入你没定义的额外字段;二是开 strict: true(OpenAI)强制模型严格按 schema 出参,结构化输出模式下模型连多余字段都塞不进来,这是压住参数幻觉最有效的一招。第六节会展开。
三、OpenAI function calling 实现
OpenAI 当前有两条 API 路径:Chat Completions(经典,仍在用)和 Responses(新一代,带会话状态)。两者工具定义结构略有不同,但循环逻辑一致。下面以 Chat Completions 为主示例——它最通用、教程最多。
注意:早期 2023 年的
functions/function_call(单数)参数已废弃,当前统一用tools/tool_choice(复数)。别再抄老教程。
import OpenAI from "openai";
const openai = new OpenAI();
// 1. 定义工具 schema(Chat Completions 格式:外层包一层 function)
const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
{
type: "function",
function: {
name: "check_inventory",
description: "查询某 SKU 的当前库存数量与所在仓库。",
parameters: {
type: "object",
properties: {
sku: { type: "string", description: "商品 SKU 编码" },
warehouse: { type: "string", enum: ["beijing", "shanghai", "guangzhou"] }
},
required: ["sku"],
additionalProperties: false
}
}
}
];
async function checkInventory(sku: string, warehouse?: string) {
// 你的真实查询逻辑:查数据库 / 调 ERP API
return { sku, qty: 142, warehouse: warehouse ?? "beijing" };
}
// 2. 模型决策:带上 tools 发请求
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{ role: "system", content: "你是库存助手,缺信息时调用工具查真实库存,绝不编造数字。" },
{ role: "user", content: "SKU-8821 在北京仓还有多少货?" }
];
const resp = await openai.chat.completions.create({
model: "gpt-5.6",
messages,
tools,
tool_choice: "auto" // auto | none | required | {type:"function",function:{name:"check_inventory"}}
});
// 3. 执行:模型决定调用 check_inventory
const msg = resp.choices[0].message;
if (msg.tool_calls && msg.tool_calls.length > 0) {
messages.push(msg); // 回填 assistant 的工具调用意图
for (const call of msg.tool_calls) {
const args = JSON.parse(call.function.arguments);
const result = await checkInventory(args.sku, args.warehouse);
// 4. 回填结果,用 tool_call_id 关联调用与结果
messages.push({
role: "tool",
tool_call_id: call.id,
content: JSON.stringify(result)
});
}
// 5. 模型拿到结果,生成最终自然语言回答
const final = await openai.chat.completions.create({ model: "gpt-5.6", messages, tools });
console.log(final.choices[0].message.content);
// => "SKU-8821 在北京仓还有 142 件。"
}几个关键字段:tool_choice 控制模型行为——"auto"(默认,模型自己判断要不要调)、"none"(禁止调)、"required"(必须调至少一个)、或指定 {type:"function",function:{name:"..."}} 强制调某函数。call.id / tool_call_id 是调用与结果的关联键,千万别丢。加 strict: true 可强制模型严格按 schema 出参(结构化输出),能大幅压住参数幻觉。
Responses API 的等价流程:用 openai.responses.create(),响应 output 里是 type:"function_call" 项(含 name/arguments/call_id),回填用 {type:"function_call_output", call_id, output}。它带 previous_response_id 会话状态,多轮不用自己攒 messages。
四、Anthropic tool use 实现
Anthropic 的 tool use 与 OpenAI 思路一致,但 API 形态有四处差异值得对照:
import anthropic, json
client = anthropic.Anthropic()
# 1. 定义工具:注意是 input_schema 不是 parameters,外层不包 function
tools = [{
"name": "check_inventory",
"description": "查询某 SKU 的当前库存数量与所在仓库。",
"input_schema": {
"type": "object",
"properties": {
"sku": {"type": "string", "description": "商品 SKU 编码"},
"warehouse": {"type": "string", "enum": ["beijing", "shanghai", "guangzhou"]}
},
"required": ["sku"]
}
}]
def check_inventory(sku, warehouse=None):
# 你的真实查询逻辑
return {"sku": sku, "qty": 142, "warehouse": warehouse or "beijing"}
messages = [{"role": "user", "content": "SKU-8821 在北京仓还有多少货?"}]
# 2. 模型决策
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto"}, # auto | any | tool | 加 disable_parallel_tool_use
messages=messages
)
# 3. 执行:response.content 里找 tool_use block
if response.stop_reason == "tool_use":
messages.append({"role": "assistant", "content": response.content})
tool_use = next(b for b in response.content if b.type == "tool_use")
result = check_inventory(**tool_use.input) # input 已是 dict,无需 JSON.parse
# 4. 回填:role 用 "user",content 是 tool_result block
messages.append({
"role": "user",
"content": [{"type": "tool_result", "tool_use_id": tool_use.id, "content": json.dumps(result)}]
})
# 5. 模型拿到结果生成最终回答
final = client.messages.create(
model="claude-opus-5", max_tokens=1024, tools=tools, messages=messages
)
print(final.content[0].text)四处差异:① schema 字段叫 input_schema(OpenAI 叫 parameters);② 工具直接平铺,不外包 function;③ 结果回填的 role 是 "user"、content 是 tool_result block(OpenAI 用 role:"tool" + tool_call_id);④ tool_use.input 已是解析好的 dict,不用 JSON.parse。tool_choice 的 {"type":"any"} 等价于 OpenAI 的 "required",{"type":"tool","name":"..."} 等价于强制指定函数。选型择哪家?如果你的 agent 主要调你自己写的工具(查你的库、调你的 API),两家都行,调用循环一致,差异只在字段命名和结果回填形态。如果你依赖服务器侧工具(如 Anthropic 的 web_search / code_execution 服务器工具),就得按对应平台的 API 走。另一个选型考量:并行工具调用,Anthropic 原生支持一轮返回多个 tool_use block、用 disable_parallel_tool_use 控制,OpenAI Chat Completions 也支持返回多个 tool_calls,你的执行循环得用 for 遍历而不是只取第一个。
五、执行与回填循环:带上错误处理
真实环境里工具会失败——数据库超时、API 403、SKU 不存在。关键原则:别把异常吞掉,也别让 agent 卡死,把错误结构化回填给模型让它自己决定下一步。
def run_tool(tool_use):
try:
result = check_inventory(**tool_use.input)
return {"content": json.dumps(result), "is_error": False}
except InventoryNotFound:
# 业务错误:SKU 不存在,回填让模型询问用户
return {"content": f"SKU {tool_use.input['sku']} 不存在,请确认编码", "is_error": True}
except Exception as e:
# 系统错误:超时 / 403,回填让模型重试或换路径
return {"content": f"工具执行失败:{type(e).__name__},可重试一次", "is_error": True}
# 回填时标记 is_error,Claude 会据此调整策略
messages.append({
"role": "user",
"content": [{"type": "tool_result",
"tool_use_id": tool_use.id, **run_tool(tool_use)}]
})加三道护栏:最大循环次数(防止模型无限重试调用失败的工具,5-10 次封顶);超时(每个工具调用设 10-30s 超时,别让一次慢查询拖垮整个 agent);错误降级(连续失败 N 次后让模型改用「抱歉,库存查询暂时不可用」的兜底话术,而不是继续撞墙)。这三道护栏背后的逻辑是「模型不可靠」:模型会反复调一个失败的工具,会在参数上微调重试,会忽略之前的错误信息。你必须在应用层为它划边界,否则一个慢查询就能燒掉十几万 token 和数十分钟时间。实践中建议把「工具调用历史 + 错误次数」记录在会话状态里,达到上限就强制截断,给用户一个明确的「当前无法完成」而不是让它永远打转。
六、五个踩坑
坑 1:schema 写得太松,参数幻觉
description 模糊、参数全 string、没 enum,模型就会编参数。表现为模型给你一个结构合法但语义错误的调用——比如 warehouse: "wuhan"(你没这仓)。修法:每个参数写清 description,枚举值用 enum,必填项明确标 required,能加 strict: true 就加。
坑 2:模型编造参数值
模型把用户随口说的「那个商品」填成 sku: "that-product"。模型擅长结构化,不擅长猜真实 ID。修法:在 system prompt 里写「缺 SKU 时先问用户,不要猜测」;或加一个 search_product 模糊搜索工具,让模型先搜后查,而不是直接编 SKU。
坑 3:不处理 tool 调用失败,agent 卡死
模型调了工具,你的代码抛异常没回填结果,下一轮请求缺了 tool_result,API 直接报错。这是最常见的线上事故。修法:所有工具执行包 try/except,失败也回填一个 is_error: true 的结果,让模型自己决定重试还是换方案。
坑 4:没有超时和循环上限 模型反复调用一个慢工具,或陷入「调用失败-重试-失败」死循环,token 和时间双双爆表。修法:每个工具加超时,整个 agent 循环加最大轮次(建议 5-10 轮),超限就强制返回兜底回答。
坑 5:工具能力太宽,安全失控
给模型一个 execute_sql(query) 工具,它可能拼出 DROP TABLE。工具粒度越细越安全——用 check_inventory(sku, warehouse) 而非 run_any_sql(sql)。涉及写操作(下单、退款、删除),务必在工具层加权限校验和二次确认,别让模型一句话就把钱退了。
七、常见问题
Q1:function calling 和 tool use 是一回事吗? 是。OpenAI 早期叫 function calling,后改名 tool calling / tools;Anthropic 一直叫 tool use。两者底层逻辑一样:模型产出结构化调用意图,应用执行后回填结果。API 字段名不同,循环相同。
Q2:模型会自己写代码执行吗? 不会。模型只产出「调哪个工具、传什么参数」的意图(一段 JSON)。真正执行的是你的应用代码。模型拿不到你的数据库密码,也跑不了你机器上的 shell——除非你显式提供了这样的工具。
Q3:OpenAI Responses API 和 Chat Completions 该用哪个?
Chat Completions 更通用、教程多、跨框架兼容好,适合大多数场景。Responses API 带会话状态(previous_response_id)、原生支持多轮和后台模式,适合复杂 agent 编排。新项目可优先 Responses,老项目继续用 Chat Completions 完全没问题。实际判断依据:你需不需要服务端会话状态(Responses 原生支持,Chat Completions 得自己收集 messages);你的框架(LangChain / LlamaIndex 等)是否已适配 Responses;团队成员对哪套更熟。别为了「新」而迁,Chat Completions 不是待废弃 API,它仍是全行业工具调用的主力路径。
Q4:怎么让模型调得更准、更少幻觉?
三招:① schema 严谨——description 写清触发时机,参数加 enum 和 description,开 strict: true;② system prompt 约束——「缺信息先问用户,不要猜测参数」「只调必要工具」;③ 少即是多——工具列表别堆几十个,模型选择压力越大越容易选错,用按需加载(OpenAI 的 tool_search)分批暴露。
Q5:和 MCP 是什么关系? 互补。MCP 是「标准化工具层」——定义工具怎么被发现、被传输,让一个工具能被所有客户端调用(见 MCP Server 开发 SOP)。function calling / tool use 是「模型侧决策机制」——定义模型怎么决定调哪个工具。一个 agent 通常两者都用:MCP 把工具标准化暴露出来,模型的 function calling 机制决定何时调、调哪个。模型侧决策 + 标准化工具层,拼起来才是完整 agent。
可复用 system prompt 模板
你是 {角色},可以调用以下工具完成任务:{工具列表}
调用纪律:
1. 缺少必填参数时,先向用户询问,绝不猜测或编造参数值
2. 只调用完成任务所需的最少工具,不冗余调用
3. 工具返回错误(is_error)时,向用户说明问题并建议下一步,不要反复重试同一调用
4. 工具返回的数据是唯一事实来源,不要用训练知识覆盖工具结果
5. 涉及写操作(下单/退款/删除)时,先向用户确认再调用
可用工具:
- check_inventory(sku, warehouse?): 查库存
- search_product(keyword): 模糊搜商品
- create_order(sku, qty, address): 下单(需用户确认)参考来源
- OpenAI Function Calling 官方指南(tools 参数、tool_choice、tool_calls、strict 模式):https://platform.openai.com/docs/guides/function-calling
- OpenAI Developers API 文档(Responses API 的 function_call / function_call_output / call_id):https://developers.openai.com/api/docs/guides/function-calling
- Anthropic Tool Use 官方文档(input_schema、tool_use block、tool_result 回填):https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview
- Model Context Protocol 官网(标准化工具层,与本文模型侧机制互补):https://modelcontextprotocol.io
- 本站《MCP Server 开发实战 SOP》:https://aiwebcool.com/zh/mcp-server-dev-sop
- 本站《MCP 客户端横评》:https://aiwebcool.com/zh/mcp-clients-comparison-review