我开源了一个极简 Agent 框架,叫 Kiso。「V 站首发」

langlang280025 2026-09-21 08:57 1

本文系统分享了关于 agent 开发的思考,全文近 一万字,阅读需要 15 分钟,对于想要学习 agent 开发的同学也许大有裨益。


我开源了一个极简 Agent 框架,叫 Kiso 。


最初其实来源于一个很朴素的念头:我能不能做出一个比 Pi 更简单、更稳定、更省 token 的 Agent 框架。对于一个多少有点技术理想的 Agent 研究者来说,如果最后没能在开源 Agent 这件事上留下点自己的痕迹,实在是手痒难耐。


所以即便是重复造轮子,我也便造了。


但“简单、稳定、省 token”是一开始写在草稿纸上的三个愿望。而愿望和工程现实之间,隔着巨大的鸿沟。一旦真正去拆解像 Pi 这样的参考实现,就会发现它已经把“极简”逼到了一个很难继续压缩的位置:默认工具极少,系统提示词非常克制,Loop 也没有多少多余的控制逻辑。在这样的前提下,如果所谓创新只是再少两个工具、再少几百个 prompt token ,工程空间其实已经非常有限。


做着做着,我发现自己真正想解决的问题变了。


我开始意识到,一个 Agent 真正难以被托付的原因,往往并不是它不会调用某个工具,也不是缺少 Memory 、Plan 、Multi-Agent 之类的组件。模型能力会继续上涨,这些组件也会不断变化,但只要 Agent 开始通过文件系统、Shell 、Git 、浏览器或者网络接口真正改变外部世界,就会出现一个更底层的问题:


当一个概率模型开始改变现实以后,系统凭什么知道什么真的发生过?


这个问题后来几乎决定了 Kiso 的整个方向。


我最开始也试图给 Agent 下一个很“第一性原理”的定义,比如:Agent 是一个在不完全信息下,通过持续观察世界、采取行动、读取反馈,让现实状态逐渐向目标状态收敛的闭环系统。后来我觉得,这个定义作为思考工具有用,但没必要把文章变成一篇“什么才是真正 Agent”的理论争论。Kiso 也不需要证明一个适用于所有 Agent 的终极定义。它只需要关心一类非常具体的系统:一个会读取外部世界、会调用有副作用的工具、而且可能长时间运行甚至中途崩溃的 Agent 。


把范围缩到这里以后,有几件事情几乎无法绕开:


Observed ≠ Current

Intent ≠ Effect

Started ≠ Succeeded

Process Completion ≠ Goal Satisfaction

Memory ≠ Durable Fact

模型看见过,不代表现实现在还是那个样子;模型产生了一个工具意图,不代表外部世界已经发生变化;一个操作开始了,不代表它已经成功;整个 Loop 正常结束,也不代表真正的任务目标已经被满足;而进程此刻记得的东西,只要还没有成为 durable state ,崩溃之后就没有资格继续被当作事实。


所以做到后来,我越来越觉得 Kiso 虽然对外仍然可以叫一个 Agent 框架,但它真正想守住的是更底层的一层东西。严格一点说,它越来越像一个 Agent Runtime 。


我给它最后收敛出来的分工只有一句话:



模型负责扩大系统能够处理的问题空间,而 Runtime 负责限制什么才有资格被称为“事实”。



这就是 Kiso 后面绝大部分工程决定的起点。




01. 控制流闭环,不等于现实闭环


最常见的 Agent Loop 其实非常简单:模型接收上下文,生成工具调用; Runtime 执行工具,把结果重新放回上下文;模型继续推理,直到最后停止。从程序控制流来看,箭头已经漂亮地绕了一圈,又回到了模型,看上去闭环成立了。


但只要真正写过 Coding Agent ,并且让它跑过稍微长一点的任务,就会发现:控制流闭环,不代表现实闭环。


因为模型面对的并不是一个静态状态机。它面对的是一个持续变化的外部世界。文件可能被人类同时修改,Git 分支可能发生变化,Shell 命令可能执行到一半,HTTP 请求可能已经发出去但响应还没有回来,进程更可能在任何一个 CPU 指令之间被 kill -9


这里面其实存在几道完全不同的语义边界。


第一道是认识论边界。模型所有输入,本质上都只是现实世界某个历史切片的投影。即使今天给它 100 万 token 的上下文,它能够保存的仍然只是“过去看见过什么”,而不是一个正在持续变化的现实本身。几十秒前读过的文件,可能已经被 IDE 、Git 、后台进程或者另一个 Agent 改过了。


第二道是因果边界。模型流里出现一个 tool_use,只代表它提出了一个行动意图。从这个 token 被生成出来,到磁盘真正写入一个字节,中间还隔着完整的 Runtime 、权限判断、工具执行以及操作系统。把“模型说要做”直接等同于“现实已经做了”,在正常情况下可能看不出问题,一旦进入崩溃恢复,整个因果关系就会立刻变得混乱。


第三道是持久化边界。进程当前知道一个工具已经执行成功,并不意味着重新启动以后还能证明这件事。如果这份确定性只存在于一个 Promise 、一个对象字段或者调用栈里,断电之后,它就跟从来没有存在过一样。


最后还有评价边界。一个工具返回成功,只能说明局部系统调用完成了。write_file 成功不代表代码正确,npm test 返回 0 也不总能证明用户真正想解决的问题已经被解决。最终目标是否完成,需要来自新的观察、测试、验收或者人类判断,而不是让负责行动的模型自己顺手宣布“任务完成”。


这几道边界看起来很普通,但它们最后几乎决定了 Kiso 的整体结构。因为接受这些区别以后,就不能再把 Agent Runtime 理解成一个负责“LLM ↔ Tool”的 while loop 。它必须维护的是一条能够在崩溃以后仍然解释得清楚的因果链:


