☰
隔离内网中部署AI Agent的完整实战指南
2026/10/8 3:37:06 网站建设 项目流程

1. 为什么要在隔离内网里跑 AI Agent

先说结论:这件事最痛的不是“AI Agent 怎么搭”,而是“在没有外网的环境里,所有现成方案都默认你能访问外网”。我这次做的是企业内部的智能工单处理助手,部署在一台完全不能出网的服务器上,要求它读私有文档、调用内部系统接口、自动生成处理建议。传统做法当然是接云端大模型API,但数据合规不允许。于是只能硬着头皮把整套 AI Agent 体系全部搬到内网,从模型权重到 Python 依赖、从向量库到 Agent 运行时,全部离线化。

隔离内网是很多行业的默认前提——政企、金融、医疗、制造业的生产网基本都这样。它带来的限制不只是“没有外网”,还包括:不能直接 pip install、不能拉取 GitHub 代码、不能下载模型文件、甚至有些环境连镜像站都访问不了。与此同时,业务方又希望 Agent 能正常工作:能理解自然语言、能检索知识库、能调用内部 REST API、能给出稳定的输出。说白了,你要在一个“断网”的环境里拼出一个具有自主决策能力的数字员工。

这篇文章适合两类人:一类是正在被隔离内网折磨的工程师,想知道依赖、模型、框架这些坎怎么一步步迈过去;另一类是打算做私有化部署的技术决策者,想提前知道成本结构和风险点在哪。我会把整个实战过程拆开讲,包含选型逻辑、离线化步骤、Agent 编排细节,以及我踩过的几个坑。所有内容不涉及任何外网访问手段,只谈“在封闭环境内把事情做成”的纯工程方法。

2. 环境评估与方案选型

2.1 先摸清家底:网络边界与硬件约束

动手之前不要急着装软件,先花半天时间把环境情况摸清楚。我这次的目标环境有三条硬性限制:

  • 服务器位于独立网段,防火墙只开了 TCP 22 和若干内部业务端口;
  • 没有 DNS 解析外网域名能力,即便物理链路通,也无法访问任何公网资源;
  • 硬件资源有限:单台物理机,32 核 CPU,内存 128 GB,显卡是一张 24 GB 显存的 GPU(没有多卡),另外还有一块 4 TB 的普通 SAS 硬盘。

如果你是做技术方案评审,这三条直接决定了后面的所有选择。硬件不够就降模型规模,网络不通就走离线介质,端口受限就只在局域网内做服务暴露。我建议你第一步先用lscpu、free -h、nvidia-smi和df -h把这些信息记录下来,形成一份基线表,后面所有容量规划都从这里出发。

还有一个容易忽略的点:确认是否有堡垒机或运维代理可以用于文件导入。很多隔离内网虽然业务服务器不能访问外网,但运维区有一台专用的“摆渡机”,可以通过管理员手动拷贝或通过审批流程把文件带进去。这个东西存在与否,决定了你后面获取模型权重、依赖包的方式是“U盘拷入”还是“刻盘导入”。我当时是借助运维审批把整体离线包以 ISO 形式挂载进服务器,效率比一个个文件传高很多。

2.2 Agent 框架选型:在线依赖越少越好

市面上的 Agent 框架五花八门,但现在主流实现多少都带着“云端优先”的基因。比如一些框架天然依赖云端模型网关、在线 Embedding API、外部工具商店,这在隔离内网里都是致命伤。选型的时候我给自己定了三条硬标准:

  1. 框架核心代码必须能完全离线运行,不能硬编码任何外部 API 地址;
  2. 模型接入层要支持本地 OpenAI 兼容接口——这样可以用本地模型服务替代云端;
  3. 工具/插件机制要简单,最好能通过自定义函数或 HTTP 调用方式扩展,方便对接内部系统。

