通过前面的文章,我们已经理解了 Agent 运行时上下文、长短期记忆、工具、Middleware 等内容。要进一步理解 Agent 的执行过程,还有一些细节需要掌握。

比如,一次 Agent 调用中,聊天消息保存在哪里?用户 ID 应该放进 State 还是 Context?configcontext 有什么区别?工具为什么既能读取 State,又能拿到 Context?RuntimeToolRuntime 又分别解决什么问题?

这些概念放在一起时很容易混淆。本文就围绕一次 Agent 执行,把 StateContextRunnableConfigRuntimeToolRuntime 之间的关系梳理清楚。

一、Agent 执行中的数据

我们先看一个具体场景。

假设我们正在开发一个客服 Agent。用户会进行多轮对话,Agent 可以查询订单、修改地址,还需要根据当前登录用户访问后台系统。

在这个 Agent 的一次执行过程中,可能同时出现下面这些数据:

代码片段Text
用户之前发送的消息
模型之前的回复
当前选中的订单
当前处理阶段
重试次数

当前登录用户的 user_id
tenant_id
数据库连接
业务 Service

thread_id
tags
metadata
callbacks

长期保存的用户偏好
当前 tool_call_id

这些数据虽然都和一次 Agent 调用有关,但它们的来源、生命周期和用途不同。

例如,聊天记录(messages)会随着执行不断变化,而且可能需要在下一轮对话恢复。

user_id 通常在调用 Agent 之前已经确定,在执行过程中不会改变。

thread_id 主要用来告诉运行系统“这次调用属于哪个 Thread”,它和聊天内容本身没有关系。

数据库连接则只是运行时依赖。

针对上面所有涉及到的信息,可以先建立这样一个大致关系:

代码片段Text
Agent Invocation

├── State
│   ├── messages
│   └── 自定义执行状态

├── Context
│   ├── user_id
│   ├── tenant_id
│   └── 运行依赖

├── RunnableConfig
│   ├── configurable
│   ├── tags
│   ├── metadata
│   ├── callbacks
│   └── ...

├── Runtime
│   ├── context
│   ├── store
│   ├── stream_writer
│   ├── execution_info
│   └── server_info

└── Tool Execution

    └── ToolRuntime
        ├── state
        ├── context
        ├── store
        ├── config
        ├── tool_call_id
        └── ...

这里最容易混淆的是 StateContextRunnableConfig

我们先把这三者区分开,后面的 Runtime 和 ToolRuntime 就比较容易理解了。

二、State 保存执行状态

状态(State),指 Agent 执行过程中持续传递,并且可能发生变化的数据。

LangChain 的 create_agent 底层运行在 LangGraph Runtime 之上。模型调用、工具调用、Middleware 执行,都可能读取当前 State,并产生新的 State Update。

Agent 默认使用 AgentState,其中最重要的字段就是:

代码片段Python
messages

平时调用 Agent 时,我们经常这样写:

代码片段Python
agent.invoke({
    "messages": [
        {"role": "user", "content": "帮我查询订单"}
    ]
})

这里传入的 messages 不只是一次模型调用的 Prompt,它同时也是 Agent State 的输入。

Agent 后续调用模型、执行工具时,消息会继续加入其中。

State 不只有 messages

实际项目中,经常还需要保存一些业务执行状态,例如:

代码片段Text
messages
current_step
selected_order_id
retry_count
approval_status

这时可以扩展 AgentState

代码片段Python
from langchain.agents import AgentState
 
 
class CustomState(AgentState):
    current_step: str
    retry_count: int

然后创建 Agent:

代码片段Python
from langchain.agents import create_agent
 
agent = create_agent(
    model="openai:gpt-5.5",
    tools=[],
    state_schema=CustomState,
)

调用时可以一起传入这些字段:

代码片段Python
result = agent.invoke({
    "messages": [
        {"role": "user", "content": "继续处理"}
    ],
    "current_step": "checking_order",
    "retry_count": 0,
})

current_stepretry_count 会和 messages 一样,成为 Agent 当前状态的一部分。

State 为什么需要单独存在

Agent 通常不是一次模型调用,而是一个连续执行过程:

代码片段Text
User

Model

Tool

Model