Model Intent

Turn Commit

Authorization

Durable STARTED

Real-world Effect

Durable Receipt

Observation / Verification

模型输出当然可以是概率的,但从什么时候开始允许现实发生变化,到现实发生变化以后系统究竟知道了什么,这些边界必须尽量确定。




02. Fail-Stop 中断与执行账本


这套思路遇到的第一个问题,就是进程崩溃。


假设 Agent 已经修改完第一个文件,接下来开始执行一条耗时的 Shell 命令。命令刚运行到一半,用户关掉终端,机器突然掉电,OOM Killer 杀掉进程,或者我们干脆在外面直接给它一个 SIGKILL。重新启动以后,它应该从哪里继续?


很多轻量 Agent 框架保存的是一份模型交互历史:User 、Assistant 、Tool Call 、Tool Result 。正常使用当然没有问题,但现实世界恰好存在一个非常麻烦的窗口:


Tool Call

真实副作用正在发生

Tool Result

如果进程刚好死在中间,磁盘上可能只有“我要执行这个操作”,没有最终结果。但外部世界并不是一个可以整体回滚的数据库事务。文件可能已经写完了,Git commit 可能已经产生了,远程 HTTP 请求也可能已经被服务端接受。这个时候如果 Runtime 简单地自动重试,可能制造第二次副作用;如果直接认为它已经成功,又可能建立在一个根本没有发生过的现实之上。


所以 Kiso 没有把“当前内存里的 Session State”作为最终真相,而是把整个 Session 建立在一条只追加的 Event Log 上。对未来判断有影响的状态,都要通过事件落到磁盘。


一个真正带副作用的工具执行,大致经历这样一条链:


模型完成一轮输出

Durable Turn Commit

权限检查

durable tool_execution_started

执行真实副作用

durable succeeded / failed receipt

把结果投影给模型

这里面有两个我认为非常重要的边界。


第一个是 Turn Commit。模型在流式生成的时候,可能先吐出半个工具调用,后面又产生非法内容、Provider 中途断流,甚至整轮 response 最终被判定为不完整。如果前半段的 Tool Call 一出现,Runtime 就立即开始改文件,那么后面即使整轮模型输出被判为无效,现实已经被改变了。


所以 Kiso 把“模型提出一个工具调用”和“这个工具调用获得执行资格”分开。只有整个 response cleanly exhausted ,并且得到了兼容的终止语义之后,这一轮才被 Durable Turn Commit 。Commit 以前的一切都只是一份 draft 。


第二个边界是 Persist Before Effect。真正发生副作用以前,tool_execution_started 必须先持久化;副作用结束以后,再持久化对应的 receipt 。


这样一来,进程不管死在哪里,恢复时至少可以非常清楚地知道自己掌握了哪些证据:没有 commit ,说明这一轮模型输出还没有获得行动资格;有 commit 但没有 STARTED ,说明这个工具还没有真正开始; STARTED 和 receipt 都存在,结果已经确定。真正无法从本地证明的只有一种情况:


durable STARTED

真实世界可能已经发生变化
X ← crash

durable Receipt

这就是最麻烦的 Unknown Effect 。


Kiso 不猜。


它会把这个 execution 标记成 uncertain,恢复流程停在这里,让人明确决定这一次操作应该 rerun 还是 abandon。只有这个悬空的因果关系被处理以后,原来的 trajectory 才继续往后推进。


我给它定的原则很简单:



Ambiguity Never Auto-Repeats 。



不确定的副作用永远不自动重试。


这里还有一个我非常喜欢的设计:Kiso 没有为了 Recovery 再维护第二份特殊状态机。磁盘上只有 Event Log ,Execution Ledger 、当前 Session State 、Recovery Plan 都只是它的 projection 。恢复逻辑本身也是一个纯函数:给它一串 durable events ,它推导出当前唯一安全的下一步——废弃未提交的 draft 、等待权限、执行已经提交但尚未开始的调用、处理 uncertain operation ,或者继续模型调用。


换句话说:



The Event Log is the truth. Everything else is a projection.



我专门为这件事情写了真实的 kill -9 测试。测试不是 mock 一个 crash flag ,而是真的启动一个 PTY 子进程,让 Agent 先完成文件修改,再开始一条慢 Shell 命令,然后对整个进程组发 SIGKILL。重新执行 kiso resume 以后,它必须能够证明前面的文件修改已经完成,被硬杀掉的那次 execution 处于 uncertain ;等人明确选择重新执行以后,再沿着原来的轨迹继续,而不是把整段 Session 当成一段聊天记录重新讲一个看起来合理的新故事。


做到这里以后,我才开始觉得 resume 这个词在 Agent 里有了真正的工程含义。




03. Context 不是事实:1M 上下文下的缓存与压缩


Event Log 确定以后,另外三样混在一起的东西也跟着分开了:对话历史、Context ,和 Agent State 。


以前我很容易把 Conversation History 、Context 和 Agent State 混成同一件事情。上下文快满了,就删一点工具输出;再满一点,就摘要;摘要之后继续把新的消息往后堆。但如果 Event Log 才是真实发生过的 durable history ,那么模型每次看到的 Context 其实只是一种临时 representation 。


Context 不是事实本身,而是 Runtime 从事实中为下一轮模型调用生成的一次投影。


这个区别非常重要。它意味着压缩 Context 并不等于篡改历史。Event Log 仍然保存实际发生过的东西,Runtime 可以根据当前模型、窗口大小、任务阶段和成本约束,把同一份历史投影成不同形式。


问题也就从“我要不要删历史”变成了:



如何在尽量保留推理连续性的前提下,用合理的成本,把 durable history 投影成下一轮模型最需要的 Context ?



