【从0搭建AI智能体·11】把 Agent 部署上线:Docker + 限流 + 熔断 + 优雅降级

【从0搭建AI智能体·11】把 Agent 部署上线:Docker + 限流 + 熔断 + 优雅降级

📚 本文是《从 0 搭建你的 AI 智能体》专栏第 11 篇。
上一篇:《Agent 可观测性:日志、追踪、成本监控》 | 专栏目录:点此查看全部

标签建议:部署Docker限流熔断FastAPIAI Agent高可用

📌 前言:本地跑得好好的,一上线就崩

你的 Agent 在本地python app.py跑得欢,于是你信心满满地丢上服务器,然后:

  • 密钥怎么安全传进容器?总不能写死在代码里带上去;
  • 来了一波流量,并发请求把上游 API 的限额打爆,全线 429;
  • 上游模型服务抽风了,你的服务跟着一起卡死、雪崩;
  • 用户请求超时,前端转圈到天荒地老,没有任何兜底。

「能在本地跑」和「能扛住线上流量」之间,隔着一整套工程化。这一篇就把 Agent 从「本地脚本」变成「生产级服务」——Docker 容器化、限流保护、熔断防雪崩、优雅降级、健康检查,一套打包给你,都是可直接用的配置和代码。

💡本文适合谁:Agent 功能已完成、准备部署到生产的开发者。以 FastAPI + Docker 为例。阅读约 14 分钟。

目录

  1. 上线前的检查清单
  2. 第一步:Docker 容器化
  3. 第二步:密钥与配置的安全注入
  4. 第三步:限流(保护上游 API 额度)
  5. 第四步:熔断(防止雪崩)
  6. 第五步:超时与优雅降级
  7. 第六步:健康检查与优雅关闭
  8. 完整部署配置
  9. 常见坑与建议
  10. 总结

① 上线前的检查清单

先给你一张「上线体检表」,逐项对照,缺哪补哪:

项目为什么本文章节
容器化环境一致、可复制、易扩缩容
密钥安全注入绝不硬编码进镜像
限流保护上游额度、防打爆
熔断上游挂了别跟着雪崩
超时 + 降级别让用户无限等
健康检查让编排系统知道死活
日志 + 监控出事看得见(上一篇)第10篇

② 第一步:Docker 容器化

Docker 让「在我机器上能跑」变成「在哪都能跑」。写一个Dockerfile

# 用官方 slim 镜像,体积小 FROM python:3.11-slim WORKDIR /app # 先装依赖(利用 Docker 层缓存,依赖不变就不重装) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 再拷代码 COPY . . # 非 root 用户运行,更安全 RUN useradd -m appuser && chown -R appuser /app USER appuser EXPOSE 8000 # 生产用多 worker 的 uvicorn/gunicorn,别用 --reload CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

配套.dockerignore(别把垃圾和密钥打进镜像):

__pycache__/ *.pyc .env # ⚠️ 关键:密钥文件绝不进镜像 .git/ *.log chroma_db/

构建与运行:

dockerbuild-tmy-agent:latest.dockerrun-d-p8000:8000 --env-file .env my-agent:latest

🎯两个高频坑

  1. .env一定要写进.dockerignore——否则密钥被打进镜像,镜像一分享就泄露;
  2. 生产别用--reload(那是开发用的),用--workers N起多进程扛并发。

③ 第二步:密钥与配置的安全注入

密钥怎么进容器?通过环境变量注入,绝不写进镜像(呼应第 1 篇的安全规范)。按环境从简到严:

# 方式1:--env-file(小项目/单机)dockerrun --env-file .env my-agent# 方式2:-e 单个注入(CI/CD 里从密文变量取)dockerrun-eAGENT_KEY=$AGENT_KEYmy-agent

生产集群(Kubernetes)用Secret

# k8s-secret.yamlapiVersion:v1kind:Secretmetadata:name:agent-secretstype:OpaquestringData:AGENT_KEY:"sk-xxx"---# Deployment 里引用spec:containers:-name:agentimage:my-agent:latestenvFrom:-secretRef:name:agent-secrets

🚨安全红线:密钥的三不原则——不进代码、不进镜像、不进 Git。永远通过运行时环境注入。云厂商的 Secrets Manager / Parameter Store 是更专业的选择。


④ 第三步:限流(保护上游 API 额度)

上游大模型 API 有 RPM/TPM 限额(第 1 篇讲过)。你的服务如果不限流,一波流量直接把额度打爆,全员 429。所以要在自己这一层先限流

用 Redis 实现一个简单的滑动窗口限流(复用第 6 篇的 Redis):

