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

文章详情

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

隔离内网AI Agent工程化落地:本地模型+MCP工具链实战

隔离内网AI Agent工程化落地:本地模型+MCP工具链实战 1. 项目缘起与整体设计思路1.1 为什么要在隔离内网里折腾 AI Agent先说清楚这个项目的背景。我所在的团队负责一套企业级运维平台的开发整个开发环境跑在完全隔离的内网里——没有外网出口没有公网镜像源连 pip install 都得走内部私有仓库。这种环境下想用 AI Agent 来提升日常工作效率比如自动生成工单摘要、辅助排查日志、批量处理配置文件就变成了一件很有挑战的事。市面上大部分 AI Agent 的教程和开源项目默认你有一个畅通的网络环境可以随时调用云端大模型 API、拉取各种依赖包、访问在线工具市场。但隔离内网把这些前提全部推翻了。所以这个项目的核心目标很明确在无外网、无公网 API、无在线依赖源的隔离内网环境中搭建一套可落地、可维护、可扩展的 AI Agent 工程体系。适合谁来参考三类人。第一类是在金融、能源、政务等对网络隔离有硬性要求的行业里做开发的朋友第二类是对 AI Agent 工程化落地感兴趣想了解底层架构而不只是调 API 的开发者第三类是正在做企业内部工具链建设需要评估 AI Agent 可行性的技术负责人。1.2 整体架构选型为什么是这套组合隔离内网的核心约束就三条模型必须本地部署、工具链必须离线可用、数据绝对不能出内网。基于这三条我把架构拆成了四层。最底层是模型推理层。隔离环境里没有云端 API 可用所以必须本地跑模型。我选的是通过内部私有仓库分发的开源模型权重配合本地推理框架做服务化。模型规模控制在 7B 到 14B 之间原因是内网服务器的 GPU 资源有限太大的模型推理延迟高Agent 的多轮工具调用会变得非常慢。实测下来7B 级别的模型在单轮工具调用场景下响应时间能控制在 2 到 4 秒基本可用。往上一层是Agent 编排层。这一层负责管理对话状态、规划任务步骤、决定什么时候调用工具、什么时候直接回复。我用的是一套基于状态机的编排逻辑而不是简单的 ReAct 循环。为什么因为隔离内网里的工具调用往往涉及内部系统的鉴权和数据格式转换简单的 ReAct 容易在工具调用失败时陷入死循环状态机可以更精确地控制每一步的流转条件。再往上是工具层也就是 MCP Tools 和 Skills 的落地。MCP 在这里扮演的是标准化工具接口的角色让 Agent 能以统一的方式调用内部系统的能力。Skills 则是更高层的封装把一组相关的工具调用和业务逻辑打包成一个可复用的技能单元。最上面是交互层包括命令行工具、内部 Web 界面和与企业 IM 的集成。这一层的关键是适配内网环境不能依赖任何外部 CDN 或在线资源。1.3 方案取舍背后的逻辑有人可能会问为什么不直接用现成的 Agent 框架比如 LangChain 或者 Spring AI Agent。我的考虑是这样的现成框架确实能省不少事但它们在隔离内网里有两个致命问题。一是依赖链太深一个框架可能间接依赖几十个包在私有仓库里逐个补齐非常痛苦二是这些框架的默认行为往往假设你有外网访问能力比如自动下载模型、自动更新工具列表在内网里这些都会失败。所以我采取的策略是核心编排逻辑自己写工具接口用 MCP 标准模型推理用本地服务。这样整个系统的依赖链是可控的每一个外部依赖都是显式引入的不会出现“框架偷偷去连外网”的情况。注意在隔离内网做技术选型时第一原则不是“哪个框架最流行”而是“哪个方案的依赖链最短、最可控”。一个依赖链清晰的简单方案远比一个功能强大但依赖复杂的框架更适合内网环境。2. 核心细节解析与实操要点2.1 本地模型服务的部署与调优隔离内网里部署本地模型第一步是解决模型权重的获取问题。我的做法是在外网环境先把模型权重下载好做完整性校验然后通过内部的文件摆渡流程导入内网。这里有个细节模型权重文件通常很大几个 GB 到几十个 GB摆渡过程要分片传输并校验哈希值避免传输损坏。模型服务化我用的是本地推理框架启动参数里最关键的是这几个最大并发数根据 GPU 显存来定。7B 模型在 24G 显存的卡上并发数设 4 比较稳妥再高会出现显存溢出。上下文长度Agent 场景下上下文消耗很快因为每轮工具调用的结果都要塞进上下文。我设的是 8192再长推理速度会明显下降。批处理大小这个参数影响吞吐量。内网 Agent 的请求量不大我设的是 1优先保证单请求延迟。启动命令大概长这样python -m local_inference_server \ --model-path /models/qwen-7b-chat \ --max-concurrent 4 \ --context-length 8192 \ --batch-size 1 \ --port 8000服务起来之后用 curl 测一下连通性curl -X POST http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d {messages:[{role:user,content:你好}],max_tokens:50}能正常返回就说明模型服务 OK 了。2.2 MCP Tools 在内网中的适配改造MCP 的核心价值是提供了一套标准化的工具描述和调用协议。但在隔离内网里原生的 MCP 实现有几个地方需要改造。第一个改造点是工具发现机制。原生 MCP 支持动态发现工具但在内网里工具列表是固定的不需要动态发现。我改成启动时从本地配置文件加载工具清单每个工具的定义包括名称、描述、参数 schema 和调用地址。这样做的好处是启动速度快而且不会因为网络探测导致启动卡顿。第二个改造点是鉴权。内网系统之间的调用也需要鉴权但用的是内部统一的 token 体系。我在 MCP 的工具调用层加了一个鉴权中间件每次调用工具时自动从本地凭证文件读取 token 并附加到请求头里。第三个改造点是超时和重试。内网系统的响应时间波动比较大有些老系统的接口可能要好几秒才返回。我把工具调用的默认超时设成了 15 秒重试次数设成 2 次重试间隔用指数退避。工具配置文件的格式大概是这样{ tools: [ { name: query_ticket, description: 根据工单ID查询工单详情, parameters: { type: object, properties: { ticket_id: {type: string, description: 工单ID} }, required: [ticket_id] }, endpoint: http://internal-api/ticket/query, timeout: 15000, retry: 2 } ] }2.3 Skills 的封装思路与复用策略Skills 和 MCP Tools 的区别在于抽象层级。MCP Tools 是原子能力比如“查询工单”“发送通知”“读取配置文件”。Skills 是组合能力比如“生成工单日报”这个 Skill内部可能调用了查询工单、统计汇总、格式化输出三个 Tools。我封装 Skills 的原则是一个 Skill 对应一个完整的业务场景输入输出都是业务语义不暴露底层工具细节。这样做的好处是 Agent 的编排逻辑可以更简洁不需要关心底层调了哪些工具。举个例子“日志排查”这个 Skill 的封装。输入是一个服务名和时间范围输出是异常日志摘要和可能的原因分析。内部实现上它先调用日志查询工具拉取原始日志然后做关键词过滤和聚类最后把处理结果交给模型做分析。整个过程对 Agent 来说就是一个函数调用。Skills 的注册也是通过本地配置文件{ skills: [ { name: log_diagnosis, description: 对指定服务在指定时间范围内的日志进行异常诊断, parameters: { service_name: {type: string}, start_time: {type: string}, end_time: {type: string} }, tools: [query_logs, cluster_errors, analyze_with_model] } ] }实操心得Skills 的粒度控制很关键。太细了Agent 编排逻辑会变得很复杂太粗了复用性差。我的经验是一个 Skill 的内部工具调用不超过 5 个执行时间不超过 30 秒这样既保证复用性又不会让单次调用太重。3. 实操过程与核心环节实现3.1 从零搭建 Agent 编排引擎编排引擎是整个系统的核心。我用 Python 写了一个基于状态机的编排器核心状态有四个理解意图、规划步骤、执行工具、生成回复。理解意图阶段把用户输入和当前对话历史一起送给模型让模型判断用户想做什么。这里有个技巧不要直接让模型输出自由文本而是让它输出结构化的意图标签。我在 prompt 里定义了有限的意图类别比如“查询类”“操作类”“咨询类”“闲聊类”模型只需要输出对应的标签。规划步骤阶段根据意图标签和可用的 Skills 列表让模型生成一个执行计划。执行计划是一个有序列表每个元素是一个 Skill 调用及其参数。这里的关键是约束模型的输出格式我要求模型输出 JSON 格式的计划如果解析失败就重试重试两次还失败就降级到直接回复。执行工具阶段按顺序执行计划中的每个 Skill。每个 Skill 执行完后把结果追加到上下文里供后续步骤使用。如果某个 Skill 执行失败根据失败类型决定是重试、跳过还是终止整个计划。生成回复阶段把执行结果和原始问题一起送给模型生成自然语言回复。这里要注意控制回复长度内网 IM 场景下太长的回复体验不好我一般限制在 500 字以内。编排引擎的核心代码结构大概是这样class AgentOrchestrator: def __init__(self, model_client, skill_registry): self.model model_client self.skills skill_registry self.state understand def run(self, user_input, history): intent self.understand_intent(user_input, history) plan self.plan_steps(intent, user_input) results self.execute_plan(plan) reply self.generate_reply(user_input, results) return reply def understand_intent(self, user_input, history): prompt build_intent_prompt(user_input, history) response self.model.chat(prompt) return parse_intent(response) def plan_steps(self, intent, user_input): available_skills self.skills.list_by_intent(intent) prompt build_plan_prompt(intent, user_input, available_skills) response self.model.chat(prompt) return parse_plan(response)3.2 工具调用的参数校验与容错工具调用最容易出问题的地方是参数不对。模型生成的参数可能缺字段、类型不对、或者值超出范围。我在工具调用层加了一层参数校验用 JSON Schema 做验证。校验失败的常见情况和处理方式失败类型典型表现处理方式缺必填字段模型忘了传 ticket_id把缺失字段和工具描述重新发给模型让它补全类型错误时间传了数字而不是字符串尝试自动转换转换失败则报错值超范围分页参数传了负数用默认值替换并记录警告日志格式错误时间格式不是 ISO 8601尝试用常见格式解析失败则报错参数校验的代码大概是这样from jsonschema import validate, ValidationError def validate_tool_params(tool_def, params): try: validate(instanceparams, schematool_def[parameters]) return True, None except ValidationError as e: return False, str(e)如果校验失败我会把错误信息和工具定义一起重新发给模型让它修正参数。这个重试最多做两次两次还不行就放弃这个工具调用在最终回复里说明情况。3.3 上下文管理与 Token 控制Agent 场景下上下文增长非常快。一轮对话可能包含用户输入、意图识别结果、执行计划、多个工具调用结果、最终回复加起来轻松超过 2000 token。如果对话轮数多了上下文会迅速膨胀到模型的上限。我的上下文管理策略是分层裁剪。把上下文分成三个优先级高优先级当前轮的用户输入、当前执行计划、最近一次工具调用结果。这些必须保留。中优先级最近三轮对话的摘要。用模型生成简短摘要而不是保留原始文本。低优先级更早的对话历史。只保留关键信息比如用户提到过的服务名、工单号等实体。裁剪的触发条件是上下文 token 数超过模型上限的 70%。裁剪时从低优先级开始丢弃直到降到 70% 以下。Token 计数我用的是本地分词器不依赖在线服务。计数逻辑很简单def count_tokens(text, tokenizer): return len(tokenizer.encode(text)) def trim_context(messages, max_tokens, tokenizer): total sum(count_tokens(m[content], tokenizer) for m in messages) while total max_tokens * 0.7 and len(messages) 3: removed messages.pop(1) # 保留第一条系统提示和最后一条用户输入 total - count_tokens(removed[content], tokenizer) return messages注意上下文裁剪是有损的可能丢掉重要信息。我的做法是在裁剪前先把关键实体抽取出来存到一个独立的实体表里每次请求时把实体表附加到系统提示中。这样即使对话历史被裁剪了关键信息也不会丢。3.4 内网环境下的日志与监控隔离内网里没有现成的监控 SaaS 可用所以日志和监控也得自己搭。我用的是本地文件日志加一个简单的统计面板。日志分三类访问日志记录每次 Agent 请求的输入输出和耗时工具调用日志记录每次工具调用的参数、结果和耗时错误日志记录所有异常和堆栈。统计面板是一个本地 Web 页面从日志文件里聚合数据展示请求量、平均响应时间、工具调用成功率、错误分布等指标。这个面板不依赖任何外部资源纯静态 HTML 加本地 API。关键监控指标和告警阈值指标正常范围告警阈值处理建议单请求响应时间2-8 秒超过 15 秒检查模型服务负载和工具调用超时工具调用成功率95% 以上低于 90%检查内部系统可用性和鉴权配置上下文裁剪频率低于 10%超过 30%优化上下文管理策略或增大模型上下文模型服务错误率低于 1%超过 5%检查 GPU 显存和模型服务日志4. 常见问题与排查技巧实录4.1 模型输出格式不稳定怎么办这是最常见的问题。模型有时候不按要求的 JSON 格式输出而是加了一些解释性文字导致解析失败。我的处理方式是三层防御。第一层是在 prompt 里明确要求“只输出 JSON不要有任何其他文字”并且给出示例。第二层是在解析时先用正则提取 JSON 部分忽略前后的多余文字。第三层是如果解析还是失败把解析错误和原始输出一起发给模型让它重新输出。实测下来这三层防御能把格式错误率从 15% 降到 2% 以下。剩下的 2% 主要是模型在复杂场景下确实理解不了任务这种就需要人工介入优化 prompt 了。4.2 工具调用超时和死锁的排查内网系统的不稳定性是常态。我遇到过几种典型的工具调用问题超时某个内部接口响应特别慢导致 Agent 整体卡住。排查方法是看工具调用日志里的耗时分布找出慢接口。解决方式是给每个工具单独设置超时慢接口的超时设长一点但整体请求的超时要有上限。死锁两个工具互相等待对方的资源。这种情况比较少见但一旦出现就很难排查。我的做法是在工具调用层加一个全局的调用链追踪记录每个工具的调用顺序和等待关系出现死锁时能快速定位。鉴权失败token 过期或者权限不足。排查方法是看错误日志里的 HTTP 状态码401 是 token 问题403 是权限问题。解决方式是加一个 token 自动刷新机制以及在工具配置里明确标注需要的权限。4.3 常见问题速查表问题现象可能原因排查步骤解决方案Agent 不调用工具直接回复工具描述不清晰或意图识别错误检查意图识别结果和工具列表优化工具描述增加意图示例工具调用参数总是错的参数 schema 太复杂或模型理解偏差查看参数校验错误日志简化 schema增加参数示例回复内容太长或太短回复生成 prompt 约束不够检查回复生成的 prompt明确字数范围增加示例多轮对话后响应变慢上下文膨胀导致推理变慢查看上下文 token 数启用上下文裁剪优化摘要策略模型服务偶尔无响应GPU 显存不足或并发过高检查 GPU 使用率和并发数降低并发数增加显存清理工具调用结果解析失败内部接口返回格式变化对比接口文档和实际返回增加格式兼容层记录原始返回4.4 几个踩过的坑和独家技巧坑一模型服务启动时的冷启动问题。本地模型服务第一次加载模型需要几十秒如果 Agent 在这期间发请求会超时。我的做法是在 Agent 启动时先发一个预热请求等模型服务完全就绪后再开始接收用户请求。坑二工具调用的幂等性。有些工具调用是有副作用的比如发送通知、修改配置。如果因为超时重试可能会重复执行。我的做法是在工具配置里标注是否幂等非幂等的工具不自动重试而是返回错误让 Agent 决定下一步。技巧一用缓存减少重复调用。查询类的工具调用结果可以缓存同样的参数在短时间内重复查询直接返回缓存结果。我用的是一个简单的内存缓存TTL 设 5 分钟。这个技巧能把重复查询的响应时间从几秒降到毫秒级。技巧二工具调用的批量合并。如果 Agent 在同一个计划里多次调用同一个工具只是参数不同可以合并成一次批量调用。比如查询多个工单的详情可以合并成一个批量查询接口。这个优化能把多次网络往返合并成一次显著降低延迟。技巧三降级策略。当模型服务不可用或者工具调用大面积失败时Agent 应该能降级到简单的规则匹配模式至少能回答一些常见问题。我在编排引擎里加了一个降级开关检测到异常时自动切换到规则模式并记录降级日志。实操心得隔离内网环境下的 AI Agent 工程最大的挑战不是模型能力而是工程可靠性。模型偶尔出错没关系关键是整个系统要有容错和降级能力不能因为一个环节失败就整体不可用。我在设计每个模块时都会问自己这个模块挂了系统还能不能提供基本服务如果答案是否定的就需要加降级方案。5. 性能优化与扩展方向5.1 并发场景下的资源调度内网 Agent 虽然用户量不大但并发请求还是有的。多个用户同时使用时模型服务和工具调用都会成为瓶颈。我的资源调度策略是分级队列。把请求分成高优先级和普通优先级高优先级请求比如故障排查优先分配模型资源普通优先级请求比如日常查询排队等待。队列长度设一个上限超过上限直接返回“系统繁忙”提示避免请求堆积导致整体雪崩。模型服务的并发控制用信号量实现import asyncio class ModelService: def __init__(self, max_concurrent4): self.semaphore asyncio.Semaphore(max_concurrent) async def chat(self, prompt): async with self.semaphore: return await self._inference(prompt)工具调用的并发控制类似但每个工具可以有不同的并发上限。查询类工具并发可以高一些操作类工具并发要低一些避免对内部系统造成压力。5.2 模型量化和推理加速的尝试内网服务器的 GPU 资源有限模型推理速度是瓶颈之一。我尝试了几种优化方式。量化把模型从 FP16 量化到 INT8显存占用减少一半推理速度提升约 30%。代价是精度略有下降但在 Agent 场景下影响不大因为 Agent 主要做的是意图理解和参数生成对精度的要求没有纯文本生成那么高。KV Cache 优化Agent 场景下多轮对话共享大量上下文KV Cache 可以显著减少重复计算。我用的推理框架支持 PagedAttention能把 KV Cache 的内存利用率提升不少。批处理把多个并发请求合并成一个批次送给模型能提升 GPU 利用率。但批处理会增加单请求的延迟因为要等批次里的其他请求。我的做法是设置一个很小的等待窗口比如 50ms窗口内的请求合并成一批窗口外的直接单独处理。5.3 后续可以扩展的方向这套系统目前能满足基本的 Agent 需求但还有不少可以扩展的地方。多模型路由不同的任务用不同的模型。简单的意图识别用小模型复杂的分析用大模型。这样能在保证效果的同时降低资源消耗。工具市场的内部化把常用的工具和 Skills 打包成内部市场其他团队可以直接引用。这需要一套版本管理和依赖解析机制。Agent 的可观测性增强目前只有基础的日志和统计后续可以加调用链追踪、性能剖析、异常自动归因等能力。与内部系统的深度集成目前工具调用还是通过 HTTP 接口后续可以考虑用消息队列做异步调用提升吞吐量和可靠性。6. 一些个人体会这套系统从零到能用大概花了三周时间。其中大部分时间不是花在写代码上而是花在调试模型输出、适配内部系统接口、处理各种边界情况上。隔离内网环境最大的特点就是“什么都得自己来”没有现成的轮子可以拿来直接用但反过来也逼着我把每个环节都理解透彻。如果让我给准备在隔离内网做 AI Agent 的朋友提建议我会说先把模型服务跑通再搭工具调用框架最后做编排逻辑。不要一上来就追求大而全的架构先从一个小场景切入比如“查询工单并生成摘要”跑通了再逐步扩展。另外日志一定要做细内网环境排查问题全靠日志日志不够细的话出了问题根本不知道从哪里下手。最后分享一个我觉得很实用的技巧在 Agent 的 prompt 里加一句“如果不确定就说不知道不要编造”。这句话能显著降低模型幻觉的概率尤其是在工具调用失败或者信息不足的时候模型会更倾向于如实告知而不是强行编一个答案。这个技巧在隔离内网场景下特别重要因为内网系统的信息往往不完整模型如果编造信息排查起来会非常麻烦。
返回列表