主题
🧭 9.10 上下文工程:Context Engineering 与动态上下文注入
“Agent 失败的十有八九,不是模型不够聪明,而是你在错误的时间,把错误的上下文递给了它。”
在 LangChain 1.4 中,官方把“给模型正确上下文”这门手艺上升为一等公民——上下文工程(Context Engineering)。本节将带你搞懂 Agent 的三类上下文、三种数据源,以及如何用中间件在运行时动态注入。
💡 先看一个直观例子:导演的剧本、道具与幕间调度
1. 为什么 Agent 会失败?
Agent 跑不通,通常只有两个原因:
- 底层模型能力不够(换更强的模型);
- 传给模型的“上下文”不对(信息缺失、工具太多太杂、格式混乱)——而这,恰恰占了大多数。
上下文工程就是:在正确的时机,把正确的信息、工具、指令,以正确的格式递进模型。这是 AI 工程师的头号工作。
2. 生活比喻:一场舞台剧
把一次 Agent 运行想象成一出舞台剧:
- Model Context(模型上下文) = 剧本与台词。演员(模型)这一场戏能看到的全部内容:角色设定(System Prompt)、前面的台词(消息历史)、可用的道具清单(工具)、要穿的衣服(response format)。它是瞬时的——戏演完就翻篇;
- Tool Context(工具上下文) = 道具与舞台布景。工具能读取和写回的东西:当前剧情(State)、跨剧集的人物档案(Store)、演出配置(Runtime Context)。它是持久的;
- Life-cycle Context(生命周期上下文) = 幕间调度。两场戏之间发生了什么:摘掉多余台词(摘要)、安全检查(护栏)、记录开销(日志)。它也是持久的。
🏛️ 三类上下文 × 三种数据源
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=False | authenticated=True | A 场景模型只能调用 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)。
📚 权威官方资料直达
- 🔗 上下文工程官方指南(本节核心):Context Engineering in Agents
- 🔗 上下文工程概念总览:Context Concepts Overview
- 🔗 中间件机制(上下文工程的底层引擎):LangChain Middleware
- 🔗 长期记忆 Store 官方指南:LangGraph Store
临时资料、运行状态和长期事实要分开放
当次请求需要的文档适合临时注入,任务执行中的步骤与结果适合放在 State,跨会话且相对稳定的信息才适合写入 Store。把所有信息都塞进提示词,会让来源、更新时间和删除方式变得模糊。
动态上下文应遵循最少够用原则。根据当前步骤选择工具和资料,并记录注入原因;来自外部文档的内容要保留来源与信任等级。上下文发生冲突时,应优先使用可验证且更新的事实,必要时暂停并请求业务判断。
🎯 本节小结与思考
- 核心收获:理解了 Agent 失败的根因——上下文不对;掌握了 Model / Tool / Life-cycle 三类上下文与 Runtime Context / State / Store 三种数据源,以及用
@dynamic_prompt、wrap_model_call + request.override()实现动态 Prompt、动态工具选择与长期画像注入。 - 下一步探索:动态注入靠的是中间件这双手。那么中间件到底有哪些钩子?如何自己动手写一个生产级中间件?下一节我们深入学习 9.11 自定义中间件与生命周期钩子。