跳到主要内容

自进化引擎:FIX / DERIVED / CAPTURED 与版本 DAG

30 秒导读: 这是 OpenSpace 的灵魂章。别的 agent 框架把技能(SKILL.md)当成人写死的静态文件; OpenSpace 让技能自己进化——每跑完一个任务,系统就用一次 LLM 复盘,判断这次用到的技能是该 修(FIX)、该衍生新版(DERIVED)、还是该把新套路捕获成新技能(CAPTURED),然后自动 生成改动、落盘、验证、把新版本连同"从谁演化来"的血缘一起存进数据库。本章讲清这条 失败 → 分析 → 定向 diff → 验证 → 版本入库 → lineage 追踪 的闭环。

本章属于 OpenSpace 系列。它的上游是主循环(任务怎么执行、怎么被录制) 和技能检索与注入(技能怎么被选中喂给 agent);它的下游边界是 工具接地层(工具质量监控如何反过来当进化触发器——本章只点到,细节看第 4 章)。 全景总览见 index


1. 这是什么(零基础也能懂)

一句话定义

自进化引擎 = 技能的"复盘 + 改稿"系统。 一次任务跑完,它像一个复盘教练:回看整场录像, 判断"这次用的攻略行不行",然后要么改攻略、要么写本进阶攻略、要么把刚摸索出的新打法记成一份新攻略。

它解决什么问题

人写的 SKILL.md 会过时、会不够用、会漏掉刚被 agent 现场摸索出来的好套路。手工维护跟不上。 自进化引擎让这三件事自动发生:

问题现象对应进化
技能坏了 / 过时攻略里的 curl 参数格式变了、工具退化了FIX(原地修,同名同目录)
技能不够强能用但常常做不完,或两个技能该合成一个DERIVED(衍生新版,新目录)
冒出新套路这次任务没靠现成技能,却摸索出一条可复用的路子CAPTURED(捕获成全新技能)

一句话直觉

把它想成 git for skills,而且提交者是 AI:每次进化都创建一个新的技能"版本节点",记录 它的父节点、改了什么(content_diff)、当时的完整快照(content_snapshot)。所有节点连成一张 版本 DAG(有向无环图),你随时能回溯一个技能"从哪进化来、改过几手"。

用起来什么样

这套系统对用户是透明的——它挂在主循环的 finally 里,任务一结束就自动跑。你只会在日志里 看到类似这样的痕迹(源自各处 logger.info):

Execution analysis saved for task t-42: completed=True, skills_judged=2, evolution_suggestions=['fix']
[Skill Evolution] Suggestions: fix(weather__imp_a1b2c3d4)
FIX: weather gen0 → gen1 [weather__v1_9f3c2e18]
evolve_skill (fixed): weather@gen1 [weather__v1_9f3c2e18] ← parents=['weather__imp_a1b2c3d4']

一次任务,一份分析,零到多条进化建议,每条建议落成一个新的技能版本。

本节不碰代码细节;下面开始由浅入深往里钻。


2. 顶层全景(这条闭环怎么转)

怎么读这张图

从上到下是时间顺序:主循环结束 → 分析 → 进化。左边一列是"分析阶段"(诊断),右边一列是 "进化阶段"(动手改)。分析产出结构化建议,进化把每条建议变成磁盘上的真实改动 + 数据库里的新版本。

┌─────────────────────── 主循环 execute() 的 finally ───────────────────────┐
│ 录制停止 → tool_layer.py:_maybe_analyze_execution(787) (带超时保护) │
└───────────────────────────────────┬───────────────────────────────────────┘

┌────────────────────────▼─────────────────────────┐
│ ① 分析阶段 ExecutionAnalyzer.analyze_execution │
│ analyzer.py:154 │
│ · 读录制轨迹 _load_recording_context(329) │
│ · 拼分析 prompt _build_analysis_prompt(508) │
│ · 跑分析 agent 循环(可调工具复现错误) │
│ · 解析成 ExecutionAnalysis _parse_analysis(841) │
└────────────────────────┬─────────────────────────┘
│ ExecutionAnalysis
│ .evolution_suggestions = [ {fix|derived|captured}, ... ]

