让 Agent 能读写文件、执行命令 ------ LocalShellBackend 实战
这不是一个"让 Agent 多说几句话"的升级教程。我们要做的,是让 Agent 从"只会算数和报时"进化成一个真正能 读写文件 、搜索代码 、执行 shell 命令 的全能助手。 整个过程基于上一篇文章的项目,增量改动集中在
agents.ts和index.ts。
目录
- [回顾:上一篇文章结束时 Agent 能做什么?](#回顾:上一篇文章结束时 Agent 能做什么? "#1-%E5%9B%9E%E9%A1%BE%E4%B8%8A%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E7%BB%93%E6%9D%9F%E6%97%B6-agent-%E8%83%BD%E5%81%9A%E4%BB%80%E4%B9%88")
- [Backend:给 Agent 配一个"文件系统管家"](#Backend:给 Agent 配一个"文件系统管家" "#2-backend%E7%BB%99-agent-%E9%85%8D%E4%B8%80%E4%B8%AA%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E7%AE%A1%E5%AE%B6")
- [LocalShellBackend:让 Agent 学会敲命令行](#LocalShellBackend:让 Agent 学会敲命令行 "#3-localshellbackend%E8%AE%A9-agent-%E5%AD%A6%E4%BC%9A%E6%95%B2%E5%91%BD%E4%BB%A4%E8%A1%8C")
- [Permissions 与 execute 的冲突:为什么不能既要又要?](#Permissions 与 execute 的冲突:为什么不能既要又要? "#4-permissions-%E4%B8%8E-execute-%E7%9A%84%E5%86%B2%E7%AA%81%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%83%BD%E6%97%A2%E8%A6%81%E5%8F%88%E8%A6%81")
- [三个新测试:从 echo 到"写文件+执行"一条龙](#三个新测试:从 echo 到"写文件+执行"一条龙 "#5-%E4%B8%89%E4%B8%AA%E6%96%B0%E6%B5%8B%E8%AF%95%E4%BB%8E-echo-%E5%88%B0%E5%86%99%E6%96%87%E4%BB%B6%E6%89%A7%E8%A1%8C%E4%B8%80%E6%9D%A1%E9%BE%99")
- [虚拟路径 vs 真实路径:一个让人抓狂的坑](#虚拟路径 vs 真实路径:一个让人抓狂的坑 "#6-%E8%99%9A%E6%8B%9F%E8%B7%AF%E5%BE%84-vs-%E7%9C%9F%E5%AE%9E%E8%B7%AF%E5%BE%84%E4%B8%80%E4%B8%AA%E8%AE%A9%E4%BA%BA%E6%8A%93%E7%8B%82%E7%9A%84%E5%9D%91")
- 回顾与展望
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 家族一览
md
BackendProtocol(接口)
├── StateBackend → 文件存在内存中(进程结束就丢)
├── FilesystemBackend → 文件存在本地磁盘上
├── LocalShellBackend → FilesystemBackend + execute()
└── CompositeBackend → 组合多个后端,按路径前缀路由
| Backend | 持久化 | 文件操作 | Shell 执行 | 适合场景 |
|---|---|---|---|---|
StateBackend |
❌ | ✅ | ❌ | 临时会话 |
FilesystemBackend |
✅ | ✅ | ❌ | 需要持久化文件 |
LocalShellBackend |
✅ | ✅ | ✅ | 开发环境,需要执行命令 |
CompositeBackend |
视路由 | ✅ | 视路由 | 多存储源混合 |
我们这次用的是 LocalShellBackend ------它在 FilesystemBackend 的基础上多了一个 execute 工具,能执行 shell 命令。
2.3 代码变化
在 agents.ts 中,新增 Backend 的创建代码:
typescript
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 的输出。
md
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
typescript
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 的文件操作:
typescript
// 示例(本项目的上一阶段用过)
const permissions = [
{ operations: ['read', 'write'], paths: ['/public/**'] }, // 允许读写 public
{ operations: ['read', 'write'], paths: ['/private/**'], mode: 'deny' }, // 拒绝读写 private
];
规则按声明顺序匹配,first match wins。
4.2 为什么 permissions + execute 不兼容?
直觉上,我们可以"限制文件操作权限,但允许执行命令"。但这有一个致命漏洞:
md
permissions 说:拒绝读 /private/secret.txt
Agent 执行:execute("cat /private/secret.txt")
→ shell 直接读文件,完全不走 Backend 的路径检查
→ 权限规则形同虚设!
shell 命令是"万能钥匙"------它可以绕过所有路径限制。所以 deepagents 做了一个务实的决定:直接禁止 permissions 和 LocalShellBackend 共存 。如果你同时传了两个,框架会抛出 ConfigurationError。
typescript
// ❌ 这会报错
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 命令
typescript
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');
流程:
md
用户消息 → LLM 判断需要 execute → execute("echo Hello from Agent!")
→ shell 返回 "Hello from Agent!" → LLM 生成回复
这是最简单的场景------Agent 执行一条命令,拿到结果,回复用户。
5.2 测试 5:执行复杂 shell 命令
typescript
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:写文件 + 执行命令(组合拳)
typescript
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 需要串联多个工具:
md
用户请求
→ 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 等)使用虚拟路径:
md
Agent 写 write_file("/public/script.sh", "内容")
→ 框架映射到 {workspaceDir}/public/script.sh
→ 文件成功创建 ✓
但 execute 工具直接调用 shell,它看到的是真实文件系统路径:
md
Agent 执行 execute("bash /public/script.sh")
→ shell 在真实文件系统中找 /public/script.sh
→ 不存在!✗(真实路径是 /Users/xxx/demo/lingshi/workspace/public/script.sh)
6.2 路径映射示意
md
┌─────────────────────────────────────────────────┐
│ 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 下两套路径空间的映射差异
完整运行
bash
pnpm dev
输出三个新测试场景:执行 echo 命令、查看文件列表并统计、写脚本并执行。
后续可以做什么
- 接入沙箱 :把
LocalShellBackend换成 Docker 沙箱,让 Agent 在隔离环境中执行 - 加入权限控制 :如果不需要 execute,切回
FilesystemBackend+ permissions,精细控制文件访问 - 自定义工具 + 文件系统组合:比如 Agent 先读取 CSV 文件,再用计算器工具分析数据
- Web 界面:把 Agent 的能力通过 SSE 推到前端,做一个真正的 AI 编程助手
从"会算数"到"会操作电脑",Agent 的能力边界又拓宽了一层。