Deep Agents 4. 虚拟文件系统、Backend、持久化与多模态文件


1. Deep Agents 文件系统解决什么问题

1.1 文件系统作为外部工作区

长任务不适合把计划、资料、代码、中间结果和最终报告全部塞进消息历史。模型上下文有长度和
成本限制,多步骤任务也需要一个可重复读取、搜索和修改的工作区。

从上下文工程角度,可以把 Deep Agents 虚拟文件系统理解为 Agent 的外部工作记忆:

  • 模型只记住文件路径和必要摘要;
  • 大内容写入工作区,需要时再分段读取;
  • 主 Agent 与 SubAgent 可以通过约定目录交换产物;
  • 底层存储可以替换,而 Prompt 继续使用同一虚拟路径。

这是一种帮助理解的工程模型,不是对操作系统文件系统的重新定义。

1.2 Tool、Middleware 与 Backend

层次 主要职责 典型对象
Tool 向模型声明能力、描述和参数 read_file、write_file、glob
Middleware 解析 Tool、规范化路径、转换结果 FilesystemMiddleware
Backend 执行读写、搜索、上传和下载 StateBackend、FilesystemBackend

一次 read_file 调用通常经过:

  1. 模型生成 read_file(file_path=”/reports/q2.md”);
  2. FilesystemMiddleware 解析参数并规范化虚拟路径;
  3. 如果应用配置了 FilesystemPermission,Middleware 执行 Tool 级权限判断;
  4. Middleware 从当前 Runtime 取得 Backend;
  5. Backend 从对应存储读取数据;
  6. Middleware 把文本格式化为带行号结果,或把二进制转换为标准内容块;
  7. ToolMessage 返回模型,Agent Loop 继续执行。

权限位于内置文件 Tool 层,不是 Backend 的自动能力。直接调用 Backend,或者让自定义 Tool
自行访问文件,不会自动继承 FilesystemPermission。

1.3 虚拟路径

Agent 使用的是逻辑路径:

/workspace/plan.md
/reports/q2.md
/memories/preferences.txt
/skills/data-analysis/SKILL.md

虚拟路径不必等于宿主机路径。例如:

Agent 路径:  /reports/q2.md
路由前缀:    /reports/
Backend 路径:/q2.md
真实文件:    /srv/deepagent/reports/q2.md

Prompt、Skill 和业务流程应该依赖虚拟路径,而不是写死宿主机绝对路径。这样从本地目录切换到
Store 或 Sandbox 时,不需要修改模型指令。

1.4 State、Checkpoint 与 Store 的生命周期

State、Checkpointer 与 Store 的生命周期

组件 保存什么 主要作用域 恢复或读取方式
LangGraph State 当前图运行的消息、Channel 和文件字段 当前 Run / Thread 图节点和 Middleware 读取
Checkpointer State 的线程快照 thread_id 使用同一线程配置继续执行
StateBackend State 中的虚拟文件数据 当前线程 跟随 State 与 Checkpoint
BaseStore 独立长期键值数据 Namespace 跨线程主动读取
StoreBackend 投影到 BaseStore 的虚拟文件 Namespace 通过 Runtime Store 访问

StateBackend 不是进程级共享内存:

  1. 同一次 Run 内可以读写;
  2. 同一 thread_id 跨 Turn 保留,通常依赖 Checkpointer;
  3. 换一个 thread_id 不会自动共享文件;
  4. 跨线程共享应使用 Store 或其他持久 Backend;
  5. 在图外直接调用 StateBackend 不能当作普通全局文件存储,它返回的 State 更新还需要由图应用。

即使进程一直运行,没有相同 Thread 和正确 Checkpoint,也不能据此推断文件会在下次调用出现。

2. 内置 Backend 与适用场景

2.1 四个选择维度

Backend 持久化与隔离边界

Backend 不是从“弱”到“强”的单轴升级关系。选型时应同时判断:

