Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions docs/trajectory-distillation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# Trajectory Distillation — 设计文档

> 状态:Draft v1 · 2026-07-16
> 关联:[weakness-mining-design.md](weakness-mining-design.md)(弱点从哪来)· [agent-harness-design.md](agent-harness-design.md)(内核与边界哲学)
> 回答的问题:weakness-mining-design.md §10.4 —— "eval 太窄会让下游 Outer Loop 过拟合……扩 eval + held-out split 是硬要求,不是可选项。"

---

## 0. 一句话

把已录制的轨迹(连同其上挖出的弱点)蒸馏成**可执行的 `EvalTaskSpec` 候选**(fixture + prompt + argv verifier),经确定性健全检查与人工 accept 后进入 held-out suite——让裁判的任务集随观察到的弱点**增长**,而不是永远停留在手写的 2 个任务上。

---

## 1. 目标与非目标

### 1.1 目标

- **弱点 → 回归任务**:每个高 support 的 playbook entry 都应能沉淀为至少一个可执行任务,使"该机制不复发"成为 gate 可以差分检验的命题。今天的 `artifact_survives_recovery.json` 就是一个手工蒸馏样本(从 `artifact_lost_after_error` 机制反推出来的任务);本设计把这门手工艺变成流水线的 **candidate 生成**环节,人只保留 accept 权。
- **成功轨迹 → smoke 任务**(后期):稳定完成的任务类型蒸馏为 capability smoke,防止 harness edit 伤害已会的能力。
- **全程 proposal-first**:蒸馏产物只是 candidate,落在 `.agent-harness/` 下;进 suite 是显式人类动作(git commit)。
- **零新格式**:产物就是现有 `EvalTaskSpec` JSON + fixture 目录,`load_eval_task` / `load_eval_suite` / 两个 gate 原样消费。

### 1.2 非目标

- 不做跨平台(Claude Code / Codex / Hermes)轨迹 ingest 与蒸馏——那是 traj-lab 的地盘(见附录)。
- 不做 SFT / preference 数据集导出——同上,需要跨平台数据体量,OkComputer 单 harness 轨迹量不够也不必要。
- v1 不做 LLM-judge / rubric 打分型评测——蒸馏产物必须是 **argv verifier 可执行任务**,确定性优先。
- **永不自动把任务加进 suite**。裁判只能由人扩。

### 1.3 设计原则(继承内核章程)

1. 内核冻结:`AgentLoop` / `Workspace` / `ToolRuntime` 一字不改;蒸馏整体是可删除扩展。
2. 确定性优先:LLM 只出现在一处(distiller subagent 合成 task + fixture);校验、生命周期、lineage 检查全部确定性。
3. proposal-first:candidate ≠ 裁判;accept 前它什么都不是。
4. **蒸馏产生裁判,所以蒸馏必须离裁判最远**——这是本文的安全命门(§6)。

---

## 2. 架构总览

```
.agent-harness/sessions/<id>/ weakness proposals / playbook [已有]
events.jsonl · transcript.jsonl │
│ │
└───────────────┬──────────────────────┘
▼
okc distill --from-session <id>
okc distill --from-playbook <entry-id>
│ spawn(TaskManager, fresh)
▼
Distiller Subagent(LLM,可删除扩展)
· 读 trajectory digest(复用 build_trajectory_digest)
· 合成最小 fixture(不是拷贝用户 workspace)
· 写 prompt(源自原始任务意图,脱敏、最小化)
· 写 argv verifier(断言 outcome + 弱点机制的可观测后果)
│ 产物只写入
▼
.agent-harness/eval_candidates/<candidate-id>/
task.json · fixture/ · provenance.json [gitignored]
│
okc distill --verify <id> ← 确定性反任务校验(§5)
│
okc distill --list / --accept / --reject ← 人审,append-only ledger
│
人工 promote:拷入 tests/e2e/tasks + fixtures,加进 suite,git commit
▼
═══ 冻结面 ═══ held_out.json / regression suites
(此后任何自动流程——包括 distiller 自己——不得触碰)
```

