跳转至

PR:分布式 SQL 优化工作流 stdout 投影(feat/evidence-projection)

分支文档快照 · 同步于 2026-09-09 · 来源:feat/evidence-projection @ 91a7d1c · 原路径:docs/project/current/IK6EG0-pr-draft.md。正文描述的是该分支的方案或进展,合入与验收状态以当前看板为准。

关联 issue:见上 Gitee issue(解决 50K 上下文墙) 分支:feat/evidence-projection 状态:草稿(待你 review,确认后再提交 Gitee PR)


一、改了什么

baseline / table_ctx / execute 三个脚本统一改为 磁盘全量、stdout 投影——磁盘仍写完整证据(落盘 schema 不变),stdout 只打印结构化的事实投影(schema_version=sqlopt-evidence/v1)。agent 默认消费投影,按需显式回退读全量。

0730 案例重放(E3):三阶段 stdout 从 ~45.4k token 压到 ~4.1k(约省 91%),步骤 5.5 前累计可从 ~52.7k 降到 ~32.9k。

二、设计:投影 + 回退机制

老做法的问题

脚本跑完,把所有结果(完整结果集行、完整表 DDL 文本、完整 EXPLAIN 文本)一次性打印到 stdout。OpenCode 把 bash 工具的 stdout 自动注入 agent 对话上下文。结果就是:完整数据既写到了磁盘(用于后续步骤比较),又原样塞进了 agent 上下文。每次 +12~15K,两三次就把 50K 余量吃光。

新做法

脚本跑完后,干两件事:

  1. 磁盘:仍然把完整数据写到 json 文件(落盘 schema 完全不变,后续 execute.py 比较链路不受影响)。
  2. stdout:只打印一份「事实投影」——一个固定结构的小 json,包含:
  3. facts:结论性字段(耗时、行数、列名清单、分布式算子特征、verdict 等)
  4. omitted_from_context:投影时砍掉的字段名清单(透明告诉 agent 哪些字段没给)
  5. known_unknowns:已知的证据缺口("分片键缺失"、"采样比较非逐行"等)
  6. blocked_decisions:软门禁规则("分片键缺失时禁止伪造"、"strength 不足时禁止声称逐行一致"等)
  7. claim_constraints:引用约束("has_cross_shard / has_full_shard_scan 只能引用 distributed_context 原值")
  8. evidence_ref:磁盘全量 json 的路径

agent 跑完脚本,默认就看这份投影推进分析、生成、验收。

什么时候才回退读全量?

只有当投影里出现以下情况,agent 才去 Read evidence_ref 路径下的磁盘全量(或子路径):

  • known_unknowns 明确列出"缺某字段才能判定某事"
  • blocked_decisions 指出某字段缺失导致无法定论
  • facts 里某个关键字段是 null 或空对象

举两个具体场景:

  • table_ctx 正常路径:tables_schema 优先从磁盘 table_ctx_dir/<table>/{ddl.sql,indexes.json,table_stats.json} 投影出列名与索引列序,stdout 不带 table_info 拼接 dump。仅当磁盘目录与结构化 table_info 都缺失时 tables_schema={},known_unknowns 会提示按路径回读小文件,而不是把整个 table_ctx.json(~27 KB dump)拉进上下文。
  • execute 投影里 comparison_strength=medium → blocked_decisions 写明"不得声称逐行完全一致"。agent 想确认是否真的逐行一致,再去 Read execute_result.json,且必须先说明"为什么 strength 不足,需要看具体行内容"。

为什么这么设计

  1. 默认走投影:agent 看到的就只有结论性事实,没有原始数据噪音。正常情况下投影的字段对分析、生成、验收完全够用。
  2. 回退是显式的:每次去磁盘读全量都是一次明确的工具调用,看得见、可计费。避免老做法那种"无意识地把全量塞进上下文"。
  3. 回退有依据:投影里的 known_unknowns / blocked_decisions 显式标出"哪里不够",agent 必须先想清楚"我需要哪个字段、为什么",才发起 read。不能盲目拉全量。
  4. 软门禁兜底:blocked_decisions 是硬规则。即使 agent 想凑结论,也会被投影里明写的约束挡住——比如"分片键缺失时禁止伪造分片键"。

反方案对比