维度 需要回答的问题
生命周期 文件只属于 Run、Thread,还是跨线程长期保存
可见范围 当前线程、用户、租户还是多个 Agent 可见
执行能力 只读写文件,还是实现 Shell 执行协议
信任边界 数据位于 State、本机、数据库还是隔离环境
Backend 数据位置 典型生命周期 适用场景
StateBackend LangGraph State 当前 Thread 草稿、临时资料、上下文卸载
FilesystemBackend 本机目录 跟随真实文件 本地项目、可信 CI
StoreBackend LangGraph BaseStore 跨 Thread 用户偏好、长期文档
CompositeBackend 多个 Backend 按路径决定 混合工作区与持久区
Sandbox Backend 隔离执行环境 由 Sandbox 服务决定 运行不可信代码和命令

2.2 StateBackend:线程工作区

没有传入 Backend 时,Deep Agents 使用 StateBackend():

agent = create_deep_agent(
    model=model,
    backend=StateBackend(),
)

它适合当前任务的笔记、草稿、自动卸载结果,以及同一线程中主 Agent 与 SubAgent 的文件交换。
需要恢复同一线程时,应配置 Checkpointer 并持续使用相同 thread_id。

2.3 FilesystemBackend 与 virtual_mode

FilesystemBackend 把文件映射到本机目录。正式示例应使用绝对根目录:

from pathlib import Path

from deepagents.backends import FilesystemBackend

project_root = Path(".backend-data/workspace").resolve()
backend = FilesystemBackend(
    root_dir=project_root,
    virtual_mode=True,
    max_file_size_mb=10,
)

virtual_mode=True 将 /notes/a.md 映射为 /notes/a.md,并拒绝 ..、~
以及解析后逃逸根目录的路径。P04 测试真实验证:

with pytest.raises(ValueError, match="Path traversal not allowed"):
    backend.default.write("/../secret.txt", "must-not-escape")

virtual_mode=False 允许真实绝对路径和更宽松的路径语义,风险更高。无论是否开启虚拟模式,
它都不是 Sandbox:路径约束不提供进程、网络、CPU、内存或系统调用隔离。

2.4 StoreBackend 与 Runtime Store

生产示例让 StoreBackend 在图执行时从 Runtime 获取 BaseStore:

from dataclasses import dataclass

from deepagents.backends import CompositeBackend, StateBackend, StoreBackend


@dataclass(frozen=True)
class AgentContext:
    tenant_id: str
    user_id: str


def build_hybrid_backend() -> CompositeBackend:
    persistent = StoreBackend(
        namespace=lambda runtime: (
            runtime.context.tenant_id,
            runtime.context.user_id,
            "filesystem",
        ),
    )
    return CompositeBackend(
        default=StateBackend(),
        routes={"/memories/": persistent},
    )

然后在 Agent 层注入 Store 和 Context Schema:

from langgraph.store.memory import InMemoryStore

store = InMemoryStore()
agent = create_deep_agent(
    model=model,
    backend=build_hybrid_backend(),
    store=store,
    context_schema=AgentContext,
)

result = agent.invoke(
    {"messages": [{"role": "user", "content": "记住我偏好中文回答"}]},
    context=AgentContext(tenant_id="tenant-a", user_id="user-1"),
)

deepagents==0.6.12 的实际构造签名仍支持 StoreBackend(store=…),适合图外的确定性 Backend
测试;本文生产主线采用 Runtime 注入,避免把 Store 生命周期固化在 Backend 对象中。部署平台也可以
为 Runtime 提供持久 Store。

Namespace 应使用应用已经验证的稳定身份,不要默认依赖某个部署平台才存在的
runtime.server_info。常见策略包括:

范围 Namespace 示例
用户级 (tenant_id, user_id, “filesystem”)
租户共享 (tenant_id, “shared-docs”)
Agent 级 (tenant_id, assistant_id, “memory”)
用户 + Agent (tenant_id, user_id, assistant_id)

Namespace 不能从会在任务中变化的 State 推导,否则同一个 Run 的前后步骤可能进入不同位置。

2.5 CompositeBackend 与最长前缀

CompositeBackend 路由与内部目录

