AI 项目上线就停:一次事件循环阻塞的完整排查过程
工信部在 2026 年 8 月 31 日印发了《关于开展人工智能应用服务商培育专项行动的通知》(工信厅科函〔2026〕414号),四类服务里专门列了一项「运营管理服务」。政策解读里有一句话值得工程师记住:技术方案落地难,症结往往不在算法,而在落地环节由谁承接、如何持续服务。
我把这句话在自己项目上验证了一遍。下面是一次真实的排查记录,含报错原文、定位过程和修复代码。
现象:长时运行 7 分钟后数字人掉线
给客户做的 AI 客服演示系统,带数字人。压测时连续跑长片,跑到第 7 分钟左右:
- 数字人画面消失,界面弹横幅:
数字人未能开启 - 同时对话区出现
Failed to fetch - 但后端进程活着 :
/api/health返回 200,审计日志持续写入,没有任何崩溃记录
真的不是网络问题。
注意这里的第一个坑:界面报的原因和真正的原因不是一回事。横幅说是「数字人未能开启」,看起来像 SDK 或网络问题,实际根因在后端。别信横幅。
短测不会暴露。跑 30 秒,一切正常。测不出来。跑 30 秒一切正常------验收当天客户通常只会让你跑五分钟。
定位:先分清「进程挂了」和「事件循环被堵了」
判据很简单:
bash
# 后端还活着吗
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8011/api/health
# 200 → 进程没挂
# 但前端为什么 Failed to fetch?看审计日志时间戳是否连续
tail -f app/audit.jsonl | jq -r '.ts + " " + .kind'
# 日志在写,但每次请求的间隔出现 20 秒以上的空洞 → 事件循环被同步调用堵住
Failed to fetch + 后端 200 + 日志有空洞,三个信号。
一起出现,基本可以锁定:异步事件循环里有同步阻塞调用。
根因:async def 里用了同步 httpx.Client
出问题的写法(网关代理,简化后):
python
@app.api_route("/api/xmov/gateway/{path:path}", methods=["GET", "POST"])
async def xmov_gateway_proxy(path: str, request: Request):
# ❌ 同步客户端:await 不到,直接把事件循环占住
with httpx.Client(timeout=25) as c:
r = c.request(request.method, url, content=await request.body(), headers=hdrs)
return Response(r.content, status_code=r.status_code)
单进程 uvicorn 下,httpx.Client.request() 是同步阻塞 的,timeout=25 意味着最坏情况事件循环被占住 25 秒。这期间前端所有请求排队,数字人 SDK 的后续调用也拿不到响应,会话被判失败。
顺带一提,识别这类问题的另一个信号在访问日志里:我们自己发起的请求 UA 是 Python-urllib/3.13 ,而浏览器请求的 UA 是 Mozilla/...。看到代理链路里出现 Python UA,说明它在自己调自己,这一跳就是排查重点。先看它。
修复很简单。
python
@app.api_route("/api/xmov/gateway/{path:path}", methods=["GET", "POST"])
async def xmov_gateway_proxy(path: str, request: Request):
body = await request.body()
# ✅ 异步客户端:让出控制权,其它请求照常处理
async with httpx.AsyncClient(timeout=25) as c:
r = await c.request(request.method, url, content=body, headers=hdrs)
return Response(r.content, status_code=r.status_code)
另一处同类问题:会话结束时的同步 HTTP 调用,挪进后台线程即可,不要放在请求链路里。
python
import anyio
anyio.from_thread.run_sync # 或在 FastAPI 里用 BackgroundTasks
同一批修复里的另外两处(也都在工程侧,不在模型侧)
① 播报分段必须在调用方串行化。 SDK 不提供「播报结束」回调时,同一个 tick 里连调 speak() 会被厂商判为「上一轮未结束」:
js
// ❌ 同 tick 全发
const segs = splitSentences(text, 60);
for (let i = 0; i < segs.length; i++) AGENT.speak(buildSsml(segs[i], opts), first, last);
// ✅ 按字数估时,上一段播完再发下一段
const EST_MS_PER_CHAR = 190, BUFFER_MS = 500;
(async () => {
for (let i = 0; i < segs.length; i++) {
AGENT.speak(buildSsml(segs[i], opts), i === 0, i === segs.length - 1);
await sleep(segs[i].length * EST_MS_PER_CHAR + BUFFER_MS);
}
})();
② 厂商消息要分级,不能一票判死。 把任何厂商消息都当成致命错误,会让一次解码抖动就把整个会话拆掉:
js
// 只有会话 / 鉴权 / 素材类错误才判死;解码抖动等记一笔并尝试重连
const FATAL = /session|auth|token|asset|not\s*found/i;
onMessage((msg) => { FATAL.test(msg.text) ? failAvatar(msg) : logOnce(msg); });
修复后的实测
| 项 | 修复前 | 修复后 |
|---|---|---|
| 连续运行 | 第 7 分钟掉线 | 170 秒连续跑全程正常 |
| 前端错误 | 稳定复现 Failed to fetch |
无 |
| 事件循环阻塞 | 最长 25 秒 | 无阻塞 |
给做交付的同行三句话
- 验收当天跑五分钟,测不出这类问题。 长时运行是唯一的暴露方式,把「连续跑 10 分钟」写进交付前必跑清单。
- 界面报错和真因经常不是一回事。 先把「进程活着没有」和「事件循环通不通」分开判断,再去看业务逻辑。
- 政策提到的「运营管理服务」,落到代码上就是这些事:监控空窗、解码抖动、会话回收。它们不性感,但决定了第 91 天系统还在不在。
AI 参与说明:本文的排查记录、命令与实测数据来自作者现场操作;文字由作者与 AI 辅助整理,事实与观点经作者核实。
信息来源:① 工业和信息化部办公厅《关于开展人工智能应用服务商培育专项行动的通知》(工信厅科函〔2026〕414号,2026-08-31);② 南方都市报《到年底破两千家!工信部专项行动:打造 AI 应用服务商资源池》(2026-09-02)。
我们专注小微企业知识库与 AI 客服落地(Apache-2.0 开源,含国标 61 项自查清单)。