Files
Video-Tree-TRM5/app/harness/momentum.py
T

179 lines
7.3 KiB
Python
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.
"""慢更新动量生成 — epoch 末为单个 skill 产出新的动量指导。
对标 SkillOpt 的 slow_update 机制:拿上一 epoch 末与当前 epoch 末两版 skill
在固定样本上各跑一遍得到纵向对比(comparison_pairs),反思上一轮动量指导是否奏效、
本轮正文改动是改善还是漂移,据此重写动量指导。新指导经 patch 引擎的 replace_momentum
写回 skill 的 momentum 受保护区,作为下一轮进化的方向锚。
从 TRM4 core/harness/momentum.py156 行)迁移 + async 化。
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from loguru import logger
from core.evolution.diagnose import extract_json_from_response
if TYPE_CHECKING:
from pathlib import Path
from core.protocols import LLMProvider
# =========================================================================
# 四类纵向对比类别名(单一真源)
# =========================================================================
IMPROVED = "improved" # 错→对
REGRESSED = "regressed" # 对→错
PERSISTENT_FAIL = "persistent_fail" # 错→错
STABLE_SUCCESS = "stable_success" # 对→对
# 类别名 → 展示标题,列表顺序即展示顺序。
# 回退(REGRESSED)刻意排在改善(IMPROVED)之前——它是最该警惕的伤害信号。
_CATEGORY_LABELS: tuple[tuple[str, str], ...] = (
(REGRESSED, "从对变错(回退,最高优先级)"),
(PERSISTENT_FAIL, "始终答错(持续失败)"),
(IMPROVED, "从错变对(改善)"),
(STABLE_SUCCESS, "始终答对(稳定成功)"),
)
# =========================================================================
# 辅助函数
# =========================================================================
def _categorize_pair(pair: dict[str, Any]) -> str:
"""按两版正误派生纵向对比类别。
用键值的真值(bool(...))表示该题在两版上各自的正误:缺 correct_prev/
correct_curr 键时直接抛 KeyError 向上传播——这是上游数据损坏(不是裁判语义
歧义),静默当 False 会伪造 persistent_fail 证据、污染动量指导,故不掩盖。
参数:
pair: 单个纵向对比对,须含 correct_prev/correct_curr 两键。
返回:
四个类别命名常量之一:IMPROVED(错→对)/REGRESSED(对→错)/
PERSISTENT_FAIL(错→错)/STABLE_SUCCESS(对→对)。
异常:
KeyError: 缺 correct_prev 或 correct_curr 键时。
"""
correct_prev = bool(pair["correct_prev"])
correct_curr = bool(pair["correct_curr"])
if not correct_prev and correct_curr:
return IMPROVED
if correct_prev and not correct_curr:
return REGRESSED
if not correct_prev and not correct_curr:
return PERSISTENT_FAIL
return STABLE_SUCCESS
def _format_comparison_pairs(comparison_pairs: list[dict[str, Any]]) -> str:
"""将纵向对比对格式化为裁判可读文本,按 _CATEGORY_LABELS 分组与排序。
参数:
comparison_pairs: 每个 dict 含 question/prev_prediction/curr_prediction/
correct_prev/correct_curr 字段,描述一道固定样本上两版的成对结果。
返回:
可读的纵向对比文本;空列表返回占位说明。
异常:
KeyError: 任一 pair 缺 correct_prev/correct_curr 键时;不掩盖的理由见
_categorize_pair docstring。
"""
if not comparison_pairs:
return "(本轮无可用纵向对比样本)"
grouped: dict[str, list[dict[str, Any]]] = {key: [] for key, _ in _CATEGORY_LABELS}
for pair in comparison_pairs:
grouped[_categorize_pair(pair)].append(pair)
lines: list[str] = [f"固定样本总数:{len(comparison_pairs)}"]
for key, label in _CATEGORY_LABELS:
entries = grouped[key]
lines.append(f"\n### {label}{len(entries)} 题)")
if not entries:
lines.append("(无)")
continue
for pair in entries:
lines.append(
f"- 题目:{pair.get('question', '')}\n"
f" 上版预测:{pair.get('prev_prediction', '')} | "
f"当前版预测:{pair.get('curr_prediction', '')}"
)
return "\n".join(lines)
# =========================================================================
# 入口
# =========================================================================
async def run_slow_momentum(
llm: LLMProvider,
diagnose_prompts_dir: Path,
skill_content: str,
prev_skill: str,
prev_guidance: str,
comparison_pairs: list[dict[str, Any]],
) -> str:
"""为单个 skill 生成新的慢更新动量指导。
参数:
llm: LLM 端口(async chat)。
diagnose_prompts_dir: 诊断 prompt 目录(根 prompts/slow_momentum.md 在此)。
skill_content: 当前版 skill 正文。
prev_skill: 上一版 skill 正文。
prev_guidance: 上一轮写下的动量指导。
comparison_pairs: 固定样本上两版 rollout 的成对结果(含 question/
prev_prediction/curr_prediction/correct_prev/correct_curr)。
返回:
新的动量指导文本;解析失败时保留 prev_guidance。
关键实现细节:
- _format_comparison_pairs 刻意置于 try 块之外(prompt 构造阶段):它对每个
pair 取 correct_prev/correct_curr,缺键抛 KeyError 直接向上传播,不被下方
针对裁判语义歧义的 except ValueError 吞掉。
- 解析失败保留上轮指导:extract_json_from_response 抛 ValueError、缺
slow_update_content 字段、或该字段非 str,均视为语义解析失败,返回
prev_guidance(判不准时保守保留上轮指导,对标 diagnose 的保护性 fallback)。
- P5 边界:仅捕 ValueError 这一语义歧义;llm.chat 的基础设施失败
(网络/API 异常)刻意不捕,向上传播,绝不用默认值掩盖。
"""
system_prompt = (diagnose_prompts_dir / "slow_momentum.md").read_text(encoding="utf-8")
# _format_comparison_pairs 刻意置于下方 try 块之外(prompt 构造阶段):它对每个
# pair 取 correct_prev/correct_curr,缺键抛 KeyError 直接向上传播,不被下方针对
# 裁判语义歧义的 except ValueError 吞掉。异常类型选 KeyError(非 ValueError),
# 即便位置疏忽落入 try 也不会被误吞。
user_prompt = (
f"## 上一版 skill 正文\n{prev_skill}\n\n"
f"## 当前版 skill 正文\n{skill_content}\n\n"
f"## 上一轮的动量指导\n{prev_guidance}\n\n"
f"## 固定样本纵向对比(上版 vs 当前版)\n"
f"{_format_comparison_pairs(comparison_pairs)}"
)
response = await llm.chat(
[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
)
raw = response.content
try:
parsed = extract_json_from_response(raw)
new_guidance = parsed.get("slow_update_content")
if not isinstance(new_guidance, str):
raise ValueError("slow_update_content 字段缺失或非字符串")
except ValueError:
logger.warning("慢更新动量解析失败,保留上轮动量指导")
return prev_guidance
return new_guidance