┌──────────────────────────────────────────────────┐
│ ② 进化阶段 SkillEvolver │
│ │
│ 三个入口(触发器) │
│ · process_analysis(266) ← 本次分析(触发1)│
│ · process_tool_degradation(310)← 工具退化(触发2)│
│ · process_metric_check(441) ← 定期体检(触发3)│
│ │ │
│ ▼ evolve(ctx) │
│ ┌───────────┼───────────┐ │
│ _evolve_fix _evolve_ _evolve_captured │
│ (702) derived(796) (928) │
│ └───────────┼───────────┘ │
│ ▼ │
│ _run_evolution_loop(1084) 产出改动文本 │
│ _apply_with_retry(1271) 落盘+校验+失败重试 │
│ │ │ │
│ ▼ ▼ │
│ patch.py store.py:evolve_skill(484) │
│ fix/derive/ 新版本入库 + 旧版失活 │
│ create_skill + lineage 血缘 │
└──────────────────────────────────────────────────┘

部件一句话职责

部件干什么在哪个文件
_maybe_analyze_execution触发链起点:任务结束后拉起分析+进化,失败绝不影响主流程openspace/tool_layer.py:787
ExecutionAnalyzer复盘一次执行,产出 ExecutionAnalysis(含进化建议)openspace/skill_engine/analyzer.py:128
SkillEvolver执行进化:三模式 × 三触发,一直到落盘入库openspace/skill_engine/evolver.py:154
patch 模块把 LLM 的改动文本(FULL/DIFF/PATCH)真正应用到技能目录openspace/skill_engine/patch.py
SkillStoreSQLite 持久化 + 版本 DAG(父子血缘、质量计数)openspace/skill_engine/store.py
types全套数据模型:EvolutionType / EvolutionSuggestion / SkillLineage / SkillRecord / ExecutionAnalysisopenspace/skill_engine/types.py:11-465

主线走一遍(高层)

一次任务从 execute() 出来后,tool_layer.py:596-616finally 里先 await 分析+进化 (带 execution_analysis_timeout 超时兜底),再周期性做工具质量进化。整条链的第一句代码就是 _maybe_analyze_execution——它先把这次执行分析成一个 ExecutionAnalysis,如果里头有进化建议, 就交给 SkillEvolver.process_analysis 逐条落地(tool_layer.py:809-837)。


3. 触发链第一段:把一次执行复盘成结构化建议

这一节讲分析阶段:录制轨迹进,ExecutionAnalysis 出。它是整个引擎的"诊断"环节, 进化阶段完全靠它喂的建议驱动。

3.1 它要解决的小问题

主循环只留下一堆录制文件(录像),不是结论。要进化,先得有人回看录像、下判断: 任务完成了吗?哪个技能被真正用上了?哪个工具出了问题?该怎么进化?这份判断就是 ExecutionAnalysis(types.py:252)。

3.2 入口与去重

analyze_execution(task_id, recording_dir, execution_result, available_tools) 是入口 (analyzer.py:154)。它先做两件防御:录制目录不存在就跳过;一个任务只分析一次—— load_analyses_for_task 查到已有分析就直接返回(analyzer.py:184-188),避免重复烧钱。

3.3 读录制轨迹:把录像整理成上下文

_load_recording_context(analyzer.py:329)把录制目录里的几份文件拼成一个 context dict:

文件提供什么关键处理
metadata.json任务描述、选中的技能、检索到的工具、执行结果缺它直接放弃(analyzer.py:340-348)
conversations.jsonlagent 的完整对话轨迹(推理 + 工具调用)逐行 JSON 解析;为空则跳过分析
traj.jsonl结构化的工具调用记录(step / backend / tool / status)由此推出"实际用过哪些工具" used_tool_keys

一个巧妙点:分析不只看 agent 说了什么(conversations),还看它实际做了什么 (traj.jsonl 的每次工具调用及其成功/失败),两路证据交叉,判断更准。

3.4 构造分析 prompt:只喂相关信息