Tool

Model

Result

后面的步骤经常需要知道前面发生了什么。

例如第一次模型调用生成了 Tool Call,工具执行完成后,第二次模型调用需要读取 Tool Result;某个 Middleware 还可能根据之前的重试次数判断是否继续执行。

这些信息如果只保存在某个函数的局部变量里,就很难在不同节点之间统一传递。

State 提供的就是这一整个过程中共享的执行数据。

如果配置了 Checkpointer,State 还可以按 Thread 保存。下一次使用相同 thread_id 调用时,之前保存的状态可以继续被使用。

三、State 会持续更新

前面已经提到,Agent 执行过程中,Middleware 和 Tool 都可能产生 State Update。

比如增加一个 request_count,记录模型执行次数:

代码片段Python
from langchain.agents import AgentState, create_agent
from langchain.agents.middleware import before_model
from langgraph.runtime import Runtime
 
 
class CustomState(AgentState):
    request_count: int
 
 
@before_model
def count_request(
    state: CustomState,
    runtime: Runtime,
) -> dict:
    return {
        "request_count": state.get("request_count", 0) + 1
    }
 
 
agent = create_agent(
    model="openai:gpt-5.5",
    tools=[],
    state_schema=CustomState,
    middleware=[count_request],
)
 
result = agent.invoke({
    "messages": [
        {"role": "user", "content": "你好"}
    ],
    "request_count": 0,
})
 
print(result["request_count"])

运行结果

代码片段Text
1

这里 before_model 先读取当前 State,然后返回:

代码片段Python
{
    "request_count": 1
}

这个返回值会作为状态更新合并回 State。

所以,我们可以看出:State 即为Agent 执行过程中持续被读取、修改和向后传递的数据。

实际开发中,可以用几个问题来判断某个值是否适合进入 State:

代码片段Text
它是否描述当前执行进度?
后续节点是否需要读取?
执行过程中是否可能变化?
之后是否可能需要恢复?

如果这些条件成立,通常就适合放进 State。

但有些数据虽然很多节点都需要,却并不属于执行状态。

最常见的就是当前用户身份。

四、Context 描述调用环境

运行时上下文(Runtime Context)描述的是:

这一次 Agent 是在什么业务环境下运行。

例如:

代码片段Text
user_id
tenant_id
权限信息
数据库连接
API Client
业务 Service
Feature Flag

这些信息通常不是 Agent 执行过程中产生的,而是在调用 Agent 之前就已经确定。

例如一个 Web 请求可能先经过认证:

代码片段Text
HTTP Request

Authentication

得到 user_id

调用 Agent

这里的 user_id 来自应用系统,而不是 Agent 推理出来的结果。

因此,可以定义一个 Context:

代码片段Python
from dataclasses import dataclass
 
 
@dataclass
class Context:
    user_id: str
    tenant_id: str

创建 Agent 时声明:

代码片段Python
from langchain.agents import create_agent
 
agent = create_agent(
    model="openai:gpt-5.5",
    tools=[],
    context_schema=Context,
)

调用时通过单独的 context 参数传入:

代码片段Python
result = agent.invoke(
    {
        "messages": [
            {"role": "user", "content": "查询我的账户"}
        ]
    },
    context=Context(
        user_id="user_123",
        tenant_id="tenant_a",
    ),
)

可以看到,接口本身已经把两种数据区分开了:

代码片段Python
agent.invoke(
    state_input,
    context=runtime_context,
)

前一个参数是 State 输入。

后面的 context 是本次调用的 Runtime Context。

我们再进一步对比一下 State 和 Context 的区别

例如:

代码片段Python
{
    "current_step": "waiting_payment",
    "retry_count": 2,
}

描述的是 Agent 当前执行到了哪里。

所以适合进入 State。

而:

代码片段Python
Context(
    user_id="user_123",
    tenant_id="tenant_a",
)

描述的是这次 Agent 以什么身份、在什么租户环境下运行。

所以适合进入 Context。

可以记住以下规则:

代码片段Text
State
执行过程中发生了什么

Context
执行发生在什么业务环境中

还有一个区别很重要。

State 可以由 Checkpointer 按 Thread 保存,而 Context 是每次 invocation 传入的运行时依赖。配置 Checkpointer 并不会自动把 Context 变成短期记忆。

