autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
281 lines (280 loc) • 9.91 kB
JavaScript
/**
* MultiSignalRanker — 6 信号加权排序
* Signals: relevance, authority, recency, popularity, difficulty, contextMatch
* 不同场景使用不同权重配置(向后兼容旧配置中的 'seasonality' 键)
*/
// 场景权重配置 (含 vectorScore 第 7 信号)
const SCENARIO_WEIGHTS = {
lint: {
relevance: 0.35,
authority: 0.2,
recency: 0.15,
popularity: 0.1,
difficulty: 0.05,
contextMatch: 0.05,
vector: 0.1,
},
generate: {
relevance: 0.25,
authority: 0.15,
recency: 0.1,
popularity: 0.15,
difficulty: 0.1,
contextMatch: 0.1,
vector: 0.15,
},
search: {
relevance: 0.2,
authority: 0.15,
recency: 0.1,
popularity: 0.1,
difficulty: 0.05,
contextMatch: 0.1,
vector: 0.3,
},
learning: {
relevance: 0.15,
authority: 0.1,
recency: 0.05,
popularity: 0.1,
difficulty: 0.25,
contextMatch: 0.2,
vector: 0.15,
},
default: {
relevance: 0.25,
authority: 0.15,
recency: 0.1,
popularity: 0.1,
difficulty: 0.1,
contextMatch: 0.1,
vector: 0.2,
},
};
/** 相关性信号 — BM25 + 标题匹配 + 内容匹配 */
export class RelevanceSignal {
compute(candidate, context) {
let score = candidate.recallScore || candidate.score || 0;
const query = (context.query || '').toLowerCase();
if (!query) {
return Math.min(score, 1.0);
}
const title = (candidate.title || '').toLowerCase();
const trigger = (candidate.trigger || '').toLowerCase();
const content = (candidate.content || candidate.code || '').toLowerCase();
// trigger 精确匹配 boost(最高优先级)
if (trigger?.includes(query)) {
score += 0.4;
}
// 标题精确匹配 boost
if (title.includes(query)) {
score += 0.3;
}
// 标题单词匹配
const queryWords = query.split(/\s+/);
const titleHits = queryWords.filter((w) => title.includes(w)).length;
score += (titleHits / queryWords.length) * 0.2;
// 内容匹配
if (content.includes(query)) {
score += 0.1;
}
return Math.min(score, 1.0);
}
}
/** 权威性信号 — 基于质量评分、使用次数、作者 */
export class AuthoritySignal {
compute(candidate) {
let score = 0;
if (candidate.qualityScore) {
score += (candidate.qualityScore / 100) * 0.5;
}
if (candidate.authorityScore) {
score += candidate.authorityScore * 0.3;
}
if ((candidate.usageCount ?? 0) > 0) {
score += Math.min((candidate.usageCount ?? 0) / 100, 1) * 0.2;
}
return Math.min(score || 0.5, 1.0);
}
}
/** 时间衰减信号 */
export class RecencySignal {
compute(candidate) {
const updated = candidate.updatedAt || candidate.lastModified || candidate.createdAt;
if (!updated) {
return 0.5;
}
// 自动识别秒级/毫秒级 Unix 时间戳 (秒级 ≤ 9999999999 即 2286 年)
const ts = typeof updated === 'number' && updated > 0 && updated <= 9999999999
? updated * 1000
: typeof updated === 'number'
? updated
: new Date(updated).getTime();
const ageMs = Date.now() - ts;
if (ageMs < 0) {
return 1.0; // 未来时间戳视为最新
}
const ageDays = ageMs / (1000 * 60 * 60 * 24);
// 指数衰减:半衰期 90 天
return Math.exp((-Math.LN2 * ageDays) / 90);
}
}
/**
* 流行度信号 — 基于使用频次的对数缩放
* usageCount 1 → 0.10, 10 → 0.37, 100 → 0.67, 1000+ → 1.0
*/
export class PopularitySignal {
compute(candidate) {
const usage = candidate.usageCount || 0;
if (usage <= 0) {
return 0;
}
// 对数缩放: log10(usage+1) / 3,上限 1.0
return Math.min(Math.log10(usage + 1) / 3, 1.0);
}
}
/** 难度信号 — 用于学习场景的难度匹配 */
export class DifficultySignal {
compute(candidate, context) {
const levels = { beginner: 1, intermediate: 2, advanced: 3, expert: 4 };
const candidateLevel = levels[candidate.difficulty || 'intermediate'] || 2;
const userLevel = levels[context.userLevel || 'intermediate'] || 2;
// 难度匹配:越接近用户等级得分越高
const diff = Math.abs(candidateLevel - userLevel);
return Math.max(0, 1 - diff * 0.3);
}
}
/**
* 上下文匹配信号 — 语言/类别/标签与搜索上下文的匹配度
* (原 SeasonalitySignal,重命名以准确反映实际语义)
*/
export class ContextMatchSignal {
compute(candidate, context) {
let score = 0;
// 语言匹配(最强上下文信号)
if (context.language && candidate.language) {
if (candidate.language === context.language) {
score += 0.4;
}
else if (_isRelatedLanguage(candidate.language, context.language)) {
score += 0.15;
}
}
// 类别匹配
if (context.category && candidate.category === context.category) {
score += 0.25;
}
// 标签重叠
if ((context.tags?.length ?? 0) > 0 && (candidate.tags?.length ?? 0) > 0) {
const ctxTags = new Set(context.tags.map((t) => t.toLowerCase()));
const hits = candidate.tags.filter((t) => ctxTags.has(t.toLowerCase())).length;
if (hits > 0) {
score += 0.25 * Math.min(hits / ctxTags.size, 1);
}
}
// 基线分 — 避免无上下文时信号全零
return Math.max(score, 0.1);
}
}
// 语言家族关系表 — 跨语言上下文匹配
const LANGUAGE_FAMILIES = {
'objective-c': ['swift', 'c', 'c++'],
swift: ['objective-c', 'c'],
javascript: ['typescript', 'jsx', 'tsx'],
typescript: ['javascript', 'jsx', 'tsx'],
java: ['kotlin'],
kotlin: ['java'],
c: ['c++', 'objective-c'],
'c++': ['c', 'objective-c'],
python: ['cython'],
};
function _isRelatedLanguage(a, b) {
const related = LANGUAGE_FAMILIES[a?.toLowerCase()] || [];
return related.includes(b?.toLowerCase());
}
/**
* 向量相似度信号 — 利用 VectorService 附加的 vectorScore
* 当向量服务不可用时, vectorScore 为 0, 信号返回 0(权重自然归零)
*/
export class VectorSignal {
compute(candidate, _context) {
const vectorScore = candidate.vectorScore;
if (typeof vectorScore === 'number' && vectorScore > 0) {
return Math.min(vectorScore, 1.0);
}
return 0;
}
}
/** MultiSignalRanker — 多信号排序引擎 */
export class MultiSignalRanker {
#signals;
#scenarioWeights;
#realtimeWeights = new Map();
#recentlyUsed = new Set();
constructor(options = {}) {
this.#signals = {
relevance: new RelevanceSignal(),
authority: new AuthoritySignal(),
recency: new RecencySignal(),
popularity: new PopularitySignal(),
difficulty: new DifficultySignal(),
contextMatch: new ContextMatchSignal(),
vector: new VectorSignal(),
};
// 合并自定义权重,支持旧配置中的 "seasonality" 键向后兼容
const customWeights = options.scenarioWeights || {};
const remapped = {};
for (const [scenario, weights] of Object.entries(customWeights)) {
remapped[scenario] = { ...weights };
if ('seasonality' in remapped[scenario] && !('contextMatch' in remapped[scenario])) {
remapped[scenario].contextMatch = remapped[scenario].seasonality;
delete remapped[scenario].seasonality;
}
}
this.#scenarioWeights = { ...SCENARIO_WEIGHTS, ...remapped };
// Phase 2: 订阅实时信号更新权重
if (options.signalBus) {
options.signalBus.subscribe('quality|usage', (signal) => {
this.#onSignal(signal);
});
}
}
#onSignal(signal) {
if (signal.type === 'quality' && signal.target) {
this.#realtimeWeights.set(signal.target, signal.value);
}
if (signal.type === 'usage' && signal.target) {
this.#recentlyUsed.add(signal.target);
}
}
/**
* 对候选列表进行多信号加权排序
* @param context { query, scenario, language, userLevel, ... }
* @returns sorted candidates with rankerScore
*/
rank(candidates, context = {}) {
if (!candidates || candidates.length === 0) {
return [];
}
const scenario = context.scenario || context.intent || 'default';
const weights = this.#scenarioWeights[scenario] ||
this.#scenarioWeights.default;
const scored = candidates.map((candidate) => {
const signals = {};
let totalScore = 0;
for (const [name, signal] of Object.entries(this.#signals)) {
const value = signal.compute(candidate, context);
signals[name] = value;
// 向后兼容: 旧配置可能用 "seasonality" 而非 "contextMatch"
const weight = weights[name] ?? (name === 'contextMatch' ? (weights.seasonality ?? 0) : 0);
totalScore += value * weight;
}
return {
...candidate,
rankerScore: totalScore,
signals,
};
});
return scored.sort((a, b) => b.rankerScore - a.rankerScore);
}
}