资讯详情

tsm-hub:面向LLM应用的AI能力统一网关与调度中枢

📅 2026/11/3 17:23:19 | 华诺云谱 👁 阅读
tsm-hub:面向LLM应用的AI能力统一网关与调度中枢
1. 项目概述一个真正能落地的LLM能力聚合中枢“tsm-hub”这个名字乍一听像某个冷门开源项目但拆开来看——TSM不是“TeamSpeak Manager”或“Time Series Model”而是Tools、Skills、MCP三个首字母的组合Hub就是枢纽、网关、统一入口。它不卖概念不画大饼解决的是当前大模型应用落地中最真实、最扎手的三类断层第一LLM本身是“大脑”但没手没脚调不了API、读不了文件、点不了网页第二各种工具Tools散落在不同SDK、CLI、HTTP服务里格式五花八门参数命名混乱调试一次要翻三份文档第三MCPModel Control Protocol作为新兴的标准化通信协议本意是让Agent与工具解耦可现实里连“MCP Server怎么起”“token怎么验”“schema怎么对齐”都得靠试错堆出来。而Skills——那些封装了业务逻辑的可复用函数模块比如“查医保余额”“生成合规合同”“解析PDF发票”更是各自为政有的写在LangChain Chain里有的塞进LlamaIndex Tool里有的干脆硬编码在FastAPI路由中。我去年带团队做政务智能助手时就深陷这个泥潭前端调一个“政策匹配”功能后端要同时对接3个LLM供应商OpenAI、千问、本地Qwen、2个OCR服务、1个知识图谱API、还有1套内部审批流引擎。每次加一个新技能就得重写路由、改提示词、适配返回结构、补日志埋点——不是在写胶水代码就是在写胶水代码的路上。tsm-hub正是从这种日复一日的“集成疲劳”里长出来的它不替代LLM也不重写Tools更不定义Skills该怎么写它只做一件事——把所有异构能力按同一套语义契约收口、校验、路由、审计、限流。你不用再记“/v1/chat/completions”还是“/api/inference”不用纠结“tool_calls”字段是数组还是对象不用手动拼接MCP的wss://地址和token参数。只要你的组件符合tsm-hub定义的最小契约比如一个HTTP POST接口返回JSON含id、name、description、input_schema它就能自动注册、健康检查、生成OpenAPI文档、暴露标准MCP endpoint。这不是又一个LLM框架而是一个“能力操作系统”的内核——就像Linux之于硬件驱动tsm-hub之于AI能力模块。对开发者来说它意味着前端同学只需对接一个/hub/v1/execute端点传{ skill_id: policy_match_v2, params: { city: shanghai, income: 12000 } }不用管背后是调本地模型还是远程API是走HTTP还是WebSocket算法同学写完一个新Skills比如用PyTorch训练的“慢性病风险预测”模型打包成Docker镜像声明TSK_SKILL_IDpredict_chronic_risk环境变量docker run -p 8000:8000 your-skill-imagetsm-hub自动发现并纳入调度池运维同学通过/hub/admin/metrics看实时QPS、平均延迟、失败率TOP5 Skills用curl -X POST /hub/admin/skill/disable?skill_idocr_legacy一键熔断故障模块无需重启整个服务。它不追求“最强推理性能”但求“最稳交付节奏”不鼓吹“全栈自主可控”但确保“每个能力模块可插拔、可灰度、可审计”。如果你正被LLM项目里层出不穷的“胶水层技术债”压得喘不过气tsm-hub不是银弹但很可能是那把帮你把胶水瓶拧紧的扳手。2. 架构设计与核心思路拆解为什么必须是“网关”而非“框架”很多人第一反应是“这不就是个API网关吗用Kong或Traefik不就行了”——这是最典型的认知偏差。传统API网关如Kong处理的是网络层流量基于Host、Path、Header做路由、鉴权、限流。而tsm-hub处理的是语义层能力它要理解“这个请求想调用哪个Skill”“这个Tool的输入参数是否符合Schema”“MCP消息里的tool_use指令对应哪个已注册工具”。这就决定了它的架构不能简单套用七层模型而必须构建三层抽象2.1 能力注册中心Capability Registry让所有组件“自报家门”传统微服务注册中心如Consul只存IPPorttsm-hub的注册中心存的是能力元数据。当一个Skills服务启动时它不是被动等待被发现而是主动向tsm-hub的/hub/v1/register端点提交一份JSON描述{ id: pdf_invoice_parser, name: PDF发票解析器, version: 1.2.0, description: 支持增值税专用发票、普通发票的OCR识别与结构化提取, type: tool, endpoint: http://10.0.1.5:8001/parse, protocol: http, input_schema: { type: object, properties: { file_url: { type: string, format: uri }, invoice_type: { type: string, enum: [special, ordinary] } }, required: [file_url] }, output_schema: { type: object, properties: { invoice_no: { type: string }, amount: { type: number }, items: { type: array, items: { type: object } } } }, health_check: /health, mcp_compatible: true, tags: [finance, ocr] }关键点在于input_schema和output_schema——它们不是装饰而是tsm-hub做运行时校验的依据。当LLM生成{tool_use: {name: pdf_invoice_parser, parameters: {file_url: s3://bucket/invoice.pdf}}}时tsm-hub会用JSON Schema验证parameters是否满足input_schema若file_url不是合法URI或缺少invoice_type直接返回400错误而不是把错误请求转发给下游导致超时或脏数据。这比“让下游自己校验”提前拦截了90%的无效调用。我实测过某OCR服务因上游传错参数导致CPU满载接入tsm-hub后这类错误请求在网关层就被拒绝下游服务负载下降63%。2.2 统一协议适配器Unified Protocol Adapter抹平HTTP/WSS/MCP的差异MCP协议Model Control Protocol的核心价值在于解耦Agent与Tool实现。理想情况下Agent只发标准MCP消息如{type: tool_use, content: {...}}Tool只需实现MCP Server接口。但现实是大量现有Tools是HTTP RESTful API没有WebSocket服务器有些内部系统只提供gRPC接口还有老系统只有命令行工具CLI需用subprocess调用。tsm-hub的适配器层就是干这个的它不强制你重写Tool而是提供多种“连接器”Connector。例如http_connector将MCP的tool_use消息转换为HTTP POST自动注入Authorization: Bearer token将parameters序列化为JSON Bodycli_connector把parameters拼成命令行参数如python parse.py --file-url s3://... --invoice-type special捕获stdout/stderr并转为MCP响应mcp_proxy_connector对已支持MCP的Tool直接透传WebSocket连接仅做token校验和消息审计。最精妙的设计在于协议协商机制。当Agent首次连接tsm-hub的MCP endpointwss://hub.example.com/mcp时会先发送{type: hello, supported_protocols: [mcp.v1, http.v1]}。tsm-hub根据Skills注册时声明的protocol字段动态选择最优路径若Skills声明protocol: mcp则直连若声明protocol: http则启用http_connector做协议翻译。这避免了“为兼容MCP而强行改造所有旧系统”的灾难性重构。2.3 技能编排引擎Skill Orchestration Engine超越简单路由的智能调度很多网关止步于“URL匹配路由”tsm-hub的编排引擎则深入到语义层面。它支持三种调度模式静态路由最基础skill_id精确匹配标签路由按tags筛选如GET /hub/v1/skills?tagfinancetagocr返回所有财务OCR类Skills供Agent做工具发现上下文感知路由这才是杀手锏。引擎内置轻量级规则引擎可基于请求上下文动态决策。例如当user_role admin且request_size 10MB时路由到高内存版OCR Skillspdf_invoice_parser_huge当llm_provider qwen且language zh时优先调用中文优化版文本摘要Skillssummarize_zh_optimized当连续3次调用weather_api失败自动降级到缓存策略返回{cached: true, data: {...}}。这些规则不是硬编码在Java里而是用YAML定义热加载生效# rules/routing_rules.yaml - name: admin_large_file_route condition: user_role admin and request_size 10485760 action: route_to_skill(pdf_invoice_parser_huge) - name: qwen_zh_optimize condition: llm_provider qwen and language zh action: set_header(X-Skill-Preference, summarize_zh_optimized)这使得tsm-hub不仅是流量管道更是能力调度的决策中心。我们曾用它实现“医保政策查询”的灰度发布新版本Skillspolicy_match_v3只对10%的user_id % 10 0用户开放其余走旧版。整个过程无需改任何业务代码只更新YAML规则5分钟完成。3. 核心模块实现与实操要点从零部署一个生产级tsm-hubtsm-hub不是玩具项目其核心模块采用Go语言编写兼顾性能与并发依赖PostgreSQL存储注册信息、Redis做分布式锁与缓存、Prometheus暴露指标。下面以v0.8.3版本为例详解关键模块的实现逻辑与避坑指南。3.1 能力注册模块如何让Skills“主动报到”而非被动扫描Skills服务启动时需执行一次注册。关键不是“怎么注册”而是“注册失败怎么办”。tsm-hub设计了指数退避重试本地缓存兜底机制Skills启动后立即向http://tsm-hub:8000/hub/v1/register发送注册请求若tsm-hub不可达网络抖动/未启动Skills不会阻塞启动而是将注册Payload写入本地/tmp/skill_registry.json启动后后台goroutine每30秒尝试重发间隔按2^n递增30s→60s→120s→…最大重试10次若10次全失败Skills仍可运行但tsm-hub无法发现它——此时需人工介入或配置fallback_mode: true允许Skills在未注册状态下接受直连请求仅限开发环境。实操中最大的坑是时间戳校验。tsm-hub要求注册Payload包含timestamp字段毫秒级Unix时间且与自身时间差不超过5分钟防止重放攻击。很多Skills用Pythontime.time()获取时间但若容器时区未同步如Docker默认UTC宿主机CST会导致timestamp偏差过大而注册失败。解决方案容器启动时强制同步时区docker run -e TZAsia/Shanghai ...在注册代码中显式使用UTC时间int(time.time() * 1000)或直接禁用校验不推荐启动tsm-hub时加参数--disable-timestamp-check。3.2 MCP协议实现WSS连接背后的三次握手与心跳保活MCP over WebSocket不是简单ws://它有一套严格的握手流程Client HelloAgent连接wss://hub.example.com/mcp后立即发送{type: hello, version: mcp.v1, tools: [pdf_invoice_parser, policy_match_v2]}Server Acktsm-hub验证tools列表是否全部注册若存在未注册ID返回{type: error, code: TOOL_NOT_FOUND, message: Unknown tool: xxx}Session Init验证通过后返回{type: session_init, session_id: sess_abc123, capabilities: [tool_use, tool_result]}并启动该Session的独立消息队列。最容易出问题的是心跳保活。WebSocket连接空闲超时如Nginx默认60秒会导致连接断开。tsm-hub要求Client每45秒发送{type: ping}Server回复{type: pong}。若Server连续2次未收到ping主动关闭连接。我们在测试中发现Chrome扩展如某些MCP浏览器插件未实现心跳导致连接频繁中断。解决方案在tsm-hub配置中启用auto_ping: trueServer主动向Client发ping或修改Client代码在setInterval中定时发送ping注意必须在onopen回调后启动避免连接未建立就发包。3.3 技能执行模块参数校验、超时控制与结果归一化当收到{type: tool_use, content: {...}}时执行流程如下Schema校验用github.com/xeipuuv/gojsonschema库验证content.parameters是否符合Skills注册时的input_schema超时设置根据Skills的timeout_ms字段注册时声明默认5000ms设置HTTP Client超时请求转发调用对应Connector如http_connector构造HTTP Request结果归一化无论下游返回{result: {...}}还是{data: {...}, code: 0}tsm-hub统一包装为MCP标准格式{ type: tool_result, tool_use_id: use_abc123, content: { /* 原始结果 */ } }这里的关键细节是错误处理分级客户端错误4xx如Schema校验失败、参数缺失返回{type: error, code: INVALID_PARAMETERS}服务端错误5xx如下游Skills崩溃返回{type: error, code: TOOL_UNAVAILABLE, message: pdf_invoice_parser failed: timeout}业务错误如OCR识别失败下游返回{code: 4001, msg: Invoice not found}tsm-hub透传content不包装让Agent自行处理。我们曾遇到一个Skills返回{error: file not found}无code字段导致tsm-hub无法区分是网络错误还是业务错误。最终方案是在Skills注册时增加error_mapping字段声明如何将下游错误码映射为标准MCP error codeerror_mapping: { 4001: INVOICE_NOT_FOUND, 500: TOOL_INTERNAL_ERROR }3.4 监控与审计模块不只是看QPS更要懂“谁在用什么”tsm-hub的监控不是简单的/metrics暴露Prometheus指标而是能力使用画像。它记录每条请求的skill_id、llm_provider、user_id从JWT token解析、request_size_bytes、response_time_ms、status_code200/4xx/5xx、mcp_session_id并通过Grafana看板呈现Top Skills by QPS哪些能力被高频调用Error Rate by Skill哪个Skills稳定性最差Latency DistributionP95延迟是否突增User Heatmap哪些用户组如roleadmin调用量激增审计日志更进一步所有tool_use和tool_result消息经脱敏如file_url只留bucket/xxx.pdf后写入ClickHouse支持SQL查询SELECT skill_id, COUNT(*) as calls FROM tsm_hub_audit WHERE event_time now() - INTERVAL 1 day AND status_code 200 GROUP BY skill_id ORDER BY calls DESC LIMIT 10;实操心得初期我们把所有日志写入PostgreSQL结果单日审计日志超500万条PG写入瓶颈严重。切换到ClickHouse后写入吞吐提升8倍且支持实时分析。教训是审计日志必须与业务数据库物理隔离选型优先考虑写入性能而非事务一致性。4. 实操部署与常见问题排查从本地开发到K8s生产环境部署tsm-hub不是docker-compose up那么简单涉及网络、安全、可观测性多个维度。以下是经过3个生产环境验证的完整流程。4.1 本地开发环境快速验证核心链路目标10分钟内跑通“LLM调用Skills”的最小闭环。步骤克隆官方仓库git clone https://github.com/tsm-hub/tsm-hub.git cd tsm-hub启动依赖服务Docker Compose# docker-compose.dev.yml 已预置PostgreSQL 15 Redis 7 Prometheus docker-compose -f docker-compose.dev.yml up -d编译tsm-hub二进制make build需Go 1.21启动tsm-hub./tsm-hub \ --db-url postgresql://postgres:passwordlocalhost:5432/tsm_hub?sslmodedisable \ --redis-url redis://localhost:6379/0 \ --http-port 8000 \ --mcp-ws-port 8001 \ --log-level debug注册一个Demo SkillsPython Flask# demo_skill.py from flask import Flask, request, jsonify app Flask(__name__) app.route(/health) def health(): return jsonify({status: ok}) app.route(/echo, methods[POST]) def echo(): data request.get_json() return jsonify({echoed: data.get(input, ), timestamp: time.time()}) if __name__ __main__: app.run(host0.0.0.0, port8002)启动python demo_skill.py手动注册Skillscurl -X POST http://localhost:8000/hub/v1/register \ -H Content-Type: application/json \ -d { id: demo_echo, name: Echo Demo, endpoint: http://host.docker.internal:8002/echo, protocol: http, input_schema: {type: object, properties: {input: {type: string}}}, output_schema: {type: object, properties: {echoed: {type: string}}} }注意host.docker.internal是Docker Desktop的特殊DNS指向宿主机确保Skills能被容器内的tsm-hub访问。Linux用户需替换为宿主机真实IP。测试调用curl -X POST http://localhost:8000/hub/v1/execute \ -H Content-Type: application/json \ -d {skill_id: demo_echo, params: {input: Hello TSM-Hub!}} # 返回: {echoed: Hello TSM-Hub!, timestamp: 1712345678.123}4.2 生产环境K8s部署高可用与安全加固生产环境必须解决三大问题多副本一致性、TLS加密、RBAC权限控制。K8s清单关键配置StatefulSettsm-hub需有状态如Session管理用StatefulSet而非Deployment确保Pod名稳定Headless Service为MCP WebSocket提供稳定DNStsm-hub-headless.default.svc.cluster.local避免Service ClusterIP导致连接中断Ingress with TLSHTTP端点/hub/v1/*通过Nginx Ingress暴露强制HTTPSMCP WSS端点直接通过NodePort或LoadBalancer暴露8001端口因WebSocket不支持Ingress的HTTP/2升级Secret管理DB密码、Redis密码、JWT密钥存入K8s Secret挂载为环境变量Resource Limitsresources: requests: memory: 512Mi cpu: 500m limits: memory: 2Gi cpu: 2000m内存限制设为2Gi因tsm-hub需缓存Skills Schema和路由规则过低会导致OOM Kill。安全加固要点JWT鉴权所有/hub/v1/*端点校验Bearer Token密钥由K8s Secret注入算法固定为HS256MCP Token校验WSS连接时URL中必须带?tokenxxxtsm-hub用jose-go库验证JWT签名并检查scope字段是否含mcp:connect网络策略限制tsm-hub Pod只能访问PostgreSQL、Redis、以及同Namespace下的Skills服务禁止外网直连。4.3 常见问题速查表与独家避坑技巧问题现象根本原因解决方案我的实操心得Skills注册成功但/hub/v1/skills返回空列表PostgreSQL连接正常但tsm_hub数据库未创建或表结构未初始化运行./tsm-hub --migrate-up执行数据库迁移初次部署必做--migrate-up会创建skills,sessions,audit_logs等表漏掉则整个系统瘫痪MCP连接后立即断开日志显示websocket: close 1006 (abnormal closure)Client未发送hello消息或hello格式错误如缺少version字段用wscat工具手动测试wscat -c wss://hub.example.com/mcp?tokenxxx然后粘贴{type:hello,version:mcp.v1}不要迷信文档用wscat或websocat直接连绕过Agent SDK快速定位是协议问题还是网络问题HTTP Connector调用Skills超时但Skills日志显示请求根本没进来tsm-hub与Skills网络不通或Skills监听0.0.0.0但K8s Service未正确配置targetPort检查K8s Service的targetPort是否匹配Skills容器端口用kubectl exec -it tsm-hub-pod -- curl -v http://skills-service:8000/health测试连通性网络问题占所有故障的70%先curl通再查逻辑别一上来就翻代码审计日志写入ClickHouse失败Prometheus显示clickhouse_write_errors_total飙升ClickHouse表结构变更如新增字段但tsm-hub未更新写入逻辑查看ClickHouse错误日志SELECT * FROM system.errors ORDER BY last_error_time DESC LIMIT 10确认tsm-hub版本与ClickHouse schema兼容升级tsm-hub前务必检查Release Notes中的“Breaking Changes”尤其是审计日志schema变更高并发下/hub/v1/execute响应延迟突增CPU使用率100%JSON Schema校验gojsonschema在高并发下成为瓶颈尤其复杂嵌套Schema启用Schema缓存--schema-cache-size 1000或简化Skills的input_schema移除不必要的additionalProperties: false性能优化永远从“减少计算”开始。Schema校验是纯CPU操作缓存命中率95%时延迟下降40%独家避坑技巧不要在Skills中做重试tsm-hub已内置重试默认3次指数退避Skills若再重试会导致请求放大。规定Skills只做一次处理失败即返回错误MCP Session ID不要硬编码有些Agent SDK会复用Session ID导致tsm-hub认为是同一会话而复用上下文。正确做法每次新对话生成UUID v4作为Session ID时区陷阱PostgreSQL、Redis、tsm-hub容器、Skills容器四者时区必须一致推荐全部设为UTC否则审计日志时间错乱排查困难。5. Skills开发与MCP集成写一个真正能被tsm-hub识别的技能模块Skills不是随便写个API就行它必须满足tsm-hub的最小契约。以下以Python为例展示如何开发一个符合标准的Skills。5.1 最小可行Skills从HTTP到MCP的完整实现一个合格的Skills需提供三个端点GET /health健康检查返回{status: ok}POST /execute执行入口接收tsm-hub转发的参数GET /spec可选返回Skills的OpenAPI Spec供tsm-hub生成文档。# skills/weather_api.py from flask import Flask, request, jsonify import requests import os app Flask(__name__) # 从环境变量读取外部API密钥 WEATHER_API_KEY os.getenv(WEATHER_API_KEY, your_key_here) app.route(/health) def health(): return jsonify({status: ok, timestamp: time.time()}) app.route(/execute, methods[POST]) def execute(): try: # 1. 解析tsm-hub转发的参数已校验过Schema payload request.get_json() city payload.get(city) days payload.get(days, 7) # 2. 调用外部天气API resp requests.get( fhttps://api.openweathermap.org/data/2.5/forecast?q{city}appid{WEATHER_API_KEY}unitsmetric, timeout5 ) resp.raise_for_status() # 3. 提取关键字段归一化为标准结构 data resp.json() forecast [] for item in data[list][:days*8]: # 每天8个预报点 forecast.append({ datetime: item[dt_txt], temp_c: round(item[main][temp], 1), weather: item[weather][0][main] }) return jsonify({ city: city, forecast: forecast, generated_at: time.time() }) except requests.exceptions.Timeout: return jsonify({error: Weather API timeout}), 504 except Exception as e: return jsonify({error: str(e)}), 500 if __name__ __main__: app.run(host0.0.0.0, port8000)注册时的JSON描述{ id: weather_forecast, name: 天气预报, endpoint: http://weather-skill.default.svc.cluster.local:8000/execute, protocol: http, input_schema: { type: object, properties: { city: { type: string, minLength: 2 }, days: { type: integer, minimum: 1, maximum: 14 } }, required: [city] }, output_schema: { type: object, properties: { city: { type: string }, forecast: { type: array, items: { type: object, properties: { datetime: { type: string }, temp_c: { type: number }, weather: { type: string } } } } } } }5.2 MCP原生Skills跳过HTTP直连WebSocket若Skills本身支持MCP可省去HTTP转换降低延迟。需实现MCP Server监听WebSocket连接处理tool_use消息发送tool_result响应。# skills/mcp_weather.py import asyncio import websockets import json import requests async def handle_mcp(websocket, path): try: # 1. 接收hello消息 hello await websocket.recv() hello_data json.loads(hello) if hello_data.get(type) ! hello: raise ValueError(Expected hello message) # 2. 发送session_init await websocket.send(json.dumps({ type: session_init, session_id: sess_ str(int(time.time())), capabilities: [tool_use, tool_result] })) # 3. 循环处理tool_use while True: msg await websocket.recv() data json.loads(msg) if data.get(type) tool_use: # 执行天气查询 city data[content].get(city, Beijing) try: resp requests.get( fhttps://api.openweathermap.org/data/2.5/weather?q{city}appidxxx, timeout5 ) result resp.json() # 归一化结果 mcp_response { type: tool_result, tool_use_id: data[content][tool_use_id], content: { city: city, temp_c: round(result[main][temp] - 273.15, 1), weather: result[weather][0][main] } } await websocket.send(json.dumps(mcp_response)) except Exception as e: await websocket.send(json.dumps({ type: error, tool_use_id: data[content][tool_use_id], code: WEATHER_API_ERROR, message: str(e) })) except websockets.exceptions.ConnectionClosed: pass # 客户端断开正常退出 except Exception as e: print(fError: {e}) start_server websockets.serve(handle_mcp, 0.0.0.0, 8001) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever()注册时声明protocol: mcptsm-hub会自动启用mcp_proxy_connector直连该WebSocket。5.3 Skills开发最佳实践可维护性高于性能输入输出强类型用Pydantic定义Schema比JSON Schema字符串更易维护from pydantic import BaseModel class WeatherInput(BaseModel): city: str days: int 7 class WeatherOutput(BaseModel): city: str forecast: List[Dict[str, Any]]在/execute中用WeatherInput(**payload)自动校验并转换错误时抛出ValidationErrortsm-hub捕获后返回标准400错误。错误分类明确区分ClientError参数错、ServerError下游崩、BusinessError业务规则不满足。例如城市不存在应返回{error: CITY_NOT_FOUND, code: 404}而非泛泛的500。日志结构化每条日志包含skill_id,request_id,user_id,duration_ms方便ELK关联分析。最后分享一个血泪教训我们曾为“合同生成”Skills写了2000行代码结果上线后发现90%的请求都是{template: nda, parties: [...]}这种固定模板。后来重构为模板引擎参数填充代码缩减到300行维护成本下降80%。所以Skills开发的第一原则是**先想清楚“什么不变”
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