跳到主要内容

kimi-code — 本课题摘录

读了哪几篇: 01-loop(无状态回合循环)、02-agent(主机层)。 其余四篇(工具套件、抹平两层差异、再工程化、交付面)本轮没读。

这一家把"循环该管什么、不该管什么"这条线画得最清楚,而且它把这条线写进了自己的说明文件第一句。

它对本课题回答了什么

决定一(架构):循环刻意不拥有世界

它的说明文件第一句就划线:"这是无状态的 agent 循环。它不拥有会话、传输、压缩执行、权限界面、持久化协议桥接。"

清单很具体:

上层拥有循环怎么"借"到它为什么不放进循环
会话状态、历史、持久化循环无状态,跑完即返回无状态才能被反复、并发、可测地调用
拼提示 / 投影历史由一个"拼消息"的回调注入循环不认识"消息该怎么拼"这件事
上下文压缩的执行只在"这一步之前"的钩子里由上层触发循环只提供"安全点",不决定压什么
权限界面 / 批准由一个"授权这次工具执行"的回调弹窗、等用户点是界面职责
传输、落盘由"发事件"的回调注入循环只产事件,不管字节去哪
系统提示、模型选择由一个模型对象携带单一来源,循环不自己拼提示

(依据:Agent 库 · Kimi Code CLI · 无状态回合循环:一次工具调用的一生 —— README 第一句划线「loop 是无状态的 agent 循环,不拥有 sessions、wire transport、compaction execution、permissions UI 或 durable protocol bridging」,这些全由 host 通过 buildMessages / authorizeToolExecution / dispatchEvent 等回调注入)

它的比喻:把循环想成一台只会转圈的马达——它只管"进一格、出一格"的机械节奏和刹车安全,至于油箱怎么加、方向盘怎么打、仪表盘怎么显示,全接在马达外面。马达自己不存油、不认路。

这是本课题"循环要薄"这条主张最彻底的一份实现。 whale 说"循环只管控制流"、webwright 说"循环体只有一行"、pi 把状态分到上一层—— kimi-code 把"不拥有什么"列成了一张带理由的表。这张表可以直接当我们的设计约束。

"循环只提供安全点,不决定压什么"这一条特别值得记: 压缩不是循环的事,但压缩需要循环给一个"现在可以安全地改历史"的时机。

决定四:三层嵌套,而且每层的边界都对应一个函数

白话边界在哪
回合用户说一句话后,agent 忙活到再次把话筒交还给用户的整段过程一次"跑回合"调用
回合里的一次模型调用(可能带一批工具调用)一次"跑一步"调用
批次一个步里模型一口气点名的那一组工具一次"跑工具批次"调用

(依据:Agent 库 · Kimi Code CLI · 无状态回合循环:一次工具调用的一生 —— 三层嵌套是 turn(用户说一句到再次交还话筒的整段)、step(回合里的一次模型调用)、batch(一个 step 里模型一口气点名的那组工具),各对应 runTurn / executeLoopStep / runToolCallBatch)

前面 codex 是"回合/采样"两级、deepseek-harness 是"轮/步"两级、pi 是"外层/内层"—— kimi-code 是三级,多出来的"批次"这一层正是"一次模型调用点了好几个工具"的那一层。 这一层不显式命名,并发和顺序的讨论就没有落点。

决定三:并发按"资源访问冲突"调度 —— 而且发和收都按厂商给的顺序

"一批工具里,互不干扰的并发跑,会互相踩的串行跑。"

判定"冲突"的是一个专门的资源访问模型:什么样的两把访问算冲突。 (依据:Agent 库 · Kimi Code CLI · 无状态回合循环:一次工具调用的一生 —— 一批工具里互不干扰的并发跑、会互相踩的串行跑,由 tool-access.ts 的资源访问模型判定什么样的两把访问算冲突,ToolScheduler 做有状态调度)

而且有一条不变量:按厂商给的顺序发出调用、按厂商给的顺序收回结果。

这条是并发的第五种答案,而且是最完整的一种:

  • openai-agents-js:按动作类型硬分;
  • haystack:按读写集合自动推;
  • nanobot / pydantic-ai:工具自己声明;
  • dexter:连续只读凑一批;
  • kimi-code:一个专门的资源访问模型 + "发和收都按原顺序"的不变量。

最后那条不变量是别家没明说的:并发执行不等于乱序回填。 codex 用有序队列达到同样效果。

决定四补充:停止原因决定要不要再转,而且还能被钩子否决

这一步的停止原因 = 「模型要用工具」? ── 是 → 再转一圈
└ 否 → 问一个钩子「还要不要继续」
要 → 转;不要 → 跳出

(依据:Agent 库 · Kimi Code CLI · 无状态回合循环:一次工具调用的一生 —— 停止原因是 tool_use 就继续转,否则问 shouldContinueAfterStop 钩子决定要续还是 break;回合级还有刹车点、max-step 闸门、用量聚合)

"终态之后还能被钩子续跑"这个口子很实用: 它让"目标没完成就继续"(kun 的 goal 续跑)这类需求 不必写进循环,而是挂在这个钩子上。

回合级还管:刹车点、步数上限闸门、用量聚合。

"中断时的用量记账"被单列成一个关注点——中断了也要如实上报花了多少。

一条工程细节:事件分发要做故障隔离

事件分发器把"要落盘的事件"写进逐字记录、把"实时事件"发布出去,而且做故障隔离。

跟 kun 的"先落盘再发布"是同一层的两件事:kun 管顺序,kimi-code 管一边挂了不影响另一边。

它没回答什么

  • 消息怎么拼——刻意不管,由上层回调。
  • 历史怎么压——刻意不管,只提供安全点。
  • 不用原生工具调用怎么办——不在这一篇。

坑与代价

  • "什么都不拥有"的代价是回调很多。 拼消息、授权、发事件、决定要不要续跑——上层要实现的接口不少。

    判断(无锚): 对我们的最小原型,这个方向是对的,但不必一次拆出六个回调。可以先只拆两个:"拼消息"和"发事件"。这两个是最先会变的。 如果错,会错在: 如果第一版就要接界面(需要事件)、要审批(需要授权)、要压缩(需要安全点),那六个回调迟早都要有,分批拆反而要改三次接口。

  • 资源访问模型需要每个工具声明自己碰什么。 声明不准,调度就不准——跟 haystack 的读写集合是同一个前提。
  • 三层嵌套意味着"停止原因"要在三层之间传递。 最内层的停止原因一路交回最外层决定是转还是收——这条链上任何一层吞掉了原因,上层就判断不了。