Skip to content

生产级 Agent 服务的 10 个坑:我的博客站和面试题教会我的发布工程

一个 8 年 Java 后端,原计划 Week 7 学限流、熔断、成本控制,结果一周里连续踩了发布事故、长连接超时、LLM 生成内容烂格式三个坑,最后发现:生产级服务的坑不在 PPT 里,在你凌晨两点收到的告警里。

这篇不讲 Sentinel 配置,讲我真实踩过、修过、并且加了防线不让它再发生的 10 个坑。


开篇:计划里的"工程化"和真实的工程化

Week 7 学习计划上写着:高可用架构(限流/超时/重试/熔断)、成本控制(Prompt 缓存/模型路由/Token 预算)、安全(注入防护/脱敏/审计)。

结果这周三件大事:一次发布把线上站点搞成裸 HTML、一次大版本部署 SSH 连接 10 分钟后超时、cron 自动生成的面试题把换行写成了字面量 \n

修完这三个事故我意识到:限流熔断可以面试前背,发布安全和数据正确性是线上流血学的。 下面 10 个坑,每个都对应一次真实故障或一个生产决策。


一、发布篇:发布系统的正确性不来自祈祷

坑 1:先删旧版再传新版,发布窗口内线上是残的

我的博客站部署流程原来是:rm -rf 服务器上的 dist 目录,然后 sftp 逐个文件上传新版本。一万多个小文件,传几十分钟。某天凌晨传到一半断了——旧版已经删了,新版的 CSS/JS 一个没传上去。

更坑的是 nginx 配置了 try_files $uri $uri.html $uri/ =404,404 兜底返回 index.html。于是浏览器请求 CSS,服务器返回 200,内容却是 176KB 的 HTML。页面能打开,完全没样式,裸 HTML 怼脸。

修复:原子发布。 新版本永远先解压到独立的 staging 目录(dist.new.<时间戳>),校验通过才切换:

bash
# staging 阶段:线上 dist 毫发无损
tar xzf dist.tar.gz -C /var/www/dist.new.20260820/

# 校验:入口文件在 + 引用的资源全落盘 + 文件数偏差 <1%
test -f dist.new/index.html || exit 1
local_count=$(tar tzf dist.tar.gz | wc -l)
remote_count=$(find dist.new -type f | wc -l)
[ "$remote_count" -ge "$((local_count * 99 / 100))" ] || exit 1

# 原子切换:同一文件系统上 mv 是原子操作
mv dist dist.old.20260820
mv dist.new dist

任一步失败,清 staging,线上保持旧版。发布安全的关键不是别出错,是出错的时候线上还是好的。

坑 2:一万个小文件逐个传,每个文件都是中断点

sftp 逐文件 put,每个文件都有一次握手 round-trip。1 万个文件几十分钟,断一次全盘皆输。

修复:打包成单个 tar.gz 传连续字节流,远端解压。 实测 10054 个文件、721M 数据,打包 222M,上传 29 秒,解压 9 秒,端到端 54 秒。

这和后端开发里"循环单条 RPC 改批量调用"是同一个道理:一万次握手打包成一次流传输。

坑 3:上传"成功"不等于文件完整

网络截断会产生半文件。脚本退出码是 0,文件少了半截。

修复:传完比对字节数,解压后再比对文件数,双重确认。

python
sftp.put(local_tar, remote_tar)
remote_size = sftp.stat(remote_tar).st_size
assert remote_size == os.path.getsize(local_tar), "上传截断,中止部署"

坑 4:没有回滚能力,发布就是单向门

原子切换时旧版不删,mvdist.old.<时间戳>,保留最近 1 份。回滚脚本三行:

bash
mv dist dist.bad.$(date +%s)   # 坏版本留着尸检
mv dist.old.* dist             # 上一版顶上

回滚的前提是发布原子。 如果发布过程中线上半新半旧,回滚都不知道往哪滚。

坑 5:长任务 SSH 连接被中间网络悄悄掐死

大版本部署 3.8 万个文件、2.3G,本地构建要 10 分钟以上。构建完进部署阶段,paramiko 报 open_session timeout——连接早死了,只是没人发现。

根因:NAT 和防火墙会回收空闲 TCP 连接,SSH 客户端默认不发心跳。

python
transport.set_keepalive(30)   # 30 秒一个心跳包
# 部署前探活,连接死了自动重连,最多 3 次

这个坑在 Java 里见得多了:数据库连接池的 testOnBorrow、HTTP 客户端的空闲连接检测,都是同一件事——长连接别假设它一直活着,客户端得自己发心跳。


二、数据篇:构建产物目录里不能放活数据

坑 6:用户数据放在构建产物目录,一次构建全没

我在博客站 dist 目录里顺手放了个照片分享小站(页面 + 照片 + 用户编辑的文案 JSON)。博客每次上线构建清空 dist 重铺——照片站连页面带数据整个被干掉,用户写的 13 条文案只救回 1 条。

