ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

隔离内网部署AI Agent的完整实践:架构、本地化与并发优化

隔离内网部署AI Agent的完整实践:架构、本地化与并发优化 我们部门上个月接到一个任务在完全隔离的内网环境里落地一套能真正干活的 AI Agent。所谓隔离内网就是这台机器所在的网络没有任何公网出口模型权重、Python 依赖、容器镜像全都要靠人工摆渡进去。这不是一个简单的把大模型跑起来的问题因为 Agent 要跟内网里的业务系统、数据库、审批流打交道网络边界、依赖分发、并发能力这些工程问题全都绕不开。这篇文章把我从踩坑到稳定运行的全过程整理出来重点讲架构设计、模型本地化、Agent 编排、离线依赖分发以及最容易被忽视的并发与稳定性问题。如果你也在搞私有化部署、企业智能助手或者内网知识库问答这篇应该能帮你少走很多弯路。1. 为什么要把 AI Agent 放进隔离内网1.1 隔离内网不是一个统一概念很多人一提隔离内网第一反应是没网的电脑。实际上企业里的隔离环境分好几种不同的隔离强度直接决定了 Agent 落地的难度和工作量。第一种是物理隔离也叫网闸隔离。办公网和业务网之间没有直连通道数据交换要么通过光盘、移动硬盘的人工摆渡要么走单向导入设备。这种环境下Agent 想实时调业务系统接口基本不现实通常只能做离线任务型 Agent比如每天晚上批量生成报表、批量处理文本再把结果导出去。第二种是逻辑隔离办公网和业务网通过防火墙、ACL 做访问控制两边可以指定端口互通。这种是目前做 Agent 最多的场景。Agent 跑在业务网一侧可以通过内网 API 网关调用各个系统的接口前提是做好服务间鉴权和流量管控。我们这次遇到的半隔离环境就属于这种数据不能出网但内部系统之间可以互相访问。第三种是开发网和测试网分离。很多企业在开发环境里联网生产环境离线。这种情况最有意思你可以在开发环境把 Agent 全部调通最后打包部署到生产环境但生产环境和开发环境的包版本、模型版本、镜像版本必须严格一致否则上线就翻车。先搞清楚你面对的是哪种隔离再做技术选型。我见过有团队在物理隔离环境下硬要做实时在线 Agent光是把模型权重搬运进去就折腾了一个月最后业务价值依然很低。隔离强度决定了交互形态这个判断要做在前面。1.2 内网 Agent 的需求从哪来隔离内网不是没有 AI 需求恰恰相反这类环境的痛点非常多。常见的是内部知识库问答规章制度、操作手册、历史工单散落在各个系统里员工检索效率极低大模型可以用来做问答和摘要。另外是业务系统的人工辅助比如运维人员要查故障处理手册客服要查客诉处理SOP财务要问报销流程这些都可以通过 Agent 对话实现。还有一个需求容易被忽略就是Agent 代替人跑流程。内网系统普遍老化很多操作没有对外开放 API只有页面。这时候 Agent 需要承担 RPA 的角色去调用已有的脚本、命令行工具、数据库操作接口把过去需要人手动完成的重复劳动自动化。我们这次做的是售前和售后场景的客户支持助手会内网检索产品资料、调用工单系统接口、生成标准回复草稿最后还要人来确认。搞明白需求之后你会发现 Agent 的核心不是会聊天而是能稳定地完成工作流。这就是为什么后面架构设计要把编排层单独拎出来而不是简单套一个对话框架。1.3 先划定 Agent 的权限边界在隔离内网里做 Agent权限边界比算法还重要。因为一旦 Agent 能调用内网 API它的行为直接关系到数据安全和业务稳定性。我建议在立项阶段就和业务方拉一份清单Agent 能读哪些库、能调哪些接口、哪些操作必须有人审核。比如查询客户订单可以放权修改订单状态发送对外邮件必须走人工审批节点。这个边界后面会被直接翻译成 LangGraph 状态图里的节点。权限划得越清楚编码阶段越省事。很多项目死在半路就是因为边界不清晰Agent 的 tool 列表越加越多最后变成了一个不可控的全能机器人。2. 整体架构四层拆分各司其职2.1 四层架构到底怎么分我最终采用的是四层结构模型层、编排层、应用层、接入层。每层独立部署接口全部走 HTTP/SSE这样每一层都可以单独扩容、单独降级。模型层跑的是本地大语言模型推理服务对外暴露 OpenAI 兼容的/v1/chat/completions接口。编排层是 Agent 的大脑用 LangGraph 实现意图理解、工具调用、人工审核、结果生成这些节点并把它们串成状态图。应用层是业务逻辑的集合对外提供对话接口、任务查询接口、审批接口对内负责调用内网业务系统。接入层是最前面的统一入口负责身份认证、限流、路由前端页面和第三方系统都从这里进来。这个分层最大的好处是模型层和业务系统彻底解耦。模型卡住了编排层可以降级返回固定回复业务系统不稳定模型层不受影响。隔离内网环境里所有组件都是单体部署解耦之后运维压力会小很多。2.2 为什么是 FastAPI LangChain LangGraph而不是别的先交代清楚做技术选型时我们对比过三套方案。第一套是纯 FastAPI 手写 Prompt 自研工具调用逻辑。优点是代码完全可控缺点是每加一个 Agent 场景都要重新写一遍编排流程而且模型该调用哪个工具这个判断逻辑非常容易写烂。我们不推荐纯手写除非你的 Agent 只有一个固定流程。第二套是 LangChain 自带的 AgentExecutor功能开箱即用但它是为简单任务设计的对人工审批动态分支状态回溯这些真实业务需求支持有限。如果你做的 Agent 只是单轮工具调用用 LangChain 没问题但像我们这种多步骤流程后面会发现状态维护和回调处理会让人想骂人。第三套就是最终选的 LangGraph。它的核心抽象是 StateGraph把 Agent 流程显式定义成一张状态图每个节点是一个 Python 函数节点之间通过状态传递数据。这个思路和我们审批流程非常契合先规划 → 调用工具 → 人工确认 → 执行。LangGraph 天然支持条件分支、循环、人工介入点而且是可以异步执行的。FastAPI 的选择没什么悬念。整个编排层天然是 IO 密集型大量时间花在等模型输出、等工具响应、等人工确认上FastAPI 的 async 支持能把这些等待时间压缩到一个事件循环里避免开大量线程池。同时 FastAPI 原生支持 SSE 流式响应这个对 Agent 应用来说几乎就是刚需。2.3 一个冷门但值得一提的技术栈选项如果你所在的团队是 Java 为主那 Spring AI 确实是一个合理选项它把大模型接入和工具调用抽象成了 Spring 风格和现有 Java 微服务体系集成非常顺畅当时我们对比过。Spring AI 的生态没有 LangChain 那么全但如果你要对接的不是复杂的多 Agent 编排而是几个固定的智能体场景Java 技术栈的统一性反而更重要。另外对于基于 Rust 语言做 AI Agent这个方向我的观点是不要用 Rust 去重写整个 Agent 框架这让开发效率大幅降低。但可以用 Rust 写一个工具执行 Sidecar专门执行 Agent 调用到的重操作比如处理大文件、跑数据处理脚本、执行外部命令。Python 进程里跑 subprocess 容易出内存问题后面踩坑部分我会细讲。把重操作剥离到 Rust Sidecar 里通过 HTTP 暴露接口Python 编排层只负责调度分工就清爽了。2.4 技术选型清单这里给一份我们最终使用的技术栈清单方便你直接照抄结构层组件选型理由模型层vLLM Qwen2.5 系列高并发吞吐、OpenAI 兼容接口编排层LangGraph LangChain显式状态图、支持人工审核节点应用层FastAPI异步支持好、SSE 流式原生工具执行Rust Sidecar隔离重命令、避免主进程崩溃记忆存储Redis pgvector短期会话记忆 长期向量检索依赖管理devpi 私有 PyPI Harbor离线安装包与镜像的一体化管理接入层Nginx 自研鉴权中间件统一入口、限流、认证这套组合的优势是每个组件都在自己擅长的地方工作边界清晰替换成本低。比如将来想换 SGLang 做推理引擎模型层单独换就行编排层的接口不用动。3. 模型本地化权重、推理引擎与显存规划3.1 模型选型从对话模型到推理模型隔离内网环境里选模型不能只看榜单分数要综合看显存、吞吐、领域适配三个维度。我们测过 Qwen2.5-7B-Instruct、Qwen2.5-14B-Instruct、ChatGLM4-9B。结论比较直接7B 模型在不复杂的业务问答上表现已经可用但如果你的 Agent 要做多步工具调用、需要遵循复杂的 JSON 输出格式14B 级别的模型成功率会明显更高。32B 级别的模型在单卡上跑不动必须多卡并行会直接拉高硬件成本。还要考虑推理模型和对话模型的区别。推理模型比如带思维链的产品适合做复杂推理但 Token 消耗大响应慢在隔离内网这种资源有限的环境里未必划算。我们的做法是复杂任务用推理模型日常指令和简单问答用对话模型路由规则放在编排层自己控制。这个模型路由设计让我在性能和成本之间找到了一个平衡点值得你参考。3.2 离线权重传输与校验这是隔离内网工程里最容易翻车的一环。几十 GB 的模型权重从能上网的开发机搬到完全离线的生产环境靠 U 盘一个个拷肯定不行。我的做法是先在被允许联网的下载机上通过模型平台把权重完整下载下来用sha256sum生成校验文件。然后对大文件做分卷压缩每一卷控制在 4GB 以内方便在摆渡设备上传输。全部传完后先解压恢复原文件再核对一次 sha256确认无误才注册到模型目录。这里有个细节模型社区下载的目录结构通常包含config.json、tokenizer文件、*.safetensors权重分片。vLLM 和 Ollama 对目录结构要求不同千万不要只把权重文件带过去config 和 tokenizer 缺一个都可能报莫名其妙的错误。最好把整个模型目录原样搬过去。分卷压缩命令可以这样处理# 在下载机上分卷打包每卷 4GB tar -cvf - /data/Qwen2.5-14B-Instruct | split -b 4096m - qwen14b_part_ # 生成校验文件 sha256sum qwen14b_part_* qwen14b.sha256到了内网目标机器上先校验再合并解压sha256sum -c qwen14b.sha256 cat qwen14b_part_* | tar -xvf -这一套操作看起来土但它是目前物理隔离环境里最可靠的方式。校验这一步千万不能省我自己就遇到过解压之后某个 shard 损坏vLLM 加载到一半直接崩溃排查了半天才发现是文件不完整。3.3 推理引擎选型vLLM 与 Ollama 的取舍隔离内网里推理引擎主要有三个选择vLLM、Ollama、SGLang。我给它们画个像。Ollama 的优势是傻瓜化一条命令就能把模型跑起来自带 OpenAI 兼容接口适合个人开发和资源受限的小规模部署。但它的高并发能力弱一些如果 Agent 同时进来几个请求响应延迟会明显拉高。vLLM 是为生产高并发设计的核心是 PagedAttention 和 Continuous Batching。它允许你在一个服务里用张量并行切多张卡吞吐量比 Ollama 高一个量级。缺点是配置参数多上手门槛高一点。SGLang 是这两年效率派的代表RadixAttention 对多轮对话的 Prefix Caching 做得很好Agent 场景里多轮调用同一个 system prompt缓存命中率上来了能省不少显存和延迟。不过它在某些国产 GPU 上的兼容性不如 vLLM 稳定。我最终选了 vLLM理由是基于对稳定性的考虑社区大、问题容易搜、OpenAI 兼容接口做得成熟在国产化硬件上踩过的坑相对少。3.4 显存规划怎么算才能不炸卡模型能不能跑显存是死条件。我提供一个简化估算法。首先是模型权重本身。FP16 精度下参数量乘以 2 字节就是权重占用的显存。7B 模型权重约 14GB14B 模型权重约 28GB32B 模型权重约 64GB这些都是理论下限。然后是 KV Cache。它和并发数、上下文长度直接相关。粗略估算公式是KV Cache 占用 层数 × 头数 × 每头维度 × 层数系数 × 序列长度 × 并发数这个公式对非算法工程师不太友好我给你一个工程化的判断标准在gpu_memory_utilization0.9的前提下24GB 的显存建议跑 7B 模型32GB 显存建议跑 14B 模型加限制并发80GB 的 A100/H100 可以舒服地跑 14B 甚至 7B 模型的超高并发。我自己踩过的坑是一开始在 24GB 卡上硬跑 14B 模型KV Cache 空间不够vLLM 疯狂做 preemption每一轮推理都要把旧的 KV 清掉重新算响应时间从 2 秒暴涨到 15 秒。后来换成双卡张量并行跑 14B或者干脆降到 7B体验才恢复正常。做容量规划时可以用这个计算表快速判断模型规模精度权重显存建议最小单卡显存备注7BFP16~14GB24GBKV Cache 空间仍偏紧7BINT8~7GB16GB精度略有损失14BFP16~28GB40GB 或双卡单卡 32GB 只能低压并发32BFP16~64GB双卡 80GB一般用于更强推理场景3.5 vLLM 启动配置示例这里给出我们实际用的 vLLM 启动命令关键参数我都标出来了python -m vllm.entrypoints.openai.api_server \ --model /data/models/Qwen2.5-14B-Instruct \ --tensor-parallel-size 2 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --served-model-name agent-llm \ --host 0.0.0.0 \ --port 8001解释几个参数tensor-parallel-size是把一张大模型切到多张卡并行计算14B 用双卡比较稳gpu-memory-utilization控制显存使用上限我建议不要超过 0.92留一点余量给 CUDA context 和临时张量max-model-len特别关键它直接决定 KV Cache 大小如果你的业务对话不会超过 4000 Token就没必要设 32768设得越大能容纳的并发数越少。served-model-name是给编排层调用时用的模型名可以起一个贴合业务的名字不需要和目录名一致。启动之后用一条简单的 curl 命令验证curl http://127.0.0.1:8001/v1/chat/completions \ -H Content-Type: application/json \ -d {model:agent-llm,messages:[{role:user,content:你好}],max_tokens:128}务必在这个阶段确认流式接口stream: true也正常因为后面 Agent 的响应全都依赖 SSE。4. Agent 编排层让模型真正下地干活4.1 把业务拆成一张状态图很多做 AI 应用的人有一个误区觉得 Agent 就是模型 工具列表然后让模型自己决定怎么调。真实跑下来你会发现纯粹让模型自由发挥流程是不可控的。比如一个客户投诉处理流程你希望 Agent 先去查订单再去查物流最后生成回复草稿这个顺序是有强约束的。自由发挥很容易漏步骤。我们用 LangGraph 把流程定义成显式的状态图核心节点有五个意图识别、任务规划、工具调用、结果审核、最终回复。意图识别节点先把用户问题分类是普通问答直接走知识库还是需要调工单系统走工具调用任务规划节点让模型产出行动计划比如先查客户订单、再查物流状态工具调用节点真正去调内部系统接口结果审核节点判断工具返回的结果是否合理不合理会触发循环重新规划最终回复节点把结果整理成自然语言返回给用户。LangGraph 里的节点就是一个普通 Python 函数好处是可以单测可以加日志可以控制重试次数。这在排障时太重要了你知道是哪一步出了问题而不是只看到一个Agent 回答得不对的黑盒。4.2 工具注册与内网 API 对接Agent 能做的每一个操作在编排层里都注册成一个 Tool。每个 Tool 需要有名字、描述、参数定义JSON Schema。这里的描述写得越清楚模型就越能准确选择工具。同样一个查询接口get_customer_order 和 getOrderStatus 这两种名字模型的理解准确率差异非常大建议工具名用动词开头的小写风格描述里包含业务含义和典型问法。内网系统对接时服务间鉴权我用的是服务账号加 Token 的方式。每个 Agent 实例分配一个独立的服务账号调用内部 API 时带上请求头和请求 ID方便追踪。另外所有工具调用必须有超时控制。一个工具长时间不返回会直接卡住整个 Agent 流程。我的做法是普通查询超时 10 秒重操作超时 60 秒超时后工具节点返回一个特殊错误让模型知道这个工具暂时不可用换一个方案。工具调用的代码结构大概是这样tool def get_customer_order(order_id: str) - dict: 根据订单号查询客户订单信息返回订单状态、金额、商品列表 resp http_client.post( http://internal-gateway/order/query, json{order_id: order_id}, headers{X-Service-Token: SERVICE_TOKEN}, timeout10, ) resp.raise_for_status() return resp.json()这个函数本身不复杂复杂的是它要接入 LangGraph 的 ToolNode。让模型按 JSON Schema 生成调用参数然后由 ToolNode 实际执行再把结果填回状态里。这个过程我们一开始经常遇到模型生成的参数格式不对后来用 Pydantic 做参数校验加上自然语言错误反馈调用成功率才上来了。4.3 记忆与上下文的工程实现Agent 的对话如果只有单轮很多场景根本没法做。用户说刚才那个订单查一下物流你必须知道刚才指的是哪个订单。所以在 Agent 里记忆不是一个加分项而是必需品。我的实现分三层。短期记忆放在 Redis用会话 ID 做 key保存最近 10 轮对话摘要和关键实体订单号、客户名、工单号。每次进入编排层时先加载短期记忆拼到系统提示词里。长期记忆用的是 pgvector 向量库。每轮对话结束之后把用户问题 Agent 回答 关键动作异步存成一条向量记录。下次用户发新问题时先从向量库检索相似的历史对话把相关内容放进上下文。这个方案特别适合售后场景客户隔几天又回来问同一个问题Agent 能记得上下文体验会好很多。隔离内网里没有云上的向量库可用pgvector 是成本最低的方案它复用已有的 PostgreSQL不需要额外部署一套 Milvus。数据量在百万条以下性能完全够用。4.4 人机协同关键动作必须有人审批这可能是整个 Agent 工程里最重要的一环。隔离内网系统一般都有严格的操作审计要求Agent 直接执行邮件发送工单关闭数据修改这类操作风险太高。我们的设计是在 LangGraph 里插入一个human_approval节点。流程是这样的Agent 完成任务规划后如果发现需要执行敏感操作先把操作详情操作对象、执行内容、影响范围写入审批队列并通过前端推送给审批人。审批人在页面上一键通过或者驳回。审批通过后 Agent 继续执行驳回则回到规划节点让模型重新生成替代方案。这个节点在 LangGraph 里实现为 interrupt就是暂停运行、等待外部信号。好处是 Agent 的执行状态完全持久化在服务端人审3分钟之后回来Agent 还能从断点继续而不是重新跑一遍。需要注意审批节点不能让模型决定要不要审批必须由代码硬编码。哪些 Tool 是敏感操作哪些可以直接放行在工具注册的时候就打上标记。这样即使模型规划得再离谱权限边界也不会被突破。5. 依赖分发与镜像同步内网工程的命门5.1 Python 依赖的离线搬运很多人觉得模型权重搬进去之后剩下的就是把代码传过去跑起来就行了。实际上 Python 依赖才是第一个让人崩溃的环节。LangChain、LangGraph、FastAPI 这一串依赖加起来几十个包直接pip install在隔离内网环境里根本不可行必须提前在联网机器上准备好离线依赖包。我的做法是在联网的开发机上用pip download把全部依赖拉下来构建一个本地 wheelhouse 目录pip download -r requirements.txt -d ./wheels \ --platform manylinux2014_x86_64 \ --python-version 311 \ --only-binary:all:这里有个大坑如果你在开发机上用的是 macOS 或者 Windows直接 download 下来的包可能带的是本平台的二进制拿到 Linux 服务器上装不了。所以必须指定--platform manylinux2014_x86_64和--python-version并且加上--only-binary:all:强制只下载二进制包。如果某些包没有 manylinux 版本那就只能在同架构的机器上先pip wheel打包。内网安装时把这些 wheel 包传到目标机器用本地源安装pip install --no-index --find-links ./wheels -r requirements.txt有条件的团队建议直接搭一个 devpi 或者 Nexus 私有 PyPI 服务把 wheel 包上传进去内网所有机器统一配置pip.conf指向私有源后续增量更新包就方便多了。5.2 容器镜像的内网迁移如果应用是用 Docker 部署的镜像迁移又是一个体力活。vLLM、Python 基础镜像、Redis 这些镜像加起来十几个 GB靠 Docker Hub 直接拉是拉不了的。我们的迁移方式是在联网机器上把需要的镜像全部 pull 下来打 tag 后推送到内网的 Harbor 镜像仓库。比如docker pull vllm/vllm-openai:latest docker tag vllm/vllm-openai:latest harbor.internal/base/vllm-openai:latest docker push harbor.internal/base/vllm-openai:latest到内网部署机上直接改成从 Harbor 拉取镜像。这里我建议全部用固定的版本 tag不要用latest否则生产环境每次部署镜像都变了出问题根本没法回溯。还有一个隐蔽的问题是 CUDA 驱动版本。vLLM 的官方镜像是基于特定 CUDA 版本编译的如果宿主机的 NVIDIA 驱动太老容器起来之后模型加载会报错。建议先在内网部署机上执行nvidia-smi确认驱动版本再选择对应的官方镜像别到时候镜像搬进去了才发现跑不了。5.3 模型文件的摆渡与校验模型文件的传输前面已经提到了分卷压缩和 sha256 校验这里再强调一次校验文件本身也要跟着模型一起传到内网并且要在一个独立的只读位置保存原始校验值。因为如果校验文件也被传输损坏到时候就会怎么校验都不通过排查非常痛苦。另外如果模型文件特别大比如超过 100GB建议在中间摆渡机器上先用rsync -P做断点续传不要搞一次性大文件拷贝。分卷压缩虽然土但在不稳定介质传输时真的救命。5.4 版本一致性内网工程的隐形雷开发环境和生产环境的版本不一致是内网部署最常见的翻车原因。一个 LangChain 的 minor 版本升级可能导致 Tool Calling 的行为大变而你在开发环境测试时用的是新版本内网还停留在旧版本。现在我要求项目里所有依赖版本全部锁定到不用或~。模型目录用一个 manifest 文件记录模型名、版本、sha256、部署日期。镜像 tag 统一用日期-构建序号的格式比如agent-api:20250520-01。每次发版前先对比开发机和内网机器的依赖清单用pip freeze输出比对确定一致再走发布流程。这个习惯看着繁琐但它能帮你省下大量我在开发环境是好的怎么到内网就炸了这类排障时间。6. 并发与稳定性让 Agent 服务在内网扛住真实业务6.1 Agent 为什么天然慢AI Agent 怎么扛并发最近在圈子里讨论很多因为 Agent 的并发问题和传统 API 服务完全不同。一个普通 REST 接口响应时间通常几百毫秒。但一个 Agent 请求内部可能要调用 2 到 5 次大模型每次几秒中间还穿插工具调用而且这些调用是串行还是并行完全取决于流程设计。用户体感上一个 Agent 请求可能要 10 到 30 秒才能返回。这带来一个直接后果并发 IPC 上来了模型层的推理请求量会以 3 到 5 倍的系数放大。假设你的对话服务要支持 10 个用户同时在线问问题实际打到 vLLM 的请求可能是 30 到 50 个并发。这个放大系数在容量规划时必须算进去。6.2 推理层的并发优化vLLM 的 Continuous Batching 已经能在一个推理批次里动态塞入新请求这是它能扛高并发的关键。但要用好它有几个参数要调整。第一是max_num_seqs它控制一个批次里最多塞多少个序列。设置得太大会导致单个请求等待时间变长设置太小会浪费算力。第二是gpu_memory_utilization前面说过调太高会导致 KV Cache 空间不够触发大量 preemption表现为机器忙但响应速度极慢。第三是启用 Prefix Caching--enable-prefix-caching对于多轮对话和大量相同 system prompt 的场景效果立竿见影。如果你跑的是 14B 模型并且并发要求很高双卡张量并行是为了把 KV Cache 的总空间翻倍性能提升比换更贵单卡来得划算。6.3 应用层的并发管控模型层都能扛并发不代表应用层随便写就能扛。FastAPI 本身是异步的但如果你在异步函数里用了同步的 HTTP 客户端比如requests一个请求就会卡住一个线程并发一高整个进程就阻塞了。我们的要求是应用层所有 IO 全部用httpx.AsyncClient数据库操作用异步驱动Redis 用redis.asyncio。这个改造很枯燥但它是保持高并发的底线。另外还要在应用层加入限流。我的做法是用asyncio.Semaphore控制全局并发的 Agent 数量。比如设定最大同时执行 20 个 Agent 任务超过的直接进入等待队列而不是无限创建协程把内存打爆agent_semaphore asyncio.Semaphore(20) async def handle_chat(request: ChatRequest): async with agent_semaphore: result await agent_runner.run(request) return result用户侧会看到排队中的提示比直接超时或者 502 要友好得多而且能保护下游系统不被激增流量打垮。6.4 流式输出把等待变成体验Agent 响应慢这是物理上的限制没法完全消除但我们可以从交互层面优化。现在所有对话接口全部走 SSE 流式输出。当 Agent 在规划、调用工具、生成回答时前端可以实时展示当前状态正在查询订单系统…正在生成回复…。用户看到进行中对延迟的容忍度会大幅提升。实现上LangGraph 原生支持astream编排层可以把状态变更事件作为 SSE 流推给前端。这里要注意 Web 前端的 SSE 断线重连逻辑Agent 任务在服务端还在跑前端重连后要能拿到当前状态这个是通过 Redis 里的任务进度缓存实现的。6.5 降级与容灾隔离内网环境没有云上的弹性能力所有组件都要考虑某一个挂了怎么办。我做了三层降级。第一层是模型不可用。vLLM 服务挂了编排层直接检测不到模型接口就自动切换成知识库检索 规则回复模式。用户问的问题从向量库里找最相似的 FAQ把固定答案返回回去。虽然没有那么智能但至少不会让用户吃一鼻子灰。第二层是工具不可用。某一项内部 API 超时或报错Agent 会把错误信息告诉模型让模型尝试换一种方式实现。比如查不到订单详情就引导用户提供更多信息或者转人工。第三层是全局熔断。如果连续出现大量超时说明系统容量已经到极限接入层直接开启拒绝服务返回当前咨询量较大请稍后再试避免雪崩。6.6 实测数据参考拿我们在 Qwen2.5-14B 双卡 A100 上的压测结果举例。纯对话场景20 并发时平均首 token 延迟约 1.8 秒完整回复平均 6 秒左右。带工具调用场景10 并发时平均完成时间约 12 秒。这两个数字都受到业务复杂度影响你的环境可能不同但可以做一个基准参照。压测工具推荐用locust或者自写的协程脚本模拟真实对话链路而不是简单压模型的/v1/chat/completions因为后者会低估 Agent 的全部开销。7. 踩坑实录这几类问题几乎每个项目都会碰到7.1 LangGraph 在异步环境回调死锁第一个大坑是 LangGraph 的异步执行。我们用 FastAPI 的async def调用 LangGraph在回调函数里又去访问同一个事件循环结果就是请求直接卡死直到超时。排查了很久才定位到问题是 LangGraph 的回调是同步执行的而我们在回调里调用了异步方法导致事件循环被阻塞。解决办法是绕开回调做异步操作。不要在 LangGraph 的 CallbackHandler 里直接做数据库写入、Redis 更新、HTTP 调用而是把需要异步处理的动作放到一个消息队列里由独立的 worker 去消费。CallbackHandler 里只做简单的状态标记瞬间就能返回。另外一个小细节回调函数的执行时间也会影响整个 Agent 的吞吐。即使只做状态标记如果里面有一个文件写入并发一高也会变成瓶颈。保持回调轻薄这是异步环境里的铁律。7.2 内网时钟漂移导致鉴权失败这个问题非常隐蔽。Agent 调用内网系统接口时用的是 JWT Token签发方和验证方都要求服务器时间准确。但内网环境的机器很多时候不联网NTP 没有配置某台机器比另外一台慢了两分钟于是所有调用都返回 401 鉴权失败而且不是每次都失败是间歇性的因为时钟漂移不是固定值。最后的解决方案是统一配置 NTP 服务让所有内网机器向一台内网时间服务器同步时钟并加监控定时检查各机器的时间偏移。如果服务器时钟无法统一那就在自己的 Token 校验逻辑里放宽 60 秒的过期冗余。但最根本的还是 NTP别偷懒。7.3 vLLM 的 max-model-len 开太大导致的性能雪崩一开始我们把max-model-len设成了 32768因为觉得上下文越长越安全。结果跑起来后发现并发一高响应速度慢到不可接受。原因前面提过max-model-len决定了 KV Cache 的预留大小长度越长显存里能同时容纳的请求数就越少vLLM 只能反复 preemption。后来把max-model-len改成了 8192同时调整了gpu-memory-utilization到 0.9性能立刻恢复正常。训练时如果没有特殊需求上下文长度按业务需求的 1.5 倍来设定就够了。为了极少数的超长输入牺牲整体并发不划算。7.4 Python subprocess 跑工具导致内存上涨我们的 Agent 要调用一个内部的数据处理脚本处理 Excel 文件。最初图省事直接在 Tool 函数里用subprocess.run()调外部脚本。结果两天后服务内存涨到了 16GB而且持续上涨。原因是 Python 的subprocess在这些内部脚本异常退出时会残留僵尸进程而且每个 Excel 处理的中间数据都留在内存里没释放。后续改造是把所有重操作工具全部迁移到独立的 Rust Sidecar 服务里Python 编排层只通过 HTTP 调用 Sidecar 的执行接口Sidecar 内部自己做进程管理和超时回收。这样一来即使某个工具崩了也只是 Sidecar 的一个请求失败不会拖垮整个 Agent 进程。这里给一个经验法则凡是超过 1 秒的、涉及外部进程的、涉及大文件操作的工具都从 Python 进程里剥离出去。Python 适合做编排和调度但不适合做沙箱。7.5 从 2 并发到 30 并发的改造清单最后把我们项目从最初勉强 2 并发改造到稳定 30 并发的过程总结一下这是最直接的实操清单优化项原始状态改造后效果模型层Ollama 单实例vLLM Continuous Batching吞吐提升 5 倍以上上下文长度327688192并发容量翻倍应用层 IO同步 requestshttpx.AsyncClient线程不再耗尽并发控制无限制asyncio.Semaphore(20)内存稳定工具执行Python subprocessRust Sidecar无内存泄漏输出方式全量返回SSE 流式用户等待感大幅降低降级策略无模型/工具/全局三层无单点故障每一步单独拿出来都不是很难难的是系统性地排查并改完。改造完之后我最大的体会是Agent 系统的并发瓶颈不在任何单一组件而在最弱的一环。只要有一环是同步阻塞整条链路都会被拖垮。我们从最早的演示可用到后来真正承载业务流量中间踩过的坑远不止上面列出的这些。但只要你把架构分层做清楚、依赖版本锁死、超时和降级提前设计好隔离内网里的 Agent 是完全能做到稳定交付的。实际运行中还有一个小技巧就是给 Agent 的每一次运行都记录完整的轨迹日志从输入、规划、工具调用到最终回复全部留痕这样不管是排查问题还是后续优化数据回流都有素材可用。
返回列表