FDE W2D1 学习手册 · Tool Calling 工具调用
W2 Week2 Day1 · A 级(必须掌握,面试核心)· 5h · 学完能讲透"模型如何决定调工具、执行、回填、再决策"的完整闭环
本日定位:W1 已讲 LLM 基础与结构化输出,本周进入"让模型连上外部世界"。W2D1 是 Agent 的命脉——工具调用,决定模型能不能查数据库、跑规则、调 OCR。
学完能回答:① 一次多轮工具调用的完整循环;② Tool Schema 怎么写、参数怎么校验;③ 工具失败了怎么重试/降级、怎么避免把异常泄露给模型。
使用方法:通读原理 → 重点看「面试话术」「易错点」→ 做自测清单 → 配合《FDE-W2D1-评测题.md》。选中不熟的词可标注(左下★重要 / 右下📌待查)。
一、Tool Calling 的本质与定位
Tool Calling(函数调用 / 工具调用)让 LLM 从"只会生成文本"变成"能驱动外部系统"。模型本身不直接执行任何工具,它只负责输出"我要调哪个工具、传什么参数",真正的执行由你的后端代码完成,再把结果回填给模型。
关键认知:模型输出的是 tool_call(工具调用意图),不是工具执行结果。执行权永远在开发者手里。这是安全边界,也是可控性的根本。
1.1 为什么需要工具
- 补知识:模型不知道实时特药价格、用户保单余额 → 调数据库工具查。
- 补能力:模型不会算风控金额、不会解析发票 → 调规则工具 / OCR 工具。
- 补动作:模型不能真提交理赔 → 调业务 API(需严格权限)。
Tool Calling 不是让模型"变厉害",而是给模型"外挂手脚":模型决定做什么,代码负责做。模型只产出调用意图(函数名 + 参数),执行和回填都在你这边。
在医疗保险理赔 Agent 里,工具调用是连接"自然语言诉求"和"核心系统(保单库 / 规则引擎 / OCR / 支付)"的桥梁。没有工具,模型只能空谈;有了工具,它才能查得到、算得准、办得成。
二、Tool Schema 设计(函数描述 + 参数 JSON Schema)
2.1 一个工具由什么组成
每个工具通常包含三部分:name(函数名)、description(给模型看的自然语言说明)、parameters(参数 JSON Schema)。
{
"name": "query_policy",
"description": "根据保单号查询医疗保险保单的基本信息,包括投保人、有效期、特药保障额度。",
"parameters": {
"type": "object",
"properties": {
"policy_no": {
"type": "string",
"description": "保单号,格式如 PA-2024-123456"
},
"fields": {
"type": "array",
"items": { "type": "string", "enum": ["holder", "valid_until", "special_drug_limit"] },
"description": "需要返回的字段,不传则返回全部"
}
},
"required": ["policy_no"]
}
}
2.2 参数 JSON Schema 的要点
| 要素 | 作用 | 面试常考 |
| type | string/number/integer/boolean/array/object | 类型错会导致解析失败 |
| required | 哪些参数必填 | 必填缺失要模型补问或报错 |
| enum | 限定取值集合 | 防止模型乱填状态/类型 |
| description | 给模型的语义说明 | 写清楚才能减少误调用 |
| default / nullable | 默认值 / 可空 | 降低必填压力 |
description 是给模型读的"使用说明书",写得越清楚,模型越不容易调错、传错参数。参数设计遵循"最小必要 + 强约束":能用 enum 就别用自由 string,能默认就别必填。
Tool Schema = 函数名 + 给模型看的描述 + 参数 JSON Schema。description 是写给人/模型看的说明,parameters 用 type/required/enum 严控输入。能用 enum 限定就别用自由文本,减少模型犯错。
① description 写得含糊(如"处理数据")会让模型猜怎么用,误调用率飙升。② 别把工具内部实现细节塞进 description,模型不需要知道 SQL 长啥样,只需要知道"这个工具能干嘛、传什么"。③ required 设太多会逼模型瞎填;设太少又会漏参。④ 嵌套 object/array 在部分模型(尤其小模型)支持不稳,必要时拆成扁平参数。
三、工具注册与路由
3.1 工具注册
"注册"就是把工具定义(Schema)放进请求里传给模型,模型在生成时可以选择调用。后端维护一个 name → 可执行函数 的映射表。
tools = {
"query_policy": query_policy_impl, # 查保单
"ocr_invoice": ocr_invoice_impl, # 识别发票
"calc_claim": calc_claim_impl, # 算理赔金额
"query_drug_db": query_drug_db_impl, # 查药品库
}
3.2 工具路由(模型决定 vs 你决定)
- 模型路由:把多个工具都交给模型,由模型根据语义选一个(或并行选多个)。最灵活,依赖模型判断。
- 你路由:先让模型做"意图分类",你根据分类显式调用对应工具。更可控,适合强约束业务(如理赔必须先校验保单再算金额)。
工具路由有两派:让模型从一堆工具里自选(灵活),或你先做意图分类再显式调用(可控)。保险理赔这种强顺序业务,常用"模型分类 + 代码编排"的混合路由。
生产里很少"把 20 个工具全扔给模型",因为:① 上下文太长、模型选错;② 业务有强顺序依赖(先查保单再算金额)。所以常见做法是分层工具集 + 代码级流程编排,模型只在前一步选定下一步工具。
四、单轮工具调用流程
最基础的"一问一调":用户问 → 模型决定调工具 → 你执行 → 结果回填 → 模型生成最终回答。
用户问题 → [模型] 输出 tool_call → [你的代码] 执行工具 → tool_result 回填 → [模型] 生成最终回答
- 请求:把用户消息 + tools 列表发给模型,temperature 设低(0~0.2),保证参数稳定。
- 解析:模型返回含
tool_calls 的响应,取出 name 和 arguments(JSON)。
- 执行:在你的映射表里找到函数,传参执行,拿到原始结果。
- 回填:把结果以
role: "tool" 消息(带 tool_call_id)追加进对话历史。
- 再请求:带着完整历史再调一次模型,模型据此生成最终自然语言回答。
回填时必须带上 tool_call_id,模型才知道"这条结果是回复刚才哪个 tool_call 的"。多数 SDK 会自动处理,但手写 HTTP 调用时漏掉 id 会导致模型困惑。
五、多轮工具调用循环(核心)
真实任务往往要调好几次工具、甚至连续决策:查保单 → 查特药目录 → 算金额 → 校验规则 → 输出结论。这就是多轮(multi-turn / agentic)工具调用。
[模型] tool_call_1 → 执行 → 回填 → [模型] tool_call_2 → 执行 → 回填 → ... → [模型] 不再调工具,输出最终答案(停止)
messages = [{"role":"user","content":"帮我看下PA-2024-123456的特药理赔能报多少,药是奥希替尼"}]
while True:
resp = call_llm(messages, tools=TOOLS, temperature=0.1)
if not resp.tool_calls: # 模型不再调工具 → 结束
print(resp.content)
break
for tc in resp.tool_calls: # 可能一次并行多个
fn = tools[tc.function.name]
result = fn(**json.loads(tc.function.arguments))
messages.append(resp.message) # 先把模型回复存进去
messages.append({ # 再回填工具结果
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result, ensure_ascii=False)
})
多轮工具调用就是个循环:模型吐 tool_call → 我执行 → 把结果回填进 messages → 再问模型,直到模型不调工具、直接给答案为止。每次回填都要带上 tool_call_id 和完整历史。
这个循环是 Agent 的心脏。工程上必须加最大轮数上限(如 8 轮),防止模型陷入"调工具→不满意→再调"的死循环烧 token。每轮都要记 Trace 便于复盘。
① 忘记把模型的 tool_call 消息本身存入 messages,只存 tool 结果 → 模型丢失上下文,会重复调用或答非所问。② 不设轮数上限 → 模型可能无限循环(尤其结果为空时反复重试)。③ 并行 tool_call 时,要等全部执行完再统一回填,不能只回填一个就再请求。
六、多工具路由与并行调用
6.1 一次决策调多个工具
模型一次响应里可以包含多个 tool_calls(如同时查"保单额度"和"药品目录"),它们之间无依赖时应并行执行以省时延。
| 场景 | 策略 | 示例 |
| 工具间无依赖 | 并行执行 | 同时查保单 + 查药品库 |
| 工具间有依赖 | 串行(下一轮) | 先查保单拿到额度,再算金额 |
| 相互排斥 | 只选一个 | 拒赔 / 理赔二选一 |
6.2 依赖关系判断
依赖判断通常由模型自己完成(它发现需要 A 的结果才能决定 B)。但你也可以在代码层做"依赖图"编排,把确定的串行关系写死,只把不确定的交给模型。
多个工具调用时,无依赖的并行、有依赖的串行。依赖一般模型自己判断,强顺序业务(理赔先查后算)也可在代码层固化。
并行能降延迟,但注意:并行工具若有一个超时/失败,整体怎么处理?要么"全部成功才继续",要么"成功的先用、失败的降级"。这在达标线②会展开。
七、参数校验与类型转换
7.1 为什么要校验模型给的参数
模型生成的 arguments 是字符串 JSON,可能:字段名拼错、类型错(把数字写成字符串)、必填缺失、枚举值非法、甚至 JSON 不合法。
7.2 校验手段
- JSON.parse / json.loads:先确保能解析,失败则反馈重试。
- Pydantic / JSON Schema:按 schema 校验类型、必填、枚举、范围。
- 业务校验:保单号格式正则、金额非负、日期合法等 schema 管不到的。
try:
args = ClaimArgs.model_validate_json(tc.function.arguments)
except ValidationError as e:
# 把错误反馈给模型,让它修正后重发
messages.append(error_feedback(str(e)))
模型给的参数是"字符串 JSON",不可信。必须先 JSON 解析 → Pydantic/Schema 校验形 → 业务规则校验义。失败就把错误信息喂回给模型修正,而不是自己猜着用。
① 别"信任模型给的参数就直接 SQL 拼接"——那是注入漏洞(见 W2D3)。② 模型可能把 "amount": "五百" 这种中文数字给你,需要归一化。③ 枚举值大小写、同义词("男"/"M")要在校验层统一映射。
八、工具超时与失败重试
8.1 超时控制
每个工具调用都要有超时(如 3s / 10s),避免下游慢查询拖垮整个对话。超时后视为失败,进入重试/降级逻辑。
8.2 重试策略
| 失败类型 | 处理 |
| 网络抖动 / 5xx | 指数退避重试 2~3 次 |
| 参数错误(模型填错) | 把校验错误反馈给模型,让它改参数重调(不是无脑重试) |
| 下游永久故障 | 停止重试,降级(返回"暂不可用"/转人工) |
| 超时 | 重试 1 次,仍超则降级 |
工具失败重试要"分类对待":网络错可重试,参数错要反馈模型,永久错要降级。重试要带退避,且计入成本和延迟预算。
工具失败分三类:可重试的(网络/超时)、要反馈模型的(参数错)、要降级的(永久故障)。关键区别——参数错了重试 N 次也没用,必须把错误喂给模型让它改。
九、工具返回结果标准化
9.1 统一返回结构
不同工具返回格式各异(DB 是行、OCR 是坐标、规则是结论)。建议统一成结构化对象再回填,方便模型消费,也方便你做日志/评测。
{
"ok": true,
"data": { "policy_no": "PA-2024-123456", "special_drug_limit": 300000 },
"error": null,
"trace_id": "tr_8f3a"
}
9.2 错误处理要"对模型友好"
- 成功:返回干净的业务数据。
- 失败:返回
ok:false + 简洁错误原因(如 "policy_not_found"),不要返回堆栈。
- 降级:返回"工具暂不可用,请转人工"之类模型能理解的话。
工具结果要统一成结构化对象(ok/data/error)再回填。失败返回简洁错误码,别把异常堆栈丢给模型——它看不懂也不该看。
标准化返回让"模型理解"和"你做可观测"两全。trace_id 贯穿工具调用,便于排障和评测。回填内容要裁剪,避免把 10 万行 DB 结果全塞进上下文。
十、不把异常泄露给模型(异常隔离)
10.1 为什么要隔离
把原始异常(SQL 报错、内部 IP、堆栈、密钥线索)回填给模型有两大风险:
- 安全风险:堆栈可能暴露内部架构、凭证、PII。
- 噪声风险:模型被无关技术细节干扰,可能胡乱决策。
10.2 隔离做法
- 工具代码内部
try/except,把异常转成业务层错误码。
- 只把"模型能用的信息"回填:错误类型、建议动作(重试 / 换参数 / 转人工)。
- 敏感字段(身份证、手机号)在回填前脱敏。
异常隔离是生产红线:工具层吞掉技术细节,对外只暴露"干净的业务错误"。这也是合规要求(保险行业 PII 脱敏)。
异常隔离=工具内部捕获异常,转成业务错误码再回填,绝不把堆栈/内部 IP/密钥/PII 给模型。模型只需要知道"失败了、为什么、该干嘛"。
① 别图省事直接 return str(e) 回填——e 里可能含 SQL、路径、密钥。② 脱敏要在"回填前"做,不是在 prompt 里要求模型忽略。③ 错误反馈给模型时要引导它改,而不是只说"出错了",否则它会重复同样的错。
十一、各家实现(Qwen / DeepSeek / Claude)
| 厂商 | 机制 | 关键点 | 官方文档 |
| 阿里云百炼 Qwen | tools / function calling | 支持并行 tool_calls,参数用 JSON Schema | help.aliyun.com/zh/model-studio/qwen-function-calling |
| DeepSeek | tools + tool_calls | 兼容 OpenAI 格式,返回 tool_calls 数组 | api-docs.deepseek.com/zh-cn/guides/function_calling |
| Claude (Anthropic) | tool_use / tool_result | 工具结果用 tool_result 块回填,强类型 | docs.anthropic.com/en/docs/build-with-claude/tool-use |
11.1 通用模式
三家本质相同:请求带 tools 定义 → 响应带回 tool_calls(含 name + arguments)→ 你执行 → 用对应格式(OpenAI 的 role:"tool" / Claude 的 tool_result)回填 → 再请求。
建议用 LangChain / 自封装适配层屏蔽厂商差异,业务代码只面向统一的 Tool 接口,换模型不改写流程。
三家都是"请求带 tools → 响应给 tool_calls → 执行 → 回填 → 再问"。差异在回填格式:OpenAI 系用 role:tool + tool_call_id,Claude 用 tool_result 块。用适配层屏蔽差异最稳。
十二、工程落地与可观测性
12.1 必须记录的 Trace
- 每轮:模型选了哪个工具、传了什么参数、参数校验结果。
- 工具执行:耗时、是否超时、返回摘要(脱敏)。
- 回填内容、最终轮数、总 token、是否降级/转人工。
12.2 防护清单
- 最大轮数上限(防循环)。
- 每个工具超时 + 重试上限。
- 工具白名单(模型只能调注册过的)。
- 危险操作(如真正提交理赔)需二次确认 / 人工审批。
- PII 脱敏与权限校验(谁能查谁的保单)。
生产级工具调用 = 流程编排 + 防护 + 可观测。没有 Trace 你无法回答"这次理赔为什么拒了";没有防护上限你无法挡住循环和越权。
工具调用上线前要有三件套:轮数上限、超时重试上限、Trace 全链路。涉及写操作(提交理赔)必须人工审批,不能让模型直接落库。
十三、面试达标线①:讲清一次多轮工具调用循环
[模型] 输出 tool_call(name+args)→ [代码] 按 name 查映射表执行 → [代码] 以 role:tool + tool_call_id 回填结果 → [模型] 看到结果再决策(继续调 or 停止)→ 直到模型不再调工具、输出最终答案
- 模型输出 tool_call(函数名 + JSON 参数),temperature 设低保证参数稳定。
- 代码执行:用 name 在注册表找到函数,解析并校验参数后执行。
- 结果回填:以
role:"tool" + tool_call_id 追加进 messages,带上完整历史。
- 再请求模型:模型结合结果继续——要么再发 tool_call,要么直接给最终自然语言答案。
- 终止条件:响应里没有 tool_calls → 循环结束。必须加最大轮数上限防死循环。
一句话:模型吐调用意图 → 我执行 → 带 id 回填 → 模型再决策,循环直到它不调了。关键是每轮都把"模型原回复 + 工具结果"都存进历史,且设轮数上限。
十四、面试达标线②:工具失败怎么处理
捕获异常 → 分类(网络/参数/永久)→ 网络超时:退避重试≤3;参数错:把错误反馈模型改参;永久错:降级(默认值/转人工)→ 全程不把堆栈/PII 泄露给模型 → 记 Trace
- 网络/超时:指数退避重试 2~3 次,仍失败降级。
- 参数错误(模型填错):把校验错误反馈给模型,让它改参数重新调——不是无脑重试(重试 N 次参数还是错)。
- 永久故障:停止重试,降级返回"工具暂不可用/转人工",不能让流程卡死。
- 异常隔离:工具内部 try/except,只回填业务错误码,绝不泄露堆栈、内部 IP、密钥、PII。
- 降级兜底:返回模型能理解的话(如"保单查询暂不可用,已为您转人工")。
工具失败三分类处理:可重试的退避重试、参数错的反馈模型改、永久错的降级转人工。核心是"异常隔离"——绝不把堆栈/PII 给模型,只给业务错误和下一步建议。
十五、W2D1 自测清单
- 能解释 Tool Calling 的本质:模型只输出调用意图,执行权在开发者手里。
- 能写一个完整的 Tool Schema(name/description/parameters + type/required/enum)。
- 知道 description 是写给模型看的,写得清楚能降误调用率。
- 能讲清单轮工具调用五步(请求→解析→执行→回填→再请求)。
- 能完整讲清多轮调用循环,并说出为什么必须存"模型回复+工具结果"、必须设轮数上限。
- 能区分并行调用(无依赖)和串行调用(有依赖),说清依赖怎么判断。
- 能对模型给的参数做 JSON 解析 + Pydantic + 业务校验,失败反馈模型。
- 能说出工具超时、重试分类(网络/参数/永久)及各自处理。
- 能解释工具返回结果标准化(ok/data/error)和不把异常泄露给模型的原因。
- 知道 Qwen/DeepSeek/Claude 三家的调用模式与回填格式差异。
- 能讲清达标线①(多轮循环)和达标线②(失败处理/异常隔离)。
- 能说出生产级工具调用的三件套:轮数上限、超时重试上限、全链路 Trace。
十六、高频面试题速记卡
Q:模型能直接执行工具吗?
不能。模型只输出 tool_call(函数名+参数),执行由你的代码完成,再把结果回填。执行权在开发者手里。
Q:多轮工具调用怎么终止?
模型响应里不再含 tool_calls 时循环结束。必须设最大轮数上限防死循环。
Q:回填工具结果为什么必须带 tool_call_id?
模型靠它把"这条结果"对应到"刚才那个调用",丢失会导致上下文错乱、重复调用。
Q:模型参数填错了,重试有用吗?
没用。要把校验错误反馈给模型让它改参数重调,而不是无脑重试。无脑重试只会重复同样的错。
Q:工具调用失败了怎么处理?
分类:网络/超时退避重试;参数错反馈模型改;永久错降级转人工。全程异常隔离,不泄露堆栈/PII。
Q:为什么不能把异常堆栈回填给模型?
两个风险:安全(暴露内部架构/密钥/PII)和噪声(干扰决策)。只回填业务错误码+建议动作。
Q:并行和串行工具调用怎么选?
无依赖并行(省延迟),有依赖串行(下一轮)。依赖通常由模型判断,强顺序业务可代码固化。
Q: Tool Schema 的 description 重要吗?
非常重要,是写给模型的使用说明。含糊会让模型猜着用、误调用飙升;用 enum 限定比自由 string 更稳。
Q:生产级工具调用要注意什么?
三件套:轮数上限、超时重试上限、全链路 Trace;危险写操作需人工审批;PII 脱敏与权限校验。
Q:Claude 和 OpenAI 系回填格式区别?
OpenAI 用 role:"tool" + tool_call_id;Claude 用 tool_result 块。建议用适配层屏蔽差异。
FDE W2D1 学习手册 · Tool Calling 工具调用(面试级)· 配合《FDE-W2D1-评测题.md》自测
📌 待查★ 重要