记忆模块:Agent的短期/中期/长期记忆 — 不是所有记忆都要放向量数据库

先说结论

很多 Agent 教程一讲记忆就是"向量数据库 + embedding 相似度检索",搞得新手以为没装 Milvus 就不配做 Agent。

不是所有记忆都要放向量数据库。 简单场景用结构化存储就够了 --- 甚至用 Python dict 就行。

在 self-media-agent 项目里,记忆分三层,每层用不同的存储方式:

层级 存什么 存哪 生命周期
短期记忆 当前对话上下文 ChatSession.messages(内存 dict) 会话结束即遗忘
中期记忆 风格偏好标签 PersonaConfig.style_preferences(结构化列表) 跨文章积累,持久化
长期记忆 风格画像摘要 PersonaConfig.style_profile(文本字段) 永久,注入每次生成

三层记忆,从简单到复杂,没有一行向量检索代码 --- 但 Agent 越用越懂你。

一、为什么 Agent 需要记忆?

没有记忆的 Agent,每次生成都是"第一次见面":

复制代码
用户:写一篇防晒推荐
Agent:(生成,风格偏正式)
​
用户:太正式了,活泼点
Agent:(重新生成,这次活泼了)
​
用户:再写一篇粉底液测评
Agent:(又变成正式风格了 ← 忘了上次说要活泼!)

有记忆的 Agent

arduino 复制代码
用户:写一篇防晒推荐
Agent:(生成,风格偏正式)
​
用户:太正式了,活泼点
Agent:(重新生成,活泼了 → 记住"用户偏好活泼")
​
用户:再写一篇粉底液测评
Agent:(自动用活泼风格 ← 读取了记忆!)

记忆让 Agent 从"一次性工具"变成"越用越懂你的助手"。 关键不是记忆技术多花哨,而是记住什么、什么时候读、什么时候写

二、三层记忆架构

项目的三层记忆,数据结构都在 Pydantic 模型里(第3篇讲过):

yaml 复制代码
PersonaConfig(人设 --- 中期+长期记忆的载体)
├── style_preferences: list[dict]    ← 中期记忆:结构化偏好标签
├── style_profile: str               ← 长期记忆:风格画像摘要
└── updated_at: datetime
​
ChatSession(会话 --- 短期记忆的载体)
├── messages: list[ChatMessage]      ← 短期记忆:对话上下文
├── revisions: list[RevisionRecord]  ← 修改记录(偏好提取的原料)
└── content_id: str                  ← 关联哪篇内容

三层记忆的关系

css 复制代码
用户修改文章
    ↓
[短期] ChatSession.messages ← 保存对话上下文
    ↓
[短期] ChatSession.revisions ← 保存修改前后 diff
    ↓
[中期] 从 diff 提取偏好 → PersonaConfig.style_preferences ← 结构化标签积累
    ↓
[长期] 汇总所有偏好 → LLM 生成画像 → PersonaConfig.style_profile ← 永久摘要
    ↓
下次生成时注入 Prompt ← 读取中长期记忆

短期记忆是原料,中期记忆是加工,长期记忆是成品。

三、短期记忆:对话上下文

chat/schema.pyChatSession 是短期记忆的载体:

ini 复制代码
class ChatSession(BaseModel):
    """聊天会话 --- 绑定到某篇内容"""
    id: str = Field(default="", description="会话 ID")
    content_id: str = Field(..., description="关联内容 ID")
    persona_id: str = Field(default="", description="关联人设 ID")
    messages: list[ChatMessage] = Field(default_factory=list, description="消息列表")
    revisions: list[RevisionRecord] = Field(default_factory=list, description="修改记录")
    created_at: datetime = Field(default_factory=datetime.now)
    updated_at: datetime = Field(default_factory=datetime.now)

短期记忆存两样东西

  1. messages --- 对话消息序列,用户和 AI 的来回交流
  2. revisions --- 每次修改的完整 diff(原文 + 修改后文 + 修改建议)

修改记录:RevisionRecord

python 复制代码
class RevisionRecord(BaseModel):
    """修改记录 --- 每次根据建议重新生成都会产生一条记录"""
    suggestion: str = Field(..., description="用户的修改建议")
    original_body: str = Field(..., description="修改前正文")
    revised_body: str = Field(..., description="修改后正文")
    original_title: str = Field(default="", description="修改前标题")
    revised_title: str = Field(default="", description="修改后标题")

为什么保存完整 diff 而不只是修改后文? 因为偏好提取需要对比"改了什么" --- 从 original_bodyrevised_body 的变化,加上 suggestion,才能推断用户偏好。

短期记忆的生命周期

