接到这个需求时,我原本以为只是把平时在公网玩的那套 AI Agent 搬到内网跑一遍。真到上线那天,我才发现自己太天真。物理隔离的内网根本没有路:pip 装不了依赖,HuggingFace 连不上,OpenAI 的 Key 就是一张废纸,连 Docker 镜像都得靠移动介质往里搬。但业务侧的需求又真实存在——工单分类、告警分析、知识问答,全都等着一个能调工具、能多步推理的 AI Agent 来接手。
这篇文章我想完整记录一下在隔离内网环境下落地 AI Agent 的过程。它不是什么高深的理论,而是把公网习以为常的一整套玩法重新翻译成离线玩法:模型怎么进去、依赖怎么装、Agent 框架怎么连、并发怎么扛、坑怎么填。如果你恰好要在政企、金融、涉密或工业内网里做 AI 落地,或者你只是好奇"没有网的时候大家怎么办",这篇应该能帮你省掉不少弯路。
1. 隔离内网跑 AI Agent:先认清楚这不是"少了一根网线"
1.1 隔离环境的真实面貌:是"没有路",不是"路太慢"
很多人对隔离内网的理解就是"不能上外网",这个说法太轻描淡写了。我这次所在的环境是典型的物理隔离网络,与公网完全断开,内部只有业务网段和管理网段。你平时写代码时的默认资源全部失效:
- pip install 连不上 PyPI,连内网镜像都未必有;
- Docker 默认拉不了镜像,HuggingFace 更是直接超时;
- 任何外部 SaaS API(包括各种大模型开放平台)都不存在;
- 内网没有 DNS 解析外网域名,即使物理上有出口也会被策略掐断。
但这不意味着里面是"数字荒漠"。恰恰相反,真正有价值的系统全在里面:ERP、工单系统、监控平台、数据库、内部 Wiki、大型文档库。AI Agent 在这里的定位不是"聊天玩具",而是帮人跨系统做事的助手——用户说一句"帮我汇总上周生产环境的告警,按级别分类并给出原因分析",Agent 需要规划步骤、调用监控接口、拉取告警数据、查历史知识库、最后生成分析报告。
这也是我为什么坚持用 Agent 而不是做一个普通 FAQ 问答机器人的原因:隔离内网里最缺的不是"能搜到答案",而是"能把答案和操作串起来"的能力。
1.2 为什么不能"偷偷联外网":合规不是借口,是底线
可能有人会问:搞个 4G 路由或者让运维开个白名单不行吗?这个念头我也有过。但隔离内网通常对应等保三级或更高级别的合规要求,"数据不出域"是硬底线。业务数据、告警日志、工单内容出了这个网段,哪怕只是过了一下公网大模型的接口,性质就变了。
所以,真正可行的路线只有一条:把大模型、Agent 框架、知识库全链路都放到内网里来。这也是本文所有方案设计的出发点。
1.3 这条路线要解决的核心问题
- 模型可达性:需要一个能在内网提供 OpenAI 兼容接口的推理服务;
- 框架可用性:LangChain/LangGraph、Spring AI、自研 Rust 等框架的依赖必须离线可用,且能指向内网模型服务;
- 工具闭环:Agent 要调用的工具全部来自内网系统;
- 知识与语料:RAG 用的文档库、嵌入模型也得离线;
- 并发与运维:多用户同时使用时,Agent 服务不能被打挂。
下面我按实际推进的顺序,把这五个问题的解法逐个展开。
2. 整体方案选型:把"公网玩法"翻译成"离线玩法"
2.1 LLM 推理服务选型:vLLM 是绕不开的主流选择
要在内网跑 Agent,第一步是让大模型"活"起来。可选方案大致有四个:
| 方案 | 优点 | 劣势 | 适合场景 |
|---|---|---|---|
| vLLM | 吞吐高、连续批处理、OpenAI 兼容接口 | 显存占用高、依赖较重 | 多用户、高并发的正式生产 |
| TGI | HuggingFace 官方维护、接口标准 | 部署灵活性稍弱 | 已有 HF 生态的团队 |
| Ollama | 安装简单、模型管理方便 | 并发能力弱、接口规范略特殊 | 单机验证、小团队试用 |
| llama.cpp | 纯 CPU/低显存也能跑 | 吞吐低、配置繁琐 | 无 GPU 的极端环境 |
我最后选了 vLLM,原因很简单:隔离内网里部署一次成本很高,与其搞一个 "能跑但撑不住" 的玩具,不如直接上生产级吞吐。加上 Qwen2.5 系列在中文场景表现不错,内网 GPU 是 A800 或 4090 的机器都能比较好的支持。
选型时最先要确定的是模型量化策略。显存不够大时,常见做法是上 AWQ 或 GPTQ 量化版本。我强烈建议:先固定 vLLM 版本,再选对应版本的量化模型。AWQ 算子和 vLLM 版本是有绑定关系的,后面踩坑章节我会详细讲。
2.2 Agent 框架选型:LangGraph 为主,但也留了替代方案
Agent 框架我在选题时重点评估了四条路:
- LangGraph:图化编排,适合复杂多步任务,离线依赖可控;
- Spring AI:团队里有 Java 背景的人会更顺手,因为隔离内网里 Java 生态的依赖私服通常更成熟;
- 自研 Rust Agent:从这个热词就能看出很多人感兴趣,控制力强、并发表现好,但开发周期长,不适合第一版落地;
- 扣子/Coze 私有化版:低代码,业务侧友好,但目前还是偏平台化部署,对纯内网交付有额外要求。
我最终选了LangGraph + FastAPI的组合,基本复刻了业界常见的 langchain + fastapi 路线。这个选择不是为了追新,而是因为 Python 生态在离线环境下虽然麻烦,但依赖搬运路径最清晰,网上踩坑资料也最多。后续即便要换成 Spring AI,核心的 LLM 接入和工具定义思路完全通用。
2.3 外部工具到内部工具的映射逻辑
公网教程里 Agent 经常调用的工具是"网页搜索""天气查询""计算器"。在隔离内网里,这些东西要么没有,要么要被替换。我的处理思路是把"工具"抽象成三层:
- 查询类工具:查数据库、查监控接口、查工单系统;
- 操作类工具:创建工单、发送内部通知、修改配置(权限要严格);
- 知识类工具:检索内部文档库、查询历史 Case。
可以理解为:公网的"搜索"在这里变成"检索内部知识库",公网的"天气 API"在这里变成"内部监控接口"。工具的本质没变,变的是数据源和权限边界。这样设计之后,Agent 的规划和推理逻辑可以完全复用公开框架的能力,只是工具的输入输出定义要重新写。
3. 从有网到无网:依赖与模型分发的完整搬运流程
3.1 pip 离线安装:一条跳板机就能解决的问题
隔离内网里最痛苦的第一件事就是装依赖。我采用的通用方案是"有网机器下载 + 移动介质搬运 + 内网机器本地安装",具体分三步。
第一步,在有网的跳板机(或者开发机)上用 pip download 把整个依赖树拉下来:
pip download -r requirements.txt -d ./offline_packages \ --platform manylinux2014_x86_64 \ --python-version 3.11 \ --only-binary=:all: \ --no-deps这里有两个关键点。第一,--platform 必须和最终目标机器一致,避免下载到 macOS 或 Windows 的包;第二,--no-deps 后面要配合另一条命令把传递依赖也拉全,否则容易出现"装的装上,但运行时 import 报错"的问题。
第二步,用 pip-compile 把依赖版本彻底锁定。这一步很容易被忽略,但在隔离环境里非常重要,因为你不能像在公网一样随时"pip install 最新版"。
pip-compile requirements.in -o requirements.txt第三步,在内网机器上离线安装:
pip install --no-index --find-links=./offline_packages \ -r requirements.txt提示:长线方案是在内网搭一个 PyPI 镜像(devpi/nexus 都可以)。前期项目迭代频繁时,用"移动介质搬运"足够;但团队人多了之后,内网源是必须的。
3.2 模型分发:几十 GB 的模型文件怎么进内网
模型文件比 Python 包更让人头疼。一个 70B 的量化模型动辄 40-60GB,用移动介质拷贝虽然可行,但要特别关注两件事。
第一,校验完整性。拷贝前和拷贝后都要算 SHA256,我在实践中遇到过移动介质本身有坏道导致模型文件静默损坏的情况,刚拷进去能加载,运行半小时后随机报错。后来学乖了,进内网之后先跑一遍完整校验再加载。
第二,保持 HuggingFace 缓存目录结构。直接拷贝模型目录本身也能用,但如果你把 HF 的缓存结构(models--Qwen--Qwen2.5-72B-Instruct/snapshots/...)原样搬进去,并设置 HF_HOME 环境变量,推理服务加载时会快很多,也便于后续管理多个模型版本。
export HF_HOME=/opt/models/huggingface export HUGGINGFACE_HUB_CACHE=/opt/models/huggingface/hub3.3 容易被忽略的验证步骤:版本、平台、扩展名
依赖装好、模型拷完,先别急着启动服务,我建议做一个"离线冒烟验证":
- 用
pip check检查所有已装依赖的冲突情况; - 用
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"确认 PyTorch 能正常识别 GPU; - 逐个 import Agent 框架的关键模块,确认二进制扩展名匹配(cp310 的包在 cp311 里会直接报错);
- 加载模型时观察日志,识别"fallback to CPU"之类的警告——如果有,说明某个算子编译版本不兼容,即便能跑速度也会很惨。
这些都是在公网开发时完全不会注意的细节,但在隔离内网里,一旦上线之后需要重新部署,整个流程的成本极高,所以验证阶段宁可多花两小时,也不要留着隐患上线。
4. 打通核心链路:LLM 推理服务与 Agent 框架对接
4.1 vLLM 推理服务启动参数与 OpenAI 兼容接口
模型就位后,我用下面的命令启动 vLLM:
python -m vllm.entrypoints.openai.api_server \ --model /opt/models/Qwen2.5-72B-Instruct \ --served-model-name qwen72b \ --port 8000 \ --host 0.0.0.0 \ --trust-remote-code \ --max-model-len 32768 \ --gpu-memory-utilization 0.85 \ --max-num-seqs 64 \ --enforce-eager几个参数说明一下:
- --served-model-name:给模型起一个内网统一的名字,Agent 端配置时要用;
- --max-model-len:根据业务场景设置,知识库问答建议至少 8K,如果 Agent 要传大文档历史,32K 更稳;
- --gpu-memory-utilization:不要设到 0.95 以上,否则 KV cache 和系统显存容易冲突;
- --enforce-eager:关闭 CUDA graph 加速,显存占用会低一些,代价是吞吐略降。如果你的内网 GPU 显存比较紧张,这个参数很实用。
启动成功以后,vLLM 会在 8000 端口暴露/v1/chat/completions、/v1/embeddings等 OpenAI 兼容接口。这意味着 Agent 框架对接时可以假装自己在调 OpenAI,只是 base_url 换成了内网地址。这是整条链路能跑通的核心。
4.2 LangGraph 中配置离线 LLM 与工具注册
在 Language Graph(我用的是 langgraph 0.2.x)里,关键代码就这么多:
from openai import OpenAI llm_client = OpenAI( base_url="http://inference-server:8000/v1", api_key="not-needed-for-local", ) # 在 LangGraph 里用一个 callable 封装 def call_llm(messages): resp = llm_client.chat.completions.create( model="qwen72b", messages=messages, temperature=0.2, ) return resp.choices[0].message.content这里要特别提醒:不要只配置这一个客户端,还要检查环境变量。openai 库在较新版本里会读取 OPENAI_BASE_URL 环境变量,如果系统里这个变量指向了公网地址,代码里传入的 base_url 会被覆盖或者行为不符合预期。这类问题极难排查,后面踩坑章节我会专门讲。
工具注册部分,我用的是 LangChain 的 @tool 装饰器:
from langchain_core.tools import tool @tool def query_workorder(date_range: str) -> list[dict]: """查询指定日期范围的工单列表""" # 这里调用内网工单系统的只读接口 response = requests.post( "http://workorder-api.internal/api/query", json={"date_range": date_range}, timeout=10, verify=False, # 内网自签名证书问题,见踩坑章节 ) return response.json()工具的关键是给足参数描述。Agent 模型要靠函数的 docstring 来理解什么参数该填什么,描述写得模糊,模型就会乱猜。跟完整的 FastAPI 接口一样,工具的输入输出最好也用 Pydantic 模型约束,让模型侧看到明确的 JSON Schema,比全靠 docstring 可靠得多。
4.3 RAG 知识库:离线嵌入模型加向量库的连接
Agent 光会调工具还不够,很多问题需要先检索内部文档再回答。我在内网搭了一套bge-m3 嵌入模型 + Milvus 向量库的方案。这里两个关键点:
- 嵌入模型同样要走 vLLM 的 /v1/embeddings 接口,这样不需要额外部署,Agent 调用时只需一次性把文档切块生成向量再存库;
- 文本切块时,中文文档我建议用按标题结构切,而不是简单按固定长度切。内网文档往往有明确的"章-节-条"结构,按结构切出来的检索准确率比固定 chunk 好很多。
入库和检索的链路可以理解成:文档先切块,再调用 bge-m3 生成向量,存入 Milvus;Agent 收到用户问题后同样调用嵌入模型生成向量,然后去 Milvus 做相似度检索,把 Top-K 文本塞进 LLM 上下文。
这套流程在公网有无数教程,但在隔离内网里,真正麻烦的是两个细节:一是向量库本身的离线安装(Milvus 依赖 etcd、minio,需要离线镜像),二是嵌入模型的上传与校验。这两个问题解决之后,RAG 反而比公网还稳定——因为没有外部干扰。
5. 隔离内网下 Agent 的"工作半径":工具与数据边界怎么设计
5.1 内部工具的类型与实现要点
我把 Agent 的工具集合分成三类,实际开发中也是按这个顺序逐步接入的。
- 只读查询类(数据库查询、监控指标查询、工单检索):这类工具最容易做,也不容易出安全事故,先接;
- 操作类(创建工单、发通知、改配置):这类工具需要审批和权限控制,我建议第一版先做"只读 + 创建",修改删除类操作一律不开放;
- 知识类(内部 Wiki、历史 Case、规范文档):这个本质是 RAG 检索工具,适合做 Agent 的长期记忆。
数据库查询工具是最典型的例子。我封装了一个只读 SQL 工具,核心逻辑是:
@tool def run_readonly_sql(query: str) -> str: """在只读副本上执行 SQL 查询,禁止 INSERT/UPDATE/DELETE/DDL""" allowed_keywords = {"select", "with", "show"} if not query.strip().lower().startswith(tuple(allowed_keywords)): return "错误:只允许 SELECT 类查询" # 走只读账号,且统一超时 5 秒 ...这背后还有一个考量:工具函数本身是 Agent 的边界,不能只靠提示词限制。隔离内网里系统之间的信任关系通常很强,一旦 Agent 能调用某个接口,就必须从代码层面限制它只能做什么。
5.2 RAG 数据的离线更新机制
内网知识库不是静态的。工单系统每天新增数据,告警案例每周都有新的,文档库也会有版本更新。我的方案是做一个离线定时任务(用系统的 cron 或内部的调度平台),每天凌晨把增量文档重新切块、生成向量、写入 Milvus。整个流程不依赖外网,但要注意增量更新时的"重复写入"问题,最好用文档的哈希值做去重。
5.3 权限、审计与敏感信息控制
隔离内网通常意味着更高的安全要求。我给 Agent 服务加了三道保险:
- 权限:用户登录后,Agent 服务通过网关把用户身份传递给下游系统,工具接口按用户权限过滤数据,不让 Agent"越权读取";
- 审计:所有 Agent 与工具的交互日志全部落盘,包括模型输入输出、工具调用参数、耗时、结果摘要,方便回溯;
- 脱敏:LLM 的输入输出层做关键词脱敏,比如手机号、身份证号在进入模型前先打码,避免模型记忆和日志泄露。
这些都是公网个人项目里不会考虑的事,但恰恰是内网项目能不能过评审的关键。
6. 抗并发与稳定性:AI Agent 上线前必须补的课
6.1 推理服务的并发瓶颈分析
"AI Agent 怎么扛并发"是很多人问我的第一个问题。先看结论:瓶颈通常不在模型生成,而在 Agent 的多轮工具调用和 IO 等待。一次复杂的 Agent 任务,可能涉及两三次 LLM 推理、四五次工具请求,每次工具请求还要等下游接口响应。如果下游接口是 500ms,那么一次用户请求在 Agent 层就可能要等 3-5 秒,即使模型推理只需要 1 秒。
所以并发设计必须分两层考虑:推理层和应用层。
推理层,vLLM 的 continuous batching 已经能扛住几十路的并发生成。A800 上跑 72B 量化模型,单卡吞吐大概在 1500-3000 tokens/s(取决于量化方式和输入长度),对大多数内网团队几十到上百人的规模是足够的。真正要调的是 --max-num-seqs,它控制同时处理的请求路数,设太小会导致排队,设太大会让单请求变慢。
6.2 应用层异步改造与请求排队
应用层我用的 FastAPI + LangGraph,主要做了三件事:
- 接口全部改成 async:FastAPI 里不要用同步 def 写接口,否则所有请求都在一个线程池里排队,并发一上来立刻卡死;
- HTTP 客户端用异步复用:httpx.AsyncClient 共享连接池,避免每次请求新建连接;
- 长任务走后台队列:Agent 一次任务可能要十几秒,不适合让用户一直停在那里等。我引入了一个任务队列(Celery 或 RQ 都可以),用户提交请求后立刻拿到 task_id,前端轮询或通过 WebSocket 拿结果。
这里给一个简单的架构分层建议:用户请求进来,API 网关先做认证与限流,然后把任务丢进队列;Worker 进程负责跑 LangGraph 流程,LLM 调用走内网推理服务,工具调用走内网各系统。这样隔离内网的部署架构和公网大型项目几乎一致,只是所有外部依赖都换成了内部组件。
6.3 真实压测数据与调参记录
我记录过一组压测数据(4 卡 A800,72B AWQ 量化),供参考:
| 场景 | 并发数 | RPS | P95 响应时间 | 显存峰值 |
|---|---|---|---|---|
| 纯问答(单轮) | 20 | 2.8 | 6.2s | 约 55GB |
| 复杂 Agent 任务(3 次工具调用) | 10 | 0.4 | 12.8s | 约 55GB |
| 复杂 Agent 任务(10 次工具调用) | 10 | 0.2 | 23.1s | 约 56GB |
数据说明了两件事:第一,Agent 任务耗时和工具调用次数几乎线性相关,所以工具设计要尽量"少而精",能一次查完不要拆成三次;第二,并发瓶颈在应用层,加 Worker 进程比换更大的卡更有效。这个结论对隔离内网特别重要,因为你很难临时租卡扩容,能做的就是在架构上留余量。
7. 踩坑实录:隔离环境下最折磨人的几个问题
7.1 OpenAI 客户端的 base_url 被环境变量覆盖
这是我在内网遇到最隐蔽的问题之一。代码里明明写了 base_url,但请求日志显示模型名称还是打到了公网地址。查了很久才发现,是某台机器的环境变量里残留了 OPENAI_BASE_URL,把它指到了外部某个 API 网关。
这类问题在内网尤其危险。因为逻辑隔离的内网往往有统一的 HTTP 代理配置,环境变量很可能已经被设置成"所有请求走代理"。解决方法是:在应用启动脚本里显式 export OPENAI_BASE_URL="",或者直接在代码里 os.environ.pop("OPENAI_BASE_URL"),然后用 OpenAI( base_url=... ) 时再确认。
提示:排查这类问题时,别只看代码。先
env | grep -i openai,再python -c "from openai import OpenAI; print(OpenAI.__module__)",往往能省下几小时。
7.2 模型量化文件与 vLLM 版本不匹配
AWQ 量化模型对 vLLM 的算子版本非常敏感。我第一次部署时,开发机上用的 vLLM 0.6.x 和模型配得好好的,内网机器因为离线安装的是另一个版本的 vLLM,结果启动时直接报算子不支持的错误。这个坑特别让人崩溃,因为报错信息是英文、看起来像 CUDA 问题,实际却是 vLLM 版本不匹配。
解决方案很粗暴:在有网环境下把 vLLM 的 whl 包和模型一起锁版本,做成一套"经过验证的组合",然后整体搬运进内网。不要分别零散地搬,否则一旦版本错位,排查成本极高。
7.3 文本切块时 NLTK 数据缺失
做 RAG 时,langchain 的文本切分器默认会用 NLTK 下载 punkt 等数据文件。在公网,它会自动下载;在内网,它会直接卡住或者报错。这个问题几乎人人会撞到。
解决办法是在有网机器上提前手动下载 NLTK 数据:
python -c "import nltk; nltk.download('punkt'); nltk.download('averaged_perceptron_tagger')"然后把下载好的 nltk_data 目录整个拷贝到内网机器的指定路径,并设置环境变量:
export NLTK_DATA=/opt/nltk_data7.4 其他零碎但致命的坑
- Docker 镜像离线搬运:内网通常不能用 Docker Hub,需要 else 环境用
docker save导出、内网docker load导入。注意检查镜像里的架构标签,ARM 镜像在 x86 机器上 load 能成功,但一跑就报 exec format error; - 临时目录容量不足:模型加载偶尔需要解压或重写临时文件,如果 /tmp 只有几 GB,加载几十 GB 的模型就可能失败。我当时直接设置了 TMPDIR=/opt/tmp 才解决;
- 自签名证书:内网系统基本都是自签名 HTTPS 证书,直接 requests 调用会报 SSL 错误。要么把内网 CA 加到受信任列表,要么在调用内部工具时显式 verify=False(内网环境风险可控,但要有心理准备,安全评审可能会问)。
这些问题每一项单独看都很小,但连在一起能把人磨到怀疑人生。我的教训是:隔离内网的项目,一定要在项目启动时就建一份"离线物料清单",把依赖、模型、NLTK 数据、Docker 镜像、系统证书全部列进去,每用到一个外部资源就立即补充,否则后期每走一步都要重新折返。
最后再分享一个个人体会:在隔离内网里做 AI Agent,心法就是把"公网世界中随手可得的资源"当作"需要提前采购的物资"来处理,搬、装、验三步走,每一步都留好校验和回滚余地。只要能突破这个思维,隔离内网反而是一个异常专注的环境——没有外部 API 的变化无常,没有模型版本偷偷升级的惊吓,所有变量都是可控的。这套路径走通之后,你会发现自己比那些只会在公网调接口的人,多了一层真正能落地的工程能力。