Skip to content

🧭 9.10 上下文工程:Context Engineering 与动态上下文注入 ​

“Agent 失败的十有八九,不是模型不够聪明,而是你在错误的时间,把错误的上下文递给了它。”
在 LangChain 1.4 中,官方把“给模型正确上下文”这门手艺上升为一等公民——上下文工程(Context Engineering)。本节将带你搞懂 Agent 的三类上下文、三种数据源,以及如何用中间件在运行时动态注入。


💡 先看一个直观例子:导演的剧本、道具与幕间调度 ​

1. 为什么 Agent 会失败? ​

Agent 跑不通,通常只有两个原因:

  1. 底层模型能力不够(换更强的模型);
  2. 传给模型的“上下文”不对(信息缺失、工具太多太杂、格式混乱)——而这,恰恰占了大多数。

上下文工程就是:在正确的时机,把正确的信息、工具、指令,以正确的格式递进模型。这是 AI 工程师的头号工作。

2. 生活比喻:一场舞台剧 ​

把一次 Agent 运行想象成一出舞台剧:

  • Model Context(模型上下文) = 剧本与台词。演员(模型)这一场戏能看到的全部内容:角色设定(System Prompt)、前面的台词(消息历史)、可用的道具清单(工具)、要穿的衣服(response format)。它是瞬时的——戏演完就翻篇;
  • Tool Context(工具上下文) = 道具与舞台布景。工具能读取和写回的东西:当前剧情(State)、跨剧集的人物档案(Store)、演出配置(Runtime Context)。它是持久的;
  • Life-cycle Context(生命周期上下文) = 幕间调度。两场戏之间发生了什么:摘掉多余台词(摘要)、安全检查(护栏)、记录开销(日志)。它也是持久的。

2. 生活比喻:导演的剧本、道具与幕间调度


🏛️ 三类上下文 × 三种数据源 ​

1. 你能控制什么 ​

上下文类型你控制什么瞬时 / 持久
Model Context进入模型调用的内容:指令、消息历史、工具清单、模型选择、输出格式瞬时(单次调用)
Tool Context工具能访问与产出什么:对 State / Store / Runtime 的读写持久(跨调用)
Life-cycle Context模型与工具调用之间发生什么:摘要、护栏、日志、重试持久(跨调用)

2. 三种数据源(呼应 9.6) ​

数据源作用域典型例子读取入口
Runtime Context会话级静态配置user_id、API Key、权限等级request.runtime.context(配合 context_schema)
State会话级短期记忆消息列表、认证状态、上传文件request.state
Store跨会话长期记忆用户偏好、画像、历史数据request.runtime.store

🔑 一句话:State 记“这一轮聊了什么”,Store 记“这个人长期什么样”,Runtime Context 记“这次请求的固定配置”。中间件是让这三者动起来的引擎。


💻 核心实操:三类动态上下文注入 ​

以下代码均来自官方 context-engineering 指南的模式,完整可运行脚本见 code/s10_context_engineering.py。

1. 动态 System Prompt:按会话长度自适应 ​

用 @dynamic_prompt 装饰器,每次模型调用前动态生成系统提示:

python
# code/s10_context_engineering.py
from langchain.agents import create_agent
from langchain.agents.middleware import dynamic_prompt, ModelRequest

@dynamic_prompt
def state_aware_prompt(request: ModelRequest) -> str:
    message_count = len(request.messages)          # request.messages = request.state["messages"]
    base = "你是一位贴心助手。"
    if message_count > 10:
        base += "\n这是一段很长的对话,请尽量简洁回答。"
    return base

agent = create_agent(model=llm, tools=tools, middleware=[state_aware_prompt])

2. 动态工具选择:按认证状态/会话阶段裁剪工具 ​

工具不是越多越好——太多会淹没模型。用 wrap_model_call 在每次调用前按需裁剪:

python
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from typing import Callable

