【AgentScope】6.文件系统(Filesystem)详解

文件系统(Filesystem)详解

一句话概括

文件系统是 Agent 的"文件柜"------它决定了 Agent 的文件存在哪里:是本机磁盘、远程存储,还是隔离的沙箱环境。

你能学到什么

  • 为什么需要"抽象文件系统"?直接用 Java 的 File API 不行吗?
  • 三种文件系统模式的区别:本机模式、共享存储模式、沙箱模式
  • 什么是"多租户隔离"?为什么要用 NamespaceFactory
  • 类层次结构:AbstractFilesystem 家族成员之间的关系
  • 如何根据你的场景选择正确的文件系统配置

前置知识

需了解 Java 接口与继承的基本概念,以及 05-memory.md 中 Agent 的双层记忆系统------记忆最终要落到文件系统上。


核心概念

AbstractFilesystem --- 文件柜的"统一说明书"

生活类比 :想象你去图书馆借书。图书馆有各种藏书的地方:普通书架、珍本室、电子资源库。但作为读者,你只需要知道"我要借这本书",图书馆员会帮你从正确的地方取。AbstractFilesystem 就是那个"统一的借书接口"------不管文件实际存在哪里,Agent 都用同样的方式操作。

技术解释AbstractFilesystem 是一个接口,定义了所有文件系统都必须支持的基本操作:

java 复制代码
public interface AbstractFilesystem {
    // 列出目录内容(就像在文件管理器里打开文件夹)
    List<String> ls(String path);

    // 读取文件内容(就像用记事本打开文件)
    String read(String path);

    // 写入文件内容(就像保存记事本内容)
    void write(String path, String content);

    // 编辑文件(就像在 Word 里修改文档)
    void edit(String path, String oldText, String newText);

    // 在文件中搜索(就像 Ctrl+F 查找)
    List<String> grep(String pattern, String path);

    // 用通配符找文件(就像搜索 *.txt)
    List<String> glob(String pattern);

    // 上传文件
    void uploadFiles(Map<String, String> files);

    // 下载文件
    Map<String, String> downloadFiles(List<String> paths);
}

为什么需要抽象? 如果直接用 Java 的 File API,Agent 就只能操作本机文件。但实际场景中,我们可能需要:

  • 把 Agent 的记忆存到云端(多台机器共享)
  • 在沙箱里执行不受信任的代码
  • 根据用户 ID 隔离数据(多租户)

抽象接口让这些需求变成"换个实现类"的事,而不是"重写整个 Agent"。


三种声明式模式 --- 选哪种文件柜?

生活类比:假设你要开一家公司,需要一个地方存档案。你有三种选择:

  1. 本机模式:在公司自己的档案室里放铁皮柜------方便、省钱,但只有一个房间,不能多地点共享。
  2. 共享存储模式:租用云端档案服务------多个分公司都能访问同一份档案,但贵一些,而且不能在档案室里"做实验"(不能执行脚本)。
  3. 沙箱模式:租一个独立的实验室------里面有档案柜,还能做化学实验(执行脚本),就算爆炸了也不会影响主楼。

技术对比表

模式 配置方法 能否执行 Shell 适用场景
模式 1:本机 + shell filesystem(LocalFilesystemSpec) 或默认 能(宿主机上) 单机部署、测试环境、受信任的 Agent
模式 2:复合 + Store filesystem(RemoteFilesystemSpec) 不能 多副本共享记忆、生产环境、不需要执行脚本
模式 3:沙箱 filesystem(SandboxFilesystemSpec) 能(沙箱内) 需要隔离执行、处理不受信任的代码

下面我们逐一详解这三种模式。


模式一:本机 + Shell(LocalFilesystemSpec)--- 自己的档案室

生活类比:你在自家书房里放了一个文件柜。你想存什么就存什么,想在柜子上贴便签、想用订书机装订文件都可以------这是你自己的地盘。但问题是:如果家里来客人了,他们也能翻你的柜子;如果房子着火了,文件就没了。

技术细节

