1. 隔离内网下的 AI Agent 工程化落地:从零搭建到稳定运行
很多做企业级交付的朋友都遇到过这种场景:客户现场只有一台跳板机能连外网,业务服务器全部在隔离内网里,没有公网出口,没有外部镜像源,甚至连 pip install 都要走内部私有仓库。在这种环境下要把 AI Agent 跑起来,难度不是模型本身,而是整条工程链路怎么在"断网"条件下自洽。我前后在三个不同行业的隔离环境里落地过 AI Agent 项目,从最早的纯脚本拼凑,到后来用 MCP 协议做工具编排,踩过的坑足够写一本小册子。这篇就把整套思路和实操细节摊开讲,适合正在做内网交付的工程师、需要把 Agent 部署到生产隔离区的团队,以及想搞清楚 MCP、Skills 这些概念在内网场景下到底怎么用的人。
先说清楚一个前提:隔离内网不等于完全离线。绝大多数企业内网是"逻辑隔离"——有内部镜像源、有内部 Git 服务、有内部模型推理服务,只是不能访问公网。真正物理隔离的环境我也做过,那种情况下连模型权重都要靠移动介质导入,工程复杂度会再上一个台阶。下面讲的内容以逻辑隔离为主,物理隔离的差异点我会单独标注。
2. 为什么隔离内网下的 Agent 工程和公网完全不是一回事
2.1 三个核心约束决定了架构走向
在公网环境里搭 Agent,你随手就能pip install langchain、npm install @modelcontextprotocol/sdk,模型直接调云端 API,工具想接什么接什么。但隔离内网里,这三个自由度全部被砍掉:
依赖获取受限。所有第三方包必须提前下载好 wheel 或 tarball,通过内部制品库分发。版本冲突、传递依赖缺失、平台架构不匹配(比如内网服务器是 ARM 而你的开发机是 x86)这些问题会在部署阶段集中爆发。
模型调用受限。云端 API 基本不可用,只能用内网部署的推理服务。常见的是 vLLM、TGI 或者公司自研的推理网关,接口协议可能是 OpenAI 兼容的,也可能是私有的。这直接影响 Agent 框架的选型——有些框架强绑定特定厂商 SDK,在内网就是死路。
工具生态受限。MCP 这类协议的价值在内网反而更突出,因为它把工具调用标准化了,你不需要为每个工具写适配代码。但 MCP Server 本身也要在内网部署,涉及进程管理、端口分配、权限隔离等一堆运维问题。
2.2 架构选型的核心权衡
我见过不少团队一上来就想上全套:LangGraph 做编排、MCP 做工具、向量库做 RAG、再加一层网关做鉴权。结果在内网部署时发现光是依赖就装了两天,最后砍到只剩核心功能。
我的建议是分阶段推进。第一阶段只解决"能跑":一个轻量 Agent 循环 + 内网模型 + 两三个核心工具。第二阶段解决"好用":引入 MCP 标准化工具接口,加上 Skills 做能力扩展。第三阶段解决"稳定":并发控制、超时重试、日志追踪、灰度发布。
这个顺序不能反。我见过直接上第三阶段然后卡在第一阶段的团队,最后项目延期两个月。
2.3 内网环境下的技术栈对比
| 维度 | 公网常用方案 | 内网推荐方案 | 选择理由 |
|---|---|---|---|
| 模型接入 | 云端 API | 内网 vLLM/TGI,OpenAI 兼容协议 | 协议标准化,框架适配成本低 |
| 工具编排 | 直接函数调用 | MCP 协议 | 解耦工具与 Agent,便于独立升级 |
| 依赖管理 | pip/npm 直连 | 内部制品库 + 离线 wheel 包 | 可控、可审计、可回滚 |
| 向量存储 | 云服务 | 本地 Milvus/Qdrant/FAISS | 数据不出内网 |
| 可观测性 | SaaS 平台 | 自建 Prometheus + Loki | 无外部依赖 |
这张表不是绝对的,但基本覆盖了 80% 的隔离内网场景。下面逐个展开。
3. 内网 Agent 工程的核心模块拆解
3.1 模型接入层:怎么让 Agent 用上内网模型
内网模型服务通常有两种形态:一种是标准的 OpenAI 兼容接口(vLLM、TGI 都支持),另一种是公司自研的私有协议。优先选前者,因为几乎所有 Agent 框架都支持 OpenAI 协议,改个 base_url 就能用。
配置上要注意几个点。第一是max_tokens和context_length要和内网模型的实际能力对齐,很多内网部署的模型是量化版本,上下文窗口比原版小。第二是超时设置,内网推理服务如果没做批处理优化,单次响应可能到几十秒,Agent 框架默认超时往往不够。第三是并发限制,内网 GPU 资源有限,Agent 如果并发调用会把推理服务打满。
# 内网模型接入的典型配置 from openai import OpenAI client = OpenAI( base_url="http://internal-llm-gateway:8000/v1", # 内网推理网关 api_key="internal-token", # 内网通常用固定 token 或走 mTLS timeout=120.0, # 内网推理慢,超时要放宽 max_retries=2, # 重试次数不宜多,避免打爆推理服务 ) # 调用时显式控制并发 response = client.chat.completions.create( model="qwen2.5-72b-instruct", # 内网部署的模型名 messages=[{"role": "user", "content": "..."}], temperature=0.1, # Agent 场景温度要低,保证稳定性 max_tokens=2048, )提示:内网模型名不要硬编码在代码里,放到配置文件或环境变量。不同环境(开发/测试/生产)的模型名可能不一样,硬编码会导致部署时改代码。
3.2 MCP 协议:内网工具标准化的关键
MCP(Model Context Protocol)本质是一套让 Agent 和工具之间通信的协议规范。它的价值在于:工具提供方只需要实现 MCP Server,Agent 侧只需要实现 MCP Client,双方通过标准协议通信,不需要知道对方内部实现。
在内网环境里,MCP 的部署方式通常是 stdio 或 SSE。stdio 模式最简单,MCP Server 作为子进程启动,通过标准输入输出通信,不需要开端口,适合单机部署。SSE 模式需要开 HTTP 端口,适合多 Agent 共享工具服务的场景。
// MCP Server 配置示例(stdio 模式) { "mcpServers": { "internal-db-query": { "command": "python", "args": ["/opt/mcp-servers/db_query_server.py"], "env": { "DB_HOST": "internal-db.internal", "DB_PORT": "5432" } }, "internal-file-search": { "command": "/opt/mcp-servers/file_search", "args": ["--index-path", "/data/index"] } } }内网部署 MCP Server 有几个坑要注意。第一是路径问题,stdio 模式下 command 的路径必须是绝对路径,相对路径在不同工作目录下会失效。第二是环境变量传递,MCP Server 子进程不会自动继承父进程的所有环境变量,需要显式配置。第三是日志,stdio 模式下 stdout 被协议占用,日志必须走 stderr 或文件,否则会污染协议通信。
3.3 Skills 机制:让 Agent 能力可插拔
Skills 这个概念在不同框架里叫法不一样,有的叫 Tools,有的叫 Actions,本质都是"Agent 可以调用的能力单元"。在内网环境里,Skills 的设计要考虑三个问题:怎么注册、怎么发现、怎么隔离。
注册方式推荐用声明式配置,而不是硬编码。每个 Skill 用一个独立的配置文件描述,包括名称、描述、参数 schema、执行入口。这样新增 Skill 不需要改 Agent 主程序,只需要加配置文件。
# skill 配置示例 name: query_internal_api description: 查询内网业务系统的订单信息 parameters: type: object properties: order_id: type: string description: 订单编号 date_range: type: string description: 日期范围,格式 YYYY-MM-DD~YYYY-MM-DD required: - order_id executor: type: http url: http://internal-api.internal/order/query method: POST timeout: 30发现机制上,内网环境建议用文件系统扫描 + 热加载。Agent 启动时扫描指定目录下的所有 skill 配置文件,运行期间定期检查文件变化,有更新就重新加载。这样运维人员新增 Skill 不需要重启 Agent。
隔离方面,不同 Skill 的权限要分开。查询类 Skill 只读,操作类 Skill 需要审批,高危 Skill(比如执行 shell 命令)要单独隔离到沙箱环境。我见过因为 Skill 权限没隔离导致 Agent 误删生产数据的案例,这个坑一定要提前防。
3.4 依赖管理:离线环境下的包分发
这是内网部署最烦人的环节。公网环境pip install一行命令搞定,内网要提前把所有依赖下载好,还要处理传递依赖和平台兼容性。
我的做法是分三步。第一步在公网环境用pip download把所有依赖下载到本地目录,包括传递依赖。第二步把下载的包上传到内网制品库(Nexus、Artifactory 都行)。第三步内网机器配置 pip 源指向内部制品库。
# 公网环境:下载所有依赖(含传递依赖) pip download -r requirements.txt -d ./offline_packages \ --platform manylinux2014_x86_64 \ --python-version 311 \ --only-binary=:all: # 内网环境:从本地目录安装 pip install --no-index --find-links=./offline_packages -r requirements.txt注意:
--platform和--python-version必须和内网目标环境完全一致,否则下载的 wheel 装不上。如果内网是 ARM 架构,要指定manylinux2014_aarch64。
对于 Node.js 项目,用npm pack或者yarn offline mirror做类似的事情。Rust 项目用cargo vendor把依赖 vendor 到本地目录。
4. 完整实操:从零在内网部署一个可用的 Agent
4.1 环境准备与依赖导入
假设内网环境是 CentOS 7 + Python 3.11 + 无公网。第一步是准备离线依赖包。
在公网机器上创建一个和内网一致的环境(可以用 Docker 模拟),然后执行依赖下载。这里有个技巧:先用pip freeze导出完整依赖树,再逐个下载,避免遗漏传递依赖。
# 1. 在公网机器创建虚拟环境 python3.11 -m venv build_env source build_env/bin/activate # 2. 安装项目依赖 pip install -r requirements.txt # 3. 导出完整依赖树 pip freeze > full_requirements.txt # 4. 下载所有包 pip download -r full_requirements.txt -d ./offline_packages \ --platform manylinux2014_x86_64 \ --python-version 311 \ --only-binary=:all: # 5. 打包 tar czf offline_packages.tar.gz offline_packages/把offline_packages.tar.gz通过内部文件传输通道传到内网,解压后配置 pip 使用本地目录。
# 内网机器上 tar xzf offline_packages.tar.gz pip install --no-index --find-links=./offline_packages -r full_requirements.txt这一步最常见的失败原因是平台不匹配。如果内网是 CentOS 7,glibc 版本比较老,很多新版本的 wheel 依赖更高版本的 glibc。解决办法是下载源码包在内网编译,或者用 manylinux2014 兼容的 wheel。
4.2 Agent 主程序搭建
主程序我推荐用 Python,生态最成熟,内网部署也最方便。核心结构分四层:配置层、模型层、工具层、编排层。
配置层负责读取环境配置,包括模型地址、MCP Server 列表、Skill 目录等。用 YAML 文件 + 环境变量覆盖的方式,方便不同环境切换。
# config.py import os import yaml from dataclasses import dataclass @dataclass class AgentConfig: model_base_url: str model_name: str model_api_key: str mcp_config_path: str skill_dir: str max_concurrency: int request_timeout: int def load_config(path: str = "config.yaml") -> AgentConfig: with open(path) as f: raw = yaml.safe_load(f) # 环境变量覆盖 raw["model_base_url"] = os.getenv("MODEL_BASE_URL", raw["model_base_url"]) raw["model_api_key"] = os.getenv("MODEL_API_KEY", raw["model_api_key"]) return AgentConfig(**raw)模型层封装内网模型调用,统一处理超时、重试、并发控制。这里的关键是加一个信号量控制并发,避免打爆推理服务。
# model_client.py import asyncio from openai import AsyncOpenAI class ModelClient: def __init__(self, config): self.client = AsyncOpenAI( base_url=config.model_base_url, api_key=config.model_api_key, timeout=config.request_timeout, ) self.model = config.model_name self.semaphore = asyncio.Semaphore(config.max_concurrency) async def chat(self, messages, tools=None): async with self.semaphore: response = await self.client.chat.completions.create( model=self.model, messages=messages, tools=tools, temperature=0.1, ) return response工具层负责加载 MCP Server 和本地 Skill,统一成 Agent 可调用的格式。编排层是 Agent 的主循环,负责决策、调用工具、处理结果。
4.3 MCP Server 内网部署实操
MCP Server 的部署方式取决于工具类型。数据库查询类用 stdio 模式最简单,Web 服务类用 SSE 模式更方便多 Agent 共享。
以数据库查询 MCP Server 为例,用 Python 实现一个最小可用的版本:
# db_query_server.py import asyncio import json import sys from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent import asyncpg app = Server("db-query") @app.list_tools() async def list_tools(): return [ Tool( name="query_orders", description="查询订单信息", inputSchema={ "type": "object", "properties": { "order_id": {"type": "string"}, }, "required": ["order_id"], }, ) ] @app.call_tool() async def call_tool(name: str, arguments: dict): if name == "query_orders": conn = await asyncpg.connect( host="internal-db.internal", database="business", user="readonly", password="***", ) rows = await conn.fetch( "SELECT * FROM orders WHERE order_id = $1", arguments["order_id"], ) await conn.close() return [TextContent(type="text", text=json.dumps([dict(r) for r in rows]))] async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ == "__main__": asyncio.run(main())部署时要注意:数据库连接用只读账号,避免 Agent 误操作。查询加超时和行数限制,避免大查询拖垮数据库。日志走 stderr,不要污染 stdout。
4.4 并发控制与稳定性保障
内网 Agent 最容易出问题的地方就是并发。公网环境模型 API 有弹性扩容,内网 GPU 资源固定,并发一高就排队甚至超时。
我的做法是三层限流。第一层是 Agent 级别的信号量,控制同时进行的对话数。第二层是模型调用级别的信号量,控制同时发给推理服务的请求数。第三层是工具调用级别的限流,不同工具根据后端承载能力设置不同阈值。
# 三层限流示例 class RateLimiter: def __init__(self, agent_limit=10, model_limit=5, tool_limits=None): self.agent_sem = asyncio.Semaphore(agent_limit) self.model_sem = asyncio.Semaphore(model_limit) self.tool_sems = { name: asyncio.Semaphore(limit) for name, limit in (tool_limits or {}).items() } async def acquire_agent(self): await self.agent_sem.acquire() async def acquire_model(self): await self.model_sem.acquire() async def acquire_tool(self, name): if name in self.tool_sems: await self.tool_sems[name].acquire()除了限流,还要加熔断。当模型调用连续失败超过阈值时,暂时停止调用,给推理服务恢复时间。熔断恢复用半开模式,放少量请求试探,成功后再全量放开。
5. 内网 Agent 常见问题与排查实录
5.1 依赖安装类问题
问题一:pip install 报 "No matching distribution found"
这是内网部署最高频的问题。原因通常是下载的 wheel 平台不匹配,或者传递依赖没下载全。排查方法是先看报错的具体包名,然后在公网环境用相同平台参数单独下载这个包,对比版本。
# 排查某个包为什么装不上 pip download <package_name> -d ./debug \ --platform manylinux2014_x86_64 \ --python-version 311 \ --only-binary=:all: -v如果这个包没有对应平台的 wheel,就需要下载源码包在内网编译。编译前要确保内网有 gcc、make、python-devel 等基础工具。
问题二:安装成功但 import 报错
通常是动态链接库缺失。用ldd检查 so 文件的依赖,缺什么补什么。CentOS 7 上常见的是 glibc 版本不够,需要升级或者用兼容版本重新编译。
5.2 模型调用类问题
问题一:请求超时
内网推理服务如果没做批处理,单次请求可能很慢。先确认推理服务本身的响应时间,用 curl 直接测。如果推理服务就慢,Agent 侧只能加大超时。如果推理服务快但 Agent 慢,检查是不是并发太高导致排队。
问题二:返回内容截断
内网模型如果是量化版本,输出质量可能下降,表现为回答不完整或者格式错乱。解决办法是降低 temperature、增加 max_tokens、在 prompt 里明确要求输出格式。
问题三:并发调用打爆推理服务
这个前面讲过,用信号量限流。但要注意信号量的位置,如果放在模型客户端内部,每个客户端实例一个信号量,多实例就失效了。要放在全局单例里。
5.3 MCP 工具类问题
问题一:MCP Server 启动失败
stdio 模式下最常见的是路径问题。command 必须是绝对路径,args 里的路径也要绝对路径。另外 Python 脚本要有可执行权限,或者用python作为 command,脚本路径作为 args。
问题二:工具调用返回空
先看 MCP Server 的 stderr 日志,通常有详细报错。常见原因是环境变量没传进去,比如数据库连接信息。stdio 模式下子进程不继承父进程环境变量,要在配置里显式声明。
问题三:工具调用卡住不返回
检查 MCP Server 内部是不是有阻塞操作。比如数据库查询没设超时,或者 HTTP 请求没设超时。所有工具调用都要设超时,超时后返回错误而不是一直等。
5.4 排查速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| pip 装不上 | 平台不匹配/依赖缺失 | 单独下载该包看报错 | 下载源码包内网编译 |
| import 报错 | 动态库缺失 | ldd 检查 so 依赖 | 补齐系统库 |
| 模型超时 | 推理慢/并发高 | curl 直测推理服务 | 加大超时/限流 |
| 输出截断 | 量化模型质量下降 | 对比原版输出 | 降温度/加 max_tokens |
| MCP 启动失败 | 路径/权限问题 | 看 stderr 日志 | 用绝对路径/加权限 |
| 工具返回空 | 环境变量缺失 | 检查配置 | 显式声明环境变量 |
| 工具卡住 | 内部阻塞无超时 | 加日志定位 | 所有调用加超时 |
6. 内网 Agent 工程化的经验与避坑
6.1 配置管理要前置
我最早做内网项目时,配置散落在代码各处,部署到新环境要改十几个文件。后来统一用 YAML + 环境变量覆盖,所有环境相关的东西都抽出来。这个习惯在内网场景下价值翻倍,因为内网环境往往有多个(开发、测试、生产),配置管理不好就是灾难。
配置文件的组织建议按环境分目录,公共配置放一份,环境差异放各自目录。启动时根据环境变量加载对应配置。
6.2 日志要能定位问题
内网环境没法用外部日志平台,所有日志要落到本地文件,并且要能按请求追踪。我的做法是每个请求生成一个 trace_id,所有相关日志都带上这个 id。排查问题时用 grep 一把捞出来。
日志级别也要控制好。DEBUG 级别日志量太大,内网磁盘有限。生产环境用 INFO,排查问题时临时开 DEBUG。
6.3 灰度发布不能省
内网 Agent 更新不像公网可以随时回滚,一旦出问题影响面很大。我的做法是先在测试环境跑通,然后生产环境先放 10% 流量,观察一天没问题再全量。灰度期间重点看错误率、响应时间、工具调用成功率。
6.4 工具权限要最小化
Agent 能调用的工具越多,出问题的风险越大。每个工具都要按最小权限原则配置。查询类工具用只读账号,操作类工具加审批,高危工具隔离到沙箱。我见过 Agent 因为工具权限过大误删数据的案例,这个坑一定要提前防。
6.5 模型降级要有预案
内网推理服务可能因为各种原因不可用,Agent 要有降级预案。最简单的降级是返回固定话术,告诉用户服务暂时不可用。好一点的降级是切换到备用模型,虽然质量差一点但能用。
7. 内网 Agent 的扩展方向
7.1 多 Agent 协作
单 Agent 能力有限,复杂任务需要多 Agent 协作。内网环境下多 Agent 的通信可以用内部消息队列,比如 RabbitMQ 或者 Redis Stream。每个 Agent 负责一个子任务,通过消息队列传递中间结果。
多 Agent 的难点是任务分解和结果聚合。任务分解可以用一个 Planner Agent 负责,结果聚合用一个 Aggregator Agent 负责。中间的执行 Agent 只关注自己的子任务。
7.2 RAG 增强
内网 Agent 接内部知识库是刚需。向量库用 Milvus 或者 Qdrant,部署在内网。文档解析、切分、向量化用本地模型,避免依赖外部服务。
RAG 的关键是检索质量。内网文档往往格式不统一,PDF、Word、Excel 都有,解析要分别处理。切分策略也要根据文档类型调整,技术文档按章节切,会议纪要按段落切。
7.3 可观测性建设
内网 Agent 的可观测性靠自建。Prometheus 采集指标,Loki 收集日志,Grafana 做展示。关键指标包括:请求量、响应时间、错误率、模型调用次数、工具调用成功率、Token 消耗量。
指标采集用埋点方式,在 Agent 主循环的关键节点打点。埋点要轻量,不能影响主流程性能。
7.4 安全加固
内网不等于安全,Agent 的安全加固不能省。输入要做注入检测,避免 prompt 注入攻击。输出要做敏感信息过滤,避免泄露内部数据。工具调用要做权限校验,避免越权操作。
安全加固的另一个维度是审计。所有 Agent 的操作都要留痕,包括谁在什么时候调用了什么工具、传了什么参数、返回了什么结果。审计日志单独存储,定期归档。
8. 一些实操中的个人体会
做内网 Agent 这几年,最大的体会是:工程复杂度远大于算法复杂度。模型本身的能力已经够用,真正难的是怎么在受限环境里把整条链路跑通、跑稳。
另一个体会是不要追求一步到位。我见过太多团队想一开始就上全套架构,结果卡在依赖安装阶段。正确的做法是先跑通最小闭环,再逐步增强。先能回答一个问题,再能调用一个工具,再能处理多轮对话,再能并发,再能容错。每一步都验证通过再往下走。
还有一点是文档要跟着代码走。内网环境人员流动时,新人接手全靠文档。部署文档、配置说明、排查手册、架构图,这些看起来费时间,但关键时刻能救命。我的习惯是每完成一个模块就写文档,不等到项目结束再补。
最后分享一个小技巧:内网环境准备一个"应急包",里面放常用的排查工具、依赖包、配置文件模板。遇到问题时不用临时找,直接解压就能用。这个习惯帮我省过好几次通宵。