OpenClaw 接入 OpenViking Agent Experience Recall 设计
日期:2026-05-28
当前结论
OpenViking 已经负责从 session commit 后的轨迹中抽取 agent experience。本 PR 不重做经验抽取、不改 commit policy,也不新增“长期记忆/经验记忆是否抽取”的控制面。
本 PR 只做运行期使用面:
- 经验记忆功能默认关闭,必须显式配置
agentExperience.enabled: true才启用。 - 启用后,OpenClaw 在
transformContext assemble阶段判断当前 turn 是否像执行任务。 - 符合条件时,从
viking://user/memories/experiences检索 agent experience。 - 同一轮也保留原有长期记忆 auto recall,并把 experience 和长期记忆分成不同 section。
- 使用统一的
<openviking-context>外壳注入到 latest user message 前;即使只有长期记忆,也使用这个外壳。 afterTurn写 session 前剥离<openviking-context>,避免把召回内容再次写回 OV。
这是一版“OpenClaw 能用 OV 经验记忆”的基准实现,不是完整的多 hook experience system。
不在本 PR 中处理的事
以下内容明确不属于本 PR:
- 不新增或修改 OV 服务端的 trajectory -> experience 抽取逻辑。
- 不新增 commit 时的 extraction policy。一次 commit 是否抽长期记忆、是否抽 experience,由 OV core 后续控制面解决。
- 不改变现有
afterTurn()session capture 主链路。 - 不改变现有
compact()/commitSession()触发策略。 - 不新增 OpenClaw host hook。
- 不实现 skill load / subagent start / write preflight 的自动注入。
- 不新增
experience_recall工具。 - 不把 experience 和普通长期记忆混成同一种记忆格式。
为什么接在 transformContext assemble
OpenClaw 插件当前有两类 assemble:
- preflight assemble:用于回读 OV session context,把已有 session 上下文给 OpenClaw。
- transformContext assemble:LLM 看到消息前最后一次改写消息上下文。
经验记忆是“执行前提醒”,应该发生在 LLM 决策前。当前 OpenClaw 插件没有 MemOS 那种 before_prompt_build hook,也没有 before_subagent_start / 可中断 before_tool_call。因此当前可落地的正确位置是 transformContext assemble。
当前代码路径:
examples/openclaw-plugin/context-engine.ts
assemble()
-> isTransformContextAssemble
-> latestMessage.role === "user"
-> prepareRecallQuery()
-> shouldRecallAgentExperience()
-> buildAgentExperienceRecallContext()
-> buildLongTermMemoryRecallContext()
-> buildOpenVikingContextBlock()
-> prependRecallToLatestUserMessage()当前 PR 的文件改动
examples/openclaw-plugin/config.ts
新增 agentExperience 配置,并在 parse 阶段补齐默认值:
agentExperience?: {
enabled?: boolean;
recallLimit?: number;
scoreThreshold?: number;
maxInjectedChars?: number;
minQueryChars?: number;
};默认值:
{
enabled: false,
recallLimit: 3,
scoreThreshold: 0.35,
maxInjectedChars: 6000,
minQueryChars: 12,
}enabled 默认是 false,这是保守发布开关:默认不触发 experience recall,也不增加额外检索请求。用户确认服务端 experience 数据质量和注入效果后,再显式打开。
ParsedMemoryOpenVikingConfig 是 schema parse 后的运行期类型,重点是让 agentExperience 的子字段也变成 required,避免 TypeScript 仍然认为 recallLimit / maxInjectedChars 可能是 undefined。
examples/openclaw-plugin/auto-recall.ts
新增经验召回相关逻辑,但仍放在 auto-recall.ts 中。原因是当前插件还没有统一 recall source 目录体系,经验召回和长期记忆召回共享 query preparation、timeout、token estimate、context block 构造等基础设施。
核心新增/调整:
OPENVIKING_CONTEXT_TAG = "openviking-context"ExperienceRecallTriggershouldRecallAgentExperience()isCronSession()buildAgentExperienceRecallContext()buildOpenVikingContextBlock()buildLongTermMemorySection()buildLongTermMemoryRecallContext()过滤掉 experience memory,避免 experience 出现在长期记忆区;同时生成Long-term Memoriessection,交给统一外壳注入。
经验召回复用配置项 autoRecallTimeoutMs,不单独开 experience timeout;默认值仍是 5000ms。
examples/openclaw-plugin/context-engine.ts
在 transformContext assemble 中接入两路 recall:
experience recall
-> 只查 viking://user/memories/experiences
-> 受 agentExperience 配置和 shouldRecallAgentExperience 控制
long-term recall
-> 查 viking://user/memories
-> recallResources=true 时查 viking://resources
-> 过滤掉 experiences然后统一生成 <openviking-context> 外壳:
const combinedBlock = buildOpenVikingContextBlock({
sections: [experienceRecall.block, recall.section],
});如果没有 experience 命中,普通长期记忆仍然作为 Long-term Memories section 放入 <openviking-context>。如果两个结果都为空,直接 passthrough,不改写 messages。
examples/openclaw-plugin/text-utils.ts
新增剥离:
<openviking-context ...>...</openviking-context>并保留历史格式清理:
<relevant-memories>...</relevant-memories>这样 afterTurn() 写 session 前会清理当前格式和历史格式注入块,避免本轮召回内容进入下一轮抽取。运行时新注入统一使用 <openviking-context>。
tests
PR 中保留单元测试:
tests/ut/agent-experience-recall.test.tstests/ut/context-engine-assemble.test.tstests/ut/context-engine-afterTurn.test.tstests/ut/text-utils.test.ts
tests/integration/test_openclaw_openviking_strict_e2e.py 只作为本地严格联调脚本保留,不进入远端 PR。
运行时流程
1. 进入 assemble
当前只在 transformContext assemble 阶段自动注入。前置条件:
- session 没有被
bypassSessionPatterns命中。 - latest message 是 user。
cfg.autoRecall或cfg.agentExperience.enabled至少一个开启。- latest user message 没有已经包含
<openviking-context>。 prepareRecallQuery()清理后的 query 非空,且长度至少 5。
如果以上任一条件不满足,直接 passthrough。
2. 生成 query
query 来自 latest user message:
const recallQuery = prepareRecallQuery(extractAgentMessageText(latestMessage));prepareRecallQuery() 会先调用 sanitizeUserTextForCapture(),因此旧的注入块、sender metadata、conversation metadata 等噪音不会进入 recall query。query 最长 4000 字符,超出会截断并写日志。
3. 判断是否要查 experience
经验召回首先受总开关控制:agentExperience.enabled 默认关闭。只有显式开启后,才会进入 shouldRecallAgentExperience()。这个 task gate 是内置逻辑,不再提供单独配置开关。
硬跳过:
- session bypass。
- query 为空或短于
agentExperience.minQueryChars。 - latest user text 已包含
<openviking-context>。
强制召回:
triggerHint不是task_start,例如cron_start。sessionKey包含:cron:,或runtimeContext.isCron === true,或runtimeContext.automationKind === "cron"。
普通 task gate 使用确定性打分:
+3 write/edit/modify/delete/migrate/deploy/release/configure/patch 等副作用动作
+2 fix/debug/test/build/run/implement/refactor/integrate/troubleshoot 等执行动作
+2 error/exception/failed/retry/traceback/test failed 等失败信号
+2 文件路径、代码对象、hook/API/tool/package/module 等工程对象
+1 经验/踩坑/最佳实践/avoid/best practice/lesson/pitfall 等经验意图
-3 闲聊、翻译、总结当前对话等非执行场景
-2 纯知识问答,并且没有工程对象和执行动词score >= 3 才自动查 experience。
4. 检索 experience
经验召回只查 agent experience 目录:
client.find(queryText, {
targetUri: "viking://user/memories/experiences",
limit: Math.max(expCfg.recallLimit * 4, 12),
scoreThreshold: expCfg.scoreThreshold,
}, agentId)后处理:
- 只保留 URI/category 看起来是 experience 的结果。
- 按 URI 去重。
- 截到
agentExperience.recallLimit条,默认 3。 level === 2时用client.read()读取完整内容;否则使用 abstract / overview / uri。- 只渲染结构化 experience,或者 metadata/URI 明确标记为 experience 的内容。
- 总注入字符不超过
agentExperience.maxInjectedChars。 - 受
autoRecallTimeoutMs控制;超时或失败只 warn,不阻塞 OpenClaw。
当前不查 raw trajectories。
5. 渲染 experience
OV experience 当前主要结构是:
## Situation
...
## Approach
...
## Reflect
...OpenClaw 注入时映射为更适合执行期阅读的字段:
### Experience: <filename>
Source: <uri>
Score: <score>
Trigger:
- from Situation
Do:
- from Approach
Avoid:
- from Reflect
Scope:
- from Situation
Check:
- from Reflect or Approach如果原文缺某个字段,会用摘要或固定 fallback 补齐,避免给 LLM 一个空标题。
6. 检索长期记忆
原有 auto recall 保留,但输出不再直接包 <relevant-memories>。它现在只生成 ## Long-term Memories section,再交给统一外壳。transformContext assemble 和非 transformContext assemble 都统一注入 <openviking-context>。
搜索范围:
viking://user/memoriesviking://resources,仅recallResources=true
后处理:
- 合并结果并按 URI 去重。
- 只保留 leaf memory。
- 过滤掉 experience memory,避免和
Agent Experiences重复。 - 使用现有
postProcessMemories()/pickMemoriesForInjection()排序和截断。 - 使用
recallMaxInjectedChars做完整条目预算控制,单条记忆不截半。
7. 注入外壳
长期记忆单独注入时也使用统一外壳:
<openviking-context>
## Long-term Memories
Source: openviking-auto-recall
The following OpenViking memories may be relevant:
- [profile] ...
</openviking-context>
<original latest user message>同时注入 agent experience 和长期记忆时:
<openviking-context>
## Agent Experiences
These are prior execution lessons learned by this agent. Use them as task guidance, not as user facts.
### Experience: openclaw-plugin-file-write-guard
Source: viking://user/default/memories/experiences/openclaw-plugin-file-write-guard.md
Score: 0.910
Trigger:
- 当修改 OpenClaw 插件 afterTurn 写回逻辑时。
Do:
- 在写回 OV session 前剥离注入上下文块。
Avoid:
- 避免把注入经验再次写回 transcript。
Scope:
- 当修改 OpenClaw 插件 afterTurn 写回逻辑时。
Check:
- 避免把注入经验再次写回 transcript。
## Long-term Memories
Source: openviking-auto-recall
The following OpenViking memories may be relevant:
- [profile] ...
</openviking-context>
<original latest user message>规则:
- 外壳统一用
<openviking-context>,表达“OpenViking 注入的上下文”,不绑定 OpenClaw/VikingBot/Codex 任一消费方。 - 内部用 Markdown section 区分经验记忆和长期记忆。
Agent Experiences在前,因为它影响执行策略。Long-term Memories在后,因为它更多是用户事实、偏好、资源。- 没有命中的 section 直接省略。
- 两个 section 都没有时,不注入任何东西。
- 清理逻辑兼容历史
<relevant-memories>和当前<openviking-context>。
为什么 experience block 和 long-term block 不合成一个 section
它们都属于 recall,但语义不同:
- long-term memory 是事实/偏好/资源,回答时可以当作上下文事实。
- agent experience 是执行策略/踩坑/验证方式,只能当作任务指导,不能当成用户事实。
如果混在同一个 bullet list 里,模型容易把“以前修 bug 的做法”当成“当前用户事实”。所以它们共享 <openviking-context> 外壳,但必须分 section。
与 MemOS 的关系
MemOS OpenClaw adapter 的自动注入点是 before_prompt_build,返回 { prependContext }。它不是每个 tool 调用前无脑注入,也不是 subagent 启动时一定注入。
OV OpenClaw 当前没有这个 hook,但 transformContext assemble 在语义上等价于“LLM prompt 构造前最后一次上下文改写”。所以本 PR 采用 transformContext assemble。
MemOS 值得参考的是三点:
- 用一个外层 block 包住注入内容,便于清理。
- 经验和普通记忆分区展示。
- 经验要渲染成行动指导,而不是原始轨迹。
本 PR 不复刻 MemOS 的 L1 Trace / L2 Policy / L3 World Model / Skill 层级。OV 服务端已经负责 trajectory 和 experience 的沉淀,OpenClaw 插件只消费结果。
与 VikingBot 的关系
VikingBot 目前已有多个经验注入点:
- skill 读取后追加
## Related Experiences - subagent task 前追加
## Agent Experience - write tool 前检测写类工具,插入
## Relevant Agent Experience - cron/benchmark 场景通过任务 prompt 读取 experience
这些是 VikingBot 内部 agent loop 的直接拼接逻辑,不是一个跨插件的公共 envelope。
因此新外壳不应该叫 vikingbot-context,也不应该叫 openclaw-context。<openviking-context> 更适合作为 OV 面向不同 agent 插件的统一注入外壳。未来 VikingBot 如果迁移到公共插件协议,也可以选择复用这个 envelope;但本 PR 不改 VikingBot。
后续更精确接入点
下列入口从原理上适合注入 experience,但需要 OpenClaw host 暴露更精确的 hook 或可中断控制面。当前 PR 不实现。
Skill 加载时
适合原因:agent 已经选择读取某个 SKILL.md,此时用 skill name/description 查经验,注入到 skill 内容旁边,相关性比全局 prompt 更高。
需要 host 支持:
tool_result_persist / after_tool_call
-> 允许插件修改 read_file(SKILL.md) 返回给 LLM 的 tool result插件逻辑:
read_file(SKILL.md)
-> parse name/description
-> query experiences
-> append "Related Experiences" to this tool result onlySubagent 启动时
适合原因:subagent 是冷启动,最需要携带“类似子任务过去怎么做”的经验。
需要 host 支持:
before_subagent_start(event: {
task?: string;
mission?: string;
profile?: string;
childSessionKey?: string;
}) -> { prependContext?: string }如果 host 只有 subagent_spawned,那通常已经太晚,只能记录元数据,不能保证改到子 agent 初始 prompt。
Write 类工具调用前
适合原因:写文件、改文件、删除文件是副作用动作,经验最能减少“写了又改”的情况。
但正确实现必须支持 cancel/replan:
LLM 准备调用 write_file/edit_file
-> plugin 查经验
-> 若命中,取消当前 tool call
-> 把经验作为 user/context message 插入
-> 重新让 LLM 决定是否还要写、怎么写如果 host 只允许不可变的 before_tool_call,那这点不能正确做。继续执行原 tool call 意味着经验来得太晚。
Cron 重复任务
cron 原理上适合注入 experience,因为任务重复、经验命中率高。
当前 PR 已经支持在 transformContext assemble 中识别:
sessionKey includes ":cron:"
runtimeContext.isCron === true
runtimeContext.automationKind === "cron"识别后 trigger 是 cron_start,并绕过普通 task gate。前提是该 cron session 没被 bypassSessionPatterns 跳过。不要默认把 cron 加入 bypass。
如果未来 host 暴露 automation name/prompt,query 应该从当前 latest user message 扩展为:
sessionKey
automation name
automation prompt
latest task text
recent failure/success summary清理与防污染
经验召回最容易出的问题是自我污染:本轮注入的经验被当成本轮用户输入写回 session,下次 commit 又把它抽成新的经验。
当前 PR 的防线:
- assemble 前检查 latest user message 是否已有
<openviking-context>,有则不重复注入。 sanitizeUserTextForCapture()会剥离:<openviking-context>- 历史
<relevant-memories>
afterTurn()写 session 前走现有文本清理路径,因此注入块不会写回 OV。- long-term recall 过滤 experience URI/category,避免 experience 被长期记忆 section 再注入一次。
测试覆盖
当前 PR 的单元测试覆盖:
- 普通知识问答不会触发 experience recall。
- 执行型任务会触发 experience recall。
- cron trigger 会强制 recall。
- experience memory 不会出现在
Long-term Memoriessection。 - 已存在
<openviking-context>会阻止重复注入。 <openviking-context>/ 历史<relevant-memories>会被清理。- afterTurn 写 session 前会剥离注入块。
本地严格 e2e 脚本保留在工作区,但不进入远端 PR。
验收标准
合并前应满足:
npm run typecheck通过。npm run build通过。npm run test通过。- PR diff 不包含
tests/integration/test_openclaw_openviking_strict_e2e.py。 - 默认配置下不会触发 experience recall;必须显式设置
agentExperience.enabled: true。 - 默认关闭 experience 时,普通长期记忆 auto recall 也使用
<openviking-context>外壳。 - 普通问答不会因为默认配置无脑注入 experience。
- 执行型任务在有相关 experience 命中时注入
Agent Experiencessection。 - 同一 block 中长期记忆和经验记忆分区清晰。
- 注入块不会被写回 OV session。
最终边界
OpenClaw 插件负责:
- 判断当前 turn 是否值得使用 agent experience
- 检索 viking://user/memories/experiences
- 渲染 Agent Experiences section
- 与 Long-term Memories 共同放入 <openviking-context>
- 写 session 前清理注入块
OpenViking 服务端负责:
- session commit
- trajectory 抽取
- experience 生成和更新
- memory vectorization
- 未来的抽取 policy / world model / skill 演进