java 复制代码
// 模式 1:本机 + shell(默认就是这种)
HarnessAgent agent = HarnessAgent.builder()
    .name("local")                    // Agent 的名字
    .model(model)                     // 使用的大模型
    .workspace(workspace)             // 工作区目录
    .filesystem(new LocalFilesystemSpec()
        .executeTimeoutSeconds(120))  // Shell 命令超时时间
    .build();

工作原理

  1. LocalFilesystemWithShell 是实际创建的类
  2. 根目录就是你的 workspace 目录
  3. 执行 Shell 命令时,直接在宿主机上跑 sh -c 你的命令
  4. 所有文件操作都是普通的 Java 文件 API(FileFiles 等)

适用场景

  • 你在开发测试阶段,想快速跑起来
  • 你信任 Agent 执行的代码(不会恶意删除文件)
  • 你只需要单机部署,不需要多机器共享数据

安全提醒 :因为能直接在宿主机执行 Shell,如果 Agent 被诱导执行 rm -rf /,后果很严重!生产环境要谨慎使用。


模式二:复合 + 共享存储(RemoteFilesystemSpec)--- 云端档案服务

生活类比:你的公司在多个城市有分部,每个分部都需要查看同一份客户档案。于是你租了一个专业的档案托管服务------所有档案存在云端,每个分部都能访问。但是,这个托管服务只负责存取档案,不能在里面做实验或开派对。

技术细节

java 复制代码
// 模式 2:共享存储(无宿主 shell)
HarnessAgent agent = HarnessAgent.builder()
    .name("store")
    .model(model)
    .workspace(workspace)
    .filesystem(new RemoteFilesystemSpec(redisStore)  // Redis 作为后端存储
        .isolationScope(IsolationScope.USER))          // 按用户隔离
    .build();

工作原理

  1. 创建的是 CompositeFilesystem------一个"组合型"文件系统
  2. 默认路径(未匹配的路径)→ 本地文件系统(无 Shell)
  3. 共享路径 (如 MEMORY.mdmemory/sessions/)→ 远程存储(Redis、数据库等)
  4. 通过 IsolationScope 控制不同用户/会话的数据隔离

共享路径是什么?

默认情况下,以下路径会存到远程存储:

复制代码
MEMORY.md           # Agent 的长期记忆
memory/             # 记忆系统的详细记录
agents/<agentId>/sessions/  # 会话日志

你可以用 addSharedPrefix("shared/") 添加更多共享路径。

为什么默认没有 Shell? 设计目标是跨节点一致的长记忆与日志。如果允许在宿主机执行 Shell,多个节点可能会产生冲突。如果你需要 Shell 能力,请选择模式 1 或模式 3。

适用场景

  • 生产环境,需要多副本共享数据
  • Agent 不需要执行 Shell 命令
  • 需要按用户/会话隔离数据(多租户)

模式三:沙箱(SandboxFilesystemSpec)--- 安全实验室

生活类比:你有一间化学实验室,里面放着一个防爆玻璃做的"安全箱"。你可以在里面做危险的化学实验------就算发生爆炸,也只损坏箱子里的东西,主实验室完好无损。而且这个安全箱有专门的通风管道和排污系统,与主楼隔离。

技术细节

java 复制代码
// 模式 3:沙箱(以 Docker 为例)
HarnessAgent agent = HarnessAgent.builder()
    .name("sandbox")
    .model(model)
    .workspace(workspace)
    .filesystem(dockerFilesystemSpec)  // 继承自 SandboxFilesystemSpec
    .build();

工作原理

  1. 创建的是 SandboxBackedFilesystem
  2. 文件操作和 Shell 执行都在沙箱容器内进行
  3. 通过 SandboxClient 与沙箱通信
  4. 沙箱有独立的生命周期管理:创建、快照、恢复、销毁

沙箱的生命周期

