去年年中,我接了一个很拧巴的项目:一套 AI Agent 系统,必须跑在物理隔离的内网里。客户方的业务人员想要一个能自然对话、能查知识库、能对接内部系统的智能助手,但安全规范相当严格,所有数据不允许出域,公有云的模型 API 一概不能碰。这直接把我之前做 Agent 的路径全打乱了——以前是“调别人的模型、接别人的向量库、用别人的网关”,这次全部要自己搭、自己养、自己护。整篇文章就是这次隔离内网下 AI Agent 工程实战的完整复盘,从架构选型到数据准备,从并发压测到排障实录,再到上线后的迭代建议。如果你是做私有化部署、信创适配、或者任何“不能上网但还想用大模型”场景的工程师,这篇应该能帮你少踩不少坑。
先声明一下:这篇文章不是纸上谈兵,所有结论都来自我在真实项目里跑出来的记录。项目前后做了三个多月,交付后系统每天要处理上千次查询,模型服务、Agent 编排、检索链路全都跑在断网的物理环境里。我把过程里关键的决策、参数、错误和反思尽量完整地写出来,希望对你有用。
1. 为什么放着主流云服务不用,非要在隔离内网里搭 AI Agent
1.1 场景是怎么来的:数据不出域,Agent 就必须本地化
当时客户给我的需求其实很简单:OA 系统里的知识库要能被“问起来”,分子机构的问题在系统内直接获得答案,不需要人工一篇篇翻文档。但他们对安全的要求是硬性的:业务数据、组织架构、合同条款、人员信息,一律不能出企业内部网络。这意味着我不能用任何云端大模型 API,不能把文档送到外部服务去解析,甚至模型服务的日志都不能传到外部。这就是典型的数据合规驱动的私有化场景。
这类需求在金融机构、央国企、政务系统里非常常见,我对接的这家客户就是类似背景。他们不是不相信云厂商,而是行业监管对数据主权、安全审计有明确要求。所以从立项那一刻起,技术路线就注定和“接 OpenAI API”那种玩法完全不同。
1.2 隔离内网和普通开发环境的真实差距
很多人对“隔离内网”的理解就是一台没网的电脑。真做起来会发现,它带来的麻烦是系统性的:
- 没有公共软件源:pip、npm、apt、Docker Hub 全部不可达。所有依赖包、基础镜像、模型权重都要想办法先弄进去。
- 证书环境不一样:隔离内网里很多服务用的是自签名证书或企业 CA,HTTP 客户端如果不做适配,请求直接原地失败。
- 模型权重成了“货”:一个大模型动辄几个 GB 到几十 GB,要过内外网隔离的传输审批流程,拷一次盘就得走一两个星期。
- 无法在线调试外部服务:平时习惯了什么报错就搜一下,但隔离网里只能靠自己的经验和系统性排查。
这些差距不只是技术上的,还是流程上的、资源上的。项目排期得把这些“非编码工作”全部算进去,不然一定延期。
1.3 先想清楚:Agent 到底要干哪几件事
在选型之前,我逼着客户和我一起把 Agent 的使用边界定义清楚。我们最后收敛成三类任务:
- 知识库问答:基于内部文档的自然语言检索和回答。
- 内部系统工具调用:通过 API 或 RPA 方式查询 OA/HR/合同系统的数据。
- 长流程任务:比如跨系统收集信息、生成报告草稿。
我强烈建议每个团队在做隔离内网 Agent 之前先做这个动作。因为“Agent 越通用越难”,尤其在离线环境里,调一次模型、跑一次工具都是有成本的。明确任务边界,后面所有的架构、并发、数据准备才有依据。
2. 架构选型的三层决策:模型推理、Agent 编排、应用服务
2.1 模型推理层:开源模型、量化方案与推理引擎的取舍
隔离内网条件下,模型选型几乎没有悬念:只能选开源权重。我们当时对比了 Qwen2.5-7B-Instruct、Qwen2.5-14B-Instruct、ChatGLM4 系列和 Llama3.1-8B-Instruct。在中文知识库问答、内部工具调用这两个场景下,Qwen 系列的表现明显更稳,函数调用(function calling)的成功率也比 Llama 高出一截。最终我们定了 Qwen2.5-14B-Instruct 作为主模型,同时配了一个轻量的 Qwen2.5-3B 作为意图识别和路由的小模型。
模型权重是定了,但推理要怎么跑还有讲究。最初我们直接用 Transformers 加载进行推理,结果并发一上来根本扛不住。后来换了 vLLM 做推理引擎,吞吐提升非常明显。这里有个关键参数我要专门说一下——GPU 显存估算。以 14B 模型为例,FP16 权重大概占 28GB,加上 KV Cache 和激活值,最少得 48GB 显存。我们手头的机器是两张 A800(80GB),单卡就能塞下 14B 模型 + 较大 Batch 的推理,另一张卡留给了 Embedding 模型和重排模型。如果显存不够,可以量化为 INT8 或 AWQ,14B 量化到 INT8 大概 14GB 权重,一张 4090 也能跑,但生成质量会略有下降,需要在效果评测时做对比。
推理引擎我们只考虑了 vLLM 和 llama.cpp。vLLM 的优势是吞吐高、支持连续批处理(continuous batching),适合服务化部署;llama.cpp 更轻量,适合单机或者 CPU 推理,但做并发服务化要自己再包一层。隔离内网场景下我推荐 vLLM,因为它自带 OpenAI 兼容接口,后面接 Agent 框架时省掉很多事。
2.2 Agent 编排层:为什么用 LangGraph 而不是硬编码
在隔离内网里做 Agent 编排,很多人第一反应是“写 if-else 硬编码算了”。如果是两三个固定流程,硬编码确实简单;但如果工具多了、分支多了、还有循环、重试、暂停恢复这些需求,硬编码会变成一场灾难。
我们最终用了 LangGraph。这里不是无脑推荐,而是从几个维度考虑过的:
- 状态管理:LangGraph 把 Agent 的执行过程建模成一张状态图,每个节点负责一个动作,边负责状态转移。这对“需要跨多轮调用工具、随时可能中断”的任务非常友好。
- 可观测性:Agent 执行到哪一步、调用了哪个工具、上下文里新增了什么,LangGraph 都能通过钩子打日志,这对隔离内网里排查问题至关重要。
- 条件分支与循环:比如“查合同信息 -> 信息缺失 -> 再问用户 -> 再查”这种循环逻辑,用状态图表达比硬编码清晰得多。
当然 LangGraph 也不是没有代价。隔离内网里没有外网,所有依赖包都必须在建设期就导进来。我们当时把 LangChain、LangGraph 及其所有依赖做了一个完整的 wheelhouse 收集,在另一台相同架构的机器上把所有包全部下载,再搬到内网安装。这一步务必提前做,千万别到现场发现某个依赖缺失。
2.3 应用服务层:FastAPI 做接口、任务队列做长任务
应用层我们选了 FastAPI + Celery + Redis 的组合。FastAPI 负责对外提供 HTTP 接口,Celery 处理异步长任务,Redis 充当消息代理和缓存。为什么要拆一层任务队列?因为 Agent 的调用不是瞬时返回的,一个复杂的 Agent 流程可能要跑几十秒甚至几分钟。如果 HTTP 接口里同步等待,前端连接会被长时间占用,网关容易超时,体验也差。
我们的做法是:HTTP 接口收到请求后,把任务丢进 Celery 队列并立即返回一个 task_id;前端用轮询或 WebSocket 订阅结果。这样既控制住了连接数,又方便做并发控制。
另外在 Agent 执行过程中,我们用 SSE(Server-Sent Events)向客户端推送中间状态,比如“正在检索知识库…”“正在调用合同系统…”这种进度提示。用户看到进度,等待焦虑会小很多,这在真实产品里是提升好感度的关键细节。
3. 数据准备才是真正的头号工程:清洗、切分、向量化全流程
3.1 数据源盘点与清洗:PDF 乱码、扫描件 OCR 都是拦路虎
很多人把“知识库”想得太简单,以为扔几个 PDF 进去就能问答了。实际上客户给我的文档五花八门:有扫描版 PDF、有网页导出文件、有表格套表格的 Word、有图片截图。如果不去清洗直接切分,检索质量会非常差。
清洗环节我们做了三件事:
- 格式统一:所有 Word、PDF、网页统一转成 Markdown,方便后面按结构切分。
- 扫描件 OCR:扫描版 PDF 先过 OCR 识别成文本。这一步在隔离内网里很麻烦,因为 OCR 模型也要本地部署。我们用的 PaddleOCR 离线版本,效果不错,但部署时要把模型文件和所有依赖一起导入。
- 去噪:去掉页眉页脚、目录、"第 X 页 / 共 Y 页"这种噪音;表格保留,但转成紧凑的文本格式。
清洗的成本远超预期,客户给了 800 多份文档,光清洗和人工抽检就花了两周。但这一步不能省,不然 Agent 的回答质量就在垃圾进、垃圾出。
3.2 切分策略:按结构切,而不是一刀切
切分(chunking)是整个 RAG 链路里最容易被低估的环节。很多人直接用固定窗口切,每 500 个字一段,重叠 50 个字。这样切出来的 chunk 经常在句子中间劈开,或者把两个完全无关的段落缝在一起,检索时非常尴尬。
我们的方案是:结合文档结构切分。Markdown 格式天然有标题层级,我们优先按标题、列表、表格结构切分,再对切出来的块设置长度上限。如果某个小节太长,再按句子边界二次切分,并让相邻块之间有 10% 左右的重叠。
最终参数参考:主切分块长度 512 到 800 个 token,重叠 50 到 100 个 token。为什么是这个区间?太短了语义不完整,太长了 embedding 向量被稀释,检索精度会下降。这个参数不是拍脑袋定的,是拿了一批带标注的测试问题跑出来的召回率对比结果。实测下来,按结构切分后,TopK=5 的召回准确率比固定窗口切分高了大概 8 个百分点。
3.3 向量化与本地检索:离线环境没有“免费午餐”
Embedding 模型我们选的 BGE-M3,中文效果在开源模型里是第一梯队,而且支持 8192 长度的输入。隔离内网里没有在线 embedding API,只能本地跑。要注意的是,BGE-M3 在导入时也要专门下载权重,而且它指令前缀(instruction)的用法会影响检索效果,建议按官方文档配置 query 和 passage 的编码方式。
向量索引我们对比了 FAISS 和 Milvus。项目早期数据量只有几万条,用 FAISS 完全够;后来越来越多,为了支持线上更新和结构化过滤,换成了 Milvus。如果你也是先小后大的节奏,建议直接上 Milvus standalone 模式,减少后面迁移的成本。另外一定要加rerank(重排)阶段:先用向量检索召回 TopK=20 的候选,再用本地 rerank 模型精排,取 TopK=5 送给大模型。BGE-Reranker-v2-m3 在这类任务上很稳,召回率提升是肉眼可见的。
这里必须强调一个“但书”:本地检索再准,也只能找到“文档里的内容”。如果文档本身没有、或者内容过时,Agent 再聪明也答错。所以上线前要做一轮知识库内容核对,和业务方一起确认“标准答案”在哪篇文档里。
4. 并发扛不住?先搞清瓶颈在哪一层
4.1 瓶颈链路分析:模型推理是最大的“卡点”
“AI Agent 怎么扛并发”是所有 Agent 项目逃不开的问题。我先说结论:Agent 链路里最卡的不是 HTTP 接口、不是 Redis、也不是向量检索,而是LLM 推理本身。一次完整问答要经历“意图识别 -> 检索 -> 生成”,其中生成环节要几秒到几十秒,且生成时 GPU 是持续占用的。并发 10 个请求,如果模型推理没有批处理能力,那 10 个请求就是一个一个排队,后面的用户只能干等。
4.2 vLLM 的连续批处理:为什么它是并发的大救星
传统 Transformers 推理是单请求独占 GPU,vLLM 引入了 continuous batching,可以动态把一个 batch 里已完成生成的请求踢出去,再把新请求塞进来。这样显卡利用率大幅提升,并发 20 个请求,每个请求的平均响应时间也不会劣化太多。
为了最大化吞吐,我们调整了几个关键参数:
- max_num_seqs:控制最多同时处理的序列数,我们设为 256(默认值可能不够)。
- gpu_memory_utilization:模型加载后的显存利用率上限,我们设为 0.9,剩下的留给其他进程。
- max_model_len:上下文窗口长度,我们设为 32768。这个值会影响显存占用,建议按实际需求设置,不要盲目开大。
我用下面这个表格记录过加压测试的数据,环境是一张 A800-80GB,14B 模型:
| 并发请求数 | 单请求平均总耗时 | 首 Token 延迟 | 每分钟完成请求数 | 是否有超时 |
|---|---|---|---|---|
| 1 | 4.8s | 0.6s | 12 | 无 |
| 5 | 5.8s | 0.7s | 45 | 无 |
| 10 | 7.2s | 0.8s | 78 | 无 |
| 20 | 9.5s | 1.1s | 126 | 无 |
| 30 | 14.3s | 1.8s | 108 | 有少量 |
可以看到并发到 20 时,总量吞吐仍在上升,但平均耗时已经明显变长;到 30 时,稳定性开始下降。生产环境我建议把并发控制在峰值吞吐的 70% 左右,留出余量应对突发请求。
4.3 任务队列限流与优先级:让重要任务先走
光靠模型层并发还不够,应用层必须有队列管控。我们做了两层:
- 全局并发闸门:用一个信号量或者 Redis 计数器控制同时进入 Agent 编排的任务数,超过阈值直接返回 503,让前端进行友好提示。
- 任务优先级:简单问答类的短任务走高优先级队列,复杂报告生成这类长任务走低优先级。这样用户问“报销流程是什么”不会被一个正在跑报告的任务堵死。
另外要设置单次 Agent 执行的超时时间。我们统一设置为 120 秒,超过就终止并把中间结果返回给用户。这既是防滥用,也是防模型“跑飞”。
4.4 Token 级别的用量控制
还有一个容易忽略的角度:Token 消耗控制。Agent 每多调用一次工具,上下文就会多出一轮往返。如果工具结果太长,模型要处理的 token 数会爆炸,推理延迟也会直线上升。我们的策略是:
- 工具返回结果做截断,通常只保留前 1000 个字符,并用提示词告诉模型“这是截断后的内容”。
- 上下文累计超过 24000 token 时,触发一次摘要压缩,把前面的对话历史压缩成一段摘要再继续。
- 所有请求记录 token 用量,方便核算 GPU 负载和容量规划。
5. 隔离内网排障实录:三个典型坑的完整排查链路
5.1 坑一:自签名证书导致所有 HTTP 调用全线失败
现象:Agent 编排层调用内网的合同系统 API 时,requests 直接抛SSLError。一开始我以为是对方服务没起来,查了服务健康状态完全正常,curl 也能通。
排查链路:
- 先用 curl 访问,发现需要加
-k才能通过,说明是证书校验问题。 - 看代码里是用
requests.post(url, json=payload),默认校验证书。 - 进一步确认:内网服务和 Agent 服务都是自签名证书,而 requests 库的证书存储里没有这个 CA。
解决:把内网根 CA 证书导出,挂到 Agent 容器里,并设置环境变量REQUESTS_CA_BUNDLE指向该 CA 文件。同时给所有 HTTP 客户端加信任配置。这个坑最麻烦的地方在于它“部分绕过”:某些服务走内部代理不带证书校验,某些服务直接 TLS,导致表现不一致,排查时容易被误导。
5.2 坑二:LangGraph 离线依赖缺失,启动即失败
现象:在内网环境第一次部署 LangGraph 时,服务启动直接报ModuleNotFoundError: No module named 'langgraph.checkpoint'。外网环境没这个问题,因为 pip 自动安装了依赖;内网环境安装时用的是离线 wheelhouse,但收集依赖时漏掉了这个子包。
排查链路:
- 在可联网的开发机上建一个虚拟环境,用
pip install langgraph后执行pip freeze导出完整依赖列表。 - 用
pip download -r requirements.txt -d ./wheelhouse下载所有包。 - 这中间的问题在于 LangGraph 部分模块是延迟导入的,只按 import 的顶层包收集会漏子依赖。
教训:离线环境收集依赖时,一定要用pip freeze而不是手工整理。我后来又写了个脚本,把所有依赖打包成 tar 包,在内网机器上用pip install --no-index --find-links安装,同时做了一次导入冒烟测试,确保启动无误。
5.3 坑三:长文本超出上下文窗口,Agent 问答结果断崖式变差
现象:用户提交一份 3 万字的合同文本做分析,模型生成结果中途戛然而止,或者只回答了一半合同内容。
排查链路:从日志看,请求在 31,000 多个 token 处停止,而我们的 max_model_len 设置的是 32768。单看数字没有超,但实际请求是“系统提示 + 全文内容 + 用户问题 + 历史对话”拼在一起,已经压在阈值边缘。一旦检索返回的 chunk 比较多,直接爆掉。
解决思路分三层:
- 应用层先对输入做摘要或者分段,不让全量文本一次性灌入。
- 检索层严格控制送入模型的 chunk 数量,按相关性排序后只取 TopK=5,并且限制每个 chunk 长度。
- 模型层调大
max_model_len到 49152,并相应调整显存分配。实测在 A800 上可以稳定运行,但并发上限有所下降,属于取舍。
这个坑再次验证了一个原则:上下文的“预算”必须由应用层统一规划,而不是全甩给模型。
6. 部署、上线与后续迭代的几点建议
6.1 镜像和离线包的固化
内网部署不能拿着 Dockerfile 现场 build。我们提前在可联网环境把所有服务构建成镜像:vLLM 推理镜像、LangGraph Agent 镜像、FastAPI 应用镜像、Milvus 镜像,全部docker save成 tar 包,拷入内网后docker load。为了应对模型权重文件,我们单独做了一个模型数据卷,配合启动脚本自动加载。
镜像版本一定要锁定到 commit 级别的 tag,否则内网里无法拉取“最新版”,出了兼容性问题根本无从查起。我把所有第三方依赖、模型文件、配置文件的版本号集中记录在一个 MANIFEST 文件里,后续升级时能追溯。
6.2 健康检查与监控指标体系
隔离内网里的监控不能依赖外部 SaaS,我们用 Prometheus + Grafana 裸机部署了一套。重点监控四个指标:GPU 显存占用、模型推理吞吐、任务队列积压数、检索耗时分位数。
这里要提一个很实用的检查:vLLM 服务启动后,健康检查接口要等到模型完全加载完成才返回 200,否则部署时容器处于“Running 但不可用”状态。我们写了一个启动脚本,先轮询 vLLM 的/v1/models接口,模型就绪后再启动 Agent 服务。
6.3 评估集与回归机制
上线前我们和业务方共建了一套 120 条的评估集,覆盖知识问答、工具调用、拒答三类场景。每次修改系统提示词、切分策略、重排参数,都跑一遍评估集,记录准确率和满意度评分。Agent 项目如果没有评估集,改提示词就像蒙眼开车,上线全凭感觉。
我强烈建议在交付初期就把“用户反馈按钮”做进去,每个回答下方放“满意 / 不满意”,不满意可备注原因。这些数据最终会成为下一轮迭代最重要的信号。
6.4 迭代优先级:先稳住确定性,再谈智能性
功能上线后,我们收到的反馈集中在两类:一是知识库没覆盖到的问题,二是工具调用时偶发的参数解析错误。前者的解法是持续补充文档和调优检索;后者的解法是完善工具定义 schema,同时增加一次模型输出校验环节——如果模型生成的函数调用参数不合法,让 Agent 重试一次而不是直接报错。
在真实业务里,用户对“稳定可用”的在意程度远超过“偶尔惊艳”。我们的迭代顺序是:先保证 95% 以上的问题能被不报错地处理,再逐步提升回答的准确性和丰富度。
这套架构跑到现在,稳定运行了三个多月。最大的收获反而不是技术上的:在隔离环境里做 Agent,每一个细节都必须提前验证,因为网不好上、信息不好查,试错成本极高。如果你也在做类似的项目,我唯一想强调的就一句话——把可联网阶段能做的准备做到极致,尤其是依赖收集、镜像构建、权重导入和评估集建设,这四样前期越扎实,后期越省心。至于模型选型、Agent 框架这种事,真的没有银弹,只能结合自己的业务场景去测、去比、去忍耐。祝你能把自己的 Agent 顺利也“下地干活”。