| 层 | 组件 | 归属 |
|---|---|---|
| 已有 | EventLog、mining proposals、playbook、`EvalTaskSpec`/`load_eval_suite`、两个 gate | 不改 |
| 新增扩展 | `distill.py`(CLI + 校验 + ledger)、distiller TaskProfile、可选 `distill-rubric` skill | 全可删 |
| 冻结面 | `tests/e2e/tasks` / `fixtures` / `suites`、verifier、gate 逻辑 | 只有人能改 |

**验收不变量**:删掉 distill 扩展后,`okc run` / `okc mine` / `okc gate` 行为与今天逐字节一致。

---

## 3. Candidate 数据契约

```
.agent-harness/eval_candidates/<candidate-id>/
├── task.json # 严格的 EvalTaskSpec 字段集——load_eval_task 必须能直接加载
├── fixture/ # 最小合成 fixture(task.json 的 fixture_path 指向它)
└── provenance.json # 蒸馏来源,格式如下
```

`provenance.json`:

```json
{
"producer": "distiller",
"source_sessions": ["<session-id>", "..."],
"playbook_entry": "<entry-id or null>",
"weakness_signature": { "agent_mechanism": "...", "editable_surface": "...", "..." : "..." },
"trace_refs": ["events.jsonl#<seq>", "..."],
"distill_rubric_version": "distill-rubric@<hash>",
"known_good_solution": "<人审用:一段说明该任务如何被正确完成>",
"status": "candidate | verified | accepted | rejected",
"verify_report": { "fail_before": true, "verifier_runnable": true, "duration_s": 1.2 }
}
```

约定与 mining 的 provenance(weakness-mining-design.md §3.3)同构:**只存 trace 引用,不复制原文**——candidate 里唯一的"内容"是合成 fixture,不是用户轨迹片段。

`task.json` 额外约定两个 tag:`distilled` + 机制名(如 `blocked_retry_loop`),供 suite 去重与后续统计。

**lineage 必须随任务走**:`task.json` 本身携带可选字段 `source_sessions`(`EvalTaskSpec` 增加一个默认为空的 field,`load_eval_task` 容忍缺失)。这是刻意的冗余——`provenance.json` 留在 gitignored 的 candidate 目录,promote 时只有 `task.json + fixture` 进 `tests/e2e/`,若 lineage 只住 provenance,§6.3 的排除过滤器在 promote 那一步就失去了数据。放在 `task.json` 上还让手写任务与蒸馏任务形态统一:手写任务的 `source_sessions` 天然为空 → 永不被排除,语义正好。

---

## 4. 蒸馏的两半:outcome 断言与机制断言

一个从弱点蒸馏出的任务要同时回答两件事:

1. **任务本身可完成**(outcome 半):verifier 断言交付物存在、测试通过——与现有手写任务无异。
2. **弱点机制不复发**(mechanism 半):这是蒸馏任务的存在理由。

mechanism 半的表达方式,按确定性从高到低:

| 层 | 表达方式 | v1 |
|---|---|---|
| verifier 内断言 | 机制有**文件系统可观测后果**时直接断言(如 `artifact_lost_after_error` → "data.txt 必须原样",即 `artifact_survives_recovery` 的做法) | ✅ 首选 |
| fixture 结构诱导 | fixture 刻意布置触发条件(易误删的产物、会失败一次的测试、诱导重试的报错信息),使"踩坑"必然反映到 outcome | ✅ |
| metrics 预算 | `max_model_calls` 收紧为"正常解法 + 小余量"——polling / retry 型机制烧预算,超了即 fail | ✅(粗但确定) |
| metrics 差分 | gate 层比较 `EvalMetrics.model_calls / tool_batches` 的回归 | 后期,不进 v1 |

诚实的边界:**不是所有机制都能蒸馏**。`over_exploration`、`serial_tool_underuse` 这类只在 metrics 上可见的机制,v1 只能靠预算收紧粗略覆盖;蒸馏优先级应给 `editable_surface` 可闭环、且机制有文件系统后果的 playbook entry。distiller 对蒸不动的 entry 应显式产出 `skip + 原因`,而不是硬造一个测不到点子上的任务。

