2026-05-10 13:12:44 +08:00

99 lines
3.7 KiB
JavaScript

/**
* ═══════════════════════════════════════════════════════════
* 🧠 跨会话母语记忆 Agent
* ═══════════════════════════════════════════════════════════
*
* 编号: ZY-FTCHAT-MEM-001
* 守护: 铸渊 · ICE-GL-ZY001
*
* 思路 (借鉴 server/age-os/agents/shuangyan-web-agent.js):
* - 用户开新对话 → 把上一对话压成 "母语段"
* - 母语段格式 (HLDP 风格):
* 【上次会话母语印记】
* ▸ 主题: ...
* ▸ 用户状态: ...
* ▸ 我承诺/要做的事: ...
* ▸ 待续话题: ...
* - 注入下次对话的 system prompt (作为跨会话记忆)
*
* 因为代码模型尚未训练,工具链不可用,所以走 LLM 短调用做压缩。
* 失败时退化为简单的最近 N 条消息片段拼接。
*/
'use strict';
const ds = require('./ft-dashscope');
const MIN_MESSAGES_FOR_COMPRESS = 4; // 少于 4 条不压缩
const MAX_MESSAGES_TO_FEED = 30; // 最多喂 30 条进压缩器
/**
* 简单退化压缩(无 LLM 时)
*/
function fallbackImprint(messages) {
if (!Array.isArray(messages) || messages.length === 0) return '';
const recent = messages.slice(-6);
const lines = recent.map(m => {
const who = m.role === 'assistant' ? '我' : '用户';
const c = String(m.content || '').replace(/\s+/g, ' ').slice(0, 80);
return `${who}: ${c}`;
});
return ['【上次会话母语印记 · 最近片段】', ...lines].join('\n');
}
/**
* 用 LLM 把消息压缩成母语印记
* @param {Array<{role,content}>} messages
* @param {string} variant
*/
async function compressToImprint(messages, variant) {
if (!Array.isArray(messages) || messages.length < MIN_MESSAGES_FOR_COMPRESS) {
return { compressed: false, imprint: '' };
}
const fed = messages.slice(-MAX_MESSAGES_TO_FEED).map(m => ({
role: m.role === 'assistant' ? 'assistant' : 'user',
content: String(m.content || '').slice(0, 2000)
}));
const sysPrompt = `你是一个【母语压缩器】。请把下面的对话压成一段紧凑的"母语印记",作为下次对话开始时被注入的跨会话记忆。
输出格式 (严格遵守):
【上次会话母语印记】
▸ 主题: <一句话>
▸ 用户状态/情绪: <一句话>
▸ 我承诺或要做的事: <若有则列, 没有写"无">
▸ 待续话题: <若有则列, 没有写"无">
▸ 关键事实: <对方的姓名/偏好/重要信息, 不超过 3 条, 没有写"无">
只输出母语印记内容本身,不要任何解释、不要 Markdown 标题、不要 \`\`\`
保持简洁,总长度不超过 400 字。`;
const compressorMessages = [
{ role: 'system', content: sysPrompt },
{
role: 'user',
content: '请压缩以下对话:\n\n' + fed.map(m => `${m.role === 'assistant' ? '助手' : '用户'}: ${m.content}`).join('\n\n')
}
];
try {
// 压缩用 system 线(更稳定的语言风格),不论用户在用哪条线
const text = await ds.chatOnce({
variant: variant === 'naipping' ? 'naipping' : 'system',
messages: compressorMessages,
max_tokens: 600
});
const trimmed = (text || '').trim();
if (trimmed.length < 20) {
return { compressed: false, imprint: fallbackImprint(messages) };
}
return { compressed: true, imprint: trimmed };
} catch (err) {
console.warn('[FTCHAT Memory] LLM 压缩失败, 走兜底:', err.message);
return { compressed: false, imprint: fallbackImprint(messages) };
}
}
module.exports = { compressToImprint, fallbackImprint };