Files
Video-Tree-TRM5/research-wiki/designs/2026-07-07-core-evolution-design.md
T

381 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design: core/evolution/ 可提取内核
**日期** 2026-07-07 · **状态** 提案 · **范围** `core/evolution/` 全部 7 个文件
## 1 定位
`core/evolution/` 是自进化循环的决策内核——诊断、进化、门控、补丁。它只依赖 Protocol 接口和标准库,可搬到无 adapters 的环境用假实现原样运行。
`app/harness/` 的分工:core/ 做决策("候选好不好"),app/ 做编排("跑推理、写版本、管缓存")。
## 2 模块结构与依赖
```text
core/evolution/
├── __init__.py
├── protocols.py # SkillStore, PromptStore, RunLog
├── types.py # ~18 个 dataclass
├── gate.py # CE-Gate e-process(算法 #5
├── patch.py # 补丁引擎 + 冻结区(算法 #9 局部)
├── validate.py # 块验证纯决策函数(算法 #7 局部)
├── diagnose.py # 两阶段诊断管线(算法 #8)
└── evolve.py # 进化引擎(算法 #9)
```
```mermaid
flowchart LR
gate["gate.py\n纯数学"] ~~~ patch["patch.py\n纯文本"]
types["types.py"] ~~~ protocols["protocols.py"]
validate["validate.py"] --> gate
validate --> types
diagnose["diagnose.py"] --> types & protocols
diagnose -.->|LLMProvider| CP["core/protocols.py"]
evolve["evolve.py"] --> types & protocols & patch
evolve -.->|LLMProvider| CP
```
依赖规则:`core/evolution/` 不 import `app/``adapters/`。LLM 调用通过已有 `core.protocols.LLMProvider`
## 3 protocols.py
三个 Protocol 均**只读**——core/ 返回结果,app/ 负责持久化。
```python
@runtime_checkable
class SkillStore(Protocol):
"""版本化技能读取端口。实现方解析 manifest 指针,core/ 不感知版本号。"""
def read_skill(self, filename: str) -> str: ...
def list_skill_files(self) -> list[str]: ...
@runtime_checkable
class PromptStore(Protocol):
"""版本化提示词读取端口。覆盖 system.md 和 tool extract/verify。"""
def read_prompt(self, filename: str) -> str: ...
def list_prompt_files(self) -> list[str]: ...
@runtime_checkable
class RunLog(Protocol):
"""实验日志查询端口。隔离 SQLitecore/ 不写 SQL。"""
async def get_predictions(
self, run_id: str, *, question_ids: list[str] | None = None,
) -> list[dict[str, Any]]: ...
async def get_traces(
self, run_id: str, *, question_ids: list[str] | None = None,
) -> list[dict[str, Any]]: ...
```
固定模板 prompt(诊断/进化用)不走 PromptStore,由调用方加载后以 frozen dataclass 束传入:
```python
@dataclass(frozen=True)
class DiagnosePrompts:
defect_vs_lapse: str; reasoning_sub: str
span_eval_system: str; span_eval_user: str
missed_nodes: str; skill_adherence: str
confirmation_bias: str; evidence_sufficiency: str
@dataclass(frozen=True)
class EvolvePrompts:
evolve_skill: str; evolve_system: str; evolve_tool: str
evolve_rank: str; consolidate_system: str
```
| 决策 | 理由 |
|------|------|
| Protocol 只读 | core/ 纯输入→纯输出,易测试;写入是 app/ 职责 |
| RunLog 用领域方法 | 避免 SQL 泄入 core/ |
| SkillStore/PromptStore 同步 | 文件读取量小且快,无需 async |
| 模板束 frozen dataclass | 零 I/O + 类型安全,不增 Protocol |
> **ARCHITECTURE.md §3.1 修订说明**ARCHITECTURE.md 定义的 SkillStore/PromptStore/RunLog 含 write_skill/write_prompt/insert 写方法。本设计有意精简为只读——core/ 返回结果 dataclass,写入由 app/harness/ 编排层执行。写方法保留在 app/ 侧的实现类中,不进 core/ Protocol。此为对 ARCHITECTURE.md 的细化,需同步更新 §3.1。
## 4 types.py
### 4.1 Gate 决策
```python
@dataclass(frozen=True)
class GateParams:
e_confirm: float; e_provisional: float; w_net_min: int
delta_min: float; lambda_dir: float; e_rollback: float
@dataclass(frozen=True)
class GateVerdict:
decision: str # accept_confirmed | accept_provisional |
# reject_directional | reject_futility |
# reject_inertia | continue
e_value: float; wald_lambda: float
delta_hat: float; delta_shrunk: float
```
### 4.2 诊断
| 类型 | frozen | 用途 |
|------|--------|------|
| `SpanMetrics` | ✓ | 单 span judge 结果(step, tool_name, completeness, hallucination, tags |
| `SkillStepAdherence` | ✓ | skill 步骤遵循度 |
| `QuestionMetrics` | ✓ | Stage 1 单题完整指标(7 规则 + 5 judge 类型:span/missed/adherence/bias/sufficiency |
| `ErrorAttribution` | ✓ | D1 归因(error_type + cause_category + lapse_note |
| `CaseSample` | ✓ | 案例包单样本 |
| `SkillCasePack` | ✓ | 按题型(failure_cases + success_cases + lapse_notes |
| `SystemCasePack` | ✓ | 跨题型行为(行为模式 ≥ 3 次触发) |
| `ToolCasePack` | ✓ | 按工具(failure_spans + success_spans |
| `DiagnosisResult` | ✓ | 管线最终输出 |
### 4.3 进化
| 类型 | frozen | 说明 |
|------|--------|------|
| `EvolutionRecord` | — | 构建过程 mutabletool 类的 evolved_content 为 JSON `{"extract":..,"verify":..}` |
| `RejectedEdit` | ✓ | 黑名单条目;gate 字段全 Optional |
| `EvolutionResult` | ✓ | 聚合输出(由 app/harness/ 编排层组装,非 core/ 函数返回) |
### 4.4 验证辅助
```python
@dataclass(frozen=True)
class PairResult:
w: int; l: int
observed: dict[str, tuple[bool, bool]] # qid → (baseline, candidate)
@dataclass(frozen=True)
class QuadrantClassification:
improvements: list[str]; regressions: list[str]
persistent_fails: list[str]; stable_successes: list[str]
```
## 5 gate.py — CE-Gate(算法 #5,纯数学)
| 常量 | 值 | 来源 |
|------|-----|------|
| `_WALD_WIN` | `ln(1.4) ≈ +0.3365` | θ₁=0.70 → 2×0.70 |
| `_WALD_LOSS` | `ln(0.6) ≈ -0.5108` | 2×(1-0.70)loss 步幅 > win(不对称) |
| `_SHRINK_PSEUDO` | 4 | Agresti-Coull 伪计数 |
**compute_e_value(w, l)**:截断 Beta 混合 `E = 2^(W+L+1)·B(W+1,L+1)·I½(L+1,W+1)`。log 空间计算;用对称性 `I½(b,a)` 代替 `1-I½(a,b)` 防灾难性消去。`w<0`/`l<0` → ValueError`tail≤0` → 0.0`W=L=0` → 1.0。
**gate_decision** 四出口优先级:confirmed(E+δ) → directional(Wald) → futility(best-case) → exhaustion(provisional/inertia) → continue。Wald 从累积 W/L 重算(非增量,避免浮点漂移)。delta_shrunk 仅观测,不进决策。
**probation_verdict(w, l)**:双向非对称——confirm 用 `E(w,l) ≥ e_confirm`rollback 用 `E(l,w) ≥ e_rollback`(参数交换)。e_rollback < e_confirm(回滚比确认更容易)。
## 6 patch.py — 补丁引擎(纯文本)
**标记常量**`APPENDIX_START/END``MOMENTUM_START/END`HTML 注释形式)、`*_MAX_CHARS=2000`
**区域解析**`appendix_region_bounds()``momentum_region_bounds()` **均严格**——标记不配对(单标记/重复/逆序)抛 ValueError,双缺合法返回 None。宽容语义仅存在于 evolve.py 的包装函数 `_strip_appendix_region`(缺标记 = no-op)和 `_appendix_span`(缺标记 = 空串),不在 patch.py 本身。
**apply_patch_with_report(content: str, edits: list[dict], protected_spans: list[str]) -> tuple[str, list[dict]]**
- edits 为 `[{"op": str, "target": str, "content": str}, ...]`(松类型 dict,保持 TRM4 格式)
- report 为 `[{"index": int, "op": str, "status": str, "target": str, "content_preview": str}, ...]`
- 4 种 opappend(最早非 frontmatter 冻结区前)、insert_after(三结果:成功/降级 append/skip)、replace、delete(均首次出现、count=1
- 每条 edit 前重算 `_protected_ranges`(坐标偏移)
- target 不 strippayload strip
- 冻结区坐标半开 `[start, end)`
**replace_momentum(content, guidance)**guidance 含标记字面量 → ValueError(注入防护)。空 guidance 合法(清除旧动量,保留标题行)。
## 7 validate.py — 纯决策函数(算法 #7 局部)
三个公开函数:`pair_block`(逐题比对 W/L)、`classify_quadrants`(四组各 sorted)、`compute_accuracy`
编排循环(materialize candidate → 双臂推理 → 缓存 → INFRA 护栏 → 块序贯 gate_decision)在 app/harness/。
## 8 diagnose.py — 诊断管线(算法 #8
### 公开入口
```python
async def run_diagnosis(
run_id: str,
questions: list[GeneratedQuestion],
tree_data: dict[str, Any], # video_id → 树 JSON
llm: LLMProvider,
run_log: RunLog,
skill_store: SkillStore,
prompts: DiagnosePrompts,
*, concurrency: int,
question_ids: list[str] | None = None,
task_types: list[str] | None = None,
only_incorrect: bool = False,
) -> DiagnosisResult:
```
### 流程
```
Stage 1asyncio.gather + Semaphoreper-question:
7 规则指标(纯函数) + 5 LLM judgespan/missed/adherence/bias/sufficiency
→ D1 归因瀑布 → defect/lapse 分类(LLM
→ ValueError 降级:规则指标保留,judge 指标 Nonedegraded=True
独立串行 pass:
reasoning_failure 子分类(仅对 error_type=reasoning_failure 的题)
Stage 2(纯逻辑):
D2 按工具聚合 → D3 按题型×正误 → D4 skill adherence → D5 跨题型行为
→ 三类案例包构建
```
### 关键保真规则
- 归因瀑布顺序:extraction(`completeness<0.5hallucination>0.5`) → search(`missed_nodes`) → reasoning(`evidence_sufficient=True`) → mixed
- defect_vs_lapse 分类:解析失败降级为 "lapse"(保护性,防错误改正文)
- single-failure fallback:某题型仅剩 1 个 defect → 降级为 lapse_note
- lapse_note 空白过滤:strip 后为空则丢弃
- SystemCasePack 触发:3 种行为模式各需 ≥ 3 次出现
- merge_system_packs stats 用 `{"per_step": [...]}` 包裹(不数值合并)
- trigram 相似度是**字符级**,取 **max**(非 mean
- `_call_judge` 重试 3 次(仅 ValueError),API 错误直传
## 9 evolve.py — 进化引擎(算法 #9
### 公开 API
| 函数 | 参数 | 返回 |
|------|------|------|
```python
async def evolve_single_skill(
llm: LLMProvider, pack: SkillCasePack,
skill_store: SkillStore, prompts: EvolvePrompts,
source_version: str, edit_budget: int,
consolidate_threshold: int, *,
skill_update_mode: Literal["patch", "rewrite"] = "patch",
rejected: list[RejectedEdit] | None = None,
) -> EvolutionRecord: ...
async def evolve_system_prompt(
llm: LLMProvider, pack: SystemCasePack,
prompt_store: PromptStore, prompts: EvolvePrompts,
source_version: str, edit_budget: int,
) -> EvolutionRecord: ...
async def evolve_single_tool(
llm: LLMProvider, pack: ToolCasePack,
prompt_store: PromptStore, prompts: EvolvePrompts,
source_version: str, edit_budget: int,
) -> EvolutionRecord: ...
def edit_budget_at(
global_step: int, total_steps: int,
start: int, end: int,
) -> int: ... # 纯数学
```
### Skill 三分支
| 分支 | 条件 | 行为 |
|------|------|------|
| A: Lapse-only | 无 defect edits + 有 lapse_notes | 合成 `applied_append` report,防循环误判 no-op |
| B: Rewrite | mode="rewrite" + 有 edits | 整篇重写;失败降级:有 lapse 转 A,否则 no-op |
| C: Patch | 默认 | rank_clip → apply_patch → validate → 最多 2 轮重试 |
所有分支后:有 lapse_notes → appendix 追加(≥ threshold 则 consolidate
### rank_and_clip 三级降级
LLM 排序 → `_select_top_edits``type(idx) is int` 排除 bool + 范围 + 去重)→ 空则降级原序前 N。
### Tool 共享预算池
extract+verify edits 合池(`_src` 标记)→ rank_clip → 按标记拆回。evolved_content 存 `json.dumps({"extract":..,"verify":..})`
### 冻结区配置
| 目标 | 冻结区 |
|------|--------|
| Skill | frontmatter + appendix + momentum |
| System | 3 个 `##` section(能力边界/输出格式/视频树结构)+ appendix |
| Tool | 输出格式 section + appendix |
### 验证规则
| 检查 | Skill | System | Tool |
|------|-------|--------|------|
| Frontmatter 三字段 | ✓ | — | — |
| 冻结 section 值相等 | — | ✓ | ✓ |
| 长度比 [0.3, 2.0](去冻结区后) | ✓ | ✓ | ✓ per file |
| 代码块闭合 | ✓ | ✓ | — |
### consolidate_appendix 四守卫
G1(`<2`直返) → G2(结果非空且≤输入) → G3(any Exception返原文) → G4(**调用方**`≥`拒绝等长)
## 10 共享工具函数
**`resolve_skill_file(skills_dir, task_type) -> str`**core/evolution/ 内部工具函数):
`resolve_skill_file(skill_store: SkillStore, task_type: str) -> str`
`task_type.lower().replace(' ', '-') + ".md"`,若文件不在 `skill_store.list_skill_files()` 中则回退 `"default-strategy.md"`。diagnose(加载 skill 内容做 adherence 判定)和 evolve(定位进化目标文件)共用此约定。接受 `SkillStore`(非 `Path`),保持 core/ 不依赖文件系统。
## 11 TRM4 → TRM5 变更总表
| 项 | TRM4 | TRM5 | 理由 |
|----|------|------|------|
| 并发 | `ThreadPoolExecutor` | `asyncio.gather + Semaphore` | TRM5 async-first |
| LLM | `LLMClient.from_env()` 每线程构造 | 共享 `LLMProvider` 注入 | Protocol 化 |
| DB | `HarnessLog` + raw SQL | `RunLog` Protocol | 隔离实现 |
| 文件 | `Path.read_text` 直读 | `SkillStore` / `PromptStore` | 可提取性 |
| 模板 | `_PROJECT_ROOT / "prompts"` 硬编码 | `DiagnosePrompts` / `EvolvePrompts` 束传入 | 零路径依赖 |
| 输出 | 写 JSON + DB + advance_version | 纯返回 dataclassapp/ 持久化 | 无副作用 |
| response 访问 | `response.choices[0].message.content` | `LLMResponse.content` | 已有统一类型 |
| validate 编排 | 在 core/ | 在 app/harness/ | Clean Architecture |
| run_evolution 编排 | 在 evolve.py | 在 app/harness/ | 版本管理属 app/ |
## 12 迁移保真约束
本节列出 TRM4 中影响正确性的实现细节,实现时必须逐条比对。
### 12.1 JSON 解析策略差异
| 模块 | 函数 | 策略 | 失败行为 |
|------|------|------|---------|
| metrics.py | `extract_json_from_response` | 三级:fenced code block → 最外层 `{...}``json_repair` | 全失败抛 ValueError |
| metrics.py | `_call_judge` | 包裹上述,max_retries=2(共 3 次),仅 ValueError 重试 | API 错误直传 |
| evolve.py | `_parse_llm_json` | 两级:fenced code block → 原文 `json.loads` | 失败返回 None(不抛) |
| metrics.py | `_parse_json_object` | 两级:`json.loads``json_repair` | 失败返回 None |
所有解析器均拒绝非 dict 结果(list/str → 视为失败)。
### 12.2 关键常量
| 常量 | 值 | 位置 | 说明 |
|------|-----|------|------|
| `_INFRA_STOP_REASONS` | `frozenset({"error", "parse_error"})` | diagnose | INFRA 排除集 |
| `_SPAN_EVAL_TOOLS` | `{"view_node", "search_similar", "observe_frame"}` | metrics | span judge + all_tool_outputs 范围 |
| `_MIN_PATTERN_COUNT` | 3 | diagnose | SystemCasePack 触发阈值 |
| `_TOOL_TARGET_FILES` | view_node→4 文件, search_similar→2, observe_frame→2 | diagnose | 工具→prompt 文件映射 |
| truncation | thought[:100], tool_output[:200] (metrics); 不截断 (diagnose) | metrics/diagnose | `_format_trace_text` 两版本不同! |
| case_sample truncation | tool_output[:500] | evolve | `_format_case_samples` |
### 12.3 案例包选择规则
| 包 | failure 选择 | success 选择 |
|----|-------------|-------------|
| Skill | 按 error_type 分组,各取 severity top-2 | `max(2, len(failures)//2)`acc≤0.3 按 budget 升序,否则按 (-adherence, budget) |
| System | 3 种行为模式(early_submit/high_conf_wrong/confirmation_bias)各取 top-2 | correct + calibrated + no_bias + 0.3≤budget≤0.8,按 abs(budget-0.5) |
| Tool | 低 completeness top-2 + 高 hallucination top-2,去重,总数≤4 | completeness≥0.9 且 hallucination==0.0 |
### 12.4 validate 编排守卫(app/harness/ 侧,非 core/
- `gate_run_prefix` 必须含 `"_gate_"` 子串(防泄漏标记)
- `ladder_items` 空 → ValueError
- INFRA guard:累计两臂 error,分母≥10 且 error_rate > `gate_guard_err` → RuntimeError
- 基线缓存补齐后 `assert all(v is not None)`
### 12.5 evolve 重试与退火
- `_run_patch_evolution_loop``range(2)` 两轮,三种失败反馈(JSON/target 未匹配/验证错误)
- `edit_budget_at``assert start >= end``total_steps ≤ 1` 返 startPython `round`banker's rounding
- `rewrite_from_suggestions`:重写不得长于原文;只捕 `ValueError/KeyError/TypeError/AttributeError`
### 12.6 范围说明
`momentum.py` 按 ARCHITECTURE.md §2.3 归属 `app/harness/`(非 core/evolution/),不在本设计范围内。其 LLM 调用、四类常量(IMPROVED/REGRESSED/PERSISTENT_FAIL/STABLE_SUCCESS)、`_format_comparison_pairs` 放在 try 外的设计意图、解析失败返回 `prev_guidance` 等规则将在 Design Bapp/harness/)中覆盖。
## 13 被拒方案
**方案 Avalidate Protocol 回调)**:给 validate 造 `InferenceRunner` Protocol 让编排留 core/。拒绝理由:leaky abstractionProtocol 签名暴露 workspace/skills_dir 等外层概念,形式反转实质耦合。
**方案 B(同步 + ThreadPoolExecutor**:保持 TRM4 同步。拒绝理由:TRM5 LLMProvider.chat() 已是 async,同步调用需 asyncio.run() 嵌套或线程桥接,增加复杂度。