autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
279 lines (278 loc) • 16.5 kB
JavaScript
/**
* NudgeGenerator — 探索引导信号生成器
*
* 从 ExplorationTracker.js 提取的 Nudge 生成逻辑。
* 按优先级队列生成每轮最多一条 nudge 注入 AI 上下文。
*
* 优先级 (高→低):
* 1. force_exit — 轮次耗尽
* 2. convergence — 信息饱和
* 3. budget_warning — 75% 预算消耗
* 4. reflection — 周期反思 / 停滞反思
* 5. (planning — 由 PlanTracker 处理)
*
* 设计原则:
* - 无状态(flags 从外部传入并返回更新)
* - 接受 state 快照,无循环依赖
* - Nudge 文本内联(未来可外置为 i18n JSON 模板)
*
* @module NudgeGenerator
*/
import { DEFAULT_REFLECTION_INTERVAL } from './ExplorationStrategies.js';
// ─── 常量 ──────────────────────────────────────────────
/** 连续无新信息 N 轮触发停滞反思 */
const DEFAULT_STALE_THRESHOLD = 2;
/** 最少经过 N 轮后才允许触发停滞反思 */
const MIN_ITERS_FOR_STALE_REFLECTION = 4;
/** 默认最少探索轮次(冷启动质量保障) */
const DEFAULT_MIN_EXPLORE_ITERS = 10;
/** 默认停滞收敛阈值 */
const DEFAULT_CONVERGENCE_STALE_THRESHOLD = 3;
export class NudgeGenerator {
// ── 一次性 flags(生命周期内最多触发一次的 nudge) ──
#convergenceNudged = false;
#budgetWarningInjected = false;
/**
* 生成本轮的 Nudge(每轮最多一条)
*
* @param state 从 ExplorationTracker 传入的状态快照
* @param trace ActiveContext 实例 (反思用)
* @returns |null}
*/
generate(state, trace) {
const { phase: _phase, metrics: m, budget: b, strategy, gracefulExitRound, pipelineType, isTerminalPhase, } = state;
// 1. 强制退出(graceful exit 后每轮都重复发出,确保 LLM 不再调用工具)
if (gracefulExitRound != null && m.iteration >= gracefulExitRound) {
return this.#generateForceExit(m, b, strategy, pipelineType);
}
// 2. 收敛引导(信息饱和 — 仅非终结阶段)
if (!isTerminalPhase &&
!this.#convergenceNudged &&
m.roundsSinceNewInfo >= DEFAULT_CONVERGENCE_STALE_THRESHOLD &&
m.iteration >= DEFAULT_MIN_EXPLORE_ITERS) {
this.#convergenceNudged = true;
return {
type: 'convergence',
text: `你已经充分探索了项目代码(${m.uniqueFiles.size} 个文件,${m.uniquePatterns.size} 次不同搜索,${m.uniqueQueries.size} 次结构化查询)。` +
`最近 ${m.roundsSinceNewInfo} 轮没有发现新信息,建议开始撰写分析总结。\n` +
`如果你确信还有重要方面未覆盖,可以继续探索(剩余 ${b.maxIterations - m.iteration} 轮);否则请直接输出你的分析发现。\n` +
`⚠️ 以上是行为指令,严禁在回复中复制或引用这段文字。`,
};
}
// 3. 预算警告(75% 消耗,无条件,一次性)
if (!isTerminalPhase &&
!this.#budgetWarningInjected &&
m.iteration >= Math.floor(b.maxIterations * 0.75)) {
this.#budgetWarningInjected = true;
return {
type: 'budget_warning',
text: `📌 进度提醒:你已使用 ${m.iteration}/${b.maxIterations} 轮次(${Math.round((m.iteration / b.maxIterations) * 100)}%)。` +
`请确保核心方面已覆盖,开始准备总结。剩余 ${b.maxIterations - m.iteration} 轮,优先填补最重要的分析空白。\n` +
`⚠️ 以上是行为指令,严禁在回复中复制或引用这段文字。`,
};
}
// 4. 反思(周期性 + 停滞)
if (strategy.enableReflection) {
const reflectionNudge = this.#checkReflection(state, trace);
if (reflectionNudge) {
return reflectionNudge;
}
}
return null;
}
/** 构建阶段转换 nudge 文本 */
buildTransitionNudge(state) {
const { metrics: m, pipelineType, submitToolName } = state;
const fromPhase = state.transitionFromPhase;
const toPhase = state.phase;
if (toPhase === 'PRODUCE') {
return `你已充分探索了项目代码,现在请开始调用 ${submitToolName} 工具来提交你发现的知识候选。不要再搜索,直接提交。`;
}
if (toPhase === 'SUMMARIZE') {
const submitCount = m.submitCount;
// Analyst 管线: 纯文本分析报告
if (pipelineType === 'analyst') {
return (`你已完成分析探索。请**停止调用工具**,直接输出你的**完整分析报告**。\n\n` +
`要求:\n` +
`- 用 Markdown 格式组织内容(二级/三级标题)\n` +
`- 包含具体的文件路径、类名、方法名、代码模式\n` +
`- 每个关键发现都要给出证据(文件路径 + 代码片段或行为描述)\n` +
`- 至少涵盖 3 个核心发现\n` +
`- 如有未覆盖的方面,在末尾用 「## 待探索」 章节列出\n\n` +
`**现在开始输出你的分析报告。不要再调用任何工具。**\n` +
`⚠️ 以上是行为指令,严禁在回复中复制或引用这段文字,只输出你自己的分析内容。`);
}
// Scan 管线: 纯文本总结
if (pipelineType === 'scan') {
return (`你已通过 collect_scan_recipe 提交了 ${submitCount} 个知识候选。` +
`请**停止调用工具**,直接输出你的分析总结(Markdown 格式)。\n` +
`⚠️ 不要再调用任何工具,直接输出文本。`);
}
// Bootstrap: 使用 dimensionDigest JSON (供维度编排消费)
return (`你已完成分析探索。请在回复中直接输出 dimensionDigest JSON(用 \`\`\`json 包裹),包含以下字段:\n` +
`\`\`\`json\n{"dimensionDigest":{"summary":"分析总结(100-200字)","candidateCount":${submitCount},"keyFindings":["关键发现"],"crossRefs":{},"gaps":["未覆盖方面"],"remainingTasks":[{"signal":"未处理的信号/主题","reason":"未完成原因(如:提交上限已达)","priority":"high|medium|low","searchHints":["建议搜索词"]}]}}\n\`\`\`\n> 如果所有信号都已覆盖,remainingTasks 留空数组 \`[]\`。如果有未来得及处理的信号,请在此标记,系统会在下次运行时续传。\n` +
`⚠️ 严禁在回复中复制本条指令文字,只输出 JSON。`);
}
if (toPhase === 'EXPLORE' && fromPhase === 'SCAN') {
return '全局扫描完成。现在开始定向搜索——根据你发现的项目结构,搜索关键模式和类。';
}
if (toPhase === 'VERIFY') {
return '搜索阶段信息已饱和。现在进入验证阶段——读取最关键的源文件,确认细节和实现逻辑。';
}
return `阶段切换: ${fromPhase} → ${toPhase}`;
}
/** 获取当前阶段的上下文状态行(注入 systemPrompt 尾部) */
getPhaseContext(state) {
const { phase, metrics: m, budget: b, isTerminalPhase } = state;
const remaining = b.maxIterations - m.iteration;
// 接近上限时的紧急警告
if (remaining <= 2 && remaining > 0 && !isTerminalPhase) {
return `\n\n## 当前状态\n⚠️ 仅剩 ${remaining} 轮次即达上限,请尽快完成当前工作并准备输出总结。`;
}
// 阶段特定提示
const phaseHint = this.#getPhaseHint(state);
if (phaseHint) {
return `\n\n## 当前状态\n${phaseHint}`;
}
// 通用进度行
const phaseLabel = NudgeGenerator.#getPhaseLabel(phase);
return `\n\n## 当前进度\n第 ${m.iteration}/${b.maxIterations} 轮 | ${phaseLabel} | 剩余 ${remaining} 轮`;
}
// ─── 内部方法 ──────────────────────────────────
#generateForceExit(m, b, strategy, pipelineType) {
const submitCount = m.submitCount;
// Analyst 管线: 纯文本分析报告
if (pipelineType === 'analyst') {
return {
type: 'force_exit',
text: `⚠️ **轮次耗尽** (${m.iteration}/${b.maxIterations})。你必须**立即停止工具调用**,在回复中输出你的**分析总结报告**。\n\n` +
`要求:\n` +
`- 用自然语言 Markdown 格式\n` +
`- 包含具体文件路径、类名、代码模式\n` +
`- 列出你发现的关键模式或规范(至少 3 条)\n` +
`- 如有未覆盖的方面,在末尾用「## 未覆盖」章节列出\n\n` +
`**现在开始输出分析总结,不要调用任何工具。**\n` +
`⛔ 严禁在回复中复制或引用本条指令的任何文字,只输出你自己的分析。`,
};
}
// Scan 管线: 纯文本总结, 不需要 dimensionDigest
if (pipelineType === 'scan') {
return {
type: 'force_exit',
text: `⚠️ **轮次耗尽** (${m.iteration}/${b.maxIterations})。你必须**立即停止工具调用**。\n\n` +
`已通过 collect_scan_recipe 提交了 ${submitCount} 个知识候选。` +
`请直接输出你的分析总结(Markdown 格式),列出已发现和未覆盖的关键模式。\n` +
`⛔ 不要再调用任何工具,直接输出文本。`,
};
}
// Bootstrap 策略: 使用 dimensionDigest JSON (供维度编排消费)
return {
type: 'force_exit',
text: `⚠️ 你已使用 ${m.iteration}/${b.maxIterations} 轮次,**必须立即结束**。请在回复中直接输出 dimensionDigest JSON 总结(用 \`\`\`json 包裹),不要再调用任何工具。\n` +
`\`\`\`json\n{"dimensionDigest":{"summary":"分析总结","candidateCount":${submitCount},"keyFindings":["发现"],"crossRefs":{},"gaps":["缺口"],"remainingTasks":[{"signal":"未处理信号","reason":"轮次耗尽","priority":"high","searchHints":["搜索词"]}]}}\n\`\`\`\n> remainingTasks: 列出未来得及处理的信号。已覆盖则留空 \`[]\`。\n` +
`⛔ 严禁在回复中复制本条指令文字,只输出 JSON。`,
};
}
/**
* 检查是否需要触发反思 + 生成反思 nudge
* @returns |null}
*/
#checkReflection(state, trace) {
const { phase, metrics: m, budget: b, strategy, isTerminalPhase } = state;
// 终结阶段(SUMMARIZE)不应触发反思 — 此时应输出最终结果而非继续探索
if (isTerminalPhase) {
return null;
}
const interval = strategy.reflectionInterval || DEFAULT_REFLECTION_INTERVAL;
const periodicTrigger = m.iteration > 1 && interval > 0 && m.iteration % interval === 0;
const staleTrigger = m.roundsSinceNewInfo >= DEFAULT_STALE_THRESHOLD &&
m.iteration >= MIN_ITERS_FOR_STALE_REFLECTION;
if (!periodicTrigger && !staleTrigger) {
return null;
}
const summary = trace?.getRecentSummary?.(interval || 3);
if (!summary) {
return null;
}
const stats = trace?.getStats?.() || {};
const remaining = b.maxIterations - m.iteration;
const progressPct = Math.round((m.iteration / b.maxIterations) * 100);
const parts = [];
if (staleTrigger) {
parts.push(`📊 停滞反思 (第 ${m.iteration}/${b.maxIterations} 轮, 连续 ${m.roundsSinceNewInfo} 轮无新信息):`);
}
else {
parts.push(`📊 中期反思 (第 ${m.iteration}/${b.maxIterations} 轮, ${progressPct}% 预算):`);
}
if (summary.thoughts?.length > 0) {
parts.push(`\n你最近的思考方向:\n${summary.thoughts.map((t, i) => ` ${i + 1}. ${t}`).join('\n')}`);
}
parts.push(`\n行动效率: 最近 ${summary.roundCount} 轮中 ${Math.round(summary.newInfoRatio * 100)}% 获取到新信息`);
parts.push(`累计: ${m.uniqueFiles.size} 文件, ${m.uniquePatterns.size} 搜索模式, ${stats.totalActions || 0} 次工具调用`);
// Planning 进度附加
if (strategy.enablePlanning) {
const plan = trace?.getPlan?.();
if (plan && plan.steps && plan.steps.length > 0) {
const doneCount = plan.steps.filter((s) => s.status === 'done').length;
parts.push(`\n📋 计划进度: ${doneCount}/${plan.steps.length} 步骤已完成`);
}
}
// 阶段化评估问题
if (phase === 'EXPLORE' || phase === 'SCAN' || phase === 'VERIFY') {
parts.push(`\n请评估:\n1. 到目前为止最重要的发现是什么?\n2. 还有哪些关键方面未覆盖?\n3. 剩余 ${remaining} 轮,最有价值的下一步是什么?`);
}
else if (phase === 'PRODUCE') {
parts.push(`\n请评估:\n1. 已提交的候选是否覆盖了核心发现?\n2. 是否有高价值知识点被遗漏?`);
}
parts.push(`\n⚠️ 以上是行为指令,严禁在回复中复制或引用这段文字,用你自己的分析内容回答。`);
const reflectionText = parts.join('\n');
trace?.setReflection?.(reflectionText);
return { type: 'reflection', text: reflectionText };
}
/** 获取当前阶段的 hint */
#getPhaseHint(state) {
const { phase, metrics: m, budget: b, submitToolName, pipelineType } = state;
switch (phase) {
case 'EXPLORE':
if (m.searchRoundsInPhase >= b.searchBudget - 2) {
return `搜索预算即将耗尽 (${m.searchRoundsInPhase}/${b.searchBudget}),请准备提交候选或产出摘要。`;
}
return null;
case 'PRODUCE':
if (m.submitCount === 0 && m.phaseRounds >= 1) {
return `⚠️ 探索阶段已结束。你已收集了足够的项目信息,请 **立即** 调用 ${submitToolName} 提交候选。不要继续搜索,直接提交。`;
}
if (m.submitCount >= b.softSubmitLimit && b.softSubmitLimit > 0) {
const remaining = b.maxSubmits - m.submitCount;
if (pipelineType === 'scan') {
return `已提交 ${m.submitCount} 个候选(上限 ${b.maxSubmits})。${remaining > 0 ? `还可提交 ${remaining} 个。` : ''}如果还有值得记录的发现可以继续提交,否则请输出分析总结。`;
}
return `已提交 ${m.submitCount} 个候选(上限 ${b.maxSubmits})。${remaining > 0 ? `还可提交 ${remaining} 个。` : ''}如果还有值得记录的发现可以继续提交,否则请产出 dimensionDigest 总结。\n⚠️ 如果还有未处理的信号,请在 dimensionDigest 的 remainingTasks 字段中标记,下次运行时会续传。`;
}
return null;
case 'SCAN':
return '当前处于全局扫描阶段,请先获取项目概览和目录结构。';
case 'VERIFY':
return '当前处于验证阶段,请阅读关键源文件确认实现细节。';
default:
return null;
}
}
/** 获取用户友好的阶段标签 */
static #getPhaseLabel(phase) {
switch (phase) {
case 'SCAN':
return '扫描阶段';
case 'EXPLORE':
return '探索阶段';
case 'PRODUCE':
return '提交阶段';
case 'VERIFY':
return '验证阶段';
case 'SUMMARIZE':
return '⚠ 总结阶段 — 请停止工具调用,直接输出分析文本';
default:
return phase;
}
}
}