最终我选择的是基于 Rust 生态的 Agent 运行时作为底座,再配合 Python 的 LangChain 做上层业务逻辑。选 Rust 的原因不是赶时髦,而是它编译出的单二进制在隔离内网里部署太方便了——不需要在一台离线机器上折腾 Python 虚拟环境,直接扔一个可执行文件就能跑。而 LangChain 胜在生态丰富,工具类、记忆类、Prompt 管理类模块齐全,社区里大量代码片段可以直接借鉴,省去很多手写胶水代码的时间。

这里要专门提醒一句:千万不要选一个需要“在线注册”或“首次启动必须连接云端激活”的框架。这类隐性联网检查往往到现场才暴露,到时候没有外网,连框架都启动不了,非常被动。我的做法是在离线环境里用一个最小 Agent 示例跑通“用户输入-LLM 推理-工具调用”全链路,作为框架选型的验收标准。

2.3 模型部署方式的取舍:在线 API 的完美替代

隔离内网里没有云端 API,但我们可以自己开一个本地推理服务,而且完全兼容 OpenAI 格式。这样上层代码几乎不用改,只需要把base_url指到本机端口即可。我评估了三个方案:

方案优势劣势适用场景
Ollama安装简单,一条命令起服务,支持 OpenAI 兼容接口并发吞吐一般,自定义采样参数不够灵活快速验证、轻量 Agent、资源有限的单机
vLLM高并发、高吞吐,PagedAttention 省显存,OpenAI 兼容依赖较复杂,离线安装需要处理大量 Python 包并发要求高、有较大显存、需要稳定服务
llama.cpp纯 C/C++ 实现,单文件可执行,CPU 也能跑动态批处理需要手动配置,生态工具偏底层无 GPU 环境、极端依赖受限场景

我最终选了 vLLM,因为业务要求并发处理多路工单请求,而且我们的 GPU 有 24 GB 显存,足够跑 13B 参数量的量化模型。如果只是自用或日均请求量小于几千次,Ollama 绝对够用;但一旦面对真实业务压力,vLLM 的连续批处理能力能明显提升吞吐,并且显存管理更高效。

模型参数的权衡上,我选用的是 Qwen2.5-7B-Instruct 的 AWQ 4bit 量化版本。为什么不是 72B?因为 24 GB 显存跑 72B 量化模型太勉强,推理速度会降到不可用;为什么不用 3B?因为 Agent 需要多轮工具调用和指令理解,模型太小会出现“忘记工具返回结果”的情况。7B 是目前“质量-显存-速度”平衡点。如果你硬件只有 16 GB 显存,可以考虑 7B 的 4bit 量化加部分 offload;如果显存只有 8 GB,建议选 3B~4B 的量化模型并接受智商下降的事实。

3. 依赖与模型资产的离线化准备

3.1 构建离线 Python 依赖仓库

这是整个项目里最耗时、最繁琐、却最决定成败的一环。在隔离内网中不能直接pip install xxx,所以必须在有外网的环境里提前把所有依赖打包好。我采用的方案是“双环境制”:在一台可访问外网的 Linux 机器上,根据目标机器的 Python 版本(3.10)和操作系统(CentOS 7)构建 wheel 包仓库,再用 ISO 方式整体拷贝进内网。

步骤大致如下:

  1. 在目标离线机器上执行python -m pip freeze得到一个最小依赖清单,但此时很多包还缺,所以需要先在联网环境里边跑边补;
  2. 联网机器上创建一个干净虚拟环境,Python 版本和离线机保持一致;
  3. 编写一个requirements.txt,包含 Agent 框架、模型库、向量数据库客户端、HTTP 框架等全部依赖;
  4. 逐项下载:pip download -r requirements.txt -d /packages --platform manylinux2014_x86_64 --python-version 310;
  5. 如果某些包是源码包需要编译,要提前在联网机上用pip wheel构建好二进制 wheel 再拷贝;
  6. 将整个 packages 目录打包,用运维审批流程导入离线机,执行pip install --no-index --find-links=/packages -r requirements.txt。