ini 复制代码
# chat.py --- 创建会话
session = ChatSession(content_id=req.content_id, persona_id=content.persona_id)
session.messages.append(welcome)  # 系统欢迎消息
​
# 用户发消息
user_msg = ChatMessage(role=MessageRole.USER, content=req.message)
session.messages.append(user_msg)
​
# AI 修改后保存记录
revision = RevisionRecord(
    suggestion=req.message,
    original_body=content.body,
    revised_body=revised_body,
)
session.revisions.append(revision)
​
# AI 回复
ai_msg = ChatMessage(role=MessageRole.ASSISTANT, content="已修改...")
session.messages.append(ai_msg)
​
# 保存会话
state.repo.store.save_chat_session(session)

短期记忆在会话内积累,会话删除即遗忘。 但在遗忘之前,有价值的偏好已经被提取到中期记忆了。

四、中期记忆:风格偏好标签

PersonaConfig.style_preferences 是中期记忆 --- 一个结构化的偏好列表,跨文章积累:

arduino 复制代码
# persona/schema.py
class PersonaConfig(BaseModel):
    style_preferences: list[dict] = Field(default_factory=list, description="结构化风格偏好列表")

偏好的结构

每条偏好是一个 dict:

bash 复制代码
{
    "category": "语气",        # 偏好类别
    "value": "活泼",          # 偏好值
    "confidence": 0.8,       # 置信度 0-1
    "count": 3,              # 出现次数
    "evidence": "太正式了,活泼点"  # 原始修改建议
}

10 个偏好类别:语气、emoji、段落、用词、结构、开头、结尾、数据偏好、情感、节奏。

偏好的提取:从修改 diff 到结构化标签

style_learner.pyextract_preference_from_revision --- 让 LLM 从修改前后对比中提取偏好:

python 复制代码
class StyleLearner:
    async def extract_preference_from_revision(self, revision: RevisionRecord) -> list[dict]:
        system_prompt = (
            "你是一位内容风格分析师。根据用户的修改建议和修改前后的文章变化,"
            "提取用户的风格偏好。\n\n"
            "输出格式:JSON 数组,每个元素包含:\n"
            '- "category": 偏好类别(语气/emoji/段落/用词/结构/开头/结尾/数据偏好/情感/节奏)\n'
            '- "value": 偏好值,如"活泼"、"低频"、"短"等\n'
            '- "confidence": 置信度 0.0-1.0\n'
            '- "evidence": 原始修改建议文本\n'
        )
​
        # 截取修改前后各 500 字,避免 token 过长
        old_snippet = revision.original_body[:500]
        new_snippet = revision.revised_body[:500]
​
        user_prompt = (
            f"# 修改建议\n{revision.suggestion}\n\n"
            f"# 修改前(前500字)\n{old_snippet}\n\n"
            f"# 修改后(前500字)\n{new_snippet}\n\n"
            f"请提取风格偏好。"
        )
​
        result = await self.llm.generate(system_prompt=system_prompt, user_prompt=user_prompt)
        return json.loads(result)  # → [{"category": "语气", "value": "活泼", ...}]

LLM 看到"太正式了,活泼点" + 修改前后对比 → 提取出 {"category": "语气", "value": "活泼", "confidence": 0.8}

偏好的合并:去重 + 置信度取最高 + 次数累加

用户多次说"活泼点",不应该有 3 条相同的偏好。_merge_preferences 做合并:

python 复制代码
@staticmethod
def _merge_preferences(preferences: list[dict]) -> list[dict]:
    """合并去重偏好:相同 category+value 合并,confidence 取最高,count 累加"""
    merged: dict[str, dict] = {}
    for p in preferences:
        key = f"{p.get('category', '')}:{p.get('value', '')}"
        if key in merged:
            merged[key]["confidence"] = max(merged[key]["confidence"], p.get("confidence", 0.5))
            merged[key]["count"] = merged[key].get("count", 1) + 1
        else:
            merged[key] = {
                "category": p.get("category", ""),
                "value": p.get("value", ""),
                "confidence": p.get("confidence", 0.5),
                "count": 1,
                "evidence": p.get("evidence", ""),
            }
​
    # 按置信度 * 出现次数排序
    result = list(merged.values())
    result.sort(key=lambda x: x.get("confidence", 0) * x.get("count", 1), reverse=True)
    return result

合并规则

  • category + value 相同 → 合并为一条
  • confidence → 取最高
  • count → 累加(说了 3 次"活泼"→ count=3)
  • 排序 → confidence * count,既置信又频繁的排前面

即时写入:chat.py 中的第1层偏好更新

每次用户修改文章,chat.py 立即提取偏好并写入人设:

