Typography

学习笔记

Deep Agents 学习04 Backends

发布于 # Deep Agents

很多人第一次看到 Deep Agents 的文件工具,会默认认为它们直接连着本地磁盘。真正支撑这一切的,其实是 backend 抽象层:同样的 read_filewrite_fileglob,可以落到内存状态、真实文件系统、持久化 store,甚至是隔离沙箱里。把这一层想明白之后,很多看似分散的能力才会重新连成一张图。

为什么值得关注

后端决定的从来不只是“文件存在哪”。它同时决定数据会不会持久化、会不会跨线程共享、Agent 有没有机会碰到真实磁盘,以及是否还能顺带执行 shell 命令。也正因为如此,backend 设计其实就是在给 Agent 设定执行环境和存储边界。

一个合适的后端组合,通常要同时支撑这些动作:

后端决定的不只是“文件存在哪”,还决定了:


后端是什么

Deep Agents 会通过一组文件系统工具暴露文件操作能力,例如:

这些工具本身并不直接操作磁盘或远程存储,而是统一通过 backend 去完成。

可以把 backend 理解为 Agent 文件系统能力的“底层适配层”。

它负责回答这些问题:


一个整体理解框架

可以把后端分成 5 类:

如果是图片文件,例如:

那么 read_file 可以直接返回多模态内容,这在所有后端上都成立。


后端关系图

graph TB
    Tools[Filesystem Tools] --> Backend[Backend]

    Backend --> State[State]
    Backend --> Disk[Filesystem]
    Backend --> Store[Store]
    Backend --> Sandbox[Sandbox]
    Backend --> LocalShell[Local Shell]
    Backend --> Composite[Composite]
    Backend --> Custom[Custom]

    Composite --> Router{Routes}
    Router --> State
    Router --> Disk
    Router --> Store

    Sandbox --> Execute[+ execute tool]
    LocalShell --> Execute[+ execute tool]

这张图可以帮助你快速建立整体认知:


快速选型

真正开始选 backend 时,最常见的困惑通常不是“不知道有哪些选项”,而是不知道该先按什么维度判断。一个实用的办法是先别从实现细节入手,而是先问:你的文件是临时 scratch pad、真实项目文件、长期记忆,还是需要顺带执行命令?沿着这个问题往下看,后面的选型会清楚很多。

StateBackend

适合:

特点:

FilesystemBackend

适合:

特点:

StoreBackend

适合:

特点:

LocalShellBackend

适合:

特点:

CompositeBackend

适合:

特点:


StateBackend

StateBackend 是默认后端。

如果创建 Agent 时不传 backend,底层就是使用它。

from deepagents import create_deep_agent
from deepagents.backends import StateBackend

agent = create_deep_agent(
    model="google_genai:gemini-3.1-pro-preview",
    backend=StateBackend()
)

它的工作方式

适合的用途

一个容易忽略的点

StateBackend 是 supervisor 和 subagent 共享的。

这意味着:

这非常适合做:

使用建议

如果你还不确定该用什么 backend,就先从 StateBackend 开始。


FilesystemBackend

FilesystemBackend 直接读写真实文件系统。

from deepagents.backends import FilesystemBackend
from deepagents import create_deep_agent

agent = create_deep_agent(
    model="google_genai:gemini-3.1-pro-preview",
    backend=FilesystemBackend(root_dir=".", virtual_mode=True)
)

它的工作方式

root_dir 的意义

root_dir 决定 Agent 的主要工作目录。

如果你希望 Agent 只接触某个项目目录,应该显式指定这个目录。

注意:

virtual_mode=True 为什么重要

这是最关键的安全配置之一。

开启后:

如果不开启:

可以把这个规则记死:

适合的场景

不适合的场景

风险

一旦给 Agent 真实文件访问能力,就意味着它可能:

推荐防护措施


LocalShellBackend

LocalShellBackendFilesystemBackend 基础上增加了 execute 工具,可以直接执行 shell 命令。

from deepagents.backends import LocalShellBackend
from deepagents import create_deep_agent

agent = create_deep_agent(
    model="google_genai:gemini-3.1-pro-preview",
    backend=LocalShellBackend(root_dir=".", env={"PATH": "/usr/bin:/bin"})
)

它的工作方式

