DeepAgents(六): 大结果卸载
DeepAgents 大结果卸载:上下文省了多少
工具返回 24 万字符,历史里只剩 1274 字符。窗口装不下的结果被搬进文件,模型那一轮只看得到路径和预览,这是虚拟文件系统最实用的一手。
省了多少好量,搬走之后的事不好猜:东西落在哪、能活多久、要用的时候取回一次付多少。
卸载省下的是上下文窗口;被搬走的内容存在哪、能活多久,取决于你装的是哪个 backend。
下面这笔账来自一次真跑。8 轮任务,工具累计返回 87 万字符,我逐轮记下模型真正收到多少,再把 4 种 backend 各装一遍看产物落到哪。全程不用 API key,读完约 6 分钟。
一、先把 3 个结论摆出来
| 问题 | 结论 |
|---|---|
| 省了多少 | 工具累计 87 万字符,收尾时历史里 15 万,占 17% |
| 存在哪 | 跟着 backend 走,默认装配下装在图 state 里 |
| 能活多久 | 由装配决定,默认装配下换一个会话就取不回来 |
后面按这 3 条逐条给证据。
二、省了多少:一次 8 轮任务的账
先看任务长什么样。8 轮工具调用,其中 5 次返回 3 万字符、3 次返回 24 万字符。卸载线是 tool_token_limit_before_evict = 20000,配上 NUM_CHARS_PER_TOKEN = 4,也就是 8 万字符。
逐轮记下模型每次调用收到的历史字符数:
| 轮 | 模型这一轮收到 | 本轮工具返回 | 被卸载 |
|---|---|---|---|
| 1 | 9 | 30000 | 否 |
| 2 | 30009 | 30000 | 否 |
| 3 | 60009 | 240000 | 是 |
| 4 | 61283 | 30000 | 否 |
| 5 | 91283 | 240000 | 是 |
| 6 | 92557 | 30000 | 否 |
| 7 | 122557 | 240000 | 是 |
| 8 | 123831 | 30000 | 否 |
| 收尾 | 153831 | 无 | 无 |
表里「模型这一轮收到」是它这次调用时看到的历史,「本轮工具返回」是这次调用之后拿到的东西,两者错开一轮。
规律很干净:一条 3 万字符的结果,下一轮原样带上,多 30000;一条 24 万字符的,下一轮只多 1274。
收尾时整张表算下来:
| 项 | 字符数 |
|---|---|
| 工具累计返回 | 870000 |
| 收尾时历史里 | 153835 |
| 占原始量 | 17% |
3 条 24 万的结果,历史里各剩 1274 字符。其余部分全在文件里,一个字没少。
要确认卸载的功劳,得把它关掉
「占了 17%」这个数字单独看说明不了什么,得知道不省会怎样。建 agent 时传一个同名的 FilesystemMiddleware,把它的 tool_token_limit_before_evict 设成 None,装配环节会按 .name 原地替换掉默认那个。替换后栈里仍然只有 1 个 FilesystemMiddleware,阈值从 20000 变成 None。
同一个任务跑 3 遍:
| 装配 | 收尾历史 | 占原始量 | 模型逐轮实收峰值 |
|---|---|---|---|
| 默认 | 153835 | 17% | 153831 |
| 关掉卸载 | 840013 | 96% | 600009 |
| 卸载与摘要都关 | 870013 | 100% | 870009 |
第二行值得停一下。把卸载关掉,历史也没长到 87 万,因为后面还有一道线接着,模型逐轮实收的峰值也跟着从 15 万涨到 60 万。
第二道线是摘要
同一批装配里,摘要在历史涨到 68 万字符左右时动手,把旧消息压成摘要,还往 /conversation_history/session_xxx.md 写了 33 万字符。所以关掉卸载只到 840013。
2 道线都关才能看到 100%,此时收尾是 87 万字符,约 21.8 万 token。
顺带解释一条容易踩的坑:摘要的触发线分 2 档。
| 模型 profile | 触发线 |
|---|---|
带 max_input_tokens |
模型窗口的 85% |
| 不带 | 固定 170000 token,也就是 68 万字符 |
假模型没有 profile,走的是第二档。用真模型时若 profile 里没写窗口大小,退到的就是这条固定线,跟窗口实际多大无关。
三、卸下来的东西落在哪
省下来的是窗口,那内容去哪了。同一份 24 万字符的结果,装 4 种 backend 各跑一次:
| backend | 产物落在哪 | 能看到什么 |
|---|---|---|
| StateBackend(默认) | 图 state 的 files 字段,键是 /large_tool_results/call_1 |
240000 字符 |
| FilesystemBackend | 磁盘 <root>/large_tool_results/call_1 |
240000 字节 |
| StoreBackend | store 里 key 为 /large_tool_results/call_1 |
240000 字符 |
| CompositeBackend 路由到磁盘 | 磁盘 <root>/call_1 |
240000 字节 |
最后一行有个细节:路由之后前缀被剥掉了,磁盘上的真实路径少一层 large_tool_results。这段不算问题,用原来的虚拟路径仍然读得回来,实测 read_file('/large_tool_results/call_1') 成功,读到 6436 字符。
默认那一行才是重点:默认 backend 把卸载产物装在图 state 里。它跟普通文件一个待遇,不会因为「是框架自动写进去的」就多一层落盘。
四、它能活多久
接着上面的结论往下问:装在图 state 里的东西活多久。同一个卸载任务跑 3 遍,每遍换个方式再读一次产物:
| 装配 | 同一会话再跑一轮 | 换一个会话 | 重开 agent |
|---|---|---|---|
| StateBackend | 失败 | 失败 | 无 |
| StateBackend + InMemorySaver | 成功 | 失败 | 无 |
| FilesystemBackend | 成功 | 无 | 成功 |
第一行要注意:默认装配下没有 checkpointer,同一个会话里再跑一轮,产物就没了。第二行加上 InMemorySaver 能留住,但换一个 thread_id 等于换一个会话,还是取不回来。
要真落盘就换 FilesystemBackend,指定一个 root_dir,写下去就是真文件,重开 agent 还能读。
FilesystemBackend(
root_dir=root,
)
五、取回一份要读几次
产物没丢,那取回贵不贵。一份 24 万字符、3886 行的产物,read_file 默认一次读 100 行:
| 项 | 数值 |
|---|---|
| 一次读 100 行 | 5851 字符,约 1467 token |
| 读完整份 | 至少 42 次 |
| 全读一遍 | 约 60004 token |
| 历史里那条引用 | 1274 字符,约 323 token |
42 次是整份读完的下限。这个数按日志文本算,一行约 62 字符,换成别的语料,行宽不一样,次数跟着变。
真实任务里通常只要其中一小段,这也正是卸载能省上下文的前提:模型先看那 1274 字符的引用,判断要不要取、取哪一段。
再验一件事,搬走的过程有没有截断:
| 工具返回 | 产物 | 历史里 |
|---|---|---|
| 120000 | 120000(100%) | 1233 |
| 240000 | 240000(100%) | 1274 |
搬走的是完整的,只有历史里那条变短了。
六、所以这笔账怎么算
3 条收口。
卸载省的是窗口,换不来留存。 省下的上下文是真的,但产物落在哪个 backend 里,就受那个 backend 的寿命管。默认装配下换一个会话,东西跟着一起走。
要留住就得换存的位置。 卸载产物和普通文件一个待遇,没有额外通道。想跨会话留住,用 FilesystemBackend 写磁盘,或者 StoreBackend 交给 store。
取回成本要算进预算。 一条 24 万字符的结果全读一遍约 60004 token,比不卸载还贵。卸载的价值建立在「只取要用的那一段」上。
选之前先想清楚这 2 件事:产物要活多久,你打算怎么取回它。