_build_analysis_prompt(analyzer.py:508)把 context 压成一份 prompt。它做了大量预算控制, 避免把 80K 字符的对话原样塞进去:

  • 对话日志走优先级截断(不是简单砍尾),由 conversation_formatter.py:format_conversations 实现——用户指令和最后一轮回复永不截断,工具错误优先保留,技能注入文本直接跳过 (conversation_formatter.py 顶部的优先级表 0–5)。
  • 工具列表按"实际用过 / 可用但没用"分组(_format_tool_list,analyzer.py:599),让 LLM 专注在 真正发生的事上。
  • 技能正文从磁盘按 skill_id 读回(_load_skill_contents_from_disk,analyzer.py:475),超长截断 并提示用 read_file 看全文。

3.5 分析循环:大多一次过,必要时可动手查

_run_analysis_loop(analyzer.py:710)是个小型 agent 循环(最多 5 轮)。多数分析一轮就出 JSON; 当轨迹有歧义时,LLM 可以调用执行时的那批工具(read_file/list_dir/run_shell 乃至 MCP 工具) 去复现错误、核对结果。最后一轮强制关掉工具、逼它输出 JSON(analyzer.py:751-760)。整个分析 对话也会被录进 conversations.jsonl(agent_name="ExecutionAnalyzer"),可回放调试。

3.6 解析成 ExecutionAnalysis

_parse_analysis(analyzer.py:841)把 LLM 的 JSON 转成强类型对象。这里有个很实用的容错: LLM 复述技能 ID 时经常把 hex 后缀写错(比如把 61f694cb 打成 61f694bc), _correct_skill_ids(analyzer.py:59)按"同名前缀 + 编辑距离 ≤ 阈值"做最近邻纠正,候选多时收紧阈值, 歧义则保留原样。这样后续进化才不会因为一个字符错位而找不到目标技能。

LLM 输出的 JSON 形状(见 prompts/skill_engine_prompts.py:308-336)大致是:

{
"task_completed": true,
"execution_note": "对执行质量与结果的 2-3 句概述",
"tool_issues": ["mcp:server:tool — 症状;可能原因"],
"skill_judgments": [
{"skill_id": "weather__imp_a1b2c3d4", "skill_applied": true, "note": "怎么用的、有无偏离"}
],
"evolution_suggestions": [
{"type": "fix", "target_skills": ["weather__imp_a1b2c3d4"], "category": "workflow", "direction": "修什么、为什么"}
]
}

其中 evolution_suggestions 就是喂给进化阶段的燃料。分析存库后(record_analysis),tool_issues 还会顺手喂给工具质量管理器(_record_tool_quality_feedback,analyzer.py:275),这条线是通往 第 4 章的交界——LLM 抓到的"语义级"工具故障会补充规则系统漏掉的部分,细节见 工具接地层


4. 三种进化模式:FIX / DERIVED / CAPTURED

这一节讲进化的"动词"。三种模式在 EvolutionType(types.py:26)里定义,分别对应 SkillOriginFIXED / DERIVED / CAPTURED(types.py:31-33to_origin)。

4.1 三者一眼对比

模式意图父节点数落到哪is_active 怎么变generation
FIX原地修坏/过时的指令恰好 1同名同目录(文件就地覆盖)旧版失活,新版激活父 + 1
DERIVED增强/特化/合并1 个(增强)或多个(合并)新名字新目录父保持激活,新版也激活max(父) + 1
CAPTURED捕获全新可复用套路0(无父)全新目录(host 技能目录)新版激活0(根节点)

这套血缘规则写在 SkillOrigin 的 docstring 里(types.py:36-56),是理解版本 DAG 的钥匙: FIXED 是同一条技能线的"下一个版本",DERIVED 是"分叉出的新技能",CAPTURED 是"新长出的根"。

4.2 FIX —— 原地修,换个版本号

_evolve_fix(evolver.py:702)要求 context 里恰好 1 个父技能。它把父目录的全部内容喂给 LLM (多文件技能也一并给),跑进化循环拿到新内容,经 _apply_with_retryfix_skill 就地覆盖磁盘。 关键处:

  • 落盘后重新从磁盘读回 name/description(evolver.py:746-748)——LLM 可能在修的过程中顺手改了描述。
  • skill_id 形如 {name}__v{gen+1}_{8位hex}(evolver.py:750),名字和路径都沿用父
  • 存库走 store.evolve_skill,并把新 skill_id 盖进 .skill_id 边车文件 (write_skill_id,evolver.py:779),这样下次 discover() 重启后仍认得。

