摘要:作为 Agent 补充篇,补齐贯穿始终的底座:Runtime 运行时与上下文工程。介绍 Runtime(贯穿生命周期的上下文载体,承载 Context/Store/Stream Writer/Execution Info/Server Info 五类信息)、上下文工程框架(三类上下文 × 三种数据源)、模型上下文进阶(dynamic_prompt 结合三数据源、wrap_model_call 注入消息)、工具上下文(一个工具同时操作三种数据源)。配合 DeepSeek 实测。
前言
上一篇,我们介绍了 Guardrails 安全护栏;
传送门:【LangChain 1.x】13、安全护栏|PII 脱敏、自定义护栏与多层叠加防护
回看整个 Agent 系列(第8--13篇),有一个贯穿始终的"底座"一直没有介绍:Runtime 运行时与上下文工程。
- 第8篇的
context_schema - 第9篇的
ToolRuntime和Command - 第10篇的
SummarizationMiddleware - 第11篇的
Store
这些,其实都建立在同一套机制上:Runtime。
本篇就把这个底座补上:
- Runtime 运行时:贯穿 Agent 生命周期的上下文载体,承载五类信息
- 上下文工程框架:三类上下文(模型/工具/生命周期)× 三种数据源(Context/State/Store)
- 模型上下文进阶 :
dynamic_prompt结合三数据源、wrap_model_call注入消息 - 工具上下文:一个工具,同时操作三种数据源
一、Runtime 运行时:Agent 的上下文载体
Runtime 是什么
create_agent 底层运行在 LangGraph 的 Runtime 之上。每次调用 Agent 时,LangGraph 会创建一个 Runtime 对象,它贯穿整个 Agent 生命周期,是传递上下文的"管道"和"载体"。
Runtime 本质上是一种**依赖注入(Dependency Injection)**机制:在调用时将各种依赖(用户身份、长期记忆、执行信息等)注入到工具和中间件,让代码更可测试、更可复用。
Runtime 承载的五类信息
Runtime 承载五类信息:
| 信息 | 类型 | 说明 |
|---|---|---|
| Context | 静态配置 | 用户 ID、角色、依赖项,调用时传入 |
| Store | 长期记忆 | BaseStore 实例,跨会话持久化 |
| Stream Writer | 流式输出 | 向 custom 流推送消息 |
| Execution Info | 执行信息 | thread_id / task_id / run_id 等 |
| Server Info | 服务端信息 | 仅 LangGraph Server,本地为 None |
用观察中间件把它们打印出来(第8篇介绍过 before_model):
python
@before_model
def inspect_runtime(state, runtime):
print(" [Runtime 五类信息总览]")
print(f" 1. context (静态配置): {runtime.context}")
print(f" 2. store (长期记忆): {type(runtime.store).__name__}")
print(f" 3. stream_writer (流式输出): {type(runtime.stream_writer).__name__ if runtime.stream_writer else None}")
print(f" 4. execution_info (执行信息): {runtime.execution_info}")
print(f" 5. server_info (服务端信息): {runtime.server_info}")
return None
输出:
ini
[Runtime 五类信息总览]
1. context (静态配置): Context(user_name='张三', user_role='customer')
2. store (长期记忆): InMemoryStore
3. stream_writer (流式输出): function
4. execution_info (执行信息): ExecutionInfo(checkpoint_id='...', task_id='...', thread_id=None, ...)
5. server_info (服务端信息): None
Context、Store、Stream Writer 前面篇章都见过(第8/9/11篇),本篇重点介绍后面两个新面孔。
在工具和中间件中访问 Runtime
工具通过 ToolRuntime 访问(第9篇核心),中间件通过钩子的 runtime 参数访问(第8篇的 before_model / after_model 签名是 state, runtime)。这部分在第8/9篇已经介绍过,这里不再重复。
execution_info:执行身份信息
execution_info 包含当前执行的 thread_id、task_id、run_id、checkpoint_id 等,在日志追踪、错误排查、多轮对话管理里很实用。
python
@before_model
def inspect_execution_info(state, runtime):
info = runtime.execution_info
print(f" thread_id: {info.thread_id}")
print(f" task_id: {info.task_id}")
print(f" run_id: {info.run_id}")
print(f" checkpoint_id: {info.checkpoint_id}")
return None
用两个不同 thread_id 跑:
yaml
--- thread_id=session_A ---
thread_id: session_A
task_id: d273b581-9069-4677-f784-de2dd0b2b509
checkpoint_id: 1f189958-14f2-6f84-8000-19728d4e35f6
--- thread_id=session_B ---
thread_id: session_B
task_id: 4a925129-7f4e-b427-14ad-30af7415a11b
checkpoint_id: 1f189958-24fc-6c36-8000-b3b1d93fc7be
注意:
thread_id来自 config(和第10篇 checkpointer 的 thread_id 一致),标识会话;task_id/checkpoint_id每次调用都不同,标识本次执行和检查点;run_id在普通 invoke 时为None(流式或特定场景才生成)。
server_info:服务端信息(仅生产环境)
server_info 只有在 Agent 部署到 LangGraph Server 后才会有值(包含 assistant_id、graph_id、认证用户等)。本地开发环境为 None,这是正常的,生产部署后才会有完整的服务端元数据。
说明:
execution_info和server_info需要deepagents>=0.5.0或langgraph>=1.1.5。我们使用的 langgraph 1.2.9 满足条件。
二、上下文工程框架:三类上下文 × 三种数据源
Runtime 是载体,承载的信息怎么用、在哪里用,就是"上下文工程"(Context Engineering)要解决的问题。
三类上下文
LangChain 把 Agent 的上下文分为三类,每类控制 Agent 循环中的不同环节:
| 上下文类型 | 控制内容 | 典型手段 |
|---|---|---|
| 模型上下文 | 每次模型调用时 LLM 看到什么(提示词、消息、工具、输出格式) | dynamic_prompt、wrap_model_call |
| 工具上下文 | 工具能读什么、能写什么 | ToolRuntime、Command |
| 生命周期上下文 | 模型调用和工具执行之间做什么(摘要、护栏、日志) | 中间件(SummarizationMiddleware 等) |
它们在 Agent 循环中的位置大致如下:
css
用户输入
↓
[生命周期上下文·before_agent] 护栏、日志
↓
[模型上下文] 提示词、消息、工具、模型(LLM 调用)
↓
[工具上下文] 读 Context/State/Store、Command 写回(工具执行)
↓
[生命周期上下文·after_model] 摘要、审核
↓
输出
三种数据源
这三类上下文的数据,都来自三种数据源:
| 数据源 | 作用域 | 典型用途 |
|---|---|---|
| Runtime Context(静态配置) | 会话级 | 用户 ID、角色、依赖项 |
| State(短期记忆) | 会话级 | 消息列表、工具结果、自定义字段 |
| Store(长期记忆) | 跨会话 | 用户偏好、历史数据 |
Context 的定义方式有三种,前面篇章用的是 dataclass,另外两种也支持:
python
# ① dataclass(从第8篇起一直在用这种方式)
@dataclass
class Context:
user_id: str
# ② Pydantic 模型(支持复杂嵌套与校验)
from pydantic import BaseModel
class Context(BaseModel):
user_id: str
# ③ dict(直接传,可省略 context_schema)
agent.invoke(..., context={"user_id": "u001"})
原理:哪些变更会写回 State
这是理解上下文工程的关键:
- 模型上下文的变更 (比如:
dynamic_prompt临时改提示词、wrap_model_call临时裁剪消息),只影响当次模型调用,不写入 State; - 工具上下文和生命周期上下文的修改 (比如:工具通过
Command写状态、摘要中间件替换历史消息),会写入 State,后续所有 Agent 步骤都能看到。
举例:dynamic_prompt 每次调用都会重新生成提示词,但提示词不会存如 State(下次调用重新生成);而工具用 Command(update={"language": "en"}) 写入的字段会存入 State,后续步骤可以读到。这个区别决定了"哪些操作是临时的、哪些是持久的"。
三、模型上下文进阶:动态提示词与消息注入
模型上下文有五个维度:系统提示词、消息列表、工具选择、模型选择、输出格式。其中,工具选择(第9篇动态工具选择)、模型选择(第8篇动态模型选择)、输出格式(第8篇 response_format)前面都介绍过,本篇主要聚焦系统提示词、消息列表两种进阶用法(对比之前用法的补充和扩展)。
系统提示词进阶:dynamic_prompt 结合三种数据源
在第8篇中, dynamic_prompt 只使用了 Context 一个数据源。
实际上,dynamic_prompt 可以同时读取 State(消息数)、Store(用户偏好)、Context(角色),实现"千人千面"的提示词。
python
@dynamic_prompt
def context_aware_prompt(request: ModelRequest) -> str:
# ① State:当前消息数量
msg_count = len(request.messages)
# ② Store:从长期记忆读取用户偏好
prefs = None
if request.runtime.store:
item = request.runtime.store.get(("preferences",), request.runtime.context.user_id)
if item:
prefs = item.value
# ③ Context:当前用户角色
role = request.runtime.context.user_role
base = "你是一个电商客服助手,帮助用户查询订单。"
if role == "admin":
base += "\n你是管理员,可以查看和修改所有用户的订单。"
else:
base += "\n你是普通客户,只能查看自己的订单。"
if prefs and prefs.get("style") == "温和":
base += "\n该用户偏好温和沟通,请使用礼貌体贴的语气。"
if msg_count > 6:
base += "\n当前对话较长,请保持回复简洁,直接给结论。"
return base
两个用户对比:
ini
--- user_id=user_001, role=customer ---
[dynamic_prompt] 角色=customer,偏好={'style': '温和'},消息数=1
回复: 您好,已经查到您的订单信息啦!😊 ...(温和语气,带表情)
--- user_id=admin_001, role=admin ---
[dynamic_prompt] 角色=admin,偏好=None,消息数=1
回复: 以下是订单 ORD-1001 的详细信息:...(管理员式表格,简洁)
同样都是查询订单,customer(温和偏好)回复带表情和体贴语气,admin(无偏好)回复是简洁表格。三种数据源在一次 dynamic_prompt 里都用上了。
说明:
msg_count来自 State,单轮对话时不会触发> 6的简洁提示;多轮长对话时消息累积,才会触发。这就是"模型上下文临时读 State、但不写回"的体现。
消息列表进阶:wrap_model_call,override(messages=)
在第8/9篇中, wrap_model_call 使用了 override(model=) 或 override(tools=)。
wrap_model_call 还可以通过 override(messages=),向消息列表中注入额外的上下文信息。
典型场景:用户问产品参数时,把产品说明书注入消息列表,让模型基于说明书回答(类似轻量 RAG)。
python
@wrap_model_call
def inject_manual(request, handler):
path = request.runtime.context.manual_path
if not path or not os.path.exists(path):
return handler(request)
with open(path, "r", encoding="utf-8") as f:
content = f.read()
injected = (
f"以下是产品说明书内容:\n"
f"========== 说明书开始 ==========\n"
f"{content}\n"
f"========== 说明书结束 ==========\n\n"
f"请严格基于以上说明书回答用户问题。"
)
# 把说明书作为一条额外的 user 消息追加到消息列表
messages = list(request.messages) + [{"role": "user", "content": injected}]
return handler(request.override(messages=messages))
注入 vs 不注入的对比:
markdown
--- 注入说明书 ---
[wrap_model_call] 已将产品说明书(206 字)注入消息列表
回复: 根据说明书,星河 X1 的电池续航为:常规使用 7 天,重度使用 5 天。
防水等级为 5ATM,可以佩戴游泳,但不可用于潜水。
--- 不注入说明书 ---
[wrap_model_call] 未提供说明书路径...消息列表保持不变
回复: 关于星河 X1...由于不同品牌和型号规格可能不同,建议查看说明书...
(模型不认识这个虚构产品,开始编造 1-3 天)
对比可以发现:
-
注入说明书后,模型基于说明书精准回答;
-
不注入时,模型不认识"星河 X1"这个虚构产品,开始编造。
这就是 override(messages=) 的价值:动态把外部内容塞进模型上下文。
同样,这次注入只影响当次调用、不写回 State。
四、工具上下文:一个工具操作三种数据源
工具上下文指工具能读什么、能写什么。
在 Runtime 体系下,一个工具可以同时操作三种数据源:
- 读 Context:
runtime.context,只读 - 读/写 State:读用
runtime.state,写用Command - 读/写 Store:
runtime.store.get/store.put
通过一个"用户操作记录"工具,在一次调用中把三种数据源的读写都演示一遍:
python
@tool
def record_action(action: str, runtime: ToolRuntime[UserContext]) -> Command:
"""记录用户的一次操作(同时读写 Context / State / Store)。"""
# ① 读 Context(静态配置,只读)
user_id = runtime.context.user_id
# ② 读 State(短期记忆):当前会话的访问计数
visit_count = runtime.state.get("visit_count", 0)
# ③ 读 Store(长期记忆):用户偏好
prefs = None
if runtime.store:
item = runtime.store.get(("prefs",), user_id)
if item:
prefs = item.value
new_count = visit_count + 1
# 写 Store:把操作日志写回长期记忆(跨会话保留)
if runtime.store:
runtime.store.put(("action_log",), user_id, {"last_action": action, "total_count": new_count})
# 写 State:通过 Command(update={...}) 更新访问计数,并返回 ToolMessage 让模型看到结果
return Command(update={
"visit_count": new_count,
"messages": [ToolMessage(content=f"已记录用户 {user_id} 的操作[{action}]...", tool_call_id=runtime.tool_call_id)],
})
同一会话调用两次,看 State 累积 + Store 更新:
ini
--- 用户操作:登录 ---
[工具内部] 读 Context.user_id=user_001 | 读 State.visit_count=0 | 读 Store.prefs={'language': '中文'}
[工具内部] → 写 State.visit_count=1 | 写 Store.action_log
--- 用户操作:浏览商品 ---
[工具内部] 读 Context.user_id=user_001 | 读 State.visit_count=1 | 读 Store.prefs={'language': '中文'}
[工具内部] → 写 State.visit_count=2 | 写 Store.action_log
--- 三种数据源的最终状态 ---
State.visit_count(短期记忆,会话内累积): 2
Store.action_log(长期记忆,跨会话保留): {'last_action': '浏览商品操作', 'total_count': 2}
注意:
- 第二次调用,读到的
State.visit_count=1(第一次写入的值)累积到 2,证明Command写入 State + checkpointer 持久化生效; Store.action_log每次更新、跨会话保留(下次新会话也能读到);- Context 是只读的(调用时注入,工具不能改)。
工具读取 Context/State/Store,之前分别在第9篇(ToolRuntime)、第11篇(Store)介绍过,Command 写 State 在第9篇介绍过,这里整合到一个工具中"一个工具同时操作三种数据源"。
生命周期上下文
生命周期上下文,控制"步骤之间的行为"(模型调用前做什么、工具执行后做什么),通过中间件实现。
第10篇的 SummarizationMiddleware对话摘要、第13篇的 Guardrails 安全护栏,都是生命周期上下文的典型场景,这里不重复展开。
五、总结
本篇,把 Agent 的底座 Runtime 运行时与上下文工程,系统梳理了一遍:
- Runtime:贯穿 Agent 生命周期的上下文载体,承载 Context/Store/Stream Writer/Execution Info/Server Info 五类信息,本质是依赖注入机制。
- 上下文工程框架:三类上下文(模型/工具/生命周期)× 三种数据源(Context/State/Store);模型上下文变更不写 State、工具/生命周期变更写入 State。
- 模型上下文进阶 :
dynamic_prompt结合三数据源(State + Store + Context)、wrap_model_call用override(messages=)注入额外上下文。 - 工具上下文 :一个工具同时读 Context/State/Store、用
Command写 State、用store.put写 Store。
一句话概括:Runtime 是 Agent 的上下文载体(依赖注入),上下文工程是"在 Agent 循环的每个环节,决定从哪个数据源读什么、写什么"的系统性框架;模型上下文临时变更、工具/生命周期上下文持久写回。
本篇,是 Agent 系列的补充篇,将第8--13篇里零散出场的 context_schema、ToolRuntime、Command、Store、中间件统一到 Runtime 和上下文工程的框架下。
回头看,整个 Agent 系列的核心其实就是"中间件 + Runtime"这套机制在不同场景下的应用。