聊天全链路:四场景验证矩阵,和一个教科书级的 Node 坑
目标
打通核心链路:/api/chat 限流 → 校验裁剪 → 配置检查 → 真 AI 流式转发,任何一步失败都优雅降级到演示模式;并用「不花一分钱」的方式完成验证。
结果
写了一个本地 mock 的 OpenAI 兼容服务器(scripts/mock-openai.mjs),把四场景验证矩阵全部跑绿:
| 场景 | 结果 |
|---|---|
| ① 无 key | 演示模式,元数据 mode: demo, reason: not_configured,FAQ 关键词命中正确 |
| ② 指向 mock 的「真 AI」 | mode: live, model: mock-model,流式逐字输出端到端打通 |
| ③ 故意用坏 key | HTTP 200(无 5xx),自动降级演示模式 reason: upstream_error,响应中零密钥/上游错误泄漏 |
| ④ 连发 12 次 | 第 9 次起 429,文案友好;5000 字超长输入 200 不崩 |
服务端结构化日志可见每次对话的模式、问题摘要与耗时,例如:
{"evt":"chat","mode":"demo","reason":"upstream_error","question":"你是真人吗?","ms":585}
踩坑与纠正
坑一(AI 的训练数据过期):convertToModelMessages 在 v6 变成了异步函数(v5 同步),AI 按旧习惯写漏了 await。tsc 直接抓住——这正是「每一步都过类型检查」的价值。
坑二(教科书级):场景②首测时,请求挂起 20 秒一个字节都不返回。逐层排查:先最小复现确认 http 模块基本用法没问题,再对比差异——元凶是 mock 里的连接清理代码 req.on("close", () => clearInterval(timer))。Node 15+ 里 IncomingMessage 的 close 在请求体读完时就触发,不代表连接断开;于是定时器在写出第一个字节前就被清掉,响应永远悬空。改成挂在 res.on("close") 上,立刻通了。
意外收获:排查期间 mock 被 kill 掉,反而无意中验证了 ECONNREFUSED 场景——服务端日志显示链路正确落入演示模式,访客无感知。顺手给 streamText 补了 maxRetries: 1 和超时配置,防止上游挂死时拖着访客陪等。
用时与备注
约 50 分钟。验证全程零 API 费用——mock 厂商这个办法本身值得复用。