4.3 DERIVED —— 衍生一份新技能

_evolve_derived(evolver.py:796)支持单父增强多父合并两种。多父时把每个父的目录内容都 拼进 prompt(evolver.py:810-818)。它比 FIX 多几步"起名字"的讲究:

  • 从新内容的 frontmatter 取 name;若没取到或跟父同名,就加 -enhanced / -merged 后缀 (evolver.py:838-842)。
  • _sanitize_skill_name(evolver.py:76)把名字规范成小写连字符,并截断到 50 字——专门防 panel-enhanced-enhanced-merged... 这种越衍生越长的链条。
  • 目录名永远等于技能名;撞名就加 6 位 hex(evolver.py:850-855)。
  • 新记录聚合所有父的工具依赖、critical tools、tags(evolver.py:873-882),generation = max(父)+1

4.4 CAPTURED —— 把新套路记成新技能

_evolve_captured(evolver.py:928)无父,靠分析里的任务描述当上下文(evolver.py:934-946)。 它最费思量的是"新技能写去哪个目录",有一条优先级链(evolver.py:966-983):

① ctx.capture_dir —— host agent 明确指定的技能目录(最高优先)
② 从分析推断 —— 这次任务用过的技能在目录 B,新技能就放 B 旁边
_infer_capture_dir_from_analysis(1045)
③ registry._skill_dirs[0] —— 兜底:注册的第一个技能根

这样捕获的技能会落在正确的宿主上下文里,而不是永远堆到第一个目录。落盘用 create_skill (多文件 FULL 格式),存库走 store.save_record(而非 evolve_skill,因为它是根节点)。


5. 三个触发器:同一进化内核,三种入口

三种模式回答"怎么改";三个触发器回答"什么时候、为什么改"。它们都汇聚到同一个 evolve(ctx)。 触发器定义在 EvolutionTrigger(evolver.py:113)。

5.1 分工一览

触发器何时触发怎么筛前台/后台入口
触发 1 · 分析每次任务分析完、且有建议直接采用 LLM 的建议前台 awaitprocess_analysis(266)
触发 2 · 工具退化工具质量管理器发现坏工具规则筛候选 → LLM 确认后台任务process_tool_degradation(310)
触发 3 · 定期体检每 5 次执行扫一遍活跃技能规则诊断健康度 → LLM 确认后台任务process_metric_check(441)

触发 1 由 _maybe_analyze_execution(tool_layer.py:835)前台调用;触发 2/3 由 _maybe_evolve_quality(tool_layer.py:856)通过 schedule_background 甩到后台,永不阻塞主流程 (tool_layer.py:895-909)。

5.2 触发 1:最直接的一条

process_analysis(evolver.py:266)把分析里的每条 EvolutionSuggestion 变成一个 EvolutionContext(_build_context_from_analysis,evolver.py:1426:FIX/DERIVED 要先按 target_skill_ids 把父技能加载齐),再并发执行(信号量限流)。全部处理完调 mark_evolution_processed(evolver.py:306)打上"已处理"戳,防重复。这条不经 LLM 确认门—— 建议本身就是 LLM 刚给的。

5.3 触发 2:工具退化 → 修依赖它的技能

process_tool_degradation(evolver.py:310)是两阶段:先按 find_skills_by_tool 找出依赖坏工具的 活跃技能当候选,再逐个走 _llm_confirm_evolution 确认门,确认了才排进并发执行。它有一套精巧的 反循环(evolver.py:332-431):

_addressed_degradations : { tool_key -> {已处理过的 skill_id, ...} }

每次进来:
· 已不在问题列表的 tool_key → 视为"工具已恢复",清空它的已处理集合
(于是工具再次退化时,依赖它的技能会被重新评估)
· 某技能已在该工具的已处理集合里 → 跳过(不重复烧 LLM)
· 无论进化成功与否,都标记为"已处理" → 失败的留给触发 1/3 用新数据再捡起

