Files
WeFlow/electron/services/agentMemoryRelevance.ts
2026-09-28 21:16:43 +08:00

54 lines
2.3 KiB
TypeScript

function normalize(value: unknown): string {
return String(value ?? '').replace(/\u0000/g, '').replace(/\s+/g, ' ').trim().slice(0, 1_200).toLocaleLowerCase()
}
function searchTerms(value: string): string[] {
const normalized = normalize(value).replace(/[^\p{L}\p{N}]+/gu, ' ').trim()
const words = normalized.split(/\s+/).filter((item) => item.length >= 2)
const compact = normalized.replace(/\s+/g, '')
const bigrams: string[] = []
for (let index = 0; index < compact.length - 1 && bigrams.length < 24; index += 1) {
bigrams.push(compact.slice(index, index + 2))
}
return Array.from(new Set([...words, ...bigrams]))
}
export type AgentMemoryRelevance = {
score: number
exact: boolean
matchedTerms: string[]
}
/**
* 用精确包含、长词命中和双字覆盖率计算保守的词面相关度。
* 该分数只用于召回排序,不能作为记忆内容真实或语义相关的证明。
*/
export function agentMemoryRelevance(query: string, memoryText: string): AgentMemoryRelevance {
const normalizedQuery = normalize(query)
const normalizedMemory = normalize(memoryText)
if (!normalizedQuery || !normalizedMemory) return { score: 0, exact: false, matchedTerms: [] }
const compactQuery = normalizedQuery.replace(/[^\p{L}\p{N}]+/gu, '')
const compactMemory = normalizedMemory.replace(/[^\p{L}\p{N}]+/gu, '')
const exact = compactQuery.length >= 2 && compactMemory.includes(compactQuery)
if (exact) return { score: 1, exact: true, matchedTerms: [compactQuery] }
const terms = searchTerms(normalizedQuery)
const matchedTerms = terms.filter((term) => normalizedMemory.includes(term))
const longMatches = matchedTerms.filter((term) => term.length >= 3)
const queryBigrams = terms.filter((term) => term.length === 2)
const matchedBigrams = matchedTerms.filter((term) => term.length === 2)
const requiredBigramMatches = Math.max(2, Math.ceil(queryBigrams.length * 0.22))
if (longMatches.length === 0 && matchedBigrams.length < requiredBigramMatches) {
return { score: 0, exact: false, matchedTerms }
}
const bigramCoverage = queryBigrams.length > 0 ? matchedBigrams.length / queryBigrams.length : 0
const distinctiveCoverage = terms.length > 0 ? longMatches.length / terms.length : 0
return {
score: Math.min(0.99, bigramCoverage * 0.72 + distinctiveCoverage * 0.28),
exact: false,
matchedTerms,
}
}