1. 为什么隔离内网里的 AI Agent 工程是另一套玩法
先把场景说清楚。所谓"隔离内网",指的是开发机、构建机、运行环境全部处在一个没有公网出口的局域网里,可能连外网 DNS 都解析不了,pip、npm、docker pull 这些命令全部失效。很多人在公网环境里把 AI Agent 玩得飞起,一进内网就懵了——模型拉不下来、依赖装不上、MCP 服务连不通、Skills 加载报错,最后只能退回"人工复制粘贴"的原始状态。
我前后在三个不同规模的内网环境里落地过 AI Agent 工程,从最早的"手动搬包"到后来形成一套相对稳定的离线交付流程,踩的坑足够写一本小册子。这篇就把这套东西完整拆开讲:内网里 AI Agent 到底难在哪、MCP 和 Skills 这两个核心概念在离线环境下怎么落地、依赖和模型怎么搬运、并发怎么扛、以及那些只有真正在内网干过才知道的细节。
需要先明确一点:内网 AI Agent 工程的核心矛盾不是"模型不够强",而是供应链断裂。公网环境下你习惯了"缺什么装什么",内网环境下每一个依赖、每一个模型权重、每一个 MCP Server 的可执行文件,都必须提前规划、离线打包、可复现地搬进去。这本质上是一个软件供应链治理问题,而不是单纯的 AI 问题。想通这一点,后面的所有设计就顺了。
这篇文章适合三类人:一是要在内网环境部署 AI Agent 的工程师;二是正在做 MCP、Skills 相关离线化改造的开发者;三是想理解"AI Agent 工程化"到底包含哪些环节的技术负责人。我会尽量把每一步的"为什么"讲透,而不是只丢一堆命令。
2. 内网 AI Agent 的三大断点:模型、依赖、服务发现
在动手之前,得先搞清楚内网环境到底断在哪几个环节。我把它们归纳成三个断点,每一个断点对应一套独立的搬运和适配策略。很多人一上来就想着"怎么把公网那套原样搬进来",结果发现根本搬不动,就是因为没分清这三个断点的性质完全不同。
2.1 模型权重断点:体积大、校验严、不能增量
模型权重是内网部署里最"重"的一块。一个 7B 的量化模型动辄 4-8GB,32B 的模型轻松上 20GB。公网环境下你可以用huggingface-cli download断点续传,内网里只能靠移动硬盘或者内部文件服务器中转。
这里有个很多人忽略的点:模型文件的完整性校验。大文件在多次拷贝、压缩、解压过程中极易损坏,而损坏的模型往往不会在加载时报错,而是在推理时输出乱码或者直接崩溃,排查起来极其痛苦。我的做法是搬运前先算好每个文件的 SHA256,落地后逐个校验,校验脚本随包一起交付。
# 打包前生成校验清单 find ./model -type f -exec sha256sum {} \; > model.sha256 # 落地后校验 sha256sum -c model.sha256另外,模型权重的目录结构要原样保留。有些团队为了省空间只拷贝.safetensors文件,把config.json、tokenizer.json、generation_config.json这些"小文件"漏掉,结果加载直接失败。这些配置文件加起来可能就几百 KB,但缺一个都跑不起来。
2.2 依赖断点:Python 生态的隐式依赖最坑
Python 依赖是内网部署里最容易被低估的部分。表面上看pip download一下就能把包下全,实际上很多包在安装时会动态编译、下载额外的二进制、或者依赖系统级的库(比如libpq、libssl、gcc工具链)。
我的经验是:永远不要指望在内网机器上现场编译。正确做法是在一台和內网目标机器操作系统版本、CPU 架构、Python 版本完全一致的"构建机"上,把所有 wheel 包下全,包括传递依赖。
# 在构建机上,针对目标平台下载所有依赖 pip download -r requirements.txt \ --platform manylinux2014_x86_64 \ --python-version 310 \ --only-binary=:all: \ -d ./offline_packages--only-binary=:all:这个参数很关键,它强制只下载预编译的 wheel,避免下到源码包后在内网现场编译。如果某个包没有对应平台的 wheel,那就要提前评估:要么换包,要么在构建机上编译好再打包。
2.3 服务发现断点:MCP Server 找不到彼此
MCP(Model Context Protocol)是 AI Agent 连接外部工具的标准协议,一个 Agent 往往要同时挂载多个 MCP Server——文件系统、数据库、内部 API 等等。公网环境下这些 Server 可以通过各种方式动态发现,内网里没有服务注册中心,全靠配置文件硬编码。
这里最容易出问题的是端口冲突和启动顺序。多个 MCP Server 如果都用默认端口,启动时就会互相抢占。我的做法是给每个 Server 分配固定的端口段,用一个统一的启动脚本按顺序拉起,并在启动后做健康检查。
| 断点类型 | 核心难点 | 应对策略 |
|---|---|---|
| 模型权重 | 体积大、易损坏 | SHA256 校验 + 原样保留目录结构 |
| Python 依赖 | 隐式编译依赖 | 构建机预下载 wheel + 平台锁定 |
| 服务发现 | 无注册中心 | 固定端口段 + 顺序启动 + 健康检查 |
把这三个断点想清楚,内网 AI Agent 的骨架就立起来了。接下来逐个展开。
3. MCP 在内网里的落地:从协议理解到离线部署
MCP 这两年火得很快,但很多人在公网用惯了现成的 MCP Server,一到内网就不知道怎么搞。要真正在内网落地 MCP,得先理解它到底解决什么问题,再谈部署。
3.1 MCP 到底解决了什么:把工具调用标准化
在没有 MCP 之前,每个 AI Agent 框架都有自己的工具调用格式。LangChain 一套、各家 SDK 又一套,你想让 Agent 调用一个内部数据库,就得为每个框架写一遍适配代码。MCP 的价值在于把"工具"抽象成一个标准协议:Agent 作为 Client,工具作为 Server,双方通过统一的 JSON-RPC 消息通信。
这个抽象在内网环境里尤其重要。因为内网的内部系统五花八门——有的老系统只有命令行接口,有的只有 HTTP API,有的甚至是文件交换。如果每个都单独适配,维护成本爆炸。用 MCP 统一封装后,Agent 侧只需要会说 MCP 协议,具体怎么调内部系统是 Server 的事。
MCP 的核心通信方式有两种:stdio(标准输入输出)和 SSE(Server-Sent Events)。内网环境我强烈推荐stdio 模式,因为它是进程内通信,不涉及网络端口,天然规避了防火墙和端口冲突问题。
3.2 离线打包一个 MCP Server 的完整流程
假设我们要把一个"内部知识库查询"封装成 MCP Server,部署到内网。完整流程是这样的:
第一步,在构建机上用官方 SDK 写好 Server 代码。Python 用mcp包,Node 用@modelcontextprotocol/sdk。代码本身不复杂,核心是实现list_tools和call_tool两个方法。
from mcp.server import Server from mcp.server.stdio import stdio_server app = Server("internal-kb") @app.list_tools() async def list_tools(): return [{ "name": "query_kb", "description": "查询内部知识库", "inputSchema": { "type": "object", "properties": {"keyword": {"type": "string"}}, "required": ["keyword"] } }] @app.call_tool() async def call_tool(name, arguments): if name == "query_kb": result = search_internal_kb(arguments["keyword"]) return {"content": [{"type": "text", "text": result}]}第二步,把 Server 的所有依赖打成离线包。Python 的用pip download,Node 的用npm pack或者直接把node_modules整个打包。
第三步,写一个启动脚本,把 Server 的启动命令固化下来。这个脚本要处理环境变量、工作目录、日志路径这些细节。
第四步,在 Agent 侧的 MCP 配置文件里注册这个 Server。以常见的配置格式为例:
{ "mcpServers": { "internal-kb": { "command": "python", "args": ["/opt/mcp/internal_kb/server.py"], "env": { "KB_ENDPOINT": "http://internal-api:8080" } } } }3.3 内网 MCP 的常见故障与排查顺序
内网 MCP 出问题,排查顺序很重要,乱查一通只会浪费时间。我总结的顺序是:先看进程起没起,再看协议通不通,最后看业务逻辑对不对。
进程起没起,直接ps aux | grep server.py。如果进程都没起来,八成是依赖缺失或者 Python 版本不对,看启动脚本的 stderr 输出。
协议通不通,用一个最小的 MCP Client 去 ping 一下。官方 SDK 都带测试工具,或者自己写个几行的脚本调用list_tools。如果这一步失败,通常是 stdio 的读写被日志污染了——MCP 的 stdio 模式下,stdout 只能输出协议消息,任何 print 调试都会破坏协议。这是新手最常踩的坑,调试信息一定要打到 stderr。
业务逻辑对不对,就是单独测试 Server 内部调用的那个函数。这一步和 MCP 无关,纯粹是业务代码的问题。
提示:stdio 模式下,日志必须走 stderr,stdout 留给协议。这一条能帮你省下至少半天的排查时间。
4. Skills 的离线化:让 Agent 在内网也能"长本事"
Skills 是最近半年 AI Agent 领域最热的概念之一。简单说,Skills 就是把"完成某类任务的固定套路"封装成可复用的模块,Agent 在需要时加载对应的 Skill。公网环境下有各种 Skills 市场可以一键安装,内网里就得自己搞一套离线分发机制。
4.1 Skills 和 MCP 的分工:一个管"怎么做",一个管"能调什么"
很多人分不清 Skills 和 MCP。用一句话概括:MCP 解决"Agent 能调用哪些外部能力",Skills 解决"Agent 遇到某类任务该怎么做"。
举个例子,你要让 Agent 处理一份内部报表。MCP 负责提供"读取文件""查询数据库"这些原子能力;Skills 则封装"如何识别报表格式、如何提取关键指标、如何生成汇总"这一整套流程。MCP 是手和脚,Skills 是经验和套路。
理解这个分工很重要,因为它决定了你的离线包该怎么组织。MCP Server 是独立进程,Skills 通常是提示词模板 + 辅助脚本 + 参考资料的组合。
4.2 一个可离线分发的 Skill 目录结构
我实践下来,一个便于离线分发的 Skill 应该长这样:
skills/ report_analysis/ SKILL.md # 技能描述和触发条件 prompt.md # 核心提示词模板 scripts/ extract.py # 辅助脚本 references/ format_spec.md # 参考资料 manifest.json # 元信息:版本、依赖、校验值SKILL.md是入口,描述这个 Skill 什么时候该被激活。manifest.json是离线分发的关键,它记录了版本号、依赖的其他 Skill、以及所有文件的校验值。内网分发时,只要校验 manifest 就能确认 Skill 完整。
{ "name": "report_analysis", "version": "1.2.0", "depends_on": ["file_reader"], "files": { "SKILL.md": "a1b2c3...", "prompt.md": "d4e5f6..." } }4.3 Skill 加载失败的四种典型原因
内网里 Skill 加载失败,基本逃不出这四种原因,按概率从高到低排:
第一种,路径问题。Skill 的加载器通常按固定目录扫描,如果打包时目录层级多了一层或少了一层,就扫不到。这个用find命令确认一下实际路径和配置里的路径是否一致即可。
第二种,编码问题。内网机器如果是某些特定语言环境,默认编码可能不是 UTF-8,导致中文提示词读进来是乱码。解决办法是在加载器里显式指定encoding='utf-8'。
第三种,依赖的 Skill 缺失。Skill 之间可以互相依赖,如果 A 依赖 B 但 B 没打包进去,A 就加载不了。这就是manifest.json里depends_on字段的价值——加载前先做依赖检查。
第四种,版本不匹配。Agent 框架升级后,Skill 的接口可能变了。内网环境升级困难,所以 Skill 和框架的版本要严格锁定,在 manifest 里记录兼容的框架版本范围。
| 失败原因 | 排查方法 | 修复方式 |
|---|---|---|
| 路径问题 | find 确认实际路径 | 调整目录层级或配置 |
| 编码问题 | 检查文件编码 | 显式指定 UTF-8 |
| 依赖缺失 | 检查 manifest | 补齐依赖 Skill |
| 版本不匹配 | 对比框架版本 | 锁定版本或适配 |
5. 依赖与模型的离线搬运:一套可复现的打包流程
前面讲了断点,这一节讲具体的搬运工程。核心目标是可复现——同样的打包产物,在任何一台符合条件的内网机器上都能一次装好,不需要现场调试。
5.1 构建机与目标机的环境对齐
搬运失败的头号原因是构建机和目标机环境不一致。要对齐的维度包括:操作系统发行版和版本、CPU 架构(x86_64 还是 ARM)、glibc 版本、Python/Node 版本、以及系统级依赖库。
我习惯在构建机上先跑一个环境快照脚本,把关键信息记录下来,随包交付:
#!/bin/bash echo "=== OS ===" > env_snapshot.txt cat /etc/os-release >> env_snapshot.txt echo "=== Arch ===" >> env_snapshot.txt uname -m >> env_snapshot.txt echo "=== glibc ===" >> env_snapshot.txt ldd --version | head -1 >> env_snapshot.txt echo "=== Python ===" >> env_snapshot.txt python3 --version >> env_snapshot.txt目标机落地前先跑同样的脚本,对比一下。glibc 版本不一致是最隐蔽的坑——wheel 包在构建机上能装,到目标机上就报GLIBC_2.xx not found。这种情况只能换更老的构建机,或者用 manylinux 标准镜像构建。
5.2 用虚拟环境锁定依赖树
依赖打包最忌讳的是"在全局环境里 pip download"。全局环境里可能已经装了一堆无关的包,pip download会把它们也带上,包体积翻倍不说,还可能引入版本冲突。
正确做法是建一个干净的虚拟环境,只装requirements.txt里的东西,然后导出完整的依赖树:
python3 -m venv build_env source build_env/bin/activate pip install -r requirements.txt pip freeze > locked_requirements.txt pip download -r locked_requirements.txt \ --only-binary=:all: \ -d ./offline_packagespip freeze导出的locked_requirements.txt包含了所有传递依赖的精确版本,这是可复现的关键。内网安装时用这个文件,而不是原始的requirements.txt。
5.3 模型权重的分卷与校验
大模型权重搬运时,单个文件超过移动硬盘的文件系统限制(比如 FAT32 单文件 4GB)就会失败。解决办法是分卷压缩:
# 分卷压缩,每卷 2GB tar czf - ./model | split -b 2G - model.tar.gz.part_ # 内网侧合并解压 cat model.tar.gz.part_* | tar xzf -分卷后一定要重新算校验值,因为分卷过程本身可能出错。校验清单要包含每个分卷的哈希和合并后完整文件的哈希,双重保险。
注意:分卷压缩的卷大小要小于目标文件系统的单文件限制,留 10% 余量。2GB 的卷在绝大多数文件系统上都安全。
6. 并发与稳定性:内网 Agent 扛压的工程细节
内网 AI Agent 上线后,很快会遇到并发问题。公网环境可以靠弹性扩容扛,内网资源固定,只能靠工程手段优化。这一节讲几个实战中真正有效的做法。
6.1 模型推理的并发瓶颈在哪
模型推理的并发瓶颈通常不在 CPU,而在显存和批处理效率。一个 7B 模型在单张消费级显卡上,如果每个请求单独推理,显存利用率很低,吞吐上不去。解决办法是动态批处理——把短时间内到达的多个请求攒成一个 batch 一起推理。
主流推理框架(vLLM、TGI 等)都内置了动态批处理。内网部署时,关键参数是max_batch_size和max_num_seqs。这两个值要根据显存大小调,调大了会 OOM,调小了吞吐上不去。我的经验是从保守值开始,逐步加压测试。
| 参数 | 作用 | 调优方向 |
|---|---|---|
| max_batch_size | 单批最大请求数 | 显存允许下尽量大 |
| max_num_seqs | 并发序列数 | 影响吞吐和延迟平衡 |
| gpu_memory_utilization | 显存占用比例 | 0.85-0.9 较稳妥 |
6.2 Agent 层的请求队列与超时控制
模型层之上是 Agent 层,这里要处理的是请求排队和超时。Agent 的一次任务可能包含多轮模型调用和多次工具调用,整体耗时可能几十秒甚至几分钟。如果没有队列和超时控制,高并发下会雪崩。
我的做法是在 Agent 入口加一个带优先级的请求队列,同时给每个任务设置总超时和单步超时。总超时防止任务无限挂起,单步超时防止某个工具调用卡死拖垮整个任务。
import asyncio async def run_agent_task(task, total_timeout=300, step_timeout=60): try: async with asyncio.timeout(total_timeout): for step in task.steps: async with asyncio.timeout(step_timeout): await execute_step(step) except asyncio.TimeoutError: return {"status": "timeout", "task": task.id}6.3 内网环境下的可观测性建设
内网没有公网的监控 SaaS,可观测性得自己搭。最小可用的方案是:结构化日志 + 本地指标采集 + 简单的可视化面板。
结构化日志用 JSON 格式,每条日志带上task_id、step、duration、status这些字段,方便后续用jq或者脚本分析。指标采集可以用 Prometheus 的 Python 客户端,把请求数、延迟、错误率这些指标暴露出来。可视化用 Grafana,全部离线部署。
这套东西搭起来不复杂,但没有它,内网 Agent 出问题就是黑盒,只能靠猜。我踩过的最大一个坑就是早期没做可观测性,一个偶发的超时问题查了整整两天,最后发现是某个 MCP Server 在特定输入下会卡住。
7. 内网 Agent 工程的几条血泪经验
最后分享几条只有真正在内网干过才会懂的经验,都是踩坑换来的。
第一条,永远保留一个"最小可运行集"。内网环境复杂,全量部署经常出问题。我的做法是维护一个最小可运行集——一个模型、一个 MCP Server、一个 Skill,能在目标机上跑通。全量部署出问题时,先用最小集验证基础环境,能快速定位是环境问题还是配置问题。
第二条,所有配置都要有默认值。内网机器千奇百怪,环境变量经常缺失。代码里读环境变量时一定要给默认值,否则一个缺失的变量就能让整个服务起不来。
第三条,日志级别要可动态调整。内网排查问题困难,如果日志级别写死在代码里,出问题时想开 debug 日志就得重新部署。用环境变量或者配置文件控制日志级别,出问题时改配置重启即可。
第四条,交付包里一定要带一份"从零到跑通"的文档。这份文档不是给写代码的人看的,是给半年后接手的人看的。要包含:环境要求、安装步骤、启动命令、验证方法、常见问题。我见过太多项目因为文档缺失,接手的人花一周才跑起来。
第五条,版本号要刻进骨子里。模型版本、依赖版本、Skill 版本、MCP Server 版本,全部要记录在案。内网升级困难,一旦出问题需要回滚,没有版本记录就是灾难。我习惯在交付包的根目录放一个VERSIONS.md,把所有组件的版本列清楚。
这套内网 AI Agent 工程实践,从最初的"手动搬包"到现在相对成熟的离线交付流程,前后迭代了三个版本。核心体会是:内网工程的难点从来不是技术本身,而是把公网环境下习以为常的便利,一件件用工程手段补回来。想清楚每一个依赖从哪来、每一个服务怎么起、每一个故障怎么查,内网 Agent 就能稳稳跑起来。