这个交界是第 4 章的地盘——"工具怎么被判定为退化"由 ToolQualityManager 决定,本章只用它的结论 ToolQualityRecord(recent_success_ratellm_flagged_count 等)。详见工具接地层

5.4 触发 3:定期体检 → 靠指标诊断

process_metric_check(evolver.py:441)每 5 次执行扫一遍活跃技能,只看攒够数据的 (total_selections ≥ min_selections,默认 5)。诊断逻辑在 _diagnose_skill_health(evolver.py:1564):

症状(阈值放宽,靠 LLM 兜)诊断
fallback_rate > 0.4(常被选中却不被应用)指令不清 → FIX
applied_rate > 0.4completion_rate < 0.35(用了却做不完)指令有误 → FIX
effective_rate < 0.55applied_rate > 0.25(能用但一般)可增强 → DERIVED
都不满足健康,不动

它的反循环是数据驱动:刚进化出的新技能 total_selections=0,天然要攒够 5 个新样本才会被再评估, 不需要时间冷却。

5.5 确认门:防止规则误伤

触发 2/3 是规则先粗筛,阈值故意放宽(evolver.py:105-110 的一堆 _..._THRESHOLD), 再由 _llm_confirm_evolution(evolver.py:588)让 LLM 二次确认"这技能真该动吗"。 确认响应用 _parse_confirmation(evolver.py:667)解析,优先读 JSON 的 proceed 字段, 读不到再退化到关键词匹配;含糊一律判为不进化(evolver.py:698-700),避免白烧一次昂贵的进化。


6. 进化的执行内核:循环 → 落盘 → 重试

无论哪个触发器、哪种模式,拿到内容后都走同一条内核:_run_evolution_loop 产文本 → _apply_with_retry 落盘校验。

6.1 令牌驱动的进化循环

_run_evolution_loop(evolver.py:1084)仿照 Grounding Agent 的设计—— 循环的终止不看是否调了工具,而看 LLM 有没有吐出显式令牌:

  • 内容含 <EVOLUTION_COMPLETE> → 成功,取出改动文本返回。
  • 内容含 <EVOLUTION_FAILED> → 失败(带原因),返回 None。
  • 两个令牌都没有 → 给一句 nudge(还剩几轮),继续。

前几轮开着工具让 LLM 探查代码库(读文件、跑 shell、web 搜索去定位根因);最后一轮关工具、逼决策 (evolver.py:1142-1155)。解析令牌的是 _parse_evolution_output(evolver.py:1241),两个令牌同现 时失败优先

6.2 apply-retry:失败了就把错误喂回去重来

_apply_with_retry(evolver.py:1271)是进化能"落地"的关键。它最多试 3 次 (_MAX_EVOLUTION_ATTEMPTS),每次:

apply_fn(content) → 成功?
成功 → 再跑结构校验 _validate_skill_dir
校验过 → 返回结果
校验砸 → 当作错误,进重试
失败 → 拿到 error_msg

进重试:
· derive/create 会先删掉上次失败留下的目标目录(cleanup_on_retry)
· 把 error + 磁盘上文件的【当前真实内容】一起塞回给 LLM
(evolver.py:1367-1383,防 LLM 凭空想象盘上是什么)
· LLM 产出修正版 → 下一轮 apply

这是闭环里的"验证"环节:不光要 LLM 说改好了,还要 patch 真能应用、目录结构真能过校验, 才算数。整个重试对话也录进 conversations.jsonl(agent_name="SkillEvolver.retry")。


7. 改动怎么落地:patch 模块的多格式与容错匹配

evolver 只管"要改成什么样",真正把文本变成磁盘上的文件,是 patch.py 的活。 三个操作 × 三种格式 × 一套容错匹配。

7.1 三个操作对应三种模式

操作服务于行为位置
fix_skillFIX就地覆盖已有目录,产出 diff + 快照patch.py:126
derive_skillDERIVED单父:拷贝目录→改;多父:新建目录→FULL/PATCH 写入patch.py:181
create_skillCAPTURED全新目录,通常 FULL 格式,diff 是"全量新增"patch.py:281

三者都返回 SkillEditResult(patch.py:88):带 content_diff(合并的 git 风格统一 diff)、 content_snapshot({相对路径: 内容} 全目录快照)、ok 标志。这两样正是喂给 lineage 的原料。

