oh-my-pi 长期记忆检索工具 recall 完全指南:从查询语义到双后端实现原理
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
本文系统讲解 oh-my-pi(coding-agent)内置的
recall工具:它负责在长期记忆中按相关性检索并返回匹配条目,服务于"过去对话、用户偏好、项目决策"等需要历史上下文的场景。读完本文,你将掌握recall的触发时机、输入输出契约、与reflect的分工、Hindsight / Mnemopi 双后端的完整调用链、作用域(bank scoping)配置,以及截断预览与memory://<id>全行读取的配合方式。
recall是 oh-my-pi 记忆子系统中最基础、最常被模型主动调用的工具之一。它本身不做归纳总结,只做"原始检索":把自然语言查询交给长期记忆后端,返回按相关性排序的匹配条目。与其配套的retain(写入记忆)、reflect(跨记忆综合)、memory_edit(编辑记忆)共同构成完整的长期记忆工具集,详见 docs/tools/retain.md 与 docs/tools/recall.md。
recall 工具定位:先检索,再判断
模型提示词(packages/coding-agent/src/prompts/tools/recall.md)对recall的定位非常明确:
Search long-term memory; return raw relevance-ranked matching entries.
即"搜索长期记忆,返回按相关性排序的原始匹配条目"。文档同时给出了两条关键使用纪律:
- 主动使用:在涉及过去对话、用户偏好、项目决策,或任何"有历史上下文能提升准确度"的话题之前,应当主动调用
recall;拿不准时,先 recall 再说(When in doubt, recall first)。 - 与
reflect分工:recall返回具体事实或条目;reflect则在众多记忆之上综合出一个整体回答(reflect: synthesized answer across many memories)。详见 packages/coding-agent/src/prompts/tools/reflect.md 中 "What do you know about this user?"、"Summarize project decisions." 这类开放式问题的适用场景。
在工具注册层面,recall、retain、reflect、memory_edit、learn被统一定义为"依赖记忆后端的工具集合",见 packages/coding-agent/src/memory-backend/tool-names.ts 中的MEMORY_BACKEND_TOOL_NAMES。
工具元数据与注册条件
recall工具的元数据定义在 packages/coding-agent/src/tools/memory-recall.ts 的MemoryRecallTool类中:
| 属性 | 值 | 含义 |
|---|---|---|
name | recall | 工具名 |
approval | "read" | 只读操作,不需要写权限审批 |
strict | true | 严格模式,参数校验严格 |
loadMode | "discoverable" | 可发现的内置工具(非显式注册) |
summary | "Search memory for relevant prior context" | 供上层 UI 展示的摘要 |
注册逻辑由createIf(session)实现(memory-recall.ts):
static createIf(session: ToolSession): MemoryRecallTool | null { const backend = session.settings.get("memory.backend"); if (backend !== "hindsight" && backend !== "mnemopi") return null; return new MemoryRecallTool(session); }这决定了recall的可用性前提:
memory.backend必须为"hindsight"或"mnemopi";- 当
memory.backend = "off"(默认值)或"local"时,recall完全不注册。
从源码结构还可以推断出两点可见性细节:
- 在未限制工具列表的会话中,只要选择了任一受支持后端,注册时会自动带上
recall/retain/reflect这一整套工具;显式指定的受限工具列表不会被自动扩充。 - 在普通的
tools.xdev会话中,可发现的内置工具可能以xd://recall的形式呈现;而显式请求的工具保持顶层形式。
执行方式为单次执行(single-shot):工具不会流式输出参数/结果更新。
输入与输出契约
输入:唯一参数 query
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
query | string | 是 | 自然语言搜索查询词 |
工具对query做透传(pass-through),不修改内容——唯一的例外是 Mnemopi 的per-project-tagged模式可能额外运行一次"共享库回落查询"(shared-bank fallback query),见下文作用域章节。参数 schema 定义在 memory-recall.ts:
const memoryRecallSchema = type({ query: type("string").describe("natural language search query"), });输出:命中时的文本格式
无论哪个后端,有匹配时都返回单条content[0].type = "text"的文本结果,details = {},文本前缀固定为:
Found <n> relevant memory/memories (as of YYYY-MM-DD HH:MM UTC): <bullet list>前缀中的 UTC 时间戳由 hindsight/content.ts 的formatCurrentTime()生成,格式为YYYY-MM-DD HH:MM。两种后端的条目格式不同:
Hindsight 条目(formatMemories,见 hindsight/content.ts):
- <text> [<type>] (<mentioned_at>)其中[<type>]和(<mentioned_at>)仅在对应字段存在时出现。
Mnemopi 条目(formatScopedRecallWithIds,见 mnemopi/state.ts):
- <content> (id: <id>) [<source>] (<YYYY-MM-DD>) c:<score>各后缀规则:
(id: <id>):携带 id 供后续read memory://<id>与memory_edit使用;id 不可用时渲染为(id unavailable);[<source>]:来源标签,缺失时省略;(<YYYY-MM-DD>):日期取时间戳前 10 位,缺失时省略;c:<score>:置信度/相关性得分(score或importance,保留一位小数),缺失时省略。
无命中时的输出
无匹配时返回:
No relevant memories found.同时details = {}、useless = true——调用方与渲染层可据此把该结果视为"无贡献上下文",避免污染后续决策。
执行流程:从注册到返回的双后端调用链
execute(...)的整体流程(memory-recall.ts):
MemoryRecallTool.createIf(...)在memory.backend为"hindsight"或"mnemopi"时暴露工具;execute(...)用untilAborted(...)包裹整个操作,支持AbortSignal取消;- 根据后端类型走 Mnemopi 或 Hindsight 分支;
- 后端失败时以
logger.warn("recall failed", ...)记录日志,并按需以Error实例重新抛出。
Mnemopi 分支:本地 scoped recall
当memory.backend == "mnemopi"时:
- 读取
session.getMnemopiSessionState(),未初始化则抛出Mnemopi backend is not initialised for this session.; - 调用
state.recallResultsScoped(params.query),其底层实现为collectScopedRecallResults(...)(mnemopi/state.ts):- 对每个已解析的 recall bank 调用
recallEnhanced(query, recallLimit, { includeFacts: true, channelId: bank }); - 按 id / content 对结果去重合并(
mergeRecallResult),优先保留得分高者; - 排序(
compareRecallResults:得分降序 → 时间戳降序 → 内容字典序)后截断到recallLimit条; - per-project 模式还会扫描安全识别的遗留 bank(legacy banks):只有
working_memory中所有行的metadata_json.$.cwd都属于当前绝对 cwd 的 bank 才会被加入 recall 集合,用于救回旧版 bank 派生方案留下的记忆;启动扫描上限为 64 个候选 bank 目录(见 mnemopi/config.ts 的extendRecallWithLegacyBanks); per-project-tagged模式下,共享 bank 可能额外执行一次回落查询:将查询中项目 bank 的字面 token 剥离后再查一遍,保证宽泛的全局记忆仍能命中(见 mnemopi/state.ts 的deriveSharedRecallFallbackQuery);
- 对每个已解析的 recall bank 调用
- 结果带 id 格式化(
formatScopedRecallWithIds),供后续全行读取与memory_edit。
Hindsight 分支:远程 HTTP recall
当memory.backend == "hindsight"时:
- 读取
session.getHindsightSessionState(),未初始化则抛出Hindsight backend is not initialised for this session.; - 调用
state.client.recall(state.bankId, params.query, {...}),携带配置的budget、maxTokens、types以及 bank 作用域下的 tag 过滤器(recallTags/recallTagsMatch); HindsightApi.recall(...)向POST /v1/default/banks/{bank_id}/memories/recall发起请求(hindsight/client.ts),请求体包含query、types、max_tokens、budget(默认"mid")、tags、tags_match;- 结果经
formatMemories(...)格式化为纯文本列表。
副作用与取消
- 网络:Hindsight 产生一次 HTTP POST;Mnemopi 本身无网络调用,除非配置了本地运行时 provider(嵌入/LLM 计算在 recall 期间发生)。
- 会话状态:显式工具路径成功后不改动会话状态——与后端自动 recall(auto-recall)不同,它不更新
lastRecallSnippet,也不刷新系统提示词。 - 取消:工具调用信号被取消时,通过
untilAborted(...)中止。
Bank 作用域(Scoping):三种模式如何决定检索范围
作用域决定recall到底"从哪几个 bank 里找"。两个后端都支持global、per-project、per-project-tagged三种模式。
Hindsight 作用域
Hindsight 使用 tag 过滤器实现(hindsight/config.ts):
| 模式 | 行为 |
|---|---|
global | 单个共享 bank,不加 tag 过滤 |
per-project | 每个项目标签一个独立 bank id(git 主检出根目录 basename;仓库外取 cwd basename) |
per-project-tagged | 共享 bank id +project:<项目标签>过滤,tagsMatch = "any",因此项目打标记忆与未打标全局记忆都能同时浮出 |
Mnemopi 作用域
Mnemopi 没有 tag 过滤式 recall,per-project-tagged通过"项目本地写入 bank + 共享可读 bank"的方式实现(mnemopi/config.ts 的computeMnemopiBankScope):
| 模式 | 写入 bank(retain) | 读取 bank(recall) |
|---|---|---|
global | 共享 bank | 共享 bank |
per-project | 由绝对 cwd basename + 该绝对 cwd 的 hash 派生的 bank | 同项目 bank |
per-project-tagged | cwd 派生项目 bank | 项目 bank + 共享 bank,结果合并 |
注意 Mnemopi 项目 bank 的派生刻意不依赖 git:旧版本先解析 git 根再 hash,导致增删.git会让同一目录漂移到不同 bank、碎片化记忆(issue #2412)。现在的派生纯粹基于cwd,稳定合约见projectBankSegment(mnemopi/config.ts):<清洗后basename>-<Bun.hash(projectRoot).toString(36)>,并限制在 64 字符以内。
另外,会话作用域为跨会话读取:recall读取的是跨会话的记忆数据,使用当前会话缓存的配置与作用域;子代理(subagent)别名共享父级的后端作用域。
限制与可调参数(Limits & Caps)
recall的默认行为受配置项约束,相关 schema 定义在 packages/coding-agent/src/config/settings-schema.ts。
Hindsight 侧
| 配置项 | 默认值 | 说明 |
|---|---|---|
hindsight.recallBudget | "mid" | 检索预算,枚举low/mid/high |
hindsight.recallMaxTokens | 1024 | 返回结果的最大 token 数 |
hindsight.recallTypes | ["world", "experience"] | 检索的记忆类型 |
hindsight.recallTimeoutMs | 30_000 | 请求超时(毫秒) |
客户端默认的 raw recall 预算也是"mid",但工具路径会从配置覆盖(hindsight/client.ts)。配置优先级为:内置默认 < 设置项(hindsight.*)<HINDSIGHT_*环境变量(如HINDSIGHT_RECALL_BUDGET、HINDSIGHT_RECALL_MAX_TOKENS),环境变量最高,便于 CI/生产环境按 shell 覆盖。
Mnemopi 侧
| 配置项 | 默认值 | 说明 |
|---|---|---|
mnemopi.recallLimit | 8 | 返回条数上限,运行时至少钳制为 1 |
mnemopi.scoping | "per-project" | 默认作用域 |
| 内容预览上限 | 500字符/条 | 由RECALL_CONTENT_PREVIEW_CHARS/RecallOptions.contentPreviewChars控制 |
关键约束:显式工具路径不应用hindsight.recallContextTurns、hindsight.recallMaxQueryChars、mnemopi.recallContextTurns、mnemopi.recallMaxQueryChars——这些上限只作用于后端自动 recall(auto-recall)的查询组合,例如HindsightSessionState.beforeAgentStartPrompt(...)/maybeRecallOnAgentStart(...)与MnemopiSessionState.beforeAgentStartPrompt(...)/maybeRecallOnAgentStart(...)中composeRecallQuery/truncateRecallQuery的"最近 N 轮上下文 + 字符预算截断"逻辑(见 hindsight/content.ts)。
截断预览与 memory:// :修改前必须读全行
Mnemopi 的 recall 内容默认是预览,每条约 500 字符;被截断的条目以…(省略号)结尾。此时:
- 内部 recall 行携带
truncated与full_length字段(RecallResult.truncated),但工具返回的是格式化文本、details = {},不暴露这两个字段; - 因此,在对任何条目执行整体式
memory_edit update之前,必须先读取完整行:read memory://<id>。
这条规则的动机很实际(见 mnemopi/state.ts 的注释):recall 裁剪了内容,若直接用预览文本做整体覆盖式更新,会覆盖掉未见过的字节(issue #4443)。memory://<id>URL 由 packages/coding-agent/src/internal-urls/memory-protocol.ts 提供支持,它对应getScopedMemory(id):按 retain → recall → global 的顺序在所有可 recall 的 bank 中查找,返回完整行(含content、source、timestamp、importance、veracity、session_id、memory_type等字段)。注意:fact表中的行是只读投影,可解析但不可编辑(issue #4725)。
错误处理与边界情况
- 后端未初始化:
memory.backend == "mnemopi"但无状态 →Mnemopi backend is not initialised for this session.;Hindsight 同理抛出对应错误。 - Hindsight 失败:HTTP、fetch、超时失败统一包装为
HindsightError;HTTP 错误携带statusCode与解析出的details(hindsight/client.ts)。超时错误信息形如recall request timed out after 30s。 - Mnemopi 失败:按目标(bank)逐个捕获并记录日志,健康目标仍正常贡献结果;若所有目标全部失败,则抛出原始错误(单目标)或携带各 bank 详情的
AggregateError(多目标),而不是把失败静默转成空结果——避免"检索失败被伪装成'没找到'"。 - 非
Error异常:统一规范化为new Error(String(err))后重新抛出。
注意事项(Notes)
- Mental models 不在此工具范围:
recall不拉取 Hindsight 的心理模型(mental models)。这些模型可能已作为<mental_models>块缓存在开发者指令中,独立于 recall 结果(Hindsight 后端在 hindsight/content.ts 中有对应的块剥离正则,用于防止记忆块回流到记忆库形成正反馈回路)。 - Mnemopi 的
<memories>块:后端自动 recall 可能把<memories>块注入开发者指令,但显式recall工具不更新该块。 - recall ≠ reflect:
recall只返回命中条目,不做跨记忆综合。需要综合时,Hindsight 用reflect做远端综合;Mnemopi 的reflect变体是"本地 recall + 格式化"。
小结:recall 的正确使用姿势
- 时机:涉及历史对话、用户偏好、项目决策等话题时,先
recall再作答(When in doubt, recall first)。 - 查询:传入自然语言查询(如 "user's preferred editor"、"why did we choose X"),查询原样透传。
- 读结果:注意条目后缀——Hindsight 条目带
[type]与(mentioned_at),Mnemopi 条目带(id: ...)、[source]、日期与c:score。 - 追全量:Mnemopi 预览被截断(
…结尾)时,用read memory://<id>取全行;任何整体式memory_edit update之前必须这样做。 - 区分 reflect:需要"综合多段记忆给结论"时改用
reflect。
recall是记忆闭环的"读"入口:retain负责写、recall负责原始检索、reflect负责综合、memory_edit负责修正。理解它的注册条件(仅hindsight/mnemopi后端)、单参数契约、双后端调用链与作用域配置,就能让模型在任何需要历史上下文的场景中准确、高效地取回记忆。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考