记录一下那天晚上的排查过程。内容审核类请求走 json_schema,遇到敏感话题时返回体里没有 content,只有一个 refusal 字符串,下游按 JSON 解析直接空指针。
刚开始盯着「finish_reason 依然是 stop,看起来和正常结束没区别」,折腾了半个多小时,排掉了。
把日志和配置对完,能解释这个现象的基本就这几处。模型有权拒答,此时结果放在 message.refusal 而不是 content。也可能是refusal 是纯文本,完全不遵循你定义的 schema。
还有一种情况,客户端只判断了 content 是否为空,没有写拒答分支。
先把完整的 message 结构打出来确认字段
curl -s https://api.openai.com/v1/chat/completions -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"敏感话题"}],"response_format":{"type":"json_schema","json_schema":{"name":"r","strict":true,"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"],"additionalProperties":false}}}}' | jq '.choices[0].message'
解析前先判断 refusal 键是否存在
python -c "import json;d=json.load(open('resp.json'));print('refusal' in d['choices'][0]['message'])"
给下游加一条拒答兜底文案
python -c "print('该问题暂不支持回答')"
等前面都确认过,再统计日志里的拒答比例
python -c "import json;print(sum(1 for l in open('log.jsonl') if 'refusal' in l))"
改完先别撤,确认这几项。敏感样本走拒答分支并返回兜底文案,普通样本正常解析成 JSON,日志里 refusal 计数与实测样本数一致,不再出现空指针。
提醒一点。拒答是正常返回不是异常,接口照样 200,想靠状态码发现它永远发现不了,解析层必须显式处理这个字段。改动前把原配置备份一份,出问题能马上回滚,这比任何技巧都实用。
