Add L0 index and anchor UI updates
This commit is contained in:
102
modules/story-summary/vector/llm/query-expansion.js
Normal file
102
modules/story-summary/vector/llm/query-expansion.js
Normal file
@@ -0,0 +1,102 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// query-expansion.js - 完整输入,不截断
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import { callLLM, parseJson } from './llm-service.js';
|
||||
import { xbLog } from '../../../../core/debug-core.js';
|
||||
import { filterText } from '../utils/text-filter.js';
|
||||
|
||||
const MODULE_ID = 'query-expansion';
|
||||
const SESSION_ID = 'xb6';
|
||||
|
||||
const SYSTEM_PROMPT = `你是检索词生成器。根据最近对话,输出用于检索历史剧情的关键词。
|
||||
|
||||
只输出JSON:
|
||||
{"e":["显式人物/地名"],"i":["隐含人物/情绪/话题"],"q":["检索短句"]}
|
||||
|
||||
规则:
|
||||
- e: 对话中明确提到的人名/地名,1-4个
|
||||
- i: 推断出的相关人物/情绪/话题,1-5个
|
||||
- q: 用于向量检索的短句,2-3个,每个15字内
|
||||
- 关注:正在讨论什么、涉及谁、情绪氛围`;
|
||||
|
||||
/**
|
||||
* Query Expansion
|
||||
* @param {Array} messages - 完整消息数组(最后2-3轮)
|
||||
*/
|
||||
export async function expandQuery(messages, options = {}) {
|
||||
const { timeout = 6000 } = options;
|
||||
|
||||
if (!messages?.length) {
|
||||
return { entities: [], implicit: [], queries: [] };
|
||||
}
|
||||
|
||||
// 完整格式化,不截断
|
||||
const input = messages.map(m => {
|
||||
const speaker = m.is_user ? '用户' : (m.name || '角色');
|
||||
const text = filterText(m.mes || '').trim();
|
||||
return `【${speaker}】\n${text}`;
|
||||
}).join('\n\n');
|
||||
|
||||
const T0 = performance.now();
|
||||
|
||||
try {
|
||||
const response = await callLLM([
|
||||
{ role: 'system', content: SYSTEM_PROMPT },
|
||||
{ role: 'user', content: input },
|
||||
], {
|
||||
temperature: 0.15,
|
||||
max_tokens: 250,
|
||||
timeout,
|
||||
sessionId: SESSION_ID,
|
||||
});
|
||||
|
||||
const parsed = parseJson(response);
|
||||
if (!parsed) {
|
||||
xbLog.warn(MODULE_ID, 'JSON解析失败', response?.slice(0, 200));
|
||||
return { entities: [], implicit: [], queries: [] };
|
||||
}
|
||||
|
||||
const result = {
|
||||
entities: Array.isArray(parsed.e) ? parsed.e.slice(0, 5) : [],
|
||||
implicit: Array.isArray(parsed.i) ? parsed.i.slice(0, 6) : [],
|
||||
queries: Array.isArray(parsed.q) ? parsed.q.slice(0, 4) : [],
|
||||
};
|
||||
|
||||
xbLog.info(MODULE_ID, `完成 (${Math.round(performance.now() - T0)}ms) e=${result.entities.length} i=${result.implicit.length} q=${result.queries.length}`);
|
||||
return result;
|
||||
|
||||
} catch (e) {
|
||||
xbLog.error(MODULE_ID, '调用失败', e);
|
||||
return { entities: [], implicit: [], queries: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存
|
||||
const cache = new Map();
|
||||
const CACHE_TTL = 300000;
|
||||
|
||||
function hashMessages(messages) {
|
||||
const text = messages.slice(-2).map(m => (m.mes || '').slice(0, 100)).join('|');
|
||||
let h = 0;
|
||||
for (let i = 0; i < text.length; i++) h = ((h << 5) - h + text.charCodeAt(i)) | 0;
|
||||
return h.toString(36);
|
||||
}
|
||||
|
||||
export async function expandQueryCached(messages, options = {}) {
|
||||
const key = hashMessages(messages);
|
||||
const cached = cache.get(key);
|
||||
if (cached && Date.now() - cached.time < CACHE_TTL) return cached.result;
|
||||
|
||||
const result = await expandQuery(messages, options);
|
||||
if (result.entities.length || result.queries.length) {
|
||||
if (cache.size > 50) cache.delete(cache.keys().next().value);
|
||||
cache.set(key, { result, time: Date.now() });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function buildSearchText(expansion) {
|
||||
return [...(expansion.entities || []), ...(expansion.implicit || []), ...(expansion.queries || [])]
|
||||
.filter(Boolean).join(' ');
|
||||
}
|
||||
Reference in New Issue
Block a user