
1. 隔离内网下的 AI Agent 工程到底在做什么先把场景说清楚。所谓“隔离内网”就是一台或者一组机器没有公网出口不能直接访问外部模型 API不能随手pip install一个包就完事甚至连 GitHub 都打不开。你要在这样的环境里把一个 AI Agent 从零搭起来让它能跑工具调用、能读本地知识、能执行多步任务还要保证它稳定、可维护、可扩展。这件事的难度不在于“Agent 是什么”而在于“什么都缺”的情况下你怎么把工程做扎实。我过去一年里前前后后在三个不同规模的隔离环境里落地过 AI Agent 项目一个是制造业的质检知识助手一个是金融后台的工单自动分派还有一个是研发内部的代码审查辅助。这三个场景有一个共同点——模型不能出网工具不能随便装日志不能外传。所以整套方案必须围绕“离线可用、依赖可控、链路可观测”来设计。这篇文章就把我踩过的坑、验证过的方案、以及那些文档里不会写的细节完整地摊开讲一遍。如果你正在面对类似的环境或者你正准备把一个 Agent 从公网 Demo 搬到内网生产那这篇内容应该能帮你省掉至少两周的试错时间。我会从整体架构讲到具体实现从 MCP 协议讲到 Skills 设计从并发扛压讲到故障排查尽量做到你照着做就能跑起来。2. 整体架构设计与技术选型思路2.1 为什么不能照搬公网方案公网环境下搭 Agent大家习惯的做法是调 OpenAI 或者 Claude 的 API用 LangChain 串起来工具调用直接走 HTTP向量库用 Pinecone 或者 Weaviate 的云服务。这套方案在隔离内网里几乎全部失效。模型 API 调不通云向量库连不上连 LangChain 的某些依赖包都可能因为缺少网络而装不上。所以第一件事是转变思路把 Agent 当成一个“离线可运行的单体服务”来设计而不是一个“编排云服务的胶水层”。这意味着模型要本地部署向量库要本地跑工具调用要走进程内或者本地 socket整个链路的每一个环节都要能在没有外网的情况下自洽运行。我试过两种极端方案。一种是“全本地化”模型用 Ollama 或者 vLLM 部署向量库用 Chroma 或者 Milvus 本地版工具全部写成 Python 函数直接注册。另一种是“内网服务化”在内网里单独搭一套模型推理服务、一套向量检索服务、一套工具网关Agent 本身只做编排。实测下来中小规模场景用第一种更省事大规模多团队共用场景用第二种更合理。2.2 核心组件拆解与选型对照一个完整的隔离内网 Agent 工程核心组件大概分五层模型推理层、编排层、工具层、记忆层、观测层。每一层的选型都会直接影响后续的维护成本。层级公网常见方案内网推荐方案选型理由模型推理OpenAI APIvLLM / Ollama / TGI本地部署支持并发可量化编排框架LangChain / LlamaIndexLangGraph / 自研轻量编排依赖少可控性强工具协议OpenAPI / HTTPMCP / 本地函数注册标准化易扩展向量存储Pinecone / WeaviateChroma / Milvus / FAISS本地持久化无网络依赖观测追踪LangSmith本地日志 Prometheus数据不出内网这里重点说一下 MCP。MCP 是 Model Context Protocol 的缩写本质上是一套让模型和外部工具、数据源之间标准化通信的协议。你可以把它理解成“AI 世界的 USB 接口”——不管对面是数据库、文件系统还是某个内部系统只要按 MCP 规范暴露能力Agent 就能统一调用。在内网环境里MCP 的价值特别大因为它把工具接入这件事从“每个工具写一套适配代码”变成了“按协议注册一次到处复用”。2.3 编排层为什么选 LangGraph 而不是纯 LangChainLangChain 的 Chain 模式适合线性流程但 Agent 的本质是“循环决策”——思考、调工具、观察结果、再思考。LangGraph 用图结构来表达这种循环节点是动作边是条件跳转状态在节点之间传递。这个模型跟 Agent 的实际运行方式更贴合。更重要的是LangGraph 的状态管理是显式的。你可以清楚地看到每一步的输入输出方便在内网环境里做审计和回放。我试过用纯 LangChain 的 AgentExecutor 做多步任务调试的时候非常痛苦因为中间状态藏在回调里日志打不全。换成 LangGraph 之后每个节点的输入输出都能落盘排查问题效率至少提升一倍。3. MCP 协议在内网环境下的落地细节3.1 MCP 的核心机制与内网适配MCP 的基本模型是 Client-Server 架构。Agent 作为 Client工具提供方作为 Server。Server 暴露三类能力Resources资源比如文件、数据库记录、Tools可执行动作、Prompts预置提示模板。Client 通过标准化的 JSON-RPC 消息跟 Server 通信。在内网环境里MCP Server 的部署方式需要调整。公网环境下大家习惯用 SSEServer-Sent Events做传输因为可以跨网络。但内网里更推荐用 stdio 模式——Server 作为子进程启动通过标准输入输出跟 Client 通信。这样做的好处是不需要开端口不需要处理网络认证进程生命周期跟 Agent 绑定天然隔离。我实际部署的时候把每个 MCP Server 写成一个独立的 Python 脚本用mcp官方库的Server类注册工具然后通过stdio_server启动。Agent 侧用mcp的ClientSession连接。整个链路没有任何网络调用全部走管道。# mcp_server_example.py from mcp.server import Server from mcp.server.stdio import stdio_server app Server(internal-tools) app.tool() async def query_ticket(ticket_id: str) - str: 根据工单号查询内部工单系统 # 这里调用内网数据库或内部 API result internal_db.query(ticket_id) return result 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())这段代码的关键点在于工具函数的 docstring 会被 MCP 自动提取成工具描述模型根据这个描述来决定什么时候调用。所以 docstring 要写得清楚、具体不要写“查询数据”这种模糊描述要写“根据工单号查询内部工单系统的详细信息包括状态、处理人、创建时间”。3.2 工具注册的粒度控制一个常见的坑是工具注册得太粗或者太细。太粗的话一个工具干太多事模型不知道该传什么参数太细的话工具数量爆炸模型选择困难。我的经验是按“业务动作”来切分而不是按“技术接口”来切分。比如“查询工单”和“更新工单状态”应该是两个工具而不是一个“工单操作”工具带一个 action 参数。因为模型在决策时是根据“我要做什么”来选工具的不是根据“我要操作哪个资源”来选的。另外每个工具的参数不要超过 5 个。超过 5 个参数的工具模型调用准确率会明显下降。如果确实需要很多参数考虑拆成多个步骤或者把部分参数做成有默认值的可选参数。3.3 MCP Server 的生命周期管理在内网环境里MCP Server 的启动和关闭需要特别注意。如果用 stdio 模式Server 是 Agent 的子进程Agent 退出时 Server 会自动结束。但如果 Server 里有连接池或者缓存需要在关闭时做清理。我遇到过一个典型问题MCP Server 里维护了一个数据库连接池Agent 重启后旧连接没有释放导致数据库连接数慢慢涨上去。后来在 Server 里加了atexit钩子确保进程退出时关闭连接池。这个细节在文档里不会写但生产环境里必须处理。4. Skills 体系的设计与工程化落地4.1 Skills 到底是什么跟 MCP 什么关系Skills 这个词最近很热但很多人把它跟 MCP 搞混。简单说MCP 解决的是“Agent 怎么调用外部能力”的问题Skills 解决的是“Agent 怎么组织自己的行为”的问题。一个 Skill 可以理解成一套预定义的工作流或者提示模板告诉 Agent 在特定场景下应该按什么步骤做事。举个例子你有一个“代码审查”Skill它定义了审查代码时应该先看什么、再看什么、输出格式是什么。这个 Skill 可能内部会调用多个 MCP 工具比如读文件、查规范、跑静态检查。但 Skill 本身不是工具它是工具的组合方式和行为指导。在内网环境里Skills 的价值在于“把领域知识固化下来”。因为内网环境通常面向特定业务通用模型对业务的理解有限通过 Skills 把业务规则、操作流程、输出规范写清楚能大幅提升 Agent 的可用性。4.2 Skill 的目录结构与加载机制我采用的 Skill 组织方式参考了社区里比较流行的做法每个 Skill 一个目录目录里放一个SKILL.md描述文件加上可选的脚本和资源文件。skills/ ├── code_review/ │ ├── SKILL.md │ ├── checklist.md │ └── scripts/ │ └── run_lint.py ├── ticket_triage/ │ ├── SKILL.md │ └── rules.yaml └── knowledge_qa/ ├── SKILL.md └── faq_index.jsonSKILL.md的结构大概是这样# Code Review Skill ## 适用场景 当用户请求审查代码、检查代码质量、或者提交代码审查任务时使用。 ## 执行步骤 1. 读取目标文件内容 2. 对照 checklist.md 中的检查项逐条核对 3. 调用 run_lint.py 执行静态检查 4. 汇总结果按严重程度排序输出 ## 输出格式 - 问题等级严重 / 警告 / 建议 - 问题位置文件路径 行号 - 问题描述一句话说明 - 修复建议具体可操作的修改方案Agent 启动时扫描 skills 目录把所有 SKILL.md 加载到上下文里。当用户请求匹配到某个 Skill 的适用场景时Agent 就按 Skill 定义的步骤执行。4.3 Skill 与 MCP 工具的协同Skill 和 MCP 工具的关系是“编排”和“被编排”。Skill 定义流程MCP 工具提供具体能力。比如代码审查 Skill 里说“调用 run_lint.py 执行静态检查”这个 run_lint.py 可以注册成一个 MCP 工具也可以直接作为 Skill 目录下的脚本被调用。我倾向于把通用能力注册成 MCP 工具把 Skill 专属的逻辑放在 Skill 目录下的脚本里。这样通用工具可以跨 Skill 复用专属逻辑又不会污染工具列表。有一个细节需要注意Skill 的加载顺序和优先级。如果多个 Skill 的适用场景有重叠需要定义优先级规则。我的做法是在 SKILL.md 里加一个priority字段数值越大优先级越高。Agent 匹配到多个 Skill 时选优先级最高的执行。5. 并发场景下的 Agent 扛压实战5.1 并发瓶颈到底在哪里很多人一提到 Agent 并发第一反应是模型推理扛不住。但实际上在我压测过的几个系统里瓶颈往往不在模型而在工具调用和状态管理。模型推理如果是本地部署的 vLLM单卡 A100 跑 7B 模型并发 20 左右问题不大。但工具调用如果是同步阻塞的比如查数据库、读文件、调内部 API这些操作的延迟会迅速堆积。再加上 Agent 的多步循环每一步都要等上一步完成整体延迟会成倍放大。我实测过一组数据单次 Agent 任务平均 5 步每步模型推理 800ms工具调用 300ms。串行执行的话单任务耗时约 5.5 秒。如果并发 10 个任务不做任何优化平均延迟会涨到 15 秒以上因为工具调用在排队。5.2 异步化改造的关键点第一个优化点是工具调用异步化。MCP 的 Python SDK 本身支持 async但很多人在写工具函数时习惯用同步的数据库驱动或者 requests 库这就把整个链路堵死了。改成asyncpg、aiohttp之后工具调用的等待时间可以重叠。第二个优化点是模型推理的批处理。vLLM 支持 continuous batching多个请求可以合并成一个 batch 推理。但前提是你的 Agent 编排层要能把并发请求汇聚到推理服务而不是每个请求单独起一个推理调用。我用 LangGraph 的时候在模型节点前面加了一个简单的请求队列攒够 4 个或者等 50ms 就发一批吞吐量大概提升了 2.5 倍。第三个优化点是状态存储。Agent 的中间状态如果存在内存里并发高了之后内存会爆。换成 Redis 或者本地 SQLite 做状态持久化每个任务的状态独立存储任务之间不共享内存扩展性会好很多。5.3 并发压测的实操方法压测不要一上来就上大并发要阶梯式加压。我的做法是从并发 1 开始每轮加 5观察三个指标P99 延迟、错误率、资源占用。当 P99 延迟超过可接受阈值比如 10 秒或者错误率超过 1% 时当前配置的极限就到了。压测脚本用locust或者简单的asyncio协程池都行。关键是要模拟真实的任务分布不要所有请求都走同一条路径。我一般会准备 5 到 10 个不同类型的任务按真实比例混合发送。并发数P99 延迟错误率CPU 占用内存占用56.2s0%45%2.1GB108.7s0%68%3.4GB1512.3s0.5%85%4.8GB2018.9s2.1%95%6.2GB这张表是我在某次压测里的真实记录。可以看到并发 15 的时候错误率开始上升说明系统极限大概在 12 到 15 之间。后来通过异步化改造和推理批处理把极限推到了 25 左右。6. 内网环境下的依赖管理与部署6.1 离线依赖包的准备隔离内网最大的痛点之一就是装包。你不能pip install只能提前在有网环境里把依赖下载好打包带进去。我的做法是用pip download把所有依赖下载成 whl 文件放到一个目录里然后在内网里用pip install --no-index --find-links./packages安装。# 有网环境 pip download -r requirements.txt -d ./offline_packages # 内网环境 pip install --no-index --find-links./offline_packages -r requirements.txt这里有个坑pip download默认只下载当前平台的包。如果内网机器的操作系统或者 Python 版本跟有网机器不一样下载的包可能装不上。所以最好在有网环境里用跟内网一致的 Docker 镜像来下载确保平台匹配。另外有些包依赖系统库比如psycopg2需要libpq-devchromadb可能需要编译工具链。这些系统级依赖没法用 pip 解决需要提前把 deb 包或者 rpm 包也准备好。6.2 模型文件的传输与加载本地模型文件通常很大7B 模型量化后也有 4 到 8 GB。传输到内网的方式取决于内网的安全策略。常见的方式有通过堡垒机上传、通过内部文件服务器中转、或者用移动存储介质拷贝。模型加载的时候vLLM 需要指定模型路径和量化配置。如果用的是 GPTQ 或者 AWQ 量化模型需要确保 vLLM 版本支持对应的量化格式。我遇到过一次版本不匹配的问题vLLM 0.4.x 不支持某个新出的 AWQ 格式换回 0.3.x 才跑起来。所以模型和推理框架的版本兼容性要提前确认。6.3 配置管理与环境隔离内网环境里不同环境开发、测试、生产的配置要严格隔离。我的做法是用环境变量加配置文件的方式环境变量指定当前环境配置文件按环境分目录存放。config/ ├── dev.yaml ├── test.yaml └── prod.yaml配置项包括模型服务地址、向量库路径、MCP Server 列表、日志级别、并发限制等。所有配置项都有默认值环境变量可以覆盖。这样部署的时候只需要改环境变量不用动配置文件。7. 常见问题与排查技巧实录7.1 模型调用超时或返回空这是最常见的问题。可能的原因有三个模型服务没起来、请求格式不对、或者超时设置太短。排查步骤先用 curl 直接调模型服务的健康检查接口确认服务活着。然后用一个最简单的请求测试模型是否能正常返回。如果都正常再看 Agent 侧的请求日志确认请求体格式是否符合模型服务的 API 规范。我遇到过一次是因为 vLLM 的max_model_len设置太小Agent 传的 prompt 超过了限制模型直接返回空。后来把max_model_len调到 8192 就正常了。7.2 MCP 工具调用失败MCP 工具调用失败通常表现为模型说“我要调用某个工具”但实际没有调用或者调用了但返回错误。第一种情况通常是工具描述不清楚模型没理解这个工具是干什么的。解决方法是优化 docstring把工具的用途、参数含义、返回值格式写清楚。第二种情况要看 MCP Server 的日志。如果是 stdio 模式Server 的 stderr 会输出到 Agent 的日志里。常见错误包括参数类型不匹配、内部依赖不可用、权限不足等。7.3 并发下的状态混乱并发场景下如果多个任务共享了同一个状态对象会出现状态互相覆盖的问题。典型表现是任务 A 的输出里出现了任务 B 的中间结果。这个问题的根源通常是用了全局变量或者单例对象来存状态。解决方法是每个任务独立创建状态对象用任务 ID 做隔离。LangGraph 的StateGraph默认就是每个调用独立状态但如果你在节点函数里引用了外部可变对象还是会出问题。7.4 常见问题速查表问题现象可能原因排查方法解决方案模型返回空prompt 超长检查 token 数调大 max_model_len工具不调用描述不清看模型输出优化 docstring工具报错参数类型错看 Server 日志修正参数定义状态混乱共享可变对象检查全局变量任务级隔离延迟飙升工具阻塞看调用链耗时异步化改造内存泄漏连接未释放监控内存曲线加清理钩子7.5 几个踩过的坑第一个坑MCP Server 的 stdio 模式在 Windows 上跟 Linux 行为不一致。Windows 上子进程的换行符处理有问题导致 JSON-RPC 消息解析失败。后来统一在 Linux 上部署问题消失。第二个坑向量库的持久化路径如果放在临时目录容器重启后索引就丢了。一定要把索引路径挂载到持久化卷上。第三个坑Agent 的日志如果打得太详细在高并发下 IO 会成为瓶颈。后来改成异步日志加采样只记录关键节点的输入输出吞吐量恢复了正常。8. 一些个人体会这套方案我在三个项目里迭代过从最初的“能跑就行”到后来的“稳定扛压”中间踩的坑比写代码的时间还多。最大的体会是内网环境下的 Agent 工程核心不是模型多强而是工程多稳。模型能力决定上限工程能力决定下限。在隔离环境里下限比上限重要得多。另外MCP 和 Skills 这两个东西刚开始会觉得是额外的复杂度但用久了会发现它们把“工具接入”和“行为编排”这两件事解耦了。解耦之后工具可以独立迭代Skill 可以独立更新互不影响。这个架构上的清晰度在长期维护里价值很大。如果让我给一个建议先把最简单的链路跑通——一个模型、一个工具、一个 Skill确认端到端能工作。然后再逐步加并发、加工具、加 Skill。不要一上来就搭大框架内网环境里调试成本很高每加一个组件都要确保它能被独立验证。