代码读到第四篇总算轮到Gateway。之前几篇文章里OpenClaw的核心模块再热闹消息还是要从某个口子进来最后从某个口子出去。这个口子就是Gateway。我的理解是前面那些模块解决的是“Agent怎么想”Gateway解决的则是“外面的话怎么递进来、Agent的话怎么递出去”。它身上同时挂着三块职责对外提供HTTP/WebSocket服务对内把各种渠道适配器Channel转成统一调用再在中间协调Session状态和模型路由。这篇文章我按实际看源码的习惯来写不按官方文档的顺序。先从目录和入口入手然后把一条请求从接收到回包的完整链路拆开接着聊Gateway的配置机制和Channel适配逻辑最后把本地部署中最容易踩的502、session锁定、模型路由不匹配这几个坑集中整理一遍。1. Gateway在OpenClaw里的定位与启动过程1.1 为什么Gateway被单独拎出来作为一层我一开始也有个疑问Agent调度和Gateway为什么不写在一起翻代码后我觉得分区是合理的。OpenClaw里Agent调度关心的是任务、工具、上下文它不关心消息到底是从哪个聊天软件来的。而Gateway恰好相反它要尽可能屏蔽上游渠道的差异把所有渠道的入参收敛成一种内部协议再统一交给Agent处理。这个抽象很像企业里的客服前台。你客服团队内部无论怎么分派工单客户只需要对着同一个入口说话就行。Gateway就是那个入口它负责登记你是谁、从哪个渠道来、想干什么然后把话转给对应的处理人。没有这层抽象每个渠道都跟Agent核心逻辑耦合后面加一个新的聊天渠道就得改一遍核心代码维护成本会指数级上升。所以Gateway在代码里是独立的一个包它以服务的形式挂在主进程里跟Session Manager、Agent运行时、模型RouteRegistry之间都只有接口往来没有直接依赖内部实现。这种边界我比较喜欢因为它决定了你可以单独替换某个渠道甚至可以单独把Gateway抽出来做成无状态服务只要Session存储能跟上。1.2 从入口函数看Gateway的启动顺序读代码时建议先找main入口再顺着启动顺序看依赖关系。OpenClaw的启动顺序大致是先加载配置文件把环境变量、默认值、用户自定义配置合并成一份统一的Config对象。初始化日志组件设置日志级别和输出位置。创建Session Manager这一步很关键后面所有请求都要靠它来定位或创建会话。初始化Gateway服务结构体把Channel工厂、模型RouteRegistry、Session Manager都注入进去。启动HTTP服务监听注册各个路由处理函数。这个顺序不是随手写的。Gateway在启动时就需要Session Manager已经就绪因为如果有channel在握手阶段就带上session信息Gateway要能在请求到达的第一时间完成会话绑定。同时模型路由注册表也必须先准备好否则Gateway无法把入站请求和具体的上游模型对应起来。我在本地跑过一次代码之后发现日志里如果出现“Gateway started”却没有看到“Session manager ready”多半是初始化顺序出了问题。这种问题一般不是代码逻辑复杂而是配置里漏了Session目录或者锁路径导致Session Manager在启动阶段就卡住了。所以看Gateway源码先把它依赖的初始化链路捋直比直接看路由函数更有价值。1.3 优雅关闭比启动更讲究启动顺序大家都会看但Gateway的优雅关闭过程我觉得更体现功底。如果你直接kill主进程正在处理的请求可能会写到一半没有回包尤其WebSocket长连接客户端那边会直接显示连接断开。OpenClaw在Gateway里面对关闭信号做了拦截。收到SIGTERM或SIGINT之后它不会再接受新的入站请求但是会给已建立的长连接留一个宽限期让它们把当前消息处理完然后把session状态落盘最后再关闭HTTP Server。宽限期默认能在配置里调我建议不要给太长否则进程退出会很慢。这里有个细节关闭顺序是“先关入口再等存量请求最后关Session Manager”顺序错了会出现已经close的session还在被写入的情况。我们自己在改Gateway代码时容易忽略这层觉得close就完事结果就是偶发的session文件损坏。了解源码里为什么这样设计后面排查问题会少走很多弯路。2. 请求路由与会话粘滞一条消息的完整生命周期2.1 从收到请求到回包Gateway内部做了哪些事我习惯把一条入站请求在Gateway里的路径画成六步第一步HTTP或WebSocket服务器收到原始请求。这一步只做传输层解析拿到method、path、header和body。第二步鉴权与渠道识别。Gateway根据请求里携带的Channel字段或Token判断消息来自哪里。常见的实现是读取Header里的Channel标识然后用配置里对应的Channel Adapter去解密或校验签名。这里如果不认识这个Channel会直接返回401或403。第三步Session定位。Gateway从请求参数中提取sessionID如果没有就根据会话规则新建一个。sessionID的生成算法通常跟渠道用户ID、会话类型有关保证同一个用户在不同渠道下会有稳定的会话上下文。第四步锁与并发控制。如果这个sessionID正在被其他请求占用Gateway会尝试获取锁拿不到就会进入等待或直接返回冲突。这个机制就是后面要重点讲的session file locked来源。第五步把消息包装成内部统一结构转发给Agent运行时。Agent在此时接入工具调用、上下文检索、模型推理最终生成回复。第六步Gateway拿到Agent的回复再通过对应Channel Adapter把消息格式化回原来渠道能识别的格式然后写回。这六个步骤看着简单实际上每一步都有不少分支。比如第三步如果sessionID传错了明明同一个人却开了新会话第五步如果Agent长时间没有返回Gateway要处理超时和取消第六步如果渠道要求分片发送长文本Gateway还需要自己拆消息。我调试Gateway时最喜欢在这六个节点各打一条日志带上sessionID和耗时。基本上哪一步慢了一眼就能从日志时间轴上判断出来。源码里的日志输出其实已经做了类似的事情只是默认日志级别看不到全部把日志级别调到Debug就能看到完整链路。2.2 为什么同一个会话必须粘滞到同一个Agent实例这里要解释一个很多刚接触OpenClaw的人会困惑的问题Gateway看起来是个无状态的入口为什么多开几个Gateway实例时反而容易出问题因为OpenClaw的Session Manager在默认配置下是把session状态存在本地文件里的。当一条请求进来Gateway要通过sessionID去读取对应的会话文件然后在这次请求期间对这个文件加锁。如果多台机器或同一个机器上的多个进程同时处理同一个sessionID它们会去锁同一个文件这时候就会出现竞争。并发高的场景下要么是后到的请求等待前面释放锁要么直接抛session file locked超时。所以Gateway在设计时就要求同一个sessionID的请求尽量路由到同一个实例。源码里能看到类似一致性哈希或者sessionID前缀匹配的路由逻辑目的就是为了让同一用户的消息始终落在同一台机器的同一进程上。这就带来一个很实际的约束如果你只是把OpenClaw水平扩展成多个Gateway实例但没有把Session存储改成Redis或数据库这类共享存储那么扩展不仅不一定提升性能反而会引入锁冲突。我在本地测试时开两个进程同时发消息第二个请求经常会等到超时。后来把Session存储切到共享目录做单实例问题才缓解。所以读源码时要留意Gateway的session粘滞策略。它不是万能的它的存在其实是为了配合“单实例单目录”的简单模型。真正要做高可用需要把session存储层替换掉同时保证Gateway路由策略和存储策略一致。否则粘滞策略写着写着单点故障和锁冲突两个问题总得踩一个。2.3 超时与会话锁的粒度控制锁是Gateway源码里藏得最深的一个点。表面上你看Gateway处理请求时只是调用了SessionManager但底层守护session的是文件锁或内存锁。我建议重点关注两个参数一个是会话超时时间另一个是锁等待时间。默认会话超时如果太短用户在聊天框里停留一会儿回来再发消息时Agent已经把上下文丢了体验很像“失忆”。锁等待时间太短则会出现“request rejected due to lock timeout”这类错误。最好把会话超时设成一个能覆盖你产品单次交互周期的值比如聊天机器人单次交互可能几分钟但如果是异步任务可能要几小时。锁等待时间则要看Agent一次回复的平均耗时给它留出两三倍的余量。我在源码里看到默认timeout是60000ms如果Agent工具调用比较重60秒确实不够。日志里频繁出现timeout时先别急着认定是并发问题二分法确认一下Agent回复平均耗时再说。3. 配置解析与Channel适配器机制3.1 Gateway配置项到底该怎么看OpenClaw的配置结构里Gateway相关的字段不算少但核心就几块。我把Gateway端口、Host、Session存储路径、Channel列表、模型路由表五类配置单独摘出来看。Gateway端口和Host决定了服务监听在哪。默认情况下监听127.0.0.1:15721意味着只有本机能访问如果你要部署到局域网或云服务器需要把Host改成0.0.0.0并在反向代理里把对应端口转发过去。很多部署问题都是因为本机curl正常、外部访问不了结果一看Host还是127.0.0.1。Session存储路径决定了session文件放在哪个目录。默认是本地目录如果你改了路径要确保Gateway进程对那个目录有读写权限。常见问题是把路径映射到Docker容器时容器内路径和宿主机路径对应不上导致启动报错或session无法持久化。Channel列表是一个数组每个Channel有自己的类型、Token和开关。Gateway启动时会遍历这个列表注册所有启用的Channel。这里有个很典型的坑配置里写了某个Channel但忘了把enable字段设为trueGateway只会打一行日志并不会报错结果那个渠道的消息始终进不来。模型路由表是Gateway和模型工厂之间的映射关系。这个配置比较关键后面单独说。为了方便记我整理了一个简单的配置速查表配置项含义常见错误host服务监听地址外部无法访问时多数是忘了改0.0.0.0port服务端口端口冲突导致启动失败sessionDirsession文件存放目录Docker挂载路径对不上channels渠道适配器列表enable未开启modelRoutes模型路由映射模型名不匹配报route错误sessionTimeout会话空闲过期时间太短导致上下文丢失3.2 Channel适配器是怎么挂到Gateway上的Channel是OpenClaw里非常核心的扩展点。Gateway本身不关心你接的是哪个聊天平台它只跟Channel接口打交道。这个接口大致包含这几个方法接收消息、发送文本消息、发送分段消息、确认已读、关闭连接。每个具体渠道只需要实现这套接口然后在Gateway初始化时把自己注册进去。源码里关于Channel注册的逻辑通常分为两步第一步扫描配置里出现的Channel类型第二步到ChannelFactory里找到对应的构造器传入配置完成实例化。这里有个我很喜欢的细节Channel注册失败不会直接让Gateway进程退出而是先把其他Channel跑起来再在日志里记录失败的Channel。这种设计在IoT或服务型网关里很常见叫“部分可用”。它比“一坏全坏”更能抗故障。但代价是如果你没仔细看日志很容易漏掉某个Channel没启动。我自己在接入Microsoft Teams时踩过一个坑I-channel底层依赖一个外部的长连接认证流程如果认证URL配错了它启动时不会立即失败而是等到第一条消息进来时才报错。所以接完新Channel一定要主动向该渠道发一条测试消息不能只看Gateway启动日志。3.3 模型路由为什么提示“expected a gateway model route”模型路由这块很多用户看到“claude doesn’t look like an anthropic model: expected a gateway model route”会发懵。其实这句话的意思是Gateway在把上游请求转发给模型服务前校验了请求里的模型名发现这个名字不在配置好的模型路由列表里。OpenClaw不会把任意模型名直接透传给下游它要求所有模型请求先经过RouteRegistry匹配匹配到具体的route之后才按该route的规则去调用对应的上游模型接口。这个设计是为了防止配置漂移和模型名拼写错误。你明明想调Claude结果请求里写了一个不在routes里的名字Gateway当然会拦下来。解决办法一般有两条要么在配置里加上这条模型到实际模型服务的映射要么把请求中的模型名改成配置里已有的route名。如果两个名字都对不上就去检查你的配置文件是不是被某个工具自动覆盖了。我在排查时经常发现不是用户写错而是配置合并逻辑把用户自定义的routes覆盖掉了一层。4. 常见问题与排查技巧实录4.1 “unexpected status 502 bad gateway”到底是谁的锅跟Gateway打交道502是我见得最多的错误。报错信息通常是这样unexpected status 502 bad gateway: unknown error, url: http://127.0.0.1:15721/v1/responses这个报错有一种迷惑性因为很多人看到bad gateway就以为是反向代理的问题但实际上OpenClaw报这个错时往往是它自己在向上游服务发起调用时收到了502。url里的127.0.0.1:15721是你的Gateway服务地址所以它本身是正常的问题是它背后要调的那个模型服务返回了502。排查步骤我建议这样先用浏览器或curl访问http://127.0.0.1:15721/v1/responses看返回什么。如果curl正常说明Gateway本身活着问题出在它依赖的上游模型服务。打开Gateway Debug日志找到最近一次模型调用的上游URL。如果curl也报502说明Gateway服务有问题可能是崩溃后没有自动恢复或者端口上的服务已经僵死。检查上游模型服务的负载情况。502的最大嫌疑是上游超时或过载而不是你的OpenClaw配置写错。我在自己环境里出现过一次很隐蔽的502上游模型服务监听了IPv6地址Gateway默认用IPv4去连每次都是瞬间返回502但从Gateway日志里看配置又没问题。排查到最后才发现是网络协议栈配置不对。所以碰到502先别改配置先确认网络连通性比瞎调参数更重要。4.2 session file locked并发与单例锁之间的对抗另一个高频错误是agent failed before reply: session file locked (timeout 60000ms)这个错我前面提到过本质是同一个会话文件同时被多个请求访问后请求等待锁超时。触发原因最常见的就三个一是多个OpenClaw进程在跑每个进程都尝试写同一个session目录二是同一个进程内一个会话被并发调用比如连续发了多条消息三是上一次进程异常退出锁文件没有释放新进程一直等旧锁。排查时先用lsof或ps找出是否有多余的OpenClaw进程把残留进程杀掉。然后找到对应的session目录看有没有.lock后缀的残留文件如果有手动删除。做完这两步大部分session file locked都能解决。但还有一类情况是代码本身的锁等待时间不够。如果你确认没有多进程并发单次请求还是报超时就要看会话锁的等待阈值。把等待时间从60秒调到90秒或者更高通常能规避正常处理时间偶发超过阈值的情况。值得多说一句如果为了高可用强行在共享目录下跑多个OpenClaw实例session file locked几乎是必然的。文件锁不适合跨进程共享目录做并发协调正确做法是切到Redis或数据库session存储或直接用单实例模式。开源社区里不少人把这个问题当成bug报其实底层是部署模型和存储模型不匹配。4.3 Gateway启动失败的几个隐蔽原因热词里也有“hermes gateway 无法启动”这类问题虽然具体报错各不相同但常见的启动失败原因就那么几个。端口被占用是最好查的但我遇到过因为反向代理转发规则把端口转发错了导致看起来像“Gateway没启动”。这时候先听一下Gateway进程是否真的在跑在高端口场景下反代配置里写错一个数字就会造成假象。配置格式错误也会让启动失败。尤其是YAML文件里缩进不一致或引号缺失Gateway启动时解析配置直接panic。建议在启动前用config校验工具先跑一遍。OpenClaw本身也会在启动时打印配置解析错误但日志容易被刷掉所以肉眼检查配置不如跑一下校验更靠谱。还有一个不起眼但常见的坑依赖外部基础服务启动失败。比如Session Manager依赖某个Redis实例Redis连不上Gateway同步等待一段时间后直接退出。这时候日志里不一定明确写着“Redis connection failed”会有很多前置的超时日志。看到启动日志里面一堆timeout基本可以朝外部依赖方向排查。4.4 模型路由报错与Channel选择问题一起排查有时候报错不只有一个。比如模型路由报错的同时Channel也可能因为选错适配器导致消息发不出去。Hot word里有人搜“openclaw agent怎么选择channel”其实这个不是用户主动选择的而是由Gateway根据请求里携带的渠道标识自动匹配的。如果你发现同一个请求到了Gateway之后总是走进错误的Channel分支多半是消息格式里缺少渠道标识或者Gateway端channel识别顺序写死了。调整的办法是在客户端请求里显式带上来源ChannelID并确保Gateway的channel匹配逻辑先按请求头精确匹配再走默认兜底逻辑。模型路由报错和Channel选择问题看似没关系但排查时经常一起遇到因为路由表里不同的route绑定不同渠道如果请求误判了渠道路由也就跟着选错了。最后出来的错误可能不是“channel mismatch”而是“model route not found”。所以遇到路由报错先确认渠道识别是否准确再回头查模型路由表。最后聊一点个人体会看OpenClaw的Gateway源码和前几篇阅读不一样的地方在于它更像在阅读一套对外服务的工程骨架。你不需要去跟复杂的算法搏斗要关注的是边界、生命周期、资源竞争和配置漂移。我自己的习惯是先跑通一条最小链路再故意破坏配置看Gateway怎么报错。这种方式比逐行读代码快得多。前面说的那些坑比如session file locked和502绝大多数都不是源码“写错”而是部署模型和源码的默认假设没对齐。如果你准备改动Gateway的行为建议先把Session存储和路由模型这两块吃透其他位置基本都是锦上添花的边缘逻辑。最后再分享一个调试技巧改完Gateway代码后不要急着接真实渠道先用一个echo类型的测试Channel发一条消息在日志里观察“Gateway receive - session acquire - agent reply - session release”这个闭环是否完整。跑通这条路径你对Gateway的认识才算真正落地。