跳到正文

本地实现

上一节课的界面本身没有任何反应。我们需要一个后端。

本地沙箱才是无聊的。它包裹的是你一直使用的readFileSyncexecSync 调用。不同的是,现在这些设备都隐藏在界面后面,所有工具都以相同的方式调用它们。

无聊才是重点。本地沙箱证明了该接口在不引入新复杂性的情况下可行。它是所有其他后端都会被比较的基线。

学习成果

src/sandbox-local.ts导出createLocalSandbox(dir),一个工厂,返回一个Sandbox,其方法包裹了Node的readFileSyncexecSync。Agent运行方式和之前一样,但通过界面实现。

快速路径

  1. 创建src/sandbox-local.ts导出createLocalSandbox(dir): Sandbox
  2. async readFile包裹readFileSync
  3. async exec包裹execSync,尝试/接球回{ stdout, exitCode }
  4. stop()变成异步无操作者

动手练习 4.2

实施本地沙箱。

要求:

  1. createLocalSandbox(dir: string): Sandbox返回一个满足接口的对象
  2. readFile解析路径dir并读UTF-8
  3. exec在有cwd: dir和30秒超时的情况下执行该命令
  4. exec错误时,{ stdout: <whatever output there was>, exitCode: <non-zero> }回击而非投掷
  5. stopasync () => {}

实现提示:

  • 整个文件大约有15行。如果你的后台更长,可能你处理的是云后端会关注而本地后端不关心的案件
  • exec绝不应该扔球,即使是在非零的出场口。工具期望结果对象。发现错误并返回是正确的形状
  • type: "local"sandboxType从模块3插入到系统提示词的部分

实现

ts
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { execSync } from "node:child_process";
import type { Sandbox } from "./sandbox";

export function createLocalSandbox(dir: string): Sandbox {
  return {
    type: "local",
    workingDirectory: dir,
    readFile: async (p) => readFileSync(resolve(dir, p), "utf-8"),
    exec: async (command) => {
      try {
        const stdout = execSync(command, {
          cwd: dir,
          encoding: "utf-8",
          timeout: 30_000,
        });
        return { stdout, exitCode: 0 };
      } catch (e: any) {
        return {
          stdout: e.stdout || e.stderr || e.message || "",
          exitCode: e.status ?? 1,
        };
      }
    },
    stop: async () => {},
  };
}

这就是整个后端。stop是无营运的,因为没有什么需要清理的。本地文件系统和child_process会比Agent更持久。

完成接线

ts
import { createLocalSandbox } from "./src/sandbox-local";
import { createReadTool, createGrepTool, createBashTool } from "./src/tools";

const sandbox = createLocalSandbox(cwd);
console.error(`Sandbox: ${sandbox.type}`);

const tools = {
  read: createReadTool(sandbox),
  grep: createGrepTool(sandbox),
  bash: createBashTool(sandbox, createApproval({ mode: "interactive" })),
};

现在工厂接管了沙箱。他们封闭它,从内部调用execute。同样的工具,同样的Agent,同样的提示词。

动手试试

用你一直在用的提示词。输出应保持不变:

bash
bun run index.ts . "Read the tsconfig.json"
bun run index.ts . "Find all TODO comments"
bun run index.ts . "List all files in this directory"

Agent应该完全一样。引擎盖下的管道不同。确认沙箱身份一次:

ts
console.error(`Sandbox: ${sandbox.type}`);

你应该看看Sandbox: local

bash
npx tsc --noEmit

**注意:如果行为发生变化,重构就会泄露**

经过这节课,Agent在同一提示词上的行为应与模块3完全一致。如果有变化(路由不同、输出不同、新的错误),找一个工具仍然直接访问节点API而不是通过sandbox的地方。

提交

bash
git add src/sandbox-local.ts index.ts
git commit -m "feat(sandbox): add local backend wrapping Node APIs"

完成标准

  • [ ] src/sandbox-local.ts 出口createLocalSandbox(dir)
  • [ ] 返回的对象满足Sandbox接口
  • [ ] readFileexec通过Node API路由,就像之前一样
  • [ ] stop是个不会崩溃的无操作者
  • [ ] 这三种工具都还能用,和模块3一样
  • [ ] npx tsc --noEmit

**注意:把执行流变成 buffer 而不是 buffer**

execSync等命令结束后,一次性把所有stdout都丢掉。对于长构建来说,这很痛苦。试着切换到spawn,流式输出回每个区块。挑战在于:Sandbox.exec签名最后一次返回{ stdout, exitCode }。要流式传输,你需要不同的形状,比如异步迭代器。注意这种情绪如何反复影响到每一个调用 exec工具。界面决策很棘手。

参考实现

ts
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { execSync } from "node:child_process";
import type { Sandbox } from "./sandbox";

export function createLocalSandbox(dir: string): Sandbox {
  return {
    type: "local",
    workingDirectory: dir,
    readFile: async (p) => readFileSync(resolve(dir, p), "utf-8"),
    exec: async (command) => {
      try {
        const stdout = execSync(command, {
          cwd: dir,
          encoding: "utf-8",
          timeout: 30_000,
        });
        return { stdout, exitCode: 0 };
      } catch (e: any) {
        return {
          stdout: e.stdout || e.stderr || e.message || "",
          exitCode: e.status ?? 1,
        };
      }
    },
    stop: async () => {},
  };
}

非官方简体中文翻译 · 原课程来自 Vercel Academy