Zero-darkbox query updates and tokenizer improvements
This commit is contained in:
@@ -12,36 +12,18 @@ import { getContext } from '../../../../../../../extensions.js';
|
||||
import { buildEntityLexicon, buildDisplayNameMap, extractEntitiesFromText } from './entity-lexicon.js';
|
||||
import { getSummaryStore } from '../../data/store.js';
|
||||
import { filterText } from '../utils/text-filter.js';
|
||||
import { tokenizeForIndex as tokenizerTokenizeForIndex } from '../utils/tokenizer.js';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// 常量
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const DIALOGUE_MAX_CHARS = 400;
|
||||
const PENDING_MAX_CHARS = 400;
|
||||
const MEMORY_HINT_MAX_CHARS = 100;
|
||||
// Zero-darkbox policy:
|
||||
// - No internal truncation. We rely on model-side truncation / provider limits.
|
||||
// - If provider rejects due to length, we fail loudly and degrade explicitly.
|
||||
const MEMORY_HINT_ATOMS_MAX = 5;
|
||||
const MEMORY_HINT_EVENTS_MAX = 3;
|
||||
const RERANK_QUERY_MAX_CHARS = 500;
|
||||
const RERANK_SNIPPET_CHARS = 150;
|
||||
const LEXICAL_TERMS_MAX = 10;
|
||||
const LEXICAL_TERM_MIN_LEN = 2;
|
||||
const LEXICAL_TERM_MAX_LEN = 6;
|
||||
|
||||
// 中文停用词(高频无意义词)
|
||||
const STOP_WORDS = new Set([
|
||||
'的', '了', '在', '是', '我', '有', '和', '就', '不', '人',
|
||||
'都', '一', '一个', '上', '也', '很', '到', '说', '要', '去',
|
||||
'你', '会', '着', '没有', '看', '好', '自己', '这', '他', '她',
|
||||
'它', '吗', '什么', '那', '里', '来', '吧', '呢', '啊', '哦',
|
||||
'嗯', '呀', '哈', '嘿', '喂', '哎', '唉', '哇', '呃', '嘛',
|
||||
'把', '被', '让', '给', '从', '向', '对', '跟', '比', '但',
|
||||
'而', '或', '如果', '因为', '所以', '虽然', '但是', '然后',
|
||||
'可以', '这样', '那样', '怎么', '为什么', '什么样', '哪里',
|
||||
'时候', '现在', '已经', '还是', '只是', '可能', '应该', '知道',
|
||||
'觉得', '开始', '一下', '一些', '这个', '那个', '他们', '我们',
|
||||
'你们', '自己', '起来', '出来', '进去', '回来', '过来', '下去',
|
||||
]);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// 工具函数
|
||||
@@ -65,10 +47,7 @@ function cleanMessageText(text) {
|
||||
* @param {number} maxLen
|
||||
* @returns {string}
|
||||
*/
|
||||
function truncate(text, maxLen) {
|
||||
if (!text || text.length <= maxLen) return text || '';
|
||||
return text.slice(0, maxLen) + '…';
|
||||
}
|
||||
// truncate removed by design (zero-darkbox)
|
||||
|
||||
/**
|
||||
* 清理事件摘要(移除楼层标记)
|
||||
@@ -84,8 +63,7 @@ function cleanSummary(summary) {
|
||||
/**
|
||||
* 从文本中提取高频实词(用于词法检索)
|
||||
*
|
||||
* 策略:按中文字符边界 + 空格/标点分词,取长度 2-6 的片段
|
||||
* 过滤停用词,按频率排序
|
||||
* 使用统一分词器(结巴 + 实体保护 + 停用词过滤),按频率排序
|
||||
*
|
||||
* @param {string} text - 清洗后的文本
|
||||
* @param {number} maxTerms - 最大词数
|
||||
@@ -94,15 +72,15 @@ function cleanSummary(summary) {
|
||||
function extractKeyTerms(text, maxTerms = LEXICAL_TERMS_MAX) {
|
||||
if (!text) return [];
|
||||
|
||||
// 提取连续中文片段 + 英文单词
|
||||
const segments = text.match(/[\u4e00-\u9fff]{2,6}|[a-zA-Z]{3,}/g) || [];
|
||||
// 使用统一分词器(索引用,不去重,保留词频)
|
||||
const tokens = tokenizerTokenizeForIndex(text);
|
||||
|
||||
// 统计词频
|
||||
const freq = new Map();
|
||||
for (const seg of segments) {
|
||||
const s = seg.toLowerCase();
|
||||
if (s.length < LEXICAL_TERM_MIN_LEN || s.length > LEXICAL_TERM_MAX_LEN) continue;
|
||||
if (STOP_WORDS.has(s)) continue;
|
||||
freq.set(s, (freq.get(s) || 0) + 1);
|
||||
for (const token of tokens) {
|
||||
const key = String(token || '').toLowerCase();
|
||||
if (!key) continue;
|
||||
freq.set(key, (freq.get(key) || 0) + 1);
|
||||
}
|
||||
|
||||
return Array.from(freq.entries())
|
||||
@@ -160,8 +138,9 @@ export function buildQueryBundle(lastMessages, pendingUserMessage, store = null,
|
||||
const clean = cleanMessageText(m.mes || '');
|
||||
|
||||
if (clean) {
|
||||
// ★ 修复 A:不使用楼层号,embedding 模型不需要
|
||||
dialogueLines.push(`${speaker}: ${truncate(clean, DIALOGUE_MAX_CHARS)}`);
|
||||
// 不使用楼层号,embedding 模型不需要
|
||||
// 不截断,零暗箱
|
||||
dialogueLines.push(`${speaker}: ${clean}`);
|
||||
allCleanText.push(clean);
|
||||
}
|
||||
}
|
||||
@@ -191,30 +170,15 @@ export function buildQueryBundle(lastMessages, pendingUserMessage, store = null,
|
||||
}
|
||||
|
||||
if (pendingClean) {
|
||||
queryParts.push(`[PENDING_USER]\n${truncate(pendingClean, PENDING_MAX_CHARS)}`);
|
||||
// 不截断,零暗箱
|
||||
queryParts.push(`[PENDING_USER]\n${pendingClean}`);
|
||||
}
|
||||
|
||||
const queryText_v0 = queryParts.join('\n\n');
|
||||
|
||||
// 6. 构建 rerankQuery(短版)
|
||||
const rerankParts = [];
|
||||
|
||||
if (focusEntities.length > 0) {
|
||||
rerankParts.push(focusEntities.join(' '));
|
||||
}
|
||||
|
||||
for (const m of (lastMessages || [])) {
|
||||
const clean = cleanMessageText(m.mes || '');
|
||||
if (clean) {
|
||||
rerankParts.push(truncate(clean, RERANK_SNIPPET_CHARS));
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingClean) {
|
||||
rerankParts.push(truncate(pendingClean, RERANK_SNIPPET_CHARS));
|
||||
}
|
||||
|
||||
const rerankQuery = truncate(rerankParts.join('\n'), RERANK_QUERY_MAX_CHARS);
|
||||
// 6. rerankQuery 与 embedding query 同源(零暗箱)
|
||||
// 后续 refine 会把它升级为与 queryText_v1 同源。
|
||||
const rerankQuery = queryText_v0;
|
||||
|
||||
// 7. 构建 lexicalTerms
|
||||
const entityTerms = focusEntities.map(e => e.toLowerCase());
|
||||
@@ -265,7 +229,8 @@ export function refineQueryBundle(bundle, anchorHits, eventHits) {
|
||||
for (const hit of topAnchors) {
|
||||
const semantic = hit.atom?.semantic || '';
|
||||
if (semantic) {
|
||||
hints.push(truncate(semantic, MEMORY_HINT_MAX_CHARS));
|
||||
// 不截断,零暗箱
|
||||
hints.push(semantic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,13 +244,15 @@ export function refineQueryBundle(bundle, anchorHits, eventHits) {
|
||||
? `${title}: ${summary}`
|
||||
: title || summary;
|
||||
if (line) {
|
||||
hints.push(truncate(line, MEMORY_HINT_MAX_CHARS));
|
||||
// 不截断,零暗箱
|
||||
hints.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 构建 queryText_v1
|
||||
// 3. 构建 queryText_v1(Hints 前置,最优先)
|
||||
if (hints.length > 0) {
|
||||
bundle.queryText_v1 = bundle.queryText_v0 + `\n\n[MEMORY_HINTS]\n${hints.join('\n')}`;
|
||||
const hintText = `[MEMORY_HINTS]\n${hints.join('\n')}`;
|
||||
bundle.queryText_v1 = hintText + `\n\n` + bundle.queryText_v0;
|
||||
} else {
|
||||
bundle.queryText_v1 = bundle.queryText_v0;
|
||||
}
|
||||
@@ -314,17 +281,8 @@ export function refineQueryBundle(bundle, anchorHits, eventHits) {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 增强 rerankQuery
|
||||
if (hints.length > 0) {
|
||||
const hintKeywords = extractKeyTerms(hints.join(' '), 5);
|
||||
if (hintKeywords.length > 0) {
|
||||
const addition = hintKeywords.join(' ');
|
||||
bundle.rerankQuery = truncate(
|
||||
bundle.rerankQuery + '\n' + addition,
|
||||
RERANK_QUERY_MAX_CHARS
|
||||
);
|
||||
}
|
||||
}
|
||||
// 5. rerankQuery 与最终 query 同源(零暗箱)
|
||||
bundle.rerankQuery = bundle.queryText_v1 || bundle.queryText_v0;
|
||||
|
||||
// 6. 增强 lexicalTerms
|
||||
if (hints.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user