AiRobotNews 人工智能机器人技术网

把「决策」做成代码能检查的东西:keel 0.2.0 里的 jev 决策层

把「决策」做成代码能检查的东西:keel 0.2.0 里的 jev 决策层

一个 coding agent 里,模型到底该在哪几个地方做决定?

作者 Avid(@Av1dlive)用三千五百字回答了这个问题,讲他怎么把一层「决策层」嵌进自己写的 Mac coding 应用 keel 0.2.0:新任务该走哪个 provider、哪个模型,交给一个叫 jev 的选择器;内嵌循环里下一步是 inspect、implement、verify 还是 answer,也交给它。但每一个决定都必须能被宿主代码检查、能在事后回放。

他反复强调的一点是:模型可以提议下一步,这一步的所有权仍然在宿主。 听着像措辞细节,实际决定了整个架构。

先对齐几个名字,后面全篇都在用:

名字在这篇文章里指什么
keel应用本体。local-first 的 Mac coding app,Rust 写,界面用 GPUI,Apple Silicon + macOS 15 起
jev托管的决策选择器,也是作者这套方法的名字(jev engineering)
laya本地选择器,跑在 Core ML 上。项目里默认用它,jev 要手动开
宿主(host)包着模型的那圈应用代码。它准备选项、检查结果、执行动作
ACPagent client protocol。keel 通过它接入外部 coding provider
MCP工具连接协议。外部 agent 靠它用上机器上装好的工具

一、先给决策一个形状

作者想做一个会变好的 coding agent,同时想知道「它变好的时候,到底是哪里变了」。这个目标要求决策是可查的,于是他先定义什么叫一个「有用的决策」。

每个小决策要凑齐三样东西:

  • 具名的选项。不是一个模糊的「选个合适的模型」,而是一份列得出的清单。
  • 代码能检查的结果。返回值得是固定格式,代码读得懂、验得了。
  • 事后留下的记录。发生过什么,得能翻出来。

第三条最容易省掉,也最要紧:多出来的那一次模型调用,必须自己证明值得。它不能因为「听起来更聪明」就占一个位置。

文章分四部分的路线图

全文走三部分:jev 怎么当决策层、为什么 coding harness 适合放这一层、keel 里各部件怎么拼起来。还有第四部分,因为标题许诺了「自我改进」:决策记录能支撑什么,以及它暂时还证明不了什么。

模型可以提议下一步。宿主持有这一步。

二、jev 只做两件小事

jev 作为决策层嵌在宿主与模型之间

菜单归宿主,jev 只在菜单里挑

在这个构建里,jev 从宿主预先备好的清单里选。哪些 provider 可用、哪些模型装了、当前任务需要什么推理等级,这些都是宿主的判断,发生在 jev 发言之前。

jev 的产出是一个 typed result:固定格式,代码能验。它只表达两种意思——「选这个」或者「我选不了」。

契约就这么点内容。界面窄是有原因的:让模型随便返回一条指令,应用里每个调用它的地方都得自己琢磨这指令什么意思。

结果要能被代码读

格式固定之后,这活就变得「令人愉快的沉闷」:读结果、对着当前状态验、接受或者走兜底。

他打了个比方:像忙碌车间里的调度员。调度员可以决定哪张工作台接这张单子,但没资格悄悄改写锯床的安全守则。

两个能决策的位置

两个决策点在流程中的位置

一个在任务进入时,一个在任务执行中。听着像一回事,其实发生在完全不同的时刻。

1. 给新任务选路由

新任务没有锁定的 provider 或模型时,宿主可以准备一批合格的候选,由 jev 或本地 laya 在其中挑,或者弃权。

已经被用户锁定的路由不参与这套选择;正在进行和恢复中的会话也不参与。选择器要保住用户已经做过的决定。

先过滤,再问。 「选最好的模型」需要一个边界,宿主必须准备好运行它给出的每一个选项。所以过滤发生在 jev 看到清单之前,宿主会剔掉这些路由:

  • provider 没装,或者没启用
  • 模型找不到
  • 请求的推理等级不被支持

