跳转到内容

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(摘要式)

相关页面