Agent用户偏好记忆设计:从原理到实战

摘要: 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 注入→效果验证→更新与遗忘

如果只记住四句话,我建议记住这四条:

  1. 聊天记录不等于长期记忆,Memory 必须经过提炼。
  2. 当前用户指令的优先级,永远高于历史偏好。
  3. 好的 Memory 系统必须既会记,也会改、会忘。
  4. 用户记忆追求的不是数量,而是相关性、准确性和可控性。

一个真正好用的 Agent,不应该让用户不断重复:

"我之前已经说过了。"

也不应该因为记住了一次历史信息,就永远固执地认为:

"你就是这样的人。"

更合理的状态是:

该记住的时候记住,该忽略的时候忽略;用户改变时能够更新,信息过期时能够遗忘。

做到这一步,Agent 才真正从一个"每次从零开始回答问题的大模型",逐渐变成一个能够长期协作的智能系统。

相关推荐
云原生指北1 小时前
Docker Sandboxes 的隔离例外:共享 Skills 与宿主机 MCP
运维·docker·ai·容器·agent
炮哥聊AI1 小时前
会调工具的 Agent 只值一半:真正的业务智能体,得会"记仇"
人工智能·agent
Erishen2 小时前
🤖 用 AutoGen 搭 PSE 三角色闭环:可重试、可追溯、可审计的多智能体框架
架构·开源·agent
vivo互联网技术2 小时前
协同文档下的 Agent 协作闭环:可回滚、可对比的透明化编辑实现
人工智能·笔记·agent
Vuji3 小时前
Pi 插件解剖|ssh.ts:只用 221 行,让 Agent 直接在远程机器干活
前端·人工智能·agent
dong_junshuai3 小时前
每天一个开源项目#73 Munder Difflin:2.3K Star 的本地多Agent办公室
开源·github·agent
leeyi3 小时前
Langfuse 集成源码:batch 协议、media 上传与 mock 测试(第89篇-E75)
llm·aigc·agent
贵慜_Derek3 小时前
DeepSeek Harness 多 Agent 解读:subagent、workflow、jobs 各管什么
人工智能·agent·deepseek
修远客3 小时前
风格进化:让Agent越来越懂你 — 从"工具"到"助手"的关键跃迁
llm·agent