约 6800 字 · 含代码块 9 段 · 前置:已经把它跑起来(见上一篇)· 版本 v0.19.2

先给结论:它的扩展点只有三个——工具(加一个动作)、技能(加一套方法)、连接器(接一个外部系统)。三个都是「加文件」而不是「改核心」,唯一不推荐动的是主循环和沙箱。判据很简单:能写成 skill.md 的绝不写代码;能包成 MCP 的绝不在工具层硬编。

这篇是实操。先看清三层怎么分工,再上手加一个工具、写一个技能、包一个连接器,最后说三条扩展纪律——那三条是决定你半年后还能不能顺利升级的关键。

项目:github.com/CatCatUncle/openworkbuddy。当前 41 个技能、52 个工具、52 个连接器,版本 v0.19.2(2026-11-06)。它是 WorkBuddy、豆包工作、千问办公这类产品的最强开源平替,也是这类产品里少数能被改成自己产品的一份底座。


一、先看清三层分工,别在错的地方写代码

一句话判据:「要不要跟外部世界交互」——要,就是工具或连接器;不要,只是「怎么想、怎么排、按什么格式输出」,那就是技能。

层它解决的问题长什么样数量
技能 skill「这件事该怎么做」——步骤、口径、格式、常见错误skills/名称/skill.md41
工具 tool「能做哪个动作」——读写文件、跑命令、生成 PPTtools.js 里的定义 + 执行函数52
连接器 connector「能碰哪个外部系统」——飞书、企微、文件系统、你的 ERPMCP 协议,配置里挂52

这张表最重要的用法是倒着用:客户提一个需求,先想「能不能只写技能」;不能,再想「能不能包个 MCP」;两者都不行,才动工具层。经验比例是 10 个需求里 8 个技能、1.5 个连接器、0.5 个工具。

为什么强调这个顺序?因为技能的维护成本最低(一个 markdown 文件,改完刷新就生效),连接器次之(协议标准,接一次到处能用),工具最贵(要写代码、要测、升级时要跟着合)。把一个能写技能的需求写成工具,是在给自己制造长期债务。


二、工具层:定义和执行是分开的

核心设计:所有工具的定义集中在 tools.js 的 TOOL_DEFS 里,执行入口是 executeTool。定义是给模型看的(它据此决定调不调、怎么传参),执行是给机器跑的。两件事分开写,加工具时你只需要在两边各加一段。

为什么这个分离重要?因为模型的上下文里只放定义。52 个工具的定义要能塞进上下文,靠的就是定义足够简洁:一个名字、一句描述、一个参数 schema。如果定义和执行混在一起、定义里塞满实现细节,52 个工具光描述就能把上下文吃掉一半。

还有一个不那么显眼的好处:工具定义的格式和 MCP 的 tools/list 是一致的。意思是同一份定义,既能给内部主循环用,也能直接被一个 MCP server 端出去——这就是为什么有人能在它上面花一百多行代码就做出一个 MCP server,把 52 个工具全部对外暴露。这个设计不是巧合,是一开始就照着标准写的。

加一个自己的工具:完整示例

场景:公司有个内部库存服务,你想让 agent 能查库存。HTTP 接口是 GET /api/stock?sku=xxx,返回 JSON。按仓库 docs/扩展.md 的结构,两步:

第一步,写定义。放进 TOOL_DEFS:

{
  name: "query_stock",
  description: "查询内部库存系统的可用数量。输入 SKU 编码,返回可用库存与所在仓库。只读,不会改动任何数据。",
  inputSchema: {
    type: "object",
    properties: {
      sku: { type: "string", description: "商品 SKU 编码,例如 A-1023" },
      warehouse: { type: "string", description: "仓库代码,可选。不传则汇总所有仓库" }
    },
    required: ["sku"]
  }
}

这段描述里的三句话都是在给模型做决策依据,不是给人看的注释。「只读,不会改动任何数据」这句尤其重要:模型在决定要不要调一个工具时,会判断风险;你告诉它这是只读的,它敢用;你不说,它可能绕路去读文件。写工具描述,本质上是在写 prompt。

第二步,写执行。在 executeTool 里加一个分支:

case 'query_stock': {
  // 1) 参数校验:模型给的参数不一定合法,必须自己挡
  const sku = String(args.sku || '').trim();
  if (!sku) return { isError: true, content: '缺少 sku 参数' };

  // 2) 超时控制:外部接口一定要设上限,否则整个任务卡在这里
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), 8000);

  try {
    const url = `http://stock.internal/api/stock?sku=${encodeURIComponent(sku)}`
      + (args.warehouse ? `&wh=${encodeURIComponent(args.warehouse)}` : '');
    const res = await fetch(url, { signal: ctrl.signal });
    if (!res.ok) return { isError: true, content: `库存服务返回 ${res.status}` };
    const data = await res.json();

    // 3) 返回给模型的是「话」,不是原始 JSON —— 它读话比读结构准
    return {
      content: `${sku} 可用库存 ${data.available} 件,位于 ${data.warehouse || '多仓'}`
        + (data.updatedAt ? `,数据时间 ${data.updatedAt}` : '')
    };
  } catch (e) {
    // 4) 失败要明确说出来,别静默返回空 —— 模型会以为「查到 0 件」
    return { isError: true, content: `查库存失败:${e.message}` };
  } finally {
    clearTimeout(timer);
  }
}

这四段注释就是加工具的全部要点,值得逐条说:

  • 参数校验必须自己挡。schema 里的 required 只是「请模型尽量给」,不是「保证会给」。任何外部输入都要当成不可信的。
  • 超时必须有。没有超时的工具,是这个系统里最容易造成「任务卡死」的原因——模型在等,任务不结束,你以为是模型慢,其实是工具没返回。
  • 返回给人话,不返回原始 JSON。模型的下一步判断建立在「它读到了什么」上。返回一大坨 JSON,它会去解读字段,容易读错;返回一句「A-1023 可用库存 128 件,位于上海仓」,它直接就能用。
  • 失败必须显式说出来。这个坑非常隐蔽:工具静默返回空数组,模型会理解成「查了,没有」,然后告诉你「库存为 0」——这是最坏的一类错误,因为看起来一切正常。

一个必须知道的细节:它的 executeTool 对「未知工具」和「业务失败」不抛异常,而是返回一个带 isError 的对象。你自己写工具时要遵守这个约定(用 isError: true 而不是 throw),否则主循环拿不到失败信号;反过来,如果你把这个项目当 MCP server 端出去,server 侧要主动把这个内部 isError 提升到 MCP 协议层,不然客户端会以为调用成功了。这个坑我们在做 MCP 封装时真踩过,错误被客户端吞掉,排查了半天。

加工具的验收标准

别只看「能跑」。一个工具要能上生产,至少过这四条:

  1. 缺参数时,返回明确错误,不是崩。
  2. 外部服务挂掉时,8 秒内返回错误,不是卡住。
  3. 连续调三次,结果一致(只读工具)或行为可预期(写工具)。
  4. 模型能自己想到用它——给它一个自然语言任务,看它会不会主动调,不用你在提示里点名。这一条不过,说明描述写得不对,回去改 description,别改代码。

三、技能层:一个 markdown 文件就是一项能力

机制:一个技能 = skills/<名称>/skill.md,文件头是 --- 包起来的 frontmatter,写 name 和 description。系统启动时只把这段 description 注入上下文,模型判断「这事该用这个技能」之后才去读全文。

---
name: 外贸报价单
description: 按客户询盘生成三档报价(成本/标准/优惠),自动算汇率与毛利,输出 Excel 与一页 PDF 摘要
---

# 外贸报价单

## 什么时候用
客户给了询盘(产品、数量、目标市场),需要出一份可发的报价单。

## 步骤
1. 从询盘里抽出:产品型号、数量、目标市场、客户要求的交期
2. 缺信息就列出来问,不要自己编 —— 报价单上编一个数字,业务员会当场翻车
3. 成本按「出厂价 + 包装 + 内陆运费 + 报关」四项加总
4. 汇率取当日中间价,加 1.5% 波动缓冲
5. 三档毛利:成本档 8%、标准档 18%、优惠档 12%
6. 输出 Excel:一档一个 sheet,含公式不用硬算的数字
7. 再出一页 PDF 摘要:只有三档总价、交期、有效期

