1. 什么是「反构图」
「反构图」是 iceCoder 双模 L2 接管阶段的核心动作,英文对应 RetrospectiveGraphBuilder。
它的本质是:在模型已经自由执行了一段代码之后,系统接管时「逆向重建」一张任务图,把已经完成的工作回填为 done,而不是从零开始重跑模板。
用户意图 → 模型自由段(无图) → 偏离触发 takeover → 反构图 → replaceGraph → 强约束段继续
与正向构图(buildGraph 在首轮 initGraph)的区别:
| 维度 | 正向构图 | 反构图 |
|---|---|---|
| 触发时机 | 任务首轮 turnCount === 1 | L2 takeover 后(§10 恢复主路径) |
| 输入依据 | goal + intent | goal + intent + WorkspaceSnapshot |
| 进度状态 | 全部 pending | 按快照回填 done |
| 游标位置 | 第一个节点 | 推进到第一个未完成节点 |
| 失败后果 | 无图自由段 | 降级为二级强提示(§19.2) |
2. 反构图的完整调用链
反构图不是单一类的行为,而是 M5→M6→M7→M8 四阶段流水线:
scss
Harness 主循环
│
▼
evaluateAfterRound 返回 decision.action === 'takeover'
│
▼
bridge.commit(nextSnapshot) → supervisorPhase 变为 'takeover'
│
▼
applyTakeoverRecoveryMainPath() [harness-recovery-main-path.ts]
│
├─ M5: WorkspaceStateExtractor.extract() → WorkspaceSnapshot
│ [workspace-state-extractor.ts]
│
├─ M6: SnapshotConfidenceEvaluator.evaluate() → confidence ∈ [0,1]
│ [snapshot-confidence-evaluator.ts]
│
├─ M7: RecoverySafetyChecker.check() → recoverable: boolean
│ [recovery-safety-checker.ts]
│
└─ M8: RetrospectiveGraphBuilder.build() → graph + markedDone
[retrospective-graph-builder.ts]
│
├─ buildGraph(goal, intent) → 模板图(全 pending)
├─ applyKnownProgress(graph, snapshot) → 回填 done
└─ advanceCursorPastDone(graph) → 游标推进
│
▼
GraphExecutor.replaceGraph(graph) [task-graph-executor.ts]
│
▼
enterTakeover() → 强约束段接管
3. 四阶段深度解析
3.1 M5 --- WorkspaceStateExtractor:工作区快照提取
文件:src/harness/supervisor/workspace-state-extractor.ts
职责:从 Harness 已有的 TaskStateSnapshot + RepoContextSnapshot 中,纯启发式地拼出 WorkspaceSnapshot,不发起任何 IO。
输入:
- task: TaskStateSnapshot --- 任务状态快照(含 filesChanged, filesRead, verificationStatus)
- repo: RepoContextSnapshot --- 仓库上下文快照(含 filesChanged, recentDiagnostics)
- buildSummary?: string --- 最近一次构建摘要(由调用方从 verification buffer 推导)
- testSummary?: string --- 最近一次测试摘要
- lintSummary?: string --- 最近一次 lint 摘要
- gitSummary?: string --- Git 工作区简述(可选,缺省由 repo.filesChanged 推导)
输出:WorkspaceSnapshot
typescript
interface WorkspaceSnapshot {
snapshotId: string; // `snap-${now}-${shortRand()}`
at: number; // 时间戳
gitSummary: string; // 如 'clean' / 'M:3 diag:1'
filesAdded: string[]; // 新增文件
filesModified: string[]; // 修改文件
filesDeleted: string[]; // V1 恒空
buildSummary?: string;
testSummary?: string;
lintSummary?: string;
}
核心算法 --- classifyFiles:
javascript
function classifyFiles(input: WorkspaceStateExtractorInput) {
const changed = new Set([...input.task.filesChanged, ...input.repo.filesChanged]);
const known = new Set(input.preExistingFiles ?? input.task.filesRead);
for (const file of changed) {
if (known.size > 0 && !known.has(file)) {
added.push(file); // 不在已知集合 → 新增
} else {
modified.push(file); // 在已知集合 → 修改
}
}
return { added, modified, deleted: [] };
}
关键设计点:
- filesAdded vs filesModified 的判定依赖 preExistingFiles(通常由初始任务 snapshot 与当前 repo 的差集得出)
- V1 不区分 deleted,保留接口兼容性
- gitSummary 缺省推导:filesChanged.length === 0 → 'clean',否则 'M:n' 或 'M:n diag:m'
3.2 M6 --- SnapshotConfidenceEvaluator:快照可信度评估
文件:src/harness/supervisor/snapshot-confidence-evaluator.ts
职责:给 WorkspaceSnapshot 打一个 0, 1 的综合分,判断是否「够格」走模板图。
五因子加权求和:
| 因子 | 权重 | 满分条件 | 评分函数 |
|---|---|---|---|
| gitClean | 0.25 | gitSummary === 'clean' 或 files 集合为空 | 空集→1.0;非空→线性衰减 |
| snapshotAge | 0.15 | roundsSinceExtract <= 1 | ≤1→1.0;≥5→0.0;中间线性 |
| verifyPassed | 0.20 | lastVerifyPassed === true | true→1.0;false→0.0 |
| repoContextMatch | 0.25 | repoFilesChanged ⊆ snapshot.filesModified ∪ filesAdded | 子集→1.0;否则按重叠比 |
| buildSignal | 0.15 | buildSummary 不含 fail/error,testSummary 不为 failed | 无 bad 标记→1.0;否则 0.0 |
阈值:templateGraphMin(默认 0.65)
关键实现细节:
- 权重归一化:weights = weights / sum(weights),避免配置漂移导致总分越界
- 缺失信号中性处理:任一因子输入为 undefined → 按 0.5 计入,避免「无信号 = 低分」
- meetsTemplateGraphThreshold 暴露给 RetrospectiveGraphBuilder 复用,不重复计算
3.3 M7 --- RecoverySafetyChecker:恢复安全性检查
文件:src/harness/supervisor/recovery-safety-checker.ts
职责:在反构图前做最后一轮安全闸门,防止在危险工作区上换图。
检查项:
| 检查项 | 触发条件 | 说明 |
|---|---|---|
| critical_file_missing | 注入的 criticalFiles 列表有文件不在 existingFiles 中 | 关键文件丢失 |
| repo_unhealthy | gitSummary 含 conflict/detached/rebase/merge/unmerged | 仓库状态损坏 |
| branch_unhealthy | 调用方显式注入 branchHealthy=false | 分支异常(合并冲突等) |
| baseline_broken | buildSummary 含 fatal/crash/panic/segfault | 编译基线损坏 |
输出:
ini
interface RecoverySafetyCheckResult {
recoverable: boolean;
reasons: RecoverySafetyReason[];
missingFiles: string[];
humanReason: string; // 如 'ok' 或 'critical_file_missing:src/foo.ts+2|repo_unhealthy'
}
关键设计点:
- criticalFiles 缺省(undefined)→ 跳过该检查,避免假阳性
- existingFiles 未注入 → 不误报缺失(调用方没准备好)
- repoHealthy / baselineBroken 支持调用方显式覆盖,避免启发式误判
3.4 M8 --- RetrospectiveGraphBuilder:反构图核心
文件:src/harness/supervisor/retrospective-graph-builder.ts
这是反构图的核心引擎,负责「建模板图 + 回填进度 + 推进游标」。
3.4.1 入口:build(input)
scss
build(input: RetrospectiveGraphBuildInput): RetrospectiveGraphBuildResult {
// Step 1: 调用 buildGraph 得到全 pending 的模板图
graph = buildGraph({ goal, intent, workspaceRoot, now, graphId });
// Step 2: 按 WorkspaceSnapshot 回填已完成节点
const markedDone = applyKnownProgress(graph, input.snapshot);
// Step 3: 把游标推到第一个未完成节点
advanceCursorPastDone(graph);
return { ok: true, graph, markedDone, signalsSummary };
}
失败处理:buildGraph 抛错或返回空图 → { ok: false, reason: 'build_threw' | 'empty_template' },不抛错,由调用方按 §19.2 降级。
3.4.2 核心算法:applyKnownProgress
这是反构图最关键的逻辑,决定哪些节点可以被标记为 done。
V1 规则(硬编码):
| 节点类型 | 标为 done 的条件 |
|---|---|
| inspect / search | snapshot.filesAdded.length + snapshot.filesModified.length > 0 |
| verify | snapshot.testSummary === 'passed' |
实现细节:
ini
function applyKnownProgress(graph: TaskGraphData, snapshot: WorkspaceSnapshot): string[] {
const markedDone: string[] = [];
const hasFileEvidence = snapshot.filesAdded.length + snapshot.filesModified.length > 0;
const verifyPassed = snapshot.testSummary === 'passed';
for (const node of graph.mainBranch.nodeIds.map(id => graph.nodes[id]).filter(Boolean)) {
if (node.status !== 'pending') continue; // 只处理 pending 节点
let shouldMark = false;
if ((node.type === 'inspect' || node.type === 'search') && hasFileEvidence) {
shouldMark = true;
} else if (node.type === 'verify' && verifyPassed) {
shouldMark = true;
}
if (shouldMark) {
node.status = 'done';
node.startedAt = node.startedAt ?? now;
node.endedAt = now;
graph.nodeHistory.push({ nodeId, status: 'done', startedAt, endedAt, retries });
markedDone.push(node.id);
}
}
// 更新 graph.progress
if (markedDone.length > 0) {
graph.progress = Math.min(100, Math.round((markedDone.length / totalNodes) * 100));
graph.updatedAt = now;
}
return markedDone;
}
重要约束:
- 只修改 status === 'pending' 的节点,不碰已完成的
- startedAt 采用「缺失才补」策略(??=),保留原始时间
- 所有被标记节点共享同一个 now 时间戳(提取时刻)
- 不修改节点内容、不修改边、不修改分支结构
3.4.3 游标推进:advanceCursorPastDone
回填完成后,需要把执行游标从已完成区域推到待办区域:
ini
function advanceCursorPastDone(graph: TaskGraphData): void {
let idx = graph.cursor.nodeIndex;
// 跳过所有 done 节点
while (idx < branchIds.length) {
const node = graph.nodes[branchIds[idx]];
if (!node || node.status !== 'done') break;
if (!graph.cursor.completedNodeIds.includes(node.id)) {
graph.cursor.completedNodeIds.push(node.id);
}
idx += 1;
}
// 重写 cursor(不使用 advanceCursor 以避免副作用)
graph.cursor.nodeIndex = Math.min(idx, branchIds.length - 1);
graph.cursor.nodeId = branchIds[graph.cursor.nodeIndex] ?? '';
if (idx < branchIds.length) {
// 停留在 pending 节点 → 显式启动
if (graph.nodes[graph.cursor.nodeId]?.status === 'pending') {
startCurrentNode(graph);
}
} else {
// 全部完成 → 触发 markGraphDone
if (last.status === 'done') {
advanceCursor(graph);
}
}
}
关键设计点:
- 直接重写 cursor.nodeIndex / cursor.nodeId,不调用 advanceCursor(后者会改 updatedAt 和 status 副作用)
- 同步维护 cursor.completedNodeIds,确保快照一致性
- 当游标停在 pending 节点时,调用 startCurrentNode 启动该节点(与正常 lifecycle 一致)
- 全部完成时,调用 advanceCursor 触发 markGraphDone 流程
4. 与 GraphExecutor 的集成
反构图完成后,通过 GraphExecutor.replaceGraph 注入运行中的图实例:
4.1 replaceGraph
ini
// task-graph-executor.ts
replaceGraph(graph: TaskGraphData): void {
this.graph = graph;
this.contractValidator = new ContractValidator(graph);
this.deviationDetector = new DeviationDetector();
// ... 重置其他依赖 graph 的组件
}
替换时机:仅在 supervisorPhase === 'takeover' 时允许,由 harness-recovery-main-path.ts 的 applyTakeoverRecoveryMainPath 调用。
4.2 takeover 段的 graph 行为变化
| 方法 | free 段 | takeover 段 |
|---|---|---|
| evaluateRound | 返回 inject_hint 文案 | evaluationMode = 'metrics_only',不返回 inject 文案 |
| checkToolCall | 节点合约检查 + 偏离检测 | 同左,但偏离信号经 CorrectionPort 以 C 类注入 |
| advanceCursor / completeCurrentNode | 正常推进 | 正常推进,但受 RecoveryBudgetManager 限制 |
| force_switch | 允许 | 允许,但经 bridge.composeGraphHint 路由 |
5. 失败降级路径(§19.2)
反构图不是强制的。当以下任一条件不满足时,走二级降级:
ini
M5 提取快照
│
▼
M6 置信度 < templateGraphMin (0.65) ──┐
│ │
M7 recoverable === false │
│ │
M8 buildGraph 失败 / 空图 │
│ │
▼ ▼
一级成功:replaceGraph 二级降级:strong_hint
│ │
▼ ▼
timeline: recover:template_graph timeline: recover:strong_hint
│
▼
inject [System Recovery] 块
forced 不回落 free
二级降级的表现:
- 不替换图(graphExecutor 保持原状或 null)
- 向 messages 注入一条 System Recovery 强提示块
- forcedDegradedTier = 'step_queue'
- 模型收到明确的接管信号和行动建议
6. 关键数据结构流转
6.1 WorkspaceSnapshot → markedDone 的映射逻辑
bash
WorkspaceSnapshot
├─ filesAdded / filesModified 非空
│ └─ 所有 type ∈ {inspect, search} 的 pending 节点 → done
│
├─ testSummary === 'passed'
│ └─ 所有 type === 'verify' 的 pending 节点 → done
│
└─ 其他类型(edit, context, final 等)
└─ V1 不自动标记,保持 pending
V1 的局限性:
- 不区分 edit 节点是否真的被完成(V1 没有语义分析能力)
- verify 节点只认 testSummary === 'passed',不认 build/lint 通过
- 不支持部分完成(节点要么全 done 要么全 pending)
6.2 游标推进的边界情况
| 场景 | 行为 |
|---|---|
| 首个节点就是 done | 游标直接跳到下一个 pending |
| 连续多个 done | while 循环批量跳过 |
| 全部 done | advanceCursor 触发 markGraphDone |
| 无节点 | 空保护,直接 return |
7. 与 RecoverySupervisor 的协作
反构图发生在 RecoverySupervisor 的 takeover 相位内,但不由 RecoverySupervisor 直接调用。
协作流程:
scss
ModeDecisionEngine / 信号累积
│
▼
RecoverySupervisor.evaluate()
│
├─ 条件满足 → decision: { action: 'takeover', signals }
│
▼
bridge.commit(nextSnapshot) → phase = 'takeover'
│
▼
Harness 调用 applyTakeoverRecoveryMainPath()
│
▼
bridge.runRecoveryMainPath() → 内部串联 M5→M6→M7→M8
职责分离:
- RecoverySupervisor:决定是否 takeover(状态机)
- SupervisorRuntimeBridge:决定 takeover 后做什么(恢复策略)
- RetrospectiveGraphBuilder:执行具体构图(模板图 + 回填)
8. 代码级关键路径追踪
8.1 从 Harness 主循环到反构图
css
harness.ts: run()
│
▼
harness-round-prep.ts: prepareHarnessRound()
│ └─ turnCount === 1 && shouldInit → initGraph(正向构图)
│
▼
callHarnessLlm() → tool_round / no_tool_round
│
▼
evaluateAfterRound() [supervisor-bridge.ts]
│ ├─ PassiveObserver.observe() → signals
│ ├─ GoalDriftDetector.evaluate() → driftSignal
│ └─ RecoverySupervisor.evaluate() → decision
│ └─ action === 'takeover' ?
│
▼ (if takeover)
bridge.commit(nextSnapshot)
│
▼
applyTakeoverRecoveryMainPath() [harness-recovery-main-path.ts]
│ ├─ buildRecoverySummaries() → build/test/lint 摘要
│ ├─ buildRecoveryExtractInput() → M5 input
│ └─ bridge.runRecoveryMainPath()
│ ├─ M5: extractor.extract()
│ ├─ M6: confidenceEvaluator.evaluate()
│ ├─ M7: safetyChecker.check()
│ └─ M8: retrospectiveBuilder.build()
│ ├─ buildGraph()
│ ├─ applyKnownProgress()
│ └─ advanceCursorPastDone()
│
▼
graphExecutor.replaceGraph(graph)
│
▼
enterTakeover() → executionMode = forced
8.2 关键文件索引
| 文件 | 核心导出 | 反构图中的角色 |
|---|---|---|
| retrospective-graph-builder.ts | RetrospectiveGraphBuilder, applyKnownProgress, advanceCursorPastDone | M8 核心引擎 |
| workspace-state-extractor.ts | WorkspaceStateExtractor | M5 快照提取 |
| snapshot-confidence-evaluator.ts | SnapshotConfidenceEvaluator | M6 置信度评估 |
| recovery-safety-checker.ts | RecoverySafetyChecker | M7 安全闸门 |
| harness-recovery-main-path.ts | applyTakeoverRecoveryMainPath, buildRecoveryExtractInput | 主路径编排 |
| supervisor-bridge.ts | runRecoveryMainPath, commit | 桥接与状态提交 |
| task-graph-executor.ts | GraphExecutor.replaceGraph | 图替换执行 |
| task-graph-builder.ts | buildGraph | 模板图构建(被 M8 复用) |
| task-graph.ts | advanceCursor, startCurrentNode, markGraphDone | 图游标操作 |
| recovery-supervisor.ts | RecoverySupervisor | L2 状态机(触发 takeover) |
9. V1 约束与未来扩展
9.1 V1 明确不做的事
- 不做 LLM 重规划:模板图完全由 buildGraph 规则驱动,不调用 LLM
- 不做语义分析:applyKnownProgress 只看文件存在性和测试结果,不读代码内容
- 不做部分完成判定:节点要么全 done 要么全 pending,无中间状态
- 不区分 deleted:filesDeleted 恒空,不影响回填逻辑
- 不修改图结构:只改节点 status 和游标,不增删节点或边
9.2 V2 潜在扩展方向
| 扩展点 | 说明 | 影响文件 |
|---|---|---|
| 语义回填 | 用 LLM 分析 diff,判断 edit 节点是否实质完成 | retrospective-graph-builder.ts |
| 部分完成 | 引入 partial 状态,支持节点粒度更细的回填 | types/task-graph.ts |
| 动态权重 | 根据任务域调整 applyKnownProgress 的判定条件 | snapshot-confidence-evaluator.ts |
| 多分支回填 | 回填时考虑 fallback branch 的完成情况 | task-graph.ts |
| 快照语义摘要 | WorkspaceSnapshot 增加 semanticSummary 字段 | types/supervisor.ts |
10. 调试与观测
10.1 Timeline 事件
反构图过程会在 EventTimeline 中留下以下事件:
| 事件类型 | 触发点 | 关键字段 |
|---------|-----------------------------|-----------------------------------|-------------------|------------------------------|
| recover | runRecoveryMainPath 入口 | tier: 'template_graph' | 'strong_hint' |
| failure | M6 置信度不足 / M7 不安全 / M8 构图失败 | reason: 'snapshot_confidence_low' | 'recovery_unsafe' | 'retrospective_build_failed' |
| switch | executionMode 切换 | from: 'free' -> 'forced' |
10.2 关键日志点
在 retrospective-graph-builder.ts 中,以下位置是调试热点:
- buildGraph 返回空图 → 检查 goal / intent 是否匹配模板库
- applyKnownProgress 标记 0 个节点 → 检查 snapshot.filesAdded / testSummary
- advanceCursorPastDone 游标未推进 → 检查 graph.cursor.nodeIndex 初始值
- replaceGraph 后图状态异常 → 检查 graphExecutor.graph 是否被正确替换
11. 总结
反构图是 iceCoder 双模机制中「弹性信任」理念的集中体现:
- 不预置全流程:首轮不 init 图,让模型自由探索
- 偏离才接管:通过 RecoverySupervisor 三条件判定是否需要 takeover
- 状态优先恢复:接管时用 WorkspaceSnapshot 回填已完成工作,避免重复劳动
- 安全降级:快照置信度不足或工作区不安全时,不强行构图,降级为强提示
其核心数据结构流转为:
scss
WorkspaceSnapshot
→ SnapshotConfidence (≥0.65?)
→ RecoverySafety (recoverable?)
→ buildGraph (模板图)
→ applyKnownProgress (回填 done)
→ advanceCursorPastDone (游标推进)
→ replaceGraph (注入执行器)
整个链路不依赖 LLM,纯规则驱动,保证了接管过程的确定性和可观测性。