调试 OpenAI Responses API 的 Web Search 踩坑记录
最近在做一个投资研究 Agent,其中有个很具体的需求:监控腾讯控股 0700.HK 的估值指标,尤其是 trailing PE 或 TTM PE。
这个需求看起来不复杂,但实际做下来踩了几个坑。直接访问 Yahoo Finance 的接口,在当前运行环境里会返回 403 Forbidden;换用模型的 web search 又遇到了 Responses API 请求格式、流式响应解析和第三方 provider 兼容性问题。
这篇文章记录一下这次调试过程。重点不是“怎么让模型搜索网页”,而是:当你真的把 OpenAI 新版 Responses API 里的 web_search 接到工程代码里时,哪些地方最容易出错。
需求背景
腾讯 PE 监控任务需要拿到 0700.HK 当前的 trailing PE 或 TTM PE。
最开始的思路很直接:从常见财经数据源获取结构化数据。但是 Yahoo Finance 在当前环境里不稳定,接口返回 403 Forbidden。后来通过 web search 找到一个当前可访问、页面结构也比较容易解析的数据源:
https://stockanalysis.com/quote/hkg/0700/statistics/
所以最后的数据源策略变成了两层:
- 优先直接抓取 StockAnalysis 页面,解析
P/E Ratio; - 如果网页结构变化、抓取失败,才使用 Responses API 的
web_search作为兜底。
这个顺序很重要。模型搜索适合发现信息和兜底,不适合作为首选的结构化行情接口。
官方写法和实际差异
OpenAI 官方文档里,Web Search 是 Responses API 的内置工具。官方示例大致是这样的:
{
"model": "gpt-5.5",
"tools": [
{ "type": "web_search" }
],
"input": "What was a positive news story from today?"
}
官方文档还说明,web_search 是 Responses API 里通用可用的工具版本,同时也存在较早的 web_search_preview 版本。文档地址:
https://developers.openai.com/api/docs/guides/tools-web-search
如果只看官方示例,容易形成两个直觉:
input可以直接传字符串;- 不开流式响应也可以正常拿到
response.output_text。
但我这次使用的是 Codex 配置里的一个 provider:
[model_providers.codexzh]
base_url = "https://api.codexzh.com/v1"
wire_api = "responses"
web_search = "live"
这个 provider 的 /models 返回了这些可用模型:
gpt-5.5
gpt-5.4
gpt-5.4-mini
gpt-5.2
gpt-5.3-codex
实际测试里,gpt-5.5 可以用于 Responses API。但请求格式不能完全照搬官方最短示例。
第一个坑:字符串 input 直接 400
最开始我按常见 Responses 示例直接传字符串:
{
"model": "gpt-5.5",
"tools": [
{ "type": "web_search" }
],
"input": "搜索腾讯控股 0700.HK 当前 trailing PE"
}
当前 provider 返回的是:
{
"error": {
"message": "openai_error",
"type": "bad_response_status_code",
"param": "",
"code": "bad_response_status_code"
}
}
为了排除是不是 web_search 工具导致的,我又测试了不带 tools、只传字符串 input 的情况,结果仍然是同样的 400。
结论是:在这个 provider 上,字符串形式的 input 不可用。
可用的请求格式
最终跑通的格式有几个关键点:
input必须传 message 数组;stream必须为true;- 请求 header 里要设置
Accept: text/event-stream; - 搜索工具名使用
web_search。
可用 payload 如下:
{
"model": "gpt-5.5",
"tools": [
{ "type": "web_search" }
],
"stream": true,
"input": [
{
"role": "user",
"content": "搜索腾讯控股 0700.HK 当前 trailing PE 或 TTM PE。只返回 JSON。"
}
]
}
请求 header:
Authorization: Bearer <OPENAI_API_KEY>
Content-Type: application/json
Accept: text/event-stream
这个地方需要特别注意:这不是说 OpenAI 官方 API 必须这样写。官方文档中,Responses API 的 stream 是可选参数;设置为 true 时,响应会通过 Server-Sent Events 返回。这里只是当前 provider 的兼容性表现。
也就是说,工程里最好不要把“官方 API 能接受的格式”和“当前 provider 实际能接受的格式”混为一谈。尤其是使用代理、聚合网关或自定义 provider 时,最好写一个最小请求脚本,把 payload、header、模型名、工具名分别验证一遍。
第二个坑:SSE 不是普通 JSON
开启 stream=true 之后,响应不再是一个普通 JSON body,而是 SSE 流。
官方 Responses API 文档也说明了:当 stream 设置为 true 时,模型响应会以 Server-Sent Events 的方式流式返回。
https://developers.openai.com/api/docs/api-reference/responses
所以解析时不能直接 response.json()。需要逐行读取 data:,再解析事件内容。
这次实现里,主要拼接这个事件:
response.output_text.delta
也就是每次有文本增量时,把 delta 追加到缓冲区。等流结束后,缓冲区就是最终输出文本。
同时还做了一个兜底:如果没有拿到 delta,就从 response.completed 事件里的完整 response 对象中再提取一次文本。这样可以兼容不同服务端对流式事件的实现差异。
实际工程里的解析逻辑放在:
src/research_agent/data_sources/valuation.py
第三个坑:工具名不要想当然
这次我在当前 provider 上测试了几个工具名:
web_search
web_search_2025_08_26
web_search_preview
web_search_preview_2025_03_11
结论很简单:
web_search:可用,但必须配合 message 数组 input 和 stream=true
web_search_2025_08_26:400
web_search_preview:400
web_search_preview_2025_03_11:400
所以当前项目里固定使用:
{ "type": "web_search" }
这里也能看出一个经验:不要根据网上旧示例或模型版本猜工具名。OpenAI 官方文档当前推荐的 Responses API 工具名是 web_search,但具体 provider 是否支持 preview 名称、日期后缀名称,仍然要实测。
为什么要求模型只返回 JSON
估值数据最终要进入监控链路,不是给人直接阅读。所以 prompt 里我会明确要求:
只返回 JSON。
更理想的输出类似:
{
"symbol": "0700.HK",
"metric": "trailing_pe",
"value": 18.73,
"source": "https://stockanalysis.com/quote/hkg/0700/statistics/"
}
当然,只要求“返回 JSON”并不等于模型永远会返回严格 JSON。工程代码里仍然要做几件事:
- 去掉可能出现的 Markdown code fence;
- 捕获 JSON 解析异常;
- 对
value做数值类型校验; - 保留原始文本,方便调试;
- 数据异常时不要静默吞掉,要让监控链路知道本次估值不可用。
模型搜索可以提升兜底能力,但不能替代数据校验。
数据源设计:搜索不是第一选择
这次调试之后,我更明确了一个原则:不要把模型 web search 当作首选结构化数据源。
更稳的顺序应该是:
- 直接访问可解析网页或正式数据 API;
- 抓取失败时,用 web search 重新发现可用来源;
- 让模型只承担“发现”和“兜底提取”的角色;
- 最终仍然接入正式行情或估值 provider。
原因也很简单。
网页结构会变,搜索结果会变,模型输出格式也可能变。相比之下,正式数据接口、可控网页解析和明确的字段校验,才更适合作为监控系统的主链路。
在这个项目里,当前策略是:
StockAnalysis 直接解析
↓ 失败
Responses API + web_search 兜底
↓ 失败
返回不可用状态,并记录错误
这比“所有估值都问模型”稳定得多。
复现命令
只验证 OpenAI web search 数据源:
PYTHONPATH=src python - <<'PY'
from research_agent.config.settings import Settings
from research_agent.data_sources.valuation import OpenAIWebSearchValuationDataSource
settings = Settings()
snapshot = OpenAIWebSearchValuationDataSource(
api_key=settings.openai_api_key,
base_url=settings.openai_base_url,
model=settings.openai_model,
).get_snapshot("0700.HK")
print(snapshot)
PY
验证完整监控链路:
PYTHONPATH=src python -m research_agent.cli monitor-once
小结
这次调试最有价值的地方,不是找到了某个 PE 数据源,而是把 Responses API + Web Search 接入工程时的几个边界摸清楚了:
- 官方示例是基准,但 provider 兼容性要单独验证;
- 当前 provider 下,字符串
input不可用,message 数组可用; - 当前 provider 下,必须使用
stream=true并按 SSE 解析; - 工具名固定使用
web_search,不要随意换 preview 或日期后缀; - 模型搜索适合作为兜底,不适合作为结构化金融数据的主来源。
以后再接类似能力,我会先写一个最小化探针:只测模型名、请求体、工具名、stream 和解析逻辑。这个探针跑通之后,再把它接进正式业务链路。这样比直接在业务代码里边猜边改,成本低很多。
版权声明: 本文首发于 指尖魔法屋-调试 OpenAI Responses API 的 Web Search 踩坑记录(https://blog.thinkmoon.cn/post/994_%E8%B0%83%E8%AF%95openai-responses-api%E7%9A%84web-search%E8%B8%A9%E5%9D%91%E8%AE%B0%E5%BD%95/) 转载或引用必须申明原指尖魔法屋来源及源地址!