importos,time,redisfromfastapiimportHTTPException# 主机名从环境变量读:本地 localhost,容器里由 compose 注入 REDIS_HOST=redisr=redis.Redis(host=os.getenv("REDIS_HOST","localhost"),port=6379,decode_responses=True)defrate_limit(user_id,max_requests=20,window=60):"""每个用户每 window 秒最多 max_requests 次请求"""key=f"ratelimit:{user_id}"now=time.time()pipe=r.pipeline()pipe.zremrangebyscore(key,0,now-window)# 清掉窗口外的记录pipe.zadd(key,{str(now):now})# 记录本次pipe.zcard(key)# 数窗口内请求数pipe.expire(key,window)count=pipe.execute()[2]ifcount>max_requests:raiseHTTPException(status_code=429,detail="请求过于频繁,请稍后再试")

在接口里调用:

@app.post("/chat")asyncdefchat(body:dict):rate_limit(body["user_id"])# 先过限流# ... 正常处理 ...

🎯两层限流最稳:①用户级(防单个用户刷爆,如上);②全局级(所有请求加总不超过上游 RPM)。再配合第 1 篇的指数退避重试兜底偶发 429,就很稳了。


⑤ 第四步:熔断(防止雪崩)

雪崩是分布式系统头号杀手:上游模型 API 挂了或变慢,你的请求全堆在那儿等超时,连接池被占满,最后你的服务也被拖垮——一个下游故障,引发全线崩溃。

熔断器(Circuit Breaker)就是「保险丝」:当上游连续失败达到阈值,熔断器「跳闸」,之后的请求直接快速失败(不再傻等上游),过一段时间再«半开»试探恢复。

importtimeclassCircuitBreaker:def__init__(self,fail_threshold=5,recovery_time=30):self.fail_threshold=fail_threshold# 连续失败多少次跳闸self.recovery_time=recovery_time# 跳闸后多久尝试恢复self.failures=0self.state="closed"# closed(正常)/open(熔断)/half-open(试探)self.opened_at=0defcall(self,func,*args,**kwargs):# 熔断中:看是否到了试探时间ifself.state=="open":iftime.time()-self.opened_at>self.recovery_time:self.state="half-open"# 进入半开,放一个请求试探else:raiseException("服务熔断中,请稍后再试")# 快速失败,不等上游try:result=func(*args,**kwargs)self.failures=0# 成功,重置self.state="closed"returnresultexceptExceptionase:self.failures+=1ifself.failures>=self.fail_threshold:self.state="open"# 连续失败,跳闸self.opened_at=time.time()raisee breaker=CircuitBreaker()# 用法:把上游调用包进熔断器defcall_llm_protected(messages):returnbreaker.call(call_llm,messages)

🎯熔断的价值:上游挂了,熔断器让你的服务快速失败并返回友好提示,而不是让成千上万请求堆积等超时、最终把自己拖死。保护自己,也给上游喘息恢复的机会。

💡 生产可用成熟库如pybreaker,或服务网格(Istio)在基础设施层做熔断,不必都手写。


⑥ 第五步:超时与优雅降级

超时是底线:任何外部调用都必须设超时,否则一个卡住的请求能拖垮整个服务。

importhttpxasyncdefcall_llm(messages):asyncwithhttpx.AsyncClient(timeout=30)asclient:# 必设超时!resp=awaitclient.post(url,json=...,headers=...)returnresp.json()[...]

优雅降级:出问题时给用户一个「兜底回复」,而不是抛个 500 错误页。

asyncdefchat_with_fallback(messages):try:returnawaitcall_llm_protected(messages)# 熔断+超时保护的调用exceptExceptionase:log_event("llm_fallback",error=str(e))# 记日志(第10篇)# 降级:返回友好兜底,而非崩溃return"抱歉,服务暂时繁忙,请稍后再试。如问题紧急,请联系人工客服。"

🎯降级的心法:用户要的是「有个说法」,不是「白屏 500」。哪怕给一句「稍后再试」,体验也远好过报错。关键路径都该有兜底。


⑦ 第六步:健康检查与优雅关闭

健康检查:让 K8s / 负载均衡器知道你的服务「还活着」,不健康就别往这台打流量。

@app.get("/health")defhealth():# 可加检查:Redis 通不通、上游可达否try:r.ping()return{"status":"ok"}exceptException:raiseHTTPException(status_code=503,detail="unhealthy")

优雅关闭:重启/扩缩容时,别粗暴掐断正在处理的请求,等它们处理完再退出。