ini 复制代码
# chat.py --- 自动提取偏好并即时更新人设
new_prefs = await learner.extract_preference_from_revision(revision)
if new_prefs:
    persona = state.repo.store.get_persona(content.persona_id)
    existing = list(persona.style_preferences)
    existing.extend(new_prefs)
    merged = StyleLearner._merge_preferences(existing)
    updated_persona = persona.model_copy(update={
        "style_preferences": merged,
        "updated_at": datetime.now(),
    })
    state.repo.store.save_persona(updated_persona)  # ← 写入中期记忆

用户说一次"活泼点" → 立刻提取偏好 → 合并到 style_preferences → 保存。 下次生成就能看到。

五、长期记忆:风格画像摘要

PersonaConfig.style_profile 是长期记忆 --- 一段自然语言摘要,由 LLM 从所有偏好中生成:

arduino 复制代码
class PersonaConfig(BaseModel):
    style_profile: str = Field(default="", description="风格画像摘要(LLM 生成,自动进化)")

画像的生成:汇总所有偏好

build_profile 方法 --- 收集所有修改记录 → 逐条提取偏好 → 合并 → LLM 生成画像:

python 复制代码
async def build_profile(self, persona: PersonaConfig) -> tuple[str, list[dict]]:
    # 1. 收集该人设下所有修改记录
    all_revisions = []
    for session in self.store.list_chat_sessions():
        if session.persona_id != persona.id:
            continue
        all_revisions.extend(session.revisions)

    # 2. 从每条修改记录提取偏好
    all_preferences = []
    for rev in all_revisions:
        prefs = await self.extract_preference_from_revision(rev)
        all_preferences.extend(prefs)

    # 3. 合并去重
    merged = self._merge_preferences(all_preferences)

    # 4. LLM 生成风格画像摘要
    profile = await self._generate_profile_summary(persona, merged)

    return profile, merged

画像摘要的生成

python 复制代码
async def _generate_profile_summary(self, persona, preferences) -> str:
    system_prompt = (
        "你是一位内容风格专家。根据用户的风格偏好列表,生成一段简洁的风格画像摘要。\n\n"
        "要求:\n"
        "1. 用第二人称('你倾向于...')\n"
        "2. 按置信度从高到低组织\n"
        "3. 语言简洁有力,每点不超过15字\n"
    )

    pref_text = "\n".join(
        f"- {p['category']}:{p['value']}(置信度 {p.get('confidence', 0.5):.1f},出现 {p.get('count', 1)} 次)"
        for p in preferences
    )

    result = await self.llm.generate(system_prompt=system_prompt, user_prompt=...)
    return result.strip()

输出示例

复制代码
你倾向于活泼口语化表达;emoji使用克制;喜欢用数据支撑观点;段落简短有力

画像注入:每次生成时读取长期记忆

build_style_hint --- 把画像和偏好注入到生成 Prompt 里:

less 复制代码
@staticmethod
def build_style_hint(persona: PersonaConfig) -> str:
    hints = []

    # 风格画像摘要
    if persona.style_profile:
        hints.append(f"【风格画像】\n{persona.style_profile}")

    # 结构化偏好提醒(最多 8 条)
    prefs = persona.style_preferences
    if prefs:
        pref_lines = []
        for p in prefs[:8]:  # ← 最多 8 条,防止 token 超限
            cat = p.get("category", "")
            val = p.get("value", "")
            count = p.get("count", 1)
            if count > 1:
                pref_lines.append(f"- {cat}:{val}(你之前 {count} 次要求此调整)")
            else:
                pref_lines.append(f"- {cat}:{val}")
        hints.append("【风格偏好提醒】\n" + "\n".join(pref_lines))

    return "\n\n".join(hints)

注入后的 Prompt 多出一段

diff 复制代码
【风格画像】
你倾向于活泼口语化表达;emoji使用克制;喜欢用数据支撑观点;段落简短有力

【风格偏好提醒】
- 语气:活泼(你之前 3 次要求此调整)
- emoji:低频(你之前 2 次要求此调整)

这不是开发者写的 Prompt,是 Agent 从用户行为中学到的 Prompt。 content/body.py 在生成正文时自动注入:

css 复制代码
# content/body.py
style_hint = StyleLearner.build_style_hint(persona)
if style_hint:
    user_prompt += style_hint + "\n\n"

六、记忆的生命周期

记忆层 什么时候写入 什么时候读取 什么时候遗忘
短期 messages 每次对话追加 会话内展示上下文 会话删除时消失
短期 revisions 每次修改追加 提取偏好时遍历 会话删除时消失
中期 style_preferences 每次修改后即时提取合并 每次生成时 build_style_hint 读取 手动清除(不自动遗忘)
长期 style_profile 手动触发 build_profile 汇总 每次生成时 build_style_hint 读取 手动清除(不自动遗忘)

