LangGraph设计与实现-第14章-Runtime 与 Context

《LangGraph 设计与实现》完整目录

第14章 Runtime 与 Context

14.1 引言

在构建 LLM 应用的图时,节点函数往往需要访问一些"运行时依赖"------当前用户的身份信息、数据库连接池、API 密钥、或者一个全局的向量存储。这些依赖既不属于图的状态(它们不随步骤变化),也不应该被硬编码在节点函数中(它们因调用而异)。传统做法是通过闭包或全局变量传递,但这在测试、多租户和类型安全方面都存在痛点。

LangGraph 1.1.6 引入了 Runtime 类和 ContextT 泛型来解决这个问题。Runtime 是一个不可变的数据容器,在图执行开始时由调用方创建,自动注入到每个节点函数中。它携带了 context(用户自定义的运行时上下文)、store(持久化存储)、stream_writer(流式写入器)、execution_info(执行元数据)等运行时信息。

本章将从 Runtime 的数据类定义出发,分析 ContextT 泛型的设计理念、ExecutionInfoServerInfo 的信息模型、context 与 state 的本质区别,以及 Runtime 在 Pregel 循环中的注入机制。

:::tip 本章要点

  1. Runtime 类的完整字段定义------context、store、stream_writer、previous、execution_info、server_info
  2. ContextT 泛型的类型传播------从 StateGraph 到节点函数的端到端类型安全
  3. ExecutionInfoServerInfo 的信息模型------执行元数据的结构化表达
  4. Context vs State 的本质区别------不可变依赖 vs 可变状态
  5. Runtime 注入机制------从编译到执行的完整链路 :::

14.2 Runtime 类的设计

14.2.1 数据类定义

Runtime 定义在 langgraph/runtime.py 中,是一个泛型冻结数据类:

python 复制代码
@dataclass(**_DC_KWARGS)  # kw_only=True, slots=True, frozen=True
class Runtime(Generic[ContextT]):
    """Convenience class that bundles run-scoped context and other runtime utilities."""

    context: ContextT = field(default=None)
    """Static context for the graph run, like user_id, db_conn, etc."""

    store: BaseStore | None = field(default=None)
    """Store for the graph run, enabling persistence and memory."""

    stream_writer: StreamWriter = field(default=_no_op_stream_writer)
    """Function that writes to the custom stream."""

    previous: Any = field(default=None)
    """The previous return value for the given thread (functional API only)."""

    execution_info: ExecutionInfo | None = field(default=None)
    """Read-only execution information/metadata for the current node run."""

    server_info: ServerInfo | None = field(default=None)
    """Metadata injected by LangGraph Server. None for open-source."""

_DC_KWARGS 展开为 kw_only=True, slots=True, frozen=True,这意味着:

  • kw_only:所有字段必须通过关键字参数传递,避免位置参数的歧义
  • slots :使用 __slots__ 优化内存和属性访问速度
  • frozen:实例创建后不可修改,确保运行时安全

14.2.2 字段语义

