
1. 从一次上下文溢出说起OpenClaw 的 Agent 架构到底在解决什么如果你正在看 OpenClaw 的源码或者准备自建一套 Agent 运行时大概率会遇到一个很具体的场景某个会话跑了三十多轮工具调用之后模型突然返回context_length_exceeded或者更隐蔽地回答开始丢失早期任务约束把已经确认过的参数又改回去。这不是模型变笨了而是上下文窗口被塞满之后运行时没有做好工作集重建。OpenClaw 的定位可以一句话概括它是一个自托管的 Agent 控制平面由 Gateway 统一接管入口、状态、权限与执行每个回合按会话串行跑一次短生命周期的 run所有事实以文件为源、以插件为扩展边界。它要解决的核心问题不是“让模型更聪明”而是把四件事拆开入口统一、状态归一、执行串行、事实落盘。适合谁适合那些已经用过大模型 API、想理解 Agent 运行时内部结构、并且希望在自己的环境里复现关键链路的开发者。我试过把 OpenClaw 当成普通聊天机器人来读结果处处别扭后来换成“控制平面 多表面接入”的视角Gateway 路由、transcript 记录、compaction 压缩这三块就串起来了。这篇就按这个视角拆重点放在可复制的 Gateway 配置片段和 compaction 触发验证步骤上让你能在自己机器上把链路跑通。先给一个最小的心智模型外部消息进来经过渠道适配归一成内部上下文Gateway 按 sessionKey 路由到某个会话队列做去重去抖和串行化然后触发一次单回合 Agent Run运行时重建上下文、模型推理、按需调工具结果流式输出最后把本回合增量追加写入 transcript。这里有两个回路执行回路负责当轮能答知识回路负责把有价值信息写进 Markdown 再建索引供后续召回。很多系统只做了第一条OpenClaw 试图把第二条也做稳。2. Gateway 路由与 transcript 落盘sessionKey 和 sessionId 为什么要拆开理解 OpenClaw 的 Gateway最关键的一点是会话归 Gateway 所有。session 列表、token 统计、transcript、配置解释都以 Gateway 看到的状态为准。macOS App、Web 控制台、TUI、聊天平台都只是 Gateway 的不同观察窗不是权威状态源。远程模式下尤其明显——如果 Gateway 跑在远端你本机看到的文件并不代表真实运行状态真实状态在 Gateway 主机上。这里有个容易被忽略的工程约束一致性单元不是 channel而是 session。同一 session 只能按顺序处理。为此 OpenClaw 显式拆开了两个概念。sessionKey 是逻辑会话键用于路由与隔离解决“这条消息属于哪个连续上下文”sessionId 是物理 transcript id用于落盘文件名可以轮换解决“当前这段上下文写到哪份物理文件”。很多实现直接把用户 id 或 channel id 当上下文边界结果在群聊、分线程、跨设备、重置、每日切片这些场景里迅速变乱。拆开之后路由键和物理文件各自演进互不绑架。transcript 本身是sessionId.jsonl它是会话回放日志不是长期知识库。它记录的不只是用户与助手消息还包括工具调用、工具结果、压缩摘要等事件所以更像“会话事件流”。它的职责是让下一轮能重建上下文并为压缩、回放、调试、导出提供统一事实记录。这里必须把四层状态分清配置与路由元数据如openclaw.json、sessions.json属于控制平面管理状态transcript 是回放日志工作区里的MEMORY.md与memory/YYYY-MM-DD.md才是可人工编辑的长期事实源SQLite FTS、向量索引或可选的 QMD 后端只是检索加速层坏了可以重建Markdown 不能丢。一句话总结这层判断Context 是运行时工作集Transcript 是回放日志Markdown 是长期事实索引是加速器。四者分清后面 compaction 和 pruning 的边界就不会搞混。3. 可复制的 Gateway 配置片段Base URL、Key 与 Model ID 三件套要让 Gateway 真正跑起来绕不开模型接入这一环。下面给一份可直接改的配置片段路径按 OpenClaw 常见约定放在工作区根目录的openclaw.json。注意三件套必须齐全Base URL、API Key、Model ID缺一个都会在启动或首次请求时报错。{ gateway: { host: 127.0.0.1, port: 8787, sessionLaneConcurrency: 1, globalLaneConcurrency: 4, dedupeWindowMs: 1500, debounceMs: 800 }, providers: { default: { type: openai-compatible, baseUrl: https://taotoken.net/api, apiKey: ${TAOTOKEN_API_KEY}, modelId: claude-sonnet-4-5, contextWindow: 200000, reserveTokens: 16000 } }, compaction: { enabled: true, triggerRatio: 0.82, chunkTokenShare: 0.35, protectToolPairs: true, memoryFlushBeforeCompact: true }, transcript: { dir: ./sessions, format: jsonl, rotateDaily: false } }几个参数值得单独说。sessionLaneConcurrency设为 1 是刻意的同一会话一次只跑一个 run避免多轮 run 抢写同一份 transcript。globalLaneConcurrency控制全局并发上限防止高峰把机器打满。dedupeWindowMs和debounceMs配合把渠道重复投递和短时间连续文本合并成一次 turn。reserveTokens是给下一次输出、工具结果和 housekeeping 留的余量compaction 的提前触发阈值就是contextWindow - reserveTokens再乘triggerRatio。如果你用的是 Claude Code 这类工具做本地编码或者通过 Cline MCP 挂载工具同样要把 Base URL 指向https://taotoken.net/apiKey 走环境变量注入Model ID 与上面保持一致。Codex 用户则在auth.json里对应填写三件套逻辑不变。把 Key 写进环境变量而不是明文配置是基本习惯export TAOTOKEN_API_KEYsk-你的key配置改完先别急着发消息用一条最小请求验证 Gateway 是否真的把 provider 接上了。这一步能省掉后面大量“以为是 compaction 问题其实是 Key 没读到”的排查时间。4. 验证请求与 compaction 触发从 401 到成功回放先验证基础链路。启动 Gateway 后用 curl 打一次最小请求确认路由和 provider 都通curl -s http://127.0.0.1:8787/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer ${TAOTOKEN_API_KEY} \ -d { model: claude-sonnet-4-5, messages: [{role: user, content: 只回复两个字通了}] }返回里能看到choices[0].message.content就说明 Gateway 到 provider 的链路是通的。如果返回 401先查 Key 是否被环境变量正确注入如果报local proxy failed查baseUrl是否写成了带路径的完整地址而不是根地址如果报reading choices相关错误通常是响应体不是标准 OpenAI 兼容格式检查 provider 的type字段。基础链路通了之后重点验证 compaction。构造一个会触发压缩的长会话最直接的办法是连续灌入多轮带工具结果的消息让 token 逼近阈值。你可以写一个小脚本循环发送for i in $(seq 1 40); do curl -s http://127.0.0.1:8787/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer ${TAOTOKEN_API_KEY} \ -d {\model\:\claude-sonnet-4-5\,\messages\:[{\role\:\user\,\content\:\第 $i 轮请复述当前任务目标并追加一条约束 C$i\}]} \ /dev/null done跑完之后去看./sessions/sessionId.jsonl。如果 compaction 生效你会看到一条类型为 compaction 的摘要条目并且带有一个类似firstKeptEntryId的字段标记从哪里开始保留未压缩后缀。之后的历史回放会把这条摘要当作旧历史的代表只继续拼接它之后保留的消息。也就是说它改变的是未来如何回放这份 transcript而不是立刻把旧 JSONL 行从文件里硬删掉——逻辑上裁掉旧历史物理上保留原始事件。验证成功的结果长这样新开一轮对话问模型“我们之前定过哪些约束”它能从压缩摘要里答出早期约束同时最近几轮的细节仍然完整。如果它只记得最近两轮、早期约束全丢说明chunkTokenShare切得太碎或者protectToolPairs没生效摘要时把工具调用和结果拆散了。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth排障这块按真实报错对照比泛泛而谈有用得多。401 Unauthorized 最常见的原因是 Key 没被读到。检查顺序环境变量是否在当前 shell 生效、配置里是否用了${TAOTOKEN_API_KEY}这种引用而不是硬编码、Gateway 进程是否继承了该环境变量。如果是通过 systemd 或容器启动环境变量不会自动继承需要在 unit 文件或 compose 里显式声明。local proxy failed通常出现在 baseUrl 配置错误时。正确写法是根地址https://taotoken.net/api不要在后面再拼/v1/chat/completions路径由客户端补全。另外确认本机没有其他进程占用 Gateway 端口port冲突也会表现成连接失败。reading choices这类错误说明响应体解析失败。可能是 provider 返回了非标准结构也可能是流式和非流式模式混用。先确认type字段与目标服务匹配再用非流式请求验证一次。如果错误里带OAuth字样说明认证方式走错了分支——API Key 模式下不应该触发 OAuth 流程检查是否误配了需要交互登录的 provider 类型。compaction 相关的隐性错误更值得注意。如果摘要后模型开始执行错上下文多半是切块边界落在了 assistant 的 tool call 和对应 tool result 之间。OpenClaw 会跟踪 tool call id 并回退边界但如果你的自定义 provider 没有正确回传 tool call id保护逻辑就失效了。另一个坑是memoryFlushBeforeCompact开启后接近阈值时会先触发一次静默回合提醒模型把耐久信息写入 Markdown。如果你在日志里看到一次没有用户可见回复的 run那不是 bug是设计。还有一个容易误判的点pruning 和 compaction 不是一回事。pruning 主要减少当轮回放体积尤其是过大的旧工具结果它不重写 transcriptcompaction 改写长期回放形态。看到“这一轮塞下了但下一轮又爆”通常是 pruning 在起作用而 compaction 没触发去查triggerRatio是不是设得太高。6. 把链路跑通之后从 Gateway 到 compaction 的复刻要点如果你要复刻这套系统真正该抄的不是界面而是这些约束。不要做“每个会话一个永久 while true 智能体”要做“每次事件触发一个短生命周期 run由控制平面重建工作集”。不要把 transcript 当长期记忆要把长期事实写入可审计文件并允许人工修正。不要把渠道差异扩散到运行时核心要在适配层完成协议归一、在出站层处理平台限制。不要把安全寄托在 prompt 里要在入口、执行、输出三层都放硬约束。不要假设上下文无限大要把上下文构建、压缩、剪枝、静默 housekeeping 当一等公民。回到 compaction 本身它的设计重点不是“把过去讲短”而是“让下一轮还能接着做事”。先估 token 并剥掉不该再喂给摘要模型的冗余明细再按 token share 切块切块时保护 tool call 与 tool result 的配对逐块摘要后合并 partial summaries最后补回近期保留回合、工具失败摘要、文件操作摘要等后缀写入 compaction 条目。未来回放看到的是“摘要头 结构化尾巴”而不是一句空泛总结。Gateway 配置和 compaction 验证都跑通之后下一步可以拿真实的长任务压一压比如让 Agent 连续做二十轮文件读写加检索观察 transcript 里 compaction 条目的位置和firstKeptEntryId的变化。想直接体验模型对话链路可以从模型对话入口进要长期跑编码和 Agent 任务用 Coding Plan 更合适接入过程中卡在 Key 或配置上去 API Keys 页面和接入文档对照排查。把三件套配齐、把 compaction 触发验证一遍这套 Agent 运行时的关键链路就算在自己环境里落地了。