'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 };