跳到主要内容

kun — 本课题摘录

读了哪几篇: 02-agent-loop(一个回合从出生到收尾)、03-cache-first-context(缓存优先的上下文工程)。 其余四篇(进程边界、工具与闸门、模型层、产品层)本轮没读。

这一家是本课题最诚实的一份材料:它明说"循环本身不难,难的是别的"。

它对本课题回答了什么

决定四:循环本身很简单,难的是四类打断和一串跑偏纠正

它的原话:

一个回合是一个以"模型这一轮有没有叫工具"为唯一续转理由的循环,默认没有步数上限(可配)、墙钟 24 小时封顶; 它的全部工程量不在"转",而在四类打断(插话 / 中止 / 审批 / 提问)和一串"跑偏纠正"(复读、空回复、压制恢复等)如何在不破坏历史一致性的前提下插进这个循环。

(依据:Agent 库 · Kun · Agent Loop:一个 turn 从出生到收尾 —— 一个 turn 以「模型这一轮有没有叫工具」为唯一续转理由,默认无步数上限、墙钟 24 小时封顶;全部工程量不在「转」,而在四类打断(插话/中止/审批/提问)与一串跑偏纠正怎么插进循环而不破坏历史一致性)

这句话应该原样进讲义。 我们前面读了三十多家,循环骨架都差不多;真正的差距全在这句话说的地方。

三个承重词它定义得很干净:

名词一句话
会话一次对话的容器
回合用户一次发言引发的一整段工作,可能包含几十次模型请求和工具调用
条目回合里的最小可持久化单位:用户消息、助手文本、思考、工具调用、工具结果、审批、提问、压缩、错误

它的直觉句:会话是一本书,回合是一章,条目是一行。循环只对条目追加,从不回头改写已完成的条目——这是后面所有"能中断、能重放、能续跑"的地基。

一条持久化顺序的硬规则:先落盘,再发布

所有事件先落盘再发布。这个顺序是刻意的: 界面先重放已落盘的日志再接实时流,反过来做会让某条事件掉进"读完积压、还没订阅上"的缝里,永久丢失。 (依据:Agent 库 · Kun · Agent Loop:一个 turn 从出生到收尾 —— 所有 event 先落盘再发布,顺序刻意——SSE 路由先重放已落盘日志再接实时流,反过来会让事件掉进「读完 backlog、还没订阅上」的缝里永久丢失)

这条是并发编程里的经典缝隙,而且很难在测试里复现。 值得直接抄这个顺序。

决定四:插话不打断循环,而是变成一条普通消息

用户在回合跑着的时候又敲了一句话:它塞进一个队列、发一条事件,不碰循环。

循环自己在两个安全点取货:回合开始前、每圈开头。取出来后把每条插话变成一个正常的用户消息追加进历史。

所以模型下一次请求就自然看见了它,不需要任何特殊通道。 (依据:Agent 库 · Kun · Agent Loop:一个 turn 从出生到收尾 —— 插话塞进 SteeringQueue 不碰循环,循环在 turn 开始前与每圈开头两个安全点 drain,把每条插话变成正常的 user_message item 追加进历史,模型下一次请求就自然看见)

队列按回合有界并防串扰:每回合最多 32 条 / 64KB;回合结算后封存,上一个回合没来得及消费的插话不会漏进新回合。

"变成一条普通消息"是最优雅的解法。 我们不需要发明"插话"这个概念在模型侧的表示——它就是一条用户消息。

决定四最有料的一段:模型没叫工具时该怎么办

朴素答案是"结束回合"。但真实的长任务里,模型经常在没干完的时候就停下来:

  • 说一句"好的,我这就去改"然后什么也没做;
  • 改完文件后回一个空响应;
  • 被输出长度截断。

它在这个分支上堆了五道回合内的纠正,加一道跨回合的续跑。判定顺序从上到下,命中即停:

条件动作
1有"软必调工具"未满足物化成计划 / 判为提问 / 判为失败
2本回合改过文件 + 文本为空 + 正常停止空回复恢复:最多补 2 次
3有活跃目标 + 正常停止复读检测
4普通工具失败过 + 只剩"进度播报"式文本失败后恢复:有界,烧完判失败
5停止原因是"长度"发一条"输出被截断"的警告条目,然后停
6其它正常完成

(依据:Agent 库 · Kun · Agent Loop:一个 turn 从出生到收尾 —— 模型没叫工具时的判定树六条从上到下命中即停——软必调工具未满足、改过文件但文本为空则空回复恢复最多补 2 次、有活跃 goal 则复读检测、工具失败过且只剩进度播报则失败后恢复、stopReason 为 length 则发 output_truncated 警告、其它正常停)