为什么不让 LLM 自由摘要? 0730 压缩输入试跑中,模型在 has_full_shard_scan=false 的情况下仍然写出"全扫"——事实被改写了。LLM 自由摘要在压 token 上便宜,但牺牲了「事实不可篡改」。

本 issue 的核心原则是:事实由脚本钉死、策略由 agent 发挥。投影不是 LLM 摘要,是脚本从落盘证据机械映射出来的结构化字段,agent 没有改写空间。has_cross_shard=true 进了投影就永远是 true,agent 不能在 narrative 里说成 false。

为什么不靠 OpenCode 的 /compact? /compact 是有损通用会话摘要,对长会话兜底有用,但:

  • 截断规则通用(tool output 截到约 2K 字符),不按 distributed_context 字段保真
  • 不可预测(不知道哪段被砍)
  • 只能做溢出兜底,不能做领域证据交接

50K 撞墙的根因是证据链流转方式,不是会话长度本身。compact 治标不治本。

三、facts 的形成方式——纯规则匹配,不调用任何模型

facts 全部由 Python 代码机械映射产生,没有 LLM、没有外部 API、没有联网。这是设计上的硬规则——必须由脚本钉死,不能让模型介入(否则会出现 0730 试跑里 has_full_shard_scan=false 还被模型写成"全扫"那种事实篡改)。

每个字段分两类生成路径:

  1. 直接取值类(占大多数):用 .get() 从脚本已有的输出里取原值。例如 baseline_facts.facts.execution_time = full.get("execution_time")、facts.distributed_context = full.get("distributed_context")。不加工、不判断、不改写。
  2. 规则分类类(少数派):基于已取出的字段做 if-else 分类,输出枚举标签。规则代码事先写死,agent 看到的就是分类结果:
  3. execute_facts.facts.verdict(优化结论标签):基于 error(执行报错)/ result_identical(结果是否一致)/ optimized_time < baseline_time(优化后是否真的更快)三组已取出的字段判定为 faster(确认提速)/ result_mismatch(结果不一致)/ error(执行报错)/ neutral(无显著变化)之一
  4. execute_facts.facts.comparison_strength(结果一致性强度,防"两次实测偶然"伪达标):基于 comparison_mode(比较模式:full 全量 / direct 直连)与 comparison_detail.all_equal(逐行比较结果)判定为 high(全量比较且完全一致)/ medium(仅部分一致或单次比较)/ single(仅一次实测,无重复验证)/ unknown(无比较数据)。强度不达 high 时禁止声称"逐行完全一致"
  5. table_ctx_facts.facts.tables_schema(结构化的表 schema 摘要):从磁盘 table_ctx_dir/<table>/{ddl.sql, indexes.json, table_stats.json} 用正则 + 字典聚合读出列名清单、索引列序(每个索引的列顺序 + 是否唯一)、表统计(行数、平均行长、表注释);不解析拼接文本、不调用模型抽取

omitted_from_context / known_unknowns / blocked_decisions / claim_constraints 这几项也全部是 projector 函数里硬编码的字符串常量——脚本事先写死哪些字段被砍、哪些缺口要警告、哪些结论禁止下,agent 看到的"红线"是固定规则,不是模型推断。

一句话:投影 = Python .get() + 正则 + if-else。事实由脚本钉死,策略才由 agent 发挥。

四、实测收益

stdout 投影 token 对比(0730 同载荷,tiktoken cl100k_base)

载荷 full stdout 事实投影 节省
baseline 11,446 949 10,497
table_ctx 11,576 2,227 9,349
execute 22,400 965 21,435
合计 45,422 4,141 41,281(~91%)

步骤 5.5 前累计:全量路径 ~52.7k(必撞 50k)→ 投影路径 ~32.9k(可继续)。

OpenCode 端到端验证(deepseek-v4-flash,0730 载荷 replay)

使用 SQLOPT_REPLAY_DIR 环境变量驱动 replay 模式(脚本不连库,从 fixture 读预录产物),通过 opencode run 跑完整 sql-distributed skill 10 步工作流。

session 概览:

指标 值
Model deepseek-v4-flash
总轮次 26
Input tokens(累计) 57,485
Output tokens 9,825
Reasoning tokens 28,718
Cache read(累计) 1,221,760

每轮 effective input(input + cache_read)增长曲线:

阶段 轮次 effective input 说明
workflow 启动 + env_check msg[1] 16,787 SKILL.md + 脚手架
baseline 投影入上下文 msg[3] 27,114 +2K(投影),旧全量 +15K
table_ctx 投影入上下文 msg[8] 36,513 +2K(投影),旧全量 +12.5K
规则匹配 + 技能路由 msg[13] 45,108 加载 rule-match + skill-analysis
生成方案(sub-skill 加载) msg[19] 66,513 +11K,加载 dml-rewrite/ddl-index/generation SKILL
execute 投影 + 验收 msg[22] 79,900 +2K(投影)
最终结果输出 msg[25] 98,862 峰值

结论:

  1. 投影机制生效:baseline + table_ctx + execute 三阶段 stdout 合计 ~6K tokens 入上下文(旧全量 ~40K),投影节省 ~34K。
  2. 50K 限制下的现状:投影把撞墙点从步骤 4(table_ctx 后)推迟到步骤 15 左右(~53K)。全流程 10 步跑完峰值 98K,在 50K 限制下仍会撞墙。
  3. 剩余瓶颈不在投影范围:
  4. SKILL.md 本身 ~30K(加载一次占大半额度)
  5. sub-skill 加载(dml-rewrite / ddl-index / generation)单次 +11K
  6. 多轮工具调用累积(26 轮 × 平均数百到数千 bytes)
  7. 投影是必要条件但非充分:50K 内跑完全流程还需 SKILL.md 瘦身、sub-skill 按需加载、或中间 compact 配合——这些不在本 PR 范围内,将单独推进。

测试数据:.tmp/message-replay-0730/session.json(opencode export 完整会话)、.tmp/token-sim-0730/(fixture 文件 + table_ctx 磁盘模拟)。

五、测试

投影契约回归(不连库,补 e2e 验不到的字段级正确性):

python3 scripts/test_evidence_projection.py

脚本在验什么(OpenCode 端到端只看工作流 / token,不保证这些):

  • 关键事实完整进投影:distributed_context(含 has_cross_shard / has_full_shard_scan)原样保留;有磁盘目录时 tables_schema 含列名 + 索引列序 + stats;execute 算出 verdict / comparison_strength 与索引审计五字段
  • 大字段只落盘、不进 stdout:result.rows / table_info 全文 / column_selectivity_text / optimized_result.rows 不出现在投影;磁盘侧仍保留全量供比较与回退
  • 缺口显式声明:磁盘与结构化 table_info 都缺时 tables_schema={},且 known_unknowns 提示回读路径(不静默丢索引信息)

27 条断言全部通过。

六、落地与验收

改动面(细节见 diff,不按文件逐条展开):

  • 脚本:_lib.output_json 支持双写;新增 3 个 project_* + 磁盘 tables_schema 加载;baseline / table_ctx / execute 接投影
  • SKILL:主 skill「证据投影」+ 步骤 4/5/9/10;generation / ddl-index / review 改消费 *_facts
  • 回归:scripts/test_evidence_projection.py(不连库);.opencode/skills/ 对应镜像已同步

边界:旧 output_json(out, path) 不传 stdout_data 仍全量兼容;未改执行语义、比较算法、write_result、落盘必填字段。

验收对照(issue 勾选项,均已满足):

  • 有 --output:磁盘全量、stdout 投影(含 evidence_ref);无 --output:stdout 仍投影(B 决议,见第七节)
  • 落盘不截断 result.rows;比较算法未动;不引入联网安装
  • table_ctx:保留 distributed_context,去掉 table_info 全文 / column_selectivity_text;有 table_ctx_dir 时 tables_schema 含索引列序
  • execute:去掉双边 rows,新增 verdict / comparison_strength
  • SKILL 默认消费 facts,缺证据才 Read evidence_ref
  • 0730 重放:三阶段投影合计 ~4.1k tokens(约省 91%)

七、行为变更(需 reviewer 留意)

B 决议——默认始终投影:原 tech-spec P-1 写明「不传 --output → 全量 stdout 便于调试」,实际实现里三个脚本即使不传 --output 也走投影。理由:agent 调用脚本的目的就是分析,原始 rows 对分析无价值;保留旧行为会让"调试用全量 stdout"成为隐形上下文膨胀入口。tech-spec 已同步更新此决议。 影响:人工直接跑脚本看 stdout 调试时,会看到投影而非全量——需要看全量请加 --output 参数读磁盘文件。