7.2 三种 LLM 输出格式,自动识别

LLM 产的改动有三种形态,detect_patch_type(patch.py:334)按特异性顺序嗅探结构标记:

① "*** Begin Patch" → PATCH 多文件 diff(Add/Update/Delete 跨文件)
② "*** Begin Files" → FULL 多文件完整内容(带信封)
③ 行首 "*** File:" → FULL 多文件(无信封,裸标记)
④ "<<<<<<< SEARCH" → DIFF 单文件 SEARCH/REPLACE
⑤ 兜底 → FULL 单文件完整内容
  • FULLparse_multi_file_full(patch.py:382)→ _apply_multi_file_full,逐文件覆盖, 并做路径逃逸检查(patch.py:431-433,防 ../ 写出技能目录)。
  • PATCHparse_patch(patch.py:626,移植自 ShinkaEvolve)解析出 hunk, _apply_multi_file_patch(patch.py:773)两阶段应用:先全部校验+算新内容,再统一落盘, 避免写一半失败留下坏状态。
  • DIFFapply_search_replace(patch.py:830)。

7.3 容错匹配:LLM 给的旧文本几乎从不逐字精确

这是整个 patch 模块工程含量最高的暗线:LLM 复述"要替换的原文"时,空白、缩进、Unicode 引号 几乎从不和磁盘一字不差。所以有两套降级匹配:

PATCH 路径seek_sequence(patch.py:498),4 级逐步放宽:

从左到右逐级降级,命中即停:
Pass1 精确相等 → Pass2 rstrip 相等 → Pass3 strip 相等 → Pass4 Unicode 归一化+strip
(弯引号→直引号、各种 dash→-、… →...)

DIFF 路径apply_search_replacefuzzy_match.fuzzy_find_match,那是一条6 级链 (fuzzy_match.py:4-9):精确 → 逐行 trim → 块锚点(首尾行 + Levenshtein 中段)→ 空白归一化 → 缩进弹性 → 整块边界 trim。匹配不到时还会用 _find_similar_lines(patch.py:979)找相近行, 拼成可读的报错回给 LLM ——正好接上 §6.2 的重试。


8. 持久化:SkillStore 与版本 DAG

进化的结果要能存、能查、能回溯。SkillStore(store.py)是 SQLite 上的一层, 既存技能档案,也维护"谁从谁演化来"的图。

8.1 存了什么

数据库在 <project_root>/.openspace/openspace.db,几张表分工(store.py:1-10):

存什么
skill_recordsSkillRecord 主表(身份 + 血缘 + 质量计数)
skill_lineage_parents父子关系(多对多)——版本 DAG 的边
execution_analyses每任务一条 ExecutionAnalysis
skill_judgments分析里的逐技能判断
skill_tool_deps / skill_tags工具依赖 / 标签

8.2 evolve_skill:原子的"新版入库 + 旧版失活"

evolve_skill(store.py:484,同步实现 _evolve_skill_sync602)是版本 DAG 的写入点, 在同一个 SQL 事务里(受 self._mu 保护)完成:

  • 若是 FIXED:把所有同名父置 is_active=0(store.py:614-620),保证一条技能线只有最新版激活
  • 若是 DERIVED:父保持激活(它仍是自己那条线的最新版),新版也激活——于是分叉出两条活跃线。
  • 都会 upsert 新记录并写好 parent_skill_ids

CAPTURED 无父,不走这里,而是 save_record(evolver.py:1027)。

8.3 版本 DAG 怎么查

两个方向的查询构成 DAG 遍历:

  • find_children(parent_skill_id)(store.py:883):查 skill_lineage_parents,找"从我派生出去的"。
  • get_versions(name)(store.py:740):按 name 拉出同一技能的所有版本(含失活的), 按 generation 升序——这就是一条技能线的完整演化史。

一张 FIX 链 + DERIVED 分叉的示意:

weather (gen0, IMPORTED, active=✗) ← 被 FIX 后失活
└─FIX→ weather (gen1, FIXED, active=✓) 同名同路径,只有它活着
└─DERIVED→ weather-enhanced (gen2, DERIVED, active=✓) 新名新目录,分叉

