模型调用的重试:唯一留下的同步重试
Parent: events
落地记录在 backend/internal/conversation/inference/http_retry.go、backend/internal/infra/httpx/retry_transport.go、backend/internal/infra/httpx/retry_budget.go、backend/internal/conversation/inference/errors.go。
模型调用在请求内部就是会重试的:只由 httpx transport 重试,最多 2 次。这和 retry-has-one-owner 并不矛盾:那篇的“只发一次”是禁第二层重试循环,transport 自身的重试是被允许的那一层。不要把那篇的日历例子推广过来:日历 free/busy/insert/delete 走 NoRetry,模型调用走带 transport 重试。
什么会重试
- 最多重试 2 次(1 次初发 + 2 次重发),基础延迟 400 毫秒。
- 重试:除调用方自身 context 取消/超时之外的传输错误;状态码 429 或 5xx 的响应。
- 绝不重试:响应头已到、开始流式输出之后(已流出的 token 不会重读);调用自身的取消或超时。
- 每次重试前等待:自身指数退避(400 毫秒、然后 800 毫秒)与服务商
Retry-After头两者取大。比服务商要求的时间更早重发只会让封禁更严重,所以头更大时永远以头为准。 - 等不起:如果等待超过本轮剩余预算,transport 立刻把响应交回去,不会把预算睡光再失败。
Public 档上限
- Public 档一轮重试总等待上限 10 秒;超过就快速失败,返回 "The AI is busy right now — give it a minute and ask again."。Coded / BYOAI 档照旧等下去。
- 原因:public 档跑在站长自己的免费服务商上;2026-09-25 那天每一步都返回 429 附 31–43 秒 retry-after,访客在沉默里等了好几分钟。
- 限流的定义就是:重试用完后服务商仍返回 429,或者 retry-after 超过本轮愿意等的时长;两种都映射到同一句 busy 提示。
重试时看得见
每次退避都记一条 "llm transient failure — retrying",带 attempt、max、status、wait_ms 和 wait_from(backoff 还是 retry-after);前端 throbber 显示 "retrying"。wait_from 的作用是证明这次等待确实尊重了服务商,而不只是碰巧等够了。
不是这个机制
写路径的 2 秒等待 + 收据(async-response-contract)是另一条路:管的是写操作落进搜索索引,不是模型调用。两者不能混:模型调用超时没有收据,也没有 index_job_id。