本期敖行客研发实战日记,邀请专业补刀GGB,分享日记不写成功学,只写从 400 错误一路挖到配置静默重写、从启动失败追到依赖树深处、从 WebSocket 断连查到端口号错一位的真实过程。每一次翻车“案发现场”,记录下关键线索与证据方便把车翻回来...

GGB 江湖人称"后端补刀位"

常年和 Kubernetes、Nacos、Spring Boot 各种诡异 bug 为伍,信奉"日志不会骗人,骗人的都是自己没记全的记忆。"

我们的服务栈横跨微服务网关、注册中心、K8s 编排和一个会"自主行动"的 AI Agent 平台。听起来是"服务多、组件全",真正接手运维后才发现,坑一个接一个,而且很多在网上搜不到现成答案。

大多数所谓的"玄学 bug",扒开一层层现象往下挖,最后落点往往就两种:状态不一致,或者有人(或 Agent)在你不知道的时候悄悄改了东西。下面把最近踩过的几个后端坑捋一遍,方便自己复盘,也方便下次不再掉进同一个坑。


一、最隐蔽的坑:Agent 有配置文件写权限,"自动优化"变成"自动破坏"

现象:

Provider 的 key 本来配得好好的(deepseek-v4-flash),过一阵子请求就开始报 400。

抓包一看,请求里的 model 字段变成了 model/deepseek-v4-flash——多了一层不该有的前缀。

排查了一圈才发现:内嵌 Agent 会定期"巡检"配置文件,判断某些字段"格式不规范",然后自动重写 openclaw.json

第一次重写加了一层 model/ 前缀,本来问题不大;但一旦触发 failover(失败自动切换到备用 Provider),Agent 又会在已经带前缀的 key 基础上再拼一次,于是出现了model/model/qwen3.7-plus 这种双重前缀,DeepSeek 直接拒绝,报 400。

根因:Agent 的"自我修复"逻辑没有做幂等校验,重写操作可以在已经"修复过"的字段上再次叠加。

经验:凡是允许 Agent 自动写回配置文件的系统,必须给关键配置项加锁加版本校验,否则"自愈能力"随时可能变成"自毁能力"。

这类 bug 最麻烦的地方在于——报错发生在请求侧(API 400),但根因发生在配置侧(几分钟甚至几小时前的一次静默重写),中间完全没有关联线索,只能靠翻配置文件的变更历史一点点核对。


二、"少了一个类"背后:Spring Boot 启动失败,报错却指向别的地方

at-gateway 服务突然启动不了,日志堆栈指向 GatewayServerBootstrap,一路往下翻,最后卡在一个 GlobalExceptionHandlerResolver 找不到的 ClassNotFoundException。乍一看以为是网关自己的异常处理类被误删,但翻了半天代码,这个类压根不在网关模块里。

顺着 Maven 依赖树往上查,才发现问题出在依赖版本没对齐:某个公共模块升级后拆分了异常处理相关的类,网关这边引用的还是旧版本坐标,编译期不报错(接口签名没变),但运行期类加载器找不到实际实现类,直接在 Bean 初始化阶段炸掉。

经验:Spring Boot 的启动异常,报错位置往往是"最后失败的地方",不是"真正出问题的地方"。看到 ClassNotFoundExceptionNoSuchBeanDefinitionException 这类错误,第一反应不要在报错类本身打转,先查依赖树和版本对齐情况,尤其是多模块微服务之间共享 jar 包的场景。


三、数据库迁移的坑:字符集和 collation 版本不匹配

从一个环境往另一个环境导 cs_im.sql 这种数据库 dump,本以为就是 mysqldumpmysql < 两步的事,结果目标环境 MySQL 版本更高,导入时 collation 不认识——新版本引入的 collation(比如 utf8mb4_0900_ai_ci)老版本压根没有,反过来老 dump 里用的 collation 在新版本里虽然认识,但排序规则细节不同,导入后个别涉及排序、分组的查询结果对不上。

