行业资讯
【从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 分钟。目录上线前的检查清单第一步Docker 容器化第二步密钥与配置的安全注入第三步限流保护上游 API 额度第四步熔断防止雪崩第五步超时与优雅降级第六步健康检查与优雅关闭完整部署配置常见坑与建议总结① 上线前的检查清单先给你一张「上线体检表」逐项对照缺哪补哪项目为什么本文章节容器化环境一致、可复制、易扩缩容②密钥安全注入绝不硬编码进镜像③限流保护上游额度、防打爆④熔断上游挂了别跟着雪崩⑤超时 降级别让用户无限等⑥健康检查让编排系统知道死活⑦日志 监控出事看得见上一篇第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两个高频坑.env一定要写进.dockerignore——否则密钥被打进镜像镜像一分享就泄露生产别用--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 篇的 Redisimportos,time,redisfromfastapiimportHTTPException# 主机名从环境变量读本地 localhost容器里由 compose 注入 REDIS_HOSTredisrredis.Redis(hostos.getenv(REDIS_HOST,localhost),port6379,decode_responsesTrue)defrate_limit(user_id,max_requests20,window60):每个用户每 window 秒最多 max_requests 次请求keyfratelimit:{user_id}nowtime.time()piper.pipeline()pipe.zremrangebyscore(key,0,now-window)# 清掉窗口外的记录pipe.zadd(key,{str(now):now})# 记录本次pipe.zcard(key)# 数窗口内请求数pipe.expire(key,window)countpipe.execute()[2]ifcountmax_requests:raiseHTTPException(status_code429,detail请求过于频繁请稍后再试)在接口里调用app.post(/chat)asyncdefchat(body:dict):rate_limit(body[user_id])# 先过限流# ... 正常处理 ...两层限流最稳①用户级防单个用户刷爆如上②全局级所有请求加总不超过上游 RPM。再配合第 1 篇的指数退避重试兜底偶发 429就很稳了。⑤ 第四步熔断防止雪崩雪崩是分布式系统头号杀手上游模型 API 挂了或变慢你的请求全堆在那儿等超时连接池被占满最后你的服务也被拖垮——一个下游故障引发全线崩溃。熔断器Circuit Breaker就是「保险丝」当上游连续失败达到阈值熔断器「跳闸」之后的请求直接快速失败不再傻等上游过一段时间再«半开»试探恢复。importtimeclassCircuitBreaker:def__init__(self,fail_threshold5,recovery_time30):self.fail_thresholdfail_threshold# 连续失败多少次跳闸self.recovery_timerecovery_time# 跳闸后多久尝试恢复self.failures0self.stateclosed# closed(正常)/open(熔断)/half-open(试探)self.opened_at0defcall(self,func,*args,**kwargs):# 熔断中看是否到了试探时间ifself.stateopen:iftime.time()-self.opened_atself.recovery_time:self.statehalf-open# 进入半开放一个请求试探else:raiseException(服务熔断中请稍后再试)# 快速失败不等上游try:resultfunc(*args,**kwargs)self.failures0# 成功重置self.stateclosedreturnresultexceptExceptionase:self.failures1ifself.failuresself.fail_threshold:self.stateopen# 连续失败跳闸self.opened_attime.time()raisee breakerCircuitBreaker()# 用法把上游调用包进熔断器defcall_llm_protected(messages):returnbreaker.call(call_llm,messages)熔断的价值上游挂了熔断器让你的服务快速失败并返回友好提示而不是让成千上万请求堆积等超时、最终把自己拖死。保护自己也给上游喘息恢复的机会。 生产可用成熟库如pybreaker或服务网格Istio在基础设施层做熔断不必都手写。⑥ 第五步超时与优雅降级超时是底线任何外部调用都必须设超时否则一个卡住的请求能拖垮整个服务。importhttpxasyncdefcall_llm(messages):asyncwithhttpx.AsyncClient(timeout30)asclient:# 必设超时respawaitclient.post(url,json...,headers...)returnresp.json()[...]优雅降级出问题时给用户一个「兜底回复」而不是抛个 500 错误页。asyncdefchat_with_fallback(messages):try:returnawaitcall_llm_protected(messages)# 熔断超时保护的调用exceptExceptionase:log_event(llm_fallback,errorstr(e))# 记日志第10篇# 降级返回友好兜底而非崩溃return抱歉服务暂时繁忙请稍后再试。如问题紧急请联系人工客服。降级的心法用户要的是「有个说法」不是「白屏 500」。哪怕给一句「稍后再试」体验也远好过报错。关键路径都该有兜底。⑦ 第六步健康检查与优雅关闭健康检查让 K8s / 负载均衡器知道你的服务「还活着」不健康就别往这台打流量。app.get(/health)defhealth():# 可加检查Redis 通不通、上游可达否try:r.ping()return{status:ok}exceptException:raiseHTTPException(status_code503,detailunhealthy)优雅关闭重启/扩缩容时别粗暴掐断正在处理的请求等它们处理完再退出。fromcontextlibimportasynccontextmanagerasynccontextmanagerasyncdeflifespan(app):yield# 启动# 关闭时的清理等待进行中的请求、关连接池log_event(shutdown,detailgraceful shutdown)appFastAPI(lifespanlifespan)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.8services:agent:build:.ports:-8000:8000env_file:-.env# 密钥从这注入不进镜像environment:-REDIS_HOSTredis# ⚠️ 容器内连 Redis 用服务名不是 localhostdepends_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 都应是 Updocker-composeps# 2. 健康检查通不通应返回 {status:ok}curlhttp://localhost:8000/health# 3. 正常对话通不通curl-XPOST http://localhost:8000/chat\-HContent-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\-HContent-Type: application/json\-d{user_id:u1,text:hi}doneecho第 4 步的预期输出前 20 个 200之后被限流挡下变 429200 200 200 ... 200 429 429 429 429 429 429 429 429 429 429 看到200变429那一刻就是你的限流防线肉眼可见地生效了。同理可测熔断把上游地址临时改错看是否快速失败而非卡死。防御性配置一定要主动验证别等真实事故来帮你测。⑨ 常见坑与建议坑说明解决.env打进镜像密钥泄露写进.dockerignore用--reload上生产性能差、不稳用--workers N没设超时一个卡请求拖垮全服务所有外部调用设 timeout不限流打爆上游额度、被薅羊毛用户级 全局级限流无熔断上游故障引发雪崩熔断器快速失败无健康检查死了还在接流量/health K8s 探针流式在线上失效Nginx 缓冲proxy_buffering off容器里连不上 Redis代码写死localhost容器内 localhost 是容器自己用 compose 服务名REDIS_HOSTredis上线前必做的一件事做一次压测用locust、wrk等模拟并发看限流、熔断、降级是否真的生效。别等真实流量来了才发现兜底没兜住。⑩ 总结这一篇把 Agent 从「本地脚本」升级成了「生产级服务」能力手段防住什么容器化Docker compose环境不一致密钥安全环境变量 / Secret凭证泄露限流Redis 滑动窗口打爆额度、薅羊毛熔断Circuit Breaker雪崩超时降级timeout fallback无限等待、白屏健康检查/health 探针死服务接流量核心认知生产环境的工作量一半在功能一半在「防止功能出问题」。限流、熔断、降级这些「防御性工程」平时看不出价值出事时就是它们在保你不被叫醒。到这里你的 Agent 已经能安全、稳定地对外服务了。但还有最后一道关——安全。下一篇本专栏收官讲 AI 应用特有的安全威胁幻觉、越权、提示词注入以及怎么防。 下一篇预告收官《常见幻觉 / 越权 / 注入攻击与防御》——AI 应用特有的安全威胁全解。 如果本文帮到你点赞 / 收藏 / 关注追更不迷路。
郑州网站建设
网页设计
企业官网