摘要: Agent 想真正做到"越用越懂你",靠的并不是简单保存聊天记录,而是一套完整的用户偏好记忆系统。本文从真实项目中的"记不住、记错了、过度记忆、旧偏好污染"等问题出发,系统拆解 Agent 用户偏好记忆的分层模型、写入策略、召回机制、冲突处理、遗忘机制与安全治理,并给出可落地的 Python 实现思路、Mermaid 架构图和验证方法。读完后你可以独立设计一套可控、可解释、可演进的 Agent Memory。建议收藏。
引言:为什么你的 Agent 聊了100轮,还是"不懂你"
做过 Agent 项目的同学,大概率遇到过这种场景。
第一天你对助手说:
"以后回答技术问题时,不要先讲一大段背景,直接给方案。"
Agent 当场回答:
"好的,我记住了。"
第二天重新打开会话,你问:
"Python 怎么实现一个 LRU Cache?"
它又开始:
"LRU,即 Least Recently Used,是一种常见的缓存淘汰算法......"
这时候用户的第一反应通常不是:
"这个模型推理能力不行。"
而是:
"你昨天不是说记住了吗?"
更麻烦的是,有些 Agent 确实会"记住",但记忆方式同样令人崩溃。
比如你随口说:
"最近晚上喜欢喝咖啡。"
系统直接把它写成永久用户画像:
text
用户长期偏好:晚上喜欢喝咖啡。
三个月后,你告诉 Agent:
"最近失眠,晚上已经不喝咖啡了。"
结果推荐系统依旧隔三差五问你:
"晚上要不要来杯咖啡?"
这说明一个事实:
Agent 的"记忆"绝不是把对话存进数据库这么简单。
真正可用的用户偏好记忆系统,至少要回答下面几个问题:
- 什么信息值得记?
- 这条信息是长期偏好,还是临时状态?
- 用户说了一次,就能当成事实吗?
- 多条偏好冲突时相信哪一条?
- 什么时候应该召回?
- 什么时候应该遗忘?
- 用户修改偏好后,旧数据怎么办?
- 敏感信息是否允许被长期保存?
- 如何避免 Memory 越积越多,最终污染 Prompt?
很多 Agent Demo 在"记忆"这件事上的实现实际上只有两行逻辑:
text
保存历史聊天
→
下一轮全部塞给大模型
短会话还能凑合,一旦进入长期使用场景,Token 成本、噪声、冲突、隐私以及上下文污染都会迅速暴露。
本文不讨论"把聊天历史存在哪里"这种基础问题,而是重点解决更关键的工程问题:
如何为 Agent 设计一套真正可用的"用户偏好记忆系统"。
一、先把问题搞清楚:聊天记录不等于用户记忆
1.1 为什么不能直接把历史聊天当 Memory
很多 Agent 项目初版都会采用类似方案:
text
User Message
↓
读取历史聊天
↓
拼接 Prompt
↓
LLM 推理
↓
保存本轮消息
这种方案实现简单,但它本质上只能算:
Conversation History,聊天历史。
而不是:
User Memory,用户记忆。
二者最大的区别在于:
聊天历史回答的是:
"用户过去说过什么?"
用户记忆回答的是:
"这些内容中,哪些信息在未来仍然值得影响 Agent 的行为?"
例如用户连续说了下面几句话:
text
1. 我今天在杭州出差。
2. 我平时主要写 Python。
3. 帮我把回答控制在 500 字以内。
4. 中午吃了一碗面。
5. 以后代码示例优先使用 Python。
显然这五句话的价值并不一样。
"中午吃了一碗面"几乎没必要长期保存。
"今天在杭州出差"属于短期上下文。
"平时主要写 Python"属于相对稳定的用户画像。
"以后代码示例优先使用 Python"则是一条明确的行为偏好。
所以,Memory 的核心不是:
存储更多信息。
而是:
从大量信息中提炼出值得长期影响未来行为的信息。
1.2 一个成熟 Agent 至少有四类上下文
实际工程中,我更建议把 Agent 上下文拆成四层:
| 类型 | 示例 | 生命周期 | 是否长期保存 |
|---|---|---|---|
| 当前上下文 | "帮我分析这段代码" | 当前任务 | 否 |
| 会话记忆 | "刚才那个接口继续优化" | 当前 Session | 可选 |
| 用户偏好 | "代码优先用 Python" | 跨 Session | 是 |
| 用户事实 | "我是后端开发" | 跨 Session | 视场景 |
这四种东西如果全部混在一个 messages 表里,系统迟早会失控。
可以把它理解成电脑里的不同存储层级:
- 当前上下文像 CPU 寄存器;
- Session Memory 像内存;
- 用户偏好像配置文件;
- 长期事实像数据库。
不同数据应该有不同的生命周期。
1.3 用户偏好又可以分成哪些类型
实际项目中,建议至少拆成下面五类。
1)表达偏好
描述用户希望 Agent 怎么回答。
例如:
text
回答尽量简洁。
不要重复我的问题。
技术方案优先给代码。
解释概念时多举例。
不要使用过多 Emoji。
这类偏好对 Agent 行为影响非常直接。
2)技术偏好
描述用户更常用的技术栈或工具。
例如:
text
默认语言:Python
前端框架:Vue 3
数据库:PostgreSQL
部署环境:Docker + Linux
包管理器:pnpm
注意:
"用户使用 Python"与"代码优先使用 Python"不是完全相同的概念。
前者是事实,后者才是明确偏好。
3)内容偏好
例如:
text
喜欢实战教程。
比起理论,更关注落地方案。
希望包含完整代码。
喜欢表格对比。
喜欢 Mermaid 架构图。
这类 Memory 很适合博客生成、知识助手、学习 Agent。
4)约束偏好
比如:
text
预算不超过 3000 元。
不要推荐 Windows。
避免使用需要付费 API 的方案。
输出不能包含公司内部信息。
这类 Memory 的优先级通常比"喜欢什么"更高。
因为:
"不要"往往比"喜欢"更加刚性。
5)临时偏好
例如:
text
这周只讨论 LangGraph。
最近正在准备 Java 面试。
本月项目暂时不用 Kubernetes。
它们可能影响未来几天的交互,但不应该永久保存。
因此必须具备:
TTL 或过期机制。
1.4 Memory 最难的不是存,而是判断
我们可以把记忆写入抽象成一个决策问题:
#mermaid-svg-4tx97eQUzLAvk0wA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4tx97eQUzLAvk0wA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4tx97eQUzLAvk0wA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4tx97eQUzLAvk0wA .error-icon{fill:#552222;}#mermaid-svg-4tx97eQUzLAvk0wA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4tx97eQUzLAvk0wA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4tx97eQUzLAvk0wA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4tx97eQUzLAvk0wA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4tx97eQUzLAvk0wA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4tx97eQUzLAvk0wA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4tx97eQUzLAvk0wA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4tx97eQUzLAvk0wA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4tx97eQUzLAvk0wA .marker.cross{stroke:#333333;}#mermaid-svg-4tx97eQUzLAvk0wA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4tx97eQUzLAvk0wA p{margin:0;}#mermaid-svg-4tx97eQUzLAvk0wA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4tx97eQUzLAvk0wA .cluster-label text{fill:#333;}#mermaid-svg-4tx97eQUzLAvk0wA .cluster-label span{color:#333;}#mermaid-svg-4tx97eQUzLAvk0wA .cluster-label span p{background-color:transparent;}#mermaid-svg-4tx97eQUzLAvk0wA .label text,#mermaid-svg-4tx97eQUzLAvk0wA span{fill:#333;color:#333;}#mermaid-svg-4tx97eQUzLAvk0wA .node rect,#mermaid-svg-4tx97eQUzLAvk0wA .node circle,#mermaid-svg-4tx97eQUzLAvk0wA .node ellipse,#mermaid-svg-4tx97eQUzLAvk0wA .node polygon,#mermaid-svg-4tx97eQUzLAvk0wA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4tx97eQUzLAvk0wA .rough-node .label text,#mermaid-svg-4tx97eQUzLAvk0wA .node .label text,#mermaid-svg-4tx97eQUzLAvk0wA .image-shape .label,#mermaid-svg-4tx97eQUzLAvk0wA .icon-shape .label{text-anchor:middle;}#mermaid-svg-4tx97eQUzLAvk0wA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4tx97eQUzLAvk0wA .rough-node .label,#mermaid-svg-4tx97eQUzLAvk0wA .node .label,#mermaid-svg-4tx97eQUzLAvk0wA .image-shape .label,#mermaid-svg-4tx97eQUzLAvk0wA .icon-shape .label{text-align:center;}#mermaid-svg-4tx97eQUzLAvk0wA .node.clickable{cursor:pointer;}#mermaid-svg-4tx97eQUzLAvk0wA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4tx97eQUzLAvk0wA .arrowheadPath{fill:#333333;}#mermaid-svg-4tx97eQUzLAvk0wA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4tx97eQUzLAvk0wA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4tx97eQUzLAvk0wA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4tx97eQUzLAvk0wA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4tx97eQUzLAvk0wA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4tx97eQUzLAvk0wA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4tx97eQUzLAvk0wA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4tx97eQUzLAvk0wA .cluster text{fill:#333;}#mermaid-svg-4tx97eQUzLAvk0wA .cluster span{color:#333;}#mermaid-svg-4tx97eQUzLAvk0wA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4tx97eQUzLAvk0wA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4tx97eQUzLAvk0wA rect.text{fill:none;stroke-width:0;}#mermaid-svg-4tx97eQUzLAvk0wA .icon-shape,#mermaid-svg-4tx97eQUzLAvk0wA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4tx97eQUzLAvk0wA .icon-shape p,#mermaid-svg-4tx97eQUzLAvk0wA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4tx97eQUzLAvk0wA .icon-shape .label rect,#mermaid-svg-4tx97eQUzLAvk0wA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4tx97eQUzLAvk0wA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4tx97eQUzLAvk0wA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4tx97eQUzLAvk0wA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
否
是
用户输入
是否包含可记忆信息
不写入 Memory
属于哪种信息
长期偏好
临时偏好
用户事实
敏感信息
置信度是否足够
设置过期时间
是否允许长期保存
等待更多证据
写入或更新 Memory
这张图里最值得注意的是:
用户说过某句话,不等于这句话必须立即成为长期记忆。
Memory 应该经过:
识别 → 分类 → 判断 → 写入。
而不是:
检测到信息 → 无脑保存。
二、设计一套可用的用户偏好记忆架构
2.1 推荐采用"原始证据 + 结构化记忆"双层设计
我在实际设计这类系统时,非常不建议只存一句自然语言,例如:
json
{
"memory": "用户喜欢 Python"
}
原因很简单:
未来你几乎无法可靠处理:
- 谁说的;
- 什么时候说的;
- 说过几次;
- 置信度多少;
- 是否已经过期;
- 是否存在冲突;
- 为什么系统认为它成立。
更合理的结构是:
text
Raw Evidence
↓
Memory Extraction
↓
Structured Memory
即:
原始证据层 + 结构化偏好层。
2.2 整体系统架构
一个相对完整的 Agent 偏好记忆链路如下:
#mermaid-svg-CfE5TFigWqfMeQWP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CfE5TFigWqfMeQWP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CfE5TFigWqfMeQWP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CfE5TFigWqfMeQWP .error-icon{fill:#552222;}#mermaid-svg-CfE5TFigWqfMeQWP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CfE5TFigWqfMeQWP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CfE5TFigWqfMeQWP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CfE5TFigWqfMeQWP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CfE5TFigWqfMeQWP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CfE5TFigWqfMeQWP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CfE5TFigWqfMeQWP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CfE5TFigWqfMeQWP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CfE5TFigWqfMeQWP .marker.cross{stroke:#333333;}#mermaid-svg-CfE5TFigWqfMeQWP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CfE5TFigWqfMeQWP p{margin:0;}#mermaid-svg-CfE5TFigWqfMeQWP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CfE5TFigWqfMeQWP .cluster-label text{fill:#333;}#mermaid-svg-CfE5TFigWqfMeQWP .cluster-label span{color:#333;}#mermaid-svg-CfE5TFigWqfMeQWP .cluster-label span p{background-color:transparent;}#mermaid-svg-CfE5TFigWqfMeQWP .label text,#mermaid-svg-CfE5TFigWqfMeQWP span{fill:#333;color:#333;}#mermaid-svg-CfE5TFigWqfMeQWP .node rect,#mermaid-svg-CfE5TFigWqfMeQWP .node circle,#mermaid-svg-CfE5TFigWqfMeQWP .node ellipse,#mermaid-svg-CfE5TFigWqfMeQWP .node polygon,#mermaid-svg-CfE5TFigWqfMeQWP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CfE5TFigWqfMeQWP .rough-node .label text,#mermaid-svg-CfE5TFigWqfMeQWP .node .label text,#mermaid-svg-CfE5TFigWqfMeQWP .image-shape .label,#mermaid-svg-CfE5TFigWqfMeQWP .icon-shape .label{text-anchor:middle;}#mermaid-svg-CfE5TFigWqfMeQWP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CfE5TFigWqfMeQWP .rough-node .label,#mermaid-svg-CfE5TFigWqfMeQWP .node .label,#mermaid-svg-CfE5TFigWqfMeQWP .image-shape .label,#mermaid-svg-CfE5TFigWqfMeQWP .icon-shape .label{text-align:center;}#mermaid-svg-CfE5TFigWqfMeQWP .node.clickable{cursor:pointer;}#mermaid-svg-CfE5TFigWqfMeQWP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CfE5TFigWqfMeQWP .arrowheadPath{fill:#333333;}#mermaid-svg-CfE5TFigWqfMeQWP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CfE5TFigWqfMeQWP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CfE5TFigWqfMeQWP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CfE5TFigWqfMeQWP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CfE5TFigWqfMeQWP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CfE5TFigWqfMeQWP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CfE5TFigWqfMeQWP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CfE5TFigWqfMeQWP .cluster text{fill:#333;}#mermaid-svg-CfE5TFigWqfMeQWP .cluster span{color:#333;}#mermaid-svg-CfE5TFigWqfMeQWP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CfE5TFigWqfMeQWP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CfE5TFigWqfMeQWP rect.text{fill:none;stroke-width:0;}#mermaid-svg-CfE5TFigWqfMeQWP .icon-shape,#mermaid-svg-CfE5TFigWqfMeQWP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CfE5TFigWqfMeQWP .icon-shape p,#mermaid-svg-CfE5TFigWqfMeQWP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CfE5TFigWqfMeQWP .icon-shape .label rect,#mermaid-svg-CfE5TFigWqfMeQWP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CfE5TFigWqfMeQWP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CfE5TFigWqfMeQWP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CfE5TFigWqfMeQWP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 新增
更新
删除
用户请求
Agent Orchestrator
Memory Retriever
Memory Store
Prompt Builder
LLM
生成回答
Memory Extractor
Memory Classifier
Validator
新增/更新/删除?
Expiration & Consolidation
整个系统可以拆成两条链路。
第一条是:
读取链路
text
用户请求
→ 检索相关 Memory
→ 注入 Prompt
→ Agent 回答
第二条是:
写入链路
text
用户请求
→ Memory 提取
→ 分类
→ 验证
→ 冲突处理
→ 写入数据库
这两个链路最好解耦。
很多项目最大的问题就是把它们写进一个 Prompt:
"请回答用户问题,同时判断有哪些值得记忆的信息并更新数据库。"
Demo 没问题。
生产环境非常容易出现不可控行为。
2.3 Memory 数据模型怎么设计
一条 Memory 至少应该包含这些字段:
| 字段 | 含义 |
|---|---|
memory_id |
唯一 ID |
user_id |
用户 ID |
category |
偏好类别 |
key |
标准化属性 |
value |
偏好值 |
confidence |
置信度 |
source |
来源 |
created_at |
创建时间 |
updated_at |
更新时间 |
expires_at |
过期时间 |
status |
active / superseded / deleted |
evidence_count |
支持证据数量 |
例如:
json
{
"user_id": "u1001",
"category": "coding_preference",
"key": "preferred_language",
"value": "python",
"confidence": 0.95,
"source": "explicit_user_statement",
"evidence_count": 3,
"status": "active"
}
这样未来才能真正实现:
text
查询:
preferred_language = ?
更新:
Python → Go
冲突:
喜欢简洁回答
VS
当前请求要求详细解释
过期:
最近一个月在学习 Rust
2.4 为什么推荐 Key-Value + 自然语言摘要并存
纯 Key-Value 很容易检索:
text
preferred_language = python
response_style = concise
diagram_preference = mermaid
但它无法表达复杂偏好:
"当我问架构问题时,可以详细解释;如果只是报错排查,优先直接给解决办法。"
因此实际工程中,可以同时保留:
结构化属性
json
{
"response_style": "concise"
}
以及:
自然语言 Memory
text
用户偏好简洁回答,但涉及架构设计时接受较详细的原理说明。
推荐分工是:
结构化字段用于:
- 精确过滤;
- 权限控制;
- 冲突检测;
- 稳定注入。
自然语言 Memory 用于:
- 语义检索;
- 复杂偏好表达;
- Few-shot 推理。
二者不是二选一,而是互补。
2.5 不要设计成只有一个 User Profile
另一个常见设计是:
json
{
"user_profile": "用户是一名后端程序员,喜欢 Python..."
}
每次更新时让 LLM:
"根据新对话重新总结用户画像。"
这种方式早期非常舒服,但长期有三个严重问题。
第一,信息不可定位更新。
你只修改一个偏好,却需要重写整段 Profile。
第二,容易发生摘要漂移。
每次总结都会有一点点信息损失,几十次以后用户画像可能已经和原始事实不同。
第三,删除非常困难。
用户要求:
"忘掉我之前关于工作的信息。"
如果所有数据已经被揉成一段 Profile,根本无法可靠删除。
因此推荐:
text
多条原子 Memory
+
动态 Profile Summary
而不是:
text
一个无限覆盖更新的大字符串
三、最关键的一步:什么时候应该写入 Memory
3.1 显式偏好与隐式偏好必须区别处理
用户说:
"以后所有代码都优先使用 Python。"
这是典型的:
Explicit Preference,显式偏好。
可以给予很高置信度:
text
confidence = 0.95
但如果用户连续三次都要求:
"用 Python 写。"
这只能说明:
存在潜在偏好。
不能马上等价成:
"用户以后永远优先使用 Python。"
这种偏好属于:
Implicit Preference,隐式偏好。
通常应该从较低置信度开始。
例如:
text
第一次使用 Python:0.30
第二次使用 Python:0.45
第三次使用 Python:0.60
用户明确说喜欢 Python:0.95
当然,生产系统不一定真的采用这个固定数值,但思路非常重要。
3.2 我更推荐"三段式写入判断"
每条候选 Memory 依次问三个问题。
第一问:未来还有没有价值?
例如:
text
"我今天有点累。"
可能只对当前对话有价值。
而:
text
"解释算法时请尽量用 Python。"
显然会影响后续很多任务。
第二问:信息是否足够稳定?
text
"最近在学 Go。"
稳定程度较低。
text
"我的主要开发语言一直是 Java。"
稳定程度明显更高。
因此可以给 Memory 一个稳定性字段:
text
stability:
temporary
medium
long_term
第三问:是否值得承担长期保存成本?
长期 Memory 不是免费的。
成本包括:
- 存储成本;
- 检索成本;
- Prompt Token;
- 用户隐私风险;
- 错误记忆带来的体验损失;
- 后续更新维护成本。
所以应该遵守一个很实用的原则:
宁可少记,也不要乱记。
3.3 给候选 Memory 打分
可以构造一个简单评分:
Score = 0.35R + 0.25C + 0.20S + 0.20F
其中:
R:未来相关性 Relevance;C:置信度 Confidence;S:稳定性 Stability;F:出现频率 Frequency。
例如:
"以后写博客请使用 Markdown。"
可能得到:
text
Relevance = 0.95
Confidence = 0.98
Stability = 0.90
Frequency = 0.60
最终分数较高,可以直接进入长期记忆。
而:
"今晚想吃火锅。"
可能是:
text
Relevance = 0.10
Confidence = 1.00
Stability = 0.05
Frequency = 0.10
即使这句话真实性非常高,也没有必要长期保存。
这说明:
真实性高,不代表记忆价值高。
3.4 偏好冲突应该怎么处理
这是 Memory 系统真正进入工程阶段以后一定会遇到的问题。
例如历史 Memory:
text
preferred_language = Python
用户今天说:
"后面的例子都不要用 Python 了,优先 Go。"
此时绝对不能得到:
text
preferred_language = Python
preferred_language = Go
然后两条一起塞进 Prompt。
合理流程应该是:
#mermaid-svg-zBY9bm3OlQ1GSEeS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zBY9bm3OlQ1GSEeS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zBY9bm3OlQ1GSEeS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zBY9bm3OlQ1GSEeS .error-icon{fill:#552222;}#mermaid-svg-zBY9bm3OlQ1GSEeS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zBY9bm3OlQ1GSEeS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zBY9bm3OlQ1GSEeS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zBY9bm3OlQ1GSEeS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zBY9bm3OlQ1GSEeS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zBY9bm3OlQ1GSEeS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zBY9bm3OlQ1GSEeS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zBY9bm3OlQ1GSEeS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zBY9bm3OlQ1GSEeS .marker.cross{stroke:#333333;}#mermaid-svg-zBY9bm3OlQ1GSEeS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zBY9bm3OlQ1GSEeS p{margin:0;}#mermaid-svg-zBY9bm3OlQ1GSEeS defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-zBY9bm3OlQ1GSEeS g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-zBY9bm3OlQ1GSEeS g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-zBY9bm3OlQ1GSEeS g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-zBY9bm3OlQ1GSEeS g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-zBY9bm3OlQ1GSEeS g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-zBY9bm3OlQ1GSEeS .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-zBY9bm3OlQ1GSEeS .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-zBY9bm3OlQ1GSEeS .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-zBY9bm3OlQ1GSEeS .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-zBY9bm3OlQ1GSEeS .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-zBY9bm3OlQ1GSEeS .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-zBY9bm3OlQ1GSEeS .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-zBY9bm3OlQ1GSEeS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zBY9bm3OlQ1GSEeS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zBY9bm3OlQ1GSEeS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zBY9bm3OlQ1GSEeS .edgeLabel .label text{fill:#333;}#mermaid-svg-zBY9bm3OlQ1GSEeS .label div .edgeLabel{color:#333;}#mermaid-svg-zBY9bm3OlQ1GSEeS .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-zBY9bm3OlQ1GSEeS .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-zBY9bm3OlQ1GSEeS .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-zBY9bm3OlQ1GSEeS .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-zBY9bm3OlQ1GSEeS .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-zBY9bm3OlQ1GSEeS .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zBY9bm3OlQ1GSEeS .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zBY9bm3OlQ1GSEeS #statediagram-barbEnd{fill:#333333;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zBY9bm3OlQ1GSEeS .cluster-label,#mermaid-svg-zBY9bm3OlQ1GSEeS .nodeLabel{color:#131300;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-zBY9bm3OlQ1GSEeS .note-edge{stroke-dasharray:5;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagram-note text{fill:black;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagram-note .nodeLabel{color:black;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagram .edgeLabel{color:red;}#mermaid-svg-zBY9bm3OlQ1GSEeS #dependencyStart,#mermaid-svg-zBY9bm3OlQ1GSEeS #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-zBY9bm3OlQ1GSEeS .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zBY9bm3OlQ1GSEeS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 提取新偏好
无历史记录
与旧偏好一致
与旧偏好冲突
用户明确修改
当前任务临时要求
隐式冲突
Candidate
NewMemory
Confirmed
Conflict
Replace
TemporaryOverride
NeedMoreEvidence
Superseded
ActiveNew
KeepOld
这里最重要的三个场景必须区分。
场景一:明确修改
用户:
"以后别用 Python 了,默认用 Go。"
应该直接更新长期偏好。
场景二:当前任务覆盖
用户:
"这道题虽然我平时喜欢 Python,但请用 Java 写。"
这只能视为:
text
Task Override
不能更新全局 Memory。
优先级应该是:
text
当前明确指令
>
当前任务上下文
>
长期用户偏好
>
系统默认值
这条优先级非常重要。
Memory 永远不应该覆盖用户本轮刚刚说出口的要求。
场景三:隐式行为变化
之前一直使用 Python,但最近连续使用 Go。
此时更适合:
text
降低旧偏好 confidence
+
提高新候选偏好 confidence
而不是直接切换。
3.5 Memory 需要支持遗忘,而不是只增不减
很多系统只设计:
text
INSERT Memory
UPDATE Memory
却没有:
text
DELETE / EXPIRE Memory
这会让 Memory Store 最终变成"数字垃圾场"。
建议至少支持三种遗忘机制。
TTL 过期
例如:
text
最近一个月正在备考 AWS
可以设置:
text
expires_at = 30 days
衰减机制
如果某条隐式偏好半年没有再次出现,可以逐渐降低置信度:
C_t = C_0 \\times e\^{-\\lambda t}
不需要纠结公式本身。
核心思想只有一句话:
时间越久、没有被重新确认的信息,影响力应该越低。
用户主动删除
必须支持类似指令:
text
忘掉我刚才说的内容。
删除关于饮食习惯的记忆。
不要再记住我的公司信息。
清空我的长期偏好。
而且真正删除 Memory 时,还要考虑:
- 向量索引是否删除;
- 缓存是否失效;
- Profile Summary 是否重新生成;
- 是否仍存在原始 Evidence;
- 日志里是否包含副本。
这也是为什么长期记忆从第一天开始就应该考虑数据生命周期。
四、Memory 怎么召回:别把所有偏好都塞进 Prompt
4.1 "存得准"只是第一步,"取对"更重要
假设一个用户长期使用半年,你已经积累了 300 条 Memory。
他今天问:
"帮我写一个 FastAPI 文件上传接口。"
如果你直接把全部 Memory 塞进去:
text
用户喜欢川菜。
用户计划明年去日本。
用户喜欢简洁回答。
用户主要使用 Python。
用户之前购买过 Mac。
用户经常使用 PostgreSQL。
......
显然大量内容没有任何价值。
因此 Memory Retriever 必须解决:
当前任务到底需要哪些记忆?
4.2 推荐采用两阶段召回
第一阶段:
过滤。
根据类别、状态、权限先筛选:
text
status = active
category in relevant_categories
expires_at > now
第二阶段:
排序。
可以采用:
FinalScore = 0.45 \\times SemanticSimilarity + 0.25 \\times Confidence + 0.20 \\times Recency + 0.10 \\times Importance
其中:
SemanticSimilarity:当前 Query 与 Memory 的语义相似度;Confidence:Memory 本身可信程度;Recency:是否近期得到确认;Importance:这条偏好的全局重要程度。
例如查询:
"帮我写一个 FastAPI 服务。"
下面三条 Memory:
text
A. 用户优先使用 Python。
B. 用户喜欢代码示例包含类型注解。
C. 用户喜欢旅游。
A、B 应该被召回。
C 不应该出现。
4.3 并不是所有 Memory 都需要向量数据库
看到"长期记忆",很多开发者第一反应就是:
上 Milvus、Pinecone、Weaviate、Qdrant。
其实没必要。
例如:
text
preferred_language = Python
response_length = concise
preferred_os = macOS
这种结构化数据,普通关系数据库甚至 Redis 都能很好解决。
向量检索真正适合的是:
text
"用户在架构设计问题上喜欢先看整体流程图,再看代码。"
这种难以提前定义固定 Schema 的语义偏好。
因此比较合理的架构是:
| Memory | 推荐存储 |
|---|---|
| 标准用户属性 | MySQL/PostgreSQL |
| 显式偏好 | PostgreSQL/Document DB |
| 临时状态 | Redis |
| 自然语言长期 Memory | Vector DB |
| Evidence | Object Storage/DB |
不要为了"Agent 技术栈看起来先进",把所有东西向量化。
4.4 Prompt 中怎么注入 Memory
推荐加入一个单独区域:
text
[User Preferences]
The following preferences may be useful when responding.
Only apply them when relevant to the current request.
- Preferred programming language: Python.
- Prefers concise explanations.
- For architecture topics, diagrams are preferred.
这里有两个关键点。
第一:
写明 Only apply them when relevant。
避免无关偏好强行影响当前任务。
第二:
Memory 是辅助上下文,不是最高级指令。
例如用户长期偏好:
text
回答尽量简短。
但本轮明确要求:
"请详细分析原因,至少 3000 字。"
当然应该执行当前要求。
4.5 给 Memory 设置 Token Budget
这是一个非常容易被忽略的工程问题。
假设上下文窗口很大,就能无限塞 Memory 吗?
不能。
因为上下文越长不代表效果一定越好。
过量 Memory 会造成:
- 注意力分散;
- 冲突信息增加;
- 推理不稳定;
- Token 成本上涨;
- 响应延迟增加。
推荐为 Memory 设置独立 Budget,例如:
text
System Prompt 3000 tokens
Retrieved Docs 6000 tokens
Conversation 5000 tokens
User Memory 1000 tokens
Current Query 500 tokens
数字并不是固定标准。
重点是:
Memory 必须有预算,而不是有多少塞多少。
可以进一步设置:
text
最大召回数量:10
强偏好:最多 5 条
语义 Memory:最多 5 条
单条 Memory:最多 100 tokens
长期来看,这比盲目扩大上下文有效得多。
五、工程实战:实现一个轻量级偏好记忆模块
下面实现一个简化版 Memory Engine。
目标不是造一个完整框架,而是展示核心思想:
text
提取候选偏好
→
标准化
→
更新或覆盖
→
按任务召回
5.1 环境准备
示例环境:
text
Python >= 3.10
为了突出 Memory 设计本身,下面暂时不依赖数据库和向量服务,使用内存结构模拟。
生产环境可以替换为:
text
PostgreSQL
Redis
Qdrant / Milvus
Embedding Service
LLM
5.2 定义 Memory 数据结构
python
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
import uuid
@dataclass
class Memory:
user_id: str
category: str
key: str
value: str
confidence: float = 1.0
source: str = "explicit"
expires_at: Optional[datetime] = None
memory_id: str = field(
default_factory=lambda: str(uuid.uuid4())
)
created_at: datetime = field(
default_factory=datetime.now
)
updated_at: datetime = field(
default_factory=datetime.now
)
status: str = "active"
这里最值得关注的不是代码,而是:
text
key
value
confidence
expires_at
status
这几个字段。
它们决定了 Memory 是否可管理。
5.3 实现新增和冲突替换
python
class MemoryStore:
def __init__(self):
self.memories = []
def upsert(self, memory: Memory):
for old in self.memories:
if (
old.user_id == memory.user_id
and old.category == memory.category
and old.key == memory.key
and old.status == "active"
):
if old.value == memory.value:
old.confidence = min(
1.0,
max(old.confidence, memory.confidence)
)
old.updated_at = datetime.now()
return old
old.status = "superseded"
self.memories.append(memory)
return memory
def active_memories(self, user_id: str):
now = datetime.now()
return [
m for m in self.memories
if m.user_id == user_id
and m.status == "active"
and (
m.expires_at is None
or m.expires_at > now
)
]
例如:
python
store = MemoryStore()
store.upsert(
Memory(
user_id="u1001",
category="coding",
key="preferred_language",
value="Python",
confidence=0.95
)
)
store.upsert(
Memory(
user_id="u1001",
category="coding",
key="preferred_language",
value="Go",
confidence=0.99
)
)
for item in store.active_memories("u1001"):
print(item.key, item.value)
预期输出:
text
preferred_language Go
旧的 Python Memory 并没有物理删除,而是变成:
text
status = superseded
这样做方便审计,也方便未来分析:
用户偏好发生过怎样的变化?
5.4 Memory Extractor 应该让 LLM 输出结构化结果
实际项目里,我们通常会让 LLM 对用户消息执行 Memory Extraction。
例如用户输入:
text
以后涉及代码的例子尽量使用 Python,
但数据库相关内容我更习惯 PostgreSQL。
Extractor 输出:
json
[
{
"category": "coding",
"key": "preferred_language",
"value": "Python",
"confidence": 0.98,
"scope": "global"
},
{
"category": "database",
"key": "preferred_database",
"value": "PostgreSQL",
"confidence": 0.95,
"scope": "global"
}
]
Extractor Prompt 里建议明确告诉模型:
text
只提取对未来对话具有持续价值的信息。
不要保存:
1. 一次性任务参数;
2. 闲聊内容;
3. 当前环境中的临时信息;
4. 没有足够证据支持的推断;
5. 与未来个性化无关的信息。
需要区分:
- explicit_preference
- implicit_preference
- temporary_preference
- user_fact
- do_not_store
这一点非常重要。
如果只告诉模型:
"提取用户信息。"
模型往往会疯狂提取。
于是:
text
用户下午三点提交过一个 Bug。
用户今天使用了一台电脑。
用户刚才说了一句谢谢。
全部变成 Memory。
这就是典型的:
Memory Over-Extraction。
5.5 写入链路不要阻塞主回答
理想请求时序如下:
Memory Store Memory Extractor LLM Memory Retriever Agent User Memory Store Memory Extractor LLM Memory Retriever Agent User #mermaid-svg-VizjxZWTGxP9sR2L{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VizjxZWTGxP9sR2L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VizjxZWTGxP9sR2L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VizjxZWTGxP9sR2L .error-icon{fill:#552222;}#mermaid-svg-VizjxZWTGxP9sR2L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VizjxZWTGxP9sR2L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VizjxZWTGxP9sR2L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VizjxZWTGxP9sR2L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VizjxZWTGxP9sR2L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VizjxZWTGxP9sR2L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VizjxZWTGxP9sR2L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VizjxZWTGxP9sR2L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VizjxZWTGxP9sR2L .marker.cross{stroke:#333333;}#mermaid-svg-VizjxZWTGxP9sR2L svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VizjxZWTGxP9sR2L p{margin:0;}#mermaid-svg-VizjxZWTGxP9sR2L .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VizjxZWTGxP9sR2L text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-VizjxZWTGxP9sR2L .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VizjxZWTGxP9sR2L .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-VizjxZWTGxP9sR2L .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-VizjxZWTGxP9sR2L .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-VizjxZWTGxP9sR2L #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-VizjxZWTGxP9sR2L .sequenceNumber{fill:white;}#mermaid-svg-VizjxZWTGxP9sR2L #sequencenumber{fill:#333;}#mermaid-svg-VizjxZWTGxP9sR2L #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-VizjxZWTGxP9sR2L .messageText{fill:#333;stroke:none;}#mermaid-svg-VizjxZWTGxP9sR2L .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VizjxZWTGxP9sR2L .labelText,#mermaid-svg-VizjxZWTGxP9sR2L .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-VizjxZWTGxP9sR2L .loopText,#mermaid-svg-VizjxZWTGxP9sR2L .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-VizjxZWTGxP9sR2L .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VizjxZWTGxP9sR2L .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-VizjxZWTGxP9sR2L .noteText,#mermaid-svg-VizjxZWTGxP9sR2L .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-VizjxZWTGxP9sR2L .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VizjxZWTGxP9sR2L .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VizjxZWTGxP9sR2L .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VizjxZWTGxP9sR2L .actorPopupMenu{position:absolute;}#mermaid-svg-VizjxZWTGxP9sR2L .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-VizjxZWTGxP9sR2L .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VizjxZWTGxP9sR2L .actor-man circle,#mermaid-svg-VizjxZWTGxP9sR2L line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-VizjxZWTGxP9sR2L :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 发送请求查询相关偏好读取 Memory返回相关记忆Top-K MemoryQuery + Memory生成回答返回结果提交本轮对话提取候选 Memory新增/更新/过期
这里的核心思想是:
Memory 写入和用户主请求解耦。
因为用户真正关心的是:
Agent 什么时候回答我?
而不是:
Agent 什么时候把我的 Preference 写进数据库?
生产环境可以将 Memory Extraction 放入:
text
Queue
Event Bus
Worker
只要当前业务允许最终一致性即可。
不过也有例外。
比如用户明确说:
"记住:后面都用 TypeScript。"
如果下一轮请求很可能立即依赖它,就应该同步写入或直接更新 Session Memory。
所以比较合理的策略是:
text
显式高优先级偏好
→ 快速同步更新
普通隐式记忆
→ 异步提取
5.6 运行结果怎么验证
假设用户第一轮说:
text
以后代码示例优先用 Python,回答尽量简洁。
写入后:
text
coding.preferred_language = Python
response.style = concise
第二轮:
text
怎么实现单例模式?
Retriever 得到:
text
Python
concise
Agent 输出应该类似:
python
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
而不是输出 Java、C++、JavaScript 三种版本。
第三轮用户说:
text
这次详细讲,而且用 Java。
此时:
text
Current Instruction:
详细 + Java
Memory:
简洁 + Python
正确行为必须是:
text
详细 + Java
而长期 Memory 本身仍保持:
text
简洁 + Python
除非用户明确说:
"以后都改成 Java。"
这个验证场景非常适合加入自动化测试。
六、别只测"记没记住":Memory 系统应该怎么评估
6.1 Memory 系统至少有四个核心指标
传统 Agent 测试往往只看:
text
Answer Quality
但长期记忆系统必须额外测试。
指标一:Memory Precision
系统保存的 Memory 中,有多少是真的值得保存?
例如:
系统保存 100 条。
人工判断只有 82 条值得长期存在。
那么:
Precision = 82 / 100 = 82%
Precision 太低会导致:
垃圾记忆越来越多。
指标二:Memory Recall
应该记住的信息,有多少被成功保存?
例如人工构造 100 条明确用户偏好:
text
以后用 Python。
回答不要超过 500 字。
不要推荐收费工具。
......
系统只正确保存 90 条:
Recall = 90%
Recall 太低,用户体验就是:
"我明明说过,你怎么又忘了?"
指标三:Retrieval Precision
当前任务召回的 Memory,有多少真正相关?
假设:
text
Top-5 Memory
里面只有三条有帮助。
那么大量无关内容正在污染 Prompt。
指标四:Preference Adherence
最终生成结果是否遵守有效偏好。
例如:
text
Memory:
preferred_language = Python
测试 100 道代码问题。
其中 95 次按照 Python 生成。
那么:
text
Preference Adherence = 95%
这通常比"Memory 有没有成功从数据库查出来"更加重要。
因为:
检索正确但模型没采用,对用户而言等于没记住。
6.2 建议准备一套冲突测试集
至少覆盖这些场景:
| 历史偏好 | 当前输入 | 期望结果 |
|---|---|---|
| 喜欢 Python | "用 Java 写" | Java |
| 喜欢简洁 | "详细解释" | 详细 |
| 喜欢 PostgreSQL | 普通 SQL 问题 | 优先 PostgreSQL |
| 喜欢 Linux | "Windows 怎么配置" | 回答 Windows |
| 不推荐付费产品 | "推荐免费工具" | 只推荐免费 |
| 喜欢 Vue | "React Hooks 怎么用" | 正常回答 React |
你会发现一个规律:
好的记忆系统不是"尽可能使用 Memory",而是"在该使用的时候正确使用"。
Memory 应该是软约束。
当前用户明确指令通常是硬约束。
6.3 还要专门测试"错误记忆"
这是很多团队容易忽略的地方。
假设系统错误记录:
text
用户使用 Windows。
而实际上用户使用 macOS。
接下来系统可能不断给出:
text
C:\Users\...
PowerShell
.exe
一次 Memory 错误会污染几十轮对话。
这叫:
Error Amplification,错误放大。
因此需要测试:
text
错误 Memory 是否容易被用户纠正?
例如用户说:
"我不是 Windows,我一直用 macOS。"
理想行为:
text
识别纠正
→
旧 Memory 标记 superseded
→
新增 macOS
→
立即刷新相关缓存
而不是继续追问:
"你确定吗?"
对明确事实纠正,应该允许高效覆盖。
6.4 用户应该能够看见 Agent 记住了什么
如果条件允许,我非常建议产品提供:
text
Memory Management
例如:
text
关于你的记忆
开发语言
Python
回答风格
简洁
数据库偏好
PostgreSQL
内容展示
喜欢 Mermaid 图
每一项支持:
text
编辑
删除
关闭记忆
这不只是用户体验问题。
更重要的是:
Memory 越透明,用户越容易纠正错误。
黑盒记忆的问题在于:
用户发现 Agent 行为奇怪,却不知道:
到底是哪条历史 Memory 在偷偷影响结果。
七、踩坑总结:真正上线后最容易出问题的7个地方
7.1 坑一:什么都想记
这是出现频率最高的问题。
开发阶段会觉得:
数据以后可能有用,先保存再说。
最后得到:
text
5000 条 Memory
+
超长 Prompt
+
大量冲突
正确方向应该是:
Memory Sparse,而不是 Memory Massive。
真正高价值的长期偏好可能只有几十条。
一个用户使用 Agent 半年,并不意味着你应该保存半年所有对话细节。
7.2 坑二:把 LLM 推测当用户事实
用户说:
"最近天天在写 Spring Boot。"
模型提取:
text
用户是一名 Java 后端工程师。
这就是推断。
有可能正确。
也可能用户只是临时维护一个项目。
建议在数据层区分:
text
explicit
implicit
inferred
并给予不同 confidence。
例如:
text
explicit = 0.95
implicit = 0.60
inferred = 0.35
对低 confidence 信息,不一定要直接影响生成结果。
7.3 坑三:没有 Scope
比如用户说:
"这篇博客不要 Emoji。"
如果系统写成:
text
emoji_preference = false
以后所有回答都不用 Emoji,就记错了。
正确理解应该是:
text
scope = current_task
因此 Memory 至少可以设计以下 Scope:
text
current_task
session
project
global
如果 Agent 面向企业场景,还可以加入:
text
workspace
team
organization
例如:
text
个人项目默认 Python
公司项目默认 Java
这两条偏好完全可以同时成立。
只要 Scope 不同。
7.4 坑四:缓存更新不及时
数据库里已经更新:
text
Python → Go
Redis 还是:
text
Python
向量数据库还是:
text
用户偏爱 Python
User Profile Summary 还是:
text
主要使用 Python
然后整个系统出现一种非常诡异的情况:
后台显示已经修改成功,但 Agent 还是按旧偏好回答。
因此 Memory 更新需要设计统一事件:
text
MemoryUpdated
触发:
text
DB Update
↓
Cache Invalidation
↓
Vector Index Update
↓
Profile Regeneration
不要让四套存储各自更新。
7.5 坑五:没有区分"事实"和"偏好"
下面两句话区别很大:
text
我公司的数据库是 MySQL。
我更喜欢 PostgreSQL。
第一条是:
text
Fact
第二条是:
text
Preference
假设用户问:
"帮我写公司这个项目的 SQL。"
应该优先考虑:
text
MySQL
而不是:
text
PostgreSQL
所以推荐将 Memory 分类至少拆成:
text
FACT
PREFERENCE
CONSTRAINT
TEMPORARY_STATE
否则模型很容易错误使用。
7.6 坑六:忽视敏感信息
长期 Memory 会天然涉及隐私。
比如:
text
健康状态
家庭情况
财务信息
公司内部信息
账号信息
政治与宗教信息
身份信息
因此设计时不要只问:
能不能提取?
还需要问:
应不应该保存?
最简单的工程策略是增加:
text
sensitivity_level
例如:
text
public
personal
sensitive
restricted
不同级别对应不同策略:
| 等级 | 默认策略 |
|---|---|
| public | 正常保存 |
| personal | 允许长期保存 |
| sensitive | 谨慎保存或显式确认 |
| restricted | 默认不进入长期 Memory |
尤其不要把密码、Token、身份证号、银行卡号之类信息当"用户事实"永久保存。
这不是 Memory 能力。
这是安全事故。
7.7 坑七:把 Memory 当成绝对真理
我认为这是整个设计里最重要的一条。
Memory 的角色应该是:
text
Helpful Context
而不是:
text
Absolute Truth
任何长期记忆都可能:
- 过期;
- 被误提取;
- 只适用于特定项目;
- 被用户改变;
- 本身就是模型推断。
因此 Prompt 最好表达为:
text
这些是可能与当前请求相关的用户偏好。
仅在当前问题适用时参考,并始终优先遵循用户当前明确提出的要求。
不要写成:
text
下面所有用户信息都是真实且必须严格执行的。
7.8 进一步优化:从"记忆数据库"走向"记忆操作系统"
当系统规模逐渐变大,Memory 最终不会只是一个表。
而会形成一套完整组件:
text
Memory Extraction
Memory Classification
Memory Validation
Memory Storage
Memory Retrieval
Memory Ranking
Memory Conflict Resolution
Memory Consolidation
Memory Forgetting
Memory Audit
换句话说:
真正复杂的 Agent Memory,本质上已经接近一个小型数据操作系统。
未来很值得继续探索三个方向。
第一:Memory Consolidation
如果存在:
text
喜欢 Python
常用 FastAPI
经常使用 Pydantic
喜欢类型注解
系统可以在不删除原子 Memory 的前提下,生成高层摘要:
text
用户偏向现代 Python Web 开发技术栈。
用于快速召回。
第二:Episodic Memory
不仅记:
用户喜欢什么。
还可以记:
用户曾经如何解决某类问题。
例如:
text
上次 Docker 网络问题最终通过修改 compose network 解决。
下次出现类似问题时,Agent 可以主动提醒:
"你之前遇到过类似问题,当时是 Docker Compose 网络配置导致的。"
这会比单纯偏好记忆更接近真正意义上的长期协作助手。
第三:Preference Learning
未来的 Memory 不一定全部来自:
text
用户明确说出来。
系统还可以观察:
text
用户频繁接受什么方案?
经常修改什么回答?
哪些生成内容被用户删除?
更常选择哪种代码?
什么时候要求展开?
什么时候嫌回答啰嗦?
逐步学习用户偏好。
但这里一定要坚持一个底线:
推测出来的偏好,置信度永远应该低于用户明确表达的偏好。
否则 Agent 很容易从:
"越来越懂你"
变成:
"越来越自作主张"。
总结:好的 Agent 记忆不是"记得多",而是"记得对"
设计 Agent 用户偏好记忆时,最容易陷入的误区是把问题理解成:
"怎么把历史聊天长期保存下来?"
真正的问题其实是:
哪些信息值得影响未来的 Agent,以及这种影响应该持续多久、在什么情况下生效。
本文从工程角度完整走了一遍偏好记忆的核心链路:
text
用户输入→Memory 提取→分类→置信度判断→结构化存储→冲突处理→相关性召回→Prompt 注入→效果验证→更新与遗忘
如果只记住四句话,我建议记住这四条:
- 聊天记录不等于长期记忆,Memory 必须经过提炼。
- 当前用户指令的优先级,永远高于历史偏好。
- 好的 Memory 系统必须既会记,也会改、会忘。
- 用户记忆追求的不是数量,而是相关性、准确性和可控性。
一个真正好用的 Agent,不应该让用户不断重复:
"我之前已经说过了。"
也不应该因为记住了一次历史信息,就永远固执地认为:
"你就是这样的人。"
更合理的状态是:
该记住的时候记住,该忽略的时候忽略;用户改变时能够更新,信息过期时能够遗忘。
做到这一步,Agent 才真正从一个"每次从零开始回答问题的大模型",逐渐变成一个能够长期协作的智能系统。