还有一条挡不住的:会话启动时 provider 仍会检查自己的登录状态,这一步可能在路由通过宿主检查之后才失败。

2. 内嵌 deepseek 循环里选下一步

这个循环给出四个焦点选项:inspect(看代码)、implement(动手改)、verify(验证)、answer(直接回答)。

每个焦点对应一套宿主定义的工具

选择器返回一个 focus id,宿主据此找到允许的工具,并在使用前对着当前的工具定义再核一遍。

answer 意味着没有工具。 这是个好例子:如果选择器说了 answer,系统不会顺手塞给它一个 shell 再指望它老实待着。允许的工具集是空的,因为在这条路径里 answer 就是这个意思。

作者特意澄清一句:这不等于让 jev 去控制应用里每一个 coding agent,它没这个能力。

一个例子:修 parser 的 bug

假设你让应用修一个 parser bug。他说这只是示意,不是这次构建里量到的结果。

如果你锁了 provider,那个决定原地不动。如果是全新的、未锁定的任务,而且自动选择开着,宿主可以把自己能跑的路由递过去。选择器在这份清单里选,它没法凭空叫出一个没装的 provider。

进入内嵌循环之后,决策变小了:inspect、implement、verify 还是 answer。选 verify 会改变被允许的工具集。

但它不能证明补丁是对的,检查还得真跑一遍,结果还得真有意义。

作者说设计到这里才「咔哒」一下对上了:「哪个工人」和「下一步用哪些工具」该是两个边界,因为它们是两种活,失败方式也不一样。

边界:外部 agent 的循环还是它自己的

keel 通过 ACP 可以接好几个 coding provider。这些 provider 自己跑工具循环,也可能自己暴露对工具、上下文大小、交接的控制。

keel 并不从头到尾管住这些步骤。

jev 与 laya 不接管外部 agent 的内部循环

命令列表里有一行,不等于有一个能用的 API。 宿主能读到 provider 的能力描述,但描述本身不提供执行动作的办法。一个列在清单里的 slash command,不会自动变成 jev 能调的 action;在当前实现里,它可能只是变成喂给 provider 的提示文本。

这个边界在图上很容易糊掉,因为「agent」这个词让一切都看着像同一个 agent。实际存在三方:宿主、决策层、以及可能由 provider 拥有的 agent 循环。宿主能把新任务路由给某个 provider,但这不说明谁控制 provider 的下一次工具调用。

能力审计帮他把这条线画了出来——keel 会列出命令、技能、模式和工具。

边界要画在代码能强制执行的地方。 那些描述告诉我们 provider 宣称了什么,宿主仍然需要一个真能调起动作、还能检查结果的路径。于是原本「jev 优先」的野心,收窄成了现在这个实现:边界跟着自己控制得了的代码走。

在架构图上再画一根箭头,变不出一个 API。

相邻的一块:computer use

keel 里还有个 keel computer-use decide。它让 laya 或 jev 从宿主准备好的低风险动作 id 里选一个,或者弃权。

这条命令不执行桌面动作。 这个区分在读代码时很有用:挑一个提议的点击,和真的点下去,是两件不同的责任。兼容 ACP 的 agent 可以通过 MCP 用上装好的桌面工具,而那些 agent 仍然拥有自己的内部循环。

选择 ≠ 授权

jev 能挑候选,它不能批准危险动作。需要权限的工具照样走权限流程,需要用户点头的地方照样要点头。模型选择不授予任何权限,一个很高的置信度分数同样不授予。

动手之前再核一次状态。 应用这个选择之前,要看动作是否还匹配当前任务状态。provider 列表变了,或者路由过期了,宿主可以拒掉这个选择、走兜底。

用工具之前再核一次工具。 在内嵌循环里,宿主要对着当前工具定义检查这套工具,真正运行前还会再按名字核一遍。两次检查目的相同:别让一个旧决定作用在一个已经变了的系统上。他把这归到「无聊」的那部分。而安全检查,无聊恰恰是你要的。

弃权与兜底的路径

给「我选不了」留一条明确的路。 弃权是合法结果,但宿主仍要知道接下来干什么。兜底规则要在调用选择器之前就定好,用的时候要记下来——不然兜底会被算成选择器的功劳。

