Notes · 2025.06.21

推理接口先写超时

Python / FastAPI。

1 min · textguard · code

前端转圈的时候,评委不会去读 uvicorn 的日志。模型推理封在 FastAPI 里,成功路径好写:文本进,分数出。难写的是超时、空结果、权限提示。这三件事我后来在企业问答接口里又碰见一次。第一次把它们当成功能来写,是在 TextGuard。我是张恒基。接口如果只在开心的时候工作,演示那一天它通常不开心。

成功路径会骗人

把推理函数塞进一个 POST,返回 JSON,本地 curl 一次通,会让人以为后端做完了。本地那次通,用的是短文本、热模型、没人跟你抢 GPU。评委换一组样本,文本更长,冷启动更慢,接口就挂。挂了如果只抛未捕获异常,前端只能转圈。转圈在答辩里是最长的一分钟。

我把超时写成明确的响应。时间到了,返回可识别的错误码和一句人能看懂的话,而不是让网关自己切断。切断之后前端只知道失败,不知道是慢还是死。慢和死的处理不同。慢可以提示再试。死要提示别再点。别再点这句话,得从接口里带出来,不能让前端猜。

空结果也是结果。空文本、纯标点、长度不够,模型可能给出无意义分数,也可能直接失败。失败如果用 500 表示,日志里像事故。事故应该留给真的事故。空输入用 4xx 和明确字段,前端才能画空状态,而不是画一条假的风险曲线。假曲线比转圈更坏,因为它看起来像成功。

权限提示不要藏在 403 里

检测报告会落到要签字的人手里。谁能看、谁能导出,后来都要有权限。权限失败如果只回 403,前端只能写「失败」。使用者不知道是没登录、没角色,还是这份报告不属于他。我把权限失败拆成可展示的理由。理由要短,短到能画在页面上。长理由只适合日志。

FastAPI 的依赖注入适合把超时、鉴权、空输入挡在推理前面。挡在前面,模型就少跑几次无意义的前向。少跑,GPU 和评委的耐心都能省一点。省下来的时间不要用来加新特征,先用来把错误字段写稳。字段不稳,前端每次联调都在猜。猜的联调看起来像在干活,实际在还债。

我是总负责人,不意味着每一条路由都自己写。意味着路由表上的失败路径我要说得出走哪条。说不出,演示当天出了超时,就只能说「后端在查」。在查,评委听成「没准备」。没准备和真的在查,现场分不出来。分不出来的时候,按没准备算。

同一套失败后来又用了一次

2026 年 1 月起我在智能知识科技跟企业 RAG。问答接口同样先对超时、空结果、权限提示。企业库有权限,论文库默认没有这层。任务不同,失败的形状很像。像,不代表可以把 TextGuard 的错误码原样贴过去。贴过去会把检测的语义带进问答。问答里「空」往往是检索没召回。检测里「空」往往是输入不配打分。两个空,页面文案不能一样。

我把「先写超时」当成个人习惯,不当成架构口号。习惯的意思是:新接口的第一张草稿里就要有这三件事的位置。位置可以先返回占位字段,不能等成功路径漂亮了再补。漂亮的成功路径会让人舍不得改响应结构。舍不得改,失败路径就永远挤在日志里。

日志仍要打。打给自己看的,和返回给前端的,分开。评委看不见日志。使用者也不看。看得见的是页面上转不转圈、空不空、有没有一句人话。人话从接口来。接口不提供,前端就会编。前端一编,口径又散。

超时时间本身也要写进配置,不写进某个函数的默认参数里当魔法数。魔法数改了没人知道,评测和演示会对不上。对不上的时候,前端说三十秒,后端已经在十五秒切了。切开之后各打各的日志,联调会变成吵架。吵架看起来像后端不稳。不稳的根子经常是超时没有单一来源。单一来源很土。土的东西我愿意先写。

超时是功能,不是异常里的一行

我收到的限制来自现场:推理可以慢,不可以无声地慢。FastAPI 这层必须把超时、空结果、权限提示变成和成功分数同级的响应。同级,前端才能当功能做。当功能做了,评委换样本时至少能看见一句解释,而不是一个永远转的圈。圈转够了,项目就不像全栈,像一次没包完的调用。

没包完的调用可以过单元测试。单元测试爱成功路径。成功路径我当然要测。测完如果没有超时用例、没有空文本用例、没有未登录导出用例,测试绿了也是假绿。假绿会在京彩的场里变成转圈。转圈的那一分钟,总负责人没有地方把责任转出去。转不出去,就该在写接口的第一张草稿里把这三件事留下位置。位置比新特征早。早这个顺序,我后来在企业问答接口里又守了一次。