跳到主要内容

deepagents — 本课题摘录

读了哪几篇: 03-filesystem-and-permissions(工具族、动态可见性与权限闸门)、05-context-engineering(压缩与卸载)。 其余四篇(装配流水线、可插拔后端、子 agent、技能与记忆)本轮没读。

这一家贡献两条:一条是"工具清单每轮可变"的完整做法,一条是"长对话怎么瘦身"的四道防线。

它对本课题回答了什么

决定一:工具清单在每次模型请求前现场增删

它先把"为什么必须运行时决定"讲清楚:后端可能还是个工厂函数,或者是个到运行时才知道构成的组合后端,所以能力只能在请求时探测。 (依据:前沿库 · Deep Agents · 文件系统中间件:工具族、动态可见性与权限闸门 —— execute 依赖后端实现沙箱协议、delete 依赖后端真的覆写了删除;构造中间件时后端可能还是工厂函数或运行时才知道路由构成的组合后端,所以能力只能在请求时探测)

关键是它点出了三件事互相牵扯,不能分开做:

牵扯说的是什么
后端不支持执行命令 → 那个工具不该出现在清单里不能只是让它调用时报错,因为模型看见了就会用
那个工具没了 → 别的工具描述里"真要正则就用它跑 rg"成了假信息必须同步换掉
权限能拦住"写某个受保护路径" → 但拦不住"列目录"把它列出来结果侧还要再过一遍滤网

(依据:前沿库 · Deep Agents · 文件系统中间件:工具族、动态可见性与权限闸门 —— 三件事互相牵扯——不支持的工具不能只报错还得从清单里消失(模型看见就会用)、删了工具后别的工具描述里提到它的话成了假信息必须同步换、权限拦得住写但拦不住 ls 列出来所以结果侧还要滤)

第二条是最容易漏的:删工具不只是删一项,还要改别的工具的描述。 我们的最小原型只有一两个工具时看不出来,但只要工具描述之间开始互相引用,这条就必然踩到。

它的新版做法更进一步:不再用系统提示罗列"你有哪些工具",而是把这个信息直接落在工具描述上。 理由写在源码注释里:内置指引和工具自己的 schema 描述重复,每次请求重发纯属浪费。 (依据:前沿库 · Deep Agents · 文件系统中间件:工具族、动态可见性与权限闸门 —— 0.7.x 起中间件默认不再生成内置工具指引,理由是内置指引与工具自己的 schema 描述重复、每次请求重发纯属浪费;同一信息改为落在工具描述的动态改写上)

一个跟缓存有关的细节:工具名顺序是恒定的,由一个固定顺序表钉死——不随集合的哈希顺序抖动。 (依据:前沿库 · Deep Agents · 文件系统中间件:工具族、动态可见性与权限闸门 —— 工具名顺序由 _FS_TOOL_ORDER 固定、不随集合哈希顺序抖动,这对 prompt 缓存前缀很重要)

这条要记进配方:凡是每轮都发给模型的东西,顺序必须稳定,否则缓存前缀天天变。 跟 whale 的"不可变前缀 + 只追加历史"、aider 的"固定段落顺序"是同一件事的三种说法。

决定一:长跑 agent 的两个爆炸点,要分开治

这是这一家最有结构的一段。 它先把两个爆炸点分开:

爆炸点谁受不了增长量级
上下文窗口模型厂商与消息总量成正比
存盘体积数据库朴素做法是平方级

(依据:前沿库 · Deep Agents · 上下文工程:压缩、卸载与不让 checkpoint 爆炸 —— 长跑 agent 有两个爆炸点——上下文窗口(与消息总量 O(N))与 checkpoint 体积(朴素做法 O(N²),因为每步把整个消息列表写一份))

存盘为什么是平方级,这条最容易被忽略: 每个超步结束都要存一份状态;如果消息通道每步都把整个列表写进去,跑 N 步就写了约 N²/2 条消息的副本。

它的直觉句很好:"窗口是内存,存盘是磁盘。内存放不下要换出去;磁盘写太频要改成写日志(增量)而不是写快照(全量)。"

存盘侧的解法是一行:给消息通道换成"只存增量 + 每 50 步一次快照"。

决定一补充:窗口侧四道按代价递增的防线

消息列表越来越长

① 工具参数截断 ──┤ 只砍旧的写文件类调用的入参
(最便宜,先试) │ 原始状态不动 → 完全可逆

② 工具结果卸载 ──┤ 超大结果写进后端文件
(工具返回时) │ 留头尾预览 + 路径 → 可以再读回来

③ 历史摘要 ──┤ 旧消息交给模型压成一段摘要
(阈值触发) │ 全文另存一份到磁盘

④ 溢出后尾部裁剪 ─┘ 厂商已经拒了,最后一招:切尾巴

(依据:前沿库 · Deep Agents · 上下文工程:压缩、卸载与不让 checkpoint 爆炸 —— 窗口侧四道按代价递增的防线依次是工具参数截断(可逆)、工具结果卸载(留头尾预览+路径)、历史摘要(全文另存)、溢出后尾部裁剪)

"按代价递增、先试便宜的"这个顺序本身就是答案。 browseros 也是这么做的(剪旧工具调用 → 压缩工具输出 → 模型摘要),两家独立收敛。

最重要的取舍:摘要不改原始历史,只做投影

它明确选择了跟上游框架不同的做法:

  • 上游的做法是在状态里把旧消息删掉;
  • 它的做法是不改状态,只在调模型那一步算出一份"有效消息"给模型看

(依据:前沿库 · Deep Agents · 上下文工程:压缩、卸载与不让 checkpoint 爆炸 —— 窗口侧摘要不删 state 里的消息,只在 wrap_model_call 里算出一份「有效消息」给模型看;上游 LangChain 的做法是用 RemoveMessage 从 before_model 改写状态,Deep Agents 不改)

好处代价
不改历史,只投影原始日志完整、可回放、可做评测状态只增不减,于是存盘侧的止血成了必需品

这两条是配套的,不能只抄一半。 nanobot 也是这个主张("发给模型的是历史的投影,真实历史绝不被污染"),deepseek-harness 也是("压缩不是删历史,而是在日志之上的可见面里遮蔽")。 三家独立收敛到同一条,这在本课题里已经够格当共识了。

它没回答什么

  • 循环本身怎么写——这两篇讲的是循环外围的中间件。
  • 停止条件——不在这两篇。
  • 工具调用怎么解析——不在这两篇。

坑与代价

  • "现场增删工具"和 langchain4j 的"只增不减"是对立的。
    • deepagents:能力探测不到就不给模型看,省上下文、防误用;
    • langchain4j:只增不减,保证模型不会在下一轮突然找不到上一轮用过的工具

    判断(无锚): 两者能共存——按"能力"增删(后端不支持就不给),但不按"这轮用不上"增删。前者是事实,后者是猜测。 如果错,会错在: 如果工具多到必须每轮裁剪(几百上千个),那就只能按相关性猜,那时"模型找不到上一轮的工具"这个问题就得靠别的办法兜(比如保留最近用过的)。

  • 四道防线里前两道会改工具调用的参数与结果。 这意味着存进历史的东西和当初真实发生的不完全一样——做评测和复盘时要注意。
  • 这套东西是中间件,不是循环。 它依附于上游框架的中间件机制;想抄到自己的裸循环里,得先有等价的"调模型前/工具后"挂点。