Agent 对话记忆设计
LLM 没有”记忆”——所有”上下文”都是你每次调用 API 时 prompt 里塞的。这导致 两个核心问题:(1) context window 有限(GPT-4o 128k token 也撑不住每天对 话)(2) 你存的明文在你的服务器,用户撤销关系时必须清理。本页给三种记忆 方案 + 何时用哪种。
三种记忆架构
方案 A:纯短期(in-context window)
每次 LLM 调用:messages = [system, ...recent_10_turns, user_input]| 维度 | 评价 |
|---|---|
| 实现 | 最简单(无 DB / 无衍生数据) |
| 隐私 | ✓ 极佳(不存任何明文) |
| 上下文 | ❌ 10 轮以上”失忆” |
| Token 成本 | 每次都重传所有历史 = 浪费 |
| 适合 | 客服 / 一次性问答 / 短交互 |
方案 B:长期 + 全文存储
DB schema: conv_messages (msg_id, conv_id, sender, text, ts) 每次 LLM 调用:messages = [system, ...db.fetch(conv_id, last_50), user_input]| 维度 | 评价 |
|---|---|
| 实现 | 中等(DB + 撤销清理) |
| 隐私 | ⚠️ 明文在你服务器 — 撤销时必清 |
| 上下文 | ✓ 50 轮内完整 |
| Token 成本 | 高(每次重传) |
| 适合 | 个人 AI 助理 / 长期协作 |
方案 C:摘要式(recommended for production)
DB schema: conv_summaries (conv_id, summary_text, updated_at) conv_messages (msg_id, conv_id, sender, text, ts, summarized BOOLEAN)
每 N 轮(如 20): 把"已 summarized=false"的消息 → LLM 总结 → 写入 conv_summaries.summary_text 标 messages.summarized = true
每次 LLM 调用: messages = [system, summary, ...db.fetch(conv_id, recent_5_unsummarized), user_input]| 维度 | 评价 |
|---|---|
| 实现 | 复杂(异步 summarize 任务 + DB 状态机) |
| 隐私 | ⚠️ 明文 + 摘要都在你服务器 — 撤销时清 |
| 上下文 | ✓ 不限长度(摘要压缩) |
| Token 成本 | 低(只传 summary + 最近 N 轮) |
| 适合 | 长期助理 / Coach / 项目协作 |
方案 C 代码骨架
import { HasheeAgent } from "@hasheeai/agent-sdk-ts";import OpenAI from "openai";import { Pool } from "pg";
const openai = new OpenAI();const db = new Pool({ connectionString: process.env.DATABASE_URL });
const agent = await HasheeAgent.init({ /* ... */ });
agent.addMessageHandler(async (msg) => { if (msg.payload?.type !== "text") return;
// 1. 写入新消息 await db.query( "INSERT INTO conv_messages (msg_id, conv_id, sender, text, ts) VALUES ($1, $2, $3, $4, NOW())", [msg.message_id, msg.conversation_id, msg.sender_id, msg.payload.text], );
// 2. 构造 prompt(summary + 最近 5 轮未摘要) const summary = await getSummary(msg.conversation_id); const recent = await db.query( `SELECT sender, text FROM conv_messages WHERE conv_id = $1 AND summarized = FALSE ORDER BY ts DESC LIMIT 5`, [msg.conversation_id], ); const messages: any[] = [ { role: "system", content: `你是用户的助理。这是你们之前的对话摘要:\n\n${summary}` }, ...recent.rows.reverse().map((r) => ({ role: r.sender === MY_AGENT_ID ? "assistant" : "user", content: r.text, })), ];
// 3. LLM 调用 const c = await openai.chat.completions.create({ model: "gpt-4o-mini", messages }); const reply = c.choices[0]?.message?.content ?? "";
await agent.send(msg.conversation_id, { type: "text", text: reply }); await db.query( "INSERT INTO conv_messages (msg_id, conv_id, sender, text, ts) VALUES ($1, $2, $3, $4, NOW())", [`reply-${msg.message_id}`, msg.conversation_id, MY_AGENT_ID, reply], );
// 4. 异步:如未摘要消息数 >= 20,触发摘要任务 const unsummarizedCount = (await db.query( "SELECT COUNT(*) FROM conv_messages WHERE conv_id = $1 AND summarized = FALSE", [msg.conversation_id], )).rows[0].count; if (Number(unsummarizedCount) >= 20) { await summarizeConversation(msg.conversation_id); }});
async function getSummary(convId: string): Promise<string> { const r = await db.query("SELECT summary_text FROM conv_summaries WHERE conv_id = $1", [convId]); return r.rows[0]?.summary_text ?? "(无历史)";}
async function summarizeConversation(convId: string) { const r = await db.query( `SELECT sender, text FROM conv_messages WHERE conv_id = $1 AND summarized = FALSE ORDER BY ts ASC`, [convId], ); if (r.rows.length === 0) return;
const previousSummary = await getSummary(convId); const conversationText = r.rows.map((row) => `${row.sender}: ${row.text}`).join("\n");
const c = await openai.chat.completions.create({ model: "gpt-4o", messages: [ { role: "system", content: "你的任务是更新对话摘要。把新对话融合进已有摘要。摘要应保留:用户偏好 / 关键事实 / 待办 / 重要决策。" }, { role: "user", content: `已有摘要:\n${previousSummary}\n\n新对话:\n${conversationText}\n\n请输出更新后的摘要(不超过 800 字)。` }, ], }); const newSummary = c.choices[0]?.message?.content ?? "";
await db.query("BEGIN"); try { await db.query( `INSERT INTO conv_summaries (conv_id, summary_text, updated_at) VALUES ($1, $2, NOW()) ON CONFLICT (conv_id) DO UPDATE SET summary_text = $2, updated_at = NOW()`, [convId, newSummary], ); await db.query( `UPDATE conv_messages SET summarized = TRUE WHERE conv_id = $1 AND summarized = FALSE`, [convId], ); await db.query("COMMIT"); } catch (e) { await db.query("ROLLBACK"); throw e; }}DB schema
CREATE TABLE conv_messages ( msg_id TEXT PRIMARY KEY, conv_id TEXT NOT NULL, sender TEXT NOT NULL, text TEXT NOT NULL, ts TIMESTAMPTZ NOT NULL, summarized BOOLEAN DEFAULT FALSE);CREATE INDEX ON conv_messages(conv_id, ts);CREATE INDEX ON conv_messages(conv_id, summarized) WHERE summarized = FALSE;
CREATE TABLE conv_summaries ( conv_id TEXT PRIMARY KEY, summary_text TEXT NOT NULL, updated_at TIMESTAMPTZ NOT NULL);撤销时清理
agent.addEventHandler(async (event) => { if (event.type !== "relation.revoked") return; const { user_id, conversation_id } = event.payload; await db.query("DELETE FROM conv_messages WHERE conv_id = $1", [conversation_id]); await db.query("DELETE FROM conv_summaries WHERE conv_id = $1", [conversation_id]); // user_id 跨多个 conv 的话,也要按 user_id 清});进阶:分主题 / 标签
ALTER TABLE conv_messages ADD COLUMN topic TEXT;-- 业务侧 LLM 给每条消息打 topic("工作 / 健身 / 旅行 / ...")-- 检索时按 topic + recent 双维度何时用哪种
对话场景: ├─ 客服 / 一次性问答 → 方案 A(in-context) ├─ 短期项目助理 (< 1 周) → 方案 B(全文存储) └─ 长期个人助理 / 关系维护 → 方案 C(摘要式)相关页面
- RAG 知识助理 — 文档型记忆
- 更新与撤销 — GDPR cleanup
- Production Checklist — 数据最小化