Claude 工具调用返回空:8 次失败里只有 1 次状态码不对,其余全带 200

文章目录

    • [一、实验设计:20 次真实调用,每一条都记账](#一、实验设计:20 次真实调用,每一条都记账)
    • [二、四种「没拿到数据」,状态码一律 200](#二、四种「没拿到数据」,状态码一律 200)
    • 三、三档判据,检出能力差一个数量级
    • [四、trace 的真正价值:能说出「哪个参数错了」](#四、trace 的真正价值:能说出「哪个参数错了」)
    • [五、成本:每条 331 字节,占响应体的 0.12%](#五、成本:每条 331 字节,占响应体的 0.12%)
    • 六、两个实现层的坑
    • 七、结论
    • 八、边界与声明
    • 参考链接

给 Claude 接工具,最烦的不是报错,是「没拿到数据」------返回是空的,任务卡住,你却说不清是接口坏了、参数写错了,还是真的没数据。

我把 8 个香港官方数据接口跑了一遍真实调用:20 次请求里 8 次「没拿到数据」,其中 7 次的 HTTP 状态码都是 200 。更坑的是,有一次返回的是合法 JSON、信封里还写着 success------但它是失败的。这篇文章用真实数据把「空」拆开,量化三档判据各能抓到多少,最后给出一条每条只要 331 字节的记账格式。

一、实验设计:20 次真实调用,每一条都记账

我对 8 个香港开放数据接口(天文台 4 个口径、金管局、统计处、运输署、入境处、环保署、1823)发起 20 次真实请求:12 次正常取数,8 次故意带上各种问题------参数拼错、参数不存在、分页越界、表号不存在。这些错误参数全部来自接口规范的真实边界 (不存在的 dataType、越界的 offset、不存在的表号),不是凭空编的。

每次调用记一条 trace,字段只有七个:

python 复制代码
def run_case(tag: str, tool: str, url: str, expect: str) -> dict:
    t0 = time.time()
    r = subprocess.run(["curl", "-sL", "-m", "60", "-w", "\n%{http_code}", url],
                       capture_output=True)
    out = r.stdout.decode("utf-8", "replace")
    body, _, code = out.rpartition("\n")
    raw = body.encode("utf-8")
    shape = shape_of(raw)                      # 响应长什么样(见第三节)
    err_code = status_name = None
    if shape == "json_envelope":               # 信封结构里挖状态字段
        d = json.loads(raw.decode("utf-8-sig", "replace"))
        h = d.get("header") or {}
        err_code = h.get("err_code")
        st = h.get("status") or {}
        status_name = st.get("name") if isinstance(st, dict) else st
    return {"tag": tag, "tool": tool, "url": url, "http": code.strip(),
            "bytes": len(raw), "ms": int((time.time() - t0) * 1000),
            "shape": shape, "err_code": err_code, "status_name": status_name,
            "arg_fp": hashlib.sha256(url.encode()).hexdigest()[:8],
            "resp_fp": hashlib.sha256(raw).hexdigest()[:16]}

注意最后两个字段:请求参数的指纹和响应内容的指纹。后面会用到。

二、四种「没拿到数据」,状态码一律 200

8 次异常调用,返回长这样:

调用 状态码 字节 返回长什么样 真实成因
hko_swt 200 10 B {"swt":[]} 真没数据(特别提示通道当时为空)
hko_typo 200 726 B 纯文本的参数说明 dataType=warnsm 拼错
hko_badlang 200 726 B 纯文本的参数说明 lang=xx 不存在
hkma_over 200 110 B JSON,"success":trueerr_code:"1001" 分页越界
csd_bad 200 550 B 合法 JSON 信封,status.name:"Fail" 表号 999-99999 不存在
td_bad 404 1,097 B HTML 错误页 文件路径不存在

这张形态表建议收藏------四种失败的字节数差了两个数量级,长度本身就是第一条线索。

三次震惊,一次比一次深:

  1. 参数写错,接口返回 HTTP 200,还附了 726 字节的参数说明文档------天文台把这个当正常响应处理。
  2. 金管局分页越界,返回里写着 "success": true ,错误藏在 err_code: "1001" 里。拿 success 字段当判据的代码会直接放行。
  3. 统计处的返回是完全合法的 JSON 信封 ,但错误藏在 header.status.name 里------和金管局用的字段都不一样。

20 次调用里状态码的分布:19 次 200,1 次 404。也就是说,如果你只盯着状态码,8 次异常里你只能发现 1 次。

三、三档判据,检出能力差一个数量级

我用三档判据对同样的 8 次异常各判一遍:

python 复制代码
def detect_by_http(tr: dict) -> bool:
    """最朴素的判据:状态码不是 200 就算失败。"""
    return tr["http"] != "200"


def detect_by_shape_generic(tr: dict) -> bool:
    """通用形态判据:空容器 / 不是 JSON / err_code 不为 0000 / 非 200,
    不读任何源特有的状态字段。"""
    if tr["http"] != "200":
        return True
    if tr["shape"] == "plain_text":
        return True
    if tr["shape"] in ("json_empty_container", "json_empty_array"):
        return True
    if tr["shape"] == "json_envelope" and tr["err_code"] not in (None, "0000"):
        return True
    return False
判据 检出 漏掉
只看 HTTP 状态码 1/8 其余 7 次
通用形态判据 7/8 统计处那条(它的失败不在通用字段里)
trace + 按源适配 8/8 ---

通用判据漏掉的那一次,恰好说明**「成功标识」在香港官方接口里没有统一约定**:金管局用 err_code,统计处用 status.name,天文台直接换返回类型,运输署靠 404。一套判据打天下,一定会漏。

四、trace 的真正价值:能说出「哪个参数错了」

判出「有问题」只是第一步。trace 里记了完整请求 URL,所以每次异常都能定位:

复制代码
hko_typo     → 请求参数 dataType=warnsm&lang=tc
hko_badlang  → 请求参数 dataType=rhrread&lang=xx
hkma_over    → offset=99999
csd_bad      → id=999-99999(表号不存在)
td_bad       → 路径 no_such_file 不存在
hko_swt      → 无参数问题(该通道当时确实没有数据)

注意 hko_swt 那条------trace 帮你排除了参数问题,确认那次空是接口的真实状态。

这五条定位记录值得收藏照抄------排查 Agent「拿不到数据」时,第一件事就是把请求参数原样打出来。**「没数据」和「查错了东西」在返回值上几乎一样,在 trace 上截然相反。**这正是 Agent 排障时最需要的一条分界线。

五、成本:每条 331 字节,占响应体的 0.12%

20 次调用的响应体合计 5,478,266 字节,20 条 trace 合计只有 6,620 字节------0.12%。记账的成本可以忽略不计。

我还把其中 6 次调用原样重放了一遍:6 次的响应 SHA-256 指纹全部一致 。这意味着这一批 trace 是可复现的------拿着旧 trace 能重建当时的现场。代价是耗时波动很大:同一个接口两次请求,一次 1,366 ms、一次 3,207 ms,差了两倍多。所以 trace 里必须记耗时------只记结果的账本,解释不了为什么慢;而「慢」在多步任务里会沿着调用链累积,最后变成用户看到的那次超时。

六、两个实现层的坑

UTF-8 BOM 会把 JSON 判成纯文本。 运输署、入境处、1823 的响应都带 BOM,我的第一版形态判定把这三个 JSON 数据源全判成了纯文本。用 utf-8-sig 解码后再判定才对。

别用「有没有逗号」判 CSV。 天文台的英文错误说明里也有逗号,会把 726 字节的错误文本判成 CSV。要求首行至少两个逗号才可靠。

七、结论

问题 实测结果
「没拿到数据」有几种 4 种形态:空容器 / 纯文本 / 信封错误(两种字段)/ 404
状态码能抓到几成 1/8(12.5%)
通用形态判据呢 7/8,漏掉 status.name 那条
trace 呢 8/8 检出 + 8/8 定位到参数,成本 0.12%

三条:

  1. HTTP 200 和 success:true 都不是「没出错」的证据。 在多源数据接入里,它们只是「没按错误方式出错」。
  2. trace 至少要记三样:完整请求参数、响应形态签名、内容指纹。 前两个用来定位,最后一个用来重放。
  3. 每个源的成功标识字段都要单独适配------这一步没有通用解,只有「逐个看过返回」这一条笨路。而 trace 就是把这条笨路固化下来,让下一次不用重走。

这套 trace 记法建议直接收藏 :字段就七个,任何 Agent 项目把 run_case 换成自己的调用函数就能用,先把账记起来,出了事才有得查。

八、边界与声明

  • 错误参数来自接口规范的真实边界(不存在的 dataType、越界 offset、不存在的表号),用于验证判据,不代表接口的常规错误率;
  • 本文只讨论工具调用层的可观测性,不涉及模型侧的行为;
  • 数据均为香港政府公开接口,2026-09-20 当天实抓,trace 与原始响应全部落盘,脚本可原样复跑。

参考链接

  1. https://data.weather.gov.hk/weatherAPI/opendata/weather.php?dataType=warnsum\&lang=tc
  2. https://api.hkma.gov.hk/public/market-data-and-statistics/daily-monetary-statistics/daily-figures-interbank-liquidity
  3. https://www.censtatd.gov.hk/tc/web_table.html?id=650-80001

原创声明:本文全部数据来自 2026-09-20 当天对香港官方接口的真实调用(20 次 + 6 次重放),trace 与原始响应已落盘。觉得有用点个关注不迷路,下一篇拆一个新数据源。

相关推荐
Web3&Basketball1 小时前
CRM Agent 后训练实战:3 倍更少错误
python·架构·大模型·agent·推理
AIFQuant2 小时前
ETF行情API接入踩坑记:从报错到跑通的七个问题
python·金融·区块链·etf·基金
GPU实战笔记3 小时前
云端 Python 开发:JupyterLab 还是 VS Code Remote-SSH?
python·vs code·jupyterlab·remote-ssh·远程开发
狗狗狗狗狗乐啊4 小时前
搭一个 AI 对话工作台 AChat:从 0 到可用的完整记录(一)
python·react.js·ai编程
白猫不黑4 小时前
Python实现简易Web弱口令爆破与防护方案
python·web安全·计算机·网络安全·黑客·信息安全·渗透测试
kuuailetianzi5 小时前
Python初识:定位、优势与发展历程
python
guoran_shini5 小时前
LLM微调-训练垂类问答模型
python·lora·sft
basketball6165 小时前
Python FastAPI 介绍以及常用方法
python·fastapi·vllm·ai infra
todoitbo5 小时前
本地图库语义搜索实战:接上蓝耘元生代,让“傍晚的海边“能搜到图
人工智能·ai·api·工具实战