【LangChain 1.x】14、补充|Runtime 运行时与上下文工程

摘要:作为 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篇的 ToolRuntimeCommand
  • 第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_idtask_idrun_idcheckpoint_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_idgraph_id、认证用户等)。本地开发环境为 None,这是正常的,生产部署后才会有完整的服务端元数据。

说明:execution_infoserver_info 需要 deepagents>=0.5.0langgraph>=1.1.5。我们使用的 langgraph 1.2.9 满足条件。

二、上下文工程框架:三类上下文 × 三种数据源

Runtime 是载体,承载的信息怎么用、在哪里用,就是"上下文工程"(Context Engineering)要解决的问题。

三类上下文

LangChain 把 Agent 的上下文分为三类,每类控制 Agent 循环中的不同环节:

上下文类型 控制内容 典型手段
模型上下文 每次模型调用时 LLM 看到什么(提示词、消息、工具、输出格式) dynamic_promptwrap_model_call
工具上下文 工具能读什么、能写什么 ToolRuntimeCommand
生命周期上下文 模型调用和工具执行之间做什么(摘要、护栏、日志) 中间件(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_calloverride(messages=) 注入额外上下文。
  • 工具上下文 :一个工具同时读 Context/State/Store、用 Command 写 State、用 store.put 写 Store。

一句话概括:Runtime 是 Agent 的上下文载体(依赖注入),上下文工程是"在 Agent 循环的每个环节,决定从哪个数据源读什么、写什么"的系统性框架;模型上下文临时变更、工具/生命周期上下文持久写回。

本篇,是 Agent 系列的补充篇,将第8--13篇里零散出场的 context_schemaToolRuntimeCommandStore、中间件统一到 Runtime 和上下文工程的框架下。

回头看,整个 Agent 系列的核心其实就是"中间件 + Runtime"这套机制在不同场景下的应用。

相关推荐
meilindehuzi_a2 小时前
Node.js + LangChain.js + Milvus 实战:从 EPUB 入库到《天龙八部》RAG 问答系统
javascript·langchain·node.js
爱奥尼欧4 小时前
【LangChain】6.聊天模型-工具调用
langchain
爱昏羔16 小时前
下篇:从检索到交互 — 物流RAG问答系统与WebUI全实现
python·langchain·agent
_Jimmy_16 小时前
Agent引用数据库知识过时的增量同步方案
人工智能·python·langchain
爱昏羔1 天前
上篇:从PDF到向量库 — 物流行业RAG系统的知识库构建全解析
python·langchain·pdf·agent·rag
可涵不会debug1 天前
【LangChain系列】 零基础入门实战:从环境搭建到 LCEL 链式完整 Demo 详解
运维·服务器·langchain·vibe coding
phltxy1 天前
LangChain_Agent中间件实战
人工智能·python·深度学习·语言模型·中间件·langchain
xian_wwq1 天前
【学习笔记】框架层坍缩——LangChain 们正在被重新定义-14/15
笔记·学习·langchain
梦想三三1 天前
LangChain模型调用与多轮对话完整实战
阿里云·langchain·大模型·api