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

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

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


目录

  1. [回顾:上一篇文章结束时 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")
  2. [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")
  3. [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")
  4. [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")
  5. [三个新测试:从 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")
  6. [虚拟路径 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")
  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 家族一览

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 命令:

  • lscatgrep --- 查看文件系统
  • npm installgit status --- 执行开发工具
  • curlwget --- 网络请求
  • 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 需要自己决定用什么命令。它可能会:

  1. 先用 ls 工具列出文件
  2. 再用 execute("wc -l") 或类似命令统计
  3. 或者一步到位 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 能力大幅跃升:

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

完整运行

bash 复制代码
pnpm dev

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

后续可以做什么

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

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

相关推荐
爱勇宝2 小时前
Claude Code 被曝暗藏“隐形检测”代码:封代理不是最可怕的,可怕的是你根本不知道它在干什么
前端·后端·程序员
小牛不牛的程序员2 小时前
我用 Claude Code 半天撸完了一个完整网站,AI 编程到底提升了多少效率?
前端
东风破_2 小时前
JavaScript 面试常考的字符串算法:从反转字符串到回文判断
前端·javascript
ITOM运维行者3 小时前
从零搭建企业级服务器监控体系:踩坑实录与架构设计
前端·后端
monologues3 小时前
深入 Vue 3 源码:响应式系统的精妙设计与编译优化
前端
hunterandroid3 小时前
Paging 3 分页:从手动分页到声明式加载
前端
用户4099322502123 小时前
Vue状态管理入门第四章:组合式store和SSR风险
前端·vue.js·后端
Csvn3 小时前
CSS :has() 选择器实战:没有它之前我们写了多少冗余 JS
前端·css
梨子同志3 小时前
TypeScript
前端