复制代码
┌─────────────────────────────────────────┐
│              单次 Agent 调用             │
├─────────────────────────────────────────┤
│                                         │
│   ┌─────────┐    ┌─────────┐           │
│   │ acquire │ →  │ execute │ →         │
│   │ (获取)   │    │ (执行)   │    ...    │
│   └─────────┘    └─────────┘           │
│          ↓              ↓               │
│   ┌─────────┐    ┌─────────┐           │
│   │ persist │ ←  │ release │           │
│   │ (持久化) │    │ (释放)   │           │
│   └─────────┘    └─────────┘           │
│                                         │
└─────────────────────────────────────────┘

适用场景

  • 需要执行不受信任的代码
  • 需要强隔离的多租户环境
  • 需要可恢复的执行状态(比如暂停后继续)
  • 需要执行快照和回滚

详细配置 :参见 11-sandbox.md


NamespaceFactory --- 多租户的"分房间管理"

生活类比 :想象一个大型写字楼,里面有很多公司租办公室。每家公司都有自己的"区域"------A 公司在 101 室,B 公司在 102 室。虽然是同一栋楼、同一个物业管理,但 A 公司不能进 B 公司的办公室。NamespaceFactory 就是那个"前台接待员"------根据你是哪家公司(用户 ID),把你带到正确的房间。

技术细节

java 复制代码
@FunctionalInterface
public interface NamespaceFactory {
    List<String> getNamespace();  // 返回路径前缀列表
}

每次文件操作时调用,返回当前请求应该使用的路径前缀。例如:

隔离级别 路径前缀示例 说明
GLOBAL [] 全局共享,无前缀
AGENT ["agents", "myAgent"] 按 Agent 隔离
USER ["users", "alice"] 按用户隔离
SESSION ["sessions", "sess-123"] 按会话隔离

实际应用

java 复制代码
// 从 RuntimeContext 获取当前用户 ID,用于命名空间
AtomicReference<String> currentUserId = new AtomicReference<>();

HarnessAgent agent = HarnessAgent.builder()
    .namespaceFactory(() -> {
        String userId = currentUserId.get();
        return List.of("users", userId);  // 每个用户有自己的目录
    })
    .build();

// 这样,用户 alice 的文件存在 /users/alice/ 下
// 用户 bob 的文件存在 /users/bob/ 下
// 互不干扰

为什么重要? 如果没有命名空间隔离:

  • 用户 A 可能读取用户 B 的私密记忆
  • 不同会话的日志可能混在一起
  • 多个 Agent 实例可能互相覆盖配置

AbstractSandboxFilesystem --- 带执行能力的文件系统

生活类比 :普通的文件柜只能存取文件。但如果你的文件柜里还有一个"小机器人",能帮你执行指令呢?AbstractSandboxFilesystem 就是这样的"带机器人的文件柜"------不仅能存取文件,还能在里面执行命令。

技术细节

java 复制代码
public interface AbstractSandboxFilesystem extends AbstractFilesystem {
    // 沙箱的唯一标识
    String id();

    // 在沙箱内执行命令
    ExecuteResult execute(String cmd, Duration timeout);
}

继承关系 :只有实现了 AbstractSandboxFilesystem 的文件系统才会注册 ShellExecuteTool(Shell 执行工具)。这意味着:

  • LocalFilesystemWithShell --- 有 Shell(本地执行)
  • SandboxBackedFilesystem --- 有 Shell(沙箱内执行)
  • CompositeFilesystem --- 没有 Shell(只是路由器)
  • RemoteFilesystem --- 没有 Shell(只是存储)

关键代码解读

文件系统配置的选择逻辑

java 复制代码
// HarnessAgent.Builder 内部的选择逻辑(简化版)