推荐结构是让 StateBackend 承担默认工作区和 Harness 内部文件,只把明确目录路由到外部存储:

backend = CompositeBackend(
    default=StateBackend(),
    routes={
        "/workspace/": FilesystemBackend(
            root_dir=Path(".backend-data/workspace").resolve(),
            virtual_mode=True,
        ),
        "/memories/": StoreBackend(
            namespace=lambda rt: (
                rt.context.tenant_id,
                rt.context.user_id,
                "filesystem",
            )
        ),
    },
)

匹配 /workspace/report.md 后,路由前缀会被移除,FilesystemBackend 收到 /report.md。
存在多个前缀时使用最长前缀:

/reports/             -> 团队报告区
/reports/private/     -> 私密报告区

/reports/private/a.md 进入更长的 /reports/private/ 路由,而不是依赖字典插入顺序。

2.6 execute 只由默认 Backend 决定

ls、read、write、edit、glob、grep 和上传下载都带路径,可以进行前缀路由;
execute 没有文件路径,不能根据命令文本猜测路由。

因此 CompositeBackend 的命令执行能力只取决于默认 Backend。把 StateBackend 设为 default,
再增加 /sandbox/ 路由,并不会让 execute 自动进入该 Sandbox。需要执行时,应明确设计默认
Sandbox 和文件路由,同时重新评估信任边界。

3. 自定义 Backend Protocol

3.1 核心方法和结果类型

自定义 S3、PostgreSQL 或企业文档 Backend 时,应实现 BackendProtocol 的结构化契约:

方法 返回类型 deepagents==0.6.12 约定
ls(path) LsResult 返回直接子项与元数据
read(path, offset, limit) ReadResult 文本分页或 Base64 二进制
write(path, content) WriteResult 创建文件;内置实现拒绝已有路径
edit(…) EditResult 精确替换并返回替换次数
glob(pattern, path) GlobResult 返回结构化文件信息
grep(pattern, path, glob) GrepResult 返回路径、行号和文本
upload_files(files) FileUploadResponse[] 批量上传字节
download_files(paths) FileDownloadResponse[] 批量下载字节

这里必须说明一个版本漂移:部分当前概念文档把 write 描述为“创建或覆盖”,但本文安装的
0.6.12 源码中,StateBackend、FilesystemBackend 和 StoreBackend 都会在路径已存在时返回
Cannot write … because it already exists。P04 也用真实 Backend 固定了这一行为。

所以本文代码遵循锁定版本的“创建后使用 edit”语义。升级 Deep Agents 时应重新执行契约测试,
不能把 0.6.12 的行为外推成永久 API,也不能仅根据未注明版本的概念页面改写 Backend。

旧的 ls_info、glob_info 和 grep_raw 属于兼容接口,0.6.12 会产生弃用警告,计划在 0.7.0
移除;新实现应使用 ls、glob 和 grep。

3.2 结构化错误与异常边界

文件不存在、目标字符串不唯一和权限不足通常是模型可以恢复的失败,应返回结构化错误:

def read(self, file_path: str, offset: int = 0, limit: int = 2000):
    item = self.storage.get(file_path)
    if item is None:
        return ReadResult(error=f"File '{file_path}' not found")
    return ReadResult(
        file_data={"content": item, "encoding": "utf-8"}
    )

Middleware 会把结果转换成模型可理解的 Tool Message。程序缺陷、无法启动的依赖和错误的初始化
配置才适合抛出异常。不能把完整堆栈、SQL、真实路径和密钥放进错误结果。

3.3 write_file 与 edit_file

在本文锁定版本中:

  • write_file 用于创建新文件;已有路径会返回错误;
  • edit_file 对现有文本执行精确字符串替换;
  • 默认 replace_all=False 时,旧字符串应恰好出现一次;
  • 批量替换必须显式设置 replace_all=True。
result = backend.edit(
    "/reports/q2.md",
    old_string="status: draft",
    new_string="status: reviewed",
)

