基于大语言模型的智能体可以通过工具扩展能力。工具是它在推理循环中调用的外部函数或 API。本课讨论为什么需要工具、如何定义和注册计算器、搜索及自定义 API,智能体如何决定调用时机,以及如何通过 LangGraph 实现工具调用循环。学习后,你应当理解:
- 工具对智能体自主性与能力的重要作用。
- 如何提供合适的 schema 和描述,组织并注册工具。
- 智能体如何决定何时、以什么方式调用工具,以及如何组织相关提示词。
- 如何在 LangGraph 循环中实现工具调用节点。
- 如何处理工具输出,返回决策过程,最终形成答案。
工具如何增强 LLM 智能体
即使先进的 LLM 也有固有限制:
- 知识存在截止时间。
- 无法始终可靠完成精确计算。
- 受到训练数据范围限制。
外部工具提供超出模型基础知识的信息与操作,使智能体能够:
- 取得动态的新信息:搜索网页或查询数据库,获得训练集中没有的最新数据。实战示例会将向量数据库封装成可调用工具。
- 执行专业或精确计算:LLM 擅长推理,但提示不合适时也可能在普通数学题上出错。计算器、代码执行或科学 API 可以提供准确结果。
- 与外部服务交互:例如查询天气、预订航班或访问专有知识库。
- 实时适应请求:通过工具取得当前信息,例如查询实时股价,再针对用户问题回答。
工具让智能体能够决定查询资料、计算或调用 API,提高实际场景中的能力和可靠性。例如,用户问“库存里有哪些耳机?”时,普通 LLM 不知道当前库存,也可能产生幻觉;具有内部库存查询工具的智能体,可以先取得最新商品信息再回答。
整体流程仍是思考 → 行动 → 观察。LLM 处理用户输入,决定调用工具还是直接回答;需要工具时,输出名称与参数;系统执行后把结果传回,模型据此继续处理并生成答案。复杂任务可能需要多轮或多个工具。
基本工具使用流程如下:
- Agent
- 函数定义与工具注册表属于 Agent;模型交互和工具调用由 Agent 执行。
- 调用阶段
- 查询、函数定义、模型交互、工具调用、再次模型交互与回答按图中编号展开。
分阶段阅读 · 4 个阶段
- 用户查询传给智能体,通常是一个 Python 应用。
- 可用函数和工具在智能体代码中定义,属于程序性记忆。
- 工具列表与用户查询一同通过提示词提供给 LLM,由模型决定调用哪些函数及参数。
- 智能体应用执行函数。
- 用户查询与函数返回数据一起再次提供给 LLM。
- 模型组织答案,通过智能体返回用户。
工具使智能体超越内置知识和文本生成能力。下面介绍如何定义和注册工具。
组织与注册工具
智能体框架中的工具通常是执行特定操作的函数或 API,例如查询数据库的 Python 函数、REST API 调用、计算函数或网页搜索。工具用途很广,通常都可以封装为 Python 函数,包括需要时的代码执行。每个工具一般具有:
- 名称:简短标识符,例如
calculator、web_search或retrieve_context。 - 描述:说明用途和预期输入。工具定义会进入模型上下文,模型需要据此决定何时调用,因此描述非常重要,也是上下文与提示词工程的一部分。
- 输入 schema 或函数签名:明确参数,例如计算器需要两个数字,搜索需要查询字符串。
- 函数实现:调用时执行的实际代码,例如算术运算或 API 请求。
工具需要向 LLM 或智能体框架注册,让模型知道它们存在以及如何调用。在 LangGraph 与 LangChain 工具接口中,通常先通过装饰器或函数包装工具,再绑定到模型。LangChain 的 @tool 可以根据 Python 函数提取输入类型和 docstring,生成工具描述:
from langchain_core.tools import tool
@tool
def calculate_sum(a: float, b: float) -> float:
"""将两个数字相加并返回结果。
Args:
a: 第一个操作数。
b: 第二个操作数。
Returns:
两个数字的和。
"""
return a + b
@tool
def retrieve_context(query: str, top_k: int = 5) -> str:
"""查询向量数据库,取得与查询相关的上下文。
Args:
query: 相似度搜索使用的查询。
top_k: 需要检索的文本块数量。
Returns:
从向量数据库取得的上下文。
"""
return query_db(query, top_k)
示例定义了计算工具 calculate_sum 和向量搜索工具 retrieve_context。@tool 将函数转换为工具对象,从函数名取得名称,从 docstring 取得描述。例如,calculate_sum 的描述是“将两个数字相加并返回结果”。其中 query_db 由应用提供真实的向量数据库查询实现。
Docstring 清楚说明用途与输入。LLM 很大程度上根据用户需求与工具描述的匹配程度选择工具,因此好的描述可以提高选对工具的机会。
将工具注册到模型:定义后,通过绑定让模型知晓工具。例如:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model_name="gpt-4.1-mini", temperature=0)
# 将工具绑定到 LLM
llm_with_tools = llm.bind_tools([calculate_sum, retrieve_context])
bind_tools([...]) 将工具及 schema 提供给聊天模型。框架在底层把描述和用法加入上下文,或使用模型原生 function calling API。llm_with_tools 的调用方式与普通模型相同,同时能够提出工具调用请求。
框架使用方便,也隐藏了一些实现细节。采用工具框架前,应理解底层过程,并尽可能能够调整实现,尤其在生产环境中需要进一步提高系统质量时。后续还会讨论如何自行实现工具选择。
工具设计建议:
- 名称清楚具体,体现功能,例如
retrieve_context。 - 描述简洁且信息充分,便于模型与用户请求比较。
- 功能集中,使用多个范围明确的小工具。
- 工具数量以必要为限。大量工具可能令模型困惑;需要较多工具时,可以通过路由或功能分组组织。
- 认真定义输入 schema,明确多参数或结构化输入的格式,利用
int、str或 JSON Schema 等标准类型。
接下来讨论提示词格式,以及模型在推理中如何决定调用工具。
工具调用的提示词格式
工具代码准备好后,还需要让模型知道调用方法。常见方式有两种:
- 模型原生 function calling:GPT-4.1 等模型允许通过 API 提供函数列表和 schema,模型需要工具时直接输出结构化名称与参数。LangChain
bind_tools会利用相应能力,框架在底层发送定义。 - 通过特殊格式提示,采用 ReAct 风格:没有专用工具 API 的模型,可以在 system 或 few-shot 提示词中描述工具,并要求采用指定格式。例如:“你可使用计算器
calculate_sum(a, b)和库存上下文检索retrieve_context(query, top_k)。需要工具时,按<tool_name>(<args>)输出行动;否则直接回答。”模型可能产生calculate_sum(2, 3),框架解析后执行。此方法需要认真设计格式与示例。
通过绑定工具,或手动加入工具定义并按照 LangGraph 所需格式组织模型消息,可以让框架识别工具请求。模型通常输出:
- 无需工具时的直接答案。
- 需要工具时,框架能够识别的结构化调用。
例如,发送普通问候:
result = llm_with_tools.invoke("Hello world!")
通常得到“你好,有什么可以帮助你?”一类普通回应,无需工具。如果询问商品库存:
result = llm_with_tools.invoke("What earphones can I get?")
输出可能包含工具调用。LangChain/LangGraph 返回的 AIMessage 具有 tool_calls 属性;也可以使用 Instructor 等框架,再转换为兼容的 AIMessage,实战采用了这种方式。例如:
[{'name': 'retrieve_context', 'args': {'query': 'earphones', 'top_k': 5}, 'id': 'xyz', 'type': 'tool_call'}]
这里模型选择 retrieve_context,参数为 query="earphones"、top_k=5。模型已经表达需求,接下来由框架或应用代码执行工具并返回结果。提示词和结构化格式让系统能够准确理解这一请求。
工具选择取决于上下文、输入与描述的匹配。清楚的工具说明和提示词策略很重要;格式不佳或描述含糊时,模型可能误用工具,或需要时没有调用。采用 ReAct 风格的框架还常加入 few-shot 调用示例。
通过 LangGraph 实现工具调用
LangGraph 将智能体定义为节点函数与转换边组成的图。接入工具通常需要:
- 定义工具函数,例如前面的
calculate_sum与retrieve_context。 - 将工具绑定到 LLM;可以使用 LangChain,也可以手动管理以获得更具体的控制。
- 创建 LLM 节点,根据当前对话状态调用模型,取得下一条响应。
- 创建工具执行节点,检查响应中的调用,执行对应工具并将结果加入状态。也可以使用
ToolNode;本课手动实现,以说明工作机制。 - 配置图边与循环:LLM 请求工具时进入工具节点,否则结束;工具完成后返回 LLM 节点,使模型利用结果继续处理。
步骤 1、2:工具与模型。假设已完成前述定义和绑定,llm_with_tools 可以使用,例如 GPT-4.1-mini 或支持工具调用的本地模型。为 LangGraph 定义状态:
from langgraph.graph import StateGraph, START, END
from operator import add
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage, AnyMessage
from typing import List, Annotated
from pydantic import BaseModel
# LangGraph 状态保存对话中的消息列表
class MessagesState(BaseModel):
messages: Annotated[List[AnyMessage], add]
MessagesState 保存对话消息。add 指定更新时追加新消息,保留已有历史。
步骤 3:LLM 节点。接收包含当前全部消息的状态,调用模型取得下一条响应:
def run_llm(state: MessagesState) -> dict:
# 使用当前对话调用已绑定工具的 LLM
output_message = llm_with_tools.invoke(state.messages)
# 返回需要追加的消息更新
return {"messages": [output_message]}
这是简化示例。invoke(...) 返回 AIMessage;如果未用 LangChain 绑定工具,需要把结果转换成相同消息格式,以便利用 LangGraph 的组件。节点将消息包装为列表返回,由 add 合并到现有消息列表。
步骤 4:工具执行节点。只有模型请求工具时才运行。它检查最新的 AIMessage,执行调用并追加结果。消息格式十分重要,ToolNode 同样依赖正确格式:
def execute_tools(state: MessagesState) -> dict:
# 最新 AIMessage 可能包含工具调用
ai_message = state.messages[-1]
results = []
if hasattr(ai_message, "tool_calls") and ai_message.tool_calls:
for tool_call in ai_message.tool_calls:
tool_name = tool_call['name']
tool_args = tool_call.get('args', {})
# 按名称找到工具,也可以使用工具字典
if tool_name == "calculate_sum":
result_content = calculate_sum.invoke(tool_args)
elif tool_name == "retrieve_context":
result_content = retrieve_context.invoke(tool_args)
else:
result_content = f"[Unknown tool {tool_name}]"
# 使用 tool_call_id 将结果关联到调用
tool_msg = ToolMessage(
content=str(result_content),
tool_call_id=tool_call['id'],
name=tool_name,
)
results.append(tool_msg)
return {"messages": results}
节点读取最新消息的 tool_calls,将每次调用分配给相应工具。实际应用中,可以用 tool_name 到工具对象的字典,使查找更清楚。工具结果封装为 ToolMessage,这是 LangChain 表示工具输出的消息类型。返回 {"messages": results} 后,LangGraph 根据 add 追加消息,对话历史便包含带工具来源标记的结果。
步骤 5:图边与循环逻辑。定义 llm_node 和 tool_node 两个节点。流程从 START 进入 LLM,需要工具时执行工具再返回 LLM,无需工具时进入 END:
# 构建状态图
workflow = StateGraph(MessagesState)
workflow.add_node("llm_node", run_llm)
workflow.add_node("tool_node", execute_tools)
def tool_needed(state: MessagesState):
# 检查最新 AIMessage 是否请求工具
ai_msg = state.messages[-1]
if hasattr(ai_msg, "tool_calls") and ai_msg.tool_calls:
return "tool_node"
return END
workflow.add_conditional_edges(
"llm_node", tool_needed, {"tool_node": "tool_node", END: END}
)
# 工具完成后,让 LLM 继续处理结果
workflow.add_edge("tool_node", "llm_node")
workflow.add_edge(START, "llm_node")
graph = workflow.compile()
tool_needed 检查最新模型输出:包含工具请求时返回 tool_node,否则返回 END。它作为 llm_node 的条件边,实现每轮继续行动或结束的选择。工具节点到 LLM 节点的普通边,则保证工具完成并追加 ToolMessage 后,控制返回模型。模型在下一次输入中看到工具结果,可继续生成解答,通常也可直接给出最终答案。图从 LLM 节点开始,当模型不再请求新工具时结束。
LangGraph 面向有状态的循环工作流设计,需要连续使用多个工具时,上述图也能重复运行:第一次工具完成后,模型可以请求第二次工具,再进入同一循环。
实际使用时,也可以采用现成的 ToolNode。理解手动实现,有助于高级定制。Pydantic 状态、节点更新和工具调用接口可参见LangGraph Graph API与LangChain Tools。
处理工具响应与智能体循环
循环建立后,还需要正确接收和使用工具响应,最终形成答案:
- 记录工具输出:
execute_tools将结果包装为ToolMessage并加入状态,让工具响应进入 LLM 上下文,对应行动 → 观察。 - 返回 LLM:模型得到原始问题与工具答案,继续处理。多数情况下,可以结合新信息形成完整回答。
- 决定停止或继续:复杂问题可能一次调用仍不足够。模型可以再次输出工具请求,循环重复。只有输出不含
tool_calls时,tool_needed才转向 END。 - 生成最终答案:信息充足时,模型返回普通答案,图结束并将结果交给用户。最终文本应清楚呈现答案,移除内部工具调用语法和无关内容,提示词应明确这一要求。
实战中,LangGraph 帮助以 ToolMessage 注入结果;开发者还需检查答案是否恰当利用了工具信息。例如,工具提供重要上下文时,模型应将其用于回答,可以通过“使用工具后,将结果纳入答案”等指令改善。循环完成后返回答案,还需考虑后续对话如何继续。LangGraph 可以维护消息状态;跨独立调用或会话持久保存,需要相应的状态传递或 checkpoint 配置。
工具使用与自主执行
工具让智能体获取信息、进行计算,并执行内置知识之外的操作。清楚的 schema 与描述帮助模型理解用途和参数,决策循环则让系统动态选择工具调用或直接回答。正确组织模型消息、执行结果与状态,是可靠工具智能体的基础。