我一开始参考了行业里很自然的一种两阶段做法:Context 到 50% 左右先做一次 microcompact ,把又旧又大的 Tool Result 剪掉一些;真正快到窗口顶部时,再进行 Summary Compaction 。这个设计从 token 数量上看非常合理:先轻度修剪,再深度总结,怎么看都应该更省钱。


但真正把 Prompt Cache 算进去以后,结论反过来了。


像 DeepSeek 这类 Prompt Cache 命中与未命中价格差异非常大的模型,只要整个会话历史保持 Append-Only ,前面已经出现过的几十万 token 后续请求几乎都可以继续作为缓存前缀命中。Context 虽然看起来越来越长,但其中很大一部分并不再按完整 Prefill 成本计费。


而如果在历史中间突然删掉几个旧的 Tool Result ,虽然表面上 Context 少了几万 token ,但从第一个删除点往后,整个 token prefix 都发生了变化。下一轮请求里,那些本来可以命中缓存的十几万甚至几十万 token ,就必须重新走 Miss 。


也就是说,你可能只是为了省掉 3 万个旧 token ,却让后面仍然保留的 20 万 token 全部重新 Prefill 。这个操作只有在未来还会继续运行足够多轮时,才可能把 cache break 的损失挣回来。如果任务再跑十轮就结束了,这种所谓“优化”反而会让账单更贵。


所以 Kiso 后来把固定比例触发的 microcompact 直接撤掉了。Primitive 还保留,但必须先经过一个 break-even 判断:只有预估未来调用次数足够覆盖破坏缓存带来的额外成本,才值得动手。不是 Context 一大,就习惯性“剪一刀”。


真正的 Summary Compaction 则是另一套逻辑。


目前在 1M context window 下,Kiso 的 Soft 阈值封顶在 400K ,Hard 阈值封顶在 700K ,并且保留最多 100K 的 Raw Tail 。这里有一点我后来特意在 ADR 里写清楚:400K 不是成本实验得到的最优值。 我们做过 threshold sweep ,如果只看账单,更早在 150K 、200K 或 300K 摘要,在一些工作负载下反而更便宜。400K 是一个有意识的质量取舍:减少摘要发生次数,少做几次有损信息压缩,接受一部分额外 Context 成本。


Soft 也不是“到了 400K 就立即摘要”。到了这里只代表进入 Eligible 。Runtime 会继续等一个自然的 phase boundary ,比如刚跑完一次测试、连续编辑第一次结束、长时间只读调研第一次开始进入修改。人类工程师也不会在脑容量刚好达到 50% 时立刻停下来写总结,我们通常是在“调查完了”“代码改完了”“测试刚跑过”这些有语义的边界上整理思路。


如果一直没有出现这样的边界,到了 Hard threshold 才强制处理。


压缩时还会强制保留最近最多 100K token 的 Raw Tail 。最近的报错、Tool Result 、刚刚发生的修改不经过二次摘要,直接原样保留。旧历史负责被压缩,最近推理链保持连续。


还有一个很关键的实现是 In-Band Summary。很多压缩实现会把当前几十万 token 的历史重新序列化成一大段文本,再用另一套 Prompt 发起一个完全新的总结请求。这样一来,Provider 看见的是一个新的 prefix ,之前已经积累的 Prompt Cache 基本全部作废。


Kiso 优先直接复用当前 session 已经存在的系统提示词和消息 prefix ,只在末尾追加总结指令。对于 Provider 来说,前面几十万 token 仍然是完全相同的前缀,因此大部分都能够继续命中缓存。


最后我对 Coding Agent 中 Context Engineering 的理解慢慢收敛成了四句话:


Event Log = Truth
Context = Projection
Summary = Compressed Projection
Prompt Cache = Projection 的经济属性

把这几层分开以后,很多以前纠缠在一起的问题一下子清楚了。Event Log 负责“发生过什么”,Context 负责“下一轮模型需要看什么”,Summary 是有损投影,而 Cache 根本不是 Agent State ,它只是当前投影方式在 Provider 侧的成本属性。


我觉得这个区别比“上下文到底应该 50% 还是 80% 压缩”重要得多。




04. 工具设计:Shell 、Budgeted Read 与过期观察


Runtime 再严谨,最后还是要通过工具去接触现实。工具设计如果只是把几个现成函数包装成 JSON Schema ,很多问题会重新从这里漏回来。


Kiso 现在默认的 Coding 工具保持得很小:read_filelist_dirsearch_textwrite_fileedit_fileshell。像 Subagents 、Skills 、Ask 、Task 、MCP 这些能力全部从 Extension 层往外长,而不是继续往基础工具表里堆。


这里面有几个看起来很小、但我自己反复推翻过的工程决定。


第一个是为什么工具叫 shell,不是 bash


这不是命名洁癖。Kiso 不希望模型继承用户完整的交互式 Shell 环境,而是通过系统 Shell 执行命令,工作目录绑定在项目 Workspace ,并且对子进程环境变量进行清洗。Kiso 自身使用的 API Key 、Token 不应该因为模型执行一次 envprintenv,就跟着整个环境一起被吐回 Context 。


更麻烦的是权限判断。


如果所有 Shell 命令都要求人确认,git statuslsgrep 每执行一次都弹一个窗口,Coding Agent 基本没法用;但反过来,如果只维护一张危险命令黑名单,同样不靠谱。Shell 的表达能力太强,一个看起来完全无害的命令,可以通过 $()、重定向、subshell 、环境变量甚至某些 Git 配置参数产生真实副作用。


所以 Kiso 没有试图判断“这条命令危险不危险”,而是采用反过来的标准:


只有能够证明只读,才自动放行。


