autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
310 lines (303 loc) • 14.9 kB
JavaScript
/**
* forced-summary.js — 强制退出后的摘要生成
*
* 强制退出后的摘要生成独立模块,
* 供 AgentRuntime.reactLoop() 在循环退出后调用。
*
* 支持三种模式 (根据 source + tracker.pipelineType 判断):
* - system + analyst: 输出 Markdown 分析报告 (供 Quality Gate 评估)
* - system + bootstrap: 输出 dimensionDigest JSON (供维度编排消费)
* - user: 输出人类可读的 Markdown 结构化总结 (前端 AI Chat 展示)
*
* @module forced-summary
*/
import Logger from '#infra/logging/Logger.js';
import { cleanFinalAnswer } from './core/ChatAgentPrompts.js';
const logger = Logger.getInstance();
/**
* 生成强制摘要
*
* @param opts.aiProvider LLM 提供商
* @param [opts.source] 'user' | 'system'
* @param opts.toolCalls 工具调用记录
* @param [opts.tracker] ExplorationTracker 实例
* @param [opts.contextWindow] ContextWindow 实例 (用于避免超出 token)
* @param opts.prompt 原始用户 prompt
* @param [opts.tokenUsage] token 用量 (会被修改)
* @returns }>}
*/
export async function produceForcedSummary({ aiProvider, source, toolCalls = [], tracker, contextWindow, prompt, tokenUsage, }) {
const isSystem = source === 'system';
const iterations = tracker?.iteration || 0;
const pipelineType = tracker?.pipelineType || (isSystem ? 'bootstrap' : 'user');
// Analyst 管线虽然 source='system',但期望 Markdown 分析报告而非 dimensionDigest JSON
const isAnalyst = pipelineType === 'analyst';
const resultTokenUsage = { input: 0, output: 0 };
logger.info(`[ForcedSummary] ⚠ producing forced summary (${iterations} iters, ${toolCalls.length} calls, source=${source}, pipeline=${pipelineType})`);
const candidateCount = toolCalls.filter((tc) => tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check').length;
let finalReply;
// 如果熔断器已打开,跳过 AI 调用直接合成摘要
const isCircuitOpen = aiProvider._circuitState === 'OPEN' || aiProvider.name === 'mock';
if (isCircuitOpen) {
const outputType = isAnalyst ? 'analysis' : isSystem ? 'digest' : 'summary';
logger.warn(`[ForcedSummary] circuit breaker is OPEN — skipping AI summary, using synthetic ${outputType}`);
}
// 收集工具调用摘要
const submitSummary = toolCalls
.filter((tc) => tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check')
.map((tc, i) => `${i + 1}. ${tc.args?.title || tc.args?.category || tc.params?.title || tc.params?.category || 'untitled'}`)
.join('\n');
try {
if (isCircuitOpen) {
throw new Error('circuit open — skip to synthetic summary');
}
let summaryPrompt;
let systemPrompt;
if (isSystem && isAnalyst) {
// Analyst 管线 (source=system): Markdown 分析报告 — 与 NudgeGenerator.buildTransitionNudge 对齐
const toolContextSummary = buildToolContextForUserSummary(toolCalls);
summaryPrompt = `你刚才通过 ${toolCalls.length} 次工具调用分析了项目代码。以下是你调用过的工具和获取到的关键信息:
${toolContextSummary}
请基于以上收集到的信息,用**清晰易读的 Markdown** 格式撰写代码分析报告。
要求:
- 使用二级/三级标题组织内容(## 和 ###)
- 包含具体的代码文件路径、类名、模式名称等细节
- 每个关键发现都要给出证据(文件路径 + 代码片段或行为描述)
- 至少涵盖 3 个核心发现
- 如有未覆盖的方面,在末尾用「## 待探索」章节列出`;
systemPrompt =
'你是项目代码分析专家。请用纯 Markdown 格式输出结构清晰的分析报告,包含具体文件路径和代码模式。不要输出 JSON 格式。';
}
else if (isSystem) {
// Bootstrap 管线 (source=system): dimensionDigest JSON
summaryPrompt = `你已完成 ${iterations} 轮工具调用(共 ${toolCalls.length} 次),提交了 ${candidateCount} 个候选。
${submitSummary ? `已提交候选:\n${submitSummary}\n` : ''}
**必须**输出 dimensionDigest JSON(用 \`\`\`json 包裹):
\`\`\`json
{
"dimensionDigest": {
"summary": "本维度分析总结",
"candidateCount": ${candidateCount},
"keyFindings": ["发现1", "发现2"],
"crossRefs": {},
"gaps": ["未覆盖方面"],
"remainingTasks": [
{ "signal": "未处理信号名", "reason": "达到提交上限/时间限制", "priority": "high", "searchHints": ["搜索词"] }
]
}
}
\`\`\`
> remainingTasks: 列出本次未来得及处理的信号/主题。已全部覆盖则留空 \`[]\`。`;
systemPrompt = '直接输出 dimensionDigest JSON 总结,不要调用工具。';
}
else {
// user 源: Markdown 结构化总结
const userQuestion = prompt ? `用户的原始问题:「${prompt.slice(0, 500)}」\n\n` : '';
const toolContextSummary = buildToolContextForUserSummary(toolCalls);
summaryPrompt = `${userQuestion}你刚才通过 ${toolCalls.length} 次工具调用分析了项目代码。以下是你调用过的工具和获取到的关键信息:
${toolContextSummary}
请基于以上收集到的信息,用**清晰易读的 Markdown** 格式撰写分析总结,直接回答用户的问题。
要求:
- 使用二级/三级标题组织内容
- 要有具体的代码文件路径、类名、模式名称等细节
- 关键发现用列表项罗列
- 如果发现了架构模式或最佳实践,用简短代码块举例
- 语言自然流畅,像一份技术分析报告`;
systemPrompt =
'你是项目分析助手。请用纯 Markdown 格式输出结构清晰的分析总结,只输出人类可读的自然语言文档,不要输出 JSON 格式的数据。';
}
// 用空 messages 避免累积上下文导致 400
const summaryResult = await aiProvider.chatWithTools(summaryPrompt, {
messages: [],
toolChoice: 'none',
systemPrompt,
temperature: isSystem ? 0.3 : 0.5,
maxTokens: 8192,
});
const result = summaryResult;
if (result.usage) {
resultTokenUsage.input += result.usage.inputTokens || 0;
resultTokenUsage.output += result.usage.outputTokens || 0;
}
// system 源 (非 analyst): dimensionDigest JSON 是预期输出,不能被 cleanFinalAnswer 剥掉
// analyst 源: Markdown 分析报告,需要 cleanFinalAnswer 清理
finalReply =
isSystem && !isAnalyst
? (summaryResult.text || '').trim()
: cleanFinalAnswer(summaryResult.text || '');
}
catch (err) {
logger.warn(`[ForcedSummary] AI call failed: ${err.message}`);
if (isSystem && isAnalyst) {
// Analyst 管线兜底: 从工具调用记录合成 Markdown 分析报告
const toolNames = [...new Set(toolCalls.map((tc) => tc.tool))];
const filesRead = toolCalls
.filter((tc) => tc.tool === 'read_project_file')
.flatMap((tc) => {
const p = tc.args || tc.params || {};
if (p.filePaths) {
return p.filePaths;
}
if (p.filePath) {
return [p.filePath];
}
return [];
})
.slice(0, 15);
const searches = toolCalls
.filter((tc) => tc.tool === 'search_project_code' || tc.tool === 'semantic_search_code')
.map((tc) => {
const p = tc.args || tc.params || {};
return p.patterns?.[0] || p.query || p.pattern;
})
.filter((v) => Boolean(v))
.slice(0, 8);
const classesExplored = toolCalls
.filter((tc) => tc.tool === 'get_class_info' || tc.tool === 'get_class_hierarchy')
.map((tc) => (tc.args || tc.params)?.className)
.filter((v) => Boolean(v))
.slice(0, 10);
finalReply = `## 代码分析报告\n\n通过 **${toolCalls.length} 次工具调用**(${iterations} 轮迭代)探索了项目代码。\n\n`;
if (filesRead.length > 0) {
finalReply += `### 分析的源文件\n${filesRead.map((f) => `- \`${f}\``).join('\n')}\n\n`;
}
if (classesExplored.length > 0) {
finalReply += `### 探索的类/模块\n${classesExplored.map((c) => `- \`${c}\``).join('\n')}\n\n`;
}
if (searches.length > 0) {
finalReply += `### 搜索的代码模式\n${searches.map((s) => `- \`${s}\``).join('\n')}\n\n`;
}
finalReply += `### 使用的工具\n${toolNames.map((t) => `- ${t}`).join('\n')}\n\n`;
finalReply += '> ⚠️ AI 服务异常,未能生成完整分析。以上为工具调用记录摘要。';
}
else if (isSystem) {
// system 源兜底: 合成 dimensionDigest JSON
const titles = toolCalls
.filter((tc) => tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check')
.map((tc) => tc.args?.title || tc.params?.title || 'untitled');
finalReply = `\`\`\`json
{
"dimensionDigest": {
"summary": "通过 ${toolCalls.length} 次工具调用分析了项目代码,提交了 ${candidateCount} 个候选。",
"candidateCount": ${candidateCount},
"keyFindings": ${JSON.stringify(titles.slice(0, 5))},
"crossRefs": {},
"gaps": ["AI 服务异常,部分分析未完成"]
}
}
\`\`\``;
}
else {
// user 源兜底: 合成 Markdown 摘要
const toolNames = [...new Set(toolCalls.map((tc) => tc.tool))];
const filesRead = toolCalls
.filter((tc) => tc.tool === 'read_project_file')
.flatMap((tc) => {
const p = tc.args || tc.params || {};
if (p.filePaths) {
return p.filePaths;
}
if (p.filePath) {
return [p.filePath];
}
return [];
})
.slice(0, 10);
const searches = toolCalls
.filter((tc) => tc.tool === 'search_project_code' || tc.tool === 'semantic_search_code')
.map((tc) => {
const p = tc.args || tc.params || {};
return p.patterns?.[0] || p.query || p.pattern;
})
.filter((v) => Boolean(v))
.slice(0, 5);
finalReply = `## 分析总结\n\n通过 **${toolCalls.length} 次工具调用**探索了项目代码。\n\n`;
if (searches.length > 0) {
finalReply += `### 搜索的关键词\n${searches.map((s) => `- \`${s}\``).join('\n')}\n\n`;
}
if (filesRead.length > 0) {
finalReply += `### 读取的文件\n${filesRead.map((f) => `- \`${f}\``).join('\n')}\n\n`;
}
finalReply += `### 使用的工具\n${toolNames.map((t) => `- ${t}`).join('\n')}\n\n`;
finalReply += '> ⚠️ AI 服务异常,未能生成完整分析。请稍后重试或缩小分析范围。';
}
}
// 兜底: 确保 finalReply 始终非空
if (!finalReply) {
logger.warn('[ForcedSummary] ⚠ finalReply is empty after all paths — using fallback');
finalReply = `## 分析总结\n\n通过 **${toolCalls.length} 次工具调用**探索了项目代码,但未能生成完整分析。请重试或缩小分析范围。`;
}
logger.info(`[ForcedSummary] ✅ forced summary — ${finalReply.length} chars`);
return { reply: finalReply, tokenUsage: resultTokenUsage };
}
/** 从工具调用记录中提取上下文摘要 (供 user 源强制总结使用) */
function buildToolContextForUserSummary(toolCalls) {
const sections = [];
// 目录结构探索
const structureCalls = toolCalls.filter((tc) => tc.tool === 'list_project_structure');
if (structureCalls.length > 0) {
const dirs = structureCalls
.map((tc) => (tc.args || tc.params)?.directory || '/')
.slice(0, 5);
sections.push(`**目录探索**: ${dirs.map((d) => `\`${d}\``).join(', ')}`);
}
// 项目概况
const overviewCalls = toolCalls.filter((tc) => tc.tool === 'get_project_overview');
if (overviewCalls.length > 0) {
sections.push('**项目概况**: 已获取');
}
// 代码搜索
const searchCalls = toolCalls.filter((tc) => tc.tool === 'search_project_code' || tc.tool === 'semantic_search_code');
if (searchCalls.length > 0) {
const queries = searchCalls
.map((tc) => {
const p = tc.args || tc.params || {};
return p.patterns?.[0] || p.query || p.pattern;
})
.filter((v) => Boolean(v))
.slice(0, 8);
sections.push(`**代码搜索** (${searchCalls.length} 次): ${queries.map((q) => `\`${q}\``).join(', ')}`);
}
// 文件读取
const readCalls = toolCalls.filter((tc) => tc.tool === 'read_project_file');
if (readCalls.length > 0) {
const files = readCalls
.flatMap((tc) => {
const p = tc.args || tc.params || {};
if (p.filePaths) {
return p.filePaths;
}
if (p.filePath) {
return [p.filePath];
}
return [];
})
.slice(0, 10);
sections.push(`**文件读取** (${readCalls.length} 次): ${files.map((f) => `\`${f}\``).join(', ')}`);
}
// AST 分析
const astCalls = toolCalls.filter((tc) => [
'get_class_hierarchy',
'get_class_info',
'get_protocol_info',
'get_method_overrides',
'get_category_map',
].includes(tc.tool));
if (astCalls.length > 0) {
const entities = astCalls
.map((tc) => {
const p = tc.args || tc.params || {};
return p.className || p.name || p.protocolName || p.rootClass;
})
.filter((v) => Boolean(v))
.slice(0, 5);
sections.push(`**AST 结构分析** (${astCalls.length} 次): ${entities.map((e) => `\`${e}\``).join(', ')}`);
}
// 知识库搜索
const kbCalls = toolCalls.filter((tc) => ['search_knowledge', 'search_recipes', 'knowledge_overview'].includes(tc.tool));
if (kbCalls.length > 0) {
sections.push(`**知识库查询**: ${kbCalls.length} 次`);
}
return sections.length > 0 ? sections.join('\n') : '(工具调用记录为空)';
}
export default produceForcedSummary;