oh-my-pi 的 learn 工具:把一次性调试经验沉淀为长期记忆与可复用技能
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
导读
在长会话式 Coding Agent 的日常工作中,最难积累的资产不是代码,而是"吃过一次亏才知道"的经验:一个非显而易见的修复、一条项目约定、一套最终跑通的工作流。oh-my-pi(@oh-my-pi/pi-coding-agent)提供的learn工具正是为此设计的——它在一次调用内把可复用的经验写入长期记忆,并在需要时同步铸造或增强一个受管(managed)技能。读完本文,你将掌握learn工具的触发时机、memory/context/skill参数的完整语义、三种记忆后端(local / mnemopi / hindsight)的落盘差异,以及技能命名、隔离与安全边界背后的源码实现原理。
1.learn工具是什么:从"解决完问题"到"沉淀下答案"
learn工具在 oh-my-pi 中扮演"经验捕获器"的角色。它的官方定义(见 learn.md)只有一句话但信息密度极高:
Capture reusable lessons in long-term memory; optionally mint/enhance a managed skill in the same call.
即:捕获可复用的经验到长期记忆;在同一调用中可选地铸造或增强一个受管技能。
关键在"reusable"(可复用)一词。learn不是随便记录任何对话,而是要求在解决了一个"很可能再次带来回报"的问题之后使用,文档给出了三类典型场景:
- 非显而易见的修复(non-obvious fix):比如某个第三方库的坑、某个平台特定的行为,下次遇到同样的错误可以少走弯路;
- 发现的项目约定(discovered project convention):例如"本仓库的测试必须用
bun test而不是jest""提交前需要跑gen-clippy-bazelrc"这类代码里不会明说、但违反就会出问题的规则; - 最终跑通的工作流(workflow that worked):一套多步骤的操作序列,例如"升级依赖后必须同时更新 Cargo.lock 与 bun.lock"。
从实现上看,learn是一个标准的 AgentTool,其注册与门控逻辑位于 tools/learn.ts:
static createIf(session: ToolSession): LearnTool | null { if (!session.settings.get("autolearn.enabled")) return null; const backend = session.settings.get("memory.backend"); if (backend !== "hindsight" && backend !== "mnemopi" && backend !== "local") return null; return new LearnTool(session); }这意味着工具并非总是可用:它要求同时满足两个前提——autolearn.enabled开启,且memory.backend处于local、mnemopi或hindsight三者之一。默认配置下autolearn.enabled为false(见 settings-schema.ts),因此learn属于"按需启用、零默认足迹"的实验性能力。
2. 调用时机:什么时候该用,什么时候不该用
原文档给出了清晰的使用判据:在解决了一个"很可能再次带来回报"的洞见之后使用。反过来说,learn不该被当作普通日志或聊天记录来用。
判断是否值得捕获,可以自问三个问题:
- 这个经验会不会在另一个任务、另一个项目阶段再次出现?不会复用的,不值得占用记忆预算;
- 它是否足够具体?"记得多用异步"是模糊的建议,而"在 A 包大于 1GB 时改用流式 API 避免 OOM"才是可执行的教训;
- 它是事实还是流程?事实("服务器地址是 x")直接进记忆即可;只有值得固化成
SKILL.md的可重复过程,才需要附带skill参数。
原文档最后一句给出了取舍原则,也是全文最核心的纪律:
Capture sparingly, specifically: one strong reusable lesson > several vague ones.
少而精地捕获:一条强而具体的可复用经验,胜过好几条模糊的经验。这不仅是提示词层面的建议,也反映在实现上——本地后端对learned.md的条目数量有硬性上限(见下文第 5 节),模糊、重复的捕获会被去重和淘汰机制自然过滤。
3. 参数详解:memory、context与可选的skill
learn工具的 JSON Schema 定义在 tools/learn.ts,共三个参数:
| 参数 | 类型 | 必填 | 语义 |
|---|---|---|---|
memory | string | 是 | 需要记住的、自包含(durable, self-contained)的经验,应说明 what / when / why |
context | string | 否 | 该经验的可选来源上下文(项目、场景、命令等) |
skill | object | 否 | 在同一调用中创建或更新一个受管技能 |
3.1memory:写出"自包含"的经验
memory的约束是self-contained(自包含)——即脱离了当前会话上下文,单独读起来依然成立。原文档要求它涵盖 what / when / why 三个维度:
- what:发现了什么、怎么做;
- when:在什么情况下适用;
- why:为什么这样做是对的(避免后人把结论当成教条)。
例如一条合格的memory长这样:
当修改本仓库 Rust crate 的公共 API 时,必须同步更新
Cargo.lock与bun.lock,否则 CI 的 bazel 构建会因版本漂移失败(bazel 与 cargo 的依赖解析相互独立)。
3.2context:轻量来源标注
context用于记录这条经验从哪里来(如某个模块、某次事故、某个构建脚本),方便日后回溯。在本地后端中,它会被内联进条目渲染为_(context: ...)_后缀(详见第 5 节)。
3.3skill:把流程固化为SKILL.md
skill是可选参数,且文档明确限定:只为"值得固化为SKILL.md的可重复过程"提供,而不是为事实提供。一条事实("公司代理端口是 8080")用memory就够了;一套"设置序列 / 调试配方 / 项目专属工作流"才需要技能化。
skill对象包含四个字段:
| 字段 | 类型 | 说明 |
|---|---|---|
action | "create" \| "update" | 新建或覆盖更新 |
name | string | kebab-case 技能名(小写字母、数字、连字符) |
description | string | 一行说明"何时使用该技能",用于技能发现 |
body | string | SKILL.md正文(不含 frontmatter) |
注意body的约束:frontmatter 由系统根据name与description自动生成,调用方只需提供纯 Markdown 正文。这一设计保证了机器生成的技能文件格式一致,也避免了调用方注入任意 frontmatter 字段。
4. 一次调用的完整执行链路
从 tools/learn.ts 的实现看,LearnTool.execute分两个阶段:
第一阶段:持久化经验到长期记忆(必然执行)。根据当前memory.backend走不同分支:
mnemopi(本地 SQLite 后端):调用state.rememberScoped,以importance: 0.8、source: "coding-agent-learn"、memoryType: "fact"写入,并携带session_id、cwd、context等元数据;若后端未初始化或写入失败(返回空 id),工具会显式抛错而不是静默丢弃;local(文件后端):调用localBackend.save走saveLearnedLesson管道(详见第 5 节);若清洗后内容为空(stored === 0),同样抛错;hindsight(远程记忆服务):调用state.enqueueRetain将经验排队交给后台保留管道,返回信息为 "Lesson queued for retention"。
第二阶段:可选地铸造/增强受管技能(失败不致命,但会如实报告)。当调用带skill时,先做两道前置校验:
- 名称清洗:
sanitizeSkillName校验名称是否符合^[a-z0-9][a-z0-9-]{0,63}$(小写字母/数字/连字符,1~64 字符),非法名称直接拒绝; - 作者技能冲突检查:
isNameClaimedByAuthoredSkill检查该名称是否已被用户手写的技能占用。由于受管技能在发现时的优先级低于作者技能,若强行创建同名受管技能,写出的文件永远不会被呈现——工具此时会返回错误,提示"换个名字"而不是谎报成功(tools/learn.ts)。
随后调用共享原语writeManagedSkill完成写入,并按action返回 "Created" 或 "Updated" 的确认信息。若技能写入失败,错误消息会同时说明"经验已存储/排队,但技能未能写入",保证调用方不会误以为整个操作失败。
此外,LearnTool的审批分级值得注意(tools/learn.ts):带skill载荷、或后端为local时,审批级别为write;否则为read。这是因为纯远程后端的记忆写入是"排队"性质的轻操作,而写文件(技能或learned.md)需要显式写权限。
5. 本地后端:learned.md的存储与读回机制
当memory.backend为local时,经验被写入项目记忆根目录下的learned.md文件(memories/index.ts)。该文件刻意与后台汇总产物(memory_summary.md、MEMORY.md、skills/)分离,保证汇总管线永远不会覆盖手动捕获的经验。
5.1 写入时的规范化管道
每条经验落盘前要经过 normalizeLearnedText 的三步处理:
- 注入中和(neutralizeInjection):剔除控制/格式字符、尖括号(
</skills>、<system-directive>)、反引号和~~~围栏。因为learned.md的内容会原样渲染进后续会话的系统提示词,必须防止经验文本里夹带提示词注入载荷; - 密钥脱敏(redactSecrets):替换疑似 token(含
ghp_等供应商令牌前缀)为[REDACTED]。顺序上有讲究——先中和再脱敏,避免分隔符被剥离后把 token 重新拼起来绕过正则; - 长度封顶(boundChars):
memory内容上限2000 字符、context上限400 字符,截断时会处理末位未配对的 Unicode 高代理项,避免产生损坏字符。
5.2 文件级约束
- 最新在前 + 精确去重:同一经验重复捕获不会产生重复条目;
- 100 条上限(
MAX_LEARNED_LESSONS):超出时淘汰最旧的条目,控制文件按条数增长; - 并发安全:
learnedWriteChains按文件路径串行化"读-改-写",同一轮次内并行调用(如多个子代理同时learn)不会互相覆盖(memories/index.ts); - 保留手工结构:对已经存在头部、散文、分节标题的
learned.md,追加操作只触碰-开头的列表条目,新条目进入第一个列表段的头部,标题与正文的相对位置不变——你可以放心手工维护这个文件。
测试用例对这些行为有完整覆盖,例如 autolearn-learn-local.test.ts 验证了空白归一化与 context 内联、L47-L64 验证了密钥脱敏、L74-L83 验证了 100 条上限、L261-L298 验证了手工结构保持与字节幂等性。
5.3 读回与注入预算
读回时(buildMemoryToolDeveloperInstructions),learned.md的条目会与memory_summary.md合并注入系统提示词,且两者共享summaryInjectionTokenLimit预算(memories/index.ts):先按 token 预算截断汇总,剩余预算才分配给经验列表。若汇总已耗尽预算,经验条目会被丢弃而非撑爆上下文——这正是"少而精"原则在系统层面的强制保证。读回时还会再次执行中和与脱敏,因此手工编辑过的learned.md即使夹带危险内容,也不会泄漏进提示词(autolearn-learn-local.test.ts)。
6. 受管技能:隔离目录、命名与安全边界
learn的skill参数写入的是受管技能(managed skill),其全部文件操作被限制在独立目录~/.omp/agent/managed-skills,与用户手写技能目录~/.omp/agent/skills严格隔离。原文档强调了两条铁律:
Managed skills: isolated
~/.omp/agent/managed-skills; surfaced as normal skills next session; NEVER touch user-authored skills.
即:受管技能下一次会话会像普通技能一样被发现和呈现;但 Agent永远不得触碰用户手写的技能。这一隔离体现在 autolearn/managed-skills.ts 的多层防护上:
- 提供者标记:受管技能被标记为
omp-managed提供者,与作者技能区分; - 名称白名单:
SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/,禁止..、斜杠、空名和大写,从源头杜绝路径逃逸; - 描述消毒:
sanitizeManagedDescription在写入和读取两个方向都剥离控制字符、<>、反引号与~~~,防止机器生成的描述在未来的会话中破坏<skills>列表结构; - frontmatter 自动生成:
toSkillFrontmatter用 YAML 序列化name与消毒后的description,形成标准---\n...\n---头部; - 64KB 大小上限:
MAX_MANAGED_SKILL_BYTES = 64_000,按最终文件 UTF-8 字节数(含 frontmatter)校验,防止一次生成把技能文件写爆; - 写入防伪:
create使用wx标志(O_CREAT|O_EXCL)原子创建、文件已存在即失败;update使用O_NOFOLLOW打开,且拒绝符号链接文件与硬链接数 >1 的文件——防止把写入重定向到用户技能或其他文件(managed-skills.ts); - 根目录防符号链接:写入前
lstat检查managed-skills根与技能子目录不是符号链接,杜绝"合法名称经符号链接写到目录外"; - 同名单次串行化:
serializeSkillMutation让同一技能名的多次变更(如同一轮里 create 与 update 并发)按提交顺序执行,不同技能名仍可并行。
6.1create与update的语义差异
| action | 行为 | 失败条件 |
|---|---|---|
create | 原子新建SKILL.md | 技能已存在(EEXIST 显式报错) |
update | 覆盖正文(frontmatter 由 name/description 重新生成) | 技能不存在 |
两个动作都要求description与body非空(空的 description 会被发现扫描静默丢弃,工具因此会在写入前直接拒绝)。这与manage_skill工具(manage-skill.md)共享同一套writeManagedSkill原语,行为完全一致,manage_skill还额外支持delete。
7. 与 Auto-Learn 体系的关系及配置
learn是 oh-my-pi Auto-Learn(实验性)体系中的一个工具。会话停止后,系统会根据 autolearn-guidance.md 的指引提示 Agent 捕获经验——learn是其中的"手动、即时"通道,manage_skill则是"构建可复用技能库"的通道。
相关配置项集中在 settings-schema.ts:
| 配置 | 类型 | 默认 | 说明 |
|---|---|---|---|
autolearn.enabled | boolean | false | 总开关;关闭时learn/manage_skill均不可用(createIf返回 null) |
autolearn.autoContinue | boolean | false | 停止时自动运行一次私有捕获回合(额外消耗 token);关闭则仅保留常驻 Auto-Learn 提示 |
autolearn.minToolCalls | number | 5 | 触发捕获提示所需的最少工具调用次数(仅配置文件可设) |
memory.backend | enum | off | off/local/hindsight/mnemopi/sharpshooter;仅前三者支持learn |
一个实用的组合是:memory.backend: "local"+autolearn.enabled: true。此时无需任何外部服务,经验会落到项目记忆根的learned.md,技能落到~/.omp/agent/managed-skills,全程本地文件、零网络依赖,也便于直接查看和手工维护。
8. 最佳实践清单
综合原文档与实现,使用learn工具时应遵循以下纪律:
- 只在洞见可能再次变现时调用——非显而易见修复、项目约定、跑通的工作流;
- 一条强经验胜过多条弱经验——
memory写自包含(what/when/why),宁缺毋滥; - 事实进
memory,流程进skill——只有可重复过程才值得SKILL.md化; skill.name用 kebab-case(小写字母/数字/连字符,1~64 字符),description写清"何时使用",body不带 frontmatter;- 不要与用户手写技能重名——受管技能永远无法覆盖作者技能,冲突时换个名字;
local后端可放心查看与手工编辑learned.md——读回时会再次消毒,结构也能被保留,但内容最终会渲染进未来会话的提示词,注意别写入不需要长期保留的信息。
掌握了这几点,你就拥有了让 Coding Agent "越用越懂你的项目"的完整闭环:解决问题 →learn捕获 → 跨会话注入 → 技能化复用。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考