这里最大的坑是依赖传递。你明明只装了 LangChain,但它会拉出一堆像是dataclasses-json、langsmith、pydantic、tenacity这样的子依赖。如果pip download时没有使用--no-deps,它会自动递归解析,但如果目标环境已经预装了某些包,版本冲突就会很离谱。我的建议是:先在一台干净容器里做一次完整安装演练,然后把freeze出的全量版本锁定,再用锁定的版本列表去下载。宁可打包多一点,也不要漏掉一个。

3.2 模型权重与 Embedding 模型获取

模型文件的获取是整个隔离内网方案里不可避免的一个环节。你不能从 Hugging Face 直接下载,只能通过镜像站或统一模型库先拉到有外网的中间机,校验 hash 后再导入。这里有一个容易被忽略的细节:Hugging Face 上的大模型仓库往往包含多个子文件,除了model.safetensors,还有tokenizer.json、config.json、generation_config.json。不要只拷 safetensors,否则模型加载会失败。

为了减少传输体积,我一般只保留必要的文件:

  • config.json
  • tokenizer.json
  • tokenizer_config.json
  • model.safetensors(如果仓库是多分片就全部带上)
  • generation_config.json
  • vocab.json(针对部分模型)

另外,如果你用 vLLM,还需要准备chat_template——有些模型仓库会把模板写进tokenizer_config.json,如果缺失,vLLM 启动时不会自动生成对话格式,Agent 的输入会被当作文本补全,效果会非常诡异。检查方法是启动后用curl请求/v1/chat/completions,如果不识别messages字段,多半是模板缺失。

Embedding 模型也不能落下。Agent 做知识库检索时通常需要一个本地向量化模型,我选的是bge-large-zh-v1.5。它针对中文效果好,而且是国内团队出品,离线部署时资源占用低。显存够的话可以常驻 GPU,不够的话用 CPU 跑也没问题,因为 embedding 请求通常不是瓶颈,一次请求只有毫秒级延迟。

3.3 本地工具链替代:没有“云端搜索”怎么活

Agent 的典型价值在于调用工具,但大多数现成工具默认走云端 API,比如在线搜索、网页抓取、地图查询、天气查询。在隔离内网里我们必须对这些工具做“本地替代”。我的思路是抽象出一层工具接口,让 Agent 以为自己在调用通用搜索,实际上后端对接的是内部数据源。

举个例子,工单助手需要“查历史工单相似案例”。传统方案是让 Agent 调用搜索引擎或向量库,但内网没法搜公网,所以我把工具定义为search_internal_tickets(keywords),这个函数直接查询内部的 MySQL 工单表,把匹配结果拼成文本返回给 LLM。关键在于:不要让模型感知你换了一个数据源,你只需要保持一致的输入输出结构。

另一个常见能力是“读取网页”。外网环境有现成的爬虫工具,内网则要面对各种内部 Web 系统。我用的方案是自建一个无头浏览器服务,通过 CDP(Chrome DevTools Protocol)暴露操作接口,Agent 用browser_click、browser_fill等函数驱动它,但前提是这些内部系统如果必须登录,还得配合内网的统一认证体系。这块工作量大,建议优先做“只读查询”类工具,交互型操作放到二期再做。另外,任何工具调用都应该有超时和最大重试次数,避免 Agent 因为一个不可用的工具而无限卡死。

4. 工程实现:从骨架到可运行 Agent

4.1 基础 Agent 编排流程与 Prompt 设计

Agent 的核心是循环:接收消息 → 调用模型 → 模型决策是否调用工具 → 如果调用则执行工具并返回结果 → 再次调用模型 → 循环直到模型给出最终回答。这个循环在 LangChain 中使用AgentExecutor或新版的create_tool_calling_agent都可以实现,但在隔离内网中我建议你显式地控制循环层数,不要依赖默认值。

我给 Agent 设置的最大循环次数是 8 次。为什么是 8?经验公式是:单轮复杂工具调用链通常需要 3~5 次模型调用,例如“理解问题 - 调搜索 - 读搜索结果 - 再调详情接口 - 总结”。如果超过 8 次还拿不到结果,多半是进入了死循环,例如工具返回格式不合法或者模型在反复修正同一个错误。设置最大次数可以快速失败,至少不会拖垮整个服务。