read-only-shell 会对命令进行解析。存在反引号、$()、subshell 、后台执行等无法安全证明的结构时直接 abstain ;由管道或分号组成的多个命令,每一段都要分别满足只读条件;访问 .env、凭据路径时仍然进入权限链;甚至类似 git -c 这种表面还是 Git 命令、但可以借配置触发外部程序的形式,也不会被当作普通只读操作。


解析器不知道,就是不知道。


Unknown 不等于 Safe 。


第二个是 read_file 为什么默认只有 200 行或者 16,000 个字符。


文件读取这件事情看起来简单,但两种极端都不好。一种是直接把整个文件往 Context 里倒,一个两三千行的文件瞬间占掉大量窗口;另一种是把窗口切得太碎,模型为了理解一段完整逻辑不断往返读取,又浪费了工具调用和推理轮次。


我最后选择的是一个非常普通的预算式读取:正常代码最多读取 200 行,同时设置 16,000 字符的体积限制。为什么不是按 token ?因为 tools-node 不应该为了读文件而绑定某个 Provider 的 tokenizer 。Claude 、OpenAI 、DeepSeek 、Qwen 的 tokenizer 都不同,基础文件工具没必要感知模型层。


行数用于保持代码结构,字符预算防止大型单行 JSON 、压缩 bundle 、source map 之类的内容一行就把 Context 炸穿。正常情况下截断发生在完整代码行边界,同时把下一段从哪里继续读告诉模型。


我一开始其实很怀疑这个数字。


早期有一次 Benchmark ,Kiso 在长任务上的成本比 Pi 高了不少。我第一反应就是:肯定是 200 行太保守,逼得模型反复读文件。


这个解释听起来实在太合理了。


于是我把当时的真实 Trace 翻出来准备验证,结果发现那组任务涉及的文件里,最长的一份只有 132 行。


200 行限制一次都没触发。


后面继续追才发现,真正多出来的成本来自 edit tool 的失败重试,和 read_file 几乎没有关系。这个小插曲后来对我影响挺大:工程上一个解释再漂亮,只要 Trace 没有支持它,就只是一种故事。


工具层还有另一个更重要的问题,就是前面说的:



Observed ≠ Current 。



Agent 读取一个文件以后,可能过几十秒甚至几分钟才真正修改它。在这段时间里,人类可能在 IDE 里改了文件,Shell 命令可能重写了内容,另一个进程也可能修改同一个路径。如果 edit_file 最后只是机械地执行模型给出的 diff ,那么模型就会拿一个过期观察覆盖一个更新的现实。


所以 Kiso 的 read_file 不只是返回内容,同时会返回这个文件当前内容对应的 revision 。后面的 write_fileedit_file 如果要修改一个已经存在的文件,必须带着当时看到的 revision 回来。


大致就是:


read_file

content + revision

模型搜索、思考、调用其他工具

edit_file(expectedRevision)

执行前重新读取当前文件

revision 一致 → 允许修改
revision 已变化 → 拒绝,要求重新观察

这里的 revision 并不证明“模型真的认真理解了这个文件”,它只证明一个非常有限的事实:模型做出这次修改所依据的文件状态,在真正写入的这一刻是否仍然成立。


Execution Ledger 解决的是“我到底有没有做过”,Revision Guard 解决的是“我刚才看到的现在还算不算数”。一个防止 Runtime 对过去产生虚假的确定性,一个防止模型拿过去的观察冒充现在的现实。


在我看来,它们本质上属于同一个问题。




05. 权限边界:能力可以扩展,裁判权不能混在一起


随着模型能力越来越强,一个很自然的倾向就是尽量少打断它。用户当然不希望一个 Coding Agent 每改一行文件都来问“可以吗”,但如果走到另一个极端,把整个 Workspace 和 Shell 彻底交给模型,出现问题时用户又很难知道自己到底授权了什么。


Kiso 当前 CLI 对外提供五种主要运行模式:defaultaccept-editsplandontAskbypass


default 下,可以证明只读的操作直接执行,真正产生副作用的动作进入审批;accept-edits 允许常规文件修改,但 Shell 等更开放的副作用仍然需要权限判断;plan 是纯只读模式,写入和其他副作用直接拒绝;dontAsk 面向无人值守场景,它不是“什么都允许”,恰恰相反——所有原本需要人确认的操作都会直接 deny ,模型只能在现有权限里找另一条路,不能把任务永远挂在一个无人回答的确认框上;bypass 则尽量放行正常操作。


但即便 bypass 也不是“把安全系统关闭”。像 Kiso 自身凭据、明显灾难性的路径操作仍然有更底层的保护。放权是运行策略,灾难底线不应该成为一个可以顺手关掉的 UI 选项。


我更感兴趣的其实不是这五种模式本身,而是 Kiso 后来形成的一个设计倾向:


能力可以不断往外扩展,但执行者和裁判者尽量不要是同一个东西。


比如 Ask Extension 。很多 Agent 在遇到不确定需求时,会在两种坏选择之间来回摆动:要么模型自己猜一个答案继续干,要么突然在自然语言里停下来问一句,让上层产品自己想办法处理。Kiso 更希望“向人提问”成为一个明确的协议动作。当前台存在交互 bridge 时,模型拥有 ask_user;没有人能回答的 headless/piped 场景,这个能力干脆不进入它的动作空间。


不知道,就是一个合法状态。


再比如 Subagent 。Kiso 可以通过 Delegate Extension 把相对独立的任务交给子 Agent ,而且子会话同样是 durable 、可以恢复。但子 Agent 并不是一个拥有完整权限的复制品。父任务可以限制允许修改的路径;在这种受限子任务里,一些高风险能力甚至不会提供给它。更重要的是,子 Agent 做完以后,不应该因为它自己说“完成了”就算完成。父 Session 可以持有独立的 acceptance check ,或者回来以后重新做更高层的验证。