上图是路由选择的几条路径。他说得清楚:没有一条路径因此获得运行工具的权限。

记录里放什么

应用可以在选择器调用过程中显示一个活动状态。等宿主验完结果、看到后续发生了什么,它就可以写一条决策事件或回执。

这些记录让一些很普通的问题变得可答:

  • 当时的候选有哪些?
  • 选择器是选了还是弃权?
  • 验证通过了没有?
  • 有没有走兜底?
  • 之后工具的结果是什么?

他把话说在前面:这些不是私有的思维链。需要知道的是「这个选择当时合不合法、宿主接受没有、接下来发生了什么」——这是一个有意做的取舍,为了攒够能评估系统的证据。模型自己写的解释,不能证明事情为什么发生。

三、为什么放进 coding harness

放在 coding harness 里的理由

编码是一串分叉

人们说起 coding agent,常当它是一次调用:提示词进去,补丁出来。真实的工作更像一串岔路:

  • 这个任务该哪个模型干?
  • 先读文件还是先改?
  • 此刻哪些工具是相关的?
  • 决策还在路上的时候,仓库变了没有?
  • 该重试、弃权,还是问人?
  • 改动编过了吗、过了针对性检查吗、保住了用户的意图吗?

这些分岔的代价不一样。用慢模型做一次改名是浪费时间。

一次错选的代价

反过来,错的比慢的贵。一个快的模型可能在大迁移上要多试几轮;同一个工具选择,在一个工作区里无所谓,换个工作区就有后果。

harness 手里握着塑造每个动作的那些细节:当前仓库状态、可用的 provider 和工具定义、权限与任务状态、检查结果。

模型是这套系统的一部分,不是系统本身。

harness 让决策有地方落地

一个独立的决策 demo,能让模型在三个标签里挑一个。那是开始,但它不能告诉你这个选择到底能不能用。

放进 coding harness 之后,宿主能回答很具体的问题:这些选项当时真的可用吗?任务是新的、锁定的,还是已经在跑的?执行开始时那个工具还在吗?权限层放行了吗?工具返回了什么?验证有没有发现回退?

作者的观点很直:把选项、选择、结果三样摆出来。 少了这三样,说「agent 变强了」基本就是在感觉上做判断。

三种模式,三个不同的选择

laya 和 jev 是两个不同的选择器,normal 模式不做选择。下图是三条路径各自需要什么、宿主在哪里核结果。

三种模式各自的路径

Core ML 是 Apple 的本地模型运行时。jev 用的是机器上已有的受保护凭据,这个构建里没有输入密钥的界面。

「永远用最聪明的模型」这句话,忽略了延迟、隐私、凭据和用户意图。把模式做成一个显式的选项,就是给这些取舍在应用里安个位置。

多出来的那次调用得自己挣位置

作者顺手跑了个题外话:路由这件事会长成自己的小官僚体系。如果选一个工人比干活还久,那只是造了个特别精致的候诊室。他想比的指标是总任务成本:选择器耗时、工人耗时、重试、评审投入。

更便宜的决策调用只有在整件事都受益时才有用。

并且他明确承认:这篇文章没有用 coding benchmark 证明这个收益。

还有一个不容易看见的限制——选择器没法选出一个宿主忘了提供的选项。

决定与执行之间要有硬边界

  • 选路由时,模型收到的是 candidate id:指向宿主准备好的选项的标签。这些 id 不让模型凭空发明命令或 provider,运行所需的细节留在宿主手里。
  • 内嵌循环里,选择器只返回一个 focus id,宿主把它变成一套具体的允许工具,然后再核一遍。

这意味着决策层是可替换的:调用方不需要相信它的散文,只需要相信宿主的检查,而检查要清楚到人能看懂的程度。作者喜欢小接口的原因就在这——你能指着说清它做了哪个选择、这个选择放行了哪些工具。多给一点自由,跑歪的时候就要多解释一份行为。

不确定性要显示出来

