手写一个 AI Agent 全栈项目:从沙箱执行到子智能体的完整实现

21 阅读8分钟

介绍一个基于 Express.js + Vercel AI SDK 的 AI Agent 练手项目,涵盖沙箱安全执行、Human-in-the-Loop 审批、动态 Skills 加载、子智能体等核心功能。


1. 背景

去年开始 AI Agent 概念大火,各种框架层出不穷。但与其直接用现成的 Agent 框架,不如亲手从零搭建一个——既能深入理解 AI Agent 的工作原理,又能把控安全、扩展性等工程细节。

这个项目是我个人的练手项目,基于 Turborepo Monorepo,包含一个 Express.js API 后端和一个 Vue 3 前端。核心目标是构建一个安全、可扩展的 AI Agent 平台,让它能自主执行文件操作、运行 Shell 命令、加载专业技能、甚至创建子智能体来并行处理任务。

源码地址:express-turbo-monorepo


2. 技术栈选型

类别选型说明
构建编排TurborepoMonorepo 管理,增量构建缓存
后端框架Express 5最新版 Express,支持异步错误处理
运行时Node.js 23 + TypeScript全栈类型安全
AI SDKVercel AI SDK v6ToolLoopAgent、流式响应
校验Zod 4Schema 校验 + OpenAPI 自动生成
沙箱@anthropic-ai/sandbox-runtimebubblewrap 内核隔离
日志Pino结构化 JSON 日志,按天轮转
前端Vue 3 + Nuxt UI 4单页聊天客户端
部署Docker 多阶段构建Alpine 镜像,生产优化

