先说结论
很多 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.py 的 ChatSession 是短期记忆的载体:
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)
短期记忆存两样东西:
messages--- 对话消息序列,用户和 AI 的来回交流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_body 到 revised_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.py 的 extract_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 读取 | 手动清除(不自动遗忘) |
关键设计:
- 短期→中期的转化是自动的 --- 用户每次修改,chat.py 自动提取偏好写入 style_preferences
- 中期→长期的转化是手动的 --- 需要调用
build_profile汇总所有偏好生成画像(因为要遍历所有会话 + 多次 LLM 调用,开销大) - 中长期记忆不自动遗忘 --- 偏好只会积累不会消失,除非用户手动清除。这是合理的:用户偏好是稳定的
- 偏好最多注入 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 就够,不过度设计 |
经验总结
- 不是所有记忆都要放向量数据库 --- 简单场景用结构化存储(dict + YAML)就够了,不过度设计
- 三层记忆各司其职 --- 短期存对话上下文,中期存偏好标签,长期存画像摘要,从原料到成品
- 短期→中期自动转化 --- 每次修改自动提取偏好,用户无感知;中期→长期手动触发,避免开销
- 偏好合并是关键 --- 相同偏好去重,confidence 取最高,count 累加,既置信又频繁的排前面
- 记忆的价值在于注入 --- 存了不读等于没存,
build_style_hint在每次生成时自动注入,让 Agent 越用越懂你
下篇预告
下一篇讲 规划模块:Agent的大脑 --- 规划决定 Agent"做什么"和"以什么顺序做",从选题规划到流水线编排到风格进化策略,当前项目是静态规划,未来如何变动态。