做AI Agent开发这段时间,我最大的感受是:项目没做多少,环境的复杂度先把自己劝退了。一个稍微认真点的Agent项目,手里至少捏着两三个大模型的API Key、五六个MCP Server地址,还有一批散落在GitHub上的Skills仓库。每个Server有自己的一套鉴权方式,有的走Header,有的走Query参数,有的还得先获取临时Token;协议有stdio、有SSE、有WebSocket;接口格式有OpenAI兼容、有Anthropic原生、还有各家自定义。新同事或者换个环境,光把这些串起来就得折腾小半天。
所以我干脆做了个小工具,把自己日常用到的LLM、Tools、MCP、Skills全部收编到一个统一网关里,内部代号就叫tsm-hub。这个网关做的事情说白了就是:统一入口、统一鉴权、统一路由、统一日志。所有Agent,包括Claude Code、自己写的脚本、甚至是一些自动化测试工具,都只跟网关说话,网关再去跟上游的各种服务打交道。折腾完这个网关,我的开发体验发生了质的改变。
这篇内容就是把我搭tsm-hub的设计思路、配置细节和踩坑过程完整记录下来。适合正在做Agent开发、被多个MCP Server和多模型切换折磨的人参考,也适合刚接触Skills和MCP协议、想搞清楚这几个概念之间关系的新手。
1. 为什么需要tsm-hub:LLM生态的碎片化困境
1.1 从"一个模型一把钥匙"说起
先说一个最基础的场景:你写了个翻译机器人,用OpenAI的GPT-4o。过几天你想试试Claude的Sonnet,发现API格式不一样。再后来团队决定接入本地部署的开源模型,还好它是OpenAI兼容格式,但你又发现超时时间、错误码、重试策略全都要重新调一遍。为了兼容这几家,代码里搞了三套调用分支,每份一套独立配置,光维护就让人头大。
这还只是单模型的情况。做Agent的时候,一个任务往往要调用多个工具:网页抓取用Playwright MCP、安全测试用BurpSuite MCP、3D建模用Blender MCP、浏览器自动化还得在Chrome扩展里启用MCP连接。工具多了之后,每个MCP Server的地址、鉴权Token、可用参数,散落在不同的配置文件和启动脚本里。有一天某个Server悄悄换了端口,你排查半天才反应过来是环境变了,而不是代码出错。
更麻烦的是Skills。在Claude Code里装一个GitHub上的Skills,要么手动拉到本地塞进指定目录,要么跑一堆安装命令。装完之后,很多人压根不知道去哪里验证这个Skill生效了没有,出了问题也不知道是描述文件写错了还是加载路径不对。再比如Superpower Skills这类社区包,它有自己的更新节奏和依赖要求,想在多个项目里复用,就不得不反复复制粘贴。这些散落的能力,本质上都在呼唤一个统一的收口层。
1.2 碎片化带来的三个真实代价
上面这些场景讲的是现象,我总结下来碎片化带来三个成本,前两个很多人能想到,第三个容易被忽略。
第一是接入成本。每接一个新能力,就要改一遍客户端代码或者改一遍启动环境,做不到"配置即接入"。第二是维护成本。N个服务就有N个要盯的点,任何一个挂了都要单独去查日志,排查链路断在哪里全靠人肉。第三是治理成本。公司内部多人共用一套模型Key,谁用了多少Token、哪个Skill消耗了大头,完全是一笔糊涂账。我见过一个团队月底收到模型账单,发现有一个诡异的接口被调了几万次,最后查出来是某个同事的脚本没关定时任务,这就是典型的无治理状态。
1.3 统一网关的本质:把复杂性从客户端移到服务端
明白了这三个成本,统一网关该做什么就非常清楚了。它不是一个花哨的功能集合,而是一个"收口"层。客户端的职责被收缩到只剩一件事:跟网关说话。你只需要把网关地址和一把虚拟Key发给客户端,剩下的模型选择、工具调用、MCP Server连接、Skills加载,全部由网关在服务端完成。
这种思维本质上就是把复杂度从客户端挪到了服务端。代价是网关本身也要维护,但它解决的是全局性的复杂度,相比在每个客户端里各维护一套环境配置,划算得多。用一个生活的类比,以前你家里电视、机顶盒、游戏机、音响各自带一套遥控器,现在来了一个万能遥控器,虽然它本身也需要设置,但一旦配好,所有设备都归它管。tsm-hub在我这边的角色就是那台万能遥控器。
2. 四个核心概念在tsm-hub里的角色定位
2.1 LLM:不仅是模型,更是路由的判断依据
在tsm-hub里,LLM不是一个模型列表那么简单。我把模型当作三类资源来管理:一类是快速便宜的模型,比如7B~14B级别的开源模型,用来处理摘要、分类、意图识别这类简单任务;一类是中等能力模型,比如Claude Sonnet、GPT-4o mini这类,处理日常Agent对话;一类是强力模型,比如Claude Opus或者更大的推理模型,只在关键复杂推理时使用。每个模型都登记了上下文窗口、单Token成本、并发上限、延迟特征。
网关在收到请求时,根据请求里带的model字段和业务标签,把流量路由到合适的模型上。模型选型我参考过Open LLM Leaderboard这类公开榜单,但说实话,榜单排名只能作为初筛,真正决定采用哪个模型,还是得拿自己的业务数据做评测。我在网关里给每个模型加了一个"评测分数"字段,这个分数完全来自我自己设计的测试集跑出来的结果,而不是网上的综合分数。
另外关于LLM的Token,网上有个归纳我觉得很准确:key是"我是谁",query是"我在找什么",value是"我能提供什么"。我在设计网关的接口参数时也借鉴了这个思路,每个上游模型映射都带清晰的标识、意图描述和能力声明,方便后续做路由和维护。模型多了之后,如果没有这套语义化的描述,你会发现自己根本记不住哪个端点对应哪个模型。
2.2 Tools:从硬编码函数到可注册能力
早期做工具调用,大家都是一堆if-else硬编码:识别到某个意图,就去调某个函数。后来有了Function Calling,模型能自己输出JSON格式的调用参数,但函数本身还是写在代码里。tsm-hub的做法是把工具变成一条条注册记录,每一条工具记录包含:名字、入参Schema、目标地址、调用协议、超时时间、出错回退策略。
宿主程序(也就是Agent)不用关心这个工具是本地函数还是远端的HTTP接口,它只需要告诉网关"我要调这个工具",网关负责把参数转成对应服务的格式,再把结果转回统一格式。这种设计的好处是,工具可以被不同的Agent复用。同一个"网页搜索"工具,既可以被翻译Agent用,也可以被数据分析Agent用,只需要在网关里配一次。
这里特别想强调:工具入参Schema一定要写严谨。很多人在网关里注册工具时,入参Schema随便写写,结果模型调用时参数经常格式错误。我的经验是,把每个字段的description写得足够具体,比如"url:要抓取的网页地址,必须是http或https开头的完整URL",模型生成的参数质量会明显提升。这是因为模型的Function Calling能力很大程度上依赖你对参数的描述是否清晰。
2.3 MCP:为什么说它是"USB-C"标准
聊到MCP,很多人第一反应是问"MCP到底是软件协议还是硬件协议"。这个问题本身说明MCP还处在概念普及期。MCP全称Model Context Protocol,它就是一种软件协议,用于规范AI模型与外部工具、数据源之间的通信。它的定位有点像所有设备统一用USB-C接口,不管是键盘、显示器还是U盘,插上就能用。
我理解MCP的核心是三个原语:Tools是"可执行的行动",Resources是"可读取的数据",Prompts是"可复用的提示"。一个MCP Server通过JSON-RPC 2.0规范暴露这三类能力,Client负责发现和调用。在tsm-hub里,我做了两类MCP支持:一类是网关自己作为MCP Client,去连接外部的MCP Server地址,把这些Server的能力纳管进来;另一类是网关对外暴露一个MCP Server接口,让Claude Desktop这类Host能直接发现网关里的所有能力。
日常接触到的MCP Server形态很多。Playwright MCP管浏览器自动化,Blender MCP管3D建模操作,BurpSuite MCP管安全测试,还有像llm wiki知识库这类,可以把整理好的LLM学习资料通过MCP Resources暴露出来当知识库用。它们本质上都是一个个独立的MCP Server,各管各的。把它们收进tsm-hub之后,客户端只需要连一个MCP端点,就能看到一个聚合后的工具列表,本质上是把"N个入口"变成了"1个入口"。
2.4 Skills:把经验固化成语义化的能力包
Skills在Claude Code这类Agent里是一种相对新的概念,核心是一个SKILL.md文件,里面描述了该技能适合处理的任务、使用步骤、注意事项和例子。它的价值在于:让模型在需要的时候才加载特定领域的指令,避免把整个系统提示词撑得无比巨大。
怎么手动装GitHub上的Skills,这个问题被问了很多次。以Claude Code为例,最简单的方式是下载仓库,把Skills目录下的文件放到项目的.skills目录下,然后在配置里声明启用。但这种方式有两个痛点:一是Skill散落各处,换台电脑就得重装一遍;二是没有版本概念,GitHub仓库更新了你也不知道,等发现时模型行为已经和预期不一致了。
tsm-hub的Skills管理正好解决这两点。我在网关里登记Skill的元信息和来源仓库,Agent启动时通过网关拉取最新版本。Superpower Skills、前端开发Skills、数学建模Skills这类社区包,都可以当作一个个插件挂进网关。我在网关的Skills记录里还加了一个"能力声明"字段,用来描述这个Skill适合什么场景,帮Agent在启动时快速判断要不要启用。这就有点像给技能做一个可搜索的元数据头,Agent初始化时不用把所有技能都读一遍,而是按需拉取。
3. tsm-hub的架构设计与核心实现
3.1 分层架构:接入层、路由层、服务层
tsm-hub的设计我分了三个层次,每一层职责单一,互不越界。
接入层是外部门面,负责接收外部请求,解析统一的API格式。我这边支持三种接入方式:OpenAI兼容的HTTP接口、MCP Client接入、以及WebSocket的流式通道。接入层做的最重要一件事是身份认证:每个接入方拿一把虚拟Key,这把Key只对网关负责,不涉及任何上游的真实密钥。客户端泄漏了虚拟Key,吊销一把就行,不用去上游重新申请。
路由层是网关的核心判断逻辑。它根据请求里的模型名、工具名、技能标签、以及调用方的优先级,决定请求该转给哪个上游。我在这里做了一个很实用的"三档回退":主模型超时就用备用模型,备用模型也失败就返回降级响应。实测下来,一个经常抽风的上游模型服务,在回退机制的保护下,对最终用户的影响可以降到很低的水平。
服务层负责真正跟上游打交道,包括MCP Server、各家模型API、以及内部自建的Skills仓库。服务层的每个上游连接都是独立进程或者线程池,互相不阻塞。比如某个MCP Server响应很慢,它只拖住自己的连接池,不会影响其他工具的调用。这里有个细节,服务层的超时设置我分了三档:快速工具5秒、普通工具15秒、长耗时任务30秒以上,避免一刀切导致部分任务频繁失败。
3.2 配置驱动的能力注册机制
统一网关最忌讳的是"代码里写死"。我在tsm-hub里规定,任何能力要接入,必须走配置文件注册,不允许在业务代码里硬编码地址和密钥。这里说的"代码里写死"包括环境变量里硬塞,也包括常量文件里放字符串,都不行。所有注册信息都放在config目录下面,按类别拆成文件。
models.yaml大概长这样:
models: - name: fast-routing provider: openai-compatible base_url: http://127.0.0.1:8000/v1 model: qwen2.5-7b-instruct context_window: 32768 cost_per_1k_tokens: 0.002 priority: 1 fallback: mid-routingtools.yaml和mcp_servers.yaml结构类似,每个工具和服务都有唯一的名称、协议类型和连接参数。skills.yaml则多了版本号和来源仓库地址。这种设计的好处是:新接入一个MCP Server,或者更新一个Skill版本,只要改配置文件后热加载即可,不用重新编译、不用重启服务。
我踩过的一个坑是:早期所有东西放一个config.yaml,结果几百行配置混在一起,改模型时候碰到工具,改工具时候碰到Skills。后来拆成多文件,按领域划分,每个文件自己管自己的块,配合一个简单的schema校验,出错的概率低了很多。配置驱动听起来是常识,但真做到"全部能力都配置化",是有一个过程的,建议从一开始就坚持。
3.3 协议转换与统一调用链路
协议转换可能是新手觉得最难理解的部分。其实核心就是把外部各种格式转换成内部统一格式,再转出去。为了讲清楚这一点,我画一张逻辑图在脑子里,用一个调用MCP工具的例子说明。
假设Agent想调用一个Playwright MCP里的浏览器截图工具,完整链路是这样的:
- Agent调用网关的HTTP接口,参数是统一JSON格式
- 网关路由层查到这个工具注册在名为"playwright-mcp"的Server上
- 网关作为MCP Client,用JSON-RPC 2.0向该Server发起tools/call请求
- Server返回截图结果,网关把结果统一封装成OpenAI tool message格式
- 把这个消息返回给Agent
这里面有一个容易忽略的细节:MCP的Streamable HTTP需要建立Session,SSE需要订阅事件流,而常规HTTP是一次请求一次响应。网关把这些底层的会话管理全部藏起来了。对外看,调用方只见过最简单的一问一答,根本感觉不到背后连接的复杂性。这就是统一网关带来的"透明性",也是我觉得它最有价值的地方。
4. 实操:从零搭建一个tsm-hub网关
4.1 环境准备与基础选型
不说废话,先讲清楚需要准备的东西。我当时的实验环境是:一台Ubuntu 22.04的机器,Python 3.10,Node.js 18以上(为了跑Claude Code),还有Docker(用来跑一些MCP Server)。tsm-hub本身我是用Python写的,依赖比较少,主要是FastAPI和httpx。如果你更熟悉Node或者Go,完全可以换语言实现,核心是那套配置驱动加转发层,语言不是重点。
先把最基本的能力打通:能用Python调用OpenAI兼容接口。我在本地用开源模型起了一个OpenAI兼容端点,后面网关会往这个端点转发模型请求。没有本地模型也可以直接配置云端API,只是我把成本敏感的流量放在本地模型,效果敏感的放在云上模型。这一步其实是个冒烟测试,确保你的开发环境能发出第一通请求。
我还建议顺手把llm wiki这类知识库项目拉下来,把它当MCP Server的Resources挂载点。很多做LLM开发的人喜欢在本地维护一个知识库文档,里面放各种模型的对比、Trick、踩坑记录,用llm wiki的方式整理好,通过MCP的Resources暴露给Agent,这样Agent在回答涉及模型选型的问题时,能直接查到你整理的最新资料,而不是依赖训练数据里可能过时的信息。
4.2 核心配置:从零开始写配置文件
创建config/models.yaml、config/mcp_servers.yaml、config/tools.yaml、config/skills.yaml四个文件。我最想强调的一点是:模型名不要写错。很多人都以为传个base_url就够了,其实上游模型服务对model字段非常敏感,写错一个字就返回400或者404。配置之前,先把上游服务支持的合法模型名拉出来核对一遍,再写进配置。我见过不少人排查了半天,最后发现是模型名拼写不一致。
mcp_servers.yaml里有个字段我很推荐加:tools_prefix。
mcp_servers: - name: playwright-mcp transport: streamable-http endpoint: http://127.0.0.1:8931/mcp headers: Authorization: Bearer <你的鉴权Token> tools_prefix: web_tools_prefix的作用是:把该Server下的所有工具统一加上前缀,避免多个Server之间有同名工具冲突。这是我实际踩过坑之后加上的设计。早期不加前缀,两个MCP Server都有个叫search的工具,网关转发时不知道该发给谁,日志里的报错也很迷惑。加前缀之后,playwright-mcp的search是web_search,另一个工具库的search是doc_search,一目了然,路由也不会撞车。
每个MCP Server的传输方式也值得注意。本地工具用stdio,网络服务用Streamable HTTP或SSE。stdio的好处是不用暴露端口,适合开发机;Streamable HTTP适合分布式部署,但要注意Session超时问题。我自己的经验是:能用stdio的先用stdio,实在跨机器才用HTTP,能少踩一半的坑。
4.3 启动网关并验证能力
配置写完之后,启动网关就一条命令。但启动只是开始,真正的挑战在验证环节。
第一步验证模型转发:用curl直接打网关的/v1/chat/completions,带上虚拟Key,看能不能得到一个正常的模型回复。第二步验证工具发现:调用网关暴露的/tools端点,看能不能列出所有聚合后的工具列表,包括来自各MCP Server的工具。这个列表非常有用,它能让你一眼看出哪个Server没连上、哪个工具没注册进去。第三步验证工具调用:让Agent发一个需要调用工具的请求,比如"打开baidu.com并截图",然后看网关日志里有没有出现对应的工具调用记录。
这一步我建议开debug级别的日志。网关会把每次请求的路由决策、目标地址、响应耗时全部打印出来。看日志时有个窍门:不要只看成功还是失败,要看耗时分布。如果某个MCP Server的调用时间从100毫秒突然涨到2秒,那大概率是连接出现了问题,即使请求最后还是成功了。
4.4 手动挂载一个GitHub上的Skills并跑通
以Claude Code手动装GitHub上的Skills为例子,我分两条路径讲一下。
不带网关的方式:把Skills仓库克隆下来,把SKILL.md复制到项目的.skills目录下,Claude Code启动时会自动扫描这个目录。这种方式确实能用,但你有几台机器就要复制几份,而且更新是个麻烦事。
带网关的方式:先在tsm-hub的skills.yaml里注册这个Skill的来源仓库地址和版本号,然后在Claude Code的配置里,把MCP端点指向tsm-hub暴露出来的地址。Claude Code启动时,通过MCP协议的Prompts原语,自动从网关拉取并注册这个Skill。这种方式下,Skill的版本由网关统一管理,多个Claude Code实例拿到的是同一个版本,不会再出现"我这台机器上是旧版"的情况。Superpower Skills这类社区包就特别适合这种方式,它本质上一堆SKILL.md文件的集合,挂在网关里变成一个统一来源。
很多人在这一步卡住,发现装了Skill之后模型根本不触发。我的排查经验是:绝大多数情况下不是网关的问题,是SKILL.md的描述写得不够清晰。模型只有在判断这个Skill和当前任务匹配时才会加载它,如果SKILL.md里全是模糊的套话,模型根本不知道什么时候该用。调试方法是临时打开调试模式,把模型内部的思考过程打印出来,看它有没有"考虑过"这个Skill。如果思考过程中完全没有提到,那就是Skill描述的问题。
5. 常见问题与排查技巧实录
5.1 工具找不到或者调用失败的排查路径
工具找不到,先别急着看代码,按这个顺序排查:先确认网关/tools端点里有没有这个工具,没有就是注册或前缀问题;有的话确认Agent传的工具名是否带对了前缀;再确认目标MCP Server是否在线;最后确认网关日志里那一次调用的耗时和返回码。
我遇到过最诡异的一次:工具在列表里,Agent也调用了,但Server没有响应。查了很久发现是Server端的工作线程被之前的某个长任务占满了,新的请求一直在排队。从那以后,我在网关里加了一个排队超时检测,如果任务在Server端排队超过3秒,就直接返回给Agent一个友好的降级提示,而不是干等着。
5.2 MCP握手失败的原因
MCP握手失败常见的几个原因:
| 原因 | 表现 | 解决办法 |
|---|---|---|
| 协议版本不兼容 | 握手阶段server返回错误 | 统一升级MCP SDK版本 |
| Session超时 | 长时间无请求后第一个请求失败 | 网关侧加心跳保活 |
| 鉴权方式不对 | 401或403 | 核对Header和Token格式 |
| 端点地址错了 | 连接被拒或404 | 检查Server真实监听地址 |
握手失败这个问题我要多说一句,很多人忽略了Server端的日志。MCP是双向交互,客户端会记录失败原因,服务端也会有日志。排查这类问题一定要两头都看,只看一头很容易误判。比如你看到客户端报"Session Not Found",以为是服务端重启了,实际上可能是网关侧把Session缓存清掉了,但它认定是上游问题。这种错判我至少遇到三次。
5.3 Token消耗异常的排查
有一个非常典型的场景:昨天Token用了100万,今天突然涨到300万,业务量明明没有变化。这种时候不要先怀疑用户量变化,先去网关日志里按调用方维度聚合看哪个Client消耗最大。
我碰到过一次,是一个测试脚本里写了循环调用,误把批量任务写成了串行,还漏了sleep,一晚上跑了上万个请求。如果没有网关的统一日志和按调用方统计,这个问题可能要等到账单出来才能发现。所以我会在网关里给每个调用方配一个"每日Token阈值",超过阈值自动告警,必要时直接限流。这个能力在任何分发的AI项目中都是刚需,不要等出问题才补。
5.4 模型回退与降级策略
模型回退是网关里最值得花时间调优的部分。我的配置策略是:每个模型绑定一个fallback链,比如"Claude Opus → Claude Sonnet → 本地Qwen"。触发回退的条件有三个:请求超时、返回5xx错误、或者连续3次返回格式错误。
有一点要特别注意:回退不等于无脑重试。如果上游返回的是400这种参数错误,回退也不会成功,因为错误在请求本身。所以我在网关里做了错误分类:只有可重试的错误才触发回退,业务性错误直接原样返回给调用方。这个逻辑很重要,否则网关会把一个本来就写错的请求转发到所有模型上,白白浪费Token。配置回退时,还要想想成本:Opus→Sonnet成本是降了,但如果Sonnet也失败了,再降一级到开源模型,响应质量能否接受,需要提前想清楚。
最后再分享一个我自己的体会,也算是个小技巧。搭完tsm-hub之后,我最大的收获其实不是少配了几个环境,而是我开始用"网关视角"去看整个AI应用架构。以前我关心的是"这个模型怎么调""那个工具怎么连",现在关心的是"流量该怎么路由""故障该怎么降级""成本该怎么控制"。当你不再被碎片的对接细节淹没,才有精力站在更高的层面去优化整个系统。这个视角的转变,可能比网关本身更有价值。如果你也被一堆MCP、Skills、模型Key搞得焦头烂额,不妨也试着搭一个自己的收口层,不用一上来追求大而全,先把模型路由和工具纳管做起来,你就能感受到差别了。