文章目录
- LangChain Tool Calling 工具调用:让大模型真正"动手"
- 一、先纠正一个常见误解
- 二、定义一个工具
- 2.1 描述写不好,工具就用不对
- 三、绑定工具并调用
- 四、手动跑完整个循环
- 五、多个工具 + 强制选择
- 六、并行调用与容错
- 6.1 一次要调多个工具
- 6.2 工具报错了怎么办
- 七、结构化输出:工具调用的另一种用法
- 八、常见坑
- 九、小结
LangChain Tool Calling 工具调用:让大模型真正"动手"
大模型会说话,但不会做事——它不知道今天的天气,查不了你的数据库,
也发不出一封邮件。
Tool Calling 就是补上这块:让模型决定调用哪个函数、传什么参数,
真正执行由你的代码来做。这篇讲清楚一次完整工具调用的数据流,以及
bind_tools背后到底发生了什么。
一、先纠正一个常见误解
大模型不会执行任何代码。
很多人以为"模型调用了天气 API",其实不是。真实的流程是:
① 你告诉模型:"你有这些工具可用"(函数名 + 描述 + 参数 schema) ② 模型收到用户问题,判断要不要用工具 ③ 模型返回一个 JSON:"我想调用 get_weather,参数是 city='北京'" ④ 【你的代码】拿到这个 JSON,真正去调天气 API ⑤ 【你的代码】把结果塞回对话,再问一次模型 ⑥ 模型基于结果生成最终回答模型只负责第 ③ 步——输出一个"调用意图"的 JSON。
执行永远在你的进程里。理解这一点,后面所有概念都会顺。
二、定义一个工具
最简单的方式是@tool装饰器:
fromlangchain_core.toolsimporttool@tooldefget_weather(city:str)->str:"""查询指定城市的当前天气。当你需要知道某地天气时调用它。"""# 真实项目里这里应该调天气 APIreturnf"{city}:晴,26℃,东南风 3 级"print(get_weather.name)# get_weatherprint(get_weather.description)# 查询指定城市的当前天气……print(get_weather.args)# {'city': {'title': 'City', 'type': 'string'}}三个细节:
- 函数名会被送给模型,所以要起得见名知义;
- docstring 就是工具描述,模型靠它判断"什么时候该用这个工具",
这行字的重要性远超你的想象; - 类型注解决定参数 schema,模型靠它知道该传什么。
2.1 描述写不好,工具就用不对
这是新手最常踩的坑。对比一下:
# ❌ 太简略,模型不知道什么时候该用@tooldefquery(sql:str)->str:"""执行 SQL"""...# ✅ 说清楚用途、参数格式、边界@tooldefquery_employee(sql:str)->str:"""查询员工信息数据库。 当用户询问员工人数、部门分布、入职时间、薪资等结构化数据时使用。 只支持 SELECT 语句,不支持增删改。 表名:employees(id, name, dept, hire_date, salary) sql 参数必须是合法的 SQLite 语法。 """...把工具描述当成给同事写的接口文档来写,模型才能用对。
三、绑定工具并调用
fromlangchain_openaiimportChatOpenAI llm=ChatOpenAI(model="gpt-4o-mini")llm_with_tools=llm.bind_tools([get_weather])result=llm_with_tools.invoke("北京今天天气怎么样?")print(result.tool_calls)bind_tools做的事:把工具的 name/description/args schema 转成模型要求的格式,
每次调用时自动带上。之后llm_with_tools就成了一个"会表达调用意图"的模型。
输出:
[{'name':'get_weather','args':{'city':'北京'},'id':'call_abc123'}]注意:这时候工具还没被执行。模型只是说"我想这么调"。result.content是空的,result.tool_calls里有内容。
四、手动跑完整个循环
把这个数据流完整写一遍,理解会牢固很多:
fromlangchain_core.messagesimportHumanMessage,ToolMessage tools_by_name={t.name:tfortin[get_weather]}messages=[HumanMessage("北京今天天气怎么样?")]# ① 模型输出调用意图ai_msg=llm_with_tools.invoke(messages)messages.append(ai_msg)print("模型想调:",ai_msg.tool_calls)# ② 你的代码真正执行forcallinai_msg.tool_calls:selected=tools_by_name[call["name"]]output=selected.invoke(call["args"])# 这里才是真的执行print("执行结果:",output)# ③ 把结果作为 ToolMessage 塞回对话messages.append(ToolMessage(content=str(output),tool_call_id=call["id"]))# ④ 再问一次,拿到最终回答final=llm_with_tools.invoke(messages)print(final.content)# 北京今天是晴天,气温 26℃,东南风 3 级。关键点:
ToolMessage必须带tool_call_id,跟tool_calls里的id对应,
模型才知道这个结果是哪个调用的返回;- 多轮工具调用(模型调完 A 又要调 B)就是把这个循环跑多次,
直到tool_calls为空。
五、多个工具 + 强制选择
@tooldefsearch_order(order_id:str)->str:"""根据订单号查询订单状态和物流信息。"""returnf"订单{order_id}:已发货,预计明天送达"@tooldefsend_email(to:str,subject:str,body:str)->str:"""发送邮件给用户。需要收件人、主题和正文。"""returnf"邮件已发送给{to}"tools=[get_weather,search_order,send_email]llm_with_tools=llm.bind_tools(tools)强制模型必须调工具(不想让它直接回答时):
llm.bind_tools(tools,tool_choice="any")# 必须调,随便哪个llm.bind_tools(tools,tool_choice="search_order")# 必须调这个llm.bind_tools(tools,tool_choice="none")# 禁止调(等于没绑)六、并行调用与容错
6.1 一次要调多个工具
用户问"北京和上海天气分别怎么样",模型可能一次返回两个tool_calls。
逐个执行即可(上面的循环已经处理了)。要并发的话可以:
importasyncioasyncdefrun_all(calls):returnawaitasyncio.gather(*[tools_by_name[c["name"]].ainvoke(c["args"])forcincalls])6.2 工具报错了怎么办
forcallinai_msg.tool_calls:try:output=tools_by_name[call["name"]].invoke(call["args"])exceptExceptionase:# 关键:把错误也告诉模型,让它自己决定重试还是换个办法output=f"工具调用失败:{e}。请检查参数后重试,或换一种方式回答。"messages.append(ToolMessage(content=str(output),tool_call_id=call["id"]))不要让异常中断整个流程。把错误信息作为 ToolMessage 返回,
模型经常会自己修正参数重试一次——这比你在代码里写重试逻辑灵活得多。
七、结构化输出:工具调用的另一种用法
如果你只是想让模型输出结构化 JSON,而不是真要调用函数,
可以用一个"空实现"的工具:
frompydanticimportBaseModel,FieldclassPerson(BaseModel):"""一个人的基本信息"""name:str=Field(description="姓名")age:int=Field(description="年龄")city:str=Field(description="所在城市")@tool(args_schema=Person)defextract_person(name:str,age:int,city:str)->str:"""从文本中提取人物信息。"""return""# 不需要实现,我们只要模型的参数llm_with_tools=llm.bind_tools([extract_person])result=llm_with_tools.invoke("张三今年 28 岁,住在杭州")print(result.tool_calls[0]["args"])# {'name': '张三', 'age': 28, 'city': '杭州'}这就是with_structured_output的底层原理之一。
不过真要做结构化输出,直接用with_structured_output更简洁,后面会单独讲。
八、常见坑
| 现象 | 原因 | 解法 |
|---|---|---|
| 模型不调工具,自己编答案 | 描述不清 / 模型觉得不需要 | 补全 docstring;或tool_choice="any" |
| 参数类型不对(传了字符串 28) | schema 约束不强 | 用 Pydantic 定义args_schema |
| 模型调用了不存在的工具 | 工具列表和描述不一致 | 检查tools_by_name是否覆盖全部 |
| 拿到结果后模型还是瞎说 | 没把 ToolMessage 塞回去 | 检查tool_call_id是否对应 |
| 工具执行了但报错中断 | 没做异常捕获 | 把错误转成 ToolMessage 返回 |
| 中文参数变成乱码/被转义 | 一般没问题,检查日志打印方式 | 直接读args字典,别打印原始 JSON |
九、小结
- 模型只输出"调用意图"的 JSON,执行永远在你的代码里——这是理解一切的前提;
@tool装饰器的三要素:函数名、docstring(决定什么时候被调用)、类型注解(决定参数);bind_tools把 schema 绑到模型上,返回的AIMessage.tool_calls是意图而非结果;- 完整循环:模型出意图 → 你执行 → 结果塞回 ToolMessage(带 tool_call_id)→ 再问模型;
tool_choice可强制调用(any/ 指定名字 /none);- 工具报错要转成 ToolMessage 返回给模型,让它自己重试,比硬编码重试更灵活。
下一篇讲自定义 Tool 的几种写法和它们各自的适用场景。