ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

隔离内网下AI Agent工程化实战:MCP与Skills架构落地

隔离内网下AI Agent工程化实战:MCP与Skills架构落地 1. 为什么“隔离内网”是 AI Agent 工程化的真正分水岭很多人第一次听到“隔离内网下跑 AI Agent”第一反应是不就是把模型换成本地部署、把网络请求掐掉吗真做过一轮的人都知道事情远没有这么简单。隔离内网意味着你面对的不是“网络慢一点”或者“偶尔断一下”而是整条依赖链被物理切断——模型权重下载不了、包管理器的源连不上、外部 API 一个都调不通、连查个文档都得靠离线镜像。这时候你才会发现平时那些“一行 pip install 搞定”的顺滑体验其实是建立在无数外部服务之上的。我之所以说这是分水岭是因为它把 AI Agent 从“Demo 玩具”逼成了“工程系统”。在外网环境里你可以用托管的大模型服务、可以随时调用各种 SaaS 工具、可以用现成的向量数据库云服务Agent 的“智能”很大一部分是外部能力堆出来的。但到了隔离内网这些全没了你必须自己回答几个硬问题模型从哪来、工具怎么接、状态怎么存、并发怎么扛、出问题怎么查。这些问题的答案才是一个 AI Agent 工程真正的骨架。这篇文章面向的是需要在隔离内网环境里落地 AI Agent 的工程师不管你是做企业内网工具、工业现场系统还是任何不能连外网的场景。我会围绕AI Agent、MCP、Skills、内网、工程实战这几个核心点把从架构设计到并发处理、从工具协议到技能封装的完整链路讲清楚。里面既有我踩过的坑也有可以直接抄的配置和代码。读完之后你应该能独立在内网里搭出一套能跑、能扛、能维护的 Agent 系统而不是一个只能演示五分钟的玩具。先说一个反直觉的结论隔离内网下Agent 的瓶颈几乎从来不是模型本身而是工具接入和状态管理。模型再强如果它调不动内网里的业务系统那它就是个聊天框状态管不好多轮对话一并发就串味。后面几个章节我会把这两块拆开讲透。2. 内网 Agent 的架构选型为什么我最终选了“本地模型 MCP Skills”三层结构2.1 三种常见架构的对比与取舍在隔离内网里搭 Agent我实际试过三种架构各有各的坑。第一种是纯本地大模型 硬编码工具调用。就是把模型跑在内网服务器上工具调用逻辑直接写死在代码里if-else 判断意图然后执行。这种方案上手最快但扩展性极差。每加一个工具就要改一次主流程工具多了之后代码变成一团乱麻而且模型换个版本可能意图识别就崩了。我早期做过一个内网工单助手接了 7 个工具之后主流程文件超过 2000 行维护成本高到离谱。第二种是本地模型 自研工具协议。自己定义一套工具描述格式和调用约定模型输出结构化 JSON框架解析后路由到对应工具。这比硬编码好很多但问题在于你等于重新发明了一套协议生态为零每个工具都要自己写适配层而且不同模型对 JSON 格式的遵循程度参差不齐解析失败率不低。第三种就是我最终采用的本地模型 MCP Skills 三层结构。MCP 负责“工具怎么被描述和调用”Skills 负责“一类任务怎么被封装和复用”本地模型负责“理解和决策”。这三层各司其职解耦得很干净。下面这张表是我对三种方案的实际对比维度硬编码工具调用自研工具协议本地模型 MCP Skills上手速度快中中偏慢扩展性差中好工具复用几乎不可能需手动适配天然支持模型兼容性强绑定中好协议层隔离内网适配难度低中中需离线部署 MCP 服务长期维护成本高中低选第三层的核心理由是MCP 把“工具”标准化了Skills 把“能力”模块化了。在内网这种没法随时拉新依赖的环境里标准化和模块化带来的复用价值远大于前期多花的那点搭建时间。2.2 MCP 在内网里到底解决了什么问题MCP 是什么用一句话说它是模型和外部工具之间的“通用插座”。模型不需要知道工具内部怎么实现只需要知道这个工具叫什么、接受什么参数、返回什么格式。工具也不需要知道模型是谁只要按 MCP 协议暴露自己的能力就行。在内网环境里MCP 的价值被放大了。因为内网里的业务系统五花八门——可能有老的 SOAP 接口、有只提供 JDBC 的数据库、有只能命令行调用的遗留系统。如果没有 MCP每接一个系统都要在 Agent 主流程里写一堆适配代码。有了 MCP你可以为每个系统写一个 MCP Server把它的能力包装成标准工具Agent 侧完全不用改。我实际的做法是每个内网业务系统对应一个独立的 MCP Server 进程通过 stdio 或本地 HTTP 通信。这样即使某个系统挂了也只影响它自己的工具不会拖垮整个 Agent。而且 MCP Server 可以独立重启、独立升级运维上清爽很多。2.3 Skills 层把“一类任务”封装成可复用单元Skills 和 MCP 的区别很多人一开始会搞混。我的理解是MCP 是“工具级”的封装Skills 是“任务级”的封装。一个 Skill 可能内部调用好几个 MCP 工具加上自己的提示词、流程控制和校验逻辑最终对外表现为“一个能完成某类任务的能力”。举个例子内网里有个常见需求是“根据工单内容自动分类并派发”。这个任务拆开看需要读取工单MCP 工具、查询分类规则MCP 工具、调用模型判断本地模型、写回派发结果MCP 工具。如果每次都让 Agent 自己串这些步骤提示词会非常长而且容易漏步骤。把它封装成一个 Skill 之后Agent 只需要说“执行工单派发 Skill”剩下的流程由 Skill 内部编排。Skills 的另一个好处是测试和迭代更聚焦。你可以单独测一个 Skill 的输入输出不用每次都跑整个 Agent。在内网这种调试手段有限的环境里能单独测一个模块效率提升非常明显。2.4 本地模型的选型与量化取舍内网里模型必须本地跑选型主要看三点显存占用、推理速度、指令遵循能力。我实测下来7B 到 14B 量级的模型在单张 24G 显存的卡上比较舒服量化到 4bit 之后显存占用能压到 8G 以内留出空间给上下文和并发。量化方式上我推荐GPTQ 或 AWQ 的 4bit 量化精度损失在可接受范围内推理速度比 FP16 快不少。GGUF 格式适合 CPU 推理但内网如果有 GPU还是走 GPU 路线更稳。这里有个经验不要盲目追求大模型在内网 Agent 场景里一个指令遵循好的 7B 模型往往比一个指令遵循差的 70B 模型更实用因为 Agent 的核心是“按格式调工具”不是“写诗”。模型部署我用的是 vLLM 或 TGI 这类推理框架它们对并发支持好能动态批处理。内网里没有外网依赖这些框架的镜像需要提前准备好后面章节会讲离线部署的细节。3. 隔离环境下的依赖准备离线包、镜像与模型权重怎么搬进去3.1 离线依赖清单的梳理方法内网部署最痛苦的一步就是“搬依赖”。我的做法是先在一台能联网的机器上把整个运行环境完整跑通一遍然后用工具把依赖全部导出。具体来说Python 环境用pip download把 wheel 包全下下来系统级依赖用apt-get download或直接打包 debDocker 镜像用docker save导出成 tar。这里有个坑很多包在安装时会动态下载额外资源比如某些 NLP 库会下载词典、某些模型库会下载配置。这些动态下载在内网里会直接失败。我的应对方法是在联网机器上跑一遍完整流程用抓包或日志把所有外部请求记下来然后逐个找离线替代方案。这个过程很枯燥但省不得。3.2 模型权重的分片搬运与校验模型权重动辄几个 G 到几十个 G直接拷贝容易出错。我一般用split把大文件切成 2G 左右的分片搬进去之后再用cat合并最后用sha256sum校验完整性。校验这一步千万别省我遇到过好几次因为传输中断导致权重文件损坏模型加载时报的错五花八门排查半天才发现是文件问题。# 联网机器上分片 split -b 2G model.safetensors model_part_ # 内网机器上合并 cat model_part_* model.safetensors # 校验 sha256sum model.safetensors3.3 内网 pip 源的搭建如果内网有多台机器要装同样的依赖搭一个本地 pip 源会省很多事。我用的是devpi或简单的nginx静态文件服务把下载好的 wheel 包放上去内网机器配置pip.conf指向这个源就行。# /etc/pip.conf [global] index-url http://内网源地址/simple/ trusted-host 内网源地址这样后续加新依赖时只要在联网机器上下好包丢进源里内网直接pip install就行不用每次手动搬。3.4 Docker 镜像的离线导入如果整个 Agent 跑在容器里镜像的离线导入是必须的。docker save导出的 tar 可能很大同样可以分片。导入时用docker load。这里注意一点镜像里的基础镜像层如果内网没有load 的时候会失败所以要么把基础镜像也一起 save要么用docker save时确保包含所有层。我一般会维护一个“基础镜像包”里面包含 Python、CUDA、常用系统库这些不常变的东西业务镜像基于它构建这样每次只需要搬业务层体积小很多。4. MCP Server 在内网里的落地从协议到进程管理4.1 MCP 通信方式的选择stdio 还是本地 HTTPMCP 支持多种通信方式内网里我主要用两种stdio 和本地 HTTP。stdio 方式下MCP Server 作为 Agent 的子进程启动通过标准输入输出通信。优点是简单、无需网络配置、进程生命周期由 Agent 管理。缺点是每个 Server 一个进程工具多了之后进程数会很多而且 Server 崩溃会直接影响 Agent。本地 HTTP 方式下MCP Server 独立跑在一个端口上Agent 通过 HTTP 调用。优点是解耦彻底、可以独立重启、方便多 Agent 共享。缺点是要管端口、要处理服务发现。我的选择是核心高频工具用 stdio重量级或共享工具用本地 HTTP。比如读写内网数据库这种高频操作stdio 延迟低而像文档检索这种可能多个 Agent 共用的服务走 HTTP 更合适。4.2 一个内网 MCP Server 的最小实现下面是一个用 Python 写的 MCP Server 骨架暴露一个“查询工单”的工具。注意这里没有用任何外部网络依赖纯本地逻辑。from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent import sqlite3 app Server(ticket-server) app.list_tools() async def list_tools(): return [ Tool( namequery_ticket, description根据工单ID查询工单详情, inputSchema{ type: object, properties: { ticket_id: {type: string, description: 工单ID} }, required: [ticket_id] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name query_ticket: conn sqlite3.connect(/data/tickets.db) cur conn.execute( SELECT id, title, status, content FROM tickets WHERE id ?, (arguments[ticket_id],) ) row cur.fetchone() conn.close() if row: return [TextContent(typetext, textf工单{row[0]}: {row[1]}, 状态{row[2]}, 内容{row[3]})] return [TextContent(typetext, text未找到该工单)] async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ __main__: import asyncio asyncio.run(main())这个 Server 的关键点是工具描述要写清楚因为模型就是靠 description 来判断什么时候调用这个工具的。description 写得含糊模型就会乱调或者不调。4.3 MCP Server 的进程守护与健康检查内网环境里MCP Server 挂了没人知道是常态。我的做法是给每个 HTTP 方式的 Server 加一个健康检查端点然后用一个简单的守护脚本定期探测挂了就重启。#!/bin/bash # health_check.sh while true; do if ! curl -s http://localhost:8081/health /dev/null; then echo $(date): MCP Server 8081 无响应重启中 pkill -f mcp_server_8081 nohup python mcp_server_8081.py /var/log/mcp_8081.log 21 fi sleep 30 donestdio 方式的 Server 由 Agent 框架管理生命周期相对省心但要注意 Agent 重启时子进程要正确清理否则会留下僵尸进程。4.4 工具描述的质量直接决定 Agent 的可用性这一点我要单独强调因为它太容易被忽视了。模型选不选一个工具、选得对不对90% 取决于工具描述的质量。我见过太多人把 description 写成“查询数据”这种废话然后抱怨模型不听话。好的工具描述应该包含这个工具做什么、什么场景下用、参数的含义和格式、返回值的结构、有什么限制。比如“查询工单”这个工具描述里应该说明“当用户提到工单号、需要查看工单状态时使用”参数说明里要写“ticket_id 是字符串格式的工单编号如 T20240101”。我实测下来把工具描述从一句话扩充到三四句话模型的工具调用准确率能提升 20% 以上。这个投入产出比非常高。5. Skills 工程化把内网业务逻辑封装成可测试的单元5.1 Skill 的目录结构与元数据设计一个 Skill 在内网里应该是一个自包含的目录包含提示词、流程定义、依赖的工具列表、测试用例。我用的结构是这样的skills/ ticket_dispatch/ skill.yaml # 元数据名称、描述、依赖工具 prompt.md # 该 Skill 的提示词模板 flow.py # 流程编排逻辑 tests/ case_01.json # 输入输出测试用例skill.yaml里声明这个 Skill 依赖哪些 MCP 工具Agent 加载时会检查这些工具是否可用不可用就禁用这个 Skill。这样避免了“Skill 加载了但工具没接上”的运行时错误。5.2 Skill 的流程编排什么时候用代码什么时候用提示词Skill 内部有两种编排方式代码编排和提示词编排。代码编排就是用 Python 写死流程模型只在需要判断的地方介入提示词编排是把整个流程交给模型用提示词引导它一步步走。我的经验是确定性步骤用代码判断性步骤用提示词。比如“先查工单再查规则再判断分类”这个流程查工单和查规则是确定的用代码调 MCP 工具判断分类需要模型理解语义用提示词让模型输出分类结果。这样既保证了流程稳定又保留了模型的灵活性。5.3 Skill 的测试内网里怎么验证一个 Skill 是对的内网里没有方便的在线调试工具所以 Skill 的测试必须自动化。我给每个 Skill 写一组测试用例输入是模拟的用户请求输出是期望的工具调用序列和最终结果。测试时用一个 mock 的模型响应避免每次测试都跑真实模型。# test_ticket_dispatch.py def test_dispatch_flow(): mock_llm_response {category: 网络故障, priority: 高} result run_skill(ticket_dispatch, input{ticket_id: T001}, mock_llmmock_llm_response) assert result[category] 网络故障 assert result[assigned_to] 网络组这种测试跑起来很快改完 Skill 立刻能验证比手动在界面上点半天高效得多。5.4 Skill 的版本管理与灰度内网里 Skill 更新也是个问题。我的做法是给每个 Skill 加版本号Agent 加载时可以选择版本。新版本先在一个测试 Agent 上跑验证没问题再切到生产。这样即使新版本有问题也能快速回滚。版本管理用简单的目录命名就行比如ticket_dispatch_v1、ticket_dispatch_v2配置里指定用哪个版本。不需要上复杂的版本控制系统内网环境越简单越可靠。6. 并发扛压内网 Agent 在高并发下的真实表现与调优6.1 并发瓶颈到底在哪模型、工具还是状态很多人一提到并发就想着加机器但实际测下来内网 Agent 的并发瓶颈往往不在模型推理而在工具调用的串行化和状态管理的锁竞争。我做过一个压测单张 24G 卡跑 7B 模型vLLM 动态批处理下模型侧能扛住 50 并发左右。但整个 Agent 系统在 20 并发时就开始出现明显延迟排查发现是 MCP 工具调用排队——每个工具调用都要等前一个完成因为工具内部用了全局锁。所以调优的第一步是定位瓶颈而不是盲目扩容。用日志记录每个环节的耗时很快就能看出卡在哪。6.2 模型推理的并发优化批处理与流式输出模型侧我用 vLLM开启连续批处理continuous batching这样多个请求可以共享一次前向计算。关键参数是max_num_seqs和max_num_batched_tokens前者控制同时处理的序列数后者控制批处理的 token 总量。内网里显存有限这两个值要调平衡太大容易 OOM太小并发上不去。流式输出对用户体验影响很大。Agent 调工具时可能需要几秒如果等全部完成再返回用户会觉得卡死。开启流式之后模型一有输出就推给前端感知延迟低很多。6.3 工具调用的并发控制连接池与超时MCP 工具调用要加连接池和超时。数据库类工具用连接池避免频繁建连HTTP 类工具设置合理的超时避免一个慢请求拖垮整个 Agent。# 工具调用超时控制 import asyncio async def call_tool_with_timeout(tool_name, args, timeout10): try: return await asyncio.wait_for( mcp_client.call_tool(tool_name, args), timeouttimeout ) except asyncio.TimeoutError: return {error: f工具 {tool_name} 调用超时}超时时间要根据工具的实际耗时设置数据库查询可能 1 秒够文档检索可能要 5 秒。设置太短会误杀正常请求太长会拖慢整体。6.4 会话状态的隔离多用户并发不串味多用户并发时会话状态必须严格隔离。我的做法是每个会话一个独立的 context 对象包含对话历史、当前 Skill 状态、临时变量。这些 context 存在内存里用会话 ID 索引请求进来先取 context处理完写回。这里有个坑如果用了全局的模型实例要注意模型本身是无状态的但提示词拼接是有状态的。每次请求都要用当前会话的 context 拼提示词不能复用上一个请求的。我早期就犯过这个错导致 A 用户的对话历史串到了 B 用户那里排查了很久。6.5 压测数据与调优前后的对比我在内网环境做了一轮压测配置是单卡 24G、7B 4bit 模型、5 个 MCP 工具。调优前后的数据对比指标调优前调优后20 并发平均延迟8.5s2.3s50 并发成功率62%94%工具调用平均耗时1.8s0.4s内存峰值22G18G调优的主要动作是工具调用加连接池、模型开连续批处理、会话状态改成本地存储、超时时间按工具分类设置。这些改动都不复杂但效果很明显。7. 内网 Agent 的可观测性没有外网监控怎么排查问题7.1 日志分级与结构化内网里没有现成的监控平台日志就是最重要的排查手段。我的做法是结构化日志每条日志是 JSON 格式包含时间戳、会话 ID、环节、耗时、结果状态。这样用grep和jq就能快速过滤分析。{ts: 2024-01-15T10:23:45, session: s001, stage: tool_call, tool: query_ticket, duration_ms: 320, status: ok}关键是每个环节都要打点请求进来、模型推理开始/结束、每个工具调用开始/结束、Skill 执行开始/结束。这样出问题时能一眼看出卡在哪。7.2 关键指标的手动采集没有 Prometheus 这类工具就用脚本定期采集关键指标写进文件。我采集的指标包括当前活跃会话数、模型推理队列长度、各工具调用成功率、平均延迟。这些数据用简单的 Python 脚本每分钟采一次存成 CSV出问题时画个图就能看出趋势。7.3 常见故障的排查链路内网 Agent 最常见的故障有三类模型不响应、工具调用失败、会话状态错乱。我的排查链路是模型不响应先看推理服务进程是否活着再看显存是否爆了最后看请求队列是否堆积。工具调用失败先看 MCP Server 进程再看工具内部日志最后看参数是否正确。会话状态错乱先看 context 的读写日志确认是不是并发写冲突。这个链路我写成了一个排查清单贴在运维文档里新人照着走就能定位大部分问题。8. 我在内网 Agent 实战中踩过的几个坑第一个坑是模型加载时的隐式网络请求。有些模型库在加载时会尝试连外网检查更新内网里会卡住直到超时。解决办法是设置环境变量禁用这些检查或者用离线模式启动。第二个坑是工具描述里的示例误导模型。我在一个工具描述里写了个示例参数结果模型不管什么情况都用那个示例值。后来把示例去掉改成参数格式说明问题就解决了。工具描述里尽量别放具体值放格式说明。第三个坑是并发下的日志交错。多个请求同时写日志如果不加锁日志会串行错乱。我后来改成每个会话写独立日志文件或者用队列异步写问题才解决。第四个坑是Skill 的隐式依赖。一个 Skill 依赖某个 MCP 工具但工具没启动Skill 加载时不报错运行时才失败。后来我在 Skill 加载时加了依赖检查缺工具直接禁用并告警避免运行时才发现。这些坑的共同点是在内网环境里任何“隐式”的东西都会变成问题。外网环境下自动完成的事情内网里都要显式处理。想清楚这一点很多坑就能提前避开。最后分享一个实用技巧在内网部署前先在联网环境用防火墙模拟断网把所有外部请求都拦掉跑一遍完整流程。这样能提前发现大部分离线适配问题比搬进内网再排查高效得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进