1 概述
Agent = LLM + 推理 + Tools + 控制流(Loop / Graph)
定义
Agent是基于LLM的执行系统,在多步推理过程中动态决定工具调用,并基于外部结果持续迭代直至完成任务。
与Chain对比
| 项 | Chain | Agent |
|---|---|---|
| 控制流 | 固定 | 动态 |
| 工具调用 | 预设 | 决策 |
| 执行方式 | 线性 | 迭代/图 |
结论:
- Chain:流程编排
- Agent:决策驱动执行
核心组成
- LLM:生成决策(Thought / Action)
- Tools:外部能力接口
- Prompt:行为约束与格式定义
- Memory(可选):状态与上下文
- Runtime(控制流):
- Loop(ReAct)
- Graph(LangGraph)
执行机制(reAct)
基本结构
Input → Reason → Act → Observe → … → Output
等价抽象:
ReAct = 推理(Reason)与行动(Act)交替的决策模式
Reason → Act → Observe → Repeat
终止条件
- 生成最终答案
- 达到最大迭代次数
- 无可执行动作
控制流模型(1.x核心变化)
Loop(传统Agent)
- 基于循环的逐步决策
- 典型:ReAct
Graph(当前主流)
- 基于状态图(State Graph)
- 节点:推理 / 工具 / 判断
- 边:条件跳转
结论:
Agent执行 = 状态驱动的控制流
LangChain 1.x实现结构
核心抽象
- LLM / ChatModel:推理引擎
- Tools:动作执行单元
- Runnable(LCEL):组合执行单元
- LangGraph:控制流与状态管理
组件关系
- LLM:生成下一步决策
- Tools:执行动作
- Runnable:组织执行链路
- LangGraph:定义执行流程(循环 / 分支 / 状态)
关键点
- Agent不再依赖AgentExecutor
- 控制流由:
- Runnable(简单场景)
- LangGraph(复杂场景)
统一表达
Agent本质是“LLM驱动的状态机”
2 tools
1. 定义
Tool = LLM可调用的函数接口
2. 作用
扩展LLM能力(数据 / 计算 / API)
3. 调用机制
LLM → Action(tool, args) → Tool → Observation → LLM
4. 结构
- name
- description
- schema
- function
5. 实现
@tool
def f(x: str) -> str:
return "..."
6. 关键点
- 模型决定是否调用
- description影响选择
实际过程拆解
调用发生时,本质是:
用户输入 → LLM → 生成:
{
tool: "xxx",
args: {...}
}
3 简单示例:
import os
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain_community.chat_models import ChatTongyi
from langchain_core.tools import tool
load_dotenv()
llm = ChatTongyi(
model="qwen-max",
api_key=os.getenv("OPENAI_API_KEY")
)
@tool(description="获取指定地区的天气情况")
def get_current_weather(location: str) -> str:
"""查询当前天气"""
weather_data = {
"北京": "晴天,温度25°C",
"上海": "多云,温度22°C",
"广州": "小雨,温度28°C",
"深圳": "晴天,温度30°C"
}
return weather_data.get(location, f"{location}: 暂无天气数据")
@tool(description="查询指定城市的空气质量指数")
def get_air_quality(city: str) -> str:
"""查询空气质量"""
aqi_data = {
"北京": "AQI: 45, 空气质量: 优",
"上海": "AQI: 68, 空气质量: 良",
"广州": "AQI: 52, 空气质量: 良",
"深圳": "AQI: 38, 空气质量: 优"
}
return aqi_data.get(city, f"{city}: 暂无空气质量数据")
@tool(description="获取指定城市的旅游景点推荐")
def get_tourist_attractions(city: str) -> str:
"""查询旅游景点"""
attractions = {
"北京": "故宫、长城、颐和园、天坛",
"上海": "外滩、东方明珠、豫园、迪士尼乐园",
"广州": "广州塔、白云山、陈家祠、长隆旅游度假区",
"深圳": "世界之窗、欢乐谷、东部华侨城、大梅沙"
}
return attractions.get(city, f"{city}: 暂无景点推荐")
# 创建智能体
agent = create_agent(
model=llm,
tools=[get_current_weather, get_air_quality, get_tourist_attractions],
system_prompt="""
你是一个 helpful 的助手,你需要根据用户的指令,调用指定的工具来获取信息。
请根据用户指令,调用指定的工具来获取信息。
回答时告诉我思考过程,让我知道你为什么调用这个工具
"""
)
# 直接调用 agent 执行任务
result = agent.invoke(
{"messages": [{"role": "user", "content": "我想去北京旅游,那里的天气和空气质量怎么样?有什么好玩的地方?"}]},
)
# 一次性输出最终结果
for message in result["messages"]:
print(f"{type(message).__name__}: {message.content}")
# 流式输出示例 - 显示工具调用信息
print("\n" + "=" * 50)
print("流式输出:")
print("=" * 50)
for chunk in agent.stream(
{"messages": [{"role": "user", "content": "帮我查一下上海的天气和空气质量"}]},
stream_mode="updates", # 启用流式更新模式,实时返回智能体执行过程中的状态变化(如工具调用结果、中间思考步骤等)
):
print( chunk)
得到:
HumanMessage: 我想去北京旅游,那里的天气和空气质量怎么样?有什么好玩的地方?
AIMessage:
ToolMessage: 晴天,温度25°C
ToolMessage: AQI: 45, 空气质量: 优
ToolMessage: 故宫、长城、颐和园、天坛
AIMessage: 北京当前的天气是晴天,温度25°C,非常适合出行。空气质量指数为45,属于"优"级别,您可以放心地进行户外活动。在北京旅游,您一定不能错过的景点有故宫、长城、颐和园以及天坛等历史文化名胜。希望您旅途愉快!
==================================================
流式输出:
==================================================
{'model': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'function': {'arguments': '{"location": "上海"}', 'name': 'get_current_weather'}, 'id': 'call_07a21106bb8945d88e3ea3', 'index': 0, 'type': 'function'}, {'function': {'arguments': '{"city": "上海"}', 'name': 'get_air_quality'}, 'id': 'call_685c77a2501b4ffcb1e23f', 'index': 1, 'type': 'function'}]}, response_metadata={'model_name': 'qwen-max', 'finish_reason': 'tool_calls', 'request_id': 'f45be2cb-60f6-98fb-b63a-b69cd88645e9', 'token_usage': {'input_tokens': 415, 'output_tokens': 77, 'prompt_tokens_details': {'cached_tokens': 0}, 'total_tokens': 492}}, id='lc_run--019d6203-6653-7af2-af75-57cacad6915b-0', tool_calls=[{'name': 'get_current_weather', 'args': {'location': '上海'}, 'id': 'call_07a21106bb8945d88e3ea3', 'type': 'tool_call'}, {'name': 'get_air_quality', 'args': {'city': '上海'}, 'id': 'call_685c77a2501b4ffcb1e23f', 'type': 'tool_call'}], invalid_tool_calls=[])]}}
{'tools': {'messages': [ToolMessage(content='多云,温度22°C', name='get_current_weather', id='4f62800c-e0a2-46df-b630-22e6466eb5fc', tool_call_id='call_07a21106bb8945d88e3ea3')]}}
{'tools': {'messages': [ToolMessage(content='AQI: 68, 空气质量: 良', name='get_air_quality', id='644dad3b-c7d0-4c8a-8d5a-b57e6ccf3a07', tool_call_id='call_685c77a2501b4ffcb1e23f')]}}
{'model': {'messages': [AIMessage(content='当前上海的天气是多云,温度为22°C;空气质量指数(AQI)为68,属于良好水平。', additional_kwargs={}, response_metadata={'model_name': 'qwen-max', 'finish_reason': 'stop', 'request_id': 'a7b81ef4-48b5-99de-81b4-0712fae103e5', 'token_usage': {'input_tokens': 488, 'output_tokens': 30, 'prompt_tokens_details': {'cached_tokens': 0}, 'total_tokens': 518}}, id='lc_run--019d6203-84c4-77a0-bc04-4209a57cf529-0', tool_calls=[], invalid_tool_calls=[])]}}
4 Middleware
Middleware是在Agent执行流程中,对输入、输出或中间步骤进行拦截和处理的机制。
中间件在reAct的每个步骤前后都暴露了钩子:

