ARTICLE DETAIL

资讯详情

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

隔离内网下AI Agent工程实战:离线部署与调优全复盘

隔离内网下AI Agent工程实战:离线部署与调优全复盘 上个月我在一个项目现场驻场机器是现成的显卡也够但网络环境是物理隔离的——外网完全不通所有代码、模型、依赖都要靠移动介质一点一点往里搬。这时候要在隔离内网里落地一个 AI Agent 系统和平时在开发环境里调云端大模型API完全是两码事。团队里好几个同学第一反应是先装个 Ollama 跑起来再说但真正做下来才发现模型能跑只是第一步Agent 编排、依赖离线化、知识库构建、并发压测每一步都有坑等着你。这篇文章就是基于这次实际项目做的复盘。我会把隔离内网下 AI Agent 工程实战这条线完整捋一遍从整体架构选型到模型层、依赖层、Agent 编排层、知识库层再到并发与生产化最后是踩过的坑复盘。适合正在做企业私有化部署、信创环境适配、或者任何不能上网但必须上AI场景的工程同学参考。1. 隔离内网与 AI Agent 的碰撞面先想清楚难在哪1.1 离线环境的真实面貌很多同学一听到隔离内网默认就是没网而已。但实际上隔离内网的环境远比想象中复杂通常包含下面几个特征物理隔离或逻辑隔离有的网络和公网之间完全断连数据只能通过U盘、光盘、网闸摆渡等方式单向导入有的则是应用层受限比如白名单域名、代理过滤但基本等价于不可用。软件白名单管控不能随便装软件很多机器连 gcc、make 都没有装个带编译的包会让人崩溃。系统版本老旧内网机器的操作系统版本、glibc 版本、内核版本往往落后主流好几年Python 3.11 在某些老系统上根本编译不过去。没有域名解析保障内网 DNS 解析规则经常很迷自建 registry、私有 PyPI 的服务名解析就可能出问题。这些东西单独看都不致命但叠加在一起就会让在公网上三分钟搞定的事变成在内网里折腾三天。1.2 四个维度的核心矛盾我在项目里梳理了一下隔离内网部署 AI Agent 的核心矛盾集中在四个方面模型参数进不来。一个 7B 的模型量化后也要 4~6GB13B 甚至 14B 模型轻轻松松 10GB 以上更大的模型动辄上百 GB。怎么安全可靠地把模型文件拷进内网本身就是个工程问题。依赖和镜像无法在线拉取。Python 包、CUDA 组件、容器镜像平时一条命令搞定离线后必须提前准备 wheelhouse 和镜像包。而且依赖是有传递依赖的漏掉一个就得重新摆渡一次代价非常高。Agent 的外部能力全部失效。AI Agent 最典型的能力是调用工具、搜索网页、访问第三方 API。隔离内网里这些外部能力都不存在Agent 只能依赖内网已有的系统接口和知识库工具边界被大大压缩。联调排错的路径变长。公网环境下出问题可以随时装个新的调试工具、查在线文档、拉最新代码内网里所有排错都依赖日志和已有知识试错成本很高。这四个维度基本决定了隔离内网里做 Agent不能照着公网方案平移而是要在架构设计阶段就把离线约束考虑进去。1.3 技术栈选型先给结论基于上面的分析我们最终敲定的技术栈是这样一套组合层级选型理由应用服务层FastAPI异步支持好与 Agent 编排层无缝衔接OpenAPI 文档方便内网对接Agent 编排层LangGraph状态图模型适合业务流程控制节点可插拔便于人工审核介入推理服务层vLLM生产/ Ollama试点都提供 OpenAI 兼容 APIAgent 框架对接成本低且支持离线部署模型层Qwen 系列 bge-m3 embedding中文效果好社区资源多量化版本丰富向量库Milvus生产/ Chroma试点都支持离线部署Milvus 可水平扩展依赖管理pip wheelhouse Docker registry 镜像离线安装可复现依赖锁定清晰这里有个很重要的原则所有组件必须优先选自带 OpenAI 兼容 API和支持离线一键启动的。比如推理服务选 vLLM 而非沿用原本的 transformers pipeline就是因为 vLLM 自带/v1/chat/completions接口LangChain、LangGraph 里很多封装都能直接复用。2. 模型层落地从模型文件到可用的离线推理服务2.1 模型文件的离线导入与校验模型文件的导入是整个项目里最不能出错的一步。因为一旦模型文件在传输过程中损坏torch.load会在莫名其妙的地方报 shape mismatch排查起来非常痛苦。我们的做法是在可联网的机器上先建一个模型暂存区按模型名和版本建目录逐个下载后固定三样东西模型文件本身的 SHA256 校验值配置文件 config.json 中的关键参数摘要HuggingFace 上下载时记录的原始文件列表。下载工具我建议用huggingface_hub的 CLI不要用浏览器手动点因为它会带上每个文件的 sha256 校验信息方便后续比对huggingface-cli download Qwen/Qwen2.5-14B-Instruct-AWQ \ --local-dir /data/models/Qwen2.5-14B-Instruct-AWQ \ --resume-download下载完成后再跑一遍本地校验cd /data/models/Qwen2.5-14B-Instruct-AWQ \ find . -type f -exec sha256sum {} \; /data/models/sha256.txt拷贝进内网后第一件事就是拿这份 sha256.txt 做比对确认所有文件完整。这一步一定不能省。我们有一次就是因为 U 盘文件系统问题导致一个分片损坏少了 200MB加载模型时直接报pytorch_model.bin.index.json里的权重缺失排查了大半天才定位到是文件拷贝不完整。另外模型量化版本的选择也要提前想好。如果显卡是 24GB 显存这个级别14B 模型用 AWQ 或 GPTQ 4bit 量化是合理选择如果是 48GB 甚至更高可以上 BF16 全精度版本。千万不要在内网机器上去跑量化转换流程——autoawq这类工具的依赖非常重离线环境下装起来很被动。2.2 Ollama还是vLLM按并发与显存选型隔离内网项目里经常出现一个争论到底用 Ollama 还是 vLLM我的建议是看阶段、看并发试点阶段、单用户调试、资源有限用 Ollama。它把模型服务封装得非常好一条ollama serve就能起服务内置 OpenAI 兼容接口离线安装也简单适合先跑通流程。生产阶段、多用户并发、对时延有要求用 vLLM。它的 Continuous Batching、PagedAttention 对并发吞吐的提升非常明显同样一张卡vLLM 能撑住的并发数是 transformers pipeline 的数倍。我在项目里见过一个很典型的例子同样的 14B AWQ 模型在 24GB 显卡上用 transformers 直接起服务8 个并发就把显存打满、响应开始排队换成 vLLM 后max-num-seqs调到 32显存占用反而更平滑首 token 时延也能稳定在 1.5 秒以内。如果是在隔离内网里做生产系统我会直接推荐 vLLM不要犹豫。2.3 CUDA、驱动与推理依赖的离线安装这一节是隔离内网项目里最容易翻车的地方没有之一。首先是显卡驱动。很多内网机器虽然装了驱动但版本很老而 vLLM 对 CUDA 版本有明确要求。我们遇到过一台机器nvidia-smi显示驱动版本 450而 vLLM 要求 CUDA 11.8 以上导致 vLLM 在初始化时直接 complaining about CUDA capability。后来是找到了对应老驱动的 CUDA 11.8 配套版本才跑通。其次是vLLM 的 pip 依赖。vLLM 的依赖列表非常长包括torch、transformers、tokenizers、safetensors等一堆包而且不同版本之间还有严格兼容关系。离线安装时不能只下载 vLLM 本身要把整棵依赖树都准备好。我强烈建议在一台和机房同构的联网机器上用同一套 Python 版本先做一次安装验证把所有 wheel 包收集好再搬到内网。pip download vllm0.6.6 \ --dest /data/wheelhouse \ --python-version 310 \ --only-binary:all:这里要注意--only-binary:all:强制只要 wheel 包不要源码包。否则内网机器一旦没有编译工具链源码安装会当场失败。2.4 推理服务的稳定性和扩展性检查离线部署完成后不能光验证能跑通一个对话就宣布成功。我建议做一个更完整的验证清单连续调用 100 次/v1/chat/completions确认没有内存泄漏和显存持续增长测试不同并发下的首 token 时延和生成吞吐画出资源基线验证max-model-len、gpu-memory-utilization等参数确认显存分配合理确认模型服务支持优雅重启因为内网环境里没有那么多自动化运维工具重启要尽量无痛。提示vLLM 启动时建议固定--gpu-memory-utilization 0.9以下留出余量给 KV Cache 的抖动否则在高并发时容易出现 OOM 导致的进程崩溃。3. 依赖与运行环境离线化搭一套不联网也能用的交付物3.1 wheelhousePython依赖的离线安装方案隔离内网里最基础也最容易被低估的就是 Python 依赖管理。项目刚开始时有人直接在联网机器上pip freeze导出一个 requirements.txt然后到内网pip install -r requirements.txt结果就是各种Could not find a version that satisfies the requirement。正确做法是在联网的构建机上用和最终运行环境一致的 Python 版本把所有依赖下载成本地 wheelhouse 目录pip install pip-tools pip-compile requirements.in -o requirements.lock pip download -r requirements.lock \ --dest /data/wheelhouse \ --only-binary:all:requirements.lock把所有传递依赖的精确版本都钉死了比手工维护的 requirements.txt 可靠得多。到内网机器上安装就变成了pip install --no-index --find-links/data/wheelhouse -r requirements.lock这一步有个经验wheelhouse 目录不要压缩直接以目录形式拷贝进内网。因为内网机器上如果我们把 wheelhouse 打成一个 tar.gz解压过程中一旦出现乱码或文件损坏后面 pip 安装会非常隐晦地报错。目录拷贝虽然慢但稳。3.2 Docker镜像离线迁移与私有仓库搭建如果内网环境允许使用 Docker那镜像的离线迁移能省掉大量环境兼容问题。做法是在外网构建机上把整个运行环境的镜像打好docker save成 tar 包拷贝进内网内网docker load导入。docker save agent-base:v1.0 | gzip agent-base-v1.0.tar.gz # 内网机器上 docker load -i agent-base-v1.0.tar.gz但如果服务多了tar 包管理很快就变得混乱。更稳妥的方案是在内网搭一个私有 Docker Registry把镜像都 push 进去各台机器从内网 registry 拉取。注意内网机器的/etc/docker/daemon.json要配置好insecure-registries否则 registry 的 HTTP 协议会被 Docker 默认拒绝。3.3 一致性校验让离线交付可复现隔离内网里运维和开发往往是脱节的开发在联网环境运维在内网两边对环境一致的判断经常靠感觉。我后来养成了一个习惯每次交付都附带一份环境一致性清单包含 sha256 校验和、版本号、构建时间三要素。这样即使过了两个月内网机器需要扩节点或者重装也能照着这份清单复现而不是靠当时就是在这台机器上装的这种模糊记忆。4. Agent编排层LangGraph与无Function Calling的工具调用4.1 为什么用LangGraph而不是链式调用AI Agent这个词大家已经听了很多但真正在做企业级落地时我一直坚持用 LangGraph 而不是那种简单的先调大模型再调工具再调大模型的链式代码。原因很简单隔离内网里的 Agent 必须可控制、可审核、可回滚。LangGraph 的核心模型是有状态图每个节点是一个具体的处理单元节点之间的跳转由条件边决定。这意味着我们可以很直观地把业务流程画成状态机并且在任意节点插入人工审核步骤。举个例子我们有个需求是Agent 根据用户请求查询内网订单系统并生成报表。用链式调用写出来工具调用和结果处理是揉在一起的用 LangGraph 写出来查询订单是一个节点生成报表是另一个节点中间可以加一个human_approve节点让有权限的人确认之后再往下走。这对于内网系统的合规要求来说非常重要。4.2 没有Function Calling也能调工具的三种做法这是项目里被问得最多的问题。很多人以为 AI Agent 必须依赖模型的 Function Calling 能力才能调工具但在隔离内网里我们部署的本地模型未必支持 Function Calling或者支持得不够稳定。这时候有三条路可以走方案一结构化输出 正则解析在 prompt 里明确告诉模型如果要调用工具严格输出 JSON 格式包含 action 和 action_input 两个字段。然后我们在代码里用正则把 JSON 块提取出来再分发到对应的工具函数。这是最朴素也最稳的办法。方案二JSON Schema 控制输出让本地模型按照指定的 JSON Schema 输出完整结果。LLM 原生 API 里的response_format参数如果不可用我们可以通过在 prompt 末尾附加一份 You must output JSON matching this schema: ... 的约束再用json.loads做解析和校验。实测下来Qwen 系列的模型对 JSON 约束的遵循程度还是不错的。方案三ReAct 风格的话术内嵌不把工具调用结构化而是让模型在回复文本里直接描述我需要调用 XXX 工具获取 YYY 数据。代码层做关键词匹配命中后自动执行工具再把结果追加到上下文里让模型继续生成。这个方法最轻量但稳定性也最差适合工具数量少、触发词明确的场景。我在实际项目里用的是方案一为主辅以方案二做输出校验。比如我们的工具调用解析函数大致长这样TOOL_ACTION_RE re.compile( r\{[^{}]*action[^{}]*action_input[^{}]*\}, re.DOTALL ) def parse_tool_call(text: str) - ToolCall | None: for match in TOOL_ACTION_RE.findall(text): try: data json.loads(match) if action in data and action_input in data: return ToolCall(actiondata[action], action_inputdata[action_input]) except json.JSONDecodeError: continue return None注意这里的正则不要写得太严否则模型输出的 JSON 里多一个字段就解析失败了。解析失败时的兜底逻辑也很重要——我们会在连续两次解析失败后把上一次的工具调用结果拼进 prompt强制模型继续生成而不是无限重试。4.3 用FastAPI把Agent包成服务Agent 编排层最终要暴露成 HTTP 接口给前端或业务系统调用。我用 FastAPI 封装主要是看中它的异步能力和 OpenAPI 文档自动生成能力。一个比较典型的接口是流式对话接口app.post(/v1/agent/chat) async def agent_chat(request: AgentChatRequest): async def event_stream(): async for chunk in graph.astream_events( {messages: request.messages, user_id: request.user_id}, versionv2 ): if chunk[event] on_chain_end: yield fdata: {json.dumps(chunk[data])}\n\n return StreamingResponse(event_stream(), media_typetext/event-stream)在隔离内网场景里FastAPI 服务还要特别注意一件事健康检查接口。内网负载均衡器、监控系统都依赖健康检查来探活没有健康检查的服务一上线就会被流量打崩。我们后来强制要求所有服务都提供/health接口里面要顺带检查推理服务、向量库这些下游依赖的状态。5. 知识库离线构建从Embedding到向量检索调优5.1 中文Embedding模型的离线部署Agent 要回答业务问题光靠模型自身参数知识是不够的必须挂知识库。知识库的第一步是文本向量化这里中文场景我推荐bge-m3或bge-large-zh-v1.5。Embedding 模型虽然不大但一定不要和推理大模型放在同一个 GPU 上随便跑。原因是 Embedding 模型和生成模型对显存的占用模式完全不同混跑容易出现显存碎片。我们的做法是把 embedding 模型单独部署在一个小容器里用/embedding接口对外提供服务加载一次后常驻内存。离线部署 embedding 模型也一样要走 wheelhouse 流程但它的依赖问题比 vLLM 少很多主要是torch、transformers和sentence-transformers三件套。需要注意的是sentence-transformers版本和transformers版本存在兼容关系锁定版本时不要随意升级。5.2 文档解析与切分的实操细节知识库的质量往往取决于文档预处理而不是模型。这是我在项目里最大的感受之一。文档解析阶段PDF 和 Word 是最常见的两种格式。PDF 解析我推荐pymupdf它对中文支持好、速度快Word 用python-docx解析。两个库都要提前准备离线 wheel注意pymupdf的 wheel 在不同架构下名字差异很大。切分阶段最重要的参数有三个chunk_size、chunk_overlap、separators。中文文本和英文不一样不能简单按空格切。我们实践中效果比较好的规则是优先按章节标题切分markdown 标题、PDF 里的大段标题再按段落的双换行符切最后才用滑动窗口兜底窗口大小根据模型最大 token 长度来定比如 512 或 768overlap 设为 80~100。这里有一句经验之谈切分不要贪大。很多人为了减少向量条数把 chunk 切到 1000 多个 token结果召回时噪音极大检索回来的一整段里只有一半是有效信息。宁可用 300~500 token 的小块换取更精准的召回。5.3 向量库选型单机、分布式与已有设施隔离内网里向量库的选择主要看规模和数据敏感度。我的建议是场景推荐理由试点、数据量小于百万级Chroma 或 Milvus Lite部署简单单文件存储离线安装快正式生产、数据量百万级以上Milvus 集群支持分布式、索引类型丰富、检索性能稳内网已有 Elasticsearch 设施Elasticsearch vector 插件复用现有运维体系减少新组件需要注意Milvus 集群依赖 etcd、MinIO 等组件离线部署的依赖链条比较长。如果内网运维能力一般建议先上 Milvus 单机模式后面再平滑迁移。5.4 检索质量调优召回、阈值与Rerank向量检索不是相似度大于 0.8 就返回这么简单。在我们项目里检索质量调优经历了三个阶段第一阶段只看 top_k 相似度结果经常答非所问 第二阶段把检索结果拼进 prompt 时加上来源标题和页码模型回答质量立刻提升 第三阶段引入了 Rerank 模型做二次精排把向量检索召回的前 20 条先粗筛再用 rerank 模型精排前 5 条。Rerank 模型同样需要离线部署。我们用的是一个小的中文 rerank 模型挂在一个独立的推理服务里每次查询多花几十毫秒但回答准确率提升非常明显。对于隔离内网的 Agent 系统这一步投入产出比极高。6. 并发与生产化单机Demo到内网服务的距离6.1 服务拆分的资源规划很多同学在隔离内网项目里喜欢一台机器全搞定——又是推理服务又是 Agent API又是向量库还挂着前端。Demo 阶段可以到了生产环境必须拆。我的建议是至少拆成三层推理服务层独占 GPU 机器部署 vLLMAgent 应用层纯 CPU 机器跑 FastAPI LangGraph负责编排和工具调用数据层向量库、业务数据库、消息队列各自独立部署。这样拆的好处有二其一推理服务的显存抖动不会拖垮整个 Agent 服务其二Agent 应用层可以独立水平扩展当并发上来了加两台 CPU 机器就能扛。6.2 AI Agent怎么扛并发实测参数与调优AI Agent 怎么扛并发这个问题的答案绝不是一个参数能解决的但我可以给出一套实测有效的组合拳。第一层是推理服务的并发参数。vLLM 下这几个参数非常关键参数建议值说明--max-num-seqs16~32控制同时处理的序列数太高会占满显存--gpu-memory-utilization0.85~0.9留出余量防止 KV Cache 波动导致 OOM--max-model-len8192 或 16384过长会占用大量显存按实际场景裁剪--enforce-eager按需显存紧张时关掉 CUDA Graph省显存但略降性能第二层是应用层的并发控制。FastAPI 本身是异步的但 Agent 编排里的内部环节调用工具、查询向量库往往是同步阻塞的所以必须在应用层做信号量限流否则推理服务还没被打满应用层的线程池先爆了。from anyio import Semaphore agent_semaphore Semaphore(20, max_waiters50) app.post(/v1/agent/chat) async def agent_chat(request: AgentChatRequest): async with agent_semaphore: # 进入 Agent 编排 ...第三层是超时与重试机制。大模型生成时间本来就比普通接口长Agent 一轮对话可能包含多个模型调用和工具调用总耗时常超过 10 秒。这里必须设置合理的超时时间并且对下游系统设计重试退避策略否则一个循环调用就能把整个 Agent 拖死。我们实际压测过一组数据在单张 48GB 显卡上跑 14B AWQ 模型max-num-seqs32时20 个并发的 Agent 对话请求端到端平均耗时约 8 秒推理服务 GPU 利用率约 85%没有出现超时或 502。这个结果说明在内网环境下24GB~48GB 显卡 vLLM 应用层限流足以支撑几十个用户的日常使用。6.3 内网环境的可观测性与审计隔离内网往往没有成熟的 APM 监控体系但这不代表可以不做可观测性。我们在项目里落地了一套轻量方案所有 Agent 请求统一打印结构化日志包含 request_id、user_id、模型调用次数、工具调用清单所有工具调用记录操作留痕写入审计表模型服务定期健康检查异常自动告警到企业微信机器人。审计这一点在隔离内网里尤其重要因为 Agent 能访问的是内网真实业务系统每一次工具调用都要对得上谁在什么时间基于什么原因发起了这次查询。7. 踩坑实录隔离内网部署中那些文档不会告诉你的问题7.1 五个印象最深的坑坑一glibc 版本过低Python 3.11 直接编译失败。内网机器系统是 CentOS 7glibc 2.17很多新版 wheel 包在 manylinux_2_17 以上才提供导致 pip 找不到可用包。后来我们统一改用 Python 3.10并手动收集 manylinux2014 兼容的 wheel才把问题绕过去。教训是提前确认内网机器的操作系统版本和 glibc 版本再决定 Python 版本。坑二HuggingFace 的缓存目录导致模型文件重复占用磁盘。用huggingface-cli download下载时模型文件会先落在~/.cache/huggingface/hub再体现到--local-dir。如果没清理缓存一个 14B 模型可能占用双倍磁盘空间。内网机器的磁盘往往不宽裕拷贝前务必确认缓存目录的情况。坑三两个模型服务共用一个 Python 环境torch 版本冲突。在试点阶段推理服务和 embedding 服务装在同一台机器的同一个 conda 环境里结果一个需要 torch 2.1另一个需要 torch 2.3升级后另一个直接起不来。教训是隔离内网里一定要用虚拟环境隔离好每个服务各管各的依赖。坑四内网 Docker pull 私有镜像时因为 registry 地址解析失败反复重试。排查下来是内网机器的/etc/hosts没有配置 registry 的映射。虽然是个小问题但在离线环境下很容易被忽略而且报错信息看起来像网络不通实际是域名解析问题。坑五离线安装sentence-transformers时编译tokenizers需要 rust 工具链。当时我们准备不充分以为所有包都有 wheel结果tokenizers在某个 Python 版本下没有对应 wheel被迫现场找 rust 工具链非常狼狈。后来学乖了所有包在构建机上强制--only-binary:all:验证不能只用pip download成功就完事。7.2 交付物清单与部署节奏基于这次项目的沉淀我把隔离内网 AI Agent 的交付物整理成了一份标准清单供大家参考wheelhouse/全部 Python 依赖的 wheel 包images/Docker 镜像 tar 包如需要models/大模型文件、embedding 模型、rerank 模型及 sha256 校验文件requirements.lock精确到版本的依赖锁定文件deploy/部署脚本、健康检查脚本、服务启动脚本docs/部署手册、版本兼容矩阵、常见问题处理手册。部署节奏上我的经验是先小步跑通再逐步放大。第一次交付只要求达到模型能对话 Agent 能调用两个内网工具 知识库能检索第二周再补并发压测、监控告警和审计。千万不要在隔离内网里直接照搬公网那样搭建一个庞大复杂的系统因为排错成本太高一次失败的尝试可能就要等下一次拷文件才能继续。几轮项目做下来我最大的体会是隔离内网部署 AI Agent本质上拼的不是大模型技术而是工程化流程的严谨程度。只要提前把依赖树、校验信息、部署脚本都沉淀成可复用的物料内网和外网的差距其实没有想象中那么大。如果你们团队正准备在隔离环境里上 Agent我建议先别急着跑代码花一两天把上面这份清单过一遍能省下后面好几周的填坑时间。
返回列表