☰
隔离内网AI Agent工程实战:MCP Tools与Skills落地指南
2026/10/3 21:39:38 网站建设 项目流程

1. 为什么要在隔离内网里折腾 AI Agent

先把场景说清楚。所谓“隔离内网”,就是一台或者一批机器,物理上或者逻辑上跟公网断开,只能在内网里互相通信,外部的 API、模型服务、包管理仓库统统够不着。很多做金融、制造、政务信息化的团队都是这种环境,代码不能出网,数据不能出网,连pip install都得走内部镜像源。在这种地方谈 AI Agent,第一反应往往是“不可能”,因为大家习惯了 Agent 背后挂一个云端大模型,靠 function calling 去调各种在线工具。

但真实需求是存在的。比如内网里有一套工单系统、一套审批流、一堆数据库表,运维和业务人员每天要重复查状态、走流程、填表单。你完全可以让一个 Agent 在内网里把这些活儿接过去,模型跑在内网的推理服务上,工具通过 MCP 协议或者本地 Skills 注册进来,整个链路不出网。这就是“隔离内网下 AI Agent 工程实战”要解决的核心问题:在没有任何外网依赖的前提下,把 Agent 的感知、决策、执行三段打通,并且让它稳定跑在生产环境里。

这篇文章适合谁看?如果你是有一定后端基础、正在或者准备在内网环境落地 Agent 的工程师,那基本可以照着抄作业;如果你只是听说过 Agent 想了解它到底怎么落地,也能从里面看到真实的工程细节,而不是 demo 级别的玩具。我会把选型逻辑、目录结构、MCP Tools 和 Skills 的写法、审批流的接入、并发处理、以及一堆踩过的坑都摊开讲。核心关键词就几个:AI Agent、MCP Tools、Skills、内网系统、审批,后面所有内容都围绕它们展开。

先说一个我自己的判断:内网 Agent 的难点从来不是模型本身,而是工具怎么注册、权限怎么收口、审批怎么卡点、并发怎么扛住。模型能力再强,工具调不通、审批绕不过,这个 Agent 就是废的。所以下面的内容,重心会放在工程侧,而不是提示词调优。

2. 整体架构设计与选型思路

2.1 内网 Agent 的分层结构

我最终落地的架构分四层,从下往上依次是:模型推理层、Agent 编排层、工具与技能层、业务接入层。这个分层不是拍脑袋定的,而是被内网环境逼出来的。

模型推理层跑在内网的 GPU 机器上,用 vLLM 或者类似的推理框架起一个 OpenAI 兼容的接口。为什么强调“OpenAI 兼容”?因为绝大多数 Agent 框架默认对接的就是这套协议,你只要把 base_url 指到内网地址,框架代码几乎不用改。这一步是整个方案能成立的前提,模型必须内网自持。

Agent 编排层是大脑,负责接收用户输入、规划步骤、决定调哪个工具、处理工具返回、生成最终答复。这一层我选的是 LangGraph 思路的编排方式,原因后面细说。工具与技能层是手脚,MCP Tools 负责对接外部系统(数据库、HTTP 接口、消息队列),Skills 负责封装可复用的能力单元(比如“查审批状态”“生成周报”)。业务接入层就是内网里那些真实系统,工单、审批、CMDB、监控告警。

提示:分层的目的不是好看,而是让每一层可以独立替换。模型换了不影响工具,工具换了不影响编排逻辑,这在长期维护里能省大量返工。

2.2 为什么选 MCP Tools 而不是硬编码函数

早期我试过最土的办法:在 Agent 代码里直接写一堆 Python 函数,每个函数对应一个内网操作,然后把这些函数塞进工具的列表里。跑 demo 没问题,一上规模就崩。问题有三个:第一,工具和 Agent 代码耦合死,加一个工具要改主流程;第二,工具的入参出参没有统一描述,模型经常传错参数;第三,权限控制没法做,任何工具一旦注册就全局可用。

