2. 让 Agent 能读写文件、执行命令 —— LocalShellBackend 实战

6 阅读11分钟

让 Agent 能读写文件、执行命令 —— LocalShellBackend 实战

这不是一个"让 Agent 多说几句话"的升级教程。我们要做的,是让 Agent 从"只会算数和报时"进化成一个真正能 读写文件搜索代码执行 shell 命令 的全能助手。 整个过程基于上一篇文章的项目,增量改动集中在 agents.tsindex.ts


目录

  1. 回顾:上一篇文章结束时 Agent 能做什么?
  2. Backend:给 Agent 配一个"文件系统管家"
  3. LocalShellBackend:让 Agent 学会敲命令行
  4. Permissions 与 execute 的冲突:为什么不能既要又要?
  5. 三个新测试:从 echo 到"写文件+执行"一条龙
  6. 虚拟路径 vs 真实路径:一个让人抓狂的坑
  7. 回顾与展望

1. 回顾:上一篇文章结束时 Agent 能做什么?

上一篇文章中,我们搭建了一个能自主调用工具的 Agent,它有两个能力:

工具能力
calculator四则运算
get_current_time报当前时间

就这两个。它不能帮你读文件,不能帮你写代码,不能执行任何系统命令。说白了,就是一个"会算数的聊天机器人"。

这一篇的目标:让它拥有文件操作和命令执行的能力


2. Backend:给 Agent 配一个"文件系统管家"

2.1 为什么需要 Backend?

想象你是 Agent,LLM 让你"读一下 /notes.txt"。问题来了:

  • /notes.txt 在磁盘的哪里?是绝对路径还是相对路径?
  • Agent 能不能读 /etc/passwd
  • 如果换一个运行环境(比如 Docker 容器),路径映射怎么处理?

直接让 Agent 操作文件系统太危险、太不灵活了。所以 deepagents 引入了 Backend 抽象层——Agent 不直接碰文件系统,而是通过 Backend 接口来操作。

2.2 Backend 家族一览

BackendProtocol(接口)
├── StateBackend      → 文件存在内存中(进程结束就丢)
├── FilesystemBackend → 文件存在本地磁盘上
├── LocalShellBackend → FilesystemBackend + execute()
└── CompositeBackend  → 组合多个后端,按路径前缀路由
Backend持久化文件操作Shell 执行适合场景
StateBackend临时会话
FilesystemBackend需要持久化文件
LocalShellBackend开发环境,需要执行命令
CompositeBackend视路由视路由多存储源混合

我们这次用的是 LocalShellBackend——它在 FilesystemBackend 的基础上多了一个 execute 工具,能执行 shell 命令。

2.3 代码变化

agents.ts 中,新增 Backend 的创建代码:

import path from 'node:path';
import { LocalShellBackend } from 'deepagents';

// 解析 workspace 目录的绝对路径
// import.meta.dirname 是当前文件所在目录(src/),往上一层是项目根目录
const workspaceDir = path.resolve(import.meta.dirname, '..', 'workspace');

const backend = await LocalShellBackend.create({
  rootDir: workspaceDir,   // Agent 的文件操作根目录
  virtualMode: true,       // 虚拟路径模式(后面详细讲)
  timeout: 30,             // shell 命令超时时间(秒)
  maxOutputBytes: 50000,   // 命令输出最大字节数,防止输出刷屏
});

几个要点:

参数说明
rootDir所有文件操作的根目录,Agent 只能看到这个目录下的文件
virtualMode开启后 Agent 用虚拟路径(如 /hello.txt),框架自动映射到真实路径
timeout命令执行超时时间,防止死循环命令卡住 Agent
maxOutputBytes输出大小限制,避免 cat 一个大文件把内存撑爆

注意 LocalShellBackend.create() 是一个异步方法(返回 Promise),所以需要用 await。这是因为底层可能需要检测系统环境、初始化子进程等。

加上 Backend 后,Agent 自动获得 7 个新工具

工具功能示例
ls列出目录内容ls("/")
read_file读取文件内容read_file("/hello.txt")
write_file创建新文件write_file("/note.txt", "内容")
edit_file编辑已有文件edit_file("/hello.txt", "旧文本", "新文本")
glob按模式搜索文件名glob("**/*.txt")
grep按内容搜索文件grep("关键词", "/")
execute执行 shell 命令execute("echo hello")

前 6 个来自 FilesystemBackend,第 7 个是 LocalShellBackend 额外提供的。


3. LocalShellBackend:让 Agent 学会敲命令行

3.1 execute 工具的本质

execute 工具做的事情很简单:把 Agent 传来的字符串当作 shell 命令执行,返回 stdout/stderr 的输出。

Agent 想执行 ls -la
  → execute("ls -la")
  → shell 执行命令
  → 返回输出结果给 Agent
  → Agent 根据结果生成回复

这看起来平平无奇,但想想这意味着什么——Agent 现在可以执行任何 shell 命令:

  • lscatgrep — 查看文件系统
  • npm installgit status — 执行开发工具
  • curlwget — 网络请求
  • python script.py — 运行脚本

