
做 AI 应用开发这一年多我最大的感受不是模型不够强而是“接入的姿势”越来越乱。LLM 要接闭源、要接开源、要接本地部署调用方式五花八门Tools 散落在各个服务里Agent 想用还得自己拼 HTTP鉴权和超时全靠人肉维护MCP 服务器从一两个攒到五六个启动方式和连接协议各不相同Skills 更是各写各的换个团队基本没法复用。后来我把这些全部收进了一个叫 tsm-hub 的统一网关对外只暴露一套 OpenAI 兼容的 API内部负责路由、编排、鉴权和观测。这篇文章就把它背后的设计取舍、四个核心模块的落地细节、以及我实际部署和踩坑的过程完整记录下来给同样被碎片化问题折磨的朋友一个能直接参考的样板。1. 为什么需要一根总线LLM、Tools、MCP、Skills 各自的混乱1.1 四个模块单独拎出来各自都有让人头疼的地方先说 LLM。模型接入本身不难难的是“管理”和“切换”。今天用 A 厂的模型写日报明天要切到 B 厂做报销单分类后天又要在本地跑一个量化版模型做脱敏推理。每家 SDK 不一样、密钥不一样、返回格式还有细微差别。如果业务代码里到处硬编码了模型厂商的 SDK换一次模型就是一次全局重构这个痛我猜不少人体验过。再说 Tools。工具这个词在 AI 应用里指的就是 Agent 或模型能调用的外部能力查天气、查库存、发邮件、操作数据库。这些东西散落在不同微服务里有的走 REST有的走 gRPC有的甚至藏在某个内部 Python 脚本里。Agent 要调用它们就得知道每个服务的地址、认证方式、参数格式和超时时间。工具一多连“有哪些工具可用”都说不清楚更别提做权限控制和调用审计了。第三个是 MCP。MCPModel Context Protocol是最近特别火的开放协议它把“外部工具和数据源”的接入方式标准化了目标是让模型应用能像插 U 盘一样接入各种能力。协议本身是好东西但服务器一多问题就来了有的是 stdio 子进程方式启动有的是 HTTPSSE 方式连接有的需要带 token 握手有的启动很慢有的隔一段时间会断连。你需要在应用里维护每一条 MCP 连接的声明周期这活儿干久了会非常疲惫。最后是 Skills。Skills 有点像是给模型用的“组合技能包”一个技能 一段精心设计的提示词 一组按需调用的工具 一套上下文处理策略。比如“每日晨报”技能要先查天气、再拉日历、再汇总待办最后用固定模板生成一份简报。问题在于不同来源的 Skills 格式不完全一样有的用 Markdown 写 prompt有的用 YAML 定义参数有的还带一堆附带的 Node 依赖。Skills 本身没有统一的“加载、运行和管理”机制用起来总觉得差一口气。1.2 网关化是必然选择不是故作玄虚如果你做过微服务架构应该立刻能反应过来这就是典型的“多源系统需要统一接入层”的场景。服务端可以有几十个下游依赖但客户端只需要面对一个网关地址由网关去做协议转换、负载均衡、鉴权过滤和链路追踪。tsm-hub 做的事情就是把这一套思路搬到 AI 应用领域。统一之后最直接的好处有三个。第一业务侧代码写一次就不用动了对接 OpenAI 的代码直接改一下 base_url 指向 tsm-hub 就能拿到所有能力。第二治理动作从“散落在业务代码里”变成“集中在网关里”密钥管理、调用限流、耗时统计、成本归因全部在网关做业务侧干干净净。第三新增能力不需要改业务代码新接一个 MCP 服务器、新增一个工具、上传一个新的 Skill都只是改网关配置或调用注册接口业务方完全无感。2. tsm-hub 的整体架构与核心设计思路2.1 分层结构接入层、编排层、适配层tsm-hub 内部我习惯分三层看。最外面是接入层也叫 Gateway API 层只暴露两类接口一类是 OpenAI 兼容的/v1/chat/completions支持流式和普通模式另一类是管理接口比如工具注册、MCP 服务器状态查询、Skills 列表。中间是编排层这是网关的大脑。收到一个请求后编排层要根据配置决定走哪条链路是直接丢给某个模型还是先加载某个 Skill 的提示词还是需要循环调用多个工具直到拿到最终结果。编排层不关心模型底层是哪个厂商也不关心工具到底在哪台机器上它只面向统一抽象干活。最下面是适配层负责跟具体系统打交道。LLM Provider 适配器封装各家模型的接口差异把结果统一成标准格式Tool Registry 管理所有已注册工具的描述和执行器MCP Client 模块负责跟各个 MCP 服务器建立和维护连接Skill Loader 负责扫描和加载技能定义文件。每一类适配器都能独立替换或扩展这是网关能持续演化的基础。2.2 核心抽象一切皆可寻址的 Endpoint在设计内部数据模型的时候我定了一个很关键的原则LLM、Tool、MCP 上的工具、Skill 运行入口全部抽象成统一的Endpoint概念。一个 Endpoint 有唯一名称、有描述、有输入输出 schema、有调用地址、有鉴权策略。这样编排层就可以用一套通用的逻辑去处理“调用一个工具”和“运行一个技能”——本质上都是路由到一个 Endpoint然后读取返回结果。抽象实体含义对应真实资源Endpoint可调用的统一入口某模型、某工具、某 MCP 工具、某 SkillToolSpec工具调用声明名称、描述、参数 JSON SchemaSkillDef技能定义提示词模板、工具列表、运行参数MCP ConnectionMCP 服务器连接stdio / SSE 传输方式、鉴权信息这个抽象帮了大忙。后来我接一个新的 MCP 服务器只需要注册它的几个工具到 Endpoint 表里业务端立刻就能查得到、调得动不需要额外写适配代码。2.3 技术选型背后的权衡对外协议为什么选 OpenAI 兼容接口理由很朴素这是目前生态最通用的“普通话”。Claude Code、OpenCode、各种 Agent 框架、LangChain 生态里的工具大多都支持自定义 OpenAI 风格的 base_url。把 tsm-hub 全能力暴露成这个格式等于客户零成本接入这是最现实的选择。内部配置为什么用 YAML 而不是数据库因为配置本身就是一种“代码资产”应该走 Git 评审、版本回滚。YAML 写清楚几个大段llm 提供商列表、工具注册表、MCP 服务器列表、Skills 目录。启动时加载并做校验有问题直接报错不会出现配置静默失效的情况。流式传输为什么用 SSE 而不是 WebSocketSSE 是单向服务器推送天然适合流式 token 输出实现简单且天然兼容 OpenAI 的stream: true参数。WebSocket 虽然是全双工但对大部分“请求-响应式”的 Agent 调用来说是不必要的复杂度。在实际运行中SSE 的稳定性也很好没有遇到性能瓶颈。3. 四个核心模块的落地细节3.1 LLM 接入与模型路由一份配置适配所有模型LLM 模块的设计目标很明确任何 AI 应用只需要知道一个 base_url至于背后是哪家模型由网关决定。每个 Provider 在配置里声明自己的 base_url、api_key 来源避免明文写死在文件里、支持的模型列表和默认超时时间。网关内部维护统一的请求上下文把各家返回的 content、token 用量、finish_reason 全部归一化。模型路由是我花了不少精力打磨的功能。最简单的路由是按模型名硬匹配稍微好一点的是给模型打标签比如“fast”“cheap”“strong”然后让客户端只传标签网关去解析真实模型名。再进阶一点就是故障转移主模型超时或者触发限流时自动切换到备用模型并在响应头里告诉调用方实际用了哪个模型方便排查。实测下来这个机制在业务高峰期非常有用不至于因为某个模型厂商抖动就把整个链路拖死。3.2 Tools 注册与调用链路让 Agent 真正“用得上”工具工具要能被模型正确调用光有后端实现不够必须让模型“看得懂”工具是什么。这就是 ToolSpec 的作用。每个工具注册时要提供名称、一句话描述、参数 JSON Schema。名称用动词_名词风格描述里写清楚适用场景参数写精确类型和枚举值。这样设计之后模型选择工具的准确率明显提升不再老是把城市名传到“日期”参数里这种低级错误。调用链路我分成了四步模型在对话过程中输出一个 tool_call 指令网关收到后先做权限校验确认这个调用方有没有执行该工具的权限然后通过工具执行器转发到真实服务并记录开始时间和超时控制最后把结果以固定格式拼装回上下文交给模型继续推理。这一套流程看起来简单但真正要处理的是边界情况工具超时怎么提示模型工具返回了超大结果怎么截断工具连续调用多轮怎么防止死循环这些我在第五部分会展开讲。3.3 MCP 网关化把零散的服务器收编成统一资源池MCP 的核心价值是把“工具接入”标准化了服务器对外暴露的每一个 capability 都有清晰的能力描述和调用方式。tsm-hub 在 MCP 这里做的事情是把所有需要连接的 MCP 服务器收编成一个资源池统一管理它们生命周期。每个 MCP 服务器在配置里声明传输方式。stdio 方式的适合本地文件系统、数据库这类私有资源网关负责拉起子进程并维护 stdin/stdout 通信SSE 方式的适合远程服务用 HTTP 握手建立事件流。网关启动时会依次初始化所有连接做一次能力列表拉取然后把每个 MCP 工具映射成内部 ToolSpec。之后模型调用映射出来的工具网关负责完成协议转换和结果组装。运维层面我加了两个很有用的能力一个是健康检查定时探测 MCP 连接是否正常断开自动重连另一个是作用域隔离同一个 MCP 服务器可以被多个租户共享但每个租户只能调用授权范围内的工具。这两个能力在生产环境里帮了大忙毕竟 MCP 服务器再标准化它也不会自己变高可用。3.4 Skills 的定义与组合提示词和工具编排的“可复用装配线”Skills 模块是我个人最喜欢的部分。它解决的根本问题是怎么把“会聊天的大模型”变成“会干活的智能体”。我的 Skill 定义格式里包含技能名称和描述、系统提示词模板、可用的工具列表、运行参数温度、最大 token 数、上下文策略比如是否带上一次对话历史、要不要把结果写入短期记忆。运行一个 Skill 的流程是这样的请求进入后Skill Loader 找到对应定义把提示词模板渲染成最终的系统提示词然后走常规的 LLM 对话循环但额外注入该 Skill 绑定的工具列表。如果 Skill 里的步骤有强依赖关系我还会在定义里写明 stage 顺序例如“先查天气再生成穿衣建议”避免模型自由发挥把步骤搞乱。Skills 和 Tools 的区别也值得强调。Tool 是原子能力一个工具只做一件事Skill 是组合策略把多个工具和提示词编排成一个完整的自动化流程。团队成员之间分享 Skills就像分享菜谱一样不用互相扒代码。这也是我把 Skills 做成纯配置化、不绑定任何业务代码的原因。4. 实操记录从零部署一套 tsm-hub 并跑通完整链路4.1 安装与初始化配置我习惯用 Docker 部署简单干净。启动之后第一次做的事是把基础配置文件写好主要包括四个段落LLM 提供商、工具注册表、MCP 服务器列表、Skills 目录。启动命令很简单docker run -d \ --name tsm-hub \ -p 8080:8080 \ -v /etc/tsm-hub:/etc/tsm-hub \ -e OPENAI_API_KEYsk-xxx \ tsmhub/tsm-hub:latest配置文件的骨架如下一看就明白大概长什么样子gateway: port: 8080 default_model: gpt-4o llm: providers: - name: openai base_url: https://api.openai.com/v1 api_key_env: OPENAI_API_KEY models: [gpt-4o, gpt-4o-mini] - name: local base_url: http://localhost:11434/v1 models: [qwen2.5:14b] mcp: servers: - name: weather_mcp transport: sse url: http://weather-mcp:8000/sse scopes: [finance] skills: dir: ./skills load_order: [daily_reporter, meeting_summary]4.2 一个完整场景的调用演示我实际跑得最多的一个场景是“用自然语言查天气并给出出行建议”。客户端只做一件事向/v1/chat/completions发起一个标准请求请求里带了技能名称和用户问题。curl http://localhost:8080/v1/chat/completions \ -H Content-Type: application/json \ -d { model: weather-advisor, messages: [ {role: user, content: 北京明天适合户外跑步吗} ], skill: daily_planner }网关收到请求后先加载daily_planner这个技能注入它绑定的工具列表里面包含了通过 MCP 接入的weather_query工具。模型决定调用工具后网关把请求转给 weather MCP 服务器拿到明天的天气数据把结果夹进对话上下文继续生成最终回答。整个过程业务侧完全无感不知道背后接了 MCP、做了工具调用只看到最终返回的一段自然语言。这正是统一网关该有的体验内部再复杂暴露出去的必须简单。4.3 对接 Claude Code 和 OpenCode 这类 Agent 框架把 tsm-hub 接进现有 Agent 框架核心动作就是换 base_url。以 Claude Code 为例它的环境变量里指定 API 地址指向http://localhost:8080/v1就行OpenCode 也是类似思路只要它支持自定义 OpenAI 兼容端点。这样可以带来一个很实际的好处你在 Claude Code 里操作背后所有的工具调用、Skills 运行、MCP 资源访问全都走 tsm-hub 的统一治理权限、日志、成本都能被管起来。团队里有人问你“这个工具怎么接的”你只需要回一句“看 tsm-hub 配置文件”比解释一长串内部调用链轻松多了。5. 常见问题与排查技巧实录5.1 MCP 连接反复失败这是我遇到最多的问题现象是网关启动时提示 MCP 服务器握手超时或运行中 SSE 连接突然断开。排查思路是按传输方式分两类看stdio 方式重点查子进程启动命令和依赖安装经常是npx找不到包或者 Node 版本不对SSE 方式重点查网络连通性和握手鉴权比如 token 过期、服务端路径变化。我后来把 MCP 连接的状态打点全部接入日志每次建立连接、拉取能力、调用工具都有记录。只要看日志就能定位是哪一步失败不用再猜。5.2 模型输出的工具调用参数解析失败现象是模型明明选择了某个工具但网关解析参数时报错。比如模型把数字写成了字符串、日期格式不对、或者是多选了工具。根源往往是工具描述不够清楚模型靠猜。解决办法是把参数描述写得极其直白枚举值尽量列全并加上示例值。此外网关里要加一层“参数矫正”数字类型自动转成 number缺失必填项时用默认值兜底实在不符合 schema 就返回一段明确错误给模型让它重新生成。5.3 Skills 加载顺序与优先级冲突配置了多个 Skills 之后同名技能、同名工具冲突问题会冒出来。我的做法是引入命名空间每个 Skill 或 Tool 都带前缀比如finance_weather_query和general_weather_query避免覆盖。加载顺序也有讲究后加载的同名定义默认忽略并打 warning 日志如果两个 Skill 都要用到同一个工具工具定义只加载一次复用不会重复创建。5.4 流式输出的首字延迟过高普遍会出现的问题配置了本地模型和多个 MCP 服务器后开启流式时第一个 token 等了 5 秒以上。原因通常是请求链路中多了一个模型路由判断、多个 MCP 连接的健康检查影响了事件循环。解决方法是把健康检查做成异步任务不在请求关键路径上同步执行模型路由的规则尽量静态化不要每次请求都做复杂计算。优化之后首字延迟降到了 1 秒以内体验提升非常明显。问题现象排查方向解决办法MCP 连接失败握手超时/中途断连传输方式分类排查用日志打点stdio 查依赖SSE 查网络与鉴权工具参数解析失败模型调用工具时参数报错查看 ToolSpec 与真实请求体写清描述、列全枚举、增加参数矫正Skills 冲突覆盖同名技能/工具互相覆盖检查加载日志引入命名空间后加载忽略并告警首字延迟过高流式输出卡顿看耗时分布日志健康检查改异步路由规则静态化6. 我踩过的坑和沉淀下来的心得6.1 网关一定要克制别做成重平台我最早的时候想把 tsm-hub 做成一个带管理后台、带可视化编排、带定时任务的大平台后来果断砍掉了。原因很简单网关的价值在于“集中和转发”不在于“编排和业务”。一旦把业务逻辑塞进网关它就会变成一个新的技术债源头。工具调用状态机、技能内部复杂逻辑这些应该留在业务侧或其他服务里网关只需要做标准动作接入、路由、转发、记录。6.2 日志和 trace 要从第一天就做没有 trace 的网关等于盲人摸象。我在每个请求入口生成一个trace_id全程透传到各个 Provider 和工具调用记录里。这样无论问题出在哪个环节都能用一条 trace_id 串起来看。后来排查线上事故80% 的情况都是靠 trace 日志五分钟内定位比对着多个系统翻日志舒服太多了。6.3 对外只认 OpenAI 格式内部爱怎么玩怎么玩这是我最坚持的一点。只要客户端侧只见 OpenAI 兼容格式你的基座模型换多少次、MCP 服务器加多少个、Skills 怎么升级对业务调用方都是透明的。生态兼容性带来的长期收益远远大于为某个定制协议付出的短期便利成本。这也是 tsm-hub 把兼容性放在第一优先级的原因。6.4 后续可以做哪些扩展目前我自己在往下推进的方向有三个一是给网关加一层轻量的多租户能力不同部门不同密钥配额独立二是把成本统计做到每个请求级别月底按模型消耗生成报表方便团队做预算归因三是研究一下 Skill 生态的插件机制希望在社区里让大家共享技能定义文件而不是每次都从零开始写。按照现在的演进速度这几个方向应该很快就能落地。我个人在实际操作中最大的体会是统一网关不是银弹但它确实把 AI 应用接入周边系统时的无序感消除了大半。如果你也正被一堆模型 SDK、工具散装代码和越来越多的 MCP 连接搞得心累不妨用类似思路搭一个轻量网关试试。从一个小山头的工具收编开始把四类资源统一起来管理效果会在几周内逐渐显现。