夜雨聆风学习资料网

ARTICLE · 1316071

长任务上下文必爆:卸载、压缩、隔离三板斧怎么选(Deep Agents 10/16)

长任务上下文必爆:卸载、压缩、隔离三板斧怎么选(Deep Agents 10/16)

长任务上下文必爆:卸载、压缩、隔离三板斧怎么选(Deep Agents 10/16)

长任务里上下文必然爆,这不是调参能躲开的。Deep Agents 内置了三板斧:卸载到文件、压缩总结、隔离到子 agent。这一课讲清楚每一招的触发条件、代价,以及症状对应该用哪一招——最后给一张诊断表。

一、为什么必然爆

agent 的上下文是单调增长的:每一轮的 AIMessage + ToolMessage 都追加进消息列表,没有任何东西会自动消失。而工具返回值的大小你控制不了——一次网页抓取可能是 200 token,也可能是 40000。

撞上之后的三种死法:

死法
表现
硬报错
模型直接返回 context length exceeded,任务失败,前面的 token 钱白花
软失忆
没报错,但模型"忘了"最初的目标,开始重复已经做过的步骤,或者答非所问
钱包爆炸
每一轮都把 180k token 重新发一遍。第 30 轮的单次调用成本是第 3 轮的十几倍

二、三板斧:机制和代价

做什么

  • 卸载 Offloading:大块内容写进 backend,上下文里换成一个路径
  • 压缩 Summarization:老消息交给 LLM 总结成一段话,替换掉原文
  • 隔离 Subagents:脏活派给子 agent,主 agent 只收最终报告

触发

  • 卸载 Offloading:自动。工具入参或返回值超 20000 token
  • 压缩 Summarization:自动。上下文到窗口的 85%
  • 隔离 Subagents:手动。模型自己决定调 task()

信息损失

  • 卸载 Offloading:零。原文还在 backend 里,随时 read_file / grep 捞回来
  • 压缩 Summarization:有损且不可逆(上下文里)。原文写去了文件,但模型未必想得起来去读
  • 隔离 Subagents:有损。中间过程主 agent 永远看不到

代价

  • 卸载 Offloading:要用回内容就多一次工具往返
  • 压缩 Summarization:一次额外的 LLM 调用(花钱、耗时);细节丢失导致后续判断变差
  • 隔离 Subagents:每个子 agent 都要重付一遍 system prompt;并行调用放大延迟和 token

你能控制吗

  • 卸载 Offloading:能:FilesystemMiddleware(tool_token_limit_before_evict=...)
  • 压缩 Summarization:能:换 SummarizationMiddleware 的 trigger / keep
  • 隔离 Subagents:能:subagents=[...] 的 description 和 system_prompt

三、卸载:20000 token 那条线

本地 0.7.3 反射 FilesystemMiddleware.__init__,阈值是硬参数:

