从零拆一个 Agent 主循环:我读 OpenWorkBuddy 源码学到的 7 个设计决定

从零拆一个 Agent 主循环:我读 OpenWorkBuddy 源码学到的 7 个设计决定

关键词:Agent 架构、工具调用、上下文管理、权限模型、开源代码

想搞明白 Agent 到底怎么转,看论文不如看代码。但大部分 Agent 项目的代码看不懂——不是难,是构建链太厚:TypeScript 编译、框架抽象、依赖注入,你想知道"一句话进去怎么变成文件",中间隔了七八层。

OpenWorkBuddy 这个项目的好处是没有构建步骤。clone 下来 npm install && npm start 就跑,agent 主循环是手写的,顺着 server.js → agent.js 能一路读下去。

项目:OpenWorkBuddy,本地优先的 AI 办公 Agent
地址:https://github.com/CatCatUncle/openworkbuddy
协议:PolyForm Noncommercial 1.0.0(公司内使用需商业授权)

我花了两天读它的实现,挑出 7 个我觉得值得学的设计决定,记在这里。


决定一:主循环是手写的,不套框架

现在做 Agent 最省事的办法是套 LangChain 或者别的框架。这个项目没套。

主循环大致是:

用户消息
  → 组装系统提示词(含技能清单、记忆、项目指令)
  → 调模型,拿回工具调用
  → 执行工具,结果塞回上下文
  → 再调模型
  → 直到模型不再要工具,或者撞上限
  → 交付前核验产物

代码落在 agent.js 里。OpenWorkBuddy 主循环最直接的好处是:它没有构建链,上面这几步在源码里就是顺序写下来的,不用先去理解一个框架的抽象层。

手写的好处不是性能,是可控。

框架帮你处理了"调模型→执行工具"这个循环,但也把中间状态藏起来了。当你想知道"为什么它第 3 轮选了这个工具没选那个",框架里你得翻源码;手写的话,那就是你自己写的 20 行。

对要学 Agent 的人来说,能读懂比跑得快重要。


决定二:技能是 Markdown,不是代码

skills/<技能名>/skill.md

写一个 Markdown 文件,存盘,下一条任务生效。不改代码、不重启、不打包。

OpenWorkBuddy 内置了 34 个技能(deep-research 深度调研、ppt-design PPT、excel-report Excel 报表、html-page 网页生成、weekly-report 周报……),全部就是这样一个个 Markdown。你要是想加一个,照格式写一份就行。

这个设计看着简单,但它解决了一个真问题:Agent 的能力扩展,大部分时候根本不需要写代码。

"把这份材料做成周报"这种能力,本质是一段提示词加一套流程规范,不是一段程序。用代码插件的方式做,你得:

  • 定义接口
  • 写 TypeScript
  • 编译
  • 重启

用 Markdown,你只需要会写字。

代价是安全边界变模糊了——Markdown 可以写任意指令,这跟 npm 包投毒是同一类问题。这个项目对这个的处理见决定五。


决定三:工具调用会被"救回来"

这是我读代码时最意外的一处。

现象是这样的:DeepSeek 这类模型的工具调用,在权重里是特殊 token。经过某些中转层(不是官方的 API 网关,是第三方聚合那类)时,这些 token 不会被解析进 tool_calls 字段,而是原样解码进正文。

结果就是:模型以为自己调用了抓网页的工具,实际上什么都没发生。然后它接着往下走,开始编造抓取结果。

这不是提示词能治的问题——模型是真的不知道工具没执行。

项目的做法是在 llm.js 里认出这些标记,还原成真正的工具调用去执行。另外在流式输出上也设了闸门,特殊 token 不会漏到界面上。参数只吐了一半就整个丢掉,宁可重来也不拿半截参数去跑。

如果读 OpenWorkBuddy 只读一处,我推荐读这块。 这是真实的工程问题,而且你在论文里看不到——论文假设工具调用是可靠的。


决定四:只读的活并发跑,写的一律串行

模型一轮里同时要了 5 个 web_search / fetch_url / read_file,就并发执行(上限 3 路),只花最慢那一次的时间。

但只要这一批里混进了写文件、跑命令、委派专家的调用,整批退回串行。

这个判断很准确:

  • 读操作之间没有顺序关系 → 并发是纯收益
  • 写操作之间往往有依赖(先创建再写入)→ 并发是 bug 制造机

"哪些能并发"不取决于性能,取决于语义。 很多项目为了快什么都并发,然后在随机的地方挂掉。

顺带一个细节:界面上的过程卡是按调用 id 配对的,不是按顺序。不然谁先回来就会把 A 的结果贴到 B 的卡上。


