UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

229 lines (228 loc) 9.26 kB
/** * RecommendationPipeline — 统一推荐管线 * * 多阶段处理: * 1. Recall — 多策略并行召回候选 (Rule / AI / Vector / Popularity) * 2. Score — 综合评分 (信号强度 × 用户偏好 × 新鲜度) * 3. Rank — 排序 + 截断 * 4. Filter — 去重、过滤已有 Skill、过滤频繁忽略的类别 * 5. Deliver — 输出最终推荐列表 + 触发 Hook * * 设计原则: * - 静默降级: 任何策略失败不影响其他策略 * - 离线优先: 无 AI 时降级到规则召回 * - 反馈闪环: 利用 FeedbackStore 调整排序权重 */ import Logger from '../../infrastructure/logging/Logger.js'; /** 最大召回超时 (ms) — 单个策略超时不阻塞整体 */ const RECALL_TIMEOUT_MS = 15_000; /** 默认返回推荐数量 */ const DEFAULT_TOP_K = 5; /** 生成唯一推荐 ID (不依赖外部库) */ function generateRecommendationId() { // crypto.randomUUID() — Node.js >=19 可用 if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return `rec_${crypto.randomUUID().replace(/-/g, '').slice(0, 12)}`; } // fallback return `rec_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; } export class RecommendationPipeline { #strategies = []; #feedbackStore; #skillHooks; #logger; constructor(opts = {}) { this.#feedbackStore = opts.feedbackStore ?? null; this.#skillHooks = opts.skillHooks ?? null; this.#logger = Logger.getInstance(); } // ─── 策略管理 ────────────────────────────────────────── /** 注册召回策略 */ addStrategy(strategy) { this.#strategies.push(strategy); this.#logger.debug(`RecommendationPipeline: strategy "${strategy.name}" registered`); } /** 获取已注册策略列表 */ getStrategies() { return this.#strategies; } // ─── 主管线 ──────────────────────────────────────────── /** * 执行推荐管线 * * @param context 推荐上下文 * @param topK 最多返回数量 * @returns 排序后的推荐结果列表 */ async recommend(context, topK = DEFAULT_TOP_K) { // 注入用户偏好 if (this.#feedbackStore && !context.userPreference) { context.userPreference = this.#feedbackStore.getUserPreference(); } // ── 1. Recall: 多策略并行召回 ── const rawCandidates = await this.#recall(context); if (rawCandidates.length === 0) { return []; } // ── 2. Score: 综合评分 ── const scored = this.#score(rawCandidates, context); // ── 3. Rank: 排序 ── scored.sort((a, b) => b.score - a.score); // ── 4. Filter: 去重 + 过滤 ── const filtered = this.#filter(scored, context); // ── 5. Truncate + Deliver ── const results = filtered.slice(0, topK); // 触发 onRecommendation hook (waterfall: 允许 hook 修改结果) if (this.#skillHooks?.has('onRecommendation')) { try { const modified = await this.#skillHooks.run('onRecommendation', results, context); if (Array.isArray(modified)) { return modified; } } catch (err) { this.#logger.warn('RecommendationPipeline: onRecommendation hook error', { error: err instanceof Error ? err.message : String(err), }); } } return results; } // ─── 阶段 1: 召回 ───────────────────────────────────── async #recall(context) { const availableStrategies = this.#strategies.filter((s) => { try { return s.isAvailable(context); } catch { return false; } }); if (availableStrategies.length === 0) { this.#logger.warn('RecommendationPipeline: no available recall strategies'); return []; } // 并行召回 — 每个策略独立超时 const results = await Promise.allSettled(availableStrategies.map((strategy) => this.#recallWithTimeout(strategy, context))); const candidates = []; for (let i = 0; i < results.length; i++) { const r = results[i]; if (r.status === 'fulfilled') { candidates.push(...r.value); this.#logger.debug(`RecommendationPipeline: "${availableStrategies[i].name}" recalled ${r.value.length} candidates`); } else { this.#logger.warn(`RecommendationPipeline: "${availableStrategies[i].name}" failed`, { error: r.reason instanceof Error ? r.reason.message : String(r.reason), }); } } return candidates; } async #recallWithTimeout(strategy, context) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error(`Strategy "${strategy.name}" timed out after ${RECALL_TIMEOUT_MS}ms`)); }, RECALL_TIMEOUT_MS); strategy .recall(context) .then((results) => { clearTimeout(timer); resolve(results); }) .catch((err) => { clearTimeout(timer); reject(err); }); }); } // ─── 阶段 2: 评分 ───────────────────────────────────── #score(candidates, context) { const preference = context.userPreference; return candidates.map((c) => { const signalScores = {}; // 基础优先级分 signalScores.priority = c.priority === 'high' ? 0.9 : c.priority === 'medium' ? 0.6 : 0.3; // 来源可信度分 signalScores.sourceConfidence = this.#getSourceConfidence(c.source); // 用户偏好匹配分 signalScores.userAffinity = this.#getUserAffinityScore(c, preference); // 综合得分: 加权平均 const weights = { priority: 0.4, sourceConfidence: 0.3, userAffinity: 0.3 }; const score = signalScores.priority * weights.priority + signalScores.sourceConfidence * weights.sourceConfidence + signalScores.userAffinity * weights.userAffinity; return { ...c, score: Math.max(0, Math.min(score, 1)), signalScores, recommendationId: generateRecommendationId(), generatedAt: new Date().toISOString(), }; }); } #getSourceConfidence(source) { if (source.startsWith('ai:')) { return 0.8; } if (source.startsWith('rule:')) { return 0.7; } if (source.startsWith('vector:')) { return 0.6; } return 0.5; } #getUserAffinityScore(candidate, preference) { if (!preference) { return 0.5; // 无偏好数据,中性分 } let score = 0.5; // 来源偏好加分 const sourceType = candidate.source.split(':')[1] ?? candidate.source; if (preference.preferredSources.includes(sourceType)) { score += 0.2; } // 避开类别降分 (通过 signals 中可能包含的 category 信息) const category = candidate.signals.category; if (category) { if (preference.preferredCategories.includes(category)) { score += 0.2; } if (preference.avoidedCategories.includes(category)) { score -= 0.3; } } return Math.max(0, Math.min(score, 1)); } // ─── 阶段 3: 过滤 ───────────────────────────────────── #filter(scored, context) { const existingSet = context.existingSkills ?? new Set(); const seen = new Set(); const results = []; for (const rec of scored) { // 去重 (同名) if (seen.has(rec.name)) { continue; } // 已有 Skill 过滤 if (existingSet.has(rec.name)) { continue; } // 频繁忽略的类别过滤 const category = rec.signals.category; if (category && this.#feedbackStore?.isFrequentlyDismissed(category)) { continue; } // 低分过滤 (得分 < 0.2 的推荐丢弃) if (rec.score < 0.2) { continue; } seen.add(rec.name); results.push(rec); } return results; } } export default RecommendationPipeline;