为什么我让 Agent 提架构提案,却不给它批准权限

0 阅读8分钟

首先让我来讲讲它是怎么做的,以及我在几个关键位置上 做的取舍。

先说结论:Jin 的核心不是“让 AI 更聪明”,而是把提案权和决策权分开。

  • Agent 可以创建提案、修改目标架构、执行校验;
  • Agent 不能批准;
  • 批准由服务器完成,带签名,并绑定到用户审阅时的目标哈希;
  • 批准或拒绝之后,服务器给会话发一条合成消息,Agent 自动继续。

这四件事串起来,才是一个完整的闭环。少任何一环,流程都会退化成“聊天里说一声 可以”,然后靠用户自己记得催下一步。

一、架构模型:L0 和 L1

Jin 把架构保存成两层小型 YAML。

L0 回答“系统由哪些部分组成,它们允许怎样依赖”:

version: 1
project:
  name: shop
  languages: [typescript]
modules:
  - id: mod-orders
    name: orders
    package: pkg-src
    path: src/orders
    public_entities: [fn-cancel-order]

L1 回答“每个模块对外暴露什么、位于哪里、依赖什么”:

version: 1
module: mod-orders
entities:
  - id: fn-cancel-order
    kind: function
    name: cancelOrder
    location:
      file: src/orders.ts
      symbol: cancelOrder
    relations:
      - type: uses
        target: fn-order-total

这里有三个刻意的设计。

1. 不做“大一统架构图”

Jin 不试图把整个代码库的所有细节都建模。它只保留对决策最有用的部分:模块、 分层、公开实体、位置、字段、方法、关系。

原因很现实:图越大,维护成本越高;维护成本越高,越容易变成一次性海报。Jin 要的是每天都能更新、每次改动都能审阅的小模型。

2. 实体 ID 是稳定引用

fn-cancel-order 这种 ID 会同时出现在架构图、结构化差异、漂移检测和提案目标 里。名称可以改,位置可以移动,但 ID 不变,历史才能对齐。

3. 位置必须指向真实声明

location.file 和 location.symbol 不是装饰。校验会去真实代码里找它们。找不到 就是错误,不是警告。

这也是 Jin 和“让另一个模型给架构打分”最大的区别:它尽量用代码事实校验,而 不是用自然语言说服自然语言。

二、提案:一次结构改动就是一个版本化快照

目录结构大致是这样:

arch/
  L0.yaml
  L1/
    <module>.yaml
  proposals/
    <REQ>/
      meta.yaml
      versions/
        v1/
          target-L0.yaml
          target-L1/<module>.yaml
          summary.yaml
      decisions/
        v1.yaml
  history/
    <REQ>.yaml

Agent 创建一个提案后,只能编辑当前版本的目标文件:

versions/vN/target-L0.yaml
versions/vN/target-L1/*.yaml

它不需要直接改 arch/L0.yaml 和 arch/L1/。批准之后,服务器才把目标写入正式 架构。

这样做有几个好处:

  • 可回滚。 每个版本都是完整快照,不是一段无法还原的 diff。
  • 可审阅。 用户看到的是目标架构与基线的结构化差异,而不是几百行 YAML。
  • 可校验。 校验针对目标快照执行,失败不会污染正式架构。
  • 可拒绝。 拒绝只保存决策和备注,不改 arch/。

提案状态大致会经历:

draft -> in_review -> applied
                   \-> rejected

jin_validate 是状态转换的关键点。校验不通过,Agent 继续修;校验通过,提案进入 in_review,同时发出审批事件,桌面端自动打开审阅面板。

三、校验:用代码事实约束架构声明

Jin 的校验器不是编译器,也不是完整的类型检查器。它做的是声明级检查:

  • 声明的文件是否存在;
  • 声明的符号是否存在;
  • 模块的路径和公开实体是否对得上;
  • 跨层依赖是否符合约束;
  • 应用后真实代码里是否出现了未登记的公开符号;
  • planned 实体是否已经提前存在。

扫描器目前支持:

语言提取内容
TypeScript / JavaScript导出声明的类、类型、函数、组件/页面/Store 实体、字段、方法和使用关系
Python顶层类和函数、方法签名、字段、点号导入
Java顶层类型
Kotlin顶层类型和函数

这套扫描器是声明级的,故意保持近似。它不追求完整的 AST 语义,也不试图替代 TypeScript 编译器。原因是:

  1. 架构审阅需要稳定、可解释的结果,而不是“编译器说行就行”;
  2. 多语言支持要能渐进扩展,不能被某一种语言的类型系统绑死;
  3. 审批门关注结构,不关注每一行实现。

代价也很明确:它可能漏掉一些复杂语义,也可能对动态用法判断得比较保守。文档里 把这一点写得很清楚,避免用户误以为它是“架构正确性证明器”。

漂移检测也是同一套思路。应用之后,Jin 会把架构里声明的实体和真实代码声明做 对比:

  • 缺少位置或符号:错误;
  • 未登记的公开符号:警告;
  • 模块外位置:警告;
  • planned 实体已经存在:提示。

它不是要消灭所有警告,而是让“架构和代码不一致”这件事可见。

四、审批门:为什么不能只做一个前端按钮

这是整个项目里最容易被低估的部分。

如果审批只是桌面端的一个按钮,那么它仍然只是 UI 约定。Agent 可以绕过 UI 直接 改 arch/,脚本也可以伪造一个“已批准”的 YAML。对于一个要给 Agent 设定边界的 系统来说,这不够。

Jin 把决策权放在服务器:

  • 服务器持有 HMAC 密钥;
  • 桌面端只发送决策和用户审阅过的目标哈希;
  • 服务器验证签名、目标哈希和基线架构,才允许应用;
  • 手动修改 meta.yaml 或 decisions/*.yaml 不会批准提案;
  • 审阅后目标被改了,哈希不匹配,批准失效;
  • 提案创建后正式架构已经变了,基线不一致,默认也不能直接应用。

签名密钥按优先级解析:

  1. JIN_APPROVAL_SECRET;
  2. JIN_APPROVAL_KEY_FILE;
  3. $XDG_DATA_HOME/opencode/jin/approval.key,否则 ~/.local/share/opencode/jin/approval.key。

自动生成的密钥文件权限是 0600。如果希望之前签名的决策长期可验证,就不要随便 换密钥。

这套设计的目标不是“防住所有攻击”。它解决的是一个更具体的问题:让 Agent 的 提案权和人类的决策权在系统层面分开,而不是靠提示词约束。

Agent 可以很聪明,也可以很主动,但它不能替用户按下那个批准按钮。

五、自动继续:审批完不能让流程断掉

很多审批工具死在一个细节上:批完之后,用户还得再发一句“继续”。

Jin 的处理方式是:决策写入后,服务器向会话发送一条合成继续消息。批准和拒绝都会 继续,区别在于 Agent 拿到什么上下文:

  • 批准:目标已应用,Agent 接着做实现、测试或后续步骤;
  • 拒绝:备注回到会话,Agent 基于反馈修订提案。

这听起来像一个小功能,但它直接决定了审批是“流程的一部分”,还是“流程里的一个 收费站”。

当然,自动继续也可能失败。比如会话已经关闭,或者服务器在审阅和决策之间重启。 这时决策已经持久化,面板会提示“决策已保存,但会话未能自动继续”,用户正常发一条 消息就能接着处理。这个失败路径也写进了文档,而不是假装永远不会发生。

六、界面:让结构差异可读

Jin 的审阅界面有两个模式。

审阅模式

  • 项目状态、模块/实体/提案数量、校验摘要;
  • 应用后漂移;
  • 提案队列和版本切换;
  • 模块、实体、变更文件的结构化差异;
  • 琥珀色审批卡:批准并应用、拒绝、可选备注;
  • 应用历史。

架构图模式

  • 系统和分层构成外层结构;
  • 模块响应式排布,实体卡内联显示字段和方法;
  • 跨层模块依赖是蓝色虚线,跨层实体依赖是青色实线;
  • 悬浮或选中节点时,只保留直接关系,其余淡出;
  • 新增、删除、变更、移动节点分别用绿、红、琥珀、紫标注;
  • 选中实体后,右侧栏显示完整检查器。

Jin 架构图

这些视觉设计不是为了好看,而是为了回答审阅时最常出现的三个问题:

  1. 这次改动到底动了哪些结构?
  2. 有没有新的跨层依赖?
  3. 这个实体的字段、方法和关系是什么?

如果一张图不能回答这三个问题,它就只是装饰。

七、我做的取舍

Jin 现在还有很多不完美的地方,其中一些是刻意取舍:

  • 桌面端优先。 先把审批闭环跑通,再考虑独立应用和更多入口。
  • 声明级扫描。 不追求完整语义,优先稳定、可解释、可扩展。
  • 不做测试执行器。 审批门只管架构决策,测试和 CI 仍然必要。
  • 不做模型内置。 模型由用户通过 OpenCode 提供,Jin 不绑定 Provider。
  • 不追求“全自动”。 结构性改动就是要停下来让人看一眼,这是设计目标,不是 缺陷。

如果你问我,AI 编码接下来最值得补的是什么,我的答案不是“让模型再快一点”,而是 让它在关键决策上停下来,并且停下来的方式足够顺滑。

Jin 是我对这个答案的一次尝试。

项目与下载

Jin 是 OpenCode 的社区分支,不是 OpenCode 官方产品;模型由用户自行配置。

项目地址:

github.com/ZYF93/openc…

v2.0.32-jin.1 下载(macOS / Windows / Linux):

github.com/ZYF93/openc…

中文完整指南(架构模型、审批契约、安全、排错):

github.com/ZYF93/openc…

如果你对提案生命周期、签名审批或扫描器实现有更好的想法,欢迎直接开 issue 或 PR。这个项目还很早期,很多设计值得被挑战。