1501 lines
52 KiB
Python
1501 lines
52 KiB
Python
"""进化引擎辅助函数与验证逻辑。
|
||
|
||
验证(validate_skill / validate_system / validate_tool)、受保护区构建、
|
||
编辑预算退火、rank-and-clip 裁剪、格式化工具等纯/准纯函数。
|
||
Task 8 将在此基础上添加进化入口(evolve_skill / evolve_system / evolve_tool)。
|
||
|
||
不依赖 app/ 或 adapters/(LLMProvider 通过 core.protocols 注入)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
from dataclasses import asdict, dataclass, field
|
||
from typing import TYPE_CHECKING, Any, Literal
|
||
|
||
from loguru import logger
|
||
|
||
from core.evolution.patch import (
|
||
APPENDIX_END,
|
||
APPENDIX_START,
|
||
append_to_appendix,
|
||
apply_patch_with_report,
|
||
extract_appendix_notes,
|
||
momentum_region_bounds,
|
||
replace_appendix_notes,
|
||
)
|
||
from core.evolution.types import EvolutionRecord
|
||
|
||
if TYPE_CHECKING:
|
||
from collections.abc import Awaitable, Callable
|
||
|
||
from core.evolution.protocols import PromptStore, SkillStore
|
||
from core.evolution.types import (
|
||
EvolvePrompts,
|
||
RejectedEdit,
|
||
SkillCasePack,
|
||
SystemCasePack,
|
||
ToolCasePack,
|
||
)
|
||
from core.protocols import LLMProvider
|
||
|
||
# =========================================================================
|
||
# 0. 局部类型
|
||
# =========================================================================
|
||
|
||
|
||
@dataclass
|
||
class ValidationResult:
|
||
"""格式验证的结果。
|
||
|
||
属性:
|
||
passed: 验证是否通过。
|
||
errors: 失败原因列表;passed=True 时为空。
|
||
"""
|
||
|
||
passed: bool
|
||
errors: list[str] = field(default_factory=list)
|
||
|
||
|
||
# =========================================================================
|
||
# A. 内部辅助函数
|
||
# =========================================================================
|
||
|
||
|
||
def _parse_frontmatter(text: str) -> dict[str, str] | None:
|
||
"""解析 YAML frontmatter,失败返回 None。
|
||
|
||
参数:
|
||
text: Markdown 文件全文。
|
||
|
||
返回:
|
||
frontmatter 字典,无有效 frontmatter 时返回 None。
|
||
"""
|
||
import yaml as _yaml
|
||
|
||
match = re.match(r"^---\n(.*?)\n---", text, re.DOTALL)
|
||
if not match:
|
||
return None
|
||
try:
|
||
return _yaml.safe_load(match.group(1))
|
||
except _yaml.YAMLError:
|
||
return None
|
||
|
||
|
||
def _strip_appendix_region(text: str) -> str:
|
||
"""剥离 appendix 受保护区(含 marker),返回其余正文。
|
||
|
||
宽松语义:APPENDIX_START / APPENDIX_END 任一缺失即当作「无区」原样返回,不报错。
|
||
"""
|
||
if APPENDIX_START in text and APPENDIX_END in text:
|
||
head, rest = text.split(APPENDIX_START, 1)
|
||
_, tail = rest.split(APPENDIX_END, 1)
|
||
return head.rstrip() + tail
|
||
return text
|
||
|
||
|
||
def _strip_momentum_region(text: str) -> str:
|
||
"""剥离 momentum 受保护区(含 marker),返回其余正文。
|
||
|
||
严格语义:委托 momentum_region_bounds 做配对检测,
|
||
marker 损坏/不配对时 raise ValueError。
|
||
|
||
异常:
|
||
ValueError: momentum marker 损坏/不配对。
|
||
"""
|
||
bounds = momentum_region_bounds(text)
|
||
if bounds is None:
|
||
return text
|
||
start, end = bounds
|
||
return text[:start].rstrip() + text[end:]
|
||
|
||
|
||
def _strip_protected_regions(text: str) -> str:
|
||
"""剥离 appendix + momentum 两个受保护区,返回正文部分。
|
||
|
||
先 appendix(宽松),再 momentum(严格)——顺序有关:
|
||
appendix 宽松剥离不会误杀 momentum marker。
|
||
|
||
异常:
|
||
ValueError: momentum marker 损坏/不配对。
|
||
"""
|
||
text = _strip_appendix_region(text)
|
||
text = _strip_momentum_region(text)
|
||
return text
|
||
|
||
|
||
def _check_length(original: str, evolved: str) -> list[str]:
|
||
"""检查改写后长度是否在 [0.3x, 2.0x] 范围内。
|
||
|
||
仅比正文,剔除 appendix + momentum 两区。orig_len==0 时跳过。
|
||
|
||
参数:
|
||
original: 改写前全文。
|
||
evolved: 改写后全文。
|
||
|
||
返回:
|
||
错误消息列表(空列表表示通过)。
|
||
"""
|
||
errors: list[str] = []
|
||
orig_body = _strip_protected_regions(original)
|
||
evol_body = _strip_protected_regions(evolved)
|
||
orig_len = len(orig_body)
|
||
if orig_len == 0:
|
||
return errors
|
||
ratio = len(evol_body) / orig_len
|
||
evol_len = len(evol_body)
|
||
if ratio > 2.0:
|
||
errors.append(f"长度超限: {evol_len} 字符是原文 {orig_len} 的 {ratio:.1f} 倍 (上限 2.0)")
|
||
if ratio < 0.3:
|
||
errors.append(f"长度不足: {evol_len} 字符是原文 {orig_len} 的 {ratio:.1f} 倍 (下限 0.3)")
|
||
return errors
|
||
|
||
|
||
def _check_code_blocks(text: str) -> list[str]:
|
||
"""检查代码块是否闭合。
|
||
|
||
参数:
|
||
text: 待检查的文本。
|
||
|
||
返回:
|
||
错误消息列表(空列表表示通过)。
|
||
"""
|
||
count = text.count("```")
|
||
if count % 2 != 0:
|
||
return [f"Markdown 格式错误: 代码块未闭合 (``` 出现 {count} 次)"]
|
||
return []
|
||
|
||
|
||
def _extract_section(text: str, heading: str) -> str | None:
|
||
"""提取 ## heading 到下一个 ## 之间的文本。
|
||
|
||
参数:
|
||
text: Markdown 全文。
|
||
heading: 二级标题名。
|
||
|
||
返回:
|
||
该 section 的完整文本(含标题行),未找到时返回 None。
|
||
"""
|
||
pattern = rf"(## {re.escape(heading)}.*?)(?=\n## |\Z)"
|
||
match = re.search(pattern, text, re.DOTALL)
|
||
return match.group(1).strip() if match else None
|
||
|
||
|
||
# =========================================================================
|
||
# B. 受保护区构建
|
||
# =========================================================================
|
||
|
||
|
||
def _appendix_span(content: str) -> str:
|
||
"""返回 appendix 受保护区整段(含 marker);不存在返回空串。
|
||
|
||
参数:
|
||
content: 文本全文。
|
||
|
||
返回:
|
||
appendix 区的完整文本(含 marker),或空串。
|
||
"""
|
||
if APPENDIX_START in content and APPENDIX_END in content:
|
||
start = content.index(APPENDIX_START)
|
||
end = content.index(APPENDIX_END) + len(APPENDIX_END)
|
||
return content[start:end]
|
||
return ""
|
||
|
||
|
||
def _momentum_span(content: str) -> str:
|
||
"""返回 momentum 受保护区整段(含 marker);不存在返回空串。
|
||
|
||
委托 momentum_region_bounds 做配对检测:
|
||
marker 损坏/不配对时由其 raise ValueError。
|
||
|
||
参数:
|
||
content: 文本全文。
|
||
|
||
返回:
|
||
momentum 区的完整文本(含 marker),或空串。
|
||
|
||
异常:
|
||
ValueError: momentum marker 损坏/不配对。
|
||
"""
|
||
bounds = momentum_region_bounds(content)
|
||
if bounds is None:
|
||
return ""
|
||
start, end = bounds
|
||
return content[start:end]
|
||
|
||
|
||
def _skill_protected_spans(text: str) -> list[str]:
|
||
"""Skill 冻结块:frontmatter + appendix 区 + momentum 区(各项可选)。
|
||
|
||
参数:
|
||
text: Skill 文件全文。
|
||
|
||
返回:
|
||
冻结文本块列表。
|
||
"""
|
||
spans: list[str] = []
|
||
match = re.match(r"^---\n.*?\n---", text, re.DOTALL)
|
||
if match:
|
||
spans.append(match.group(0))
|
||
appendix = _appendix_span(text)
|
||
if appendix:
|
||
spans.append(appendix)
|
||
momentum = _momentum_span(text)
|
||
if momentum:
|
||
spans.append(momentum)
|
||
return spans
|
||
|
||
|
||
def _system_protected_spans(text: str) -> list[str]:
|
||
"""System Prompt 冻结块:能力边界 / 输出格式 / 视频树结构 三段 + appendix 区。
|
||
|
||
参数:
|
||
text: system.md 全文。
|
||
|
||
返回:
|
||
冻结文本块列表。
|
||
"""
|
||
spans: list[str] = [
|
||
section
|
||
for section in (
|
||
_extract_section(text, name) for name in ("能力边界", "输出格式", "视频树结构")
|
||
)
|
||
if section
|
||
]
|
||
appendix = _appendix_span(text)
|
||
if appendix:
|
||
spans.append(appendix)
|
||
return spans
|
||
|
||
|
||
def _tool_protected_spans(text: str) -> list[str]:
|
||
"""Tool Prompt 冻结块:输出格式段 + appendix 区。
|
||
|
||
参数:
|
||
text: Tool Prompt 全文。
|
||
|
||
返回:
|
||
冻结文本块列表。
|
||
"""
|
||
spans: list[str] = []
|
||
section = _extract_section(text, "输出格式")
|
||
if section:
|
||
spans.append(section)
|
||
appendix = _appendix_span(text)
|
||
if appendix:
|
||
spans.append(appendix)
|
||
return spans
|
||
|
||
|
||
# =========================================================================
|
||
# C. 验证函数
|
||
# =========================================================================
|
||
|
||
|
||
def validate_skill(original: str, evolved: str) -> ValidationResult:
|
||
"""校验 Skill 改写结果。
|
||
|
||
检查项: frontmatter 三字段保留(name / description / task_type)、
|
||
长度比在 [0.3, 2.0]、代码块闭合。
|
||
|
||
参数:
|
||
original: 改写前的 Skill 文件全文。
|
||
evolved: 改写后的 Skill 文件全文。
|
||
|
||
返回:
|
||
ValidationResult 实例。
|
||
"""
|
||
errors: list[str] = []
|
||
orig_fm = _parse_frontmatter(original)
|
||
evol_fm = _parse_frontmatter(evolved)
|
||
if orig_fm is None:
|
||
errors.append("原文缺少有效 frontmatter")
|
||
elif evol_fm is None:
|
||
errors.append("改写后缺少有效 frontmatter")
|
||
else:
|
||
for key in ("name", "description", "task_type"):
|
||
if orig_fm.get(key) != evol_fm.get(key):
|
||
errors.append(
|
||
f"frontmatter 字段 {key} 被修改: {orig_fm.get(key)!r} → {evol_fm.get(key)!r}"
|
||
)
|
||
errors.extend(_check_length(original, evolved))
|
||
errors.extend(_check_code_blocks(evolved))
|
||
return ValidationResult(passed=len(errors) == 0, errors=errors)
|
||
|
||
|
||
def validate_system(original: str, evolved: str) -> ValidationResult:
|
||
"""校验 System Prompt 改写结果。
|
||
|
||
检查项: 三个冻结区值比较(能力边界 / 输出格式 / 视频树结构)、
|
||
长度比在 [0.3, 2.0]、代码块闭合。
|
||
|
||
参数:
|
||
original: 改写前的 system.md 全文。
|
||
evolved: 改写后的 system.md 全文。
|
||
|
||
返回:
|
||
ValidationResult 实例。
|
||
"""
|
||
errors: list[str] = []
|
||
frozen_sections = ["能力边界", "输出格式", "视频树结构"]
|
||
for section_name in frozen_sections:
|
||
orig_section = _extract_section(original, section_name)
|
||
if orig_section is None:
|
||
continue
|
||
evol_section = _extract_section(evolved, section_name)
|
||
if evol_section is None:
|
||
errors.append(f"冻结区 '## {section_name}' 在改写后缺失")
|
||
elif orig_section != evol_section:
|
||
errors.append(f"冻结区 '## {section_name}' 在改写后被修改")
|
||
errors.extend(_check_length(original, evolved))
|
||
errors.extend(_check_code_blocks(evolved))
|
||
return ValidationResult(passed=len(errors) == 0, errors=errors)
|
||
|
||
|
||
def validate_tool(
|
||
orig_extract: str,
|
||
evol_extract: str,
|
||
orig_verify: str,
|
||
evol_verify: str,
|
||
) -> ValidationResult:
|
||
"""校验 Tool Prompt 改写结果。
|
||
|
||
检查项: 输出格式 section 保留(per file)、长度比在 [0.3, 2.0]。
|
||
与 skill / system 不同,**不检查代码块闭合**。
|
||
|
||
参数:
|
||
orig_extract: 改写前的 extract prompt。
|
||
evol_extract: 改写后的 extract prompt。
|
||
orig_verify: 改写前的 verify prompt。
|
||
evol_verify: 改写后的 verify prompt。
|
||
|
||
返回:
|
||
ValidationResult 实例。
|
||
"""
|
||
errors: list[str] = []
|
||
for label, orig, evol in [
|
||
("extract", orig_extract, evol_extract),
|
||
("verify", orig_verify, evol_verify),
|
||
]:
|
||
orig_fmt = _extract_section(orig, "输出格式")
|
||
if orig_fmt is not None:
|
||
evol_fmt = _extract_section(evol, "输出格式")
|
||
if evol_fmt is None:
|
||
errors.append(f"{label}: 冻结区 '## 输出格式' 在改写后缺失")
|
||
elif orig_fmt != evol_fmt:
|
||
errors.append(f"{label}: 冻结区 '## 输出格式' 在改写后被修改")
|
||
errors.extend(_check_length(orig, evol))
|
||
return ValidationResult(passed=len(errors) == 0, errors=errors)
|
||
|
||
|
||
# =========================================================================
|
||
# D. 纯数学
|
||
# =========================================================================
|
||
|
||
|
||
def edit_budget_at(global_step: int, total_steps: int, start: int, end: int) -> int:
|
||
"""按 global_step 线性退火的 per-target 编辑预算。
|
||
|
||
借鉴 SkillOpt LinearScheduler:在 [0, total_steps] 上把预算从 start
|
||
线性退火到 end。total_steps<=1 直接返回 start(避免单步取到最小值)。
|
||
round 用 Python banker's rounding,max(end, ...) 兜底硬下限。
|
||
|
||
参数:
|
||
global_step: 当前全局步(0-indexed)。
|
||
total_steps: 退火地平线(step 数),即分母。
|
||
start: 退火起点(需 >= end)。
|
||
end: 退火终点(亦为硬下限)。
|
||
|
||
返回:
|
||
当步 per-target 最大 edit 条数。
|
||
|
||
异常:
|
||
AssertionError: start < end。
|
||
"""
|
||
assert start >= end, f"edit_budget_at 要求 start >= end,实际 start={start}, end={end}"
|
||
if total_steps <= 1:
|
||
return start
|
||
t = min(global_step, total_steps) / total_steps
|
||
return max(end, round(start + (end - start) * t))
|
||
|
||
|
||
# =========================================================================
|
||
# E. JSON 解析(进化版本,不同于 metrics 版)
|
||
# =========================================================================
|
||
|
||
|
||
def _parse_llm_json(raw: str) -> dict | None:
|
||
"""从 LLM 响应中解析 JSON。
|
||
|
||
仅两种策略:(1) 提取 ```json 代码块;(2) 直接 json.loads。
|
||
不做 outermost braces 推断、不用 json_repair。失败返回 None。
|
||
|
||
参数:
|
||
raw: LLM 原始输出文本。
|
||
|
||
返回:
|
||
解析后的字典;失败或结果非 dict 时返回 None。
|
||
"""
|
||
text = raw.strip()
|
||
# 策略 1:提取 ```json ... ``` 代码块
|
||
code_block = re.search(r"```json\s*\n(.*?)```", text, re.DOTALL)
|
||
if code_block:
|
||
text = code_block.group(1).strip()
|
||
# 策略 2:直接解析
|
||
try:
|
||
result = json.loads(text)
|
||
if isinstance(result, dict):
|
||
return result
|
||
return None
|
||
except (json.JSONDecodeError, ValueError):
|
||
return None
|
||
|
||
|
||
# =========================================================================
|
||
# F. rank_and_clip(async)
|
||
# =========================================================================
|
||
|
||
|
||
def _select_top_edits(
|
||
indices: list[Any],
|
||
edits: list[dict[str, Any]],
|
||
max_edits: int,
|
||
) -> list[dict[str, Any]]:
|
||
"""按 rank LLM 给出的优先级索引筛选 edits。
|
||
|
||
依次保留首个 max_edits 条合法、不重复、在范围内的索引对应 edit。
|
||
用 type(idx) is int(非 isinstance)以排除 bool。
|
||
|
||
参数:
|
||
indices: rank LLM 返回的 0-based 优先级索引。
|
||
edits: 候选 edit 列表。
|
||
max_edits: 最多保留条数。
|
||
|
||
返回:
|
||
按优先级顺序保留的 edit 列表。
|
||
"""
|
||
selected: list[dict[str, Any]] = []
|
||
seen: set[int] = set()
|
||
for idx in indices:
|
||
if type(idx) is int and 0 <= idx < len(edits) and idx not in seen:
|
||
selected.append(edits[idx])
|
||
seen.add(idx)
|
||
if len(selected) >= max_edits:
|
||
break
|
||
return selected
|
||
|
||
|
||
async def _request_rank_indices(
|
||
llm: LLMProvider,
|
||
prompts: str,
|
||
original: str,
|
||
edits: list[dict[str, Any]],
|
||
max_edits: int,
|
||
label: str,
|
||
) -> list[int]:
|
||
"""调 rank LLM 取重要性降序的索引列表。
|
||
|
||
守 P5:响应无法解析或 selected_indices 非列表时直接 raise ValueError。
|
||
|
||
参数:
|
||
llm: LLM 调用端口。
|
||
prompts: evolve_rank 模板内容。
|
||
original: 当前 prompt 全文(排序上下文)。
|
||
edits: 候选 edit 列表。
|
||
max_edits: 本轮预算上限。
|
||
label: 目标标签(仅用于报错信息)。
|
||
|
||
返回:
|
||
rank LLM 返回的原始索引列表(尚未去重/越界过滤)。
|
||
|
||
异常:
|
||
ValueError: 响应无 selected_indices,或其值非列表。
|
||
"""
|
||
edits_desc = "\n".join(
|
||
f"[{i}] op={e.get('op')} support_count={e.get('support_count', 0)} "
|
||
f"target={str(e.get('target', ''))[:60]!r} "
|
||
f"content={str(e.get('content', ''))[:60]!r}"
|
||
for i, e in enumerate(edits)
|
||
)
|
||
user_msg = (
|
||
f"## 当前文件\n\n{original}\n\n"
|
||
f"## 候选 edits({len(edits)} 条,预算 {max_edits} 条)\n\n{edits_desc}\n\n"
|
||
f"请选出最重要的 {max_edits} 条,返回其 0-based 索引(重要性降序)。"
|
||
)
|
||
response = await llm.chat(
|
||
[
|
||
{"role": "system", "content": prompts},
|
||
{"role": "user", "content": user_msg},
|
||
]
|
||
)
|
||
parsed = _parse_llm_json(response.content)
|
||
if not parsed or "selected_indices" not in parsed:
|
||
raise ValueError(f"{label} rank LLM 未返回 selected_indices,拒绝静默截断")
|
||
indices = parsed["selected_indices"]
|
||
if not isinstance(indices, list):
|
||
raise ValueError(f"{label} rank LLM selected_indices 非列表")
|
||
return indices
|
||
|
||
|
||
async def rank_and_clip(
|
||
llm: LLMProvider,
|
||
original_content: str,
|
||
edits: list[dict[str, Any]],
|
||
max_edits: int,
|
||
label: str,
|
||
*,
|
||
rank_prompt: str = "",
|
||
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
||
"""超预算时调 rank LLM 排序取 top-L;未超则原样返回。
|
||
|
||
三级降级:LLM rank → _select_top_edits → empty → fallback to edits[:max_edits]。
|
||
对 rank LLM 的输出波动一律优雅降级而非中止。
|
||
|
||
参数:
|
||
llm: LLM 调用端口。
|
||
original_content: 当前 prompt 全文(排序上下文)。
|
||
edits: 候选 edit 列表。
|
||
max_edits: 本轮预算上限。
|
||
label: 目标标签(skill/system/tool,仅用于日志)。
|
||
rank_prompt: evolve_rank 模板内容。
|
||
|
||
返回:
|
||
(裁剪后 edits, {"triggered": bool, "clipped": int})。
|
||
"""
|
||
if len(edits) <= max_edits:
|
||
return edits, {"triggered": False, "clipped": 0}
|
||
|
||
try:
|
||
indices = await _request_rank_indices(
|
||
llm, rank_prompt, original_content, edits, max_edits, label
|
||
)
|
||
except Exception as exc:
|
||
logger.warning(
|
||
"{} rank LLM 排序不可用({});退化为按原序取前 {} 条",
|
||
label,
|
||
exc,
|
||
max_edits,
|
||
)
|
||
indices = []
|
||
|
||
selected = _select_top_edits(indices, edits, max_edits)
|
||
if not selected:
|
||
selected = edits[:max_edits]
|
||
logger.warning("{} rank 有效索引为 0;退化为按原序取前 {} 条", label, max_edits)
|
||
elif len(selected) < max_edits:
|
||
logger.warning(
|
||
"{} rank 仅得 {} 条有效(<预算 {});按更保守的条数应用",
|
||
label,
|
||
len(selected),
|
||
max_edits,
|
||
)
|
||
logger.info("{} edits 超预算裁剪 {}->{}", label, len(edits), len(selected))
|
||
return selected, {"triggered": True, "clipped": len(edits) - len(selected)}
|
||
|
||
|
||
# =========================================================================
|
||
# G. resolve_skill_file
|
||
# =========================================================================
|
||
|
||
|
||
def resolve_skill_file(skill_store: SkillStore, task_type: str) -> str:
|
||
"""按运行时规则解析 task_type 对应的 skill 文件名。
|
||
|
||
转换规则:小写 + 空格替换为短横线 + .md 后缀。
|
||
若 store 中不存在匹配文件,退化到 default-strategy.md。
|
||
|
||
参数:
|
||
skill_store: 版本化技能读取端口。
|
||
task_type: 题目任务类型(如 "Action Reasoning")。
|
||
|
||
返回:
|
||
匹配的 skill 文件名。
|
||
"""
|
||
file_name = f"{task_type.lower().replace(' ', '-')}.md"
|
||
available = skill_store.list_skill_files()
|
||
if file_name in available:
|
||
return file_name
|
||
return "default-strategy.md"
|
||
|
||
|
||
# =========================================================================
|
||
# H. 格式化辅助
|
||
# =========================================================================
|
||
|
||
|
||
def _format_case_samples(cases: list[Any]) -> str:
|
||
"""将 CaseSample 列表格式化为 LLM 可读文本。
|
||
|
||
对 trace 中的 tool_output 截断到 500 字符。
|
||
|
||
参数:
|
||
cases: CaseSample 实例列表(也兼容 dict)。
|
||
|
||
返回:
|
||
格式化后的多行文本。
|
||
"""
|
||
lines: list[str] = []
|
||
for case in cases:
|
||
if not isinstance(case, dict):
|
||
case = asdict(case)
|
||
lines.append(f"### {case.get('question_id', 'unknown')}")
|
||
lines.append(f"- question: {case.get('question', '')}")
|
||
options = case.get("options", [])
|
||
if options:
|
||
lines.append(f"- options: {json.dumps(options, ensure_ascii=False)}")
|
||
lines.append(f"- answer: {case.get('answer', '')}")
|
||
lines.append(f"- prediction: {case.get('prediction', '')}")
|
||
lines.append(f"- error_type: {case.get('error_type', '')}")
|
||
lines.append(f"- selection_reason: {case.get('selection_reason', '')}")
|
||
trace = case.get("trace", [])
|
||
if trace:
|
||
lines.append("- trace:")
|
||
for step in trace:
|
||
output_text = str(step.get("tool_output", ""))
|
||
if len(output_text) > 500:
|
||
output_text = output_text[:500] + "..."
|
||
lines.append(
|
||
f" - step {step.get('step', '?')}: "
|
||
f"tool={step.get('tool_name', '')} "
|
||
f"args={json.dumps(step.get('tool_args', {}), ensure_ascii=False)} "
|
||
f"output={output_text}"
|
||
)
|
||
lines.append("")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def _format_spans(spans: list[dict[str, Any]]) -> str:
|
||
"""将工具 span 字典列表格式化为 LLM 可读文本。
|
||
|
||
对 tool_output 截断到 500 字符。
|
||
|
||
参数:
|
||
spans: span 字典列表,每个包含 step / tool_name / tool_args 等字段。
|
||
|
||
返回:
|
||
格式化后的多行文本。
|
||
"""
|
||
lines: list[str] = []
|
||
for span in spans:
|
||
lines.append(f"### step {span.get('step', '?')}")
|
||
lines.append(f"- tool_name: {span.get('tool_name', '')}")
|
||
lines.append(f"- tool_args: {json.dumps(span.get('tool_args', {}), ensure_ascii=False)}")
|
||
output_text = str(span.get("tool_output", ""))
|
||
if len(output_text) > 500:
|
||
output_text = output_text[:500] + "..."
|
||
lines.append(f"- tool_output: {output_text}")
|
||
lines.append(f"- extraction_completeness: {span.get('extraction_completeness', '')}")
|
||
lines.append(f"- hallucination_rate: {span.get('hallucination_rate', '')}")
|
||
missed = span.get("missed_info_tags", [])
|
||
if missed:
|
||
lines.append(f"- missed_info_tags: {json.dumps(missed, ensure_ascii=False)}")
|
||
hall_tags = span.get("hallucination_tags", [])
|
||
if hall_tags:
|
||
lines.append(f"- hallucination_tags: {json.dumps(hall_tags, ensure_ascii=False)}")
|
||
lines.append("")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def _format_rejected_edits(rejected: list[RejectedEdit]) -> str:
|
||
"""将已验证无效的改法列表格式化为 LLM 可读文本。
|
||
|
||
gate 证据格式:W=... L=... E={:.2f} delta_hat={:+.3f}。
|
||
|
||
参数:
|
||
rejected: RejectedEdit 实例列表。
|
||
|
||
返回:
|
||
格式化后的多行文本。
|
||
"""
|
||
lines: list[str] = []
|
||
for edit in rejected:
|
||
lines.append(f"### {edit.target_file} | delta {edit.delta:+.2f}")
|
||
lines.append(f"- 已验证无效的改法: {edit.change_summary}")
|
||
if edit.gate_e_value is not None:
|
||
lines.append(
|
||
f"- 已验证无效: W={edit.gate_w} L={edit.gate_l} "
|
||
f"E={edit.gate_e_value:.2f} δ̂={edit.gate_delta_shrunk:+.3f}"
|
||
)
|
||
lines.append("")
|
||
return "\n".join(lines)
|
||
|
||
|
||
# =========================================================================
|
||
# I. 进化循环内部类型
|
||
# =========================================================================
|
||
|
||
|
||
@dataclass
|
||
class _PatchEvolutionAttempt:
|
||
"""单次补丁式进化尝试的中间结果。
|
||
|
||
属性:
|
||
evolved_content: 改写后内容。
|
||
validation: 校验结果。
|
||
suggestions: LLM 输出的改动建议列表。
|
||
edits: LLM 输出的补丁列表。
|
||
apply_report: 补丁逐条应用状态。
|
||
clip_info: 超预算裁剪信息。
|
||
"""
|
||
|
||
evolved_content: str
|
||
validation: ValidationResult
|
||
suggestions: list[dict[str, Any]] = field(default_factory=list)
|
||
edits: list[dict[str, Any]] = field(default_factory=list)
|
||
apply_report: list[dict[str, Any]] = field(default_factory=list)
|
||
clip_info: dict[str, Any] = field(default_factory=lambda: {"triggered": False, "clipped": 0})
|
||
|
||
|
||
# =========================================================================
|
||
# J. 进化循环辅助函数
|
||
# =========================================================================
|
||
|
||
|
||
def _count_applied_reports(reports: list[dict[str, Any]]) -> int:
|
||
"""统计补丁报告中成功应用的条数。
|
||
|
||
以 ``status`` 前缀 ``"applied"`` 为判据,涵盖 applied_append /
|
||
applied_replace / applied_rewrite 等所有成功状态。
|
||
|
||
参数:
|
||
reports: apply_patch_with_report 或合成报告的列表。
|
||
|
||
返回:
|
||
成功应用的条数。
|
||
"""
|
||
return sum(1 for r in reports if r["status"].startswith("applied"))
|
||
|
||
|
||
def _with_report_source(reports: list[dict[str, Any]], source: str) -> list[dict[str, Any]]:
|
||
"""给补丁报告补上来源字段(extract / verify 标注)。
|
||
|
||
参数:
|
||
reports: 原始补丁报告列表。
|
||
source: 来源标签("extract" / "verify")。
|
||
|
||
返回:
|
||
每条追加 ``"source"`` 字段的新列表。
|
||
"""
|
||
return [{**r, "source": source} for r in reports]
|
||
|
||
|
||
def _append_retry_messages(
|
||
messages: list[dict[str, Any]],
|
||
raw_content: str,
|
||
feedback: str,
|
||
) -> None:
|
||
"""向对话中追加一次失败后的重试反馈(assistant + user)。
|
||
|
||
参数:
|
||
messages: 当前对话消息列表(原地修改)。
|
||
raw_content: 上一轮 LLM 原始输出。
|
||
feedback: 给 LLM 的纠正反馈。
|
||
"""
|
||
messages.append({"role": "assistant", "content": raw_content})
|
||
messages.append({"role": "user", "content": feedback})
|
||
|
||
|
||
# =========================================================================
|
||
# K. 补丁进化循环
|
||
# =========================================================================
|
||
|
||
|
||
async def _run_patch_evolution_loop(
|
||
*,
|
||
llm: LLMProvider,
|
||
messages: list[dict[str, Any]],
|
||
attempts: list[dict[str, Any]],
|
||
target_file: str,
|
||
target_type: str,
|
||
original_content: str,
|
||
source_version: str,
|
||
log_target: str,
|
||
attempt_builder: Callable[[dict[str, Any]], Awaitable[_PatchEvolutionAttempt]],
|
||
) -> EvolutionRecord:
|
||
"""执行带补丁应用与 no-op 重试的两轮进化循环。
|
||
|
||
恰好 2 次尝试(range(2)),三种失败模式各有对应重试提示:
|
||
1. JSON 解析失败 → "你的输出不是合法 JSON,请重新输出"
|
||
2. 0 条 applied 补丁 → "你的 edit 的 target 都没在原文中匹配到…"
|
||
3. 校验失败 → 具体校验错误文本
|
||
|
||
首轮失败追加重试提示继续第二轮;第二轮失败直接 reject。
|
||
校验通过立即返回 accepted。
|
||
|
||
参数:
|
||
llm: LLM 调用端口。
|
||
messages: 对话消息列表(原地修改,追加重试上下文)。
|
||
attempts: 尝试摘要列表(原地追加)。
|
||
target_file: 目标文件名。
|
||
target_type: 目标类型(skill / system / tool)。
|
||
original_content: 改写前原文。
|
||
source_version: 改写前版本号。
|
||
log_target: 日志标签。
|
||
attempt_builder: 异步构建尝试的回调,接收 parsed JSON dict。
|
||
|
||
返回:
|
||
EvolutionRecord 实例。
|
||
"""
|
||
for attempt_idx in range(2):
|
||
response = await llm.chat(messages)
|
||
raw_content = response.content
|
||
attempts.append({"attempt": attempt_idx + 1, "raw_length": len(raw_content)})
|
||
parsed = _parse_llm_json(raw_content)
|
||
|
||
# 失败模式 1:JSON 解析失败
|
||
if parsed is None:
|
||
logger.warning("{} 进化 LLM 响应 JSON 解析失败: {}", target_type, log_target)
|
||
if attempt_idx == 0:
|
||
_append_retry_messages(
|
||
messages,
|
||
raw_content,
|
||
"你的输出不是合法 JSON,请重新输出。",
|
||
)
|
||
continue
|
||
return EvolutionRecord(
|
||
target_file=target_file,
|
||
target_type=target_type,
|
||
original_content=original_content,
|
||
evolved_content=original_content,
|
||
reason="LLM 响应 JSON 解析失败",
|
||
status="rejected",
|
||
source_version=source_version,
|
||
attempts=attempts,
|
||
validation_errors=["JSON 解析失败"],
|
||
)
|
||
|
||
attempt = await attempt_builder(parsed)
|
||
|
||
# 失败模式 2:0 条 applied 补丁
|
||
if _count_applied_reports(attempt.apply_report) == 0:
|
||
if attempt_idx == 0:
|
||
_append_retry_messages(
|
||
messages,
|
||
raw_content,
|
||
"你的 edit 的 target 都没在原文中匹配到,请逐字摘抄原文锚点后重输。",
|
||
)
|
||
continue
|
||
return EvolutionRecord(
|
||
target_file=target_file,
|
||
target_type=target_type,
|
||
original_content=original_content,
|
||
evolved_content=original_content,
|
||
reason="补丁无有效改动(target 全未匹配)",
|
||
status="rejected",
|
||
source_version=source_version,
|
||
suggestions=attempt.suggestions,
|
||
attempts=attempts,
|
||
edits=attempt.edits,
|
||
apply_report=attempt.apply_report,
|
||
clip_info=attempt.clip_info,
|
||
)
|
||
|
||
# 成功:校验通过
|
||
if attempt.validation.passed:
|
||
return EvolutionRecord(
|
||
target_file=target_file,
|
||
target_type=target_type,
|
||
original_content=original_content,
|
||
evolved_content=attempt.evolved_content,
|
||
reason="验证通过",
|
||
status="accepted",
|
||
source_version=source_version,
|
||
suggestions=attempt.suggestions,
|
||
attempts=attempts,
|
||
edits=attempt.edits,
|
||
apply_report=attempt.apply_report,
|
||
clip_info=attempt.clip_info,
|
||
)
|
||
|
||
# 失败模式 3:校验失败
|
||
error_feedback = "\n".join(attempt.validation.errors)
|
||
if attempt_idx == 0:
|
||
_append_retry_messages(
|
||
messages,
|
||
raw_content,
|
||
f"验证失败,请修正后重新输出:\n{error_feedback}",
|
||
)
|
||
continue
|
||
return EvolutionRecord(
|
||
target_file=target_file,
|
||
target_type=target_type,
|
||
original_content=original_content,
|
||
evolved_content=original_content,
|
||
reason="验证失败(重试后仍未通过)",
|
||
status="rejected",
|
||
source_version=source_version,
|
||
suggestions=attempt.suggestions,
|
||
attempts=attempts,
|
||
validation_errors=attempt.validation.errors,
|
||
edits=attempt.edits,
|
||
apply_report=attempt.apply_report,
|
||
clip_info=attempt.clip_info,
|
||
)
|
||
|
||
# 兜底(正常流程不可达)
|
||
return EvolutionRecord(
|
||
target_file=target_file,
|
||
target_type=target_type,
|
||
original_content=original_content,
|
||
evolved_content=original_content,
|
||
reason="未知错误",
|
||
status="rejected",
|
||
source_version=source_version,
|
||
attempts=attempts,
|
||
)
|
||
|
||
|
||
# =========================================================================
|
||
# L. Lapse-only 尝试构建
|
||
# =========================================================================
|
||
|
||
|
||
def _build_lapse_only_attempt(
|
||
original_content: str,
|
||
lapse_notes: list[str],
|
||
) -> _PatchEvolutionAttempt:
|
||
"""构造「仅 appendix 更新」尝试:无 defect edit,只把 lapse 提醒落进受保护区。
|
||
|
||
LLM 没给 defect edit 但有 lapse 提醒时,正常补丁路径会因 0 条 applied 报告被
|
||
``_run_patch_evolution_loop`` 判为 no-op 而丢弃。这里给出一条 ``applied_append``
|
||
合成报告(前缀 ``applied`` 使 ``_count_applied_reports > 0``),令该记录走
|
||
accepted 路径、appendix 真正落盘。
|
||
|
||
参数:
|
||
original_content: 改写前全文。
|
||
lapse_notes: 待落 appendix 的 LAPSE 提醒。
|
||
|
||
返回:
|
||
含 appendix 更新内容与合成 apply_report 的 _PatchEvolutionAttempt。
|
||
"""
|
||
evolved_content = append_to_appendix(original_content, lapse_notes)
|
||
apply_report = [
|
||
{
|
||
"op": "append",
|
||
"target": "",
|
||
"content_preview": "appendix LAPSE 提醒",
|
||
"status": "applied_append",
|
||
"index": 1,
|
||
}
|
||
]
|
||
return _PatchEvolutionAttempt(
|
||
evolved_content=evolved_content,
|
||
validation=validate_skill(original_content, evolved_content),
|
||
suggestions=[],
|
||
edits=[],
|
||
apply_report=apply_report,
|
||
clip_info={"triggered": False, "clipped": 0},
|
||
)
|
||
|
||
|
||
# =========================================================================
|
||
# M. Appendix consolidation
|
||
# =========================================================================
|
||
|
||
|
||
_CONSOLIDATE_SYSTEM = (
|
||
"你在压缩一个 agent skill 的「执行提醒 appendix」。每条提醒都重申一条 skill 已有"
|
||
"规则、是 agent 没遵循的点。你的任务是周期性压缩:去重、合并近义、精简措辞,但"
|
||
"保留每条的可执行性。禁止发明新规则;禁止写入任何具体题目/选项/实体名等案例事实。"
|
||
"只返回 JSON。"
|
||
)
|
||
|
||
|
||
async def consolidate_appendix(llm: LLMProvider, notes: list[str]) -> list[str]:
|
||
"""LLM 压缩 appendix notes(去重/合并/精简),失败永不丢内容。
|
||
|
||
四关守卫(对标 TRM4 consolidate_appendix):
|
||
G1. clean 后 <2 条直接短路返回(无需压缩,不调 LLM)。
|
||
G2. 只接受「非空且 len(compacted) <= len(clean)」的压缩结果。
|
||
G3. 任何异常(解析/空/网络)→ 返回 clean(绝不丢 appendix)。
|
||
G4. 在调用方 _append_lapse_with_consolidation 中:
|
||
len(compacted) >= len(notes) → 拒绝等长压缩。
|
||
|
||
参数:
|
||
llm: LLM 调用端口。
|
||
notes: 待压缩的 appendix 提醒列表。
|
||
|
||
返回:
|
||
压缩后的提醒列表;任何守卫未通过时返回 clean 后的原 notes。
|
||
"""
|
||
# G1:clean 后不足 2 条,直接返回
|
||
clean = [str(n).strip() for n in (notes or []) if str(n).strip()]
|
||
if len(clean) < 2:
|
||
return clean
|
||
|
||
numbered = "\n".join(f"{i}. {n}" for i, n in enumerate(clean, 1))
|
||
user = (
|
||
f"## 当前执行提醒(共 {len(clean)} 条)\n{numbered}\n\n"
|
||
"压缩为更短的列表,不丢失可执行信息;合并重复与近义;保持每条简短具体可复用。"
|
||
'只返回 JSON:{ "appendix_notes": ["压缩后提醒1", "压缩后提醒2"] }'
|
||
)
|
||
try:
|
||
response = await llm.chat(
|
||
[
|
||
{"role": "system", "content": _CONSOLIDATE_SYSTEM},
|
||
{"role": "user", "content": user},
|
||
]
|
||
)
|
||
parsed = _parse_llm_json(response.content)
|
||
compacted = [
|
||
str(n).strip() for n in (parsed or {}).get("appendix_notes", []) if str(n).strip()
|
||
]
|
||
# G2:非空且确实压缩了
|
||
if compacted and len(compacted) <= len(clean):
|
||
return compacted
|
||
except Exception as exc: # noqa: BLE001
|
||
# G3:任何失败降级为保留原 notes。设计授权的优雅降级(非 P5 违规):
|
||
# consolidation 是纯优化,失败不应中断 evolve 或丢 appendix;记 warning 非静默。
|
||
logger.warning("appendix consolidation 失败,保留原 notes:{}", exc)
|
||
return clean
|
||
|
||
|
||
async def _append_lapse_with_consolidation(
|
||
text: str,
|
||
lapse_notes: list[str],
|
||
llm: LLMProvider,
|
||
consolidate_threshold: int,
|
||
) -> str:
|
||
"""把 lapse 提醒追加进 appendix,超阈值时触发 LLM consolidation。
|
||
|
||
回写侧二确认——即便 consolidate_appendix 守卫已保证 <=,这里再校验「确实
|
||
变短」才 replace,避免等长压缩带来无意义改写抖动(守卫 G4)。
|
||
|
||
参数:
|
||
text: 待追加的 skill 全文(正文已改完)。
|
||
lapse_notes: 本轮待落 appendix 的 LAPSE 提醒。
|
||
llm: LLM 调用端口,供 consolidation 使用。
|
||
consolidate_threshold: appendix note 条数 >= 此值时触发压缩。
|
||
|
||
返回:
|
||
追加(必要时压缩)后的 skill 全文。
|
||
"""
|
||
after = append_to_appendix(text, lapse_notes)
|
||
notes = extract_appendix_notes(after)
|
||
if len(notes) >= consolidate_threshold:
|
||
compacted = await consolidate_appendix(llm, notes)
|
||
# G4:压缩结果必须严格变短才替换
|
||
if len(compacted) < len(notes):
|
||
after = replace_appendix_notes(after, compacted)
|
||
return after
|
||
|
||
|
||
# =========================================================================
|
||
# N. 整篇重写
|
||
# =========================================================================
|
||
|
||
|
||
_REWRITE_SYSTEM = (
|
||
"你负责根据改动建议整篇重写 Agent Skill 文件。保留 frontmatter(---...---)中的 "
|
||
"name / description / task_type 不变。保持文件精简,重写后长度不得超过原文。"
|
||
'只返回 JSON:{ "rewritten": "重写后的完整文件内容" }'
|
||
)
|
||
|
||
|
||
async def rewrite_from_suggestions(
|
||
llm: LLMProvider,
|
||
original: str,
|
||
suggestions: list[dict[str, Any]],
|
||
) -> str:
|
||
"""从抽象 suggestion 整篇重写 Skill;校验失败回退原文(skill 不变)。
|
||
|
||
硬约束由系统提示下达 + 本函数校验双重保证。三条拒绝条件任一触发
|
||
即返回原文(保守不改):
|
||
1. 解析失败(JSON / rewritten 字段缺失或非字符串)
|
||
2. 重写后长度 > 原文
|
||
3. validate_skill 校验不过
|
||
|
||
仅捕获 ValueError / KeyError / TypeError / AttributeError;API 错误向上传播。
|
||
|
||
参数:
|
||
llm: LLM 调用端口。
|
||
original: 改写前 Skill 文件全文。
|
||
suggestions: 抽象改动建议列表。
|
||
|
||
返回:
|
||
校验通过的重写全文;任一守卫未通过时返回 original。
|
||
"""
|
||
sugg_text = "\n".join(f"- {s.get('change', '')}" for s in (suggestions or []))
|
||
user_msg = f"## 当前 Skill 文件\n\n{original}\n\n## 改动建议\n\n{sugg_text or '(无)'}"
|
||
try:
|
||
response = await llm.chat(
|
||
[
|
||
{"role": "system", "content": _REWRITE_SYSTEM},
|
||
{"role": "user", "content": user_msg},
|
||
]
|
||
)
|
||
parsed = _parse_llm_json(response.content)
|
||
rewritten = (parsed or {}).get("rewritten")
|
||
if not isinstance(rewritten, str) or not rewritten.strip():
|
||
raise ValueError("rewrite 未返回非空 rewritten 字符串")
|
||
except (ValueError, KeyError, TypeError, AttributeError):
|
||
logger.warning("rewrite 解析失败,回退原文")
|
||
return original
|
||
|
||
# 拒绝条件 2:不许变长
|
||
if len(rewritten) > len(original):
|
||
logger.warning("rewrite 变长({}->{}),回退原文", len(original), len(rewritten))
|
||
return original
|
||
# 拒绝条件 3:冻结区/格式校验
|
||
if not validate_skill(original, rewritten).passed:
|
||
logger.warning("rewrite 校验未过(冻结区/格式),回退原文")
|
||
return original
|
||
return rewritten
|
||
|
||
|
||
# =========================================================================
|
||
# O. 单目标进化函数
|
||
# =========================================================================
|
||
|
||
|
||
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:
|
||
"""进化单个 Skill 文件。
|
||
|
||
三分支构建:
|
||
A. lapse-only:无 defect edit + 有 lapse_notes → 仅 appendix 更新。
|
||
B. rewrite:mode="rewrite" + 有 edit → 整篇重写;失败回退 A 或 no-op。
|
||
C. patch(默认):rank_and_clip → apply_patch_with_report。
|
||
分支 B/C 完成后,若有 lapse_notes,追加 appendix(超阈值 consolidation)。
|
||
|
||
用户消息结构(按顺序):
|
||
1. (可选)黑名单
|
||
2. 当前 Skill 文件原文
|
||
3. 聚合统计 JSON
|
||
4. 失败案例
|
||
5. 成功案例
|
||
|
||
参数:
|
||
llm: LLM 调用端口。
|
||
pack: 该题型的案例包。
|
||
skill_store: 版本化技能读取端口。
|
||
prompts: 进化模板束。
|
||
source_version: 改写前版本号。
|
||
edit_budget: per-target 编辑预算上限。
|
||
consolidate_threshold: appendix note 条数 >= 此值触发 consolidation。
|
||
skill_update_mode: 正文更新模式,"patch"(局部 edit)/ "rewrite"(整篇重写)。
|
||
rejected: 已验证无效的历史改法列表。
|
||
|
||
返回:
|
||
EvolutionRecord 实例。
|
||
"""
|
||
target_file = pack.target_file
|
||
original_content = skill_store.read_skill(target_file)
|
||
|
||
# 构建用户消息
|
||
stats_json = json.dumps(pack.stats, ensure_ascii=False, indent=2)
|
||
user_msg = (
|
||
f"## 当前 Skill 文件\n\n{original_content}\n\n"
|
||
f"## 聚合统计\n\n```json\n{stats_json}\n```\n\n"
|
||
f"## 失败案例\n\n{_format_case_samples(pack.failure_cases)}\n\n"
|
||
f"## 成功案例\n\n{_format_case_samples(pack.success_cases)}"
|
||
)
|
||
rejected = rejected or []
|
||
if rejected:
|
||
user_msg = (
|
||
"## 已验证无效的改法(黑名单,勿重复)\n\n"
|
||
+ _format_rejected_edits(rejected)
|
||
+ "\n\n"
|
||
+ user_msg
|
||
)
|
||
|
||
messages: list[dict[str, Any]] = [
|
||
{"role": "system", "content": prompts.evolve_skill},
|
||
{"role": "user", "content": user_msg},
|
||
]
|
||
attempts: list[dict[str, Any]] = []
|
||
|
||
async def _build_attempt(parsed: dict[str, Any]) -> _PatchEvolutionAttempt:
|
||
suggestions = parsed.get("suggestions", [])
|
||
edits = parsed.get("edits", [])
|
||
|
||
# 分支 A:lapse-only(无 defect edit、仅有 lapse 提醒)
|
||
if not edits and pack.lapse_notes:
|
||
return _build_lapse_only_attempt(original_content, pack.lapse_notes)
|
||
|
||
# 分支 B:rewrite 模式(有 defect edits 时整篇重写)
|
||
if skill_update_mode == "rewrite" and edits:
|
||
rewritten = await rewrite_from_suggestions(llm, original_content, suggestions)
|
||
if rewritten == original_content:
|
||
# 重写校验失败/变长/解析失败 → 正文无改动
|
||
if pack.lapse_notes:
|
||
return _build_lapse_only_attempt(original_content, pack.lapse_notes)
|
||
return _PatchEvolutionAttempt(
|
||
evolved_content=original_content,
|
||
validation=validate_skill(original_content, original_content),
|
||
suggestions=suggestions,
|
||
edits=[],
|
||
apply_report=[],
|
||
clip_info={"triggered": False, "clipped": 0},
|
||
)
|
||
evolved_content = rewritten
|
||
apply_report = [
|
||
{
|
||
"op": "rewrite",
|
||
"target": "",
|
||
"content_preview": "整篇重写",
|
||
"status": "applied_rewrite",
|
||
"index": 1,
|
||
}
|
||
]
|
||
clip_info: dict[str, Any] = {"triggered": False, "clipped": 0}
|
||
|
||
# 分支 C:patch 模式(默认)
|
||
else:
|
||
edits, clip_info = await rank_and_clip(
|
||
llm,
|
||
original_content,
|
||
edits,
|
||
edit_budget,
|
||
"skill",
|
||
rank_prompt=prompts.evolve_rank,
|
||
)
|
||
evolved_content, apply_report = apply_patch_with_report(
|
||
original_content,
|
||
edits,
|
||
protected_spans=_skill_protected_spans(original_content),
|
||
)
|
||
|
||
# 分支 B/C 完成后:lapse 提醒追加 + consolidation
|
||
if pack.lapse_notes:
|
||
evolved_content = await _append_lapse_with_consolidation(
|
||
evolved_content,
|
||
pack.lapse_notes,
|
||
llm,
|
||
consolidate_threshold,
|
||
)
|
||
|
||
return _PatchEvolutionAttempt(
|
||
evolved_content=evolved_content,
|
||
validation=validate_skill(original_content, evolved_content),
|
||
suggestions=suggestions,
|
||
edits=edits,
|
||
apply_report=apply_report,
|
||
clip_info=clip_info,
|
||
)
|
||
|
||
return await _run_patch_evolution_loop(
|
||
llm=llm,
|
||
messages=messages,
|
||
attempts=attempts,
|
||
target_file=target_file,
|
||
target_type="skill",
|
||
original_content=original_content,
|
||
source_version=source_version,
|
||
log_target=target_file,
|
||
attempt_builder=_build_attempt,
|
||
)
|
||
|
||
|
||
async def evolve_system_prompt(
|
||
llm: LLMProvider,
|
||
pack: SystemCasePack,
|
||
prompt_store: PromptStore,
|
||
prompts: EvolvePrompts,
|
||
source_version: str,
|
||
edit_budget: int,
|
||
) -> EvolutionRecord:
|
||
"""进化 System Prompt。
|
||
|
||
无 lapse notes、无 appendix consolidation、无 rewrite 模式。
|
||
使用 system protected spans 保护冻结区。
|
||
用户消息中统计标题为「D5 行为模式统计」。
|
||
|
||
参数:
|
||
llm: LLM 调用端口。
|
||
pack: 跨题型行为模式案例包。
|
||
prompt_store: 版本化提示词读取端口。
|
||
prompts: 进化模板束。
|
||
source_version: 改写前版本号。
|
||
edit_budget: per-target 编辑预算上限。
|
||
|
||
返回:
|
||
EvolutionRecord 实例。
|
||
"""
|
||
target_file = "system.md"
|
||
original_content = prompt_store.read_prompt(target_file)
|
||
|
||
stats_json = json.dumps(pack.stats, ensure_ascii=False, indent=2)
|
||
user_msg = (
|
||
f"## 当前 System Prompt\n\n{original_content}\n\n"
|
||
f"## D5 行为模式统计\n\n```json\n{stats_json}\n```\n\n"
|
||
f"## 失败案例\n\n{_format_case_samples(pack.failure_cases)}\n\n"
|
||
f"## 成功案例\n\n{_format_case_samples(pack.success_cases)}"
|
||
)
|
||
|
||
messages: list[dict[str, Any]] = [
|
||
{"role": "system", "content": prompts.evolve_system},
|
||
{"role": "user", "content": user_msg},
|
||
]
|
||
attempts: list[dict[str, Any]] = []
|
||
|
||
async def _build_attempt(parsed: dict[str, Any]) -> _PatchEvolutionAttempt:
|
||
suggestions = parsed.get("suggestions", [])
|
||
edits = parsed.get("edits", [])
|
||
edits, clip_info = await rank_and_clip(
|
||
llm,
|
||
original_content,
|
||
edits,
|
||
edit_budget,
|
||
"system",
|
||
rank_prompt=prompts.evolve_rank,
|
||
)
|
||
evolved_content, apply_report = apply_patch_with_report(
|
||
original_content,
|
||
edits,
|
||
protected_spans=_system_protected_spans(original_content),
|
||
)
|
||
return _PatchEvolutionAttempt(
|
||
evolved_content=evolved_content,
|
||
validation=validate_system(original_content, evolved_content),
|
||
suggestions=suggestions,
|
||
edits=edits,
|
||
apply_report=apply_report,
|
||
clip_info=clip_info,
|
||
)
|
||
|
||
return await _run_patch_evolution_loop(
|
||
llm=llm,
|
||
messages=messages,
|
||
attempts=attempts,
|
||
target_file=target_file,
|
||
target_type="system",
|
||
original_content=original_content,
|
||
source_version=source_version,
|
||
log_target=target_file,
|
||
attempt_builder=_build_attempt,
|
||
)
|
||
|
||
|
||
async def evolve_single_tool(
|
||
llm: LLMProvider,
|
||
pack: ToolCasePack,
|
||
prompt_store: PromptStore,
|
||
prompts: EvolvePrompts,
|
||
source_version: str,
|
||
edit_budget: int,
|
||
) -> EvolutionRecord:
|
||
"""进化单个工具的 extract + verify prompt。
|
||
|
||
extract 与 verify 的 edits 合并到 SHARED 预算池(打 ``_src`` 标签),
|
||
整体 rank_and_clip 到 edit_budget 后按 ``_src`` 拆回各自文件应用。
|
||
``evolved_content`` 以 ``json.dumps({"extract": ..., "verify": ...})`` 存储。
|
||
``target_file`` 固定为 ``{tool_name}_extract.md``。
|
||
apply_report 每条带 ``"source"`` 注解("extract" / "verify")。
|
||
|
||
参数:
|
||
llm: LLM 调用端口。
|
||
pack: 该工具的案例包。
|
||
prompt_store: 版本化提示词读取端口。
|
||
prompts: 进化模板束。
|
||
source_version: 改写前版本号。
|
||
edit_budget: per-target 编辑预算上限(extract + verify 共享)。
|
||
|
||
返回:
|
||
EvolutionRecord 实例。
|
||
"""
|
||
tool_name = pack.tool_name
|
||
target_file = f"{tool_name}_extract.md"
|
||
orig_extract = prompt_store.read_prompt(f"{tool_name}_extract.md")
|
||
orig_verify = prompt_store.read_prompt(f"{tool_name}_verify.md")
|
||
original_combined = json.dumps(
|
||
{"extract": orig_extract, "verify": orig_verify},
|
||
ensure_ascii=False,
|
||
)
|
||
|
||
stats_json = json.dumps(pack.stats, ensure_ascii=False, indent=2)
|
||
user_msg = (
|
||
f"## 当前 extract prompt\n\n{orig_extract}\n\n"
|
||
f"## 当前 verify prompt\n\n{orig_verify}\n\n"
|
||
f"## 工具质量统计\n\n```json\n{stats_json}\n```\n\n"
|
||
f"## 失败 span 案例\n\n{_format_spans(pack.failure_spans)}\n\n"
|
||
f"## 成功 span 案例\n\n{_format_spans(pack.success_spans)}"
|
||
)
|
||
|
||
messages: list[dict[str, Any]] = [
|
||
{"role": "system", "content": prompts.evolve_tool},
|
||
{"role": "user", "content": user_msg},
|
||
]
|
||
attempts: list[dict[str, Any]] = []
|
||
|
||
async def _build_attempt(parsed: dict[str, Any]) -> _PatchEvolutionAttempt:
|
||
suggestions = parsed.get("suggestions", [])
|
||
edits_extract = parsed.get("edits_extract", [])
|
||
edits_verify = parsed.get("edits_verify", [])
|
||
|
||
# 合并打来源标记,对单 tool target 整体裁到 edit_budget
|
||
pool: list[dict[str, Any]] = [{**e, "_src": "extract"} for e in edits_extract] + [
|
||
{**e, "_src": "verify"} for e in edits_verify
|
||
]
|
||
|
||
pool, clip_info = await rank_and_clip(
|
||
llm,
|
||
original_combined,
|
||
pool,
|
||
edit_budget,
|
||
"tool",
|
||
rank_prompt=prompts.evolve_rank,
|
||
)
|
||
|
||
# 按 _src 拆回,并剥离 _src 字段
|
||
extract_kept = [
|
||
{k: v for k, v in e.items() if k != "_src"} for e in pool if e["_src"] == "extract"
|
||
]
|
||
verify_kept = [
|
||
{k: v for k, v in e.items() if k != "_src"} for e in pool if e["_src"] == "verify"
|
||
]
|
||
|
||
# 分别应用,使用各自的 tool protected spans
|
||
evolved_extract, extract_report = apply_patch_with_report(
|
||
orig_extract,
|
||
extract_kept,
|
||
protected_spans=_tool_protected_spans(orig_extract),
|
||
)
|
||
evolved_verify, verify_report = apply_patch_with_report(
|
||
orig_verify,
|
||
verify_kept,
|
||
protected_spans=_tool_protected_spans(orig_verify),
|
||
)
|
||
|
||
# 报告注解来源
|
||
apply_report = _with_report_source(extract_report, "extract") + _with_report_source(
|
||
verify_report, "verify"
|
||
)
|
||
|
||
evolved_combined = json.dumps(
|
||
{"extract": evolved_extract, "verify": evolved_verify},
|
||
ensure_ascii=False,
|
||
)
|
||
validation = validate_tool(orig_extract, evolved_extract, orig_verify, evolved_verify)
|
||
return _PatchEvolutionAttempt(
|
||
evolved_content=evolved_combined,
|
||
validation=validation,
|
||
suggestions=suggestions,
|
||
edits=extract_kept + verify_kept,
|
||
apply_report=apply_report,
|
||
clip_info=clip_info,
|
||
)
|
||
|
||
return await _run_patch_evolution_loop(
|
||
llm=llm,
|
||
messages=messages,
|
||
attempts=attempts,
|
||
target_file=target_file,
|
||
target_type="tool",
|
||
original_content=original_combined,
|
||
source_version=source_version,
|
||
log_target=tool_name,
|
||
attempt_builder=_build_attempt,
|
||
)
|