Clawdbot(OpenClaw)架构深度拆解与实现全流程
最近这半年,但凡深入写过或用过Agent的朋友,大概率都踩过同一类坑:模型本身可能不是瓶颈,但系统一旦“动起手来”,工程侧的问题立刻暴露无遗。
并发乱成一锅粥、状态飘忽不定、日志像天书一样不可读、工具权限没有边界、失败了没法回放……说句扎心的话,提示词写得再漂亮,也兜不住这些底层问题。
今天这篇文章,我想把 OpenClaw(Clawdbot) 的架构拆开,好好聊聊。但我关注的不是它“有多牛”,而是它怎么通过工程化的设计,让整个系统变得更稳、更可控。
事情的起因,是前几天在X上刷到一篇关于Clawdbot架构的深度拆解帖。作者 @Hesamation 的出发点很实在:一开始只是想搞清楚它的记忆系统怎么工作、可靠性如何。结果一路挖下去,发现真正值得学习的东西,不是“它能做什么”,而是“它怎么把这些事做得更稳妥”。
太长不看版(6条核心判断)
- • OpenClaw 本质上是一个 TypeScript CLI 进程,外加一个处理多渠道接入的 Gateway Server;它不是个Web App。
- • 它把可靠性放在绝对优先级:默认串行,只有声明了才能并行(lane queue)。并发在这里不是“性能技巧”,首先是个“可靠性问题”。
- • Agent Runner 更像一条装配线:模型选择与Key冷却、Prompt组装、历史加载、上下文窗口守护,一切就序后才开始驱动工具循环。
- • 记忆系统不神秘:JSONL 转录(用来回放)+ Markdown 记忆文件(用来编辑);检索用的是 向量 + 关键词 的混合模式,底层存储是 SQLite(FTS5)。
- • 工具调用的安全边界必须系统化:allowlist + 结构化拦截(重定向、命令替换、子Shell、链式执行等直接拒绝),别把“自觉”当机制。
- • 浏览器交互不主要靠截图:人家用 语义快照(Accessibility Tree/ARIA),把“看网页”降维成“读结构”,成本更低,成功率也更稳。
先把它放回正确的分类里:它到底是什么
很多人一聊Agent,开口就是“自治”、“多智能体”、“进化”。但落地的时候,你得先问自己一个最根本的问题:你做的究竟是一个聊天机器人,还是一个“能在你机器上执行工具的系统”?
OpenClaw的定位非常清晰:它是一个运行在你机器上的进程,核心职责只有三件:
- • 接收入口来自不同渠道的消息(Telegram、Discord、Slack等)。
- • 调用LLM API(OpenAI、Anthropic、本地模型等)。
- • 在本地或受控环境里执行工具(Shell、文件、浏览器、进程),再把结果回传给你。
这番话听起来很朴素,但它一下就把架构的重心给锁死了:执行的可控性、状态的可追溯性、失败的可解释性。所有设计都得为这三件事服务。
一条主链路讲清楚:消息进来之后发生了什么
把OpenClaw翻译成工程视角,就是一条清晰的流水线:
- 1. Channel Adapter:把不同渠道的输入统一成标准消息,并提取附件。
- 2. Gateway Server:会话协调器,决定这条消息该进入哪个会话、排到哪个队列。
- 3. Lane Queue:每个会话默认串行;只有你明确标注了低风险的任务,才允许并行。
- 4. Agent Runner:拼上下文、选模型、发起调用,驱动工具循环。
- 5. Agentic Loop:模型提出tool call → 执行 → 结果回填 → 下一轮,直到输出或触达轮次上限。
- 6. Response Path:把最终内容流式回写到渠道,同时把全过程写入可回放的转录文件(JSONL)。
这类架构的价值不在“酷”,而在于边界清楚。出了问题,你一眼就能看懂“卡在哪一步”,也更容易把问题隔离出来。
再说一个很实用但经常被忽略的点:响应链路本身也应该被当成系统的一部分。帖子中提到,LLM调用是流式输出,最终响应通过渠道回到用户;同时会话会被持久化成JSONL(每行一个JSON,对应着用户消息、工具调用、执行结果、模型响应等)。这意味着你“看到的输出”只是冰山一角,下面还有一条可回放的证据链。
默认串行,显式并行:lane queue 为什么这么关键
我见过太多Agent项目,一开始只是想“多开几个async/await”,结果半年后,全都变成了同一个鬼样子:
- • 日志像一团毛线,交织在一起完全看不懂。
- • 共享状态一多,竞态条件(race condition)开始像幽灵一样挥之不去。
- • “偶现”问题占据了你绝大多数的调试时间。
OpenClaw的克制之处在于:它把并发的选择权,从“开发者随手写的异步”收回到一个显式的系统约束里。这个思路,和Cognition那篇“Don't Build Multi-Agents”的博文不谋而合:一个简单的async设置,就足够让你陷入日志交织、状态竞争的泥潭。
lane queue 的直觉
- • 每个会话都有自己的“泳道”(lane)。
- • 泳道里默认 串行执行。
- • 只有你明确标注“低风险、可并行”的任务,才会进入并行泳道(比如某些定时任务)。
这会强迫你换一个心理模型:从“我该锁哪里”转向“哪些任务真的能安全并行”。Lane是对队列的一层抽象,它把“串行”作为默认架构,而不是事后补丁。作为开发者,你正常写代码就行了,队列帮你处理竞态条件。
让并发“站到台前”
从工程实践的角度,我建议你把并发决策做成三个明确的层级:
- • 默认串行:先保证任何一条链路都能稳定复现。
- • 显式并行:只开放少数“无共享状态、幂等、可重试”的任务。
- • 隔离失败域:并行任务失败了,不能影响主会话;失败原因在日志里能单独定位。
你会发现,这套约束比任何“并发技巧”都值钱,因为它直接减少了你未来的debug成本。
Agent Runner:把“提示词工程”变成“上下文装配线”
真正能跑起来的系统,往往都把提示词当成装配线上的一环,而不是拿来搞宗教崇拜的。
在OpenClaw的描述里,Runner做的事非常具体:
- • Model Resolver:决定用哪个模型;Key失效就标记冷却并切换;主模型失败自动换备用。
- • System Prompt Builder:动态拼装系统提示词,把工具、技能、记忆都整合进去。
- • Session History Loader:加载会话历史(来自
.jsonl转录文件)。 - • Context Window Guard:上下文快满时进行压缩(总结)或降级/停止,避免“撑爆了才知道”。
这套拆分带来两个立竿见影的好处:
- • 你能把“模型质量”和“系统质量”拆开看:模型偶尔波动时,系统依然可控。
- • 你能做复盘:因为历史、工具调用、工具结果都是结构化记录,能回放、能对账。
顺着这条线,原帖还强调了LLM调用层的两个关键点:
- • 流式输出:让“生成过程”可观察,也更利于把中间态回传给上层。
- • 多厂商抽象:同一层对接不同的模型提供方;如果模型支持,还可以请求“扩展思考”能力。
工程上的启示是:你不该把“换模型、换Key、失败兜底”这些逻辑写散在业务代码里,而应该收敛在Runner的职责边界内。
Agentic Loop:奇迹发生的地方,也是事故高发区
工具循环大家都懂:tool call → 执行 → 回填 → 下一轮,直到输出或触达轮次上限(比如20轮)。
但工程上真正需要盯住的是三件事:
- • 终止条件:什么时候该停?停得是否可解释?
- • 工具输出的格式:工具返回的是“日志”,还是“可被模型消费的证据”?
- • 回填策略:回填太多会撑爆上下文,回填太少模型又会“失明”。
Context Window Guard的价值也在这里:把“上下文爆炸”做成一个显式组件,而不是靠经验去拍脑袋。你可以不追求每次都成功,但你不能接受失败时完全无从定位。
记忆:别神化,文件也能打
我很喜欢它对待“记忆”的态度:简单、可解释、可迁移。去神秘化,做得非常到位。
它主要靠两条线:
- 1. JSONL 会话转录:每一行一个JSON对象,包含用户消息、工具调用、执行结果、模型响应。
- 2. Markdown 记忆文件:放在
MEMORY.md或memory/目录下。
这相当于把记忆拆成了两类:
- • “发生过什么”(转录,偏事实、偏审计)
- • “应该记住什么”(摘要与沉淀,偏经验、偏复用)
检索:向量 + 关键词的混合
很多系统只做向量检索,最后都会踩一个坑:你以为找的是“精确概念”,结果召回的都是一堆“语义相近但不对”的东西。
OpenClaw的思路更稳当一些:向量检索负责语义召回,关键词匹配负责精确命中。原帖给了一个很直观的例子:你搜 “authentication bug”,系统既能命中写了 “auth issues” 这种同义表达(语义),也能命中精确短语(关键词)。这能明显降低“看起来相关但其实不对”的噪声。
在实现上,它用了SQLite:
- • 向量搜索:SQLite(向量索引的实现细节这里就不展开了,重点在于最终都落地到了同一个存储里)。
- • 关键词搜索:SQLite FTS5。
嵌入模型(embedding provider)也是可配置的。
同步:Smart Syncing + 文件监控
记忆文件的更新也不需要一套“专用记忆API”。更朴素的方式就够了:Agent用“写文件工具”写入 memory/*.md,文件监控器检测到变化后触发同步与索引更新。
它还提到一个很贴心的细节:新对话开始时,会有一个钩子把上一轮对话抓出来,写一份Markdown总结。你可以把它理解成“把经验沉淀变成默认动作”,而不是全靠用户手动整理。这点我非常认可:工具越底层,越不容易被你自己未来的“架构升级”搞死。
原帖作者也提到,这套记忆系统和 @CamelAIOrg 实现的工作流记忆非常相似:没有复杂的记忆合并,没有按周/月的记忆压缩。简单可解释,胜过复杂的意大利面。
代价:没有遗忘曲线
它的记忆会长期保存,旧记忆与新记忆权重接近,这也就意味着“它不太会自然遗忘”。这既是优势也是风险:
- • 优势:可追溯、可解释,复盘很方便。
- • 风险:过期的经验可能持续被召回;你需要显式的版本化、有效期或冲突解决策略。
如果你打算照搬这一套,我建议至少补两个机制:
- • 给记忆加上
updated_at与confidence的元信息(哪怕只是写在Markdown里)。 - • 定期把“已过期/已被推翻”的结论写成一条新记忆,去覆盖旧结论,而不是悄悄删掉。
工具与“电脑操作”:强大,但别自欺欺人
OpenClaw给Agent的能力很直白:你给它一台电脑,它就能用。
它的 exec 工具支持三种执行环境:
- • 沙箱(Sandbox):默认在容器里跑命令。
- • 宿主机:直接在本机执行。
- • 远程设备:在远程机器执行。
配套的还有三类常见能力:
- • 文件系统工具:读、写、编辑。
- • 浏览器工具:基于Playwright,并使用语义快照。
- • 进程管理:启动后台任务、查看状态、终止进程。
这里我要强调一句非常现实的话:只要你允许“宿主机执行”,你就等于把最终的责任从系统转交给了用户本人。所以它后面那套安全护栏,才显得没那么“锦上添花”,而更像是一件必需品。
安全:护栏不靠祈祷,靠 allowlist
很多人做工具调用安全,最后都会变成一句话:“请模型不要做危险操作。”这在工程上等同于没有。
OpenClaw的做法更像 Claude Code:维护一个 允许列表(allowlist),把“能跑什么命令”当成一个可审计的配置文件。原帖里给出了一个示例:配置会记录每个agent允许的命令模式,以及上次使用时间。大概长这样:
{
"agents": {
"main": {
"allowlist": [
{ "pattern": "/usr/bin/npm", "lastUsedAt": 1706644800 },
{ "pattern": "/opt/homebrew/bin/git", "lastUsedAt": 1706644900 }
]
}
}
}
另外,一些“明显安全”的命令会被默认放行(比如 jq、grep、cut、sort、uniq、head、tail、tr、wc)。
同时,它还做了一个我很欣赏的判断:不仅拦住命令本身,还拦住 危险的Shell结构,比如:
- • 重定向(
>) - • 命令替换(
$(...)) - • 子Shell(
(...)) - • 链式执行(
||/&&)
原帖给了几个会被直接拒绝的例子:
# 这些在执行前就会被拒绝:
npm install $(cat /etc/passwd) # 命令替换
cat file > /etc/hosts # 重定向
rm -rf / || echo "failed" # 链式执行
(sudo rm -rf /) # 子 Shell
为什么这很重要?因为真正的攻击,往往会把危险行为藏在一段看起来无害的组合里,而不是明着写“直接跑rm”。
你可以把这套机制理解成一句话:在用户允许的范围内,尽可能给Agent自主权。你不需要在提示词里反复喊“别乱来”。系统层面直接定义“你最多只能走到哪”,效果会更稳定。
浏览器:语义快照,比截图更像“工程选择”
OpenClaw的浏览器工具不以截图为主,而是使用 语义快照(Semantic Snapshots)。它的本质是把网页转成可访问性树(Accessibility Tree / ARIA)的结构化文本,让Agent去“读结构”,而不是“看像素”。
Agent看到的页面大概是这样:
- button "Sign In" [ref=1]
- textbox "Email" [ref=2]
- textbox "Password" [ref=3]
- link "Forgot password?" [ref=4]
- heading "Welcome back"
- list
- listitem "Dashboard"
- listitem "Settings"
它带来的优势非常现实:
- • 体积:截图可能到5MB级别;语义快照往往不到50KB。
- • 成本:Token开销通常只是图像方案的一小部分。
- • 精度:从“点坐标”变成“选节点引用”,成功率更稳定。
- • 速度:纯文本解析比CV处理快得多。
你可以把它当成一种工程化的取舍:只要任务不是强视觉依赖(比如识别图表细节、读验证码、找颜色差异),语义快照往往更可靠。浏览网页本质上不一定是个视觉任务——这正是语义快照能成立的前提。
我会怎么抄:10个可以直接落地的改造点
如果你正在做Agent工程化,这里是我觉得“抄了就能立刻变稳”的10件事:
- 1. 默认串行先跑稳:先让一条链路能稳定复现,再谈并行与性能。
- 2. 把并发变成系统决策:用lane queue + 会话隔离;并行必须显式声明。
- 3. Runner组件化:把Model Resolver / Prompt Builder / History Loader / Context Guard拆开,方便替换与观测。
- 4. 把工具调用当事件:每次tool call都写JSONL(请求、参数、结果、耗时、错误码),保证能回放。
- 5. 工具输出要做“证据化”:工具返回尽量结构化(表格/JSON摘要),避免把噪声日志塞满上下文。
- 6. 记忆先文件化:用
MEMORY.md/memory/*.md,配上updated_at、来源、置信度,比黑盒DB更可控。 - 7. 检索别只押宝在向量上:向量召回 + 关键词精确命中;必要时再加一个“必须命中词”的hard filter。
- 8. 安全从allowlist开始:明确允许什么、拒绝什么,写成配置;危险的shell结构直接拒绝。
- 9. 浏览器优先用语义快照:能读结构就别看像素;把强视觉任务(验证码、图表、颜色等)单独隔离出来。
- 10. 失败要可解释:把“环境不满足”、“偶发失败”、“策略拦截”分开报错,别让用户只看到一句冷冰冰的“失败了”。
收个尾:工程化Agent的“现实感”
我越来越觉得,Agent真正的分水岭是:失败时你能不能解释清楚。至于“能不能做”,反而没那么稀缺了。
提示词当然重要,模型当然重要。但真正让系统可用的,是这些看似朴素的工程约束:队列、日志、文件、allowlist、可访问性树。它们不性感,却很可靠。