chore: CRM-NEW baseline
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const { createEmbeddingClient } = require('./embedding');
|
||||
|
||||
const SIM_THRESHOLD = 0.85;
|
||||
|
||||
// 语义缓存:只缓存确定性政策问答,避免订单/库存等实时数据被缓存。
|
||||
function createLlmCache(cfg, deps = {}) {
|
||||
if (!cfg.pgUrl || !cfg.embApiKey || !cfg.embBaseUrl) {
|
||||
return { enabled: false, get: async () => null, store: async () => false };
|
||||
}
|
||||
const emb = deps.embedding || createEmbeddingClient(cfg, { pgUrl: cfg.pgUrl });
|
||||
const pool = deps.pool || emb.pool;
|
||||
let ready = null;
|
||||
let failed = false;
|
||||
|
||||
function ensureTable() {
|
||||
if (failed) return Promise.reject(new Error('cache unavailable'));
|
||||
if (!ready) {
|
||||
ready = (async () => {
|
||||
await pool.query('CREATE EXTENSION IF NOT EXISTS vector');
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS bridge_llm_cache (
|
||||
id SERIAL PRIMARY KEY,
|
||||
query_vec vector(${Number(cfg.embDim) || 1024}),
|
||||
query_text TEXT NOT NULL,
|
||||
intent TEXT NOT NULL,
|
||||
verified_scope TEXT NOT NULL,
|
||||
lang TEXT NOT NULL,
|
||||
kb_fingerprint TEXT,
|
||||
system_hash TEXT,
|
||||
reply TEXT NOT NULL,
|
||||
hit_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (query_text, intent, verified_scope, lang)
|
||||
)
|
||||
`);
|
||||
await pool.query('CREATE INDEX IF NOT EXISTS bridge_llm_cache_vec_idx ON bridge_llm_cache USING hnsw (query_vec vector_cosine_ops)');
|
||||
})().catch((err) => {
|
||||
failed = true;
|
||||
console.error(JSON.stringify({ ts: new Date().toISOString(), warn: `llm cache init failed: ${err.message}` }));
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return ready;
|
||||
}
|
||||
|
||||
async function get({ query, intent, verifiedScope, lang, kbFingerprint, systemHash }) {
|
||||
try {
|
||||
await ensureTable();
|
||||
const vec = await emb.embedQuery(query);
|
||||
const r = await pool.query(
|
||||
`SELECT id, reply, kb_fingerprint, system_hash,
|
||||
1 - (query_vec <=> $1::vector) AS sim
|
||||
FROM bridge_llm_cache
|
||||
WHERE intent = $2 AND verified_scope = $3
|
||||
AND 1 - (query_vec <=> $1::vector) > ${SIM_THRESHOLD}
|
||||
ORDER BY query_vec <=> $1::vector
|
||||
LIMIT 1`,
|
||||
[emb.vecStr(vec), intent, verifiedScope || 'anonymous']
|
||||
);
|
||||
const hit = r.rows && r.rows[0];
|
||||
if (!hit) return null;
|
||||
if (hit.kb_fingerprint && hit.kb_fingerprint !== (kbFingerprint || '')) return null;
|
||||
if (hit.system_hash && hit.system_hash !== (systemHash || '')) return null;
|
||||
pool.query('UPDATE bridge_llm_cache SET hit_count = hit_count + 1 WHERE id = $1', [hit.id]).catch(() => {});
|
||||
return hit.reply;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function store({ query, intent, verifiedScope, lang, kbFingerprint, systemHash, reply }) {
|
||||
try {
|
||||
if (!reply) return false;
|
||||
await ensureTable();
|
||||
const vec = await emb.embedQuery(query);
|
||||
await pool.query(
|
||||
`INSERT INTO bridge_llm_cache
|
||||
(query_vec, query_text, intent, verified_scope, lang, kb_fingerprint, system_hash, reply)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
||||
ON CONFLICT (query_text, intent, verified_scope, lang) DO UPDATE SET
|
||||
query_vec = EXCLUDED.query_vec,
|
||||
kb_fingerprint = EXCLUDED.kb_fingerprint,
|
||||
system_hash = EXCLUDED.system_hash,
|
||||
reply = EXCLUDED.reply,
|
||||
created_at = NOW()`,
|
||||
[emb.vecStr(vec), String(query).slice(0, 500), intent, verifiedScope || 'anonymous', lang || 'en', kbFingerprint || '', systemHash || '', reply]
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return { enabled: true, get, store };
|
||||
}
|
||||
|
||||
module.exports = { createLlmCache, SIM_THRESHOLD };
|
||||
Reference in New Issue
Block a user