这一点和前面的 Execution Ledger 其实还是同一个思想。


模型可以提出行动,但不能顺手把行动结果写成历史;模型可以执行任务,但不应该因为自己说“完成了”,目标状态就自动成立。


我并不是认为模型会故意撒谎。问题恰恰在于它是一个概率系统,它非常擅长根据当前上下文生成一个“看起来已经完成”的连续叙事。如果提出假设、执行动作、记录事实、评价结果全部由同一个概率系统掌握,那么最后得到的很容易只是一个内部非常自洽的故事。


Runtime 要做的,不是限制模型变强,而是在模型越来越强以后,仍然保留几条独立于它的事实边界。




06. 2,200 行内核门禁与 Benchmark


到这里还有一个很现实的问题:如果这些语义真的重要,怎样避免它们随着项目不断增加 Provider 、Tool 、Extension 、TUI 、MCP 、Subagent 以后,慢慢被埋在一个越来越大的中央 Loop 里?


这几乎是所有框架最自然的退化路径。


因为中央循环永远最方便。它手里拿着最多状态,新功能遇到问题时,在这里加一个 if 最快;两个本来没有关系的模块想通信,把它们都接进核心 Loop 也最快。每一次改动单独看都很合理,几年以后就会变成一个谁也不敢碰的 Blob 。


所以 Kiso 给 packages/core/src 设了一条非常笨的硬门禁:



有效代码不能超过 2,200 行。



目前是 2,192 行。


2,200 并不是什么理论推导出来的神圣数字。换一门语言、换一种 LOC 统计方式,同一套逻辑完全可能得到另一个数字。它只是一个 Tripwire 。


当核心只剩 8 行余地的时候,每次有人想往 Kernel 里塞新逻辑,就会被迫回答几个问题:这个判断为什么属于内核,而不是 Runtime 、Tool 、Extension 或产品层?它如果做错,会不会真正改变底层执行语义?如果非加不可,现有 Kernel 里有没有东西应该先被移出去?


这个限制真正保护的不是“代码少”本身,而是决策所有权


Kiso 现在大致把系统拆成几层:Product Shell 处理 CLI/TUI ,Composition 负责把 Provider 、Tool 、Extension 、Store 组装起来,Agent 负责定义和创建 Session ,Session 拥有 durable conversation 和 recovery entry ,Run 拥有一次执行的生命周期,Kernel Loop 只处理模型与工具之间最核心的驱动语义;旁边还有 Store 负责 single-writer 、CAS 、torn-tail 之类真正的持久化判决。


我也不希望把“2,200 行 Kernel”变成一种廉价营销。核心只有两千多行,不等于整个系统的可靠性只依赖这两千多行。


真正的 Trusted Computing Base 明显更大。崩溃恢复依赖 Store 、进程间锁、操作系统和文件系统; Revision Guard 在 tools-node ; Provider 自己也可能违反协议;磁盘都不能保证写进去的东西真正永久存在。拿一个小内核去暗示“系统全部可靠性都被两千行代码罩住了”,这在工程上不成立。


这个门禁的作用只是一件事:Kernel 只保留那些一旦做错,就会改变 Kiso 对事件顺序、行动资格和因果关系承诺的决定。其余东西尽量往外生长。


当然,这些架构决定如果最后只存在 ADR 里,也没有多少意义。


所以发布前我另外搭了一套 Kiso 和参考实现 Pi 的对照 Benchmark 。双方使用相同模型、相同参数,每一对任务交替交换执行顺序,尽量抵消服务端状态和时间带来的系统性影响;每次任务开始之前都恢复到干净 Git 仓库。固定任务包括跨文件修改和长 Session ,另外还有一组通过随机种子生成的隐藏题,运行以前我自己也不知道具体内容。


整组测试一共留下了 48 对、96 条任务腿。


结果倒是挺符合我想要的 Benchmark 应该有的样子:不是 Kiso 全赢。


固定的跨文件修改任务双方 12:12 全部完成,长 Session 也都完成,隐藏随机题双方都是 22/24 。在隐藏随机任务里,Kiso 的成本低了大约 32%;但在那组长会话里,Kiso 反而贵了 19%。


那 19% 一开始让我很自然地怀疑 read_file


前面说过,我原本觉得 200 行读取可能太保守,模型需要不断分段,产生额外推理轮次。等真实 Trace 拉出来以后,发现最长文件只有 132 行,这个解释当场就没了。


真正的差异出在编辑工具。


那组长任务里,Kiso 一共进行了 360 次代码编辑,其中 26 次失败;对照组 287 次编辑,只有 3 次失败。继续往下看,失败模式高度集中:模型想在文件末尾追加一段代码,但 Kiso 当前的 edit_file 要求提供严格的 search 锚点和 replace 内容。长上下文下,模型经常把原内容和追加后的目标内容混到一起,工具找不到匹配,只能拒绝;模型随后重新推理、重新组织参数,再执行一次。


工具拒绝本身没有错。如果锚点不匹配却强行修改,安全性反而更糟。


但“防御机制正确触发了”,不代表“工具接口设计得很好”。


如果模型持续在同一种调用模式上失败,说明接口本身可能缺少更适合它表达意图的原子操作,比如 append 。那多出来的 19%,最后给出的不是“模型太笨”,而是一个非常具体的工具设计问题。


我没有在 Benchmark 跑完以后马上把接口改掉,再挑一组更漂亮的数据放出来。因为一旦工具定义变了,那一整轮已经花掉真实调用成本跑出来的对照数据就失去了原本的意义。


