上一篇我们已经知道:
scss
create_agent()
底层本质依然是:
Model
↓
Tool
↓
Model
但如果新版 LangChain 只是帮我们把:
python
while True
隐藏掉,那它其实并没有带来多大价值。
真正发生变化的是:
State
Context
Runtime
Middleware
这四个概念。
它们让 Agent 从一个简单循环,逐渐变成一个真正的应用 Runtime。
一、Agent State 是什么?
假设:
py
result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": "你好"
}
]
}
)
你传进去的:
json
{
"messages": [...]
}
其实不是普通参数。
它是:
Agent 的初始 State。
执行前:
py
State
messages:
[
HumanMessage
]
执行后:
py
State
messages:
[
HumanMessage,
AIMessage
]
如果发生 Tool Calling:
py
messages:
[
HumanMessage,
AIMessage(tool_calls),
ToolMessage,
AIMessage
]
因此:
py
result["messages"]
本质上是:
Graph 执行结束之后的最终 State。
二、自定义 Agent State
新版可以继承:
AgentState
例如:
py
from typing import NotRequired
from langchain.agents import AgentState
class CustomAgentState(AgentState):
current_topic: NotRequired[str]
user_preferences: NotRequired[
dict[str, str]
]
这样 Agent State 就从:
messages
扩展成:
messages
current_topic
user_preferences
例如:
py
{
"messages": [...],
"current_topic": "LangChain",
}
三、NotRequired、Annotated 到底是什么?
这一块特别容易把人绕进去。
例如:
py
tool_call_count: NotRequired[
Annotated[
int,
operator.add,
]
]
其实包含三层含义:
csharp
int
↓
值应该是整数
NotRequired
↓
这个 key 可以不存在
Annotated[..., operator.add]
↓
LangGraph 合并这个字段时使用加法
所以它不是单纯:
类型限制
其中:
Annotated
更多是:
给框架提供额外 metadata。
例如:
csharp
Annotated[int, operator.add]
表示多个节点同时更新:
count
时不要直接覆盖,而是:
sql
old + new
比如:
ini
当前 count = 5
Tool A → 1
Tool B → 1
最终:
5 + 1 + 1 = 7
这就是 Reducer。
四、State 和 Context 最大区别
这两个概念第一次看特别像。
可以先记一句:
ini
State
=
Agent 正在做什么
Context
=
Agent 在什么环境下做
例如 State:
messages
current_topic
任务阶段
临时结果
这些东西会不断变化。
而 Context:
user_id
role
tenant_id
request_id
通常一次调用过程中不应该变化。
例如:
py
from dataclasses import dataclass
@dataclass
class AgentContext:
user_id: str
username: str
role: str
创建 Agent:
py
agent = create_agent(
model=model,
tools=tools,
context_schema=AgentContext,
)
调用:
py
await agent.ainvoke(
state,
context=AgentContext(
user_id="user_001",
username="shuilei",
role="admin",
),
)
五、为什么 user_id 不应该让 LLM 生成?
假设你写:
py
@tool
def get_user_profile(
user_id: str
):
...
那么模型会看到:
py
get_user_profile(
user_id: string
)
这意味着:
user_id
由 LLM 生成。
如果用户说:
帮我看看 user_999 的资料
模型就有可能传:
json
{
"user_id": "user_999"
}
如果后端直接信任它,就可能越权。
正确方式应该是:
JWT
↓
FastAPI
↓
认证用户
↓
AgentContext
↓
ToolRuntime
↓
Tool
而不是:
用户 Prompt
↓
LLM
↓
user_id
六、ToolRuntime
新版 Tool 可以写:
py
from langchain.tools import (
ToolRuntime,
tool,
)
@tool
def get_my_profile(
runtime: ToolRuntime[AgentContext],
):
context = runtime.context
return (
f"user_id={context.user_id}, "
f"role={context.role}"
)
非常关键的是:
runtime
不会暴露到 Tool Schema 里。
模型只知道:
scss
get_my_profile()
不知道:
runtime
user_id
role
这些值由 Runtime 注入。
因此 Tool 参数可以分成:
模型决定的参数
↓
普通 Tool 参数
后端决定的参数
↓
ToolRuntime
七、ToolRuntime 不只是 Context
我们目前可以通过:
runtime.context
拿用户身份。
也可以:
runtime.state
读取当前 Agent State。
例如:
py
@tool
def get_current_topic(
runtime: ToolRuntime,
) -> str:
topic = runtime.state.get(
"current_topic"
)
return topic or "暂无主题"
还可以:
runtime.tool_call_id
拿当前 ToolCall ID。
以及后面会用到:
py
runtime.stream_writer
实现 Tool Streaming。
所以:
ToolRuntime
本质可以理解为:
Tool 和 Agent Runtime 之间的桥梁。
八、Tool 怎么修改 State?
错误直觉是:
arduino
runtime.state["current_topic"] = "LangChain"
不推荐这么干。
LangGraph 更推荐返回:
py
Command(
update={...}
)
例如:
py
from langgraph.types import Command
@tool
def set_current_topic(
topic: str,
):
return Command(
update={
"current_topic": topic
}
)
意思不是:
Tool 返回一个业务值
而是:
Tool 告诉 LangGraph:请更新 Graph State。
如果还要让模型知道 Tool 执行结果,可以:
py
return Command(
update={
"current_topic": topic,
"messages": [
ToolMessage(
content=f"主题已设置为 {topic}",
tool_call_id=
runtime.tool_call_id,
)
],
}
)
九、Middleware 是新版真正的大变化
如果以前所有逻辑都写在:
python
while True:
里面,生产 Agent 很快会变成:
python
while True:
# prompt
# model
# log
# permission
# retry
# tool
# exception
# monitoring
# state
# human approval
最终非常难维护。
新版引入:
Middleware
处理横切逻辑。
你可以类比:
Axios Interceptor
FastAPI Middleware
NestJS Interceptor
Agent Middleware 可以插在:
before_agent
before_model
wrap_model_call
after_model
wrap_tool_call
after_agent
等位置。
十、Tool 日志 Middleware
例如:
py
from langchain.agents.middleware import (
awrap_tool_call,
)
@awrap_tool_call
async def log_tool_call(
request,
handler,
):
tool_name = request.tool_call[
"name"
]
print(
f"开始执行:{tool_name}"
)
result = await handler(request)
print(
f"执行完成:{tool_name}"
)
return result
最关键的是:
py
await handler(request)
它代表:
继续真正执行 Tool。
因此 Middleware 大概就是:
scss
Middleware Before
↓
handler(request)
↓
真正 Tool
↓
Middleware After
十一、Middleware 可以阻止 Tool
假设:
get_system_stats
只能管理员调用。
可以写:
py
@awrap_tool_call
async def permission_middleware(
request,
handler,
):
tool_name = request.tool_call[
"name"
]
context = request.runtime.context
if (
tool_name == "get_system_stats"
and context.role != "admin"
):
return ToolMessage(
content="权限不足",
tool_call_id=
request.tool_call["id"],
)
return await handler(request)
如果返回:
scss
ToolMessage(...)
而没有:
scss
await handler(request)
那么 Tool 根本不会执行。
这比写:
sql
System Prompt:
普通用户不能调用管理员工具
安全得多。
因为:
ini
Prompt
=
行为引导
而:
ini
Middleware
=
真正执行层控制
十二、Dynamic Prompt
新版可以写:
py
from langchain.agents.middleware import (
dynamic_prompt,
)
@dynamic_prompt
def role_based_prompt(request):
role = request.runtime.context.role
if role == "admin":
return """
你正在服务管理员用户。
可以协助处理管理任务。
"""
return """
你正在服务普通用户。
不要声称用户拥有管理员权限。
"""
于是 System Prompt 不再是:
写死的一段字符串
而可以根据:
sql
User
Role
Tenant
Language
Task
动态变化。
十三、Error Middleware
Tool 报错以后,不应该直接让 FastAPI:
500
例如:
py
calculator(
10,
0,
"divide",
)
抛:
ValueError
可以统一:
py
@awrap_tool_call
async def tool_error_middleware(
request,
handler,
):
try:
return await handler(request)
except ValueError as exc:
return ToolMessage(
content=(
f"工具执行失败:{exc}"
),
tool_call_id=
request.tool_call["id"],
)
except Exception as exc:
print(
"internal error",
repr(exc),
)
return ToolMessage(
content="工具内部执行失败",
tool_call_id=
request.tool_call["id"],
)
这里还有一个很重要的工程原则:
不要把所有内部 Exception 原文直接发给 LLM。
否则可能泄漏:
sql
数据库地址
文件路径
SQL
内部服务信息
十四、最终 Agent 结构会很干净
例如:
py
agent = create_agent(
model=model,
tools=tools,
context_schema=AgentContext,
state_schema=CustomAgentState,
middleware=[
role_based_prompt,
permission_middleware,
tool_error_middleware,
log_tool_call,
],
)
Agent 文件只需要描述:
Model
Tools
State
Context
Middleware
而不是把:
日志
权限
错误
Prompt
监控
全部塞进一个函数。
十五、这一代 Agent 的核心架构
可以整理成:
markdown
create_agent
│
┌──────────────┼──────────────┐
│ │ │
Model Tools Middleware
│ │ │
└──────────────┼──────────────┘
│
Runtime
│
┌──────────────┼──────────────┐
│ │ │
State Context Config
其中:
ini
State
=
可变化的 Agent 工作流状态
ini
Context
=
一次调用的外部业务环境
ini
ToolRuntime
=
Tool 获取 Runtime 能力的入口
ini
Middleware
=
Agent 执行生命周期中的横切逻辑
如果只记住这一套关系,你已经抓住新版 LangChain Agent 架构非常重要的一部分。
下一篇我们继续把它升级成真正的应用:
Checkpointer、Thread Memory、Streaming、SSE 和 Structured Output。