模型系统即使内部一团糟,也常输出一份整洁的总结。harness 该保留那些要紧的乱处:选择器弃权了,就显示弃权;宿主拒了一个过期动作,就显示拒绝;回退到用户原来的路由,就把兜底记下来。

这些不是噪声,它们是追溯链的三段:宿主提供了这些选项、选择器返回了这个 id、宿主按这套规则接受或拒绝了它。

harness 保证不了什么

把决策层放进 harness,不会让每个结果都正确,它只是让边界更容易写清、结果更容易检查。

它不证明 jev 选了最好的模型,也不说明一次成功的构建是这个选择器带来的。

还有一点容易被「本地」两个字盖过去:laya 指的是选择器本地跑,你的 coding worker 可能仍在调用托管模型;一个本地运行的 ACP 客户端同样可以调用托管模型。在把整条流程描述成「本地」「私有」之前,先把每一跳追一遍。

四、keel 0.2.0 的样子

keel 0.2.0 的构成

构建是 keel 0.2.0,一个 local-first 的 Mac coding 应用:

  • 基础:一个从 avid 衍生的 coding 工作区
  • 语言:Rust
  • 界面框架:GPUI
  • 平台:Apple Silicon,macOS 15 或更新

这个细节重要,因为故事里有两类工作:一类是应用本身,一个有会话、provider 连接、工具、本地决策模式的真实工作区;另一类是 jev engineering 的研究与评估材料。两者相关,但不是同一个交付物。

版本与运行环境

起点是一个已经在用的工作台。它本来就有任务、仓库、provider 会话和工具,于是有个务实的中心:当决策被挂到任务、仓库、会话和工具执行上时,它的行为如何。

五步设计

设计过程的取舍

有用的设计问题只有一句:哪里能让 jev 做一个选择,同时应用还能检查这个选择?这句话既决定了它该放哪,也给了「别往宿主管不了的地方加」的理由。

  1. 找出决策边界。 新任务路由、有界的下一步焦点,都成立。provider 内部的动作为什么不行?因为那些循环不归宿主。
  2. 把选项显式化。 宿主得知道哪些 provider 和模型装了、启用了、对当前任务有效。选择器收到候选,它不制造候选。
  3. 保住用户意图。 有人锁了路由就保留;会话在跑就别悄悄换道。全新、未锁定的活儿,才是自动选路该待的窄口。
  4. 把普通路径建起来。 默认是本地 laya,jev 是明确的 opt-in。normal 模式保留给想要原有行为的人。
  5. 给失败一个形状。 格式错、id 非法、任务信息过期、弃权,每种都要有明确结果:拒绝、兜底,或者问用户。没人需要假装每个模型回复都可用。

作者自己接了一句「保守吗?是的」,然后给了立场:宁可要一个能调得动的窄选择,也不要一个漂亮但走不进代码的承诺。

顺着文件读

从入口文件顺着读的顺序

读代码时先读「应用这个选择」的那一段。这张图给的是阅读顺序和主要的应用层,卡片之间的箭头表示读的顺序,不是函数调用。文末的源码笔记链到关键实现。

作者提醒:设置里的一个开关只说明这个模式存在,真正决定它能干什么的,是那个接受路由、分派工具的代码。

请求路径上的 8 步

jev 在路由请求路径里的位置

用平话说,路由是这样走的:

  1. 新任务进来,没有显式锁定的路由
  2. 宿主读当前 provider 与模型可用性
  3. 拼一份有限的合格候选
  4. 选定的决策模式调用本地 laya 或托管 jev(如果这条路径启用了)
  5. 选择器返回一个 candidate id,或者弃权
  6. 宿主核对这个选择是否仍然合格、仍然最新
  7. 应用按策略启动这条路由,或走兜底
  8. 一条决策记录把发生的事记下来

锁定的路由和已经在跑的会话绕开自动选择。作者强调这不是脚注,而是核心产品规则。

步选择器另有一套检查。 内嵌 deepseek 循环里顺序不同:宿主给出 focus id 和候选工具集,选择器返回一个 focus,宿主对着当前工具 schema 准备对应的工具包;分派时再按名字核一遍。原则一样,生命周期不同。

