Agent工具链太乱?用统一网关收敛MCP、Skills与多模型调用
我花了大半个周末在整理 Agent 项目的工具调用链被一堆 MCP server、skills 和各家 LLM API 的差异搞到头皮发麻。事情看起来都有解MCP 有协议文档、skills 有模板规范、LLM 也有五花八门的 SDK。可一旦把它们组合在一起组合复杂度才是真正压垮人的地方。后来我动手做了一个叫 tsm-hub 的小网关把 LLM、Tools、MCP、Skills 统一收进一个入口本意是让自己别再因为切换 provider 或者接一个新的 MCP server 就熬夜改代码。这篇文章就是这次折腾的完整记录包含资源模型设计、MCP 接入、skills 管理、多模型调度以及我踩过的几个比较隐蔽的坑。1. 工具管理的熵增我为什么最终走到统一网关这条路1.1 Agent 工程里工具散落的真实状态先说说我的项目背景。早期我做的 Agent 很简单一个模型 几个内部函数跑通一个垂直流程就完事。但随着能力扩展工具链变成了这样的状态LLM 层面至少三个 provider 在轮换代码生成、任务拆解、摘要总结各自用的模型都不一样有的走 OpenAI 兼容接口有的走 Anthropic 格式有的还有特殊的 tool_choice 参数。工具层面自研的搜索、爬虫、数据库查询工具散落在不同的 Python 服务里各自有鉴权方式参数风格也不统一。MCP 层面陆陆续续接入了 Playwright MCP 做浏览器自动化、Blender MCP 处理 3D 操作、蓝湖 MCP 拉设计稿标注还有一些内部开发的 MCP server。Skills 层面从 GitHub 上拉了不少技能包有 Superpowers Skills、OpenCode Skills、Codex Skills还有前端开发类的常用技能每个包的格式、依赖、入口都不太一样。真正让我崩溃的时刻是一次 Agent 任务在快结束时想去调用一个 MCP 工具但那个 server 因为 token 过期直接拒绝。而我的代码里根本没有一个地方统一管理 MCP 的鉴权策略只能每个调用点单独处理。那一刻我就意识到这不是某个工具的问题是整个工具层的结构问题。1.2 网关层到底解决什么问题如果你做过微服务治理再看 Agent 的工具层感觉会非常熟悉。微服务多了以后大家不会让每个服务之间直连而是加一层 API 网关做路由、鉴权、限流和协议转换。Agent 的工具层也是一样的困境MCP server 越来越多、skills 越来越杂、LLM provider 各有各的脾气如果让业务代码直接面对这些散点最后一定是一堆兼容补丁叠补丁。tsm-hub 要做的就是把能力提供方和能力使用方解耦。对上层 Agent/业务流程来说它只暴露一套稳定的接口给我一个任务描述我返回工具的调用结果。对下层来说它统一管理 MCP server 的注册和鉴权、skills 的装载与依赖校验、LLM provider 的协议转换和模型路由。用生活类比来说以前你家每个电器都自带一根专属插头你要准备一堆转接头tsm-hub 相当于把所有插头改成了统一标准接口然后由一个总闸统一供电、统一控制开关。这样做的收益不是单次调用变快了而是当你新增第 20 个工具、第 10 个 skill 的时候整个系统的复杂度不会跟着线性上涨太多。1.3 tsm-hub 的定位不替代框架只做能力收敛这里有个边界必须先说清楚。tsm-hub 不是一个 Agent 编排框架也不打算替代 LangChain、Claude Agent SDK 这类东西。它定位是能力网关框架负责思考和决策tsm-hub 负责把 Agent 需要的所有外部能力收敛成一个统一入口。设计上我参考了这么一条原则工具调用链路中凡是多个工具共有的横切关注点——鉴权、协议转换、超时控制、缓存、审计日志——全都下沉到网关凡是单个工具独有的业务逻辑留在工具自身。比如 Playwright MCP 怎么开浏览器、Blender MCP 怎么操作网格这些细节网关不管但连接是否可用、token 是否需要刷新、工具返回结果要不要统一包装这些事由网关统一处理。这个定位非常重要因为它决定了你要不要自己也做一个这样的网关。如果你的项目只是在本地玩几个 demo直接用框架自带工具注册机制就够了完全没有必要引入一层新东西。但一旦工具数量超过一定规模、跨多个技术栈、需要多人协作共享能力网关层的价值就会非常明显。2. 资源模型设计把 LLM、Tools、MCP、Skills 压成同一套抽象2.1 为什么必须统一抽象tsm-hub 的第一步设计也是最关键的一步是建立统一资源模型。否则你依然是在分别管理四套不同的东西网关只是个四不像的容器。统一抽象有个天然抓手所有 LLM 最终都通过 function calling 或 tool use 来消费工具能力也就是说在最底层工具就是一堆 JSON Schema 执行函数。既然如此我就把 MCP server 提供的工具、自研工具、以及 skill 所依赖的工具全部归一化成统一的 tool descriptor。但光统一 Tools 还不够因为 LLM、Skills 在网关里还有另外的角色。我最后设计的资源模型是四个维度ProviderLLM 厂商接入信息包括 base_url、模型列表、协议兼容类型、密钥引用。Tool单一可执行能力统一为 name、description、input_schema、execute endpoint。MCPLink一个 MCP server 的注册配置网关负责把它下面的 tools 批量拉取并转为 Tool。Skill一个技能包包含触发条件、提示词模板、以及依赖的 Tool 列表。所有路由、鉴权、限流策略都挂在这四类资源之上。也就是说网关管理员不会直接跟某个具体的 MCP SDK 打交道而是声明一个 MCPLink 配置网关自动同步它下面的工具清单。2.2 网关侧的配置模型我用的配置格式是 YAML原因是可读性好、方便跟 GitOps 配合。一个典型的 MCPLink 配置长这样mcplinks: - name: playwright_mcp transport: stdio command: npx args: - playwright/mcplatest env: HEADLESS: true BROWSER_TIMEOUT: 30000 tools_prefix: browser_ auth: type: none - name: internal_search transport: sse url: https://mcp.internal.example.com/sse headers: Authorization: Bearer ${SEARCH_MCP_TOKEN} tools_prefix: search_ sync_interval: 300这里有两个设计点值得展开。第一是tools_prefix我强制要求每个 MCP server 下的工具都带一个命名空间前缀。为什么因为多个 MCP server 很容易出现同名工具比如 Playwright 里有navigate内部工具可能也有navigate。如果直接冲突LLM 在选择工具时会出现歧义甚至被 provider 侧的 schema 校验直接拒绝。加前缀虽然让工具名变长了但换来了确定性我觉得完全值得。第二是sync_interval。MCP server 的工具列表并不是静态的远程 server 可能随时新增工具。网关会定时通过 MCP 协议拉取工具清单同步本地索引。这个设计让我可以在不重启 Agent 的情况下让新工具在几分钟内对 LLM 可见。2.3 从统一模型到 function calling收益与代价把四类资源统一抽象之后上层 Agent 向 LLM 发请求时看到的就只是一份合并后的 function list。比如一个混合场景里网关最终会产生这样的 tools 数组[ { name: browser_navigate, description: Navigate to a URL in headless browser, input_schema: { type: object, properties: { url: { type: string, description: Target URL } }, required: [url] } }, { name: search_query, description: Query internal knowledge base, input_schema: { type: object, properties: { q: { type: string } }, required: [q] } } ]这个转换过程本质上是把 MCP 的 tool schema、skills 里声明的依赖工具、自研工具的 JSON Schema 全部翻译成目标 LLM 要求的格式。但统一抽象是有代价的。最大的代价在于不同 LLM 的 tool schema 方言差异往往会在转换过程中丢失一些细节。比如某些模型支持strict: true强制 JSON Schema 模式某些模型支持tool_choice细粒度控制还有一些模型会对description里有特殊符号的内容过敏。我在设计网关时专门加了一层方言适配器针对不同 provider 做 schema 清洗。简单说就是过滤掉目标模型不支持的字段同时保证name、description、input_schema三个核心字段的完整性。3. MCP 接入实战从协议到网关的一站式托管3.1 MCP 里真正需要网关处理的只有三件事MCPModel Context Protocol本质上是一个 JSON-RPC 2.0 协议定义了客户端与 server 之间交换上下文能力的方式。对网关来说MCP 里最值得关注的是 tools、resources 和 prompts 三类能力。我不打算在这里大段科普协议因为官方文档写得很清楚我只说网关视角下的三件事工具发现通过tools/list拉取 server 提供的工具清单。工具调用通过tools/call执行工具并拿到结构化结果。资源与提示词同步把 resources 和 prompts 映射到网关内部的 skill 或知识库索引。很多 MCP server 不仅仅是纯工具集合还会暴露一些 prompt 模板。比如一些设计协作类的 MCP它会自带如何分析蓝湖设计稿之类的提示词模板。tsm-hub 在同步时会把这类 prompt 转成 skill 的预设上下文这样 Agent 不光是多了一个工具还学会了用这个工具的正确姿势。3.2 本地 MCP 与远程 MCP 的接入差异MCP 传输方式主要有 stdio、SSE 和 streamable HTTP 三种。我实际接入下来发现本地 MCP 和远程 MCP 在网关里的处理方式要区分开。本地 MCP 用 stdio比如接一个 Node 写的 Playwright MCP。这类 server 生命周期短网关最好用按需拉起策略有请求时启动进程空闲一段时间后关闭避免一堆 MCP 进程常驻吃内存。我还做了个简单的进程管理给每个 stdio server 设置最大并发调用数防止 Agent 同时发起多个浏览器自动化请求把机器搞挂。远程 MCP 走 SSE 或 HTTP比如连接团队内部的知识库 MCP、或者某个 SaaS 提供的 MCP API。这类接入要处理的问题就变成了网络策略和鉴权。我踩过的一个典型坑是某些远程 MCP 返回的 SSE 流如果不及时消费连接会被服务端主动断开而且不会自动重连。所以网关里必须有一个连接池 心跳机制专门维护远程 MCP 的长连接状态。3.3 一个典型报错的排查provider rejected the request schema or tool payload实际操作中我遇到最头疼的一个报错信息是llm request failed: provider rejected the request schema or tool payload.刚开始看到这句我以为是网关把参数传错了后来排查了很久发现导致这个错误的原因有三个层次第一层是 schema 字段不兼容。某些 provider 要求 tools 里每个 property 必须要有description如果你的工具定义里有字段没写描述直接把 MCP 原始 schema 透传过去就会触发拒绝。解决办法是网关在做方言适配时对缺失description的字段补成No description provided兜底。第二层是type嵌套问题。部分 MCP server 的 schema 里会出现类似type: [string, null]这种联合类型写法很多 LLM provider 的 API 并不接受这种写法。网关需要在适配层把联合类型改写成type: string并增加nullable: true。第三层是最隐蔽的工具数量超过 provider 上限。有一次我一次性把 60 多个工具全塞给 LLM结果某些 provider 在工具数量超过 32 个或总体积超过一定大小后直接返回拒绝报错恰恰就是 provider rejected the request schema or tool payload。这个排查过程很折腾人因为报错信息没有明确指出是工具太多还是schema 非法。最终我在网关里做了两层防护一是对工具总字节数设阈值超过就启用按需工具加载通过 keywords 匹配备选工具子集二是对每个 provider 单独配置最大工具数量到达上限前自动裁剪低优先级工具。4. Skills 技能包把散落在 GitHub 的技能装进自己的网关4.1 SKILL.md 与技能包的事实标准先说清楚 Skills 到底是什么。它不是你写的一个普通 prompt而是一个可复用能力包通常包含一个SKILL.md文件描述技能名称、触发场景、使用步骤、可能还有少量脚本或依赖的工具列表。Claude Code 带火了这种格式现在很多项目都在用比如 Superpowers Skills、OpenCode Skills、Codex Skills还有各种前端开发技能包。我在处理这些技能包时发现它们之间的格式差异很大。有的把逻辑写在SKILL.md的 frontmatter 里有的用 json 描述元数据有的直接把整个 prompt 模板放在主文件里。tsm-hub 在加载技能包时我设计了一个统一的 skill descriptorskill: name: frontend_audit version: 1.2.0 description: Analyze frontend page structure and performance issues source: repo: https://github.com/example/frontend-skills path: skills/frontend_audit triggers: - check frontend - 页面性能分析 requires_tools: - browser_navigate - browser_snapshot prompt_template: | You are an experienced frontend engineer...requires_tools是技能包和网关索引之间的关键接口。skill 本身不包含工具实现它只声明我运行的时候需要哪些工具。网关在装载 skill 时会校验这些requires_tools是否都已在 MCPLink 或内部工具里注册如果缺失就把该 skill 标记为不完整并且不向 LLM 暴露避免出现模型决定使用这个技能但底层工具根本不存在的尴尬。4.2 从 GitHub 手动安装 skills 的正确姿势很多教程会让你直接git clone一个 skills 仓库然后把文件夹丢进某个目录。我也这么干过但我后来发现这不够skills 的依赖工具如果没装好光复制文件是没有意义的。我的手动安装流程通常是四步先把仓库 clone 到网关的skills_catalog目录下每个仓库一个子目录。读取所有SKILL.md的 frontmatter提取 name 和 description交给开发者审核。检查每个技能声明的requires_tools对照当前网关里已注册的工具列表缺什么先去接对应 MCP server。确认无误后把 skill 加入启用列表并测试一条最小触发消息。这个过程我在网关里做成了半自动化。Git 仓库同步是自动的但技能启用必须人工确认。为什么因为有些 skills 写的 prompt 模板质量很差直接启用会在 Agent 里引入干扰。我遇到过某个技能包在加载之后模型开始频繁试图用专家思维模式回复所有问题反而把基础任务带偏了。后来所有第三方 skill 在默认情况下都处于disabled状态只有显式启用才会进入调度池。4.3 网关里做技能版本管理和依赖解析既然 skill 从 GitHub 来就绕不开版本管理。我除了上面说的目录式存储还在网关里维护了一个简短索引表技能名称来源仓库当前版本依赖工具状态frontend_auditexample/frontend-skills1.2.0browser_navigate, browser_snapshotenabledcode_reviewerexample/review-skills0.9.1git_diff, search_querydisabledllm_wiki_retrieverinternal/llm-wiki2.0.0wiki_searchenabled这张表就是整个技能的账本。网关会在每天早上或手动触发时做一次 git pull如果发现远端版本变化就把新版本下载到临时目录diff 之后提示我人工确认升级。因为技能包本质是 prompt 模板改一行字都可能改变 Agent 行为所以自动升级我是不敢开的。依赖解析也在这里受益。以前自己手动管理技能依赖时经常忘记某个技能要用哪个 MCP server。现在技能声明了requires_tools而 MCPLink 同步过来的工具带tools_prefix网关可以直接通过命名前缀反查依赖关系甚至帮我提示frontend_audit技能需要browser_前缀的工具但当前没有启动任何浏览器类 MCP server。另外如果做内部知识库场景我推荐把 LLM Wiki 这类检索服务封装成一个不需要模型调用的纯工具而不是把它做成一个 prompt 型 skill。因为知识库检索本质是取数据做成工具模型才知道什么时候调、传什么参数做成技能的话模型很容易只是读一段描述而没有真正执行检索。5. LLM 调度与工具路由多模型环境下网关里的交通管制5.1 多 Provider 下的统一请求入口tsm-hub 里我最开始只想管理工具但后来发现 LLM provider 的管理也必须放进网关否则统一抽象就是空谈。原因很简单同一个工具返回结果后究竟交给哪个模型去做下一步决策这本身就是一个路由问题。统一请求入口的核心是把各家模型的 SDK 差异抹平。我在网关里实现了一个轻量适配层支持 OpenAI 兼容、Anthropic 原生、以及一些自托管模型的协议。业务代码只需发一个统一格式的请求{ model: code-default, messages: [ { role: user, content: 分析一下当前页面渲染性能 } ], tools: [browser_navigate, browser_snapshot, audit_performance] }model: code-default不是真实的模型 ID而是网关里的一个逻辑别名。这个别名会解析成具体的 provider 和模型 ID比如anthropic/claude-sonnet-4-5或openai/gpt-4.1。这样业务代码不需要关心哪个模型叫啥需要换模型时只改网关配置。5.2 按任务和工具自动路由模型网关里最有用也最危险的功能是根据工具前缀自动路由模型。我实际配置的经验是这样的涉及浏览器自动化、多步操作的任务路由到上下文窗口大、工具调用稳定的模型。简单检索、单轮问答路由到便宜且响应快的小模型。涉及代码生成的路由到特定代码类模型。这套策略在成本上的收益非常明显。我把原来所有请求都走最大模型的情况改了之后账单降了大概 30% 以上而用户体验几乎没变。但这里有个陷阱不能只看工具名路由还要看任务复杂程度。有一次我对所有browser_开头工具请求强制走最强模型结果某些简单截图任务也跑了大模型延迟增加明显。后来我增加了预期调用链长度的判断网关会根据本次请求要调用的工具数估算任务复杂度再决定是否升级模型。5.3 限流、重试与降级策略网关一旦成为所有工具请求的必经之路它就不可避免要承担容错职责。我在这一层做了三件比较常规但极其重要的事限流按用户、按技能、按 MCP server 三档限流。防止某一个 Agent 任务因为 bug 疯狂调用 MCP server把内部服务打爆。重试只对幂等工具做自动重试非幂等工具一律不重试。这是血的教训有一次对创建订单类工具做了自动重试结果重复下单了。降级当某个 MCP server 不可用时网关自动从工具列表里摘除它的所有工具而不是把错误直接抛给 LLM。关于降级多说一点。以前没做摘除时模型如果选了一个宕机的工具整个会话就会卡在等待错误上几轮之后才会恢复。现在网关会在心跳检测失败后把该 MCP 的工具标记为不可用并在调用 LLM 时直接从 tools 数组里去掉模型就不会再选了。这一步对 Agent 体验的稳定提升非常明显。6. 生产环境里的边界与取舍tsm-hub 不解决什么6.1 我要的安全边界审批与最小权限统一网关最大的红利之一是安全策略可以集中落地。在我自己的部署里所有高风险的 MCP 工具比如能写文件的、能执行 shell 的、能发消息的都设置成了需要人工审批模式。网关在这个场景下可以做得更细工具参数级别的策略。比如允许调用浏览器截图但禁止浏览器下载文件。因为网关统一收口了所有工具的入参和出参这类策略实现起来反而简单不需要在每个工具内部加判断。我还把 token 和密钥集中放到网关配置里用环境变量注入技能包仓库里不再存任何凭据。这样即使 skills 目录不小心同步到公开仓库也不会直接泄露密钥。6.2 性能与观测网关不要成为新的瓶颈引入网关最怕的就是延迟放大。MCP server 调用本身已经有网络开销如果网关再串行处理多个请求整个链路会慢得让人怀疑人生。我做了两件事缓解这个问题一是工具发现结果缓存。MCP 的工具清单不常变化同步后缓存在内存里调用时不需要每次走tools/list。 二是调用链路中间结果的缓存。有些 MCP 工具本身是只读查询比如获取设计稿标注这类接口短时间内重复调用结果一样。网关按参数 hash 缓存返回结果设置 5 分钟过期实测让部分工具的重复调用延迟从 2~3 秒降到了毫秒级。观测方面Gateway 的访问日志、MCP server 健康状态、工具调用频次、token 消耗这几个指标我都做了埋点。实际上工程上统一入口的价值在排查问题时会充分体现你可以直接在网关日志里看到一次 Agent 会话调了哪个工具、花了多久、返回了什么而不是去各个工具的日志里大海捞针。6.3 一些最终的使用建议如果你也在考虑做一个类似的工具网关我给几条比较务实的建议不要一上来就追求接入所有东西。先把两个 MCP server、两个自研工具、一个模型 provider 跑通验证统一抽象是否真的简化了你的代码。skill 的启用按钮必须人工控制。技能包是 prompt 模板它可能提升模型能力也可能带偏模型行为最好经过测试再启用。命名空间前缀在一开始就要定好。中途再重命名工具会导致之前 Agent 会话里已经学会的工具名全部失效。网关的配置尽量走 Git 管理回滚和多人协作会舒服很多。最后说一点个人体会这类网关最核心的价值不是某个花哨的调度算法而是它逼着你把所有工具能力显式化、配置化。当你不得不用统一 schema 描述每一个工具、每一个技能、每一个 MCP server 的时候隐式的散乱问题自然就暴露出来了。tsm-hub 对我来说不是一个完美产品但它让我在没有被工具复杂度拖垮的前提下继续安心地把 Agent 业务往前推进。如果你也正在被同样的复杂度困扰可以从小规模开始试试这套思路哪怕只是给工具加一层薄薄的统一入口也大概率能让你少熬夜。