FilesystemMiddleware(    backend=...,    tool_token_limit_before_evict=20000,          # 工具返回值超这个数就卸载    human_message_token_limit_before_evict=50000,  # 用户消息超这个数才卸载(宽松得多)    grep_max_count=1000,    max_execute_timeout=3600,)

两种卸载方向,别搞混:

卸载工具入参
卸载工具返回值
典型场景
write_file / edit_file——完整文件内容留在了 tool_call 参数里
web_search / 自定义工具返回一大坨
为什么冗余
内容已经存进文件系统了,参数里那份是重复的
本来就该存文件
替换成什么
指向文件的一个指针(截断老的 tool_call)
文件路径 + 前 10 行预览
什么时候动手
上下文过 85% 窗口时才回头截断历史
返回值超 20000 token,当场

要点:卸载是零信息损失的。模型随时能 read_file 读回全文、grep 搜关键词。这是三招里唯一"白赚"的——代价只是多一次工具往返。

【最省的一招其实是砍工具】

压缩和卸载都是"事后补救"。真正从每一轮开始就省的,是减少工具 schema——工具描述是每次请求都要发的固定开销。用不到的内置工具直接砍掉(D7 的 HarnessProfile(excluded_tools={...})),比什么压缩都直接。

四、压缩:85% / 保留 10%

每次 create_deep_agent 都自动带 SummarizationMiddleware,不用你挂。它做两件事:

  • 上下文内摘要:LLM 生成一段结构化摘要(本轮意图、已产出的文件、下一步),替换掉工作记忆里的历史消息。
  • 文件系统留档:原始消息渲染成文本,写到 /conversation_history/{thread_id}.md。每次压缩追加一节,形成一份完整流水账。消息里的 base64 媒体单独存到 conversation_history/media/,用路径引用,保证这个 md 文件是纯文本的。

默认参数(本地 deepagents/middleware/summarization.py 反射):

条件
trigger(何时压)
keep(留多少)
模型有 profile(知道 max_input_tokens)
("fraction", 0.85)
("fraction", 0.10)
模型没有 profile
("tokens", 170000)
("messages", 6)

另外:任何一次模型调用抛 ContextOverflowError,会立刻回退到"摘要 + 保留最近消息"再重试一次。这是最后一层保险。

【DeepSeek 走的是没有 profile 的那条路】

本地实测:init_chat_model("deepseek-v4-pro", model_provider="openai", ...) 建出来的模型,model.profile 是 None。

后果是压缩用保守的固定值:170000 token 触发、只保留最近 6 条消息。这两个数跟 DeepSeek 的真实窗口没关系。如果你的模型窗口小于 170k,压缩可能永远来不及触发——先撞的是硬报错。这种情况必须自己显式配:

from deepagents.middleware.summarization import SummarizationMiddlewaresumm = SummarizationMiddleware(model=model, backend=backend,                               trigger=("tokens", 50000),   # 按你的真实窗口算                               keep=("messages", 10))agent = create_deep_agent(model=model, backend=backend, middleware=[summ])

官网文档只写了"85% / 10% / 回退 170000 / 6 条",没提这套回退在非官方 provider 上是常态而不是例外。用 DeepSeek、本地模型或任何走 openai 兼容接口的模型时,先 print(model.profile) 确认一下。

想让模型主动压缩(比如两个子任务之间),可以给它一个 compact_conversation 工具:

from deepagents.middleware.summarization import create_summarization_tool_middlewareagent = create_deep_agent(    model=model,    middleware=[create_summarization_tool_middleware(model, backend)],)

它不会关掉自动压缩,两者共用同一套摘要引擎和 state。价值在于"在干净的边界上压"——任务 A 做完、任务 B 开始前压,比在任务中间被 85% 阈值打断要少丢东西。

【关键输入 / 输出】

输入(三个可调旋钮,都不是必填):

  • 卸载阈值:FilesystemMiddleware(tool_token_limit_before_evict=20000)
  • 压缩阈值:SummarizationMiddleware(trigger=("fraction", 0.85), keep=("fraction", 0.10))
  • 隔离:subagents=[{"name":..., "description":..., "system_prompt":...}]

怎么验证卸载真的发生了(这是唯一的硬证据,别靠感觉):

result = agent.invoke({"messages": [...]})# 1. 虚拟文件系统里有东西 —— 用默认 StateBackend 时它在返回值里for path in sorted(result.get("files", {})):    print(path)#   /retrieved/c3ec5240/chunk_1.md#   /conversation_history/thread-1.md      ← 这个出现 = 已经压缩过了# 2. 消息列表里没有对应的正文for msg in result["messages"]:    for tc in getattr(msg, "tool_calls", None) or []:        print(tc["name"], str(tc["args"])[:90])#   task {'description': '...at /retrieved/c3ec5240/chunk_1.md...'}   ← 只有路径,没正文

压缩产生的 token 会混进流式输出。用 stream_mode="messages" 时靠 metadata 过滤掉:

for chunk in agent.stream({"messages": [...]}, stream_mode="messages", version="v2"):    token, metadata = chunk["data"]    if metadata.get("lc_source") == "summarization":        continue          # 不然用户会看到一段莫名其妙的会话总结

五、隔离:唯一需要你设计的那一招

卸载和压缩都是自动的,隔离不是——它取决于你怎么定义子 agent、模型愿不愿意派活。三条实践:

  1. 子 agent 的 system_prompt 里明确要求返回摘要。不写的话,子 agent 会把它读到的一大坨原样吐回来,隔离就失效了: research_subagent = {     "name": "researcher",     "description": "Conducts research on a topic",     "system_prompt": """You are a research assistant.     IMPORTANT: Return only the essential summary (under 500 words).     Do NOT include raw search results or detailed tool outputs.""",     "tools": [web_search], }
  2. 大数据走文件,不走返回值。让子 agent 把结果 write_file,只在回复里说"存到了 /research/x.md"。主 agent 需要细节时自己去读。这是隔离 + 卸载的组合,也是 D6 那套 RAG 的完整形态。
  3. description 决定派不派得出去。写得含糊,模型就自己硬扛,隔离一次都不会发生。这条规律从 L11 一直贯穿到这里。

六、诊断表:症状 → 该用哪招

症状
根因
该用哪招
某一次工具调用直接把上下文顶满
单个返回值过大
卸载。调低 tool_token_limit_before_evict,或让工具自己 upload_files + return 路径
token 缓慢爬升,第 20 步左右爆
很多个中等大小的返回值累积
隔离。把这类工具挪到子 agent 里,主 agent 只收摘要
模型跑到第 15 步忘了最初要干嘛
历史被压缩了,摘要没保住目标
挂 TodoListMiddleware()——todo 清单一直在上下文里,是最便宜的目标锚点
模型重复做已经做过的事
压缩把"已完成"的证据摘没了
让 agent 把进度写进文件(/progress.md),并在 prompt 里告诉它每步先读一遍
压缩之后答案质量明显下降
keep 太少,最近上下文不够
调大 keep;或用 compact_conversation 在任务边界主动压,别等 85%
成本比预期高好几倍
每轮重发全量上下文 + 摘要本身的 LLM 调用
隔离(子 agent 上下文短)+ 砍工具 schema。压缩省不了钱,它花钱
用户看到一段莫名其妙的"会话总结"
摘要 token 混进了流式输出
按 metadata["lc_source"] == "summarization" 过滤
子 agent 返回一大坨原始数据
子 agent prompt 没约束输出
在它的 system_prompt 里写死字数上限 + "不要贴原始工具输出"
压缩从来没触发过,直接硬报错
模型没 profile,用了 170000 的保守回退值,比真实窗口大
显式传 SummarizationMiddleware(trigger=("tokens", N))

七、和 LangChain 层上下文工程的差异

LangChain 层(L9 / L10)
Deep Agents 层
关注点
数据放在哪条通道:state / context / store
数据放不下了怎么办
默认行为
什么都不做,你自己挂中间件
卸载和压缩默认就开着
存储抽象
store(键值 + 命名空间)
backend(文件系统:路径、ls、grep、read_file)
为什么用文件而不用 KV
—
模型天生会用文件系统这套词汇(路径、目录、grep),不用额外教
跨会话持久化
checkpointer + store
CompositeBackend 把 /memories/ 路由到 StoreBackend,其余留在 state

一句话:LangChain 层给你通道,Deep Agents 层给你策略。Deep Agents 的 backend 底下仍然是 LangGraph 的 state / store,只是包了一层文件系统的外壳——因为"路径"这个概念模型用得比"命名空间键"顺手得多。

【别把这三招当成万能药】

  • 压缩是有损的,而且损在哪你不知道。摘要由 LLM 生成,它认为不重要的细节就没了。涉及金额、ID、时间这类不能错的信息,必须写进文件,别指望摘要保住。
  • 压缩会花钱和耗时。它是一次完整的 LLM 调用,用长上下文当输入。一个跑 100 步的任务可能压缩三四次。
  • 卸载不等于模型会去读。路径摆在那儿,模型可能就是不 read_file。prompt 里要明确说"细节在 X,需要时读它"。
  • 本地 0.7.3:write_todos 是 opt-in 的。从 v0.7 开始 TodoListMiddleware 不在默认栈里(反射 create_deep_agent 源码确认)。想用规划工具锚定长任务的目标,得自己 middleware=[TodoListMiddleware()]。D1 里"规划自动内置"的说法是 v0.7 之前的行为。

八、什么时候不该用

场景
为什么
该怎么做
任务只有 5–10 步
三招一个都不会触发,白白多背一堆中间件
直接 create_agent,见 L2
需要完整对话记录做审计
压缩会把历史替换掉(虽然留档到文件,但不在上下文里)
关掉压缩,靠隔离和卸载控容量;或者用 LangSmith 追踪
答案必须逐字引用
摘要和子 agent 报告都是有损的
只用卸载(零损失),需要原文就 read_file 精确读
低延迟对话
压缩是一次额外 LLM 调用,会造成明显卡顿
调高 trigger 让它尽量不触发;把长活儿挪到异步
想靠压缩省钱
压缩本身就是一次长上下文调用,它是成本项不是节流项
省钱靠隔离(子 agent 上下文短)和砍工具 schema

小结

  • 长任务的上下文单调增长,三种死法:硬报错、软失忆、成本爆炸。短任务碰不到,长任务躲不掉。
  • 卸载(超 20000 token 自动,零信息损失)、压缩(85% 自动,有损且花钱)、隔离(手动设计,从源头避免)。前两招是兜底,第三招是设计。
  • 压缩默认 85% 触发 / 保留 10%;模型没有 profile 时回退到 170000 token / 保留 6 条消息——DeepSeek 走的就是回退路径,务必自己显式配 trigger。
  • 原始消息压缩后留档在 /conversation_history/{thread_id}.md;result["files"] 里出现这个文件就说明压过了。
  • 子 agent 的 system_prompt 必须写死输出字数上限,否则隔离形同虚设。
  • 压缩产生的 token 会混进流式输出,按 metadata["lc_source"] == "summarization" 过滤。
  • 本地 0.7.3 起 write_todos 需要手动挂 TodoListMiddleware(),它是长任务里最便宜的目标锚点。
这是 Deep Agents 系列第 10 课,共 16 课。下一课:Deep Agents 流式:让跑几分钟的任务看得见进度。

GitHub:langchain-ai/deepagents

#DeepAgents #AIAgent #LangChain #ClaudeCode #Python #大模型应用 #程序员进阶

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料