captured-xyz (gen0, CAPTURED, active=✓) ← 无父,新长出的根

8.4 防重复处理:evolution_processed_at

触发 1 处理完一条分析,会 mark_evolution_processed(task_id)(store.py:856)在 execution_analyses 上盖 evolution_processed_at 时间戳。候选查询默认只捞 evolution_processed_at IS NULL 的(store.py:843-847),保证同一份分析不会被进化两遍。 这个列是后加的,靠 _migrate_add_evolution_processed_at(store.py:269)对老库幂等地 ALTER TABLE ADD COLUMN——已存在就吞掉 OperationalError

8.5 质量计数在哪更新

技能的 total_selections / total_applied / total_completions / total_fallbacks 不在 Python 里算, 而是分析入库时用 SQL 原子自增(_record_analysis_sync,store.py:584-595):选中即 +1 selections, 应用了 +applied,应用且完成 +completions,没应用且失败 +fallbacks。这些计数正是触发 3 体检的输入 (SkillRecordapplied_rate 等派生属性,types.py:380-398)。


9. 数据模型速览(types.py)

前面反复出现的类型,集中在这里对照一次。全在 openspace/skill_engine/types.py:11-465

类型是什么关键字段位置
EvolutionType进化动词枚举FIX / DERIVED / CAPTURED;to_origin() 映射到 SkillOrigintypes.py:26
EvolutionSuggestion分析产出的一条进化建议evolution_typetarget_skill_idscategorydirectiontypes.py:197
ExecutionAnalysis一次任务的完整复盘(1 任务 1 份)task_completedtool_issuesskill_judgmentsevolution_suggestions;candidate_for_evolution 属性types.py:252
SkillLineage一个版本节点的血缘origingenerationparent_skill_idschange_summarycontent_diffcontent_snapshottypes.py:69
SkillRecord技能的完整档案身份 + lineage + 工具依赖 + 4 个质量计数 + is_activetypes.py:333

关于 SkillLineagecontent_diff 有一条按父数量分档的策略(types.py:96-118),值得记住:

父节点数content_diff 内容
0(IMPORTED / CAPTURED)全量新增 diff(每行前缀 +)
1(FIXED / 单父 DERIVED)父目录 ↔ 本版目录的正常统一 diff
N(多父 DERIVED)空字符串——多父合并是"创作"不是"打补丁",意图记在 change_summary

10. 巧妙之处(可借鉴的技术)

  • 一段录像,两路证据交叉判断。 分析同时看 conversations.jsonl(agent 怎么想)和 traj.jsonl (agent 实际调了什么、成没成),used_tool_keys 由后者推出(analyzer.py:405-416),判断更贴事实。

  • LLM 技能 ID 纠错。 LLM 复述 hex 后缀常出错,_correct_skill_ids(analyzer.py:59)按同名前缀 + 自适应编辑距离纠正,让"一个字符错位就找不到技能"这类脆弱点消失。

  • 令牌驱动 + 最后一轮强制决策。 进化循环靠 <EVOLUTION_COMPLETE>/<EVOLUTION_FAILED> 令牌收敛, 而非"调没调工具";最后一轮关工具逼输出(evolver.py:1142),避免无限打转。

  • 把磁盘真实内容喂回重试。 _apply_with_retry 重试时附上盘上当前内容当 ground truth (evolver.py:1367-1383),LLM 不再凭空想象要改的文件长啥样,配合多级容错匹配显著提升落盘成功率。

  • 两阶段应用 patch。 _apply_multi_file_patch 先全量校验+算好新内容,再统一写盘(patch.py:784-826), 多文件改动要么全成要么全不落,不留半吊子状态。

  • 反循环两副面孔。 触发 2 用"已处理集合 + 工具恢复即清空"(状态驱动);触发 3 用"新技能 selections=0 得攒够新样本"(数据驱动)。两者都不靠拍脑袋的时间冷却。

  • 进化全程可回放。 分析、确认、进化、重试四类对话都写进 conversations.jsonl,出问题能原样复盘。


