docs/design/openclaw-agent-experience-memory-design.md
日期:2026-05-28
OpenViking 已经负责从 session commit 后的轨迹中抽取 agent experience。本 PR 不重做经验抽取、不改 commit policy,也不新增“长期记忆/经验记忆是否抽取”的控制面。
本 PR 只做运行期使用面:
agentExperience.enabled: true 才启用。transformContext assemble 阶段判断当前 turn 是否像执行任务。viking://user/memories/experiences 检索 agent experience。<openviking-context> 外壳注入到 latest user message 前;即使只有长期记忆,也使用这个外壳。afterTurn 写 session 前剥离 <openviking-context>,避免把召回内容再次写回 OV。这是一版“OpenClaw 能用 OV 经验记忆”的基准实现,不是完整的多 hook experience system。
以下内容明确不属于本 PR:
afterTurn() session capture 主链路。compact() / commitSession() 触发策略。experience_recall 工具。OpenClaw 插件当前有两类 assemble:
经验记忆是“执行前提醒”,应该发生在 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()
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 Memories section,交给统一外壳注入。经验召回复用配置项 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>。
PR 中保留单元测试:
tests/ut/agent-experience-recall.test.tstests/ut/context-engine-assemble.test.tstests/ut/context-engine-afterTurn.test.tstests/ut/text-utils.test.tstests/integration/test_openclaw_openviking_strict_e2e.py 只作为本地严格联调脚本保留,不进入远端 PR。
当前只在 transformContext assemble 阶段自动注入。前置条件:
bypassSessionPatterns 命中。cfg.autoRecall 或 cfg.agentExperience.enabled 至少一个开启。<openviking-context>。prepareRecallQuery() 清理后的 query 非空,且长度至少 5。如果以上任一条件不满足,直接 passthrough。
query 来自 latest user message:
const recallQuery = prepareRecallQuery(extractAgentMessageText(latestMessage));
prepareRecallQuery() 会先调用 sanitizeUserTextForCapture(),因此旧的注入块、sender metadata、conversation metadata 等噪音不会进入 recall query。query 最长 4000 字符,超出会截断并写日志。
经验召回首先受总开关控制:agentExperience.enabled 默认关闭。只有显式开启后,才会进入 shouldRecallAgentExperience()。这个 task gate 是内置逻辑,不再提供单独配置开关。
硬跳过:
agentExperience.minQueryChars。<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。
经验召回只查 agent experience 目录:
client.find(queryText, {
targetUri: "viking://user/memories/experiences",
limit: Math.max(expCfg.recallLimit * 4, 12),
scoreThreshold: expCfg.scoreThreshold,
}, agentId)
后处理:
agentExperience.recallLimit 条,默认 3。level === 2 时用 client.read() 读取完整内容;否则使用 abstract / overview / uri。agentExperience.maxInjectedChars。autoRecallTimeoutMs 控制;超时或失败只 warn,不阻塞 OpenClaw。当前不查 raw trajectories。
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 一个空标题。
原有 auto recall 保留,但输出不再直接包 <relevant-memories>。它现在只生成 ## Long-term Memories section,再交给统一外壳。transformContext assemble 和非 transformContext assemble 都统一注入 <openviking-context>。
搜索范围:
viking://user/memoriesviking://resources,仅 recallResources=true后处理:
Agent Experiences 重复。postProcessMemories() / pickMemoriesForInjection() 排序和截断。recallMaxInjectedChars 做完整条目预算控制,单条记忆不截半。长期记忆单独注入时也使用统一外壳:
<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 任一消费方。Agent Experiences 在前,因为它影响执行策略。Long-term Memories 在后,因为它更多是用户事实、偏好、资源。<relevant-memories> 和当前 <openviking-context>。它们都属于 recall,但语义不同:
如果混在同一个 bullet list 里,模型容易把“以前修 bug 的做法”当成“当前用户事实”。所以它们共享 <openviking-context> 外壳,但必须分 section。
MemOS OpenClaw adapter 的自动注入点是 before_prompt_build,返回 { prependContext }。它不是每个 tool 调用前无脑注入,也不是 subagent 启动时一定注入。
OV OpenClaw 当前没有这个 hook,但 transformContext assemble 在语义上等价于“LLM prompt 构造前最后一次上下文改写”。所以本 PR 采用 transformContext assemble。
MemOS 值得参考的是三点:
本 PR 不复刻 MemOS 的 L1 Trace / L2 Policy / L3 World Model / Skill 层级。OV 服务端已经负责 trajectory 和 experience 的沉淀,OpenClaw 插件只消费结果。
VikingBot 目前已有多个经验注入点:
## Related Experiences## Agent Experience## Relevant Agent Experience这些是 VikingBot 内部 agent loop 的直接拼接逻辑,不是一个跨插件的公共 envelope。
因此新外壳不应该叫 vikingbot-context,也不应该叫 openclaw-context。<openviking-context> 更适合作为 OV 面向不同 agent 插件的统一注入外壳。未来 VikingBot 如果迁移到公共插件协议,也可以选择复用这个 envelope;但本 PR 不改 VikingBot。
下列入口从原理上适合注入 experience,但需要 OpenClaw host 暴露更精确的 hook 或可中断控制面。当前 PR 不实现。
适合原因: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 only
适合原因:subagent 是冷启动,最需要携带“类似子任务过去怎么做”的经验。
需要 host 支持:
before_subagent_start(event: {
task?: string;
mission?: string;
profile?: string;
childSessionKey?: string;
}) -> { prependContext?: string }
如果 host 只有 subagent_spawned,那通常已经太晚,只能记录元数据,不能保证改到子 agent 初始 prompt。
适合原因:写文件、改文件、删除文件是副作用动作,经验最能减少“写了又改”的情况。
但正确实现必须支持 cancel/replan:
LLM 准备调用 write_file/edit_file
-> plugin 查经验
-> 若命中,取消当前 tool call
-> 把经验作为 user/context message 插入
-> 重新让 LLM 决定是否还要写、怎么写
如果 host 只允许不可变的 before_tool_call,那这点不能正确做。继续执行原 tool call 意味着经验来得太晚。
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 的防线:
<openviking-context>,有则不重复注入。sanitizeUserTextForCapture() 会剥离:
<openviking-context><relevant-memories>afterTurn() 写 session 前走现有文本清理路径,因此注入块不会写回 OV。当前 PR 的单元测试覆盖:
Long-term Memories section。<openviking-context> 会阻止重复注入。<openviking-context> / 历史 <relevant-memories> 会被清理。本地严格 e2e 脚本保留在工作区,但不进入远端 PR。
合并前应满足:
npm run typecheck 通过。npm run build 通过。npm run test 通过。tests/integration/test_openclaw_openviking_strict_e2e.py。agentExperience.enabled: true。<openviking-context> 外壳。Agent Experiences section。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 演进