如果未来版本把 write_file 改为整体覆盖,它与 edit_file 的职责仍然不同:一个替换完整文件,
另一个对已知旧文本执行局部、可检查的修改。

3.4 二进制上传与下载

v2 文件数据结构包含文本或 Base64 内容:

{
    "content": "文本或 Base64 字符串",
    "encoding": "utf-8" | "base64",
    "created_at": "可选 ISO 时间",
    "modified_at": "可选 ISO 时间",
}

二进制文件通常由应用通过 upload_files 上传:

responses = backend.upload_files(
    [("/inputs/architecture.png", image_bytes)]
)
if responses[0].error:
    raise RuntimeError(responses[0].error)

批量接口允许部分成功,调用方必须逐项检查 error,不能因为函数没有抛异常就认定全部成功。

3.5 外部持久化、State 与并发

对纯外部持久化 Backend,文件正文通常不应该再通过 files_update 复制进 LangGraph State,
否则同一内容会同时进入外部存储和 Checkpoint,造成重复和膨胀。

如果业务需要在 State 保存索引、ETag 或同步状态,应定义独立、明确的 State 字段;这不等于把
完整文件正文重复存入 files_update。自定义 Backend 还要处理路径规范化、租户隔离、分页、
乐观锁、部分成功、配额、保留期限和删除语义。

4. 多模态文件处理

4.1 从 Backend 字节到模型能力

多模态文件的三层转换边界

多模态文件不是“Backend 直接交给模型”,而是五层转换:

Backend bytes
    -> Deep Agents read_file
    -> LangChain standard content block
    -> Model Integration Adapter
    -> Provider-native request
    -> Model capability

Deep Agents 负责读取与标准表达,模型 Integration 负责转换为 Provider 请求,最终是否支持某种
媒体仍由 Provider、模型和端点决定。

4.2 read_file 标准内容块

图片经过真实 read_file Tool 可以得到 LangChain 标准内容块:

{
    "type": "image",
    "base64": "iVBORw0KGgo...",
    "mime_type": "image/png",
}

P04 不是手工拼接该结构,而是创建 FilesystemMiddleware,取得真实 read_file Tool,上传
最小二进制样本后检查 ToolMessage.content_blocks。

4.3 本文锁定环境中的实测映射

在下述锁定版本中,测试观察到:

文件 LangChain 内容块 MIME Type
diagram.png image image/png
report.pdf file application/pdf
meeting.mp3 audio audio/mpeg
demo.mp4 video video/mp4

这是作者对当前依赖组合的实测,不是所有 Deep Agents 版本、Backend、MIME 推断环境和 Provider
的统一保证。官方明确保证和具体扩展名映射应继续以相应版本文档、源码和测试为准。

4.4 URL、Base64、Backend 路径与 File ID

媒体已经有可信 URL 时,可以使用 LangChain 标准消息格式:

def image_question(url: str) -> dict:
    return {
        "role": "user",
        "content": [
            {"type": "text", "text": "请解释这张架构图。"},
            {"type": "image", "url": url},
        ],
    }

这不是所有 Provider 都直接接受的原生 HTTP JSON。具体 Chat Model Integration 还要把标准内容块
转换为 Provider 格式,端点本身也必须支持 URL 图片。

入口 优点 风险或限制
URL 消息体小 SSRF、链接失效、Provider 无法访问
Base64 不依赖公网地址 请求体膨胀、大小限制
Backend 路径 统一工作区、可反复读取 最终仍需转换成模型内容块
Provider File ID 可复用上传文件 绑定 Provider 和文件生命周期

4.5 大文件预处理与上下文成本

文本使用 offset 和 limit 分页。代码库与日志通常先 glob,再 grep,最后读取相关范围。

图片、PDF、音频和视频不能使用文本行分页。大媒体进入模型前应按任务进行压缩、切页、OCR、
抽帧、转写或摘要。把几百 MB 视频直接 Base64 写进消息既昂贵,也很容易超过请求限制。

5. 自动上下文卸载

5.1 大 Tool Result 如何进入 Backend

