autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
361 lines (354 loc) • 18.1 kB
JavaScript
/**
* insight-analyst.js — Insight Analyst 领域函数
*
* 从旧 AnalystAgent.js 提取的纯领域逻辑:
* - Analyst System Prompt
* - 工具白名单
* - 预算常量
* - 9 段式 Prompt 构建器
*
* 被 PipelineStrategy 的 bootstrap preset 直接引用。
* 不再包含任何 Agent 类 — Agent 由 AgentRuntime + PipelineStrategy 驱动。
*
* @module insight-analyst
*/
import { getDimensionSOP } from '#domain/dimension/DimensionSop.js';
// ──────────────────────────────────────────────────────────────────
// System Prompt — Analyst 专用 (~100 tokens)
// ──────────────────────────────────────────────────────────────────
export const ANALYST_SYSTEM_PROMPT = `你是一位高级软件架构师,正在深度分析一个真实项目的某个维度。
## 执行计划
你有 **N 轮**工具调用机会(系统会告知具体数字)。请严格按以下节奏分配:
| 阶段 | 轮次占比 | 目标 |
|------|---------|------|
| 1. 全局扫描 | 第 1-3 轮 | get_project_overview + list_project_structure 了解项目结构 |
| 2. 结构化探索 | 第 4-N×60% 轮 | get_class_hierarchy / get_class_info 理解核心类;search_project_code 批量搜索关键模式 |
| 3. 深度验证 | 第 N×60%-N×80% 轮 | read_project_file 阅读关键实现,确认细节 |
| 4. 输出总结 | 最后 20% | **停止调用工具**,直接输出你的分析文本 |
## 关键规则
- **到达 80% 轮次时必须开始写总结**,不要等系统提醒
- 每一轮都必须调用工具获取新信息,不要花轮次在纯文本思考上
- 不要重复搜索相同关键词或读取相同文件(系统会返回缓存并扣轮次)
## 工具效率
- **批量搜索**: search_project_code({ patterns: ["keywordA", "keywordB", "keywordC"] }) — 一次搜 3-5 个
- **批量读文件**: read_project_file({ filePaths: ["a.m", "b.m", "c.m"] }) — 一次读 3-5 个
- **结构化查询优先**: get_class_hierarchy / get_class_info 比文本搜索更精确高效
## 输出要求
输出你的分析发现,包括具体的文件完整相对路径(从项目根目录开始)和行号。
每个文件引用格式: (来源: Full/Relative/Path/FileName.ext:行号)
禁止只写文件名,必须写从项目根开始的完整路径。
标注每个发现所属的模块/包名。
用自然语言描述你的理解,不需要特定格式。`;
// ──────────────────────────────────────────────────────────────────
// Analyst 可用工具白名单 — 只做探索,不做提交
// ──────────────────────────────────────────────────────────────────
export const ANALYST_TOOLS = [
// AST 结构化分析
'get_project_overview',
'get_class_hierarchy',
'get_class_info',
'get_protocol_info',
'get_method_overrides',
'get_category_map',
// 文件访问
'search_project_code',
'read_project_file',
'list_project_structure',
'get_file_summary',
'semantic_search_code',
// 前序上下文 (可选)
'get_previous_analysis',
// Agent Memory (v4.0) — 工作记忆 + 情景记忆
'note_finding',
'get_previous_evidence',
// Phase E: 代码实体图谱查询
'query_code_graph',
];
// ──────────────────────────────────────────────────────────────────
// Analyst 预算 — 使用 analyst 策略(自由探索,无阶段约束)
// ──────────────────────────────────────────────────────────────────
/** 默认 Analyst 预算(24 轮基线) */
export const ANALYST_BUDGET = {
maxIterations: 24,
searchBudget: 18,
searchBudgetGrace: 10,
maxSubmits: 0,
softSubmitLimit: 0,
idleRoundsToExit: 2,
};
/**
* 根据项目规模自适应计算 Analyst 预算
*
* 策略: 以文件数为主要缩放因子,保持 searchBudget/maxIterations 的比例关系。
* - ≤40 文件: 基线 24 轮(小型项目无需额外预算)
* - 41~100 文件: 线性插值到 32 轮
* - 101~200 文件: 线性插值到 40 轮
* - >200 文件: 封顶 40 轮(避免单维度成本失控)
*
* searchBudget 按比例随 maxIterations 缩放(保持 75%)。
* timeoutMs 按比例随 maxIterations 缩放(基线 300s 对应 24 轮)。
*/
export function computeAnalystBudget(fileCount) {
const clamped = Math.max(0, fileCount);
let maxIter;
if (clamped <= 40) {
maxIter = 24;
}
else if (clamped <= 100) {
// 40→100 文件: 24→32 轮(线性插值)
maxIter = Math.round(24 + ((clamped - 40) / 60) * 8);
}
else if (clamped <= 200) {
// 100→200 文件: 32→40 轮
maxIter = Math.round(32 + ((clamped - 100) / 100) * 8);
}
else {
maxIter = 40;
}
return {
...ANALYST_BUDGET,
maxIterations: maxIter,
searchBudget: Math.round(maxIter * 0.75),
// 超时随轮次等比缩放: 24轮→300s, 40轮→500s
timeoutMs: Math.round((maxIter / 24) * 300_000),
};
}
/**
* 构建 Analyst Prompt
*
* 12 段结构:
* §1 任务描述
* §2 维度指引
* §3 SOP (分析步骤 + 常见错误)
* §4 输出要求
* §5 工具提示
* §6 前序维度上下文 (SessionStore / DimensionContext)
* §7 Tier Reflection 洞察
* §8 历史语义记忆 (Tier 3)
* §9 代码实体图谱 (Phase E)
* §ES 分析起点证据 (Phase 1-4 Evidence Starters)
* §M1 全景上下文 (Panorama Phase 1.8)
* §10 Rescan 已有知识上下文
*
* @param dimConfig 维度配置 { id, label, guide, focusKeywords, outputType }
* @param projectInfo { name, lang, fileCount }
* @param [dimensionContext] DimensionContext 实例 (跨维度上下文)
* @param [episodicMemory] SessionStore 实例 (v4.0 增强上下文)
* @param [semanticMemory] PersistentMemory 实例 (v4.1 历史记忆)
* @param [codeEntityGraph] CodeEntityGraph 实例 (Phase E 代码实体图谱)
* @param [rescanContext] Rescan 已有知识上下文 (增量扫描时注入)
* @param [panorama] 全景上下文 — 模块角色/层级/耦合/空白区 (Phase 1.8)
* @param [evidenceStarters] Phase 1-4 证据启发 — 维度级分析起点
* @param [evolutionResult] Evolution Stage 产出 — 避免重复分析已处理的 Recipe
*/
export async function buildAnalystPrompt(dimConfig, projectInfo, dimensionContext, episodicMemory, semanticMemory, codeEntityGraph, rescanContext, panorama, evidenceStarters, evolutionResult) {
const parts = [];
// §1 任务描述
parts.push(`分析项目 ${projectInfo.name} (${projectInfo.lang}, ${projectInfo.fileCount} 个文件) 的 ${dimConfig.label}。`);
// §2 维度指引
if (dimConfig.guide) {
parts.push(dimConfig.guide);
}
// §3 结构化 SOP (优先) — 替代 focusAreas
const sop = getDimensionSOP(dimConfig.id);
if (sop) {
parts.push('## 分析步骤 (SOP)');
for (const step of sop.steps) {
parts.push(`### ${step.phase}`);
parts.push(step.action);
if (step.expectedOutput) {
parts.push(`→ 预期产出: ${step.expectedOutput}`);
}
}
// §3.1 常见错误 (关键质量防护)
if (sop.commonMistakes && sop.commonMistakes.length > 0) {
parts.push('## ⚠️ 常见错误(务必避免)');
for (const m of sop.commonMistakes) {
parts.push(`- ${m}`);
}
}
}
else if (dimConfig.guide) {
const items = dimConfig.guide
.split(/[、,,/]/)
.map((s) => s.trim())
.filter(Boolean);
if (items.length > 1) {
parts.push(`重点关注:\n${items.map((f) => `- ${f}`).join('\n')}`);
}
else {
parts.push(`重点关注: ${dimConfig.guide}`);
}
}
// §4 输出要求
const outputType = dimConfig.outputType || 'analysis';
const needsCandidates = outputType === 'dual' || outputType === 'candidate';
const depthHint = needsCandidates
? '你的分析将被转化为知识候选,请确保每个发现都有足够的代码证据和文件引用。按实际发现总结,有几个独立知识点就写几个。'
: '';
parts.push(`请将分析组织成结构化段落,包含:
1. 在哪些文件/类中发现 (写出从项目根目录开始的完整相对路径+行号,如 Packages/ModuleName/Sources/.../FileName.swift:42)
2. 具体的实现方式和代码特征
3. 为什么选择这种方式(设计意图)
4. 统计数据 (如数量、占比)
5. 所属模块/包名(特别是来自本地子包的发现)
每个关键发现用编号列表呈现,引用 3 个以上具体文件(完整相对路径)。
禁止只写文件名(如 NetworkClient.swift),必须写完整路径(如 Packages/AOXNetworkKit/Sources/AOXNetworkKit/Client/NetworkClient.swift:42)。
${depthHint}
重要: 务必使用 read_project_file 阅读代码确认,不要假设文件存在。引用的每个文件路径都必须是你亲眼看到的。
【跨维度去重】只分析属于当前维度视角的内容。不要将其他维度的知识点混入本维度来充数。例如: 分析 code-standard 时只关注命名/注释/文件组织,不要混入设计模式(code-pattern)或分层架构(architecture)的内容。如果某个发现与多个维度相关,则只从当前维度的核心视角分析,避免与其他维度产生重叠。
【本地子包覆盖】如果项目有本地子包/模块(如 Packages/ 目录下的包),必须同时分析其内部实现,不得仅看主项目对其的调用。`);
// §5 前序上下文提示
parts.push('可以调用 get_previous_analysis 获取前序维度的分析结果,避免重复分析。');
parts.push('使用 note_finding 工具记录关键发现到工作记忆,确保重要信息不会在后期被遗忘。');
parts.push('使用 get_previous_evidence 工具查询前序维度对特定文件/类的分析证据,避免重复搜索。');
// §6 前序维度分析摘要 (Tier 2+ 才有)
if (episodicMemory) {
const emContext = episodicMemory.buildContextForDimension(dimConfig.id, dimConfig.focusKeywords || []);
if (emContext) {
parts.push(emContext);
}
// §7: Tier Reflection 洞察
const reflections = episodicMemory.getRelevantReflections(dimConfig.id);
if (reflections) {
parts.push('## 跨维度综合洞察');
parts.push(reflections);
}
}
else if (dimensionContext) {
const snapshot = dimensionContext.buildContextForDimension(dimConfig.id);
const prevDims = Object.entries(snapshot.previousDimensions);
if (prevDims.length > 0) {
parts.push(`## 前序维度分析摘要(避免重复探索)`);
for (const [dimId, digest] of prevDims) {
parts.push(`### ${dimId}\n${digest.summary || '(无摘要)'}`);
if (digest.keyFindings?.length > 0) {
parts.push(`关键发现: ${digest.keyFindings.join('; ')}`);
}
if (digest.crossRefs?.[dimConfig.id]) {
parts.push(`💡 对本维度的建议: ${digest.crossRefs[dimConfig.id]}`);
}
}
}
}
// §8: 历史语义记忆 (Tier 3)
if (semanticMemory) {
try {
const query = `${dimConfig.label} ${dimConfig.guide || ''} ${projectInfo.lang}`;
const section = await semanticMemory.toPromptSection({
source: 'bootstrap',
query,
limit: 10,
});
if (section) {
parts.push(section);
}
}
catch {
/* SemanticMemory retrieval failed, non-critical */
}
}
// §9: 代码实体图谱 (Phase E)
if (codeEntityGraph) {
try {
const graphCtx = codeEntityGraph.generateContextForAgent({ maxEntities: 20, maxEdges: 40 });
if (graphCtx) {
parts.push(graphCtx);
parts.push('使用 query_code_graph 工具可以查询更详细的继承链、影响分析等。');
}
}
catch {
/* CodeEntityGraph context failed, non-critical */
}
}
// §ES: 分析起点证据 (Phase 1-4 Evidence Starters)
if (evidenceStarters && Object.keys(evidenceStarters).length > 0) {
const esLines = ['## 📊 分析起点 (Phase 1-4 自动检测)'];
esLines.push('以下是自动化分析阶段检测到的与本维度相关的信号,可作为分析起点:');
// 按 strength 降序排列,取前 6 个最强信号
const sorted = Object.entries(evidenceStarters)
.sort(([, a], [, b]) => (b.strength ?? 50) - (a.strength ?? 50))
.slice(0, 6);
for (const [key, entry] of sorted) {
const strengthBadge = (entry.strength ?? 50) >= 75 ? '⚠️' : '📝';
esLines.push(`${strengthBadge} **${key}**: ${entry.hint}`);
if (entry.data) {
const dataStr = Array.isArray(entry.data)
? entry.data
.slice(0, 5)
.map((d) => ` - ${typeof d === 'string' ? d : JSON.stringify(d)}`)
.join('\n')
: typeof entry.data === 'string'
? ` ${entry.data}`
: ` ${JSON.stringify(entry.data, null, 0).slice(0, 300)}`;
esLines.push(dataStr);
}
}
esLines.push('');
esLines.push('利用上述信号作为分析切入点,用 read_project_file 验证并深入探索。');
parts.push(esLines.join('\n'));
}
// §M1: 全景上下文 (Panorama Phase 1.8) — 模块角色/层级/耦合/空白区
if (panorama) {
const pLines = ['## 🏗️ 项目全景 (Panorama)'];
if (panorama.layerContext) {
pLines.push(`架构层级: ${panorama.layerContext}`);
}
if (panorama.moduleRole) {
pLines.push(`当前模块角色: ${panorama.moduleRole}${panorama.moduleLayer !== null ? ` (L${panorama.moduleLayer})` : ''}`);
}
if (panorama.moduleCoupling) {
pLines.push(`耦合度: fanIn=${panorama.moduleCoupling.fanIn}, fanOut=${panorama.moduleCoupling.fanOut}`);
}
if (panorama.knownGaps.length > 0) {
pLines.push(`已知空白区: ${panorama.knownGaps.join(', ')}`);
pLines.push('分析时请特别关注上述空白区,它们是最可能产出新知识的方向。');
}
parts.push(pLines.join('\n'));
}
// §EVO: Evolution 结果 — 避免重复覆盖已被 Evolution Agent 处理的模式
if (evolutionResult && evolutionResult.totalRecipes && evolutionResult.totalRecipes > 0) {
const evoLines = [
'## 🔄 Evolution 结果',
`Evolution Agent 已审查本维度 ${evolutionResult.totalRecipes} 个现有 Recipe:`,
`- 进化: ${evolutionResult.evolved ?? 0} 个(已提交新版本替代旧 Recipe)`,
`- 废弃: ${evolutionResult.deprecated ?? 0} 个(已确认过时)`,
`- 跳过: ${evolutionResult.skipped ?? 0} 个(仍然有效或信息不足)`,
'',
'**你的分析应关注发现新知识点**,不要重复覆盖已处理的模式。',
];
parts.push(evoLines.join('\n'));
}
// §10a: Rescan 有效知识上下文 — 避免重复分析已覆盖的模式
if (rescanContext && rescanContext.existingRecipes.length > 0) {
const lines = [
'## ⚠️ 增量扫描模式 — 已有知识 (勿重复)',
`本维度已有 ${rescanContext.existing} 个有效 Recipe,需补齐 ${rescanContext.gap} 个。`,
'已有 Recipe 标题:',
];
for (const r of rescanContext.existingRecipes.slice(0, 10)) {
const triggerTag = r.trigger ? ` (trigger: ${r.trigger})` : '';
lines.push(`- "${r.title}"${triggerTag}`);
}
if (rescanContext.existingRecipes.length > 10) {
lines.push(`- ... 共 ${rescanContext.existingRecipes.length} 个`);
}
lines.push('');
lines.push('**你的任务**: 专注发现上述 Recipe **尚未覆盖**的新模式。不要重复分析相同的代码特征。');
parts.push(lines.join('\n'));
}
// §10b: Rescan 衰退知识 — 可以替换
if (rescanContext?.decayingRecipes && rescanContext.decayingRecipes.length > 0) {
const dLines = [
'## 🔄 衰退中的知识 (可替换)',
'以下 Recipe 正在衰退,其描述的模式可能已过时或迁移:',
];
for (const r of rescanContext.decayingRecipes.slice(0, 5)) {
dLines.push(`- "${r.title}" — 衰退原因: ${r.decayReason || '未知'}`);
}
dLines.push('如果你在分析中发现了这些模式的**更新版本**(例如类改名、迁移到新模块),');
dLines.push('请记录下来,后续 Producer 可以用 supersedes 参数提交替代版本。');
parts.push(dLines.join('\n'));
}
return parts.join('\n\n');
}