MCP(Model Context Protocol)解决的正是这几个问题。它把工具的定义标准化成一份带 schema 的描述,Agent 通过协议去发现和调用工具,工具本身可以独立部署、独立鉴权。在内网里,你可以起一个 MCP Server 专门管数据库工具,再起一个管审批工具,Agent 只认协议不认实现。这样加工具就是加一个 Server,主流程一行不改。

选 MCP 还有一个现实原因:生态在往这个方向收敛。不管是哪家的 Agent 框架,对 MCP 的支持都在变好,你现在按 MCP 写工具,将来换框架迁移成本最低。内网项目最怕的就是技术债,选一个正在成为事实标准的协议,比选一个当下最顺手的私有方案要稳。

2.3 Skills 的定位:把“会做的事”沉淀下来

MCP Tools 解决的是“能调什么”,Skills 解决的是“会做什么”。这两者容易混,我一开始也混了。举个具体例子:查审批状态是一个 Tool,它只负责发一个请求拿回原始数据;而“判断一个审批是否超时、超时了该催谁、催的时候话术怎么组织”这一整套逻辑,是一个 Skill。Tool 是原子能力,Skill 是编排好的能力包。

Skills 的价值在于复用和可维护。内网里很多操作是重复的,比如“根据工单号拉详情、判断优先级、决定是否升级、生成通知”。如果每次都让模型现场规划,既慢又不稳定。把它固化成一个 Skill,模型只需要决定“现在该用这个 Skill”,剩下的步骤由 Skill 内部确定性执行。这样既降低了模型负担,又提高了结果一致性。

我用的 Skills 组织方式是每个 Skill 一个目录,里面放一份描述文件(说明这个 Skill 干什么、什么时候用、需要什么参数)和一份执行逻辑。Agent 启动时扫描 Skills 目录,把描述注入到系统提示里,模型就能知道有哪些技能可用。这套机制在多个框架里都有对应实现,核心思想一致。

2.4 审批环节为什么必须单独设计

内网系统里,审批是绕不开的。Agent 可以自动查数据、自动生成内容,但一旦涉及“改状态”“发通知”“提交申请”这类有副作用的操作,就必须过审批。这不是技术问题,是合规问题。我见过有团队图省事,让 Agent 直接调写接口,结果误操作把一批工单状态改了,事后追责都找不到人。

我的做法是把审批做成 Agent 执行链路里的一个强制卡点。具体来说,凡是标记为“需审批”的 Tool,Agent 调用时不会直接执行,而是生成一条待审批记录,推给审批人,审批通过后才真正执行。审批流本身用内网已有的工作流引擎(Java 1.8 环境下可用的开源审批工作流就够用),Agent 只负责发起和查询,不负责审批逻辑。这样职责清晰,也符合内网的既有规范。

3. 核心细节解析与实操要点

3.1 内网模型服务的对接细节

模型服务这块,坑主要集中在接口兼容性和上下文长度上。内网推理服务起好之后,先确认它暴露的是不是标准的/v1/chat/completions接口,请求体和返回体字段是否和主流协议一致。有些自研推理框架字段名会不一样,比如把max_tokens写成max_new_tokens,这种就得在 Agent 侧做一层适配。

上下文长度要提前算清楚。内网模型往往是量化过的,上下文窗口可能只有 8K 或者 16K。Agent 的系统提示、工具描述、历史对话、工具返回结果全都要塞进这个窗口。我的经验是,工具描述尽量精简,只保留模型决策必需的信息;工具返回结果做截断,比如数据库查询只返回前 20 行加一个总数。不然跑几轮对话就爆窗口,模型开始胡言乱语。

还有一个细节是超时设置。内网推理服务如果并发高,响应会变慢。Agent 调用模型的超时不能设太短,否则正常请求也被判失败;也不能太长,否则一个卡住的请求会拖垮整个会话。我一般设 60 秒,配合重试一次。重试要幂等,也就是同样的输入重发不会产生副作用,这点在调用模型时天然满足。

3.2 MCP Tools 的注册与鉴权