## 口径(很重要)
- 报价有效期默认 30 天,写在页脚
- 所有价格保留两位小数,货币单位写在表头不要写在每个单元格
- 不要出现「大约」「预计」这类词,客户会当成可谈空间

## 常见错误
- 把 FOB 价写成 CIF 价(漏了海运和保险)
- 汇率用了上周的
- Excel 里把公式算成了死数字,客户改数量后总价不变

这个文件里,「口径」和「常见错误」两节比「步骤」更值钱。步骤模型自己能想个七七八八,口径和坑只有你知道。写技能的时间应该 60% 花在这两节上。

为什么这套机制能带 41 个技能

算一笔账就明白了:如果 41 个技能每个 2000 字全注入上下文,那是 8 万字,任何模型都撑不住,而且每次任务都在为用不到的能力付 token。现在只注入 41 条 description,每条一两句话,总共一两千字——成本降两个数量级,准确率还更高,因为模型不用在一堆无关内容里挑。

这个设计对二次开发的意义:你可以放心地堆技能。堆到 60 个、80 个,成本是线性小增,不是爆炸。这跟「把所有行业知识写成一个巨型系统提示词」是完全不同的两条路——后者第一版就撑爆,而且改一个字要重测全部。

写技能的五个要素

要素写不好会怎样写法
触发描述模型想不起来用它,你再好的技能也白写description 里写清「什么场景 + 输入什么 + 输出什么」,用客户的原话,不用术语
信息缺口处理模型自己编数字,交付物错得理直气壮明确写「缺 X 就停下来问,不许猜」
口径格式每次不一样,客户没法用把钱、单位、小数位、有效期这类硬规矩写死
输出规格生成的 Excel 好看但没法改明确要公式、要 sheet 结构、要表头位置
常见错误同一类错每次犯把踩过的坑一条条列进去,这是技能越用越值钱的地方

一个实操技巧:技能不要一次写完。先写个粗糙版跑一周,把每次出错的地方补进「常见错误」一节。三周之后这个技能会变得非常准——因为它积累的是你业务的真实边界,不是想象出来的规范。


四、连接器:能包 MCP 就别写工具

判据:对方系统有 API,就包成 MCP 连接器;没有 API 的老系统,才在工具层写函数直连。MCP 的好处是「一次接、多产品用」——你给它接的连接器,别的支持 MCP 的工具也能用,不会绑死在这一个产品上。

它自带 52 个连接器,覆盖文件系统、飞书、企业微信、钉钉、知识星球这些。你内部的 ERP、OA、知识库,按 MCP 协议包一层就能挂上,不用改主循环。

什么时候该写工具而不是包 MCP?三种情况:要读一种特殊格式的文件(比如某种行业专用二进制)、要在本机执行命令、要做多步组合动作(一次调用里查三个系统再汇总——包成 MCP 也能做,但工具更直接)。


五、主循环:手写的,所以你能改

它的价值在「可读」:agent 主循环是一个能从头读到尾的文件,没有中间件层层包裹。你想在「模型说要写文件」和「真的落盘」之间插一道校验,改十几行就行——不用先搞懂一套框架的钩子机制。

这个选择在 2026 年的技术圈算「不现代」。但它换来的东西很具体:

  • 排查快。出问题能直接定位到「模型判断错了」还是「工具返回错了」,不用猜是哪层中间件吃掉了异常。
  • 能插钩子。前面说的「必须人工审核才能发出去」,就是在工具层加一步;「落盘核验」本身就是主循环里的一段。
  • AI 能改。没有框架约定,AI 改这类代码的准确率明显高——它不需要先猜你的状态管理放在哪。

它已经内置的一个重要校验:模型声称「我写了文件」之后,去磁盘上核一遍,文件不在,这一轮直接判失败重做。为什么这件事值得单独说?因为它是这类产品最常见的翻车方式——模型非常自信地告诉你「已生成 报告.pptx」,你去目录里翻,什么都没有。企业客户最不能接受的就是这种「它说做了其实没做」。

