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 而不是自己造协议这里要解释一下 MCPModel Context Protocol。简单说它是一套让 AI 模型和外部工具之间标准化通信的协议。你可以把它理解成“AI 世界的 USB 接口”——不管什么工具只要实现了 MCP ServerAgent 就能通过统一的方式调用它。在内网环境里自己造一套协议也不是不行但有几个问题维护成本高每个工具都要单独适配工具一多就失控。复用性差今天给 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( namesearch_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(typetext, textjson.dumps(results, ensure_asciiFalse))]这个 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 真正用到生产环境迟早会遇到这些问题。我的建议是先把核心流程跑通再根据实际需求逐步扩展不要一开始就追求大而全。最后分享一个我在内网部署时总结的小经验所有配置都要有默认值所有外部依赖都要有降级方案。内网环境的不确定性比外网高得多一个服务挂了可能半天没人发现。把容错做在前面后面能省很多事。