五、Config 控制一次执行

除了 State 和 Context,调用 Agent 时还经常会看到另一个参数:

代码片段Python
config

例如短期记忆中常见的写法:

代码片段Python
config = {
    "configurable": {
        "thread_id": "user-1"
    }
}
 
agent.invoke(
    {
        "messages": [
            {"role": "user", "content": "我叫 Bob"}
        ]
    },
    config=config,
)

这里的 config 到底是什么?

当前 LangChain 中,更准确的类型名称是:

代码片段Python
RunnableConfig

RunnableConfig 定义在 langchain_core.runnables 中,用来配置一次 Runnable 执行。Agent 本身也是 Runnable,因此调用 invokestream 等方法时,都可以传入 config

它常见的字段包括:

代码片段Text
configurable
tags
metadata
callbacks
run_name
run_id
max_concurrency
recursion_limit

例如:

代码片段Python
config = {
    "tags": ["customer-service"],
    "metadata": {
        "request_source": "web"
    },
    "configurable": {
        "thread_id": "user-1"
    }
}

这里其实包含了两类执行配置。

tagsmetadatacallbacks 等主要用于 tracing、logging 和执行控制。

而:

代码片段Python
"configurable": {
    "thread_id": "user-1"
}

用于向已经声明为 configurable 的运行组件传递运行时配置。LangGraph 的 Checkpointer 就会通过这里的 thread_id 识别当前 Thread。

Config 和 Context 不是一回事

这两个对象很容易混淆,因为它们都是调用 invoke 时传入的。

例如:

代码片段Python
agent.invoke(
    input,
    config=config,
    context=context,
)

但它们的职责不同。

Context 更偏向业务运行环境:

代码片段Text
当前用户是谁
属于哪个租户
使用哪个数据库连接
注入哪个业务 Service

RunnableConfig 更偏向这一次 Runnable 怎样执行:

代码片段Text
属于哪个 thread_id
使用哪些 tags
附带哪些 metadata
使用哪些 callbacks
并发限制是多少
递归限制是多少

可以对比一下:

代码片段Text
Context

业务依赖和调用环境

RunnableConfig

Runnable 的执行配置

例如 user_id 最好不要因为 Config 支持 metadataconfigurable,就全部塞进 Config。

如果它确实是 Tool、Middleware 需要使用的业务身份,更清晰的方式通常是放进 Runtime Context。

thread_id 则不同。

它不是 Agent 的业务状态,也不是业务依赖,而是运行系统识别 Thread 和 Checkpoint 的配置,因此放在 configurable 中。

六、Runtime 提供运行环境

前面已经分别介绍了 State、Context 和 Config。

接下来再看 Runtime。

Runtime 是 LangGraph 在 Agent 执行期间提供的运行环境对象,它既不是另一份状态,也不能替代 Config。它承载着执行过程中的上下文与工具调用能力,是 Agent 运行时的重要组成部分。

当前 Runtime 主要包含:

代码片段Text
context
store
stream_writer
execution_info
server_info

其中:

context 就是前面传入的 Runtime Context。

store 用于访问长期存储。

stream_writer 用于向 Custom Stream 输出数据。

execution_info 提供当前 Thread、Run、节点重试次数等执行信息。

server_info 则用于 LangGraph Server 环境,例如读取 assistant ID、graph ID 和认证用户信息。

例如 Middleware 可以直接访问 Runtime:

代码片段Python
from dataclasses import dataclass
 
from langchain.agents import AgentState, create_agent
from langchain.agents.middleware import before_model
from langgraph.runtime import Runtime
 
 
@dataclass
class Context:
    user_name: str
 
 
@before_model
def log_user(
    state: AgentState,
    runtime: Runtime[Context],
) -> None:
    print(runtime.context.user_name)
 
 
agent = create_agent(
    model="openai:gpt-5.5",
    tools=[],
    middleware=[log_user],
    context_schema=Context,
)
 
agent.invoke(
    {
        "messages": [
            {"role": "user", "content": "你好"}
        ]
    },
    context=Context(user_name="Bob"),
)

运行结果

代码片段Text
Bob

