夜雨聆风学习资料网

ARTICLE · 1146688

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 件事:产物要活多久,你打算怎么取回它。

— 代码的江湖 —

相关学习资料