很多架构推理在白板上都可以讲得非常漂亮,但最终还是要让实际运行轨迹来证明。Kiso 做对的地方,希望 Benchmark 能证明;做错的地方,也最好能够留下一个具体数字,让下一步优化知道应该打哪里。




写在最后


最后说一下为什么把第一个落地场景选在 Coding Agent 。


我之前把自己对 AGI 的工程理解写成过一个很简单的式子:



AGI = Weights (模型权重) + Harness (控制外壳) + Experience Stream (经验流) + Update Mechanism (更新机制)



Coding Agent 很特别的一点,是它天然处在一个可以被快速验证的世界里。Agent 每修改一行代码、执行一次命令,外部世界都会很快给它反馈:测试是红还是绿、编译器有没有报错、diff 到底发生了什么、最终程序到底能不能运行。


这些真实执行过程中留下来的轨迹,未来可能成为评估数据,成为经验沉淀,更往后也可能变成某种更新机制的输入。


当然,现在的 Kiso 离这些还远得很。


眼下它首先得把最开始的三个愿望做好:更简单、更稳定、更省 token 。


前面的 Benchmark 也已经说明,这三个目标我并没有全部做到。可靠执行这件事已经有了一套我自己比较认可的骨架,但成本上有些任务更低,有些长任务仍然更贵;工具接口还有具体缺陷,Context 策略也远谈不上最终答案。


这也是我现在愿意把它 launch 出来的原因。很多设计如果永远只在自己的仓库里循环,很容易越来越自洽。真正让别人拿去跑、拿去杀进程、拿去和别的 Agent 对比以后,哪些东西是必要设计,哪些只是我自己的偏见,反而会更快暴露出来。


代码、目前的 42 篇架构决策记录( ADR )以及 Benchmark 相关代码都在 GitHub:


github.com/vincemakes/kiso


如果只是想先看看它:


npm install -g @vincemakes/kiso-code
kiso

第一次启动不需要配置 API Key ,会先运行一段内置的模拟轨迹。


如果想验证前面关于崩溃恢复的机制,也可以找一个真实项目,让它开始执行任务,在一条有副作用的慢命令中间直接 kill -9,然后重新运行:


kiso resume

看看它重新醒来以后,到底能不能把刚才真正发生过的事情说清楚。


哪个决定你觉得我做错了,欢迎随时来开 Issue 指出来。做这个项目的近两个月里,我已经亲手推翻过自己好几次,绝不差你这一次。

