IK7LI3 技术规格:sql-distributed 步骤 5.5+6 拆出 Task 子 agent¶
关联 issue: IK7LI3 状态: 草案(实施前) 维护: 本文件是 Task 子 agent 拆分 POC 的设计事实来源,执行 agent 按本文件 + todo 推进。
0. 执行约束¶
- 本 POC 不动业务核心质量环节(步骤 7 生成 / 步骤 9 执行 / 步骤 10 验收),只验证步骤 5.5+6 路由层。
- 本分支死走 Task 路径,不做开关:POC 分支只验证 Task 调度方案;inline 路径留在
develop,需要对比时切回去。POC 失败直接弃分支,不需要"秒回退"开关。开关是主分支给用户选长短上下文用的抽象,本 POC 不需要。 - 不引入新抽象:不抽工具池、不改脚本目录结构、不动 evidence-projection 分支。POC 只验证 Task 工具契约本身。
- 不写无意义注释:子 agent 定义文件只写必要约束,不写"为什么这么拆"的解释(理由留本文件)。
- 基于 fixture 测试,不实跑库:本机无分布式 MySQL,baseline.py / table_ctx.py 无法真跑。POC 准备 3 套手工构造的 fixture json(含真实分布式 EXPLAIN 特征),从步骤 5 之后开始测。脚本实际执行验证(需分布式库)排到 follow-up issue。
1. 范围与定位¶
1.1 在拆(POC 范围内)¶
| 项 | 说明 |
|---|---|
| 步骤 5.5 规则匹配 | 规格层保持 skill({name:"rule-match"}) 标准调用;OpenCode runtime 通过 .opencode/agents/sql-dis-router.md 子 agent 内联 rule-match 逻辑后单次 Task 调度 |
| 步骤 6 子技能路由 | 规格层保持 skill({name:"sql-distributed-skill-analysis"}) 标准调用;OpenCode runtime 同上打包进同一次 Task 调度 |
| 新子 agent | .opencode/agents/sql-dis-router.md(runtime 层,规格层不动 SKILL.md) |
| 主控 SKILL.md | 不动:保持两个独立 skill 调用的标准写法;Task 调度细节在 runtime 层(详见 §4) |
1.2 不在拆(明确排除)¶
| 项 | 理由 |
|---|---|
| 步骤 4 baseline / 步骤 5 table_ctx 脚本实际执行 | 本机无分布式 MySQL,跑不了;改用 fixture json 喂入。脚本本身执行正确性验证(需库)排到 follow-up issue |
| 步骤 7 候选方案生成 | 核心质量环节,POC 阶段不碰 |
| evidence-projection 集成 | 已暂存(feat/evidence-projection 分支),本 POC 不依赖 |
| 工具池抽象 | 等 POC 跑通、看清子 agent 真实输入/输出契约后再做 |
| 长短上下文开关 | 本 POC 分支死走 Task 路径,不需要开关;开关留给主分支后续合入时设计 |
2. OpenCode Task 工具契约(v1.18.12 二进制实测)¶
2.1 入参(zod schema 实测,从二进制反解)¶
{
"description": String, // 必填,"A short (3-5 words) description of the task"
"prompt": String, // 必填,"The task for the agent to perform";支持 {file:...} 解析
"subagent_type": String, // 必填,"The type of specialized agent to use";必须在 .opencode/agents/ 注册
"task_id": optional String, // 复用同一子 session;不传则新建。**本 POC 永远传 undefined**
"background": optional Boolean, // 需 OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true;本 POC 不用
"command": optional String // 内部字段,slash command 触发时填;本 POC 不用
}
2.2 出参(Ur() 函数实测 + P1 探针实证)¶
主对话收到的字符串模板(这是全部回写内容,无其他):
<task id="{子 sessionID}" state="completed|error">
<summary>...</summary> # 可选
<task_result|task_error>
{子 agent 最后一次 tool 调用之后的所有 assistant text}
</task_result>
</task>
关键事实:
- 回写不是"最后一条 assistant text part"——而是"最后一次 tool 调用之后产生的所有 text"。如果子 agent 在最后一次 tool 调用之后输出了多段 text(中间无 tool),这些 text 全部回写主对话。
- tool 调用之前产生的 text(包括 narrative、reasoning)不回写。
- tool 调用 / tool 输出本身(如 Bash stdout、Read 内容)不回写主对话。
- 子 agent 若 Read 了文件,Read 内容不会回主对话;必须在最终 text 里显式复述。
- background 模式先回
<task state="running">,子 session 完成后再注入<task state="completed">。本 POC 不用 background。
2.2.1 P1 探针实证(2026-08-10)¶
探针子 agent 按顺序输出了 4 段 text + 1 次 Bash:
| 步骤 | 时序相对 Bash | 主对话收到? |
|---|---|---|
<probe-step-1> |
Bash 之前 | ❌ 未回写 |
Bash echo <probe-tool-output> |
tool 本体 | ❌ 未回写 |
<probe-step-2> |
Bash 之后、与 probe-final 同 turn | ✅ 回写 |
<probe-final> + PROBE_MARK_8823 |
Bash 之后、与 step-2 同 turn | ✅ 回写 |
结论:step-2 和 probe-final 因中间无 tool 调用,被当作同一 text part 整体回写。
2.2.2 对子 agent 设计的约束¶
子 agent 的最终一轮 assistant turn必须只含 JSON,不能在 JSON 前后掺 narrative:
- ❌ 错误:"规则匹配完成,下面是结果:\n
json\n{...}\n"(narrative 会回写主对话) - ✅ 正确:直接输出
{...},前后无任何文字
sql-dis-router 的 system prompt 必须包含此硬规则。
2.3 子 agent 上下文构成¶
子 session 单步 input ≈
agent system prompt(.opencode/agents/sql-dis-router.md 的 frontmatter 之后的正文)
+ 主对话通过 Task.prompt 传入的内容(resolved 后)
+ 子 agent 自己累积的 tool 调用 / 输出历史
子 agent 完全不继承主对话历史。主对话的 SKILL.md、prior turns、tool 结果——除非塞进 Task.prompt——子 agent 一律看不到。
2.4 嵌套限制¶
let h = 0; while (c.parentID) { h++; c = parent; }
if (h >= ($.subagent_depth ?? 1)) fail("Subagent depth limit reached");
默认 subagent_depth = 1:主 → 子 可以,子 → 孙 默认禁止。子 agent 默认权限 task: deny,无法再嵌套。
2.5 权限继承¶
子 agent 默认被强制加上:
- todowrite: deny(除非 agent 配置里显式开)
- task: deny(除非显式开)→ 嵌套禁止的另一道闸
- 主 session 的 permission 列表
本 POC 的 sql-dis-router 显式设 task: deny / edit: deny,只允许 read 和受限 bash。
3. 子 agent 设计:sql-dis-router¶
3.1 注册位置¶
.opencode/agents/sql-dis-router.md(项目级,跟随分支合入;不入 ~/.config/opencode/agents/)
3.2 Frontmatter¶
---
description: sql-distributed 步骤 5.5+6 路由子 agent,做规则匹配与子技能路由
mode: subagent
permission:
read: allow
bash:
"python3 *": allow
"*": ask
edit: deny
task: deny
---
设计要点:
- mode: subagent 必填
- bash 只放行 python3 *(子 agent 可能要跑 sqlglot 解析),其他 ask
- task: deny 显式禁嵌套
- edit: deny 防止子 agent 误改业务文件
- 不设 model —— 继承主对话模型(保证路由质量与 inline 路径可比)
3.3 System prompt(frontmatter 之后的正文)¶
子 agent system prompt 采用 B 方案(嵌入而非 Read):把 rule-match 判定流程 + skill-analysis 路由维度直接写进 system prompt,子 agent 只 Read 一份 pending_rules_1.md 数据文件,不 Read 任何 SKILL.md。理由:子 agent 单步 input 是累积上下文,Read 两本 SKILL.md 会让子 session 单步峰值轻易突破 5k 目标。
核心要求:
- 接收 4 个变量:
USER_SQL/DISTRIBUTED_CONTEXT(JSON 字符串)/ROUND_DIR/PENDING_RULES_PATH(默认skills/rule-match/rules/pending_rules_1.md) - 执行步骤:
- Read 一次
PENDING_RULES_PATH,得到候选规则全集 - 前置门禁:扫描
DISTRIBUTED_CONTEXT.tables.shard_key_source与missing_info,若任一表分片键缺失 → 立即降级返回(selected_skills=[]/missing_context非空),跳过后续 - 规则匹配:按 system prompt 内嵌的 rule-match 判定逻辑对
USER_SQL逐条判定 - 子技能路由:按 system prompt 内嵌的 skill-analysis 路由维度综合规则命中 +
DISTRIBUTED_CONTEXT信号 - 输出 JSON:自检
json.loads通过后输出,禁止 narrative - 输出契约(严格 JSON,禁止 narrative):
{
"matched_rule_entries": ["<RULE_ID> 规则条目原文,含 matcher 标注"],
"selected_skills": ["sql-distributed-dml-rewrite"],
"routing_reason": "必须引用 distributed_context 中的具体信号(shard_key_source / operators / missing_info)",
"missing_context": ["信息不足时列缺失项;非空则主对话必须补充后再调度"]
}
- 硬规则:
matched_rule_entries必须是 pending_rules 原文(含## <RULE_ID>标题、matcher 标注),禁止摘要 / 改写routing_reason必须引用DISTRIBUTED_CONTEXT字段名 + 取值;不得泛泛"信号不足"- 信息不足以路由时,
missing_context非空,selected_skills留空数组 - 禁止 narrative,禁止 Read 全量 baseline.json / table_ctx.json
4. 三层一致性:spec / runtime / design¶
本节澄清规格层(SKILL.md)、runtime 层(OpenCode 子 agent)、设计层(本文件)的分工。POC 验证的是"在 OpenCode runtime 下,把两个 skill 的逻辑打包调度是否可行",不是"把 SKILL.md 改成 Task 调度"。
4.1 规格层:SKILL.md 保持框架无关¶
skills/sql-dis-opt/sql-distributed/SKILL.md 步骤 5.5 / 6 维持两个独立 skill 调用的标准写法:
### 步骤 5.5:规则匹配
skill({ name: "rule-match" })
### 步骤 6:选择子技能
skill({ name: "sql-distributed-skill-analysis" })
- 规格层只描述"做什么",不掺 runtime 细节(不写
Task()/subagent_type/OpenCode字样) - 任何 runtime(Claude Code、其他 agent framework、纯 inline 执行)都能按这两个 skill 名实现自己的调度
- 与步骤 4 / 5 / 7 / 9 配套的"引用替代内容"约定(baseline.json / table_ctx.json 等数据落盘,message 只保留摘要)一致
4.2 Runtime 层:OpenCode 子 agent 把两个 skill 打包调度¶
.opencode/agents/sql-dis-router.md 是 OpenCode runtime 的实现优化,把规格层的两个 skill 逻辑打包进单次 Task 调度:
- 为什么打包:OpenCode 调用
Task(subagent_type=...)会创建独立 session;若按规格层逐个 skill 调度,需要两次 Task 往返(两次 system prompt 加载、两次出入主对话),ROI 低 - 怎么打包:把
skills/rule-match/SKILL.md的判定流程 +skills/sql-dis-opt/sql-distributed-skill-analysis/SKILL.md的路由维度直接嵌入sql-dis-router.md的 system prompt;子 agent 只 Read 一份pending_rules_1.md数据文件,不再 Read 任何 SKILL.md - 不破坏规格层:runtime 实现可以内联 skill 知识,但不能修改 SKILL.md 本身;当 rule-match / skill-analysis 的 SKILL.md 更新时,runtime 子 agent 的嵌入段需要同步(CI 检查项,见 §4.4)
主对话在步骤 5.5 处通过 Task(subagent_type="sql-dis-router", prompt=...) 调起子 agent,子 agent 返回单次 JSON,主对话消费后同时拿到 matched_rule_entries + selected_skills,等价于 inline 执行两本 SKILL.md 的结果。
4.3 其他 runtime 的等价实现¶
同一份 SKILL.md 在不同 runtime 下的等价实现:
| runtime | 步骤 5.5 + 6 的实现 |
|---|---|
| OpenCode(本 POC) | Task(subagent_type="sql-dis-router") 单次调度,子 agent 内联两本 SKILL.md 知识 |
| Claude Code | 按 SKILL.md 字面执行两次独立 skill({name}) 调用,inline 累加上下文 |
| 无 Task 工具的 runtime | 主对话 Read 两本 SKILL.md 后自行做规则匹配 + 路由 |
三种实现都遵守同一份规格(输入 USER_SQL + DISTRIBUTED_CONTEXT + pending_rules;输出 matched_rule_entries + selected_skills + routing_reason + missing_context)。POC 只验证 OpenCode 这一种实现路径。
4.4 同步约束¶
runtime 子 agent 嵌入了两个 skill 的逻辑,存在漂移风险:
skills/rule-match/SKILL.md修改了判定流程,但.opencode/agents/sql-dis-router.md未同步 → runtime 行为与规格层不一致skills/sql-dis-opt/sql-distributed-skill-analysis/SKILL.md修改了路由维度,同理
合入 develop 前需要:
- 在 sql-dis-router.md 顶部注明"嵌入自 rule-match@
/ sql-distributed-skill-analysis@ " - CI 加 diff 检查:两本 SKILL.md 改动时,提醒同步 sql-dis-router.md
4.5 回退方式¶
POC 分支失败 → git checkout develop:
- SKILL.md 在 develop 上仍是两个独立 skill 调用(规格层未动)
- runtime 子 agent 文件留在本分支,不合入
- 主对话恢复 inline Read SKILL.md 的执行方式
5. 步骤 4+5 fixture 方案(不实跑库)¶
5.1 问题¶
本机无分布式 MySQL,baseline.py / table_ctx.py 无法真跑。但 POC 核心验证对象(Task 工具契约 + 子 agent 路由质量)不依赖脚本执行,只需要合理输入。
5.2 方案:手工 fixture json + 预派生 facts¶
为每条用例(B1 / B2 / B3)准备:
tests/IK7LI3/fixtures/<case_id>/
├── baseline.json # 手工构造,含 baseline_mode / execution_time / fallback_reason
├── table_ctx.json # 手工构造,含 distributed_context 完整字段
└── distributed_context_facts.json # 从 table_ctx.json 派生的紧凑 facts,作为 Task.prompt 输入
fixture 来源:
- 优先从历史 case 库 skills/sql-dis-opt/sql-distributed/workspace/ 找真实跑过的 json
- 找不到则参考真实分布式 EXPLAIN 输出格式手工构造(必须含 Shuffle / Gather 等真实算子名,不得编造)
- B3 用例的 distributed_context.missing_info 必须含 shard_key 项
5.3 测试入口¶
POC 测试从步骤 5 之后开始:
- 把 fixture json 复制到
$ROUND_DIR/ - 主对话读
distributed_context_facts.json作为DISTRIBUTED_CONTEXT - 发起 Task 调度,收路由结果
- 不进入步骤 7 生成(POC 不验证生成质量)
5.4 与 evidence-projection 的关系¶
本方案是 evidence-projection 的最小子集(手工派生 facts,不下沉到通用 schema)。POC 完成后,若 evidence-projection 合入 develop,派生逻辑可由脚本 stdout 投影替代,主对话消费逻辑不变。
5.5 改动位置¶
- 新增
tests/IK7LI3/fixtures/目录及 fixture 文件 - 不改
skills/sql-dis-opt/sql-distributed/scripts/下任何脚本 - 主控 SKILL.md 不动步骤 4 / 5(POC 测试入口在测试脚本里,不在 SKILL.md 流程里)
6. 验证用例¶
6.1 用例选取¶
| 用例 | fixture 特征 | 期望路由结果 |
|---|---|---|
| B1 | 纯 DML 问题:单表 UPDATE,WHERE 含分片键,EXPLAIN 含 Shuffle;无索引可优化点 |
selected_skills = [dml-rewrite],routing_reason 引用 Shuffle 算子 |
| B2 | 混合问题:两表 JOIN,分片键不命中,局部索引缺失 | selected_skills = [dml-rewrite, ddl-index],routing_reason 区分两类信号 |
| B3 | 信号不足:distributed_context.missing_info 含 shard_key 缺失 |
missing_context 非空,selected_skills = [] |
fixture SQL 与 json 优先从历史 case 库 skills/sql-dis-opt/sql-distributed/workspace/ 找真实跑过的样本;不足则参考真实分布式 EXPLAIN 输出格式手工构造。
6.2 跑法¶
每条用例只跑 Task 路径(POC 分支死走 Task)。
inline 路径的基线数据复用 issue 草稿引用的 2026-08-06 实测(21 步流程,峰值 19.6k,总 75.8k),不重复跑——重复跑也会因没库失败。
6.3 计量字段¶
每条用例产出一份 JSON 报告,落在 $ROUND_DIR/dispatch_poc_report.json:
{
"case_id": "B1",
"main_session": {
"peak_input_tokens": <int>,
"total_input_tokens": <int>,
"api_calls": <int>
},
"sub_session": {
"peak_input_tokens": <int>,
"total_input_tokens": <int>,
"api_calls": <int>
},
"routing_result": {
"selected_skills": [...],
"matched_rule_count": <int>,
"missing_context_count": <int>
},
"quality_checks": {
"matched_rule_entries_intact": true | false,
"routing_reason_references_distributed_context": true | false,
"missing_context_triggers_main_retry": true | false
}
}
数据来源:opencode session jsonl 日志(路径在 P1 探针阶段确认),用 jq 抽 usage.input_tokens 求最大值与和。
7. 退出标准¶
- ✅ G1:3 条用例 Task 路径下,子 session 单步峰值 input < 5k tokens
- ✅ G2:3 条用例 Task 路径下,主对话峰值 input ≤ 16k tokens(基线:19.6k,来自 2026-08-06 实测)
- ✅ 路由合理:3 条用例 Task 路径的
selected_skills符合 6.1 期望(B1=[dml-rewrite],B2 含两个,B3=[])。inline 对比留到 follow-up(需分布式库环境) - ✅ 规则原文:
matched_rule_entries字段值是 pending_rules 原文(人工核对,不是摘要) - ✅ 降级正确:B3 用例
missing_context非空,主对话补充字段后重试 Task 能正确路由
任一条不达标,停在 POC 阶段,写复盘到本文件 §8,不合入 develop。
8. 风险登记与复盘位¶
实施过程中发现的新风险 / 偏差,追加到本节。
| 风险 / 偏差 | 触发条件 | 处理 | 状态 |
|---|---|---|---|
| Task 出参理解偏差 | P1 探针发现:不是"最后一条 assistant text",而是"最后一次 tool 调用之后的所有 text" | spec §2.2 已更新;sql-dis-router system prompt 加硬规则"最终 turn 只含 JSON,无 narrative" | ✅ 已闭环 |
| jsonl 路径未实测确认 | opencode session jsonl 实际落点未在本机验证 | P4 阶段实测;候选 ~/.local/share/opencode/sessions/ / ~/.config/opencode/sessions/ / 项目内 .opencode/sessions/ |
⏳ 待验证 |
| B3 cherry-pick missing_info | P2.2 B3 实测:子 agent 引用"索引作用域 unknown"做路由,但忽略"分片键缺失"项未触发降级 | sql-dis-router system prompt 加"前置门禁"段(步骤 4):扫描 tables.shard_key_source=missing 或 missing_info 含"分片键缺失" → 强制降级;加反例;硬规则从 7 条扩到 10 条 | ✅ 已修,待重测 |
| B3 JSON 结构疑似不合法 | P2.2 B3 返回 selected_skills: "a", "b" 形式(缺 []),需用户确认原文 |
sql-dis-router system prompt 强化"selected_skills/missing_context 必须是 JSON 数组"+"输出前自检 json.loads"硬规则 | ✅ 已修,待重测 |
| B1 fixture 信号不足 | P2.2 B1 实测:原 UPDATE+点更新无 DML 改写空间,子 agent 按规则返回两者(符合 skill-analysis 信号不足 fallback),与期望 [dml-rewrite] 不符 |
B1 fixture 改为带 dependent subquery + TDSQLSetRewrite 算子,让 DML 改写信号明确 | ✅ 已修,待重测 |
| agent frontmatter 断裂 | 2026-08-12 发现 sql-dis-router.md(连开头 --- 都缺)与 sql-dis-generator.md 的闭合 --- 被误替换成 ## task: deny,mode: subagent 落进正文,两个 agent 无法注册为 subagent、task: deny 未生效 |
已修复现有 agent;新增 sql-dis-dml-advisor 时沿用 --- 开闭 + 解析自检约束;todo 对应卡片补自检 |
✅ 已修 |
| 步骤 7 拆分粒度选错 | 2026-08-12 复盘:dml-rewrite / ddl-index 不应耦合进单个 generator,也不应把 dml 留在主 agent inline;两个 specialist 应平级并行分析,generation 只汇总 | §10 整节改为 sql-dis-dml-advisor + sql-dis-index-advisor 并行,主 agent generation 汇总;契约去重见 §10.9 |
⏳ 待实施(todo P7-P12) |
| effective token 口径误算 | 2026-08-12 P10 实测:extract_token_usage.py v1 把 tokens.input 当作"该 call 总输入",忽略了 tokens.cache.read;实际上 cache_read 仍要被 LLM 重处理,是真实上下文组成。v1 报告 G1′/G2′ raw 全部通过、G2′=16,290 是严重低估 |
修脚本 v2:effective = input + cache_read;重抽 P10 数据:主对话 peak_eff=82,933(v1 报 16,290)、4 子 agent 中 3 个超 20k 阈值;tech-spec §10.6 退出标准按 effective 重判 |
✅ 已修脚本;❌ 实测全部不达标,POC 决策见 §10.6 |
| B 方案嵌入的代价被低估 | P10 实测:子 agent 嵌入整本 SKILL.md 后,即使不 Read 任何 SKILL.md 文件,system prompt 本身每轮 cache_read 累积,把 effective 推到 25-36k(rule-match 25,815 / dml-advisor 26,857 / index-advisor 35,911)。cache_read 命中只省钱,不省 LLM 看到的上下文规模 | 三个修正方向:(A) Task 输出落盘 + 引用替代主对话内联 JSON;(B) 总控 SKILL.md 自身 11.9k 瘦身;(C) specialist 输出极简化(禁内联规则原文) | ⏳ 待选向 |
| 主对话上下文反向膨胀 | P10 实测:拆 4 子 agent 后,主对话峰值 effective=82,933,是拆前 baseline 19.6k 的 4.2 倍。根因:4 个 Task 返回的 JSON(matched_rule_entries 全文 + dispatch + 两 specialist 候选)累积进主对话,每轮 cache_read 重发 SKILL.md 自身 | 主对话不内联 Task 返回 JSON 全文,落 $ROUND_DIR/advisor_outputs/*.json + message 只保留 50 字摘要 + 路径(即"引用替代内容"路径,原 tech-spec §4.1 提及但未落到主对话消费侧) |
✅ A 方向已落地(ack + summary 投影) |
| Task prompt 内联 SQL/DC 仍占主对话 | P12 实测:5 份 Task.prompt 各自内联 USER_SQL (2-4k) + DISTRIBUTED_CONTEXT (~1k),写入主 history 后每轮 cache_read 累积重读,合计贡献 ~11k | §11 路径化输入契约(2026-08-17 落地):主对话步骤 3.6 统一写 $ROUND_DIR/user.sql,Task.prompt 只传 USER_SQL_PATH / TABLE_CTX_PATH 等路径引用 |
✅ 已落地,待实测复核 |
9. 关联文档¶
docs/project/current/skill-task-dispatch-feasibility.md(前置可行性分析)docs/project/current/naming-convention-cleanup.md(命名规范)docs/project/current/IK7LI3-todo.md(实施 todo,本 spec 拆出的有序步骤)
10. 第二档扩展:步骤 7 拆分(平级 specialist Task + 主 agent 汇总)¶
第一档(5.5+6 → sql-dis-router)POC 验证通过后追加本节。 2026-08-12 修订:原设计(步骤 7 两个子 skill 打包进单个
sql-dis-generator,后改为 7a inline + 7b 串行)均废弃。当前设计是两个平级 specialist Task 并行分析,主 agent 只负责汇总、校验和执行控制。
10.0 设计判据:拆分轴是"吞多大证据",不是"是不是子 skill"¶
由 §2.3 的上下文构成推出收益公式:Task 的成本是「子 session 独立 system prompt + prompt 序列化 + 子 agent 自己的读文件累积」,收益是「这些 token 不进主对话」。步骤 7 的 dml 与 ddl 两个 specialist 都是输入大、输出小的分析任务,应由平级 Task 隔离;主 agent 保留工作流编排、结果汇总、校验和轮次控制,不再内联任何一个 specialist 的规则文本。
各单元实际体量(chars ÷ 2.5,与 §3 token 口径一致):
| 单元 | ≈tokens | 性质 |
|---|---|---|
sql-distributed(总控) |
11.9k | 编排状态机,常驻主对话 |
sql-distributed-optimization-generation |
5.3k | 产出 optimized_sql 的契约与优先级 |
sql-distributed-optimization-review |
4.0k | 判定函数,吞 explain 对比 |
sql-distributed-ddl-index |
3.6k | 规则文本 + 需吞 table_info 全量 DDL / column_selectivity |
rule-match + pending_rules_1.md |
2.7k + 12.8k | 判定函数,已由 sql-dis-router 隔离 |
sql-distributed-dml-rewrite |
2.2k | 纯规则文本,输入只需 USER_SQL + 紧凑 facts |
sql-distributed-skill-analysis |
2.1k | 判定函数,输入小 |
废弃 sql-dis-generator 单 agent 的三条理由:
- 两个 specialist 应独立承担各自分析:dml-rewrite / ddl-index 虽然在规格层表现为规则文本,但 runtime 必须把它们分别封装为有明确输入输出契约的 specialist Task;不能把两套规则再耦合进一个 generator agent。
- 契约分裂:
generation要求 10 个输出字段(含need_optimization/route_decision/performance_bottleneck/summary),sql-dis-generator只输出 6 个,且optimization_strategy(数组 vs 字符串)、estimated_improvement(文本 vs 0-1 数字)类型冲突。同一步骤两份互斥契约。 - 主 agent 仍掌握最终交付物:specialist 只返回候选与证据,不直接决定最终结果。主 agent 的 generation 阶段消费两个 specialist 的小结果,负责合并冲突、做 SQL/schema/precheck 校验和决定是否进入执行。
并行方案现在采用:dml specialist 与 ddl-index specialist 都基于同一份原始 SQL、EXPLAIN、distributed_context 并行分析。索引 specialist 不假设 dml 已经发生;它返回“基于原 SQL 的索引候选及适用条件”,主 agent generation 汇总时必须重新判断:若最终采用的 dml 改写已把输入规模压小或改变访问路径,则撤销/降级该索引候选。这样并行的是分析,不是未经汇总就直接执行两个结果。
OpenCode 的并行调度需要单独验证:两个 Task 应在同一轮由主 agent 发起,优先使用 runtime 支持的并发调用;不依赖未开启的 background 实验开关。若 runtime 实际串行执行,仍保持契约正确,但记录为性能偏差。
10.1 范围与不在范围¶
| 在第二档范围内 | 不在范围内 |
|---|---|
步骤 7 dml specialist:新子 agent .opencode/agents/sql-dis-dml-advisor.md |
步骤 10 optimization-review 拆子 agent(记 backlog) |
步骤 7 ddl-index specialist:.opencode/agents/sql-dis-index-advisor.md |
总控 SKILL.md 11.9k 瘦身(记 backlog) |
| 主 agent generation 汇总两个 specialist 结果 | Task 并行能力探针(本档必须验证) |
契约去重:删 generation 的 route_decision(§10.8) |
evidence-projection 集成 |
删除 .opencode/agents/sql-dis-generator.md,删除 OpenCode 子 skill 镜像 |
规格层"框架无关"原则本身 |
10.2 步骤 6:选择 specialist Task¶
步骤 6 不再把 skills 当成后续 inline 加载清单,而是把它转换成 specialist Task 的 dispatch 计划。两个布尔字段分别控制两个平级 agent:
skill-analysis 输出契约调整(加字段,不删字段,保持向后兼容):
{
"skills": ["sql-distributed-dml-rewrite", "sql-distributed-ddl-index"],
"needs_dml_rewrite": true,
"needs_index_evidence": true,
"reason": "必须引用 operators / shard_key_source / index_scope / missing_info 的具体取值"
}
needs_dml_rewrite=true⇔skills含sql-distributed-dml-rewrite⇔ 发起sql-dis-dml-advisorneeds_index_evidence=true⇔skills含sql-distributed-ddl-index⇔ 发起sql-dis-index-advisor- 两个字段都为
true时,两个 Task 必须基于同一份输入并行发起 - 信号不足时两个字段都默认
true;关键分片证据缺失时两个字段都为false并降级
10.3 步骤 7:两个 specialist Task 并行¶
步骤 7 的 runtime 分成两个独立 specialist:
| specialist | 输入重点 | 输出重点 |
|---|---|---|
sql-dis-dml-advisor |
USER_SQL、DISTRIBUTED_CONTEXT、MATCHED_RULE_ENTRIES |
DML 改写候选 SQL、改写策略、语义风险 |
sql-dis-index-advisor |
USER_SQL、DISTRIBUTED_CONTEXT、TABLE_CTX_PATH |
索引 DDL 候选、命中路径、作用域、冗余 |
二者均基于原始 SQL并行分析,不互相读取对方 session。主 agent 后续加载 generation 规则,消费两个结果并生成最终候选:
- 发起需要的 specialist Task;两个都需要时同一轮并行发起
- 收集并解析两个 JSON 结果
- 将
dml_candidate_sql、索引候选和各自风险交给sql-distributed-optimization-generation - generation 负责合并冲突、判断 dml 改写是否使索引候选失效、产出最终
optimized_sql+create_index_ddl - 主 agent 执行 sqlglot、
precheck.py、schema 和风险校验
关键约束:并行只发生在 specialist 分析阶段,不代表两个结果未经 generation 汇总就直接执行。若 dml 改写后索引候选不再必要,最终生成必须撤销该 DDL。
10.4 子 agent 设计:sql-dis-dml-advisor¶
10.4.1 注册位置与 Frontmatter¶
.opencode/agents/sql-dis-dml-advisor.md(项目级,跟随分支合入)
---
description: OpenCode runtime 子 agent:步骤 7 DML specialist,基于原始 SQL 与分布式上下文生成结构改写候选
mode: subagent
permission:
read: allow
bash:
"python3 *": allow
"*": ask
edit: deny
task: deny
---
10.4.2 System prompt(嵌入而非 Read)¶
嵌入 sql-distributed-dml-rewrite 的改写规则(整段复制,不摘要),不 Read 任何 SKILL.md。该 agent 不负责最终 generation 编排,只返回 DML specialist 候选。
10.4.3 输入变量(由 Task.prompt 注入)¶
USER_SQL:原始 SQLDISTRIBUTED_CONTEXT:紧凑 facts JSONMATCHED_RULE_ENTRIES:步骤 5.5 命中的完整规则原文 JSON 数组ROUND_DIR:仅用于日志,不写文件
10.4.4 输出契约(严格 JSON,禁止 narrative)¶
{
"dml_candidate_sql": "候选改写 SQL;无安全改写时与 USER_SQL 一致",
"optimization_strategy": "应用的 RULE_ID、结构改写和分布式收益依据",
"risk_assessment": "语义、schema、跨分片物化风险",
"estimated_improvement": "基于算子/扫描/数据移动的定性或定量估计",
"missing_context": []
}
dml_candidate_sql必须是可解析的单条 SQL;不能输出最终create_index_ddloptimization_strategy必须引用实际MATCHED_RULE_ENTRIES中的 RULE_ID;无命中时不得伪造- 不能因 SQL 更短就认定优化成立;必须说明 Shuffle / Gather / 扫描 / 重复计算等收益依据
- 高风险改写必须明确标注,不得把 specialist 候选直接视为最终方案
- 最终 turn 只能包含一个合法 JSON 对象
10.5 子 agent 设计:sql-dis-index-advisor¶
10.5.1 注册位置与 Frontmatter¶
.opencode/agents/sql-dis-index-advisor.md(项目级,跟随分支合入)
---
description: OpenCode runtime 子 agent:步骤 7 索引 specialist,吞 table_ctx 的 table_info 与 column_selectivity,输出索引候选与作用域判定
mode: subagent
permission:
read: allow
bash:
"python3 *": allow
"*": ask
edit: deny
task: deny
---
frontmatter 必须以 --- 开闭。2026-08-12 发现 sql-dis-router.md / sql-dis-generator.md 的闭合 --- 被误替换成 ## task: deny,导致 mode: subagent 落进正文、两个 agent 无法注册为 subagent、task: deny 未生效。新增或修改 agent 后必须跑 frontmatter 解析自检。
10.5.2 System prompt(嵌入而非 Read)¶
嵌入 sql-distributed-ddl-index 的索引规则(整段复制,不摘要),不 Read 任何 SKILL.md。不依赖 dml specialist 的结果;本 agent 基于原始 SQL 返回候选及适用条件。
10.5.3 输入变量(由 Task.prompt 注入)¶
USER_SQL:原始 SQL;并行分析阶段唯一的 SQL 输入DISTRIBUTED_CONTEXT:紧凑 facts JSON(tables[*].shard_key_source/shard_keys/index_scope/operators/missing_info)TABLE_CTX_PATH:$ROUND_DIR/table_ctx.json,子 agent 从中只取table_info+column_selectivityROUND_DIR:仅用于日志,不写文件
不注入 MATCHED_RULE_ENTRIES 与 SELECTED_SKILLS;主 agent 在 generation 汇总阶段负责判断 dml 改写是否使索引候选失效。
10.5.4 执行步骤¶
- 前置门禁:扫描
DISTRIBUTED_CONTEXT.tables与missing_info,任一表shard_key_source=missing或含"分片键缺失" → 立即降级返回(create_index_ddl=[]、missing_context非空) - Read
TABLE_CTX_PATH:只取table_info(含现有索引与 DDL)+column_selectivity - 原始 SQL 必要性判定:若原始 SQL 的主要瓶颈是残留跨分片 Shuffle / Gather,或分片键未等值命中导致局部索引不能减少分片数 → 返回
create_index_ddl=[];generation 汇总时还必须复核 dml 改写后的必要性 - 作用域判定(强制前置):按
index_scope→ DDL 关键字 →local_unknown的优先级判定 - 冗余评估:对照
table_info现有索引,检查重复 / 前缀变体 / 旧索引是否变冗余 - 输出 JSON:
json.loads自检通过后输出,禁止 narrative
10.5.5 输出契约(严格 JSON,禁止 narrative)¶
{
"create_index_ddl": ["CREATE INDEX llm_idx_t_x_eq ON t(x)"],
"index_scope_declaration": "每条索引的作用域:local / global / local_unknown + 依据",
"hit_path": "每条索引预期命中的过滤 / join / 排序路径",
"redundancy_check": "与现有索引的重叠评估;不建议加索引时写明理由",
"missing_context": []
}
- 契约只覆盖索引侧,不含
optimized_sql/optimization_strategy/estimated_improvement/risk_assessment——这些由主 agent 的 generation 汇总阶段产出,从结构上消除 specialist 与最终生成契约的冲突 create_index_ddl:JSON 数组,0 条时为[];索引名必须llm_idx_前缀;不含DROP INDEX(回滚由 execute.py 负责)index_scope_declaration/hit_path/redundancy_check:JSON 字符串,中文;主 agent 把三者并入最终risk_assessmentmissing_context:JSON 数组,门禁降级时必须非空
10.5.6 硬规则¶
- 最终 turn 只含合法 JSON(依据 §2.2 回写契约):输出前用
python3 -c "import json,sys; json.loads(sys.stdin.read())"自检 - 只 Read 一次:只读
TABLE_CTX_PATH,且只取table_info+column_selectivity;禁止 Read 任何 SKILL.md 或baseline.json全量 - 前置门禁不可绕过,且不得 cherry-pick
missing_info(逐项判定,不得只引用不阻塞项) - 不改 SQL:不输出
optimized_sql,不对USER_SQL做任何改写 - 分片键优先:不得跳过"分片键等值过滤 / Join 对齐"判断直接建索引;SQL 仍跨多分片时不得把局部索引包装成主要收益
- 不伪造证据:
table_info缺字段时写入missing_context,不猜测 - 不写文件、不嵌套 Task
- 中文:三个说明字段使用中文
10.6 主控 SKILL.md 步骤 7 改动(三层一致性)¶
10.6.1 规格层(框架无关,不掺 Task 字样)¶
### 步骤 7:生成候选方案
skill({ name: "sql-distributed-optimization-generation" })
skill({ name: "sql-distributed-dml-rewrite" }) # 若步骤 6 选中
skill({ name: "sql-distributed-ddl-index" }) # 若步骤 6 判定需要索引取证
10.6.2 Runtime 层(OpenCode)¶
若步骤 6 返回 needs_dml_rewrite=true,发起 dml specialist;若返回 needs_index_evidence=true,发起 index specialist。两个都为 true 时必须在同一轮并行发起:
Task(
description="sql-distributed DML 改写分析",
subagent_type="sql-dis-dml-advisor",
prompt="USER_SQL=<...>\nDISTRIBUTED_CONTEXT=<紧凑 facts JSON>\nMATCHED_RULE_ENTRIES=<规则原文 JSON>\nROUND_DIR=$ROUND_DIR"
)
Task(
description="sql-distributed 索引分析",
subagent_type="sql-dis-index-advisor",
prompt="USER_SQL=<...>\nDISTRIBUTED_CONTEXT=<紧凑 facts JSON>\nTABLE_CTX_PATH=$ROUND_DIR/table_ctx.json\nROUND_DIR=$ROUND_DIR"
)
主对话收齐 specialist <task> 返回后:
- 分别解析 JSON,校验 specialist 字段完整性
- 将 dml specialist 的
dml_candidate_sql与 index specialist 的索引候选、作用域、命中路径、冗余说明放入 generation 上下文 - generation 汇总两个结果,判断 dml 改写是否使索引候选失效,产出最终
optimized_sql/create_index_ddl/risk_assessment - 主对话校验最终 JSON、sqlglot、
precheck.py、schema 和索引 DDL;失败时基于 specialist 结果重试或降级
10.6.3 其他 runtime 的等价实现¶
| runtime | 步骤 7 的实现 |
|---|---|
| OpenCode(本 POC) | 条件并行 Task(sql-dis-dml-advisor) + Task(sql-dis-index-advisor),随后主 agent inline generation 汇总 |
| Claude Code | 按 SKILL.md 字面执行三次独立 skill({name}) 调用,inline 累加上下文 |
| 无 Task 工具的 runtime | 主对话 Read 三本 SKILL.md 后自行 inline 完成 |
三种实现遵守同一份规格:specialist 输入 USER_SQL + distributed_context,dml 额外接收 matched_rule_entries,index 额外接收 table_ctx;主 generation 汇总 specialist 结果并输出 optimized_sql + create_index_ddl + 风险说明。
10.7 验证用例(复用第一档 fixture)¶
| 用例 | 步骤 6 dispatch | DML specialist | Index specialist |
|---|---|---|---|
| B1 | dml=true, index=false | 发起,返回 DML 候选 | 不发起 |
| B2 | dml=true, index=true | 与 index Task 并行发起 | 与 dml Task 并行发起 |
| B3 | 两者=false(门禁) | 不发起,返回缺口 | 不发起,返回缺口 |
B1 额外验证点:needs_index_evidence=false 时主对话不发起 index Task,也不 Read table_ctx.json 的 table_info / column_selectivity。
10.8 退出标准(第二档,2026-08-12 P10 实测后再次校准)¶
口径变更(2026-08-12):所有 token 数采用 effective = input + cache_read(cache_read 仍要被 LLM 重处理,是真实上下文组成)。v1 脚本只取 input 字段严重低估了上下文规模,已修脚本(tests/IK7LI3/extract_token_usage.py v2)。
原阈值被证伪情况:
- 原 G1
< 5k:第一档已证伪(input_tokens是累积上下文而非增量,见 feasibility-evidence §5.2) - G1′/G1″/G2′ ≤ 20k / ≤ 3 calls:P10 实测再次证伪——4 子 agent 中 3 个超 20k effective,dml-advisor 6 calls、index-advisor 5 calls 都超 3,主对话 effective 峰值 82,933 是原 baseline 19.6k 的 4.2 倍
当前阈值(待重新校准):
| 标准 | 阈值 | P10 实测 | 状态 |
|---|---|---|---|
| G1′ 子 session effective 峰值 | ≤ 20k | rule-match 25,815 / skill-analysis 14,685 / dml-advisor 26,857 / index-advisor 35,911 | ❌ 3/4 不达标 |
| G1″ 子 session api_calls | ≤ 3 | rule-match 2 / skill-analysis 1 / dml-advisor 6 / index-advisor 5 | ❌ 2/4 不达标 |
| G2′ 主对话 effective 峰值 | ≤ 20k | 82,933 | ❌ 严重不达标(4.2 倍) |
| G3 dispatch 行为 | B1=dml / B2=并行 / B3=不发起 | B2 实测两 specialist 同轮发起 ✅ | ✅ B2 通过;B1/B3 待测 |
| G4 DDL 合规 | llm_idx_ 前缀、无 DROP INDEX、过 precheck |
B2 无 DDL(index-advisor 克制) | ✅(B2 不构成压力测试) |
| G5 作用域声明 | local / global / local_unknown | 三表 local_unknown 已写入 risk_assessment | ✅ |
| G6 sqlglot + precheck | 通过 | sqlglot ✅、precheck 1 条 warning(派生表扁平化预期假阳性,sqlglot 复核过) | ✅ |
| G7 risk 三方汇总 | 含 dml+index+generation 三方风险 | generation 已合并 dml + index 作用域风险 | ✅ |
P10 综合判定:核心质量目标(G3-G7)全过,但 token 目标(G1′/G1″/G2′)全部不达标。
决策选项(待用户拍板):
| 选项 | 描述 | 影响 |
|---|---|---|
| A | 实施"引用替代内容"路径:specialist 输出落 $ROUND_DIR/advisor_outputs/*.json,主对话 message 只保留 50 字摘要 + 路径 |
预计主对话 -30k+;不改子 agent |
| B | 总控 SKILL.md 自身 11.9k 瘦身,下沉"常见错误"等到 references/ |
主对话 -3-5k;改一处 |
| C | specialist 输出极简化:禁内联规则原文、RULE_ID 替代条目复述 | specialist -5k 每个 |
| 终止 | POC 失败,回退 develop;4 子 agent 不合入 | 工作留分支 |
10.9 契约去重清单(必须一次改完,否则拆完仍是两个真理源)¶
| 重复项 | 现状 | 处理 |
|---|---|---|
| 瓶颈分类 | 步骤 6 skill-analysis.skills + 步骤 7 generation.route_decision |
删 generation 输出契约中的 route_decision,分类只在步骤 6 判一次 |
| 步骤 7 输出契约 | generation 最终输出与两个 specialist 输出混杂 |
generation 保持最终契约;dml/index 各自只输出 specialist 契约,由主 agent 汇总 |
| 索引规则文本 | ddl-index/SKILL.md + sql-dis-generator.md §7 |
转移到 index-advisor,顶部标注"嵌入自 ddl-index@\<commit>",CI 检查同步 |
| DML 规则文本 | dml-rewrite/SKILL.md + 旧 generator §6 |
转移到 sql-dis-dml-advisor,主 agent 不再 Read dml 规则 |
11. 第三档:SQL 输入契约路径化(2026-08-17)¶
前置:A 方向(落盘 + ack)与 D 方向(summary 投影)已落地;P12 实测主对话峰值仍 ~52k,其中 5 份 Task.prompt 内联 USER_SQL + DISTRIBUTED_CONTEXT 合计贡献 ~11k。服务器运行环境的输入本来就是
.sql文件路径,内联注入属于双重浪费(主会话 + 子 session 各一份)。
11.1 设计¶
双模输入:
- 优先
SQL_FILE(.sql文件路径,服务器标准) - 降级
USER_SQL(内联文本,本地测试兼容) scripts/_lib.py: read_sql_arg已同时支持--sql/--sql-file,脚本零改动
主流程改动(.opencode/skills/sql-distributed/SKILL.md):
| 位置 | 改动 |
|---|---|
| 步骤 1 / 参数解析 | 新增 SQL_FILE 必要输入(优先)+ --sql-file PATH 参数 |
| 步骤 3.6(新增) | SQL_FILE 存在则 cp 到 $ROUND_DIR/user.sql,否则 echo "$USER_SQL" 落盘 |
| 步骤 4 / 5 / 离线采集 | baseline.py / table_ctx.py 改用 --sql-file "$ROUND_DIR/user.sql" |
| 步骤 6 / 7 / 8 Task prompt | 只传 USER_SQL_PATH=$ROUND_DIR/user.sql + TABLE_CTX_PATH=$ROUND_DIR/table_ctx.json(+ MATCHED_RULE_ENTRIES_PATH) |
| 步骤 8 generation | 产出后落盘 echo "$OPT_SQL" > $ROUND_DIR/optimized_sql.sql |
| 步骤 9 / 10 | precheck.py --original-sql-file / --optimized-sql-file;execute.py --optimized-sql-file |
| 步骤 11 Task prompt | ORIGINAL_SQL_PATH + OPTIMIZED_SQL_PATH + TABLE_CTX_PATH,替代内联 SQL 与 DC |
5 个子 agent 输入契约(.opencode/agents/sql-dis-*.md):
| agent | 输入变量变化 |
|---|---|
| sql-dis-rule-match | USER_SQL + DISTRIBUTED_CONTEXT → USER_SQL_PATH + TABLE_CTX_PATH(内部 Read) |
| sql-dis-skill-analysis | 同上 |
| sql-dis-dml-advisor | 同上 |
| sql-dis-index-advisor | USER_SQL + DISTRIBUTED_CONTEXT → USER_SQL_PATH(TABLE_CTX_PATH 原有,扩展为同时提供 distributed_context) |
| sql-dis-review | ORIGINAL_SQL / OPTIMIZED_SQL / DISTRIBUTED_CONTEXT → ORIGINAL_SQL_PATH + OPTIMIZED_SQL_PATH + TABLE_CTX_PATH |
11.2 预期收益与验证¶
- 主对话峰值从 ~52k 降至 ~40k(Task prompt 不再内联 SQL/DC 原文,仅路径 ~100B/份)
- 验证:
tests/IK7LI3/fixtures/{B1,B2,B3}/冒烟 +extract_token_usage.py复测主对话峰值 - 子 agent 侧 Read 一次 SQL 文件(2-4k)进自己 session,不影响主对话
11.3 三层一致性¶
- 规格层(
skills/sql-dis-opt/):输入契约仍写user_sql或input_path,框架无关;不掺 OpenCode 路径字样 - Runtime 层(
.opencode/):本节 §11.1 的路径化契约 - 其他 runtime:等价实现 = 把 SQL 文本按路径传给等价的子调用,或直接内联(代价自担)
12. 第四档:规则指纹预筛(2026-08-20)¶
服务器使用
dsv4-flash-int8时单 session 最大上下文为 32k;步骤 6sql-dis-rule-match直接读取约 33kB 的规则库全文,叠加 agent prompt、SQL 和 table context 后仍可能触顶。规则库必须保留完整来源 SQL 对,不能通过删减正文解决。
12.1 设计¶
采用“确定性脚本预筛 + LLM 严格复核”两阶段:
rule_filter.py解析 SQL,提取固定 16 维结构指纹。- 脚本从
pending_rules_1.md顶部“规则指纹索引”读取RULE_ID → 布尔表达式,不在代码中硬编码规则。 - 脚本按指纹表达式筛出候选,并从完整规则库裁剪对应
## <RULE_ID>段落。 sql-dis-rule-match只消费候选 JSON,对候选正文重新校验触发特征与关键约束;预筛命中不等于最终规则命中。- 脚本失败时保守降级,禁止回退为 LLM 直接读取规则库全文。
固定指纹维度:has_count、has_left_join、has_inner_join、has_union、has_union_all、has_distinct、has_in_subquery、has_in_list、has_single_in、has_lateral、has_derived_table、has_scalar_subq、has_concat、has_order_limit、has_or、has_not_in。
12.2 可扩展性契约¶
- 新增规则:只修改规则库索引表和规则正文,
rule_filter.py零改动。 - 新增 SQL 结构维度:才需要扩展脚本指纹提取和索引表允许变量。
- canonical
skills/rule-match/rules/pending_rules_1.md与 runtime.opencode/skills/sql-distributed/references/pending_rules_1.md必须保持一致。 - 布尔表达式仅允许指纹名、
true、&&、||、!和括号,并通过 AST 白名单求值。
12.3 验证目标¶
- 代表性单测覆盖指纹提取、表达式求值、索引解析、候选裁剪和新增规则零代码变更。
- 简单
COUNT + LEFT JOIN样例中,子 agent 可见规则内容从约 33kB 降至约 4.5kB。 - 最终以服务器 OpenCode session 统计确认
sql-dis-rule-match峰值低于 32k;本地只验证确定性预筛逻辑,不宣称完整分布式执行链已通过。
13. 第五档:总控 SKILL 渐进加载(2026-08-20)¶
13.1 触发证据¶
本地 Northwind 下界用例仅 116 字符、1 张表,OpenCode 主 session 仍在完成步骤 2 前陷入重复 Read / 压缩循环:
| 指标 | 实测 |
|---|---|
| main API calls | 34 |
| main peak effective | 32,242 |
| main sum effective | 615,402 |
| main output | 40,263 |
| rule-match peak effective | 14,546 |
总控 .opencode/skills/sql-distributed/SKILL.md 为 34,072B / 569 行,其中 generation 常驻段(原步骤 8.1-8.5)为 10,460B。日志显示模型因技能内容截断反复按 offset Read 自身并生成压缩摘要;rule-match 子 session 未触顶,因此第五档优先处理总控常驻文本。
13.2 设计边界¶
采用“短总控状态机 + generation 按需 reference”:
- 总控继续负责 13 步编排、两个 specialist 冲突汇总、最终 generation、机械校验与轮次控制。
- dml/index specialist 仍平级并行;不恢复已废弃的单体
sql-dis-generator。 - 原步骤 8.1-8.5 完整规则移到
.opencode/skills/sql-distributed/references/generation.md,仅在两个 specialist ack 收齐且确实进入步骤 8 后 Read 一次。 - 步骤 8 被 missing-context 门禁跳过时禁止 Read generation reference。
- 正常路径不得预读
docs/、runtime-cheatsheet.md或再次 ReadSKILL.md;错误模板只在对应失败发生时按需读取。 - canonical
skills/sql-dis-opt/保持框架无关分层,不因 OpenCode runtime 渐进加载改写架构。
13.3 常驻/按需契约¶
| 内容 | 常驻总控 | 按需加载 |
|---|---|---|
| 参数默认值、ROUND_DIR、13 步状态机 | 是 | — |
| baseline/table_ctx/precheck/execute 命令与门禁 | 是(紧凑) | 脚本全量结果落盘 |
| 5 个 Task prompt 与 ack/summary 消费 | 是(紧凑) | agent 完整契约留各 agent 文件 |
| generation 硬规则、必查项、输出契约 | 否 | 步骤 8 Read references/generation.md 一次 |
| baseline 错误模板 / 常见错误 | 否 | 发生失败时查 runtime-cheatsheet.md |
| review 四类 verdict 详细判据 | 否 | sql-dis-review.md 独立执行,主控只消费 ack |
13.4 验收标准¶
OpenCode 当前模型硬上限为 context=32,000。2026-08-20 调整前 output=8,192,自动压缩安全触发点为 32,000 - 8,192 = 23,808;调整后 output=6,144,触发点为 25,856。文件字节数仅用于观察结构收敛,不作为模型上下文验收门禁。session 用量按调用统计:
effective_input = input + cache_read
context_total = effective_input + output
- 初始
.opencode/skills/sql-distributed/SKILL.md一次加载包含完整 13 步状态机;记录 UTF-8 字节数作为辅助回归数据,但不得换算或冒充 token 数。 - Northwind 下界 workflow 必须完成到合法停止状态,不得停在步骤 2,不得重复 Read
SKILL.md。 - 下界 main
peak_effective ≤ 22,000,为32,000 - 8,192 = 23,808可用输入预算预留安全余量;所有调用context_total < 32,000。 - generation reference 仅步骤 8 读取一次;门禁跳过步骤 8 时读取次数为 0。
- 若下界仍重复压缩、
peak_effective > 22,000或任一调用context_total ≥ 32,000,停止继续按字节删规则,重新评估将 generation 汇总隔离为独立 Task;该降级决策必须另行修订 §10 的“主 agent 掌握最终交付物”边界。
13.5 API 流式阻塞样本(2026-08-20)¶
P16.6 项目 runtime 下界 session ses_fe2110a84ffeglOliX3PenGE3M 正确完成 baseline / table_ctx 并进入步骤 6;其子 session ses_fe20e9972ffeC2XoxqVxxDIFL6 在 sql-dis-rule-match 首次 tool-call 完成后,下一条 assistant 记录持续为 finish=None、token 全 0、无 error。主 session 超过 11 分钟、子 session 超过 10 分钟无更新时间,而本地 opencode run 进程仍存活,判定为模型 API / 流式响应阻塞,不是脚本执行或 context 上限失败。
处理与计量规则:
- 保留原进程、session 和
$ROUND_DIR证据;向同一主 session 发送一次“继续”探针,观察 runtime 是否能恢复或明确报告 Task 阻塞。 - 该轮不得计入 P16.6 token 验收,因为未完成到合法停止状态,且未结束的 0-token assistant 占位记录不能代表真实调用成本。
- 若继续探针恢复,分别记录恢复前后 API calls、
peak_effective、peak_context_total和是否重复 dispatch;若仍阻塞,另开全新 session 复测,不通过杀进程或重跑脚本伪装为成功。 - 流式阻塞属于 provider/runtime 稳定性风险,与总控渐进加载的 token 验收分开判定。
继续探针结果:主 session 被唤醒后未恢复旧子调用,而是创建第二个 sql-dis-rule-match 子 session ses_fe1fc5838ffem1pz3BlYxteXQI。第二个子 session 同样超过 16 分钟停在 finish=None、token 全 0、无 error;旧子 session 同期超过 36 分钟无更新。消息序列显示旧子 session 已完成一次 15,106-token 的 tool-call 响应,卡在工具后的下一次模型请求;新子 session 则从首个模型请求起即为 0-token 占位,说明阻塞不在 rule_filter.py 或落盘脚本。由此确认“继续”只会重复 dispatch,不能恢复挂起的 Task/provider 请求,并引入重复执行与孤儿子 session 风险。本样本保持判废,P16.6 必须待 provider/runtime 恢复后用全新 session 重测。两个相关 opencode run 进程已按用户要求终止,并确认无匹配残留进程。
runtime 可控修复边界:
- 步骤 6 的父 agent 必须直接派发
sql-dis-rule-match;不得自行运行rule_filter.py、读取rule_candidates.json或替子 agent 做严格匹配。 - 步骤 6 唯一完成产物是
$ROUND_DIR/rule_match.json;不得检查旧名matched_rule_entries.json。 - Task 被人工中断或无 ack 时,主流程报告
Task API阻塞并停在当前步骤,禁止自动重复 dispatch;是否重试由 interactive 用户明确决定,且重试应使用全新 workflow session。 - macOS 不得使用外部
timeout/gtimeout包装脚本;baseline 只使用baseline.py --timeout 300的内置数据库超时。 - 这些约束只能避免重复 dispatch、错误路径和额外 token,不能修复 provider/OpenCode Task 流式实现本身。
13.6 人工 TUI 压缩循环样本(2026-08-20)¶
人工 session ses_fe1e0d6ccffekMewGNvb50mJG6 未完成 workflow,审计结果:
| 指标 | 实测 |
|---|---|
| main API calls | 16 |
| main peak effective | 29,393 |
| main peak context_total | 29,580 |
| main sum effective | 266,039 |
| main output | 15,335 |
| auto compactions | 5 |
| child sessions | 0 |
5 次自动压缩发生在约 3 分钟内。每次压缩后 effective input 暂降到约 3k–9k,随后 1–4 个调用又升至 22k–29k,形成“压缩 → 重建上下文 → 再压缩”循环;最终调用被人工中断,记录为 MessageAbortedError,不是 provider 挂起。
该样本实际使用 ~/.claude/skills/sql-distributed 旧全局 skill:命令与 Read 均指向全局目录,todo 仍是旧 5.5/6/7 编号,并读取了全局 30KB SKILL.md、config.ini 和旧脚本;未创建任何项目 runtime 子 session。因此它证明旧全局 skill 在 32k 配置下仍会反复压缩,但不能用于验收 .opencode/skills/sql-distributed 的 P16 瘦身结果。
后续人工验收必须:退出旧 TUI 后启动全新 OpenCode 进程和全新 session,不使用 opencode -s <旧 session>;先用只读 skill-resolution 探针确认 SKILL_DIR=.opencode/skills/sql-distributed 和 13 步编号,再执行数据库 workflow。
13.7 缺分片键重采假象审计(2026-08-20)¶
人工 session ses_fe1d056d4ffehhWB6nz48ZuGPZ 同样不是项目 runtime 验收样本。首轮 skill tool 返回 /Users/wjy777/.claude/skills/sql-distributed,该旧全局 skill 仍规定 shard_key_source=missing 时重跑一次步骤 5;旧全局 _lib.py 生成的 table_ctx.json.missing_info 也带“可重跑一次”文案。
| 指标 | 实测 |
|---|---|
| main API calls | 11 |
| main peak effective | 26,366 |
| main peak context_total | 26,411 |
| main sum effective | 208,537 |
| main output | 8,118 |
| compaction events | 4(3 完成,1 个 finish=None/0 tokens) |
| child sessions | 0 |
table_ctx.py 实际执行 |
1 次 |
第三次压缩摘要后,模型读取已落盘 table_ctx.json 并计划按旧规则重采;真正再次执行前已进入第四次 compaction。因此“反复重跑”是旧规则经摘要复活形成的计划循环,不是脚本已经多次采集。项目 runtime 仍坚持:table_ctx.json 成功落盘后即为本轮证据快照,禁止重跑步骤 5、重复步骤 7 或经步骤 12 开新轮补采。该段原先把“任一 missing 直接终止本轮”作为防循环手段;服务器真实 DDL 证明该门禁混淆了 noshardkey_allset 与元数据未知,终态部分由 §13.9 的候选级门禁取代。
13.8 OpenCode compaction 预算(v1.18.19 实现核验)¶
当前模型未设置 limit.input。OpenCode v1.18.19 此路径的自动压缩阈值按 context - maxOutputTokens 计算,compaction.reserved 不参与该分支;所以只调低 reserved 不会延后压缩。采用:
{
"limit": {"context": 32000, "output": 6144},
"compaction": {
"auto": true,
"tail_turns": 3,
"preserve_recent_tokens": 6000,
"reserved": 6144
}
}
触发点从 32,000 - 8,192 = 23,808 放宽到 32,000 - 6,144 = 25,856,同时最坏预算仍为 25,856 + 6,144 = 32,000。tail_turns 由 2 增至 3、最近原文预算由 4,000 增至 6,000,用于减少人工 Continue 后状态丢失;代价是压缩后保留上下文略大。配置只在新进程加载;若 stress SQL 出现输出截断,回退 output/reserved 到 8,192,不放松 context_total <32,000 门禁。
13.9 分片状态归一化与候选级门禁(2026-08-21)¶
服务器真实用例中,主表分片键已知,但 LEFT JOIN 维表 t_gcs_bsubacctinfo 被判为 shard_key_source=missing,导致步骤 8–12 整体跳过。仓库历史 DDL 证明该表实际声明 shardkey=noshardkey_allset;当前 _lib.py 过滤该占位值后又把空 shard_keys 统一映射成 missing,混淆了“DDL 明确无分片键”和“证据未采到”。
分片状态统一为:
shard_key_source |
语义 | 门禁 |
|---|---|---|
ddl_explicit / inferred_from_partition / dba_confirmed |
分片键已知 | 正常分析 |
no_shard_key |
DDL 明确包含 noshardkey_*;保留 distribution_marker 原值 |
不是缺失,不自动当作 broadcast |
broadcast |
DDL 或人工证据明确为广播 | 正常按广播语义分析 |
missing |
DDL 不可用或现有证据无法判定 | 仅限制依赖该表路由关系的候选 |
同时增加 metadata_status=complete|partial|unavailable,辅助统计或 PARTITIONS 查询失败只产生 warning,不抹掉已成功的 DDL。table_ctx.json 仍是本轮证据快照,禁止为补证据重跑采集。
门禁从整轮一票否决改为候选级:
- skill-analysis 遇到
missing仍按 SQL、规则和已有 EXPLAIN 路由,并把受影响表写入missing_context。 - DML specialist 可生成不依赖未知路由关系的语义等价候选;禁止假定未知表 colocated、broadcast 或必然 Shuffle。
- Index specialist 可继续分析,但
shard_key_source=missing或index_scope=local_unknown的表不得输出自动执行 DDL;其他证据完备表不受影响。 - generation 逐候选过滤;安全 SQL 继续进入 precheck、execute 和 review。
- review 只有在收益依赖未证实的分布式判断、或执行了作用域不明 DDL 时降级;不得因无关表存在
missing_info全局否决实测正优化。
该设计取代 §3.3、§6.1、§10.2、§10.5 和 §10.7 中历史 B3“任一 missing 全局降级”的旧约束;这些段落保留为 POC 历史记录,不再作为当前 runtime 契约。
13.10 子 agent 显式 runtime 路径契约(2026-08-25)¶
0824 真实会话中,DML advisor 已成功读取 rule_match.json,但其落盘契约要求从 $SKILL_DIR/scripts/_lib.py 导入 specialize_summary_for,controller 却没有在 Task.prompt 注入 SKILL_DIR。子 session 不继承主会话变量,因而先搜索路径、再两次 Read _lib.py 源码,最终以 Input length 34,466 > 32,000 失败且未生成 dml_advisor.json。这不是规则匹配失败,而是 runtime 输入契约缺口。
方案 A 规定:
- controller 向 rule-match、skill-analysis、dml-advisor、index-advisor、review 五个 Task.prompt 显式传入
SKILL_DIR=$SKILL_DIR;传入值必须是项目内.opencode/skills/sql-distributed的已解析绝对路径。 SKILL_DIR是 Task 输入,不依赖子 session 的 shell 环境、主对话历史或工作目录推断。- 子 agent 直接使用
$SKILL_DIR/scripts执行rule_filter.py或导入specialize_summary_for;不得为补齐路径而搜索或模型 Read_lib.py、rule_filter.py、SKILL.md。 - 路径缺失或无效时 fail fast:返回 degraded ack、
file=null,并在missing_context报告SKILL_DIR missing or invalid,不得发现式回退。 - 本次只修改
.opencoderuntime 契约;skills/sql-dis-opt/**canonical 规格保持框架无关,不引入SKILL_DIR。
方案 B(移除子 agent 对 specialize_summary_for 的 runtime import)暂不实施。只有全新 session 已确认 A 生效、排除 provider 故障后,仍出现 _lib.py 搜索/读取、import 反复失败,或 token 触顶可归因于该 helper 依赖,才进入下一轮 SSD。若 A 后无源码发现、产物与 ack 完整且每次 context_total < 32,000,则 B 暂无必要。
14. 第六档:每轮独立 OpenCode session 与状态化衔接(2026-08-25 设计,2026-08-31 否决)¶
最终决定(2026-08-31):本档方案否决,不实施。多轮优化继续在同一个主 session 内推进,依赖 OpenCode 强压缩(§13.8 预算配置)控制上下文。否决理由:每轮新开顶层 session 会导致环境校验与 baseline 全部重跑,成本不可接受;同 session 方案可直接复用首轮
PYTHON_BIN校验与固定 baseline。多轮上下文问题后续从压缩配置与主对话读取面收敛入手(如 §13.7 记录的摘要复活风险),不再考虑 session 拆分。以下设计仅作历史记录保留。
14.1 触发证据与目标¶
当前步骤 12 只创建新的 ROUND_DIR,仍在同一个主 session 中递增 ROUND_N 并读取上一轮 review.json.summary。文件分轮不能隔离模型历史,第二、三轮仍累积主会话 prompt、tool output 与 generation 内容,必然重新逼近 32k;同时没有 workflow 状态、最佳轮指针、幂等恢复或执行不确定性记录。
目标是把 session 生命周期与 SQL 优化轮次对齐:每轮由一个全新顶层 opencode run session 执行,轮间只通过结构化文件衔接。现有五个 mode: subagent / task: deny agent 不能承担整轮 worker,因为完整一轮仍需继续调度这些 Task。
14.2 Controller 与 round worker 边界¶
新增确定性 workflow_controller.py 作为唯一跨轮状态机:负责参数/路径校验、case 锁、state/handoff 原子写、seed 选择、新 session 启动、产物校验、best 晋升和停止判定。它不得生成 SQL、调用五个 specialist、从自然语言 stdout 判断成功或写 pipeline 最终 result JSON。
Runtime sql-distributed 分两个入口:
- 普通入口:归一化参数后调用 controller,只消费紧凑状态与最佳产物路径。
- 内部
--round-worker:在一个新顶层 session 中只执行指定一轮的环境校验、证据采集、五个 Task 路由、generation、precheck、execute、review,然后退出;不得递增轮次、创建下一轮或解释MAX_ROUNDS。
Controller 启动命令必须使用 argv 数组:
opencode run --format json --dir <PROJECT_ROOT> \
--title sql-distributed:<workflow_id>:round-<N> \
<round-worker prompt>
禁止加入 -c、--continue、-s、--session。--attach 只允许连接 server,不得选择旧 conversation。Prompt 只注入绝对 PROJECT_ROOT、SKILL_DIR、WORKFLOW_STATE_PATH、ROUND_DIR、ORIGINAL_SQL_PATH、INPUT_SQL_PATH、BASELINE_PATH、PREVIOUS_HANDOFF_PATH 和归一化配置。每轮 JSON event stdout 落盘 opencode_events.jsonl,但 exit code 0、stdout 文案或 Task state=completed 都不是完成证据;必须校验对应 artifact 存在且可解析。
14.3 workflow_state.json 契约¶
位置固定为 <CASE_DIR>/workflow_state.json,schema sql-distributed-workflow/v1,至少包含:
- identity:
workflow_id、owner=pipeline|standalone、status、stop_reason; - runtime:绝对
project_root、skill_dir、workspace_root、case_id、case_dir; - immutable input:
original_sql.path/sha256/immutable=true; - normalized config:
db_section、mode、threshold_percent、max_rounds、max_tables; - fixed baseline:
status/path/sha256/execution_state_path; - cursor:
next_round、active_round; best_candidate:round、SQL path/hash、完整 DDL bundle、baseline/optimized time、improvement、execute/review/handoff 路径;round 0 表示原始 SQL;- append-only
rounds[]:round_dir、session_id、launch status、conclusion、handoff/failure; - timestamps。
workflow_id、原始 SQL hash、case_dir 与归一化配置初始化后不可变;不一致直接失败。状态文件使用同目录临时文件、flush/fsync、os.replace 和目录 fsync 原子替换;workflow.lock 防止两个 controller 并发推进同一 case。
14.4 round_handoff.json 契约¶
位置固定为 <ROUND_DIR>/round_handoff.json,schema sql-distributed-round-handoff/v1。由 controller 从已校验的 generation、execute、review 产物确定性生成,不由模型自由写 summary。至少包含:
- workflow/round/status;
- input SQL path/hash、
seed_round、carried DDL; - candidate SQL path/hash、generation path、完整 DDL bundle;
- 必要 artifact path/hash;
- 相对固定原始 baseline 的 metrics;
- typed review:verdict、approved、risk/missing/feedback codes;
- eligibility:precheck/execution/rollback/safe/promotable/promoted/threshold_met;
- next round:allowed、seed path/hash/round、carried DDL;
- failure:stage、稳定 code、有界 message、
retry_safe。
handoff write-once;重复 finalization 必须得到字节等价内容。下一轮只 Read 新的 input.sql、前一轮 round_handoff.json 和本轮必要证据,不继承旧 session 历史。现有嵌套且截断的 review.json.summary.summary 只用于轮内主 agent,不作为跨轮协议。
14.5 固定 baseline、seed 与 best 晋升¶
<CASE_DIR>/user.sql永远保存不可变原始 SQL;round 1input.sql是其副本。- baseline 只执行一次且针对原始 SQL;后续所有候选均与该固定 baseline 的结果和耗时比较,避免相对上一轮累计收益漂移。
- round N>1 的
input.sql来自当前 best SQL 的不可变副本;未晋升的慢 SQL、功能风险 SQL 不得成为 seed。 - 候选仅在 precheck 成功、执行完成、结果完全一致、无 execute error、DDL 全部可审计、rollback 零失败、review 非功能风险、artifact/hash 合法且优化耗时有限正数时可晋升。
- 仅当候选
optimized_time严格小于 current best 时替换;相等保留更早轮。SQL 与其完整 DDL bundle 作为整体晋升。 - threshold 只决定是否提前停止,不决定 best 资格;安全正收益但未达阈值的候选仍可晋升并进入下一轮。
- 达到 max rounds 返回 best,不默认返回最后一轮。
14.6 停止、恢复与执行未知¶
正常停止:best 已 approved 且达阈值、达到 max rounds、无安全候选,或 interactive 用户拒绝继续。
结果不一致、功能风险、rollback 失败、hash/schema 异常均不得晋升;保留旧 best,workflow 进入 manual_intervention,禁止自动下一轮。
baseline.py / execute.py 必须写原子 execution journal,状态为 not_started|started|completed|completed_with_error,记录 operation_id、是否尝试 DDL、rollback 状态。process 退出而 journal 仍为 started 时判 execution_unknown/manual_intervention,禁止自动重跑。
恢复规则:有效 handoff 优先于滞后的 mutable state,可重放更新 state;执行完成且产物完整但 handoff 缺失时只重建 handoff;执行未知不重启;pre-execute 失败也只允许显式 safe-resume 创建新的顶层 session,不在旧 session 重派 Task。
14.7 Workspace 与 pipeline 所有权¶
Controller 接受显式 CASE_DIR,首版不强制迁移历史目录:pipeline 使用既有 ${WS_DIR}/cases/${FOLDER_NAME};standalone 使用 ignored workspace 下的 case 目录。单个 workflow 只能绑定一个 case_dir,不镜像、不 symlink。case 内统一为 user.sql、workflow_state.json、workflow.lock、round_N/*。
Pipeline 的 write_result.py 仍唯一拥有 ${WS_DIR}/results/{folder}_result.json。Controller 只返回 state 与 best bundle 路径;pipeline 必须从 best 指针发布结果,不能默认取最后一轮危险候选。Canonical skills/sql-dis-opt/** 只同步框架无关的原始 SQL、best-round 与 handoff 语义,禁止出现 OpenCode CLI/session 细节。
14.8 验收标准¶
- 无数据库 probe 连续启动两轮,顶层 session ID 不同,argv 无任何 continue/session flag,且每个 worker 可调用 Task。
- worker 内不存在轮次循环;controller 是唯一 next-round owner。
- stdout 伪报成功但 artifact 缺失时 controller 判失败。
- 后轮变慢或功能风险时 best 指针保持早期安全候选;max rounds 返回 best bundle。
- execution journal=
started时中断进程,workflow 进入 manual intervention 且不自动重试。 - 每轮 API 调用
context_total <32,000,lower-bound 每轮 main peak effective ≤22,000;generation reference 每轮最多 Read 一次。 - P17 的
SKILL_DIR显式注入继续生效;P18 验收不能替代 P17.6/P17.7 的独立结论。