重要认知

只要给了 shell,virtual_mode=True 就不再构成真正安全边界。

原因很简单:

适合的场景

绝对不适合的场景

风险等级为什么更高

有了 shell 之后,Agent 理论上可以:

推荐防护措施

常用参数理解

在安全敏感场景下,环境变量暴露也是风险点。


StoreBackend

StoreBackend 用于跨线程持久化存储。

from langgraph.store.memory import InMemoryStore
from deepagents.backends import StoreBackend
from deepagents import create_deep_agent

agent = create_deep_agent(
    model="google_genai:gemini-3.1-pro-preview",
    backend=StoreBackend(
        namespace=lambda rt: (rt.runtime.context.user_id,),
    ),
    store=InMemoryStore()
)

它的工作方式

适合的用途

为什么 StoreBackend 很重要

很多 Agent 场景并不是“一轮执行完就结束”,而是:

这时 StateBackend 就不够了,必须使用 StoreBackend


namespace 的作用

StoreBackend 的核心不是“能存”,而是“怎么隔离”。

它通过 namespace 决定数据读写在哪个命名空间下。

本质上,namespace 决定了:

一个关键原则

多用户场景下,namespace 绝不能偷懒。

如果 namespace 设计不当,可能导致:

常见 namespace 设计方式

按用户隔离

backend = StoreBackend(
    namespace=lambda rt: (rt.server_info.user.identity,),
)

适合:

按 assistant 隔离

backend = StoreBackend(
    namespace=lambda rt: (rt.server_info.assistant_id,),
)

适合:

按 thread 隔离

backend = StoreBackend(
    namespace=lambda rt: (rt.execution_info.thread_id,),
)

适合:

组合隔离

例如:

适合:

namespace 设计建议

多用户生产场景默认原则:


CompositeBackend

当单一 backend 开始不够用时,通常不是因为某个后端本身有问题,而是因为你的系统已经同时存在“临时材料”“长期记忆”“真实工作区”这几种完全不同的存储诉求。CompositeBackend 的价值,就是把这些差异整合成一个统一文件系统视图,而不是让 Agent 自己理解多个分裂存储。

CompositeBackend 用于把不同路径前缀路由到不同后端。

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

agent = create_deep_agent(
    model="google_genai:gemini-3.1-pro-preview",
    backend=CompositeBackend(
        default=StateBackend(),
        routes={
            "/memories/": StoreBackend(),
        }
    ),
    store=InMemoryStore()
)

它的工作方式

一个典型例子

from deepagents import create_deep_agent
from deepagents.backends import CompositeBackend, StateBackend, FilesystemBackend

agent = create_deep_agent(
    model="google_genai:gemini-3.1-pro-preview",
    backend=CompositeBackend(
        default=StateBackend(),
        routes={
            "/memories/": FilesystemBackend(root_dir="/deepagents/myagent", virtual_mode=True),
        },
    )
)

此时:

它最适合什么

最常见的用法是:

例如:

路由规则注意点

这使得 Agent 看到的是一个统一文件系统视图,而不是多个割裂系统。


如何指定 backend

最直接的方法就是在 create_deep_agent(...) 里传入:

agent = create_deep_agent(
    model="google_genai:gemini-3.1-pro-preview",
    backend=StateBackend()
)

如果不传:

因此可以理解为:


自定义虚拟文件系统

再往前走一步,你会发现 backend 真正厉害的地方,不在于框架内置了多少种实现,而在于它把“文件系统”抽象成了一层稳定接口。只要你的外部存储能被映射成路径和内容,理论上就可以让 Agent 继续用同一种文件心智去操作它。

如果内置后端不够,你可以实现自己的 backend,把远程存储或数据库映射成文件系统。

适合的外部存储包括:

自定义 backend 的设计目标

核心目标是让 Agent 仍然用统一文件系统心智去工作:

自定义实现时要考虑什么

S3 风格示例骨架

from deepagents.backends.protocol import (
    BackendProtocol, WriteResult, EditResult, LsResult, ReadResult, GrepResult, GlobResult,
)

