智能体中的工具调用

Tool use in agents

基于大语言模型的智能体可以通过调用外部工具(即在推理循环中调用的外部函数或 API)来扩展自身能力。本课介绍为什么智能体需要工具、如何定义并注册计算器、搜索及自定义 API,智能体如何判断调用时机,以及如何基于 LangGraph 实现完整的工具调用循环。本课涵盖以下核心要点:

  • 工具对提升智能体自主性与能力边界的作用。
  • 如何编写清晰的 schema 与描述来组织并注册工具。
  • 智能体如何决定何时、以何种方式调用工具,以及相关提示词的组织方式。
  • 如何在 LangGraph 循环中实现工具调用节点。
  • 如何处理工具输出、将其传回决策循环并生成最终回答。

工具如何增强 LLM 智能体

即便能力再强的 LLM,也存在固有的局限性:

  • 预训练知识存在截止时间。
  • 无法始终可靠地完成精确数值计算。
  • 知识储备受限于训练数据覆盖范围。

引入外部工具可以为模型补充预训练知识之外的实时信息与操作能力,使智能体能够:

  • 获取动态实时信息:通过搜索网页或查询数据库,获取训练集中不存在的最新数据(课程实战示例即将向量数据库检索封装为可调用工具)。
  • 执行专业或精确计算:LLM 擅长语义推理,但在提示不当时仍可能在基础算术题上出错;接入计算器、代码执行环境或科学计算 API 可提供精确结果。
  • 与外部服务交互:如查询实时天气、预订航班或访问企业专有知识库。
  • 实时适应请求:通过工具取得当前信息,例如查询实时股价,再针对用户问题回答。

根据实时状态响应请求:先通过工具拉取当前状态(如查询实时股价或商品库存),再结合用户提问作答。例如,当用户询问“库存里有哪些耳机?”时,纯 LLM 既不掌握实时库存也容易产生幻觉,而配备了内部库存查询工具的智能体则可先查出最新商品数据再作答。

工具调用依然遵循 思考(Think)→ 行动(Act)→ 观察(Observe) 循环:LLM 分析用户输入后,决定是调用工具还是直接回答;若需调用工具,则输出目标工具名称及参数;系统执行对应函数后将结果传回,模型据此继续推演并组织最终答案。面对复杂任务时,可能需要在多轮循环中组合调用多个工具。

基本工具调用的六个阶段如下:

图 1 · Agent工具调用的六个阶段
Agent
函数定义与工具注册表属于 Agent;模型交互和工具调用由 Agent 执行。
调用阶段
查询、函数定义、模型交互、工具调用、再次模型交互与回答按图中编号展开。
Agent把函数定义提供给模型,根据模型选择调用工具,再把结果交给模型生成面向用户的回答。
  1. 用户将查询发送给智能体(通常是一个 Python 应用程序)。
  2. 智能体代码中预先定义了可用函数与工具列表(构成系统的程序性记忆)。
  3. 智能体将工具定义列表与用户查询一并通过提示词提交给 LLM,由模型决定调用哪些函数并生成对应参数。
  4. 智能体应用程序在本地或远端执行被选中的函数。
  5. 智能体将原始用户查询与函数返回的数据再次一并提交给 LLM。
  6. 模型结合工具返回结果组织最终回答,由智能体返回给用户。

工具使智能体超越内置知识和文本生成能力。下面介绍如何定义和注册工具。

组织与注册工具

在智能体框架中,工具通常是执行特定任务的 Python 函数或 API 封装(如数据库查询函数、REST API 请求、算术函数、网页搜索乃至代码执行器)。每个工具一般包含以下四个要素:

  • 名称(Name):简短明确的标识符,如 calculator、web_search 或 retrieve_context。
  • 描述(Description):说明工具用途与预期输入。由于工具定义会直接注入模型上下文供模型判断调用时机,工具描述本身就是上下文与提示词工程的重要组成部分。
  • 输入 schema 或函数签名:明确声明参数结构(例如计算器接收两个数值,搜索工具接收查询字符串)。
  • 函数实现:工具被调用时实际执行的业务代码(如算术运算或 API 调用)。

