DeepAgents(五):DeepAgents 的 backend 怎么选

2026-10-08 17:400 条评论19 次阅读

DeepAgents 的 backend 怎么选:7 个类的实测差异

DeepAgents 把文件操作抽成了一层 backend,建 Agent 的时候传一个参数就能换掉。包里一共导出 7 个实现类,文档开头都在说「文件存在哪儿」。

我把这 7 个类逐个装上跑了一遍,发现差异不在那儿。

backend 决定了 3 件事:模型这一轮拿到哪些工具、同名工具背后是谁在干活、文件能活多久。

跑这些对照不需要 API key,也不用联网。我用一个假模型顶替真模型,它只在工具面上留个钩子,把送进来的工具原样记下来。下面每个数字都能在本机复现,口径都写在对照旁边。

一、先把 3 个结论摆出来

结论 硬证据
换 backend 就换工具面 默认装配 8 个工具,带 shell 的 9 个
同名工具的实现可以完全不同 同一个 grep,一个扫内存字典,一个起子进程
文件活多久由装配决定 加不加 checkpointer,同样的一次读取结果相反

二、7 个实现类,和一个没有抽象方法的协议

backends.__all__ 导出 10 项,拆开是这样:

项目 数量 名字
实现类 7 StateBackend / FilesystemBackend / StoreBackend / CompositeBackend / LocalShellBackend / ContextHubBackend / LangSmithSandbox
基类 1 BackendProtocol
常量 2 DEFAULT_EXECUTE_TIMEOUT / NamespaceFactory

BackendProtocol 是个 abc.ABC,但源码里一个 @abstractmethod 都没用上。类注释写得很直白:

@abstractmethod to avoid breaking subclasses that only implement a subset

所以这个协议不强制实现任何东西。9 个同步文件方法在基类里全是 raise NotImplementedError,另配 9 个异步版本:

ls / read / write / edit / delete
grep / glob
upload_files / download_files

7 个实现类把这 9 个都写全了,但那是自觉,不是协议逼的。漏实现的后果也不是实例化就报错,得真调用那一下才炸。

三、换 backend,工具面就变了

这是最实用的一条。我装上 4 种 backend,抓模型侧真正绑定到的工具:

backend 工具数 有 execute 吗
StateBackend(默认) 8 没有
FilesystemBackend 8 没有
CompositeBackend(默认 State) 8 没有
LocalShellBackend 9 有

8 个是 7 个文件工具加 task。

最值得留意的是 FilesystemBackend。它能直接读写你硬盘上的文件,工具面却和内存态一模一样,也没有 execute。能碰真实文件,不等于能跑命令。

判定逻辑在 supports_execution 里,就一句话:这个 backend 是不是 SandboxBackendProtocol 的实例。是 CompositeBackend 就看它的 default。

工具不只多一个,grep 的描述也换了版本:

backend grep 描述长度 带 execute 退路吗
默认 / Filesystem / Composite 557 字符 没有
LocalShellBackend 638 字符 有

多出那 81 个字符是一句话:「如果你确实需要正则,改用 execute 工具跑 rg」。只有装了 shell 的 backend 才配这句。

传一个 backend 进去

supports_execution 判定

装了 shell 就是 9 个工具

grep 描述换成 638 字符版

工具描述的总成本也跟着涨。7 个文件工具 2230 token,加 task 499 token,一共 2729。换成 LocalShellBackend 是 3077,多 348。

prompt 也会变。默认几种 backend 注入 0 字符,但把 CompositeBackend 的 default 换成 LocalShellBackend,会多出 667 字符一段:

## Shell paths vs. virtual paths

标题下面讲的是虚拟路径和宿主路径怎么对应。没有 shell 的 backend 用不上这段,就不给。

命令通道是通的,我跑了一条:

backend.execute("echo hello")
# exit_code=0  output='hello'

把参数换成 exit 3,拿到的 exit_code 就是 3。

四、同一个 grep,两种引擎

两边都是字面匹配。我拿同一批语料、同一批问法分别喂进去:

问法 StateBackend FilesystemBackend
卸载阈值(原文里的字) 1 行 1 行
call_abc123(原文里的字) 2 行 2 行
压缩|卸载(正则写法) 0 行 0 行
20\d+ token(正则写法) 0 行 0 行
上下文快满了怎么办(换了说法) 0 行 0 行
TOKEN 预算(换了说法) 0 行 0 行

行为完全一致。区别在引擎:StateBackend 的底层是一行子串判断,扫的是内存里的字典。FilesystemBackend 先起 ripgrep 子进程,取不到结果再退到 Python 搜索。

