记录一下那天晚上的排查过程。在 responses.create 里加上 tools=[{type:web_search_preview}],回答里能看到角标编号,但 output 里只有 message 一类条目,annotations 取出来是空数组,用 openai 1.55 复现稳定。
刚开始盯着「解析沿用 chat 的 message.content 当字符串取,取到的其实是块数组」,折腾了半个多小时,排掉了。
原因不止一个,得分开看。来源链接挂在 content 里 output_text 块的 annotations 上,不在 message 顶层。还有一种是工具调用记录单独放在 type=web_search_call 的条目里,容易被过滤掉。
还有一种情况,工具名写成了 web_search,而当前账号只开了 preview 版本。
动手时先打印 output 里每个条目的 type
python -c "from openai import OpenAI;r=OpenAI().responses.create(model='gpt-5-mini',input='最近的汇率',tools=[{'type':'web_search_preview'}]);print([o.type for o in r.output])"
遍历内容块把 annotations 里的 url 抽出来
curl -s https://api.openai.com/v1/responses -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" -d '{"model":"gpt-5-mini","input":"最近的汇率","tools":[{"type":"web_search_preview"}]}' | jq -r '.output[]|select(.type=="message")|.content[].annotations[]?.url'
接着确认工具调用条目本身带哪些字段
curl -s https://api.openai.com/v1/responses -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" -d '{"model":"gpt-5-mini","input":"最近的汇率","tools":[{"type":"web_search_preview"}]}' | jq '.output[]|select(.type=="web_search_call")'
剩下的就是按角标序号把回答和来源对齐后再落库
python -c "import json,sys;d=json.load(sys.stdin);print(len(d['output']))" < resp.json
别只看报错消失,验证要多做一步。annotations 里能取到带 url 和 title 的条目,条数与正文角标数量一致,web_search_call 条目里能看到实际发起的查询串。
提醒一点。内置工具的引用是逐块注解而不是全局列表,落库时要保留块与条目的对应关系,只存一个 url 数组,后面复核时对不上角标。监控比日志更早给出信号,事后回看经常能提前半小时发现异常。