首页
/ Mem0 OpenClaw 插件召回协议:如何正确使用召回记忆与重写 memory_search 查询

Mem0 OpenClaw 插件召回协议:如何正确使用召回记忆与重写 memory_search 查询

2026-09-04 21:22:47作者:傅爽业Veleda

在 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.” 这句话对应的实现链条在源码中清晰可查:

  1. 生成区段recall.tsformatRecalledMemories() 把筛选后的记忆按类别分组,输出形如:

    <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>". 的占位区段。

  2. 注入上下文index.ts 在 skills 模式的 before_prompt_build 钩子中执行 skillRecall(...),并把结果放入 prependContext(注释明确写着 “Dynamic recall goes in prependContext (changes every turn)”),即每轮动态变化、与静态系统提示分离。

  3. 降级策略:检索失败只打 warn 日志不阻塞 Agent(recall.ts 中的 try/catch),协议文档因此可以假定“区段存在、内容可能为空”。

使用召回记忆的四条行为准则

协议文档 “Acting on Recalled Memories” 一节给出四条必须遵循的准则,原文要点完整如下:

  • 自然个性化,不暴露机制:知道用户名字就用名字,知道偏好就遵守,但绝不能说 “I remember that you...” 或 “According to my memory...”。直接使用信息,不引起对机制的注意。
  • 身份类记忆是 ground truth:名字、角色、时区、系统配置等身份信息默认可信,除非用户明确纠正。这与 skill-loader.tsidentity 类别的默认配置呼应——identity 是唯一带 immutable: trueimportance: 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.tsSkillsConfig.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” 这类噪声词,稀释信号且在记忆库中匹配不到任何东西——所以禁止把用户原始消息直接当查询

四步流程(每次调用必须完整执行)

  1. Step 1. 命名目标(Name your target):写查询前先识别期望命中的记忆类别,防止无目的检索。
  2. Step 2. 提取信号词(Extract signal words):从用户消息中抽出所有专有名词、技术术语、领域概念和具体细节;丢弃对话框架、疑问句、代词和填充词。
  3. Step 3. 桥接到存储语言(Bridge to storage language):想象这条记忆被存储时的写法——记忆是第三人称事实句,包含 “User”、“configured”、“decided”、“prefers”、“rule”、“team”、“project”、“based in”、“works at” 这类词。有助于命中时可追加类别词:identity、decision、rule、preference、configuration、relationship。
  4. 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 配近期日期;
  • 消息中既无时间也无类别信号:不要加过滤器,让查询独立完成。

过滤器语法

  • 运算符:eqnegtgteltlteincontainsicontains
  • 逻辑组合:ANDORNOT(条件用数组包裹)
  • 日期格式: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.filterstypes.tsfilters 定义为 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 全览),可以完整覆盖 “存什么 → 怎么注入 → 怎么搜 → 怎么用” 的闭环。

登录后查看全文
热门项目推荐
相关项目推荐