UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

294 lines (293 loc) 12.3 kB
/** * PlanTracker — 探索计划跟踪器 * * 从 ExplorationTracker.js 提取的规划与质量评估逻辑。 * 管理首轮 plan elicitation、周期性 replan、计划进度匹配和质量评分。 * * @module PlanTracker */ import { DEFAULT_REPLAN_INTERVAL } from './ExplorationStrategies.js'; // ─── 常量 ────────────────────────────────────────────── /** 默认偏差阈值 */ const DEFAULT_DEVIATION_THRESHOLD = 0.6; /** 最少经过 N 轮后才允许再次触发 replan(防止 replan 风暴) */ const MIN_REPLAN_GAP = 3; export class PlanTracker { /** 等待 AI 输出 replan */ #pendingReplan = false; /** 计划进度 */ #planProgress = { coveredSteps: 0, totalSteps: 0, deviationScore: 0, unplannedActions: 0, lastReplanIteration: null, consecutiveOffPlan: 0, }; /** 获取计划进度 */ get progress() { return { ...this.#planProgress }; } /** * 检查是否需要触发规划 + 生成规划 nudge * * @param state 从 ExplorationTracker 传入 * @param trace ActiveContext 实例 * @returns |null} */ checkPlanning(state, trace) { const { metrics, budget: b, strategy } = state; const m = metrics; // 第 1 轮: plan elicitation if (m.iteration === 1) { trace?.expectPlan?.(); return { type: 'planning', text: this.#buildPlanElicitationPrompt(b.maxIterations || 30), }; } // 有计划时: 检查 replan const plan = trace?.getPlan?.(); if (!plan) { return null; } const progress = this.#planProgress; const interval = strategy.replanInterval || DEFAULT_REPLAN_INTERVAL; const baseIteration = progress.lastReplanIteration || plan.createdAtIteration; const periodicTrigger = interval > 0 && m.iteration > 1 && m.iteration - baseIteration >= interval; const deviationTrigger = progress.consecutiveOffPlan >= 3 || (progress.totalSteps > 0 && progress.deviationScore > DEFAULT_DEVIATION_THRESHOLD); if (!periodicTrigger && !deviationTrigger) { return null; } // 冷却间隔 if (progress.lastReplanIteration && m.iteration - progress.lastReplanIteration < MIN_REPLAN_GAP) { return null; } const remaining = b.maxIterations - m.iteration; const parts = []; if (deviationTrigger) { parts.push(`📋 计划偏差检查 (第 ${m.iteration}/${b.maxIterations} 轮):`); if (progress.consecutiveOffPlan >= 3) { parts.push(`你的行为已连续 ${progress.consecutiveOffPlan} 轮偏离原定计划。`); } } else { parts.push(`📋 计划进度回顾 (第 ${m.iteration}/${b.maxIterations} 轮):`); } const doneSteps = plan.steps.filter((s) => s.status === 'done'); const pendingSteps = plan.steps.filter((s) => s.status === 'pending'); if (doneSteps.length > 0) { parts.push(`\n✅ 已完成 (${doneSteps.length}/${plan.steps.length}):`); for (const s of doneSteps) { parts.push(` - ${s.description}`); } } if (pendingSteps.length > 0) { parts.push(`\n⏳ 未完成 (${pendingSteps.length}/${plan.steps.length}):`); for (const s of pendingSteps) { parts.push(` - ${s.description}`); } } if (progress.unplannedActions > 0) { parts.push(`\n⚡ 计划外行为: ${progress.unplannedActions} 次`); } parts.push(`\n剩余 ${remaining} 轮。请评估:`); parts.push(`1. 未完成的步骤是否仍然相关?`); parts.push(`2. 是否需要根据新发现调整后续步骤?`); parts.push(`3. 请更新你的探索计划(用编号列表)。`); progress.lastReplanIteration = m.iteration; this.#pendingReplan = true; trace?.expectPlan?.(); return { type: 'planning', text: parts.join('\n') }; } /** * 更新计划进度(从 ReasoningLayer 迁入) * 将本轮工具调用与 plan 步骤进行模糊匹配 * * @param trace ActiveContext 实例 */ updatePlanProgress(trace) { const steps = trace?.getPlanStepsMutable?.() || []; if (steps.length === 0) { return; } // 处理 pending replan if (this.#pendingReplan) { const plan = trace?.getPlan?.(); if (plan) { this.#planProgress.coveredSteps = plan.steps.filter((s) => s.status === 'done').length; this.#planProgress.totalSteps = plan.steps.length; this.#planProgress.unplannedActions = 0; this.#planProgress.consecutiveOffPlan = 0; this.#pendingReplan = false; } } const actions = trace?.getCurrentRoundActions?.() || []; if (actions.length === 0) { return; } let matchedThisRound = false; for (const action of actions) { const matchedStep = this.#findMatchingStep(steps, action); if (matchedStep) { matchedStep.status = 'done'; matchedThisRound = true; } else { this.#planProgress.unplannedActions++; } } if (matchedThisRound) { this.#planProgress.consecutiveOffPlan = 0; } else { this.#planProgress.consecutiveOffPlan++; } this.#planProgress.coveredSteps = steps.filter((s) => s.status === 'done').length; this.#planProgress.totalSteps = steps.length; this.#planProgress.deviationScore = steps.length > 0 ? 1 - this.#planProgress.coveredSteps / steps.length : 0; } /** * 推理质量评分 * @returns } */ getQualityMetrics(trace) { const stats = trace?.getStats?.() || { totalRounds: 0, thoughtCount: 0, totalActions: 0, totalObservations: 0, reflectionCount: 0, }; const totalRounds = stats.totalRounds || 1; const thoughtRatio = stats.thoughtCount / totalRounds; const reflectionRatio = stats.reflectionCount / totalRounds; const actionEfficiency = Math.min(stats.totalActions / totalRounds / 3, 1); const observationCoverage = stats.totalObservations > 0 ? 1 : 0; const plan = trace?.getPlan?.(); const hasPlan = plan && plan.steps.length > 0; let planScore = 0; if (hasPlan) { const completionRate = this.#planProgress.totalSteps > 0 ? this.#planProgress.coveredSteps / this.#planProgress.totalSteps : 0; const adherenceRate = 1 - (this.#planProgress.deviationScore || 0); planScore = completionRate * 0.6 + adherenceRate * 0.4; } const score = hasPlan ? Math.round((thoughtRatio * 0.3 + reflectionRatio * 0.15 + actionEfficiency * 0.15 + observationCoverage * 0.15 + planScore * 0.25) * 100) : Math.round((thoughtRatio * 0.4 + reflectionRatio * 0.2 + actionEfficiency * 0.2 + observationCoverage * 0.2) * 100); const breakdown = { ...stats, thoughtRatio: Math.round(thoughtRatio * 100), reflectionRatio: Math.round(reflectionRatio * 100), actionEfficiency: Math.round(actionEfficiency * 100), observationCoverage: Math.round(observationCoverage * 100), }; if (hasPlan) { breakdown.planCompletion = Math.round((this.#planProgress.totalSteps > 0 ? this.#planProgress.coveredSteps / this.#planProgress.totalSteps : 0) * 100); breakdown.planAdherence = Math.round((1 - (this.#planProgress.deviationScore || 0)) * 100); breakdown.planScore = Math.round(planScore * 100); } return { score, breakdown }; } // ─── 内部方法 ────────────────────────────────── #buildPlanElicitationPrompt(maxIter) { return [ `📋 在开始探索前,请先制定一个简要的探索计划。`, ``, `你有 ${maxIter} 轮工具调用机会。请在你的回复中用编号列表简述 3-6 个探索步骤:`, `- 每个步骤应描述要搜索/阅读的目标(具体的类名、模式、文件路径)`, `- 步骤应从宏观到微观递进(先概览 → 再搜索关键模式 → 再深入关键文件)`, `- 最后一步应是"总结分析发现"`, ``, `例如:`, `1. 获取项目概览和目录结构,识别核心模块`, `2. 搜索网络请求相关类,分析请求模式`, `3. 搜索错误处理和响应解析模式`, `4. 深入阅读 3-5 个典型实现文件,确认关键细节`, `5. 总结分析发现`, ``, `制定计划后请立即开始执行第 1 步(可在同一轮中同时输出计划文本并调用工具)。`, ].join('\n'); } /** * 模糊匹配: 将工具调用匹配到 plan 步骤 * @param action { tool, params } */ #findMatchingStep(steps, action) { const toolName = action.tool; const argsStr = JSON.stringify(action.params || {}).toLowerCase(); for (const step of steps) { if (step.status === 'done') { continue; } // 策略 1: 关键词匹配 if ((step.keywords?.length ?? 0) > 0) { const matched = step.keywords.some((kw) => argsStr.includes(kw.toLowerCase())); if (matched) { return step; } } // 策略 2: 工具类型 → 步骤描述的语义匹配 const desc = step.description.toLowerCase(); if (toolName === 'get_project_overview' && (desc.includes('概览') || desc.includes('overview') || desc.includes('结构') || desc.includes('项目'))) { return step; } if (toolName === 'list_project_structure' && (desc.includes('目录') || desc.includes('结构') || desc.includes('structure'))) { return step; } if ((toolName === 'get_class_info' || toolName === 'get_class_hierarchy') && (desc.includes('继承') || desc.includes('类') || desc.includes('hierarchy') || desc.includes('class'))) { return step; } if (toolName === 'read_project_file' && (desc.includes('阅读') || desc.includes('read') || desc.includes('深入') || desc.includes('查看') || desc.includes('文件'))) { return step; } if (toolName === 'search_project_code' && (desc.includes('搜索') || desc.includes('search') || desc.includes('查找') || desc.includes('分析'))) { return step; } if ((toolName === 'query_code_graph' || toolName === 'query_call_graph') && (desc.includes('图谱') || desc.includes('graph') || desc.includes('调用') || desc.includes('call') || desc.includes('关系') || desc.includes('依赖'))) { return step; } } return null; } }