根因不是"构建会清目录",是"活数据放进了可丢弃目录"。 dist/target 这种构建产物是无状态的、随时可以重新生成的;用户数据是有状态的、丢了就没了。两者必须物理隔离:

nginx
# 数据站迁出 dist,nginx alias 指到独立目录
location /lwx/love/wj/ {
    alias /var/www/love-wj/;
    index index.html;
}

修完手动 rm -rf dist 模拟构建清空,照片站纹丝不动,才算完。

面试金句版:把用户数据放在构建产物目录,等于把数据库建在 /tmp。重新部署不丢数据是及格线。

坑 7:文件上传功能的 413 和超时

照片站加了网页上传功能,上线就踩两个 nginx 默认值:

  • client_max_body_size 默认 1m,传视频直接 413
  • 代理超时默认 60s,大文件传不完就 504
nginx
location /lwx/love/wj/api/ {
    client_max_body_size 600m;
    proxy_read_timeout 600s;
    proxy_send_timeout 600s;
}

服务端还要配套:文件类型白名单、按日期前缀 + 自增序号防重名、文件名防路径穿越(不能含 ../)、写完全量重建索引。


三、LLM 篇:Agent 时代的新坑

坑 8:LLM 生成的内容没有校验就落盘

我有个 cron 每天自动生成 5 道面试题 markdown。有一天生成的文件格式全烂:LLM 把多行答案当 JSON 字符串写,换行变成字面量 \n、3500 个字符挤在一行、"}] 这种 JSON 残留把日期标题都吞了、还有大段重复。

这种问题手工改一次没用,明天还犯。解法是加 lint 质量门,和代码过 CI 一个道理:

python
# lint-questions.py:9 类检查
# 1. 字面量 \n   2. 超长行(>400字符)   3. JSON 残留( "]} / \" / \uXXXX )
# 4. 标题尾脏引号  5. 重复片段  6. 每天 5 题 5 答案
# 7. 答案编号连续  8. 日期倒序  9. anchor 存在

支持 --fix 自动修(修前备份),拿坏文件回归测试,6 个错误 1 个警告全抓到。cron 流程加硬性规范:必须用文件编辑工具写多行文本、禁止手拼 JSON 字符串、写完强制跑 lint,不过不许发布。

原则:LLM 的输出和用户输入一样——永不信任,始终校验。

坑 9:Agent 服务的 HTTP 超时和传统服务不是一个量级

传统 API 请求路径确定:查库、计算、返回。Agent 请求内部是个 while 循环:LLM 决策 → 调工具 → LLM 再决策……循环几次、跑多久都不确定,一次 30 秒到 2 分钟很正常。

同步 HTTP 等到底,nginx/网关的 60 秒 idle timeout 先把你切了。流式响应(SSE)不是体验优化,是超时刚需:

python
@app.post("/chat")
async def chat(req: ChatRequest):
    async def gen():
        async for event in agent.astream(req.message, session_id=req.session_id):
            yield f"data: {event.json()}\n\n"   # 边推理边推,连接一直有字节
    return StreamingResponse(gen(), media_type="text/event-stream")

配套两个断路器:循环最大迭代次数(防死循环烧 token)、整体硬超时(防挂死)。

坑 10:重试策略不分副作用,就是在制造事故

LLM 调用超时了,重试一下很合理。但 Agent 的工具调用里混着发邮件、创建工单、扣款这种写操作——如果第一次其实成功了只是响应丢了,自动重试就是重复执行。

重试按副作用分类:

调用类型重试策略
检索、生成(只读、幂等)重试 1 次,指数退避
发邮件、建工单、扣款(有副作用)禁止自动重试,靠幂等键去重
LLM 连续失败熔断,快速失败 + 降级话术

写操作的幂等设计:客户端带 idempotency key,服务端去重。这和消息队列的"至少一次 + 消费端幂等"完全同构。


总结:10 个坑背后的 4 条原则

原则对应坑
切换前不动线上:staging 校验 + 原子切换,故障不进生产1/3/4
批量优于循环,流优于碎文件:减少中断点和 round-trip2/5
有状态和无状态物理隔离:构建产物可丢弃,用户数据必须独立6/7
永不信任,始终校验:LLM 输出过 lint,外部调用分副作用8/9/10

面试时怎么答"你做过什么生产级工程"?我不会背限流算法,我会讲:我的发布流程中断过、线上裸奔过、数据被构建清过、LLM 把文件写烂过——每个事故后面都有一道现在还在跑的防线。

故障不可怕,同一个故障发生两次才可怕。第一次修好,第二次让脚本拦。

参考:个人实战项目 deploy_dist.py(原子部署)、rollback-blog.py(回滚)、lint-questions.py(LLM 内容质量门);部署经验已沉淀至个人站点运维手册

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1