选择这些技术栈的理由:

  • Express 5: 轻量灵活,适合理解 Agent 的请求-响应生命周期
  • Vercel AI SDK: 提供高阶抽象 ToolLoopAgent,让 Agent 自动循环调用工具,无需手动管理多轮逻辑
  • Turborepo: 天然支持 apps/* 多包结构,API 和前端共享配置
  • bubblewrap 沙箱: 使用 Linux 内核命名空间实现真正的文件系统和网络隔离

3. 项目结构

express-turbo-monorepo/
├── apps/
│   └── api/                       # Express.js API 服务
│       ├── src/
│       │   ├── index.ts          # 应用入口点
│       │   ├── app.ts            # Express 应用工厂
│       │   ├── errors/           # 错误处理
│       │   ├── lib/              # 工具库(日志、AI 提供商)
│       │   ├── middleware/       # 中间件
│       │   ├── modules/          # 功能模块
│       │   │   ├── chat/         # AI 对话模块
│       │   │   └── skills/       # Skills 管理模块
│       │   ├── sandbox/          # 沙箱运行时
│       │   ├── skills/           # Skills 发现与加载
│       │   ├── tools/            # AI 工具定义
│       │   ├── schema/           # Zod 校验规则
│       │   └── util/             # 工具函数
│       └── package.json
│   └── dev-web/                   # Vue 3 前端
├── turbo.json                     # Turborepo 配置
└── package.json

每个模块采用一致的文件结构:

modules/<name>/
  <name>.module.ts       # 路由注册
  <name>.routes.ts       # 路由定义
  <name>.controller.ts   # 请求处理
  <name>.service.ts      # 业务逻辑

4. 核心功能详解

4.1 AI 对话与工具调用

基于 Vercel AI SDK 的 ToolLoopAgent,Agent 会自动循环:思考 → 调用工具 → 获取结果 → 再思考,直到完成用户任务。

const { text, steps } = streamText({
  model: provider,
  system: agentSystemPrompt(availableTools, skills),
  tools: agentTools,
  maxSteps: 30, // 最大工具调用步数
  onStepFinish: ({ toolResults }) => {
    // 每个 step 完成后压缩历史结果,节省 token
    return compressToolResults(toolResults, config);
  },
});

核心设计点:

  • 30 步上限:防止 Agent 无限循环
  • Tool Result 压缩:历史工具结果超过 120 字符时,替换为压缩提示文本,保留最近 3 条完整
  • 大结果持久化:输出超过 30000 字符时自动写入磁盘,AI 收到预览 + 文件路径引用

4.2 内置工具一览

项目为 AI 注册了丰富的工具集:

工具功能安全等级
readFile按字符范围读取文件低风险
editFile搜索替换文件内容中风险
createFile创建新文件中风险
deletePath删除文件或目录高风险
runCommand执行 Shell 命令高风险
readDir列出目录内容低风险
loadSkill加载专业技能低风险
difyWorkflow调用 Dify API中风险
plan分步任务规划低风险
subagent创建子智能体中风险

4.3 沙箱安全执行

这是项目最重要的设计之一。AI 执行 Shell 命令时,不能直接暴露宿主系统。

沙箱方案采用双层防护:

第一层:应用层权限过滤

const permissionRules: PermissionRule[] = [
  // deny - 直接拒绝的危险操作
  { tool: 'runCommand', behavior: 'deny', content: 'rm -rf', path: null },
  { tool: 'runCommand', behavior: 'deny', content: 'sudo', path: null },
  { tool: 'runCommand', behavior: 'deny', content: 'curl.*\\|.*sh', path: null },
  // allow - 安全的只读操作
  { tool: 'runCommand', behavior: 'allow', content: 'ls', path: null },
  { tool: 'runCommand', behavior: 'allow', content: 'cat', path: null },
  // ask - 需要人工审批
  { tool: 'editFile', behavior: 'ask', content: null, path: null },
];

第二层:系统级沙箱隔离

使用 @anthropic-ai/sandbox-runtime(基于 bubblewrap)进行内核级隔离:

  • 文件系统限制:只能访问 SANDBOX_DIRAGENTS_DIR,禁止访问 ~/.ssh 等敏感路径
  • 写保护:禁止覆盖 .env*.pem*.key 等关键文件
  • 网络限制:支持域名白名单/黑名单
  • 深度防护mandatoryDenySearchDepth: 10 防止符号链接逃逸

4.4 Human-in-the-Loop 审批系统

这是让 AI Agent 安全可用的关键。项目设计了四种聊天模式,控制工具执行的严格程度:

模式读操作写操作适用场景
default需审批需审批默认模式,最安全
plan需审批拒绝计划阶段,只读探索
auto允许拒绝自动执行,安全可控
yolo允许允许完全信任,无限制

用户可以通过请求体中的 metadata.mode 切换模式:

{
  "prompt": "帮我创建一个 React 组件",
  "metadata": { "mode": "yolo" }
}

在前端,当工具需要审批时,消息气泡会显示工具输入参数和批准/拒绝按钮,用户确认后对话自动继续。

4.5 Skills 动态加载系统

Skills 是注入 AI 领域知识的一种方式。每个 Skill 就是一个包含 SKILL.md 的目录,放在 AGENTS_DIR 下:

agents/
  python-developer/
    SKILL.md          # 含 YAML frontmatter 的 Markdown
  react-optimizer/
    SKILL.md

AI 通过 loadSkill 工具按需加载。系统支持:

  • 自动发现目录下的所有 Skill
  • ZIP 上传/下载/删除 Skill(通过 Skills Module API)
  • 运行时动态注入系统提示词

4.6 子智能体(Subagent)

对于复杂任务,主 Agent 可以创建子 Agent 并行处理:

用户: "帮我分析这个项目的代码结构并生成文档"
  → 主 Agent 创建计划
    → 子 Agent A: 探索 src/tools/ 目录
    → 子 Agent B: 探索 src/modules/ 目录
    → 子 Agent C: 阅读 package.json 和配置文件
  → 汇总结果,生成文档

子 Agent 拥有独立的指令和工具集,执行完毕后返回结构化总结给主 Agent。

4.7 OpenAPI / Swagger 文档

从 Zod Schema 自动生成 OpenAPI 3.1 规范文档,访问 /docs 即可查看交互式 API 文档:

new OpenApiRegistryZodExtensions([
  registry.register('ChatRequest', chatRequestSchema),
  registry.register('ChatResponse', chatResponseSchema),
]);

5. 架构设计要点

5.1 工厂模式

Express 应用通过工厂函数创建,方便测试和生命周期管理:

// app.ts
export function createApp() {
  const app = express();
  app.use(helmet());
  app.use(cors());
  app.use(httpLogger);
  app.use(express.json());

  app.get('/health', (_req, res) => {
    res.json(jsonSuccess({ status: 'ok' }));
  });

  app.use('/api', helloModule);
  app.use('/api', chatModule);
  app.use('/api', skillsModule);

  app.use(notFoundHandler);
  app.use(errorHandler);
  return app;
}

5.2 统一的响应与错误码

所有 API 响应遵循统一格式:

interface ApiJsonResponse<T> {
  responseCode: string;  // "000000" 成功,"400001" 校验错误等
  responseMsg: string;   // 描述信息
  data: T;               // 响应数据
}

5.3 环境变量即配置

所有配置通过 .env 加载,启动时用 Zod 校验,缺失环境变量直接阻止启动:

const envSchema = z.object({
  PORT: z.coerce.number().default(3000),
  OPENAI_API_KEY: z.string().min(1),
  OPENAI_BASE_URL: z.string().url(),
  SANDBOX_ENABLED: z.coerce.boolean().default(true),
  // ...更多配置
});

6. Docker 部署

项目提供多阶段 Docker 构建,生产镜像只有 150MB 左右:

# 阶段 1: 编译
FROM node:23-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# 阶段 2: 运行
FROM node:23-alpine
RUN apk add --no-cache bash ripgrep bubblewrap socat
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]

关键点:因为沙箱依赖 bubblewrap 操作内核命名空间,运行容器时需要添加权限:

docker run --cap-add=SYS_ADMIN --cap-add=NET_ADMIN -p 3000:3000 your-image

7. 开发流程与规范

项目制定了清晰的开发规范,适合团队协作:

  • 类型优先:所有类型先在 schema 中用 Zod 定义,再导出 TypeScript 类型供业务代码使用
  • 工具函数复用:重复逻辑提取到 src/util/,禁止 Copy-Paste
  • 错误码同步:新增错误必须同步更新 AppErrorCodeApiResponseCode
  • 模块自包含:每个模块独立注册到 app.ts,模块间通过接口通信
  • 提交前检查:每次变更后执行 npm run typecheck 确保类型安全

8. 收获与思考

通过这个练手项目,我对 AI Agent 的理解更深了:

  1. 安全是第一优先级:让 AI 执行 Shell 命令不是难事,难的是确保它不会"闯祸"。双层防护(权限过滤 + 沙箱隔离)是必要的。

  2. Human-in-the-Loop 是实用关键:完全自主的 Agent 在现阶段还是不安全的。好的设计是在"自主"和"可控"之间找到平衡点——这就是四种模式的由来。

  3. Token 管理是工程问题:AI 对话上下文有限,历史工具结果会快速消耗 token。压缩 + 持久化的双策略能有效延长 Agent 的"注意力"。

  4. Skills 是扩展性的核心:通过 Skills 系统,领域知识可以和 Agent 逻辑解耦,这比把所有 prompt 塞进系统提示词要优雅得多。

如果你是 AI Agent 的初学者,想深入理解 Agent 的工作原理,不妨从这个项目开始,亲手跑起来、改一改、加个新工具,收获会很大。


项目地址:express-turbo-monorepo

欢迎 Star、Fork、提 Issue,一起交流 AI Agent 工程实践。