分为Built-in Middleware(内置中间件)和Custom Middleware(自定义中间件)。
4.1Built-in Middleware 内置中间件
下面介绍几种常用的内置中间件:
| Middleware 名称 | 作用/核心功能 | 典型使用场景 | 工作机制 | 优点 | 注意点 |
|---|---|---|---|---|---|
| SummarizationMiddleware(摘要中间件) | 对长对话或上下文进行压缩总结 | 长对话(Chat)、Agent记忆管理、Token限制优化 | 在调用模型前,将历史消息压缩成摘要,再传递给模型 | 节省Token、提升性能、支持长上下文 | 摘要可能丢失细节;需调优摘要策略 |
| HumanInTheLoopMiddleware(人机协作中间件) | 在关键步骤引入人工审批或干预 | 高风险操作(如发邮件、执行代码)、审批流程、AI辅助决策 | 在特定节点暂停执行,等待人类输入或确认后继续 | 提高安全性、可控性强 | 会降低自动化程度;需要UI或交互支持 |
| RetryMiddleware(重试中间件) | 自动重试失败的模型调用或工具调用 | API不稳定、网络波动、LLM调用失败 | 捕获异常后按策略(次数/间隔)自动重试 | 提高系统稳定性、减少偶发失败 | 需避免无限重试;注意幂等性问题 |
LangChain v1.0 Middleware(中间件)使用指南_langchain middleware-CSDN博客
例:
以HumanInTheLoopMiddleware为例:
# ========= 0) 初始化 =========
load_dotenv()
llm = ChatTongyi(
model="qwen-max",
api_key=os.getenv("OPENAI_API_KEY")
)
# ========= 1) 定义工具 =========
@tool
def search_customer_info(name: str) -> str:
"""查询客户信息"""
fake_db = {
"张三": "张三,VIP客户,最近关注企业版报价。",
"李四": "李四,老客户,正在续费阶段。",
}
return fake_db.get(name, f"未找到客户 {name} 的资料")
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""发送邮件(敏感操作,需要人工审批)"""
print("\n[send_email 已执行]")
print(f"To: {to}")
print(f"Subject: {subject}")
print(f"Body: {body}\n")
return f"邮件已发送给 {to},主题:{subject}"
# ========= 2) 创建 Agent =========
agent = create_agent(
model=llm,
tools=[search_customer_info, send_email],
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"search_customer_info": False, # 不拦截
"send_email": {
"allowed_decisions": ["approve", "reject"], # 只保留两个
"description": "发送邮件前需要人工审批",
},
},
)
],
checkpointer=InMemorySaver(), # 仅用于 demo(进程结束会丢失)
)
# ⚠️ 必须有 thread_id,用于恢复执行
config = {
"configurable": {
"thread_id": "demo"
}
}
# ========= 3) 人工审批函数 =========
def get_human_decision():
print("\n⚠️ 检测到敏感操作:发送邮件")
print("请选择:approve(通过) / reject(拒绝)")
while True:
decision = input("你的选择: ").strip().lower()
if decision == "approve":
return Command(
resume={
"decisions": [
{"type": "approve"}
]
}
)
elif decision == "reject":
reason = input("请输入拒绝原因: ").strip()
return Command(
resume={
"decisions": [
{
"type": "reject",
"reason": reason or "未提供原因"
}
]
}
)
else:
print("输入无效,请输入 approve 或 reject")
# ========= 4) 执行流程 =========
if __name__ == "__main__":
user_request = {
"messages": [
{
"role": "user",
"content": "请先查看张三的信息,然后给他发一封邮件,告诉他企业版本周有 9 折优惠。"
}
]
}
# 第一次执行(会在 send_email 前中断)
result = agent.invoke(user_request, config=config)
print("\n===== 第一次 invoke(已中断)=====")
print(result)
# 人工审批
human_command = get_human_decision()
for chunk in agent.stream(
human_command,
config=config,
stream_mode="updates", # 启用流式更新模式,实时返回智能体执行过程中的状态变化(如工具调用结果、中间思考步骤等)
):
print(chunk)
4.2 Custom Middleware 自定义中间件
0.1 概述
定义
Custom middleware 是 LangChain 1.x 提供的一套“执行期拦截机制”。它允许开发者在 agent 执行流程中的固定节点,或围绕 model/tool 调用的外层,插入日志、鉴权、状态更新、上下文改写、路由、降级、异常处理等逻辑。官方文档把它分成两大类:节点型(node-style) 和 环绕型(wrap-style)。
两大分类
1 节点型(Node-style hooks)
节点型钩子是在固定执行点顺序运行的 middleware。官方列出的四个点是:
before_agent:agent 整体开始前,每次 invoke 只执行一次before_model:每次模型调用前after_model:每次模型返回后after_agent:agent 整体结束后,每次 invoke 只执行一次
节点型适合做:
- 记录日志
- 参数校验
- 状态补丁
- 消息数限制
- 审计与观测
- 简单 guardrail
因为它们是“到点触发”的,不负责包裹整段调用过程。
2 环绕型(Wrap-style hooks)
环绕型钩子是对 某一次 model call 或 tool call 进行“包裹”。官方列出两个点:
wrap_model_callwrap_tool_call
它们的典型特征是:你的函数会拿到一个 request 和一个 handler。你可以:
- 先看请求
- 改写请求
- 再调用
handler(request) - 对返回值做二次处理
- 或直接短路,不走真正调用
所以它更像拦截器 / decorator / around advice。
0.2 State(状态)
定义
state 是 agent 在整个执行过程中共享的“可变数据结构”,通常是一个 dict-like 对象(如 state["messages"])。它是 middleware 最常操作的对象之一。(docs.langchain.com)
它存什么
典型字段包括:
messages:对话历史(最核心)intermediate_steps:中间推理轨迹(tool 调用等)- 自定义字段(如
user_id、risk_level、budget)
特点
- 跨 middleware 共享
- 跨节点持久
- 会被后续步骤读取
重要理解
state 是:
agent 的“记忆 + 当前上下文快照”
0.3 Runtime(运行时上下文)
定义
runtime 是每次执行时的“外部上下文容器”,通常不属于 agent 内部逻辑,而是由调用方注入。(docs.langchain.com)
它存什么
常见内容:
- 用户信息(user_id / plan)
- UI 参数(选中的模型)
- request metadata(trace_id / headers)
- feature flags
- 实验配置(A/B test)
特点
- 只读为主
- 不一定持久
- 不一定写回 state
典型用途
- 根据用户套餐选模型
- 根据 region 控制工具
- 灰度发布 feature
- 注入 trace 信息
与 state 的区别
| state | runtime | |
|---|---|---|
| 生命周期 | agent 内部 | 外部请求 |
| 是否持久 | 是 | 通常否 |
| 是否可修改 | 是 | 一般不建议 |
| 用途 | 推理上下文 | 控制执行策略 |
一句话:
state 是“agent 内部记忆”,runtime 是“外部控制参数”。
0.4 Request(请求对象)
定义
request 是在 wrap-style middleware 中出现的对象,代表一次具体的调用请求:
- model request
- tool request
它是“即将被执行的动作”。
在不同 wrap 中的含义
wrap_model_call
request 通常包含:
- messages(送给模型的内容)
- model(当前模型)
- parameters(temperature、top_p 等)
wrap_tool_call
request 通常包含:
- tool name
- arguments
- context
特点
- 只影响当前调用
- 通常是“可改写”的
- 不等同于 state
核心理解
state = 全局上下文
request = 单次调用输入
示例理解
- state.messages = 全部历史
- request.messages = 本次发给 LLM 的 prompt(可能被裁剪/改写)
0.5 Handler(下游调用器)
定义
handler 是 wrap-style middleware 中的“下一层执行器”,你调用它才会真正触发:
- 模型调用
- 工具执行
结构类比
middleware A
→ middleware B
→ middleware C
→ actual model/tool
每一层 middleware 都拿到一个 handler:
handler = “调用下一层”
使用方式
典型结构:
def wrap_model_call(request, handler):
# 1. 前处理
modified_request = ... # 2. 调用下游
response = handler(modified_request) # 3. 后处理
return modified_response
能力(非常关键)
你可以:
- ✔ 调用 handler(正常流程)
- ✔ 改写 request 再调用
- ✔ 捕获异常
- ✔ 修改 response
- ❗ 不调用 handler(短路)
为什么重要
handler 让 middleware 具备:
“控制调用是否发生”的能力
这也是 wrap-style 比 node-style 强的根本原因。
0.6 Response(响应对象)
(虽然你没提,但这是必须补的)
定义
response 是:
- model 输出
- tool 返回结果
在 wrap 中的角色
- 来自
handler(...) - 可以被 middleware 修改
- 可以被替换
常见用途
- 统一错误格式
- 输出过滤(如敏感信息)
- fallback(替换为备用结果)
自定义middleware例:
import os
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain.agents.middleware import before_model, after_model, wrap_model_call, wrap_tool_call
from langchain.agents.middleware import AgentState, ModelRequest, ModelResponse, dynamic_prompt
from langchain.agents.middleware import before_agent
from langchain_community.chat_models import ChatTongyi
from langchain_core.tools import tool
from langgraph.runtime import Runtime
load_dotenv()
llm = ChatTongyi(
model="qwen-max",
api_key=os.getenv("OPENAI_API_KEY")
)
@tool(description="获取指定地区的天气情况")
def get_current_weather(location: str) -> str:
"""查询当前天气"""
weather_data = {
"北京": "晴天,温度25°C",
"上海": "多云,温度22°C",
"广州": "小雨,温度28°C",
"深圳": "晴天,温度30°C"
}
return weather_data.get(location, f"{location}: 暂无天气数据")
def get_state_dict(state: AgentState):
"""保证 state 中有 my_dict"""
if "my_dict" not in state:
state['my_dict'] = {
"counter": 0,
"trace": []
}
return state["my_dict"]
@before_agent
def before_agent_middleware1(state:AgentState,runtime:Runtime):
print(f"[before_agent_middleware1],附带{len(state['messages'])}条信息")
my_dict=get_state_dict(state)
my_dict["counter"] += 1
my_dict["now"] = "before_agent_middleware1"
my_dict["trace"].append("before_agent_middleware1")
print(state["my_dict"])
@before_agent
def before_agent_middleware2(state: AgentState, runtime: Runtime):
print(f"[before_agent_middleware2],附带{len(state['messages'])}条信息")
my_dict = get_state_dict(state)
my_dict["counter"] += 1
my_dict["now"] = "before_agent_middleware2"
my_dict["trace"].append("before_agent_middleware2")
print(state["my_dict"])
@before_model
def before_model_middleware(state: AgentState, runtime: Runtime):
print(f"[before_model_middleware],附带{len(state['messages'])}条信息")
my_dict = get_state_dict(state)
my_dict["counter"] += 1
my_dict["now"] = "before_model_middleware"
my_dict["trace"].append("before_model_middleware")
print(state["my_dict"])
@after_model
def after_model_middleware(state: AgentState, runtime: Runtime):
print(f"[after_model_middleware],附带{len(state['messages'])}条信息")
my_dict = get_state_dict(state)
my_dict["counter"] += 1
my_dict["now"] = "after_model_middleware"
my_dict["trace"].append("after_model_middleware")
print(state["my_dict"])
@wrap_model_call
def wrap_model_call_middleware(request,handler):
state = request.state
print(f"[wrap_model_call_middleware],附带{len(state['messages'])}条信息")
print(request)
my_dict = get_state_dict(state)
my_dict["counter"] += 1
my_dict["now"] = "wrap_model_call_middleware"
my_dict["trace"].append("wrap_model_call_middleware")
print(state["my_dict"])
return handler(request)
@wrap_tool_call
def wrap_tool_call_middleware(request,handler):
state = request.state
print(f"[wrap_tool_call_middleware],附带{len(state['messages'])}条信息")
print(f"工具执行:{request.tool_call['name']}")
print(f"工具参数:{request.tool_call['args']}")
print(request)
my_dict = get_state_dict(state)
my_dict["counter"] += 1
my_dict["now"] = "wrap_tool_call_middleware"
my_dict["trace"].append("wrap_tool_call_middleware")
print(state["my_dict"])
return handler(request)
agent = create_agent(
model=llm,
tools=[get_current_weather],
middleware=[
before_agent_middleware2,
before_agent_middleware1,
before_model_middleware,
after_model_middleware,
wrap_model_call_middleware,
wrap_tool_call_middleware
],
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "帮我查一下上海的天气"}]},
)
print("\n===== agent输出 begin=====")
for message in result["messages"]:
print(f"{type(message).__name__}: {message.content}")
print("===== agent输出 end=====\n")
得到:
[before_agent_middleware2],附带1条信息
{‘counter’: 1, ‘trace’: [‘before_agent_middleware2’], ‘now’: ‘before_agent_middleware2’}
[before_agent_middleware1],附带1条信息
{‘counter’: 1, ‘trace’: [‘before_agent_middleware1’], ‘now’: ‘before_agent_middleware1’}
[before_model_middleware],附带1条信息
{‘counter’: 1, ‘trace’: [‘before_model_middleware’], ‘now’: ‘before_model_middleware’}
[wrap_model_call_middleware],附带1条信息
ModelRequest(model=ChatTongyi(client=<class ‘dashscope.aigc.generation.Generation’>, model_name=’qwen-max’, model_kwargs={}, dashscope_api_key=SecretStr(‘**********’)), messages=[HumanMessage(content=’帮我查一下上海的天气’, additional_kwargs={}, response_metadata={}, id=’3eb61343-7d47-449a-a3e9-9803a7d62d5e’)], system_message=None, tool_choice=None, tools=[StructuredTool(name=’get_current_weather’, description=’获取指定地区的天气情况’, args_schema=<class ‘langchain_core.utils.pydantic.get_current_weather’>, func=<function get_current_weather at 0x000002092E3B4160>)], response_format=None, state={‘messages’: [HumanMessage(content=’帮我查一下上海的天气’, additional_kwargs={}, response_metadata={}, id=’3eb61343-7d47-449a-a3e9-9803a7d62d5e’)]}, runtime=Runtime(context=None, store=None, stream_writer=<function Pregel.stream.<locals>.stream_writer at 0x000002092E499120>, previous=None), model_settings={})
{‘counter’: 1, ‘trace’: [‘wrap_model_call_middleware’], ‘now’: ‘wrap_model_call_middleware’}
[after_model_middleware],附带2条信息
{‘counter’: 1, ‘trace’: [‘after_model_middleware’], ‘now’: ‘after_model_middleware’}
[wrap_tool_call_middleware],附带2条信息
工具执行:get_current_weather
工具参数:{‘location’: ‘上海’}
ToolCallRequest(tool_call={‘name’: ‘get_current_weather’, ‘args’: {‘location’: ‘上海’}, ‘id’: ‘call_6756f9363f2e481ca74575’, ‘type’: ‘tool_call’}, tool=StructuredTool(name=’get_current_weather’, description=’获取指定地区的天气情况’, args_schema=<class ‘langchain_core.utils.pydantic.get_current_weather’>, func=<function get_current_weather at 0x000002092E3B4160>), state={‘messages’: [HumanMessage(content=’帮我查一下上海的天气’, additional_kwargs={}, response_metadata={}, id=’3eb61343-7d47-449a-a3e9-9803a7d62d5e’), AIMessage(content=”, additional_kwargs={‘tool_calls’: [{‘function’: {‘arguments’: ‘{“location”: “上海”}’, ‘name’: ‘get_current_weather’}, ‘id’: ‘call_6756f9363f2e481ca74575’, ‘index’: 0, ‘type’: ‘function’}]}, response_metadata={‘model_name’: ‘qwen-max’, ‘finish_reason’: ‘tool_calls’, ‘request_id’: ‘a46e844c-8e0d-90ce-aad9-63995aee3d06’, ‘token_usage’: {‘input_tokens’: 236, ‘output_tokens’: 18, ‘prompt_tokens_details’: {‘cached_tokens’: 0}, ‘total_tokens’: 254}}, id=’lc_run–019d6b45-4c80-7d03-a927-9de2781f4794-0′, tool_calls=[{‘name’: ‘get_current_weather’, ‘args’: {‘location’: ‘上海’}, ‘id’: ‘call_6756f9363f2e481ca74575’, ‘type’: ‘tool_call’}], invalid_tool_calls=[])]}, runtime=ToolRuntime(state={‘messages’: [HumanMessage(content=’帮我查一下上海的天气’, additional_kwargs={}, response_metadata={}, id=’3eb61343-7d47-449a-a3e9-9803a7d62d5e’), AIMessage(content=”, additional_kwargs={‘tool_calls’: [{‘function’: {‘arguments’: ‘{“location”: “上海”}’, ‘name’: ‘get_current_weather’}, ‘id’: ‘call_6756f9363f2e481ca74575’, ‘index’: 0, ‘type’: ‘function’}]}, response_metadata={‘model_name’: ‘qwen-max’, ‘finish_reason’: ‘tool_calls’, ‘request_id’: ‘a46e844c-8e0d-90ce-aad9-63995aee3d06’, ‘token_usage’: {‘input_tokens’: 236, ‘output_tokens’: 18, ‘prompt_tokens_details’: {‘cached_tokens’: 0}, ‘total_tokens’: 254}}, id=’lc_run–019d6b45-4c80-7d03-a927-9de2781f4794-0′, tool_calls=[{‘name’: ‘get_current_weather’, ‘args’: {‘location’: ‘上海’}, ‘id’: ‘call_6756f9363f2e481ca74575’, ‘type’: ‘tool_call’}], invalid_tool_calls=[])]}, context=None, config={‘tags’: [], ‘metadata’: {‘langgraph_step’: 6, ‘langgraph_node’: ‘tools’, ‘langgraph_triggers’: (‘__pregel_push’,), ‘langgraph_path’: (‘__pregel_push’, 0, False), ‘langgraph_checkpoint_ns’: ‘tools:453ffa75-6cab-a3aa-e476-132d73f02f9f’, ‘checkpoint_ns’: ‘tools:453ffa75-6cab-a3aa-e476-132d73f02f9f’}, ‘callbacks’: <langchain_core.callbacks.manager.CallbackManager object at 0x000002092E4BC0A0>, ‘recursion_limit’: 25, ‘configurable’: {‘__pregel_runtime’: Runtime(context=None, store=None, stream_writer=<function Pregel.stream.<locals>.stream_writer at 0x000002092E499120>, previous=None), ‘__pregel_task_id’: ‘453ffa75-6cab-a3aa-e476-132d73f02f9f’, ‘__pregel_send’: <built-in method extend of collections.deque object at 0x000002092E4979A0>, ‘__pregel_read’: functools.partial(<function local_read at 0x000002092CB8FBE0>, PregelScratchpad(step=6, stop=10000, call_counter=<langgraph.pregel._algo.LazyAtomicCounter object at 0x000002092E407B80>, interrupt_counter=<langgraph.pregel._algo.LazyAtomicCounter object at 0x000002092E407490>, get_null_resume=<function _scratchpad.<locals>.get_null_resume at 0x000002092E4992D0>, resume=[], subgraph_counter=<langgraph.pregel._algo.LazyAtomicCounter object at 0x000002092E404AC0>), {‘messages’: <langgraph.channels.binop.BinaryOperatorAggregate object at 0x000002092E4A4400>, ‘jump_to’: <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000002092E43D740>, ‘structured_response’: <langgraph.channels.last_value.LastValue object at 0x000002092E43EF80>, ‘__start__’: <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000002092E43E400>, ‘__pregel_tasks’: <langgraph.channels.topic.Topic object at 0x000002092E4A4200>, ‘branch:to:model’: <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000002092E4A5140>, ‘branch:to:tools’: <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000002092E4A51C0>, ‘branch:to:before_agent_middleware2.before_agent’: <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000002092E4A5200>, ‘branch:to:before_agent_middleware1.before_agent’: <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000002092E4A5240>, ‘branch:to:before_model_middleware.before_model’: <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000002092E4A5280>, ‘branch:to:after_model_middleware.after_model’: <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000002092E4A52C0>}, {}, PregelTaskWrites(path=(‘__pregel_push’, 0, False), name=’tools’, writes=deque([]), triggers=(‘__pregel_push’,))), ‘__pregel_checkpointer’: None, ‘checkpoint_map’: {”: ‘1f133005-7244-6296-8005-6f8a37187cdc’}, ‘checkpoint_id’: None, ‘checkpoint_ns’: ‘tools:453ffa75-6cab-a3aa-e476-132d73f02f9f’, ‘__pregel_scratchpad’: PregelScratchpad(step=6, stop=10000, call_counter=<langgraph.pregel._algo.LazyAtomicCounter object at 0x000002092E407B80>, interrupt_counter=<langgraph.pregel._algo.LazyAtomicCounter object at 0x000002092E407490>, get_null_resume=<function _scratchpad.<locals>.get_null_resume at 0x000002092E4992D0>, resume=[], subgraph_counter=<langgraph.pregel._algo.LazyAtomicCounter object at 0x000002092E404AC0>), ‘__pregel_call’: functools.partial(<function _call at 0x000002092CBD1CF0>, <weakref at 0x000002092E4A1F30; to ‘PregelExecutableTask’ at 0x000002092E407D90>, retry_policy=None, futures=<weakref at 0x000002092E4A1210; to ‘FuturesDict’ at 0x000002092E4A11C0>, schedule_task=<bound method SyncPregelLoop.accept_push of <langgraph.pregel._loop.SyncPregelLoop object at 0x000002092E407460>>, submit=<weakref at 0x000002092E49D770; to ‘BackgroundExecutor’ at 0x000002092E4077F0>)}}, stream_writer=<function Pregel.stream.<locals>.stream_writer at 0x000002092E499120>, tool_call_id=’call_6756f9363f2e481ca74575′, store=None))
{‘counter’: 1, ‘trace’: [‘wrap_tool_call_middleware’], ‘now’: ‘wrap_tool_call_middleware’}
[before_model_middleware],附带3条信息
{‘counter’: 1, ‘trace’: [‘before_model_middleware’], ‘now’: ‘before_model_middleware’}
[wrap_model_call_middleware],附带3条信息
ModelRequest(model=ChatTongyi(client=<class ‘dashscope.aigc.generation.Generation’>, model_name=’qwen-max’, model_kwargs={}, dashscope_api_key=SecretStr(‘**********’)), messages=[HumanMessage(content=’帮我查一下上海的天气’, additional_kwargs={}, response_metadata={}, id=’3eb61343-7d47-449a-a3e9-9803a7d62d5e’), AIMessage(content=”, additional_kwargs={‘tool_calls’: [{‘function’: {‘arguments’: ‘{“location”: “上海”}’, ‘name’: ‘get_current_weather’}, ‘id’: ‘call_6756f9363f2e481ca74575’, ‘index’: 0, ‘type’: ‘function’}]}, response_metadata={‘model_name’: ‘qwen-max’, ‘finish_reason’: ‘tool_calls’, ‘request_id’: ‘a46e844c-8e0d-90ce-aad9-63995aee3d06’, ‘token_usage’: {‘input_tokens’: 236, ‘output_tokens’: 18, ‘prompt_tokens_details’: {‘cached_tokens’: 0}, ‘total_tokens’: 254}}, id=’lc_run–019d6b45-4c80-7d03-a927-9de2781f4794-0′, tool_calls=[{‘name’: ‘get_current_weather’, ‘args’: {‘location’: ‘上海’}, ‘id’: ‘call_6756f9363f2e481ca74575’, ‘type’: ‘tool_call’}], invalid_tool_calls=[]), ToolMessage(content=’多云,温度22°C’, name=’get_current_weather’, id=’971a8fc0-99f1-46ad-b59c-8056b741a781′, tool_call_id=’call_6756f9363f2e481ca74575′)], system_message=None, tool_choice=None, tools=[StructuredTool(name=’get_current_weather’, description=’获取指定地区的天气情况’, args_schema=<class ‘langchain_core.utils.pydantic.get_current_weather’>, func=<function get_current_weather at 0x000002092E3B4160>)], response_format=None, state={‘messages’: [HumanMessage(content=’帮我查一下上海的天气’, additional_kwargs={}, response_metadata={}, id=’3eb61343-7d47-449a-a3e9-9803a7d62d5e’), AIMessage(content=”, additional_kwargs={‘tool_calls’: [{‘function’: {‘arguments’: ‘{“location”: “上海”}’, ‘name’: ‘get_current_weather’}, ‘id’: ‘call_6756f9363f2e481ca74575’, ‘index’: 0, ‘type’: ‘function’}]}, response_metadata={‘model_name’: ‘qwen-max’, ‘finish_reason’: ‘tool_calls’, ‘request_id’: ‘a46e844c-8e0d-90ce-aad9-63995aee3d06’, ‘token_usage’: {‘input_tokens’: 236, ‘output_tokens’: 18, ‘prompt_tokens_details’: {‘cached_tokens’: 0}, ‘total_tokens’: 254}}, id=’lc_run–019d6b45-4c80-7d03-a927-9de2781f4794-0′, tool_calls=[{‘name’: ‘get_current_weather’, ‘args’: {‘location’: ‘上海’}, ‘id’: ‘call_6756f9363f2e481ca74575’, ‘type’: ‘tool_call’}], invalid_tool_calls=[]), ToolMessage(content=’多云,温度22°C’, name=’get_current_weather’, id=’971a8fc0-99f1-46ad-b59c-8056b741a781′, tool_call_id=’call_6756f9363f2e481ca74575′)]}, runtime=Runtime(context=None, store=None, stream_writer=<function Pregel.stream.<locals>.stream_writer at 0x000002092E499120>, previous=None), model_settings={})
{‘counter’: 1, ‘trace’: [‘wrap_model_call_middleware’], ‘now’: ‘wrap_model_call_middleware’}
[after_model_middleware],附带4条信息
{‘counter’: 1, ‘trace’: [‘after_model_middleware’], ‘now’: ‘after_model_middleware’}
===== agent输出 begin=====
HumanMessage: 帮我查一下上海的天气
AIMessage:
ToolMessage: 多云,温度22°C
AIMessage: 上海现在的天气是多云,温度是22°C。
===== agent输出 end=====
进程已结束,退出代码为 0
发现自维护的字典并没有被正常更新
怎么去更新state?
查阅源文档:Custom middleware – Docs by LangChain,得到
状态更新(State updates)
Node 风格和 Wrap 风格的钩子都可以用于更新 agent 的状态,但它们的机制不同:
Node 风格的钩子(before_agent、before_model、after_model、after_agent)
直接返回一个字典。
该字典会通过图中定义的 reducers(归约函数) 应用到 agent 的状态中。
Wrap 风格的钩子(wrap_model_call、wrap_tool_call)
对于模型调用(model calls):
返回一个 ExtendedModelResponse,其中包含一个 Command,用于在返回模型响应的同时注入状态更新。
对于工具调用(tool calls):
直接返回一个 Command。
当你需要在模型或工具调用执行过程中,根据运行时逻辑来跟踪或更新状态时,应使用这种方式,例如:
摘要触发时机(summarization trigger points)
使用量元数据(usage metadata)
基于请求或响应计算的自定义字段总结:
Node-style = 返回 dict + reducers 自动合并状态
Wrap-style = 返回 Command + 显式控制状态更新
把“修改 state”理解成“返回一份 update,让框架去合并
还有个大问题:
my_dict不是已声明的自定义 state 字段。
LangChain 的 middleware 文档明确说,想在 hooks 之间持久跟踪自定义状态,需要扩展AgentState,把你的字段声明到state_schema里;否则只有默认的 agent state 会被可靠管理。你现在所有 hook 都是AgentState,my_dict属于“临时塞进去的键”,所以后续 hook 读到的 state 不会按你预期持续累加。且要让被管理,2种方法:
1.在create_agent添加state_schema=MyState;
2.在@after_model等装饰器后加参(state_schema=MyState)
最终修正代码与结果:
import os
from typing import Any
from langchain_core.messages import ToolMessage
from typing_extensions import NotRequired
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain.agents.middleware import before_model, after_model, wrap_model_call, wrap_tool_call,ExtendedModelResponse
from langchain.agents.middleware import AgentState, ModelRequest, ModelResponse, dynamic_prompt
from langchain.agents.middleware import before_agent
from langchain_community.chat_models import ChatTongyi
from langchain_core.tools import tool
from langgraph.runtime import Runtime
from langgraph.types import Command
load_dotenv()
llm = ChatTongyi(
model="qwen-max",
api_key=os.getenv("OPENAI_API_KEY")
)
@tool(description="获取指定地区的天气情况")
def get_current_weather(location: str) -> str:
"""查询当前天气"""
weather_data = {
"北京": "晴天,温度25°C",
"上海": "多云,温度22°C",
"广州": "小雨,温度28°C",
"深圳": "晴天,温度30°C"
}
return weather_data.get(location, f"{location}: 暂无天气数据")
class MyState(AgentState):
my_dict: NotRequired[dict[str, Any]]
def get_state_dict(state: MyState):
"""保证 state 中有 my_dict"""
if "my_dict" not in state:
state['my_dict'] = {
"counter": 0,
"trace": []
}
return state["my_dict"]
@before_agent
def before_agent_middleware1(state:MyState,runtime:Runtime):
print(f"⭐[before_agent_middleware1],附带{len(state['messages'])}条信息")
my_dict=get_state_dict(state)
new_my_dict={
"counter": my_dict["counter"] + 1,
"now": "before_agent_middleware1",
"trace": my_dict["trace"] + ["before_agent_middleware1"]
}
print("new_my_dict",new_my_dict)
return {"my_dict": new_my_dict}
@before_agent
def before_agent_middleware2(state: MyState, runtime: Runtime):
print(f"⭐[before_agent_middleware2],附带{len(state['messages'])}条信息")
my_dict = get_state_dict(state)
new_my_dict = {
"counter": my_dict["counter"] + 1,
"now": "before_agent_middleware2",
"trace": my_dict["trace"] + ["before_agent_middleware2"]
}
print("new_my_dict", new_my_dict)
return {"my_dict": new_my_dict}
@before_model
def before_model_middleware(state: MyState, runtime: Runtime):
print(f"⭐[before_model_middleware],附带{len(state['messages'])}条信息")
my_dict = get_state_dict(state)
new_my_dict = {
"counter": my_dict["counter"] + 1,
"now": "before_model_middleware",
"trace": my_dict["trace"] + ["before_model_middleware"]
}
print("new_my_dict", new_my_dict)
return {"my_dict": new_my_dict}
@after_model
def after_model_middleware(state: MyState, runtime: Runtime):
print(f"⭐[after_model_middleware],附带{len(state['messages'])}条信息")
my_dict = get_state_dict(state)
new_my_dict = {
"counter": my_dict["counter"] + 1,
"now": "after_model_middleware",
"trace": my_dict["trace"] + ["after_model_middleware"]
}
print("new_my_dict", new_my_dict)
return {"my_dict": new_my_dict}
@wrap_model_call
def wrap_model_call_middleware(request,handler):
state = request.state
print(f"⭐[wrap_model_call_middleware],附带{len(state['messages'])}条信息")
print(request)
my_dict = get_state_dict(state)
new_my_dict = {
"counter": my_dict["counter"] + 1,
"now": "wrap_model_call_middleware",
"trace": my_dict["trace"] + ["wrap_model_call_middleware"]
}
print("new_my_dict", new_my_dict)
response = handler(request)
return ExtendedModelResponse(
model_response=response,
command=Command(
update={
"my_dict": new_my_dict,
}
)
)
@wrap_tool_call
def wrap_tool_call_middleware(request,handler):
state = request.state
print(f"⭐[wrap_tool_call_middleware],附带{len(state['messages'])}条信息")
print(f"工具执行:{request.tool_call['name']}")
print(f"工具参数:{request.tool_call['args']}")
print(request)
my_dict = get_state_dict(state)
new_my_dict = {
"counter": my_dict["counter"] + 1,
"now": "wrap_tool_call_middleware",
"trace": my_dict["trace"] + ["wrap_tool_call_middleware"]
}
print("new_my_dict", new_my_dict)
print("handler(request)前")
response = handler(request)
print("handler(request)后")
#修改response
response.content = f"[wrap_tool_call_middleware已处理] {response.content}"
return Command(
update={
"my_dict": new_my_dict,
"messages": [response],
}
)
agent = create_agent(
model=llm,
tools=[get_current_weather],
middleware=[
before_agent_middleware2,
before_agent_middleware1,
wrap_model_call_middleware,
before_model_middleware,
after_model_middleware,
wrap_tool_call_middleware
],
state_schema=MyState,
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "帮我查一下上海的天气"}]},
)
print("\n===== agent输出 begin=====")
for message in result["messages"]:
print(f"{type(message).__name__}: {message.content}")
print("===== agent输出 end=====\n")
结果:
⭐[before_agent_middleware2],附带1条信息
new_my_dict {'counter': 1, 'now': 'before_agent_middleware2', 'trace': ['before_agent_middleware2']}
⭐[before_agent_middleware1],附带1条信息
new_my_dict {'counter': 2, 'now': 'before_agent_middleware1', 'trace': ['before_agent_middleware2', 'before_agent_middleware1']}
⭐[before_model_middleware],附带1条信息
new_my_dict {'counter': 3, 'now': 'before_model_middleware', 'trace': ['before_agent_middleware2', 'before_agent_middleware1', 'before_model_middleware']}
⭐[wrap_model_call_middleware],附带1条信息
ModelRequest(model=ChatTongyi(client=<class 'dashscope.aigc.generation.Generation'>, model_name='qwen-max', model_kwargs={}, dashscope_api_key=SecretStr('**********')), messages=[HumanMessage(content='帮我查一下上海的天气', additional_kwargs={}, response_metadata={}, id='f3230def-43ce-468f-b4a2-c51b0d585f1f')], system_message=None, tool_choice=None, tools=[StructuredTool(name='get_current_weather', description='获取指定地区的天气情况', args_schema=<class 'langchain_core.utils.pydantic.get_current_weather'>, func=<function get_current_weather at 0x000001B9FB118A60>)], response_format=None, state={'messages': [HumanMessage(content='帮我查一下上海的天气', additional_kwargs={}, response_metadata={}, id='f3230def-43ce-468f-b4a2-c51b0d585f1f')], 'my_dict': {'counter': 3, 'now': 'before_model_middleware', 'trace': ['before_agent_middleware2', 'before_agent_middleware1', 'before_model_middleware']}}, runtime=Runtime(context=None, store=None, stream_writer=<function Pregel.stream.<locals>.stream_writer at 0x000001B9FB207760>, previous=None, execution_info=ExecutionInfo(checkpoint_id='1f13339f-d228-68f2-8003-c154ac1a4e6f', checkpoint_ns='model:02eac91c-a802-37c5-178a-5855843f6b36', task_id='02eac91c-a802-37c5-178a-5855843f6b36', thread_id=None, run_id=None, node_attempt=1, node_first_attempt_time=1775645957.8247411), server_info=None), model_settings={})
new_my_dict {'counter': 4, 'now': 'wrap_model_call_middleware', 'trace': ['before_agent_middleware2', 'before_agent_middleware1', 'before_model_middleware', 'wrap_model_call_middleware']}
⭐[after_model_middleware],附带2条信息
new_my_dict {'counter': 5, 'now': 'after_model_middleware', 'trace': ['before_agent_middleware2', 'before_agent_middleware1', 'before_model_middleware', 'wrap_model_call_middleware', 'after_model_middleware']}
⭐[wrap_tool_call_middleware],附带2条信息
工具执行:get_current_weather
工具参数:{'location': '上海'}
ToolCallRequest(tool_call={'name': 'get_current_weather', 'args': {'location': '上海'}, 'id': 'call_4882d0bdd37a4531bb3ccd', 'type': 'tool_call'}, tool=StructuredTool(name='get_current_weather', description='获取指定地区的天气情况', args_schema=<class 'langchain_core.utils.pydantic.get_current_weather'>, func=<function get_current_weather at 0x000001B9FB118A60>), state={'my_dict': {'counter': 5, 'now': 'after_model_middleware', 'trace': ['before_agent_middleware2', 'before_agent_middleware1', 'before_model_middleware', 'wrap_model_call_middleware', 'after_model_middleware']}, 'messages': [HumanMessage(content='帮我查一下上海的天气', additional_kwargs={}, response_metadata={}, id='f3230def-43ce-468f-b4a2-c51b0d585f1f'), AIMessage(content='', additional_kwargs={'tool_calls': [{'function': {'arguments': '{"location": "上海"}', 'name': 'get_current_weather'}, 'id': 'call_4882d0bdd37a4531bb3ccd', 'index': 0, 'type': 'function'}]}, response_metadata={'model_name': 'qwen-max', 'finish_reason': 'tool_calls', 'request_id': 'c717e196-1e00-9474-80d0-5aa7dcd38a97', 'token_usage': {'input_tokens': 236, 'output_tokens': 18, 'prompt_tokens_details': {'cached_tokens': 0}, 'total_tokens': 254}}, id='lc_run--019d6cbf-1ec3-7a01-859b-21c45cea0ec0-0', tool_calls=[{'name': 'get_current_weather', 'args': {'location': '上海'}, 'id': 'call_4882d0bdd37a4531bb3ccd', 'type': 'tool_call'}], invalid_tool_calls=[])]}, runtime=ToolRuntime(state={'my_dict': {'counter': 5, 'now': 'after_model_middleware', 'trace': ['before_agent_middleware2', 'before_agent_middleware1', 'before_model_middleware', 'wrap_model_call_middleware', 'after_model_middleware']}, 'messages': [HumanMessage(content='帮我查一下上海的天气', additional_kwargs={}, response_metadata={}, id='f3230def-43ce-468f-b4a2-c51b0d585f1f'), AIMessage(content='', additional_kwargs={'tool_calls': [{'function': {'arguments': '{"location": "上海"}', 'name': 'get_current_weather'}, 'id': 'call_4882d0bdd37a4531bb3ccd', 'index': 0, 'type': 'function'}]}, response_metadata={'model_name': 'qwen-max', 'finish_reason': 'tool_calls', 'request_id': 'c717e196-1e00-9474-80d0-5aa7dcd38a97', 'token_usage': {'input_tokens': 236, 'output_tokens': 18, 'prompt_tokens_details': {'cached_tokens': 0}, 'total_tokens': 254}}, id='lc_run--019d6cbf-1ec3-7a01-859b-21c45cea0ec0-0', tool_calls=[{'name': 'get_current_weather', 'args': {'location': '上海'}, 'id': 'call_4882d0bdd37a4531bb3ccd', 'type': 'tool_call'}], invalid_tool_calls=[])]}, context=None, config={'tags': [], 'metadata': {'ls_integration': 'langchain_create_agent', 'langgraph_step': 6, 'langgraph_node': 'tools', 'langgraph_triggers': ('__pregel_push',), 'langgraph_path': ('__pregel_push', 0, False), 'langgraph_checkpoint_ns': 'tools:9dc5a5f2-71c9-f4c8-7458-e256dddbe622', 'checkpoint_ns': 'tools:9dc5a5f2-71c9-f4c8-7458-e256dddbe622'}, 'callbacks': <langchain_core.callbacks.manager.CallbackManager object at 0x000001B9FB221900>, 'recursion_limit': 9999, 'configurable': {'__pregel_runtime': Runtime(context=None, store=None, stream_writer=<function Pregel.stream.<locals>.stream_writer at 0x000001B9FB207760>, previous=None, execution_info=ExecutionInfo(checkpoint_id='1f13339f-df07-6ff4-8005-1d93672e1460', checkpoint_ns='tools:9dc5a5f2-71c9-f4c8-7458-e256dddbe622', task_id='9dc5a5f2-71c9-f4c8-7458-e256dddbe622', thread_id=None, run_id=None, node_attempt=1, node_first_attempt_time=1775645959.1745522), server_info=None), '__pregel_replay_state': None, '__pregel_task_id': '9dc5a5f2-71c9-f4c8-7458-e256dddbe622', '__pregel_send': <built-in method extend of collections.deque object at 0x000001B9FB217820>, '__pregel_read': functools.partial(<function local_read at 0x000001B9F99140D0>, PregelScratchpad(step=6, stop=9999, call_counter=<langgraph.pregel._algo.LazyAtomicCounter object at 0x000001B9FB221120>, interrupt_counter=<langgraph.pregel._algo.LazyAtomicCounter object at 0x000001B9FB2203D0>, get_null_resume=<function _scratchpad.<locals>.get_null_resume at 0x000001B9FB22CA60>, resume=[], subgraph_counter=<langgraph.pregel._algo.LazyAtomicCounter object at 0x000001B9FB221300>), {'messages': <langgraph.channels.binop.BinaryOperatorAggregate object at 0x000001B9F9D4FA80>, 'jump_to': <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000001B9FB1FCE40>, 'structured_response': <langgraph.channels.last_value.LastValue object at 0x000001B9FB1FCBC0>, 'my_dict': <langgraph.channels.last_value.LastValue object at 0x000001B9FB1FCB40>, '__start__': <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000001B9FB1FE140>, '__pregel_tasks': <langgraph.channels.topic.Topic object at 0x000001B9FB1FE080>, 'branch:to:model': <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000001B9FB1FCDC0>, 'branch:to:tools': <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000001B9FB1FCE80>, 'branch:to:before_agent_middleware2.before_agent': <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000001B9FB211F80>, 'branch:to:before_agent_middleware1.before_agent': <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000001B9FB210AC0>, 'branch:to:before_model_middleware.before_model': <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000001B9FB210E00>, 'branch:to:after_model_middleware.after_model': <langgraph.channels.ephemeral_value.EphemeralValue object at 0x000001B9FB211BC0>}, {}, PregelTaskWrites(path=('__pregel_push', 0, False), name='tools', writes=deque([]), triggers=('__pregel_push',))), '__pregel_checkpointer': None, 'checkpoint_map': {'': '1f13339f-df07-6ff4-8005-1d93672e1460'}, 'checkpoint_id': None, 'checkpoint_ns': 'tools:9dc5a5f2-71c9-f4c8-7458-e256dddbe622', '__pregel_scratchpad': PregelScratchpad(step=6, stop=9999, call_counter=<langgraph.pregel._algo.LazyAtomicCounter object at 0x000001B9FB221120>, interrupt_counter=<langgraph.pregel._algo.LazyAtomicCounter object at 0x000001B9FB2203D0>, get_null_resume=<function _scratchpad.<locals>.get_null_resume at 0x000001B9FB22CA60>, resume=[], subgraph_counter=<langgraph.pregel._algo.LazyAtomicCounter object at 0x000001B9FB221300>), '__pregel_call': functools.partial(<function _call at 0x000001B9F9942290>, <weakref at 0x000001B9FB21DF30; to 'PregelExecutableTask' at 0x000001B9FB2216F0>, retry_policy=None, futures=<weakref at 0x000001B9FB21EFC0; to 'FuturesDict' at 0x000001B9FB21EC50>, schedule_task=<bound method SyncPregelLoop.accept_push of <langgraph.pregel._loop.SyncPregelLoop object at 0x000001B9FB220DF0>>, submit=<weakref at 0x000001B9FB20B990; to 'BackgroundExecutor' at 0x000001B9FB2206D0>)}}, stream_writer=<function Pregel.stream.<locals>.stream_writer at 0x000001B9FB207760>, tool_call_id='call_4882d0bdd37a4531bb3ccd', store=None, execution_info=ExecutionInfo(checkpoint_id='1f13339f-df07-6ff4-8005-1d93672e1460', checkpoint_ns='tools:9dc5a5f2-71c9-f4c8-7458-e256dddbe622', task_id='9dc5a5f2-71c9-f4c8-7458-e256dddbe622', thread_id=None, run_id=None, node_attempt=1, node_first_attempt_time=1775645959.1745522), server_info=None))
new_my_dict {'counter': 6, 'now': 'wrap_tool_call_middleware', 'trace': ['before_agent_middleware2', 'before_agent_middleware1', 'before_model_middleware', 'wrap_model_call_middleware', 'after_model_middleware', 'wrap_tool_call_middleware']}
handler(request)前
handler(request)后
⭐[before_model_middleware],附带3条信息
new_my_dict {'counter': 7, 'now': 'before_model_middleware', 'trace': ['before_agent_middleware2', 'before_agent_middleware1', 'before_model_middleware', 'wrap_model_call_middleware', 'after_model_middleware', 'wrap_tool_call_middleware', 'before_model_middleware']}
⭐[wrap_model_call_middleware],附带3条信息
ModelRequest(model=ChatTongyi(client=<class 'dashscope.aigc.generation.Generation'>, model_name='qwen-max', model_kwargs={}, dashscope_api_key=SecretStr('**********')), messages=[HumanMessage(content='帮我查一下上海的天气', additional_kwargs={}, response_metadata={}, id='f3230def-43ce-468f-b4a2-c51b0d585f1f'), AIMessage(content='', additional_kwargs={'tool_calls': [{'function': {'arguments': '{"location": "上海"}', 'name': 'get_current_weather'}, 'id': 'call_4882d0bdd37a4531bb3ccd', 'index': 0, 'type': 'function'}]}, response_metadata={'model_name': 'qwen-max', 'finish_reason': 'tool_calls', 'request_id': 'c717e196-1e00-9474-80d0-5aa7dcd38a97', 'token_usage': {'input_tokens': 236, 'output_tokens': 18, 'prompt_tokens_details': {'cached_tokens': 0}, 'total_tokens': 254}}, id='lc_run--019d6cbf-1ec3-7a01-859b-21c45cea0ec0-0', tool_calls=[{'name': 'get_current_weather', 'args': {'location': '上海'}, 'id': 'call_4882d0bdd37a4531bb3ccd', 'type': 'tool_call'}], invalid_tool_calls=[]), ToolMessage(content='[wrap_tool_call_middleware已处理] 多云,温度22°C', name='get_current_weather', id='28658a40-5f0c-46ed-bcac-b62eeff3a53e', tool_call_id='call_4882d0bdd37a4531bb3ccd')], system_message=None, tool_choice=None, tools=[StructuredTool(name='get_current_weather', description='获取指定地区的天气情况', args_schema=<class 'langchain_core.utils.pydantic.get_current_weather'>, func=<function get_current_weather at 0x000001B9FB118A60>)], response_format=None, state={'messages': [HumanMessage(content='帮我查一下上海的天气', additional_kwargs={}, response_metadata={}, id='f3230def-43ce-468f-b4a2-c51b0d585f1f'), AIMessage(content='', additional_kwargs={'tool_calls': [{'function': {'arguments': '{"location": "上海"}', 'name': 'get_current_weather'}, 'id': 'call_4882d0bdd37a4531bb3ccd', 'index': 0, 'type': 'function'}]}, response_metadata={'model_name': 'qwen-max', 'finish_reason': 'tool_calls', 'request_id': 'c717e196-1e00-9474-80d0-5aa7dcd38a97', 'token_usage': {'input_tokens': 236, 'output_tokens': 18, 'prompt_tokens_details': {'cached_tokens': 0}, 'total_tokens': 254}}, id='lc_run--019d6cbf-1ec3-7a01-859b-21c45cea0ec0-0', tool_calls=[{'name': 'get_current_weather', 'args': {'location': '上海'}, 'id': 'call_4882d0bdd37a4531bb3ccd', 'type': 'tool_call'}], invalid_tool_calls=[]), ToolMessage(content='[wrap_tool_call_middleware已处理] 多云,温度22°C', name='get_current_weather', id='28658a40-5f0c-46ed-bcac-b62eeff3a53e', tool_call_id='call_4882d0bdd37a4531bb3ccd')], 'my_dict': {'counter': 7, 'now': 'before_model_middleware', 'trace': ['before_agent_middleware2', 'before_agent_middleware1', 'before_model_middleware', 'wrap_model_call_middleware', 'after_model_middleware', 'wrap_tool_call_middleware', 'before_model_middleware']}}, runtime=Runtime(context=None, store=None, stream_writer=<function Pregel.stream.<locals>.stream_writer at 0x000001B9FB207760>, previous=None, execution_info=ExecutionInfo(checkpoint_id='1f13339f-df12-6138-8007-630c2e99aa6f', checkpoint_ns='model:a6c96939-fbd1-79b1-77e3-43fd8627b337', task_id='a6c96939-fbd1-79b1-77e3-43fd8627b337', thread_id=None, run_id=None, node_attempt=1, node_first_attempt_time=1775645959.1786807), server_info=None), model_settings={})
new_my_dict {'counter': 8, 'now': 'wrap_model_call_middleware', 'trace': ['before_agent_middleware2', 'before_agent_middleware1', 'before_model_middleware', 'wrap_model_call_middleware', 'after_model_middleware', 'wrap_tool_call_middleware', 'before_model_middleware', 'wrap_model_call_middleware']}
⭐[after_model_middleware],附带4条信息
new_my_dict {'counter': 9, 'now': 'after_model_middleware', 'trace': ['before_agent_middleware2', 'before_agent_middleware1', 'before_model_middleware', 'wrap_model_call_middleware', 'after_model_middleware', 'wrap_tool_call_middleware', 'before_model_middleware', 'wrap_model_call_middleware', 'after_model_middleware']}
===== agent输出 begin=====
HumanMessage: 帮我查一下上海的天气
AIMessage:
ToolMessage: [wrap_tool_call_middleware已处理] 多云,温度22°C
AIMessage: 上海现在的天气是多云,温度是22°C。
===== agent输出 end=====
实验结论:
- middleware执行顺序与加入链的顺序(create_agent处)有关
- before_model_middleware在wrap_model_call_middleware之外(与加入顺序无关)
- 环绕型middleware中可通过request.state得到state
- 自定义变量需要自定义state类,且要继承AgentState,并通过上文两种方式被管理
- state更新本质是通过LangGraph reducer完成:
- node通过返回字典
- warp_model_call 使用ExtendedModelResponse
- warp_tool_call 返回 Command
- wrap_tool_call 必须手动写
"messages": [response],因为工具节点不会自动把结果加入对话流