public HarnessAgent build() {
    AbstractFilesystem fs;

    if (remoteFilesystemSpec != null) {
        // 模式 2:复合 + 共享存储
        fs = remoteFilesystemSpec.toFilesystem();
        // 注意:CompositeFilesystem 不实现 AbstractSandboxFilesystem
        // 所以不会注册 ShellExecuteTool

    } else if (sandboxFilesystemSpec != null) {
        // 模式 3:沙箱
        fs = sandboxFilesystemSpec.toFilesystem();
        // SandboxBackedFilesystem 实现了 AbstractSandboxFilesystem
        // 所以会注册 ShellExecuteTool

    } else {
        // 模式 1:本机 + shell(默认)
        fs = new LocalFilesystemWithShell(workspace, executeTimeout);
        // LocalFilesystemWithShell 实现了 AbstractSandboxFilesystem
        // 所以会注册 ShellExecuteTool
    }

    // 如果实现了 AbstractSandboxFilesystem,注册 ShellExecuteTool
    if (fs instanceof AbstractSandboxFilesystem) {
        toolkit.register(new ShellExecuteTool((AbstractSandboxFilesystem) fs));
    }

    // 注册基础文件工具
    toolkit.register(new FilesystemTool(fs));
}

逐行注释

  • 第 5 行 :检查是否配置了 RemoteFilesystemSpec(共享存储模式)
  • 第 7 行 :创建 CompositeFilesystem 实例
  • 第 9-10 行 :关键点!CompositeFilesystem 不继承 AbstractSandboxFilesystem,所以不能执行 Shell
  • 第 12-16 行:检查是否配置了沙箱模式,创建沙箱文件系统
  • 第 18-22 行:默认情况,创建本机文件系统,可以执行 Shell
  • 第 25-27 行 :判断是否注册 Shell 工具的关键逻辑------只有实现了 AbstractSandboxFilesystem 才注册
  • 第 30 行:无论哪种模式,都注册基础的文件操作工具

CompositeFilesystem 的路由逻辑

java 复制代码
// CompositeFilesystem 的读写路由(简化版)

public class CompositeFilesystem implements AbstractFilesystem {

    private final AbstractFilesystem defaultBackend;     // 默认后端(本地)
    private final Map<String, AbstractFilesystem> routes; // 前缀 -> 后端的映射

    @Override
    public String read(String path) {
        // 遍历所有前缀,找最长匹配
        String matchedPrefix = findLongestMatchPrefix(path);

        if (matchedPrefix != null) {
            // 找到匹配的前缀,用对应的远程后端
            AbstractFilesystem backend = routes.get(matchedPrefix);
            return backend.read(stripPrefix(path, matchedPrefix));
        } else {
            // 没匹配到,用默认的本地后端
            return defaultBackend.read(path);
        }
    }

    private String findLongestMatchPrefix(String path) {
        String longestMatch = null;
        for (String prefix : routes.keySet()) {
            if (path.startsWith(prefix)) {
                if (longestMatch == null || prefix.length() > longestMatch.length()) {
                    longestMatch = prefix;
                }
            }
        }
        return longestMatch;
    }
}

逐行注释

  • 第 5 行 :默认后端,通常是 LocalFilesystem(无 Shell)
  • 第 6 行:路由表,存储"前缀 -> 后端"的映射
  • 第 10 行 :最长前缀匹配------如果有 memory/memory/important/ 两个前缀,memory/important/file.txt 会匹配后者
  • 第 13-15 行 :找到匹配后,去掉前缀再传给后端。例如 memory/diary.txt 匹配 memory/ 前缀后,传给远程后端的路径是 diary.txt
  • 第 17-19 行:没匹配到就用默认后端

WorkspaceIndex 索引加速

java 复制代码
// RemoteFilesystem 使用本地索引加速查询

public class RemoteFilesystem implements AbstractFilesystem {

    private final BaseStore store;           // 远程存储后端
    private final WorkspaceIndex index;      // 本地 SQLite 索引

    @Override
    public List<String> ls(String path) {
        // 先查本地索引(快)
        List<String> fromIndex = index.list(path);
        if (!fromIndex.isEmpty()) {
            return fromIndex;
        }

        // 索引没命中,回退到远程存储扫描(慢但完整)
        return store.listAll(path);
    }

    @Override
    public List<String> grep(String pattern, String path) {
        // 先用索引找候选文件
        List<String> candidates = index.findCandidates(path);

        if (candidates.isEmpty()) {
            // 索引返回空,可能是还没有索引到
            // 回退到全量扫描,确保不遗漏其他节点写入的内容
            return store.grepAll(pattern, path);
        }

        // 在候选文件中搜索
        return searchInCandidates(pattern, candidates);
    }
}

