1. 为什么要在隔离内网里折腾 AI Agent
先把场景说清楚。所谓隔离内网,就是那种物理上跟公网断开、或者只允许极少数白名单出口的环境,常见于金融、制造、能源、医疗这类对数据外流极度敏感的行业。你在这种环境里想跑一个 AI Agent,第一反应通常是"模型调不到、依赖装不上、工具连不通",三座大山直接把人劝退。但恰恰是这种环境,才是 AI Agent 真正能产生价值的地方——因为内网里沉淀着企业最核心的数据、最真实的业务流,Agent 一旦跑通,替代的是大量重复性的人肉操作。
我这次做的项目,目标很明确:在一套完全隔离的内网环境里,从零搭起一个可用的 AI Agent 工程体系,包含模型推理服务、MCP 工具协议层、Skills 技能编排、以及跟内网已有业务系统的对接。整套东西不依赖任何公网服务,所有组件本地化部署,最终让 Agent 能真正"下地干活",而不是停留在 demo 阶段。
这篇文章适合三类人看:一是在内网环境里被各种限制卡住、想找一条可落地路径的工程师;二是正在评估 AI Agent 能不能进生产、想了解工程细节的技术负责人;三是对 MCP、Skills 这些概念听过但没实操过、想看看真实项目长什么样的开发者。我会把选型逻辑、踩过的坑、参数怎么算、代码怎么写全部摊开讲,你照着抄作业基本能复现个七八成。
需要提前说明的是,内网环境千差万别,有的能开白名单出口,有的连 U 盘都要审批,我下面讲的方案会尽量做成分层的——核心链路必须纯离线,边缘能力可以按你的实际网络策略灵活取舍。这个思路很重要,别一上来就追求大而全,先把最小闭环跑通。
2. 整体架构设计与选型逻辑
2.1 分层架构:把"必须离线"和"可以妥协"分开
内网工程的第一原则是分清哪些东西绝对不能碰公网,哪些可以有限度地借助外部资源。我把它拆成四层:
- 模型推理层:必须纯离线。用本地部署的开源模型,通过推理框架暴露 OpenAI 兼容接口。这一层是地基,不能有任何公网依赖。
- Agent 编排层:必须纯离线。负责意图理解、任务规划、工具调用决策,是 Agent 的"大脑"。
- 工具协议层(MCP):必须纯离线。所有工具通过 MCP 协议注册和调用,这是让 Agent 能操作内网系统的关键。
- 技能层(Skills):可以半离线。核心技能本地化,一些辅助性的、非敏感的技能可以预打包后导入。
这么分层的好处是,当你的内网策略收紧时,你知道砍掉哪一层不会伤筋动骨。我见过太多项目一上来就把所有东西耦合在一起,结果网络策略一变,整个系统推倒重来。
2.2 为什么选 MCP 而不是自己写工具调用
很多人第一反应是"我自己写个 function calling 不就行了,为什么要引入 MCP 这个协议"。我一开始也这么想,直到工具数量超过十个之后,维护成本爆炸。
MCP(Model Context Protocol)本质上是给"模型怎么调用外部工具"这件事定了一套标准。它的价值在于解耦:工具提供方只需要按 MCP 规范实现一个 Server,Agent 侧只需要一个统一的 Client,双方不用关心对方内部怎么实现。在内网环境里这个特性尤其重要,因为内网的业务系统往往是多个团队维护的,你不可能要求每个团队都来适配你的 Agent 框架。
对比一下两种方案:
| 维度 | 自研 Function Calling | MCP 协议 |
|---|---|---|
| 工具接入成本 | 每个工具都要改 Agent 代码 | 工具侧实现 Server 即可 |
| 跨团队协作 | 强耦合,沟通成本高 | 协议解耦,各管各的 |
| 工具热插拔 | 需要重启 Agent | 支持动态注册 |
| 权限控制 | 散落在业务代码里 | 可在协议层统一管控 |
| 学习曲线 | 低 | 中,需要理解协议规范 |
内网环境里,跨团队协作的成本被进一步放大,因为沟通往往靠线下会议、文档流转,效率极低。MCP 的协议化解耦在这里省下的时间,远超学习成本。
2.3 Skills 的定位:把"会做"和"做得好"分开
Skills 这个概念容易被误解成"工具"。我的理解是:工具是能力,Skill 是经验。举个例子,"查询数据库"是一个工具,但"根据用户模糊描述定位到正确的表、拼出正确的 SQL、处理字段类型转换、对结果做业务口径修正"这一整套,是一个 Skill。
在内网工程里,Skills 的价值在于把老师傅的经验固化下来。内网系统往往文档缺失、字段命名混乱、业务逻辑藏在代码里,新人根本玩不转。把这些隐性知识写成 Skill,Agent 就能像老员工一样干活。
Skills 的实现方式我选的是声明式配置加少量代码。核心是一个 YAML 描述文件,定义这个 Skill 的触发条件、依赖的工具、执行步骤、异常处理。这样非核心开发人员也能参与维护,降低了长期运维的门槛。
3. 核心组件落地细节
3.1 本地模型推理服务的部署要点
内网跑模型,绕不开显存和吞吐的权衡。我这次用的是 32B 级别的开源模型,量化到 4bit,单卡 24G 显存能跑起来,并发控制在 4 路左右比较稳。
关键参数怎么定,我列一下计算过程。假设模型 32B 参数,4bit 量化后权重占用约 16GB,KV Cache 按每路 2GB 估算(上下文 8K),那么单卡 24G 的可用空间是 24 - 16 = 8GB,8 / 2 = 4 路并发。这是理论上限,实际要留 20% 余量,所以生产环境我压到 3 路。
推理框架的选择上,我对比了几个主流方案:
| 框架 | 优势 | 内网适配注意点 |
|---|---|---|
| vLLM | 吞吐高,PagedAttention 省显存 | 依赖较多,需提前打包 wheel |
| TGI | 部署简单,HuggingFace 生态好 | 镜像较大,离线导入耗时 |
| llama.cpp | 纯 CPU 也能跑,依赖极少 | 吞吐偏低,适合低并发场景 |
| Ollama | 开箱即用,管理方便 | 默认会尝试联网拉模型,需改配置 |
我最终选了 vLLM,因为内网场景下并发虽然不高,但单次请求的上下文往往很长(要喂大量业务数据),PagedAttention 对显存的优化在这里收益明显。部署时最大的坑是依赖打包,vLLM 依赖的 CUDA 相关库版本很敏感,我建议在联网机器上用相同的基础镜像先pip download全部依赖,再整体拷贝进内网。
注意:内网部署模型时,务必把模型的 tokenizer 配置、chat template 一起打包。我踩过一次坑,模型权重拷进去了,但 chat template 没带,导致模型输出格式全乱,排查了大半天。
3.2 MCP Server 的内网实现
MCP Server 在内网里的实现,核心是解决"工具怎么被发现"和"调用怎么被管控"两个问题。
工具发现这块,我用的是一个本地注册中心。每个 MCP Server 启动时向注册中心上报自己的元信息(工具名、参数 schema、权限标签),Agent 侧通过注册中心拉取可用工具列表。这样新增工具不用改 Agent 代码,重启 Server 就行。
调用管控这块,我在 MCP 协议层加了一个拦截器,所有工具调用都要过一遍权限校验和审计日志。内网环境对审计的要求往往很严,这一步不能省。拦截器的逻辑大概是:
def intercept_tool_call(agent_id, tool_name, params): # 1. 权限校验 if not check_permission(agent_id, tool_name): raise PermissionDenied(f"{agent_id} 无权调用 {tool_name}") # 2. 参数脱敏(针对敏感字段) sanitized = sanitize_params(tool_name, params) # 3. 审计日志 audit_log(agent_id, tool_name, sanitized, timestamp=now()) # 4. 实际调用 return dispatch(tool_name, params)这里有个细节值得说:参数脱敏和审计日志的顺序。我一开始是先审计再脱敏,结果日志里全是敏感数据,被安全团队打回来了。正确顺序是先脱敏再落日志,原始参数只在内存里流转,不落盘。
MCP Server 的通信方式,内网里我推荐用 stdio 而不是 HTTP。原因是 stdio 不占端口,不受内网防火墙策略影响,进程间通信也更简单。缺点是 Server 和 Agent 必须同机部署,但对于内网场景,这个限制通常可以接受。
3.3 Skills 的编排与版本管理
Skills 编排最容易出问题的地方是版本管理。内网环境里,Skill 的更新往往是通过离线包分发的,如果没有版本控制,很容易出现"Agent 用的 Skill 版本和预期不一致"的情况。
我的做法是给每个 Skill 打上语义化版本号,并在 Skill 描述文件里声明它依赖的工具版本范围。Agent 加载 Skill 时会做一次依赖检查,版本不匹配直接拒绝加载,而不是带着错误继续跑。这个"快速失败"的策略在内网里特别重要,因为内网排查问题的成本远高于公网。
Skill 的执行流程我设计成三段式:前置校验、主体执行、后置处理。前置校验负责检查输入参数、确认依赖工具可用;主体执行是核心逻辑;后置处理负责结果格式化、异常兜底。这样拆分的好处是每一段都可以单独测试,内网环境里没法频繁联调,单元测试的覆盖率直接决定了上线后的稳定性。
举个实际 Skill 的例子,一个"生成月度经营报表"的 Skill:
name: monthly_business_report version: 1.2.0 triggers: - "生成.*月.*报表" - "月度经营分析" dependencies: tools: - db_query: ">=2.0.0" - chart_render: ">=1.5.0" steps: - precheck: validate: [month_format, permission_check] - execute: - db_query: "SELECT ... WHERE month = {month}" - chart_render: "type=line, data={query_result}" - postprocess: - format: "markdown" - fallback: "return_raw_data"这个 YAML 描述的是"做什么",具体"怎么做"在对应的 Python 实现里。这种声明式加实现分离的设计,让业务人员也能看懂 Skill 的逻辑,参与维护。
4. 完整实操流程与关键环节
4.1 环境准备:离线依赖打包的完整流程
内网部署最耗时的一步就是依赖打包。我的流程是这样的:
第一步,在联网机器上准备一个跟内网目标机尽可能一致的基础环境。操作系统版本、Python 版本、CUDA 版本都要对齐,差一个小版本都可能出问题。
第二步,用pip download把所有依赖下载到本地目录:
pip download -r requirements.txt -d ./offline_packages \ --platform manylinux2014_x86_64 \ --python-version 310 \ --only-binary=:all:这里--platform和--python-version必须指定,否则会下载成当前机器的版本,拷到内网就装不上。
第三步,把整个目录打包,通过内网允许的介质(通常是审批过的移动存储或内部文件服务器)传进去。
第四步,在内网机器上离线安装:
pip install --no-index --find-links=./offline_packages -r requirements.txt--no-index是关键,它强制 pip 不去联网,只用本地包。如果这一步报错说找不到某个包,说明打包时漏了,得回联网机器补。
实操心得:依赖打包建议分两批,第一批是核心依赖(模型框架、MCP 库),第二批是辅助依赖(日志、监控)。核心依赖优先保证,辅助依赖实在装不上可以先用简化版替代,别让一个边缘依赖卡住整个项目。
4.2 模型服务启动与验证
模型服务启动后,第一件事是验证它真的能跑,而不是看日志说"启动成功"就完事。我的验证清单:
- 发一个最简单的请求,确认返回格式正确。
- 发一个长上下文请求(接近上限),确认不 OOM。
- 并发发 3 个请求,确认吞吐符合预期。
- 发一个带特殊字符的请求,确认编码没问题。
这四步走完,基本能确认服务可用。我见过太多次"日志显示正常但实际请求就崩"的情况,多花十分钟验证,省下的是后面几小时的排查。
启动命令我一般这么写:
python -m vllm.entrypoints.openai.api_server \ --model /models/qwen-32b-4bit \ --served-model-name local-agent-model \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --max-num-seqs 3 \ --port 8000--gpu-memory-utilization 0.9是留 10% 给系统,--max-num-seqs 3对应前面算的并发数。这两个参数要根据实际显存微调,别照抄。
4.3 Agent 主循环的实现
Agent 的主循环是整个系统的核心,逻辑是"接收输入 → 规划 → 调用工具 → 观察结果 → 决定下一步 → 输出"。内网环境里,这个循环要加上超时控制和最大步数限制,防止 Agent 陷入死循环把资源耗光。
def agent_loop(user_input, max_steps=10, timeout=120): context = build_initial_context(user_input) start_time = time.time() for step in range(max_steps): if time.time() - start_time > timeout: return fallback_response("超时") plan = planner.plan(context) if plan.is_final: return plan.answer tool_result = mcp_client.call(plan.tool, plan.params) context = update_context(context, plan, tool_result) return fallback_response("步数超限")max_steps=10和timeout=120是我根据实际业务调出来的。太小的值会导致复杂任务做不完,太大的值会让异常情况下的资源占用失控。建议你先设一个保守值,观察一段时间再调。
4.4 与内网业务系统的对接
对接内网业务系统是最后也是最麻烦的一步。内网系统往往有这些特点:接口文档过时、认证方式五花八门、返回格式不统一。我的策略是给每个业务系统写一个适配器,适配器负责把业务系统的接口包装成标准的 MCP 工具。
适配器要处理的核心问题有三个:认证、重试、数据转换。认证方面,内网系统常见的是 token 加 IP 白名单,适配器要能自动刷新 token。重试方面,内网网络虽然稳定但偶有抖动,适配器要带指数退避的重试。数据转换方面,业务系统返回的字段名往往很随意,适配器要统一映射成 Agent 能理解的格式。
class BusinessSystemAdapter: def __init__(self, base_url, auth_config): self.base_url = base_url self.auth = auth_config self.token = None def _ensure_token(self): if not self.token or self._is_expired(): self.token = self._refresh_token() def call(self, endpoint, params, retries=3): self._ensure_token() for i in range(retries): try: resp = requests.post( f"{self.base_url}/{endpoint}", json=params, headers={"Authorization": f"Bearer {self.token}"}, timeout=30 ) return self._transform(resp.json()) except Exception as e: if i == retries - 1: raise time.sleep(2 ** i)这个适配器模式的好处是,新增业务系统只需要写一个新的 Adapter 子类,Agent 侧完全不用改。
5. 常见问题与排查技巧实录
5.1 模型输出格式错乱
这是内网部署最高频的问题。表现是模型返回的内容里混入了奇怪的标记,或者 JSON 格式解析失败。原因通常是 chat template 没配对,或者模型本身对指令的遵循能力不足。
排查思路:先用最简单的 prompt 测试,看输出是否正常。如果简单 prompt 正常、复杂 prompt 出错,那是上下文管理的问题;如果简单 prompt 就错,那是 chat template 的问题。chat template 的问题,去模型的官方仓库找对应的 template 文件,别自己瞎写。
5.2 MCP 工具调用超时
工具调用超时在内网里很常见,因为内网系统的响应时间往往比公网服务慢。我的处理策略是分级超时:查询类工具给 30 秒,写入类工具给 60 秒,批量操作类工具给 300 秒。超时后不是直接失败,而是先重试一次,重试还失败才报错。
注意:写入类工具的重试要特别小心,必须保证幂等。我踩过一次坑,一个"创建工单"的工具因为超时重试,结果创建了两条工单。后来给所有写入类工具都加了幂等键。
5.3 显存溢出
显存溢出通常发生在长上下文或者高并发场景。排查时先看是哪种:如果是单请求就溢出,那是上下文长度超了,要调小max-model-len;如果是并发时溢出,那是并发数设高了,要调小max-num-seqs。
一个容易被忽略的点是 KV Cache 的碎片化。长时间运行后,即使并发不高也可能溢出,这时候重启服务能临时缓解,但根本解决要靠推理框架的显存管理优化。vLLM 的 PagedAttention 在这方面做得比较好,如果还有问题,可以考虑定期重启服务。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 模型输出乱码 | chat template 错误 | 用简单 prompt 测试 | 替换正确的 template |
| 工具调用超时 | 内网系统响应慢 | 看具体哪个工具慢 | 分级超时 + 幂等重试 |
| 显存溢出 | 上下文过长或并发过高 | 看是单请求还是并发 | 调小 max-model-len 或 max-num-seqs |
| 依赖装不上 | 打包时平台不匹配 | 看报错的具体包 | 重新用正确 platform 打包 |
| Skill 加载失败 | 版本不匹配 | 看依赖检查日志 | 对齐 Skill 和工具版本 |
| Agent 死循环 | 规划逻辑有缺陷 | 看步数日志 | 加 max_steps 和超时 |
5.5 几个独家避坑技巧
第一个,内网部署前一定要做一次"断网演练"。把联网机器断网,模拟内网环境跑一遍完整流程,能提前发现 80% 的联网依赖问题。
第二个,所有配置文件里的路径都用绝对路径。内网环境里工作目录经常变,相对路径是万恶之源。
第三个,日志级别默认设成 INFO,但给关键模块留一个 DEBUG 开关。内网排查问题不方便,日志是你唯一的眼睛,但全开 DEBUG 又会把磁盘写满。
第四个,给 Agent 加一个"紧急停止"机制。内网环境里 Agent 一旦失控,影响面可能很大。我的做法是监听一个本地文件,文件存在就停止所有 Agent 任务,运维人员可以快速介入。
6. 性能调优与并发扛压
6.1 并发瓶颈到底在哪
很多人一提到"AI Agent 怎么扛并发",第一反应是加机器。但在内网环境里,加机器往往不现实,你得先搞清楚瓶颈在哪。我实测下来,Agent 系统的并发瓶颈通常不在模型推理,而在工具调用和上下文管理。
模型推理这块,vLLM 的连续批处理已经能扛住相当高的并发,只要显存够。真正的瓶颈是工具调用——内网业务系统的 QPS 往往很低,Agent 并发一高,工具调用就成了排队。这时候加模型算力没用,得从工具侧优化,比如加缓存、做批量查询。
上下文管理也是瓶颈。每个 Agent 任务都要维护一份上下文,并发一高,内存占用飙升。我的做法是给上下文设上限,超过就做摘要压缩,把不重要的历史信息丢掉。
6.2 缓存策略的设计
内网 Agent 的缓存分三层:模型输出缓存、工具结果缓存、Skill 执行缓存。
模型输出缓存针对的是相同或相似的输入。内网场景下,很多查询是重复的,比如"查一下今天的订单量",这种完全可以缓存。缓存键用输入的语义哈希,而不是字面哈希,这样"今天订单多少"和"今日订单量"能命中同一个缓存。
工具结果缓存针对的是变化不频繁的数据。比如组织架构、产品目录这类,可以缓存较长时间。但要注意缓存失效策略,内网数据虽然变化慢,但一旦变化影响很大,我一般设 5 到 15 分钟的 TTL。
Skill 执行缓存针对的是完整任务的执行结果。这个要谨慎,因为 Skill 往往涉及多步操作,中间状态可能变化。我只对纯查询类的 Skill 做缓存,涉及写入的一律不缓存。
6.3 压测方法与指标
内网压测不能像公网那样随便打,得控制影响。我的方法是先在测试环境压,测试环境跟生产环境配置一致,但数据是脱敏的。
压测指标我关注四个:P50 延迟、P99 延迟、吞吐(QPS)、错误率。P50 反映一般情况,P99 反映最差情况,内网场景下 P99 比 P50 重要得多,因为用户对偶发的慢请求容忍度很低。
压测工具我用的是 locust,配置简单,能模拟真实的用户行为。压测时逐步加压,从 1 并发加到目标并发,观察各项指标的变化。如果 P99 延迟在某个并发点突然飙升,那就是瓶颈点。
from locust import HttpUser, task, between class AgentUser(HttpUser): wait_time = between(1, 3) @task def query_report(self): self.client.post("/agent/query", json={ "input": "生成本月经营报表", "session_id": "test_session" })压测结果我一般会画成曲线图,横轴是并发数,纵轴是延迟和吞吐。找到曲线的拐点,那就是系统的容量上限。生产环境的并发要控制在拐点的 70% 以下,留足余量。
7. 安全边界与合规考量
7.1 数据不出内网的技术保障
内网 Agent 最核心的安全要求是数据不出内网。技术上要做到这点,需要从几个层面保障:模型推理本地化、工具调用本地化、日志存储本地化、缓存本地化。任何一个环节走了公网,整个保障就破了。
我建议在部署完成后做一次网络流量审计,用内网自己的抓包工具,确认 Agent 系统的所有流量都在内网范围内。这个审计要定期做,因为后续的配置变更可能引入新的外联。
7.2 权限最小化原则
Agent 能调用的工具、能访问的数据,都要遵循最小权限原则。具体做法是给每个 Agent 实例分配一个角色,角色绑定一组工具权限,工具权限再细到数据行级。
行级权限在内网里特别重要。比如一个销售 Agent,它只能查自己负责区域的订单,不能查全国。这个限制要在 MCP 工具层实现,而不是在 Agent 的 prompt 里靠模型自觉。模型是不可信的,权限必须硬编码在工具层。
7.3 审计与追溯
内网环境对审计的要求通常很严。Agent 的每一次工具调用、每一次数据访问,都要有完整的审计日志。日志要包含:谁(哪个 Agent 实例)、什么时候、调用了什么工具、传了什么参数、返回了什么结果。
审计日志的存储要独立于业务数据,防止被篡改。我一般用单独的日志服务,日志写入后不可修改,只能追加。查询审计日志需要单独的权限,跟业务权限分离。
提示:审计日志里的敏感字段要做脱敏,但脱敏规则要可配置。有些场景下审计需要看到原始数据,有些场景下必须脱敏,这个要跟安全团队确认清楚。
8. 后续扩展方向
这套系统跑通之后,扩展方向其实很多。我目前在做的是把 Skills 的编写门槛进一步降低,让业务人员能通过可视化界面配置 Skill,而不是写 YAML。这个方向的价值在于,内网环境里最懂业务的人往往不是开发,让他们能直接参与,Agent 的实用性会大幅提升。
另一个方向是多 Agent 协作。单个 Agent 的能力有上限,复杂任务需要多个 Agent 分工。内网环境里多 Agent 协作的挑战是通信和协调,我倾向于用消息队列做 Agent 间的通信,用中心化的协调器做任务分配。
还有一个方向是 Agent 的自我进化。让 Agent 根据历史执行记录,自动优化 Skill 的参数和流程。这个方向比较前沿,内网环境里数据充足,是个很好的试验场。但要注意,自我进化必须有人工审核环节,不能让 Agent 自己改自己。
我个人在实际操作中的体会是,内网 AI Agent 工程最难的不是技术,而是平衡。平衡离线与在线、平衡能力与安全、平衡自动化与可控性。技术方案可以抄,但这个平衡点每个环境都不一样,得自己摸索。踩过几次坑之后你会发现,那些看起来"保守"的设计,往往才是内网环境里最靠谱的。