class S3Backend(BackendProtocol):
    def __init__(self, bucket: str, prefix: str = ""):
        self.bucket = bucket
        self.prefix = prefix.rstrip("/")

    def _key(self, path: str) -> str:
        return f"{self.prefix}{path}"

    def ls(self, path: str) -> LsResult:
        ...

    def read(self, file_path: str, offset: int = 0, limit: int = 2000) -> ReadResult:
        ...

    def grep(self, pattern: str, path: str | None = None, glob: str | None = None) -> GrepResult:
        ...

    def glob(self, pattern: str, path: str = "/") -> GlobResult:
        ...

    def write(self, file_path: str, content: str) -> WriteResult:
        ...

    def edit(self, file_path: str, old_string: str, new_string: str, replace_all: bool = False) -> EditResult:
        ...

Postgres 风格思路

如果用数据库实现,可以设计一张 files 表:

然后把:

一个关键约束

对于外部持久化后端,例如:

写入结果通常应返回 files_update=None

因为只有纯状态型后端才需要通过 state update 机制回写文件更新。


权限控制

权限用于在 backend 被调用之前,先声明性地决定哪些路径允许读写。

也就是说,权限系统是 Agent 文件系统访问的第一道门。

示例

from deepagents import create_deep_agent, FilesystemPermission
from deepagents.backends import CompositeBackend, StateBackend, StoreBackend

agent = create_deep_agent(
    model="google_genai:gemini-3.1-pro-preview",
    backend=CompositeBackend(
        default=StateBackend(),
        routes={
            "/memories/": StoreBackend(
                namespace=lambda rt: (rt.server_info.user.identity,),
            ),
            "/policies/": StoreBackend(
                namespace=lambda rt: (rt.context.org_id,),
            ),
        },
    ),
    permissions=[
        FilesystemPermission(
            operations=["write"],
            paths=["/policies/**"],
            mode="deny",
        ),
    ],
)

这个例子在表达什么

权限适合控制什么

一条实用原则

优先把“能否访问”这类规则放在 permissions 层;
把“访问时还要做什么附加检查”放在 backend 包装层。


增加策略钩子

如果路径 allow/deny 还不够,可以在 backend 外面再包一层策略逻辑。

适合的场景包括:

方式 1:直接继承 backend

from deepagents.backends.filesystem import FilesystemBackend
from deepagents.backends.protocol import WriteResult, EditResult

class GuardedBackend(FilesystemBackend):
    def __init__(self, *, deny_prefixes: list[str], **kwargs):
        super().__init__(**kwargs)
        self.deny_prefixes = [p if p.endswith("/") else p + "/" for p in deny_prefixes]

    def write(self, file_path: str, content: str) -> WriteResult:
        if any(file_path.startswith(p) for p in self.deny_prefixes):
            return WriteResult(error=f"Writes are not allowed under {file_path}")
        return super().write(file_path, content)

    def edit(self, file_path: str, old_string: str, new_string: str, replace_all: bool = False) -> EditResult:
        if any(file_path.startswith(p) for p in self.deny_prefixes):
            return EditResult(error=f"Edits are not allowed under {file_path}")
        return super().edit(file_path, old_string, new_string, replace_all)

方式 2:包装任意 backend

from deepagents.backends.protocol import (
    BackendProtocol, WriteResult, EditResult, LsResult, ReadResult, GrepResult, GlobResult,
)

class PolicyWrapper(BackendProtocol):
    def __init__(self, inner: BackendProtocol, deny_prefixes: list[str] | None = None):
        self.inner = inner
        self.deny_prefixes = [p if p.endswith("/") else p + "/" for p in (deny_prefixes or [])]

    def _deny(self, path: str) -> bool:
        return any(path.startswith(p) for p in self.deny_prefixes)

    def ls(self, path: str) -> LsResult:
        return self.inner.ls(path)

    def read(self, file_path: str, offset: int = 0, limit: int = 2000) -> ReadResult:
        return self.inner.read(file_path, offset=offset, limit=limit)

    def grep(self, pattern: str, path: str | None = None, glob: str | None = None) -> GrepResult:
        return self.inner.grep(pattern, path, glob)

    def glob(self, pattern: str, path: str = "/") -> GlobResult:
        return self.inner.glob(pattern, path)

    def write(self, file_path: str, content: str) -> WriteResult:
        if self._deny(file_path):
            return WriteResult(error=f"Writes are not allowed under {file_path}")
        return self.inner.write(file_path, content)

    def edit(self, file_path: str, old_string: str, new_string: str, replace_all: bool = False) -> EditResult:
        if self._deny(file_path):
            return EditResult(error=f"Edits are not allowed under {file_path}")
        return self.inner.edit(file_path, old_string, new_string, replace_all)