逐行注释

  • 第 5-6 行:索引是可选的性能优化,存储在本地 SQLite 中
  • 第 10-12 行:先查本地索引,速度快
  • 第 15-16 行:索引没命中时,回退到远程扫描。这是"尽力而为"的策略------索引可能不包含其他节点新写入的内容,但全量扫描保证不遗漏
  • 第 20-30 行grep 同样的策略------先索引,后回退

整体流程图

文件系统类层次结构

复制代码
                        ┌─────────────────────┐
                        │  AbstractFilesystem │ ← 文件操作的统一接口
                        │  ───────────────────│
                        │  ls/read/write/edit │
                        │  grep/glob/upload   │
                        │  download           │
                        └─────────┬───────────┘
                                  │
          ┌───────────────────────┼───────────────────────┐
          │                       │                       │
          ▼                       ▼                       ▼
┌─────────────────┐   ┌─────────────────┐   ┌─────────────────┐
│ LocalFilesystem │   │ RemoteFilesystem│   │CompositeFilesystem│
│ (纯本地,无Shell)│   │ (KV存储后端)    │   │ (路由器,组合多个) │
└────────┬────────┘   └─────────────────┘   └─────────────────┘
         │
         │ 继承
         ▼
┌─────────────────────────┐
│LocalFilesystemWithShell │ ← 本地 + Shell 执行
│ implements              │
│ AbstractSandboxFilesystem│
└─────────────────────────┘

                        ┌─────────────────────┐
                        │AbstractSandboxFilesystem│ ← 带执行能力的接口
                        │ ───────────────────│
                        │ + id()             │
                        │ + execute()        │
                        └─────────┬───────────┘
                                  │
          ┌───────────────────────┼───────────────────────┐
          │                       │                       │
          ▼                       ▼                       ▼
┌─────────────────────┐ ┌─────────────────┐ ┌─────────────────────┐
│LocalFilesystemWithShell│ │BaseSandboxFilesystem│ │SandboxBackedFilesystem│
│ (本地执行)           │ │ (远程Unix基类)   │ │ (沙箱代理)         │
└─────────────────────┘ └─────────────────┘ └─────────────────────┘

简单解读

  1. AbstractFilesystem 是最顶层的接口,定义了所有文件系统都能做的事:读、写、列目录、搜索等。

  2. AbstractSandboxFilesystem 扩展了上层接口,增加了"执行命令"的能力。只有继承它才能注册 ShellExecuteTool

  3. LocalFilesystem 是最简单的实现------就是操作本地磁盘,不能执行命令。

  4. LocalFilesystemWithShell 在本地文件系统基础上,增加了"执行 Shell 命令"的能力。

  5. RemoteFilesystem 把文件存到远程 KV 存储(如 Redis),适合多实例共享。

  6. CompositeFilesystem 是个"路由器"------根据文件路径前缀,把请求转发到不同的后端。

  7. SandboxBackedFilesystem 把所有操作都转发到沙箱容器内执行。

三种模式的工作流程

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        Agent 调用文件操作                         │
└─────────────────────────────────┬───────────────────────────────┘
                                  │
                                  ▼
                    ┌─────────────────────────────┐
                    │    选择哪种文件系统模式?     │
                    └─────────────┬───────────────┘
                                  │
        ┌─────────────────────────┼─────────────────────────┐
        │                         │                         │
        ▼                         ▼                         ▼
┌───────────────┐     ┌───────────────────┐     ┌───────────────────┐
│ 模式 1:本机   │     │ 模式 2:复合+Store │     │ 模式 3:沙箱      │
│               │     │                   │     │                   │
│ 本地磁盘      │     │ 路由到不同后端     │     │ Docker/容器       │
│ sh -c 执行    │     │ 共享路径→Redis    │     │ 隔离执行          │
│               │     │ 本地路径→本地     │     │                   │
└───────┬───────┘     └─────────┬─────────┘     └─────────┬─────────┘
        │                       │                         │
        ▼                       ▼                         ▼
