leonx.ai Don't trust, verify
← 回首页

中文 · English

为了搞懂 Yegge 在讲的多 agent 编排,我手写了一个玩具 orchestrator

2026-06-13 · 一个学习用玩具的 build log

起因是 Steve Yegge:他一直在讲多 agent 编排、还做了个开源 orchestrator 叫 Gastown,说大多数工程师还停在「在 IDE 里问一句、仔细审一遍」的底层。我想搞懂那到底是怎么回事——与其听他讲,不如自己手写一个最小的玩具摸一遍。

东西叫 townhall,纯 Ruby、DeepSeek 驱动:一句话进去、一个能点开的网页出来。目标就一个,把「协调 / 并行 / 收敛」从「听说过」变成「亲手摸过」。

(写着写着跑偏了:编排其实是表层,真正花我时间的是「怎么知道生成出来的东西是对的」——这个后面会讲。)

下面每一节基本都对应仓库里一个能跑的脚本。Don't trust, verify——自己跑一遍,这本来就是全文的主题。

一句话 → N 个版本 → 挑一个

第一版很笨:同一个需求,起 3 个 worker,各按一个风格角度(简洁 / 华丽 / 信息密集)各出一份完整 HTML,再让一个 judge 挑。Parallel.map 起线程——MRI 有 GIL,但等 LLM 的 HTTP 响应时 GIL 会释放,所以三个几乎同时跑完,不排队。

这就是 Yegge 说的 slot machine programming,多拉几次杆挑中奖的。跑通了,但说实话这时候它还只是个批量生成器,谈不上编排。

当时卡住的一个问题:任何任务都该这么「生成 N 个挑一个」吗?

不是。这招只在三件事同时成立时才划算——产物质量本身抖得厉害、你能可靠挑出最好的、而且结果值这个 N 倍的钱。像「3847 × 2913 等于几」或者「把这句话分类」,答案唯一,抖只是噪声,生成一次就够。Yegge 说 Claude 团队「做 20 个原型挑 1 个」能成立,是因为原型这三条全顶满了。

把 agent 放进 loop

Yegge 有句话我记到现在:completion 是 loop,agent 是 chat 放进 loop,orchestrator 就是 agent 放进 loop。

这个「放进 loop」最纯粹的样子是工具调用——模型不直接作答,而是请求调个工具,你跑完把结果喂回去,它再接着想:

$ ruby bin/try-tools
A) 不给工具:模型把 3847×2913 答成 11,204,511   ❌(真实是 11206311)
B) 给它一个 calc 工具:
  ① 模型请求调工具:calculator({"a"=>3847, "b"=>2913})   ← 不是答案,是个请求
  ② 代码执行,得到 11206311   ← 喂回给模型
  ③ 模型给终答:11206311   ✅

模型自己判断「这个我算不准」,借了工具——这就是从「会说」到「能做」。

回到 townhall。我的 worker 是单次调用,打一枪。于是加了个自检自改:生成 → 自检 → 没过就带着错误反馈再要一版,最多 N 轮。它和上面那个工具循环是同一个形状的近亲,只不过「工具」换成了「校验器 + 错误反馈」。

加完我没直接信它。设了个环境变量 TOWNHALL_BREAK_FIRST,故意把第一版 HTML 砍掉一半,跑一次,看日志从「自改 0 轮」变成「自改 1 轮,自检通过」——这才确认那个 loop 真的会触发。一个从没见它跑过的分支,我不敢信。这条后面救了我好几次。

然后发现:不报错 ≠ 功能对

转折在这。我的「自检」一开始就是把 HTML 当字符串读:有没有 DOCTYPE、标签配没配平。结果正常需求跑下来,三个 worker 全是「自改 0 轮,自检通过」——那个 loop 一次都没触发过。

因为这种检查太浅,只看得出「结构残废」,看不出功能错:倒计时器加载得好好的但点开始根本不减,井字棋三个连成线了却不报胜负。这些页面活着、不报错,但是错的。

所以加了真沙箱:ferrum 开 headless Chrome 把 HTML 真跑起来,抓 JS 运行时异常、抓空白渲染。设计上让它跟静态检查共用一个返回——check(html) → [错误列表],空数组算通过。这样它能零改动插进已有的 loop,因为 loop 只认那个数组,不在乎错误是读字符串还是真跑查出来的。

但沙箱也只回答「崩没崩」,回答不了「做到需求没」。后者得让一个 agent 来判——读「需求 + 产物」,挑功能没满足的地方。我叫它 Critic。

写到这才反应过来,我大半精力已经从「怎么生成」挪到了「怎么知道生成得对不对」。生成不难,验证才难。这句话后面成了整个项目的主轴。

全文最较真的一段:你凭什么信你的验证器

Critic 是个 LLM,非确定——同一份产物,这次说有问题、下次说没问题。那它到底准不准?跑一次看一眼根本判断不了。

