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 的
recall工具展开:它是 coding agent 面向长期记忆后端的单次检索入口,负责把自然语言查询翻译成对 Hindsight 远程记忆服务或本地 Mnemopi 记忆库的召回请求,并将命中结果格式化为可注入上下文的纯文本列表。读完本文,你将掌握 recall 的注册与可见性规则、输入输出契约、两种后端的执行链路、bank 作用域模式、全部相关配置项与错误处理语义,并能据此正确配置和使用记忆检索能力。
工具定位:agent 的"主动回忆"入口
recall是 oh-my-pi coding agent 提供的记忆检索工具,在工具元数据中标注为approval = "read"、strict = true、loadMode = "discoverable"。它被设计为单次执行(single-shot):不流式输出参数/结果更新,调用一次即返回完整结果。其模型面向的提示词(packages/coding-agent/src/prompts/tools/recall.md)给出了明确的使用指引:
Search long-term memory; return raw relevance-ranked matching entries. Use proactively before questions about past conversations, user preferences, project decisions, or topics where prior context improves accuracy. When in doubt, recall first.
即:在回答涉及历史对话、用户偏好、项目决策或任何"有先例可循"的问题之前,应当主动召回;拿不准时先 recall。提示词还区分了 recall 与 reflect 的职责边界:recall返回具体事实条目,reflect则跨多条记忆做综合归纳。
注册与可见性:什么条件下 recall 才会出现
工具的可用性由会话设置memory.backend决定。核心实现位于 packages/coding-agent/src/tools/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); }注册规则可以归纳为以下几点:
- 仅在
memory.backend为"hindsight"或"mnemopi"时注册;"off"与"local"两种取值下该工具完全不可见。 - 在无限制会话中,若显式给出了工具列表,注册逻辑会自动为任一受支持后端附带
recall/retain/reflect三件套;受限工具列表不会被擅自扩宽。 - 在普通
tools.xdev会话中,discoverable 的内置工具可能以xd://recall形式呈现;若用户显式请求了该工具,则保持顶层可用。
测试 packages/coding-agent/test/memory-tools.test.ts 对这条规则做了直接验证:memory.backend不等于hindsight时三个工厂返回null;等于hindsight或mnemopi时MemoryRecallTool.createIf(...)返回工具实例。
输入输出契约:一次查询,一份结构化结果
输入
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
query | string | 是 | 自然语言检索查询。工具会原样透传,唯一例外是 Mnemopiper-project-tagged模式可能额外执行一次内部共享 bank 的兜底查询 |
有命中时的输出
content[0].type = "text"content[0].text = "Found <n> relevant memory/memories (as of YYYY-MM-DD HH:MM UTC):\n\n<bullet list>"details = {}
时间戳由 packages/coding-agent/src/hindsight/content.ts 的formatCurrentTime(...)生成,严格使用 UTC,格式为YYYY-MM-DD HH:MM。
Hindsight 子弹列表由formatMemories(...)生成(见 content.ts):每条为- <text> [<type>] (<mentioned_at>),type与mentioned_at仅在对应字段存在时追加。
Mnemopi 子弹列表由formatScopedRecallWithIds(...)生成(见 packages/coding-agent/src/mnemopi/state.ts):每条为- <content> (id: <id>) [<source>] (<YYYY-MM-DD>) c:<score>,其中:
- id 不可用时渲染为
(id unavailable); source、日期、c:score在缺失时省略;- score 取自
result.score ?? result.importance,格式化为一位小数。
内容预览与截断语义
Mnemopi 的召回内容是预览,默认上限 500 字符,定义在 packages/mnemopi/src/core/beam/recall.ts:
export const RECALL_CONTENT_PREVIEW_CHARS = 500;RecallOptions.contentPreviewChars可覆盖此值;设为0或负数表示不裁剪。显式工具路径使用默认值。被裁剪的预览以…结尾——这对应提示词中提到的truncated: true/full_length内部标记。重要实践:预览不等于全文,执行memory_edit update之前,必须先用read memory://<id>拉取完整行,避免覆盖未看到的字节(这正是 mnemopi/state.ts 中getScopedMemory(...)支撑memory://<id>URL 的原因)。值得注意的是,虽然内部召回行携带truncated与full_length,但本工具返回的是格式化文本、details = {},并不暴露这两个字段。
无命中时的输出
content[0].text = "No relevant memories found."details = {}useless = true,允许调用方/渲染器将该结果视为"无贡献上下文",从而不占用有效上下文预算。
执行流程:从工厂到结果的全链路
工具的执行入口封装在untilAborted(...)中(支持调用信号取消),随后按后端分叉。整体流程(memory-recall.ts):
MemoryRecallTool.createIf(...)在memory.backend为"hindsight"或"mnemopi"时暴露工具;execute(...)用untilAborted(...)包裹整个操作;- 后端为
mnemopi时走本地召回链路; - 后端为
hindsight时走远程 HTTP 召回链路; - 任一后端失败都以
logger.warn("recall failed", ...)记录,并按需以Error实例重新抛出。
Mnemopi 本地链路
Mnemopi 是本地 SQLite 记忆后端,无网络依赖(除非配置的本地运行时 provider 在召回时执行 embedding/LLM 工作)。执行要点:
- 读取
session.getMnemopiSessionState(),后端未启动则抛出Mnemopi backend is not initialised for this session.; - 调用
state.recallResultsScoped(params.query); - 作用域召回会对每个解析出的 recall bank 执行
recallEnhanced(query, recallLimit, { includeFacts: true, channelId: bank })(见 mnemopi/state.ts),按 id/content 合并去重、排序(score → 时间戳 → 内容),最终截断到recallLimit; - 结果以带 id 的格式输出,供后续
memory://<id>全文读取与memory_edit操作使用。
Hindsight 远程链路
Hindsight 是远程记忆服务。执行要点:
- 读取
session.getHindsightSessionState(),未启动则抛出Hindsight backend is not initialised for this session.; - 调用
state.client.recall(...),携带bankId、查询、配置的budget、maxTokens、types以及 bank 作用域标签过滤器; HindsightApi.recall(...)向POST /v1/default/banks/{bank_id}/memories/recall发起请求;- 结果经
formatMemories(...)格式化为纯文本列表。
注意一个细节:Hindsight 侧state.client.recall(...)的默认预算(raw API 层)是"mid",而本工具会用配置覆盖(见下文 Limits 小节),配置优先级高于 client 默认。
取消与副作用
- 取消:工具调用信号被取消时,通过
untilAborted(...)中止; - 网络副作用:Hindsight 为一次
POST /v1/default/banks/{bank_id}/memories/recall;Mnemopi 无网络调用; - 会话状态副作用:显式工具路径在成功时不更新
lastRecallSnippet、不刷新系统提示词——这与后端自动召回(auto-recall)行为不同,后者会更新lastRecallSnippet并把<memories>块注入开发者指令。
工具召回 vs 自动召回:两条并行的检索路径
文档强调,recall工具本身"不从不远的轮次组合上下文"(explicit query-only recall)。更丰富的查询组合逻辑存在于后端会话状态的自动召回路径中:
- Hindsight:
HindsightSessionState.beforeAgentStartPrompt(...)/maybeRecallOnAgentStart(...)(见 packages/coding-agent/src/hindsight/state.ts); - Mnemopi:
MnemopiSessionState.beforeAgentStartPrompt(...)/maybeRecallOnAgentStart(...)(见 packages/coding-agent/src/mnemopi/state.ts)。
自动召回在首次 turn 的提示词生成前触发:用composeRecallQuery(...)把最近recallContextTurns轮(按 user 消息切分、剥离记忆标签)拼进 "Prior context:" 块,再用truncateRecallQuery(...)在字符预算内截断——始终保留最新用户消息,优先丢弃更早的上下文行(实现见 content.ts)。命中后,Hindsight 端生成<memories>块并刷新系统提示词。
显式工具路径与自动召回共享同一套底层检索能力,但互不干扰:工具调用不会更新lastRecallSnippet,自动召回的<memories>/<mental_models>块也不会因工具调用而变化。
作用域模式:记忆按项目隔离的三种形态
recall 的检索范围由后端的作用域(scoping)配置决定,两个后端各有实现。
Hindsight bank 作用域
| 模式 | 行为 |
|---|---|
global | 无标签过滤,检索全局共享 bank |
per-project | 每个项目标签一个独立 bank id(git 主检出根目录 basename;仓库外则为 cwd basename) |
per-project-tagged | 共享 bank id +project:<项目标签>过滤器,tagsMatch = "any",使项目标签记忆与无标签全局记忆都能浮出 |
Hindsight 默认 scoping 为per-project-tagged(见 packages/coding-agent/src/hindsight/config.ts),且该模式下的 recall 携带recallTags与recallTagsMatch标签过滤参数。
Mnemopi bank 作用域
Mnemopi 无标签过滤召回能力,因此per-project-tagged被映射为"项目本地写 bank + 共享可召回 bank"的组合(见 packages/coding-agent/src/mnemopi/config.ts):
| 模式 | 行为 |
|---|---|
global | 只读共享 bank |
per-project | 读取由绝对 cwd basename + 绝对 cwd 哈希派生的 bank |
per-project-tagged | 同时读 cwd 派生项目 bank 与共享 bank,合并结果 |
Mnemopi 默认 scoping 为per-project。项目 bank id 的稳定性契约值得注意(config.ts):它仅由绝对 cwd 派生(<sanitized-basename>-<Bun.hash(projectRoot).toString(36)>),不依赖 git 根目录查找——早期版本基于 git 根解析,导致增删.git会改变 bank id 并碎片化记忆(issue #2412)。
此外,per-project模式还会执行安全旧 bank 扫描:探测<dbDir>/banks/下所有working_memory行都属于当前绝对 cwd 的 bank,将其加入召回集合以抢救旧方案下的记忆;扫描上限为 64 个候选目录(LEGACY_BANK_SCAN_LIMIT),缺失目录、不可读或损坏的 SQLite 文件会被静默跳过(config.ts)。
在per-project-tagged模式下,共享 bank 还可能收到一次额外的兜底查询:剥离项目 bank 的字面 token 后重新检索,使被项目术语"污染"的查询仍能命中宽泛的全局记忆(deriveSharedRecallFallbackQuery,见 mnemopi/state.ts)。
会话作用域
recall 读取跨会话的记忆数据,但使用当前会话缓存的配置与作用域。子代理(subagent)别名继承父代理的后端作用域:Hindsight 与 Mnemopi 的 alias 状态共享父级的 bank、scope、config 与 client(例如 hindsight/state.ts 中aliasOf字段的注释说明),因此子代理调用 recall/retain/reflect 会持久化到与父代理相同的 bank。
配置项与上限:一张表掌握全部开关
可用性前提
- 工具可用性要求
memory.backend为"hindsight"或"mnemopi";默认值为"off",即开箱默认不启用记忆检索。
Hindsight recall 相关设置
| 设置项 | 默认值 | 说明 |
|---|---|---|
hindsight.recallBudget | "mid" | 召回预算档位(low/mid/high) |
hindsight.recallMaxTokens | 1024 | 召回结果的最大 token 数 |
hindsight.recallTypes | ["world", "experience"] | 召回的记忆类型过滤 |
hindsight.recallTimeoutMs | 30_000 | recall 请求客户端截止时间(毫秒) |
环境变量(如HINDSIGHT_RECALL_BUDGET、HINDSIGHT_RECALL_MAX_TOKENS、HINDSIGHT_RECALL_TIMEOUT_MS)的优先级高于持久化设置(hindsight/config.ts 明确"env wins"),便于 CI/生产环境按 shell 覆盖。测试桩 memory-tools.test.ts 也以mid/1024/["world","experience"]/30_000作为默认值验证。
Mnemopi recall 相关设置
| 设置项 | 默认值 | 说明 |
|---|---|---|
mnemopi.recallLimit | 8 | 合并去重后返回的最大结果数,运行时钳制为至少 1 |
mnemopi.scoping | "per-project" | 作用域模式 |
| 内容预览上限 | 500字符/条 | 由RECALL_CONTENT_PREVIEW_CHARS定义 |
显式工具路径不应用的配置
以下四个配置只影响后端自动召回的查询组合,显式工具路径直接透传用户query,不适用:
hindsight.recallContextTurnshindsight.recallMaxQueryCharsmnemopi.recallContextTurnsmnemopi.recallMaxQueryChars
也就是说,自动召回会对查询做"多轮上下文拼接 + 字符截断",而工具调用则完全忠实于用户给出的查询文本(Mnemopiper-project-tagged的共享 bank 兜底查询除外)。
错误处理语义:失败的边界在哪里
- 后端未初始化:
memory.backend == "mnemopi"但无会话状态时抛Mnemopi backend is not initialised for this session.;Hindsight 同理抛Hindsight backend is not initialised for this session.(memory-recall.ts)。 - Hindsight 失败:HTTP、fetch 与超时失败统一转为
HindsightError;HTTP 错误携带statusCode与解析后的details。 - Mnemopi 失败:按目标 bank 逐个捕获并记录日志(
Mnemopi: scoped recall target failed)。健康的 bank 仍贡献结果;仅当所有目标都失败时:单目标失败抛出原始错误,多目标失败抛出带各 bank 详情的AggregateError,而不会静默转成空结果(mnemopi/state.ts)。 - 非 Error 异常归一化:工具捕获的非
Error失败统一转为new Error(String(err))后重抛。
此外,文档与源码都强调了一个自动召回侧的关键保障:Hindsight 自动召回引擎的失败不会逃逸到会话生命周期——maybeRecallOnAgentStart/beforeAgentStartPrompt内部吞掉异常并记日志(测试 memory-tools.test.ts 专门验证了"后台自动召回引擎失败不逃逸"与"agent-start 召回失败在其内部 bank 守卫之前被包含")。
与 retain、reflect 的分工
- 共享后端:recall、retain、reflect 共用同一个记忆后端,存储、子代理别名、bank 作用域、任务设定与心智模型行为详见 docs/tools/retain.md。
- 心智模型:recall不会主动拉取 Hindsight 心智模型(mental models)。后端会把
<mental_models>块独立于召回结果缓存在 agent 的开发者指令中,因此无需本工具重复获取。 - Mnemopi 的
<memories>块:可能由自动召回写入开发者指令;显式工具调用不会更新该块。 - 综合归纳:本工具只返回命中条目、不做跨条目的综合。需要远程 Hindsight 综合归纳时用
reflect;Mnemopi 的reflect变体是"本地召回 + 格式化"。
实战建议小结
- 配置启用:将
memory.backend设为"hindsight"(远程)或"mnemopi"(本地 SQLite),二者默认均为关闭("off");"local"也不注册 recall。 - 查询写作:
query是自然语言检索词,工具原样透传(自动召回才会拼接多轮上下文),因此在工具调用时尽量把关键实体、时间、项目名写进单条查询。 - 预览不等于全文:看到以
…结尾的 Mnemopi 预览后,先用read memory://<id>读全文,再决定是否memory_edit update。 - 按项目隔离:Hindsight 默认
per-project-tagged(共享 bank + 标签过滤),Mnemopi 默认per-project(cwd 派生 bank);需要跨项目全局记忆时切换作用域模式或依赖per-project-tagged的共享 bank 兜底查询。 - 超时与预算:Hindsight recall 超时默认 30s,结果 token 上限 1024,可通过
hindsight.recallTimeoutMs/hindsight.recallMaxTokens或对应HINDSIGHT_*环境变量调整;Mnemopi 结果条数默认 8 条(mnemopi.recallLimit)。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考