1. 让 LLM、Tools、MCP、Skills 们停下来握手:tsm-hub 的出发点
1.1 模型、工具、协议、技能各玩各的,项目熵增到失控
我接手的 AI 应用项目,从第一天起就注定要同时面对四类组件:LLM 要对接多家供应商,Tools 要以函数调用的形式暴露业务能力,MCP 客户端要挂载各种服务端,Skills 又要提炼团队里的高阶玩法。每一样单独拎出来都有成熟的方案,可一旦放进同一个项目里,它们就开始互相打架。
最典型的场景是这样的:产品经理要一个"代码审查助手",背后至少要串联一个代码补全模型、一个仓库文件搜索工具、一个合规扫描的 MCP Server,还有一套沉淀下来的评审规则——这套规则本质上就是 Skills。传统做法是每个模块各起一个微服务,LLM 走 A 端口,Tools 走 B 接口,MCP 走 C 协议,Skills 干脆躺在 GitHub 仓库里靠人肉拷贝。结果就是:调用链越拉越长,配置散落各处,任何一个组件升级都要连带排查另外三个。项目熵增的速度,远远超过了我们写代码的速度。
这种"各玩各的"状态持续了大概一个季度,直到有一次线上事故让我下了决心。当时一个技能调用了三个工具,其中某个工具因为模型返回的 JSON 参数格式变化导致超时,而超时后又没有自动走另一个模型的兜底,整个会话卡死了。我排查了半天才发现,模型路由、工具调用、技能调度是三套独立的逻辑,各自都不知道对方的存在。从那一刻起我意识到,我需要一个统一的网关层,把所有 AI 相关组件收编进去,让它们在一个入口、一套规则、一张监控面板下协同工作。这就是 tsm-hub 的出发点。
1.2 tsm-hub 解决的四个具体问题
在动手之前,我先把自己逼到墙角问了一个问题:到底什么样的网关才值得自己写,而不是直接上现成方案?市面上模型网关不少,有的能做 LLM 路由,有的能接 MCP,有的能管理 Agent,但几乎没有一个能同时覆盖 LLM、Tools、MCP、Skills 四类对象,并且让它们互相感知、配合调度。我需要的是一个"AI 组件操作系统"级别的中间层,而不是又一个独立的代理服务。
具体拆解下来,tsm-hub 至少要解决四个层面的问题:
- 统一入口:客户端无论发什么请求,都只面对一个 API 地址,由网关决定调哪个模型、加载哪些工具、挂哪个 MCP Server、套哪套技能模板。对外是一张干净的嘴,对内是一张完整的调度图。
- 统一注册中心:模型供应商、工具函数、MCP Server、Skill 定义,全部有结构化的注册表。想加一个新模型,不用改业务代码,注册表加一条,再配好密钥就能上线。
- 统一路由策略:路由不能只看模型名称,要看请求特征——是一轮对话还是长任务?需要低延迟还是高精度?允不允许调用外部工具?这些信息决定了我把请求交给谁,路由规则全部下放到网关层。
- 统一可观测性:模型调用的 token 消耗、工具的耗时与失败率、MCP 连接的稳定性、Skill 的命中率,一切都要能在一张监控面板上拉出来对比。没有数据,所谓优化就是拍脑袋。
这四个问题,表面上是技术问题,本质上是平台化管理思维的问题。项目早期可以用脚本把模块串起来,可走到多模型、多工具、多人协作的阶段,没有统一网关,代码里就会长满"if 用 OpenAI else 用本地模型"这样的屎山。tsm-hub 的存在,就是让组件之间只剩下一种交互方式——全部通过网关。
2. 统一网关的分层设计:入口、路由、协议层、注册中心
2.1 四大组件的边界划分
设计统一网关第一步,不是写代码,而是划边界。这四类组件在 tsm-hub 里各有各的位置,约束清楚之后,后面的调度才能不打架。
LLM 在架构里是最底层的能力提供者,它的职责只有一件事:把提示词变成输出。它不该去关心工具怎么调用,也不该负责技能的编排。在 tsm-hub 里,LLM 被抽象成一个标准的"模型适配器",每个供应商对应一个适配器实现,统一暴露generate(prompt, context, options)这样的接口。适配器内部处理供应商的 API 差异、鉴权、超时、重试,但对外部完全屏蔽。
Tools 是业务能力的原子单元,越简单越好。一个查询天气的工具、一个读写文件的工具、一个调用内部 API 的工具,每个工具只做一件事。但在网关层面,工具必须被标准化成统一的结构:工具名称、描述、参数 JSON Schema、执行方式、超时时间、权限标签。有了这个标准,模型才能通过 function calling 准确地选择工具,网关才能对工具的调用做统一的日志和限流。
MCP 是一个边界比较微妙的东西。MCP 本质上是一种协议,它把工具"暴露"和"调用"的格式标准化了。所以在 tsm-hub 里,我把它定位成一个"协议适配层"——MCP Server 提供的工具,最终会被映射成网关内的标准工具结构,也就是说,在 LLM 的视角里,MCP 工具和本地 Tools 没有任何区别。这样一来,模型的 function calling 逻辑可以完全统一,不用为 MCP 单独写一套调用分支。
Skills 则处在这一整条链的最上层。一个 Skill 是一段可复用的"经验包",通常由指令模板、关联的工具列表、默认参数、以及后处理逻辑组成。例如"代码审查技能"这个 Skill,它内部会声明:使用仓库搜索工具读取代码、使用静态扫描 MCP 做检查、用 GPT-4o 生成评审意见、最后按团队模板格式输出。网关调度 Skill 时,会把它展开成一系列对 LLM 和 Tools 的普通调用操作,Skill 本身不产生新的调用类型。
这四层的关系可以简单理解为:Skills 指挥 LLM,LLM 通过 Tools 和 MCP 工具执行动作,网关负责把这一切串起来并发号施令。每一层的边界只要清晰,出问题的时候定位起来就特别快。
2.2 请求流转链路
说清楚边界之后,请求流转的链路就自然清晰了。我在 tsm-hub 里把一条典型请求拆成了七个阶段,这个拆解值得每个想设计网关的人参考:
- 接入识别:请求进来,网关先认身份。是内部服务调用,还是外部 Agent?API Key 对应哪一级权限?
- 意图分类:根据请求的路由参数判断这一次要走哪条链路——普通对话、工具调用任务、还是需要完整执行某个 Skill。
- Skill 匹配:如果命中了某个 Skill,网关把 Skill 模板加载进来,注入会话上下文。
- 路由决策:根据模型策略、成本预算、当前可用状态,选定具体模型供应商。
- 上下文拼装:把系统提示词、Skill 指令、历史对话、工具定义全部组装成一次模型请求。
- 执行与循环:模型可能返回 function call,网关就去执行对应的工具,把结果回填,再继续调模型,循环直到模型给出最终回答或达到最大轮次。
- 记载与观测:每一轮调用的 token 数、耗时、工具结果、失败原因,全部写入日志存储。
这个链路里最有技术含量的是第 6 步的执行与循环。因为这不是简单的一次"请求-响应",而是一个 Agent 式的循环。网关要自己去判断"模型是不是想要调用工具",然后跨层去执行工具、把结果装回上下文、再发起下一次模型调用。中间涉及的超时控制、上下文裁剪、轮次上限,都必须做得非常保守,否则一次任务消耗的 token 会超出预期好几倍。
3. LLM 接入层:多模型路由、重试与成本控制的实现思路
3.1 模型注册与供应商适配
LLM 接入层是 tsm-hub 里最基础也最不能出错的部分。任何模型要接入,第一步都是在注册表里声明自己。我这里放一份真实项目里的注册配置作参考:
models: - id: gpt-4o-mini provider: openai model_name: gpt-4o-mini api_key_env: OPENAI_API_KEY default_params: temperature: 0.7 max_tokens: 2048 capabilities: function_calling: true vision: false context_window: 128000 cost_per_1k_tokens: input: 0.00015 output: 0.0006 - id: deepseek-chat provider: openai-compatible base_url: https://api.deepseek.com model_name: deepseek-chat api_key_env: DEEPSEEK_API_KEY default_params: temperature: 0.3 capabilities: function_calling: true vision: false cost_per_1k_tokens: input: 0.000014 output: 0.000028注意到第二个模型 provider 我写成了openai-compatible,因为国内大多数模型厂商的 API 都在模仿 OpenAI 的格式。tsm-hub 里内置了这样的兼容适配器,可以大幅减少对接成本。但兼容不等于完全一致,实际测下来,不同厂商对 function calling 的返回值格式仍有细微差异,适配器里必须要做归一化处理,把各家参数统一成内部结构,否则任何一处差异都会在下游链路埋雷。
注册时的cost_per_1k_tokens字段很重要,很多网关会忽略它。但如果你想让路由策略做成本感知,就必须把成本数据放进来。后面谈到的"低成本优先"路由,靠的就是这个字段算出来的。
3.2 基于权重和优先级的动态路由
路由策略是统一网关最核心的价值。我一般不推荐简单的随机或轮询路由,那种方式完全不会考虑请求的实际特点。tsm-hub 里我实现了三种策略,实际用下来覆盖面很广:
优先级路由:给每个模型配一个优先级数字,数字小的优先。只有高优先级模型不可用时,才 fallback 到下一个。这种策略适合生产环境——你希望尽量用强模型,但如果强模型限流或者故障,自动降级到弱模型,保证服务不中断。
成本优先路由:网关根据注册表里的成本数据,结合请求的预估输入长度,动态计算哪个模型最便宜。适合对成本敏感但不要求绝对质量的批量任务,比如日志分类、标题生成这类场景。
语义路由:其实我试过基于请求分类模型做语义路由,但性能和开销都偏高,最后放弃了对小项目来说性价比太低。如果你确实有这类需求,注意要控制分类模型的调用频次,绝对不能在路由这个环节就消耗掉大量 token。
路由策略可以做成可设计的组合规则。比如"日常请求优先 gpt-4o-mini,遇到复杂推理请求自动切 deepseek-chat,如果 5 秒内没有返回则 fallback 到本地 llama"。这些规则在 tsm-hub 里都是 YAML 声明式的,改配置不需要重新部署。
实际运营中我发现,fallback 触发条件和重试策略必须分开配置。重试是"同一个模型再试一次",fallback 是"换一个模型试一次"。很多网关把这俩混在一起,结果模型明明已经崩了,还在同样的模型上反复重试几十秒,用户早就跑了。tsm-hub 里我对重试默认限制最多 2 次,fallback 最多 3 级,且每级 fallback 之前都会记录一次事件日志,方便事后回溯。
3.3 可观测性:调用链与延迟数据
这部分我要着重讲一下,因为太多网关做出来之后,调用很顺,可一出了问题就抓瞎,原因就是观测做得不够。tsm-hub 里我坚持每个请求都必须生成一条完整的调用链记录,包含六个核心字段:
request_id:全链路唯一 ID,所有日志都带上它。route_path:实际走了哪个模型,经历了哪几次重试和 fallback,按顺序记录下来。token_usage:输入、输出、卡在哪个环节。tool_calls:模型请求了几次工具,每个工具的入参、出参、耗时。skill_id:命中了哪个技能模板,模板的版本号也记录下来。error_details:失败的类型码、供应商返回的原始错误信息。
有了这个结构,你去查"为什么昨天下午某些请求响应特别慢",就能直接按时间窗口拉出所有请求的route_path,一眼看到是不是某个模型供应商在那个时段限流了。这种排障速度,是以前靠查日志关键字完全比不了的。
还有一个容易被忽略的点:token 用量统计必须按路由结果聚合。也就是说,不仅要看总消耗,还要看每个模型、每个技能分别消耗了多少。不这么统计,成本优化就无从谈起——你根本不知道钱花在了哪个模型哪类任务上。我每周都会拉一张表,按模型、按 Skill 汇总 token 消耗和延迟,这是后续做模型选型和预算控制的决策依据。
4. Tools 与 MCP 的桥接:工具从"硬编码"到"即插即用"
4.1 统一工具注册表
LLM 接入层解决的是"模型说话"的问题,而 Tools 层解决的是"模型动手"的问题。我见过太多项目把工具调用直接写在业务代码里,每个工具一套调用方式,参数校验写在各自的函数里,连个统一入口都没有。这样做一两个工具没问题,但一旦工具数量超过十个,维护成本就开始指数增长。
tsm-hub 的做法是维护一张统一工具注册表,每个工具都按同一套标准登记入库:
# 工具注册的标准结构(简化示例) tool_definition = { "name": "search_repo", "description": "在指定代码仓库中搜索关键词,返回匹配的文件和行号", "parameters": { "type": "object", "properties": { "keyword": {"type": "string"}, "repo": {"type": "string", "enum": ["core", "web", "mobile"]}, "limit": {"type": "integer", "default": 10} }, "required": ["keyword"] }, "execution": { "type": "python_callable", "target": "tool_impl.search.search_repo", "timeout_seconds": 15, "auth": {"required_permission": "repo:read"} } }这里的关键在于parameters必须是严格的 JSON Schema,因为这条定义不光是给人看的,它会被直接塞进模型的 system prompt,作为 function calling 的参数约束。Schema 写得不严格,模型返回的参数就乱七八糟,工具执行必炸。我实践下来的经验是:参数类型能窄就窄,能用 enum 就不要用自由字符串,description写得越具体,模型选错工具的概率就越低。
所有工具执行之前会先经过网关的统一校验层:参数结构校验、权限校验、额度校验。校验通过才真正进入执行函数。这样任何一个工具都很难被绕过权限滥用,模型的误调用也会被拦截在成本产生之前。
4.2 MCP 客户端的接入
MCP 的接入方式可以简单理解成"把外部工具服务包成标准插件"。一个 MCP Server 通常以子进程或远程 HTTP 服务的形式存在,tsm-hub 内置了 MCP 客户端,负责维护连接、发送请求、解析结果。
拿最常用的 filesystem MCP Server 举例,只需要在配置里声明一个条目:
mcp_servers: - name: filesystem transport: stdio command: npx args: - "-y" - "@modelcontextprotocol/server-filesystem" workspaces: - /home/user/projects allowed_dirs: - /home/user/projects cache_ttl: 60启动后,网关会主动向 MCP Server 发起tools/list请求,拿到它暴露的所有工具,然后批量映射成内部的统一工具结构,自动完成注册。不用写一行代码,LLM 立刻就能调用这些外部工具。这正是 MCP 最有价值的地方——工具的能力边界,由外部服务决定,网关这边只需要做好桥接。
但我必须提醒:MCP 工具的自动注册虽然方便,也意味着所有暴露出来的工具都要过一个安全审核。默认配置里,我将 MCP 工具全部标记为auto_registered: true,并且要求人工确认后才可对 LLM 可见。否则,万一某个 MCP Server 意外暴露了删除类工具,而模型又被诱导调用了它,后果可能会非常严重。
4.3 安全边界控制
MCP 桥接之后,工具数量通常会一下子膨胀很多倍。本地工具加 MCP 工具,轻松突破几十个。这么多工具一股脑全部塞给模型,肯定不行。t-sm-hub 的做法是按会话动态裁剪工具列表。
具体逻辑是:每次请求进来时,根据意图分类结果,只加载与当前任务相关的工具。比如代码审查任务,只加载仓库搜索、静态扫描、文件读取这几个工具;日常问答任务,只加载一两个基础工具就够了。工具裁剪不仅大幅减少了模型处理 function calling 时 token 的消耗,还明显降低了模型选错工具的概率。实测中,工具数从 40 个裁剪到 8 个后,function call 准确率从 91% 提升到了 98%。
安全这块我还做了几个兜底:
- 敏感工具白名单:删除、写库、发送消息等危险操作,必须在配置里显式加白才能被模型调用,而且执行后强制要求人工复核。
- 调用频率限制:同一工具在同一会话中设置了最大调用次数,防模型陷入死循环。
- 超时熔断:单个工具超过设置的超时时间,直接熔断,并把错误信息回传给模型让它调整策略,而不是无限等下去。
5. Skills 技能库:将提示词与工具调用序列沉淀为资产
5.1 Skill 的组成结构
如果说 LLM 是大脑、Tools 是手、MCP 是外接的传感器,那 Skills 就是"已经训练好的肌肉记忆"。一个 Skill 把一段复杂的操作流程封装成一行调用触发,这是团队经验沉淀的最高效形式。
我在 tsm-hub 里把 Skill 定义成一个结构化的包,一个完整的 Skill 通常包含五个部分:
- 元信息:名称、版本号、作者、描述、适用场景。版本号是硬性要求,没有版本控制的技能库,改来改去迟早要出事故。
- 指令模板:插入到系统提示词中的核心指令文本。这里要写得很有讲究,既要任务导向清晰,还不能太死板,要给模型留出合理的判断空间。
- 工具依赖:该 Skill 需要调用哪些工具,有严格的名称列表,并且允许声明"必须""可选"两级依赖。
- 参数入口:Skill 对外暴露的参数定义,调用方只需传入核心参数,其余由 Skill 内部消化。参数定义也用 JSON Schema,保证调用体验一致。
- 后处理钩子:模型输出完成后,允许插入一段自定义代码做后处理,比如给回答追加引用来源、格式化输出、触发另一个流程等。
我在项目里做的第一个成规模 Skill 叫"新需求技术评估",输入是一段需求描述,输出是一份结构化的技术方案草稿。这个 Skill 内部声明了三个可选工具:代码库搜索、依赖库查询、相似项目检索。实际跑起来之后,团队不再需要每次从头开始分析需求,只要把需求丢进来就能拿到初稿,后续人工修一修就能用。节省的时间是非常直观的。
5.2 网关如何调度 Skills
Skill 在网关里的调度,本质上是一次"模板展开"。当请求命中某个 Skill,网关并不是把它当作一个整体去调模型,而是把它展开成一串标准的组件调用序列。我习惯把这个调度过程想象成"解读菜谱"——Skill 就是菜谱,网关是厨师,菜谱里写着要放盐、放糖、大火、小火,厨师按照步骤一步步执行,最后做出一道完整的菜。
一个简化版的 Skill 调度配置如下:
skills: - name: code_review version: "2.1" description: 对指定代码变更进行系统性评审 parameters: type: object properties: diff_uri: { type: string } language: { type: string, default: "python" } steps: - action: load_context uri: "{parameters.diff_uri}" - action: call_tool tool: search_repo params: keyword: "TODO|FIXME" - action: call_tool tool: static_scan params: target: "{steps.load_context.result.path}" - action: llm_generate model_hint: "strong" # 由网关解析为具体模型策略 prompt_template: | 请基于以下代码变更和扫描结果,输出评审意见。 变更内容:{steps.load_context.result} 扫描发现:{steps.call_tool.result} {{ skill_instructions.code_review }}调度器按steps顺序执行,上一个步骤的输出可以作为下一个步骤的输入。model_hint: "strong"是一个路由提示,网关会把它映射到当前可用的最强模型上,同时也可以被成本控制策略改写成次强模型——Skill 定义里不写死具体模型,而写模型档位,这是我踩过坑之后总结出来的经验:一旦在某次测试里觉得某个模型效果好就写死到 Skill 里,后续模型升级、供应商调整时,这个 Skill 就会被绑架。
Skill 的执行结果同样会写进可观测日志里,方便我在后续做效果复盘。我一般每月拉一次 Skill 使用报告,看哪个 Skill 的调用成功率低、哪个 Skill 的平均延迟高、哪个 Skill 消耗 token 最多。一次调优可以让团队节省大量重复劳动时间。
6. 实际跑通 tsm-hub:一份可复现的配置与踩坑记录
6.1 最小化部署配置
讲完设计思路,该聊落地了。我先把一份最小化部署配置放在最前面,方便你直接照着上手:
# tsm-hub 最小化配置 server: port: 8080 api_prefix: /v1 api_keys: - key: ${ADMIN_API_KEY} models: - id: gpt-4o-mini provider: openai model_name: gpt-4o-mini api_key_env: OPENAI_API_KEY capabilities: { function_calling: true } - id: llama-3.1-8b provider: local base_url: http://localhost:11434/v1 model_name: llama3.1:8b capabilities: { function_calling: true } tools: - name: http_get description: 发起 HTTP GET 请求获取 URL 内容 parameters: type: object properties: url: { type: string } required: [url] - name: python_exec_sandbox description: 在受控沙箱中执行 Python 代码片段 parameters: type: object properties: code: { type: string } mcp_servers: - name: fetch transport: stdio command: npx args: ["-y", "@modelcontextprotocol/server-fetch"] cache_ttl: 30 skills_trunk: - name: web_search_followup version: "1.0" steps: - action: call_tool tool: http_get params: { url: "{parameters.target_url}" } - action: llm_generate model_hint: "fast" prompt_template: | 总结以下网页内容,提取关键信息:{steps.call_tool.result}这份配置跑起来之后,我可以同时做三类基础验证:直接对话调模型、让模型通过 function calling 调用 http_get 工具、挂载 MCP fetch 服务后让模型从指定 URL 拉取内容再总结。这一套通了,tsm-hub 的核心链路就都验证完了,后面再加新组件都只是往注册表里塞记录的事。
6.2 踩过的坑
我不想让这篇分享看起来一路顺风顺水,实际上这个项目踩过的坑一点不少,挑三个最有代表性的写出来:
第一个坑:MCP Server 的子进程崩溃拉垮整个网关。stdio 模式的 MCP Server 作为子进程常驻,某个 Server 代码里一个异常就能导致整个进程退出。第一次部署时,一个不稳定 MCP Server 直接拖垮了网关进程,所有请求全部 502。修复方案是给每个 MCP Server 独立进程托管,并且加上崩溃自动拉起和健康检查,发生故障时只影响自己,不影响主链路。这个隔离思路,所有想接多 MCP 的人都要提前考虑。
第二个坑:Skill 展开后的上下文爆炸。我第一次把 Skill 配了十几个步骤时,每个步骤都把前一步完整输出塞进上下文,结果跑到第五步,上下文已经占了几万个 token,模型都快"忘"了最初的指令。后来我做了上下文瘦身工具,步骤之间只传"摘要 + 关键字段",并把历史中间结果存到外部缓存,需要时再按 ID 取回。Skill 步骤越多,上下文管理越要精心设计,这不是可选项。
第三个坑:模型对工具描述里的"长尾文字"过度敏感。我给一个工具 description 写了一大段背景说明,结果模型频繁在无关问题中去调用这个工具,因为里面某些词触发了它的联想。后来我把 description 压缩成"做什么 + 什么时候用 + 什么时候不要用"三句话格式,误调用率才降下来。工具描述不是文档,是给模型看的路标,越精炼越有效。
每次掉进坑里,我都会顺手把处理方案补回 tsm-hub 的默认配置模板里。一个网关是"修"出来的,不是"写"出来就完事了。把这些容易踩的坑收敛成默认的护栏,后来者就不需要重新经历一遍我踩过的那些糟糕体验。
7. 我最后提醒自己的三件事
项目从设计到跑通,前后横跨了两个多月,最后我的体会有三件事值得反复提醒自己和团队。
第一件事,统一网关的价值不是"省代码",而是"省决策"。它让团队不用再思考"这个请求应该走哪条路",而是把决策权集中到规则配置里。配置可以讨论、可以评审、可以灰度,业务代码里不再散落着各种 if else 的分流逻辑。这比少写几个函数重要得多。
第二件事,组件之间最好用协议沟通,而不是用代码沟通。MCP 之所以值得集成进去,就是因为它把工具的暴露和调用方式标准化了,让工具能力的边界可以动态伸缩。tsm-hub 里我尽量让所有交互都走标准结构,哪怕多写几行适配代码,也为未来的扩展留了余地。
第三件事,过段时间回头重构一下配置里的默认值。模型会出新版、工具会调整、Skill 会过时,一套配置用上半年基本就不太合适了。我每个季度会做一次全量 review,把成本、延迟、成功率这些指标翻出来,重新调整路由策略和 Skill 模板。统一网关给了我们"只改配置就能整体优化"的底气,这是最大的杠杆。
如果你也在同时对接多个模型、维护一堆工具函数、尝试用 MCP 扩展能力、考虑沉淀团队技能库,那 tsm-hub 这套思路大概率对你有参考价值。别急着上来就写代码,先把你手上已有的组件列个清单,想一想如果它们都要通过一个网关互相配合,你会怎么划分边界——这个想清楚了,后面都是水磨工夫。