tsm-hub实战:用统一网关整合LLM、Tools、MCP与Skills
发布时间:2026/9/30 5:22:23来源:尧图网络
我最早想搭这个网关纯粹是被逼的。手底下同时维护着三四个 Agent 应用每个都得接一遍大模型接口每个都要配一堆工具Claude 的 Skills 放一套在这边MCP Server 的鉴权 token 又散落在另一台机器的环境变量里。时间一长连我自己都搞不清楚某个工具到底是在哪个项目里被注册的更不用说前端同事要调一个技能还得来问我“这个函数入参是什么”。痛到一定程度自然就会想能不能把 LLM、Tools、MCP、Skills 全都收进一个统一网关让上层应用只跟这一个口子打交道。于是就有了 tsm-hub 这个项目。它不是又一个 Agent 框架而是一层很薄的收纳与转发层——上面接各种应用和脚本下面挂模型、工具、MCP Server 和技能包。这篇文章就把整个项目的设计思路、核心实现、踩过的坑和排查经验完整写出来适合正在做 Agent 应用、或者被工具链碎片化折磨的团队参考。哪怕你只有一两个应用这套收敛思路也值得看一看。1. 为什么要做 tsm-hub先想清楚它到底解决什么问题很多人一听“网关”两个字第一反应是又要引入一个重东西。实际上恰恰相反tsm-hub 的核心价值不在“多”而在“少”——让上层应用少配参数、少写胶水代码、少维护一堆分散的 token 和地址。1.1 碎片化才是 Agent 应用最大的隐性成本先看看没有网关的时候一个典型的 Agent 项目要面对什么接入 OpenAI 或国产模型要走一套 SDK接入 Anthropic 又要另一套格式写一个天气查询工具要在代码里自己定义 function calling 的 schema要用浏览器自动化得去翻 Playwright MCP 的文档单独开一个子进程自己管理它的生命周期想把一段写得不错的提示词沉淀成可复用技能又得考虑怎么装进 Claude Code、怎么在别的应用里引用。这些东西单看都不难但组合起来每次新起一个项目都是同样的重复劳动而且每个项目对这些东西的处理方式还不一样时间久了就成技术债。tsm-hub 换个思路应用只管发一个统一的请求说自己要“做某件事”网关负责找出合适的模型、合适的工具、合适的技能然后编排执行。这个过程对上层是透明的调用方不需要关心背后到底走的是哪个 MCP Server也不需要知道某个技能存在哪个目录下。1.2 网关的定位不是框架是收纳层我自己定的原则是tsm-hub 不做 Agent 决策不写业务逻辑它是纯粹的“管线基础设施”。决策这件事应该留在应用层比如你写一个客服机器人判断用户意图、决定调用哪个工具那是你业务的事网关只负责把你决定要用的东西快速、稳定、安全地找到并执行。这样一来网关的职责边界非常清晰模型路由、工具注册与调度、MCP 连接管理、技能装载与版本管理、统一的鉴权与审计。这个定位要守住挺难的。写着写着就容易膨胀想把状态机、记忆、工作流全塞进去。我的经验是一旦发现某个功能需要在网关里做“业务判断”立刻停下来把它丢回给上层。保持这一条tsm-hub 的代码量才能一直控制在可维护的范围里。1.3 哪些东西不该进网关有了边界之后还要明确哪些东西不要收进来。不要收业务数据。用户聊天记录、订单数据、文档内容这些属于业务系统网关不该碰更不能落盘。不要收大文件。工具返回的超大响应比如几 MB 的图片 Base64网关只负责转发元信息具体内容让应用直接去对象存储取。不要收各个业务方自定义的复杂状态。网关里的会话状态只保留工具调用上下文、token 用量、错误信息这类运行数据业务自己的状态自己管。这三个“不收”能避免网关变成一个数据黑洞。有同事曾经提议把知识库的向量检索结果也缓存到网关里被我否了原因很简单一旦缓存了出问题的时候谁都说不清数据新不新这不该是网关的职责。2. 整体架构与核心设计思路tsm-hub 的整体结构可以拆成三层接入层、协议层、执行层。每一层只做一件单一的事层与层之间通过标准化的数据模型通信。2.1 三层结构接入层、协议层、执行层接入层面向调用方提供一个统一的 HTTP/WebSocket 入口。我选的是 FastAPI单纯因为异步支持好、内建 OpenAPI 文档方便前端对接。所有请求进来的时候带着一个全局唯一的请求 ID这一层只做身份识别、参数校验和转发不做任何业务处理。协议层是网关最核心的部分。它负责把外部各种各样的协议翻译成内部统一格式。比如 OpenAI 的 chat/completions 接口、Model Context Protocol 的 tools/call以及 Anthropic 的 Messages API进来之后全部转成一套内部定义的“工具调用事件”结构。这样执行层永远只看到一种形态写起来非常顺。执行层负责真正干活。它维护一个工具注册表registry每个工具进来的时候都会被打平成统一的 JSON Schema 描述需要跑在独立进程里的 MCP Server 由执行层管理生命周期Skills 技能包则被编译成带前缀的系统提示词和可调用脚本塞进模型上下文里。这一层最需要关注的不是功能而是稳定性一个不返回结果的工具调用、一个超时的 MCP Server都不能拖垮整个请求链路。2.2 统一数据模型把一切描述为标准 JSON Schema这是整个项目地基中的地基。工具也好、MCP 工具也好、Skills 里带的外部脚本也好在网关里全部归一为同一种描述结构{ name: search_web, description: 通过搜索接口查询互联网信息返回前 N 条结果, parameters: { type: object, properties: { query: { type: string, description: 搜索关键词 }, top_k: { type: integer, default: 5, minimum: 1, maximum: 10 } }, required: [query] } }有了这个统一模型后面的路由、校验、执行就都能用同一套逻辑处理。你注册一个本地 Python 函数是这套结构配置一个远程 MCP Server 的搜索工具也是这套结构模型在做 function calling 时候拿到的参数描述还是这套结构。三个环节全对上了。这也是我踩过坑之后定下来的一开始每个来源的 tools 都用自己的字段命名一个 snake_case 一个 camelCase模型经常把参数填错位置后来全部转成统一 schema问题立刻少了八成。2.3 为什么选异步流式 独立执行进程技术上两个关键决定必须说明白。第一个是流式。网关面向的是 Agent 场景模型输出的实时体验和工具调用的中间过程都需要及时反馈给用户。如果网关同步转发一个可能要执行 30 秒的 Python 工具前端就是一片空白。所以 tsm-hub 的接入层统一走 SSEServer-Sent Events后端每完成一个步骤就往前端推一条事件比如“正在调用搜索工具…”“已获取 5 条结果正在生成总结…”。这种体验上的差距用户感受非常明显。第二个是执行进程隔离。凡是外部 MCP Server我一律建议用子进程方式拉起不要跟网关跑在同一个进程里。原因是很多 MCP 的 SDK 存在资源占用不可控的问题比如 Playwright MCP 会启动浏览器万一浏览器崩溃不能把网关也带崩。子进程隔离之后即使某个工具死了网关只要检测到心跳断了就自动重启这个子进程对上层的表现只是那一次调用失败了其余请求完全不受影响。2.4 一个典型的网关配置长什么样配置方面我用 YAML因为人对配置的直觉比代码好调。下面这个是最小可用的例子gateway: port: 8400 log_level: info request_timeout_seconds: 120 models: default_provider: anthropic providers: anthropic: base_url: https://api.anthropic.com api_key_env: ANTHROPIC_API_KEY openai_compatible: base_url: https://your-endpoint.example.com/v1 api_key_env: OPENAI_API_KEY mcp_servers: playwright: command: npx args: [-y, playwright/mcplatest] env: PLAYWRIGHT_MCP_ALLOWED_ORIGINS: https://example.com fetch: url: https://api.example-mcp.com/mcp headers_env: FETCH_MCP_TOKEN skills: paths: - ./skills allow_remote: true remote_sources: - https://raw.githubusercontent.com/example/skills/main/manifest.json auth: token_env: TSM_HUB_API_TOKEN配置里每个字段都有讲究比如request_timeout_seconds别设太小MCP 工具第一次冷启动可能要拉依赖你给个 30 秒很容易超时我后来统一放宽到 120 秒问题迎刃而解。3. Tools 与 MCP 的接入实操从注册到调用这一块是日常用得最多的。下面把步骤拆开手把手说清楚怎么把一个本地工具和一个远程 MCP Server 接进来。3.1 本地工具注册三步走本地工具指的是直接跑在网关进程里的 Python 函数适用于轻量、无副作用的操作。注册分三步。第一步在代码里定义一个普通的异步函数然后用装饰器暴露# tools/web_search.py from tsm_hub import register_tool register_tool( namesearch_web, description搜索互联网内容返回标题、链接和摘要, parameters{ type: object, properties: { query: {type: string}, top_k: {type: integer, default: 5} }, required: [query] } ) async def search_web(query: str, top_k: int 5) - dict: # 业务实现... return {results: [...]}第二步在网关启动时指定加载哪个模块tsm-hub start --config config.yaml --load-tools tools.web_search第三步用测试请求验证工具是否被正确识别curl -X POST http://localhost:8400/v1/tools/search_web \ -H Authorization: Bearer $TSM_HUB_API_TOKEN \ -d {query: tsm-hub, top_k: 3}这一步能通说明工具已经进入注册表并且可以被路由调用。这个流程里最容易犯的错是参数 schema 和函数签名不一致比如 schema 里声明了top_k是 integer函数定义却是top_k: str网关的校验器会直接拒绝报 422。所以我后来在装饰器里加了运行时校验启动的时候就对一遍宁可启动失败也不要运行时炸。3.2 MCP Server 动态发现与连接管理MCP Server 的接入比本地工具复杂因为它可能是独立的进程stdio 模式也可能是远程的 HTTP 服务sse/http 模式。tsm-hub 的处理方式是配置驱动 生命周期管理。以 Playwright MCP 为例配置里写好命令和参数之后网关首次请求时会自动拉起这个子进程建立连接然后通过tools/list拿到该 Server 的全部工具列表合并进全局注册表。这个“动态发现”很关键你不用手动把 Playwright 的每一个工具比如browser_navigate、browser_click都写进配置只要指向 Server 本身工具列表自动同步。连接管理上我做了三个状态空转已拉起但没请求、活跃正在处理调用、异常进程退出或心跳超时。没用的 MCP Server 进程默认闲置 15 分钟后自动关停省内存。曾经有个同事问“为什么我用完浏览器后进程还在”其实就是闲置时间还没到这在低成本机器上算是很有用的默认策略。3.3 工具冲突与命名空间动态发现带来的新问题是命名冲突。你本地已经注册了一个叫fetch的工具结果某个 MCP Server 里也有一个fetch同名工具进注册表的时候如果不处理轻则后注册的把前面的覆盖掉重则系统里出现两个 schema 不一样的同名工具模型调用时直接乱套。我的解决方案是命名空间隔离。MCP Server 的所有工具自动加前缀比如playwright__browser_navigate、fetch__get。前端和模型看到的名字都带来源标识一眼就能看出来自哪里。如果你确定某两个工具同名但用途一致也可以在配置里显式指定 mergemcp_servers: fetch: url: https://api.example-mcp.com/mcp tool_prefix: # 不自动加前缀 merge_tools: [fetch]merge 操作有风险只有当你确认两个实现的行为完全等价时再用。一般情况下带前缀的命名空间是最省心的代价只是名字长一点换来的是确定性和可排查性。3.4 鉴权与安全的默认值MCP 工具往往是安全薄弱点。一个 Playwright 浏览器工具意味着任何能调用它的人都可以访问你的内网页面一个execute_command工具等同于远程代码执行。tsm-hub 在安全上做了两个默认强制策略。第一个是默认拒绝。新接入的工具在没有显式授权之前调用时一律返回权限不足不会静默放行。在工具元数据里可以声明required_permissions{ name: execute_command, required_permissions: [shell.execute] }然后在应用调用的请求头里带X-Request-Scope: shell.execute网关校验应用级别的 token 是否绑定了这个 scope没有就拒绝。第二个是敏感信息过滤。MCP Server 返回内容里的密钥、token 等字段如果匹配sk-、ghp_这类模式网关会在返回给模型之前做掩码处理。这个是我吃过亏才加的——有个测试用例里工具把别的东西的 API key 打印到了响应里幸好是内网环境。这个过滤功能现在已经默认开启。4. Skills 技能包的装载与版本治理Skills 这个东西在 Claude Code 里用得已经很熟了但很多人对它的理解停留在“一段提示词”。实际上一个像样的技能包应该是“提示词 脚本 资源文件”的组合体。4.1 Skills 到底是什么一组可复用的能力封装我给你举个直观例子。假设你有一个“代码审查技能”它的内容可以拆成三部分系统提示词负责约束模型以审查者的视角分析一个 Python 脚本负责调用静态检查工具一个requirements.txt负责声明依赖环境。这三者组合在一起才是一个完整的技能。tsm-hub 对技能的装载方式是把它们编译成一种可以在模型上下文中使用的“技能描述条目”。每个技能被映射成两部分内容一段 Markdown 格式的说明描述触发场景和用法一组可调用的辅助命令或脚本。当用户请求触发了某个技能网关会把对应的说明注入系统提示词并允许模型在回答过程中调用该技能的辅助命令。这种方式的好处是模型不需要把整个技能脚本读进上下文只需要知道“有这个技能、它擅长什么、什么时候用它”。4.2 目录结构与 manifest 设计一个标准的 tsm-hub 技能包目录结构如下skills/ code-review/ SKILL.md # 技能说明描述触发条件和用法 scripts/ run_linter.py # 执行静态检查 assets/ rule_custom.yaml # 规则配置 manifest.json # 元数据名称、版本、依赖、作者manifest.json是技能的灵魂它声明了这个技能的入口和依赖{ name: code_review, version: 1.2.0, description: 用静态分析和人工规则审查代码提交, entry_point: scripts/run_linter.py, dependencies: { python: 3.10, npm_packages: [eslint9] }, tags: [code-quality, review], permissions: [repo.read, lint.run] }有了 manifest网关就可以做依赖检查和版本判断。我见过太多人把技能包丢到文件夹里就算“装了”结果换了一台机器连 Python 版本对不对都不知道。manifest 的存在让“装技能”从复制文件变成了一次可控的部署过程。4.3 装载来源与信任策略技能包的来源有三类本地路径、GitHub 仓库、远程 manifest 列表。GitHub 来源最常用基本命令是tsm-hub skills install https://github.com/your-name/code-review-skill内部实现是克隆仓库、校验 manifest 合法性、执行依赖安装、最后把编译结果写进网关技能库。但这里必须提醒从 GitHub 装的技能本质上是无人审查过的代码跟你pip install一个陌生包的风险一模一样。我的建议是设置一个信任清单不在清单里的技能包一律以“未信任”模式运行脚本跑在沙箱容器里不挂载内网目录。这种信任策略不是摆设。我自己就遇到过从第三方仓库装的技能它的辅助脚本会尝试读取环境变量里的密钥并打印出来。如果网关环境是共享的这等于泄露了所有服务的凭据。所以默认不信任陌生技能。想省事可以加一行配置trust_all: true但后果自负——反正现在的我不敢这么干。4.4 热加载与回滚机制技能包更新很频繁不可能每次都重启网关。tsm-hub 的技能库支持热加载当监听目录里的manifest.json文件被修改或者新增时网关会重新编译这个技能并替换旧版本全程不需要重启进程。这个机制来自一次真实的麻烦生产环境的网关跑了一个旧版摘要技能模型输出质量突然下滑排查了半天发现是某个技能包被人本地改坏了。从那以后我在网关里强制要求版本号递增、变更记录必须写清楚否则拒绝热加载。回滚也很简单因为每次装载都会在本地留一个版本快照tsm-hub skills rollback code_review --to 1.1.0这个命令会把技能库恢复到你指定的版本同时生成一条审计记录。对于线上环境来说这个能力比重启服务优雅得多。5. 完整实操链路从初始化到一次带工具的 Agent 调用说了这么多设计现在把整个链路走一遍。下面以一个“帮我查一下 tsm-hub 这个项目的最新动态并总结成一段话”的任务为例展示网关完整的工作流程。5.1 初始化网关与基础配置假设你在一台干净的 Ubuntu 22.04 机器上操作。先安装基础环境并启动pip install tsm-hub tsm-hub init --config-dir ./tsm-hub vim ./tsm-hub/config.yaml初始化会在指定目录生成默认配置文件和一个空的 skills 目录。接下来配置模型提供商。这里我建议优先用一个兼容 OpenAI 协议的自建网关或云厂商端点因为调试起来最顺手。给网关配一个固定 token作为所有调用方的访问凭证export TSM_HUB_API_TOKEN$(openssl rand -hex 32)5.2 接入两个工具一个本地工具 一个 MCP Server继续上面的例子我们先注册一个本地工具get_github_releases用于获取 GitHub 仓库的 releases 信息from tsm_hub import register_tool import httpx register_tool( nameget_github_releases, description获取指定 GitHub 仓库的 releases 列表, parameters{ type: object, properties: { repo: {type: string, description: 仓库名格式 owner/repo} }, required: [repo] } ) async def get_github_releases(repo: str): async with httpx.AsyncClient() as client: r await client.get(fhttps://api.github.com/repos/{repo}/releases) r.raise_for_status() return r.json()[:5]再配置一个 Playwright MCP用于访问网页查看项目详情。配置好之后启动网关观察启动日志[INFO] Registered local tool: get_github_releases [INFO] Connecting MCP server: playwright [INFO] Discovered 18 tools from playwright, prefixed with playwright__ [INFO] Skill loaded: code_review v1.2.0 [INFO] Gateway ready on port 8400看到三行日志分别代表工具、MCP 和技能都就位。这时候执行层的注册表里已经有一批可用能力了。5.3 发起一次完整的 Agent 调用现在上层应用向网关发起请求要求“查一下 tsm-hub 的 GitHub 动态并总结”。应用层把用户意图拆解成几步先搜 GitHub releases再打开网页看项目 README。请求到了网关之后curl -N -X POST http://localhost:8400/v1/chat/completions \ -H Authorization: Bearer $TSM_HUB_API_TOKEN \ -H Content-Type: application/json \ -d { messages: [ {role: user, content: 查一下 tsm-hub 项目的最新 release 动态并打开它的 GitHub 首页看看项目定位最后用三句话总结} ], tools: [get_github_releases, playwright__browser_navigate, playwright__browser_snapshot] }这句请求传给模型后模型会先决定调用get_github_releases。网关执行时返回{ tool_call_id: call_001, tool_name: get_github_releases, tool_input: {repo: your-name/tsm-hub}, result: [ {tag_name: v0.3.0, published_at: 2025-01-10T08:00:00Z, body: - 新增 MCP 热加载... } ] }得到结果后模型又要求调用playwright__browser_navigate打开项目首页。网关判断这个 MCP Server 还在空转状态于是自动拉起 Playwright 子进程等待 readiness 信号后执行导航。这一串过程全部通过 SSE 推送给前端展示成流式的中间步骤。整个链路里的关键观察点在于上层应用完全不需要知道 Playwright MCP 的启动参数、token 从哪个环境变量读、技能文件在哪个目录。这些全被封在网关内部调用方只面向统一接口。5.4 组合技能后的调用效果如果这个任务需要按照团队规范生成总结报告可以在请求中指定启用report_writer技能。网关会把技能说明注入系统提示词并在模型生成总结完之后使用技能自带模板对内容排版。效果上模型对格式的遵守度会高很多因为技能的描述是经过专门调校过的。5.5 性能与资源观察点网关在处理这类请求时耗时主要分布在三个环节模型响应约 1-2 秒、工具执行MCP 冷启动时 2-5 秒热状态下 50-200 毫秒、流式返回过程。如果发现 MCP 冷启动频繁建议把闲置时间从默认的 15 分钟调长到 30 分钟用内存换延迟mcp_servers: playwright: idle_timeout_seconds: 18006. 常见问题与排查技巧实录最后这部分全是实际碰过的坑。我把几个重复率最高的问题整理出来按症状、原因、解决办法的顺序写清楚。6.1 MCP 连接超时但单独启动 Server 又是好的这个太经典了。网关内部用异步 I/O 管理进程如果你的 MCP Server 启动时需要下载依赖并打印大量日志网关可能在 readiness 检查阶段就超时了。解决办法是把启动超时从默认的 10 秒调大同时让 Server 在就绪前输出一个明确的令牌行比如__MCP_READY__网关读到这行才算连接成功。日志太多会撑爆管道缓冲区我建议 MCP Server 端把日志输出重定向到文件而非 stderr。6.2 技能加载成功但模型就是不按技能指示走如果模型拿到了技能描述却无视它十有八九是你的技能说明写得太抽象。比如写“当你需要总结文档时使用此技能”模型会在犹豫不决时干脆不用。更有效的写法是给出具体触发条件“当用户用中文要求生成简报、周报、汇报材料时必须使用 summarize_document 技能。”另一个原因是技能说明被塞到了上下文太靠后的位置权重太低。在网关里把技能说明放在 system prompt 紧邻位置效果会好很多。6.3 工具调用参数校验失败模型返回的参数是 JSON但经常会带进多余的字段或者top_k传了个字符串5。网关内部用一个严格校验器拒绝未知字段。一开始很多人觉得严格校验太死板实际跑起来之后发现这正是要的宁可报错让模型重新生成一次参数也不能让一个手滑写错的参数直接进到工具里执行。6.4 流式输出拼接混乱前端 SSE 收到多条数据后如果把每条当成完整的消息直接渲染会出现句子被打碎、内容跳动的问题。解决办法是在事件里带上类型字段比如event: tool_start、event: content_delta前端根据类型决定是刷新工具状态提示还是追加正文片段。这个约定要在项目一开始就定好不然后面改接口成本很高。6.5 鉴权配置不当导致工具可以被任意调用这是一个安全排查里最需要重视的。网关的 token 默认是放在请求头的但有些应用会把它暴露给前端。我强烈建议网关只接受服务端之间的调用前端先请求你自己的后端再由后端带 token 访问网关。另外定期轮换 token换起来也不复杂改环境变量重启网关就行比换大量散落在各项目里的直连密钥简单得多。下面这张表是几种典型问题的速查现象大概率原因处理方式MCP 工具首次调用特别慢子进程冷启动依赖未缓存调大请求超时或者预热脚本提前拉起工具返回大量数据导致模型上下文超限工具结果未截断在网关里按工具类型设置最大返回长度模型选错同名工具未启用命名空间前缀开启 tool_prefix 并核对注册表名称技能更新后行为异常旧版本未被替换检查版本号执行 rollback 快速回退某个 MCP Server 崩溃拖垮全部请求缺少进程隔离确保外部 MCP 一律走子进程模式我现在对各类问题的第一反应永远是先查审计日志请求 ID、工具名、耗时、错误码、调用者身份这些信息在 tsm-hub 里全部都按结构化方式落盘。遇到问题先翻这个日志比瞎猜配置要高效太多。这也是整个网关项目我最后悔没早点做的事情——如果从一开始就带上完整审计前面至少三分之一的问题根本不用花那么多时间追溯。最后一个想分享的小经验网关不是装完就能一劳永逸的。随着接入的工具越来越多定期的工具清单审查非常有必要把没人用的、重复的、风险高的统统下线或合并。保持收纳层干净Agent 应用才会真正受益。这比追求一次接满所有能力更有意义。
网站建设高端定制企业官网