写一个 MCP Tool 的流程大概是:定义工具名和描述、定义入参 schema、实现执行逻辑、注册到 MCP Server。描述这块要下功夫,模型是靠描述来决定用不用这个工具的。描述里要写清楚“这个工具做什么”“什么场景下用”“参数是什么含义”。我见过描述写得太简略,模型该调的时候不调,不该调的时候乱调。

鉴权是内网 Agent 的重点。我的方案是每个 Tool 调用都带一个身份标识,这个标识来自当前会话的用户,而不是 Agent 自己的服务账号。这样工具侧可以根据用户身份做权限判断,Agent 无法越权。实现上,Agent 在发起工具调用时把用户 token 透传下去,MCP Server 校验 token 后再执行。这样即使 Agent 被诱导去调一个它不该调的工具,工具侧也会拒绝。

注意:千万不要给 Agent 配一个万能的服务账号。一旦这个账号泄露或者被滥用,整个内网系统就裸奔了。透传用户身份是底线。

3.3 Skills 的编写规范

Skills 写得好不好,直接决定 Agent 稳不稳。我的规范是三条:单一职责、确定性执行、可观测。

单一职责是说一个 Skill 只干一件事。“处理工单”这种太宽泛,应该拆成“查询工单详情”“判断工单优先级”“生成工单通知”三个 Skill。拆细了模型更容易选对,出问题也更容易定位。

确定性执行是说 Skill 内部的步骤要写死,不要在里面再让模型自由发挥。Skill 是编排好的流程,输入确定、输出确定。如果 Skill 内部还要模型决策,那就失去了固化的意义,不如让主 Agent 直接规划。

可观测是说每个 Skill 执行都要打日志,记录输入、输出、耗时、成功失败。内网排查问题全靠日志,没有日志的 Skill 等于黑盒。我一般会在 Skill 执行前后各打一条日志,中间关键步骤也打点,出问题时能快速定位是哪一步挂了。

3.4 审批卡点的实现方式

审批卡点的实现,核心是“拦截 + 挂起 + 恢复”。Agent 调用一个需审批的 Tool 时,不直接执行,而是生成审批单,把这次调用的上下文(谁发起的、要调什么工具、参数是什么)存下来,然后返回一个“已提交审批”的结果给 Agent。Agent 告诉用户“操作已提交,等待审批”。

审批通过后,需要一个机制把这次调用恢复执行。我的做法是审批系统在通过时回调 Agent 的一个接口,Agent 根据存下来的上下文重新发起工具调用,这次带上“已审批”标记,工具侧放行。整个链路里,Agent 是无状态的,状态存在审批单里,这样 Agent 重启也不影响待审批的任务。

这里有个容易忽略的点:审批超时怎么办。审批单不能无限期挂着,得设一个过期时间,过期自动作废,并通知发起人。不然一堆僵尸审批单堆在那里,既占资源又容易误批。

4. 实操过程与核心环节实现

4.1 环境准备与目录结构

内网环境准备的第一步是确认 Python 版本和依赖。我用的 Python 3.10,因为很多 Agent 框架对 3.8 支持在减弱,3.10 是比较稳的选择。依赖全部走内网镜像源,提前把需要的包同步进来。这一步没什么技巧,就是耐心,把requirements.txt里的包一个个确认内网源里有。

目录结构我按功能划分,大致是这样:

agent/ config/ # 配置文件,模型地址、超时、日志级别 core/ # Agent 编排主逻辑 mcp_servers/ # 各个 MCP Server db_server/ approval_server/ skills/ # 各个 Skill query_ticket/ judge_priority/ adapters/ # 模型接口适配层 logs/ # 日志输出

这个结构的好处是每一块都能独立测试。MCP Server 可以单独起起来用 curl 测,Skill 可以单独写单测跑,不用每次都把整个 Agent 拉起来。内网调试成本高,能单独测的绝不整体测。

4.2 模型适配层的代码实现

适配层的职责是把内网模型的接口包装成 Agent 框架认识的格式。核心是一个 chat 方法,接收消息列表,返回模型回复。下面是一个简化版的实现:

import requests class InternalLLM: def __init__(self, base_url, model_name, timeout=60): self.base_url = base_url.rstrip("/") self.model_name = model_name self.timeout = timeout def chat(self, messages, tools=None): payload = { "model": self.model_name, "messages": messages, "temperature": 0.1, } if tools: payload["tools"] = tools resp = requests.post( f"{self.base_url}/v1/chat/completions", json=payload, timeout=self.timeout, ) resp.raise_for_status() return resp.json()["choices"][0]["message"]

temperature设 0.1 是为了让输出稳定,Agent 场景不需要创意,需要的是可预测。tools参数是可选的,有些内网模型不支持 function calling,那就得走提示词里描述工具、让模型输出特定格式再由代码解析的路子。这两种方式我都用过,支持原生 function calling 的优先用原生,稳定得多。

4.3 一个完整的 MCP Tool 实现

以“查询工单详情”为例,走一遍完整实现。工具定义部分:

TOOL_DEFINITION = { "name": "query_ticket", "description": "根据工单号查询工单的详细信息,包括状态、优先级、创建时间、处理人。当用户询问某个工单的情况时使用。", "input_schema": { "type": "object", "properties": { "ticket_id": { "type": "string", "description": "工单编号,格式如 TK-20240101-001" } }, "required": ["ticket_id"] } }

执行逻辑部分,关键是鉴权和错误处理:

def execute(ticket_id, user_token): if not check_permission(user_token, "ticket:read"): return {"error": "无权限查询工单"} try: row = db.query_one( "SELECT * FROM tickets WHERE ticket_id = %s", (ticket_id,) ) if not row: return {"error": f"工单 {ticket_id} 不存在"} return { "ticket_id": row["ticket_id"], "status": row["status"], "priority": row["priority"], "created_at": str(row["created_at"]), "assignee": row["assignee"], } except Exception as e: log.error(f"query_ticket failed: {e}") return {"error": "查询失败,请稍后重试"}

注意返回结果里不要带敏感字段,比如内部备注、客户联系方式。工具返回什么,模型就能看到什么,也会原样转述给用户。返回前做一层字段过滤是必要的。

4.4 审批流的接入代码

审批接入分两步:发起和执行。发起部分,Agent 检测到工具需要审批时,不执行工具,而是调审批接口:

def request_approval(tool_name, params, user_token): approval_id = approval_client.create( title=f"Agent 请求执行 {tool_name}", detail=json.dumps(params, ensure_ascii=False), applicant=get_user_from_token(user_token), expire_hours=24, ) return { "status": "pending_approval", "approval_id": approval_id, "message": "操作已提交审批,请等待审批人处理" }

执行部分,审批通过后的回调:

def on_approval_passed(approval_id): record = approval_store.get(approval_id) if not record or record["status"] != "pending": return result = execute_tool( record["tool_name"], record["params"], record["user_token"], approved=True, ) notify_user(record["user"], result) approval_store.mark_done(approval_id)

这里approved=True是关键,工具侧看到这个标记才放行。整个链路里,审批单是唯一的状态载体,Agent 本身不存状态,这样水平扩展 Agent 实例时不会有状态同步问题。

4.5 并发处理的实际做法

“AI Agent 怎么扛并发”是个高频问题。我的答案是:Agent 本身要无状态,把并发压力分散到各个无状态实例上,前面挂一个负载均衡。会话状态存 Redis,工具调用走异步,审批走队列。

具体来说,每个用户请求进来,负载均衡分给某个 Agent 实例,实例从 Redis 拉会话历史,处理完写回 Redis。这样任何一个实例挂了,请求转到别的实例照样能继续。工具调用如果是耗时的,不要同步等,扔到消息队列里异步处理,处理完再回调通知。审批本身就是异步的,天然适合队列。

实测下来,单实例在 4 核 8G 的机器上,配合内网模型服务,能稳定支撑几十路并发会话。瓶颈通常不在 Agent 代码,而在模型推理服务的吞吐。所以并发上不去的时候,先看模型服务是不是排队了,别急着优化 Agent。

