首页/设计深潜/扩展与治理

扩展与治理

Trove 的扩展面很多:数据源适配器、组织技能、判定规则、行动模板、语义模型、 MCP 工具面。它们有三个共同的工程问题——扩展点真的通电了吗 (函数存在但零调用方,是历史上真实发生过的)、 组织资产谁说了算(自动生成的草稿绝不自己生效),以及 这个资产到底是什么(读什么、作用在哪个域、装上去会改变什么)。 这页讲一条注册路径、一套统一的治理形状、信封与 guard 断言档、 一个能进 CI 的干跑校验(装前试跑与影响面回放共用同一条退出码契约)、 两个粒度的停用(整层总开关与单资产停用)、能带着来源链搬走的资产包, 以及每次运行留下的那两份证据。

扩展点的两种死法

一个扩展点可以死于两件事:没通电——接口做好了、文档写了, 但没有任何一处代码真的走它(注册表空着,扩展者照着文档接上却发现不生效); 两条路——内置走一条内部捷径、外部走公开入口,两条路迟早行为分叉, 而分叉只会在扩展者那边暴露。Trove 的适配器注册表两种情况都经历过, 所以现在的形态被刻意收窄成一句话:

单一注册路径

register_adapter(dialect, cls) registry.py:45 register_adapter 是唯一入口:内置的 8 个方言在模块末尾走同一函数自我注册 registry.py:516 内置自我注册—— 没有任何「内部第二条注册路径」。新增数据源 = 实现 DatabaseAdapter + 调一次这个函数, 文件末尾那 8 行 import + 注册本身就是落地样板,照抄一行即可——早先这条路径上用注释掉的一行 # register_adapter("snowflake", …) 充当「未实现示例」,A7 落地 Snowflake 时它被真实注册取代。

统一治理形状:草稿不自证

组织资产(技能、判定规则、行动模板、语义模型的草稿)全部走同一个生命周期: 自动生成的内容落 pending,管理员确认后才进入消费面, 拒绝是终态;每次变更进 git,可查历史、可回滚。这不是四个系统的四套规矩, 是同一条纪律在四个面上的实例:

flowchart LR
    DRAFT["自动草稿(LLM 起草 / 学习回路 / 候选收件箱)"]:::gen --> PENDING["pending
不进消费面 · 不进提示词"]:::gate HUMAN["管理员手写"]:::entry --> PENDING PENDING -->|"confirm"| LIVE["confirmed
下一问即生效(消费点每问现读,无缓存)"]:::entry PENDING -->|"reject"| DEAD["rejected(终态)"]:::entry LIVE --> GIT["GitVersioning 提交
scoped: git add -- 仅具名路径"]:::entry GIT --> HIST["历史 / 回滚"]:::entry HIST -->|"rollback sha"| LIVE classDef entry fill:#71717a14,stroke:#71717a59,stroke-width:1px; classDef gen fill:#6366f114,stroke:#6366f199,stroke-width:1.5px; classDef gate fill:#f59e0b14,stroke:#f59e0b99,stroke-width:1.5px; linkStyle 6 stroke:#8b5cf6,stroke-width:1.5px
图 1 · 组织资产的生命周期。自动内容永不自确认;每一次确认、拒绝、回滚都是一条 git 提交,回滚本身也留下一条提交。 上面这段以源码显示:图渲染库没加载起来(离线、CDN 被拦或浏览器禁用了 JavaScript 时会这样)。这段 Mermaid 文本本身就说明了上面那些框与连线。

三个实现细节让这条链是真的:

同样的形状在三个管理面重复出现:技能(draft / llm-draft / confirm / reject / tier / body / history / rollback / disable / enable, skills.py:266 history)、 语义模型草稿(drafts / confirm / reject / history / rollback, semantic.py:221 history)、 行动模板(confirm / reject, 行动安全架构)。 学习回路(闭环与契约)的产物也全部落在这一形状里—— 自动内容从不为自己背书。

信封:能力是推导的,不是声明的

治理要回答的第一个问题不是「谁批准了」,而是「这个东西到底是什么」: 它读哪些输入、产生什么效果、作用在哪个域(提示词、结果,还是 SQL)。 每个组织资产都能编译出一份这样信封(envelope)—— 它不是新的文件格式,而是从资产本体推导出的编译产物: trove extensions list / show 读的就是它。

关键在「推导」二字:能力清单不是作者声明的,是从引用闭包里算出来的。 断言表达式实际读到的标识符闭包,就是它需要的变量域;效果取自闭集; 作用域跟着表达式使用的域走——一个 guard 断言的信封上,targets 自然是 sql。推导不出引用的(表达式里出现了域外的东西), 信封不猜,而是把它列进 unresolved 响亮报出来。 「作者声明了一套、表达式读的是另一套」这种漂移从结构上就没有发生的余地: 声明与校验读的是同一份事实。

信封还出现在另外两处:--run 试跑的语料按信封枚举资产 (测什么由信封决定,不由测试脚本手抄清单);extensions plan 把信封与运行时装配对账(见下文)。

envelope.py:122 Capabilities envelope.py:257 derive_check_capabilities sources.py:113 collect_assets

guard 档:把组织底线放上 SQL 闸

治理形状之外,扩展面还有一道运行时的闸:技能的第四档 guard——正文从不进模型的声明清单,由引擎在 execute_sql 节点 「授权门之后、执行之前」逐条判定。判定机制(九个 SQL 结构变量、 三值落点、两档 severity)在 Skills 页, 这里讲它为什么长这样:

guards.py:71 GUARD_TARGETS execute_sql.py:62 ORG_GUARD_TAG

干跑校验:一条能进 CI 的退出码契约

治理不能只靠事后审计。所有扩展面有一场零 LLM 的干跑校验: trove validate (validate_cmds.py:92 main_validate) 把 KB、规则、技能全部试编译一遍,退出码就是判定: 0 clean · 1 有硬错误(--strict 下警告也算)· 2 用法/环境错误 validate_cmds.py:9 退出码契约。 这让「资产是否健康」成了一个可以挂在提交前/CI 里的布尔量, 而不是管理员肉眼巡检。

同一条命令还带两条语料回放——--run 装前试跑与 --impact 影响面回放(见下两节)。它们零 LLM、与静态检查共用同一条退出码, 并把它扩写了一格:在试跑与回放里,2 意为「这次没验成」—— 语料缺失、判定执行出错、首选方案读不出、一条判定都没跑过,都走 2。 静默的 0 会把「没验成」卖成「验过了」,所以这里宁可响亮。

开关作用
--datasource demo只体检一个数据源
--json机器可读整份报告(CI 消费)
--strict警告也视为失败(退出码 1)
--live额外连数据源:枚举缺口探测 + 真方言编译 + 声明验证(自然键唯一 / 关系基数 / fan_out: dedup 前提,对库只读探测,四态裁决——违例即硬错误)——静态检查证明不了的,交给真实连接证明(service.py:1431 run_validate)
--run装前试跑:用本机语料对已确认资产做「装 / 不装」双态消融(零 LLM;语料见 --fixtures / --episodes)
--impact <包|目录>影响面回放:候选资产集(待导入的包,或直接的资产目录)装上前后,同一份语料的判定差分;与 --run 互斥
--packs反作弊检查:KB 示例 SQL 与本地 gold 集(gold.sql)的相似度——命中只出警告,绝不自动拒载或改写 KB;没有 gold 集时如实报 skipped(未检查 ≠ 检查过,service.py:1359 _check_packs)

装前试跑:装上会拦什么

--run 拿本机语料(默认读 .trove/kb/<ds>/fixtures.yml; --episodes 追加本机历史问答)把已确认资产「装 / 不装」两种装配各判一遍, 差分出这批资产的净效果(service.py:1573 run_validate_with_dryrun)。 两个细节:

dryrun.py:957 run_dryrun dryrun.py:636 Assembly

影响面回放:换一批,判定差在哪

--impact 面向「换资产」这件事:候选包(或资产目录)装上前后 各判一遍同一份语料,差分落进四个桶——新增拦截 / 新放行 / 无变化 / 无法判定 (impact.py:313 classify), 每一行都带证据:哪条断言触发、表达式是什么、变量当时的取值。 --json 的顶层 metrics 就是回归门 scripts/eval_gate.py 认的 scorecard 形状——回放报告不需要转换脚本就能挂进离线评测回归门 (impact.py:881 run_impact)。

同一套校验在 REPL 里是 /validate [--live] [--run [--episodes]] [--impact <包|目录>] (validate_cmds.py:157 /validate,不出退出码——结论落在输出末尾的「退出码 N」一行)。 技能面单独有一层检查(service.py:554 _check_skills)—— 包括 validator 与 guard 两型技能的断言规格(结果域 / SQL 域两套闭集校验, 见判定内核)。

停用开关:停消费面,不停写入面

扩展面治理有一个总开关 agent.extensions.org_extensions_enabled (config.py:361 ExtensionsConfig, 默认全开)。它停用的是组织扩展这一层的消费面:组织技能的注入与 load_skill 广告、validator 断言(结果域)与 guard 断言(SQL 域)、 决策规则的执行。 两条边界写死在文档字符串里:

flowchart TB
    SW["org_extensions_enabled = false"]:::gate --> STOP1["停:组织技能注入与 load_skill 广告"]:::entry
    SW --> STOP2["停:validator 断言(结果域)与 guard 断言(SQL 域)"]:::entry
    SW --> STOP3["停:决策规则的执行(DecisionService 评估入口)"]:::entry
    SW -.-> KEEP1["不动:code skills(trove/prompts/skills/)"]:::entry
    SW -.-> KEEP2["不动:KB / few-shots / 语义模型"]:::entry
    SW -.-> KEEP3["不动:写入面——照常起草 / 确认 / 编辑"]:::entry

    classDef entry fill:#71717a14,stroke:#71717a59,stroke-width:1px;
    classDef gate fill:#f59e0b14,stroke:#f59e0b99,stroke-width:1.5px;
图 2 · 总开关的作用边界。产品默认(随代码发布的方法论)不是组织资产,一个开关不许顺手把它们也停了——否则「停用」与「功能破坏」就分不清了。 上面这段以源码显示:图渲染库没加载起来(离线、CDN 被拦或浏览器禁用了 JavaScript 时会这样)。这段 Mermaid 文本本身就说明了上面那些框与连线。

配套的一条响度纪律值得单独说 (service.py:364 _org_enabled): 停用期间,定时决策任务报错(「已停用」),绝不静默按零规则通过—— 一个安静跑着的健康态和一个被关掉的判定,从运行记录上看必须不一样。 这是全仓同一哲学的另一处实例:失败要响亮, 沉默是最难排查的状态。

颗粒停用:停一个资产

总开关停的是一层;颗粒停用停的是技能面上的一条:org 技能的第四个状态 disabled(状态机与消费面细节见 Skills · 颗粒停用)。 两条停用语义一致——停消费面、不碰写入面:停用期间资产照常被编辑、 确认、回滚,重开后即刻生效。状态机严格非幂等(disable 仅 confirmed→disabled,enable 仅 disabled→confirmed),每次变更一条 git 提交; 自动路径绝不写入 disabled——停用永远是管理员的显式动作。

与总开关的第二处一致:停就要停得干净。四个消费面(节点提示注入、 load_skill 广告与按名直取、validator / guard 执行、信封枚举与试跑语料) 共用同一个过滤点,停一个资产等于四处同时熄灭;load_skill 即使被点名直取也拒绝——点名不是提权。

service.py:731 disable skills.py:173 disable

装配清单:plan 与信封对账

trove extensions plan 回答「现在到底哪些资产在动」: 它是信封在运行时装配视角的投影——已停用的资产整封摘除。每个条目给出两侧的存活状态: 预期存活(静态信封的 state)与运行时存活(消费面真读一遍), 再逐条对账(plan.py:170 reconcile_plan)。 对账漂移即退出码 1——静默的成功码会让 CI 把「两份副本已经对不上」当成「一致」。

两侧刻意走不同路径:静态侧读信封的 state 列,运行时侧走消费面真读函数 (不过信封)——如果两边都读信封,对账就成了自己跟自己比。 它是影响面回放的运行时镜像:--impact 回答「装上会改变什么」, plan 回答「现在有哪些在动」。

plan.py:106 expected_liveness plan.py:121 runtime_liveness

包与来源:搬走也带着出处

组织资产可以打成一个包搬走: trove extensions export <name> <dir> 把技能、生效的决策规则、 预设原样落盘(导出物 = 文件逐字节原样,往返保真因此是可断言的性质), 加一份 manifest 与逐文件 sha256(service.py:583 export_pack)。 只收组织资产:内置 / 代码资产不进包——它们随代码分发,打包会制造第二份会漂移的事实源; 会话数据(episodes、用户事实)永不进包。空包直接拒绝生成。

导入是包级校验 + 逐条落 pending:

pack.py:208 PackManifest pack.py:364 read_pack

运行证据:一次 run 留下什么

治理的另一半是可回溯。每次运行收尾在唯一一个写入点落盘 (RunTracer.finish runlog.py:278 finish), 产出与 <run_id>.log 成对的结构化清单 <run_id>.assembly.json (assembly.py:39 ASSEMBLY_SUFFIX):

节回答
blocks进了生成提示词的上下文块逐块逐项的成本与截断(few_shots / rules / … / plan / skill_injections)——「这一问为什么带上了/没带上这条规则」
tools生成阶段工具注册表的每个工具(name / level / roles / lazy / activated / calls)——工具级治理的观测面
verdicts确定性判定(validator_hits / rule_hits / fast_path_hit)——这一步回答过哪些确定性检查

两个设计约束让它可用而不添乱:数据全部来自既有产物 (装配时已算过的同一份 token 计数、state 上的既有字段),不做二次计算; 字段名一次定死、缺失一律降级为空值——观测物绝不允许因为自身形状问题 打断或影响一次查询。工具这一侧还有配套的治理维度: ToolSpec agent_loop.py:199 ToolSpec 上 roles 是可见性门(谁能看到这个工具), level 是纯分类(core / catalog,它从不决定可见性, 只被运行记录消费)——门与分类分开,工具面才有可审计的授权模型。

怎么用

# 提交前 / CI:全量干跑(零 LLM 零网络)
trove validate                     # 退出码 0/1/2 即判定
trove validate --json | jq .       # 机器可读
trove validate --live --datasource demo   # 连真源:枚举缺口 + 真方言编译

# 语料回放(同样零 LLM;--json 顶层 metrics 可直喂 eval_gate)
trove validate --run --datasource demo              # 装前试跑:装/不装 双态消融
trove validate --impact ./my-pack --datasource demo # 影响面回放:四桶差分
trove validate --packs                              # 反作弊:KB 示例 vs 本地 gold 集

# REPL 内
/validate [--live] [--run [--episodes]] [--impact <包|目录>]
# 信封与包(零 LLM 零网络)
trove extensions list --kind skill   # 资产信封:能力从引用推导
trove extensions show <名称>        # 单个信封:挂点 / 能力 / 来源 / unresolved
trove extensions plan                # 运行时装配清单 + 与信封对账(漂移 → 退出码 1)
trove extensions export my-pack ./my-pack-dir
trove extensions import ./my-pack-dir   # 全落 pending;--force 也不覆盖生效规则
# conf/agent.yml —— 扩展面总开关(管理端可配)
agent:
  extensions:
    org_extensions_enabled: true   # false = 停组织扩展消费面(写入面照常)

管理端对应面:技能治理(草稿/确认/档位/正文/历史/回滚)在 技能, 语义模型草稿与回滚在 语义层, 判定规则在 判定规则, 行动模板在 行动安全架构。

取舍

选择放弃了什么为什么值得
单一注册路径(内置也走 register_adapter) 内部捷径的启动便利 两条路迟早分叉,而分叉只会在扩展者那边暴露;函数存在但零调用方的历史教训就来自捷径
草稿一律 pending,自动内容不自证 「AI 起草大多对,自动生效」的吞吐 组织方法论是人工资产;一次错误自动注入污染的是所有后续回答。免确认的资格只发给能确定性验证的档(见语义编译边界的 A/B 门)
版本上界守卫(新版本文件直接拒读) 「尽量读,认得的字段先用」的宽容 旧代码读新文件不报错,只会静默丢字段、再保存时抹掉——这种损坏无声且不可逆
scoped 提交 + 回滚也留提交 裸 git add 的顺手 提交必须对应「这一次资产变更」,历史才可审计;回滚不改写历史,审计线永远 append-only
干跑校验做进 CLI 退出码 纯 UI 的巡检体验 退出码让资产健康成为 CI 可挂的布尔量;--live 补上静态检查证明不了的真方言编译
总开关只停组织层,且停消费不停写入 「一键全停」的简洁语义 产品默认不是组织资产;停用期照常起草、重开后即刻生效,停用是治理动作而不是破坏动作
观测 dump 单点、纯既有产物、失败降级为空值 更丰富的运行中实时观测 观测物不得影响被观测对象;一次 run 一份清单,写入点唯一才不会有半个状态
能力从引用推导(信封),不采信作者声明 「capabilities: 字段想写什么写什么」的自由 声明与表达式会漂:推导让信封、校验、试跑语料读同一份事实;推导不出的引用响亮进 unresolved,不猜
试跑与回放共用退出码,2 = 「这次没验成」 「没跑成 = 当没事发生」的宽松 零 LLM 让语料回放便宜到可以进 CI;静默的 0 会把「没验成」卖成「验过了」——同一个数字不许兼职两种结论
导入全落 pending(--force 也不覆盖生效规则) 「导包即生效」的迁移效率 包是外来资产,最需要过确认门;半导入的包在接收方看起来和完整导入一样,那是最贵的失败
颗粒停用走文件状态 + git 提交 内存里的热停用 治理动作不落盘等于两处事实源;「谁关的、谁开的」必须留在审计史里,所以状态机严格非幂等

优势在哪

继续读

想了解去哪
组织技能的四档(注入 / 按需 / validator / guard)与颗粒停用技能
判定规则文件与条件语言判定内核
自动内容的确认门在记忆子系统里怎么用记忆
行动模板的治理(与技能同门)行动安全架构
本页源码锚点的基准提交:anchor-base: cdf460d GitHub 仓库 文档首页