FilesystemMiddleware 不只处理模型主动调用的文件 Tool。Tool Result 超过阈值时,它可以把
完整结果写入 Backend,在消息中只留下截断预览和文件引用。

在 deepagents==0.6.12 默认配置中:

tool_token_limit_before_evict = 20000

这是 Middleware 的版本相关默认值,不是跨版本协议常量,也可能被配置或 Harness Profile 改变。

5.2 Harness 内部目录

当前版本会在 artifacts_root 下使用内部目录,包括:

/large_tool_results/
/conversation_history/

当普通 Backend 直接作为默认 Backend 时,artifacts_root 为 /。如果 default 是
FilesystemBackend,这些内部文件会落到真实磁盘,并可能与项目文件混在一起。

推荐让 StateBackend 作为 default,再把 /workspace/、/reports/ 和 /memories/ 显式路由
到对应 Backend。这样 Harness 内部产物默认留在线程 State,业务文件的生命周期更清晰。

5.3 配额、清理与审计

自动生成的文件也需要治理:

  • 单文件与总容量限制;
  • 线程、用户和租户配额;
  • 过期和删除策略;
  • 敏感数据扫描;
  • 写入来源、大小、路径和 Trace ID 审计;
  • 外部持久区的备份与恢复。

如果根路径误路由到长期 Store,大 Tool Result 和历史内容可能被永久保留,形成成本和合规风险。

6. 安全与生产实践

6.1 四种不能互相替代的控制

虚拟路径:统一路径并限制常见路径逃逸
Permissions:约束内置文件 Tool 的路径和操作
Sandbox:隔离进程、文件、网络和资源
Human-in-the-loop:敏感操作执行前等待人工决定

virtual_mode=True 仍允许 Agent 删除根目录内文件;Permissions 不会自动约束自定义 Tool;
Sandbox 仍需限制网络和密钥;人工审批也不能代替服务端授权。

6.2 Namespace 与多租户

Namespace 必须包含经过认证的租户或用户身份,并在服务端构造,不能直接信任模型参数。

测试应至少覆盖:

  • 同一租户和用户可以重建图后读取长期文件;
  • 不同租户访问同一路径得到隔离结果;
  • 用户被删除或迁移时如何处理旧 Namespace;
  • Namespace 字段是否合法且不会产生通配符注入。

6.3 LocalShellBackend 风险

LocalShellBackend 同时提供宿主机文件和 Shell 能力,适合受控本地开发,不适合作为多租户
服务的默认执行环境。生产执行不可信代码时,应使用隔离、有限权、可审计、可销毁的 Sandbox。

6.4 自定义 Backend 检查清单

检查项 需要验证的问题
路径 是否规范化并阻止 ..、软链接逃逸和非法前缀
身份 Namespace 是否包含可信租户和用户
数据格式 UTF-8、Base64、MIME 和时间字段是否一致
查询 ls、glob、grep 是否服务端过滤并分页
并发 是否使用 ETag、版本号或事务防止覆盖
批处理 上传下载是否逐项返回成功和失败
可靠性 超时、重试、限流和熔断是否有边界
生命周期 大小、配额、保留、删除和清理是否明确
可观测性 日志是否脱敏,能否关联 Trace 与调用者

7. 测试、排错与 Backend 选型

7.1 测试环境

项目 本文环境
操作系统 macOS 15.7.3,arm64
Python 3.12.11
Deep Agents deepagents==0.6.12
LangChain Core langchain-core==1.4.9
LangGraph langgraph==1.2.9
模型 Integration langchain-openai==1.3.5
在线端点 Endpoint A,第三方 OpenAI-compatible 网关
在线模型 ID qwen3.7-plus,可能是网关别名

7.2 十一项离线契约测试

运行:

cd source/_posts/deepagent/examples
.venv_deepagent/bin/python -m pytest p04_backends_multimodal -q
.venv_deepagent/bin/python -m ruff check p04_backends_multimodal

结果:

...........                                                              [100%]
All checks passed!

