新版 LangChain Agent 核心架构:State、Context、ToolRuntime 与 Middleware

上一篇我们已经知道:

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",
}

三、NotRequiredAnnotated 到底是什么?

这一块特别容易把人绕进去。

例如:

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。

相关推荐
神秘的猪头1 小时前
新版 LangChain Agent 入门:从 `bind_tools + while` 到 `create_agent`
langchain·fastapi
神秘的猪头1 小时前
把新版 LangChain Agent 做成真正应用:Memory、Streaming、SSE 与 Structured Output
langchain·fastapi
武子康2 小时前
Agent 的工具没变,SGLang 缓存为什么没命中?
人工智能·llm·agent
MomentYY2 小时前
大模型 Memory 管理:它凭什么知道你之前说过什么?
llm·agent·ai编程
染指11103 小时前
110.Agent-LangChain核心组件-Messages消息和提示词工程
人工智能·microsoft·langchain·agents
minhuan3 小时前
搭建本地人脸识别系统:解析FastAPI+InsightFace+FAISS全栈实现原理与优化方案26.6
fastapi·faiss·insightface·大模型应用·人脸识别模型·本地化人脸识别系统
桃西西呀3 小时前
换个会话就失忆?拆开 Agent 记忆的 4 层与 4 个流派,附9个坑的自检清单
人工智能·llm·ai编程
桃西西呀3 小时前
风控模型说自己 80% 准,却漏掉了 76% 该拦的人:一次把模型评估讲透
人工智能·机器学习·llm
tryCbest4 小时前
FastAPI中passlib包的作用
python·fastapi·passlib