步选择器的检查顺序

五、「自我改进」目前只到记录

第四部分:自我改进该是什么样

发版只记录,不训练

作者想把这条线划清楚:keel 记录选择器的活动和可以支撑评估的结果,它不会拿聊天历史自动训练 laya,也不会在一次失败的运行之后悄悄改写自己的策略。这个发布没有证明任何科幻意义上的自我改进。

记录变成可复现的场景

它给的是一个反馈回路的起点:一条回执可以变成一场景,场景可以对着改过的提示词、策略或模型回放,人可以看差异、决定改不改。

不那么电影化,但至少是「一条改过的规则、旧结果、新结果」这种能动手的东西。「agent 学到了东西」这种说法,只会让人继续猜。

一个失败样例:路由选到一个刚下线的 provider

假设路由选择器挑了一个 provider,而那个 provider 一秒后变得不可用。别只说一句「jev 搞混了」,把这些存下来:当时提供的选项和任务状态、选中的 id、宿主的检查结果、用到的兜底。

然后做一个能回放的场景:

  1. 复现同一组候选和相关任务标记
  2. 跑基线选择器或策略
  3. 在同样条件下跑候选改法
  4. 比有效选择、弃权、兜底、延迟、下游结果
  5. 由人评审这次改动有没有让目标行为变好

工具定义变了的回放

如果是工具焦点选择,场景可能不一样:模型选了一套工具包,但 schema 在分派前变了。要检查重建工具集时旧工具被过滤掉了,还要检查宿主在对方坚持要它时会不会拒绝。

这就是结构化记录的价值:它给了你重建决策条件的材料。「agent 好像有点懵」只给你一种心情。

跟原版 harness 比

一个任务通过,不能说明该归功于哪个组件——也许工人用原来的路由也能解出来。

对比时的控制变量

任务集和评分规则要固定。工人、提示词、仓库、评审者的改动,同样会影响结果。

留好失败样例,守住人工门禁

最终对比用单独的任务集:别在每个样例上调完,再把熟悉的那几个当证明。难看的那次运行,可能正好暴露一个干净 demo 藏起来的缺失规则。失败样例、兜底率、总耗时、评审投入,都记下来。

作者明确:这是提议的评估设计,文章不宣称量到了编码质量的提升。

改进回路可以提议修改这些:候选构造规则、选择器提示词或类型 schema、兜底策略、本地模型或托管路由、焦点选择挂的工具包。每一项都保留旧版本、回放相关场景、检查回退,然后由人批准哪个版本成为新基线。没有静默自训练,没有「它因为学习了所以变了」却不给 diff 和不给回滚路径。

作者说从哪开始

  • jev 在宿主准备好的选项里挑,它不拥有宿主
  • laya 和 jev 是两个模式,不是一个模型的两个名字
  • provider 自己的内部循环,归 provider
  • 选择不授予权限
  • 记录支撑未来的评估回路,但不等于应用在自我训练
  • 公开的 jev engineering 仓库放框架和例子,keel 是这里讨论的应用

他喜欢这类工程的地方在于:模型可以很聪明,外面那套系统仍然可以很清楚。

六、自己动手先写四件事

如果你也在搭 coding harness,作者建议先写下四行:

  1. 它被允许做的那一个决定
  2. 它能看到的确切选项
  3. 它弃权时会发生什么
  4. 你怎么知道结果有没有帮上忙

然后只建那条能验证这四条规则的最窄路径。小到一个坏选择无处可藏。

更好的 agent,需要一个能记住哪里出过错。

七、源码入口

作者给的链接都指向文章对应的 0.2.0 源码快照,主分支还留着更早的原型:

仓库现状(2026-11-01 现查):

项目数据
星数 / Fork340 / 45
主语言 / 许可Rust / MIT
创建 / 最后推送2026-09-22 / 2026-09-25
未关闭 issue3
版本标签未打任何 release tag(0.2.0 出现在作者的构建报告里)

最后一点和文章对得上:作者说主分支还是更早的原型,文章链接指向 0.2.0 那个快照。要照着重做一遍,读快照里的文件比读 main 稳。

资料来源