fromcontextlibimportasynccontextmanager@asynccontextmanagerasyncdeflifespan(app):yield# 启动# 关闭时的清理:等待进行中的请求、关连接池log_event("shutdown",detail="graceful shutdown")app=FastAPI(lifespan=lifespan)

K8s 里配上探针:

livenessProbe:# 死了就重启httpGet:{path:/health,port:8000}initialDelaySeconds:10periodSeconds:15readinessProbe:# 没就绪就不给流量httpGet:{path:/health,port:8000}initialDelaySeconds:5periodSeconds:10

⑧ 完整部署配置

docker-compose把 Agent + Redis 一键拉起(中小项目够用):

# docker-compose.ymlversion:"3.8"services:agent:build:.ports:-"8000:8000"env_file:-.env# 密钥从这注入,不进镜像environment:-REDIS_HOST=redis# ⚠️ 容器内连 Redis 用服务名,不是 localhost!depends_on:-redisrestart:always# 挂了自动重启deploy:resources:limits:memory:1G# 限制内存,防单容器吃爆宿主机redis:image:redis:7-alpinerestart:alwaysvolumes:-redis_data:/data# 持久化会话数据volumes:redis_data:

一键启动:

docker-composeup-d# 后台启动全部服务docker-composelogs-fagent# 看日志docker-composedown# 停止

前面加个Nginx反代(记得第 4 篇的proxy_buffering off;让流式生效):

location / { proxy_pass http://localhost:8000; proxy_buffering off; # 流式输出必须关缓冲(第4篇) proxy_read_timeout 120s; # 给长回复留足时间 }

8.1 验证部署成功(别凭感觉,动手确认)

启动后,逐项验证各道防线真的生效,而不是"看起来跑起来了":

# 1. 容器都在跑?(agent 和 redis 都应是 Up)docker-composeps# 2. 健康检查通不通?应返回 {"status":"ok"}curlhttp://localhost:8000/health# 3. 正常对话通不通?curl-XPOST http://localhost:8000/chat\-H"Content-Type: application/json"\-d'{"user_id":"u1","text":"你好"}'# 4. 限流真的生效?连发 30 次,应该在第 20 次后开始返回 429foriin$(seq130);docurl-s-o/dev/null-w"%{http_code} "\-XPOST http://localhost:8000/chat\-H"Content-Type: application/json"\-d'{"user_id":"u1","text":"hi"}'doneecho

第 4 步的预期输出(前 20 个 200,之后被限流挡下变 429):

200 200 200 ... 200 429 429 429 429 429 429 429 429 429 429

🔍 看到200429那一刻,就是你的限流防线肉眼可见地生效了。同理可测熔断(把上游地址临时改错,看是否快速失败而非卡死)。防御性配置一定要主动验证,别等真实事故来"帮你测"。


⑨ 常见坑与建议

说明解决
.env打进镜像密钥泄露写进.dockerignore
--reload上生产性能差、不稳--workers N
没设超时一个卡请求拖垮全服务所有外部调用设 timeout
不限流打爆上游额度、被薅羊毛用户级 + 全局级限流
无熔断上游故障引发雪崩熔断器快速失败
无健康检查死了还在接流量/health+ K8s 探针
流式在线上失效Nginx 缓冲proxy_buffering off
容器里连不上 Redis代码写死localhost,容器内 localhost 是容器自己用 compose 服务名(REDIS_HOST=redis

🔍上线前必做的一件事:做一次压测(用locustwrk等模拟并发),看限流、熔断、降级是否真的生效。别等真实流量来了才发现兜底没兜住。


⑩ 总结

这一篇把 Agent 从「本地脚本」升级成了「生产级服务」:

能力手段防住什么
容器化Docker + compose环境不一致
密钥安全环境变量 / Secret凭证泄露
限流Redis 滑动窗口打爆额度、薅羊毛
熔断Circuit Breaker雪崩
超时降级timeout + fallback无限等待、白屏
健康检查/health + 探针死服务接流量

核心认知:生产环境的工作量,一半在功能,一半在「防止功能出问题」。限流、熔断、降级这些「防御性工程」,平时看不出价值,出事时就是它们在保你不被叫醒。

到这里,你的 Agent 已经能安全、稳定地对外服务了。但还有最后一道关——安全。下一篇(本专栏收官)讲 AI 应用特有的安全威胁:幻觉、越权、提示词注入,以及怎么防。


🔜 下一篇预告(收官):《常见幻觉 / 越权 / 注入攻击与防御》——AI 应用特有的安全威胁全解。
👍 如果本文帮到你,点赞 / 收藏 / 关注,追更不迷路。