全部文章

AI 接口排障实录:环境变量、适配器与上游响应

沿浏览器、应用进程、模型适配器和服务提供方逐层排查,区分配置加载、请求失败与流式结束异常。

“模型没反应”是一个用户感受,却还不是一个可以直接修复的问题。在之前接入 AI 服务的任务中,我遇到过环境变量没有进入目标进程、模型列表可以读取但对话失败,以及响应很短却被应用记成正常结束等不同情况。

这些现象都可能落在同一个聊天窗口里。排查时如果反复修改模型、超时和 token 参数,很容易同时改变多个条件,最后无法解释究竟是什么起了作用。

我更习惯先画出请求经过的边界,再为每一层找最小证据。本文整理这套方法,日志与配置均为合成示例;历史问题只代表当时的调用结果,不代表某个服务当前仍然异常。

先确定请求在哪里停止

一个常见链路可以写成:

浏览器 → 应用 API → Agent / 适配器 → 模型提供方
   ↑         ↑            ↑             ↑
界面状态   进程配置     请求与流转换     上游状态和事件

浏览器没有收到内容时,我先确认请求是否发出、应用是否接到、上游是否返回,以及适配器有没有把事件传回来。每一步都使用同一个请求关联 ID,避免把不同重试的日志拼在一起。

如果服务端已经记录上游拒绝,而前端仍显示“正在思考”,问题至少包含错误传播或状态收敛。此时只换一个模型,即使暂时能用,也没有修复界面永远等待的路径。

环境变量属于具体的进程

有 .env 文件,不等于某个 Node 进程已经加载了它。启动目录、启动命令、框架规则和容器配置都会影响最终结果。

Node 提供 --env-file 等加载方式,具体可用性和优先级需要按运行版本确认。采用显式加载时,启动命令应清楚表达文件位置,并让缺失配置尽早失败,而不是等到第一次模型调用才发现。Node 命令行文档说明了该选项的行为。

我会把下面几种变量分开检查:

位置 常见误解 应检查的对象
前端构建 改了服务端变量,已构建网页就会更新 前端构建时注入的公开配置
应用进程 终端能读取,后台服务也一定能读取 实际处理请求的进程
Compose 插值 .env 里的值会自动全部进入容器 渲染后的服务配置与容器环境
容器内进程 改了宿主机文件,旧容器就会更新 容器重建方式和进程启动配置

Compose 的 .env 可以参与配置插值,但变量是否注入容器还取决于服务的 environment、env_file 等配置。Docker 的插值文档对此有具体说明。检查时应只输出所需字段的存在状态,不把整个环境或密钥打印出来。

例如可以在应用启动校验中使用下面的简化代码:

function requireConfig(name: string): string {
  const value = process.env[name]?.trim();
  if (!value) throw new Error(`缺少配置:${name}`);
  return value;
}

const apiKey = requireConfig('AI_API_KEY');
const baseURL = new URL(requireConfig('AI_BASE_URL'));
console.info({
  event: 'ai_config_ready',
  credentialPresent: Boolean(apiKey),
  protocol: baseURL.protocol,
});

这只证明配置存在且 URL 可以解析,不证明认证有效。密钥必须留在服务端;不能为了方便调试,把它放进会打包到浏览器的公开变量。

模型列表和生成请求是两项检查

模型列表返回 200,可以缩小连通性和该端点权限的排查范围,但它不能证明目标模型支持当前请求方式、账号有足够额度,或流式生成可以完成。

接下来需要一个最小生成请求:固定一条短输入、固定模型和参数,记录上游状态、错误类型与实际响应体结构。如果怀疑流转换,再在相同条件下比较非流式与流式结果。不要同时更换提供方、模型和适配器版本。

历史排查中出现过上游返回 402 的情况。可靠的结论是“该请求被提供方以该状态拒绝”,具体额度或计费原因还要对应当次响应说明和账号状态,不能把所有 402 都归到同一个原因,更不能由此推断 OCR 服务也失效。

正常结束标记需要保留来源

短响应尤其容易误判。应用日志中的 stop,可能来自原始服务,也可能是适配器结束流时补上的归一化值。没有区分来源,就无法判断模型主动停止还是链路没有完整传递。

我希望诊断记录至少包含:

{
  "requestId": "demo-request",
  "upstreamStatus": 200,
  "providerFinishReason": null,
  "adapterFinishReason": "stop",
  "streamClosed": true,
  "toolArgumentParse": "failed"
}

这是虚构的字段样例。它展示的是保留原始信号与转换结果的思路,不能直接作为某个 SDK 的事件格式使用。streamClosed 也应定义清楚:连接结束与收到提供方规定的完成事件,可能需要分别记录。

当时关于短响应的证据不足以确认 token 耗尽。要支持这样的判断,需要提供方的结束原因、用量信息或可重复实验;仅凭“字数很少”和应用层 stop 都不够。

修复后确认新配置确实生效

修改文件之后,我会沿同一条链路再验证一次:目标进程是否重启或容器是否重建、启动校验是否通过、最小生成是否成功,以及真实界面是否正确退出加载状态。只执行健康检查,无法覆盖模型调用;只在命令行请求上游,也无法覆盖应用适配器。

这里最有用的习惯,是让每个结论都有范围。“应用已加载配置”“上游生成成功”“流式事件转换正常”“浏览器展示正确”,可以分别成立或失败。把这些证据连接起来,才容易判断下一步该改代码、运行配置,还是去核对提供方的请求限制。