深入 Mem0 OpenCode 插件:mem0-search 技能的查询解析、双路记忆召回与紧凑输出规范
在 Mem0 平台的 OpenCode 插件(@mem0/opencode-plugin)中,/mem0-search 是一条面向"快速记忆检索"的斜杠命令:用户给出查询词,Agent 即以并行双路语义搜索当前项目的 Mem0 记忆,并按单行紧凑格式返回结果;若查询词本身是一条记忆 ID 或 [mem0:<hex>] 引用,则直接按 ID 取回单条记忆。本文以 mem0-search 技能定义文件 为主体,完整还原其三步执行流程(查询解析 → 双路搜索 → 紧凑展示),并结合插件入口 opencode-mem0.ts 与范围模块 scope.ts 的源码,说明身份过滤、scope 模型和输出约束背后的实现细节。
技能定位:比 /mem0-tour 更轻量的检索入口
mem0-search 的技能描述(frontmatter description 字段)明确了它的四个典型用途:快速记忆查询、确认某个决策是否已被记录、解析 [mem0:id] 引用、以及在不展开全部分类细节的情况下浏览记忆。文件开头的一句话也点明了定位:"Quick search with compact output. Lighter than /mem0-tour。"
对比同目录下的 mem0-tour 技能:tour 面向"盘点项目全部记忆",先 get_memories 全量拉取(page_size=100),再并行跑三路补充语义搜索,最后按平台 categories 分组、每分类展示最多 5 条;而 search 只针对单个查询词,固定走两路搜索 + 去重 + 单行渲染,开销与输出体量都显著更小。在 OpenCode 插件 README 的组件清单中,/mem0-search 与 /mem0-remember、/mem0-tour、/mem0-status、/mem0-scope、/mem0-dream、/mem0-forget、/mem0-pin、/mem0-context-loader 一起构成插件自带的 9 个技能。
该命令在 Claude Code 侧的对应物是 /mem0:peek(见 skills/peek/SKILL.md),两者执行逻辑完全一致,仅命令前缀不同——mem0-plugin 同时维护两套技能目录,一套供 Claude Code/Codex 等走 MCP 的工具使用,一套供 OpenCode 原生工具链使用。
命令如何注册到 OpenCode:从 SKILL.md 到斜杠菜单
技能文件本身只是"提示词模板",真正让它变成可输入的 /mem0-search 的是插件入口的 config 钩子。在 opencode-mem0.ts 的 registerCommands 函数 中,插件遍历 opencode-skills/ 目录下的每个子目录,读取其中的 SKILL.md,用正则 /^description:\s*(.+)$/m 提取 frontmatter 里的 description 作为命令说明,并向 OpenCode 的 config.command 注册一条斜杠命令,其 template 形如:
Load and execute the `mem0-search` skill.
Use the mem0 memory tools (add_memory, search_memories, get_memories, get_memory, ...) as instructed by the skill.
Identity context (resolved at plugin startup):
- user_id: ${userId}
- app_id: ${appId}
- session_id: ${sessionId}
- branch: ${branch}
这里有两点值得注意:
- 身份上下文由插件启动时解析并注入模板。
userId来自getUserId()(优先MEM0_USER_ID环境变量,否则取系统用户名);appId来自getProjectId(),解析顺序为MEM0_APP_ID环境变量 →git remote get-url origin的 owner/repo(跨 clone、worktree 稳定)→ git 仓库根目录名 → 当前目录名。技能文件里的filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}中的占位符<id>/<pid>就由这段身份上下文填实。 shell.env钩子(L384-L395)把MEM0_USER_ID、MEM0_APP_ID、MEM0_SESSION_ID、MEM0_BRANCH导出给 shell,供技能脚本读取,避免每条命令都重新跑 git。
执行流程 Step 1:查询解析与 Memory ID 检测
用户输入形如:
/mem0-search auth middleware
若未提供查询词,技能要求 Agent 反问 "What should I search for?"。
真正的关键设计在 Memory ID 检测:当查询词匹配以下任一模式时,不再做语义搜索,而是转为按 ID 直接取回单条记忆——
- 裸 hex 短 ID:
^[a-f0-9]{8}$(8 位十六进制) - 完整 UUID:
^[a-f0-9]{8}-[a-f0-9-]+$ - 引用标记:
[mem0:<hex>],提取其中的 hex 部分
检测到 ID 后的处理链是:
- 直接调用
get_memory(<id>)(若只有 8 位短 ID,则把它当作完整 UUID 的前缀去尝试); - 若找到,跳过 Step 2,直接按 Step 3 的格式展示这一条结果;
- 若未找到,回落到普通搜索,把该 ID 字符串当作查询文本。
这个设计之所以成立,是因为插件生态里到处都在输出 [mem0:<short_id>] 引用。例如 Python 侧预取钩子的公共格式化函数 format_results_for_context 对每条记忆都会生成 - [<type>] <text> [mem0:<前8位ID>] 行;dream、health、memory-reviewer 等技能的输出也大量使用 [mem0:<id>] 标注待处理记忆。用户看到某条自动注入的记忆后,把 [mem0:a3f8b2c1] 原样丢给 /mem0-search,即可精确回查该条记忆全文——这就是技能描述中 "resolving [mem0:id] citations" 的用意。
get_memory 工具本身在 opencode-mem0.ts 的 tool 定义 中实现为对 mem0ai SDK 的 mem0.get(args.id) 的一次调用,返回单条记忆的完整内容(memory 文本 + metadata + 平台字段)。
执行流程 Step 2:双路并行搜索
未命中 ID 时,技能要求运行 两个并行的 search_memories 调用:
| 路 | query | filters | top_k | rerank |
|---|---|---|---|---|
| Broad(宽泛) | 用户原始查询词 | {"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]} |
10 | true |
| Targeted(定向) | 用户原始查询词 | {"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]} |
5 | true |
两条路的区别只在第三条 AND 子句:定向路额外约束 metadata.type = "decision",专门捞"决策"类记忆(例如"Auth module uses JWT with RS256 keys"),因为开发者问得最多的往往就是"我们当时为什么这么定"。两路合并去重后,既覆盖了相关度排序的宽泛召回,又保证决策类记忆不被淹没。
对照同目录 mem0-tour 的补充搜索 可以看到一个刻意相反的约定:tour 明确警告"不要在这些调用里按 metadata.type 过滤,平台自动分配的 categories 可能不含显式 metadata.type"——那是"全量盘点"场景;而 search 的定向路只针对单一 decision 类型做补充召回,主召回由 Broad 路兜底,因此不会因部分记忆缺 metadata.type 而整体漏检。
关于 rerank=true:插件 Python 侧钩子辅助库 _search.py 的注释说明了平台 REST 搜索端点(POST /v3/memories/search/)的行为——省略 rerank 时结果按原始向量相似度排序,最相关的一条可能落在 top_k 窗口之外;显式开启 rerank 有约 150–200ms 的额外开销,钩子路径默认开启并允许用 MEM0_RERANK=0/false/no/off 关闭。技能在展示型搜索中固定要求 rerank=true,是为了让少量结果(10+5 条)的排序质量最大化。需要注意,OpenCode 原生 search_memories 工具的参数 schema(L466-L490)暴露的是 query、filters、limit/top_k、scope 等参数,rerank 能力由平台搜索端处理,技能指令是面向"搜索语义"的约定而非工具字面参数。
从源码结构看,search_memories 工具的 execute 实现为:topK = args.limit ?? args.top_k ?? 10(默认 10,与技能 Broad 路的 top_k=10 对齐),过滤条件经 readScopeFilters 解析后调用 mem0.search(query, {filters, topK})。
执行流程 Step 3:去重与紧凑单行渲染
两路结果先按 memory ID 去重,再按下述格式渲染:
## mem0 search: "<query>" (<N> results)
1. [decision] Auth module uses JWT with RS256 keys (2025-05-15) [mem0:a3f8b2c1]
2. [anti_pattern] Don't use symmetric HS256 — leaked in env (2025-05-10) [mem0:7e2d9f4a]
3. [convention] All middleware in src/middleware/ (2025-05-08) [mem0:c4d5e6f7]
单行格式约定为:
<序号>. [<type>] <内容, 截断到 80 字符> (<日期>) [mem0:<短ID>]
其中 [<type>] 取自记忆的 metadata 类型或平台分类(示例中的 decision / anti_pattern / convention),[mem0:<短ID>] 取 ID 前 8 位——与 mem0-tour 的分组映射表一致,decision 对应平台分类 architecture decisions / architecture_decisions / decision 等。这个短 ID 又构成了下一轮 /mem0-search 的输入,形成"列表 → 精确回查"的闭环。
无结果时的输出被固定为:
No memories matching "<query>" for project <project_id>.
源码纵深:过滤条件如何被插件解析
技能文件里的 filters={"AND": [...]} 与插件工具侧的过滤解析逻辑是同一套语义。resolveFilters 的合并规则是:
- 若调用方已传
filters,优先保留其中的AND子句,检查是否已包含user_id/app_id/agent_id,缺哪个补哪个,最终仍收敛为{AND: [...]}形态; - 未传
filters且开启了global_search(~/.mem0/settings.json中global_search: true,覆盖所有用户)时,使用{OR: [{user_id: "*"}]}; - 传了
agent_id时按{AND: [{agent_id}, {app_id}]}收敛; - 其余情况默认
{AND: [{user_id: <当前用户>}, {app_id: <当前项目>}]}——即技能 Step 2 中 Broad 路 filters 的原型。
在此基础上,scope.ts 定义了三种调用级 scope,search_memories 等工具可通过 scope 参数覆盖默认过滤(L17-L32 的 scopeSearchFilters):
| scope | 读过滤 | 语义 |
|---|---|---|
project(默认) |
user_id + app_id |
仅本仓库 |
session |
user_id + app_id + run_id |
仅本次会话 |
global |
user_id + app_id: "*" |
该用户所有项目 |
默认 scope 可由 /mem0-scope 技能 修改,持久化在 ~/.mem0/settings.json 的 default_scope 字段;插件在每次记忆操作时重新读取该设置(loadDefaultScope),所以切换立即生效、无需重启。技能里固定的 user_id + app_id AND 过滤对应的是默认的 project scope;若用户默认 scope 已改为 session 或 global,readScopeFilters 会在未显式传 scope/filters 时优先采用持久化默认值。
另外值得了解的是自动注入路径:插件的 chat.message 钩子在每条用户消息(≥10 字符)上都会自动跑一次 mem0.search(top_k=5,同样的 user/app AND 过滤,先经 redact() 用正则抹掉 sk-/m0-/AKIA/ghp_ 等密钥模式),把命中记忆以 "Relevant memories" 注入系统上下文。也就是说 /mem0-search 是用户主动发起的、面向终端展示的检索;钩子自动注入则是后台静默召回,两者共用同一套过滤与搜索语义。
输出约束:OpenCode TUI 不使用 Markdown
技能文件末尾的 "Output formatting" 一节是一条硬性约束:
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like bold,
## headers, and| table |syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
由于 OpenCode 的 TUI 按原文渲染文本,**bold**、## 标题、| 表格会原样显示为字符垃圾,因此技能要求结果用纯文本 + 缩进组织、用破折号代替列表符号、用空格对齐代替 Markdown 表格。这也解释了上面示例中 ## mem0 search: 这种写法是"示意标题",Agent 实际输出时应以纯文本呈现。该约定在 mem0-plugin 的所有 OpenCode 技能(tour、scope、search 等)中重复出现,是同一渲染约束下的统一规范。
使用前提与验证方式
该技能随插件安装即得,前置条件只有两条:
-
环境中已设置
MEM0_API_KEY(m0-开头的 Mem0 平台密钥,如export MEM0_API_KEY="m0-your-key");未设置时插件入口会记录错误日志并直接不注册任何工具(opencode-mem0.ts L262-L277); -
已安装插件并重启 OpenCode:
opencode plugin @mem0/opencode-plugin该命令把插件写入
~/.config/opencode/opencode.json,插件通过自身的config钩子注册记忆工具、钩子与技能路径,无需单独配置 MCP server。安装后重启会话,输入/mem0-search出现在斜杠菜单中即为注册成功。验证可执行一次实际查询,例如
/mem0-search auth middleware;若项目尚无记忆,应看到No memories matching "auth middleware" for project <project_id>.的空态输出——这本身也证明了搜索链路(API key、身份解析、过滤条件)已贯通。
小结
mem0-search 技能的完整行为链条可以概括为:插件在启动时解析 user/app/branch 身份并注册 /mem0-search 斜杠命令 → 收到查询后先用 8 位 hex / UUID / [mem0:<hex>] 三种模式做 ID 检测,命中则 get_memory 直达、未命中回落搜索 → 否则并行执行 Broad(top_k=10)与 Targeted(metadata.type=decision, top_k=5)两路 rerank 搜索 → 按 ID 去重后以 <序号>. [<type>] <80字符内容> (<日期>) [mem0:<短ID>] 的纯文本单行输出。技能文件(mem0-search/SKILL.md)定义了行为契约,opencode-mem0.ts 提供 search_memories/get_memory 等原生工具与过滤解析,scope.ts 提供 project/session/global 三级范围模型,三者共同构成 OpenCode 中"轻量、可回查、可引用"的记忆检索入口。
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 StartedRust0622
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