11. 边界与局限(诚实)

  • 进化质量上限 = 分析 LLM 的判断力。 整条链由 ExecutionAnalysis 驱动,分析看走眼(误判完成、 漏掉工具故障),下游进化就跟着偏。确认门只挡触发 2/3 的规则误伤,挡不住触发 1 ——它直接采纳分析建议 (evolver.py:266,无确认门)。

  • 有超时,可能半成品。 分析+进化被 asyncio.wait_for(...) 包着(tool_layer.py:600-604),超时会 被取消并标 analysis_timed_out,此时"已进化技能可能不完整"(日志明说)。

  • 重试 3 次仍失败就放弃。 _apply_with_retry 耗尽 3 次后返回 None 并清理残目录 (evolver.py:1344-1352),这次进化就丢了(留待后续触发用新数据重来)。

  • 多父 DERIVED 不留 diff。 合并的 content_diff 是空串(types.py:112-118),要看具体改了什么 只能各父的 content_snapshot 自己比。

  • 本章不覆盖工具质量监控本身。 "工具怎么被判退化、ToolQualityManager 如何积累证据"属于 第 4 章;本章只消费它的结论触发 FIX。


12. 代码地图(导航索引)

用真实符号名 grep(比行号抗漂移)。所有引用 as-of sourceCommit 228f8f7

主题文件路径符号名
触发链起点(分析+进化)openspace/tool_layer.py_maybe_analyze_execution
后台触发 2/3 调度openspace/tool_layer.py_maybe_evolve_quality
分析入口openspace/skill_engine/analyzer.pyExecutionAnalyzer.analyze_execution
读录制轨迹openspace/skill_engine/analyzer.py_load_recording_context
构造分析 promptopenspace/skill_engine/analyzer.py_build_analysis_prompt
分析 agent 循环openspace/skill_engine/analyzer.py_run_analysis_loop
解析成 ExecutionAnalysisopenspace/skill_engine/analyzer.py_parse_analysis
技能 ID 容错纠正openspace/skill_engine/analyzer.py_correct_skill_ids
进化统一入口openspace/skill_engine/evolver.pySkillEvolver.evolve
FIX 模式openspace/skill_engine/evolver.py_evolve_fix
DERIVED 模式openspace/skill_engine/evolver.py_evolve_derived
CAPTURED 模式openspace/skill_engine/evolver.py_evolve_captured
触发 1 · 分析openspace/skill_engine/evolver.pyprocess_analysis
触发 2 · 工具退化openspace/skill_engine/evolver.pyprocess_tool_degradation
触发 3 · 定期体检openspace/skill_engine/evolver.pyprocess_metric_check
LLM 确认门openspace/skill_engine/evolver.py_llm_confirm_evolution
健康诊断openspace/skill_engine/evolver.py_diagnose_skill_health
后台进化调度openspace/skill_engine/evolver.pyschedule_background
令牌驱动进化循环openspace/skill_engine/evolver.py_run_evolution_loop
落盘+校验+重试openspace/skill_engine/evolver.py_apply_with_retry
FIX 落盘openspace/skill_engine/patch.pyfix_skill
DERIVED 落盘openspace/skill_engine/patch.pyderive_skill
CAPTURED 落盘openspace/skill_engine/patch.pycreate_skill
格式自动识别openspace/skill_engine/patch.pydetect_patch_type
多文件 PATCH 解析openspace/skill_engine/patch.pyparse_patch
SEARCH/REPLACE 应用openspace/skill_engine/patch.pyapply_search_replace
4 级降级匹配openspace/skill_engine/patch.pyseek_sequence
6 级模糊匹配链openspace/skill_engine/fuzzy_match.pyfuzzy_find_match
原子进化入库openspace/skill_engine/store.pyevolve_skill / _evolve_skill_sync
找子节点(DAG)openspace/skill_engine/store.pyfind_children
一技能全版本openspace/skill_engine/store.pyget_versions
标记已进化openspace/skill_engine/store.pymark_evolution_processed
processed_at 迁移openspace/skill_engine/store.py_migrate_add_evolution_processed_at
数据模型openspace/skill_engine/types.pyEvolutionType / EvolutionSuggestion / SkillLineage / SkillRecord / ExecutionAnalysis
对话优先级截断openspace/skill_engine/conversation_formatter.pyformat_conversations