EdgeOne Makers Agents 对话存储
你的 Agent 要能记住用户之前说了什么,就得有个地方存对话历史。这篇讲三种常用的存储方案,以及怎么管理多轮对话的上下文。
为什么需要存储对话
大模型本身不记得上一轮聊了什么。每次调用都得把完整对话历史传过去,模型才能”记住”上下文。
| 场景 | 不存储的后果 |
|---|---|
| 多轮问答 | 用户说”继续”,模型不知道继续什么 |
| 客服机器人 | 客户重复描述问题,体验差 |
| 编程助手 | 忘了之前的代码上下文,给出错误建议 |
所以对话存储是 Agent 的基础设施。
三种存储方案
方案一 内存存储
最简单的方式,把对话存在内存里。
// 用 Map 存储,key 是 sessionId
const conversations = new Map();
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const sessionId = url.searchParams.get('session_id') || 'default';
if (request.method === 'POST') {
const { message } = await request.json();
// 获取历史对话
let history = conversations.get(sessionId) || [];
// 添加用户消息
history.push({ role: 'user', content: message });
// 调用大模型
const response = await env.AI.chat('deepseek-v3', {
messages: history
});
// 添加助手回复
history.push({ role: 'assistant', content: response.message.content });
// 更新存储
conversations.set(sessionId, history);
return Response.json({ reply: response.message.content });
}
}
};
优点简单快速,开发调试方便
缺点服务重启就丢失,不适合生产环境
方案二 KV 存储
用 EdgeOne 的 KV 服务持久化存储,按 sessionId 隔离。
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const sessionId = url.searchParams.get('session_id') || 'default';
if (request.method === 'POST') {
const { message } = await request.json();
// 从 KV 读取历史
const historyKey = `chat:${sessionId}`;
let history = [];
try {
const stored = await env.KV.get(historyKey, 'json');
if (stored) history = stored;
} catch (e) {
// 首次对话,历史为空
}
// 添加用户消息
history.push({ role: 'user', content: message });
// 调用大模型
const response = await env.AI.chat('deepseek-v3', {
messages: history
});
// 添加助手回复
history.push({
role: 'assistant',
content: response.message.content
});
// 存回 KV,设置过期时间(7天)
await env.KV.put(historyKey, JSON.stringify(history), {
expirationTtl: 7 * 24 * 60 * 60
});
return Response.json({ reply: response.message.content });
}
}
};
优点持久化存储,支持过期时间,适合生产环境
缺点每次都要读写 KV,有网络延迟
方案三 数据库存储
用 Supabase 或 EdgeOne 的数据库服务,适合需要复杂查询的场景。
import { createClient } from '@supabase/supabase-js';
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const sessionId = url.searchParams.get('session_id') || 'default';
if (request.method === 'POST') {
const { message } = await request.json();
// 初始化 Supabase 客户端
const supabase = createClient(
env.SUPABASE_URL,
env.SUPABASE_KEY
);
// 查询历史对话
const { data: history } = await supabase
.from('messages')
.select('role, content')
.eq('session_id', sessionId)
.order('created_at', { ascending: true });
// 添加用户消息
const messages = [
...(history || []),
{ role: 'user', content: message }
];
// 调用大模型
const response = await env.AI.chat('deepseek-v3', { messages });
// 保存用户消息和助手回复到数据库
await supabase.from('messages').insert([
{ session_id: sessionId, role: 'user', content: message },
{
session_id: sessionId,
role: 'assistant',
content: response.message.content
}
]);
return Response.json({ reply: response.message.content });
}
}
};
优点支持复杂查询,可以做数据统计分析
缺点需要建表,开发成本稍高
三种方案对比
| 方案 | 持久化 | 查询能力 | 性能 | 适用场景 |
|---|---|---|---|---|
| 内存 | 否 | 无 | 最快 | 开发调试、临时测试 |
| KV | 是 | 简单 | 快 | 大多数生产场景 |
| 数据库 | 是 | 强 | 较慢 | 需要分析统计的场景 |
上下文管理
对话存下来了,但大模型有上下文长度限制。不同模型的 token 限制不一样,得控制历史消息的数量。
截断策略
只保留最近 N 轮对话,丢弃更早的内容。
// 只保留最近 10 轮对话(20条消息)
const MAX_TURNS = 10;
function trimHistory(history) {
const maxMessages = MAX_TURNS * 2;
if (history.length <= maxMessages) {
return history;
}
// 保留系统提示词(如果有)和最近的消息
const systemMsg = history[0]?.role === 'system'
? [history[0]]
: [];
const recentMessages = history.slice(-maxMessages);
return [...systemMsg, ...recentMessages];
}
// 使用
const trimmedHistory = trimHistory(history);
const response = await env.AI.chat('deepseek-v3', {
messages: trimmedHistory
});
摘要策略
把早期对话总结成一段话,压缩上下文。
async function summarizeOldMessages(history, env) {
if (history.length <= 20) {
return history; // 不需要摘要
}
// 分离早期消息和近期消息
const earlyMessages = history.slice(0, -10);
const recentMessages = history.slice(-10);
// 把早期消息转成文本
const earlyText = earlyMessages
.map(m => `${m.role}: ${m.content}`)
.join('\n');
// 调用模型生成摘要
const summaryResponse = await env.AI.chat('deepseek-v3', {
messages: [
{
role: 'system',
content: '请把下面的对话内容总结成一段话,保留关键信息'
},
{ role: 'user', content: earlyText }
]
});
const summary = summaryResponse.message.content;
// 用摘要替换早期消息
return [
{ role: 'system', content: `之前的对话摘要: ${summary}` },
...recentMessages
];
}
// 使用
const optimizedHistory = await summarizeOldMessages(history, env);
const response = await env.AI.chat('deepseek-v3', {
messages: optimizedHistory
});
两种策略对比
| 策略 | 优点 | 缺点 |
|---|---|---|
| 截断 | 简单,不额外消耗 token | 丢失早期信息 |
| 摘要 | 保留关键信息 | 需要额外调用模型,有成本 |
一般推荐摘要策略,用户体验更好。
速查卡片
| 要点 | 说明 |
|---|---|
| 为什么要存储 | 大模型无状态,每次都要传完整历史 |
| 内存存储 | 简单快速,但重启丢失 |
| KV 存储 | 持久化,支持过期,适合生产环境 |
| 数据库存储 | 支持复杂查询,适合分析统计 |
| sessionId 作用 | 隔离不同用户的对话历史 |
| 截断策略 | 只保留最近 N 轮,简单但有信息丢失 |
| 摘要策略 | 压缩早期对话,保留关键信息 |
| 推荐方案 | 生产环境用 KV + 摘要策略 |