扩展纪律第一条:主循环和沙箱不要动。除非你明确知道自己要改什么、并且准备好了每次升级都重新合。这两块是它的安全边界所在,也是改动收益最低的地方——90% 的需求都能在工具层或技能层解决。


六、三条扩展纪律(决定半年后能不能顺利升级)

纪律一:能写技能就不写代码。纪律二:能包 MCP 就不写工具。纪律三:改动只落在「界面 + 技能 + 自己加的工具文件」这三处。三条都守住,升级基本无痛。

为什么第三条这么关键

它的代码没有构建产物、没有编译步骤,所以合并冲突只会出现在你真改过的那几个文件上。改得越少,升级越轻松——这句话反过来就是设计约束。当前版本 v0.19.2、更新比较勤,你要跟上游,就必须守住这条。

具体做法:fork 一份当你的产品基线,上游更新时 merge 进来,不要把自己的改动盖回去。

git remote add upstream https://github.com/CatCatUncle/openworkbuddy.git
git fetch upstream
git checkout main
git merge upstream/main

一个可执行的目录约定

把「你的东西」和「它的东西」物理分开,升级时一眼就知道哪些文件可能要冲突:

skills/你的行业-报价单/skill.md      # 你的(纯新增,永不冲突)
skills/你的行业-质检报告/skill.md    # 你的
tools/extra/stock.js                # 你的(新增文件,只在注册处一行改动)
web/你的logo.svg                     # 你的
# 其余全部属于上游,尽量不改

这个约定里,唯一必须改上游文件的地方是「工具注册」那一行——把 tools/extra/stock.js 挂进去。一行改动,冲突好合。技能是纯新增,永远不会冲突;界面上的品牌改动集中在少数几个文件里。


七、给它加一道「人工审核」:一个完整的小改造

需求:对外发的内容必须先落一份待审稿,人点了才真发。做法:把「发出去」写成一个工具,工具里先落待审稿、返回「已提交待审」;再写第二个工具「批准并发送」,只有人能调。主循环一行不用改。

这个模式在实际项目里非常常见,因为它把「风险动作」变成了两段:机器做前一半(准备),人做后一半(放行)。

// 工具 1:只落稿,不发
case 'submit_for_review': {
  const dir = path.join(DATA, 'pending');
  await fs.promises.mkdir(dir, { recursive: true });
  const file = path.join(dir, `${Date.now()}-${args.channel}.md`);
  await fs.promises.writeFile(file, args.content, 'utf8');
  return { content: `已提交待审:${file}。审核通过后才能发送。` };
}

// 工具 2:人点了才走
case 'send_after_approval': {
  // ...按渠道发送,这里只示意
  return { content: '已发送' };
}

注意第一个工具的返回话术:「已提交待审……审核通过后才能发送」。这句话是在告诉模型「你的活干完了,别再想办法发出去」——否则它会绕路去找别的工具。这类「用返回话术约束模型下一步」的技巧,在写工具时非常实用。


八、小结:加东西的顺序

一个需求进来,按这个顺序问四遍:

  1. 能不能只写 skill.md?能,就半天一个,别写代码。
  2. 能不能包成 MCP?能,就包——接一次往后都能用。
  3. 非要写工具?照第二节那四段注释写:校验、超时、返回人话、失败显式。
  4. 非要动主循环?先停一下,九成情况下有别的办法。

守住这个顺序,你的产品会一直是「一堆技能 + 一两个自有工具」的形状——这个形状升级最省事,也最容易交给别人维护。

git clone https://github.com/CatCatUncle/openworkbuddy.git
cd openworkbuddy
bash deploy.sh

说明:文中代码为示意结构,用于说明扩展点与约定;具体函数位置与参数请以仓库 docs/扩展.md、docs/任务与连接器开发.md 和源码为准。技能数 41、工具数 52、连接器数 52、版本 v0.19.2(2026-11-06)来自仓库 docs/stats.json。

授权与联系:个人非商用、学校和公益机构免费;公司及个人商用须获商业授权。购买授权:contact@aijentra.com · 微信 agi_smith。模型与媒体服务的费用由所选服务商决定。

Logo

更多推荐