101 lines
3.7 KiB
JavaScript
101 lines
3.7 KiB
JavaScript
'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 };
|