1. 项目缘起:为什么要在隔离内网里折腾 AI Agent
先交代一下背景。我在一家做企业级软件的公司负责内部工具链建设,去年下半年开始,团队想把 AI Agent 引入到日常研发流程里——代码审查、日志分析、工单自动分类、内部知识库问答,这些场景都挺适合让 Agent 来分担。但问题来了:我们的开发环境是完全隔离的内网,没有外网出口,不能访问任何公有云的大模型 API,也不能随便装外部依赖。
这就意味着,网上那些“五分钟搭一个 Agent”的教程基本全部作废。你不能pip install一个需要联网拉模型的包,不能调用外部推理服务,甚至连 MCP 协议里默认走远程连接的 Server 都得重新设计。所有东西必须在内网自给自足。
我前后折腾了大概三个月,踩了无数坑,最终跑通了一套可用的方案。这篇文章就是把整个过程拆开讲清楚:隔离内网下 AI Agent 工程到底难在哪、怎么选型、怎么落地、怎么排障。适合两类人看——一类是同样在内网环境里做 AI 落地的工程师,另一类是想理解 Agent 工程化到底涉及哪些环节的开发者。不管你现在用的是哪种框架,这里面的思路都能借鉴。
需要先说明一点:本文涉及的模型部署、MCP 协议实现、Skills 编排等内容,都是基于内网离线环境的通用工程实践,不涉及任何特定网络接入手段。所有方案的前提是——你手里已经有可用的内网算力和模型权重。
2. 隔离内网带来的核心约束与整体设计思路
2.1 先搞清楚“隔离”到底隔离了什么
很多人一听到“内网隔离”就笼统地觉得“啥都干不了”,其实要拆开看。隔离环境通常限制的是这几层:
- 网络层:没有公网出口,DNS 不解析外部域名,HTTP/HTTPS 出站被防火墙拦截。
- 依赖层:不能从外部包仓库拉取依赖,npm、pip、maven 这些都得走内网镜像。
- 模型层:不能调用外部推理 API,模型权重必须提前导入。
- 工具层:Agent 需要调用的外部工具(浏览器、数据库客户端、API 网关)都得在内网有对应部署。
这四层里,模型层和工具层是最容易被低估的。很多人以为只要把模型部署到内网就完事了,结果发现 Agent 要调用的工具全在外面,一样跑不起来。
我一开始就犯了这个错。模型用内网的推理服务部署好了,MCP Server 也写好了,结果 Agent 要执行一个“查内部工单系统”的动作,发现工单系统的 API 需要走一个外部网关——直接卡死。后来我们把工单系统的查询接口在内网做了一层代理,才解决。
2.2 整体架构:三层分离
最终我们采用的架构是三层分离:
| 层级 | 职责 | 部署位置 |
|---|---|---|
| 推理层 | 提供 LLM 推理能力 | 内网 GPU 集群 |
| 编排层 | Agent 逻辑、Skills 调度、MCP 通信 | 内网应用服务器 |
| 工具层 | 具体执行单元(代码执行、文件操作、API 调用) | 内网各业务系统 |
这三层之间全部走内网通信,编排层是核心。它负责把用户的自然语言请求拆解成一系列工具调用,然后通过 MCP 协议分发给工具层执行,最后把结果汇总返回。
为什么这么分?因为隔离环境下最怕的就是耦合。如果 Agent 逻辑和工具执行混在一起,一旦某个工具不可用,整个 Agent 就挂了。分层之后,工具层某个服务挂了,编排层可以降级处理,至少保证核心流程不断。
2.3 为什么选 MCP 而不是自己造协议
这里要解释一下 MCP(Model Context Protocol)。简单说,它是一套让 AI 模型和外部工具之间标准化通信的协议。你可以把它理解成“AI 世界的 USB 接口”——不管什么工具,只要实现了 MCP Server,Agent 就能通过统一的方式调用它。
在内网环境里,自己造一套协议也不是不行,但有几个问题:
- 维护成本高:每个工具都要单独适配,工具一多就失控。
- 复用性差:今天给 A 项目写的工具调用逻辑,B 项目用不了。
- 调试困难:没有标准协议,出问题了很难定位是 Agent 的问题还是工具的问题。
MCP 的好处是,它把“工具描述”和“工具调用”标准化了。Agent 只需要知道有哪些工具可用、每个工具接受什么参数,剩下的通信细节由 MCP 协议处理。在内网里,我们把 MCP Server 全部部署在内网服务器上,走内网 WebSocket 或 stdio 通信,完全不依赖外部网络。
注意:MCP 本身是协议标准,不绑定任何特定网络环境。内网部署时,关键是确保 MCP Server 的传输层走内网可达的地址,不要硬编码外部端点。
3. 核心细节解析:模型、MCP、Skills 三件套怎么落地
3.1 内网模型部署:不是跑起来就行
内网部署模型,很多人以为把权重下载下来、起个推理服务就完事了。实际上有几个关键决策点:
第一,模型选型。隔离环境下你没有试错成本,不能今天用 A 模型明天换 B 模型。选型时要考虑:
- 模型大小和显存匹配:7B 模型至少需要 16GB 显存(FP16),量化后可以降到 8GB 左右。
- 推理框架兼容性:vLLM、TGI、Ollama 这些框架在内网的安装难度不同,要提前确认依赖是否齐全。
- 工具调用能力:不是所有模型都擅长 Function Calling,选型时要专门测试这一点。
我们最后选的是一个 14B 级别的模型,做了 4-bit 量化,跑在两张 A 系列卡上。实测下来,工具调用的准确率比 7B 模型高出一大截,尤其是在多步推理场景下。
第二,推理服务的稳定性。内网环境没有云厂商的自动扩缩容,服务挂了就是挂了。我们的做法是:
- 推理服务做双实例部署,前面挂一个内网负载均衡。
- 加健康检查接口,每 30 秒探测一次。
- 设置请求超时和重试机制,避免单个请求卡死整个队列。
第三,Token 限制和上下文管理。内网模型的上下文窗口通常比云端小,我们用的是 8K 上下文。这意味着 Agent 的对话历史不能无限增长,需要做截断或摘要。我们的策略是:保留最近 5 轮对话原文,更早的对话做摘要压缩。
3.2 MCP Server 的内网适配
MCP 协议本身是标准化的,但在内网部署时,有几个地方需要特别注意:
传输层选择。MCP 支持两种传输方式:stdio(标准输入输出)和 WebSocket。内网环境下:
- stdio 适合本地工具,比如文件操作、代码执行,直接在同一台机器上起进程。
- WebSocket 适合远程工具,比如数据库查询、API 调用,需要跨机器通信。
我们大部分工具用的是 stdio,因为部署简单、延迟低。只有少数需要跨机器调用的工具用了 WebSocket,走内网地址。
工具描述的设计。MCP Server 需要向 Agent 描述自己提供哪些工具、每个工具的参数是什么。这个描述直接决定了 Agent 能不能正确调用工具。我们的经验是:
- 工具名称要语义清晰,比如
query_ticket_by_id比get_data好得多。 - 参数描述要包含类型、是否必填、示例值。
- 每个工具最好附带一个使用场景说明,帮助模型理解什么时候该调用它。
下面是一个我们实际使用的 MCP 工具描述示例(JSON 格式):
{ "name": "query_internal_ticket", "description": "根据工单ID查询内部工单系统的详细信息,包括状态、处理人、创建时间", "parameters": { "type": "object", "properties": { "ticket_id": { "type": "string", "description": "工单ID,格式为 TK-开头加8位数字,例如 TK-20240101" } }, "required": ["ticket_id"] } }错误处理。内网工具调用失败是常态——服务可能没启动、网络可能抖动、参数可能不对。MCP Server 必须把错误信息结构化返回,而不是直接抛异常。我们统一了错误返回格式:
{ "success": false, "error_code": "TOOL_TIMEOUT", "error_message": "工单系统查询超时,请稍后重试", "retryable": true }这样 Agent 可以根据retryable字段决定是否重试,而不是直接崩溃。
3.3 Skills 编排:让 Agent 知道“先干什么再干什么”
Skills 这个概念在不同框架里叫法不一样,有的叫“工作流”,有的叫“任务链”。本质上就是把多个工具调用按逻辑顺序编排起来,完成一个复杂任务。
举个例子:用户说“帮我查一下上周所有未处理的工单,按优先级排序”。这个任务需要:
- 调用工单查询工具,获取上周所有工单。
- 过滤出状态为“未处理”的工单。
- 按优先级字段排序。
- 格式化输出。
如果让模型自己一步步推理,很容易漏步骤或者顺序搞错。Skills 的作用就是把这些步骤固化下来,模型只需要判断“用户意图匹配哪个 Skill”,然后按预定义流程执行。
我们的 Skills 定义用的是 YAML 格式,大致长这样:
name: query_unhandled_tickets description: 查询指定时间范围内未处理的工单并按优先级排序 trigger: 用户提到"未处理工单"、"待处理工单"等关键词 steps: - tool: query_internal_ticket params: date_range: "{{user.date_range}}" status: "unhandled" output: ticket_list - tool: sort_by_priority params: list: "{{ticket_list}}" output: sorted_list - tool: format_output params: data: "{{sorted_list}}" format: "table"这里的关键是{{}}模板变量,它把上一步的输出传递给下一步。这种设计让 Skill 变得可组合、可复用。
实操心得:Skills 不要设计得太复杂。我们一开始搞了一个 15 步的 Skill,结果调试了整整两天。后来拆成三个小 Skill,每个不超过 5 步,维护成本直线下降。
4. 实操过程:从零搭建一个内网 Agent 的完整记录
4.1 环境准备清单
在开始之前,你需要确认内网环境里已经具备以下条件:
| 资源类型 | 具体要求 | 检查方式 |
|---|---|---|
| GPU 算力 | 至少 1 张 16GB 显存以上的卡 | nvidia-smi |
| 模型权重 | 已下载到内网存储 | 检查文件是否存在 |
| 推理框架 | vLLM 或同类框架已安装 | 尝试启动服务 |
| Python 环境 | 3.10 以上,依赖已离线安装 | pip list检查 |
| 内网通信 | 各服务之间网络可达 | curl或telnet测试 |
我们当时卡在 Python 依赖上最久。内网 pip 源里缺了好几个包,最后是从外部拷贝 whl 文件进去手动安装的。建议提前把所有依赖列出来,一次性解决。
4.2 第一步:启动内网推理服务
我们用 vLLM 部署模型,启动命令大致如下:
python -m vllm.entrypoints.openai.api_server \ --model /path/to/model/weights \ --served-model-name internal-agent-model \ --host 0.0.0.0 \ --port 8000 \ --max-model-len 8192 \ --gpu-memory-utilization 0.85 \ --dtype float16几个参数解释一下:
--max-model-len 8192:限制上下文长度,避免显存溢出。--gpu-memory-utilization 0.85:留 15% 显存给其他进程,防止 OOM。--dtype float16:如果显存不够,可以改成bfloat16或做量化。
启动后,用curl测试一下:
curl http://localhost:8000/v1/models返回模型列表就说明服务正常。
4.3 第二步:编写第一个 MCP Server
我们以“查询内部知识库”为例,写一个最简单的 MCP Server。用的是 Python 的mcp库(需要提前在内网安装好):
from mcp.server import Server from mcp.types import Tool, TextContent import json app = Server("knowledge-base-server") @app.list_tools() async def list_tools(): return [ Tool( name="search_knowledge", description="在内网知识库中搜索相关文档", inputSchema={ "type": "object", "properties": { "query": {"type": "string", "description": "搜索关键词"}, "limit": {"type": "integer", "description": "返回结果数量", "default": 5} }, "required": ["query"] } ) ] @app.call_tool() async def call_tool(name: str, arguments: dict): if name == "search_knowledge": query = arguments["query"] limit = arguments.get("limit", 5) # 这里调用内网知识库的搜索接口 results = internal_search(query, limit) return [TextContent(type="text", text=json.dumps(results, ensure_ascii=False))]这个 Server 启动后,Agent 就能通过 MCP 协议调用search_knowledge工具了。
4.4 第三步:Agent 编排逻辑
Agent 的核心逻辑是:接收用户输入 → 判断意图 → 选择 Skill 或直接调用工具 → 执行 → 返回结果。
我们用 Python 写了一个简单的编排器:
class InternalAgent: def __init__(self, llm_client, mcp_clients, skills): self.llm = llm_client self.mcp = mcp_clients self.skills = skills async def run(self, user_input: str): # 第一步:意图识别 intent = await self.llm.classify(user_input, self.skills.keys()) # 第二步:如果匹配到 Skill,按 Skill 流程执行 if intent in self.skills: return await self.execute_skill(self.skills[intent], user_input) # 第三步:没有匹配 Skill,走通用工具调用 return await self.general_tool_call(user_input) async def execute_skill(self, skill, user_input): context = {"user_input": user_input} for step in skill["steps"]: tool_name = step["tool"] params = self.resolve_params(step["params"], context) result = await self.mcp.call(tool_name, params) context[step["output"]] = result return context[skill["steps"][-1]["output"]]这段代码的关键是resolve_params,它负责把模板变量替换成实际值。比如{{user.date_range}}会被替换成用户输入里提取的时间范围。
4.5 第四步:联调与验证
联调阶段最容易出问题。我们的经验是从最简单的场景开始,逐步增加复杂度:
- 先测试单个工具调用:直接让 Agent 调用
search_knowledge,看能不能返回结果。 - 再测试 Skill 编排:用一个两步的 Skill,确认参数传递正确。
- 最后测试多 Skill 切换:给 Agent 多个 Skill,看意图识别准不准。
每一步都要记录日志,包括:用户输入、意图识别结果、工具调用参数、工具返回结果、最终输出。这样出问题了才能快速定位。
5. 常见问题与排查技巧实录
5.1 工具调用失败排查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Agent 不调用工具 | 工具描述不清晰 | 检查 MCP 工具描述 | 补充使用场景说明 |
| 调用参数错误 | 参数类型不匹配 | 打印实际调用参数 | 在描述中明确类型和示例 |
| 工具返回超时 | 内网服务响应慢 | 检查工具服务日志 | 增加超时时间或重试 |
| 结果解析失败 | 返回格式不标准 | 检查返回 JSON 结构 | 统一返回格式 |
| Skill 执行中断 | 某一步骤失败 | 查看步骤执行日志 | 增加错误处理和降级 |
5.2 几个我踩过的坑
坑一:模型不认识工具名称。我们一开始用了一些缩写工具名,比如qry_tkt,结果模型完全不知道这是干什么的。后来改成query_ticket,调用准确率立刻上来了。工具名称一定要用完整的英文单词,不要缩写。
坑二:MCP Server 启动顺序问题。Agent 启动时如果 MCP Server 还没起来,会直接报错退出。我们的解决方案是:Agent 启动时先做一次工具发现,如果某个 Server 不可用,记录警告但不退出,等实际调用时再重试。
坑三:上下文溢出。有一次用户连续问了十几个问题,对话历史把 8K 上下文占满了,模型开始胡言乱语。后来加了上下文管理逻辑:超过 6K token 就自动截断最早的对话。
坑四:并发调用冲突。多个用户同时调用同一个工具时,如果工具内部有状态,会出现数据混乱。我们的做法是:所有工具设计成无状态的,每次调用传入完整参数,不依赖上一次调用的结果。
独家技巧:在内网环境里,建议给每个 MCP Server 加一个
/health接口,Agent 定期探测。这样可以在工具不可用时提前降级,而不是等到用户请求失败了才发现。
5.3 性能优化建议
内网环境的算力通常有限,性能优化很重要。我们做了这几件事:
- 批量推理:把多个小请求合并成一个批次,提高 GPU 利用率。
- 缓存工具结果:对于查询类工具,相同参数的请求在 5 分钟内直接返回缓存。
- 异步调用:多个不相关的工具调用并行执行,减少总耗时。
- 模型量化:从 FP16 降到 4-bit,显存占用减少 60%,推理速度提升约 30%。
实测下来,优化前一个复杂 Skill 平均耗时 12 秒,优化后降到 4 秒左右。
6. 内网 Agent 工程的扩展方向
跑通基础流程之后,我们陆续做了一些扩展,这里简单提几个方向,给有类似需求的同学参考。
第一个方向是多 Agent 协作。单个 Agent 处理复杂任务时容易顾此失彼,我们尝试了“规划 Agent + 执行 Agent”的模式:规划 Agent 负责拆解任务,执行 Agent 负责具体工具调用。两个 Agent 通过内网消息队列通信。实测下来,复杂任务的完成率提升了大概 20%。
第二个方向是 Skills 的动态加载。一开始 Skills 是硬编码在配置文件里的,每次新增都要重启服务。后来改成了从内网配置中心动态拉取,支持热更新。这样业务方可以自己定义 Skill,不需要我们介入。
第三个方向是调用链追踪。内网环境出问题了很难排查,我们加了一套轻量级的追踪机制:每次 Agent 调用生成一个 trace_id,所有工具调用日志都带上这个 ID。出问题时,通过 trace_id 就能把整个调用链串起来。
第四个方向是权限控制。不同用户能调用的工具应该不一样。我们在 MCP 层加了权限校验,每个工具调用前先检查用户是否有权限。这个在内网环境里尤其重要,因为内网系统往往涉及敏感数据。
这些扩展不是必须的,但如果你打算把 Agent 真正用到生产环境,迟早会遇到这些问题。我的建议是:先把核心流程跑通,再根据实际需求逐步扩展,不要一开始就追求大而全。
最后分享一个我在内网部署时总结的小经验:所有配置都要有默认值,所有外部依赖都要有降级方案。内网环境的不确定性比外网高得多,一个服务挂了可能半天没人发现。把容错做在前面,后面能省很多事。