ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

tsm-hub:统一网关融合LLM、工具、MCP与Skills的架构实践

tsm-hub:统一网关融合LLM、工具、MCP与Skills的架构实践 从四套系统到一个网关tsm-hub 如何把 LLM、Tools、MCP、Skills 收进同一个入口如果你最近在做 Agent 相关的东西大概率已经受够了这种状态大模型 API 要单独对接函数调用要自己实现MCP server 这个仓库一个、那个仓库一个Skills 脚本又是另一套加载逻辑。我也是被这几样东西来回折腾了几个月最后忍无可忍才动手写了 tsm-hub 这个统一网关。简单说它就是把 LLM、Tools、MCP、Skills 四类组件收进同一个入口让你面向一个网关编程而不是同时维护四五套集成方式。这篇文章讲讲我为什么这么设计、核心模块怎么拆以及你照着落地会碰到的那些坑。这个内容适合谁适合正在做 AI Agent、企业内部 AI 中台或者单纯觉得“工具调用越来越乱”的开发者。不管你是刚接触 MCP 协议还是已经写了十几个 Agent 服务这套网关的思路都能直接拿过去用——就算你不打算引入 tsm-hub光是把下面这些模块划分和配置规范看明白也能帮你把现有项目理清楚。1. 为什么需要 tsm-hub四个孤岛问题1.1 一个真实任务暴露出的四个孤岛先看一个很普通的场景你想做一个“帮我查会议室、预订空闲时段、顺便在群里发通知”的 Agent。就这么一个小任务你至少要面对以下四套东西第一是 LLM。你选一个模型供应商搞定它的鉴权、消息格式、流式响应如果哪天想换成另一家模型所有调用代码都要跟着改。第二是 Tools。你写好了查会议室、订会议室的业务函数要把它们变成模型可以“看见”的工具描述还要自己处理函数参数的 JSON Schema。第三是 MCP server。公司有个统一会议系统开放了 MCP 协议你要在 Agent 里初始化 MCP client管理 server 的进程生命周期、连接握手、协议版本。第四是 Skills。你可能之前已经在 Claude Code 或同类工具里攒了不少 skill 包它们靠自然语言指令驱动跟你现在写代码调函数的 Agent 完全是两套逻辑。这些组件单独用都挺顺手一旦组合起来就出问题。最常见的一种崩溃现场是模型说“我要调 get_meeting_room”你的 Tools 注册表里确实有这个函数但 MCP server 那边也暴露了同名工具两边优先级没定义模型随机选择结果一会儿走直连函数、一会儿走 MCP 通道日志里行为全靠猜。更麻烦的是 Skills它本质上是一套“教模型怎么做”的文本指令跟普通的“工具函数调用”还不一样没法用统一方式触发。这就催生了我的核心需求能不能做一个网关把四类组件注册到同一套路由表里让模型、工具、MCP server、skill 都通过一个入口对外服务1.2 统一网关的三种解法思路针对上面这个问题按经验大体上有三条路可以走。第一种是自以为省事的“协议翻译层”方案。在每个 Agent 里分别装 SDK把 MCP 协议翻译成内部工具调用把 Skills 翻译成 prompt。这种做法初期开发快但每加一个新 Agent、每接一个新 MCP server翻译逻辑就要复制一遍后期维护成本翻着倍往上涨。第二种是做“服务网关”但只做透传。它把 MCP server 的地址统一管理起来LLM 和 Tools 仍然走各自的路径。这种网关解决不了“一个 Agent 同时需要模型、工具、技能”的编排问题治标不治本。第三种才是 tsm-hub 走的路子定义一中立的运行时内核让 LLM、Tools、MCP、Skills 全部以“能力单元”的形式注册到网关里由网关统一处理鉴权、路由、批量调度与重试。Agent 侧只有一个输入输出协议你给网关发一条带目标意图的请求网关自己决定是调模型、走工具、连 MCP 还是激活某个 skill。我最终选了第三种因为它的长期维护成本最低后续新增能力时只是给网关增加一个注册项不会污染业务代码。2. tsm-hub 架构拆解核心模块与统一协议2.1 核心模块模型网关、工具路由、MCP 适配器、技能库tsm-hub 内部拆成四个核心模块它们的职责边界非常清楚。第一个模块是模型网关Model Gateway。它统一封装了各家 LLM API对外只暴露 chat 和 stream 两类接口。你需要在这里做供应商适配器比如 OpenAI 系列的 function calling 格式、Anthropic 的 tool use 格式、国产模型的 tool 格式——各家有各家的差异但内部都翻译成统一的消息体。这个模块的另一个关键职责是超时管理因为不同模型供应商的响应速度差异极大必须有统一的超时策略否则一个慢模型会把整个网关拖住。第二个模块是工具路由Tool Router。它维护一张能力注册表每一项工具或能力都有唯一标识、用途描述、输入输出 Schema以及可调用的后端列表。这个模块解决的核心问题是“同名工具”冲突当你同时接入了直连函数和 MCP server 暴露的同名工具时可以按服务商优先级、健康状态、响应延迟来决定路由到谁。没有这个路由层你就只能在业务代码里写死 if else。第三个模块是 MCP 适配器MCP Adapter。它负责所有 MCP server 的生命周期管理。MCP 协议比较特别它同时支持 HTTP SSE 传输和 stdio 进程内传输两种方式适配器要处理连接池、心跳检测、重连、以及 MCP 版本兼容。这一层还承担一个隐藏任务把 MCP server 暴露的“工具列表”拉回来动态注册进工具路由表。这样你不需要在网关里预先写死“某个 MCP server 有什么工具”启动时自动发现即可。第四个模块是技能库Skills Registry。它有点像插件管理器。一个 skill 包通常包含一个 SKILL.md 文件自然语言说明书以及可被调用的脚本、Prompt 模板、参考资源。技能库负责打包、索引、版本管理这些 skill 包并且把它们从“prompt 文本的世界”映射成“可供路由的工具描述”。核心转化手段是给每个 skill 生成一个“技能触发描述”告诉模型什么场景下该激活这个 skill然后网关去按描述匹配调用。这四个模块之间的消息流是一条完整链路Agent 发请求进来模型网关先让 LLM 判断意图如果模型决定需要外部能力就会输出结构化工具调用工具路由根据调用目标去查注册表找到对应的 MCP server 或本地函数MCP 适配器随后执行远程调用或进程内调用把结果返回给模型网关最终由模型生成面向用户的答案。2.2 为什么选 MCP 做统一协议基于各家工具的融合我在设计 tsm-hub 时最被反复问到的就是“为什么统一协议选了 MCPTools 和 Skills 不是各自有各自的格式吗”我讲讲我的取舍过程。两点考虑。第一MCP 已经事实上成为工具调用的开放标准之一生态里能直接接入的现成服务越来越多比如设计稿解析、浏览器操作、接口调试、测试平台这些方向都有可用的 MCP server。你不接入 MCP 生态就等于要手动为每一个外部服务重写一套工具接口那工作量可观。第二MCP 协议本身定义得足够完整——它有 client/server 模型、工具发现机制、标准化的参数描述格式这些都跟 tsm-hub 想要的“能力注册与自动发现”高度匹配。那 Tools 和 Skills 怎么办我的处理方式是“全部向 MCP 靠齐”本地函数写一层薄薄的 adapter 包装成 MCP serverSkills 也通过技能库包装成一个“虚拟 MCP server”暴露一个名为 invoke_skill 的统一工具。这样从路由层的视角看所有能力都是 MCP tool统一走同一套发现、调用、返回链路。你可能觉得这有点过度设计但实际操作中你会发现这个好处特别明显新增能力时不用改 Agent 的代码只需要注册一个新的 MCP server整个系统的可扩展性一下子打开了。3. 实操从零搭建 tsm-hub 的完整落地过程3.1 技术选型与前置准备这部分是实操我就直接说我自己的选型以及为什么这么选方便你拿来参考或者按你的偏好替换。后端主体我用的 Python 3.11 FastAPI。原因有三一是 LLM 生态里 Python 的库最齐全各个模型供应商的 SDK 基本都是 Python 优先二是 MCP 官方的 Python SDK 支持比较完善能省掉不少协议层的手写工作三是 FastAPI 的异步能力足够支撑流式响应和 SSE 推送。内部通信走 JSON配置管理用 YAML 文件加环境变量覆盖。进程管理器我用的是内置的 asyncio 任务调度——因为 MCP server 里有不少是 stdio 模式必须由主进程拉起子进程并通信用 asyncio.create_subprocess_exec 去管理生命周期最方便。前置准备其实不复杂你需要在环境里安装这些依赖fastapi提供 HTTP 服务框架uvicornASGI 服务器用来跑服务mcpMCP 官方 Python SDKhttpx异步调用 LLM APIpydantic做数据校验和 Schema 定义安装好之后建议在项目根目录建好 config/ 和 adapters/ 两个目录。前者放全局配置和供应商账号信息后者专门放各家 LLM 的适配器。目录清晰了后面每一步都会省力很多。3.2 第一步定义统一消息格式在接入任何东西之前先把内部消息格式定好。这是整个网关的地基后面所有模块都要围绕这个格式工作。我定义的核心消息体由三层组成。第一层是请求头部 request带上请求 ID、Agent 标识、会话上下文。第二层是意图结构 intent记录模型判断出的目标代号、原始自然语言指令、可选的参数列表。第三层是能力路由结果 route记录最终命中的工具类型、服务地址、请求超时时间。这里有一个比较隐蔽的设计点不要把某个模型供应商的执行结果直接塞进内部消息体。比如 OpenAI 返回的 tool_calls 数组和 Anthropic 返回的 tool_use 块结构差别很大如果你直接透传将来换模型时就得多写一层兼容逻辑。正确做法是在供应商适配器里把这些差异统一翻译成内部格式——统一的工具名、统一的参数 JSON、统一的执行状态码。这样后面无论对接哪家供应商路由层看到的都是同一种结构。统一消息结构定义完成后需要一个注册中心来登记所有能力。我把注册表做成了 YAML 文件加一个自动发现机制启动时扫描 rules/ 目录把每个 MCP server 的 tools 列表拉回来合并成一个全局能力表。这部分逻辑我用一个简化的伪代码表示async def load_tools(): all_tools {} for server in config.mcp_servers: client await MCPClient.connect(server) tools await client.list_tools() for tool in tools: # 统一封装成 ToolSchema all_tools[tool.name] { description: tool.description, input_schema: tool.input_schema, server: server.name, } return all_tools3.3 第二步接入一个 LLM 供应商适配器接口统一了接着写适配器。我以最常用的 OpenAI 兼容接口为例来讲。适配器要做什么简单说就是把网关的内部消息体——包括系统提示、历史对话、工具定义列表——翻译成某家供应商 API 能识别的格式。以 OpenAI 为例你需要把工具定义列表映射成 functions 数组每个函数包含名称、描述、参数 JSON Schema。当模型返回 tool_calls 时你要把其中的 function name 和 arguments 抽取出来转换成内部路由请求。实操里比较容易被坑的点是流式响应。OpenAI 的流式返回里工具调用的内容是分片到达的——有的 chunk 给函数名有的 chunk 给参数片段有的 chunk 追加内容。你要自己维护一个“正在流式构建的 tool call 缓冲区”等所有分片到达后再组装完整参数。我建议把这个逻辑写死在适配器里不要让上层业务感知到分片过程。我的实现里采用了异步生成器配合一个简单的状态机管理工具调用的开始、进行、结束三个阶段。不同供应商之间的差异主要体现在消息字段和鉴权方式上。OpenAI 用 Authorization Bearer tokenAnthropic 除了 token 还要额外带 version 头部分国产模型需要在请求体里加 extra_body 字段。我把这些差异全部收敛到 adapter 类的 get_request_headers 和 format_request_body 两个方法里每加一家模型就是新增一个 adapter 文件完全不影响其他模块。这里有一个关于超时的经验统一网关的超时不要只看“整体请求超时”要拆成“首 token 等待时间”和“流式空闲超时”两段。前者用来暴露模型服务是否挂了后者用来处理流式传输中断的情况。我在配置里默认设置首 token 等待 30 秒流式空闲超时 60 秒——你根据模型供应商的实际情况去调。相关的参数发散我放到 4.3 节再细讲。3.4 第三步把普通 Tools 注册成 MCP Server现在到了最关键的一步把已有的普通 Python 函数变成可以通过 MCP 协议调用的工具。理论上你可以给这些本地函数直接建一个内部 tool registry不走 MCP但为了统一协议我更推荐把它们包成 MCP server哪怕用 stdio 模式在本地起进程。好处是如果你以后想把这个工具开放给其他服务直接把这个 MCP server 部署出去就行不用改任何代码。整个包装过程分三步。第一步定义工具 Schema明确函数名称、描述、参数结构这一步会直接影响模型能不能正确调用。第二步用 MCP SDK 注册 FastMCP 实例把函数绑定进 server。第三步启动 MCP server 并把它配置到网关的连接池里。下面是一个实际例子。假设你有一个查询订单状态的函数那么注册成一个 MCP server 的代码大致是from mcp.server.fastmcp import FastMCP mcp FastMCP(order-service) mcp.tool() async def get_order_status(order_id: str) - dict: 根据订单号查询订单当前状态。 return {order_id: order_id, status: 已发货} if __name__ __main__: mcp.run(transportstdio)然后你在网关的 config 里加一行 MCP server 地址配置把这个 server 挂进来。就这么简单。我踩过的一个小坑是工具描述写得太随意。模型对工具的理解完全依赖这段描述你不能写“查询订单”而要写“根据订单号查询订单当前物流状态及签收时间适用于用户追问物流进度时调用”。描述越具体选工具的正确率越高。这个点怎么强调都不过分很多人在 MCP server 跑通之后抱怨“模型老选错工具”八成就是描述写得太模糊。3.5 第四步把 Skills 纳入网关Skills 的处理思路跟普通 Tools 不太一样。一个 skill 包一般不是单纯的“输入参数-返回结果”的函数它更像是一份操作指南加一组可执行脚本。比如“用 Playwright 做表格数据抓取”这个 skill里面的 SKILL.md 会告诉你抓取步骤、翻页策略、反爬注意事项配套脚本才是真正执行动作的代码。tsm-hub 处理 Skills 的策略是这样的技能库在启动时扫描 skills 目录读取每个 skill 的 SKILL.md提取出标题、用途、触发条件和主入口。然后把这些信息映射成一个虚拟工具工具名按 skill 包名生成比如 skill_web_scraper。当模型决定调用这个虚拟工具时网关就把“目标自然语言指令skill 的说明文档”一起交给底层的 driver——通常是 Claude Code 或 Codex 这类具备 agent 能力的执行器——去实际执行。这个设计的核心是把 skill 调用变成一种“懒执行”。模型不需要理解 skill 内部的复杂步骤只需要识别“这个场景适合触发某个 skill”执行细节全部交给 executor。这条路走通之后我发现一个新玩家入场特别快比如想接入“前端开发”相关的新 skill只需准备一个标准格式的 skill 包把它丢进 skills 目录网关重启后自动注册业务代码一行都不用动。对团队的扩展效率来说这种体验提升是非常明显的。4. 常见问题与排查技巧实录4.1 从 MCP 连接超时到设计原理的排查第一批问题集中在 MCP 连接的建立阶段最典型的是两种。一种是 stdio 模式的 MCP server 启动失败。特征是日志里出现类似“子进程退出退出码 1”的报错。排查思路按下面这几步走先手动在同目录下跑一遍启动命令看是不是缺依赖再确认子进程的 cwd 设置是否正确最后检查 stdout/stderr 重定向——stdio 模式的 MCP server 会把协议数据都打在主进程创建的管道上但有些 server 会把正常的 print 日志混进 stdout导致协议解析错乱。解决方案是让 server 端的日志全部走 stderr或者在网关的 stdio 适配逻辑里把 stdout 和 stderr 分开处理我在 tsm-hub 里就是分别建立了两个管道流下面的代码片段展示了一个可用配置proc await asyncio.create_subprocess_exec( cmd, stdinasyncio.subprocess.PIPE, stdoutasyncio.subprocess.PIPE, stderrasyncio.subprocess.PIPE, cwdworkdir, )另一个高频问题是 HTTPSSE 模式连接上了但 tools 列表返回为空。这通常不是网关的错而是 server 端工具的注册目录跟你预期的不一致。远程 server 经历过多次重新部署后工具列表可能发生变化。解决方法是把“工具发现结果”加一层持久化缓存并在启动日志里打清楚工具总数和明细——否则出了问题你连问谁都说不清。建议在配置里加一个 auto_sync_tools 开关启动时从 server 拉取工具列表运行中每 60 秒做一次增量同步确保 tools 表与真实暴露接口一致。4.2 从 model 误调用到 Schema 兼容性日常报错排查等链路跑通之后最常见的报错集中在模型与工具的 Schema 兼容上。一个小数问题在 AI 工程里其实非常常见但好多人第一次碰到会慌。首先是“模型返回了不存在的工具名”。这种情况十有八九是工具描述与工具名之间的关联做错了比如 Agent 的 system prompt 里出现了一个旧版工具名称或者你把两个不同 MCP server 的同名工具合并时没有做好路由。建议在工具注册阶段就对工具名做命名空间隔离比如 order_service_get_status 和 legacy_order_get_status能极大减少误调用。第二类是“参数校验失败的报错”。模型生成的参数有时候会不符合 JSON Schema——比如该传整数却传了字符串。处理这类问题的工具化方案是双保险网关做一层预校验把不合法参数拦截下来再添加一个 repair 机制将错误信息连同原参数回传给模型让模型自己纠正输出。实测下来修复成功率很高比直接报错强太多。下面是可供参考的实现def validate_args(schema: dict, args: dict) - list[str]: # 此处用 jsonschema 库做严格校验 errors [] validator Draft202012Validator(schema) for err in validator.iter_errors(args): errors.append(f{err.json_path}: {err.message}) return errors第三类是流式上下文被截断的问题。某些模型在流式返回工具调用过程中会中途插话或吐出与工具调用无关的文本。这时你要在适配器里把工具调用数据与模型的自然语言回复分开缓存不能混在一起交付给路由层。我自己曾因为这个问题排查整晚最后发现原因荒谬某供应商的流式输出中文本与 tool_call 是无序的必须做完整的重排。如果你对接的供应商比较多这类“各家有各家的毛病”的场景会不断出现写适配器时不要假设所有格式都规范。4.3 资源隔离与性能问题并发、超时、查找优化网关跑起来后性能与隔离的问题是我后续才注意到的以下三件事比预想的更重要。一是并发度限制。MCP stdio server 本质上是本地子进程它的并发能力很有限。你在网关里如果同时喂给单个 stdio server 几十个请求它会彻底卡死或者出错。我建议给每个 server 配置独立的并发池比如 max_concurrent_tasks 4超出部分的请求走排队策略而不是无限并发。对于 HTTP 型 SSE server并发上限可以放宽到 10 到 20实测这个配置可以让集群的吞吐发挥到较优水平。二是超时参数必须要分层。最早我统一设了一个 60 秒超时结果有的工具调用量大、耗时天然就长而有些模型供应商的流式响应原本只需 2 秒偶尔的慢请求把整个链路拖到用户不可接受。后来我拆成三层模型供应商请求超时 30 秒MCP server 单次调用超时 45 秒整个 Agent 任务的最长执行时间 120 秒。这三层超时用配置变量独立管理比单一大超时要准确得多。三是工具数量变大之后的查找链路优化。开始只有十几个工具时线性遍历完全没问题当工具规模上升到几百个甚至上千个时每次模型请求都把所有工具描述塞给模型是不现实的——上下文长度不够、token 成本也太高。我当时设计并测试后的方案是给工具描述做“索引标签”比如按领域打上“meeting”“email”“order”等标签在模型调用之前先做一次粗粒度的标签过滤只把命中的工具子集——通常不超过 30 个——放进模型请求里这样既快又省 token。实测下来工具调用准确率反而比全量塞进去更好因为模型在大量无关工具干扰下容易选错。4.4 避坑清单我踩过的 7 个坑最后整理一份自己反复踩过的坑清单希望能帮你少走点弯路。第一定义工具 Schema 时一定要写“何时用、何时不用”的边界而不是只写一句话。第二stdio 模式的 MCP server 千万不要在代码里往 stdout 打印业务日志一旦混入协议数据整个调用链路直接废了。第三网关里的工具注册表一定要有版本和来源字段不然等工具来自多个 MCP server 的时候你根本没法回溯是谁定义的。第四统一网关里不要直接透传底层异常细节给上游否则用户会看到一堆内部错误堆栈安全性和体验都差包装成一个标准错误码结构更合适。第五所有连接到网关的 Agent 要有独立的 API Key不然无法做审计和权限隔离。第六技能包的版本更新一定要走完整的注册与审核流程不要直接在线上覆盖目录否则会出现 SKILL.md 更新了但配套脚本文件还是旧的这种诡异状态。第七监控不能只看调用成功率和延迟还要专门看“工具调用重试率”和“工具误选率”——这两个指标才是网关质量的核心直接反映了你工具定义和描述写得好不好。我自己的体会是做一个 Agent 中台最难的从来不是写代码而是把各种不同形态的能力收敛到一致的抽象层上。tsm-hub 这个项目的一次次迭代让我确认了一件事——统一网关的价值 80% 来自“路由规划与 Schema 治理”的设计只有 20% 来自 API 代理本身的代码实现。如果你正苦恼于模型、工具、MCP 与技能各管各的、改一处牵连一片试试先把它们收进同一个注册表里。这比任何单点优化都值得你花时间。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表