┌───────────────┐     ┌───────────────────┐     ┌───────────────────┐
│ workspace/    │     │ MEMORY.md → Redis │     │ 沙箱容器内        │
│ ├── MEMORY.md │     │ memory/   → Redis │     │ 独立的文件系统    │
│ ├── memory/   │     │ agents/   → Redis │     │ 独立的进程空间    │
│ └── sessions/ │     │ 其他     → 本地   │     │ 可快照/恢复      │
└───────────────┘     └───────────────────┘     └───────────────────┘

与其他模块的关系


⬅️ 上一篇:05-memory | 📖 回到目录 | ➡️ 下一篇:07-tool


学习要点

必须记住的三个概念

  1. 抽象接口的价值AbstractFilesystem 让 Agent 代码与存储实现解耦。今天存本地,明天存 Redis,后天存 S3------Agent 代码不需要改。

  2. 三种模式的本质区别

    • 本机模式 = 文件在本地 + Shell 在本地执行
    • 复合模式 = 文件在远程 + 无 Shell
    • 沙箱模式 = 文件在沙箱 + Shell 在沙箱执行
  3. 命名空间隔离 :多租户场景下,必须用 NamespaceFactory 隔离不同用户的数据。

常见错误

错误 现象 解决方案
在复合模式下调用 Shell ShellExecuteTool 不存在 改用本机模式或沙箱模式
忘记配置命名空间 用户 A 能看到用户 B 的数据 配置 NamespaceFactory
在本机模式执行危险命令 误删宿主机文件 使用沙箱模式隔离
沙箱忘记持久化 容器重启后数据丢失 配置 SandboxStateStore

选择指南

复制代码
                    ┌─────────────────────┐
                    │ 需要执行 Shell 吗? │
                    └─────────┬───────────┘
                              │
              ┌───────────────┴───────────────┐
              │ 否                            │ 是
              ▼                               ▼
    ┌─────────────────┐             ┌─────────────────┐
    │ 需要多实例共享? │             │ 信任执行的代码? │
    └────────┬────────┘             └────────┬────────┘
             │                               │
     ┌───────┴───────┐               ┌───────┴───────┐
     │ 否            │ 是            │ 是            │ 否
     ▼               ▼               ▼               ▼
┌─────────┐   ┌─────────────┐  ┌─────────┐   ┌─────────┐
│本机模式 │   │复合+Store模式│  │本机模式 │   │沙箱模式 │
│(默认)   │   │             │  │         │   │         │
└─────────┘   └─────────────┘  └─────────┘   └─────────┘

进一步阅读

  • 沙箱详解11-sandbox.md------深入了解沙箱的生命周期管理
  • 工具详解07-tool.md------了解 FilesystemToolShellExecuteTool 的使用
  • 工作区详解03-workspace.md------了解 WorkspaceManager 如何使用文件系统
相关推荐
utf8mb4安全女神1 小时前
怎么写shell/bash脚本【if嵌套】【case】【while死循环】【while嵌套if】【for】【随机数】
开发语言·bash
ziyue75751 小时前
python进行磁盘文件迁移,不影响软件使用
开发语言·数据库·python
爱和冰阔落1 小时前
【Python基础】从变量到面向对象:打通 Python 入门核心语法
开发语言·python
凡人叶枫1 小时前
Effective C++ 条款05:了解 C++ 默默编写并调用哪些函数
java·linux·开发语言·c++·effective c++·编程范式
少司府1 小时前
C++进阶:AVL树
开发语言·数据结构·c++·二叉树·avl树
某风吾起2 小时前
C语言总结
c语言·开发语言
winlife_2 小时前
全程用 AI 做一款商业级手游 · EP7 表现层与手感:从“能跑“到“摸起来爽“
java·开发语言·人工智能·unity·ai编程·游戏开发·mcp
千纸鹤の脉搏2 小时前
多线程的初步使用
java·开发语言·学习·多线程
专注VB编程开发20年2 小时前
阿里通义灵码插件安装失败
开发语言·ide·c#·visual studio