11 · 工具调用:让模型从”会说话”到”会动手”
本片目标:让模型在回答中”调用你提供的工具”——用@tool定义工具、bind_tools让模型知道有工具可用、手动执行循环把结果塞回。全程不用 agent、不用 LangGraph、不用弃用包。 新增规定性:11(工具调用协议:tool_calls/ToolMessage+ 手动执行循环) 数据字典:StructuredTool / ToolCall / ToolMessage / bind_tools(内部是 RunnableBinding)。 进程线程模型:一次工具问答 = 多次模型调用串行(声明 → 执行 → 再问),工具本身跑在主线程。 网络模型:模型调用是网络请求;工具若是本地函数则 0 网络,若是查外部 API 则多一次 HTTP。
1. 上集回顾
第 10 篇 RAG 能回答文档问题了。但有个天花板:RAG 是”只读”的——模型从资料里找答案、念给你听。但”转正答辩还有几天?“这种问题,手册里没有现成答案,得算出来。你当然可以写死:
答案 = 答辩日 - 今天。但用户问法千变万化(“我 10 月 1 号答辩,来得及准备吗?”),写死永远跟不上。
真正的解法:让模型在回答时调用你写的函数——这就是工具调用(Tool Calling)。模型负责”理解问题 → 决定调哪个工具 → 填好参数”,你负责”执行函数 → 把结果喂回去 → 模型组织成答案”。
关键认知:工具 ≠ agent。agent 是”模型自主循环调用工具直到完成任务”的框架(LangGraph 那套)。工具调用是更底层的协议——bind_tools + 手动循环就能实现,完全不用 agent。
2. 数据字典:四个主角
2.1 @tool 装饰器(把函数变成工具)
@tool 自动从函数签名 + docstring 生成三样东西(实测):
为什么 docstring 必须写清楚:模型靠 description 决定”什么时候该用这个工具”。写”计算两个日期差几天”比写”算天数”好——模型理解得越准,调用越对。
2.2 bind_tools(让模型知道有工具可用)
bind_tools 返回的是 _ChatModelBinding——它是 RunnableBinding 的子类,本质是**“原模型 + 额外参数”的绑定层**:
- 把工具列表
[days_until]转成模型厂商要的 tools schema(Anthropic/OpenAI 协议格式); - 存进
kwargs,每次请求自动带上; - 返回一个”绑定后的模型”——
invoke()的返回类型不变,还是AIMessage,只是这个 AIMessage 可能带tool_calls而不是普通文本。
bind_tools 返回一个绑定了工具 schema 的模型绑定。之后调用它,模型在需要时返回 tool_calls 而不是普通回答:
2.3 ToolCall(模型声明的数据结构)
注意:模型只”声明”要调工具,不执行。执行是你的责任——这就是”手动”的含义。
2.4 ToolMessage(工具执行结果回传)
tool_call_id 是配对的钥匙:模型靠它知道”这个结果是对应我刚才哪次调用”。
3. 手动执行循环(核心代码)
for,不是框架自动的。你控制轮数上限(防死循环)、决定执行哪些工具(白名单)、处理失败(try/except)。主动权在你——这正是本系列”不用 agent 也能做生产级”的哲学。
3.5 工具消息必须留在历史里(关键认知)
第 2 次调用前,messages 里实际装着什么(实测,2026-08):
AIMessage.tool_calls里的id记住”我上次说要调days_until这个工具”;ToolMessage.tool_call_id认出”这个56就是那次调用的结果”;- 两者配对后,它才能组织出最终回答:“还有 56 天。”
56 直接当新消息发),模型会”失忆”——它不知道这个 56 是干嘛的,甚至可能当成用户说的话。工具调用循环 = 记忆在”一轮对话内部”的微缩版(呼应第 06 篇):模型声明 → 执行结果 → 最终回答,都是同一条消息链上的环节,必须全部保留。
配对失败会怎样:如果ToolMessage.tool_call_id对不上任何tool_calls里的id,模型会报错或把工具结果当无效输入。所以tool_call_id一定要从tc["id"]原样抄过来,不能自己编。
4. 关键方法
5. 进程线程模型
- 每次工具调用 = 多一次模型往返。1 个工具 = 2 次模型调用,延迟 ≈ 2 倍单次问答。
- 工具是本地函数 → 主线程直接跑;工具查外部 API → 也是网络阻塞(和模型调用同性质)。
- 高并发下多个工具调用可并行(
RunnableParallel,第 12 篇讲),但要注意工具是否有副作用(如写库)。
6. 网络模型
7. 验证:跑起来
配套代码code/11_tools.py(七幕):
@tool定义工具,打印 name/description/args(看工具的数据字典);bind_tools后问一个需要算日期的问题,看模型返回tool_calls的结构;- 手动执行循环:模型声明 → 执行 → ToolMessage 塞回 → 最终回答;
- 工具 vs RAG 分工对比;
- 多工具:两个工具(days_until/days_since),看模型按语义选哪个;
- 失败重试:multiply 触发安全限制报错,看模型自我纠正;
- 边界。
7.5 进阶:多工具与失败重试
多工具:模型自己选
bind_tools 接受多个工具,模型按问题语义挑对的:
days_since,问”还有几天答辩”就选 days_until。工具描述(docstring)是模型选工具的”说明书”——描述含糊,模型就会选错。
失败重试:工具报错,模型自我纠正
工具抛异常不中断对话——把错误信息作为ToolMessage 塞回去,模型自己调整:
8. 边界
- 工具声明要花 token——工具 schema 每次请求都带上,工具越多、描述越长越贵。
- 模型可能调错参数——
args是模型猜的。生产环境要在执行前校验(args类型检查),失败就返回错误信息给模型让它重试。 - 工具结果也进上下文——工具返回超长内容会爆上下文,工具函数要控制返回长度。
- 安全红线——别让模型调有副作用的工具(删除、写库、发邮件)。生产用白名单:只 bind 你允许的工具,且工具内部做权限校验。
tool_calls为空的回答——模型不一定每次都调工具,它可能直接回答。这是正常行为,你的循环要能处理”没有 tool_calls”的情况(就是上面的if not resp.tool_calls)。
推荐资料(延伸阅读)
- LangChain 官方文档 · 工具 —— 工具接口、ToolMessage 权威说明
- LangChain 官方文档 · 模型工具调用 —— bind_tools 与服务端工具
9. 未完待续
模型现在能”动手”了——但这引出一个生产级问题:一次工具问答 = 多次模型调用(1 工具 = 2 次)。如果 100 个用户同时这么问,每个请求都阻塞 1~3 秒,你的服务扛得住吗?这就是第 12 篇:并发与网络模型——把 LangChain 的调度机制(线程池、事件循环、HTTP 连接)彻底拆开看。 → 12 · 并发与网络模型