guanghulab/src/brain/context-trimmer.js

79 lines
2.3 KiB
JavaScript
Raw Normal View History

// src/brain/context-trimmer.js
// 上下文裁剪器 — 从前端迁出的核心脑逻辑
// 职责:基于滑动窗口策略,在 token 预算内裁剪消息历史
'use strict';
const CONTEXT_CONFIG = {
maxTokens: 200000, // 编号登录用户 200k
maxTokensGuest: 32000, // 访客 32k
systemPromptReserve: 8000, // 系统提示词预留
overflowStrategy: 'sliding-window',
};
/**
* 估算文本 token
* CJK 字符约 1.5 /token拉丁字符约 4 /token
* @param {string} text
* @returns {number}
*/
function estimateTokens(text) {
if (!text) return 0;
const s = String(text);
let cjk = 0, lat = 0;
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
if ((c >= 0x4E00 && c <= 0x9FFF) || (c >= 0x3000 && c <= 0x30FF) || (c >= 0xAC00 && c <= 0xD7AF)) {
cjk++;
} else {
lat++;
}
}
return Math.ceil(cjk / 1.5 + lat / 4);
}
/**
* 滑动窗口裁剪消息
* @param {Array} systemMessages - 系统消息数组 [{role:'system', content:'...'}]
* @param {Array} messages - 用户/助手消息历史
* @param {Object} opts
* @param {boolean} opts.isGuest - 是否为访客
* @param {number} opts.contextBudget - 模型路由建议的上下文窗口可覆盖默认值
* @returns {{ messages: Array, trimmed: number, totalTokens: number }}
*/
function trimMessages(systemMessages, messages, opts = {}) {
const { isGuest = false, contextBudget = 0 } = opts;
// 确定 token 上限
let limit;
if (contextBudget > 0) {
limit = contextBudget;
} else {
limit = isGuest ? CONTEXT_CONFIG.maxTokensGuest : CONTEXT_CONFIG.maxTokens;
}
const sysTokens = systemMessages.reduce((n, m) => n + estimateTokens(m.content), 0);
const reserve = Math.max(sysTokens, CONTEXT_CONFIG.systemPromptReserve);
const budget = limit - reserve;
// 从最新消息向前累加,保留预算内的消息
const kept = [];
let used = 0;
for (let i = messages.length - 1; i >= 0; i--) {
const t = estimateTokens(messages[i].content);
if (used + t > budget && kept.length > 0) break;
kept.unshift(messages[i]);
used += t;
}
const trimmed = messages.length - kept.length;
return {
messages: [...systemMessages, ...kept],
trimmed,
totalTokens: reserve + used,
};
}
module.exports = { trimMessages, estimateTokens, CONTEXT_CONFIG };