graph TB Runtime[Runtime 对象] Runtime --> Context["context: ContextT
用户自定义上下文
如 user_id, db_conn"] Runtime --> Store["store: BaseStore | None
持久化存储
跨线程记忆"] Runtime --> SW["stream_writer: StreamWriter
自定义流式写入
发射中间结果"] Runtime --> Prev["previous: Any
上次执行的返回值
仅函数式 API"] Runtime --> EI["execution_info: ExecutionInfo
执行元数据
checkpoint_id, task_id 等"] Runtime --> SI["server_info: ServerInfo
服务器元数据
assistant_id, user 等"]

六个字段覆盖了节点函数可能需要的所有运行时信息:

字段 类型 来源 可变性
context ContextT(泛型) 调用方传入 整个执行期间不变
store BaseStore 图编译时配置 引用不变,内容可变
stream_writer StreamWriter 框架自动注入 每个任务独立
previous Any Checkpoint 读取 只读
execution_info ExecutionInfo 框架生成 每个任务独立
server_info ServerInfo LangGraph Server 只读

14.2.3 不可变性与 override/merge

虽然 Runtime 是 frozen 的,但它提供了两个方法来创建修改后的副本:

python 复制代码
def merge(self, other: Runtime[ContextT]) -> Runtime[ContextT]:
    """Merge two runtimes together. If a value is not provided in other,
    the value from self is used."""
    return Runtime(
        context=other.context or self.context,
        store=other.store or self.store,
        stream_writer=other.stream_writer
            if other.stream_writer is not _no_op_stream_writer
            else self.stream_writer,
        previous=self.previous if other.previous is None else other.previous,
        execution_info=other.execution_info or self.execution_info,
        server_info=other.server_info or self.server_info,
    )

def override(self, **overrides) -> Runtime[ContextT]:
    """Replace the runtime with a new runtime with the given overrides."""
    return replace(self, **overrides)

merge 用于子图继承父图的 Runtime 时,合并两个 Runtime 对象。override 用于框架在任务准备阶段注入特定字段(如 execution_info)。

14.3 ContextT 泛型

14.3.1 定义

python 复制代码
# langgraph/typing.py
ContextT = TypeVar("ContextT", bound=StateLike | None, default=None)

ContextT 是一个带默认值的类型变量,约束为 StateLike | NoneStateLike 包括 TypedDictBaseModeldataclass 等结构化类型。默认值为 None,这意味着如果不指定 context_schema,Runtime 的 context 字段类型就是 None

14.3.2 类型传播链路

flowchart LR Schema["context_schema=MyContext"] --> SG["StateGraph[State, MyContext]"] SG --> Compile["compile()"] Compile --> CSG["CompiledStateGraph[State, MyContext, ...]"] CSG --> Invoke["invoke(input, context=MyContext(...))"] Invoke --> RT["Runtime[MyContext]"] RT --> Node["node(state, runtime: Runtime[MyContext])"]

类型从 StateGraphcontext_schema 参数开始,贯穿编译、调用、注入的全过程。IDE 和类型检查器可以在每一步提供准确的类型补全。

14.3.3 使用示例

python 复制代码
from dataclasses import dataclass
from langgraph.graph import StateGraph
from langgraph.runtime import Runtime
from typing_extensions import TypedDict

@dataclass
class AppContext:
    user_id: str
    api_key: str
    is_admin: bool = False

class State(TypedDict, total=False):
    response: str

def my_node(state: State, runtime: Runtime[AppContext]) -> State:
    # IDE 知道 runtime.context 的类型是 AppContext
    user_id = runtime.context.user_id
    if runtime.context.is_admin:
        return {"response": f"Admin {user_id}: full access"}
    return {"response": f"User {user_id}: limited access"}

graph = (
    StateGraph(state_schema=State, context_schema=AppContext)
    .add_node("my_node", my_node)
    .set_entry_point("my_node")
    .set_finish_point("my_node")
    .compile()
)

result = graph.invoke({}, context=AppContext(user_id="alice", api_key="sk-..."))

14.4 ExecutionInfo:执行元数据

14.4.1 数据结构

python 复制代码
@dataclass(frozen=True, slots=True)
class ExecutionInfo:
    """Read-only execution info/metadata for the current thread/run/node."""

    checkpoint_id: str
    """The checkpoint ID for the current execution."""

    checkpoint_ns: str
    """The checkpoint namespace for the current execution."""

    task_id: str
    """The task ID for the current execution."""

    thread_id: str | None = None
    """None when running without a checkpointer."""

    run_id: str | None = None
    """None when run_id is not provided in RunnableConfig."""

    node_attempt: int = 1
    """Current node execution attempt number (1-indexed)."""

    node_first_attempt_time: float | None = None
    """Unix timestamp for when the first attempt started."""

ExecutionInfo 提供了节点函数可能需要的所有执行上下文信息,而无需直接操作低层的 RunnableConfig

14.4.2 字段用途

graph TB EI[ExecutionInfo] EI --> CID["checkpoint_id
当前检查点 ID
用于状态追踪"] EI --> CNS["checkpoint_ns
检查点命名空间
标识子图层级"] EI --> TID["task_id
任务 ID
唯一标识本次执行"] EI --> ThID["thread_id
线程 ID
跨轮次对话标识"] EI --> RID["run_id
运行 ID
单次调用标识"] EI --> NA["node_attempt
重试次数
1 表示首次执行"] EI --> NFAT["node_first_attempt_time
首次尝试时间
用于超时计算"]

典型的使用场景:

python 复制代码
def my_node(state: State, runtime: Runtime) -> State:
    info = runtime.execution_info
    # 日志中记录执行上下文
    logger.info(f"Thread={info.thread_id}, Task={info.task_id}, Attempt={info.node_attempt}")

    # 根据重试次数调整行为
    if info.node_attempt > 1:
        logger.warning("Retrying, using fallback strategy")

    # 使用 thread_id 做线程级缓存
    cache_key = f"{info.thread_id}:{info.task_id}"
    ...

14.4.3 patch 方法

ExecutionInfo 是 frozen 的,但提供了 patch 方法创建修改后的副本:

python 复制代码
def patch(self, **overrides: Any) -> ExecutionInfo:
    """Return a new execution info object with selected fields replaced."""
    return replace(self, **overrides)

框架在重试时使用这个方法更新 node_attemptnode_first_attempt_time

14.5 ServerInfo:服务端元数据

14.5.1 数据结构

python 复制代码
@dataclass(frozen=True, slots=True)
class ServerInfo:
    """Metadata injected by LangGraph Server."""

    assistant_id: str
    """The assistant ID for the current execution."""

    graph_id: str
    """The graph ID for the current execution."""

    user: BaseUser | None = None
    """The authenticated user, if any."""

ServerInfo 只在 LangGraph Platform(部署服务)环境中被填充。在本地开源运行时,runtime.server_info 始终为 None

14.5.2 BaseUser 协议

python 复制代码
# 来自 langgraph_sdk.auth.types
class BaseUser:
    """认证用户协议,支持属性访问和字典访问"""
    identity: str  # 用户唯一标识
    # 支持 user.identity 和 user["identity"] 两种访问方式

这使得节点函数可以在有认证的环境中安全地获取用户信息:

python 复制代码
def secure_node(state: State, runtime: Runtime) -> State:
    if runtime.server_info and runtime.server_info.user:
        user_id = runtime.server_info.user.identity
    else:
        user_id = "anonymous"
    ...

14.6 Context vs State 的本质区别

14.6.1 概念对比

这是理解 LangGraph 运行时模型的关键区分:

graph LR subgraph "State(状态)" direction TB S1[可变] --> S2[在节点间流动] S2 --> S3[被 Channel 管理] S3 --> S4[支持 reducer 合并] S4 --> S5[被 Checkpoint 持久化] end subgraph "Context(上下文)" direction TB C1[不可变] --> C2[在整个执行期间固定] C2 --> C3[由调用方提供] C3 --> C4[不参与状态管理] C4 --> C5[不被 Checkpoint 持久化] end
维度 State Context
可变性 每个节点可以修改 整个执行期间不变
流转方式 通过 Channel 在节点间传递 通过 Runtime 注入到所有节点
持久化 被 Checkpoint 保存 不被保存
典型内容 消息列表、处理结果 用户 ID、API 密钥
定义方式 state_schema=State context_schema=Context
传入方式 graph.invoke(input) graph.invoke(input, context=ctx)

14.6.2 为什么 Context 不放在 State 中?

把运行时依赖放在 State 中存在几个问题:

  1. Checkpoint 污染:数据库连接、API 密钥不应被序列化到 checkpoint
  2. 类型混淆:状态字段应该是"数据",而不是"工具"
  3. 安全风险:checkpoint 可能被导出或共享,敏感信息不应出现在其中
  4. 语义错误 :reducer 不应该对"用户 ID"做 operator.add

Context 通过将依赖项与数据分离,彻底解决了这些问题。

14.6.3 为什么 Context 不放在 Config 中?

LangGraph 0.6.0 之前,运行时依赖通过 RunnableConfig.configurable 传递(即旧的 config_schema 参数)。这种方式有几个缺点:

  1. 类型不安全 :config 是 dict[str, Any],失去了泛型类型信息
  2. API 混乱:config 的主要用途是传递 thread_id、checkpoint_id 等框架参数
  3. 嵌套访问 :需要 config["configurable"]["user_id"] 这样的深层访问

context_schemaRuntime[ContextT] 提供了一流的、类型安全的替代方案:

python 复制代码
# 旧方式(已弃用)
def my_node(state, config: RunnableConfig):
    user_id = config["configurable"]["user_id"]  # 无类型提示

# 新方式
def my_node(state, runtime: Runtime[AppContext]):
    user_id = runtime.context.user_id  # IDE 自动补全

14.7 Runtime 注入机制

14.7.1 注入链路总览

flowchart TB subgraph 调用层 Caller["graph.invoke(input, context=ctx)"] end subgraph Pregel 初始化 Caller --> CreateRT["创建 Runtime(context=ctx, store=store)"] CreateRT --> InjectConfig["写入 config[CONF][CONFIG_KEY_RUNTIME]"] end subgraph 任务准备 InjectConfig --> PNT["prepare_next_tasks"] PNT --> PST["prepare_single_task / prepare_push_task_send"] PST --> Override["runtime.override(
previous=...,
store=...,
execution_info=...)"] Override --> TaskConfig["写入 task config[CONF][CONFIG_KEY_RUNTIME]"] end subgraph 节点执行 TaskConfig --> GetRT["节点接收 runtime 参数"] GetRT --> UseRT["runtime.context.user_id"] end

14.7.2 Pregel 初始化阶段

当调用 graph.invoke(input, context=ctx) 时,Pregel 的 stream 方法将 context 封装到 Runtime 中,并存入配置:

python 复制代码
# Pregel.stream 中的简化逻辑
runtime = Runtime(context=context, store=self.store)
config = patch_configurable(config, {CONFIG_KEY_RUNTIME: runtime})

14.7.3 任务准备阶段

prepare_single_task 中,框架从配置中取出 Runtime,注入任务级别的信息:

python 复制代码
# prepare_single_task 中的 PULL 任务逻辑
runtime = cast(
    Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME)
)
runtime = runtime.override(
    previous=checkpoint["channel_values"].get(PREVIOUS, None),
    store=store,
    execution_info=ExecutionInfo(
        checkpoint_id=checkpoint["id"],
        checkpoint_ns=task_checkpoint_ns,
        task_id=task_id,
        thread_id=configurable.get(CONFIG_KEY_THREAD_ID),
        run_id=str(rid) if (rid := config.get("run_id")) else None,
    ),
)

每个任务都获得一个新的 Runtime 实例(因为 frozen,所以是 replace 创建的新对象),其中 execution_info 包含了该任务特有的元数据。

14.7.4 节点函数接收

框架通过检查节点函数的参数签名,自动注入 Runtime:

python 复制代码
# 节点函数可以声明 runtime 参数
def my_node(state: State, runtime: Runtime[AppContext]) -> State:
    ...

# 或者通过 get_runtime() 手动获取
from langgraph.runtime import get_runtime

def my_node(state: State) -> State:
    runtime = get_runtime(AppContext)  # 返回 Runtime[AppContext]
    ...

get_runtime 函数从当前线程的配置中提取 Runtime:

python 复制代码
def get_runtime(context_schema: type[ContextT] | None = None) -> Runtime[ContextT]:
    runtime = cast(Runtime[ContextT], get_config()[CONF].get(CONFIG_KEY_RUNTIME))
    return runtime

14.8 DEFAULT_RUNTIME 与空操作

python 复制代码
DEFAULT_RUNTIME = Runtime(
    context=None,
    store=None,
    stream_writer=_no_op_stream_writer,
    previous=None,
    execution_info=None,
)

DEFAULT_RUNTIME 是当没有显式提供 context 时使用的默认值。它的所有字段都是"空"或"无操作"的,确保节点代码在没有 context 的情况下也能安全运行。

python 复制代码
def _no_op_stream_writer(_: Any) -> None: ...

空操作的 stream_writer 意味着节点调用 runtime.stream_writer(data) 不会产生任何效果------数据会被静默丢弃。这个设计让节点代码不需要检查 runtime 是否"可用"。

14.9 子图中的 Runtime 传播

14.9.1 merge 语义

当执行进入子图时,子图可能有自己的 store 和 context。Runtime 的 merge 方法用于合并父图和子图的 Runtime:

python 复制代码
def merge(self, other: Runtime[ContextT]) -> Runtime[ContextT]:
    return Runtime(
        context=other.context or self.context,        # 子图优先
        store=other.store or self.store,              # 子图优先
        stream_writer=other.stream_writer             # 子图优先
            if other.stream_writer is not _no_op_stream_writer
            else self.stream_writer,
        previous=self.previous if other.previous is None else other.previous,
        execution_info=other.execution_info or self.execution_info,
        server_info=other.server_info or self.server_info,
    )

合并策略是"子图覆盖父图"------如果子图提供了自己的 context,则使用子图的;否则继承父图的。

14.9.2 传播示意

graph TB subgraph 父图 PR["Runtime[ParentCtx]
context=ParentCtx(...)"] PR --> N1[Node A] PR --> SubEntry[子图入口] end subgraph 子图 SubEntry --> Merge["merge(parent_rt, child_rt)"] Merge --> CR["Runtime[ChildCtx]
context 继承或覆盖"] CR --> N2[Node B] CR --> N3[Node C] end

14.10 设计决策

14.10.1 为什么 Runtime 是 frozen 的?

frozen dataclass 带来三个好处:

  1. 线程安全:并发执行的多个节点读取同一个 Runtime 实例时不会发生数据竞争
  2. 语义正确性:context 代表"不变的运行时依赖",frozen 在类型层面强制了这个语义
  3. 可哈希性:frozen dataclass 默认可哈希,便于缓存和去重

14.10.2 为什么 execution_info 在 Runtime 中而不是单独注入?

execution_info 放在 Runtime 中而非作为独立参数注入,有两个原因:

  1. 减少参数数量 :节点函数只需要一个 runtime 参数就能访问所有运行时信息
  2. 一致的生命周期:所有运行时信息在同一个对象中创建和传递,生命周期一致

14.10.3 ToolRuntime 与 Runtime 的关系

langgraph.prebuilt 中还有一个 ToolRuntime 类,它是专为工具函数设计的:

python 复制代码
class ToolRuntime(_DirectlyInjectedToolArg, Generic[ContextT, StateT]):
    """Runtime context automatically injected into tools."""
    context: ContextT     # 与 Runtime 共享
    store: BaseStore      # 与 Runtime 共享
    stream_writer: StreamWriter  # 与 Runtime 共享
    config: RunnableConfig       # 工具特有
    state: StateT                # 工具特有
    tool_call_id: str            # 工具特有

ToolRuntime 共享了 Runtime 的 contextstorestream_writer 字段,但增加了工具特有的 configstatetool_call_id。它们之间的关系是互补而非继承------Runtime 服务于节点,ToolRuntime 服务于工具。

14.11 实战:完整的 Runtime 使用案例

14.11.1 多租户 Agent 系统

以下示例展示了如何使用 Runtime 构建一个支持多租户的 Agent 系统,每个用户有独立的数据隔离和权限控制:

python 复制代码
from dataclasses import dataclass
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.runtime import Runtime
from langgraph.store.memory import InMemoryStore

@dataclass
class TenantContext:
    """多租户上下文"""
    tenant_id: str
    user_id: str
    role: str  # "admin" | "editor" | "viewer"
    db_connection_string: str

class AgentState(TypedDict, total=False):
    messages: list
    response: str

store = InMemoryStore()

def access_control_node(state: AgentState, runtime: Runtime[TenantContext]) -> dict:
    """访问控制节点:根据角色决定权限"""
    ctx = runtime.context
    info = runtime.execution_info

    # 记录审计日志到 Store
    if runtime.store:
        runtime.store.put(
            ("audit", ctx.tenant_id),
            f"access_{info.task_id}",
            {
                "user": ctx.user_id,
                "role": ctx.role,
                "action": "query",
                "thread_id": info.thread_id,
            }
        )

    if ctx.role == "viewer":
        return {"response": "You have read-only access."}
    return state

def process_node(state: AgentState, runtime: Runtime[TenantContext]) -> dict:
    """业务处理节点:使用租户隔离的数据"""
    ctx = runtime.context

    # 从租户命名空间读取配置
    if runtime.store:
        config = runtime.store.get(("tenants", ctx.tenant_id), "config")
        model_name = config.value["model"] if config else "default-model"
    else:
        model_name = "default-model"

    # 流式输出处理进度
    runtime.stream_writer({"status": "processing", "model": model_name})

    return {"response": f"Processed by {model_name} for tenant {ctx.tenant_id}"}

graph = (
    StateGraph(state_schema=AgentState, context_schema=TenantContext)
    .add_node("access_control", access_control_node)
    .add_node("process", process_node)
    .add_edge(START, "access_control")
    .add_edge("access_control", "process")
    .add_edge("process", END)
    .compile(store=store)
)

# 不同租户使用不同的 context
result = graph.invoke(
    {"messages": ["Hello"]},
    context=TenantContext(
        tenant_id="acme",
        user_id="alice",
        role="admin",
        db_connection_string="postgresql://acme:...",
    )
)

14.11.2 Runtime 在重试场景中的行为

当节点配置了 RetryPolicy 时,Runtime 的 execution_info 会在每次重试中更新:

python 复制代码
def flaky_node(state: AgentState, runtime: Runtime) -> dict:
    info = runtime.execution_info
    print(f"Attempt {info.node_attempt}")  # 1, 2, 3...

    if info.node_attempt == 1:
        raise ConnectionError("Temporary failure")

    # 第二次尝试成功
    return {"response": "Success after retry"}

框架会通过 runtime.patch_execution_info(node_attempt=2) 创建新的 Runtime 副本,传递给重试的执行。

14.12 小结

本章深入分析了 LangGraph 的 Runtime 与 Context 机制。Runtime[ContextT] 通过泛型类型参数将运行时依赖注入从一个"约定"提升为一个"类型安全的协议"。六个字段------contextstorestream_writerpreviousexecution_infoserver_info------覆盖了节点函数可能需要的全部运行时信息。frozen 语义确保了并发安全,overridemerge 方法提供了不可变更新的能力。

Context 与 State 的分离是 LangGraph 架构中的关键决策:State 是"随步骤变化的数据",通过 Channel 管理和 Checkpoint 持久化;Context 是"整个执行期间不变的依赖",通过 Runtime 注入且不被持久化。这种分离让状态管理更纯粹,同时为敏感信息(如 API 密钥)提供了安全的传递通道。

下一章我们将探讨 BaseStore 接口和 InMemoryStore 实现,了解 LangGraph 如何提供跨线程的长期记忆能力。

相关推荐
杨艺韬4 小时前
LangGraph设计与实现-第18章-设计模式与架构决策
langchain·agent
杨艺韬4 小时前
LangGraph设计与实现-第11章-子图与嵌套
langchain·agent
杨艺韬4 小时前
LangGraph设计与实现-第16章-预构建 Agent 组件
langchain·agent
杨艺韬4 小时前
LangGraph设计与实现-第5章-图编译:从 StateGraph 到 CompiledStateGraph
langchain·agent
杨艺韬4 小时前
LangGraph设计与实现-第12章-Send 与动态并行
langchain·agent
杨艺韬4 小时前
LangGraph设计与实现-第8章-Checkpoint 持久化
langchain·agent
杨艺韬4 小时前
LangGraph设计与实现-第4章-Channel 状态管理与 Reducer
langchain·agent
杨艺韬4 小时前
LangGraph设计与实现-第1章-为什么需要理解 LangGraph
langchain·agent
杨艺韬4 小时前
LangGraph设计与实现-第3章-StateGraph 图构建 API
langchain·agent