测试覆盖:

  1. 默认路径与 /reports/ 进入不同本机目录;
  2. 最长路由前缀优先;
  3. virtual_mode=True 拒绝路径穿越;
  4. 锁定版本拒绝使用 write 覆盖已有路径;
  5. Store 通过图 Runtime 注入,重建图后仍能读取长期文件;
  6. 两个租户 Namespace 相互隔离;
  7. URL 图片消息采用 LangChain 标准 image 块;
  8. PNG 经真实 read_file 转换为图片块;
  9. PDF 转换为文件块;
  10. MP3 转换为音频块;
  11. MP4 转换为视频块。

这些是 Backend、Middleware 和内容块的离线契约测试,不是完整 Agent E2E。取得真实
read_file Tool 并不代表某个 Provider 已经接受和理解内容块。

7.3 真实视觉模型在线验证

离线测试之后,本文把 Backend 产生的图片块交给真实视觉模型:

{
  "scene": "p04_backend_vision",
  "status": "passed",
  "model": "qwen3.7-plus",
  "latency_ms": 7612,
  "final_answer": "可恢复 Run 生命周期",
  "backend_block": {
    "type": "image",
    "mime_type": "image/png",
    "base64_bytes": 114480
  },
  "usage": {
    "input_tokens": 1022,
    "output_tokens": 94,
    "total_tokens": 1116
  }
}

qwen3.7-plus 是 Endpoint A 返回的模型 ID,可能是网关别名,不代表所有 Provider 都存在同名
模型。结果只证明在本文锁定的 Integration、OpenAI-compatible 网关和模型组合中,
PNG 的三层转换可以贯通;不能外推到其他端点、PDF、音频或视频。

7.4 常见错误

现象 常见原因 处理方式
文件落到意外目录 root_dir 是相对路径 使用 Path(…).resolve()
同一 Thread 下次读不到 State 文件 没有 Checkpointer 或换了 thread_id 配置 Checkpointer 并保持线程 ID
/memories/ 没有跨线程 使用临时 Store 或没有 Runtime Store 注入持久 BaseStore 并测试重建图
不同用户看到同一文件 Namespace 缺少租户/用户 重构 Namespace 并迁移旧数据
write_file 报已存在 0.6.12 内置实现采用创建语义 使用 edit_file;升级后重跑契约测试
路由进入错误 Backend 前缀重叠或格式错误 使用绝对目录前缀并测试最长匹配
Composite 没有 execute 默认 Backend 不支持执行 重新设计默认 Sandbox,不根据路径猜命令路由
图片被识别为普通文件 扩展名或 MIME 推断不匹配 检查锁定版本内容块和 MIME
Provider 拒绝媒体 Integration 转换或模型能力不支持 独立做真实 Provider 能力测试
本机磁盘出现内部文件 FilesystemBackend 是 default 使用 State default 和显式业务路由

7.5 选型顺序

  1. 只在当前 Thread 使用:StateBackend;
  2. 修改可信本地项目:FilesystemBackend;
  3. 跨 Thread 或跨会话读取:StoreBackend;
  4. 不同目录需要不同生命周期:CompositeBackend;
  5. 执行不可信代码:Sandbox Backend;
  6. 数据已经位于 S3、数据库或内部平台:实现 BackendProtocol;
  7. 无论使用哪种 Backend,都单独设计身份、权限、配额、清理和审计。

8. 总结

Deep Agents 文件系统提供统一工作区,Backend 才决定数据位置、生命周期、可见范围和执行能力。

本篇最重要的边界是:

  1. StateBackend 属于 Thread State,跨 Turn 依赖 Checkpointer 和同一 thread_id;
  2. StoreBackend 通过 Runtime BaseStore 和 Namespace 提供跨线程长期数据;
  3. CompositeBackend 使用最长路径前缀路由,但 execute 只看默认 Backend;
  4. 多模态要经过 Backend、LangChain 内容块、模型 Integration 和 Provider 四层能力验证;
  5. Permissions、虚拟路径、Sandbox 和人工审批解决不同安全问题,不能互相替代。

文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录