1. 为什么隔离内网里的 AI Agent 是另一场游戏
先把场景说清楚。所谓隔离内网,就是一台或者一批机器,物理上或者逻辑上跟公网断开,没有外网出口,DNS 解析不了外部域名,pip、npm、apt 这些包管理器全部失效,甚至连时间同步都得靠内网 NTP。你在这种环境里要跑一个 AI Agent 工程,第一反应往往是"把公网那套搬进来不就行了",然后你会发现,搬进来的每一步都在撞墙。
我在几个不同类型的隔离环境里落地过 Agent 项目,从最开始的"能跑起来就行",到后来要求"可复现、可交付、可维护",中间踩的坑足够写一本小册子。这篇东西不讲虚的,就讲隔离内网这个约束条件下,AI Agent 工程到底该怎么搭、怎么选型、怎么排错、怎么交付。核心关键词就几个:AI Agent、MCP、Skills、内网、工程实战。如果你正在或者即将在一个没有外网的环境里做 Agent,这篇基本能帮你省掉至少两周的试错时间。
先明确一个认知:隔离内网不是"网速慢一点"或者"访问受限一点",它是整个依赖生态的断裂。公网环境下你敲一行pip install就解决的事,在内网里可能意味着你要手动下载 wheel 包、处理依赖树、解决版本冲突、再通过某种受控方式传进去。Agent 工程比普通项目更麻烦的地方在于,它天然依赖三类外部资源:模型服务、工具协议(MCP)、以及技能包(Skills)。这三样在公网都是"拉一下就有",在内网全是问题。
所以这篇的定位很明确:给已经懂一点 Agent 概念、但没在隔离环境里真正落地过的人,一份工程视角的实战记录。我会把架构选择、依赖处理、MCP 和 Skills 的内网适配、并发与稳定性、以及最终的交付打包,一层层拆开讲。每一块都会说清楚"为什么这么选"和"我当时踩了什么坑"。
2. 内网 Agent 的架构选型:先想清楚哪些东西必须进来
2.1 模型服务:本地推理还是内网网关
Agent 的大脑是模型。公网环境下你调个 API 就完事,内网里第一个要决策的就是模型怎么来。常见的有两条路:一是内网自建推理服务,把开源模型部署在内网的 GPU 机器上;二是内网里已经有一个统一的模型网关,你只需要对接它的接口。
我个人的经验是,如果内网已经有模型网关,优先对接网关,不要自己另起炉灶。原因很实际:网关通常已经解决了模型版本管理、鉴权、限流、日志这些脏活,你自己部署一套,等于把这些活重做一遍,而且内网的 GPU 资源往往是稀缺且被统一调度的,你私自占一块卡,运维那边迟早找你。
如果确实没有网关,必须自建,那选型上要注意几点。模型大小要跟内网硬件匹配,别看着公网上动辄几百 B 的模型眼馋,内网一张卡跑不动就是跑不动。量化版本是内网的好朋友,INT4 或者 INT8 量化能在几乎不损失可用性的前提下把显存需求砍下来一大截。推理框架方面,vLLM 和 TGI 是内网部署里比较常见的选择,前者吞吐好,后者部署简单,看你更看重哪个。
提示:内网自建推理服务时,一定要提前确认 CUDA 驱动版本和推理框架要求的版本是否匹配。我见过太多次因为驱动版本差一个小版本,整个部署卡半天的案例,而内网里升级驱动往往要走审批流程,代价极高。
2.2 Agent 框架:轻量优先,别把公网的重型框架硬搬
Agent 框架的选择在内网里有个反直觉的原则:越轻越好。公网环境下大家喜欢用功能全、生态大的框架,因为装依赖方便。内网里每多一个依赖,就多一份手动搬运和解决冲突的成本。
我的建议是,如果团队有能力,核心的 Agent 循环(感知、规划、调用工具、观察结果、再规划)自己写,控制在几百行以内。这样依赖极少,通常只需要一个 HTTP 客户端库和一个 JSON 处理库,内网适配成本最低。如果一定要用现成框架,优先选那些依赖树浅、纯 Python 实现、不强依赖特定云服务的。
这里要提一个热词里反复出现的概念:AI Agent 主流架构。目前主流的分两类,一类是 ReAct 风格的"思考-行动-观察"循环,一类是 Plan-and-Execute 风格的"先规划再执行"。内网环境下我更倾向 ReAct,因为它对模型的单步推理能力要求相对低,容错性好,某一步工具调用失败可以重试,不会像 Plan-and-Execute 那样一步规划错、后面全崩。
2.3 依赖清单:内网工程的第一份"资产"
在公网,依赖清单是requirements.txt或者package.json,敲个命令就装好了。在内网,这份清单是你最重要的资产之一,因为你要拿着它去"离线采购"。
我的做法是,在公网环境里先把整个依赖树完整导出,包括间接依赖和精确版本号。Python 用pip download把所有 wheel 包下到一个目录,Node 用npm pack或者直接打包node_modules。关键是版本要锁死,不能有>=这种模糊约束,否则内网装的时候解析出来的版本可能跟公网测试的不一样,行为就有差异。
# 在公网环境导出完整依赖并下载所有包 pip download -r requirements.txt -d ./offline_packages --no-binary :all: # 或者只下 wheel,速度更快 pip download -r requirements.txt -d ./offline_packages下载完之后,在内网里用pip install --no-index --find-links=./offline_packages -r requirements.txt来安装。--no-index这个参数很关键,它强制 pip 不去联网找包,只用本地目录,避免内网里 pip 卡在超时上。
3. MCP 在内网:协议本身不难,难的是它想连的东西
3.1 MCP 到底是什么,为什么内网要特别对待
MCP,全称 Model Context Protocol,是给 Agent 和外部工具之间定的一套通信规范。你可以把它理解成"Agent 世界的 USB 接口"——只要工具实现了 MCP,Agent 就能用统一的方式去调用它,不用为每个工具写一套适配代码。
在内网里,MCP 的协议本身没有任何问题,它就是个基于 JSON-RPC 的通信规范,跑在本地进程或者内网服务之间,完全不需要外网。真正的问题在于,很多现成的 MCP Server 实现,默认会去连外网。比如某些搜索类、地图类、云服务类的 MCP Server,启动时就要访问外部 API,内网里直接卡死或者报错。
所以内网用 MCP 的第一原则是:只选那些纯本地、无外网依赖的 MCP Server。文件操作、数据库查询、内网 API 调用、代码执行这类工具,天然适合内网。凡是需要连公网服务的,要么找内网替代品,要么自己写一个。
3.2 自己写一个内网 MCP Server 的最小骨架
内网里最靠谱的做法,往往是自己写 MCP Server。因为你对内网有什么、能调什么最清楚,写出来的东西也最贴合实际需求。一个最小的 MCP Server 骨架其实很简单,核心就是注册几个工具函数,然后通过标准输入输出或者 HTTP 跟 Agent 通信。
# 一个极简的内网 MCP Server 示例(stdio 模式) import json import sys def handle_request(req): method = req.get("method") if method == "tools/list": return { "tools": [ { "name": "query_internal_db", "description": "查询内网数据库", "inputSchema": { "type": "object", "properties": { "sql": {"type": "string"} } } } ] } elif method == "tools/call": tool_name = req["params"]["name"] if tool_name == "query_internal_db": sql = req["params"]["arguments"]["sql"] # 这里接内网的数据库连接 result = run_sql(sql) return {"content": [{"type": "text", "text": str(result)}]} return {"error": "unknown method"} def main(): for line in sys.stdin: req = json.loads(line) resp = handle_request(req) print(json.dumps(resp), flush=True) if __name__ == "__main__": main()这段代码看着简陋,但它把 MCP 的核心逻辑讲清楚了:接收请求、分发到对应工具、返回结果。内网里你不需要花哨的功能,需要的是稳定、可控、无外网依赖。我见过太多团队一上来就想搞个大而全的 MCP 工具集,结果每个工具都要处理外网依赖,最后没一个能跑通。
3.3 MCP 工具流式输出到文件的实战细节
热词里有个很具体的需求:"使用 MCP 工具流式输出内容到文件"。这个在内网场景里其实很常见,比如 Agent 生成的内容要落盘、要写日志、要产出报告。流式输出的难点在于,你不能等全部内容生成完再写,那样内存占用大,而且中途失败就全丢了。
我的做法是,MCP 工具里维护一个文件句柄,每收到一块内容就 flush 一次。这里有个坑:flush 太频繁会拖慢速度,flush 太少又可能丢数据。实测下来,按行 flush 或者按固定大小(比如 4KB)flush 是比较平衡的选择。
def stream_to_file(content_chunk, filepath): with open(filepath, "a", encoding="utf-8") as f: f.write(content_chunk) f.flush() # 关键:确保内容真正落盘注意:内网环境里如果文件是写在网络挂载盘上,flush 的语义可能跟本地盘不一样,极端情况下断电还是会丢。如果数据重要,写完关键节点后手动调一次
os.fsync。
4. Skills 体系:内网里怎么让 Agent "学会"新本事
4.1 Skills 和 MCP 的分工,别搞混
很多人把 Skills 和 MCP 混为一谈,其实它们解决的是不同层面的问题。MCP 解决的是"Agent 怎么连上工具",是通信层的事。Skills 解决的是"Agent 知道在什么场景下该怎么做",是知识和流程层的事。
打个比方,MCP 像是给 Agent 装了一双手,能操作工具了;Skills 像是给 Agent 一本操作手册,告诉它遇到什么情况该用哪只手、按什么顺序操作。内网里这两样都要有,但它们的适配方式完全不同。MCP 的适配重点是"去掉外网依赖",Skills 的适配重点是"把知识本地化"。
4.2 内网 Skills 的存放与加载
公网环境下,Skills 往往是从某个市场或者仓库动态拉取的。内网里没有这个条件,所以 Skills 必须提前打包、随项目一起交付。我的做法是在项目里建一个skills/目录,每个 Skill 一个子目录,里面放描述文件和相关的脚本或模板。
加载的时候,Agent 启动时扫描这个目录,把 Skill 的元信息(名称、描述、触发条件)读进内存,需要用到具体内容时再按需读取。这样既保证了启动速度,又避免了把所有 Skill 内容一次性塞进上下文。
import os import json def load_skills(skills_dir): skills = [] for name in os.listdir(skills_dir): skill_path = os.path.join(skills_dir, name) meta_file = os.path.join(skill_path, "meta.json") if os.path.exists(meta_file): with open(meta_file, encoding="utf-8") as f: meta = json.load(f) meta["path"] = skill_path skills.append(meta) return skills这里有个经验:Skill 的描述要写得像给新同事的交接文档,说清楚"什么时候用、怎么用、有什么坑"。我见过很多 Skill 描述写得极其简略,结果 Agent 根本判断不出该不该触发,等于白写。
4.3 Skill 的测试:内网里没有"试错自由"
公网环境下,Skill 写错了大不了改一改再试。内网里每次改动都要走一遍打包、传输、部署的流程,成本高得多。所以内网的 Skill 必须在公网环境充分测试后再进内网。
我的测试方法是,构造一批典型的用户输入,看 Agent 是否能正确触发对应的 Skill,触发后执行结果是否符合预期。这里要特别注意边界情况,比如输入模糊时 Agent 会不会乱触发 Skill,多个 Skill 都可能适用时会不会选错。这些在公网测试阶段就要覆盖到,别指望进内网再调。
5. 并发与稳定性:内网 Agent 扛并发的真实做法
5.1 内网并发为什么比公网更难
热词里有个问题很扎眼:"AI Agent 怎么扛并发"。这个问题在内网里比公网更难,原因有几个。第一,内网的模型推理资源通常是固定的,不像公网可以弹性扩容,并发一上来,推理服务就是瓶颈。第二,内网的网络带宽和延迟虽然稳定,但总量有限,大量并发请求可能把内网带宽打满。第三,内网里排查问题的手段少,出了并发问题往往不好定位。
所以内网 Agent 的并发策略,核心不是"扛更高的并发",而是"在有限资源下稳定地服务"。这个思路的转变很重要,别拿公网那套"加机器就行"的思路来套内网。
5.2 请求队列与限流:把并发变成可控的排队
最实用的做法是加一层请求队列。所有 Agent 请求先进队列,然后由固定数量的工作协程从队列里取任务执行。这样无论外面来多少请求,实际并发执行的数量是可控的,不会把模型服务打垮。
import asyncio class AgentQueue: def __init__(self, worker_count=4, max_queue=100): self.queue = asyncio.Queue(maxsize=max_queue) self.worker_count = worker_count async def worker(self): while True: task = await self.queue.get() try: await self.process(task) finally: self.queue.task_done() async def process(self, task): # 实际的 Agent 处理逻辑 pass async def start(self): for _ in range(self.worker_count): asyncio.create_task(self.worker())worker_count设多少合适?这取决于你的模型推理服务能同时处理多少请求。我的经验是,先设成推理服务并发能力的 70% 左右,留点余量,然后根据实际压测结果调整。max_queue则是保护机制,队列满了就直接拒绝新请求,返回"系统繁忙",总比让所有请求都超时强。
5.3 超时与重试:内网里更要小心
内网环境稳定,但一旦出问题往往是大问题。超时设置上,我建议比公网环境更宽松一点,因为内网的模型推理可能比公网 API 慢,设太短会导致大量误超时。但也不能无限长,否则一个卡死的请求会一直占着工作协程。
重试要谨慎。Agent 的很多操作是有副作用的,比如写文件、调内网 API,盲目重试可能导致重复操作。我的做法是,只对幂等的读操作做自动重试,写操作失败就报错,让人来判断。
| 操作类型 | 是否自动重试 | 原因 |
|---|---|---|
| 模型推理 | 是,最多 2 次 | 无副作用,失败多为临时问题 |
| 内网查询 | 是,最多 3 次 | 幂等,重试安全 |
| 文件写入 | 否 | 可能重复写入 |
| 内网 API 调用 | 视情况 | 幂等的可重试,非幂等的不重试 |
6. 内网交付:怎么把 Agent 工程完整搬进去
6.1 交付包的结构设计
内网交付最怕的就是"少了个文件,进去跑不起来"。所以交付包的结构要设计得清清楚楚,让人一看就知道每个目录是干嘛的。我的标准结构是这样的:
agent-delivery/ ├── app/ # Agent 主程序 ├── skills/ # 所有 Skill ├── mcp_servers/ # 内网 MCP Server ├── offline_packages/ # 离线依赖包 ├── models/ # 模型文件(如果自建推理) ├── config/ # 配置文件模板 ├── scripts/ # 安装、启动、检查脚本 └── README.md # 部署文档scripts/目录特别重要,里面要有安装脚本、启动脚本、健康检查脚本。内网里运维人员往往不熟悉你的项目,脚本写得越傻瓜越好。
6.2 配置与密钥的处理
内网里配置和密钥的处理有个原则:代码里绝对不能硬编码任何环境相关的信息。数据库地址、模型服务地址、端口这些,全部走配置文件。交付包里给一份配置模板,运维人员按实际情况填。
密钥方面,内网里相对安全,但也不建议明文写在配置文件里。可以用环境变量,或者内网自己的密钥管理服务。如果都没有,至少把配置文件权限设紧一点,别让所有人都能读。
6.3 部署后的验证清单
交付进去不等于完事,必须有一套验证清单,确认每个环节都正常。我的清单通常包括:模型服务能通、MCP Server 能启动、Skill 能加载、一个端到端的 Agent 请求能跑通、日志能正常输出、并发压测能过。
这套清单最好写成脚本,一键跑完,输出每一项的通过情况。这样每次部署后跑一遍,心里有底。
7. 那些只有踩过才知道的内网 Agent 经验
7.1 时间同步问题会坑死你
内网机器如果时间不同步,Agent 的日志时间戳会乱,更严重的是,如果 Agent 逻辑里涉及时间判断(比如"这个缓存是否过期"),时间不一致会导致各种诡异问题。我遇到过一次,两台内网机器时间差了十几分钟,导致 Agent 判断缓存永远过期,疯狂重新请求,把模型服务打挂了。所以内网部署前,务必确认所有机器的时间是同步的。
7.2 日志要足够详细,但别把磁盘写满
内网排查问题难,所以日志要详细。但 Agent 的日志量可能很大,尤其是开了 debug 级别。我的做法是分级输出,正常运行时 info 级别,出问题时能动态调到 debug。同时加日志轮转,别让日志把磁盘写满,内网里磁盘满了清理起来很麻烦。
7.3 给 Agent 加一个"紧急停止"开关
这个经验来自一次事故。当时 Agent 因为一个 Skill 的逻辑 bug,陷入了循环调用,不断消耗模型资源。内网里没法快速改代码重启,最后是手动 kill 进程才停下来的。从那以后,我所有的内网 Agent 都会加一个紧急停止机制,比如监听一个特定文件,文件出现就停止所有任务。简单但救命。
7.4 版本管理在内网里更重要
公网里版本乱了,重新拉一下就行。内网里版本乱了,你都不知道该用哪个包。所以内网交付的每个组件都要有明确的版本号,交付包里带一份版本清单,记录每个组件的版本。下次更新时,对比版本清单,清楚知道改了什么。
8. 从公网到内网:一套可复用的迁移流程
把上面这些串起来,其实可以总结成一套可复用的流程。第一步,在公网环境把 Agent 工程完整跑通,包括模型对接、MCP、Skills、并发处理,全部验证过。第二步,导出完整依赖树,下载所有离线包,锁死版本。第三步,把所有外网依赖替换成内网版本,MCP Server 该重写的重写,Skill 该本地化的本地化。第四步,打包成标准交付包,写好部署脚本和文档。第五步,进内网部署,跑验证清单。第六步,根据内网实际情况微调配置,记录所有改动。
这套流程我在几个项目里用过,基本能把内网落地的周期从"不可控"压缩到"可预期"。当然每个内网环境都有自己的特殊性,具体问题还得具体分析,但大框架是通用的。
最后说一句实在话,隔离内网做 Agent 工程,技术难度其实不是最高的,最高的是工程纪律。公网环境里可以随意试错、随意拉包、随意改配置,内网里每一个随意都会变成成本。把依赖锁死、把配置外置、把流程固化、把验证自动化,这四件事做到位,内网 Agent 就能稳稳跑起来。