Prompt 设计上,隔离内网与公网场景有一个重要差异:你的模型不知道互联网上的最新信息,所以你必须明确告诉它“只能根据提供的文档和工具结果回答”。否则,模型会一本正经地编造出不存在的外部数据。我在 System Prompt 前部加了一句:

你是一个运行在内部安全网络中的智能助手。你无法访问互联网,所有信息均来自内部知识库和工具调用结果。如果工具返回为空,请直接说明没有获取到相关信息,不要推测或虚构。

这句话的效果立竿见影,显著降低了幻觉率。它本质上是在给模型做一个“认知边界”,把模型拉回到工程可控的范围内。

4.2 设计 Agent 与内部系统对接的“插件层”

很多 Agent 实战文章讲完框架和模型就结束了,但真正工程上卡人的地方在“工具接入”。每个内部系统都有自己的鉴权方式、参数格式、返回结构,如果让 Agent 逻辑直接散落在各处,后面维护会非常痛苦。我采用的方案是插件化中间层,每个工具独立成一个 Python 模块,统一接口:

# tools/base.py from pydantic import BaseModel class ToolResult(BaseModel): ok: bool content: str error: str = "" data: dict = {} class InternalTool: name: str = "" description: str = "" args_schema: type[BaseModel] = BaseModel def run(self, **kwargs) -> ToolResult: raise NotImplementedError

具体实现比如查内部知识库的工具:

# tools/search_kb.py import requests from tools.base import ToolResult, InternalTool class SearchKB(InternalTool): name = "search_kb" description = "搜索内部知识库,返回相关文档片段" args_schema = SearchKBSchema def __init__(self, endpoint, token): self.endpoint = endpoint self.token = token def run(self, query: str, top_k: int = 5) -> ToolResult: resp = requests.post( self.endpoint, json={"query": query, "top_k": top_k}, headers={"Authorization": f"Bearer {self.token}"}, timeout=10, ) if resp.status_code != 200: return ToolResult(ok=False, error=resp.text, content="调用知识库失败") return ToolResult(ok=True, content=format_docs(resp.json()))

这个统一抽象带来的好处是,Agent 侧根本不需要关心工具是怎么实现的,只要让模型知道“有这样一个工具,它的作用是×××,参数是×××”。同时,插件层也方便你单独为每个工具写测试用例,在离线环境中用模拟数据验证调用正确性。

实际开发中,我强烈建议给每个工具加上“返回内容截断”。模型上下文有限,如果知识库搜索返回了几千字,Agent 会把注意力分散到无关内容上,反而影响最终回答质量。我的做法是每个工具默认最多返回 1500 字,超出部分用“...”截断,并在返回文本末尾加一句“以上为截断内容,如需更多细节请用更具体的关键词重新搜索”。这样既保护了上下文窗口,又能引导模型精细化检索。

4.3 模型服务与 Agent 服务的局域网部署

隔离内网环境下,服务部署形态最常用的三种:单机一体化、前后端分离、容器化。我这次没有用容器,原因是容器镜像也需要提前做好离线导入,而且很多内网环境没有私有镜像仓库,用 docker 命令行手动导入镜像会比直接跑进程麻烦得多。我的做法是 systemd 托管两个服务:

  • 模型服务:vLLM 监听 8000 端口,绑定0.0.0.0,OpenAI 兼容接口;
  • Agent 服务:FastAPI 应用监听 8080 端口,对外提供POST /agent/run接口。

两者之间通过 HTTP 通信,模型服务的base_url由环境变量注入,Agent 服务读取LLM_BASE_URL设置为http://127.0.0.1:8000/v1。局域网内其他业务系统要访问 Agent,直接调用 8080 端口的接口即可。为了安全,我在 Nginx 层加了内网 mTLS 认证,白名单对外开放。