关键设计

  1. 短期→中期的转化是自动的 --- 用户每次修改,chat.py 自动提取偏好写入 style_preferences
  2. 中期→长期的转化是手动的 --- 需要调用 build_profile 汇总所有偏好生成画像(因为要遍历所有会话 + 多次 LLM 调用,开销大)
  3. 中长期记忆不自动遗忘 --- 偏好只会积累不会消失,除非用户手动清除。这是合理的:用户偏好是稳定的
  4. 偏好最多注入 8 条 --- prefs[:8],防止 Prompt token 超限

七、持久化:YAML 文件存储

三层记忆最终都要持久化 --- 项目用 YAML 文件,不是数据库:

存储架构

javascript 复制代码
storage/
├── memory.py        ← MemoryStore:纯内存 dict 存储
├── persistence.py   ← FilePersistence:序列化到 YAML/JSON
└── repository.py    ← Repository:统一接口,屏蔽存储实现

Repository:统一接口

python 复制代码
class Repository:
    def __init__(self, persist_dir=None, persist_format="yaml"):
        self.store = MemoryStore()          # 内存存储
        self.persistence = None
        if persist_dir:
            self.persistence = FilePersistence(store_dir=persist_dir, format=persist_format)
            self.persistence.load(self.store)  # 启动时从文件恢复

    def _maybe_persist(self):
        if self.persistence:
            self.persistence.save(self.store)  # 操作后落盘

两层存储:内存 dict 做读写(快),YAML 文件做持久化(重启不丢)。

为什么不用数据库?

YAML 文件 SQLite 向量数据库
依赖 Python 内置 需要额外安装
适合数据量 < 1000 条 < 100 万条 任意
查询能力 全量加载 SQL 相似度检索
项目选择

自媒体 Agent 的数据量:几个人设 + 几十个选题 + 几百篇内容 --- YAML 足绑有余。引入数据库只增加部署复杂度,不增加价值。

当数据量真的大了再换 SQLite,当需要语义检索了再换向量数据库 --- 不过度设计。

踩坑总结

根因 修复
每次生成风格随机 没有记忆 加三层记忆,生成时注入风格画像
用户反复说"活泼点" 偏好没积累 style_preferences 合并去重,count 累加
画像太长 token 超限 偏好全量注入 prefs[:8] 最多注入 8 条
偏好提取 JSON 解析失败 LLM 输出不稳定 try/except 降级,不影响主流程
修改后原文丢失 只存修改后文 RevisionRecord 保存完整 diff(原文+修改后文)
重启后记忆丢失 纯内存存储 加 FilePersistence 持久化到 YAML
过度设计用向量数据库 教程误导 数据量小用 YAML 就够,不过度设计

经验总结

  1. 不是所有记忆都要放向量数据库 --- 简单场景用结构化存储(dict + YAML)就够了,不过度设计
  2. 三层记忆各司其职 --- 短期存对话上下文,中期存偏好标签,长期存画像摘要,从原料到成品
  3. 短期→中期自动转化 --- 每次修改自动提取偏好,用户无感知;中期→长期手动触发,避免开销
  4. 偏好合并是关键 --- 相同偏好去重,confidence 取最高,count 累加,既置信又频繁的排前面
  5. 记忆的价值在于注入 --- 存了不读等于没存,build_style_hint 在每次生成时自动注入,让 Agent 越用越懂你

下篇预告

下一篇讲 规划模块:Agent的大脑 --- 规划决定 Agent"做什么"和"以什么顺序做",从选题规划到流水线编排到风格进化策略,当前项目是静态规划,未来如何变动态。

相关推荐
DS随心转小程序2 小时前
巧用 AI 导出鸭攻克各类难题完善 ChatGPT 输出 word 文档转化工作
人工智能·chatgpt·aigc·word·豆包·deepseek·ai导出鸭
乘风gg2 小时前
AI Coding:从单兵提效到多 Agent 的团队全链路协作模式
前端·ai编程·claude
咖啡星人k3 小时前
AI编程Agent为何要配真服务器:拆解MonkeyCode云端沙箱
人工智能·aigc
AI创界者4 小时前
最新 ComfyUI + LTX-2.5 本地一键整合包:文生视频/图生视频/数字人全流程指南
人工智能·aigc·音视频
xiezhr4 小时前
DeepSeek 不能识图?装个开源 Skill,纯文本模型也能"睁眼"了
github·aigc·deepseek
程序员黑豆4 小时前
Java字符串拼接全解析:6种方式性能对比与实战指南
java·前端·ai编程
wangruofeng10 小时前
连 agent loop 自己都是插件,拆开 DeepSeek 新开源的 harness 底座
aigc·ai编程
程序员黑豆11 小时前
Java字符串详解
java·前端·ai编程
小虎AI生活12 小时前
从写代码到被出版社认作"科普推广人":一场新书发布会给我的触动
ai编程