随着用户聊天越来越多,可能一个对话能聊上上千条,那就不得不优化一下历史会话记录的输入了,不然太费token了。
这里只做滑动窗口方案和摘要提取方案,向量数据库方案等学到了补充。
把过程摘抄出来,方便复习
基础方案-滑动窗口
两种方案都是从最近的消息中截取n条,截取之后都要对数据的头部和尾部做合法清洗。
- 按照消息的长度截取
- 按照token的长度截取
js
// 工具函数处理上下文截取
const MAX_HISTORY_LENGTH = 10;
export const sliceMessages = (
messages,
maxHistoryLength = MAX_HISTORY_LENGTH,
) => {
let result = messages.slice(-maxHistoryLength);
// 处理头部
while (result.length > 0 && result[0].role === "tool") {
result.shift(); // 移除数组第一个元素
}
if (result.length === 0) return result;
// 处理尾部:openai要求的上下文格式,tool_calls后面必须跟着tool_calls长度个数的tool调用结果,不然调用大模型会报错400
const lastOne = result[result.length - 1];
const getRecentAssistant = (list, index) => {
while (index >= 0) {
if (list[index].role === "assistant") return index;
index--;
}
return -1;
};
if (lastOne.role === "tool") {
const assistantIdx = getRecentAssistant(result, result.length - 1);
if (assistantIdx === -1) {
while (result.length > 0 && result[result.length - 1].role === "tool") {
result.pop();
}
return result;
}
const { tool_calls } = result[assistantIdx];
if (tool_calls.length === result.length - 1 - assistantIdx) {
return result;
} else {
return assistantIdx > 1 ? result.slice(0, assistantIdx - 1) : [];
}
} else if (lastOne.role === "assistant" && lastOne.tool_calls) {
return result.slice(0, -1);
}
return result;
};
js
// route文件中使用
const messages = [
{
role: "system",
content: finalSystemPrompt,
},
...sliceMessages(sessionHistory),
{
role: "user",
content: message,
},
];
// ...调用ai发起会话
中级方案-摘要提取
当历史记录条数超过阈值,截取前n条数据调用ai生成摘要,将摘要作为systemPrompt输入,剩下的记录作为历史消息喂给ai
js
// controller
// 生成或更新记忆摘要
const generateMemorySummary = async (oldSummary, messagesToCompress) => {
const conversationText = messagesToCompress
.map((item) => `${item.role}:${item.content || "调用了工具"}`)
.join("\n");
const summaryPrompt = `
你是一个负责管理对话上下文的记忆助手。
XXXXXXXXXXXXX 输入你的定制化需求
【之前的摘要】: ${oldSummary || "无"}
【最新对话记录】:
${conversationText}
`;
try {
const response = await client.chat.completions.create({
model: "deepseek-chat",
messages: [{ role: "system", content: summaryPrompt }],
temperature: 0.3, // 降低温度,保证摘要客观
});
return response.choices[0].message.content;
} catch (err) {
console.error("摘要生成失败:", err);
return oldSummary;
}
};
js
// route 中使用
// 当历史消息超过20条时,压缩前10条
const COMPRESS_THRESHOLD = 20;
const COMPRESS_COUNT = 10;
if (sessionHistory.length > COMPRESS_THRESHOLD) {
// 截取需要压缩的历史消息
const messagesToCompress = sessionHistory.slice(0, COMPRESS_COUNT);
// 调用ai生成新的摘要
currentSummary = await generateMemorySummary(
currentSummary,
messagesToCompress,
);
// 将新的摘要写入数据库
await updateSessionSummary(sessionId, currentSummary);
// 删除已压缩的历史记录,异步操作,不要阻塞对话
deleteCompressedMessages(
sessionId,
messagesToCompress.map((item) => item.id),
).catch((err) => {
console.error(`异步删除压缩消息失败:${sessionId}: `, err);
});
// 更新当前内存里的历史记录
sessionHistory = sessionHistory.slice(COMPRESS_COUNT);
}
// 摘要作为长期记忆,拼接到系统提示词
const finalSystemPrompt = currentSummary
? `${systemPrompt}\n\n【关于用户的长期记忆】:\n${currentSummary}`
: systemPrompt;
const slicedMessages = sliceMessages(sessionHistory).map((item) => {
const { id, ...rest } = item;
return rest;
});
const messages = [
{
role: "system",
content: finalSystemPrompt,
},
...slicedMessages,
{
role: "user",
content: message,
},
];
// xxxx会话内容
中级方案优化-摘要提取
当你提前截取前m条去提取摘要了,剩下的n-m条就要做数据合法清洗,那么中间必然要漏掉重要的业务数据,因此对截取索引做优化,保证信息不会遗漏
js
// tools
// 动态寻找安全切割点, 防止一刀切,漏掉中间重要业务信息
export const getSafeSplitIndex = (messages, targetIndex) => {
let safeIndex = targetIndex;
if (safeIndex >= messages.length || safeIndex <= 0) return safeIndex;
while (
safeIndex >= 0 &&
(messages[safeIndex].role === "tool" ||
(messages[safeIndex - 1] &&
messages[safeIndex - 1].role === "assistant" &&
messages[safeIndex - 1].tool_calls))
) {
safeIndex--;
}
return safeIndex;
};
js
// route中使用
// 当历史消息超过20条时,压缩前10条
const COMPRESS_THRESHOLD = 20;
const COMPRESS_COUNT = 10;
if (sessionHistory.length > COMPRESS_THRESHOLD) {
// 获取安全的切割点
const safeSplitIndex = getSafeSplitIndex(sessionHistory, COMPRESS_COUNT);
if (safeSplitIndex > 0) {
// 截取需要压缩的历史消息
const messagesToCompress = sessionHistory.slice(0, safeSplitIndex);
// 调用ai生成新的摘要
currentSummary = await generateMemorySummary(
currentSummary,
messagesToCompress,
);
// 将新的摘要写入数据库
await updateSessionSummary(sessionId, currentSummary);
// 删除已压缩的历史记录
deleteCompressedMessages(
sessionId,
messagesToCompress.map((item) => item.id),
).catch((err) => {
console.error(`异步删除压缩消息失败:${sessionId}: `, err);
});
// 更新当前内存里的历史记录
sessionHistory = sessionHistory.slice(safeSplitIndex);
}
}
// 摘要作为长期记忆,拼接到系统提示词
const finalSystemPrompt = currentSummary
? `${systemPrompt}\n\n【关于用户的长期记忆】:\n${currentSummary}`
: systemPrompt;
const messages = [
{
role: "system",
content: finalSystemPrompt,
},
...sessionHistory.map((item) => {
const { id, ...rest } = item;
return rest;
}),
{
role: "user",
content: message,
},
];