什么时候用 wrapper 而不是 subclass

如果你希望同一套策略能作用于:

那优先用 wrapper,会更通用。


沙箱的定位

沙箱本质上也是后端的一种,只不过它额外提供 execute 能力,并且运行在隔离环境中。

可以把它看成:

适合场景:

一个简单判断标准:


迁移方式:不要再用 backend factory

旧写法里,backend 可能通过工厂函数构造。

例如以前会这样写:

backend=lambda rt: StateBackend(rt)

现在推荐直接传实例:

backend=StateBackend()

为什么发生这个变化

新版 backend 已经能自己通过 LangGraph 提供的运行时助手拿到所需上下文,不再需要你手动把 runtime 传进去。

旧写法与新写法对照

旧写法

backend=lambda rt: CompositeBackend(
    default=StateBackend(rt),
    routes={"/memories/": StoreBackend(rt)},
)

新写法

backend=CompositeBackend(
    default=StateBackend(),
    routes={"/memories/": StoreBackend()},
)

迁移建议

如果你的代码里还在:

都应该尽快改成直接实例写法。


namespace 工厂从 BackendContext 迁移到 Runtime

新版里,namespace 工厂接收的是 Runtime,而不是旧的 BackendContext 包装对象。

旧写法

StoreBackend(
    namespace=lambda ctx: (ctx.runtime.context.user_id,),
)

新写法

StoreBackend(
    namespace=lambda rt: (rt.server_info.user.identity,),
)

一个重要变化

旧写法中可以访问 ctx.state,但这种方式不适合用于 namespace 推导。

原因是:

因此 namespace 应该来自稳定上下文,例如:

而不应该从可变执行状态推导。


BackendProtocol 要实现什么

如果要自定义 backend,必须实现 BackendProtocol

必需方法

ls(path: str) -> LsResult

作用:

要求:

read(file_path: str, offset: int = 0, limit: int = 2000) -> ReadResult

作用:

要求:

grep(pattern: str, path: Optional[str] = None, glob: Optional[str] = None) -> GrepResult

作用:

要求:

glob(pattern: str, path: str = "/") -> GlobResult

作用:

要求:

write(file_path: str, content: str) -> WriteResult

作用:

要求:

edit(file_path: str, old_string: str, new_string: str, replace_all: bool = False) -> EditResult

作用:

要求:


结构化返回值为什么重要

Backend 协议要求大量操作都返回结构化结果而不是直接抛异常。

这样做的意义是:

常见返回类型包括:

以及这些基础结构:

如果要自定义 backend,建议先把这些结构彻底理解清楚,再开始编码。


常见错误与排查

用了 FilesystemBackend,却以为已经很安全

常见误区:

后果:

解决:

用了 LocalShellBackend,却以为 virtual_mode=True 也能兜底

这是错误理解。

因为只要给了 shell:

解决:

StoreBackend 数据串用户

常见原因:

解决:

CompositeBackend 路由不符合预期

常见原因:

解决:

自定义 backend 直接抛异常

常见问题:

解决:

从旧版升级后 backend 配置报弃用警告

常见原因:

解决:


验收标准

可以用下面这些标准判断后端配置是否合理:


推荐实操顺序

建议按这个顺序理解和落地:

  1. 先用 StateBackend 跑通最小链路
  2. 需要落盘时切换到 FilesystemBackend
  3. 需要长期记忆时引入 StoreBackend
  4. 需要混合存储时再上 CompositeBackend
  5. 需要执行命令时优先考虑 sandbox
  6. 只有在明确受控的本地环境下才使用 LocalShellBackend
  7. 最后再增加 permissions、policy wrapper 和自定义 backend

关键要点


写在最后

后端配置的核心,不是“让 Agent 能读写文件”这么简单,而是决定 Agent 在真实环境里如何存储、隔离、执行和受控。

可以把整个问题拆成三层:

当这三层设计清楚之后,Deep Agent 的文件系统能力才真正具备可落地性。