---

## 5. 反任务校验(`okc distill --verify`)

蒸馏出的 verifier 本身不可信,accept 前必须过两道**确定性、零 API**检查:

1. **fail-before**:在裸 fixture 上直接跑 verifier,**必须失败**。空过的 verifier 什么都没测——这是挡"太松"的硬门。
2. **runnable**:verifier argv 可执行、在 timeout 内退出、fixture 可被 `load_eval_task` 加载且 `_copy_fixture` 可复制。

"太紧"(正确解法也过不了)无法确定性检验,v1 靠两个软措施:provenance 里强制 `known_good_solution` 字段供人审对照;后期可选用 scripted transport 重放一个理想解证明 verifier 可满足(复用 `regression_fixtures.py` 的手法,仍零 API)。

未过 `--verify` 的 candidate 不允许 `--accept`——ledger 层面硬约束。

---

## 6. 安全命门:蒸馏与裁判的距离

蒸馏是整条自我改进链里唯一**生产裁判**的环节,因此它是 reward hacking 的最高价值目标。四条纪律:

1. **candidate 与 suite 物理隔离**:candidate 住 `.agent-harness/`(gitignored),suite 住 `tests/e2e/`(git 管控)。promote 必须经过 git commit——人是唯一的搬运工。`okc distill` 不提供任何写 `tests/e2e/` 的代码路径,连 `--promote` 也只打印"该拷贝哪些文件"的指引。
2. **accept 后即冻结**:已 promote 的任务进入冻结面,distiller、proposer、apply 层在结构上够不到(同 weakness-mining-design.md §5.6 的冻结面纪律,此处再加一员)。
3. **held-out 卫生(lineage 排除)**:从 session S 蒸馏的任务,**不得**用于验证"由 S 的弱点催生的 harness edit"——那是用作业题当期末考。确定性检查:holdout gate 装配 suite 时,比对任务 `task.json` 的 `source_sessions`(见 §3——lineage 随任务进 suite,不依赖留在 candidate 目录里的 provenance)与待验 edit 的 provenance `source_sessions`,相交则该任务从本次差分中排除(排除后 suite 为空则拒绝出结论,报"需要无关任务")。这就是 held-in / held-out split 在蒸馏时代的具体形态。
4. **distiller 权限最小化**:TaskProfile `context_mode="fresh"`,工具面 = 只读(`read_file` / `search_text` / `list_files`)+ 仅限 `eval_candidates/<id>/` 子树的写入。不给 shell,不给 `memory_propose`——蒸馏不是挖掘,它不写知识,只写 candidate 文件。

---

## 7. Fixture 合成(最难的一环,诚实处理)

真实 workspace 不能当 fixture:有隐私、有体积、有不可复现的外部状态。distiller 的任务是**合成最小等价物**——一个刚好能触发该弱点机制、且任务可在 ≤10 个 model call 内完成的玩具环境(对齐现有 fixture 的粒度:`artifact_survives_recovery` 是 3 个小文件)。

这意味着接受一个语义降级:**蒸馏任务证明的是"该机制在该 fixture 上不复发",不是"原任务被修好了"**。这正是回归任务的正确语义——我们要的是机制级覆盖,不是案例级复现。原始轨迹只通过 `trace_refs` 留在 provenance 里供人审溯源,不进 fixture。

合成失败(机制依赖无法本地化的外部状态,如真实网络服务)时,distiller 显式 skip。v1 不做 checkpoint 快照回放——`checkpoints/` 里的真实工作区内容有同样的隐私/体积问题,留作后期可选路径。

---

## 8. 对现有模块的映射(复用清单)

