LLM应用集成太难?统一网关tsm-hub实现模型、工具与协议一站式管理
发布时间:2026/9/28 15:34:20来源:尧图网络
去年我在折腾自己的 AI 应用时反复被同一个问题卡住单看 LLM、Tools、MCP、Skills 任何一个其实都不复杂但一旦想把它们组合进同一个系统立刻变成一场灾难。LLM 的调用要按各家 SDK 的格式来Tools 要符合 function calling 的 JSON SchemaMCP server 默认走自己的传输协议Skills 又经常只是一堆 Markdown 模板——四套体系各说各话每次联调都像翻译修罗场。后来我把它们全部收进 tsm-hub 这个统一网关事情才顺下来。tsm-hub 做的事情很简单把所有模型、工具、MCP 服务、技能包统一注册进来对外暴露一套规整的请求/响应格式内部再按配置去路由、转换、编排。这篇文章就聊聊我为什么坚持做这个网关、四个核心模块怎么拆、一个真实接入流程长什么样以及我踩过的几个坑。适合正在做 LLM 应用集成、想把手上的模型/工具/协议统一管起来的人看如果你还不了解 MCP 和 Skills 是什么也能在里面找到通俗的解释。1. 为什么我会被逼着做这个统一网关1.1 先说清楚四个词在我这里意味着什么LLM 不用多说就是大语言模型。Tools 指的是给模型用的外部能力常见实现是 function calling也就是把函数写成 JSON Schema 描述给模型模型在回答时选择要不要调用。MCP 是 Model Context Protocol 的缩写它把工具、资源、提示词统一成一种客户端-服务端协议理论上任何支持 MCP 的 AI 客户端都可以直接挂载远程或本地的工具服务。Skills 则更像是“打包好的能力包”里面通常有一段指令、一些上下文说明以及一组它要用的工具清单比如“代码审查 Skill”“发版检查 Skill”。这四个概念在领域内其实都有交叉。Tools 是函数层MCP 是协议层Skills 是应用层而 LLM 是执行层。但现在的问题在于很多团队把四者做成四个孤岛。1.2 四分五裂才是真实痛点先说一个我很反感的场景业务代码里直接 new 一个模型 client然后散落着十几个工具函数每个工具函数各自处理鉴权、重试、超时。MCP server 又独立跑在别的地方要单独维护端口和 token。Skills 呢可能就躺在开发者的本地目录里完全没有版本管理。这种“散装”架构会带来几个真实的麻烦。上下文不共享。模型调完一个工具拿到结果下一个工具完全不知道前面发生了什么。Skills 里写的背景信息只存在于提示词模板里没法被工具消费。鉴权散落。LLM 要一套 keyMCP 服务可能要自定义 token内部工具又要走内部 SSO。每次接入新模块都像是在给不同门配不同钥匙。能力无法编排。你很难表达“先调用 MCP 的搜索服务再把结果交给内部 Tools 里的分析函数最后让模型总结”这种流程。更别提做条件分支、熔断和降级了。调用方式不统一。业务方想用一个搜索功能有的人直接 curl MCP server有的人调内部 HTTP API有的人甚至在 prompt 里塞结果。接口风格五花八门出了问题排查成本极高。一个很贴切的类比这就像一家公司销售用自己的 CRM财务用自己的表格人事用自己的文档全公司没有一个统一入口去查询“这个客户现在什么状态”。tsm-hub 想解决的就是这件事把所有能力收口变成一套可以被统一查询、统一调用的体系。2. tsm-hub 的核心设计一切能力都变成同一种请求2.1 网关只做四件事注册、路由、转换、编排我见过很多网关项目一上来就设计复杂的分层架构最后把自己绕进去了。tsm-hub 的核心思路反而很朴素所有能力模块进入网关后只经过四个阶段。注册阶段负责把 LLM 供应商、工具函数、MCP 服务、Skills 包全部登记到能力目录。每个能力拿到一个全局唯一的标识符例如llm:deepseek-chat、mcp:playwright:browser_snapshot、skill:release-check。路由阶段根据请求里的目标标识符找到对应的注册条目并结合当前配置决定具体走哪条通道。转换阶段是网关的“翻译官”把统一的请求格式转换成下游模块自己的格式比如把网关的 JSON 请求转成 MCP 协议的 JSON-RPC。编排阶段则负责处理多步骤的调用链一个请求可以触发多个能力顺序或并行按配置走。这四个阶段顺序固定但每个阶段内部是可插拔的。这样设计的好处是新增一个能力类型时不需要改动其他阶段只需要给转换层加一个适配器即可。2.2 统一出入参核心协议长什么样统一网关最大的价值是让业务方只需要记住一种请求格式。我设计了一条极简协议核心就两个字段你要调什么你要传什么。{ target: mcp:playwright:browser_snapshot, params: { url: https://example.com, full_page: true }, context: { session_id: chat_123456, user_id: u_8899 } }响应格式同样统一{ status: ok, output: { type: mcp_resource, content: [ { type: text, text: 页面截图已保存 } ] }, usage: { tokens: 1280, duration_ms: 340, tool_call_id: call_0001 } }为什么要做成这样因为下游模块几乎没有一个是这种格式。LLM 流式返回是 SSETools 返回的是函数结果MCP 返回的是 JSON-RPC 包装后的 resources 或 tools 结果Skills 则可能输出自然语言。如果不在网关层做一次归一化业务方就得自己处理这四种格式的差异那才是真正的维护噩梦。2.3 为什么配置化比写死代码更重要早期版本我犯过一个错误就是把路由规则硬编码在代码里。比如某个函数 if 判断用户请求走哪个模型。结果每次调整模型优先级都要发版MCP server 地址变了也要改代码。后来我彻底改成配置驱动能力注册、路由策略、超时时间、鉴权方式全部放在 YAML 或 JSON 配置里支持热加载。配置化的核心意义不在于节省几个小时的发版时间而在于让网关变成一个通用平台。前端团队不用知道 Python 侧怎么实现 MCP 桥接只需要在配置中心填一个条目新能力就能被网关发现和使用。后面我会展示完整的配置示例这一节先记住结论能力即配置入口即统一。3. 四个核心模块的接入细节与路由策略3.1 LLM别让业务代码直接 new 一个 client大多数 LLM 项目的问题不是模型不够强而是模型接入写得太散。业务代码里直接new OpenAI()、new Anthropic()换供应商就要改业务代码。tsm-hub 把 LLM 也当作一种可路由资源业务方只传target: llm:deepseek-chat网关负责处理 API Key、重试、超时以及多家供应商之间的互备。我实际配置过的一条 LLM 路由策略是这样的路由场景首选模型降级顺序选择理由日常对话deepseek-chatchatglm-turbo, qwen-max性价比优先速度要快代码生成claude-3.5-sonnetgpt-4o, deepseek-coder代码理解能力更强长文档总结qwen-longminimax-long上下文窗口更大意图识别gpt-4o-minideepseek-chat低延迟低成本为什么网关层有资格做这种路由因为网关掌握全局上下文。它知道当前请求来自哪个业务线知道用户的历史会话长度知道哪家模型最近延迟偏高。单点接入的代码拿不到这些信息只能死板地调用写死的那一个 client。这里补充一个很多文章不会提到的细节LLM 路由的降级策略一定要做“预探测”不能在模型真正报错时才切换。我会在网关上做定期健康检查主动向各供应商发一个极小的请求探测可用性和响应时间。健康检查结果直接参与路由权重计算这样当某家模型开始变慢时新请求已经自动切到备用通道而不是等超时之后才被动降级。3.2 Tools注册、鉴权和限流三条线Tools 是 function calling 的具体实现但在网关层面Tools 注册不只是把函数加进一个列表那么简单。我把它拆成三条线来管理。第一是注册线。每个工具需要声明自己的名称、参数 Schema、描述、超时时间。名称必须全局唯一命名空间用冒号分隔比如internal:db_query、internal:file_write。参数 Schema 要严格写清楚否则 LLM 在选参数时很容易编造出不存在的字段。第二是鉴权线。很多工具调用之所以出问题不是因为代码错误而是因为权限边界模糊。tsm-hub 在每个工具上挂了三级权限谁能调用用户级、能传什么参数数据级、能触发什么副作用操作级。比如internal:db_query允许“查询”租户自己的数据但不允许“删除”表结构。这种策略在网关层做比在各工具内部自行判断要清晰得多。第三是限流线。LLM 在循环里疯狂调用同一个工具是真实会发生的事尤其当模型试图“多试几次”修复错误时。网关上要给每个工具单独配置 QPS 上限和并发上限超过上限时直接拒绝调用并返回错误给模型模型看到错误后通常会停止重复调用。没有这层防护一个失控的 Agent 循环就能把下游数据库打爆。3.3 MCP标准协议是最大公约数MCP 的出现让我意识到工具生态迟早要走标准化。早期 Function Calling 的问题在于每家模型厂商都定义了自己的工具描述格式OpenAI 和 Anthropic 的 Schema 结构不同接入两套就要写两份适配代码。MCP 作为协议层把工具描述、资源访问、提示词管理统一成一套 JSON-RPC 风格协议理论上能被任何支持 MCP 的客户端复用。tsm-hub 对 MCP 的处理方式很简单网关即 MCP 客户端。每个 MCP server 启动后网关注册进去把它的 tools 列表拉出来再映射到网关自己的统一路由表里。例如 Playwright MCP 会暴露一堆浏览器操作工具比如browser_navigate、browser_snapshot、browser_click映射到网关后它们就变成mcp:playwright:browser_navigate这样的能力标识符。Blender MCP 则提供 3D 场景操作能力比如blender:create_mesh、blender:render_viewport映射后变成mcp:blender:create_mesh。这里有一个关键心得MCP 的工具名有时会出现重复且语义过于宽泛。比如两个不同的 MCP server 都可能叫search或query如果直接全部映射到同一层级就会出现路由遮蔽。我在网关里强制要求每个 MCP server 必须声明自己的 namespace 前缀比如mcp:playwright、mcp:blender然后在这个前缀下挂具体的工具名。这样即使底层工具名重复在网关层也不会冲突。3.4 Skills把提示词和工具组合成能力Skills 在我这里不是单纯的提示词。真正好用的 Skills 至少包含三样东西一段明确的指令一组可用的工具清单以及一段上下文说明。它本质上是“一次性配置好的业务逻辑包”。举个例子我在项目里写了一个“发布前检查”Skill它做的事情包括读取 git 最近提交记录运行测试套件调用内部发布接口查询当前环境状态再把结果汇总给 LLM 判断是否可以发布。如果没有 Skill这个流程需要用户手动一条条发指令或者写一堆编排代码。有了 Skill用户只需要在网关里触发skill:release-check网关按顺序调用相关工具并把结果组织成上下文喂给模型最后模型输出一段可执行的检查结论。Skills 的版本管理值得多说一句。我见过太多团队的 Skill 直接放在共享文件夹里谁都能改改完没人知道改了什么。在 tsm-hub 里Skill 是一个配置实体带版本号、作者、依赖工具清单。更新 Skill 相当于发布一个新版本可以灰度、可以回滚。这种机制的启发其实来自 Claude Code 社区里手动安装 GitHub Skills 的做法——本质上是把 Markdown 模板放进固定目录但缺少版本管理。把 Skills 升级成“被管理的实体”这才是网关层该做的增值。4. 实操记录一个 Demo 从零到可调用4.1 初始化配置四个区块一次拉齐闲话不多说直接上一份最小的配置示例。这台本地网关同时接入了两个 MCP 服务一个 Playwright一个内部数据查询工具另外配了一个 LLM 供应商和一个 Skill。server: port: 8321 hot_reload: true llm: providers: - name: deepseek model: deepseek-chat api_base: https://api.deepseek.com api_key_env: DEEPSEEK_API_KEY timeout: 30s priority: 1 mcp: servers: - name: playwright transport: stdio command: npx args: [-y, playwright/mcplatest] namespace: playwright - name: internal-data transport: sse url: http://127.0.0.1:8762 namespace: internal tools: entries: - name: internal:db_query schema_file: ./schemas/db_query.json timeout: 5s auth: role:data-reader rate_limit: 30/min skills: entries: - name: skill:release-check version: 1.2.0 manifest_file: ./skills/release-check.yaml这份配置里transport: stdio表示 Playwright MCP 以本地子进程方式运行transport: sse表示内部数据服务走 HTTP 事件流。两者都有是因为我实际接的 MCP server 既有本地工具也有远程服务网关需要对两种传输方式都透明。4.2 起服务、注册第一个 MCP 工具网关启动时会依次完成几件事先加载 LLM 配置并做连通性探测再启动配置里的各 MCP 客户端把它们的 capabilities 拉取进来然后扫描 tools 目录注册本地函数最后加载 Skills 清单。如果一切顺利终端里会看到类似下面的日志[tsm-hub] llm provider deepseek - online, latency 240ms [tsm-hub] mcp server playwright - connected, 12 tools available [tsm-hub] mcp server internal-data - connected, 5 tools available [tsm-hub] tools registered - internal:db_query [tsm-hub] skill loaded - skill:release-check v1.2.0 [tsm-hub] gateway listening on :8321这时候就已经有了能力目录。我习惯在验收环境里跑一条简单的调用命令验证通路curl -X POST http://127.0.0.1:8321/v1/run \ -H Content-Type: application/json \ -d {target:mcp:playwright:browser_snapshot,params:{url:https://example.com,full_page:false}}返回会是一个带output字段的 JSON 结果。这里我想强调一下第一次调用成功并不代表接入完成一定要专门测试一下错误路径比如故意传入一个不存在的工具名看看网关返回的错误信息是否清晰可读。很多网关挂了半天没人发现往往是因为错误路径从来不返回明确的可诊断信息。4.3 写一个 Skill把模型和工具串起来光有工具调用还不够真正体现网关价值的是把多个能力串成一个 Skill。下面的 Skill 定义展示了一个组合逻辑先查 git 状态再查服务健康最后让模型给出结论。name: skill:release-check version: 1.2.0 description: 发布前检查代码与线上状态 steps: - target: internal:git_status params: {} assign: git_state - target: internal:service_health params: service: order-api assign: health_state - target: llm:deepseek params: prompt: | 以下是发布前的检查数据 git 状态{git_state} 服务健康{health_state} 请判断是否可以发布指出风险点。 assign: final_verdict output: - git_state - health_state - final_verdict执行时网关会按顺序调用internal:git_status和internal:service_health把结果存在临时变量里然后拼进提示词模板发给 LLM最后把三组结果打包成统一响应返回。这个过程中业务方不需要关心每个工具的参数格式和返回格式只需要触发一个 Skill 名。我在写这种 Skill 时学到一件事每一步的assign变量名要取得可读性强因为后续提示词模板会直接引用这些变量。取名为a、b、c的后果就是三个月后你自己都看不懂模板里{a}到底是什么数据。4.4 验证效果一次调用能看到什么当一个 Skill 被正确执行后响应大概是这样的{ status: ok, output: [ { key: git_state, value: {branch: main, ahead: 2, dirty: false} }, { key: health_state, value: {status: healthy, latency_ms: 86} }, { key: final_verdict, value: {conclusion: 可以发布, risks: [订单服务响应略慢]} } ], usage: { tokens: 2160, duration_ms: 4820 } }这种统一结构解决了我在开头提到的那个大问题业务方不用关心路径是走 MCP 还是走本地函数也不用关心中间到底编排了几步它们拿到的始终是一个结构清晰的响应体。下游前端只需要写一套渲染逻辑就能展示所有能力的执行结果。如果你用的是流式场景比如对话类需求网关也支持 SSE 输出。做法是把响应里的output换成流式事件每个事件包含 step 名称和增量内容。两种模式之间可以在配置层面切换不需要业务方改代码。这点对真实项目很重要因为对话类应用和流程类应用对响应格式的要求完全不同统一网关不能强制所有场景都用同一种形态。5. 常见问题与排查技巧实录5.1 六个高频故障速查表我在实际接管 tsm-hub 的这半年里遇到的故障有很强的共性。整理成表如下症状常见原因排查方向MCP 启动后工具为空传输方式配错或 stdio 进程秒退先手动跑一次启动命令看报错调用工具超时下游服务未启动或端口写错用 curl 直接测下游健康接口LLM 返回内容为空provider 连通性问题或参数过长看网关日志里的上游响应体工具名冲突两个 MCP server 未设置独立 namespace检查能力目录里的完整标识符Skill 执行到一半失败中间某步骤抛错且未配置重试在 skill 配置里加retry: 2限流误伤正常请求QPS 上限设置过小先看日志里的 rate_limit 字段再调参5.2 工具名冲突与路由遮蔽最容易踩的坑工具命名冲突在初期几乎必现。比如内部数据查询函数叫query某个 MCP server 里的工具也可能叫query。如果网关不做命名空间隔离两个工具注册到同一张表里后注册的会意外覆盖先注册的而且没有任何警告。我现在的策略是三层命名空间接入类型用mcp:、internal:、skill:区分接入方名称做第二层具体工具名做第三层。完整标识符一定是mcp:playwright:browser_snapshot这样三段式不允许只有两段。配置中心加了一条校验规则注册时发现标识符冲突会直接拒绝启动而不是静默覆盖。这套规则强制推广之后路由问题基本绝迹。5.3 MCP 断线与 Skill 注入顺序MCP 服务是长连接不像普通 HTTP 请求那样一次性结束。远程 MCP server 可能因为空闲回收、网络抖动或对端重启而断开。很多网关做不到自动重连断一次就要人工拉起。tsm-hub 的做法是每个 MCP 客户端内置心跳检测断线后按退避策略自动重连。我的实际经验是第一次重连失败后等 2 秒第二次 5 秒之后固定 30 秒一次如果连续失败 10 次才进入人工告警流程。这个策略能兼顾快速恢复和避免频繁无效连接。Skill 注入顺序是另一个容易被忽略的坑。所谓注入顺序指的是执行 Skill 步骤时前面步骤生成的数据能否被后面步骤正确引用。我在实现上要求 Skill 必须显式声明依赖关系如果第 3 步要使用第 1 步的结果必须在配置里写清楚depends_on: step1。这能避免一种隐蔽的错误步骤并行执行时A 还没完成B 就开始读取 A 的中间结果导致拿到空数据。显式声明依赖后网关会按拓扑排序来调度步骤彻底断了这类 bug 的根源。5.4 可观测性每一笔调用都要有记录统一网关最大的副产品是可以获得全局的可观测性。只要所有请求都经过网关你就可以给每一笔调用打上 trace_id记录它的目标、耗时、token 消耗、状态、错误信息。我现在每个请求落地后都会输出一条结构化日志{ ts: 2025-01-12T14:33:22.221Z, trace_id: tr_8f1a2b, target: skill:release-check, steps: [internal:git_status, internal:service_health, llm:deepseek], duration_ms: 4820, tokens: 2160, status: ok }有了这份日志很多过去靠猜的问题都变成了可检索的问题。比如“为什么某段时间用户觉得回复变慢”你只需要按 trace_id 过滤耗时分布马上能看到是不是某家 LLM provider 的 p95 延迟飙升。没有网关照单点调用这类数据只能靠每个业务方自己埋点口径还不统一。6. 从统一网关到智能体平台6.1 网关是自主 Agent 的骨架现在圈内讨论 LLM Powered Autonomous Agents 时很多人都在讲 ReAct、规划、记忆这些上层概念但很少提底层的基础设施。实际上一个 Agent 要稳定工作必须依赖一套可靠的工具生命周期管理。Agent 在循环中不断调用工具一个工具超时不能卡死整个循环一个工具重试不能无限重试一个工具的错误必须能反馈给模型去调整策略。这些能力正是网关层提供的基础保障。没有这种基础设施Agent 看起来跑得通但只限于 demo。一旦进入生产环境面对真实流量和真实数据立刻会被工具超时、权限缺失、上下文溢出这些问题压垮。6.2 把知识库接进来来自 LLM Wiki 的启发我在社区里看到不少人维护 LLM Wiki 这类知识库项目把模型文档、提示词模板、最佳实践整理成结构化资料。起初我觉得这跟网关没关系后来想通了知识库内容如果能以“能力资源”的形式挂到网关里价值会大得多。比如网关在调用 LLM 前可以先检索知识库中与当前问题相关的条目把条目内容注入到上下文里模型回答的准确率会明显提升这就是“reliable LLM”的实现路径之一——不是依赖模型本身变得更可靠而是在网关层把可靠的信息喂给它。我接下来的扩展方向是做一个 knowledge connector把 LLM Wiki 这类知识库暴露成 MCP resources让网关注册后可被任意 Skill 引用。这样一来Skills 不仅要工具还能按需拉取实时知识模型就不会只靠训练时的那点记忆来回答新问题了。6.3 多团队共享能力集市的方向统一网关的终极形态是变成组织内的能力集市。每个团队可以把自己的内部工具注册成标准能力其他团队通过网关调用不需要互相了解对方的代码结构和部署方式。权限由网关统一管控调用量可以被计量能力质量通过日志和 trace 持续评估。这条路我还没有完全走通但方向已经很明确。当前我至少做到了让“新能力接入”变成一个纯配置行为服务端开一个 namespace客户端请求一个 target中间由网关完成剩余的一切。如果后续把接入流程再产品化做出一套自助注册的 UI那就真正可以铺开给整个团队用了。维护 tsm-hub 半年之后我最大的体会是统一网关最值钱的地方并不是那层“路由转发”而是它让所有能力第一次有了一个可以被统一观察、统一管理、统一编排的入口。以前模型、工具、协议、技能各自为战出了问题只能靠人肉排查现在不管调用来自哪条业务线我都能在一张日志表里看到全貌。如果你也正在被“多个模型多套工具接不拢”折磨建议别急着写业务代码先把这层网关搭起来。它不一定要像我这样做成独立服务哪怕只是在代码里抽出一个统一的调用入口也会让你的后续开发轻松很多。
网站建设高端定制企业官网