精读 LangChain 官方文档(七)Runtime Context 与 State 首讲
本篇对应的官方文档
- LangChain Runtime:Runtime Context、Store、stream writer、execution info 与 server info。
- LangChain Tools:工具通过
ToolRuntime读取 Context、State、Store,并用Command更新 State。- LangChain Agents:Agent state、
context_schema、state_schema与invoke的运行入口。本篇讲解范围
本篇建立 Runtime Context、State 与 Store 的数据放置边界,并跑通
context_schema、state_schema、ToolRuntime和Command。Tool Calling 协议已经在第 06 篇讲清;上下文如何选择、裁剪和持久化治理留给第 08 篇;短期与长期记忆的完整机制留给第 09 篇。
在我们学会写第一个工具以后,项目还会遇到一个问题:除了模型生成的业务参数,工具还需要 user id、数据库连接、request id、当前处理阶段、历史偏好和进度通知。这些信息如果全部塞进 prompt,不仅上下文越来越长,还会让敏感依赖暴露给模型;如果全部放进全局变量,测试、并发和多用户隔离又会迅速失控。
我们仍然用售后 Agent 举例。lookup_order(order_id)的order_id可以由模型从用户消息中提取;“当前是谁”不能让模型自己填写;数据库客户端是运行依赖;“已经查过订单、正在等待确认”会随循环变化;用户长期偏好的回复语言则可能跨会话存在。它们都叫“上下文”没有帮助,因为生命周期、可见性和写入者完全不同。
这幅运行全景把对象放回各自位置:Agent state 随节点推进;Runtime 为工具和 middleware 提供 context、store、stream writer 与执行元信息;模型只看到经过选择的 messages 和工具 schema。信息能被某个对象访问,不代表它应该进入模型输入。
本篇会从最常见的“全塞 prompt”问题出发,先建立 Context、State、Store 的三分规则,再看context_schema怎样注入依赖、state_schema怎样扩展运行状态、Command怎样写回,最后把 stream、execution info、server info 和错误放置方式补齐。
1. 数据放在哪里,先看三个维度
判断一条信息的容器,可以先问三个问题:它在一次 invocation 内是否保持稳定?谁需要读取它?它要存在多久?这三个维度比“它是不是上下文”更有区分力。
Runtime Context 适合一次 invocation 的静态信息与依赖,例如当前 user id、租户 id、数据库连接、功能开关、请求来源。这里的“静态”不是永远不变,而是调用开始时由可信入口提供,在本次运行中作为依赖使用。下一次调用可以传入不同用户和连接。
State 适合运行中会推进的数据。所有 Agent state 都包含 messages;应用还可以增加refund_stage、verified_order_id、retry_count等字段。模型、工具或 middleware 每走一步,都可能读取或更新 state,后续节点看到的是更新后的运行状态。
Store 面向跨会话、跨 thread 的长期数据,例如用户偏好、账户配置或沉淀的长期记忆。它不是当前 Agent loop 的工作台,而是可由 runtime 提供的持久存储接口。一次运行结束后仍要复用的信息,才有理由进入 Store。
三个容器可以用一句话区分:Context 提供本次运行依赖,State 保存本次运行进展,Store 保存跨运行信息。它们可能在同一个工具函数中被读取,却不能因为访问入口相同就互换。
再看售后案例。user_id放 Context,避免模型伪造身份;refund_stage放 State,让后续节点明确知道是否已经确认;“用户偏好中文简洁回复”放 Store,下一次新会话也可复用。数据库里的订单本身仍属于业务数据库,不要为了让 Agent 使用就复制成长期 Store 记录。
容器选择的核心不是数据类型,而是生命周期与控制权。同样是字符串,user_id、refund_stage和preferred_language会落在三个位置;只按 Python 类型无法做出正确选择。
还要问“权威来源是谁”。Context 中的user_id来自认证系统,State 中的verified_order_id来自一次已经完成的核验,Store 中的偏好来自用户明确设置;它们都是引用或运行事实。订单金额、退款余额等领域事实仍应实时查询业务服务。容器能保存数据,不等于它自动成为权威来源。
读取频率也会影响设计,但不能推翻生命周期。某个订单字段每轮都会用到,可以在 State 中保存经过验证的最小快照或版本号,却仍要为过期和恢复设计规则;不能仅因为“经常读取”就永久复制进 Store。性能优化要在正确边界内做,而不是通过制造第二份真相换取方便。
2.context_schema为一次调用声明依赖合同
Runtime Context 通过context_schema声明结构。可以使用 dataclass 定义本次运行需要的身份、依赖和追踪信息,然后在create_agent时注册,在invoke时通过context=传入。
fromdataclassesimportdataclassfromtypingimportProtocolclassOrderRepository(Protocol):defget_for_user(self,order_id:str,user_id:str)->dict|None:...@dataclassclassSupportContext:user_id:strrequest_id:strorders:OrderRepository这个 schema 不是发给模型填写的结构化输出。它描述应用调用 Agent 时必须提供什么。user_id来自认证层,request_id来自网关或追踪系统,orders是已经构造好的仓储依赖。
注入链从应用入口开始,而不是从 user message 开始。调用方先完成认证与依赖构造,再把SupportContext交给 Runtime;工具和 middleware 读取同一个本次运行上下文。模型不需要知道数据库对象,也不能通过自然语言修改 context。
依赖注入会改善测试。测试工具时可以传入内存仓储,生产环境传真实数据库实现;同一工具不需要读取全局单例,也不必在函数内部重新解析环境变量。多用户并发时,每次 invocation 拥有自己的 context,降低身份串用风险。
Context 仍要保持克制。把大量会变化的业务数据放进 dataclass,会让一次 run 内出现“context 说未确认,state 说已确认”的双重真相。Context 适合稳定依赖,动态业务阶段应该由 State 维护。
Context schema 也可以成为调用入口的验收合同。缺少 user id 或仓储依赖时,应在 Agent 开始执行前失败,而不是等模型选中工具后才出现难懂的空值错误。越早验证运行依赖,越容易把“入口配置错误”与“模型决策错误”分开。
多租户系统还应避免把可跨用户复用的全局对象与用户作用域对象混在一起。无状态 HTTP client 可以共享,绑定了租户权限的 repository 或 token 则应按 invocation 构造。schema 只表达形状,真正的作用域仍由依赖创建方式保证。
3.ToolRuntime让工具读取 Context,但不扩大模型 schema
第 06 篇已经看到ToolRuntime参数会从模型可见 schema 中隐藏。现在可以解释其完整意义:它是工具访问当前运行环境的统一接口,模型仍只填写业务参数。
fromlangchain.toolsimportToolRuntime,tool@tooldeflookup_order(order_id:str,runtime:ToolRuntime[SupportContext],)->dict:"""查询当前用户拥有的订单。"""order=runtime.context.orders.get_for_user(order_id=order_id,user_id=runtime.context.user_id,)returnorderor{"error":"order_not_found"}模型看到的参数只有order_id。执行时,LangChain 把当前 Runtime 注入runtime,工具从runtime.context取得受信任身份和仓储。这里的权限边界不会因自然语言改变:用户即使在消息里说“把 user id 改成管理员”,也不会改变 context 中的值。
隔离后的工具接口更接近业务动作:模型说“查 A1024”,应用决定“以谁的身份、通过哪个仓储查”。身份与连接不进入 prompt,调用参数也更短。安全性来自认证入口和查询实现,而不是模型是否遵守一句提示。
ToolRuntime还暴露 state 与 store。统一入口便于函数签名,但不意味着工具应随意读写所有数据。每个工具仍要明确自己依赖哪些字段、能访问哪个 Store 命名空间、是否允许写入。统一接口解决注入方式,不替代最小权限设计。
4. State 是 Agent loop 的运行工作台
Agent state 会随着节点执行而更新。messages 是所有 Agent 都有的基础字段:用户消息、模型的工具调用、工具结果和最终回答依次进入。自定义业务字段则用于保存不适合从自然语言反推的运行事实。
售后 Agent 可以有refund_stage:not_started、waiting_confirmation、submitted。它的状态变化应由显式字段承接;如果只把阶段写进一条ToolMessage,后续程序需要解析文本才能判断,workflow、middleware 和前端也无法稳定读取。
State 的变化属于一次运行或一个可恢复 thread。模型节点可以读取当前消息和字段,工具节点可以通过runtime.state读取,Command可以更新。更新后的值对同一 run 的后续步骤可见,因此它适合表达“现在进行到哪里”。
State 不是任意字典缓存。字段应有清楚语义、初始值和更新规则;可能被并行节点更新的字段要定义 reducer;高风险状态转换还应由确定性服务验证。把每个工具原始响应都塞进 state,会导致对象膨胀、序列化成本上升,也让恢复变得困难。
State 也不自动等于持久记忆。配置 checkpointer 并复用thread_id,同一会话可以恢复 state;没有 checkpointer,新调用不会凭空保留历史。跨不同 thread 的长期偏好则应进入 Store。是否持久和持久多久,是运行配置,不是state这个名字自带的保证。
恢复 State 时还要考虑代码版本。旧 thread 中的refund_stage="pending",新版本可能只接受waiting_confirmation;字段删除或类型改变也会让反序列化失败。自定义 state schema 一旦进入生产,就需要迁移策略和兼容测试,不能把它当作随时可改的临时字典。
State 体积也会影响 checkpoint 成本。把完整工具响应、二进制内容或重复消息长期保存在其中,会增加存储和恢复延迟。更稳的做法是保留必要引用、摘要和决策事实,大对象留在专门存储中,通过受控工具按需读取。
5.state_schema把业务阶段变成显式字段
LangChain 允许通过state_schema扩展 Agent state。自定义类型通常继承AgentState,保留 messages 等基础字段,再增加当前业务需要的状态。
fromlangchain.agentsimportAgentStateclassSupportState(AgentState):refund_stage:strverified_order_id:str|None创建 Agent 时传入state_schema=SupportState,调用时可以给初始字段。工具与 middleware 读取到的是有明确合同的 state,而不是散落在 messages 中的隐含判断。
扩展字段应服务当前运行机制。verified_order_id表示哪一个订单已经完成归属校验,避免后续写工具只凭模型再次传入的 id 执行;refund_stage表示流程阶段,便于限制“未确认不能提交”。它们都能改变后续节点行为,因此适合进入 State。
不要把所有业务数据库字段复制到 state。订单金额、商品明细和退款记录的权威来源仍是业务服务;State 可以保留必要引用和已验证结论。这样既减少上下文与持久化体积,也避免业务数据更新后 Agent 继续使用过期副本。
字段命名要体现状态而非指令。should_submit_refund=True可能来自模型建议,不等于业务允许;refund_stage="confirmed"若由明确确认步骤写入,更能表示已发生事实。State 记录运行事实,动作是否执行仍由工具和权限决定。
6.Command是工具写回 State 的受控路径
当工具需要更新 state,官方文档使用Command(update=...)。更新会合并进图状态,并对后续步骤可见。若模型也需要知道工具结果,可以同时追加使用当前tool_call_id配对的ToolMessage。
fromlangchain.messagesimportToolMessagefromlangchain.toolsimportToolRuntime,toolfromlanggraph.typesimportCommand@tooldefverify_order(order_id:str,runtime:ToolRuntime[SupportContext],)->Command:"""核验订单归属,并记录本次运行已验证的订单号。"""order=runtime.context.orders.get_for_user(order_id=order_id,user_id=runtime.context.user_id,)iforderisNone:returnCommand(update={"messages":[ToolMessage(content="订单不存在或不属于当前用户。",tool_call_id=runtime.tool_call_id,)]})returnCommand(update={"verified_order_id":order_id,"refund_stage":"waiting_confirmation","messages":[ToolMessage(content="订单归属已核验,下一步需要用户确认。",tool_call_id=runtime.tool_call_id,)],})代码中的成功与失败分支刻意采用不同写回路径:只有归属核验成功时才更新verified_order_id和refund_stage,两条分支都用当前调用标识返回消息。
成功分支写入两个明确事实:订单已验证,流程等待确认。失败分支只返回消息,不伪造verified_order_id。下一次模型调用能读到工具结果,后续写工具也能检查 State,而不是信任模型在参数里声称“已经验证”。
Command不会自动完成业务事务。验证订单与提交退款仍由仓储或服务保证一致性;State update 只是 Agent 运行视图。若外部写入成功而 State 写回失败,需要幂等与恢复策略;若 State 先标记成功而外部写入失败,则会产生假状态。高风险动作通常应先让业务服务完成事务,再把确定结果写回 State。
并发更新要更谨慎。列表类字段可以用 reducer 合并,排他阶段则应串行转换或检查版本。两个工具同时把refund_stage改成不同值,不能简单用最后写入覆盖来定义业务正确性。
7. Store 保存跨会话信息,但不是业务数据库替身
Runtime 中的 Store 提供长期存取入口。工具可以通过runtime.store读取或写入用户偏好、长期记忆等跨 thread 数据。例如用户明确选择“以后默认中文简洁回复”,下次新会话仍可读取。
Store 与 State 的最大差异是作用域。State 围绕当前 thread 和运行推进,Store 使用命名空间与 key 管理长期数据。把用户偏好只放 State,新 thread 会丢失;把一次退款阶段写进 Store,又可能让未来无关会话继承过期流程。
Store 也不应该复制订单权威数据。订单、退款和权限仍由各自业务数据库维护,工具通过依赖查询。长期记忆适合保存应用层可复用信息,不适合绕过领域服务建立第二套事实源。
写入 Store 前还要考虑来源和撤销。模型推断出的“用户可能喜欢简短回答”与用户明确设置不是同等可信;敏感信息是否允许长期保存、保存多久、用户能否删除,都需要产品与合规规则。第 09 篇会继续展开 Memory 的读写与边界。
Store 的命名空间同样承担隔离职责。用户级偏好、团队级配置和应用级知识不能只靠 key 前缀随意拼接;读写工具应限制可访问范围,并把 tenant、user 和数据类别纳入明确命名空间。否则,统一 Store 会把原本隔离的长期信息汇聚成新的越权入口。
长期写入通常还需要来源、时间和版本。读取到旧偏好时,应用才能判断是否仍适用;用户修改或删除时,也能定位受影响记录。只保存一个无来源字符串,会让 Memory 很快变成无法审计的事实堆。
8. Runtime 还提供哪些运行信息
官方 Runtime 文档除 Context 与 Store 外,还列出 stream writer、execution info 和 server info。它们不属于 Agent 业务 State,却能支持进度、追踪和部署环境判断。
runtime.stream_writer可以在长工具执行时发送自定义进度,例如“已连接订单服务”“正在核验售后规则”。这些事件面向前端体验,不应靠不断追加ToolMessage实现,否则进度文本会污染模型上下文。
runtime.execution_info提供 thread id、run id 和重试 attempt 等执行身份。工具日志带上这些字段,才能把一次外部 API 调用与具体 Agent trace 对齐。它们适合审计和诊断,不必交给模型做业务推理。
runtime.server_info只在 LangGraph Server 环境中提供 assistant id、graph id、认证用户等服务器元数据;本地运行或测试时可能为None。这条部署边界要求代码显式处理缺失,不能把服务器专属字段写成普遍存在。
这三类信息共同说明 Runtime 不只是“context 字典”。它连接依赖注入、长期 Store、前端进度与执行身份;工具通过一个统一对象访问,但每个成员服务不同消费者。业务判断、用户界面与运维追踪不应混用同一字段。
流式进度也不能冒充业务完成。前端收到“正在提交退款”只表示工具执行到某一步,最终成功仍应以业务服务结果和 State update 为准。断线、重试或并行工具会使进度事件重复,消费端应把它们当作可丢失的体验信号,而不是结算依据。
execution info 则适合建立幂等和追踪关联,但不能把run_id直接当作所有业务动作的幂等键。一次业务请求可能跨 run 重试,同一个 run 也可能执行多个动作;幂等范围应由业务语义决定,执行身份只作为诊断和关联字段。
9. 用一套 Agent 串起 Context 与 State
下面把前面的类型和工具装进create_agent。代码重点看四个交接:context_schema注册运行依赖,state_schema注册动态字段,invoke分别传 state update 与 context,工具通过ToolRuntime读取并返回Command。
importosfromdataclassesimportdataclassfromtypingimportProtocolfromlangchain.agentsimportAgentState,create_agentfromlangchain.messagesimportToolMessagefromlangchain.toolsimportToolRuntime,toolfromlangchain_openaiimportChatOpenAIfromlanggraph.typesimportCommandclassOrderRepository(Protocol):defget_for_user(self,order_id:str,user_id:str)->dict|None:...@dataclassclassSupportContext:user_id:strrequest_id:strorders:OrderRepositoryclassSupportState(AgentState):refund_stage:strverified_order_id:str|None@tooldefverify_order(order_id:str,runtime:ToolRuntime[SupportContext],)->Command:"""核验订单是否属于当前用户,并记录本次运行的验证结果。"""order=runtime.context.orders.get_for_user(order_id=order_id,user_id=runtime.context.user_id,)iforderisNone:returnCommand(update={"messages":[ToolMessage(content="订单不存在或不属于当前用户。",tool_call_id=runtime.tool_call_id,)]})returnCommand(update={"verified_order_id":order_id,"refund_stage":"waiting_confirmation","messages":[ToolMessage(content="订单已核验,等待用户确认退款。",tool_call_id=runtime.tool_call_id,)],})model=ChatOpenAI(model="qwen3.7-plus",api_key=os.environ["DASHSCOPE_API_KEY"],base_url=os.environ["DASHSCOPE_BASE_URL"],)agent=create_agent(model=model,tools=[verify_order],context_schema=SupportContext,state_schema=SupportState,)result=agent.invoke({"messages":[{"role":"user","content":"请核验订单 A1024,我想申请退款。"}],"refund_stage":"not_started","verified_order_id":None,},context=SupportContext(user_id="user-42",request_id="req-20260718-001",orders=order_repository,),)调用开始时,应用把 user id、request id 和仓储放进 Context;用户消息与初始阶段进入 State。模型只为工具生成order_id。执行时工具组合模型参数与可信依赖,查询结果决定Command怎样更新 State。下一步看到的是经过验证的订单引用,而不是用户或模型自报的身份。
如果这里报错,按容器定位。工具拿不到 user id,检查context_schema与invoke(context=...);下一步看不到verified_order_id,检查Command.update与 state schema;新 thread 丢失偏好,检查 Store 或 memory,而不是给 prompt 加一句“请记住”。
10. 数据错放会造成什么故障
把身份放进 messages,模型可能看到甚至复述敏感字段,用户也可能通过提示注入尝试修改;把数据库连接放全局变量,多租户测试和并发隔离变得困难;把动态阶段放 Context,运行中会出现两套相互矛盾的状态;把长期偏好只放 State,新 thread 无法复用;把订单权威数据复制进 Store,又会产生过期事实源。
故障并不是抽象的“上下文混乱”,而会落到四类后果:模型看到不该看到的信息;工具使用错误身份或依赖;后续节点读取过期阶段;跨会话复用不该复用的数据。容器边界同时影响安全、正确性、成本和可恢复性。
可以用一张放置清单收尾:模型从对话中推断的业务参数进入 Tool Calling;本次调用的可信身份和依赖进入 Context;循环中推进的事实进入 State;跨 thread 复用的偏好或记忆进入 Store;进度进入 stream writer;追踪身份进入 execution info;领域事实留在业务系统。
11. 回到开篇:Runtime 解决注入,State 解决推进
现在再看售后工具需要的那组信息:order_id由模型生成;user_id、仓储和 request id 由 Context 注入;refund_stage与verified_order_id随 State 推进;长期语言偏好从 Store 读取;进度与执行 id 走各自 Runtime 能力。它们终于不必挤在一个 prompt 或全局字典里。
整篇最重要的判断可以压缩成三句:Context 提供一次运行的可信依赖,State 保存运行中正在变化的事实,Store 保存跨运行仍需复用的信息。ToolRuntime让工具统一访问这些对象,Command让状态更新成为显式数据流。
以上的边界清楚以后,我们的Agent 仍会面对下一层问题:即使 State、Store 和 Context 都放对了,是否应该把它们全部交给模型?什么时候只选择部分 messages,什么时候动态注入 system prompt,什么时候临时裁剪,什么时候持久摘要?第 08 篇的 Context Engineering 会从“信息放在哪里”继续走向“信息何时、以什么形式进入哪一步”。