5. 常见问题与排查技巧实录

5.1 模型不调工具或者乱调工具

这是最高频的问题。表现是模型该调工具的时候不调,或者调了一个完全不相关的工具。排查顺序是这样:先看工具描述是不是太模糊,模型看不懂什么时候该用;再看工具数量是不是太多,一次给模型几十个工具,它选择困难;最后看系统提示里有没有明确告诉模型“遇到 X 情况必须用 Y 工具”。

我的经验是工具数量控制在 10 个以内,超过就分组,按场景动态加载。比如用户问工单相关的问题,只加载工单类工具,别把审批、监控的工具全塞进去。这样模型选择范围小,准确率明显提升。

5.2 工具返回结果太长导致上下文爆炸

数据库查询返回几百行,模型上下文直接爆掉。解决办法是在工具侧做截断和摘要。查询类工具默认返回前 20 行,同时返回总数,让模型知道“还有更多”。如果模型需要更多,再让它带分页参数重新查。这样既控制了上下文,又保留了继续查的能力。

5.3 审批卡住导致会话挂起

用户提交了审批,然后就没下文了。排查发现是审批回调没配好,审批通过了但 Agent 不知道。解决办法是加一个轮询兜底:Agent 定期扫描自己发起的、状态还是 pending 的审批单,发现已通过的主动恢复执行。回调是快路径,轮询是慢路径,两条路都通才稳。

5.4 内网依赖缺失导致启动失败

内网装包是个老大难。我的做法是提前把所有依赖打成离线包,包括间接依赖,放到内网源里。启动前先跑一个依赖检查脚本,缺什么一目了然。另外注意有些包在安装时会去外网拉东西,这种要在内网源里提前准备好对应的资源,或者找替代包。

下面这张表是我整理的高频问题速查:

问题现象可能原因排查动作解决方式
模型不调工具描述模糊/工具过多检查工具描述和数量精简描述,分组加载
上下文爆炸工具返回过长看工具返回体大小截断+分页
审批不恢复回调未配/失败查审批回调日志加轮询兜底
启动失败依赖缺失跑依赖检查脚本补离线包
并发上不去模型服务排队看模型服务指标扩推理实例
权限报错token 未透传查工具侧鉴权日志透传用户身份

5.5 几个只有踩过才知道的坑

第一个坑:内网模型的 function calling 支持不完整。有些模型声称支持,但实际调用时参数格式不对,或者多轮工具调用会丢上下文。遇到这种,退回提示词解析方案,虽然土但稳。

第二个坑:审批单的并发。同一个用户短时间内提交多个审批,如果审批系统没做幂等,可能重复执行。我的做法是审批单带一个业务唯一键,重复提交直接返回已有单子。

第三个坑:日志里别打敏感数据。工具参数里可能有用户信息、内部编号,日志脱敏要做在前面,别等出事再补。

6. 一些关于内网 Agent 的个人体会

内网 Agent 这个方向,技术上的天花板其实不高,难的是把工程细节磨平。我做了几个内网项目下来,最大的感受是:别追求 Agent 多智能,先追求它多可靠。一个只会查工单、走审批的“笨” Agent,只要稳定不出错,业务方就愿意用;一个什么都能聊但时不时调错工具的“聪明” Agent,没人敢让它碰生产系统。

Skills 的沉淀是长期价值所在。每解决一类问题,就把它固化成一个 Skill,日积月累,Agent 的能力边界是靠 Skills 撑起来的,不是靠模型。模型会换,Skills 不会白写。

最后分享一个小技巧:内网 Agent 上线前,一定要做一轮“对抗测试”,故意用模糊的、带歧义的、甚至诱导性的输入去试它,看它会不会越权、会不会调错工具、会不会绕过审批。这轮测试暴露的问题,比正常测试多得多。我每次上线前都跑一遍,基本每次都能揪出几个隐患。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询