OpenClaw 设计分析
介绍了Open claw的基本设计
OpenClaw 更准确的定位是:
- 一个自托管的 Agent 控制平面(Gateway)
- 一套按会话串行执行的单回合运行时
- 一组以文件为事实源、以插件为扩展边界的工程系统
它不是“每个聊天窗口里养着一个持续思考的机器人”,而是“由 Gateway 统一接管入口、状态、权限与执行”的系统。
1. 先说本质:它到底解决什么问题
如果把 OpenClaw 抽象到最小,它解决的是四件事:
- 把不同聊天平台的消息入口统一起来,让上层 Agent 不必理解 Telegram、Slack、WhatsApp、iMessage 的协议差异。
- 把一次次离散消息组织成稳定会话,避免多设备、多渠道、多轮工具调用把状态搅乱。
- 把模型“当前看到的上下文”与“真正持久化的事实”区分开,避免把上下文窗口误当成记忆系统。
- 把高能力工具放进可控边界内执行,而不是把“会调工具”直接等同于“可以安全自动化”。
所以它的重点不是 UI,也不是某个单独模型,而是:
- 入口统一
- 状态归一
- 执行串行
- 事实落盘
- 能力受控
2. 最重要的架构判断:Gateway 才是权威主体
OpenClaw 的核心不是聊天客户端,也不是某个 agent 实例,而是 Gateway。
- 会话归 Gateway 所有:session 列表、token 统计、会话 transcript、配置解释,都以 Gateway 看到的状态为准。
- 客户端只是表面:macOS App、Web 控制台、TUI、聊天平台,都不是权威状态源;它们只是 Gateway 的不同观察窗。
- 多表面不保证完全回灌:多个设备或渠道可以映射到同一 session,但历史并不会完整同步回每个客户端;真正完整的记录仍以 Gateway transcript 为准。
- 远程模式尤其如此:如果 Gateway 跑在远端,那么“本机看到的文件”并不代表真实运行状态;真实状态在 Gateway 主机上。
这点很关键,因为它决定了 OpenClaw 不是“端上智能体”,而是“控制平面 + 多表面接入”。
3. 执行模型:不是常驻思考,而是单回合重建
OpenClaw 的运行单位不是“一个永远活着的 agent 线程”,而是 一次 run。
每次入站消息触发的大致流程是:
- 渠道接收消息并归一成内部上下文。
- Gateway 根据
sessionKey找到或创建对应会话。 - 运行时重建本回合上下文。
- 模型推理,按需调用工具。
- 结果流式输出到目标渠道。
- 将本回合增量追加写入 transcript。
- 本回合结束,Gateway 继续等待下一次事件。
这里最容易误解的一点是:
- Gateway 是常驻的
- Agent run 是短生命周期的
也就是说,它不是严格意义上的 Serverless,但它确实采用了“每回合重建工作集”的思路。
这样做的直接收益是:
- 同一会话天然可以串行化,减少工具竞态与 transcript 写冲突。
- 运行边界清晰,失败恢复、超时控制、重试与压缩更容易做。
- 系统不会因为“维持很多常驻 agent 线程”而把复杂度提前透支。
代价也很明确:
- 每次都要重新组装上下文,延迟与 token 成本更高。
- 如果上下文治理做不好,系统会越来越依赖压缩、摘要与检索。
4. 一致性单元:不是 channel,而是 session
OpenClaw 最核心的工程约束不是“消息来了就处理”,而是“同一 session 只能按顺序处理”。
这里要区分三个概念:
sessionKey:逻辑会话键,用于路由与隔离。sessionId:物理 transcript id,用于落盘文件名,可轮换。<sessionId>.jsonl:实际 transcript,记录对话、工具结果、压缩摘要等事件。
本质上:
sessionKey解决“这条消息属于哪个连续上下文”sessionId解决“当前这段上下文写到哪份物理文件”
这也是它比很多聊天机器人实现更稳的地方。很多系统把“用户 id”或者“channel id”直接当上下文边界,结果会在群聊、分线程、跨设备、重置、每日切片这类场景里迅速变乱。OpenClaw 显式把“路由键”和“物理文件”拆开了。
5. 真正的状态分层:不要把 transcript、memory、context 混为一谈
这是当前文档原本提到但还不够本质的一块。OpenClaw 至少有四层不同性质的状态:
第一层:配置与路由元数据
- 例如
openclaw.json、认证资料、sessions.json。 - 作用是告诉系统“怎么连外部世界”“当前会话映射到哪里”“会话启用了哪些开关”。
- 这是控制平面的管理状态。
第二层:transcript
- 即
<sessionId>.jsonl。 - 它是会话回放日志,不是长期知识库。
- 它的职责是让下一轮能够重建上下文,并为压缩、回放、调试和导出提供统一事实记录。
- 它记录的不只是用户与助手消息,还包括工具调用、工具结果、压缩摘要等事件,因此它更像“会话事件流”而不是纯聊天记录。
compaction会把旧历史压成持久摘要写回 transcript,后续回放看到的是“压缩摘要 + 最近保留的后缀”。pruning更像运行时减载,主要减少当轮回放负担,不改写这份长期回放记录。- 所以 transcript 的核心职责不是“永久存所有原文”,而是“在有限窗口里尽量保住会话连续性”。
第三层:可人工编辑的长期事实
- 即工作区里的
MEMORY.md与memory/YYYY-MM-DD.md。 - 它们才是持久事实源。
- 用户和模型都可以读写,且内容具备可审计性与可迁移性。
第四层:检索加速层
- 例如 SQLite FTS、向量索引,或可选的 QMD 后端。
- 它们只是为了更快召回 Markdown / 会话切片,不是事实源本身。
- 索引坏了可以重建,Markdown 才不能丢。
所以最重要的判断是:
- Context 是运行时工作集
- Transcript 是回放日志
- Markdown 是长期事实
- 索引是加速器
这四者分清了,很多设计就不会跑偏。
6. 上下文装配:OpenClaw 自带系统提示词,再叠加用户工作区与会话状态
OpenClaw 有一个很重要、但很多类似系统没讲清楚的点:每次 run 的输入不是“用户写了什么就发什么”,而是 Gateway 现组的一份工作集。
每次 run 发给模型的内容至少包括:
- OpenClaw 构建的系统提示词
- 会话历史
- 工具调用与工具结果
- 附件或转录内容
- 工具 schema
其中,系统提示词内部通常还包含:
- 工具列表与简述
- 技能列表元信息
- 时间与运行时元数据
- 注入的工作区 bootstrap files(如
AGENTS.md、SOUL.md、TOOLS.md、USER.md)
这里最关键的事实是:OpenClaw 本身有一套编码在运行时里的系统提示词骨架,SOUL.md 只能影响其中一部分,不能覆盖全部。
更准确地说:
SOUL.md不是和系统提示词平级的输入。SOUL.md是被注入到系统提示词里的Project Context文件之一。- 系统还会在更高优先级的位置注入一批用户通常不会直接从
SOUL.md感知到的框架级提示,例如工具调用风格、执行偏向、安全提醒、静默回复约定、运行时信息、消息表面规则等。
这意味着用户即使精心编写了 SOUL.md,也只能塑造:
- 语气、人格、风格边界
- 某些稳定身份设定
- 一部分工作区规则
但用户无法仅通过 SOUL.md 感知或替换:
- OpenClaw 自带的系统提示词骨架
- provider / runtime 注入的额外提示
- 工具 schema 本身携带的调用约束
- 某些运行时特定段落,例如静默回复、审批提示、运行时元数据
所以从结构上看,更像是:
OpenClaw-owned system prompt
+ provider/runtime 注入
+ Project Context(含 SOUL.md / AGENTS.md / TOOLS.md ...)
+ 会话历史
+ 工具调用与结果
+ 附件
+ 工具 schema
如果继续追问“OpenClaw 自己到底注入了什么”,可以把它理解成几类固定 section:
- Tooling / Tool Call Style / Execution Bias:告诉模型当前有哪些工具、怎么调用、何时少说话直接做、何时该继续执行直到完成。
- Safety:给出最上层的行为约束,例如不要追求权限扩张、不要绕过监督、不要擅自改 system prompt 或安全规则。
- Skills / Memory / Docs / CLI Quick Reference:告诉模型“去哪里找能力说明”和“遇到 OpenClaw 自身问题时先查什么”。
- Workspace / Sandbox / User Identity / Time / Runtime:把这次 run 所处的具体运行环境显式告诉模型,避免它凭空假设路径、权限、时间和宿主能力。
- Messaging / Voice / Silent Replies / Reactions / Assistant Output Directives:约束它在具体交付表面上的行为,例如不要乱发流式草稿、何时可以静默、何时该靠原生审批按钮而不是手写
/approve。 - Project Context / Dynamic Project Context / Group Chat Context / Subagent Context:把真正与当前工作区或当前会话相关的动态材料挂进来。
这些 section 之所以需要由 OpenClaw 自己硬编码,而不是全交给 SOUL.md,是因为它们承担的是控制平面责任:
SOUL.md适合表达“我是谁、怎么说话、什么风格”。- 框架 section 负责表达“你此刻拥有什么能力、受什么约束、运行在什么环境、应该如何交付”。
如果把这两类东西混在一起,会出现两个问题:
- 用户 persona 文件会膨胀成运行时规范,既难维护,也难确保每次都正确注入。
- 安全、审批、沙箱、消息表面这些和运行环境强绑定的规则,不能依赖用户文件自觉维护。
从设计上看,OpenClaw 用的是“框架骨架 + 用户文件填充 + provider/runtime 局部覆写”:
- 框架骨架负责稳定的控制平面语义。
- 用户文件负责人格、工作区规则、局部项目语境。
- provider/runtime 只在少数 section 做覆写或增补,而不是整体替换。
这样设计的好处是:
- 大部分运行约束始终存在,不会因为用户没写
SOUL.md就消失。 - 用户仍然能通过工作区文件显著影响 agent 的表现。
- 不同模型族可以在不推翻整套 prompt 的前提下做局部调优。
再往里看,这份系统提示词也不是一整块平铺文本,而是分层组织的:
- 稳定骨架:工具、执行、安全、技能、工作区、运行时等框架段落。
- Project Context:工作区 bootstrap files,被截断后注入。
- 动态附加段:例如群聊上下文、subagent 上下文、provider 动态后缀。
- 缓存边界:系统会尽量把稳定段和频繁变动段分开,减少每次完全失效。
如果要更严格,应该把“上下文装配”拆成两层:
- 第一层:system prompt 内部有哪些层次
- 第二层:最终请求对象里,system prompt、messages、tools 是什么关系
先看第一层,也就是 buildAgentSystemPrompt() 产出的 system prompt 内部结构:
再看第二层,也就是 最终请求对象:
其中 messages 内部更接近下面这个顺序:
这里有两个容易混淆的点:
SOUL.md、AGENTS.md、TOOLS.md不是和历史平级的主输入;它们先被注入到 system prompt 的Project Context里。- 当前轮用户回答不在 system prompt 里,而是在
messages的尾部,通常是最新一条 user message。 - 附件或转录内容通常不是独立悬浮的一段全局文本,而是挂在某条 user message 上的内容块。
tool schema也不是普通 prompt 文本段落;它更像“与 system prompt / messages 并列发送的结构化能力描述”,但同样占上下文预算。
这正是为什么 OpenClaw 的“人格”和“行为”看起来像混合物:
- 一部分来自
SOUL.md - 一部分来自
AGENTS.md/TOOLS.md - 一部分来自框架硬编码提示
- 一部分来自工具可用性和渠道约束
用户不是完全无法感知这些东西,只是默认不会从 SOUL.md 这条入口完整感知。只有查看上下文报告或系统提示词细节时,才能看到更完整的装配结果。
这意味着几个很现实的结论:
- 记忆不是“写在磁盘上就天然进入模型脑中”。
- bootstrap 文件不是越多越好,它们直接挤占上下文窗口。
- 工具能力越多,schema 开销越大。
- transcript 越长,越需要压缩与剪枝。
所以 OpenClaw 的上下文系统,本质上是在做 受 token 预算约束的工作集构建,而不是简单的 prompt 拼接。
7. compaction 与 pruning:如何在有限窗口里保住连续性
如果只说“历史长了就做摘要”,会低估 OpenClaw 的压缩机制。它实际解决的是一个更难的问题:
- 旧历史太长,模型窗口放不下。
- 但如果随便摘要,又会把当前回合继续所需的局部细节压没。
- 特别是工具调用场景里,摘要边界切错一次,就可能让后续回放失去上下文配对关系。
所以 OpenClaw 的 compaction 不是“定期简单总结”,而是“在有限 token 内保留会话可继续执行的最小连续结构”。
它大致有几层机制:
先直接回答一个容易误会的问题:
compaction不是只保存一个索引。- 它会在 transcript 里持久化写入一条 compaction 摘要条目,并记录从哪里开始保留未压缩后缀,例如
firstKeptEntryId。 - 之后的历史回放会把这条摘要当作“旧历史的代表”,只继续拼接它之后保留的消息。
- 也就是说,它改变的是未来如何回放这份 transcript。
- 从当前实现与文档语义看,它更接近“逻辑上裁掉旧历史,物理上保留原始事件”,而不是立刻把旧 JSONL 行从文件里硬删除。
所以更准确的说法是:
- 对未来上下文来说,旧历史被实际移出了回放主路径。
- 对磁盘 transcript 来说,旧事件通常仍作为历史痕迹存在,只是不再是默认 replay 输入。
如果按实际执行过程展开,可以近似理解为下面这条链:
-
先估 token,不直接相信原始长度
- 系统会先估算消息 token。
- 在 compaction 估算里,还会先剥掉
toolResult.details这类不该再喂给摘要模型的冗长明细。 - 同时会带安全余量,避免估算偏小导致摘要时再次超窗。
-
决定要不要分块,以及块有多大
- 如果消息还不够多,或者总 token 还在单块可处理范围内,就直接摘要。
- 如果历史过长,就先按 token share 切成多个 chunk。
- chunk 比例不是写死的;平均消息越大,块会切得越小,防止摘要阶段自己先爆掉。
-
切块时保护执行结构
- 系统会跟踪 assistant 发出的 tool call id。
- 如果某个切分点正好落在 tool call 与 tool result 之间,会把边界回退到更安全的位置。
- 这样做是为了避免摘要后一半记着“调用了什么”,另一半丢了“结果是什么”。
-
分块摘要,再合并摘要
- 每个 chunk 先单独摘要。
- 如果有多个 partial summary,再把这些 partial summary 当成新的输入继续合并成一份总摘要。
- 合并时不是随便归纳,而是明确要求保留:当前任务进度、最近用户请求、决策与理由、未完成事项、约束、承诺等。
-
如果摘要失败,走降级路径
- 先尝试完整摘要。
- 失败后,会把过大的单条消息标记出来,只摘要其余较小消息。
- 再失败,就退化成“当前上下文过大,摘要不可用”的说明,而不是让整次 compaction 直接失真崩掉。
-
把摘要和保留后缀重新拼成新的 replay 入口
- 最终落盘的不是“一个裸摘要”。
- 系统还会尽量附上近期保留回合、拆分回合提示、工具失败摘要、文件操作摘要、工作区上下文等后缀。
- 所以后续看到的实际回放入口,更像“摘要头 + 结构化尾巴”,而不是一句空泛总结。
把这个过程压成图,大致是这样:
第一层:触发时机
- 一种是在真正溢出后触发,也就是模型已经明确报上下文过长。
- 另一种是在接近阈值时提前触发,也就是运行时发现
contextTokens已逼近contextWindow - reserveTokens。
后者很重要,因为系统不是只为“这一次能塞进去”服务,还要给下一次输出、工具结果和 housekeeping 留余量。
第二层:压缩后的保留结构
- 老历史不会原样保留,而是变成持久化的
compaction摘要条目。 - 最近的后缀消息不会全部吞掉,而是继续以未压缩形式保留。
- 后续回放看到的,不是“全量历史”,而是“压缩摘要 + 最近保留段”。
也就是说,compaction 的目标不是保真归档,而是构造一个更短、但还能继续工作的 replay 入口。
第三层:切块边界不是随便切
- 如果切分点刚好落在 assistant 的 tool call 和对应 tool result 之间,系统会调整边界,尽量不把这对关系拆开。
- 如果尾部还挂着一个尚需保留的工具结果块,系统会倾向于把这段留在未压缩后缀里,而不是强行塞进摘要。
- 失败或中止的工具块不会无限阻止压缩,否则系统会被少量异常数据拖死。
这说明 OpenClaw 的压缩不是只看 token 数,而是同时保护“执行结构”。
第四层:摘要并非只有一段自然语言
- 默认路径会走内建的摘要流水线,而不是简单拼一段摘要文本。
- 在 safeguard 路径里,系统还会尽量保留最近回合、拆分回合提示、部分工具失败信息、文件操作摘要等后缀信息。
- 如果配置了 compaction provider,插件也可以替换摘要后端;失败时再回退到内建 LLM 摘要路径。
所以 compaction 既是“语义压缩”,也是“执行连续性修复”。
这里顺带能回答“为什么要这么复杂”:
- 因为 OpenClaw 不是纯聊天系统,而是工具执行系统。
- 纯聊天摘要丢一点过程,通常只是回答变笨。
- 工具系统摘要丢掉一次关键配对或当前任务状态,下一轮就可能直接执行错上下文。
所以它的 compaction 设计重点不是“把过去讲短”,而是“让下一轮还能接着做事”。
第五层:压缩前还会先做一次 memory flush
- 当上下文接近自动压缩阈值时,系统可以先触发一次静默回合。
- 这次回合的目的不是回复用户,而是提醒模型先把值得长期保留的事实写入 Markdown。
- 这样即使后面 transcript 被压缩,真正该长期保留的内容也已经从“会话回放层”迁移到了“长期事实层”。
这一步说明 OpenClaw 已经明确承认一个现实:transcript 不适合承担全部长期记忆职责。
和它相对的 pruning,则更像“本轮为了塞进窗口而做的减载”:
- 它主要减少回放时的体积,尤其是过大的旧工具结果。
- 它不重写 transcript,不改变那份长期回放事实。
- 它解决的是“这一轮怎么塞得下”,而不是“之后如何持续回放”。
所以可以把两者简单理解成:
- compaction:改写长期回放形态,换取后续多轮可持续运行。
- pruning:减轻当轮回放负担,换取本次上下文装配成功。
8. 记忆系统的关键,不是“能搜到”,而是“先落盘,再召回”
OpenClaw 的记忆设计里,真正重要的不是向量库,而是这条顺序:
- 先把值得保留的信息写成 Markdown
- 再把 Markdown 建索引
- 最后通过
memory_search/memory_get召回
这和很多 SaaS Agent 把“记忆”藏在内部数据库里不同。它的优点是:
- 可审计
- 可手工纠错
- 可迁移
- 可与普通工程文档共存
但代价也很明确:
- 模型不会自动拥有永久记忆,必须触发写入。
- 检索新鲜度依赖 watch、debounce、按需同步或定期更新,不是无条件实时。
- 混合检索虽然提升召回,但会引入更多参数与调优面。
项目中比较成熟的一点,是它没有把“向量检索”神化,而是采用更务实的混合路子:
- 关键词检索:补足精确 token、ID、符号名查询。
- 向量检索:补足语义相近但措辞不同的召回。
- 时间衰减:让近因信息更容易浮上来。
- MMR 重排:降低重复片段挤占结果。
这不是为了追求“学术上最优”,而是为了让真实工作区里的 Markdown 更能用。
9. 渠道抽象的本质:统一入站,不强求统一能力
OpenClaw 并不是把所有渠道“做成一样”,而是:
- 把入站事件统一成可路由的上下文
- 把出站渲染按各渠道约束单独处理
这是更现实的做法。因为不同渠道本来就不对称:
- Telegram / Slack / Discord 更像开放 Bot API。
- WhatsApp 依赖 Web 协议模拟。
- iMessage 这类能力则需要本机桥接;新接入更推荐 BlueBubbles,
imsg属于遗留路径。 - 还有一部分渠道根本不在 core,而是通过单独插件安装。
因此,OpenClaw 的渠道层目标不是“抹平平台差异”,而是把差异压缩到适配层,不让上层运行时和工具系统被协议细节淹没。
10. 插件边界的本质:核心只做控制,能力尽量外插
OpenClaw 的扩展面比较清晰:
- Channel 插件:扩展入口与出站能力。
- Provider 插件:扩展模型提供方。
- Tool / Skill / Hook 插件:扩展执行能力与生命周期干预点。
- Memory / Context Engine slot:替换部分核心子系统。
这说明它的核心思路不是“把所有东西都内建”,而是:
- 核心负责控制平面与稳定边界。
- 变化快、供应商相关、协议相关的部分尽量插件化。
这比“一个超大核心里堆满各种渠道和工具特殊逻辑”更容易长期演进。
当然,插件化不是零成本:
- 调试路径更长。
- 插件装载、启用、优先级与配置校验更复杂。
- hook 过多时,问题定位会从“哪段代码错了”变成“哪条生命周期被谁改写了”。
11. 安全模型的本质:不是禁止能力,而是把能力放进边界
OpenClaw 的安全设计,不是靠一句“请小心操作”完成的,而是分层做约束:
- 入口约束:DM allowlist、pairing、群聊 mention / 激活规则。
- 执行约束:危险工具审批、策略限制、只读工作区、沙箱工作区。
- 输出约束:对工具结果做截断与净化,避免把外部不可信输出原样塞回后续上下文。
- 可见性约束:
NO_REPLY、静默 flush、消息发送抑制,避免系统内部 housekeeping 泄漏到用户表面。
其中比较容易被忽略但很关键的一点是:
- OpenClaw 不只控制“能不能调工具”,还控制“结果是否应该被用户看到”
例如:
- 接近 compaction 时会触发一次静默 memory flush,提醒模型把耐久信息写入 Markdown。
- 精确的
NO_REPLY会被出站层识别并抑制。 - 当消息工具已经对外发出内容时,系统还会做重复确认抑制,避免模型再回一遍“我已经发出了消息”。
这类机制看似细碎,实际上决定了系统会不会显得“啰嗦、穿帮、把内部流程暴露给用户”。
12. 队列与回压:系统活性来自调度,不来自模型自觉
OpenClaw 没把“并发控制”交给模型,而是放在 Gateway 调度层。
几个关键点:
- 按 session lane 串行:同一会话一次只跑一个 run。
- 按 global lane 限流:全局并发也受上限控制。
- 入站去重:处理渠道重复投递。
- 入站去抖:把短时间连续文本合并成一次 turn。
- 队列模式:支持
collect、followup、steer、interrupt等策略。
这套机制背后的本质是:
- 一致性优先于极限实时性
- 系统活性依赖显式调度,而不是寄希望于模型“自己别乱来”
代价是:
- 高峰时会产生排队等待。
- 若配置不当,用户会感知到“怎么消息发了但要过一会儿才处理”。
但这个代价比“多轮 run 抢写同一 session”要小得多。
13. 一个更准确的数据流心智模型
可以把它理解成下面这条链路:
外部消息
-> 渠道适配
-> 归一上下文
-> session 路由
-> 队列与串行化
-> 单回合 agent loop
-> 工具/技能/插件
-> transcript 与 memory 落盘
-> 渠道渲染与发送
注意这里有两个关键回路:
- 执行回路:消息进入 -> run 执行 -> 结果发出
- 知识回路:有价值信息 -> 写入 Markdown -> 建索引 -> 后续再召回
很多系统只把第一条做出来,于是“当轮能答”;OpenClaw 试图把第二条也做稳,所以它更像控制平面而不是聊天壳子。
14. 如果要复刻这种系统,真正该抄的不是界面,而是这些约束
- 不要做“每个会话一个永久 while true 智能体”。
- 要做“每次事件触发一个短生命周期 run,并由控制平面重建工作集”。
- 不要把 transcript 当长期记忆。
- 要把长期事实写入可审计文件,并允许人工修正。
- 不要把渠道差异直接扩散到运行时核心。
- 要在适配层完成协议归一,在出站层处理平台限制。
- 不要把安全寄托在 prompt 里。
- 要在入口、执行、输出三层都放硬约束。
- 不要假设上下文无限大。
- 要把上下文构建、压缩、剪枝、静默 housekeeping 当一等公民。
15. OpenClaw 最有价值的地方
它最值得学的不是“支持很多渠道”,也不是“工具很多”,而是它把 Agent 系统里几件最容易混淆的东西拆清楚了:
- 控制平面 和 一次 run
- 会话日志 和 长期事实
- 记忆事实源 和 检索加速层
- 入口适配 和 能力执行
- 可调用 和 可安全交付
这使得它可以用相对朴素的工程材料去搭复杂系统:
sessions.json*.jsonl- Markdown
- SQLite
- CLI / Cron / Hooks
- 插件与 slot
朴素不代表简单,而是指这些部件的边界比较清楚,可审计、可替换、可恢复。
更多推荐




所有评论(0)