所以我给它建了个 eval。手写两个井字棋:一个完整正确,一个我故意只删掉「判平局」那一句(能玩、能判胜负,就是下满了不提示平局)。我知道标准答案,就能对答案。每个喂给 Critic 跑 6 次。

第一组结果挺难看:完整(正确)那个被挑了 2 次刺,两条都是编的——说「O 能在 X 回合下棋」(假)、「平局后没禁止点击」(也假);缺平局那个,6 次里一次都没说中「没判平局」,报的全是别的、且都不对。

我故意埋的 bug,Critic 0/6 抓到;不存在的 bug 反而瞎报。更坑的是,如果我只数「报了几个问题」,会看到「3/6 报了」,误以为召回还行——只有去读它报的内容,才发现一条都没报对。数 LLM 输出的条数会骗你。

真跑出来长这样(·=说 OK,x=报了问题但没报到点子上,O=真抓到目标缺口):

$ ruby bin/eval-critic            # 默认 deepseek-chat
【完整版(有胜负+平局)】 ·x·x··   报问题 2/6   —— 这两条全是编的:
    "轮到 X 时 O 也能下棋"(假)、"平局后没禁止继续点击"(假)
【缺平局版(下满不提示平局)】 x·x··x   报问题 3/6,却没一条说中平局
    报的是"没切换轮到谁""初始未渲染""格子显示空格" —— 全不对
误报 2/6 · 真召回 0/6

换上会思考的模型,同一份输入:

$ JUDGE_MODEL=deepseek-reasoner ruby bin/eval-critic
【完整版】 ······   报问题 0/6        —— 零幻觉
【缺平局版】 ····OO   其中真正抓到目标缺口 2/6
    "没有平局检测,填满棋盘后无结果提示,游戏无法结束"
误报 0/6 · 真召回 2/6

然后做了两件便宜事,对着数字调:一是换会思考的模型(reasoner),误报从 2/6 掉到 0/6,而且它一旦开口报的就是真问题;二是改 prompt 让它逐条对照需求查边界,召回小幅上去,但很快到顶——靠读源码去脑补「棋盘下满会怎样」,再强的模型也不稳。

到这我能用数据下判断了:精度问题换模型免费解决;召回卡在「读源码」这个地基上,要再上得换地基(真跑+看行为),那是另一笔投入,但至少现在我知道为什么、也知道便宜的路已经走到头——不是拍脑袋。

附带一个小决定:既然 Critic 的绝对分不可比,我让「采纳哪版修改」改成两两比较——LLM 不会打绝对分,但很会说 A 和 B 哪个好;还问两遍、把顺序对调,抵消它的位置偏好。

把 eval 学到的记下来,下次别再犯

那条「井字棋容易漏判平局」,我没让它白翻,存进了一个跨运行的记忆。下次再做井字棋,orchestrator 先把相关经验捞出来塞给 worker:

$ bin/townhall "做一个井字棋,两人轮流下,分出胜负有提示"
[townhall] 想起 1 条相关经验,带给工人:
[townhall]   ↺ 井字棋这类棋局最容易漏判平局:棋盘下满且无人连成线时,要提示「平局」并停止继续落子。

一条用 eval 挖出来的教训,变成了会自动避开的记忆。但紧接着冒出个问题:要是让 agent 自己往记忆里写呢?一条错的就成了「带公章的错误」,会被自动塞进以后每个相关任务、污染一大片。所以写入得过一道闸——一个独立评审小组投票,多数通过才算数:

$ bin/learn "井字棋" "井字棋棋盘是 4x4 共 16 格"
  评审1:REJECT — 标准井字棋为 3x3 共 9 格,4x4 是常见错误,不真实
  评审2:REJECT — 井字棋标准棋盘为 3×3,4×4 描述错误
  评审3:REJECT — 4×4 是变体,以偏概全
❌ 只 0/3 通过 → 拦下,不写入。

错的、过拟合的经验进不来。这其实是在防一种叫 heresy 的错误信念传染——后面会专门说。

Don't trust, verify

后来想把到处手撕字符串的解析(judge 抠数字、critic 认 "OK" 那些)换成结构化输出,让模型直接吐合法 JSON。

库里有 with_schema,能力表也写着新模型支持严格 schema。我差点照着写。习惯使然先打了个探针,一次最小调用,回来一行:

RubyLLM::BadRequestError: This response_format type is unavailable now

DeepSeek 拒了。我以为是旧模型,换上最新的 v4-pro 再试——还是拒。可库的能力表明明声称 v4 支持 structured_output

能力表撒谎了。只有真打到接口,才知道它到底吃不吃。那个探针省下的不是几行代码,是一次「照文档写、上线才炸」。最后落到 DeepSeek 真支持的那档(JSON 模式 + 自己校验字段)。

