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 调用通常经过:
- 模型生成 read_file(file_path=”/reports/q2.md”);
- FilesystemMiddleware 解析参数并规范化虚拟路径;
- 如果应用配置了 FilesystemPermission,Middleware 执行 Tool 级权限判断;
- Middleware 从当前 Runtime 取得 Backend;
- Backend 从对应存储读取数据;
- Middleware 把文本格式化为带行号结果,或把二进制转换为标准内容块;
- 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 的生命周期

| 组件 | 保存什么 | 主要作用域 | 恢复或读取方式 |
|---|---|---|---|
| LangGraph State | 当前图运行的消息、Channel 和文件字段 | 当前 Run / Thread | 图节点和 Middleware 读取 |
| Checkpointer | State 的线程快照 | thread_id | 使用同一线程配置继续执行 |
| StateBackend | State 中的虚拟文件数据 | 当前线程 | 跟随 State 与 Checkpoint |
| BaseStore | 独立长期键值数据 | Namespace | 跨线程主动读取 |
| StoreBackend | 投影到 BaseStore 的虚拟文件 | Namespace | 通过 Runtime Store 访问 |
StateBackend 不是进程级共享内存:
- 同一次 Run 内可以读写;
- 同一 thread_id 跨 Turn 保留,通常依赖 Checkpointer;
- 换一个 thread_id 不会自动共享文件;
- 跨线程共享应使用 Store 或其他持久 Backend;
- 在图外直接调用 StateBackend 不能当作普通全局文件存储,它返回的 State 更新还需要由图应用。
即使进程一直运行,没有相同 Thread 和正确 Checkpoint,也不能据此推断文件会在下次调用出现。
2. 内置 Backend 与适用场景
2.1 四个选择维度

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 映射为
以及解析后逃逸根目录的路径。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 与最长前缀

推荐结构是让 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!
测试覆盖:
- 默认路径与 /reports/ 进入不同本机目录;
- 最长路由前缀优先;
- virtual_mode=True 拒绝路径穿越;
- 锁定版本拒绝使用 write 覆盖已有路径;
- Store 通过图 Runtime 注入,重建图后仍能读取长期文件;
- 两个租户 Namespace 相互隔离;
- URL 图片消息采用 LangChain 标准 image 块;
- PNG 经真实 read_file 转换为图片块;
- PDF 转换为文件块;
- MP3 转换为音频块;
- 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 选型顺序
- 只在当前 Thread 使用:StateBackend;
- 修改可信本地项目:FilesystemBackend;
- 跨 Thread 或跨会话读取:StoreBackend;
- 不同目录需要不同生命周期:CompositeBackend;
- 执行不可信代码:Sandbox Backend;
- 数据已经位于 S3、数据库或内部平台:实现 BackendProtocol;
- 无论使用哪种 Backend,都单独设计身份、权限、配额、清理和审计。
8. 总结
Deep Agents 文件系统提供统一工作区,Backend 才决定数据位置、生命周期、可见范围和执行能力。
本篇最重要的边界是:
- StateBackend 属于 Thread State,跨 Turn 依赖 Checkpointer 和同一 thread_id;
- StoreBackend 通过 Runtime BaseStore 和 Namespace 提供跨线程长期数据;
- CompositeBackend 使用最长路径前缀路由,但 execute 只看默认 Backend;
- 多模态要经过 Backend、LangChain 内容块、模型 Integration 和 Provider 四层能力验证;
- Permissions、虚拟路径、Sandbox 和人工审批解决不同安全问题,不能互相替代。