定义好的工具需要注册到 LLM 或智能体框架中。在 LangGraph 与 LangChain 工具接口中,通常先使用 @tool 装饰器包装 Python 函数,自动从函数签名和 docstring 中提取输入类型与工具描述:

Python
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 作为工具描述(其中 query_db 由应用层提供具体的向量库查询实现)。由于 LLM 很大程度上依据用户意图与 docstring 的语义匹配度来挑选工具,编写清晰准确的 docstring 有助于提高模型选对工具的概率。

Docstring 清楚说明用途与输入。LLM 很大程度上根据用户需求与工具描述的匹配程度选择工具,因此好的描述可以提高选对工具的机会。

将工具绑定到模型:定义完成后,通过 bind_tools 将工具列表绑定到聊天模型实例:

Python
import os
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model=os.environ["TOOLS_MODEL"], api_key=os.environ["TOOLS_API_KEY"], base_url=os.environ["TOOLS_BASE_URL"], temperature=0.1)

# 将工具绑定到 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 传入函数列表与 JSON 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) 后,再由外部代码通过正则或解析器提取并执行。这种方式需要精心设计格式说明与 Few-shot 调用示例。

通过绑定工具,或手动加入工具定义并按照 LangGraph 所需格式组织模型消息,可以让框架识别工具请求。模型通常输出:

  • 无需工具时的直接答案。
  • 需要工具时,框架能够识别的结构化调用。

例如,发送普通问候:

Python
result = llm_with_tools.invoke("Hello world!")

模型无需调用工具,会直接返回常规问候回复。而当询问商品库存时:

Python
result = llm_with_tools.invoke("What earphones can I get?")

返回的 AIMessage 中则会包含 tool_calls 属性(若使用 Instructor 等库提取结构化调用,也可在转换后组装为兼容的 AIMessage):