最新回复 (49)
  • langlang280025 楼主 09-21 09:12
    1
    kiso 开发近两个月,几乎融入了我最近一年半所有对 Agent 的思考,他可能不是最强的 coding agent ,也可能有很多 Bug ,界面当前甚至只有 TUI ,但是他一定算是一个好的开始。
  • murongxdb 09-21 09:13
    2
    介绍太长了,看起来太累,你直接说明它有什么特点更好点
  • langlang280025 楼主 09-21 09:30
    3
    @murongxdb 对于 agent 研究或者学习的一个完整的思路可能有些帮助,但是你说得也没错

    简单说 Kiso 就几个特点:

    极简:核心只有 2,200 行左右,能力通过 Extension 往外扩
    可恢复:Agent 被 kill -9 、断电或中途崩溃后,可以从原来的执行轨迹继续,不确定的副作用不会自动重跑
    更严格的执行语义:模型意图、真实执行、执行结果是分开的,Event Log 是唯一事实源
    更关注长任务成本:针对 Prompt Cache 和 1M 上下文重新设计了压缩策略
    自带一个可以直接用的 Coding Agent:kiso-code
  • skuuhui 09-21 09:30
    4
    直接给我最直白、最清晰、最实用、最不绕弯的介绍
  • ihciah 09-21 09:32
    5
    请你用最直白,最直接,最不绕弯子、最真相、最不绕弯、最扎心、最硬核、最干脆、最不墨迹、最戳痛点、最不留情面、最一针见血、最开门见山、最单刀直入、最不铺垫、最不客套、最不煽情、最不废话、最不拐弯、最不磨叽、最不装、最不端着、最不啰嗦、最不拖沓、最不委婉、最不掩饰、最不藏着掖着、最直白、最露骨、最实在、最通透、最毒辣、最爽快、最解气、最上头、最够劲、最过瘾、最粗暴、最有效、最狠、最准、最稳、最顶、最炸、最刚、最烈、最飒、最莽、最冲、最猛、最脆、最亮、最透、最干、最净、最硬核、最透彻、最赤裸、最凛冽、最尖锐的方式总结一个三句话的版本
  • langlang280025 楼主 09-21 09:35
    6
    @skuuhui 豆包:可能即将成为最强的 coding agent! (豆包说的,不是我说的:)
  • wsseo 09-21 09:36
    7
    最直白,最不绕弯的介绍就是:v 友又搞了一个 agent 。
  • langlang280025 楼主 09-21 09:36
    8
    @ihciah 我给你三个词:极简内核,极强稳定性,coding agent
  • langlang280025 楼主 09-21 09:38
    9
    @wsseo 重复造轮子很爽,重复造轮子把成品拿出来给 v 友们审查,挺折磨的
  • pke 09-21 09:38
    10
    建议直接给一个和其他 agent 的对比表格
  • langlang280025 楼主 09-21 09:42
    11
    @pke 好建议,和 pi 做了一次小对比,任务完成度同模型同推理强度裸测一样效果,部分隐藏任务 kiso 比 pi 省 token ,远比 Claude code 和 codex 省 token
  • lp7631010 09-21 09:47
    12
    我要的不是极致省 token 我要的是出活。当然 v 友牛逼
  • woshishui2022 09-21 09:54
    13
    感谢分享;
    “正常代码最多读取 200 行”,针对日志类文件是不是有特殊处理或者 tool
  • Cynicsss 09-21 09:55
    14
    @langlang280025 #6 怎么定义强呢。有评测结果吗
  • langlang280025 楼主 09-21 09:58
    15
    @lp7631010 同样追求,我要的是既省 token 又出活儿,随着模型智力提升,强如 Claude code ,内部也有大量的不必要的 token 损耗,当然他们不必要追求 token 效率,但是对于资源有限的开发者来说,比如我,省 token 也是一个很重要的考量
  • dabbit 09-21 10:04
    16
    我以前用 pi + 5.6 sol 的时候,因为他执行了错了命令,我直接停止了他,然后把命令也停了。然后跟他说了一句“继续”,我看他的思考过程里是有出现“进程没了,可能是用户刚刚把他停掉了”之类的话。我怎么觉得 op 搞的这个 kiso resume 有点多余?

    不过我以前用的是别人的 GUI+pi ,有可能他封装的时候实现了类似的东西吧。而且我也不好说模型自身是不是通过了各种思考和各种排查才得出了“用户刚刚把他停掉了”的结论。
  • langlang280025 楼主 09-21 10:04
    17
    @woshishui2022 200 行只是 read_file 的默认窗口,不是上限,支持 offset/limit 继续分段读取,显式指定 limit 也可以超过 200 。日志目前没有单独的特殊 tool ,一般先 search 定位,再按需分段读,主要是避免一次把大量日志灌进上下文
  • langlang280025 楼主 09-21 10:06
    18
    @Cynicsss 这里的“强”主要指 runtime / agent 工程能力,当然工程能力也可能是主观的。不是模型本身更聪明。launch 前我们用同模型、同 ds v4 flash high effort 、隔离环境和 pi 0.84.2 做了 48 对 / 96 legs 的对照:跨文件任务两边都是 12/12 ,隐藏题都是 22/24 ;隐藏题里 Kiso 的 cost v2 中位数比 pi 低约 32%,T5 基本持平(-2%),24-turn 长 Session Kiso 反而高 19%。完整 bench 和协议都在仓库里。
  • langlang280025 楼主 09-21 10:08
    19
    @dabbit 你这个例子里我同意,主动 stop 后宿主把中断信息告诉模型,模型一般能自己接上。Kiso resume 主要解决的不是这个,而是进程直接死亡后的“事实恢复”:比如 shell 已经开始执行,但进程 kill -9 了,结果没写回来,这时候模型没法知道副作用到底发生没有。Kiso 会持久化 execution start / receipt / approval ,成功过的不重跑,结果未知的标成 uncertain 让人裁决,而不是让模型猜
  • dabbit 09-21 10:10
    20
    @langlang280025 #19 我当时的确把 shell 也杀掉了。然后模型猜到了我把 shell 杀了。不知道他是怎么猜到的。也许你这个 kiso resume 会更直接,不用模型去猜。但我是先停 agent 再停 shell 的。
  • langlang280025 楼主 09-21 10:16
    21
    换个非 Coding 场景可能更直观。比如支付 Agent 发起一笔转账,本地已经记了 Started ,银行其实已经扣款了,但客户端这时断线,本地没拿到 Receipt 。重启后,“没收到成功”不等于“转账没成功”。

    这时候如果直接重试,可能扣两次;如果直接当成功,也可能其实一分钱没转出去。Kiso 处理的就是这种状态:结果未知就明确标成 uncertain ,等确认,而不是让模型根据上下文去猜。

    你这个例子因为是人为按顺序 stop ,宿主和模型确实还能从上下文推断; Kiso 主要是防这种没有人来得及留下解释的 crash window 。
  • woshishui2022 09-21 10:43
    22
    @langlang280025 #17 NVlabs/SoL-Pi 这个项目设计思路不错
  • langlang280025 楼主 09-21 10:50
    23
    @woshishui2022 好项目 值得学习 这就是开源的意义啊 不开源我都不知道这个项目
  • tuangouzi 09-21 11:13
    24
    最近也想学习 agent 框架的开发,纯兴趣,个人只有 python 与算法开发经验,up 有啥建议不
  • langlang280025 楼主 09-21 11:33
    25
    @tuangouzi py 和 ts 都可以做 Agent 。早期很多 Agent 框架本来就是 Python 生态出来的,现在模型能力也很强,语言更多只是工具。

    我觉得真正值得学的是 Agent 工程背后的东西:模型怎么和工具交互、状态怎么管理、出错怎么恢复、上下文怎么控制、effect 怎么安全落地。这些技术视野很难靠看一两篇教程建立起来。

    如果给一个建议,就是多看优秀的开源项目,直接读它们为什么这么设计,比如 pi 就很值得深度阅读
  • ErnestSu 09-21 11:44
    26
    vibe coding 了一个 rust 写的,跑在 android shell 的单二进制 agent 。帮助我这个越来越健忘的中登减少大部分敲命令查各种状态的。https://github.com/nl2sh/nl2sh
  • dd864140130 09-21 11:45
    27
    应该重点讲讲 sandbox/memory 的设计,这是考验功底的地方,其他的地方设计中规中矩,另外 benchmark 中可以加入 codex/Claude/Pi 等框架的任务完成情况下,这种可以方便大家参考
  • langlang280025 楼主 09-21 12:03
    28
    @dd864140130 对,这个建议很好。Kiso 现在肯定还不完美,sandbox 、memory ,包括 benchmark 的覆盖面,后面都还需要继续补强。

    之所以选择现在 launch ,而不是等所有东西都做到自己觉得“足够完整”,一方面是因为 0.40 对我来说已经是一个阶段性工作的总结;另一方面我也越来越觉得,一个健康的开源项目应该在还不完美的时候就尽早面对真实用户。否则很容易变成自己自嗨式定义“什么叫强、什么叫好”,最后社区并不一定认可,那就真成闭门造车了。

    所以这次 launch 对我来说也不只是发布,更是开始接受外部反馈。你提到的这些方向后面都会陆续补上,感谢建议,也欢迎继续拍砖:—)
  • coosir 09-21 14:09
    29
    非常好的分享。确实前面可以放下简单版的特性介绍和对比,方便没有耐心看的人先了解好概况
  • cli007 09-21 14:34
    30
    感谢!值得好好学习下
  • CareiOS 09-21 14:46
    31
    楼主是花了心思的
  • a4545645 09-21 14:54
    32
    你应该了解一下 pi agent 的 hashline edit pro 这个扩展,把它做成原生,你的编辑能力会比 pi 还要强和稳定
  • a4545645 09-21 14:57
    33
    还有你的 readme 看着非常像 gpt 5.6 sol 写的非人类文字,读起来非常费劲,建议用 gemini 或者 opus 4.6 重新润色
  • langlang280025 楼主 09-21 14:58
    34
    @coosir 确实需要先声夺人,这样才能在大家有限的注意力里,留下点印象🤣
  • langlang280025 楼主 09-21 14:59
    35
    @a4545645 嗯嗯 感谢你的建议 后面我会优化下 readme
  • Hi3333 09-21 16:00
    36
    我在思考 使用 agent 框架的意义,包括 pi 。 因为现在市场上有大量非常成熟大厂做的 agent ,为什么还要重复造轮子呢?
    用在哪个场景上? 写代码吗?包括之前火的龙虾等。
    望解答
  • niboy 09-21 16:06
    37
    写了那么多,感觉是 OP 的心路历程,建议表格对比,一目了然
  • lyog 09-21 16:11
    38
    写的很好,中间的心得部分,个人认为比框架本身更有价值
  • langlang280025 楼主 09-21 16:16
    39
    @Hi3333 我觉得不同的组织、个人,本来就会对 Agent 有不同的理解和工程取舍,这也是技术发展的正常过程。

    Agent 框架的意义,一方面是把模型调用、工具、状态、上下文、权限、恢复这些通用能力抽出来,让开发者可以更快地做自己的 Agent 产品;另一方面,我也不觉得“重复造轮子”一定是坏事。很多新的工程思想,本来就是在不同实现不断碰撞、验证之后才逐渐收敛出来的。

    像 pi 、Kiso ,或者其他框架,本质上都在表达作者对 Agent 应该怎么工作的理解。大家把这些思考公开出来,哪怕最后其中很多代码没人直接使用,甚至只是变成了后来模型和开发者学习的材料,我觉得也依然是在推动这个领域往前走。

    至于具体用在哪,我觉得不一定局限于写代码。Coding Agent 只是目前最容易验证、工具链最成熟的一类场景,Agent runtime 本身可以承载的范围要更广。
  • crazy0x 09-21 16:34
    40
    这种大段的非人类文字实在头疼,我除了扔给 AI 读实在想不到怎么看,如果 op 你自己来讲能解释清楚文章里面的这些细节吗。。
  • langlang280025 楼主 09-21 16:40
    41
    @crazy0x 你说的是 readme 还是本文啊,readme 是 fable 和 opus 写的,确实需要优化,本文是我写完初始草稿然后 AI 润色,然后我又修改的,本文我觉得还是有点价值的,因为他就是我的开发过程,如果你还是没办法直接读本文的话,上面做过总结,可以查看
  • jzyyaorenzhen 09-21 17:06
    42
    @ihciah 那就是:你自己去看吧
  • kidult 09-21 17:37
    43
    https://workbuddy.link/p/chmbxQ4Ne5uqB7IjrNYfB9?source=2 这个工作我来做吧,帮大家省亿点点 token
  • langlang280025 楼主 09-21 17:50
    44
    @kidult 哇 感谢 豁然开朗 我甚至想给你置顶 可惜 v 站没这个功能
  • vexify 09-21 18:03
    45
    虽然但是,不支持视窗系统
    ```
    PS C:\Users\xxx> npm install -g @vincemakes/kiso-code
    npm error code EBADPLATFORM
    npm error notsup Unsupported platform for @vincemakes/[email protected]: wanted {"os":"darwin,linux"} (current: {"os":"win32"})
    npm error notsup Valid os: darwin,linux
    npm error notsup Actual os: win32
    npm error A complete log of this run can be found in: D:\nodejs\node_cache\_logs\2026-09-21T10_02_19_363Z-debug-0.log
    PS C:\Users\xxx>
    ```
  • langlang280025 楼主 09-21 18:09
    46
    @vexify 额 还没适配 windows ,我的开发任务实在是太重了,我会尽快完整接下来的 todo ,然后准备适配 windows
  • deplives 09-21 18:19
    47
    太长不看
  • crazy0x 09-21 18:46
    48
    @langlang280025 两边都差不多,介绍的内容也没有太多特别之处,更像是想通过造轮子加深自己对 AI 工程的理解。控制闭环、边界划分、执行账本这些本质上都是已有的可靠系统与工作流设计,完全可以根据具体业务再通过一层简单的 harness 进行组合,这样固化反而牺牲通用性。
  • langlang280025 楼主 09-21 19:56
    49
    @crazy0x 这点我基本认同,很多思想本来就来自可靠系统和工作流设计,所以我一开就说这可能是重复造轮子,kiso 也没打算把它们包装成新发明。我的兴趣更多是把这些取舍落成一个具体的 Agent runtime ,并把边界和失败语义做成可验证的 contract 。至于这层抽象最终值不值得,还是让真实使用和社区反馈来判断,比我自己定义更有意义。kiso 会继续迭代,希望可以和开源社区碰撞出更有价值的版本~
* 帖子来源V2EX
返回