同一批语料扫 1,000 次(5 轮、每轮 200 次,取中位数),StateBackend 在 0.08 毫秒上下,FilesystemBackend 在 0.8 毫秒上下。内存里扫字典快 9 倍左右。

回退路径还有个不声不响的边界。FilesystemBackend 有个 max_file_size_mb,默认 10,只在 Python 回退里生效。我造了一个 11.8 MB 的文件,里面确实写着要找的那串字:

谁去搜 命中
StateBackend 1 条
FilesystemBackend(默认 10 MB) 0 条
FilesystemBackend(调到 50 MB) 1 条

0 条的意思是它压根没去搜这份文件。这个过程不报错,也不给警告。

还有一条:FilesystemBackend 的 grep 带一个 context_lines 参数,能多带回上下文行。协议注释写明它故意不暴露给 agent 的 grep 工具。backend 的 API 面比工具面宽。

五、文件能活多久,由装配决定

StateBackend 的文档第一句就是 "ephemeral"。我实测了两种装配:

装配 同一线程下一轮读 换一个线程读
StateBackend 失败 失败
StateBackend + InMemorySaver 成功 失败

同一段代码,加不加 checkpointer,同样的一次读取结果相反。加上之后文件在会话内留得住,跨线程还是各管各的。

要做到跨会话,得换 StoreBackend,它的定位写在文档里:"persistent, cross-conversation"。代价是构造时必须给一个 namespace 工厂。

StoreBackend(
    namespace=ns_factory,
)

FilesystemBackend 走的是另一条路:写下去就是真文件。我写了一个 /probe.txt,磁盘上真的出现了。

往 StateBackend 写一个文件

同一线程下一轮还读得到

换一个线程就读不到了

要跨会话得换成 StoreBackend

六、前缀是路由,不是隔离

CompositeBackend 按路径前缀分发。我配了 2 条路由:

routes = {
    "/memories/": mem,
    "/notes/": mem,
}
comp = CompositeBackend(
    default=fs,
    routes=routes,
)

写 3 个路径进去:

写进去的 落到哪个 backend 落到哪个 key
/top.txt FilesystemBackend /top.txt
/memories/a.txt StoreBackend /a.txt
/notes/deep/b.txt StoreBackend /deep/b.txt

留意后 2 行:前缀被剥掉了再交给对应的 backend。

这里有个坑。上面 2 条路由用的是同一个 StoreBackend 实例、同一个 namespace。结果是 /memories/a.txt 和 /notes/a.txt 剥掉前缀后都是 /a.txt,在 store 里成了同一个 key。

我读 /notes/a.txt,拿到的是写进 /memories/a.txt 的那份内容。

前缀是路由规则,不是隔离边界。要真隔离,得给每个前缀配各自的 namespace。

七、一张选型表

你的需求 选哪个 要付什么
会话内临时记点东西 StateBackend 想跨轮留住,必须配 checkpointer
跨会话、多用户隔离 StoreBackend 必填 namespace 工厂
读写真实项目文件 FilesystemBackend 要指定 root_dir;默认 10 MB 以上的文件搜不到
上面几种按路径混用 CompositeBackend 必填 default 和 routes;前缀不隔离
还要跑命令 LocalShellBackend 工具描述多 348 token,且有本机权限
远端沙箱 LangSmithSandbox 构造要传一个现成的 sandbox
托管版本化存储 ContextHubBackend 构造要一个 identifier

最后 2 个都得先有对应的外部环境,本机跑不起来,我没测它们的运行时行为。

还有个开关值得提一句。FilesystemBackend 的 virtual_mode 默认开着,路径穿越会被拦住:

virtual_mode 读 /../outside.txt 读一个绝对路径
True 拦下,报 Path traversal not allowed 拦下
False 找不到 读到了根目录外的文件

它是路径语义开关,不是安全边界。

LocalShellBackend 的源码里还有一段安全警告,措辞比一般文档重。它给 Agent 的是「直接文件访问和不受限的 shell 执行」,建议只在本地开发 CLI 这类场景用。生产环境应该换成 StateBackend 或 StoreBackend,或者自己扩展 BaseSandbox 做隔离。

警告里专门提了一句:有 shell 的时候,virtual_mode 和基于路径的限制不提供任何安全性,因为命令本来就能访问系统上的任意路径。上面那张对照表正好印证了这一点。

八、所以怎么选

先问自己一句话:这些文件是谁写下去的、要活多久。再看一句:要不要跑命令。

自己写、当轮用完就扔,StateBackend 最省。要跨会话,StoreBackend。要动真实文件,FilesystemBackend。

要跑命令,LocalShellBackend。几种混着用,用 CompositeBackend 按前缀接起来。

选完记得回头看一眼工具面。多一个 execute,成本和风险都跟着涨。