1. 为什么要在隔离内网里折腾 AI Agent
先把场景说清楚。所谓隔离内网,就是那种物理上跟公网断开、或者只允许极少数白名单流量出入的办公网络,常见于金融、制造、科研院所、涉密单位。这类环境有个共同特点:你能用的东西,基本都得自己搬进去。外网那些一键部署、在线调用 API、云端托管的路子,在这里统统走不通。
我最近刚在一个完全离线的内网环境里,从零把一套 AI Agent 工程跑了起来。整个过程踩的坑比我想象的多得多,但也正因为踩过,才值得写下来。这篇文章面向的是那些手上有内网服务器、有算力、但被网络隔离卡住、不知道怎么把 Agent 这套东西落地的工程师。不管你是刚听说 AI Agent 想上手,还是已经在公网玩过 Dify、LangChain 想往内网迁移,这篇都能给你一条能直接抄的路径。
核心关键词先摆出来:AI Agent、MCP、Skills、内网、工程实战。这五个词基本概括了整件事的全貌——你要在内网里搭一个能调用工具、能加载技能、能自主完成任务的智能体系统。听起来玄乎,拆开看其实就是三件事:模型怎么跑起来、工具怎么接进来、技能怎么管起来。
先说清楚一个前提:内网环境里,模型推理、工具调用、技能加载这三层必须全部本地化。任何一层依赖外部服务,整个链路就断了。这也是为什么很多人公网玩得溜,一进内网就懵——因为公网方案默认你随时能联网。
提示:本文所有方案均基于完全离线的内网环境设计,不涉及任何形式的网络穿透或外部代理。所有组件均需提前准备好离线安装包,通过物理介质导入。
2. 整体架构设计与选型思路
2.1 三层架构:模型层、编排层、工具层
内网 AI Agent 的架构,我最终收敛成了三层。这个分层不是拍脑袋定的,是被内网环境逼出来的。
模型层负责推理,也就是大模型本身。内网里你不可能调云端 API,所以必须本地部署。可选方案有 Ollama、vLLM、llama.cpp 这几种。我最后选了 vLLM,原因是它对并发请求的处理明显更好,Agent 场景下工具调用频繁,吞吐量是硬指标。模型本身选的是 Qwen 系列的开源权重,中文能力强,工具调用格式支持也成熟。
编排层是 Agent 的大脑,负责决定"下一步干什么"。这一层我用了 LangChain 加自研的一层轻量调度。为什么不用现成的 Agent 框架一把梭?因为内网环境里很多框架的默认行为会去联网拉取配置或者校验版本,跑着跑着就卡住了。自己包一层,把所有外部依赖掐断,反而更稳。
工具层就是 MCP 和 Skills 发挥作用的地方。MCP 负责把外部能力(数据库、文件系统、内部 API)标准化成 Agent 能调用的工具;Skills 负责把一套固定的操作流程封装成可复用的技能包。这两者配合,Agent 才真正有了"干活"的能力。
2.2 为什么是 MCP 而不是自己写函数调用
很多人会问,工具调用我自己写个函数注册进去不就行了,为什么要用 MCP?这个问题我在项目初期也纠结过。
自己写函数调用,短期看确实简单,一个字典映射就搞定了。但问题在于扩展性和复用性。当你接了十几个工具之后,每个工具的入参格式、返回格式、错误处理都不一样,维护成本会指数级上升。MCP 的价值就在于它定义了一套标准协议,工具的描述、参数、调用方式全部统一,Agent 侧只需要一套解析逻辑就能对接所有工具。
更关键的是,MCP 让工具和 Agent 解耦了。工具可以独立开发、独立测试、独立部署,Agent 只管调用。在内网这种多人协作的环境里,这个解耦带来的收益非常大——做数据库的同事只管写数据库的 MCP Server,做文件系统的只管写文件系统的,互不干扰。
2.3 Skills 的定位:把经验固化成可复用资产
Skills 这个概念容易被误解。它不是工具,工具是"能做什么",Skills 是"怎么做"。举个例子,工具层面你有一个"查询数据库"的能力,但"先查订单表、再关联用户表、最后按地区聚合"这一整套流程,就是一个 Skill。
在内网工程实战里,Skills 的价值在于把老员工的经验沉淀下来。以前这些流程都藏在人脑子里,新人来了得手把手教。现在把它写成 Skill,Agent 直接就能按这个流程干活,而且每次执行都一致,不会因为人的状态波动。
我自己的做法是,每个 Skill 用一个独立的目录管理,里面包含流程描述、依赖的工具列表、参数模板、以及几个典型的输入输出样例。这样 Agent 加载 Skill 的时候,能通过样例快速理解这个技能该怎么用。
3. 内网环境的前置准备与依赖处理
3.1 离线包的准备与导入
内网部署最烦的就是依赖。公网一句pip install搞定的事,内网得折腾半天。我的做法是在外网准备一台同架构、同系统的机器,把所有依赖装好,然后整体打包。
具体操作上,Python 环境用pip download把依赖的 wheel 包全部下下来,注意要指定平台和 Python 版本,否则下下来的包在内网装不上。命令大概是这样:
pip download -r requirements.txt -d ./offline_packages --platform manylinux2014_x86_64 --python-version 310 --only-binary=:all:模型权重文件动辄几十个 G,用移动硬盘拷贝是最实在的办法。拷进去之后校验一下哈希值,我吃过一次亏,硬盘中途出问题导致模型文件损坏,排查了大半天才发现是文件本身的问题。
注意:所有离线包导入前务必做完整性校验,模型文件尤其重要。一个字节的损坏可能导致推理结果完全错乱,而且这种错误很难定位。
3.2 内网 DNS 与服务发现
内网里没有公网 DNS,服务之间怎么找到对方是个问题。我的方案是在内网搭一个轻量的 DNS 服务,或者更简单点,直接在每台机器的 hosts 文件里写死映射。
如果 Agent 和工具服务部署在不同机器上,建议用固定的内网 IP 加端口的方式通信,别用主机名。主机名解析在内网环境里经常出幺蛾子,尤其是跨网段的时候。我现在的做法是维护一张服务清单表,所有服务的 IP 和端口都记录在案,配置里直接写 IP。
| 服务 | 内网 IP | 端口 | 说明 |
|---|---|---|---|
| 模型推理服务 | 192.168.10.21 | 8000 | vLLM 提供 OpenAI 兼容接口 |
| Agent 编排服务 | 192.168.10.22 | 5000 | 自研调度层 |
| MCP 工具网关 | 192.168.10.23 | 9000 | 统一工具入口 |
| 向量数据库 | 192.168.10.24 | 6333 | 用于 Skill 检索 |
3.3 权限与安全边界
内网不等于没有安全要求,恰恰相反,内网的安全边界往往更严格。Agent 能调用的工具必须做权限控制,不能让它随便访问任何资源。
我的做法是给每个 MCP Server 配置一个能力清单,明确它能访问哪些路径、哪些表、哪些接口。Agent 侧再叠一层权限校验,双重保险。这样即使 Agent 被诱导去调用不该调的工具,也会在工具层被拦下来。
4. MCP 工具层的落地实操
4.1 MCP Server 的最小实现
MCP 的核心是协议,实现一个 MCP Server 其实不复杂。我用 Python 写了一个最简版本,核心就是暴露几个标准方法:列出可用工具、描述工具参数、执行工具调用。
from mcp.server import Server from mcp.types import Tool, TextContent app = Server("internal-tools") @app.list_tools() async def list_tools(): return [ Tool( name="query_database", description="查询内网业务数据库,支持标准 SQL", inputSchema={ "type": "object", "properties": { "sql": {"type": "string", "description": "要执行的 SQL 语句"} }, "required": ["sql"] } ) ] @app.call_tool() async def call_tool(name: str, arguments: dict): if name == "query_database": result = execute_sql(arguments["sql"]) return [TextContent(type="text", text=str(result))]这段代码看着简单,但有几个细节值得说。description字段非常关键,Agent 就是靠这个描述来判断什么时候该调用这个工具。描述写得含糊,Agent 就会乱调或者不调。我一般会把使用场景、限制条件都写进去,比如"仅支持 SELECT 查询,不支持写操作"。
4.2 工具描述怎么写才让 Agent 会用
这是我在实战中花时间最多的地方。工具描述写得好不好,直接决定 Agent 的调用准确率。
我的经验是,描述里必须包含三样东西:这个工具干什么、什么时候用、有什么限制。举个例子,同样是查询工具,写成"查询数据"和写成"根据用户 ID 查询订单历史,适用于需要了解用户购买行为的场景,单次最多返回 100 条",效果天差地别。
另外,参数描述也要具体。别写"sql: SQL 语句",要写"sql: 标准 SQL 查询语句,表名必须是 orders、users、products 之一"。把约束条件写清楚,Agent 就不会瞎猜。
4.3 多工具编排的坑
当工具数量超过十个之后,Agent 的选择困难症就来了。它经常在几个相似的工具之间反复横跳,或者该用 A 工具的时候用了 B。
我的解决办法是给工具分组。把功能相近的工具归到一个命名空间下,Agent 先选组,再选具体工具。这样选择空间从十几个降到三四个,准确率明显提升。
另一个坑是工具调用的超时处理。内网环境里某些工具(比如查大表)可能跑很久,如果不设超时,Agent 会一直等,整个流程就卡死了。我给每个工具都配了独立的超时时间,超时后返回一个明确的错误信息,Agent 收到错误会自己决定重试还是换方案。
5. Skills 技能体系的设计与实现
5.1 Skill 的目录结构规范
Skills 要能复用,目录结构必须规范。我定的规范是这样的:
skills/ order_analysis/ skill.yaml # 技能元信息 prompt.md # 技能提示词 tools.json # 依赖的工具列表 examples/ # 输入输出样例 case1_input.json case1_output.jsonskill.yaml里记录技能名称、描述、版本、作者。prompt.md是这个技能的核心,写清楚执行步骤。tools.json声明这个技能需要哪些工具,Agent 加载技能时会自动检查这些工具是否可用。
5.2 用提示词固化操作流程
Skill 的本质是一段结构化的提示词。我写 Skill 提示词的时候,遵循一个固定模板:先说明目标,再列步骤,最后给约束。
比如一个"月度销售报告生成"的 Skill,提示词大概是这样:
目标:生成指定月份的销售报告 步骤: 1. 调用 query_database 工具,查询该月所有订单 2. 按地区分组统计销售额 3. 找出销售额前三的地区 4. 调用 generate_chart 工具生成柱状图 5. 汇总成 Markdown 格式报告 约束: - 金额单位统一为万元 - 如果某地区无数据,标注为"无销售" - 报告必须包含环比数据这种写法比自然语言描述靠谱得多,Agent 执行起来步骤清晰,不容易漏步骤。
5.3 Skill 的版本管理与灰度
Skills 是会迭代的。今天写的流程,明天业务变了就得改。所以版本管理很重要。
我的做法是每个 Skill 目录下保留历史版本,用版本号区分。Agent 加载时默认用最新版,但可以通过配置指定用某个特定版本。这样新版本出问题的时候,能快速回滚。
灰度发布这块,我是在 Agent 侧做的。新版本 Skill 先只对部分请求生效,观察一段时间没问题再全量。内网环境里没有现成的灰度工具,都是自己写逻辑控制。
6. 模型层的内网部署要点
6.1 模型选型:能力与资源的平衡
内网部署模型,第一个要面对的就是资源约束。你不可能像公网那样随便调 70B 的模型,得看手上有多少卡。
我的经验是,Agent 场景对模型的要求和纯对话不一样。Agent 更看重指令遵循能力和工具调用格式的准确性,而不是知识广度。所以一个 14B 到 32B 的模型,只要工具调用训练得好,完全够用。我最后用的是 Qwen2.5-32B 的量化版本,在两张卡上跑得挺稳。
如果资源实在紧张,7B 级别的模型也能用,但工具调用的准确率会下降,需要靠更严格的提示词和更多的校验来补。
6.2 推理服务的参数调优
vLLM 部署起来之后,有几个参数必须调。max_model_len决定了上下文长度,Agent 场景下上下文会很长(工具描述、历史对话、技能提示词都占地方),我设的是 8192。gpu_memory_utilization控制显存占用比例,设太高容易 OOM,设太低浪费资源,我一般从 0.85 开始试。
还有一个容易被忽略的参数是enable_prefix_caching。Agent 场景下系统提示词是固定的,开启前缀缓存能显著降低重复计算,吞吐量能提升不少。
6.3 工具调用格式的适配
不同模型的工具调用格式不一样。有的用特定的 token 标记,有的用 JSON 格式。内网部署时,必须确保模型的输出格式和 Agent 的解析逻辑对得上。
我的做法是在 Agent 侧做一层格式适配,不管模型输出什么格式,都先归一化成标准结构再处理。这样换模型的时候,只需要改适配层,不用动上层逻辑。
7. 常见问题与排查实录
7.1 工具调用失败排查表
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| Agent 不调用工具 | 工具描述不清 | 检查 description 字段 | 补充使用场景和限制 |
| 调用参数错误 | 参数 schema 不明确 | 查看 inputSchema | 细化参数类型和约束 |
| 调用超时 | 工具执行慢 | 看工具侧日志 | 加超时和异步处理 |
| 返回结果解析失败 | 格式不匹配 | 对比返回和预期 | 加格式适配层 |
| 工具选择错误 | 工具太多太像 | 统计调用分布 | 分组或合并工具 |
7.2 模型输出不稳定的处理
内网模型有时候会抽风,同样的输入两次输出不一样。这在 Agent 场景下很要命,因为工具调用需要确定性。
我的处理办法是降低温度参数。Agent 场景下温度设 0 到 0.1 就够了,不需要创造性。另外,关键的工具调用步骤加校验,如果模型输出的参数不符合 schema,直接打回重试,而不是硬着头皮执行。
7.3 内网环境特有的坑
内网有几个坑是公网遇不到的。一个是时间同步,内网机器时间不一致会导致日志混乱、缓存失效。我现在的做法是内网搭一个 NTP 服务,所有机器定期同步。
另一个是磁盘空间。模型文件、日志、缓存加起来很占地方,内网扩容又麻烦。我养成了定期清理的习惯,日志按天切割,超过七天的自动删。
还有一个是依赖版本锁定。内网装包不方便,所以一旦装好就别乱动。我把所有依赖的版本号都锁死在配置文件里,避免有人手贱升级导致环境崩掉。
8. 实操心得与经验沉淀
8.1 先跑通最小闭环再扩展
我见过太多人一上来就想搭个大而全的系统,结果卡在某个环节动弹不得。我的建议是先跑通最小闭环:一个模型、一个工具、一个技能,能完成一个最简单的任务就行。
这个最小闭环跑通之后,你会对整个链路的瓶颈有清晰的认识。是模型推理慢,还是工具调用不稳,还是技能加载有问题,一目了然。然后再针对性地扩展,效率高得多。
8.2 日志要打全,但别打太多
内网排查问题全靠日志。我的做法是每个环节都打日志,但要分级。INFO 级别记录关键流程节点,DEBUG 级别记录详细参数,默认只开 INFO,出问题的时候临时开 DEBUG。
日志格式要统一,带上时间戳、服务名、请求 ID。请求 ID 特别重要,一个请求跨多个服务的时候,靠它才能把链路串起来。
8.3 技能库要持续维护
Skills 不是写完就完事了,得持续维护。业务变了,技能就得跟着改。我现在的做法是每个月review一次技能库,把没人用的技能归档,把常用的技能优化。
另外,鼓励一线同事贡献技能。他们最清楚实际业务怎么跑,写出来的技能最接地气。我搭了个简单的技能提交和审核流程,大家提交,我审核后合并进主库。
8.4 性能优化的几个方向
内网 Agent 的性能瓶颈通常在三个地方:模型推理、工具调用、技能检索。
模型推理这块,量化、批处理、前缀缓存是三个最有效的手段。工具调用这块,异步化和连接池能显著降低延迟。技能检索这块,如果技能多了,得用向量检索而不是关键词匹配,否则找技能本身就慢。
我实测下来,优化前后整体响应时间能差三到五倍。所以别急着堆硬件,先把这几个软件层面的优化做扎实。
最后分享一个我踩过的坑:内网环境里,千万别在高峰期做模型热更新。有一次我图省事,业务跑着的时候换了模型,结果显存没释放干净,新模型加载失败,整个服务挂了半小时。后来我学乖了,所有更新都安排在低峰期,而且更新前先做好回滚预案。这个教训值不少钱,希望你别再踩一遍。