1. 凌晨三点接口返回 HTML,Claude Code 调用本地模型 JSON 解析失败怎么排查
凌晨三点被报警叫醒,打开日志一看:本该返回 JSON 的接口,响应体里躺着一整段<!DOCTYPE html>开头的错误页。前端JSON.parse直接抛Unexpected token '<',整条链路雪崩。这个场景我太熟了——Claude Code 通过 Base URL 指向本地 Ollama 或某个 GPT-4 兼容网关时,只要中间任何一层出问题,返回的往往不是规范的 JSON 错误对象,而是一张 HTML 错误页。
先说清楚问题本质:Claude Code 本身是个客户端,它只负责把请求发到你配置的 Base URL,然后解析响应。当它拿到 HTML 时,说明请求根本没到达真正的模型服务,或者到达了但被中间层拦截。HTML 错误页通常来自这几个地方:反向代理(Nginx/Caddy)的 502/504 页面、网关的 401/429 拦截页、Ollama 未启动时某些封装层返回的默认页、以及端口写错打到了别的 Web 服务上。
为什么这个坑特别隐蔽?因为 Claude Code 的报错信息往往只告诉你「解析失败」,不会告诉你响应体长什么样。你得自己去抓。我试过最直接的办法就是在 Claude Code 的配置里把日志级别调高,或者干脆用 curl 手动打一遍同样的请求,把原始响应打出来看。
适合谁看:正在用 Claude Code 接本地 Ollama、接自建 GPT-4 兼容接口、或者接第三方聚合网关的开发者。如果你只是用官方云端接口,基本不会遇到 HTML 替换 JSON 的问题,因为官方网关的错误响应也是 JSON。但只要你的 Base URL 指向自己可控或半可控的中间层,这个坑迟早会踩。
排查的核心思路是分层定位:从客户端配置 → 网络可达性 → 网关鉴权 → 模型服务本身,一层层往下剥。下面我会把每一层的检查命令和判断标准都写清楚,你照着敲就行。
2. TaoToken 前置配置:Base URL 与 auth.json 三件套怎么填
在动手排查之前,先把「正确的配置长什么样」钉死。很多 HTML 返回问题,根源就是 Base URL 或 Key 填错,导致请求打到了错误的服务上。这里以 Claude Code 接入兼容接口为例,讲清楚三件套:Base URL、API Key、Model ID。
Claude Code 的配置入口通常在~/.claude/settings.json或项目级的.claude/settings.json,而认证信息走~/.claude/.credentials.json或环境变量。如果你用的是 Codex 风格的auth.json,结构类似。下面给一份可直接复制的settings.json片段,路径按你实际安装位置调整:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的Key", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514" } }如果你走的是auth.json方式(部分 CLI 工具会读这个文件),结构是这样:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model": "claude-sonnet-4-20250514" }三件套缺一不可,而且必须语义一致:Base URL 指向的服务,必须认识你填的 Key,也必须支持你填的 Model ID。常见错误是 Base URL 填了聚合网关,Model ID 却写了个网关不认识的别名,网关直接返回 HTML 错误页说「model not found」。
关于 Key 的获取,你可以到控制台创建:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。创建后建议先别急着塞进 Claude Code,用 curl 单独验证一次,确认这个 Key + Base URL + Model 的组合能返回正常 JSON。
这里有个容易忽略的点:Base URL 结尾要不要带/v1。不同网关要求不一样。有的网关要求https://xxx/api,有的要求https://xxx/api/v1。填错的话,请求会打到网关的根路径,返回的很可能就是一张 HTML 首页或 404 页。判断方法很简单——用 curl 打一下,看返回的是 JSON 还是 HTML。
另外,如果你同时配了多个环境变量(比如系统里残留了旧的ANTHROPIC_BASE_URL),Claude Code 可能读到了旧值。排查时一定要确认「实际生效的配置」是哪一个,而不是你以为的那个。可以用env | grep ANTHROPIC看一眼当前 shell 里的值。
3. 可复制配置:curl 验证命令与逐层检查清单
配置填好后,别直接上 Claude Code,先用 curl 把链路打通。这一步能帮你把「客户端解析问题」和「服务端返回问题」彻底分开。
第一条命令,验证基础连通性和返回格式:
curl -i -X POST "https://taotoken.net/api/v1/messages" \ -H "Content-Type: application/json" \ -H "x-api-key: sk-你的Key" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-sonnet-4-20250514", "max_tokens": 64, "messages": [{"role": "user", "content": "只回复两个字:收到"}] }'重点看-i打出来的响应头第一行和响应体第一行。如果响应体第一行是{,说明链路正常,问题在 Claude Code 客户端侧;如果第一行是<,说明你拿到的是 HTML,问题在服务端或网关侧。
第二条命令,专门测本地 Ollama 是否活着:
curl -i http://localhost:11434/api/tags正常返回是 JSON 数组,列出本地模型。如果返回 HTML 或连接被拒,说明 Ollama 没起来,或者端口被别的服务占了。Ollama 默认端口 11434,如果你改过,记得同步改 Claude Code 的 Base URL。
第三条命令,测 GPT-4 兼容接口的 models 列表:
curl -i "https://taotoken.net/api/v1/models" \ -H "Authorization: Bearer sk-你的Key"这条能帮你确认 Key 是否有效、网关是否认识你的账号。返回 401 说明 Key 错,返回 429 说明限流,返回 HTML 说明打到了错误路径。
逐层检查清单,按顺序过一遍:
| 层级 | 检查项 | 正常表现 | 异常表现 |
|---|---|---|---|
| 客户端 | Base URL 结尾路径 | 与网关文档一致 | 多/少/v1 |
| 客户端 | Key 是否生效 | curl 返回 JSON | 401 HTML 页 |
| 网络 | 端口是否可达 | curl -i有响应 | 连接超时/拒绝 |
| 网关 | 鉴权头格式 | x-api-key或Bearer | 401/403 |
| 网关 | 限流状态 | 200 | 429 HTML 页 |
| 模型服务 | Ollama 进程 | /api/tags返回 JSON | 连接拒绝 |
| 模型服务 | 模型是否加载 | 列表含目标模型 | 空列表 |
这张表建议截图存下来,下次再遇到 HTML 返回,从上往下过一遍,基本十分钟内能定位。
4. 验证请求与成功结果:从 HTML 到 JSON 的对比
配好之后跑一次完整验证,把「错误态」和「正确态」并排看,你就能一眼分辨问题出在哪。
错误态长这样(这是你凌晨三点看到的东西):
<!DOCTYPE html> <html> <head><title>502 Bad Gateway</title></head> <body> <center><h1>502 Bad Gateway</h1></center> <hr><center>nginx</center> </body> </html>看到nginx字样,基本可以断定是反向代理层挂了,模型服务本身可能没收到请求。这时候去查 Nginx 的 error.log,通常是 upstream 连接超时或后端进程崩了。
正确态长这样:
{ "id": "msg_01Xxx", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "收到"}], "model": "claude-sonnet-4-20250514", "stop_reason": "end_turn", "usage": {"input_tokens": 12, "output_tokens": 4} }拿到这个,说明 Base URL、Key、Model 三件套全部正确,链路通畅。这时候再回到 Claude Code,如果它还报解析错误,那就是客户端缓存了旧配置,重启一下 Claude Code 进程即可。
再给一个本地 Ollama 的成功响应示例:
{ "model": "qwen2.5:7b", "created_at": "2025-01-01T00:00:00Z", "response": "{\"status\":\"ok\"}", "done": true }注意 Ollama 的/api/generate返回的response字段本身是个字符串,里面才是模型生成的 JSON。如果你要的是纯 JSON,得用/api/chat并配合format: "json"参数。这一点很多人搞混,以为 Ollama 返回的顶层就是 JSON,结果解析出来一堆嵌套字符串。
验证通过后,建议把这条 curl 命令存成一个 shell 脚本,比如check_llm.sh,下次出问题直接跑,省得重新拼参数。
5. 本篇常见错排查:401、429、local proxy failed 与 reading choices
这一节把几个高频报错逐个拆开,每个都给你判断依据和修复动作。
401 Unauthorized:响应体是 HTML 还是 JSON 很关键。如果是 JSON 且带error.message,说明网关认识请求但 Key 无效,去控制台重新生成 Key:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。如果是 HTML,说明请求根本没到鉴权层,打到了 Nginx 的默认页,检查 Base URL 路径。
429 Too Many Requests:通常是限流。HTML 版的 429 多半来自网关的限流页,JSON 版的 429 来自模型服务本身。前者要调低并发或升级套餐,后者要等冷却。判断方法还是看响应体格式。
local proxy failed:这个报错在 Claude Code 里出现,通常意味着它尝试连本地代理端口失败。检查你的 Base URL 是不是写成了http://localhost:xxxx,而那个端口上的服务没起来。用lsof -i :端口确认进程是否存在。
reading choices:这是 OpenAI 兼容接口解析响应时的典型报错,意思是响应里没有choices字段。原因通常是返回了 HTML 错误页,或者返回了非 OpenAI 格式的 JSON。修复动作:先用 curl 确认响应结构,再检查 Base URL 是否指向了 OpenAI 兼容端点(通常带/v1/chat/completions)。
OAuth 相关报错:如果你用的是需要 OAuth 的网关,token 过期会返回 HTML 登录页。这时候 curl 会拿到一张登录表单的 HTML。修复方法是重新走一遍授权流程,或者改用 API Key 方式。
排查顺序建议:先 curl 看响应体格式 → 是 HTML 就查网关和代理 → 是 JSON 就查字段结构 → 字段缺失就查 Base URL 路径和 Model ID。这个顺序能覆盖 90% 的场景。
6. 长期编码与 Agent 场景:把链路稳定下来
单次排查解决的是「这一次」,但如果你要长期用 Claude Code 跑编码任务或 Agent 流程,得把链路稳定性做成常态。
第一,把验证脚本纳入日常。每次改配置后跑一遍check_llm.sh,确认返回 JSON 再上生产。第二,给 Base URL 配一个健康检查端点,定时探测,发现返回 HTML 就告警。第三,Claude Code 的配置用版本管理,改之前先备份,出问题能秒回滚。
如果你跑的是长时间编码任务,建议走 Coding Plan 这类稳定通道:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。它的好处是链路经过优化,不会因为本地 Ollama 进程重启就断掉。对于 Agent 场景,稳定性比单次延迟更重要。
模型对话类的快速验证,可以用这个入口:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。接入文档在这里:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,里面有各语言的完整示例。
最后说个实操细节:Claude Code 的日志默认不打印响应体,你可以在启动时加--debug或设置ANTHROPIC_LOG=debug,把原始响应打到文件里。下次再遇到 HTML 替换 JSON,直接翻日志就能看到那张错误页的完整内容,比猜快得多。链路排查这件事,工具到位了,剩下的就是耐心。