Mem0 Pi Agent 插件的 search 技能:紧凑语义记忆检索与 Memory ID 直查实现解析
本文以 search 技能定义 为主体,讲解 Mem0 为 Pi Agent 提供的 search(Search / Peek)技能如何工作:它如何解析用户查询、识别 Memory ID、调用 mem0_memory 工具完成语义检索,并以紧凑单行格式输出结果。结合插件 源码实现、范围解析 与单元测试,读完本文你将掌握该技能的完整执行链路、检索参数与 scope 过滤机制,并能复现一套可被 Agent 自主调用的轻量记忆检索方案。
技能定位:介于"随手一查"与"全量浏览"之间
search 技能是 pi-agent-plugin 提供的 8 个技能之一,其 frontmatter 中声明的用途是:
Searches memories and displays compact one-liner results, or looks up a specific memory by ID. Use for quick memory lookups, checking if something was recorded, resolving [mem0:id] citations, or browsing memories without full category detail.
它与 tour 技能形成明确的分工:tour 走 formatMemoryList + groupByCategory 按类别分组做完整走查,而 search 是"Lighter than /mem0-tour"的轻量路径——只针对一条查询返回紧凑单行结果,不展开完整类别明细。它的四类典型使用场景是:
- 快速记忆查询(quick memory lookups);
- 确认某件事是否被记录过(checking if something was recorded);
- 解析
[mem0:id]引用——即前文检索结果中携带的记忆 ID 标记; - 轻量浏览记忆,不需要按类别展开全部细节。
完整执行流程
Step 1:解析查询
用户以 /mem0-search <query> 的形式提供搜索词,例如 /mem0-search favorite restaurants。若未提供查询,技能要求反问:"What should I search for?"——这一点与斜杠命令实现一致:/mem0-search 命令在 commands.ts 中对空参数直接提示 Usage: /mem0-search <query> 并返回。
Memory ID 识别:如果查询匹配 UUID 模式(^[a-f0-9-]{20,}$),则将其视为一次按 ID 的直接定位,而不是语义搜索。这一判断是纯文本层的正则匹配,不产生额外 API 调用。
需要注意一个实现细节:ToolParams 中定义的动作只有 search | add | get_all | update | delete | delete_all 六种,并不存在独立的 get by id 动作。从源码结构看,"按 ID 直查"在技能层面复用 action="search" 的调用路径,由 Agent 将 ID 作为 query 传入同一工具;而斜杠命令侧则被测试明确约束为"即使是十六进制样式的字符串也走语义搜索"(见 commands.test.ts 中 uses semantic search even for hex-looking strings 用例,并断言 mem0.get 未被调用)。两条路径对 ID 型输入的意图解读不同,这正是"技能引导 Agent"与"命令直接执行"两种模式的差异所在。
Step 2:执行搜索
技能指示 Agent 调用 mem0_memory 工具,参数为 action="search"、query=<用户查询>。在 tools.ts 中,该分支的执行逻辑是:
case "search": {
if (signal?.aborted) throw new Error("Cancelled");
if (!params.query) throw new Error("query is required for search");
const filters = resolveSearchFilters(scope, scopeCtx);
const result = await mem0.search(params.query, { filters });
const memories = result.results ?? [];
return {
content: [{ type: "text" as const, text: truncateOutput(formatMemoryList(memories)) }],
details: { matchCount: memories.length },
};
}
关键行为有三点:
query必填,缺失时直接抛错,防止 Agent 发起无效检索;- 检索前通过 resolveSearchFilters 生成 scope 过滤条件(见下文"scope 与过滤条件"一节);
- 结果统一经
formatMemoryList格式化,并经truncateOutput截断——上限为 200 行 / 50KB(MAX_OUTPUT_LINES / MAX_OUTPUT_BYTES),超限后会追加[Output truncated: showing N of M lines]提示,避免长结果撑爆 Agent 上下文。
Step 3:紧凑格式展示
技能规定的展示格式是每行一条的单行结构:
## mem0 search: "<query>" (<N> results)
1. [preferences] Prefers window seats on flights (2026-05-15) [mem0:a3f8b2c1]
2. [goals] Wants to visit Japan in 2027 (2026-05-10) [mem0:7e2d9f4a]
3. [identity] Lives in San Francisco (2026-05-08) [mem0:c4d5e6f7]
单行格式定义为:
<number>. [<category>] <content, 80 chars> (<date>) [mem0:<short_id>]
若查询无命中,输出固定的一句话:
No memories matching "<query>".
值得对照的是工具层的原始输出格式。formatMemoryCompact 生成的是:
const cat = mem.categories?.[0] ?? "uncategorized";
const age = mem.createdAt ? ` (${formatAge(mem.createdAt)})` : "";
return `[${cat}] ${mem.memory ?? "(empty)"}${age} [mem0:${mem.id}]`;
即 [类别] 全文 (相对时间) [mem0:完整ID],其中时间由 formatAge 渲染为 3m ago / 5h ago / 12d ago 这样的相对时间,且空结果时返回 "No memories found."。对比之下可以发现:技能要求的是展示层二次格式化——Agent 拿到工具输出后,将内容压缩到 80 字符、把 ID 缩略为 short_id、并以日期形式呈现。也就是说,[mem0:<short_id>] 这类紧凑引用是技能约定,而非工具直接产出的字段。
[preferences]、[goals]、[identity] 这些类别标签并非随意选取,它们对应 types.ts 中定义的 10 个默认分类:identity、preferences、goals、projects、decisions、technical、relationships、routines、lessons、work。记忆写入时通过 customCategories 传入这组分类定义(见 tools.ts 的 add 分支),检索结果中的 categories[0] 即取自同一命名空间。
scope 与过滤条件:project / session / global
search 技能调用时若未显式传 scope,则回落到配置的 defaultScope(buildToolExecute 中 const scope = params.scope ?? defaultScope),默认为 project。三种 scope 到 mem0 服务端过滤条件的映射在 scoping.ts 中定义:
| Scope | filters 字段 | 语义 |
|---|---|---|
project(默认) |
{ user_id, app_id } |
当前项目记忆池,app_id 取 git 仓库根目录名 |
session |
{ user_id, app_id, run_id } |
仅本次会话,run_id 为会话文件 SHA-256 前 12 位 |
global |
{ user_id, app_id: "*" } |
跨所有项目 |
两个上下文字段的推导方式同样值得注意(detectAppId / detectRunId):
app_id通过git rev-parse --show-toplevel取仓库根的 basename,因此 monorepo 下所有子目录共享同一记忆池,检测失败时回退为当前目录名;run_id是会话文件路径的sha256前 12 位,无会话文件时为"unknown"。
工具注册时还通过 promptGuidelines 明确了 scope 使用纪律:常规查询不要传 scope(自动落到项目默认值),只有用户明确要求跨项目回忆时才使用 global——这与 search 技能"轻量、聚焦当前上下文"的定位一脉相承。
检索参数:阈值、topK 与重排序
search 技能背后实际调用的 mem0 检索能力,在不同入口暴露了不同的参数组合:
斜杠命令入口(用户手动执行 /mem0-search)在 commands.ts 的 searchMemories 中固定携带完整参数:
const SEARCH_TOP_K = 10;
...
const result = await mem0.search(query, {
filters,
threshold: config.searchThreshold, // 默认 0.3
topK: SEARCH_TOP_K, // 固定 10
rerank: true,
});
其中 searchThreshold 在 config/index.ts 中默认为 0.3,可在 ~/.pi/agent/mem0-config.json 中覆盖(如 {"apiKey": "...", "searchThreshold": 0.2, ...}),MEM0_API_KEY、MEM0_USER_ID 环境变量优先级更高。它的语义是相似度下限(0–1):没有足够相似的记忆时报告"无匹配",而不是返回最接近的无关记忆——调高更严格,调低则放宽。
Agent 工具入口(即 search 技能实际走的路径)在 tools.ts 的 search 分支 中只透传了 filters,未附带 threshold / topK / rerank,相关策略由服务端默认处理。从源码结构看,这一差异是有意为之:命令路径服务于人类一次性交互,需要客户端可控的相关性门槛;工具路径服务于 Agent 高频、多轮、多措辞的主动检索(工具描述中明确建议 "run multiple searches with different phrasings for multi-part questions"),把过滤权交给服务端默认行为更合适。
测试对行为的约束
commands.test.ts 中的 /mem0-search 用例组为这条链路提供了可验证的行为契约:
performs server-side semantic search with a relevance threshold:断言mem0.search以{ threshold: 0.3, topK: 10, rerank: true }被调用,并确认结果通过customType: "mem0-search"反馈消息发出;uses semantic search even for hex-looking strings:输入abcd1234这类十六进制样式字符串时,命令路径仍然执行语义搜索,mem0.getAll与mem0.get均不被调用;shows a no-matches message naming the query:空结果时消息包含No matches并带上原始查询词,与技能定义的No memories matching "<query>".输出约定一致;shows all results the API returns (relevance gating is server-side):客户端不做本地分数过滤,API 返回多少条就展示多少条——相关性门控完全在服务端完成。
此外,每次搜索调用都会经 telemetry 记录 result_count(命令路径)或 latency_ms + matchCount(工具路径,见 execute 中的 captureToolEvent),用于观测检索质量与延迟。
小结
search 技能把一次记忆检索拆解为"解析 → 检索 → 紧凑呈现"三步,用一行 [类别] 内容 (日期) [mem0:id] 的格式在保证信息密度的同时压低上下文开销,并以 [mem0:<short_id>] 标记为后续的 ID 直查、/mem0-pin、/mem0-forget 等操作留下引用锚点。其底层由 mem0_memory 工具的 search 动作支撑,配合 scope 过滤、输出截断与遥测埋点,构成了一条从 Agent 意图到 mem0 服务端检索的完整可验证链路——理解这条链路,也就理解了 pi-agent-plugin 中"轻量检索"这一能力面的全部实现细节。
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