首页 / 文章

智能体的文件系统即决策依据:Deep Agents后端详解

LangChain Deep Agents的后端决定了哪些文件能够保留:五种内置后端、CompositeBackend的路由机制,以及多用户存储和沙箱环境中的命名空间限制。

发布者:REACTAPP.TOP阅读约 9 分钟2174 词

来源: LangChain Deep Agents: Your Agent’s Filesystem Is a Decision. Most People Make It by Accident. IV

最可怕的代理漏洞并非导致程序崩溃,而是它会悄悄遗忘昨日的内容,窃取他人的记忆,或将宝贵的知识写入本不应丢失的临时存储空间。在 LangChain 的 Deep Agents 中,这些结果的决策机制位于后端,其目标就是不再将文件系统视为神奇的存在,而是有意识地决定哪些内容会保留、哪些会消失,以及谁有权查看它们。

每个 Deep Agents 代理都有一个文件系统,但该文件系统实际存储的位置由你决定:可以存储在内存中、磁盘上、跨线程的持久存储中,或是多种方式的组合。选择不当会导致代理在多次对话之间悄悄遗忘所有内容。

笔记消失背后的漏洞

假设你创建了一个开启了文件系统工具的智能体,发现它在聊天过程中会将笔记保存到 /memories/findings.md 中。之后它在同一对话中又会读取这些笔记。一切正常,于是你将其发布上线。第二天有用户回来,提到了智能体 supposedly 应该知道的事情,但智能体却一无所知。那些笔记不见了,既没有错误提示,也没有日志记录。智能体只是重新开始工作,却并未告知用户。

没人会故意编写这样的漏洞。问题源于默认设置:智能体的文件会被存放在与对话同步结束的线程级临时空间中。如果跳过决定文件存储位置的步骤,框架会自动处理。对于临时笔记来说这没问题,但对于需要长期存储的记忆内容而言,这却是错误的选择。

权限、内存、沙箱环境以及部署功能都基于这一选择而定。只要后端配置正确,其他一切都会顺利运作;而一旦配置错误,就只能花费整个下午时间去寻找那些消失的文件。

文件系统是虚拟的,后端决定了其实际结构

代理程序始终看到的是一个带有普通路径的普通文件系统。后端负责决定该文件系统由什么组成,而且更换后端也不会改变代理程序的运行方式。

提供了六种文件系统工具:ls、read_file、write_file、edit_file、glob和grep。对模型而言,这些工具操作的是一个普通的文件系统。模型会写入/workspace/plan.md,稍后读取它,并像拥有磁盘一样在/repo中执行搜索操作,而无论底层使用的是哪种后端,这些操作都不会改变。

后端是该抽象层背后的实现部分。它负责处理代理从未想过的问题。如果模型写了/workspace/plan.md,这些字节会去往何处?是进入进程退出后会消失的Python内存中?还是存储到真正的磁盘上?又或是存入会在用户后续的所有对话中都保留的数据库中?不同的答案对应不同的后端。

选择方式是通过将特定的后端传递给create_deep_agent(model=..., backend=...)函数来实现。如果不传递任何参数,则会使用默认后端,这也就解释了上文提到的笔记会消失的现象。

为了让你的代码与当前版本保持一致,有一个建议:为代理提供一个已准备好的实例,比如StateBackend()。旧版本的示例中使用的是lambda rt:这种包装方式,但现在它已被废弃且不再必要。

按数据保留方式排序的五种后端

区分这五种内置后端的简单方法是根据其生命周期而非内部实现:通过每种方式写入的文件会保留多久?

StateBackend是默认选择,属于线程级临时存储空间。文件存储在智能体的状态中,会在一次对话(即一个线程)的多个轮次中持续存在,但不会在不同线程之间共享。它相当于智能体的便签本,非常适合用于那些两步之后需要再次读取、而在对话结束时会被清除的中间处理内容。这是最合适的默认选择,但也是那种若被误认为是内存就会导致“笔记消失”现象的后端。

StoreBackend 是一种跨线程的持久化存储方式。文件会被存储在 LangGraph 的存储系统中,并在多次对话之间保持不变,这正是长期记忆所需要的。不过有一个相当重要的问题:你必须明确告知系统这些数据属于谁。具体内容将在下文说明。

