atypica.AI
atypica.AI

为「主观世界」建模

© 2026 BMRLab. 保留所有权利。

系统运行中

产品

关于 atypica.AI技术价格AI 研究AI PersonaAI 访谈Insight 播客skill0Game Lab

解决方案

企业版案例展示内容创作者网红营销人员产品经理创业者咨询顾问

资源

产品文档常见问题使用指南AI Persona 模拟API 文档词汇表更新日志博客研究工程

公司

加入我们联盟推广技术介绍项目介绍服务条款隐私政策

Universal Agent + Skills 教程:从零构建你的 10 人工程团队

2026年1月17日

Universal Agent + Skills 教程:从零构建你的 10 人工程团队

想象一下:

你对 AI Agent 说:"帮我写份技术文档",它立刻加载 technical-writer.skill,变身技术作家。

说完文档,你又说:"分析一下竞品的广告策略",它换上 marketing-analyst.skill,开始爬数据。

更酷的是,如果你需要一个特定领域的专家——比如"SQL 性能优化"——而市面上没有?让 Agent 自己写一个 skill 给自己用。

这不是科幻。实现起来,极其简单。


这篇文章会教你构建一个 Universal Agent:

  • ✅ 加载任意技能 - 拖进去一个 .skill 文件,Agent 立刻获得新能力
  • ✅ 自己写技能 - Agent 可以为自己创建新的专业技能包
  • ✅ 持久化工作 - 文件跨对话保留,上午写的代码下午继续改
  • ✅ 零基础设施 - 不需要 Docker、K8s,一个 Next.js 项目就够

把它想象成组建一支 10 人的创始团队:每个技能就是一个专家,Agent 是项目经理,你是 CEO。

架构概览

用户上传 skill 包 → S3 存储                      ↓请求开始 → 加载 skills + workspace → 内存沙箱                      ↓Agent 读取技能、创建文件、使用工具                      ↓请求结束 → 保存 workspace 变更 → 磁盘                      ↓下次对话 → 文件持久化 → Agent 继续工作

核心组件:

  • bash-tool - 内存文件系统,支持 bash、readFile、writeFile
  • Skills 在 sandbox/user/{id}/skills/(只读,来自 S3)
  • Workspace 在 sandbox/user/{id}/workspace/(可读写,持久化)
  • AI SDK 处理流式响应、工具调用、token 统计

核心实现

1. Skill 结构

每个 skill 是一个 .skill 文件(zip 格式),结构简单:

my-skill/├── SKILL.md          # 指令和专业知识└── references/       # 可选:参考文档    └── examples.md

SKILL.md 示例:

markdown
---name: technical-writerdescription: 创建技术文档、API 指南和教程---
# Technical Writer Skill
你是一位专业技术写作专家,擅长创建清晰简洁的文档。
## 专业领域- API 文档(带示例)- 架构决策记录(ADR)- 用户指南和教程- 代码注释和内联文档
## 写作准则- 先讲为什么,再讲是什么,最后讲怎么做- 提供实用示例- 使用主动语态- 保持 DRY——用链接而非重复
## 激活方式当用户说:- "写一份文档..."- "创建 API 指南..."- "为这个代码库写文档..."

上传流程:用户上传 zip → 存储到 S3 → 数据库记录 → Agent 按需加载。

2. 路由处理器

文件:app/api/chat/universal/route.ts

