【LangGraph实战】《LangGraph实战》_18.[第1章 AI智能体原理] 工具使用是Agent的超能力:从Function Calling说起

你的LLM再聪明,不会调用工具也只是个“光说不练”的嘴炮侠。Function Calling不是简单的API对接,而是Agent从“空想家”进化为“实干家”的成人礼。本文将带你打通从工具定义、绑定、执行到LangGraph多步编排的全链路,彻底告别“模型瞎编、工具报错、节点乱飞”的新手噩梦。读完这篇,你会明白:手里没剑和有剑不用,真的是两码事。
文字目录
- 一、Function Calling:Agent从“纸上谈兵”到“动手干活”的分水岭
- 二、工具定义:别让Schema成了你的第一道坎
- 三、工具绑定:LLM和工具如何“对上眼”
- 四、执行与观察:工具调用的闭环才是灵魂
- 五、LangGraph中的工具节点:从单次调用到多步编排
- 写在最后
嗨,大家好呀,我是你的老朋友精通代码大仙。接下来我们一起学习 《LangChain核心技术与LLM项目实践》。
老话说得好:“手里没剑,和有剑不用,是两码事。” 很多新手刚接触AI Agent那会儿,总觉得只要Prompt写得够骚、提示词工程玩得够花,大模型就能上天入地、无所不知。结果现实啪啪打脸——你一问她今天北京天气怎么样,她一本正经地给你编一个“晴转多云,18度”;你让她去查一下数据库里有多少条订单记录,她直接给你凭空捏造一个JSON数组。你气得想砸键盘,她却无辜地眨眨眼:“我尽力了呀。”
为啥会这样?因为你没给模型“佩剑”——工具(Tools)。Function Calling就是递剑的那只手,而LangGraph则是教模型怎么舞剑的剑谱。今天咱就把这整套“剑诀”掰开了、揉碎了,用最接地气的方式讲给你听。坐稳了,发车!
一、Function Calling:Agent从“纸上谈兵”到“动手干活”的分水岭
1. 点题
Function Calling,国内也常叫“工具调用”或者“函数调用”。它最早是OpenAI在2023年年中推出来的,现在已经成了各大主流模型(GPT-4、Claude、Qwen、Llama等)的标配能力。说白了,Function Calling就是LLM的一种结构化输出意图:模型在生成文本的过程中,可以自己判断“现在我需要调用某个外部工具”,然后按照预定的格式(通常是一段JSON)把工具名和参数吐出来。
注意,这里的关键词是**“模型自己判断”**。不是你在代码里写一堆正则表达式去匹配模型的回复,也不是你逼着模型说“请输出[CALCULATOR]”。是模型在推理阶段,基于你给的工具描述,主动决定“我要调用计算器,参数是{"expression": "123*456"}”。这一步,就是Agent从“动嘴皮子”进化到“动手干活”的分水岭。
2. 痛点分析
新手最容易踩的坑,就是把Function Calling当成普通的API调用,或者干脆以为Agent就是“高级一点的Prompt工程”。我见过太多人这样写代码:
# 新手误区:口头允许 + 期望模型自觉
response = llm.invoke(
"你可以使用计算器。请计算 123 乘以 456 等于多少?"
)
print(response.content)
# 模型输出:123 乘以 456 等于 56088(其实是瞎编的,经常算错)
你发现了没?你虽然嘴上说“你可以用计算器”,但模型底层并没有获得任何结构化的工具契约。它既不知道计算器具体有几个参数,也不知道参数类型是string还是number。它只能靠自己的“幻觉”去硬算,错了也不自知。这就好比你让一个实习生去盖章,但你没告诉他章在哪个抽屉、盖在哪个位置,他只能靠猜。
还有一种更隐蔽的误区:在LangChain里混用Tool和普通的Runnable。有些小伙伴写了个函数,用@tool装饰器包了一下,然后直接丢给llm.invoke(),结果发现模型根本不调用。为啥?因为你没做bind_tools,模型根本“看不见”这个工具的存在。
3. 解决方案与正确做法
正确的姿势是:把工具当成模型推理空间里的头等公民,通过框架提供的标准API显式绑定。
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
@tool
def calculator(expression: str) -> str:
"""计算数学表达式并返回字符串结果。
当用户需要进行加减乘除、幂运算、取余等数学计算时调用。
"""
try:
return str(eval(expression))
except Exception as e:
return f"计算错误:{e}"
llm = ChatOpenAI(model="gpt-4o-mini")
# 关键一步:bind_tools,让模型真正“看见”这把剑
llm_with_tools = llm.bind_tools([calculator])
response = llm_with_tools.invoke("123 乘以 456 等于多少?")
print(response.tool_calls)
# 输出:[{"name": "calculator", "args": {"expression": "123*456"}, "id": "call_xxx"}]
看到没?bind_tools就像在给模型发装备栏,告诉它:“你现在有且只有一把计算器,当你遇到数学题时,请输出一段标准的JSON来调用它。” 模型的输出不再是直接的文本,而是一个结构化的tool_calls列表。你的代码拿到这个列表后,再去执行真实的函数,这就完成了意图识别和物理执行的解耦。
这样做的好处简直不要太香:第一,模型不再需要“硬背”所有知识,遇到实时数据、数学计算、外部系统交互,统统交给专业工具;第二,调用过程标准化,你不用再写一堆脆弱的正则去匹配;第三,扩展性极强,今天加个计算器,明天加个搜索引擎,后天接个数据库,装备栏随时升级。
4. 小结
Function Calling不是Prompt的附属品,而是Agent的“手”。没有这只手,LLM永远只能坐在原地动嘴;有了这只手,它才能真正的“起身干活”。
二、工具定义:别让Schema成了你的第一道坎
1. 点题
模型之所以能决定调用哪个工具,全靠你给它看的“说明书”——也就是工具的Schema。这个Schema通常包含三件套:name(工具名)、description(工具描述)、parameters(参数定义)。在LangChain里,如果你用@tool装饰器,框架会自动帮你从函数签名和docstring里提取Schema;如果你用Pydantic,还能做更精细的控制。
很多新手觉得,定义工具不就是写个函数吗?这有啥难的?但问题恰恰出在这里——你写的不是给人看的代码,而是给模型看的“决策依据”。模型不像人类那样有常识,它只会根据你给的文本描述做概率推断。你的描述多模糊,模型的决策就有多离谱。
2. 痛点分析
最常见的错误,就是把description写得像写给领导看的周报——假大空。比如:
@tool
def search(query: str) -> str:
"""搜索工具"""
# ...
@tool
def get_weather(city: str) -> str:
"""获取天气"""
# ...
你想想,模型看到这两个工具时,它怎么判断“北京今天冷不冷”该用search还是get_weather?它的description完全一样空泛,参数也只有个query和city的区别。结果呢?模型掷骰子随便选一个,选错了就给你返回一堆不相关的搜索结果,或者把城市名当成搜索关键词。
还有一种坑,是Schema和实现不一致。比如你在参数里定义了date: str,但description里写的是“日期格式为YYYY/MM/DD”,结果模型输出了“2024-06-01”,你的函数接收后解析报错。更隐蔽的是类型错误:模型输出{"age": "25"},但你的函数期望的是int,一调用就炸。
# 错误示范:类型和描述都模糊不清
@tool
def query_user(name: str, age):
"""查询用户"""
...
3. 解决方案与正确做法
定义工具的时候,请把模型当成一个“刚入职、零常识、但听话”的实习生。你要给它写一份边界清晰、示例充足、类型严格的接口文档。
from pydantic import BaseModel, Field
from langchain_core.tools import tool
class WeatherInput(BaseModel):
city: str = Field(description="城市中文名或英文名,例如:北京、Shanghai")
date: str = Field(description="查询日期,格式严格为 YYYY-MM-DD,例如:2024-06-01")
@tool(args_schema=WeatherInput)
def get_weather(city: str, date: str) -> str:
"""
获取指定城市在指定日期的天气预报信息。
仅在用户明确询问天气、温度、降雨、风力时使用。
如果用户询问的是新闻、股票、历史知识,请勿调用此工具。
"""
# 模拟调用天气API
return f"{city} {date} 天气:晴,25℃,东南风2级"
看到这份“说明书”的含金量了吗?
- description里明确了使用时机(“仅在询问天气时”)和反例(“不用于新闻、股票”)。
- 参数用了Pydantic的
Field,每个字段都有 human-readable 的描述和格式示例。 - 边界清晰,模型不容易把它和搜索工具搞混。
另外,强烈建议给参数设置好required和默认值。如果某个参数是可选的,在Pydantic里用Optional或者默认值标明,否则模型可能会为了凑参数而 hallucination(幻觉生成)一个出来。
4. 小结
Schema是模型与外部世界之间的“契约”。你写得敷衍,模型就给你摆烂;你写得严谨,模型才能精准地“指哪打哪”。
三、工具绑定:LLM和工具如何“对上眼”
1. 点题
定义好了工具,下一步是让LLM和工具“看对眼”。在LangChain的世界里,这个过程叫bind_tools。你可以把它理解为“装备挂载”:你把打造好的宝剑(Tool)挂到模型(LLM)的腰间,模型在推理时才会知道“我有这些装备可用”。
但绑定这件事,远不是llm.tools = [tool1, tool2]这么简单。不同的模型提供商(OpenAI、Anthropic、阿里通义等)对工具绑定的底层协议并不完全相同。LangChain的价值就在于抹平了这些差异,但你得用对它封装好的标准方法。
2. 痛点分析
我见过最野的路子,是有人在System Prompt里手动拼接工具描述:
# 野路子:企图用Prompt替代bind_tools
system_msg = """你有以下工具可用:
1. calculator: 用于计算
2. search: 用于搜索
请按以下格式调用:
工具名: xxx
参数: xxx
用户:1+1等于几?
"""
response = llm.invoke(system_msg)
# 模型输出格式百花齐放,根本不可控
这种做法堪比在2024年还在用记事本写前端,没有类型检查、没有格式校验、没有错误兜底。模型有时候输出JSON,有时候输出Markdown代码块,有时候直接给你一段自然语言解释。你去解析吧,一解一个不吱声。
另一个常见误区是工具数量过多。有的小伙伴一激动,把二十几个工具全部bind_tools上去,结果发现模型开始“选择困难症”了。明明该调用天气工具,它却给你调了个股票查询,因为它的上下文里塞了太多工具描述,注意力被带偏了。更严重的是,有些模型对工具总长度有隐性限制,超长的Schema会被截断,导致模型只能看见前半截工具。
还有一种情况:绑定了工具但设置了错误的tool_choice。tool_choice="auto"是默认的,由模型自己决定;tool_choice="required"会强制模型必须调用至少一个工具(哪怕用户只是打招呼“你好”,它也会强行调个工具);tool_choice="none"则禁用工具。新手经常在该用auto的时候用了required,结果模型像个憨憨,每句话都要调一下工具。
3. 解决方案与正确做法
标准做法永远是走LangChain/LangGraph的官方通道。在LangGraph的节点中,推荐在调用前动态绑定,而不是全局写死:
# 正确示范:在LangGraph节点中标准绑定
def agent_node(state: State):
# 根据业务动态选择要挂载的工具子集
available_tools = [calculator, get_weather]
llm_with_tools = llm.bind_tools(
available_tools,
tool_choice="auto" # auto | required | none
)
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
这里有几个关键点:
- 动态绑定:不要一上来就把所有工具全挂上去。可以根据对话阶段、用户身份、业务场景动态选择工具子集。比如用户只是闲聊,就只挂通用工具;用户进入了“数据分析模式”,再把SQL查询工具挂上。
- 控制
tool_choice:绝大多数场景用auto。只有当你明确知道“这一步必须调用工具”时(比如用户上传了一张图,你必须调用OCR),才用required。 - 版本对齐:确保你的
langchain-openai(或其他provider包)版本是最新的。OpenAI的Function Calling协议升级过好几次,老版本可能不支持并行工具调用(Parallel Tool Calling)等新特性。
4. 小结
工具绑定不是“挂载插件”就完事了,而是要让模型在正确的时机、看见正确的选项。少即是多,别让你的Agent腰间挂了二十把剑,拔的时候都不知道抽哪把。
四、执行与观察:工具调用的闭环才是灵魂
1. 点题
Function Calling流程里有一个最容易被忽略的环节——闭环。很多新手以为,模型输出tool_calls,我去执行一下函数,拿到结果,直接返回给用户,齐活。错!这顶多算“半闭环”。真正的闭环是:模型决策 → 系统执行 → 结果回传 → 模型再决策/总结。少了最后一步,模型就像蒙着眼干活,完全不知道自己执行的结果是对是错。
在LangChain的消息体系里,这个闭环对应着四种消息的流转:
HumanMessage:用户提问AIMessage:模型的回复,可能包含tool_callsToolMessage:工具执行后的观察结果(Observation)AIMessage:模型基于观察结果给出的最终总结
2. 痛点分析
最常见的断环错误,长这样:
messages = [HumanMessage(content="北京今天天气怎么样?")]
ai_msg = llm_with_tools.invoke(messages)
if ai_msg.tool_calls:
tool_call = ai_msg.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
result = selected_tool.invoke(tool_call["args"])
# 错误:直接把结果扔给用户,模型根本没看见结果!
return {"final_answer": result}
你发现问题了吗?result确实是你想要的数据,比如“晴,25℃”。但这个结果没有以任何方式告知LLM。LLM上一次输出还停留在“我决定调用天气工具”,它压根不知道工具返回了啥。你直接把工具返回的raw data丢给用户,用户体验极差——如果是JSON,用户直接看懵了;如果是长文本,没有重点提炼。
还有一种情况是工具报错处理不当。比如网络超时、数据库连接失败、参数不合法。新手经常让异常直接往上抛,导致整个LangGraph图执行中断,页面直接报500。你想想,用户就问个天气,结果因为API超时,整个Agent崩了,这谁顶得住?
# 错误示范:裸奔式调用,毫无兜底
def bad_tool_call(tool_call):
tool = tool_map[tool_call["name"]]
return tool.invoke(tool_call["args"]) # 一炸全炸
3. 解决方案与正确做法
完整的闭环,必须用ToolMessage把观察结果塞回消息列表,再给模型一次“审阅”的机会。
from langchain_core.messages import ToolMessage
def agent_loop(state: State):
messages = state["messages"]
# 第一步:模型决策
ai_msg = llm_with_tools.invoke(messages)
if not ai_msg.tool_calls:
# 没有工具调用,直接回答
return {"messages": [ai_msg]}
# 关键:先把模型的决策(AIMessage)追加到历史
messages.append(ai_msg)
# 第二步:执行工具,并构造 ToolMessage
for tool_call in ai_msg.tool_calls:
selected_tool = tool_map.get(tool_call["name"])
# 重要:工具内部也要做 try-except 兜底
try:
observation = selected_tool.invoke(tool_call["args"])
except Exception as e:
observation = f"工具执行失败,错误信息:{str(e)}"
# 关键中的关键:ToolMessage 必须带上 tool_call_id
messages.append(ToolMessage(
content=str(observation),
tool_call_id=tool_call["id"]
))
# 第三步:模型基于观察结果做最终总结
final_ai_msg = llm.invoke(messages)
return {"messages": [final_ai_msg]}
这段代码里有几个生死攸关的细节:
messages.append(ai_msg):必须把模型要调工具的意图先存进历史,不然上下文会断片。ToolMessage:不是普通的字符串,也不是HumanMessage,是专门用来承载工具返回结果的消息类型。tool_call_id:必须和ai_msg.tool_calls里的id一一对应。这是OpenAI等平台的强校验,对不上就直接报错。- 异常兜底:工具执行失败时,不要把异常抛出去,而是把错误信息包装成字符串,通过
ToolMessage喂给模型。聪明的模型看到“工具执行失败”,会自己决定重试、换工具,或者礼貌地告诉用户“刚才查不到,可能是网络问题”。
这样做的好处是什么?Agent有了“反思”的能力。它第一次调用天气工具返回了数据,第二次调用时它能基于这些数据说:“北京今天晴天,25度,挺暖和的,适合出门。” 而不是扔给用户一段冰冷的API返回。
4. 小结
没有Observation的Agent,就像做题不看答案,永远拿不到满分。ToolMessage不是可选项,是闭环的必经之路。
五、LangGraph中的工具节点:从单次调用到多步编排
1. 点题
如果你只做一个“单轮工具调用”,用LangChain的Expression Language(LCEL)其实就够了。但真正的Agent往往要走很多步:先思考,再调用工具,观察结果,发现不够,再调用第二个工具,再观察,最后总结。这种多步循环,就是LangGraph登场的时刻。
LangGraph把Agent的执行流程抽象成了状态图(StateGraph):节点(Node)负责干活,边(Edge)负责决定下一步去哪。对于工具调用,LangGraph甚至提供了开箱即用的ToolNode和create_react_agent,让你彻底告别“面条代码”。
2. 痛点分析
不用LangGraph,自己手写多步Agent,代码很快会变成一锅粥:
# 面条代码:所有逻辑塞在一个超级节点里
def super_agent(state):
msg = llm.invoke(state["messages"])
if msg.tool_calls:
results = []
for tc in msg.tool_calls:
if tc["name"] == "search":
results.append(search(**tc["args"]))
elif tc["name"] == "calculator":
results.append(calculator(**tc["args"]))
elif tc["name"] == "get_weather":
results.append(get_weather(**tc["args"]))
# 无限 elif,每次加工具都要改这里...
state["messages"].extend(results)
# 还要手动判断要不要继续...
second_msg = llm.invoke(state["messages"])
if second_msg.tool_calls:
# 再来一轮?代码已经没法看了
pass
return state
这种写法有几个致命问题:
- 高耦合:工具执行逻辑和Agent决策逻辑缠在一起,加个新工具要改三处。
- 不可观测:出了问题你都不知道卡在哪一步,打印日志像大海捞针。
- 无法中断:如果想在工具执行前加个人类确认(Human-in-the-loop),基本不可能。
- 状态混乱:消息列表、中间变量、最终结果全塞在一个字典里,稍不留神就把上下文覆盖了。
还有一种State设计上的坑:有些小伙伴把State定义成{"query": "", "result": ""},完全抛弃了消息列表。LangGraph的精髓就是基于消息历史的循环,你丢了messages,就像炒菜不放盐,食之无味。
3. 解决方案与正确做法
在LangGraph里,请无条件拥抱节点拆分 + 条件边(Conditional Edges)。
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from langgraph.graph.message import add_messages
# 1. 定义状态:核心就是消息列表
class State(TypedDict):
messages: Annotated[list, add_messages]
# 2. 准备工具和模型
tools = [calculator, get_weather, search]
tool_node = ToolNode(tools)
# 3. Agent节点:负责思考
def agent(state: State):
llm_with_tools = llm.bind_tools(tools)
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
# 4. 条件边:判断是继续调工具,还是结束
def should_continue(state: State):
last_msg = state["messages"][-1]
if last_msg.tool_calls:
return "tools"
return END
# 5. 构图
builder = StateGraph(State)
builder.add_node("agent", agent)
builder.add_node("tools", tool_node)
builder.set_entry_point("agent")
# 核心:条件边实现“思考-行动”循环
builder.add_conditional_edges("agent", should_continue, {
"tools": "tools",
END: END
})
# 工具执行完,必须回到agent重新思考
builder.add_edge("tools", "agent")
graph = builder.compile()
看看这套架构的优雅之处:
- 单一职责:
agent节点只负责让LLM思考,tools节点只负责干活,互不干涉。 - 循环自然:
tools→agent的边天然构成了ReAct循环(Reasoning + Acting)。 - 无限扩展:新加工具?只需要在
tools = [...]里append一行,ToolNode会自动处理分发。 - 可视化:你可以随时
graph.get_graph().draw_mermaid()把图画出来,排查流程问题。 - 随时中断:LangGraph支持
interrupt_before=["tools"],在工具执行前弹出来让人类审核,这在金融交易、敏感操作场景下是刚需。
对于更简单的场景,你甚至可以直接用create_react_agent一行代码搞定:
from langgraph.prebuilt import create_react_agent
graph = create_react_agent(llm, tools=tools)
但建议你先把底层节点构图的原理吃透,再去看封装好的API。就像学Spring Boot之前,得先懂Servlet和IoC的本质。不然一旦封装不满足需求,你就傻眼了。
4. 小结
LangGraph不是来增加复杂度的,而是把“思考-行动”的混沌逻辑,变成清晰的节点与边。学会用ToolNode和条件边,你的Agent才算真正迈进了工程化的大门。
写在最后
读到这儿,你应该已经明白了:Function Calling不是某个模型的“小花活”,而是整个AI Agent体系的基石。从定义一把趁手的Schema,到正确地bind_tools装备上腰,再到严谨地完成执行与观察的闭环,最后在LangGraph里用节点和边编织出复杂而优雅的多步工作流——这一路走来,每一步都至关重要。
很多新手总幻想着Prompt里多写几行“你必须调用工具”就能万事大吉。但现实是,Agent的稳定性没有捷径,它藏在你对Schema的精雕细琢里,藏在你对ToolMessage的认真回填里,藏在你把超级节点拆成职责清晰的子图的那份耐心里。
编程之路从来不易,但每一步成长都算数。今天的你,可能还在为一个tool_call_id对不上而抓狂;明天的你,就能从容地设计出让同事眼前一亮的Agent工作流。保持好奇,保持折腾,别怕报错——报错是编译器在教你做人,也是你在变强的痕迹。
工具已经递到你手里了,去打造属于你的智能体吧。
关注私信备注:“资料代找获取”,全网计算机学习资料代找:例如:
《课程:AI 大模型工程师系统课程 (22 章完整版 持续更新)》
《课程:AI 大模型系统实战课第四期 (2026 年开课 持续更新)》
《课程:2026 年 AGI 大模型系统课 23 期》
《课程:2026 年 AGI 大模型系统课 21 期》
《课程:AI 大模型实战课 8 期 (2026 年 2 月最新完结版)》
《课程:AI 大模型系统实战课三期》
《课程:AI 大模型系统课程 (2026 年 2 月开课 持续更新)》
《课程:AI 大模型全阶课程 (2025 年 12 月开课 2026 年 6 月结课)》
《课程:AI 大模型工程师全阶课程 (2025 年 10 月开课 2026 年 4 月结课)》
《课程:2026 年最新大模型 Agent 开发系统课 (持续更新)》
《课程:LLM 多模态视觉大模型系统课》
《课程:大模型 AI 应用开发企业级项目实战课 (2026 年 1 月开课)》
《课程:大模型智能体线上速成班 V2.0》
《课程:Java+AI 大模型智能应用开发全阶课》
《课程:Python+AI 大模型实战视频教程》
《书籍:软件工程 3.0: 大模型驱动的研发新范式.pdf》
《课程:人工智能大模型系统课 (2026 年 1 月底完结版)》
《课程:AI 大模型零基础到商业实战全栈课第五期》
《课程:Vue3.5+Electron + 大模型跨平台 AI 桌面聊天应用实战 (2025)》
《课程:AI 大模型实战训练营 从入门到实战轻松上手》
《课程:2026 年 AI 大模型 RAG 与 Agent 智能体项目实战开发课》
《课程:大模型训练营配套补充资料》
更多推荐


所有评论(0)