Python
[{'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 将智能体工作流建模为由节点函数(Nodes)与转换边(Edges)组成的状态图。在 LangGraph 中接入工具调用循环通常包含五个步骤:

  1. 定义工具函数(如前文的 calculate_sum 与 retrieve_context)。
  2. 将工具绑定到 LLM(可使用 LangChain bind_tools 或手动管理)。
  3. 创建 LLM 节点:读取当前对话状态调用模型,获取下一条响应消息。
  4. 创建 工具执行节点:检查最新响应中的工具调用请求,执行对应函数并将结果写回状态(可直接使用内置的 ToolNode,本课通过手动编写该节点来展示底层机制)。
  5. 配置 条件边与循环:当 LLM 请求调用工具时路由至工具节点,工具执行完毕后连回 LLM 节点继续推理;当 LLM 不再请求工具时路由至 END 结束流程。

假设已完成前文的工具定义与 llm_with_tools 绑定(使用如 GPT-4.1-mini 或支持工具调用的本地模型),接下来定义保存对话消息列表的 LangGraph 状态:

Python
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 指定更新时追加新消息,保留已有历史。

接收包含完整历史消息的当前状态,调用已绑定工具的模型并返回新增响应:

Python
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 同样依赖正确格式:

Python
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:
                raise PermissionError(f"tool_not_allowed: {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 追加消息,对话历史便包含带工具来源标记的结果。

注册 llm_node 与 tool_node,通过条件边 tool_needed 控制分支走向:

Python
# 构建状态图
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 面向有状态的循环工作流设计,需要连续使用多个工具时,上述图也能重复运行:第一次工具完成后,模型可以请求第二次工具,再进入同一循环。

工作流从 START 进入 llm_node:若 tool_needed 检测到最新消息包含 tool_calls,则转入 tool_node 执行工具,随后沿普通有向边 workflow.add_edge("tool_node", "llm_node") 回到模型节点继续下一轮处理(如需连续调用多个工具,该循环可反复执行多轮);一旦模型输出的最新消息不再包含 tool_calls,条件边即导向 END 结束图运行。关于 Pydantic 状态定义、节点更新及内置 ToolNode 的更多用法,可参考 LangGraph Graph API 与 LangChain Tools。

处理工具响应与智能体循环

循环建立后,还需要正确接收和使用工具响应,最终形成答案:

  1. 记录工具输出:execute_tools 将执行结果包装为带 tool_call_id 的 ToolMessage 追加进状态列表,完成从“行动(Act)”到“观察(Observe)”的衔接。
  2. 返回 LLM 继续推理:模型在更新后的上下文中同时看到原始问题与工具返回结果,据此组织回答或规划下一步。
  3. 判断停止或继续循环:若单次工具调用返回的信息仍不足以解答复杂问题,模型可再次输出 tool_calls 触发下一轮循环;仅当输出不含 tool_calls 时才由 tool_needed 路由至 END。
  4. 生成面向用户的最终答案:信息充足时,模型输出干净的自然语言回答(提示词中应明确要求剔除内部工具调用语法与无关中间文本)。如需跨多次独立请求或会话保留对话历史,还需配合 LangGraph 的状态持久化或 Checkpointer 机制。

实战中,LangGraph 帮助以 ToolMessage 注入结果;开发者还需检查答案是否恰当利用了工具信息。例如,工具提供重要上下文时,模型应将其用于回答,可以通过“使用工具后,将结果纳入答案”等指令改善。循环完成后返回答案,还需考虑后续对话如何继续。LangGraph 可以维护消息状态;跨独立调用或会话持久保存,需要相应的状态传递或 checkpoint 配置。

工具使用与自主执行

工具让智能体获取信息、进行计算,并执行内置知识之外的操作。清楚的 schema 与描述帮助模型理解用途和参数,决策循环则让系统动态选择工具调用或直接回答。正确组织模型消息、执行结果与状态,是可靠工具智能体的基础。

调用同一份知识索引的只读工具

课程主线示例使用百炼 Function Calling 调用只读检索工具 search_knowledge:模型侧仅能传入查询字符串 query,当前用户身份(principal)由后端直接注入。每次执行工具前,后端严格校验函数名称与参数合法性,并通过 tool_call_id 关联返回结果,整个问答最多允许执行三轮调用。

examples/pipeline.pyPython
class SearchArgs(BaseModel):
    model_config = ConfigDict(extra='forbid', strict=True)
    query: str = Field(min_length=1, max_length=1000)


def tools_answer(client, model, index, principal, question):
    tools = [{'type': 'function', 'function': {'name': 'search_knowledge',
        'description': '只读查询当前用户获准访问的知识资料。', 'parameters': SearchArgs.model_json_schema()}}]
    messages = [{'role': 'user', 'content': question}]
    for _ in range(3):
        reply = client.chat.completions.create(model=model, messages=messages, tools=tools).choices[0].message
        if not reply.tool_calls:
            return reply.content or ''
        if len(reply.tool_calls) > 3:
            raise ValueError('too_many_tool_calls')
        messages.append(reply.model_dump(exclude_none=True))
        for call in reply.tool_calls:
            if call.function.name != 'search_knowledge':
                raise PermissionError('tool_not_allowed')
            args = SearchArgs.model_validate_json(call.function.arguments)
            contexts = index.retrieve(principal, args.query)
            messages.append({'role': 'tool', 'tool_call_id': call.id,
                             'content': json.dumps(contexts, ensure_ascii=False)})
    raise ValueError('tool_round_limit')

完整实现见随课下载包中的 pipeline.py。工具执行过程中的任何异常均由后端记录并立即终止当前请求;严禁开放任意 URL 访问、动态代码执行或允许模型在参数中自行指定用户身份。若后续业务确需新增写入类工具,必须另行设计独立授权、业务规则校验与幂等机制。

搜索课程

正在加载课程目录…