FilesystemBackend 和 LocalShellBackend 则使用主机上的真实磁盘。它们会在你指定的目录中读取和写入实际文件,而 LocalShellBackend 还为智能体提供了 execute 工具,允许其在无需沙箱环境的情况下直接在你的机器上运行shell命令。这类方式适合用于你在办公桌前开发的本地编程助手,但在其他任何场景下都存在安全隐患,因此文档明确警告不要在已部署的智能体中使用它们。原始主机访问权限与动态用户输入相结合,极容易引发安全问题。

ContextHubBackend 是 LangSmith 原生的持久化方案。文件存储在 LangSmith Hub 仓库中,并以提交记录的形式保存,这样不仅能在多次运行间保持数据持久性,还能记录每一次文件系统的变更,而无需自行搭建存储系统。对于已身处 LangSmith 生态系统且希望无需配置数据库即可实现持久化的用户而言,这是非常理想的选择。

综合来看,这些选项都围绕同一条轴线分布:无任何内容保留、仅对话内容保留、所有内容保留,或是所有内容连同历史记录一起保留。请根据功能需求选择合适的选项。

CompositeBackend:不同的路径,不同的生命周期

让后端系统高效运行的关键在于:你通常不会希望为整个文件系统采用单一的持久化策略。工作文件应当成本低、处理速度快且可随时丢弃,而内存数据则需具备持久性。这对应两种不同的后端,CompositeBackend 通过路径前缀的路由机制,在同一个文件系统中同时实现这两种功能。

为大多数路径指定一个默认后端,再设置一个路由表来处理特定的前缀路径。常见的配置是将所有临时数据都存放在临时区域,而持久化存储则位于 /memories/ 下:

from deepagents import create_deep_agent
from deepagents.backends import CompositeBackend, StateBackend, StoreBackend
from langgraph.store.memory import InMemoryStore

agent = create_deep_agent(
    model="anthropic:claude-sonnet-4-6",
    backend=CompositeBackend(  # ①
        default=StateBackend(),  # ②
        routes={  # ③
            "/memories/": StoreBackend(namespace=lambda _rt: ("memories",)),  # ④
        },
    ),
    store=InMemoryStore(),  # passed to create_deep_agent, not to the backend  ⑤
)

逐条查看该列表中的标记:

  1. CompositeBackend 为代理呈现一个文件系统,并将所有操作转发给路径匹配的子后端。
  2. default 会处理那些没有对应路由的请求。在本例中,它就是线程作用域内的 StateBackend。
  3. routes 用于将路径前缀与后端对应起来。如果多个前缀匹配,较长的那个优先,因此 /memories/projects/ 会优先于 /memories/。
  4. 写入 /memories/ 下的数据会发送到持久化的 StoreBackend,其 namespace 可调用对象会返回用于标识所有者并限定数据范围的元组。
  5. LangGraph 的 store 会被传递给 create_deep_agent,而非后端,这是初学者容易忽略的实现细节。

其效果是,任何写在 /memories/ 下的内容都会被存储在持久化存储中,从而在多次对话中依然存在,而其他所有内容,无论是 /workspace 中的计划、临时文件还是缓存读取结果,都会被存入默认的 StateBackend,在线程结束时会消失。智能体既不知道也不关心这些,它只负责写入路径。

面向真实智能体的文件系统策略

以一个代码维护智能体为例,比如名为 buggy-shop 的智能体。当它处理某个代码库时,该代码库中的文件就代表了当前的工作状态。这些文件会被加载、编辑和测试,没有理由让它们泄露到下一次对话中。智能体在处理过程中学到的知识则是另一回事,而这些正是它在下一次请求时应当记住的内容:某个模块中不稳定的测试用例、多个测试套件共用的固定装置,以及之前发现的页码计算错误。

buggy-shop采用了上述的复合结构。代码仓库作为线程级临时存储,位于默认的StateBackend之下;而累积的笔记则存储在持久化的StoreBackend中的/memories/路径下。这样设计了一个专门处理哪些内容应保留下来的智能体。将临时工作状态与持久知识分开是关键,而路径前缀路由正是实现这一目标的手段。

代码运行位置与文件存储位置