多 agent 真正难的,不是并发

研究「多个 agent 改同一份东西」之前,我以为难点是并发——竞态、锁、丢更新。看下来发现那部分基本是分布式系统重演,几十年前就有答案(分区、加锁、乐观并发、actor)。有人问过我这是不是就是传统软件工程,基本是,除了一块。

那一块 Yegge 叫 heresy(异端):一个错误想法在 agent 之间扎根、传染、反复复发。你把错的代码清了,但某个文档、某句注释里残留一个引用,下一个 agent 读到觉得「哦这说得通」,又照着把它重建出来。锁治不了这个——不是数据在竞争,是信念在竞争。两个 agent 可以完美轮流写、零竞态,却同时信着同一个错的东西。

因为 agent 本质是对周围一切信号做推断的东西,没有一个权威「真相源」时它只能猜,猜错了还把错的写成新信号喂给下一个。治它得靠一份权威的事实来源 + 明确点名「这是常犯的错,别犯」 + 用工具硬挡。这是认识论问题,不是并发问题。

编排核其实不绑任务

中途停下来问自己:这玩意儿只会生成网页,换个任务是不是就废了?拆开看,真正绑 HTML 的只有两块——怎么生成(prompt)、怎么验(检查器)。剩下的 fan-out、loop、收敛、记忆全是任务无关的。把那两块抽成一个可替换的「领域包」,用同一套编排核跑通了一个完全不同的任务:生成正则表达式,拿测试用例验。编排核一行没改。

当时卡住的一个问题:那要做个「通用的」,是不是就一直加领域、加到天荒地老?

不是,那是用体力填填不满的坑。通用靠杠杆(几个强工具,比如「能写代码并跑测试」就覆盖一大片),不是靠枚举无穷多专用领域。而且这里有个不对称我没料到:生成几乎免费就通用(一个模型什么都能试着生成),验证不通用——一个「对任意任务都可靠」的验证器约等于通用智能本身,做不到。所以你永远得为在意的领域亲手配验证器。

还有个对照挺有意思:正则的验证器是硬的(跑测试用例,对错可数),所以直接挑「错误最少」就行,根本用不上 Critic/Judge 那套。HTML 质量是软的、数不出来,才需要软收敛。验证器硬不硬,决定了你要不要那套又重又贵的机制。

回头看,其实就两句话

写完通读一遍,发现所有东西在反复说同一件事:把「对 / 安全 / 该停」从靠模型自觉,变成被系统强制。

它换着位置出现。自检 loop 靠 MAX_REPAIRS 兜底,不然它不知道何时停。递归分解那块我实测过一组数:

$ bin/try-recursion "做一个在线测验平台:能出题、答题、判分、排行榜" 3 12
叶子 20 个,其中:
  模型自己说够小(atomic):5
  被【深度上限】逼停    :15
  被【预算】逼停        :0

20 个叶子,只有 5 个是模型自己收的手,剩下 15 个是被深度上限逼停的——75% 的「停」不是模型自愿,是外面的闸按下去的。Critic 和路由器靠 eval 量化;记忆靠一个写入闸(就是上面那个);安全靠输出侧的确定性校验。模型的自我判断信不过,这不是 bug,是现实。

中间还被问住过一次,值得记:我一度把「一个多轮对话、上下文一直滚下去」的东西命名成了「记忆」。一句「这不就是对话历史累计吗」把我噎住了——对,那只是上下文长,chat 一死就没;真记忆得能持久、能跨对话取回,是两码事。当场认错改了名。上面那些数字,大多是这种「被问住→去量→更新」逼出来的。

第二句其实是第一句的方法,一句老话:Don't trust, verify。能力表说支持 schema,真打 API 才知道被拒;Critic 看着在干活,建了 eval 才发现它幻觉又漏判;路由器规则版看着够用,量一下才知道换个说法就漏:

$ ruby bin/try-route
请求:帮我整一个能在浏览器里玩的猜数字
  规则:question ❌      LLM:html_app ✅
请求:给我一个能识别手机号的模式
  规则:question ❌      LLM:regex ✅
准确率 —— 规则路由:3/5 · LLM 路由:5/5

一次结果说明不了什么,要量分布、要 A/B、要打到真接口。

最后

townhall 是个玩具,代码扔了不可惜,带得走的是那套判断:发现组件不靠谱 → 建 eval 量化 → 便宜的杠杆先试到顶 → 用数据决定要不要上大改。

说句私货:我做这个,其实是给一件真事磨刀——一个帮理科老师审题的 AI 助手,判一道题物理自不自洽、有没有命中目标能力、哪里要改。审题助手本质就是个验证器。这趟玩具走下来最大的收获恰好是「验证才是一切,而验证器的价值全在它可不可信」——正好是那件真事的命门。

如果你也在琢磨验证 / 教育 / agent 编排,欢迎来聊。