1. 为什么隔离内网里的 AI Agent 工程是另一套玩法
先把场景说清楚。所谓"隔离内网",指的是开发机和生产环境都跑在一个没有公网出口、没有外部包源、没有在线模型 API 的封闭网络里。你能用的只有内网镜像仓库、内网文件服务器、内网模型推理服务,以及一台能进出的跳板机。这个前提一摆出来,网上那些"五分钟搭一个 AI Agent"的教程基本全部作废——它们默认你能pip install、能调云端大模型、能拉 GitHub 上的 MCP 服务,而这些在隔离内网里全是断的。
我在这种环境里做过几轮 AI Agent 的落地,最大的感受是:内网 Agent 工程的难点从来不是"Agent 本身",而是"依赖治理"和"能力边界"。公网环境下你写 Agent,思路是"缺什么装什么";内网环境下你写 Agent,思路必须反过来,变成"我手上有什么,就用什么拼"。这两种思维方式的差异,直接决定了你的架构选型和工程组织方式。
这篇文章面向的是这样几类人:一是在金融、制造、能源这类有强隔离要求的环境里做 AI 应用落地的工程师;二是想把 Agent 能力搬进内网、但被依赖问题卡住的开发者;三是正在评估 MCP、Skills 这类新范式在内网可行性的技术负责人。我会把整个工程链路拆开讲——从依赖怎么搬进去、模型怎么接、MCP 和 Skills 怎么在内网落地,到并发怎么扛、踩过哪些坑。所有内容都基于真实可复现的实践,不讲空话。
需要先明确一个概念边界:隔离内网不等于完全没有网络,它通常有内网 pip 源、内网 npm 源、内网容器镜像仓库。真正"物理隔离、连内网源都没有"的极端情况,我会单独在依赖搬运那一节讲。绝大多数人遇到的是前者,别自己吓自己。
2. 依赖搬运:把公网生态"搬"进内网的三种姿势
2.1 先搞清楚你到底缺什么
很多人一上来就想着"把整个 PyPI 镜像同步进来",这是典型的用力过猛。正确的第一步是做依赖清单审计。你需要的不是全量生态,而是你这个 Agent 项目实际 import 的那几十个包,以及它们的传递依赖。
我的做法是在一台能联网的机器上,用干净虚拟环境装一遍项目,然后导出精确依赖:
python -m venv .venv source .venv/bin/activate pip install -r requirements.txt pip freeze > requirements.lockrequirements.lock里是带版本号的完整依赖树,这才是你要搬的东西。注意一定要锁版本,内网环境最怕的就是"今天能跑明天不能跑",而版本漂移在无法随时联网排查的环境里是灾难。
2.2 三种搬运姿势的取舍
| 姿势 | 适用场景 | 优点 | 代价 |
|---|---|---|---|
| 离线 wheel 包 | 依赖量小、变更少 | 简单直接,可控 | 每次加依赖都要重新打包 |
| 内网私有源 | 团队多人、长期迭代 | 一次搭建长期受益 | 搭建和维护成本高 |
| 容器镜像整体搬运 | 环境复杂、依赖重 | 环境一致性最好 | 镜像体积大,传输慢 |
离线 wheel 是最常用的起步方案。在联网机器上把所有依赖下载成 wheel:
pip download -r requirements.lock -d ./offline_pkgs \ --platform manylinux2014_x86_64 \ --python-version 311 \ --only-binary=:all:这里有个坑我必须提醒:--platform和--python-version必须和内网目标机完全一致,否则下载下来的 wheel 装不上。我踩过一次,联网机是 Python 3.11、内网机是 3.9,结果一半的包因为 ABI 不兼容直接报错,白搬了一趟。搬运前先在内网机上跑python -V和uname -m,把这两个值记死。
搬进去之后,在内网机上这样装:
pip install --no-index --find-links=./offline_pkgs -r requirements.lock--no-index是关键,它强制 pip 不去联网找源,只用本地 wheel。如果这一步报"找不到某个包",说明你的 wheel 没下全,通常是某个包只有源码包没有 wheel,需要单独处理。
2.3 那些"没有 wheel"的包怎么办
总有一些包只提供源码(sdist),比如某些带 C 扩展的库。这时候你有两个选择:一是在联网机上先编译好 wheel 再搬,二是把编译工具链也搬进内网。我强烈建议选第一个,因为在内网里配编译环境是纯粹的浪费时间。
编译 wheel 的命令:
pip wheel -r requirements.lock -w ./offline_pkgs --no-deps--no-deps表示只编译当前包不递归依赖,避免重复下载。编译出来的 wheel 是平台相关的,同样要注意目标机架构一致。
提示:搬运依赖时顺手把
pip、setuptools、wheel这三个基础包也下进去。内网机的 pip 版本往往很老,装新格式的 wheel 会失败,先升级基础工具能省掉一堆莫名其妙的报错。
3. 模型接入:内网里没有云端 API 时怎么让 Agent 跑起来
3.1 内网模型服务的两种形态
隔离内网里让 Agent 有"脑子",无非两条路:内网自建的推理服务,或者内网部署的开源模型。前者通常是团队已经有的统一推理网关,后者是你自己用 vLLM、TGI 这类框架在内网 GPU 机器上起的服务。
不管哪种,对 Agent 来说它就是一个 OpenAI 兼容的 HTTP 接口。所以你的 Agent 代码里,模型调用层要做的第一件事就是把 base_url 和 api_key 做成配置项,而不是硬编码。这是内网工程的基本素养,因为内网的服务地址经常变,硬编码会让你改到怀疑人生。
# config.py import os LLM_CONFIG = { "base_url": os.getenv("LLM_BASE_URL", "http://internal-llm.svc/v1"), "api_key": os.getenv("LLM_API_KEY", "internal-placeholder"), "model": os.getenv("LLM_MODEL", "qwen2.5-32b-instruct"), "timeout": int(os.getenv("LLM_TIMEOUT", "120")), }注意api_key这里给个占位符就行,内网服务通常不做鉴权,但很多 SDK 强制要求这个字段非空,不给会直接报错。
3.2 内网模型的三个现实约束
用内网模型和用云端 API,体感差异巨大,主要体现在三点:
第一是上下文窗口小。内网部署的模型往往只有 8K 或 32K 上下文,而云端动辄 128K。这意味着你的 Agent 不能无脑把历史对话全塞进去,必须做上下文压缩。我的做法是保留最近 N 轮完整对话,更早的用摘要替代,摘要本身也由模型生成。
第二是并发能力弱。一台内网 GPU 机器能同时处理的请求数很有限,超过就会排队甚至超时。这直接决定了你的 Agent 架构不能是"每个用户请求都直接打模型",必须有队列和限流。这一点我在第 6 节会展开。
第三是输出稳定性差。开源模型在结构化输出(比如要求返回 JSON)上的遵从度不如云端大模型。所以内网 Agent 里凡是需要模型返回结构化数据的场景,都要加解析容错和重试,不能假设它一定返回合法 JSON。
import json import re def parse_json_safely(text: str, retries: int = 2): for _ in range(retries + 1): try: return json.loads(text) except json.JSONDecodeError: # 尝试从 markdown 代码块里抠出 JSON match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.S) if match: try: return json.loads(match.group(1)) except json.JSONDecodeError: pass text = text.strip() raise ValueError("模型未能返回合法 JSON")这段代码看着朴素,但在内网环境里救过我无数次。开源模型特别喜欢在 JSON 外面裹一层解释文字或者 markdown 代码块,直接json.loads必挂。
3.3 模型选型的经验判断
内网能部署的模型,参数量通常受限于 GPU 显存。我的经验是:Agent 场景下,模型"会不会用工具"比"聪不聪明"更重要。一个 32B 但工具调用格式规范的模型,实际效果往往好过一个 70B 但经常乱输出工具调用的模型。
选型时重点测三件事:一是能不能稳定按格式输出工具调用;二是多轮对话里会不会"忘记"系统提示;三是长上下文下会不会开始胡言乱语。这三项过关,参数量小一点完全可以接受。
4. MCP 与 Skills 在内网的落地方式
4.1 先分清 MCP 和 Skills 各自解决什么问题
这两个概念经常被混着讲,但在内网工程里它们的定位完全不同,必须分清楚。
MCP(Model Context Protocol)解决的是"Agent 怎么统一接入外部能力"。它定义了一套标准协议,让 Agent 通过统一的客户端去调用各种工具服务——数据库查询、文件操作、内部 API 调用,都可以包装成 MCP Server。它的价值在于解耦:工具的实现和 Agent 的调用逻辑分开,加一个新工具不用改 Agent 核心代码。
Skills 解决的是"Agent 怎么获得领域知识和固定流程"。它更像是一份给模型看的操作手册,把某个任务的步骤、注意事项、可用工具打包成一个可复用的能力单元。它的价值在于沉淀:把老师傅的经验固化成模型能读懂的结构化文档。
在内网环境里,这两个东西的落地难度是不一样的。MCP 是纯内网可实现的,因为它本质就是本地进程间通信或内网 HTTP;Skills 更依赖模型的理解能力,对模型质量要求更高。
4.2 内网 MCP Server 的部署要点
内网部署 MCP Server,最省事的方式是本地 stdio 模式——MCP Server 作为子进程被 Agent 拉起,通过标准输入输出通信,完全不涉及网络。这种方式在内网里最稳,因为它不依赖任何端口和网络配置。
{ "mcpServers": { "internal-db": { "command": "python", "args": ["-m", "mcp_servers.db_server"], "env": { "DB_HOST": "internal-db.svc", "DB_PORT": "5432" } } } }如果工具需要被多个 Agent 共享,那就得用内网 HTTP 模式(SSE 或 streamable HTTP),这时候要注意两点:一是内网防火墙要放行对应端口,二是要自己做一层简单的鉴权,别裸奔。内网虽然相对安全,但"内网无威胁"是个危险的假设。
注意:MCP Server 里凡是涉及文件路径、命令执行的工具,一定要做白名单校验。内网 Agent 一旦被诱导执行了危险操作,影响范围是整片内网,这个责任谁都担不起。
4.3 Skills 在内网的写法
Skills 的落地,核心是把流程写清楚,而不是把知识堆进去。我见过很多人写 Skill 就是复制一堆文档进去,结果模型根本抓不住重点。好的 Skill 应该像一份给新人的 SOP:什么情况下触发、第一步做什么、每步用什么工具、遇到异常怎么处理。
一个内网场景的 Skill 示例(比如"生成内网数据报表"):
# 内网数据报表生成 ## 触发条件 用户要求生成某业务线的数据报表时使用。 ## 执行步骤 1. 用 internal-db 工具查询目标业务线的原始数据,时间范围默认最近 7 天。 2. 用 python-exec 工具对数据做聚合,输出 CSV 到 /data/reports/ 目录。 3. 用 internal-doc 工具把 CSV 转成带图表的文档。 ## 注意事项 - 查询必须带时间范围,禁止全表扫描。 - 报表文件命名格式:{业务线}_{日期}.xlsx - 如果查询返回空,直接告知用户"该时间段无数据",不要编造。这份 Skill 的关键在于每一步都指定了工具,且给了明确的边界条件。内网模型能力有限,你越具体,它执行得越准。
4.4 MCP 和 Skills 怎么配合
实际工程里,这两个东西是互补的。MCP 提供"手"(能操作什么),Skills 提供"脑"(该怎么操作)。一个成熟的内网 Agent,通常是 Skills 里引用 MCP 工具,模型读到 Skill 后按步骤调用对应的 MCP 工具。
这种分层的好处是:工具变了只改 MCP Server,流程变了只改 Skill,互不影响。我在一个项目里就是靠这个分层,把工具从 5 个扩到 20 多个,Agent 核心代码一行没动。
5. 内网 Agent 的工程结构:怎么组织才不乱
5.1 分层结构设计
内网 Agent 项目最容易变成一坨,因为大家都在往里面塞工具和逻辑。我的经验是强制分四层,每层职责单一:
- 接入层:处理用户输入、会话管理、权限校验。
- 编排层:Agent 主循环,负责调模型、解析工具调用、决定下一步。
- 能力层:MCP Server 和 Skills 的集合,是 Agent 的"手脚和手册"。
- 基础设施层:模型客户端、日志、配置、队列。
这个分层不是画着好看的,它解决的是变更隔离问题。内网环境里,模型服务地址会变、工具会增删、流程会调整,如果全糊在一起,改一处崩一片。
5.2 配置外置是内网工程的命根子
内网环境最忌讳硬编码。所有会变的东西——模型地址、数据库连接、工具开关、超时时间——全部走配置文件或环境变量。我习惯用一个config.yaml加环境变量覆盖的机制:
# config.yaml llm: base_url: http://internal-llm.svc/v1 model: qwen2.5-32b-instruct timeout: 120 agent: max_iterations: 10 max_context_tokens: 28000 tools: enabled: - internal-db - python-exec - internal-docmax_iterations这个参数特别重要。Agent 主循环如果没有迭代上限,遇到模型"钻牛角尖"就会无限循环,把内网模型资源耗光。我一般设 10 到 15,超过就强制中断并返回当前结果。
5.3 日志要能"事后复盘"
内网环境排查问题比公网难得多,因为你不能随时上网搜报错。所以日志必须记全:每次模型调用的输入输出、每次工具调用的参数和结果、每次决策的分支。我甚至会把完整的对话轨迹落盘,方便出问题时回放。
import logging import json from datetime import datetime def log_trace(session_id: str, event: str, payload: dict): record = { "ts": datetime.now().isoformat(), "session": session_id, "event": event, "payload": payload, } logging.getLogger("agent.trace").info(json.dumps(record, ensure_ascii=False))日志里注意脱敏。内网数据往往涉及业务敏感信息,工具返回的内容落盘前要过滤掉关键字段。这个不是可选项,是合规底线。
6. 并发与稳定性:内网 Agent 怎么扛住真实流量
6.1 内网并发的瓶颈到底在哪
很多人以为 Agent 的并发瓶颈在 Agent 代码本身,其实不是。内网 Agent 的瓶颈几乎永远在模型推理服务上。一台内网 GPU 机器,并发请求数超过它的处理能力,延迟就会指数级上升,最后全部超时。
所以内网 Agent 的并发设计,核心不是"让 Agent 处理更多请求",而是**"让请求有序地排队,别把模型打垮"**。这个思路和公网完全相反——公网你可以靠弹性扩容,内网你只能靠限流和排队。
6.2 用信号量做模型调用限流
最直接的限流手段是信号量。给模型调用加一个全局并发上限,超过就等待:
import asyncio class ModelClient: def __init__(self, max_concurrency: int = 4): self._sem = asyncio.Semaphore(max_concurrency) async def chat(self, messages, **kwargs): async with self._sem: return await self._do_request(messages, **kwargs)max_concurrency设多少,取决于你的内网模型服务能扛多少并发。我的经验值是从 2 开始试,逐步加到延迟开始明显上升为止。别一上来就设 16,那只会让所有请求一起变慢。
6.3 请求队列与超时控制
信号量解决的是"同时打模型的数量",但请求本身还需要排队。用一个有界队列,队列满了就直接拒绝,比让请求堆积到超时更友好:
import asyncio class RequestQueue: def __init__(self, max_size: int = 100): self._queue = asyncio.Queue(maxsize=max_size) async def submit(self, task): try: self._queue.put_nowait(task) except asyncio.QueueFull: raise RuntimeError("系统繁忙,请稍后重试")超时控制要分两层:单次模型调用超时和整个 Agent 任务超时。前者防止单次调用卡死,后者防止 Agent 陷入多轮循环出不来。两个都要设,缺一不可。
6.4 缓存能省掉大量重复调用
内网 Agent 场景里,很多请求是重复的——同样的查询、同样的报表、同样的问答。加一层结果缓存,能显著降低模型压力。缓存 key 用"用户意图 + 关键参数"的哈希,命中就直接返回。
但缓存要小心时效性。数据类查询的缓存时间要短(比如 5 分钟),知识类问答可以长一些。缓存过期策略没做好,用户会拿到过期数据,这比慢一点更糟糕。
7. 踩坑实录:内网 Agent 工程里那些让人抓狂的问题
7.1 编码问题:中文乱码的连环坑
内网环境里中文乱码是个高频问题,根源往往在文件读写没指定编码。Python 在有些内网机器上默认编码不是 UTF-8,导致读进来的中文全是乱码,模型再一处理,输出就彻底崩了。
解决办法是所有文件操作强制指定encoding="utf-8",包括读配置、写日志、读写数据文件。这个习惯能帮你避开 90% 的乱码问题。
with open("data.txt", "r", encoding="utf-8") as f: content = f.read()7.2 时间同步:分布式节点的隐形杀手
内网多台机器如果时间不同步,会导致日志时间错乱、缓存过期判断出错、任务调度混乱。我遇到过一次,Agent 节点和模型节点时间差了 3 分钟,导致缓存一直"未过期",用户拿到的全是旧数据。
排查方法很简单:在每台机器上跑date,对比一下。如果差异超过几秒,就要检查内网的 NTP 配置。这个问题不解决,后面所有基于时间的逻辑都不可靠。
7.3 工具调用的"幻觉参数"
内网模型能力有限,经常会给工具调用编造参数。比如你定义的工具只接受start_date和end_date,模型却传了个date_range进来。如果不做参数校验,工具直接报错,Agent 就卡住了。
我的做法是在 MCP Server 里严格校验参数,遇到非法参数返回明确的错误信息,让模型有机会自我纠正:
def validate_params(params: dict, required: list, allowed: list): missing = [p for p in required if p not in params] if missing: return f"缺少必需参数: {missing}" extra = [p for p in params if p not in allowed] if extra: return f"包含未知参数: {extra},请只使用 {allowed}" return None返回的错误信息要具体且可操作,模型看到"请只使用 [start_date, end_date]"才知道怎么改。含糊的报错模型是修不好的。
7.4 长任务的中断与恢复
内网 Agent 处理长任务(比如批量数据处理)时,如果中途服务重启,任务就丢了。解决办法是把任务状态持久化,重启后能从断点继续。
我一般用一个简单的状态表,记录每个任务的进度:
| 字段 | 说明 |
|---|---|
| task_id | 任务唯一标识 |
| status | pending/running/done/failed |
| progress | 已完成步骤 |
| checkpoint | 中间结果存储位置 |
| updated_at | 最后更新时间 |
重启后扫描status=running的任务,从checkpoint恢复。这个机制在内网环境里特别值钱,因为内网服务重启往往没有公网那么平滑。
8. 一些让内网 Agent 更好用的实战心得
8.1 给 Agent 加"能力自述"
内网模型对"自己有什么工具"的认知经常模糊。我的做法是在系统提示里明确列出所有可用工具及其用途,并且每次工具集变化时同步更新。这能显著减少模型"调用不存在的工具"的情况。
更进一步,可以让 Agent 在不确定时主动询问用户,而不是硬猜。内网场景下,多问一句比做错一件事代价小得多。
8.2 降级策略要提前设计
内网模型服务不是永远可用的。当模型不可用时,Agent 应该能降级到"规则模式"——用预设的规则处理常见请求,而不是直接报错。这个降级逻辑要提前写好,别等出事了才临时加。
8.3 定期做"离线演练"
内网环境没法随时上网查资料,所以团队要定期做离线演练:模拟模型服务挂掉、模拟依赖缺失、模拟网络分区,看 Agent 能不能优雅处理。这种演练能暴露很多平时发现不了的问题。
8.4 文档要跟着代码走
内网项目最怕"只有一个人懂"。所有工具的定义、Skill 的写法、配置的含义,都要有文档,而且要放在内网能访问的地方。我见过太多项目因为核心开发者离职,整个 Agent 变成黑盒,谁都不敢动。
我个人在实际操作中的体会是:内网 Agent 工程,七分靠工程规范,三分靠模型能力。把依赖管好、把配置外置、把日志记全、把并发控住,剩下的模型能力问题反而好解决。反过来,如果工程基础没打好,再强的模型也救不了你。这个领域没有银弹,但有大量可以复用的经验,希望上面这些能帮你少走点弯路。