这里的两种访问方式正好反映了 State 和 Runtime 的职责:

代码片段Python
state["messages"]

读取的是当前执行状态。

代码片段Python
runtime.context.user_name

读取的是本次调用的业务上下文。

因此,在 Node 或 Node 风格 Middleware 中,经常会看到这样的函数签名:

代码片段Python
def node(
    state: AgentState,
    runtime: Runtime[Context],
):
    ...

State 提供“当前执行数据”。

Runtime 提供“当前运行环境”。

七、ToolRuntime 服务于工具

工具也需要访问 Agent 当前的运行环境。

例如:

代码片段Python
@tool
def get_order(order_id: str):
    ...

这里让模型提供:

代码片段Text
order_id

很合理,因为订单编号来自用户请求或模型判断。

但如果工具还需要:

代码片段Text
user_id
tenant_id
当前 State
当前 Tool Call ID
RunnableConfig
Store

就没有必要继续把这些字段暴露给模型。

否则工具可能变成:

代码片段Python
get_order(
    order_id,
    user_id,
    tenant_id,
    tool_call_id,
)

其中很多参数都不应该由模型决定。

LangChain 为工具提供了 ToolRuntime

代码片段Python
from dataclasses import dataclass
 
from langchain.tools import ToolRuntime, tool
 
 
@dataclass
class Context:
    user_id: str
 
 
@tool
def get_account(
    runtime: ToolRuntime[Context],
) -> str:
    """Get the current user's account."""
 
    user_id = runtime.context.user_id
 
    return f"Account of {user_id}"

runtime 参数由运行系统自动注入,不会作为普通 Tool Argument 暴露给模型。

模型并不需要知道 user_id 从哪里来。

工具自己从:

代码片段Python
runtime.context.user_id

读取即可。

ToolRuntime 中有什么

ToolRuntime 可以访问:

代码片段Text
state
context
store
stream_writer
execution_info
server_info
config
tool_call_id

其中前几项和普通 Runtime 的运行能力有关。

但 ToolRuntime 还额外提供:

代码片段Text
state
config
tool_call_id

这些信息和当前 Tool 调用直接相关。

例如:

代码片段Python
runtime.state

可以读取当前 Agent State。

代码片段Python
runtime.context

可以读取 Runtime Context。

代码片段Python
runtime.config

得到当前执行的 RunnableConfig

代码片段Python
runtime.tool_call_id

得到当前工具调用对应的 Tool Call ID。

所以可以这样理解:

代码片段Text
Runtime

Node、Middleware 使用的运行环境

ToolRuntime

Tool 使用的运行环境
并补充工具执行所需的信息

八、ToolRuntime 连接几类数据

ToolRuntime 很适合用来观察前面几个概念之间的关系。

例如:

代码片段Python
from dataclasses import dataclass
 
from langchain.agents import AgentState
from langchain.tools import ToolRuntime, tool
 
 
class CustomState(AgentState):
    selected_order_id: str
 
 
@dataclass
class Context:
    user_id: str
 
 
@tool
def inspect_current_order(
    runtime: ToolRuntime[Context],
) -> str:
    """Inspect the currently selected order."""
 
    user_id = runtime.context.user_id
    order_id = runtime.state["selected_order_id"]
 
    thread_id = runtime.config.get(
        "configurable", {}
    ).get("thread_id")
 
    return (
        f"user={user_id}, "
        f"order={order_id}, "
        f"thread={thread_id}"
    )

这里一次读取了三类数据。

代码片段Python
runtime.state["selected_order_id"]

来自 State。

它表示 Agent 当前正在处理哪个订单。

代码片段Python
runtime.context.user_id

来自 Context。

它表示这次调用对应哪个登录用户。

代码片段Python
runtime.config

则是 RunnableConfig。

其中可以读取当前执行传入的 Config,例如 thread_id。ToolRuntime 明确提供 config,类型就是当前执行的 RunnableConfig

三者虽然最终都能被 Tool 读取,但来源和用途并不相同:

代码片段Text
selected_order_id

执行过程产生

State

user_id

调用前由业务系统提供

Context

thread_id

控制 Runnable / Checkpointer 执行

RunnableConfig

九、工具也可以修改 State

