多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

隔离内网部署AI Agent实战:MCP、Skills与harness工程闭环

隔离内网部署AI Agent实战:MCP、Skills与harness工程闭环 1. 为什么“隔离内网 AI Agent”是个真问题先把场景说清楚。所谓隔离内网就是一台或者一批机器物理上或者策略上跟公网断开不能访问外部的模型 API、包管理仓库、镜像源甚至连 DNS 都不一定通。很多做 AI Agent 的朋友第一反应是那不就废了吗Agent 不就是靠调大模型 API 活着的吗这个反应对了一半。Agent 的“大脑”确实依赖模型推理但推理能力不一定非得从公网拿。隔离内网里跑 Agent核心矛盾其实集中在三件事上模型怎么进来、工具怎么调、状态怎么存。把这三件事拆开看你会发现它不是一个“能不能做”的问题而是一个“怎么把公网那套工程习惯搬到断网环境”的问题。我在实际项目里接触过不少类似需求典型来源有几类一是制造业、能源、医疗这类对数据出境极度敏感的行业模型必须本地化二是某些研发团队的内网开发环境代码和文档不允许外流但又想用 Agent 做代码理解、文档问答、流程自动化三是一些特殊作业环境网络条件本身就受限。这些场景的共同点是数据不能出去但智能要进来。关键词里提到的 MCP、Skills、harness 这些概念本质上都是围绕“让 Agent 在内网里也能有手有脚”展开的。MCP 解决的是工具接入的标准化问题Skills 解决的是能力封装和复用问题harness 解决的是 Agent 运行时怎么把模型、工具、记忆串起来的问题。这三个东西凑在一起基本就是一套内网 Agent 工程的最小闭环。这篇文章不打算讲空泛的概念我会按照一个真实项目的推进顺序来写从环境盘点开始到模型落地、工具接入、Skills 设计、并发处理最后聊几个我踩过的坑。每一段都会说清楚“为什么这么做”而不只是“怎么做”。如果你正在或者准备在内网环境里搞 Agent这篇应该能帮你少走不少弯路。2. 动手之前把内网环境的家底摸清楚2.1 先搞清楚“隔离”到底隔离到什么程度很多人一上来就开始装模型、配环境结果卡在第一步。我的习惯是动手前先做一张环境盘点表。隔离内网不是一个二元状态它有好几个层级不同层级对应的方案完全不一样。隔离层级网络特征典型限制对 Agent 的影响完全物理隔离无任何外部网络无法访问任何外部资源所有依赖必须离线搬运策略隔离可访问特定白名单只能访问内部镜像源模型和包需内部源支持单向隔离只出不进或只进不出数据传输需审批模型更新需走摆渡流程逻辑隔离有网络但走内部网关需配置代理和证书工具调用需适配网关这张表看着简单但它决定了你后面所有技术选型。比如完全物理隔离的环境你就别想着用在线模型 API 了老老实实做本地推理。策略隔离的环境如果内部有模型服务那优先用内部服务别自己折腾部署。我见过一个团队花了两个月在内网部署了一个 70B 的模型结果发现内部其实早就有一个模型服务平台只是没人告诉他们。所以第一步不是技术活是沟通活找运维、找安全、找平台团队把能用的资源问清楚。2.2 硬件资源决定模型上限模型能不能跑跑多快取决于你手里有什么卡。这个账要提前算不能等部署到一半发现显存不够。一个粗略的估算方法模型参数量乘以 2FP16或者乘以 0.5INT4 量化得到的是大概的显存需求。比如一个 7B 模型FP16 大概需要 14GB 显存INT4 量化后大概 3.5GB 到 4GB。再留出 KV Cache 和推理框架本身的开销实际占用会更高。模型规模FP16 显存需求INT4 显存需求推荐卡型7B约 14GB约 4GB单张 16GB 卡13B约 26GB约 7GB单张 24GB 卡或双卡32B约 64GB约 16GB双张 48GB 卡70B约 140GB约 35GB多卡并行这个表是经验值实际会因为推理框架、batch size、上下文长度有浮动。我的建议是内网环境优先考虑量化模型。原因很简单内网场景通常对延迟的容忍度比公网高但对资源的要求更苛刻。INT4 量化后模型体积小、显存占用低虽然精度有损失但在大多数 Agent 任务工具调用、信息抽取、流程判断上完全够用。2.3 离线依赖的搬运策略内网环境最烦人的就是装东西。pip install 用不了npm install 用不了docker pull 也用不了。这时候你需要一套离线搬运方案。我的做法是分三层准备基础层Python 运行时、CUDA 驱动、推理框架比如 vLLM、llama.cpp、Ollama的离线安装包。这些体积大但版本稳定一次性搬进去。依赖层项目用到的 Python 包、Node 包用pip download或者npm pack在公网机器上打包然后整体搬入。模型层模型权重文件通常是几个 GB 到几十个 GB用移动硬盘或者内部文件服务传输。提示搬运前一定要在公网机器上把依赖装一遍确认版本兼容。我踩过最坑的一次是搬进去才发现某个包的版本跟推理框架冲突又得重新走一遍审批流程。这里有个细节值得说尽量用容器化方案。把模型、依赖、代码全部打进一个 Docker 镜像在公网构建好导出成 tar 包搬进内网后直接docker load。这样能避免大量“在我机器上能跑”的问题。如果内网连 Docker 都没有那就用 conda-pack 或者 venv 打包效果类似。3. 模型落地内网推理服务怎么搭才稳3.1 推理框架选型别只看性能公网环境选推理框架大家习惯看吞吐、看延迟、看并发。内网环境选型逻辑不太一样我总结下来优先级是这样的稳定性 易部署 资源占用 性能。为什么稳定性排第一因为内网环境出问题排查成本极高。公网服务挂了重启一下、看个日志、搜个 issue 就解决了。内网服务挂了你可能连日志都导不出来只能靠记忆复现。所以选一个成熟、文档全、社区活跃的框架比选一个性能高但小众的框架要明智。目前内网场景比较常用的几个方案Ollama部署最简单一条命令拉起模型适合快速验证和小规模使用。缺点是并发能力一般大规模生产环境不太够。vLLM吞吐和并发能力强支持 PagedAttention适合多用户场景。部署稍复杂对显存管理要求高。llama.cppCPU 推理友好量化支持好适合没有 GPU 或者 GPU 资源紧张的环境。速度慢一些但胜在能跑。TGIText Generation Inference功能全支持多种量化适合生产部署。配置项多学习曲线陡一些。我的建议是先用 Ollama 跑通流程再根据并发需求决定要不要换 vLLM。很多团队一上来就上 vLLM结果发现业务量根本没那么大反而增加了运维负担。3.2 模型格式与量化精度和资源的平衡模型格式这块内网环境优先选 GGUF 或者 AWQ。GGUF 是 llama.cpp 的格式量化选项丰富从 Q2 到 Q8 都有适合资源受限场景。AWQ 是 GPU 上的量化方案精度损失小推理速度快适合有 GPU 的环境。量化等级怎么选我的一般原则是显存充足能放下 FP16直接用 FP16别折腾量化。显存紧张但要求精度用 INT8 或者 Q8损失很小。显存很紧张用 INT4 或者 Q4大多数任务够用。极端受限Q2 或 Q3但要做好精度下降的心理准备。注意量化不是免费的午餐。Q4 以下的量化在复杂推理任务上会出现明显的质量下降尤其是需要多步推理的 Agent 任务。如果你的 Agent 需要做复杂的工具调用规划建议至少用 Q5 以上。3.3 把模型服务封装成内网 API模型跑起来之后下一步是把它封装成一个内网可访问的 API。这样 Agent 的 harness 层才能统一调用不用关心底层是 Ollama 还是 vLLM。一个典型的做法是用 FastAPI 包一层from fastapi import FastAPI from pydantic import BaseModel import httpx app FastAPI() class ChatRequest(BaseModel): messages: list temperature: float 0.7 max_tokens: int 2048 app.post(/v1/chat/completions) async def chat(req: ChatRequest): async with httpx.AsyncClient() as client: resp await client.post( http://localhost:11434/api/chat, json{ model: qwen2.5:7b, messages: req.messages, options: { temperature: req.temperature, num_predict: req.max_tokens } }, timeout120.0 ) return resp.json()这层封装的价值在于统一接口、统一鉴权、统一限流。后面不管换什么模型后端Agent 侧代码不用动。同时你可以在这一层加内网访问控制比如只允许特定 IP 段调用。3.4 模型服务的健康检查与降级内网环境最怕的就是服务悄悄挂了没人知道。我的做法是加一个健康检查端点配合内网的监控系统做定时探测。app.get(/health) async def health(): try: async with httpx.AsyncClient() as client: resp await client.get(http://localhost:11434/api/tags, timeout5.0) if resp.status_code 200: return {status: ok, backend: ollama} except Exception as e: return {status: error, detail: str(e)} return {status: degraded}降级策略也很重要。如果主模型服务挂了Agent 应该能切换到一个更小的备用模型或者直接返回一个友好的错误提示而不是整个流程卡死。这个逻辑要写在 harness 层不能指望模型服务自己处理。4. MCP 与 Skills让 Agent 在内网里“有手有脚”4.1 MCP 到底解决了什么问题MCP 全称 Model Context Protocol直译过来是模型上下文协议。它的核心价值是把工具调用标准化。在没有 MCP 之前每个 Agent 框架都有自己的工具定义方式换个框架就得重写一遍。MCP 出现之后工具提供方只要实现一个 MCP Server任何支持 MCP 的 Agent 都能调用。在内网环境里MCP 的价值更明显。因为内网工具往往是定制化的比如内部工单系统、内部知识库、内部部署平台。如果每个 Agent 都单独对接一遍工作量巨大。用 MCP 统一封装一次开发多处复用。MCP 的基本架构是这样的Agent 作为 Client工具作为 Server两者通过标准协议通信。通信方式支持 stdio本地进程和 SSEHTTP 流式。内网环境推荐用 stdio因为不涉及网络端口部署简单安全性也好。4.2 内网 MCP Server 的开发要点写一个内网 MCP Server跟写普通服务有几个不一样的地方。第一工具描述要写得极其清楚。因为内网模型通常比公网模型弱对工具描述的理解能力差一些。工具名、参数说明、返回值格式都要写得直白别用缩写和内部黑话。第二错误处理要健壮。内网工具经常依赖内部系统这些系统可能不稳定。MCP Server 要做好超时、重试、降级不能因为一个工具调用失败就把整个 Agent 流程搞崩。第三权限控制要前置。内网环境对权限敏感MCP Server 应该在调用内部系统前就做好鉴权而不是依赖内部系统自己判断。一个简单的 MCP Server 示例Pythonfrom mcp.server import Server from mcp.types import Tool, TextContent server Server(internal-tools) server.list_tools() async def list_tools(): return [ Tool( namequery_ticket, description根据工单号查询内部工单系统的工单详情返回标题、状态、负责人, inputSchema{ type: object, properties: { ticket_id: { type: string, description: 工单编号格式如 INC-2024-001 } }, required: [ticket_id] } ) ] server.call_tool() async def call_tool(name: str, arguments: dict): if name query_ticket: ticket_id arguments[ticket_id] result await query_internal_ticket(ticket_id) return [TextContent(typetext, textresult)]4.3 Skills 的设计哲学能力封装而非功能堆砌Skills 这个词最近很火但很多人理解偏了。Skills 不是“功能列表”而是“能力封装”。区别在哪功能是“我能查工单”能力是“我能帮用户解决工单相关的问题”。一个好的 Skill 应该包含四部分触发条件、执行逻辑、输出格式、失败处理。触发条件决定什么时候用这个 Skill执行逻辑是具体做什么输出格式决定结果怎么呈现失败处理决定出错了怎么办。在内网环境里Skills 设计有几个原则粒度适中太细会导致 Skill 数量爆炸太粗会导致复用性差。我的经验是按“用户意图”划分而不是按“系统接口”划分。依赖明确每个 Skill 依赖哪些 MCP 工具、哪些内部服务要写清楚。这样排查问题时能快速定位。可测试每个 Skill 都要有独立的测试用例不能等到集成到 Agent 里才发现问题。4.4 Skills 与 MCP 的配合方式MCP 是“工具层”Skills 是“能力层”harness 是“调度层”。三者的关系可以这样理解MCP 提供原子操作Skills 把原子操作组合成业务能力harness 根据用户输入决定调用哪个 Skill。举个例子。用户说“帮我看看 INC-2024-001 这个工单现在什么情况”。harness 解析意图识别出这是“工单查询”类请求。匹配到“工单状态查询”这个 Skill。Skill 内部调用 MCP 的query_ticket工具。MCP Server 访问内部工单系统拿到数据。Skill 把原始数据格式化成用户友好的回复。harness 把回复返回给用户。这个链路里每一层职责清晰替换任何一层都不影响其他层。这就是为什么内网 Agent 工程要强调分层设计。5. 并发与稳定性内网 Agent 的工程底线5.1 内网 Agent 的并发瓶颈在哪公网 Agent 的瓶颈通常在模型 API 的 rate limit。内网 Agent 的瓶颈不一样通常在三个地方模型推理的显存、工具调用的内部系统承载、harness 自身的连接管理。模型推理这块如果是单卡部署并发数基本就是显存除以单次推理的显存占用。比如一张 24GB 卡跑 7B INT4 模型单次推理占 4GB理论上能并发 5 到 6 个请求。但实际要考虑 KV Cache 的动态增长保守估计并发 3 到 4 个比较稳。工具调用这块内部系统往往没有为高并发设计。比如工单系统可能只支持每秒 10 个查询你的 Agent 一上来并发 50 个直接把人系统打挂了。所以工具调用必须加限流这个限流要放在 MCP Server 层而不是 harness 层。harness 自身的连接管理主要是 HTTP 连接池、超时设置、重试策略。这些看着是细节但在内网环境里特别重要因为内网网络抖动比公网更常见。5.2 用队列削峰填谷内网 Agent 最实用的并发方案是队列 工作池。用户请求先进队列工作池按固定并发数消费。这样既能控制对下游系统的压力又能保证请求不丢失。import asyncio from asyncio import Queue class AgentWorkerPool: def __init__(self, concurrency: int 3): self.queue Queue() self.concurrency concurrency self.workers [] async def start(self): for i in range(self.concurrency): worker asyncio.create_task(self._worker(i)) self.workers.append(worker) async def _worker(self, worker_id: int): while True: task await self.queue.get() try: result await self._process(task) task[future].set_result(result) except Exception as e: task[future].set_exception(e) finally: self.queue.task_done() async def submit(self, task: dict): future asyncio.get_event_loop().create_future() task[future] future await self.queue.put(task) return await future这个模式的好处是并发数可控、请求不丢失、下游压力可预测。缺点是用户可能要等所以要在前端做好“排队中”的提示。5.3 超时、重试与熔断内网环境的不确定性比公网高所以超时和重试策略要更保守。模型推理超时建议 60 到 120 秒。内网模型通常比公网慢超时设太短会导致大量失败。工具调用超时建议 10 到 30 秒。内部系统响应慢的话可以适当放宽但要有上限。重试策略只对幂等操作重试重试次数不超过 2 次重试间隔用指数退避。熔断某个工具连续失败超过阈值直接熔断避免拖垮整个 Agent。提示重试一定要加 jitter随机抖动否则多个请求同时重试会形成“重试风暴”把下游系统打得更惨。5.4 可观测性内网环境怎么排查问题内网环境排查问题比公网难因为很多外部工具用不了。我的做法是把可观测性做进代码里而不是依赖外部系统。具体来说每个请求要有一个 trace_id贯穿 harness、Skill、MCP Server 三层。日志要结构化方便用 grep 和 awk 分析。关键指标请求数、成功率、延迟分布要定期落盘方便事后分析。import logging import json import time logger logging.getLogger(agent) def log_event(trace_id: str, event: str, **kwargs): record { ts: time.time(), trace_id: trace_id, event: event, **kwargs } logger.info(json.dumps(record, ensure_asciiFalse))这种结构化日志的好处是出问题时可以用grep trace_id把整个链路串起来不用在多个系统之间来回跳。6. 踩坑实录那些文档里不会写的问题6.1 模型加载慢导致的启动超时内网环境第一次加载模型特别慢因为要从磁盘读几十 GB 的权重文件。如果 harness 启动时同步等待模型加载很容易超时。我的解决方案是模型服务和 harness 解耦。模型服务独立启动harness 启动时只做健康检查不等待模型加载完成。如果模型还没准备好harness 返回“服务初始化中”而不是直接报错。6.2 工具描述里的中文编码问题内网环境经常遇到编码问题。MCP Server 返回的中文内容在某些终端里会变成乱码。这个问题的根源是 stdio 通信的编码不一致。解决办法是在 MCP Server 和 Client 两端都显式指定 UTF-8 编码。Python 里可以用sys.stdout.reconfigure(encodingutf-8)Node 里可以在启动参数里加--encodingutf-8。6.3 长上下文导致的显存溢出Agent 任务经常需要长上下文比如把整个文档塞进去做问答。但长上下文会显著增加 KV Cache 的显存占用容易导致 OOM。我的做法是在 harness 层做上下文裁剪。不是所有历史消息都需要保留只保留最近 N 轮对话和关键的系统提示。同时设置一个上下文长度上限超过就触发摘要或者截断。6.4 内部系统的“隐形限流”很多内部系统没有明确的限流文档但实际上有隐形限流。比如某个接口每秒超过 5 次就开始返回 429但文档里没写。这种问题只能靠实测发现。我的做法是在 MCP Server 层加一个自适应限流器根据响应状态动态调整并发数。遇到 429 就降低并发稳定一段时间后再慢慢提升。6.5 Skills 之间的依赖冲突当 Skills 数量多了之后会出现依赖冲突。比如 Skill A 依赖 MCP 工具的 v1 版本Skill B 依赖 v2 版本两个版本不兼容。解决办法是在 Skill 定义里显式声明依赖版本harness 在加载 Skill 时做版本检查。如果冲突要么升级 Skill要么隔离运行环境。这个问题在项目初期不明显但到了几十个 Skill 的时候会集中爆发所以要提前设计好。7. 一些实操建议与个人体会先说一个反直觉的结论内网 Agent 项目的成败技术只占三成沟通占七成。你要跟运维确认网络策略跟安全确认数据边界跟平台团队确认可用资源跟业务方确认需求优先级。这些沟通做不好技术再强也推不动。再说几个具体的建议。第一从最小闭环开始。别一上来就搞大而全的架构先做一个能跑通的最小版本一个模型、一个工具、一个 Skill、一个用户入口。跑通之后再逐步扩展。我见过太多项目死在“设计太宏大落地太困难”上。第二把配置和代码分离。内网环境的配置变化频繁模型地址、工具地址、限流参数都可能调整。把这些放在配置文件里改配置不用改代码能省很多事。第三留好降级路径。模型服务挂了怎么办工具调用失败了怎么办用户输入超出能力范围怎么办。这些都要有明确的处理逻辑不能指望“不会出问题”。第四重视日志和监控。内网环境排查问题成本高所以要把能记录的都记录下来。日志级别要可调关键路径要有埋点异常要有告警。最后分享一个我自己的习惯每次部署新版本前先在内网做一次全链路演练。从用户输入开始走完 harness、Skill、MCP、模型、内部系统确认每一环都正常。这个演练看着费时间但能避免上线后才发现问题实际上省的时间更多。内网 Agent 工程不是一个纯技术问题它更像是一个系统工程问题。技术方案要选对但更重要的是把工程习惯建立起来分层设计、配置分离、可观测性、降级策略。这些东西在公网环境里可能是“最佳实践”在内网环境里就是“生存必需”。
返回列表