决定五:装技能是另一道门

上面那些闸门守的是"agent 跑的时候想干什么"。技能守的是另一头:它照着谁写的指令跑。

装一个技能 = 把陌生人写的指令接进来,而且装完之后没有第二道门——权限档位管的是执行,管不了"谁在下指令"。

所以 skill-guard.js 卡在安装路径上,34 条规则,三档结论:

结论 条数 装不装
block 10 默认不装,平台管理员可显式强装(写日志)
warn 24 摊开给你看,点一次"我看过了"才装
ok — 直接装

几个特有判断值得记:

① frontmatter 里的 description 命中会升一级。

因为它每一条任务都会进系统提示词(agent.js 里那份技能清单),不管这次用不用得上。写在正文里的注入还得等技能被加载,写在这儿的是常驻。

② 不逐个文件扫 node_modules / .venv。

那里头一条都不是技能作者写的,逐个扫只会把真该看的几行淹掉。实测一个 .venv 能贡献 60 多条假命中。

③ 技能是整台机器一份的。

skills/ 只有一个目录,不分组织。所以加这道闸之前,任何一个登录用户都能给所有人的 agent 递指令。这也是为什么增删改装全部锁在平台管理员。

它的上限也很诚实:定位是"装之前把话说清楚",不是查毒。公开标注集上纯静态规则检出率约 74.9%,四个漏一个。所以 block 必须能被人显式放行——一道人绕不过去的闸,人就会绕过整个工具(直接把目录拷进 skills/,什么也拦不住)。


决定六:状态文件全部原子写 + .bak

会话、账本、定时任务表这些"丢了就真丢了"的文件,统一走 store.js:

写:先写临时文件再改名(同一分区上改名是原子的),改名前把上一版留成 .bak。写到一半断电,盘上要么是旧的要么是新的,不会是半个 JSON。

读:分得清"没有这个文件"和"文件坏了"。坏了先拿 .bak 顶上;连 .bak 都没有,就把坏文件改名隔离成 .corrupt-时间戳 留着人工捞。

绝不静默当成空的——那样下一次保存就把用户的对话/账号/定时任务永久覆盖掉了,全程一句提示都没有。

这里有个分档处理我觉得特别对:账本(users.json)走 strict,坏了直接报错停下来,不自动回退 .bak。

理由写得很清楚:回退一版可能正好吞掉一笔充值,这种事得让人自己拍板。

什么时候该自动恢复、什么时候必须停下来报警,这个区分比"统一容错"高明。


决定七:假绿裁定

"没抛异常"不等于"干成了"。

有四类情况以前在运行记录里全是 ✅:

  1. 撞步数/时长上限被强制收尾
  2. 整条回复就是上游的报错
  3. 一个字都没吐
  4. 把活丢后台就收工

现在的做法是每次运行都过一道裁定,判红就写清为什么红和下一步干什么(充值 / 换 Key / 等限流窗口 / 开自动续跑),并按死因分档标出"重跑能好"还是"重跑没用"。

这个设计背后的意识是:自动化系统最危险的状态不是失败,是"看起来成功"。

失败你知道要处理,假成功你什么都不知道,等下游发现问题时上游已经查不回去了。

同一套思路还用在产物核验上:声称生成了文件却不在磁盘上、或者只有 0 字节,会被打回去重做(最多 2 次)。


几个可以直接抄走的模式

如果我只给三条:

1. 状态文件永远原子写,且区分"没文件"和"文件坏"

前者正常,后者必须报警。

2. 完成任务的条件是产物存在,不是模型说它存在

去问文件系统,不信自我报告。便宜而且权威。

3. 并发与否看语义,不看性能

读并发、写串行,混了就整体退回串行。


怎么开始读

git clone https://github.com/CatCatUncle/openworkbuddy.git
cd openworkbuddy && npm install
npm start   # → http://localhost:3800
npm test    # 不需要 API Key,全绿

npm test 不需要 Key 这点挺重要——用模拟 LLM,纯逻辑测试。所以你在没配任何模型的情况下,就能跑通测试、看懂数据流。

代码里的入口:server.js(运行时)→ agent.js(主循环)→ llm.js(模型与工具调用)→ skills.js(技能加载)→ skill-guard.js(安装闸门)。


本文基于该项目公开源码与文档整理,项目采用 PolyForm Noncommercial 1.0.0 协议,公司内使用需商业授权,细节见仓库 LICENSE。文中不涉及任何价格信息。

posted @ 2026-09-26 19:34  大道无形1998  阅读(14)  评论(0)    收藏  举报