这里要特别说一下多模型并发场景。如果一台机器既要跑 7B 对话模型又要跑 embedding 模型,建议用不同端口加载两个模型服务,比如 vLLM 起一个 8000 端口的对话服务,再用一个轻量服务起 8001 端口的 embedding。显存分配上,7B 量化模型大约占 8~10 GB,embedding 模型占 2 GB,剩余留给 KV cache。如果你把两个模型放到同一个 vLLM 实例里,新增 embedding 模型会导致动态加载,可能在请求高峰时出现显存不足,不如分开部署来得可控。

5. 典型问题与排查实录

5.1 模型服务启动后反复 OOM

这是一个让我折腾了一整个下午的问题。vLLM 在加载 7B 量化模型时显存占用正常,但一旦并发请求上来,GPU 内存就莫名其妙涨满,进程被 kill。后来排查发现是我忽略了一个关键参数:--max-model-len。默认情况下 vLLM 会根据模型配置推断最大序列长度,有些模型配置是 32768,这会导致 KV cache 预留显存过大,而瓦塔缓存其实根本用不到。

解决办法很简单,在启动命令里显式设置--max-model-len 8192。Agent 场景下,单轮输入的历史消息和工具结果加起来很少超过 8k token,8k 的上下文长度足够用,而且 KV cache 占用会大幅下降。如果你的显存特别紧张,可以进一步调低到 4096,但要小心长文档检索场景会截断。

5.2 Agent 一直不调用工具,复读用户问题

这是新手最容易踩的坑。模型明明支持工具调用,但 Agent 就是不进入工具分支,每次都说“好的,我来帮您查询,请稍等”,然后就结束了。这种问题十有八九是 Prompt 里的描述和工具 description 写得太含糊。

比如你写search_kb:搜索知识库,模型根本不知道这个工具能解决什么问题,也不会想到去调用它。你需要提供“什么情况下用、入参长什么样、出参是什么”。我改良后的描述是:

当用户询问某系统的操作步骤、报错解决方案、产品功能说明时,使用 search_kb 工具。入参 query 为搜索关键词,尽量提取用户问题中的核心实体。工具返回相关文档片段列表。注意:只根据返回片段回答,不要臆造。

注意工具描述要给出“触发时机”,这对 LLM 的行为引导非常关键。好的工具描述应该是行为化的,而不是功能化的。

另外,如果你用的模型是 7B 或以下,工具调用能力相对较弱,尽量把工具数量控制在 5~8 个以内。工具太多模型会记混参数名,出现把query写成q或漏传必填参数的情况。我在前期测试时挂了 12 个工具,准确率只有 70%,精简到 6 个之后准确率恢复到 95% 以上。这不是模型变聪明了,是选择空间变小了。

5.3 离线包导入后仍报缺包

即使你使用--find-links从本地安装,也可能会遇到“ModuleNotFoundError: No module named xxx”的情况。最常见的原因是:你在联网机器上用pip download时没有锁全版本,导致下载了一部分与目标环境不兼容的版本。例如pydantic在 2.x 和 1.x 之间的 API 完全不同,LangChain 的不同版本依赖不同的 pydantic 接口,很容易翻车。

我的建议是分模块逐个验证。先建一个最小虚拟环境,安装核心框架和模型库,跑通一个最简单的“Hello LLM”脚本;再逐步添加向量库、工具库、Web 框架。每加一批依赖,就用freeze重新锁定版本。这样一旦出现缺包或版本冲突,你能很快定位到是哪个模块的问题,而不是面对一堆纠缠不清的依赖报错。

另一个经验是:如果服务器上有空闲空间,可以保留一个完整的 Anaconda 安装包,因为 Anaconda 自带几百个常用包和编译工具链,很多第三方库的源码安装都需要 gcc、make 等工具,而隔离内网往往没有安装编译器。利用 Anaconda 自带的 conda 仓库离线安装部分包,可以绕过很多繁琐的依赖编译问题。

5.4 向量库检索与模型答案“对不上”

