《Deep Agents 实战笔记》第 7 篇 | 基于 deepagents (Python) 官方文档 原文:https://docs.langchain.com/oss/python/deepagents/context-engineering
上一篇我们让子代理并发跑了起来。但你可能很快会遇到新的问题:任务一长——比如让 Agent 调研十个方向、连续工作半小时——它会逐渐"变笨":早期读过的资料想不起来、明明刚搜过的结论又去重搜一遍,最后干脆报一个上下文超限的错,前功尽弃。
这不是模型的锅,是你的上下文工程没做。官方文档对上下文工程的定义很精炼:以正确的格式,在正确的时机,提供正确的信息和工具。 好消息是,Deep Agents 把这件事的大部分脏活内置了。这一篇我们把它的四层上下文来源和两台"自动压缩机"拆开看。
一、上下文从哪来:四层来源
先建立一个心智模型。一个深度代理的上下文窗口里,信息来自四个层次,每层的控制方式和生命周期完全不同:
| 层次 | 你能控制什么 | 生命周期 |
|---|---|---|
| 系统提示词 | 角色定义、内置指引、记忆、技能摘要 | 静态,每次运行都会生效 |
| 运行时上下文 | 调用时传入的配置:用户元数据、API 密钥、连接 | 每次运行有效,会传播到子代理 |
| 文件系统 | 大结果的存与取、子代理协作黑板 | 按需读写 |
| 长期记忆 | 跨对话持久化的偏好与知识 | 跨线程持久 |
flowchart TB
subgraph L1["第一层:系统提示词"]
A1["自定义 system_prompt"]
A2["内置指引与工具提示词"]
A3["记忆 AGENTS.md 始终加载"]
A4["技能列表 仅 frontmatter 摘要"]
end
subgraph L2["第二层:运行时上下文"]
B1["用户元数据 / API 密钥"]
B2["传播到所有子代理"]
end
subgraph L3["第三层:文件系统"]
C1["大结果卸载落盘"]
C2["read_file / grep 按需取回"]
end
subgraph L4["第四层:记忆"]
D1["/memories/ 路由到 Store"]
end
L1 --> WIN["LLM 上下文窗口"]
L2 -.->|"仅当工具或中间件读取时"| WIN
L3 -.->|"需要时才拉取"| WIN
L4 --> WIN
AUTO["自动机制:卸载 + 总结压缩"] -.->|"保持窗口不超限"| WIN
style WIN fill:#eef4ff,color:#1a3a8b,stroke:#4a72d9
style L1 fill:#f0f7f0,color:#1d4a1d,stroke:#4a9e4a
style L2 fill:#fbf3e6,color:#6b4a12,stroke:#c99a3d
style L3 fill:#f4effb,color:#4a2170,stroke:#8b52c9
style L4 fill:#fdeef0,color:#701d2a,stroke:#c94a5e
正在渲染图表…
注意虚线和实线的区别:系统提示词和记忆是始终在场的,所以它们的内容必须精挑细选;运行时上下文和文件系统是按需进场的,适合放那些"不是每轮都需要"的东西。很多人把用户 ID、密钥这类信息硬塞进系统提示词,这就是层次用错了——下面逐层说。
二、系统提示词:拼出来的,不是写出来的
你以为 system_prompt 参数就是全部?它只是拼图的第一块。深度代理最终发给模型的系统消息,由八部分按序组装:
- 自定义
system_prompt(如果提供) - 基础代理提示词(内置的文件系统工具和子代理使用指引)
- 记忆提示词:
AGENTS.md+ 使用准则(仅在提供memory时) - 技能提示词:技能位置 + 各技能 frontmatter 摘要(仅在提供技能时)
- 虚拟文件系统提示词
- 子代理提示词:
task工具的用法 - 自定义中间件追加的提示词
- 人机协同提示词(设置
interrupt_on时)
两个实操要点。第一,system_prompt 是静态的。 如果你的提示词依赖运行时信息(比如"当前用户是管理员还是只读访客"),要用 @dynamic_prompt 中间件来构建感知上下文的指令;但如果只是工具需要读上下文,不用加中间件——工具会直接收到 ToolRuntime 对象。第二,没用的内置工具请主动排除。 未使用的内置工具每一轮都会发送完整 schema,用 excluded_tools 把只读代理不该碰的 write_file、execute 之类移除,能缩小整个运行过程的基础提示词体积。
还有一层容易被忽视的"提示词":你自己工具的描述。工具的名称、描述和参数说明是模型决定何时调用、怎么调的唯一依据。描述里要写清楚什么场景该用这个工具,每个参数是干什么的。
三、运行时上下文:不该进提示词的东西
用户 ID、API 密钥、功能开关——这些是"每次运行的配置",不是"给模型看的指令"。Deep Agents 的做法是用 context_schema 定义结构,通过 invoke 的 context 参数传入,工具内部从注入的 ToolRuntime 里读取:
from dataclasses import dataclass
from deepagents import create_deep_agent
from langchain.tools import ToolRuntime, tool
@dataclass
class Context:
user_id: str
api_key: str
@tool
def fetch_user_data(query: str, runtime: ToolRuntime[Context]) -> str:
"""Fetch data for the current user."""
user_id = runtime.context.user_id
return f"Data for user {user_id}: {query}"
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
tools=[fetch_user_data],
context_schema=Context,
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "Get my recent activity"}]},
context=Context(user_id="user-123", api_key="sk-..."),
)
关键设计:运行时上下文会自动传播到所有子代理,子代理运行时收到与父代理相同的配置。这意味着密钥只需要传一次,深层的子代理照样能调你的内部 API。
顺带一提边界:如果你的数据需要"跨越整个代理生命周期并经受住 checkpoint"(比如跨调用的计数器、审计标志),那该用 state_schema 自定义状态(需要 deepagents>=0.6.6);不可变的每次运行输入,才用运行时上下文。一个是可变的工作状态,一个是不可变的请求配置,别混。
四、卸载:大结果落盘,按需读取
前两层解决"放什么进来",接下来两台自动机制解决"窗口装不下怎么办"。第一台是卸载(offload)。
深度代理使用内置文件系统工具自动卸载内容:当工具调用的输入或结果超过 token 阈值(默认 20,000)时触发。具体分两种情况:
- 工具调用结果超过 20,000 token:响应被卸载到所配置的 Backend,上下文中用一个文件路径引用 + 前 10 行预览替代原始结果。代理之后可以按需重新读取或搜索这些内容。
- 工具调用输入超过 20,000 token:文件写入和编辑操作会在对话历史里留下包含完整文件内容的工具调用,而内容已经持久化到文件系统了,通常是冗余的。当会话上下文超过模型可用窗口的 85% 时,较旧的工具调用会被截断,替换为指向磁盘上文件的指针。
flowchart TB
TC["工具调用返回结果"] --> CHK{"超过 20000 token?"}
CHK -->|"否"| KEEP["原样留在上下文"]
CHK -->|"是"| OFF["卸载:写入 Backend"]
OFF --> REF["上下文替换为<br/>文件路径 + 前 10 行预览"]
REF --> NEED{"之后需要细节?"}
NEED -->|"是"| REREAD["read_file / grep 按需读回"]
NEED -->|"否"| GO["继续任务,窗口保持轻量"]
style OFF fill:#f4effb,color:#4a2170,stroke:#8b52c9
style REF fill:#f4effb,color:#4a2170,stroke:#8b52c9
正在渲染图表…
这套机制的美妙之处在于对代码完全透明——你没有写任何一行"保存结果"的逻辑,create_deep_agent 默认就带。之前第 2 篇建议搜索工具保留 include_raw_content 参数,底气就在这里:两万字的网页原文也不怕,harness 会替你落盘。
五、总结压缩:85% 阈值上的自动手术
卸载能处理"单条大结果",但对话本身的增长——几十轮工具调用、推理过程——靠卸载救不了。第二台机制是总结压缩(summarization),同样内置于每次 create_deep_agent 调用,无需添加任何中间件。
触发逻辑和动作是这样的:
flowchart TB
START["每一轮模型调用"] --> MEASURE{"上下文超过模型窗口的 85%?"}
MEASURE -->|"否"| RUN["正常调用"]
MEASURE -->|"是"| CANOFF{"还有可卸载的内容?"}
CANOFF -->|"是"| OFFLOAD["先卸载"] --> RUN
CANOFF -->|"否"| SUMM["触发总结压缩"]
RUN -.->|"抛出 ContextOverflowError"| SUMM
SUMM --> S1["LLM 生成结构化摘要<br/>替换完整对话历史"]
SUMM --> S2["原始消息文本渲染<br/>写入文件系统留存"]
S1 --> RETRY["用摘要 + 保留的近期消息重试"]
S2 --> RETRY
style SUMM fill:#fdeef0,color:#701d2a,stroke:#c94a5e
正在渲染图表…
注意它是双管齐下的:一部分是"上下文内摘要"——LLM 生成一份结构化摘要,涵盖会话意图、已创建的产出物和后续步骤,替换掉完整历史;另一部分是"文件系统留存"——原始对话消息的文本渲染写入文件,作为规范记录。前者保证 Agent 记得目标和进度,后者保证细节随时可以搜回来。这是我认为 Deep Agents 上下文管理里最聪明的一笔:摘要有损但轻,留档无损但沉在磁盘上,两者配合才敢放心压缩。
几个关键参数(来自官方文档):
- 达到模型
max_input_tokens的 85% 时触发(来自模型 profile) - 保留 10% 的 token 作为近期上下文
- 模型 profile 不可用时,回退为 170,000 token 触发阈值 / 保留 6 条消息
- 任何模型调用抛出标准
ContextOverflowError时,立即回退到总结压缩并重试——这是兜底保险丝
除了自动触发,你还可以给 Agent 一个按需压缩工具,让它在任务间隙主动"整理桌面",不必等到 85% 阈值:
from deepagents import create_deep_agent
from deepagents.backends import StateBackend
from deepagents.middleware.summarization import create_summarization_tool_middleware
backend = StateBackend # 使用默认 backend 时
model = "google_genai:gemini-3.6-flash"
agent = create_deep_agent(
model=model,
middleware=[
create_summarization_tool_middleware(model, backend),
],
)
它不会禁用自动总结压缩,两者共享同一个引擎和状态。另外,流式输出时总结压缩步骤生成的 token 会混进来,用元数据过滤掉即可:
for token, metadata in agent.stream(
{"messages": [...]},
stream_mode="messages",
):
if metadata.get("lc_source") == "summarization":
continue
else:
...
六、上下文工程 Do & Don't
机制讲完了,落到实践。以下对照全部来自官方文档的最佳实践,我按"该做 / 别做"重排:
| Do | Don't |
|---|---|
| 记忆保持精简,只放始终相关的约定;任务专属能力交给聚焦的技能 | 把所有指令塞进系统提示词——与任务无关的信息每轮都在烧 token |
| 把多步骤、输出量大的工作委派给子代理,主代理只收结果摘要 | 让主代理亲自跑重负载工具,被搜索原始结果灌满窗口 |
在子代理的 system_prompt 里写明"返回摘要而非原始数据" | 对子代理的冗长输出放任不管,委派反而变成污染源 |
大输出持久化到文件,需要时用 read_file 和 grep 拉取片段 | 把两万字工具结果原样留在消息历史里"以防万一" |
在系统提示词里告诉代理 /memories/ 中存放着什么、如何使用 | 记忆目录全靠 Agent 自己猜结构 |
用 context 传递用户元数据和 API 密钥 | 把密钥、用户 ID 写死在系统提示词里 |
一句话总纲:静态的、每轮都用的,进系统提示词;动态的、一次性的,进运行时上下文;大的、偶尔回看的,进文件系统;永久的、跨会话的,进记忆。 四个抽屉各归其位,长任务自然跑得动。
七、本篇踩坑点
- 别把卸载当成省钱开关:卸载和总结压缩解决的是"不崩",不是"便宜"。结果落盘后按需
read_file依然要花 token,只是把"一次性全量"变成"按需增量"。 - 模型 profile 决定阈值:85% 的触发线来自模型的
max_input_tokens,接小窗口模型时压缩会更频繁地触发,别误以为是 bug。 - 子代理描述偷懒会反噬上下文:子代理返回冗长原始数据,等于把隔离的上下文又倒回主代理窗口——这是文档里反复强调的头号反面案例。
- 流式输出里的"杂音":看到与任务无关的 token 涌出,先检查是不是总结压缩在生成摘要,用
lc_source元数据过滤。
上下文管好了,Agent 能"活着跑完"长任务。但它每次跑完就忘——怎么让它记住你的偏好、沉淀踩过的坑、学会新的工作流?下一篇我们讲 Deep Agents 的另外两个一等问题:记忆与技能——你会发现它们竟然都是 Markdown 文件。
参考资料:
- Deep Agents 上下文工程(官方文档,本文主要素材):https://docs.langchain.com/oss/python/deepagents/context-engineering
- Deep Agents 子代理:https://docs.langchain.com/oss/python/deepagents/subagents
- Deep Agents 长期记忆:https://docs.langchain.com/oss/python/deepagents/memory