| 需求 | 复用现有 | 新增(扩展) |
|---|---|---|
| 轨迹读取 | `build_trajectory_digest`(mining.py)+ `read_file`/`search_text` | —— |
| 弱点来源 | `kind="weakness"` proposals + `weakness_playbook.py` 的 `(id, description)` entry | —— |
| 隔离执行 | `TaskManager.spawn` + `TaskProfile(context_mode="fresh")` | `eval-distiller` profile |
| 任务格式 | `EvalTaskSpec` / `load_eval_task` / `load_eval_suite`(eval.py) | —— |
| 校验执行 | `VerifierSpec` argv 执行路径、`_copy_fixture` | `--verify` 的 fail-before 检查 |
| 生命周期 | 学 `harness_edits_apply.py` 的 append-only ledger + 显式状态机 | `eval_candidates` ledger |
| lineage 排除 | `holdout_gate.make_eval_suite_runner` 的 suite 装配点 | `source_sessions` 相交过滤器(读 task.json,见 §3) |
| CLI | `run.py` CLI 框架 | `okc distill` 子命令 |
| taxonomy | skills 注入机制 | 可选 `distill-rubric/SKILL.md`(哪些机制值得蒸、fixture 合成守则) |

---

## 9. 分阶段落地

**P0 · 契约与手动通路**
`eval_candidates/` 数据契约 + `okc distill --verify/--list/--accept/--reject`(ledger,确定性,零 API)。此阶段 candidate 由人手写——先让生命周期和校验跑通,蒸馏智能后置。产出即有用:手写新 held-out 任务从此有了标准校验与台账。

**P1 · LLM distiller**
`okc distill --from-session <id>` / `--from-playbook <entry-id>`:spawn distiller subagent,digest → task.json + fixture + provenance。scripted-transport 外壳测试保证管线确定性可测,真实蒸馏按需花预算。

**P2 · lineage 排除接进 holdout gate**
`source_sessions` 相交过滤器 + "排除后为空则拒绝出结论",装配点在 `holdout_gate.make_eval_suite_runner`。从此走 held-out 门的两条路——`okc propose --llm --validate-held-out` 与 `okc edits --revalidate --holdout-provider`——的结论都带 held-out 卫生保证。

**P3 · 成功轨迹与 metrics 半**
成功轨迹 cluster → capability smoke 任务;`EvalMetrics` 差分(model_calls 回归)进 gate 的对比维度。

---

## 10. 风险与取舍(诚实)

1. **fixture 合成失真**:玩具环境可能触发不了真实机制。缓解:fail-before 校验 + 人审 `known_good_solution`;接受"机制级近似"的语义(§7),不追求案例复现。
2. **蒸馏费 token**:只对 playbook 高 occurrence entry 蒸,批量跑,`--from-playbook` 是主入口而非逐 session 扫。
3. **任务同质化**:同一 entry 蒸多次会得到近亲任务。缓解:`distilled` + 机制 tag 去重,promote 时人审"这个机制是否已有覆盖"。
4. **verifier 太紧**:确定性校验只能挡太松(fail-before),太紧靠 `known_good_solution` 人审 + 后期理想解回放。接受 v1 的这个盲区。
5. **suite 膨胀让 held-out gate 变贵**:real-provider 差分是双倍成本。缓解:suite 分层——core(每次差分必跑)+ extended(按机制采样轮换);蒸馏任务默认进 extended,人工提拔进 core。
6. **最微妙的风险——用自己的弱点考自己**:蒸馏任务天然偏向"已见过的失败",suite 会漂向历史弱点分布,对未知失败盲。缓解只有纪律:手写任务不停止增补;蒸馏任务在 suite 中占比可见(tag 统计),不许淹没手写核心。

---

## 11. 测试策略

- **契约测试**:candidate 的 task.json 能被 `load_eval_task` 加载;provenance 必填字段齐全。
- **fail-before 纯函数测试**:给定"空过 verifier"的 candidate,`--verify` 必须拒绝。
- **ledger 状态机测试**:未 verify 不可 accept;accept/reject append-only;重复操作幂等。
- **lineage 过滤测试**:构造 source_sessions 相交/不相交的任务与 edit,断言排除行为与"空 suite 拒绝出结论"。
- **distiller 外壳测试**:scripted transport 喂固定轨迹 digest,断言产出 candidate 目录结构与 task.json 字段(零 API)。
- **解耦不变量**:不装 distill 扩展,现有全部测试套件保绿。

---

## 附:与 traj-lab 的关系

