ARTICLE · 1279955
mu源码阅读:agent 的流程决定,交给一个小模型当裁判

先说 mu 是什么。它是一个开源的 coding agent,命令行加桌面应用,建立在另一个开源 coding agent pi 之上——mu 自己几乎不动 pi 的代码,而是把全部新增能力装进一个叫「判断内核」(judgment kernel)的扩展里(packages/kyrn-judge,两百多个 TypeScript 文件)。能在终端里读写代码、跑命令、开子代理、驱动浏览器,这些是 pi 原本就会的;mu 加上的,是一层别家没有的东西:一个专门做判断的小模型。
动机是 README 开头的一段话。一个 coding agent 在一轮对话里要做几百个决定,其中大部分和编码无关:这段工具输出要不要进上下文、这条命令放不放行、这个发现值不值得告诉别的 agent、模型说做完了到底做完没有。交给大模型,费的是 token、延迟和注意力;交给写死的规则,错得又太频繁。mu 的做法是把它们交给一个 judge——一个小而快的判断模型,不写代码,只在几十个预设的「决策点」上作答,每道题都是一句话状态加一个类型化问题,答案是一个概率:
A coding agent makes hundreds of decisions per session that are not about the code... mu gives them to a judge: a small, fast model that answers one bounded question at a time, at 38 decision points in every turn. The big model keeps its attention for the work.
项目文档里还有一句更凝练的:「判断模型判断,代码执行,主模型思考」。
三种题型,一道门槛,一个逃生口
judge 被问的题型只有三种:boolean(是/否,带概率)、choice(封闭选项)、score(序数量表),没有开放式问答。答案怎么变成动作,policy.ts 里只有几行:
export const DEFAULT_THRESHOLDS: Thresholds = { no: 0.2, yes: 0.8 };
export function threeZone(answer: BooleanAnswer, thresholds: Thresholds = DEFAULT_THRESHOLDS): Verdict {
if (answer.probability >= thresholds.yes) return "yes";
if (answer.probability <= thresholds.no) return "no";
return "unsure";
}
概率不低于 0.8 才算「是」,不高于 0.2 才算「否」,中间全是「不确定」。中间区为什么这么宽,注释写得直白:
The middle zone exists so a wrong-but-confident reading is the only way the judge can cause harm: anything the model is unsure about falls through to the caller's default.
judge 唯一能造成伤害的方式,是「错而自信」。choice 题还必须带一个逃生口(none / other / unclear),定义时没写会直接抛错——否则它会被迫在离题的输入上满置信地选一个真选项。每个决策点还要声明 fallback:拿不到裁决时的默认行为,「等于没有 judge 时的宿主行为」。
fail-open,但安全闸门例外
judge 是外部服务,会超时、欠费、答非所问。引擎的注释一句话定调:
Runs decisions fail-open:
decidenever rejects because of the judge.
超时、宕机、无效答案、弃权,全部回落 fallback;连记台账失败都不影响 agent 行为。唯一例外是安全类决策点,方向反过来:
The judge can add a gate, never open one: no verdict means the user is asked.
judge 只能加闸,不能开闸。tool.risk 的 fallback 是 confirm 而不是 allow——被规则标记为「递归删除」「强制推送」的命令,judge 挂了不会悄悄放行,而是弹到你面前。分工也清楚:guard.ts 里一组正则负责挑出值得多看一眼的命令(从 rm -rf 到 PowerShell 的等价写法),judge 只回答一件事——这是不是用户自己要求的。
judge 还能级联:便宜的在前,贵的在后面兜灰区,注释的说法是「贵的 judge 只为灰区付费」。每层声明实测出来的弱项,弱项题直接跳过;没有任何一层可信的题,答案强制中性,策略读到中性就走默认行为。
说做完了,和真做完了
这些决策点挂在整条链路上:输入侧有 input.preflight(这轮是什么类型的活,定出思考档位)、task.frame(消息是新任务、新约束还是闲聊,只有任务真的变了才重写任务框);工具侧有 tool.admission(长输出逐块准入)、tool.injection(网页逐段筛查提示词注入)。收尾侧的两个最有意思。
turn.completion 管模型说「完成了」但改完文件后什么都没跑的场景。它的一半根本不问 judge:改了几个文件、之后有没有跑过命令,是 harness 自己知道的事实;验收清单上还有没勾的项,直接触发追问,注释一行:an open acceptance item contradicts "done"。
turn.continue 管模型停在「接下来我去跑测试」却没做、或者用户已经让它做事它还在问「要不要改」。它会把任务发回去,但有刹车:每条消息最多推两次;「下一步难以撤销吗、要不要出这台机器(push、deploy、delete、pay)」单独占一问,只要不是确定的「否」就收手不催。
措辞决定一切
内核里最实证的部分是问题的措辞。同一模型、同一份状态,一题长问法 42% 正确率——不如瞎猜——改成短问法 74%:
长问法:Does `user_message` ask for any file to be created, edited, or deleted?
短问法:Does `user_message` request a code change?
枚举、量词、复合条件都会把小模型打到随机水平;短、具体、单一谓词的问法才有效。tool.admission 上有另一组对照:问「这一块与意图相不相关」,实测把噪音全放行;问「这一块是什么」,噪音和错误全部放对位置。于是题面定成 "What kind of output is c1?"——问它是什么,不问它要不要紧。
测试日志的实验把这种较真推到了极致:先在合成样本上测出漂亮数字,回放真实会话后发现分布完全不对,文档开头就承认「我最初的合成基准测的是一个在这位用户的真实用法里不出现的分布」。真实发现是:15 份测试日志 58% 的字符是「连续 6 行以上与前文逐字节相同」——同一份 diff 为每个失败测试各打印一遍。拿走这部分冗余的是一条不调模型的确定性规则:139,820 字符净省 51%,且无损,标记展开后能逐字节还原原文。剩下的才轮到 judge 做目标感知选择:调试失败时通过记录是噪音,问「哪些测试跑了」时是证据。judge 净省 40.2%,必要证据一条没丢;对照组「只留失败」省 61.9%,但丢了 9 条。全部 282 次真实调用花了约 0.017 美元,中位延迟 345 毫秒。
上下文是丢得起的
管上下文的决策点共享同一条原则:准入优于遗忘,追加优于改写。每个点声明自己的缓存影响,改前缀的只在缓存边界做。
tool.admission 是入口闸:超过 4000 字符的工具输出按 1200 字符切块,首尾两块永远保留(命令把摘要和退出码放在头尾),中间逐块分类,只有「高置信噪音」被丢出去——归档到临时文件,原位留一行指针,判错了可以读回来:
[mu: 214 lines (12,083 chars) of progress output omitted; full output: /tmp/kyrn-xxxx/xxx.txt]
context.forget 是后半程的闸:上下文占用越过 50%、70%、85% 时清理旧工具结果。judge 看到的只有调用名、大小、距今几轮,没有正文——判断一个 30K 的输出要不要留,成本几十个 token。缩成头尾各 600 字符加一行占位,只改出站请求,会话文件原文一字不动。
最反直觉的是 context.compact:一般压缩是让模型写总结替换旧对话,mu 的做法是不写总结——旧对话逐字保留,只裁过时的工具输出。注释里那个词很准:Compaction by judgment; no summary is written。
蜂与蜂之间传什么,裁判说了算
mu 也有多智能体机制 hive:一个拆不开的难题,放 2 到 6 只「蜂」从不同角度同时查,每只独立进程、独立上下文。它的重点不在怎么组织蜂群,而在蜂与蜂之间传什么。文档先把市面方案分了类:不传、只向主代理回报的,各自重复劳动、做出冲突假设;全传的,每个上下文被灌满;让每只蜂自己决定的,用最贵的主模型做分拣,费 token 还经常忘了发;靠静态规则的,不看内容。然后一句:
没有一家用又快又便宜的专职判断模型把关「什么信息值得跨上下文传播」。
源码注释把这个位置讲得更透:
Sharing everything floods every context; letting each bee's main model decide what to share is slow, costly, and forgotten. So the judge does it, once per step, like the judges of a waggle dance.
结构不复杂:共享白板是一个 append-only 的 JSONL 文件,蜂之间从不直接对话,消息原样上板。三个闸门全归 judge——hive.publish(这条对其他蜂是新消息吗、是什么类型,两问都过才上板,注释:Routine progress is not news, however relevant it sounds)、hive.deliver(板上别人的新消息对这只蜂有用吗)、hive.relate(新便签对旧便签是更新替代、冲突还是佐证)。
hive.relate 里藏着一个最能体现工程化程度的数字:跨蜂的 supersedes 门槛设到 0.9,理由写在常量旁边——用 11 次真实 hive 的 153 对人工标注校准过,跨蜂读成「更新替代」的在 0.6 门槛上没有一对是对的,而每次误判都会把别人的一条发现从板上摘掉。一个阈值背后是 153 对标注。冲突则两边都保留、标 in dispute,注释说 a judge does not get to vote on which of two workers is right;冲突挂 60 秒没人解决,才加派一只验证蜂,它的调查角度就是那两条消息。
蜂的默认角色 investigator,工具白名单是 read, grep, find, ls, locate, bash, browse——没有 write 和 edit。调查者不改文件,改动由主模型做,这是工具注册层面的硬约束,不是提示词里的请求。README 记录了一次真实运行:三只蜂、九分钟、117 个候选被判定、27 条上板、16 条投递到需要它的蜂;每一次闸门判定都写进运行目录的 gate.jsonl。
先陪跑,攒够数据再上岗
每个决策点三档模式:off(不问)、shadow(问了、记了账,行为不变)、active(按裁决行动)。新点上线默认陪跑:真问、真记账,但不据此改变行为,等台账里攒够「裁决」和「事后结果」的对照才允许上岗。architecture 文档一句话:
Shadow first. A point earns
activewith numbers from real sessions.
台账本身完整:每次判定一条记录,来源、原因、各题概率、延迟、token 用量,全部落进会话文件,mu ledger 随时回看。文档还写了它的远期用途:这份台账天然是蒸馏数据——让 Jev 当老师跑一段时间,微调一个专用本地小模型,之后每题约 13 毫秒、数据不出本机。
写在最后
这个项目把「agent 系统里的判断」当成独立于生成和执行之外的第三种计算,并为它建了全套基础设施:类型化的问题协议、三区间概率策略、fail-open 引擎、级联路由、shadow 机制、全量台账。单项拆开都不惊人,惊人的是纪律——规则抓事实(同样调用三次同样结果就是循环),judge 抓判断(跑偏了吗、值得共享吗),主模型只管生成;几乎每条阈值旁边都留着实测数字:42% 对 74%,0 对 6,72/72,51.0%。
以及一个贯穿所有决策点的姿态:错了要便宜,而且要可撤回。丢掉的输出有归档路径,缩过的结果在会话文件里原样保留,安全闸门上 judge 只能加闸不能开闸。一个会答错的小模型是可接受的,前提是它答错的代价被算过、被兜住、被记下来。
参考资料
mu 源码:https://github.com/qybaihe/mu(本文分析基于仓库内文档与 packages/kyrn-judge源码)pi:https://github.com/earendil-works/pi















































