第 5 条它单独说明了理由:模型撞到输出上限被截断时,如果直接当成"干净完成",用户看到的是"它好像放弃了"。所以专门写一条警告说明"这是被截断了,去调大输出上限或让它分步"。

这条跟 fara 的"步数耗尽被标成完成"是同一个位置的正反例: fara 把异常结束混进了正常结束;kun 专门把它拆出来还给了用户一句可操作的建议。

复读检测:为什么不能用相等判断

目标续跑有个副作用:模型只要不叫工具就会被重新提问,于是可能陷入"我这就去做 X"的空转。

用相等判断抓不住它——同一句废话每次的标点、大小写、语序都略有不同。

它的做法是两级:

一级:归一化后完全一致(转小写 + 去掉所有空白/标点/符号,中英文标点一起清)
二级:两串都够长(≥12 字)时,算字符二元组的相似度,≥0.85 判为复读

(依据:Agent 库 · Kun · Agent Loop:一个 turn 从出生到收尾 —— 复读检测两级——归一化(转小写、用 Unicode 属性类去掉所有空白/标点/符号)后完全一致,或两串≥12 字时字符二元组 Dice 相似度≥0.85;抓到后前 3 次注入恢复指令再给一次机会,第 4 次写警告并停)

"太短的不判,避免误伤"这个细节很实在。

抓到复读之后不是立刻停:前 3 次注入一段恢复指令再给一次机会,第 4 次才写警告并停。

对比 cline 的"工具名+参数当签名"、openmanus 的"数消息重复"、cowagent 的"同参连败分档": kun 这一份是唯一处理"模型说的是同一个意思但字面不同"的。 前三家都用精确比对,抓不住换了标点的废话。

决定一:请求被硬切成两段

前段(系统提示 + 工具格式说明 + 示例)必须逐字节不变;后段(对话历史 + 工具结果)可以随便修、随便砍、随便折叠。

而且这不是外部推断——它的系统提示正文里就有一节专讲缓存行为,明说稳定指令与稳定工具格式要跨回合逐字节稳定,可变内容必须排在稳定前缀之后。 (依据:Agent 库 · Kun · 缓存优先:上下文工程与省钱三件套 —— 请求被硬切成「字节稳定的前缀」与「可随便修改的尾巴」,而且 Kun 的 system prompt 正文里就有一节 Cache behavior 明说稳定指令与稳定工具 schema 要跨回合逐字节稳定、可变内容必须排在稳定前缀之后)

前缀用指纹上锁,工具目录只许增不许改。

一条极精妙的取舍:实时用量刻意不注入

目标续跑的指令里只告诉总预算,实时的 token/耗时用量刻意不注入——因为它每个步骤都变,会破坏缓存前缀。

注释明说:这笔钱由宿主侧的预算闸门来管。 (依据:Agent 库 · Kun · Agent Loop:一个 turn 从出生到收尾 —— goal 续跑指令只告诉 tokenBudget 总额,实时的 token/耗时用量刻意不注入——因为它每步都变会破坏缓存前缀,注释明说这笔钱由宿主侧的预算闸门管)

这是"哪些东西不能进提示词"的一个绝佳例子: 不是因为它没用,而是因为它每轮都变。 凡是每轮都变的东西,要么别进提示词,要么放在最末尾。(nanobot 把时间放末尾是同一条。)

一条权限设计:有些状态模型碰不到

目标的状态更新工具只开放两个取值(完成 / 受阻),运行时还再拦一次——暂停、恢复、预算限制这些状态只能由用户或系统改,模型碰不到,工具描述里也把这句写死了。

还有一条很实的规则:同一个阻塞条件连续三个回合都在,才准报"受阻"——第一次遇到阻塞不许报。

这两条都是"别让模型自我降标"的设计。 它把一整段续跑指令写成了"如何防止 agent 自我降标"的检查单。

它没回答什么

  • 不用原生工具调用怎么办——它假设模型支持。
  • 工具怎么定义——在本轮没读的那一篇。

坑与代价

  • 默认没有步数上限,只有 24 小时墙钟。这是把"什么时候该停"整个交给了跑偏纠正那套机制。 那套机制失效就会一直转。
  • 五道纠正 + 一道续跑意味着"模型没叫工具"这个分支比主路径还复杂。 这是真实产品的代价,但也说明"模型不再要工具就结束"这条朴素判据在长任务里是不够的。
  • 复读检测的阈值(0.85、12 字、3 次)是经验值。 换语言、换场景要重调。

    判断(无锚): 我们的最小原型不需要这一整套,但应该从第一天就区分"正常停"和"异常停",并且给异常停配一句人能看懂的原因。 如果错,会错在: 如果第一版任务都很短(三五轮),模型根本没机会跑偏,那这套纠正是纯粹的过度设计。