typescript
import { streamText } from "ai";import { createBashTool } from "bash-tool";import { loadAllSkillsToMemory } from "@/lib/skill/loadToMemory";import { loadUserWorkspace, saveUserWorkspace } from "@/lib/skill/workspace";
export async function POST(req: Request) {  const { userId, userChatId } = await authenticate(req);
  // 从 S3/磁盘加载 skills  const skills = await prisma.agentSkill.findMany({ where: { userId } });  const skillFiles = await loadAllSkillsToMemory(skills);
  // 加载持久化的 workspace  const workspaceFiles = await loadUserWorkspace(userId);
  // 给 skills 加前缀实现隔离  const skillFilesWithPrefix = Object.fromEntries(    Object.entries(skillFiles).map(([path, content]) => [`skills/${path}`, content])  );
  // 创建沙箱  const { tools: bashTools, sandbox } = await createBashTool({    files: {      ...workspaceFiles,         // 用户的工作文件(根目录)      ...skillFilesWithPrefix,   // Skills(skills/ 子目录)    },  });
  // 合并所有工具  const tools = {    ...baseTools,              // webSearch, reasoningThinking 等    bash: bashTools.bash,    readFile: bashTools.readFile,    writeFile: bashTools.writeFile,    exportFolder: exportFolderTool({ sandbox, userId }),  };
  // 流式响应 + 持久化  const result = streamText({    model: llm("claude-sonnet-4-5"),    system: buildSystemPrompt({ skills, locale }),    messages: await loadMessages(userChatId, { tools }),    tools,
    onStepFinish: async (step) => {      await saveStepToDB(step);      await trackTokens(step);    },
    onFinish: async () => {      // 持久化 workspace 变更      await saveUserWorkspace(userId, sandbox);    },  });
  return result.toUIMessageStreamResponse();}

关键点:

  • 启动时加载 skills 和 workspace
  • Skills 隔离在 skills/ 前缀下(只读)
  • Workspace 在根目录(可读写,持久化)
  • onFinish 将所有变更写回磁盘

3. Workspace 持久化

文件:lib/skill/workspace.ts

typescript
import type { Sandbox } from "bash-tool";
export async function loadUserWorkspace(userId: number): Promise<Record<string, string>> {  const workspacePath = getWorkspacePath(userId); // .next/cache/sandbox/user/{id}/workspace  const files: Record<string, string> = {};
  await loadDirectoryRecursively(workspacePath, "", files);  return files;}
export async function saveUserWorkspace(userId: number, sandbox: Sandbox): Promise<void> {  const workspacePath = getWorkspacePath(userId);
  // 获取所有非 skills/ 的文件  const findResult = await sandbox.executeCommand(    `find . -type f ! -path "./skills/*" 2>/dev/null || echo ""`  );
  const filePaths = findResult.stdout.split("\n").filter(Boolean);
  // 清空 workspace 并保存最新状态  await fs.rm(workspacePath, { recursive: true, force: true });  await fs.mkdir(workspacePath, { recursive: true });
  for (const filePath of filePaths) {    const content = await sandbox.readFile(filePath);    const fullPath = path.join(workspacePath, filePath.replace(/^\.\//, ""));    await fs.mkdir(path.dirname(fullPath), { recursive: true });    await fs.writeFile(fullPath, content);  }}

工作流程:

  1. 加载:从磁盘读取所有文件到内存
  2. Agent 工作:在沙箱中创建/修改文件
  3. 保存:提取非 skill 文件,写回磁盘
  4. 下次请求:文件重新出现在沙箱中

4. System Prompt

文件:app/prompt/index.ts

typescript
export async function buildUniversalSystemPrompt({ userId, locale, skills }) {  const skillsXml = skills.map(s => `<skill>  <name>${s.name}</name>  <description>${s.description}</description>  <location>skills/${s.name}/SKILL.md</location></skill>  `).join('\n');
  return `你是一个拥有专业技能的 Universal Agent。
## 可用技能
${skillsXml}
## Workspace 结构
\`\`\`sandbox/├── skills/                 # 只读技能(来自 S3)│   ├── technical-writer/│   └── market-researcher/└── my-project/            # 持久化工作空间    └── README.md\`\`\`
## 如何使用技能
1. **加载技能**:\`cat skills/technical-writer/SKILL.md\`2. **扮演角色**:完全遵循技能的指令3. **使用参考资料**:\`cat skills/technical-writer/references/examples.md\`
## 文件操作
- **创建**:\`writeFile({ path: "project/index.js", content: "..." })\`- **读取**:\`cat project/index.js\` 或 \`readFile({ path: "project/index.js" })\`- **导出**:\`exportFolder({ folderPath: "project" })\` 供用户下载
根目录的所有文件都会在对话间持久化。
## 准则
- 用户请求专业工作时加载相应技能- 精确遵循技能指令——这是你的专业知识- 在根目录创建文件(不在 skills/ 下)- 使用 bash 命令探索(ls、find、grep 等)`;}

关键设计:

  • 技能用 XML 格式(Claude 原生格式)
  • 清晰区分 workspace 和 skills
  • 强调持久化行为
  • 简单、可执行的指令

实战场景

场景 1:技术文档生成

用户:"为我们的支付系统写 API 文档"
Agent:  1. cat skills/technical-writer/SKILL.md  2. [加载技能,扮演技术作家角色]  3. writeFile({ path: "docs/api-reference.md", content: "..." })  4. writeFile({ path: "docs/examples.md", content: "..." })  5. exportFolder({ folderPath: "docs" })
→ 用户下载完整文档包

场景 2:跨会话工作

用户:"研究 AI 编程工具市场,创建报告"
Agent:  1. cat skills/market-researcher/SKILL.md  2. webSearch({ query: "AI coding tools 2025 市场分析" })  3. [分析结果,综合洞察]  4. writeFile({ path: "research/market-analysis.md", content: "..." })  5. [用户中断对话,去开会]
稍后:用户:"添加竞争格局章节"
Agent:  1. cat research/market-analysis.md  # 文件还在!  2. [继续处理现有文件]  3. writeFile({ path: "research/market-analysis.md", content: "..." })  4. exportFolder({ folderPath: "research" })

场景 3:Agent 创建自己的技能

用户:"创建一个 SQL 查询优化的技能"
Agent:  1. mkdir -p sql-optimizer  2. writeFile({ path: "sql-optimizer/SKILL.md", content: "..." })  3. writeFile({ path: "sql-optimizer/references/patterns.md", content: "..." })  4. exportFolder({ folderPath: "sql-optimizer" })
用户:下载,上传为 .skill 文件→ 现在可以在 skills/ 目录中使用了

关键设计决策

1. 为什么选 bash-tool?

考虑过的方案:

  • 直接文件系统访问 → 安全风险
  • Code interpreter(Python)→ 太局限
  • 完整 VM(@vercel/sandbox)→ 文件操作太重

bash-tool 的优势:

  • 内存隔离沙箱
  • 熟悉的 bash 命令(ls、cat、grep、find)
  • 不执行脚本(安全)
  • 完美适配文件操作 + 探索

2. 为什么分离 Skills 和 Workspace?

✅ 当前方案:skills/(只读)+ workspace/(可读写)❌ 备选方案:所有内容都在根目录
问题:Agent 可能意外覆盖 skills解决:清晰分离,在 prompt 中明确说明

3. 为什么用完整同步而非增量?

typescript
// ❌ 增量方案:跟踪并只保存变更的文件await saveChangedFiles(changedFiles);
// ✅ 完整同步:保存完整 workspace 状态await saveEntireWorkspace(allFiles);

理由:

  • 实现更简单(无需变更跟踪)
  • 自然处理删除(文件缺失 → 已删除)
  • 性能:workspace 通常 < 100 个文件,同步很快
  • 可靠性:避免同步漂移问题

4. 为什么技能用 Claude XML?

xml
<skill>  <name>technical-writer</name>  <description>创建技术文档</description>  <location>skills/technical-writer/SKILL.md</location></skill>

Claude 模型在训练时就使用 XML 结构处理工具和 artifacts。这种格式:

  • Claude 训练原生支持
  • 易于解析
  • 可扩展(添加元数据不会破坏现有结构)
  • Claude 技能发现时更熟悉

部署考虑

Docker 配置

挑战: bash-tool 使用原生模块(@mongodb-js/zstd、node-liblzma)做压缩。

解决方案: 不复制原生模块——它们是可选的。

dockerfile
# DockerfileCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# 注意:不复制原生压缩模块# - exportFolder 使用 jszip(纯 JS)# - just-bash 有 JS 降级方案处理压缩# 如果需要在沙箱中使用 tar -z,取消注释:# COPY --from=deps /app/node_modules/.pnpm/@mongodb-js+zstd@*/node_modules/@mongodb-js ./node_modules/@mongodb-js

next.config.ts:

typescript
webpack: (config, { isServer, webpack }) => {  if (isServer) {    // 只外部化原生二进制文件    config.externals.push("@mongodb-js/zstd", "node-liblzma");
    // 忽略仅浏览器的 worker.js    config.plugins.push(      new webpack.IgnorePlugin({        resourceRegExp: /^\.\/worker\.js$/,        contextRegExp: /just-bash/,      })    );  }  return config;}

存储管理

typescript
// 定期清理旧的导出文件export async function cleanupOldExports() {  const exportsDir = path.join(process.cwd(), ".next/cache/sandbox");  const cutoff = Date.now() - 24 * 60 * 60 * 1000; // 24 小时
  for (const userId of await fs.readdir(path.join(exportsDir, "user"))) {    const exportsPath = getExportsPath(Number(userId));    for (const file of await fs.readdir(exportsPath)) {      const stat = await fs.stat(path.join(exportsPath, file));      if (stat.mtimeMs < cutoff) {        await fs.unlink(path.join(exportsPath, file));      }    }  }}

性能基准

基准测试(M1 Mac,50 个 skills,200 个 workspace 文件):

操作时间备注
加载 skills + workspace120ms并行 I/O
创建 10 个文件5ms内存操作
保存 workspace80ms写入磁盘
导出文件夹(50 个文件)150msZip 创建

优化建议:

  1. 跨请求缓存 skill 文件(同一用户、同样的 skills)
  2. 大文件导出使用流式传输
  3. workspace > 1MB 时用 zstd 压缩

下一步

扩展系统:

  1. 版本控制:为 workspace 集成 Git
  2. 技能市场:用户间共享技能
  3. 协作工作空间:多个 agent,一个 workspace
  4. 技能组合:Agent 同时加载多个技能
  5. 技能继承:基础技能 + 专业化变体

核心理念: 每个用户都能成为拥有完整工程团队的创始人。每个技能就是一个专家。Agent 负责协调。Workspace 持久化。可能性无限。

代码参考

  • 主路由:src/app/(universal)/api/chat/universal/route.ts
  • Workspace 持久化:src/lib/skill/workspace.ts
  • Skill 加载:src/lib/skill/loadToMemory.ts
  • 导出工具:src/app/(universal)/tools/exportFolder.ts
  • System prompt:src/app/(universal)/prompt/index.ts
  • README:src/app/(universal)/README.md

基于 Next.js 15、Vercel AI SDK、bash-tool 和 Claude 4.5 Sonnet 构建。

用户上传 skill 包 → S3 存储                      ↓请求开始 → 加载 skills + workspace → 内存沙箱                      ↓Agent 读取技能、创建文件、使用工具                      ↓请求结束 → 保存 workspace 变更 → 磁盘                      ↓下次对话 → 文件持久化 → Agent 继续工作
my-skill/├── SKILL.md          # 指令和专业知识└── references/       # 可选:参考文档    └── examples.md
---name: technical-writerdescription: 创建技术文档、API 指南和教程---
# Technical Writer Skill
你是一位专业技术写作专家,擅长创建清晰简洁的文档。
## 专业领域- API 文档(带示例)- 架构决策记录(ADR)- 用户指南和教程- 代码注释和内联文档
## 写作准则- 先讲为什么,再讲是什么,最后讲怎么做- 提供实用示例- 使用主动语态- 保持 DRY——用链接而非重复
## 激活方式当用户说:- "写一份文档..."- "创建 API 指南..."- "为这个代码库写文档..."
import { streamText } from "ai";import { createBashTool } from "bash-tool";import { loadAllSkillsToMemory } from "@/lib/skill/loadToMemory";import { loadUserWorkspace, saveUserWorkspace } from "@/lib/skill/workspace";
export async function POST(req: Request) {  const { userId, userChatId } = await authenticate(req);
  // 从 S3/磁盘加载 skills  const skills = await prisma.agentSkill.findMany({ where: { userId } });  const skillFiles = await loadAllSkillsToMemory(skills);
  // 加载持久化的 workspace  const workspaceFiles = await loadUserWorkspace(userId);
  // 给 skills 加前缀实现隔离  const skillFilesWithPrefix = Object.fromEntries(    Object.entries(skillFiles).map(([path, content]) => [`skills/${path}`, content])  );
  // 创建沙箱  const { tools: bashTools, sandbox } = await createBashTool({    files: {      ...workspaceFiles,         // 用户的工作文件(根目录)      ...skillFilesWithPrefix,   // Skills(skills/ 子目录)    },  });
  // 合并所有工具  const tools = {    ...baseTools,              // webSearch, reasoningThinking 等    bash: bashTools.bash,    readFile: bashTools.readFile,    writeFile: bashTools.writeFile,    exportFolder: exportFolderTool({ sandbox, userId }),  };
  // 流式响应 + 持久化  const result = streamText({    model: llm("claude-sonnet-4-5"),    system: buildSystemPrompt({ skills, locale }),    messages: await loadMessages(userChatId, { tools }),    tools,
    onStepFinish: async (step) => {      await saveStepToDB(step);      await trackTokens(step);    },
    onFinish: async () => {      // 持久化 workspace 变更      await saveUserWorkspace(userId, sandbox);    },  });
  return result.toUIMessageStreamResponse();}
import type { Sandbox } from "bash-tool";
export async function loadUserWorkspace(userId: number): Promise<Record<string, string>> {  const workspacePath = getWorkspacePath(userId); // .next/cache/sandbox/user/{id}/workspace  const files: Record<string, string> = {};
  await loadDirectoryRecursively(workspacePath, "", files);  return files;}
export async function saveUserWorkspace(userId: number, sandbox: Sandbox): Promise<void> {  const workspacePath = getWorkspacePath(userId);
  // 获取所有非 skills/ 的文件  const findResult = await sandbox.executeCommand(    `find . -type f ! -path "./skills/*" 2>/dev/null || echo ""`  );
  const filePaths = findResult.stdout.split("\n").filter(Boolean);
  // 清空 workspace 并保存最新状态  await fs.rm(workspacePath, { recursive: true, force: true });  await fs.mkdir(workspacePath, { recursive: true });
  for (const filePath of filePaths) {    const content = await sandbox.readFile(filePath);    const fullPath = path.join(workspacePath, filePath.replace(/^\.\//, ""));    await fs.mkdir(path.dirname(fullPath), { recursive: true });    await fs.writeFile(fullPath, content);  }}
export async function buildUniversalSystemPrompt({ userId, locale, skills }) {  const skillsXml = skills.map(s => `<skill>  <name>${s.name}</name>  <description>${s.description}</description>  <location>skills/${s.name}/SKILL.md</location></skill>  `).join('\n');
  return `你是一个拥有专业技能的 Universal Agent。
## 可用技能
${skillsXml}
## Workspace 结构
\`\`\`sandbox/├── skills/                 # 只读技能(来自 S3)│   ├── technical-writer/│   └── market-researcher/└── my-project/            # 持久化工作空间    └── README.md\`\`\`
## 如何使用技能
1. **加载技能**:\`cat skills/technical-writer/SKILL.md\`2. **扮演角色**:完全遵循技能的指令3. **使用参考资料**:\`cat skills/technical-writer/references/examples.md\`
## 文件操作
- **创建**:\`writeFile({ path: "project/index.js", content: "..." })\`- **读取**:\`cat project/index.js\` 或 \`readFile({ path: "project/index.js" })\`- **导出**:\`exportFolder({ folderPath: "project" })\` 供用户下载
根目录的所有文件都会在对话间持久化。
## 准则
- 用户请求专业工作时加载相应技能- 精确遵循技能指令——这是你的专业知识- 在根目录创建文件(不在 skills/ 下)- 使用 bash 命令探索(ls、find、grep 等)`;}
用户:"为我们的支付系统写 API 文档"
Agent:  1. cat skills/technical-writer/SKILL.md  2. [加载技能,扮演技术作家角色]  3. writeFile({ path: "docs/api-reference.md", content: "..." })  4. writeFile({ path: "docs/examples.md", content: "..." })  5. exportFolder({ folderPath: "docs" })
→ 用户下载完整文档包
用户:"研究 AI 编程工具市场,创建报告"
Agent:  1. cat skills/market-researcher/SKILL.md  2. webSearch({ query: "AI coding tools 2025 市场分析" })  3. [分析结果,综合洞察]  4. writeFile({ path: "research/market-analysis.md", content: "..." })  5. [用户中断对话,去开会]
稍后:用户:"添加竞争格局章节"
Agent:  1. cat research/market-analysis.md  # 文件还在!  2. [继续处理现有文件]  3. writeFile({ path: "research/market-analysis.md", content: "..." })  4. exportFolder({ folderPath: "research" })
用户:"创建一个 SQL 查询优化的技能"
Agent:  1. mkdir -p sql-optimizer  2. writeFile({ path: "sql-optimizer/SKILL.md", content: "..." })  3. writeFile({ path: "sql-optimizer/references/patterns.md", content: "..." })  4. exportFolder({ folderPath: "sql-optimizer" })
用户:下载,上传为 .skill 文件→ 现在可以在 skills/ 目录中使用了
✅ 当前方案:skills/(只读)+ workspace/(可读写)❌ 备选方案:所有内容都在根目录
问题:Agent 可能意外覆盖 skills解决:清晰分离,在 prompt 中明确说明
// ❌ 增量方案:跟踪并只保存变更的文件await saveChangedFiles(changedFiles);
// ✅ 完整同步:保存完整 workspace 状态await saveEntireWorkspace(allFiles);
<skill>  <name>technical-writer</name>  <description>创建技术文档</description>  <location>skills/technical-writer/SKILL.md</location></skill>
# DockerfileCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# 注意:不复制原生压缩模块# - exportFolder 使用 jszip(纯 JS)# - just-bash 有 JS 降级方案处理压缩# 如果需要在沙箱中使用 tar -z,取消注释:# COPY --from=deps /app/node_modules/.pnpm/@mongodb-js+zstd@*/node_modules/@mongodb-js ./node_modules/@mongodb-js
webpack: (config, { isServer, webpack }) => {  if (isServer) {    // 只外部化原生二进制文件    config.externals.push("@mongodb-js/zstd", "node-liblzma");
    // 忽略仅浏览器的 worker.js    config.plugins.push(      new webpack.IgnorePlugin({        resourceRegExp: /^\.\/worker\.js$/,        contextRegExp: /just-bash/,      })    );  }  return config;}
// 定期清理旧的导出文件export async function cleanupOldExports() {  const exportsDir = path.join(process.cwd(), ".next/cache/sandbox");  const cutoff = Date.now() - 24 * 60 * 60 * 1000; // 24 小时
  for (const userId of await fs.readdir(path.join(exportsDir, "user"))) {    const exportsPath = getExportsPath(Number(userId));    for (const file of await fs.readdir(exportsPath)) {      const stat = await fs.stat(path.join(exportsPath, file));      if (stat.mtimeMs < cutoff) {        await fs.unlink(path.join(exportsPath, file));      }    }  }}