第 3 篇:「提取大脑」------ ADDITIVE_EXTRACTION_PROMPT 深度解剖
系列 :Mem0 深度分析(02 篇走完 add 八阶段管线,本篇解剖 Phase 2 那次 LLM 调用的"大脑")
对应官方文档 :Memory Operations - Add、Custom Instructions
核心源码 :
mem0/configs/prompts.py:468-944(ADDITIVE_EXTRACTION_PROMPT)、mem0/configs/prompts.py:947-1063(组装层)、mem0/memory/main.py:940-953(OSS 调用点)阅读本文你将了解: 这个 1062 行文件里提示词的七段式结构;九个输入段与 OSS 实际传参的落差(有两个输入永远是空的);"双时间锚"机制在 OSS 里的一个真实缺陷;质量标准七原则与六条 Integrity Rules 的精华;以及两个"设计意图 vs 管线现实"的错位------LLM 被要求输出的记忆级链接,在开源版管线里被整个丢弃。
1. 宏观结构:一个提示词就是一份产品需求文档
ADDITIVE_EXTRACTION_PROMPT(mem0/configs/prompts.py:468-944,约 480 行、五千余 token)不是一段"提示语",而是一份结构化规格书。七段构成:
| 段落 | 行号 | 作用 |
|---|---|---|
# ROLE |
468-494 | 定位"Memory Extractor",明确 ADD-only 单一职责 |
# INPUTS |
478-549 | 九个输入段的语义契约(见 §2) |
# GUIDELINES |
551-701 | 提取范围 + 质量标准七原则 + Integrity 六规 + 链接规则 |
# EXAMPLES |
704-904 | 12 个 few-shot 示例(见 §7) |
# CRITICAL: Exhaustive Extraction Checklist |
907-915 | 输出前的自检清单 |
# OUTPUT FORMAT |
918-942 | JSON schema 与字段定义 |
| (组装层) | 947-1063 | AGENT_CONTEXT_SUFFIX + generate_additive_extraction_prompt |
值得注意的开头设计:mem0/configs/prompts.py:470-476 把"你只有 ADD 一个操作"写进角色定义------"Your sole operation is ADD"。这是给 LLM 划定决策空间:不需要判断"该不该更新旧记忆",只需要"把值得记的都吐出来"。整个提示词的成本预算都花在查全率上,查准率交给下游去重与检索排序(02 篇 §5 成本账的提示词侧证据)。
2. 九个输入段 vs OSS 实际传参:两个永远为空的输入
提示词 # INPUTS 段声明了九个输入(mem0/configs/prompts.py:478-549):New Messages、Summary、Recently Extracted Memories、Existing Memories、Last k Messages、Observation Date、Current Date、以及两个 Optional(includes/excludes、custom_instructions/feedback_str)。组装函数 generate_additive_extraction_prompt(mem0/configs/prompts.py:1016-1062)能组装全部九段。但看 OSS 管线的实际调用(mem0/memory/main.py:948-953):
python
user_prompt = generate_additive_extraction_prompt(
existing_memories=existing_memories, # Phase 1 召回的 top-10
new_messages=parsed_messages, # 当前对话
last_k_messages=last_messages, # SQLite 回捞的最近 10 条
custom_instructions=custom_instr, # MemoryConfig.custom_instructions 或 add(prompt=...)
)
四个参数之外的输入全部缺省 。逐一对照默认值(mem0/configs/prompts.py:1033-1045):
| 输入段 | OSS 实际值 | 后果 |
|---|---|---|
| Summary | 空字符串(_format_summary(None) → "") |
提示词说"用它丰富提取",OSS 里这段恒空 |
| Recently Extracted Memories | "[]" |
提示词称其为"主去重参考"(mem0/configs/prompts.py:503),OSS 里这份参考不存在 |
| Observation Date | = Current Date = 今天(_resolve_dates,mem0/configs/prompts.py:1007-1013) |
见 §3,历史对话重放时错锚 |
| includes / excludes / feedback_str | 缺失 | 平台版功能,OSS 只有 custom_instructions 一条通道 |
这个落差的工程含义:OSS 的去重实际只有两道防线 ------MD5 精确去重(02 篇 Phase 4/5)和提示词里"Existing Memories 只用于去重"的约束(mem0/configs/prompts.py:511)。提示词设计里最强的一道语义去重参考(Recently Extracted)在开源版是空转的。同一轮对话里换措辞重复提及的事实在 OSS 里更容易产生重复入库。
3. 双时间锚:设计精巧,OSS 缺时间源
提示词对时间的处理是全文最精细的部分(mem0/configs/prompts.py:524-540):Observation Date (对话发生的日期)是解析相对时间引用的唯一锚点,Current Date (今天的系统日期)只做背景、绝不允许用于解析用户的话。理由写在提示词里:"User went to Paris last week" 六个月后无用,"User went to Paris the week of May 15, 2023" 永久有用(mem0/configs/prompts.py:535)。Example 7(mem0/configs/prompts.py:788-800)专门演示:2022-01-16 说 "recently",在 2026-02-18 回看时必须锚到 2022 年 1 月。
设计意图成立的前提是调用方能提供真实的 Observation Date 。OSS 的现实藏在组装函数的日期兜底逻辑里(mem0/configs/prompts.py:1007-1013):
python
def _resolve_dates(current_date=None, observation_date=None):
"""Resolve current and observation dates, defaulting to today."""
if current_date is None:
current_date = datetime.now(timezone.utc).date().isoformat()
if observation_date is None:
observation_date = current_date
return current_date, observation_date
而 OSS 调用点(mem0/memory/main.py:948-953)没有传 current_date/timestamp 中的任何一个------两个日期双双落到 datetime.now()。也就是说:你重放三个月前的对话记录(比如批量导入历史会话),"last week" 会被锚到今天 往前推一周------错误恰好是这个机制设计出来要防止的。平台版通过 timestamp 参数传真实观察时间;OSS 的 add() 见到 timestamp 参数直接抛错(02 篇 §1,mem0/memory/main.py:817-818)。开源版用户规避方案:重放历史对话时,把绝对日期写进消息文本本身("on 2026-03-01 I ..."),别依赖相对时间词。
4. 质量标准七原则:值得抄走的四条
# GUIDELINES 段的质量标准(mem0/configs/prompts.py:605-674)共七条:Contextually Rich / Clean Factual / Self-Contained / 15-80 words / Temporally Grounded / Numerically Precise / Preserve Specific Details。其中四条有超出记忆系统的一般价值:
(a)转变必须连新旧状态一起记 (mem0/configs/prompts.py:612-620)。三组 Bad/Good 对比里最好的一组:"Bad: User prefers oat milk lattes / Good: User switched from almond milk to oat milk lattes after developing an almond sensitivity"。孤立的新事实没有信息量,变化轨迹才是记忆系统的核心资产------这不只是提示词技巧,是对"记忆"这个数据模型的定义。
(b)语义保真的三个反例 (mem0/configs/prompts.py:668-674):"Didn't get to bed until 2 AM" ≠ "slept until 2 AM";"Can't stop eating chocolate" ≠ "stopped eating chocolate";"I used to love hiking" = 已经不爱了。三个例子全是否定/时态陷阱,提示词作者显然被这类错误伤过。结尾一句值得刻在所有数据管线里:"Misinterpreting the user's words is worse than not extracting at all."
(c)专名是最高价值资产 (mem0/configs/prompts.py:646-652):"Book titles, movie titles... are the HIGHEST-VALUE details in a memory. Users search by name --- a memory without the name is unfindable." 这条把提示词决策和下游检索机制(04 篇的 BM25 关键词路)连起来了:专名正是 keyword_search 命中的主力词元,丢专名等于丢检索入口。
(d)Never Generalize (mem0/configs/prompts.py:654-666):"promoted to assistant manager" 不许写成 "manager","aerial yoga" 不许写成 "yoga"。每条都配 Bad→Good 对比------few-shot 密度在这里比规则陈述更有效。
5. 六条 Integrity Rules:防幻觉的提示词侧防线
mem0/configs/prompts.py:677-689 的六条完整性规则,按防线价值排序:
- No Fabrication:"If you can't point to where it came from, don't include it"------证据锚定要求,和 02 篇 Phase 1 的 UUID→序号映射同属反幻觉体系;
- No Echo Extraction (
mem0/configs/prompts.py:682):助手复述用户的话不重复提取------配了完整正反例(用户说 7:30 每日提醒 + 助手说"已设置",只从用户消息提一次); - No Within-Response Duplication (
mem0/configs/prompts.py:683):批内语义去重要求------"keep the richer one and drop the other",这条补偿了 §2 所说 Recently Extracted 为空的部分损失,但只覆盖单次调用内; - No Meta-Extraction (
mem0/configs/prompts.py:684-688):用户分享文档时提取文档内容而非"用户分享了文档"这个动作------D&D stat block 的正反例非常具体("4 Mummies (AC 11, 45 HP)..." vs "Assistant created a D&D adventure"); - No Implicit Attribute Inference:禁止从名字/语境推断性别年龄族裔------合规要求进提示词;
- No Detail Contamination (
mem0/configs/prompts.py:689):新消息说 "I had a great meal",已有记忆说 "favorite restaurant is Olive Garden",禁止合成出 "great meal at Olive Garden"------防止跨源事实融合产生假记忆,这条在 RAG 场景极易踩坑。
5.1 第七条规则的量化窗口:15-80 词的取舍
mem0/configs/prompts.py:631-632 给了这条标准少见的量化约束:"1-2 sentences per memory (up to 3 for content with multiple proper nouns...)... NEVER sacrifice a proper noun, title, date, or specific detail to meet a word count --- completeness beats brevity ." 这段话其实是在调停两个互相冲突的目标:窗口上限(15-80 词)保证单条记忆可注入 prompt 不膨胀,"拆分优先于压缩"("split into multiple focused memories rather than compressing details away")保证信息不丢失。配合 mem0/configs/prompts.py:628-629 的 Self-Contained(代词必须还原成专名或 "User"),三条一起构成了"每条记忆独立可用"的完整定义------这也是为什么 Mem0 的记忆可以不经改写直接拼进 system prompt(官方 quickstart 的标准用法)。
6. 输出 Schema 与两个"设计意图 vs 管线现实"的错位
输出格式(mem0/configs/prompts.py:918-942):
json
{
"memory": [
{"id": "0", "text": "...", "attributed_to": "user", "linked_memory_ids": ["uuid-of-related"]},
{"id": "1", "text": "...", "attributed_to": "assistant"}
]
}
四个字段在管线里的命运并不平等:
attributed_to 被消费 :mem0/memory/main.py:1036-1037 读出后写入记忆 payload,search 返回时还会提升到结果顶层(mem0/memory/main.py:1690-1699 的 promoted keys)。
linked_memory_ids 被丢弃 。提示词用整整一节定义链接规则(mem0/configs/prompts.py:692-701:同实体/偏好更新/叙事延续/矛盾四种链接时机),Example 10 和 12 专门演示,要求"Use the exact IDs from the Existing Memories list"。但全文件搜索证明:linked_memory_ids 在 mem0/memory/main.py 中的全部出现(624/626/641/655/674/693/1157/1175/1797 行)都属于实体库的实体↔记忆链接 (05 篇),没有任何一行读取 LLM 输出里的记忆↔记忆链接。管线 Phase 4/5 构建 records 时只取了 text 和 attributed_to(mem0/memory/main.py:1016-1039)。换句话说:LLM 每次调用都在费 token 产出记忆级链接,OSS 管线把它们全部扔掉了 。这不是小瑕疵------提示词中"graph of related memories"的能力承诺(mem0/configs/prompts.py:858)在开源版没有兑现,v1 的图记忆能力在 v3 OSS 里实际处于半启用状态(实体图活着,记忆图没有)。
uuid_mapping 是只写变量 。02 篇 §4 提到的防幻觉映射(mem0/memory/main.py:933-938),构建后(sync 935/937、async 2597/2599 两处)没有任何回读点------LLM 返回的序号 id 从未被映射回真实 UUID。当前它无害(因为 linked_memory_ids 也没被消费),但若未来管线开始消费链接,这将是第一个 bug 源头:LLM 按 Example 10 的示范输出的是 Existing Memories 里的 id------而 OSS 实际传给 LLM 的 Existing Memories id 已经被换成序号(mem0/memory/main.py:938),提示词示例(真实 UUID)与运行时输入("0","1")不一致,LLM 可能输出它自创的 UUID。提示词静态文本与动态组装内容不一致,是提示词工程里最难在 code review 里发现的一类 bug,这里就是现成案例。
7. 12 个 few-shot 示例的覆盖矩阵
# EXAMPLES 段(mem0/configs/prompts.py:704-904)用 12 个示例覆盖提取决策空间的每个象限------这本身是一份"什么值得记"的分类学:
| 示例 | 教什么 | 对应章节 |
|---|---|---|
| 1 | 多主题拆分(晋升/餐厅/宝宝三件事三条记忆) | 多维度提取 |
| 2 | 助手推荐也算记忆 | INPUTS 段的 assistant 内容 |
| 3 | 纯寒暄输出空数组 | 空提取示范 |
| 5 | 已捕获内容跳过 | 去重 |
| 6 | 别被首话题吸走(职业/观影/资源三维) | 次要信息 |
| 7 | 相对时间锚到 Observation Date | 双时间锚 |
| 8 | 文档内容 vs 分享动作 | No Meta-Extraction |
| 9 | 结构化数值全保留(D&D stat block) | Numerically Precise |
| 10 | linked_memory_ids 链接 | Memory Linking |
| 11 | 5 条消息 5 条记忆 | 穷举清单 |
| 12 | 多说话人场景(Maria 的猫) | 群聊归属 |
| (4 缺号) | --- | 编号从 3 跳到 5,疑为删改残留 |
两个编排特征:其一,负例密度高 ------3、5 两个"什么都不提取"的示例压制 LLM 的过度提取倾向,与 checklist 段"提取不足重读对话"的强制(mem0/configs/prompts.py:912:"If you have fewer than 3, re-read the conversation")形成张力,一个压虚一个防漏;其二,示例 12 明确了群聊语义------"assistant" 角色里冒号的具名发言人("Maria: ...")的个人事实必须以真名归属提取,这是 Group Chat 场景的提示词级支持(平台版另有专门 feature 页)。
7.5 组装层的出处注释:开源与平台同源的直接证据
mem0/configs/prompts.py:960-963 有一段源码注释值得单独引用:# V3 Prompt Builder --- constructs the user-side prompt for additive extraction / # Ported from platform/backend/shared/core/utils/prompt_builder.py。这是开源版提取管线与 Mem0 托管平台后端同源 的书面证据------OSS 的 generate_additive_extraction_prompt 是从平台后端的 prompt_builder 移植来的。同时它也解释了 §2 的落差从何而来:平台版能传 summary / recently_extracted / 真实 observation date,是因为平台后端有自己的会话摘要与去重缓存组件;OSS 移植了提示词与组装函数,但没有移植(也没有开源)那两个上游组件。读开源仓库时看到 "Ported from platform/..." 类注释,基本可以直接推断"提示词同源、基础设施不同源"------这是评估开源版与托管版能力差异时比文档更可靠的一手材料。
同层还有一个 OSS 未启用的能力开关:use_input_language(mem0/configs/prompts.py:1026,调用点未传)。传 True 时组装函数会追加一整段语言强制要求(mem0/configs/prompts.py:1047-1059):输入什么语言就用什么语言提取、保留原文文字系统("if they write in Korean, extract in Korean")、专有名词保留原形,甚至包括日语省略主语的补全规则和 CJK 语域(formality)保持规则。OSS 用户提取非英文对话时,中文事实会被默认翻成英文记忆(提示词模型倾向英文输出),想保持中文记忆就自己在这个调用点传 use_input_language=True------一行改动,免维护自己那套语言约束。
8. 注入点:AGENT_CONTEXT_SUFFIX 与 custom_instructions
系统提示词有两个动态拼接点(02 篇 §4 Phase 2 已给调用处)。AGENT_CONTEXT_SUFFIX(mem0/configs/prompts.py:947-957)在纯 agent 作用域时追加,核心是把归属框架翻转:"Agent was informed that fact" / "Agent recommended X",同时明确 attributed_to 仍按原始来源标 user/assistant------视角翻转但证据归属不翻转。
custom_instructions 的注入位置在用户提示词尾部(mem0/configs/prompts.py:1044-1045),提示词正文给它最高的优先级声明:"custom_instructions: User-defined rules (highest priority )"(mem0/configs/prompts.py:547)。官方文档 custom-instructions 页建议写领域过滤规则(如"只记录与交易相关的信息")。工程提醒:custom_instructions 拼进的是 user prompt 而非 system prompt,用户消息内容与指令在同一段文本里,注入攻击面比 system prompt 注入略大------自托管多租户场景慎用终端用户可控的自定义指令。
9. 工程启示:这 5000 token 花得值吗
粗算成本:提示词骨架约 5k token + 动态段(top-10 旧记忆 + 最近 10 条消息 + 新消息)典型 2-6k,每次 add 一次调用,gpt-5-mini 下约 $0.003-0.01。对比收益:省掉 v1 的第二段决策调用(同样不便宜)+ 换取固定成本可预算。两个优化方向:
- 裁剪:12 个示例约占提示词四成篇幅,self-hosted 场景若提取模型够强(gpt-4o 级以上),砍到 4-5 个示例(3/5/7/8/10)可省约 2k token/次;但本地小模型不要砍------few-shot 是小模型格式遵循的主要支撑。
- 别只盯着提示词 :03 篇的三个落差(Summary 空、Recently Extracted 空、Observation Date = 今天)意味着 OSS 管线没有吃满提示词设计。有工程能力的团队可以在
_add_to_vector_store调用点自行传入这三个参数(generate_additive_extraction_prompt的签名本来就支持,mem0/configs/prompts.py:1016-1027),把平台级的提取质量拉回开源版------这比调提示词文本本身的 ROI 高得多。
9.5 评测视角:怎么给提取质量建立回归基准
改提示词或换提取模型前,建议先建一个最小评测集------不需要官方的评测框架(core-concepts/memory-evaluation 页描述的是平台端能力),三步就能在 OSS 上自建:
- 造 fixture 对话:把 §7 覆盖矩阵的每个象限各写 2-3 条对话(多主题/寒暄/文档分享/相对时间/多说话人),人工标注期望提取集合------注意标注的是"事实集合"而非"原文",因为提示词允许改写,逐字比对必然误报;
- 语义级断言 :对每条期望事实,用
m.search()或直接对提取结果做嵌入相似度匹配(阈值 0.85 起调),统计查全率/查准率;特别关注两个提示词明确要求的点------数值保真("416 pages" 不许变 "about 400")与空提取纪律(寒暄对话必须返回空数组); - 回归对比:换模型/改提示词/升级 mem0 版本前后各跑一遍,diff 两个指标。官方 memory-evaluation 页提到的 LoCoMo 类长对话基准可以后置------fixture 集小而准,更适合持续集成。
这套基准也能量化 §2/§3 的两个 OSS 落差的实际影响:补传 summary 与真实 observation date 前后各跑一遍,数据会告诉你这两个参数对你的对话分布值不值得补。
10. 验证检查点
- 打印
m.add()期间发出的 user prompt(在 LLM 适配层加日志),确认 Summary 段为空、Recently Extracted 为[](mem0/memory/main.py:948-953只传了四个参数) - 重放含 "last week" 的历史对话,验证提取结果把相对时间锚到了今天而非对话日期(
mem0/configs/prompts.py:1033) - 构造"旧记忆里已有同实体记忆"的对话,检查返回记忆的 payload------
attributed_to在,linked_memory_ids不在(mem0/memory/main.py:1016-1039无消费点) - 纯
agent_id作用域 add,观察 system prompt 是否追加了 Entity Context 段(mem0/configs/prompts.py:947-957) -
custom_instructions="只记录饮食偏好"后,非饮食事实被抑制
下一篇 :04 篇转向读取端------
search()的三信号混合检索:语义过采样、BM25 sigmoid 归一化、实体 boost 的衰减公式,以及自适应分母的加法评分。