能力边界一下子从"算数+报时"扩展到了"整台电脑"。

3.2 创建 Agent 时传入 Backend

export const agent = createDeepAgent({
  model,
  tools: [calculatorTool, getCurrentTimeTool],
  backend,   // ← 新增:传入文件系统后端
  // 注意:这里没有 permissions,因为 LocalShellBackend 支持 execute,两者不兼容
  systemPrompt:
    '你是一个乐于助人的 AI 助手。\n' +
    '你可以进行数学计算、查询时间,还可以读写文件、搜索文件、执行 shell 命令。\n' +
    '操作文件时,路径是相对于工作目录的。',
    '你有持久记忆(AGENTS.md),可以记住用户偏好。\n' +
    '你还有领域知识库(Skills),可以回答产品相关问题。\n' +
    '当用户要求你写代码、创建文件时,请使用 write_file 工具将代码保存到文件中,而不是只在回复中展示代码。',
  checkpointer: new MemorySaver()
});

createDeepAgent 看到 backend 参数后,内部会自动创建 FilesystemMiddleware,把上面那 7 个工具注册进去。我们不需要手动做任何事。

systemPrompt 也做了更新——告诉 Agent 它现在拥有文件操作和命令执行的能力,这样 LLM 才知道什么时候该用这些工具。

生产建议LocalShellBackend 没有沙箱隔离,命令以当前用户身份执行,拥有与你终端相同的权限。Agent 执行 rm -rf / 是真的会删文件的。生产环境请使用 Docker 沙箱(如 LangSmithSandbox)。


4. Permissions 与 execute 的冲突:为什么不能既要又要?

4.1 Permissions 是什么?

deepagents 提供了一套声明式权限系统 permissions,用来限制 Agent 的文件操作:

// 示例(本项目的上一阶段用过)
const permissions = [
  { operations: ['read', 'write'], paths: ['/public/**'] },            // 允许读写 public
  { operations: ['read', 'write'], paths: ['/private/**'], mode: 'deny' }, // 拒绝读写 private
];

规则按声明顺序匹配,first match wins

4.2 为什么 permissions + execute 不兼容?

直觉上,我们可以"限制文件操作权限,但允许执行命令"。但这有一个致命漏洞:

permissions 说:拒绝读 /private/secret.txt

Agent 执行:execute("cat /private/secret.txt")
  → shell 直接读文件,完全不走 Backend 的路径检查
  → 权限规则形同虚设!

shell 命令是"万能钥匙"——它可以绕过所有路径限制。所以 deepagents 做了一个务实的决定:直接禁止 permissions 和 LocalShellBackend 共存。如果你同时传了两个,框架会抛出 ConfigurationError

// ❌ 这会报错
createDeepAgent({
  backend: localShellBackend,
  permissions: [...],   // ConfigurationError!
});

这就是为什么我们的代码里没有 permissions 参数——既然要用 execute,就必须放弃文件权限限制。

4.3 那安全怎么办?

方案安全级别灵活性适用场景
FilesystemBackend + permissions✅ 高只需要文件读写,不需要执行命令
LocalShellBackend(无 permissions)❌ 低✅ 高开发/学习环境
LangSmithSandbox(Docker 容器)✅✅ 最高✅ 高生产环境

开发阶段用 LocalShellBackend 图方便,生产环境切到 Docker 沙箱。 这是安全与灵活性的平衡。


5. 三个新测试:从 echo 到"写文件+执行"一条龙

index.ts 中,我们注释掉了之前的计算器/时间/多轮对话测试,新增了 3 个 shell 相关的测试。

5.1 测试 4:执行简单 shell 命令

console.log('--- 测试 4:执行 shell 命令 ---');
console.log('用户:执行 echo "Hello from Agent!" 命令');
process.stdout.write('助手:');

const stream4 = await agent.stream(
  { messages: [{ role: 'user', content: '执行 echo "Hello from Agent!" 命令' }] },
  { ...config, streamMode: 'messages' },
);
await printStream(stream4);
console.log('\n');

流程:

用户消息 → LLM 判断需要 execute → execute("echo Hello from Agent!")
→ shell 返回 "Hello from Agent!" → LLM 生成回复

这是最简单的场景——Agent 执行一条命令,拿到结果,回复用户。

5.2 测试 5:执行复杂 shell 命令

console.log('--- 测试 5:执行复杂 shell 命令 ---');
console.log('用户:帮我查看当前目录下的文件列表,并统计文件数量');
process.stdout.write('助手:');

const stream5 = await agent.stream(
  { messages: [{ role: 'user', content: '帮我查看当前目录下的文件列表,并统计文件数量' }] },
  { ...config, streamMode: 'messages' },
);
await printStream(stream5);
console.log('\n');

这个测试更有意思——用户的请求比较模糊("查看文件列表并统计数量"),LLM 需要自己决定用什么命令。它可能会:

  1. 先用 ls 工具列出文件
  2. 再用 execute("wc -l") 或类似命令统计
  3. 或者一步到位 execute("ls | wc -l")

