UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

286 lines (280 loc) 15.4 kB
/** * insight-producer.js — Insight Producer 领域函数 * * 从旧 ProducerAgent.js 提取的纯领域逻辑: * - Producer System Prompt * - 工具白名单 * - 预算常量 * - Prompt 构建器 (v1 + v2) * - 代码上下文注入 (evidenceMap → prompt section) * - 拒绝率门控 (producerRejectionGateEvaluator) * * 被 PipelineStrategy 的 bootstrap preset 直接引用。 * 不再包含任何 Agent 类 — Agent 由 AgentRuntime + PipelineStrategy 驱动。 * * @module insight-producer */ import { buildProducerStyleGuide, SUBMIT_REQUIREMENTS } from '#domain/knowledge/StyleGuide.js'; // ────────────────────────────────────────────────────────────────── // System Prompt — Producer 专用 (~150 tokens) // ────────────────────────────────────────────────────────────────── export const PRODUCER_SYSTEM_PROMPT = `你是知识管理专家。你会收到一段代码分析文本,需要将其中的知识点转化为结构化的知识候选。 核心原则: 分析文本已经包含了所有发现,你的唯一工作是将它们格式化为 submit_knowledge 调用。 每个候选必须: 1. 有清晰的标题 (描述知识点的核心,使用项目真实类名,不以项目名开头) 2. 有项目特写风格的正文 (content.markdown 字段,结合代码展示) 3. 标注相关文件的完整相对路径 + 行号(从项目根目录开始,如 Packages/AOXNetworkKit/Sources/.../NetworkClient.swift:42) 4. 选择正确的 kind (rule/pattern/fact) 5. 提供完整的 Cursor 交付字段 (trigger, doClause, whenClause 等) 6. 标注所属模块/包名(如「所属模块: AOXNetworkKit」),特别是来自本地子包的知识 工作流程: 1. 阅读分析文本,识别每个独立的知识点/发现 2. 用 read_project_file 批量获取关键代码片段: read_project_file({ filePaths: ["Full/Path/To/FileA.swift", "Full/Path/To/FileB.swift"], maxLines: 80 }) 3. 立刻调用 submit_knowledge 提交 4. 重复直到分析中的所有知识点都已提交 关键规则: - 分析中的每个要点/段落都应转化为至少一个候选 - read_project_file 支持 filePaths 数组批量读取多个文件,一次调用完成 - read_project_file 时读取足够多的行数(startLine + maxLines 至少 30 行) - reasoning.sources 必须是非空数组,填写文件的完整相对路径如 ["Packages/AOXNetworkKit/Sources/AOXNetworkKit/Client/NetworkClient.swift"](禁止只写文件名) - content.markdown 中的来源标注必须使用完整相对路径+行号: (来源: Full/Path/FileName.ext:行号) - 如果分析提到了 3 个模式,就应该提交 3 个候选,不要合并 - 禁止: 不要搜索新文件、不要做额外分析,专注于格式化和提交 - 【跨维度去重】每条候选必须聚焦当前维度独有的视角,不得将同一知识点换个说法重复提交。相同的类/模式只在最相关的维度出现一次,宁可少提交也不要充数 容错规则: - 如果 read_project_file 返回"文件不存在"或错误,不要重试同一文件的其他路径变体 - 文件读取失败时,直接使用分析文本中已有的代码和描述来提交候选 - 永远不要因为文件读取失败而跳过知识点 — 分析文本已经包含足够信息 - 先提交候选,再考虑是否需要读取更多代码(提交优先于验证)`; // ────────────────────────────────────────────────────────────────── // Producer 可用工具白名单 — 只做格式化和提交 // ────────────────────────────────────────────────────────────────── export const PRODUCER_TOOLS = ['submit_knowledge', 'submit_with_check', 'read_project_file']; // ────────────────────────────────────────────────────────────────── // Producer 预算 // ────────────────────────────────────────────────────────────────── export const PRODUCER_BUDGET = { maxIterations: 24, searchBudget: 4, searchBudgetGrace: 3, maxSubmits: 10, softSubmitLimit: 10, idleRoundsToExit: 3, }; // ────────────────────────────────────────────────────────────────── // 项目特写风格指南 (从共享 StyleGuide.js 获取) // ────────────────────────────────────────────────────────────────── const STYLE_GUIDE = buildProducerStyleGuide(); // ────────────────────────────────────────────────────────────────── // Prompt 构建 // ────────────────────────────────────────────────────────────────── /** * 构建 Producer Prompt (v1 — 用于 AnalysisReport) * * @param dimConfig { id, label, allowedKnowledgeTypes, outputType } * @param projectInfo { name } */ export function buildProducerPrompt(analysisReport, dimConfig, projectInfo) { const parts = []; parts.push(`将以下对 ${projectInfo.name} 项目 "${dimConfig.label}" 维度的分析,转化为知识候选:`); parts.push(`---\n${analysisReport.analysisText}\n---`); if (analysisReport.referencedFiles.length > 0) { parts.push(`分析中引用的关键文件: ${analysisReport.referencedFiles.join(', ')}`); } parts.push(`维度约束: - dimensionId: ${dimConfig.id} - 允许的 knowledgeType: ${(dimConfig.allowedKnowledgeTypes || []).join(', ') || '(all)'} - category: ${dimConfig.id}`); parts.push(STYLE_GUIDE); parts.push(SUBMIT_REQUIREMENTS); return parts.join('\n\n'); } /** * 构建 Producer Prompt v2 — 用于 AnalysisArtifact * * 相比 v1 增加: * - §3 结构化发现 (findings) * - §4 代码证据 (evidenceMap → code context) * - §5 负空间信号 * - §9 (可选) Rescan 模式约束 * - §M1 (可选) 全景上下文 */ export function buildProducerPromptV2(artifact, dimConfig, projectInfo, rescanContext, panorama) { const parts = []; parts.push(`将以下对 ${projectInfo.name} 项目 "${dimConfig.label}" 维度的分析,转化为知识候选:`); parts.push(`---\n${artifact.analysisText}\n---`); // §3 结构化发现 if (artifact.findings?.length > 0) { const findingLines = ['## 关键发现 (Analyst 已确认)']; const sorted = [...artifact.findings].sort((a, b) => b.importance - a.importance); for (const f of sorted) { const badge = f.importance >= 8 ? '⚠️' : '📋'; findingLines.push(`${badge} **[${f.importance}/10]** ${f.finding}`); if (f.evidence) { findingLines.push(` 证据: ${f.evidence}`); } } findingLines.push(''); findingLines.push('☝️ 上述每个发现都应至少转化为一个候选。'); parts.push(findingLines.join('\n')); } // §4 代码证据 const codeContext = buildCodeContextSection(artifact.evidenceMap); if (codeContext) { parts.push(codeContext); } // §5 负空间信号 if (artifact.negativeSignals?.length > 0) { const nsLines = ['## ⛔ 不存在的模式 (不要猜测)']; for (const ns of artifact.negativeSignals.slice(0, 5)) { nsLines.push(`- "${ns.searchPattern}" — ${ns.implication}`); } parts.push(nsLines.join('\n')); } // §6 引用文件 if (artifact.referencedFiles.length > 0) { parts.push(`分析中引用的关键文件: ${artifact.referencedFiles.slice(0, 15).join(', ')}`); } // §7 维度约束 parts.push(`维度约束: - dimensionId: ${dimConfig.id} - 允许的 knowledgeType: ${(dimConfig.allowedKnowledgeTypes || []).join(', ') || '(all)'} - category: ${dimConfig.id}`); // §8 写作指南 + 提交要求 parts.push(STYLE_GUIDE); parts.push(SUBMIT_REQUIREMENTS); // §M1 全景上下文 — 帮助 Producer 理解模块定位 if (panorama) { const pLines = []; if (panorama.moduleRole) { pLines.push(`模块角色: ${panorama.moduleRole}${panorama.moduleLayer !== null ? ` (L${panorama.moduleLayer})` : ''}`); } if (panorama.layerContext) { pLines.push(`架构层级: ${panorama.layerContext}`); } if (panorama.knownGaps.length > 0) { pLines.push(`知识空白区: ${panorama.knownGaps.join(', ')} — 优先为这些方向创建候选。`); } if (pLines.length > 0) { parts.push(`## 🏗️ 项目全景\n${pLines.join('\n')}`); } } // §9a Rescan 模式约束 — 限制提交数量,避免重复 if (rescanContext && rescanContext.gap > 0) { const lines = [ '## ⚠️ 增量扫描模式 — 补齐约束', `本维度已有 ${rescanContext.existing} 个有效知识,需补齐 **${rescanContext.gap}** 个。`, `**提交上限: ${rescanContext.gap} 个候选**。达到目标后立即停止,不要多提交。`, ]; if (rescanContext.occupiedTriggers.length > 0) { lines.push(`已占用的 trigger: ${rescanContext.occupiedTriggers.slice(0, 15).join(', ')}`); lines.push('禁止使用上述已占用的 trigger,必须为新模式创建新 trigger。'); } if (rescanContext.existingRecipes.length > 0) { lines.push('已有知识标题 (禁止重复):'); for (const r of rescanContext.existingRecipes.slice(0, 8)) { lines.push(`- "${r.title}"`); } } parts.push(lines.join('\n')); } // §9b 衰退 Recipe — 可用 supersedes 替换 if (rescanContext?.decayingRecipes && rescanContext.decayingRecipes.length > 0) { const dLines = [ '## 🔄 可替换的衰退知识', '以下 Recipe 正在衰退,如果 Analyst 发现了更新版本的模式,', '你可以用 `supersedes` 参数提交替代版本:', ]; for (const r of rescanContext.decayingRecipes.slice(0, 5)) { dLines.push(`- [${r.id || '?'}] "${r.title}" — ${r.decayReason || '衰退中'}`); dLines.push(` → 替换方式: submit_knowledge({ ...newRecipe, supersedes: "${r.id || ''}" })`); } dLines.push('注意: supersedes 提交会创建观察窗口(72h),不是立即替换。'); dLines.push('替换的新 Recipe 必须基于当前代码,不要复制旧 Recipe 内容。'); parts.push(dLines.join('\n')); } return parts.join('\n\n'); } // ────────────────────────────────────────────────────────────────── // 代码上下文注入 (Producer v2 辅助) // ────────────────────────────────────────────────────────────────── /** * 从 evidenceMap 构建代码上下文段 * * 策略: 按代码片段数量排序 * 预算: ≤ 4000 chars (~1000 tokens) */ export function buildCodeContextSection(evidenceMap) { if (!evidenceMap || evidenceMap.size === 0) { return null; } const parts = ['## 📄 Analyst 已读取的代码 (直接引用, 无需 read_file)']; let totalChars = 0; const BUDGET = 4000; const sortedEntries = [...evidenceMap.values()] .filter((e) => e.codeSnippets.length > 0) .sort((a, b) => b.codeSnippets.length - a.codeSnippets.length); for (const entry of sortedEntries) { if (totalChars >= BUDGET) { break; } const header = `### ${entry.filePath}${entry.role ? ` (${entry.role})` : ''}`; parts.push(header); totalChars += header.length; if (entry.summary) { parts.push(entry.summary); totalChars += entry.summary.length; } for (const snippet of entry.codeSnippets.slice(0, 2)) { if (totalChars >= BUDGET) { break; } const codeBlock = `\`\`\`\n// L${snippet.startLine}-${snippet.endLine}\n${snippet.content}\n\`\`\``; if (snippet.analystNote) { parts.push(`> ${snippet.analystNote}`); totalChars += snippet.analystNote.length + 4; } parts.push(codeBlock); totalChars += codeBlock.length; } } return parts.length > 1 ? parts.join('\n') : null; } // ────────────────────────────────────────────────────────────────── // PipelineStrategy gate.evaluator — 拒绝率门控 // ────────────────────────────────────────────────────────────────── /** * Producer 拒绝率门控 — 面向 PipelineStrategy gate.evaluator * * 当 produce 阶段的提交拒绝率过高时触发 retry。 * * @param source produce 阶段的 reactLoop 返回值 * @returns } */ export function producerRejectionGateEvaluator(source, _phaseResults, _strategyContext = {}) { if (!source?.toolCalls) { return { action: 'pass', reason: '' }; } // 可配置的提交工具名 — bootstrap 用 submit_knowledge/submit_with_check,scan 用 collect_scan_recipe const submitToolNames = _strategyContext.submitToolNames || [ 'submit_knowledge', 'submit_with_check', ]; const submitCalls = (source.toolCalls || []).filter((tc) => submitToolNames.includes(tc.tool || tc.name || '')); const rejected = submitCalls.filter((tc) => { const res = tc.result; if (!res) { return false; } if (typeof res === 'string') { return res.includes('rejected') || res.includes('error'); } return (res.status === 'rejected' || res.status === 'error' || res.reason === 'validation_failed'); }).length; const success = submitCalls.length - rejected; if (rejected > success && rejected >= 2) { return { action: 'retry', reason: `${rejected} rejections vs ${success} successes` }; } return { action: 'pass', reason: '' }; }