ToolRuntime 不只用于读取数据。

如果工具执行结果会影响后续 Agent 执行,可以通过 Command 更新 State。

例如:

代码片段Python
from langchain.messages import ToolMessage
from langchain.tools import ToolRuntime, tool
from langgraph.types import Command
 
 
@tool
def set_language(
    language: str,
    runtime: ToolRuntime,
) -> Command:
    """Set the preferred response language."""
 
    return Command(
        update={
            "preferred_language": language,
            "messages": [
                ToolMessage(
                    content=f"Language set to {language}.",
                    tool_call_id=runtime.tool_call_id,
                )
            ],
        }
    )

示例输出

假设模型执行:

代码片段Text
set_language(language="Chinese")

工具执行之后,State 中可以得到:

代码片段Text
preferred_language = "Chinese"

同时 messages 中新增:

代码片段Text
ToolMessage("Language set to Chinese.")

这里两个更新承担的职责不同。

ToolMessage 让模型知道工具刚才做了什么。

preferred_language 则作为结构化状态保留下来,供后面的 Middleware、Tool 或 Node 使用。

如果工具执行之后产生的是:

代码片段Text
订单已经选中
审批已经完成
用户已经认证
当前任务进入 reviewed 阶段

并且这些结果会影响后续流程,就适合更新 State。

如果结果只需要告诉模型一次,普通 Tool Result 通常已经足够,没有必要额外增加 State 字段。

十、综合对比

最后,我们可以把这些概念放到一起看。

对象主要负责什么典型内容生命周期
StateAgent 当前执行状态messages、current_step、selected_order_id当前执行或 Thread
Context业务运行环境user_id、tenant_id、数据库连接单次调用
RunnableConfigRunnable 执行配置thread_id、tags、metadata、callbacks单次执行
Runtime提供运行环境能力context、store、stream writer当前执行
ToolRuntimeTool 的运行环境state、context、config、tool_call_id当前 Tool 调用
Store跨 Thread 长期数据用户偏好、用户画像长期

实际开发中,最容易出现的问题还是 State、Context 和 Config 混用。

下面这些通常更适合 State:

代码片段Text
messages
current_step
selected_order_id
retry_count
approval_status
中间处理结果

下面这些通常更适合 Context:

代码片段Text
user_id
tenant_id
数据库连接
业务 Service
API Client
权限信息
Feature Flag

下面这些则属于 RunnableConfig:

代码片段Text
thread_id
tags
metadata
callbacks
run_name
recursion_limit
max_concurrency

如果某些数据需要跨 Thread 长期保存,例如用户偏好、用户画像和历史事实,更适合 Store。

而某个值如果只在一个函数内部临时使用,后续执行完全不需要,就没有必要进入这些对象,普通局部变量已经足够。

尤其不要因为 State 可以扩展字段,就把所有运行数据都放进去。

数据库连接、Service、Tracing Metadata、Thread 配置和业务状态如果全部混在 State 中,不仅边界会越来越模糊,还会增加 Checkpoint、序列化和调试的复杂度。

可以用下面这组判断作为参考:

代码片段Text
执行过程中需要持续变化

      State

调用开始时确定的业务环境

     Context

控制本次 Runnable 怎样执行

  RunnableConfig

跨 Thread 长期保存

      Store

仅当前函数临时使用

  Local Variable

总结

本文介绍了 LangChain Agent 运行时中数据管理职责的清晰划分:State、Context 与 RunnableConfig 各司其职,分别对应状态更新、业务环境描述和运行控制。

在 Agent 执行过程中,不同类别的数据不应混存于同一对象中。

State 负责记录执行状态,并随时间持续更新;Context 描述调用所处的业务环境;RunnableConfig 则控制 Runnable 的运行方式。Runtime 是执行环境,提供 Context、Store、Streaming 等能力;Tool 层通过 ToolRuntime 访问这些能力,同时也可读取 State、RunnableConfig 与 Tool Call ID。它们并非彼此孤立的机制,而是分布在 Agent Runtime 的不同层次,对不同生命周期的数据进行读取、更新、持久化与注入。

理解它们的职责与作用,有助于在复杂 Agent 场景中更清晰地把控数据流与运行时职责边界。