到目前为止讨论的都是文件相关内容,许多智能体其实并不需要更多功能。但那些需要通过运行pytest来验证修复方案的智能体则需要一个shell,因为执行命令并非文件系统操作。沙箱后端正好解决了这个问题:除了文件系统外,它还提供了用于执行命令的execute工具,所有操作都在隔离的容器中进行,从而避免智能体损坏主机。

代理程序可以通过两种方式与沙箱相连。一种是将代理程序置于沙箱内部,使其在沙箱的文件系统上运行。这种方式能让本地开发环境更接近生产环境,但会将 API 密钥置于沙箱中。另一种则是将沙箱视为一种工具:代理程序仍运行在您的服务器上,仅通过调用沙箱来执行代码。凭证仍保留在服务器外部,您可以丢弃沙箱,而且即便沙箱崩溃,代理程序的状态也会安全无损。Deep Agents 能够支持这两种方式,这与仅允许第一种方式的框架形成了鲜明对比。

请记住,代码执行与文件存储是两个不同的问题。文件系统后端负责处理其中一个,而沙箱后端则负责另一个,许多高级代理程序都需要两者。

不可或缺的配置

当 StoreBackend 为多个用户提供服务时,就需要一个命名空间生成器。如果没有它,虽然不会导致程序崩溃,但会出现内存泄漏。后端需要知道自己正在管理哪些文件,而这一信息正是通过命名空间获得的:一个简单的函数,接收运行时环境作为参数,返回一个表示文件所有者的元组。通过按用户划分作用域,可以为每个人提供独立的存储空间:

from deepagents.backends import StoreBackend
backend = StoreBackend(
    namespace=lambda rt: (rt.server_info.user.identity,),
)

如果不使用命名空间以及助手默认的旧版作用域,那么同一智能体的所有用户将共享同一套存储空间,并且能够读取彼此本应私密的记忆内容。

由于只有你一个用户,这个漏洞在你自己的机器上可以通过所有测试。但一旦有第二个人登录,它就会变成隐私问题。对于非单用户本地测试环境之外的任何用途,都必须使用命名空间生成器。文档中说明这即将成为强制要求,因此现在就编写相关代码,以后再不必进行迁移。

默认后端也配有较小的解决方案。StateBackend会在每一步都对智能体的全部状态进行快照保存,这正是让对话能够顺利继续的原因。但其代价是存储在状态中的大文件会逐步骤被反复序列化,因此这些大型数据会悄悄消耗性能和存储空间。解决办法仍是同样的组合模式:将大型或持久性的数据存放到专用存储中,而让StateBackend仅用于处理小型、临时的临时数据。

今日检查清单

  • 查明你的智能体使用的是哪种后端。如果未在create_deep_agent函数中指定backend=参数,那么就使用的是StateBackend,此时智能体会忘记对话之间的所有内容。请确认这确实是你的预期。
  • 确定哪些内容应保留下来以供后续使用。将正在处理的文件与持久性知识分开列出。属于持久性类别的内容都应存放在StoreBackend中。
  • 使用组合模式。构建一个默认使用StateBackend的CompositeBackend,并将/memories/路径路由到StoreBackend。数据存储操作本身应传递给create_deep_agent,而非直接传给后端。
  • 为每个多用户StoreBackend提供一个命名空间生成器。将其与用户身份关联,并在第二个人登录之前完成设置。
  • 对状态中的大文件进行审核。如果智能体生成了较大的文件,应将其从StateBackend中导出,以避免在每一步操作中都重新序列化这些文件。
  • 后端选择成为一项决策

    将后端视为一种可选方案,从一开始就能避免下午进食综合征的问题。你可以根据生命周期来描述这五种内置后端,使用CompositeBackend为不同的生命周期指定不同的路径,同时也会明白多用户存储系统离不开命名空间工厂。如此设计的代理对其文件有明确的处理方案:工作仓库使用临时存储,而其获取的文件则存储在持久内存中。

    代理程序能够自行写入持久化存储数据,这一功能确实值得警惕,而这正是合理的反应。具备文件写入能力的代理可能会写错文件、删除正确文件,或执行用户根本不希望其运行的命令。文件系统就相当于给了它双手。接下来则需要用“牵引绳”来约束它,也就是通过声明式的权限限制它可以操作的文件范围,同时通过人工审批让它在执行操作前先暂停并询问用户。只有在你已经明确决定了文件存储位置的情况下,牵引绳才能起到作用。