这就是 Agent 的"自主决策"能力——你不告诉它具体用什么命令,它自己判断。

5.3 测试 6:写文件 + 执行命令(组合拳)

console.log('--- 测试 6:组合使用(写文件 + 执行命令) ---');
console.log('用户:在 /public/ 下创建一个 script.sh,内容是 echo "Hello World",然后执行它');
process.stdout.write('助手:');

const stream6 = await agent.stream(
  { messages: [{ role: 'user', content: '在 /public/ 下创建一个 script.sh,内容是 echo "Hello World",然后执行它' }] },
  { ...config, streamMode: 'messages' },
);
await printStream(stream6);
console.log();

这是复杂的场景——Agent 需要串联多个工具

用户请求
  → LLM 规划步骤
  → 步骤 1:write_file("/public/script.sh", "#!/bin/bash\necho Hello World")
  → 步骤 2:execute("bash workspace/public/script.sh")
  → 拿到执行结果
  → 生成最终回复

这就是 Agent Loop 的威力——一次用户请求,Agent 可能执行多轮工具调用,直到完成所有步骤。

注意:测试 6 中 Agent 写文件用虚拟路径 /public/script.sh,但执行时可能需要用真实路径。这里有个坑,下面详细说。


6. 虚拟路径 vs 真实路径:一个让人抓狂的坑

6.1 两套路径空间

开启 virtualMode: true 后,文件工具(ls/read_file/write_file 等)使用虚拟路径

Agent 写 write_file("/public/script.sh", "内容")
  → 框架映射到 {workspaceDir}/public/script.sh
  → 文件成功创建 ✓

execute 工具直接调用 shell,它看到的是真实文件系统路径

Agent 执行 execute("bash /public/script.sh")
  → shell 在真实文件系统中找 /public/script.sh
  → 不存在!✗(真实路径是 /Users/xxx/demo/lingshi/workspace/public/script.sh)

6.2 路径映射示意

┌─────────────────────────────────────────────────┐
│                Agent 视角(虚拟路径)              │
│                                                   │
│  /hello.txt          /public/script.sh            │
│      ↓                      ↓                     │
├─────────────────────────────────────────────────┤
│                框架映射层                          │
│                                                   │
│  virtualMode: true 时自动映射                      │
│  /hello.txt → {workspaceDir}/hello.txt            │
│  /public/script.sh → {workspaceDir}/public/script.sh │
├─────────────────────────────────────────────────┤
│              真实文件系统                           │
│                                                   │
│  /Users/xxx/demo/lingshi/workspace/hello.txt      │
│  /Users/xxx/demo/lingshi/workspace/public/script.sh│
│                                                   │
│  ↑ 文件工具经过映射层,能正确访问                    │
│  ↑ execute 工具直接访问,不经过映射层!              │
└─────────────────────────────────────────────────┘

6.3 怎么解决?

在测试 6 中,Agent 需要"聪明地"在 execute 时使用正确的路径:

  • 方案 A:用相对路径 — execute("bash public/script.sh")(相对于 rootDir
  • 方案 B:用绝对路径 — execute("bash /Users/xxx/demo/lingshi/workspace/public/script.sh")
  • 方案 C:让 systemPrompt 里说明路径规则,引导 Agent 正确处理

实际上,LLM 通常能通过试错学会这个规则——第一次用虚拟路径执行失败后,它会反思并调整路径。

踩坑记录:最初我在测试 6 中让 Agent "创建脚本并执行",Agent 写完文件后直接用 execute("bash /public/script.sh") 去执行,结果报"文件不存在"。这是因为 execute 不走虚拟路径映射。Agent 在收到错误后自己调整成了相对路径才执行成功。这个行为取决于 LLM 的错误恢复能力,不是所有模型都能自动修正。


7. 回顾与展望

我们做了什么

在上一篇文章的基础上,增量改动让 Agent 能力大幅跃升:

  1. 引入 LocalShellBackend — Agent 从"只会算数"进化为能读写文件 + 执行命令
  2. 理解 Backend 抽象层 — 4 种 Backend 的对比和选择
  3. 理解 permissions 与 execute 的冲突 — shell 是万能钥匙,路径权限管不住它
  4. 新增 3 个 shell 测试 — 从简单 echo 到"写文件+执行"的组合拳
  5. 踩过了虚拟路径 vs 真实路径的坑 — virtualMode 下两套路径空间的映射差异

完整运行

pnpm dev

输出三个新测试场景:执行 echo 命令、查看文件列表并统计、写脚本并执行。

后续可以做什么

  • 接入沙箱:把 LocalShellBackend 换成 Docker 沙箱,让 Agent 在隔离环境中执行
  • 加入权限控制:如果不需要 execute,切回 FilesystemBackend + permissions,精细控制文件访问
  • 自定义工具 + 文件系统组合:比如 Agent 先读取 CSV 文件,再用计算器工具分析数据
  • Web 界面:把 Agent 的能力通过 SSE 推到前端,做一个真正的 AI 编程助手

从"会算数"到"会操作电脑",Agent 的能力边界又拓宽了一层。