@wrap_model_call
def state_based_tools(request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse:
    state = request.state
    is_authenticated = state.get("authenticated", False)
    if not is_authenticated:
        # 未认证:只开放 public_ 前缀的安全工具
        request = request.override(tools=[t for t in request.tools if t.name.startswith("public_")])
    return handler(request)

agent = create_agent(model=llm, tools=[public_search, private_search, advanced_search],
                     middleware=[state_based_tools])

3. 长期画像注入:Store + Runtime Context 双剑合璧 ​

把 9.6 的长期记忆用起来——每次调用前读用户画像,动态改写系统提示:

python
from dataclasses import dataclass
from langchain.agents.middleware import dynamic_prompt, ModelRequest
from langgraph.store.memory import InMemoryStore

@dataclass
class Context:
    user_id: str                                  # 运行期静态配置

@dynamic_prompt
def store_aware_prompt(request: ModelRequest) -> str:
    user_id = request.runtime.context.user_id     # 读 Runtime Context
    prefs = request.runtime.store.get(("preferences",), user_id)   # 读 Store 长期记忆
    base = "你是一位贴心助手。"
    if prefs:
        style = prefs.value.get("communication_style", "balanced")
        base += f"\n用户偏好 {style} 风格的回复。"
    return base

agent = create_agent(
    model=llm,
    tools=tools,
    middleware=[store_aware_prompt],
    context_schema=Context,        # 声明运行期上下文的 Schema
    store=InMemoryStore(),         # 挂载长期记忆 Store
)

3.5 三个演示的真实调用效果(Gradio 实验台 9.10 页可交互) ​

code/s10_context_engineering.py 的三个 demo_*() 函数已全部升级为真实调用模型的双场景对比,点击实验台按钮即可观察:

演示场景 A场景 B观察点
动态 System Prompt短对话(1 条消息)长对话(13 条消息)B 场景人设自动追加「请简洁回答」,回复明显变短
动态工具选择authenticated=Falseauthenticated=TrueA 场景模型只能调用 public_search(tool_calls 可见),敏感工具被真实裁掉
Store 画像注入老用户(简洁直接画像)新用户(无画像)同一个问题,A 回复极简、B 回复详细科普

⚠️ 踩坑实录:演示 3 必须用 agent.invoke(..., context=Context(user_id="user-123")) 显式传 context—— 声明了 context_schema 不代表运行时会自动构造,漏传时 request.runtime.context 为 None, request.runtime.context.user_id 会直接抛 AttributeError。

4. 瞬时 vs 持久更新:一条重要分界线 ​

  • wrap_model_call 里的 request.override(...) = 瞬时更新——只改这一次模型调用看到的内容,不写回 State;
  • before_model / after_model 返回 dict = 持久更新——通过 graph reducer 真正合并进 State。

💡 工程直觉:想让模型“临时多看一页资料”用 override;想让 Agent“记住这件事以后一直知道”用 State 更新(返回 dict)。


📚 权威官方资料直达 ​


临时资料、运行状态和长期事实要分开放 ​

当次请求需要的文档适合临时注入,任务执行中的步骤与结果适合放在 State,跨会话且相对稳定的信息才适合写入 Store。把所有信息都塞进提示词,会让来源、更新时间和删除方式变得模糊。

动态上下文应遵循最少够用原则。根据当前步骤选择工具和资料,并记录注入原因;来自外部文档的内容要保留来源与信任等级。上下文发生冲突时,应优先使用可验证且更新的事实,必要时暂停并请求业务判断。

🎯 本节小结与思考 ​

  1. 核心收获:理解了 Agent 失败的根因——上下文不对;掌握了 Model / Tool / Life-cycle 三类上下文与 Runtime Context / State / Store 三种数据源,以及用 @dynamic_prompt、wrap_model_call + request.override() 实现动态 Prompt、动态工具选择与长期画像注入。
  2. 下一步探索:动态注入靠的是中间件这双手。那么中间件到底有哪些钩子?如何自己动手写一个生产级中间件?下一节我们深入学习 9.11 自定义中间件与生命周期钩子。