分界依据"谁拥有执行环境":**可执行评测蒸馏住在 OkComputer**——它拥有 `EvalRunner`、`Workspace` 沙箱和两个 gate,蒸馏产物有现成消费者;**traj-lab 做它独有的事**——跨平台轨迹的 rubric / LLM-judge 评测集与数据集(SFT / preference)蒸馏,那需要多 agent ingest 的数据体量,且不需要执行环境。两边的交换物是 `WeaknessSignature` / evidence bundle JSON(模型已同构),由人决定是否转写为对方形态的任务,不建自动管线。**依旧:借脑,不借水管。**
4 changes: 2 additions & 2 deletions docs/weakness-mining-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ LLM 聚合(合并同义机制、命名 cluster)仅作 opt-in 第二层,处
| `harness_runtime` | HookBus 策略 / RunPolicy / loop-breaker 配置 | 可闭环 |
| `context_config` | `ContextBudget` 旋钮 | 可闭环(皇冠明珠自调) |
| `skill` | skill candidate | 可闭环 |
| `evaluator` | eval 校准 / 回归任务 | 半闭环 |
| `evaluator` | eval 校准 / 回归任务 | ~~半闭环~~ → proposal-first 闭环:弱点蒸馏成 `EvalTaskSpec` 候选,人是唯一的 promote 通道(设计见 [trajectory-distillation.md](trajectory-distillation.md)) |
| `personal_workflow` | 用户侧建议 | **开环(仅人看)** |

> **安全命门**:`evaluator` 及 eval fixtures / verifier 属于**冻结面**——mining 可以*建议*校准,但 Outer Loop 的自动应用永远不许改裁判,否则 reward hacking。冻结面 vs 优化面写进 AGENTS.md「禁止」清单。
Expand Down Expand Up @@ -262,7 +262,7 @@ run 任务 ──emit──► EventLog(events.jsonl / transcript.jsonl) [核
- **提议器(stage-2 v1)**:`harness_proposer.py` —— 读 playbook entry 的 `editable_surface` → 产**有界 mechanics edit**(白名单 `SessionControllerConfig` 旋钮,冻结面结构上够不到)→ 过 held-in 差分门 → **proposal-first 落 pending**(`.agent-harness/harness_edits/proposals.jsonl`),绝不自动应用。`okc propose`。Proposer 可插拔;种子 `RuleBasedProposer` 确定性零 API。
- **LLM proposer tier** ✅ 已落地:`llm_proposer.py`(`okc propose --llm`)toolless agent 产 JSON edit 数组 → 按 surface 路由;prompt-space 落地面为新建的 `system_prompt_addendum` 有界 overlay(`session_controller.py`,`danger_reason` + 长度守卫);prompt edit 走 `holdout_gate.py` 的 real-provider held-out 差分门(`--validate-held-out`,否则 `pending_unvalidated`)。管线确定性可测,真实验证按需花预算。
- **accepted edit 应用/回滚面** ✅ 已落地:`harness_edits_apply.py`(`okc edits`)—— append-only accept/revert ledger + **每 workspace apply 开关**(默认关,方案 A);`apply_accepted_harness_edits` 在 top-level `okc run`/`chat`/tui 合并 active edits,apply 时重验白名单 + 重筛 overlay(纵深防御);受约束的 miner/proposer/gate 直接构建 config,不受影响。系统由**建议器**变**改进器**。
- **仍未做(最后一条)**:「有效性」判据(weakness 复发下降 / held-out 成功率提升,当前门只证 safe 不证 helps);skill-file edit surface。冻结面纪律:loop/eval/verifier 永不进白名单。
- **仍未做(最后一条)**:「有效性」判据(weakness 复发下降 / held-out 成功率提升,当前门只证 safe 不证 helps)——**设计已就绪、实现未动**:[trajectory-distillation.md](trajectory-distillation.md) 把弱点蒸馏为可执行 held-out 任务,让"该机制不复发"成为 gate 可差分检验的命题;skill-file edit surface。冻结面纪律:loop/eval/verifier 永不进白名单,蒸馏候选与 suite 物理隔离、人是唯一 promote 通道。

---

Expand Down
Loading