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 余量吃光。
新做法¶
脚本跑完后,干两件事:
- 磁盘:仍然把完整数据写到 json 文件(落盘 schema 完全不变,后续 execute.py 比较链路不受影响)。
- stdout:只打印一份「事实投影」——一个固定结构的小 json,包含:
facts:结论性字段(耗时、行数、列名清单、分布式算子特征、verdict 等)omitted_from_context:投影时砍掉的字段名清单(透明告诉 agent 哪些字段没给)known_unknowns:已知的证据缺口("分片键缺失"、"采样比较非逐行"等)blocked_decisions:软门禁规则("分片键缺失时禁止伪造"、"strength 不足时禁止声称逐行一致"等)claim_constraints:引用约束("has_cross_shard / has_full_shard_scan 只能引用 distributed_context 原值")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 不足,需要看具体行内容"。
为什么这么设计¶
- 默认走投影:agent 看到的就只有结论性事实,没有原始数据噪音。正常情况下投影的字段对分析、生成、验收完全够用。
- 回退是显式的:每次去磁盘读全量都是一次明确的工具调用,看得见、可计费。避免老做法那种"无意识地把全量塞进上下文"。
- 回退有依据:投影里的
known_unknowns/blocked_decisions显式标出"哪里不够",agent 必须先想清楚"我需要哪个字段、为什么",才发起 read。不能盲目拉全量。 - 软门禁兜底:
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 还被模型写成"全扫"那种事实篡改)。
每个字段分两类生成路径:
- 直接取值类(占大多数):用
.get()从脚本已有的输出里取原值。例如baseline_facts.facts.execution_time = full.get("execution_time")、facts.distributed_context = full.get("distributed_context")。不加工、不判断、不改写。 - 规则分类类(少数派):基于已取出的字段做
if-else分类,输出枚举标签。规则代码事先写死,agent 看到的就是分类结果: execute_facts.facts.verdict(优化结论标签):基于error(执行报错)/result_identical(结果是否一致)/optimized_time < baseline_time(优化后是否真的更快)三组已取出的字段判定为faster(确认提速)/result_mismatch(结果不一致)/error(执行报错)/neutral(无显著变化)之一execute_facts.facts.comparison_strength(结果一致性强度,防"两次实测偶然"伪达标):基于comparison_mode(比较模式:full全量 /direct直连)与comparison_detail.all_equal(逐行比较结果)判定为high(全量比较且完全一致)/medium(仅部分一致或单次比较)/single(仅一次实测,无重复验证)/unknown(无比较数据)。强度不达high时禁止声称"逐行完全一致"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 | 峰值 |
结论:
- 投影机制生效:baseline + table_ctx + execute 三阶段 stdout 合计 ~6K tokens 入上下文(旧全量 ~40K),投影节省 ~34K。
- 50K 限制下的现状:投影把撞墙点从步骤 4(table_ctx 后)推迟到步骤 15 左右(~53K)。全流程 10 步跑完峰值 98K,在 50K 限制下仍会撞墙。
- 剩余瓶颈不在投影范围:
- SKILL.md 本身 ~30K(加载一次占大半额度)
- sub-skill 加载(dml-rewrite / ddl-index / generation)单次 +11K
- 多轮工具调用累积(26 轮 × 平均数百到数千 bytes)
- 投影是必要条件但非充分: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 参数读磁盘文件。