ARTICLE DETAIL

资讯详情

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

隔离内网部署AI Agent:vLLM离线推理与LangGraph编排实践

隔离内网部署AI Agent:vLLM离线推理与LangGraph编排实践 接到这个需求时我原本以为只是把平时在公网玩的那套 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 兼容接口显存占用高、依赖较重多用户、高并发的正式生产TGIHuggingFace 官方维护、接口标准部署灵活性稍弱已有 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 兼容接口模型就位后我用下面的命令启动 vLLMpython -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_urlhttp://inference-server:8000/v1, api_keynot-needed-for-local, ) # 在 LangGraph 里用一个 callable 封装 def call_llm(messages): resp llm_client.chat.completions.create( modelqwen72b, messagesmessages, temperature0.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}, timeout10, verifyFalse, # 内网自签名证书问题见踩坑章节 ) return response.json()工具的关键是给足参数描述。Agent 模型要靠函数的 docstring 来理解什么参数该填什么描述写得模糊模型就会乱猜。跟完整的 FastAPI 接口一样工具的输入输出最好也用 Pydantic 模型约束让模型侧看到明确的 JSON Schema比全靠 docstring 可靠得多。4.3 RAG 知识库离线嵌入模型加向量库的连接Agent 光会调工具还不够很多问题需要先检索内部文档再回答。我在内网搭了一套bge-m3 嵌入模型 Milvus 向量库的方案。这里两个关键点嵌入模型同样要走 vLLM 的 /v1/embeddings 接口这样不需要额外部署Agent 调用时只需一次性把文档切块生成向量再存库文本切块时中文文档我建议用按标题结构切而不是简单按固定长度切。内网文档往往有明确的章-节-条结构按结构切出来的检索准确率比固定 chunk 好很多。入库和检索的链路可以理解成文档先切块再调用 bge-m3 生成向量存入 MilvusAgent 收到用户问题后同样调用嵌入模型生成向量然后去 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主要做了三件事接口全部改成 asyncFastAPI 里不要用同步 def 写接口否则所有请求都在一个线程池里排队并发一上来立刻卡死HTTP 客户端用异步复用httpx.AsyncClient 共享连接池避免每次请求新建连接长任务走后台队列Agent 一次任务可能要十几秒不适合让用户一直停在那里等。我引入了一个任务队列Celery 或 RQ 都可以用户提交请求后立刻拿到 task_id前端轮询或通过 WebSocket 拿结果。这里给一个简单的架构分层建议用户请求进来API 网关先做认证与限流然后把任务丢进队列Worker 进程负责跑 LangGraph 流程LLM 调用走内网推理服务工具调用走内网各系统。这样隔离内网的部署架构和公网大型项目几乎一致只是所有外部依赖都换成了内部组件。6.3 真实压测数据与调参记录我记录过一组压测数据4 卡 A80072B AWQ 量化供参考场景并发数RPSP95 响应时间显存峰值纯问答单轮202.86.2s约 55GB复杂 Agent 任务3 次工具调用100.412.8s约 55GB复杂 Agent 任务10 次工具调用100.223.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 加到受信任列表要么在调用内部工具时显式 verifyFalse内网环境风险可控但要有心理准备安全评审可能会问。这些问题每一项单独看都很小但连在一起能把人磨到怀疑人生。我的教训是隔离内网的项目一定要在项目启动时就建一份离线物料清单把依赖、模型、NLTK 数据、Docker 镜像、系统证书全部列进去每用到一个外部资源就立即补充否则后期每走一步都要重新折返。最后再分享一个个人体会在隔离内网里做 AI Agent心法就是把公网世界中随手可得的资源当作需要提前采购的物资来处理搬、装、验三步走每一步都留好校验和回滚余地。只要能突破这个思维隔离内网反而是一个异常专注的环境——没有外部 API 的变化无常没有模型版本偷偷升级的惊吓所有变量都是可控的。这套路径走通之后你会发现自己比那些只会在公网调接口的人多了一层真正能落地的工程能力。
返回列表