让 Agent 能读写文件、执行命令 —— LocalShellBackend 实战
这不是一个"让 Agent 多说几句话"的升级教程。我们要做的,是让 Agent 从"只会算数和报时"进化成一个真正能 读写文件、搜索代码、执行 shell 命令 的全能助手。 整个过程基于上一篇文章的项目,增量改动集中在
agents.ts和index.ts。
目录
- 回顾:上一篇文章结束时 Agent 能做什么?
- Backend:给 Agent 配一个"文件系统管家"
- LocalShellBackend:让 Agent 学会敲命令行
- Permissions 与 execute 的冲突:为什么不能既要又要?
- 三个新测试:从 echo 到"写文件+执行"一条龙
- 虚拟路径 vs 真实路径:一个让人抓狂的坑
- 回顾与展望
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 命令:
ls、cat、grep— 查看文件系统npm install、git status— 执行开发工具curl、wget— 网络请求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 需要自己决定用什么命令。它可能会:
- 先用
ls工具列出文件 - 再用
execute("wc -l")或类似命令统计 - 或者一步到位
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 能力大幅跃升:
- 引入
LocalShellBackend— Agent 从"只会算数"进化为能读写文件 + 执行命令 - 理解 Backend 抽象层 — 4 种 Backend 的对比和选择
- 理解 permissions 与 execute 的冲突 — shell 是万能钥匙,路径权限管不住它
- 新增 3 个 shell 测试 — 从简单 echo 到"写文件+执行"的组合拳
- 踩过了虚拟路径 vs 真实路径的坑 — virtualMode 下两套路径空间的映射差异
完整运行
pnpm dev
输出三个新测试场景:执行 echo 命令、查看文件列表并统计、写脚本并执行。
后续可以做什么
- 接入沙箱:把
LocalShellBackend换成 Docker 沙箱,让 Agent 在隔离环境中执行 - 加入权限控制:如果不需要 execute,切回
FilesystemBackend+ permissions,精细控制文件访问 - 自定义工具 + 文件系统组合:比如 Agent 先读取 CSV 文件,再用计算器工具分析数据
- Web 界面:把 Agent 的能力通过 SSE 推到前端,做一个真正的 AI 编程助手
从"会算数"到"会操作电脑",Agent 的能力边界又拓宽了一层。