Mem0 OpenClaw 插件召回协议:如何正确使用召回记忆与重写 memory_search 查询
在 Mem0 的 OpenClaw 插件(@mem0/openclaw-mem0)中,skills 模式通过 triage、recall、dream 三个技能让 Agent 自主管理记忆。integrations/openclaw/skills/memory-triage/recall-protocol.md 定义的是其中的“召回协议”(memory-recall):Agent 每轮对话前会看到一个由插件注入的 <recalled-memories> 区段,本协议规定了如何自然地使用这些记忆、何时主动发起 memory_search、以及如何把用户的口语化消息重写成 3~6 个关键词的检索查询(含 filters 结构过滤)。读完本篇,你可以完整掌握该协议的查询重写四步法、失败模式规避清单、filters 语法,并对照插件源码理解 <recalled-memories> 区段是如何生成与注入的。
协议文档的定位与 frontmatter
该协议文档位于 recall-protocol.md,其 YAML frontmatter 声明如下:
---
name: memory-recall
description: Protocol for searching and using recalled memories. Defines query rewriting for retrieval.
applies_to: memory-triage
---
从 skill-loader.ts 的加载逻辑看,插件读取 skills/<skill>/SKILL.md 主文件、以及 skills/<skill>/domains/<domain>.md 领域叠加层(通过 applies_to 字段校验归属技能),而 recall-protocol.md 与 triage 主协议 SKILL.md 同处 memory-triage 目录,属于召回侧的配套协议。从源码结构看,这份文档中的核心检索规则(四步重写、WRONG/RIGHT 示例、filters 运算符列表)与 loadTriagePrompt() 在运行时动态拼入系统提示的 “Searching Memory” 段落内容高度一致——例如 skill-loader.ts 中硬编码的:
"When calling memory_search, ALWAYS rewrite the query. NEVER pass the user's raw message."
"WRONG: memory_search(\"Who was that nutritionist my wife recommended?\")"
"RIGHT: memory_search(\"nutritionist wife recommended relationship\")"
这正是协议文档 Example 1 的原文,说明文档是协议规范、源码是它的落地注入点。
召回记忆如何到达 Agent 上下文
协议文档开篇说明:“Below your instructions you will find a <recalled-memories> section containing stored facts about this user. These memories persist across sessions and channels.” 这句话对应的实现链条在源码中清晰可查:
-
生成区段:recall.ts 的
formatRecalledMemories()把筛选后的记忆按类别分组,输出形如:<recalled-memories> Stored memories for "alice" (3 total, ranked by importance): Identity: - User is Chris, senior platform engineer at Mem0 [identity] (95%) </recalled-memories>无结果时输出
No stored memories found for "<userId>".的占位区段。 -
注入上下文:index.ts 在 skills 模式的
before_prompt_build钩子中执行skillRecall(...),并把结果放入prependContext(注释明确写着 “Dynamic recall goes in prependContext (changes every turn)”),即每轮动态变化、与静态系统提示分离。 -
降级策略:检索失败只打
warn日志不阻塞 Agent(recall.ts 中的 try/catch),协议文档因此可以假定“区段存在、内容可能为空”。
使用召回记忆的四条行为准则
协议文档 “Acting on Recalled Memories” 一节给出四条必须遵循的准则,原文要点完整如下:
- 自然个性化,不暴露机制:知道用户名字就用名字,知道偏好就遵守,但绝不能说 “I remember that you...” 或 “According to my memory...”。直接使用信息,不引起对机制的注意。
- 身份类记忆是 ground truth:名字、角色、时区、系统配置等身份信息默认可信,除非用户明确纠正。这与 skill-loader.ts 中
identity类别的默认配置呼应——identity是唯一带immutable: true且importance: 0.95的类别。 - 规则类记忆具有强制性:若召回记忆写着 “User rule: never do X”,必须遵守,规则覆盖 Agent 的默认行为(
rule类别默认 importance 为 0.9)。 - 检查时间锚点:项目与操作类记忆带有时间锚(“As of ...”)。若记忆看起来过时,先验证再依赖。这与 triage 协议中 “TEMPORAL ANCHORING” 的写入要求(“As of YYYY-MM-DD, ...”)形成读写闭环。
基于记忆给出建议前先验证
协议文档特别强调一个认知边界:
A memory is a claim about what was true when it was written. It may no longer be true. "The memory says X" is not the same as "X is true now."
因此基于记忆给出建议前需按类型验证:
- 记忆提到某个工具、服务或配置:确认它当前仍在使用;
- 记忆提到某个偏好:偏好可能已演化,把它当作默认值而非绝对结论;
- 用户即将依据你的建议采取行动:先验证记忆本身。
这条准则与 triage 侧的 “Updating Existing Memories”(材料性差异才更新、临时约束不删除原偏好)共同构成记忆的时效性管理策略。
何时搜索、何时不搜索
memory_search 是 Agent 侧的主动检索工具(实现见 memory-search.ts)。协议文档给出明确的触发边界:
应当使用 memory_search 的场景:
- 用户引用了召回记忆未覆盖的内容;
- 对话主题切换到新领域;
- 用户问 “do you remember...”、“what was...” 或引用过去的对话;
- 需要更新某条记忆前,先找到已有版本。
不应搜索的场景:
- 召回记忆已覆盖该主题(“Do not re-search for what is in front of you”);
- 当前轮次没有任何与记忆相关的内容(“Most turns do not need a search”);
- 查询太泛,不可能返回有用结果。
从 types.ts 的 SkillsConfig.recall.strategy 定义看,该协议与 manual 策略的语义完全对应——“zero plugin searches, agent controls all search”;而默认的 smart 策略(每轮 1 次长期记忆自动检索)和 always 策略(长期 + 会话双检索)下,手动搜索只在自动注入上下文不足时补充使用。loadTriagePrompt() 正是按 strategy 值输出不同的引导语(见 skill-loader.ts)。
查询重写:为什么以及四步流程
为什么必须重写
协议文档 “Why Rewriting Matters” 解释了检索引擎的工作方式:向量相似度 + 关键词重叠。而存储中的记忆是第三人称事实陈述,例如 “User is a data scientist based in Berlin” 或 “User decided to adopt weekly sprint reviews because biweekly was too slow”。用户的口语消息则充满 “can you”、“I was wondering”、“help me” 这类噪声词,稀释信号且在记忆库中匹配不到任何东西——所以禁止把用户原始消息直接当查询。
四步流程(每次调用必须完整执行)
- Step 1. 命名目标(Name your target):写查询前先识别期望命中的记忆类别,防止无目的检索。
- Step 2. 提取信号词(Extract signal words):从用户消息中抽出所有专有名词、技术术语、领域概念和具体细节;丢弃对话框架、疑问句、代词和填充词。
- Step 3. 桥接到存储语言(Bridge to storage language):想象这条记忆被存储时的写法——记忆是第三人称事实句,包含 “User”、“configured”、“decided”、“prefers”、“rule”、“team”、“project”、“based in”、“works at” 这类词。有助于命中时可追加类别词:identity、decision、rule、preference、configuration、relationship。
- Step 4. 组合关键词查询(Compose a keyword query):把 Step 2 和 Step 3 的词条拼成 3~6 个关键词的字符串。不要问号、不要代词、不要句子结构。查询读起来应像索引词条,而非自然语言。
七个完整示例(覆盖不同领域,防止锚定)
协议文档刻意让示例横跨不同领域,完整继承如下:
Example 1:查找一个人(relationship/reference)
User: "Who was that nutritionist my wife recommended?"
Step 1: Target = a relationship or reference memory about a nutritionist
Step 2: Signal = nutritionist, wife, recommended
Step 3: Bridge = stored memory likely contains the name, "nutritionist", "wife recommended", "relationship"
Step 4: memory_search("nutritionist wife recommended relationship")
Example 2:查找偏好(preference)
User: "How do I like my reports formatted again?"
Step 1: Target = a preference about report formatting
Step 2: Signal = reports, formatted
Step 3: Bridge = stored memory likely says "User prefers", "reports", "format", a specific style
Step 4: memory_search("report format preference style")
Example 3:查找技术决策(decision)
User: "Remind me why we picked that message queue"
Step 1: Target = a decision memory about message queue technology
Step 2: Signal = message queue, picked, why
Step 3: Bridge = stored memory likely says "decided", "chose", the queue name, "because", a rationale
Step 4: memory_search("message queue decision chose rationale")
Example 4:查找身份信息(identity)
User: "What timezone am I in?"
Step 1: Target = identity memory with timezone
Step 2: Signal = timezone
Step 3: Bridge = stored memory likely says "User is based in", a city, a timezone abbreviation
Step 4: memory_search("user timezone location based")
Example 5:查找规则(rule)
User: "Is there anything I told you to always do before deploying?"
Step 1: Target = a rule memory about deployment
Step 2: Signal = deploy, always do, before
Step 3: Bridge = stored memory likely says "User rule:", "always", "before deploying", a specific action
Step 4: memory_search("rule deploy always before")
Example 6:查找项目状态(project)
User: "Where are we with the onboarding redesign?"
Step 1: Target = a project memory about onboarding
Step 2: Signal = onboarding, redesign
Step 3: Bridge = stored memory likely says "As of", "onboarding", "redesign", "status", a milestone
Step 4: memory_search("onboarding redesign project status")
Example 7:查找生活事件(relationship/life event)
User: "When's my sister's birthday?"
Step 1: Target = a relationship or life event memory about the user's sister
Step 2: Signal = sister, birthday
Step 3: Bridge = stored memory likely contains "sister", a name, "birthday", a date
Step 4: memory_search("sister birthday date relationship")
失败模式清单
协议文档 “Failure Patterns” 给出了 7 种产生劣质结果的查询模式及修正方法,建议逐条对照自查:
| 模式 | 失败原因 | 修正 |
|---|---|---|
| 直接拿用户原始消息当查询 | 噪声词("can you"、"help me")稀释信号 | 只抽取实体与概念 |
| 查询含疑问词 | "what"、"how"、"when"、"who" 不出现在存储记忆中 | 去掉全部疑问框架 |
| 查询含代词 | "we"、"our"、"my"、"I" 不出现在第三人称记忆中 | 用 "user" 或实体名 |
| 单个关键词 | 太窄,漏掉相关上下文 | 使用 3~6 个词 |
| 超过 8 个关键词 | 太宽,所有结果排名趋同 | 收敛到最强的 4~5 个词 |
| 只有模糊类别词 | "user information stuff" 能匹配一切 | 至少包含一个具体实体或概念 |
| 重复同一搜索 | 一次没搜到,换措辞重搜大概率也没结果 | 换角度,或接受该记忆不存在 |
最后一条尤其值得注意:它定义了检索的停止条件,避免 Agent 陷入无意义的重试循环。
Filters:查询管语义,过滤器管结构
协议文档 “Constructing Filters” 一节规定:filters 参数按时间、类别或元数据收窄结果。query 负责语义相关性,filters 负责结构化约束,两者配合使用。
何时添加过滤器
当用户意图隐含了语义相似度之外的结构约束时:
- 时间指称("last week"、"recently"、"in January"、"yesterday"):加
created_at过滤器,使用 gte/lte 日期; - 类别请求("my preferences"、"any rules"、"what decisions"):加
categories过滤器; - 近期偏好("latest"、"most recent"、"current"):
created_at配近期日期; - 消息中既无时间也无类别信号:不要加过滤器,让查询独立完成。
过滤器语法
- 运算符:
eq、ne、gt、gte、lt、lte、in、contains、icontains - 逻辑组合:
AND、OR、NOT(条件用数组包裹) - 日期格式:
YYYY-MM-DD
(skill-loader.ts 注入的运行时提示中列出的运算符为 eq, ne, gt, gte, lt, lte, in, contains,逻辑符 AND, OR, NOT,与文档一致。)
六个带过滤器的完整示例
User: "What did we decide last week about the migration?"
Query: "decision migration chose rationale"
Filter: created_at >= 7 days ago
Call: memory_search("decision migration chose rationale", filters: {"created_at": {"gte": "2026-03-25"}})
User: "What are all my standing rules?"
Query: "user rule always never"
Filter: category = rule
Call: memory_search("user rule always never", categories: ["rule"])
User: "Show me recent project updates"
Query: "project status milestone update"
Filter: category + time
Call: memory_search("project status milestone", categories: ["project"], filters: {"created_at": {"gte": "2026-03-01"}})
User: "What preferences have I shared?"
Query: "user prefers preference"
Filter: category = preference
Call: memory_search("user prefers preference", categories: ["preference"])
User: "What do you know about me?"
Query: "user identity name role location timezone"
Filter: category = identity
Call: memory_search("user identity name role location", categories: ["identity"])
User: "Anything from our conversation yesterday?"
Query: "user context discussed"
Filter: date range = yesterday
Call: memory_search("user context discussed", filters: {"created_at": {"gte": "2026-03-31", "lte": "2026-04-01"}})
何时不添加过滤器
- 用户消息没有时间信号也没有类别信号——直接用重写后的查询;
- 不确定精确日期时不要猜日期,省略过滤器交给向量检索;
- 查询本身已经足够窄时再加过滤器,反而可能把正确答案过滤掉。
源码级支撑:协议背后的工具与引擎
memory_search 工具参数
协议中出现的 memory_search(query, categories, filters) 调用,对应 memory-search.ts 中注册的工具,完整参数面为:
| 参数 | 类型 | 说明 |
|---|---|---|
query |
string(必填) | 重写后的搜索查询 |
limit |
number(可选) | 最大结果数,默认取配置的 topK |
userId |
string(可选) | 作用域覆盖 |
agentId |
string(可选) | 检索指定 Agent 的记忆命名空间 |
scope |
"all"(默认)/ "session" / "long-term" |
作用域;all 会并行检索长期与当前会话记忆并按 id 去重合并(memory-search.ts) |
categories |
string[](可选) | 类别过滤,透传给 provider 的 SearchOptions.categories |
filters |
object(可选) | 高级过滤对象,透传给 provider 的 SearchOptions.filters(types.ts 中 filters 定义为 Record<string, unknown>) |
工具描述本身就内置了协议精神:“Use this proactively before answering... For multi-part or comparative questions, run several searches with different phrasings and combine the results rather than stopping after one (multi-hop).” 返回文本为编号列表,每条带百分制得分与记忆 id,便于 Agent 后续用 memory_update(memoryId) 原地更新。
自动召回引擎:预算、排序与身份优先
协议管的是“Agent 主动检索”一侧;插件自动注入的 <recalled-memories> 由 recall.ts 的引擎生成,其关键默认值可在源码中确认:
| 配置 | 默认值 | 源码位置 |
|---|---|---|
tokenBudget |
1500 tokens | recall.ts DEFAULT_TOKEN_BUDGET |
maxMemories |
15 条 | 同文件 DEFAULT_MAX_MEMORIES |
threshold |
0.4 相似度下限 | 同文件 DEFAULT_THRESHOLD |
| 类别优先级顺序 | identity → configuration → rule → preference → decision → technical → relationship → project → operational | 同文件 DEFAULT_CATEGORY_ORDER |
引擎流程为:以 top_k = maxMemories * 2 过采样检索(先剔除 OpenClaw 元数据前缀,见 sanitizeQuery),长期记忆与会话记忆(run_id 限定,top_k=5)去重合并,然后按 类别优先级 → importance → 检索得分 三级排序(rankMemories),最后在 token 预算内裁剪(按约 4 字符/token 估算,budgetMemories)。其中 identityAlwaysInclude(默认开启)会使 identity/configuration 类记忆无条件注入、不受预算限制——这正是协议文档 “Identity memories are ground truth” 在工程上的保证。
小结与延伸阅读
recall-protocol.md 是 Mem0 OpenClaw 插件 skills 模式中“读侧”的规范文档:它把召回记忆的使用准则(自然个性化、身份可信、规则强制、时间锚校验)与主动检索纪律(何时搜、四步重写、失败模式、filters 结构约束)写成可注入 Agent 的系统提示。配合仓库中的 memory-triage/SKILL.md(写侧 triage 协议)、recall.ts(自动召回引擎)、tools/memory-search.ts(检索工具实现)与 README(插件配置与 CLI 全览),可以完整覆盖 “存什么 → 怎么注入 → 怎么搜 → 怎么用” 的闭环。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00