遥测:上游状态码被压平、三分之二失败行没有归因列、21 条错误详情为空 #19
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
一句话
上游过载回的 529 传到调用方手里变成了
bad_response_status_code,而三分之二的失败遥测行连是哪个模型都没记。2026-08-26 那一跑为了搞清「gemini 那条渠道到底怎么了」查了两个多小时,
最后是靠去中转后台翻日志才知道上游在回 529——这两条缺口各自贡献了一半的弯路。
口径
llm_calls表,窗口 2026-08-26 07:04 ~ 14:11 UTC,共 4372 行qwen3.7-plus/gpt-5.5/gemini-3.1-pro走中转newapi.iomgaa.online,monkey-ocr走内网一、上游的状态码在中转那一层被压平,PolyGateway 原样透传,于是丢了
gemini-3.1-pro110 次调用错 46 次,其中 35 条错误正文是同一句:上游实际回的是 529(过载)。这个数是去中转后台翻日志才拿到的,
从 PolyGateway 记的这一行里看不出来——429、500、503、529 到这边长得一模一样。
这两种的处理方式是相反的:配额打满该退避重排,上游过载该换渠道或者等。
分不开就只能一律当瞬时错误重试,而重试正是过载时最不该做的事。
压平这一步是中转做的,PolyGateway 只是原样收下。但PolyGateway 是唯一有机会
把「我实际收到的 HTTP 状态码」单独记一列的地方——它手里有响应对象,中转的 body
再怎么糊,status line 是它自己看见的。现在这个数只以字符串形式出现在
error的开头(
503 | {...}),要用得先 split。建议:
llm_calls加一列存实收的 HTTP 状态码,以及一列存上游 body 的原文(现在 body 是拼进
error的,但只有出错时才有,且和我们自己的前缀混在一起)。二、三分之二的失败行没有归因列
333 条失败行里,218 条的
model、provider、source_name三列全是空串。这批是 PolyGateway 自己的渠道级拒绝:
渠道名明明就在
error那个自由文本里,却没有进任何一个结构化列。后果是「哪条渠道熔断了几次」只能拿 LIKE 去凑,而
circuit_open那 182 条恰恰是判断一条渠道健康与否最要紧的信号——排查时我是先按
model分组,得到的结论是「gemini 只错了 46 次」,直到发现还有 218 条无归因行才知道
真实的失败规模是三倍。
熔断时具体用哪个 source 可能还没选出来,但渠道是知道的。
建议:给渠道名一个自己的列。source/model/provider 选不出来时留空可以接受,
渠道名留空不行。
三、21 条错误的详情是空的
冒号后面什么都没有。一条超时不说自己撞的是哪个上限、撞的时候等了多久;
一条网络错误不说是连接失败还是读到一半断了。
这一条不是理论问题。这一跑里判断「12:22 前后中转不可达三分钟」这件事,
靠的是恰好有几条
网络错误带上了 httpx 的原文:而另外 11 条同类的是空的。同一类异常,有的带原文有的不带,
说明是某条路径上
str(exc)拿到空串就直接拼进去了。建议:拿不到
str(exc)时退回到异常类名。超时那一类还应该把撞上的是哪个上限(连接超时 / 读超时 / 流活性 / 总时长)和实际等了多久记进去。
附:这次排查里被证伪的几条猜想
留个记录,免得下次重复查。这几条在
llm_calls现有的列上都查得出来,说明这张表本身的骨架是对的,缺的是上面那三处。
circuit_open的时延中位 452 ms,179 条
nursing_extract_primary里 176 条在 10 秒内。怀疑过「先在并发闸上等满槽位、再被熔断拒掉」,不成立。
失败的均值 0.50、成功的 0.41,最大都是 1。
messages平均 5820 字符,成功的 6447,失败的反而更小,区间完全重叠。
monkey-ocr全程 1343 次只错 4 次(0.3%)。