☰
隔离内网部署AI Agent的完整实践:架构、本地化与并发优化
2026/10/6 15:13:14 网站建设 项目流程

我们部门上个月接到一个任务:在完全隔离的内网环境里,落地一套能真正干活的 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 模型权重约 14GB,14B 模型权重约 28GB,32B 模型权重约 64GB,这些都是理论下限。

然后是 KV Cache。它和并发数、上下文长度直接相关。粗略估算公式是:KV Cache 占用 = 层数 × 头数 × 每头维度 × 层数系数 × 序列长度 × 并发数,这个公式对非算法工程师不太友好,我给你一个工程化的判断标准:在gpu_memory_utilization=0.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}, timeout=10, ) 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 的每一次运行都记录完整的轨迹日志,从输入、规划、工具调用到最终回复全部留痕,这样不管是排查问题还是后续优化数据回流,都有素材可用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询