经验:跨大版本迁移 MySQL 数据前,先用 SHOW COLLATION 在两边分别确认目标 collation 是否存在;mysqldump 时显式指定 --default-character-set 和目标兼容的 collation,而不是依赖源库的默认值;涉及排序敏感的业务(比如中文姓名排序、模糊匹配)迁移完必须做一次结果比对,不能只看"导入没报错"就算完事。


四、WebSocket 联调两小时,问题是端口写错一位

allowedOrigins 里配的是 :403,不是 :443。表现是 web 端和 iPhone 端偶发连不上WebSocket,日志里能看到大量 unknown method: ping 和连接被拒绝,一开始以为是心跳协议不兼容,排查了心跳间隔、序列化格式、代理超时,最后才发现是端口号手滑打错了一位数字。

顺带把这次一起处理的另一个噪音也理清楚了:unknown method: ping 本身是无害噪音,是心跳探测触发的正常日志,不代表连接异常;真正要关注的是关闭码——1006(异常关闭,通常是网络层或跨域被拒)和 1008(协议违规,通常是业务层主动拒绝)要分开看,混在一起排查只会越查越乱。

经验:偶发性网络类问题,先把"噪音日志"和"真正的错误信号"分开列出来,再逐条核对配置文件里的每一个端口、域名、协议字段,尤其是复制粘贴改配置的时候,最容易在数字上出错。


五、会话卡死:上游没超时,下游就"僵尸"了

AI Agent 平台里发现一批会话长期卡在 model_call 状态,recovery 字段是 none,也就是说系统认为这个会话"不需要恢复",但实际上它已经没有任何进展。

根因是调用上游模型 API 时,既没有 socket 级别的超时设置,也没有配 AbortController,一旦上游模型服务不返回、也不主动断连,请求会话就会一直悬挂,系统层面完全监测不到。

另外还牵出一个相关问题:某些模型的"思考模式"(thinking mode)必须开启流式(streaming)才能正常吐字,如果客户端没开流式,请求本身也可能长时间没有任何可观察的输出,容易和真正的挂死混淆。

经验:任何对外部服务的调用都要有显式超时取消机制,不能依赖"对方总会返回"这种假设;recovery=none 这种"系统认为没问题"的状态标记本身也要能被主动巡检覆盖,否则它会变成排查时的盲区,而不是安全区。这个问题目前仍是待解决的基础设施层长期项。


六、小结

回头看这一圈踩过的坑,能归成三类:

  1. 状态和现实脱节:Agent 自动重写配置、recovery=none 但实际卡死——系统"以为"的状态和"实际"的状态对不上,排查时必须以实际现象为准,不能只信状态标记。

  2. 报错位置≠根因位置:类找不到、请求 400,报错永远发生在"最后失败的一环",往上游查依赖版本、查配置变更历史,才能找到真正的根因。

  3. 细节决定生死:一个端口号打错一位、一个 collation 版本不对齐,表面看是低级失误,但恰恰是这类细节问题最难被日志直接指出来,只能靠一条一条核对配置排除。

后端修 bug,说到底是在和"看不见的状态变化"打交道。工具和框架能帮你定位到"哪里报错了",但"为什么会变成这样",永远需要人把链路重新走一遍,记录下来,也是给下一次自己留一份地图。

敖行客介绍:
敖行客(Allthinker)聚焦服务企业研发团队及开发者,以搭载自研企业级智能体引擎的 AT Work-Agent 研发工作台为核心支撑,打造 AI 原生一体化研发协同体系,依托企业智能体重构研发协作范式,致力于赋能各类研发团队轻量化完成智能化升级。

AT Work介绍:
AT Work-Agent 研发工作台是国内首个分钟级部署、AI 原生全链路研发协同平台,依托企业级智能体赋能研发全流程,零门槛打造专属 AI 研发团队,实现研发效率与数据安全的双重飞跃。

官网:www.allthinker.com

邮箱:allthinker@allthinker.com

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