知识库检索是 Agent 最常见的辅助能力,但经常出现“检索出来的片段相关但答案仍答非所问”的情况。这个问题分为两层:第一层是 embedding 模型不够好,第二层是 Prompt 没有把片段利用规则说明白。

我之前用text2vec类的小模型时,检索结果的语义匹配度很差;换成bge-large-zh-v1.5后明显改善。如果你受限于内网资源只能用小模型,建议在入库前做“语义增强”:把文档的关键词、摘要和原始内容拼接为一条扩展文本再进行切分与向量化,这样检索返回的片段信息密度更高。

Prompt 层面,我会在 System Prompt 中增加这样一段:

系统会提供若干“参考资料片段”。在回答问题时优先使用这些片段,并在回答末尾附上来源编号。如果参考资料片段不足以支持完整回答,请明确说明“根据现有资料无法完全回答”。

这个细节看起来简单,但对输出质量影响非常大。它既约束了模型不能脱离资料乱编,又给予了模型承认不确定的空间,实际上提高了用户对系统的信任度。

6. 从一次上线到长期维护的运行心得

前面讲了大量硬核步骤,最后我再分享一些软性的运行维护经验,这些内容往往比代码更关键,属于“做了才知道”的隐性成本。

第一点是版本锁的重要性。在隔离内网中,没有外网就意味着一旦升级某个依赖包,后续所有包都要同步升级,而且升级过程中很可能遇到新的兼容性问题。所以非必要不升级,每次升级前要在中间环境做全量回归测试,并做好回滚方案。我的习惯是在离线机的/opt/packages-backup里保存上一版完整包目录,升级失败时可以秒级回退。

第二点是 Agent 的可观测性。很多 Agent 框架的日志是把整个 Prompt 和工具调用链打印到控制台,但当请求多起来后,这个日志量会非常庞大且难以检索。我会在框架层自定义一个钩子,把每次调用的thought,action,action_input,observation以 JSON 格式外发到内部的日志采集服务,然后用 Grafana 做一个简单的仪表板,展示工具调用成功率、平均循环次数、失败分布。没有可观测性的 Agent 就是一个黑盒,出了问题只能靠猜。这在内网环境尤其重要,因为你不能像公网那样方便地接入外部 APM 服务。

第三点是定期抽查回答质量。模型不是人,它会突然在某些天表现得特别差,有时候是输入 Prompt 中隐藏了微妙的 pattern,有时候是后端某个内部工具的参数发生了变化。我设计了一个简单的回归测试集,包含 50 条典型的工单问题,每次模型版本或工具逻辑变更后,跑一遍并人工抽查其中 10 条。虽然不够自动化,但性价比很高。你也可以利用 LLM 来给 LLM 的回答打分,但我个人发现还是人工重点抽查更可靠。

第四点是资源预留。隔离内网环境申请资源流程很慢,所以尽量在初期就多留出 30% 的计算和存储余量。我的 4 TB 硬盘看着不小,但大模型、向量库、日志、离线安装包加起来,用不了多久就去掉了大半。建议给模型目录和向量库目录单独立卷,并开启定期清理策略,避免日志和临时文件把根分区塞满。

最后说说 Agent 的“守卫”问题。内网系统往往比公网系统更脆弱,很多老系统的接口并发能力弱,Agent 一旦进入循环调用,极有可能拖垮下游应用。我给所有工具调用加了一层全局限流:默认每个工具每秒最多调用 2 次,同一个会话中单个工具最多调用 20 次。这样即便 Agent 出错,也不会引发内部系统雪崩。另外所有工具的写操作默认禁用,只有显式配置白名单后才开放,这是真正经历过生产事故的人才会理解的保守策略。

隔离内网下的 AI Agent 工程,表面上是技术问题,本质上是系统化的工程取舍问题。模型可以小一点,但工具层和运维层必须扎实。只要把依赖、模型、工具、可观测性四条线理顺,Agent 完全可以在一个断网环境里稳定服务业务。希望这份实战记录能给你省下几个月的试错时间。

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

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

立即咨询