Agents 核心能力 [ 1 ]

接下来我们一起来看 Agent 的核心能力,搞清楚 Agent 到底能够实现哪些功能。前面之所以我们学完中间件之后,再来学习 Agent 的各项能力,是因为绝大多数 Agent 能力,都需要依靠中间件才能落地实现。大家也能直观感受到中间件的重要程度。
指定模型
模型是 Agent 的推理引擎,其配置方式分为静态模型和动态模型两种。
静态模型
在创建 Agent 时一次性配置,执行期间保持不变。有两种指定方式:
使用模型标识符字符串(最直接):
先说 Agent 基础的模型绑定写法,之前我们写代码时是类似这样的逻辑:创建 Agent 的时候,直接给它绑定一个模型,后续 Agent 全程执行推理都只会使用这一个模型,这也是最简单、最直接的模型配置方式。
agent = create_agent("openai:gpt-4o-mini", tools=tools)
支持自动推断(如 "gpt-5" 自动映射为 "openai:gpt-5")。
使用模型实例(更精细控制):
如果我们想要对模型做更精细化的控制,比如调整 temperature 温度值、设置 max_tokens 最大 token 数量,就可以采用第二种写法。 如果使用 OpenAI 系列模型,我们可以通过ChatOpenAI实例化模型对象,在实例化阶段自定义温度、最大 token、超时时间等参数,实现对模型的精细调控。 除了ChatOpenAI之外,之前学过的init_chat_model也能生成模型实例,同样支持精细化参数配置。我们先创建好 model 对象,再把这个对象传入 Agent 即可。
上面这两种配置方式,统一叫做静态模型配置:也就是在创建 Agent 时一次性完成配置,Agent 完整执行周期内,绑定的模型不会发生改变。
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o-mini", temperature=0.1, max_tokens=1000, timeout=30)
agent = create_agent(model, tools=tools)
适合设置 temperature、max_tokens、timeout 等特定参数。
上面这两种配置方式,统一叫做静态模型配置:也就是在创建 Agent 时一次性完成配置,Agent 完整执行周期内,绑定的模型不会发生改变。
动态模型
讲到这里大家会有一个疑问:有没有办法能在 Agent 运行过程中,动态更新、替换正在使用的模型?答案是有的,也就是我们这部分的重点 —— 动态模型。
动态模型指在程序运行阶段,根据上下文、运行状态自动切换选用的模型;想要实现这个功能,必须搭配中间件,依靠@wrap_model_call包装类型的装饰器中间件来完成。
接下来我们解释一下:为什么动态替换模型只能选用包装风格的中间件,不能用节点风格中间件?
中间件分为两种实现风格:节点风格、包装风格。我们先分析节点风格的中间件,节点风格里有before_model钩子,理论上我们需要在大模型发起调用前替换模型才有意义,执行完模型再替换完全没有作用。 我们看before_model钩子的入参:第一个参数是状态 state,第二个是运行上下文 context,两个参数里都没有携带当前待调用的模型实例,只能读取上下文、存储相关信息,没有任何修改、替换模型的入口。 由此可以得出结论:节点风格钩子不适合做模型的动态替换,没有修改模型的操作入口。
from typing import Any, Annotated, Sequence
import operator
from langchain_core.messages import BaseMessage
from langchain.agents import create_agent
from langchain.agents.middleware import before_model
from langgraph.runtime import Runtime
from langchain_openai import ChatOpenAI
# 1. 完全自定义业务状态,不使用框架内置AgentState
class CustomAgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
session_id: str
context_length_limit: int
user_level: str # 自定义业务字段,区分普通/高级用户
# 2. 节点风格钩子,入参是你自己定义的CustomAgentState
@before_model
def model_hook(state: CustomAgentState, runtime: Runtime) -> dict[str, Any] | None:
# 可以自由读写自定义字段
user_level = state["user_level"]
limit = state["context_length_limit"]
# 缺陷:没有任何 ModelRequest,不存在本次待调用模型实例
# 仅能修改全局状态,无法拦截、替换当前要执行的LLM
if user_level == "vip":
state["context_length_limit"] = 16000
return None
# 基础模型
base_llm = ChatOpenAI(model="gpt-4o-mini")
# 构建Agent,传入自定义状态
agent = create_agent(
model=base_llm,
tools=[],
state_schema=CustomAgentState, # 指定自定义状态
system_prompt="通用对话助手",
middleware=[model_hook]
)
再来看包装风格中间件,我们要操作模型,就需要选用处理模型调用的wrap_model_call装饰器,而不是处理工具的中间件。
from typing import Callable, Annotated, Sequence
import operator
from langchain_core.messages import BaseMessage
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain_openai import ChatOpenAI
# 自定义业务状态
class CustomAgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
user_level: str
max_context_tokens: int
# 多模型实例
lite_model = ChatOpenAI(model="gpt-4o-mini")
premium_model = ChatOpenAI(model="gpt-4o")
# 包装风格拦截器:无state入参,通过request.metadata/runtime读取业务标识
@wrap_model_call
def switch_model_by_user_level(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse]
) -> ModelResponse:
# 从元数据读取自定义状态携带的用户等级(invoke时传入configurable)
user_level = request.metadata.get("user_level", "normal")
# 直接修改本次调用的模型实例
if user_level == "vip":
request.model = premium_model
return handler(request)
# 创建Agent,绑定自定义状态
agent = create_agent(
model=lite_model,
tools=[],
state_schema=CustomAgentState,
system_prompt="对话助手",
middleware=[switch_model_by_user_level]
)
# 调用时通过config传递自定义状态业务参数,中间件可读取
res = agent.invoke(
input={"messages": [("user", "帮我分析方案")]},
config={"configurable": {"user_level": "vip"}}
)
我们的目标是在模型发起调用前替换目标模型,包装风格的入参里会传入ModelRequest请求对象,这个 request 内部已经携带了本次即将执行的模型实例。既然入参里自带了模型相关信息,就说明框架给我们预留了修改、替换模型的操作渠道,接下来我们结合代码实操讲解具体实现步骤。
我们新建一段代码来演示动态模型切换,沿用之前 Agent 代码的基础结构,清理掉无关冗余代码,只保留核心逻辑:
-
定义 Agent,后续把我们编写好的动态模型中间件注册进去;
-
最后调用
agent.invoke()执行 Agent,打印返回的 response 结果,通过返回内容验证模型是否替换成功。
动态替换模型的核心逻辑是运行时切换模型,所以我们至少要提前实例化两个不同的模型。这里依旧使用ChatOpenAI来创建模型实例,分别定义一个轻量小模型、一个高阶大模型,当然大家也可以用init_chat_model生成模型,两种方式都可行。
创建好两个模型之后,我们给 Agent 先绑定一个默认小模型,再编写@wrap_model_call装饰的中间件函数,在模型执行前完成动态切换逻辑。
我们标准化书写中间件函数的入参、出参类型,入参是ModelRequest和 handler,返回值为ModelResponse,大家在其他项目里看到同类写法也能看懂。 函数内部逻辑分为两步:第一步根据规则动态选择目标模型;第二步把修改后的请求交给 handler 执行,拿到返回结果后直接 return。
动态选择模型的核心操作是修改 request 对象:原始 request 里保存的还是默认模型,我们需要替换它内部绑定的模型。 这里我们设定判断规则:依靠对话消息的数量区分对话复杂度,对话消息越多代表场景越复杂,就选用高阶大模型;消息数量少、对话简单,就使用轻量化小模型。
从 request 的 state 状态中,可以读取到完整的 messages 消息列表,我们先取出消息列表,再获取列表长度 message_count 做判断:
- 如果 message_count > 1,代表对话有多轮、复杂度更高,选用高阶大模型;
- 如果 message_count ≤ 1,代表单轮简单对话,选用轻量化小模型。
选定最终要使用的 final_model 后,不能直接通过字典赋值、属性 setter 的方式修改 request 内部模型,框架提供了专属的request.override()方法,传入 model 参数并赋值为我们选好的 final_model,即可完成请求内模型的重置。
除了 model 之外,override 方法还支持重置其他请求参数,大家可以查阅官方文档查看完整支持项,修改完成后,handler 会携带更新后的 request 调用模型,此时执行的就是我们动态选定的新模型。
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.tools import tool
from langchain_openai import ChatOpenAI
@tool
def get_weather_for_location(city: str) -> str:
"""获取指定城市的天气信息。"""
return f"{city}总是阳光明媚!"
model_mini = ChatOpenAI(model="gpt-4o-mini")
model = ChatOpenAI(model="gpt-4o")
@wrap_model_call
def dynamic_model_selection(request: ModelRequest, handler) -> ModelResponse:
"""根据对话的复杂程度(如消息数量)来选择模型。"""
message_count = len(request.state["messages"])
if message_count > 1:
# 使用高级模型进行更长时间的对话
final_model = model
else:
final_model = model_mini
return handler(request.override(model=final_model))
# 定义 agent
agent = create_agent(
model=model_mini, # 默认模型
tools=[get_weather_for_location],
system_prompt="你是一位乐于助人的助手。",
middleware=[dynamic_model_selection],
)
# 执行 agent
response = agent.invoke(
{"messages": [{"role": "user", "content": "北京的天气如何?"}]}
)
print(response["messages"])
# [
# HumanMessage(content='北京的天气如何?', ...),
# AIMessage(content='', ... , 'model_name': 'gpt-4o-mini-2024-07-18', ...),
# ToolMessage(content='在北京总是阳光明媚!', ...),
# AIMessage(content='北京的天气总是阳光明媚!', ... , 'model_name': 'gpt-4o-2024-08-06', ...)
# ]
代码编写完成后运行,通过打印的 response 返回内容验证效果:返回消息中AIMessage内部会携带model_name字段,记录本次 AI 回复使用的模型名称。
第一次执行时,输入仅一条用户单轮消息,message_count=1,判断不满足大于 1 的条件,会走 else 分支使用轻量化小模型,对应 AIMessage 的 model_name 就是小模型标识;当对话产生多轮消息后,message_count 大于 1,中间件会自动切换为高阶大模型,后续生成的 AI 回复 model_name 就会变为高阶模型标识。
这里有个容易踩坑的关键点:编写 Agent 实例时,必须把我们自定义的动态模型中间件放进 middleware 参数列表注册,如果遗漏注册中间件,动态切换逻辑不会生效,全程只会使用默认绑定的模型。
最后总结一下:本次实操演示了运行时动态替换模型的完整流程,重点掌握@wrap_model_call包装模型调用中间件的使用场景与编写逻辑即可。
指定工具
讲完指定模型后,我们来看 Agent 第二个核心能力:指定工具。
工具的作用是赋予 Agent 调用外部能力、执行具体业务操作的权限,框架除了支持基础的模型调用工具之外,还额外封装了多类增强功能:顺序多工具调用(一次提示词触发多次工具调用)、并行工具调用(适用同时执行)、基于前置结果动态选工具、工具调用失败重试与异常处理、跨多轮工具调用的状态持久化。
静态工具
先讲解静态工具配置:在创建 Agent 时,通过 tools 参数一次性传入工具列表,Agent 执行全程可用工具固定不变。工具支持两种定义形式:普通 Python 函数、添加@tool装饰器的函数,装饰器方式可以自定义工具名称、功能描述、入参校验 schema 等信息。 如果给 tools 传入空列表,当前 Agent 就只会保留基础 LLM 对话节点,完全不具备任何工具调用能力。
from langchain.tools import tool
from langchain.agents import create_agent
@tool
def get_weather_for_location(city: str) -> str:
"""获取指定城市的天气信息。"""
return f"{city}总是阳光明媚!"
# 定义 agent
agent = create_agent(
model="gpt-4o-mini",
tools=[get_weather_for_location],
system_prompt="你是一位乐于助人的助手。",
)
在创建 Agent 阶段直接绑定工具,无论绑定 1 个、2 个还是多个工具,后续大模型做决策、选择工具调用时,只会从我们提前绑定的工具备选池里挑选,可以单选一个工具,也可以一次性调用多个工具,这就是静态工具绑定。
看到这里,你可能已经对静态工具配置有了清晰的认识。但在实际落地之前,我们需要先停下来思考一个问题:这种"全量注入、全程固定"的方式,真的适合所有场景吗?
答案是:适合,但有明确的边界。
静态绑定的底层机制
在深入讨论局限性之前,有必要先弄清楚"模型到底是怎么选工具的"——这能帮你更准确地判断,什么时候该用静态,什么时候该换方案。
当你把 tools=[get_weather_for_location, ...] 传入 create_agent 时,LangChain 底层会做两件事:
-
Schema 编译:将每个工具的名称、功能描述、参数 JSON Schema,按照 LLM 提供商要求的格式(如 OpenAI 的
tools参数或 Anthropic 的tool_choice字段)进行序列化。 -
全量注入:将这些序列化后的工具定义,作为系统级指令,随每一次 LLM 请求一起发送。
也就是说,无论用户问的是"今天天气怎么样"还是"帮我算个数学题",所有工具的定义都会一字不差地被塞进每一轮对话的 Prompt 中。
而模型在接收到这些信息后,会利用其内部的注意力机制(Attention Mechanism),将用户问题与每一个工具的 name 和 description 进行语义相关性计算,最终挑出最匹配的那个工具 ID 输出。这个过程是并行计算的,所以从模型推理的角度看,它确实"看"了所有工具——但代价是,每一次计算都要处理全部工具的信息。
这种机制带来的三重挑战
当你的工具数量控制在 10~20 个以内时,上述机制运转良好,几乎感觉不到负担。可一旦工具规模扩大到 30 个、50 个,甚至上百个,三个现实问题就会逐渐浮出水面:
| 挑战 | 具体表现 | 量化参考 |
|---|---|---|
| Token 成本激增 | 每个工具的描述平均占 80~150 Token,50 个工具意味着每轮对话至少多消耗 4000~7500 Token。按 GPT-4o 的定价,千次调用额外成本可能达到数十美元。 | 50 个工具 ≈ 每轮 5000+ Token |
| 语义干扰加剧 | 当工具描述高度相似时(如 get_weather_today 和 get_weather_tomorrow),全量注入会让模型在相似项之间摇摆不定,选择幻觉的概率显著上升。 |
相似度 > 0.85 时,选错率提升约 20% |
| 系统指令被稀释 | 大量工具描述占据了 Prompt 的"注意力预算",导致 system_prompt 中的核心指令(如"必须优先确认用户身份")被模型相对忽略。 |
工具 > 30 时,指令遵循率下降 5%~15% |
以上数据基于业内公开的基准测试(如 Berkeley Function-Calling Leaderboard)和多家企业的生产实践总结,具体数值会因模型版本和工具描述质量而异,但整体趋势是确定的。
什么时候该用静态,什么时候该换方案?
基于上面的分析,我们可以给出一个相对清晰的判断标准:
-
✅ 适合静态绑定的场景:工具数量 ≤ 20 个,工具之间的语义区分度较高,且业务逻辑相对集中(如一个"天气助手"只绑天气相关工具,一个"数学助手"只绑计算工具)。
-
⚠️ 需要谨慎评估的场景:工具数量在 20~40 个之间,且部分工具描述相似。此时建议先优化工具描述(增加区分度),再考虑是否仍用静态。
-
🚫 静态不再适用的场景:工具数量超过 40 个,或涉及多个业务领域(如订单、支付、物流、客服),或工具描述不可避免有重叠。
对于后两种场景,工程上通常有两种进阶方案来替代或补充静态绑定:
-
方案一:RAG 工具检索(动态筛选) —— 收到用户问题后,先用向量检索从工具库中召回 Top-K 个最相关的工具,只把这 K 个工具动态注入到当前 Agent 中。这种方式能做到"按需加载",既保留了模型的选工具能力,又将上下文开销控制在可控范围内。
-
方案二:Router Agent(路由分发) —— 创建一个轻量级的主 Agent,它只负责根据用户问题选择一个"子 Agent"(每个子 Agent 绑定 5~10 个同类工具),再将任务转发。这种方式适合工具分类边界清晰的场景。
回到我们刚才讲的静态工具配置:它简单、直接、透明,是入门 Agent 开发时最自然的起点。但正如我们所见,它本质上是一种全量注入策略,在小规模工具集下表现优异,在大规模工具集下则会暴露出成本、准确性和指令遵循三个维度的瓶颈。
因此,在实际项目中,我们通常建议遵循这样的原则:
默认用静态,够用就好;一旦工具规模扩大,或出现选工具不准、成本超标的问题,就果断切换到 RAG 动态检索或路由分发方案。【这是我本人自己的看法,如果有更好的策略,欢迎探讨!】
动态工具
到这里,我们已经掌握了静态工具配置的完整用法。但你可能已经在思考一个更实际的问题:如果我的系统里有几十上百个工具,难道每一次对话都要把全部工具的描述塞进上下文吗? 这样做不仅Token消耗惊人,过多的工具描述还可能干扰模型的判断,导致选错工具或忽略系统指令。
那有没有办法优化呢?当然有。
一个很自然的思路是:在把工具交给大模型之前,我们先自己做一个预筛选——根据用户的问题,从庞大的工具库里挑出最相关的几个,只把这几个工具注入给Agent。
比如用户问"帮我查一下订单物流",我们就只把query_order、get_logistics这类订单相关的工具注入;用户问"这个月花了多少钱",我们就只注入get_expenses、sum_by_category这类财务相关的工具。
那怎么实现这个"预筛选"呢?一种常见的做法就是借助RAG(检索增强生成)。我们可以提前把所有工具的描述文本向量化,存入向量数据库。当用户问题进来时,通过向量检索召回语义最相似的Top-K个工具,然后再将这些工具动态注入到Agent中。
你看,这个"先RAG检索、再动态注入"的过程,本质上就已经跳出了静态绑定的范畴——工具不再是在创建Agent时一次性写死,而是在运行时根据用户意图灵活决定加载哪些工具。
静态工具适用于大多数场景,但在运行时,有些场景需要灵活调整工具集如:
-
根据认证状态、用户权限、功能开关或对话阶段,再决定哪些工具可用。
-
避免工具过多导致模型上下文过载或出错,同时避免工具过少限制能力。
而这种"运行时动态决定工具列表"的能力,就是我们接下来要重点学习的核心主题——动态工具(Dynamic Tools)。
也就是说,RAG检索+动态注入,只是动态工具的一种具体实现方式。除了这种方式,还有基于路由分发的动态工具、基于多轮对话逐步加载的动态工具等多种形态。
接下来,我们就从RAG检索+动态注入这个最直观的切入点开始,一步步深入理解动态工具的完整面貌。
动态工具分为两种典型业务场景:
-
第一种:运行时,根据业务条件筛选预先注册好的工具;
-
第二种:运行时,动态新增之前没有注册过的工具。
我分开给大家解释两种场景的含义:
第一种,运行时筛选已存在工具:假设创建 Agent 时我们预先绑定了 3 个工具,正常情况下模型能自由选用全部 3 个;但运行过程中我们可以通过逻辑做过滤,只保留其中 1-2 个工具供模型选择,把剩下不符合条件的工具直接剔除,只让模型在筛选后的工具池里做选择,这就是条件筛选预注册工具。
第二种,运行时新增工具:创建 Agent 时备选池只有 1 个工具,运行阶段可以额外新增全新工具加入备选池,模型后续就能调用新增的工具。
两种场景的底层逻辑都是依托中间件实现,不管是筛选原有工具,还是动态新增工具,都能在 Agent 运行过程中动态调整工具池。下面我们结合代码实操,来演示各自的场景。
运行时,根据条件动态选择预先注册好的工具
我们先定义好 3 个搜索类工具,三个工具用途区分明确:
-
公开搜索
public_search:无需用户认证,所有人都能调用,返回通用基础信息,打印日志带有专属标识方便我们区分; -
私有搜索
private_search:仅限完成认证的登录用户调用,未认证用户禁止使用; -
高级搜索
advanced_search:提供深度数据分析,同样需要权限校验。
三个工具定义完成后,开始创建 Agent 实例:指定基础模型为gpt-4o-mini,把上面三个搜索工具全部放进 tools 参数完成预注册,再配置系统提示词,提示词告知模型:你是乐于助人的客服助手,结合用户问题挑选匹配工具给出回答。
Agent 基础配置写完后,核心是自定义中间件,这里我们先做选型判断:我们要做工具池筛选操作,中间件分为节点风格、包装风格,节点风格的钩子无法修改工具备选池,只有@wrap_model_call包装风格的钩子能实现运行时修改工具列表,因此我们选用wrap_model_call装饰器编写中间件。
中间件的核心逻辑是基于运行状态做工具过滤,所有能从 request 对象中获取到的数据,都可以作为筛选条件,比如对话状态 state、运行时存储 store、运行上下文 context 都能拿来做判断。本次示例我们以用户认证状态作为筛选条件。
先梳理中间件函数的入参与返回:入参为ModelRequest类型的 request 对象、handler 处理函数,返回值为ModelResponse。函数内部第一步先从request.state读取对话状态,拿到用户认证标识;这里有个关键点:如果要自定义 state 里的字段(比如本次的authenticated鉴权标识),必须自定义继承AgentState的状态类,并且在装饰器中通过state_schema绑定状态类型,否则代码读取不到我们传入的自定义状态字段,默认会返回 False。
编写 Agent 调用逻辑,调用agent.invoke()时传入字典参数,字典分为两部分:一是用户对话消息 messages,二是自定义状态字段 authenticated,用来模拟用户是否完成登录认证,传 False 代表未登录无权限,传 True 代表已登录有权限。
from typing import Callable
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.tools import tool
@tool
def public_search(query: str) -> str:
"""公开搜索: 无需认证即可使用,返回基础信息。"""
print(f"[公开搜索结果] 关于 '{query}' 的基础信息: 这是公开可获取的内容。")
return f"[公开搜索结果] 关于 '{query}' 的基础信息: 这是公开可获取的内容。"
@tool
def private_search(query: str) -> str:
"""私有搜索: 仅已认证用户可用,返回敏感或个性化数据。"""
print(f"[私有搜索结果] 关于 '{query}' 的私密数据: 仅限认证用户查看。")
return f"[私有搜索结果] 关于 '{query}' 的私密数据: 仅限认证用户查看。"
@tool
def advanced_search(query: str) -> str:
"""高级搜索: 提供深度分析。"""
print(f"[高级搜索结果] 关于 '{query}' 的深度分析报告: 包含详细统计和趋势。")
return f"[高级搜索结果] 关于 '{query}' 的深度分析报告: 包含详细统计和趋势。"
class State(AgentState):
authenticated: bool
@wrap_model_call(state_schema=State)
def state_based_tools(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse]
) -> ModelResponse:
"""基于对话状态的过滤工具。"""
# 读取状态: 检查用户是否已认证 (举例)
state = request.state
is_authenticated = state.get("authenticated", False)
# 未认证用户只能使用以 "public_" 开头的工具
if not is_authenticated:
tools = [t for t in request.tools if t.name.startswith("public_")]
request = request.override(tools=tools)
else:
# 其他条件
pass
return handler(request)
# 定义 agent
agent = create_agent(
model="gpt-4o-mini",
tools=[public_search, private_search, advanced_search], # 预先注册工具
system_prompt="你是一位乐于助人的客服助手。根据用户的问题,选择合适的工具来提供答案。",
middleware=[state_based_tools],
)
# 执行 agent
response = agent.invoke(
{
"messages": [{"role": "user", "content": "北京的天气如何?"}],
"authenticated": False,
}
)
中间件内部的判断逻辑:
-
先从 state 中读取
authenticated,如果读取不到则默认赋值 False; -
判断
is_authenticated为 False(用户未认证),此时对 request 里完整的工具列表做过滤,只保留名称以public_开头的工具,私有、高级搜索全部剔除; -
使用
request.override(tools=筛选后的工具列表)更新 request 内部的工具备选池; -
else 分支暂时不做额外处理,保留全部工具;
-
将更新完成、工具池已过滤的 request 交给 handler 继续执行,此时大模型只能看到筛选后的工具,无法调用被过滤掉的工具。
这里给大家解释一个容易踩坑的核心知识点:为什么修改工具池要用@wrap_model_call(模型调用包装钩子),而不是@wrap_tool_call(工具调用包装钩子)?
-
wrap_tool_call是在模型已经选定工具、准备执行工具的阶段触发,此时模型已经确定要调用哪个工具,没办法再修改整体工具备选池,过滤操作完全失效; -
wrap_model_call是在大语言模型执行推理、选择工具之前触发,我们可以提前修改、替换 request 内的工具列表,再交给模型做工具选择,模型只能看见我们筛选后的工具池,才能实现权限过滤效果。
我们分两次测试验证效果:
-
第一次调用 invoke 传入
authenticated=False(未认证),中间件过滤后仅保留public_search,运行打印日志只有公开搜索的输出,私有、高级搜索不会被调用; -
补充测试逻辑:当
authenticated=True(已认证)时,全部三个工具都会保留,模型可以自由调用私有、高级搜索;我们也可以修改过滤逻辑,实现仅保留私有 / 高级搜索的效果。
运行代码后,根据过滤条件,会打印 public_search 中的日志。除了可以从 state 中获取数据进行过滤,还可以从 store(request.runtime.store)、context(request.runtime.context)中获取并基于获取到的数据进行筛选。
运行时,动态加入新工具
这个场景的核心含义是:创建 Agent 的时候,我们仅预先绑定少量工具(比如只绑定 1 个天气查询工具),运行过程中如果需要调用一个完全没提前注册过的工具,我们可以在执行流程里把新工具追加到工具备选池,让大模型能够识别并调用这个新增工具。
下面我们结合代码实操讲解,复用之前的代码基础,把上一段演示权限筛选的搜索工具全部删除,清理无关逻辑,重新编写演示案例。
我们先给 Agent 预先绑定一个天气查询工具,这个工具的作用是查询指定城市天气。现在提出一个需求:让 Agent 计算 80 元账单对应的小费。
这里先说明小费计算逻辑:账单总额乘以小费比例,举例账单 80 元、小费比例 20%,小费金额 = 80×20%,最终应付总金额 = 账单 + 小费。
如果只绑定了天气工具,没有小费计算工具,大模型没办法精准按照 20% 固定比例算出结果,只会随便估算一个数字;如果不新增工具,Agent 全程不会发起工具调用,仅靠大模型原生知识直接文字回复。
================================ Human Message =================================
计算 80 元账单小费是多少?
================================== Ai Message ==================================
为了计算80元账单的小费,我需要知道您想给多少百分比的小费。通常小费比例有:
- **15%** → 80 × 0.15 = **12 元**
- **18%** → 80 × 0.18 = **14.4 元**
- **20%** → 80 × 0.20 = **16 元**
请问您想按哪个比例计算呢?😊
我们先不新增小费工具,执行一次测试验证效果:执行agent.invoke传入用户提问 “计算 80 元账单小费是多少”,查看返回的消息列表,只会出现两条消息:用户的 HumanMessage、AI 直接回复的 AIMessage,全程没有 ToolMessage,说明完全没有触发工具调用,模型只能模糊回答,达不到精准计算的效果。
想要解决这个问题,就要在运行流程里动态新增小费计算工具,第一步先定义计算小费的工具calculate_tip: 入参有两个:bill_amount账单金额、tip_percentage小费比例,比例默认值设为 20.0,代表 20%; 内部计算逻辑:小费 = 账单金额 ×(小费比例 ÷ 100); 返回格式化字符串,同时打印日志,展示小费金额和账单合计总价。
@tool
def calculate_tip(bill_amount: float, tip_percentage: float=20.0) -> str:
"""
计算一笔账单的小费金额。
根据账单总额和小费百分比,计算应支付的小费以及总金额(账单+小费)。
Args:
bill_amount (float): 原始账单金额,单位:元。
tip_percentage (float, optional): 小费百分比,默认 20.0%。
Returns:
str: 格式化后的字符串,包含小费金额和总金额。
例如:"小费: 17.00元,一共: 102.00元"
"""
print("消费计算中...")
tip = bill_amount * (tip_percentage / 100)
return f"消费:{tip:.2f}元,一共:{bill_amount + tip:.2f}元"
工具定义完成后,核心问题是:怎么在运行时把这个小费工具追加进 Agent 的工具备选池?
和上一种筛选工具的逻辑一致,新增工具的操作必须放在模型调用之前完成。我们梳理执行流程:初始状态下,模型调用时能访问的工具只有天气工具,我们需要在大模型执行推理、选择工具之前,把小费工具添加进工具列表,因此依旧选用@wrap_model_call包装模型调用的钩子。
这里我们换一种中间件写法:通过类继承的方式自定义中间件,创建DynamicToolMiddleware类,继承框架提供的AgentMiddleware,在类内部编写两类钩子方法,分别处理模型调用、工具调用两个阶段。
首先编写wrap_model_call方法(模型调用包装钩子): 入参包含self、ModelRequest类型的 request、handler 处理函数,返回ModelResponse。这个方法只做一件事:给 request 追加新工具。 操作方式是使用request.override()重置 tools 列表: 先用解包运算符*request.tools取出原有的全部旧工具,完整保留原有工具; 在列表末尾追加我们新定义的calculate_tip小费工具; 生成更新完成的 request 对象,传递给 handler 继续执行。
class DynamicToolMiddleware(AgentMiddleware):
"""能够注册并处理动态工具的中间件。"""
def wrap_model_call(self, request: ModelRequest, handler):
# 在请求中添加动态工具
updated = request.override(tools=[*request.tools, calculate_tip])
return handler(updated)
def wrap_tool_call(self, request: ToolCallRequest, handler):
# 处理动态工具的执行过程
if request.tool_call["name"] == "calculate_tip":
return handler(request.override(tool=calculate_tip))
return handler(request)
执行完这一步后,大模型推理选择工具时,工具池同时包含天气工具、小费计算工具,就能识别并调用新增的小费工具。
完成工具追加只是第一步,还需要补充第二个钩子wrap_tool_call(工具调用包装钩子),用来处理新增工具的实际执行流程: 入参为self、ToolCallRequest类型的 request、handler,返回 ToolMessage。
这里给大家解释底层原理:wrap_model_call只是把工具名称、参数格式提供给大模型,让模型知道 “存在这个小费工具、该传什么参数”;但底层执行时,框架还没有绑定这个动态新增的工具实体,只拿到了工具名称字符串,没办法直接运行函数。
也就是说:如果不写 wrap_tool_call,会有大问题!
用户:"计算85元的账单小费是多少"
↓
1️⃣ wrap_model_call 执行
→ 把 calculate_tip 添加到工具列表
→ 大模型看到了 calculate_tip 的定义
↓
2️⃣ 大模型决定调用 calculate_tip
→ 返回 tool_call: {"name": "calculate_tip", "args": {...}}
↓
3️⃣ Agent 框架尝试执行工具
→ 查找名为 "calculate_tip" 的工具
→ ❌ 找不到!
→ 因为 calculate_tip 只在"请求"中被注入了描述,
但实际执行时,Agent 的工具执行器(ToolExecutor)并不知道
如何执行这个工具!
↓
4️⃣ 报错:Tool not found 或类似错误
因此我们要在工具调用阶段做匹配: 从request.tool_call["name"]获取模型选中的工具名称; 判断如果名称等于calculate_tip,就调用request.override(tool=calculate_tip),把工具实体绑定到本次调用请求; 再将更新后的 request 交给 handler 执行; 如果是原有天气工具,无需额外处理,直接传递原 request 即可。
两个钩子方法全部写完后,把自定义中间件实例注册到 Agent 的 middleware 参数中,执行代码测试效果。
整体代码:
from langchain.tools import tool
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware, ModelRequest, ToolCallRequest
@tool
def get_weather_for_location(city: str) -> str:
"""获取指定城市的天气信息。"""
return f"{city}总是阳光明媚!"
# 该工具将在运行时动态添加的工具
@tool
def calculate_tip(bill_amount: float, tip_percentage: float = 20.0) -> str:
"""计算一笔账单的小费金额。"""
print("小费计算中...")
tip = bill_amount * (tip_percentage / 100)
return f"小费: {tip:.2f}元,一共: {bill_amount + tip:.2f}元"
class DynamicToolMiddleware(AgentMiddleware):
"""能够注册并处理动态工具的中间件。"""
def wrap_model_call(self, request: ModelRequest, handler):
# 在请求中添加动态工具
updated = request.override(tools=[*request.tools, calculate_tip])
return handler(updated)
def wrap_tool_call(self, request: ToolCallRequest, handler):
# 处理动态工具的执行过程
if request.tool_call["name"] == "calculate_tip":
return handler(request.override(tool=calculate_tip))
return handler(request)
agent = create_agent(
model="gpt-4o-mini",
tools=[get_weather_for_location], # 只注册天气工具
system_prompt="你是一位乐于助人的客服助手。根据用户的问题,选择合适的工具来提供答案。",
middleware=[DynamicToolMiddleware()],
)
# agent 可以同时使用这两个工具
result = agent.invoke(
{
"messages": [{"role": "user", "content": "计算80元的账单小费是多少?"}],
}
)
for msg in result.get('messages',[]):
msg.pretty_print()
查看执行返回的消息列表,此时一共会产生 4 条消息: HumanMessage:用户提问; AIMessage:模型判断需要调用小费工具,生成工具调用指令; ToolMessage:小费工具执行日志,打印出小费计算过程,算出 80 元账单小费 16 元,合计 96 元; AIMessage:整理计算结果,回复用户最终金额。
消费计算中...
================================ Human Message =================================
计算80元的账单小费是多少?
================================== Ai Message ==================================
我来帮您计算80元账单的小费。请问您希望按多少百分比的小费来计算呢?如果没有指定,我默认按20%来计算。
让我先按20%计算一下:
Tool Calls:
calculate_tip (call_00_lup9HLyGQ0g949NQEYV91834)
Call ID: call_00_lup9HLyGQ0g949NQEYV91834
Args:
bill_amount: 80
================================= Tool Message =================================
Name: calculate_tip
消费:16.00元,一共:96.00元
================================== Ai Message ==================================
按默认的 **20%** 小费比例计算:
- **小费金额**:16.00元
- **总金额(账单+小费)**:96.00元
如果您想要按其他百分比计算(比如15%、18%等),请告诉我,我可以重新帮您计算哦!😊
消息列表里出现 ToolMessage,证明动态新增的小费工具成功被识别、调用,功能生效。
到这里,动态工具的两种场景(运行时筛选预注册工具、运行时新增全新工具)的底层逻辑、代码实现、使用时机就全部讲解完毕。
工具错误处理
补充拓展知识点:在wrap_tool_call工具调用钩子内,我们还可以统一处理工具执行异常,自定义错误返回信息。 如果工具运行抛出异常,我们捕获 Exception,直接返回标准ToolMessage对象封装错误提示: content字段填写自定义错误文案,例如 “工具执行错误,请检查输入账单金额是否合法,错误详情:{异常信息}”; 必须传入tool_call_id=request.tool_call["id"],用来和前面 AI 消息里的工具调用 ID 做关联匹配,框架依靠这个 ID 区分哪一次工具调用出现报错。
返回带错误信息的 ToolMessage 后,大模型会读取这条报错消息,自主做后续决策:可以让用户修正参数后重新调用工具,也可以直接终止流程告知用户失败,完整保留 Agent 循环链路,不会因为工具异常直接中断程序。
所以:错误处理中间件可提高 Agent 的鲁棒性,避免因工具调用失败而中断流程。通过 @wrap_tool_call 装饰器创建中间件,自定义工具执行失败时的错误响应:
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call
from langchain.messages import ToolMessage
@wrap_tool_call
def handle_tool_errors(request, handler):
"""使用自定义消息来处理工具执行过程中的错误。"""
try:
return handler(request)
except Exception as e:
# 向模型返回自定义错误消息
return ToolMessage(
content=f"工具错误: 请检查您的输入并重新尝试。({str(e)})",
tool_call_id=request.tool_call["id"]
)
agent = create_agent(
model="gpt-4.1",
tools=[search, get_weather],
middleware=[handle_tool_errors]
)
错误发生时,Agent 会向模型返回定制的 ToolMessage,帮助模型更好地恢复。
工具在 ReAct 循环中的使用
Agent 遵循 ReAct(Reasoning + Acting)模式:
推理(Reasoning):分析当前状态,决定需要调用的工具。
行动(Acting):执行工具调用,获取观察结果(Observation)。
循环:将观察结果反馈给模型,继续推理和行动,直到得出最终答案。
典型流程示例:
用户提问 → Agent推理 → 调用工具1 → 获取结果 → 再次推理 → 调用工具2 → 获取结果 → 给出最终答案
ReAct 循环使 Agent 能够通过逐步推理、按需调用工具的方式,自主解决复杂任务。它的核心思想是让 Agent 在推理(Reasoning)和行动(Acting)之间交替进行,直到获得足够信息来回答用户的问题。
为了更好地理解这个过程,我们来看一个具体例子:Agent 如何解决 "找出当前最受欢迎的无线耳机,并确认其库存状态" 这一任务。
整个流程分为三个轮次:
第一轮:推理 → 行动
Agent 首先进行推理:"流行程度是实时变化的,静态知识无法回答,我需要使用提供的搜索工具来获取最新数据。"
基于这个推理,Agent 采取行动:调用搜索工具 search_products("无线耳机")。
工具返回观察结果:"找到 5 款匹配产品,排名第一的是 WH-1000XM5。"
第二轮:推理 → 行动
拿到搜索结果后,Agent 继续推理:"我已经知道了最热门的型号,但用户还想确认库存情况,需要进一步查询。"
于是 Agent 采取第二个行动:调用库存查询工具 check_inventory("WH-1000XM5")。
工具返回观察结果:"WH-1000XM5 当前有 10 件库存。"
第三轮:推理 → 最终输出
Agent 进行最终推理:"我已经获得了最热门型号及其库存信息,可以完整回答用户的问题了。"
最后,Agent 生成最终答案:"我找到了当前最受欢迎的无线耳机,型号为 WH-1000XM5,目前有 10 件库存。"
这个示例完整呈现了 ReAct 模式的运作方式:推理 → 行动 → 观察 → 再推理 → 再行动 → ... → 最终答案。Agent 不是一次性给出答案,而是通过多轮迭代,逐步获取必要信息,直到具备足够的知识来生成最终回复。这正是 Agent 能够自主解决复杂任务的核心机制所在。
更多推荐



所有评论(0)