LLM、Tools、MCP、Skills 统一网关:tsm-hub 架构设计与实操 1. 为什么要把 LLM、Tools、MCP、Skills 塞进同一个网关第一次听到“tsm-hub”这个名字很多人会以为又是一个套壳的模型路由项目。但真正动手接过几个 Agent 项目之后你会发现问题根本不在模型调用本身而在于模型之外的那一大堆东西怎么统一管理。LLM 负责推理Tools 负责执行具体动作MCP 负责把外部能力标准化地接进来Skills 负责把一套可复用的行为模式打包起来——这四样东西如果各管各的项目稍微大一点就会变成一团乱麻。我自己的经历很典型。最早做 Agent 的时候工具调用是硬编码在业务逻辑里的每加一个能力就要改一遍主流程后来 MCP 协议火起来开始用 MCP Server 接外部工具结果每个 Server 的启动方式、鉴权方式、参数格式都不一样再后来团队开始沉淀 Skills发现 Skills 里引用的工具和 MCP 又是散落各处。到最后一个请求进来光是搞清楚“这次该用哪个模型、调哪个工具、走哪个 MCP、加载哪个 Skill”就要写几百行调度代码。tsm-hub 想解决的就是这件事把 LLM、Tools、MCP、Skills 这四类资源统一收进一个网关对外只暴露一套接口对内做统一的路由、鉴权、编排和可观测。你可以把它理解成一个“Agent 能力的中转站”——上游是各种客户端和业务系统下游是模型服务、工具执行器、MCP Server 和 Skill 仓库中间这层网关负责把请求翻译成正确的调用组合。这个定位决定了它适合谁。如果你只是写个 demo 调一次大模型 API那完全用不上但如果你在做多 Agent 协作、企业级知识库问答、或者需要把几十个工具和技能统一管理起来的产品那这层网关几乎是绕不开的基础设施。下面我按自己的理解把这套东西的设计思路、核心细节、实操过程和踩坑经验完整拆一遍。2. 四类资源的定位与统一抽象思路2.1 LLM、Tools、MCP、Skills 各自解决什么问题先把四个概念摆清楚不然后面统一抽象就是空中楼阁。LLM是推理核心负责理解意图、生成内容、决定下一步动作。它本身是无状态的输入一段上下文输出一段文本或结构化调用意图。在网关里LLM 的角色是“决策者”但它不该直接接触外部世界。Tools是具体的执行单元比如查数据库、发请求、算数学、读文件。一个 Tool 通常有明确的入参 schema 和出参格式。传统做法是把 Tool 定义写死在代码里问题是复用性差、版本管理混乱。MCP是一套标准化协议把外部能力以 Server 的形式暴露出来客户端通过统一协议去发现和调用。它的价值在于解耦——工具提供方和工具使用方不需要互相知道实现细节只要遵守协议即可。MCP Server 可以是本地的也可以是远程的这给了部署很大的灵活性。Skills是更高层的封装它描述的是“完成某类任务的一套流程”内部可能组合了多个 Tools 和 MCP 调用还带有提示词模板、参数约定和失败处理逻辑。你可以把 Skill 理解成“给 Agent 看的操作手册”。这四者的关系是层层递进的LLM 做决策Tools 做执行MCP 做标准化接入Skills 做流程封装。tsm-hub 的核心工作就是把这四层用一套统一的资源模型描述出来。2.2 统一资源模型把四类东西抽象成同一种“能力”统一网关最关键的一步是抽象。我的做法是定义一个通用的Capability能力描述结构四类资源都映射到它上面资源类型映射后的角色关键字段LLM推理能力model_id、context_window、cost、capability_tagsTool原子执行能力name、input_schema、output_schema、timeoutMCP Server能力集合入口endpoint、transport、auth、discovered_toolsSkill组合能力steps、required_capabilities、prompt_template这样抽象之后网关内部只需要处理一种东西——Capability。路由、鉴权、限流、日志、计费全部基于这套统一模型来做不用为每种资源写一套逻辑。这是整个项目能收敛复杂度的地方也是我踩过坑之后最想强调的一点抽象做对了后面所有功能都是顺水推舟抽象做错了每加一个功能都要改五处代码。2.3 为什么不做成“大而全”而是“统一入口”有人会问为什么不干脆把模型推理、工具执行、MCP 通信全部自己实现一遍我的答案是网关的职责是编排和治理不是替代。模型该用哪家就用哪家MCP Server 该谁写就谁写网关只负责把它们串起来、管起来、看得见。这个取舍背后有很现实的理由。第一模型和工具生态变化太快自己实现等于给自己挖坑第二网关层做薄出问题的时候定位范围小第三统一入口的价值在于“一致性”——一致的鉴权、一致的日志、一致的错误处理而不是把所有功能都吞进来。3. 核心细节解析路由、鉴权与编排怎么落地3.1 请求进来之后网关到底做了什么一个请求打到 tsm-hub内部大致走这么几步解析请求识别调用方身份、目标能力类型、请求参数。能力匹配根据请求意图从注册表中找到对应的 Capability。鉴权与配额检查确认调用方有权限且没超配额。编排决策如果是 Skill展开成步骤序列如果是单次调用直接路由。执行与聚合调用下游资源收集结果必要时做多步聚合。记录与返回写日志、算成本、返回统一格式的响应。这里面最容易出问题的是第 4 步。Skill 展开的时候如果步骤之间有依赖就需要一个轻量的执行引擎。我的做法是不引入重型工作流引擎而是用一个基于 DAG 的简单调度器每个步骤声明自己的依赖调度器按拓扑序执行。这样既够用又不会把网关搞得过于复杂。3.2 路由策略怎么决定用哪个模型、哪个工具路由是网关的核心能力之一。我的策略分三层静态路由根据请求里显式指定的能力 ID 直接匹配适合确定性场景。标签路由请求只声明“我需要一个支持长上下文的模型”网关根据 capability_tags 筛选候选。策略路由结合成本、延迟、可用性做加权选择适合生产环境做负载均衡和降级。举个实际例子。用户问一个需要读文件再总结的问题网关会这样路由先匹配到一个 Skill文件总结Skill 声明需要“文件读取 Tool”和“长上下文 LLM”网关分别按标签路由到具体实现。如果主模型不可用策略路由会自动切到备用模型整个过程对调用方透明。提示路由策略一定要可配置、可观测。我见过太多项目把路由逻辑写死在代码里结果线上出问题只能改代码重新发版。3.3 鉴权与多租户别等到出事才补统一网关天然是多租户场景鉴权必须一开始就设计好。我的方案是每个调用方分配一个API KeyKey 上绑定租户 ID 和权限范围。权限范围用Capability 白名单表达比如某租户只能用某几个 Skill 和 Tool。MCP Server 的凭证单独管理网关代理调用时注入不暴露给调用方。这里有个细节值得说MCP 的鉴权凭证绝对不能透传给客户端。我早期图省事把 MCP 的 token 直接放在响应里返回后来意识到这等于把下游系统的钥匙交给了调用方。正确做法是网关持有凭证代理调用调用方只拿到结果。3.4 可观测性日志、指标、追踪一个都不能少网关是流量的必经之路也是可观测性的最佳埋点位置。我关注三类数据数据类型采集内容用途日志请求参数、路由决策、执行结果、错误堆栈问题排查指标QPS、延迟分布、错误率、Token 消耗容量规划、告警追踪一次请求经过的所有 Capability 调用链性能分析、依赖梳理追踪这块特别重要。一个 Skill 可能触发五六次下游调用没有 trace_id 串起来排查问题就是大海捞针。我在网关里给每个请求生成唯一 trace_id所有下游调用都带上日志系统按 trace_id 聚合一眼就能看出瓶颈在哪。4. 实操过程从零搭一个最小可用的 tsm-hub4.1 环境准备与依赖选型先说技术栈。网关这类服务我倾向于选生态成熟、并发模型清晰的方案。Python 的 FastAPI 上手快、异步支持好适合快速验证如果追求极致性能Go 也是好选择。我这里以 Python 为例因为大部分做 LLM 应用的团队 Python 基础更好。核心依赖pip install fastapi uvicorn httpx pydantic pip install mcp # MCP 官方 SDK数据库用 PostgreSQL 存资源注册表和调用日志Redis 做配额计数和短期缓存。这两个都是成熟组件不折腾。注意MCP SDK 版本迭代较快建议锁定版本号避免协议不兼容导致连接失败。4.2 资源注册表的设计与初始化资源注册表是整个网关的“通讯录”。我用一张表统一存四类资源CREATE TABLE capabilities ( id VARCHAR(64) PRIMARY KEY, type VARCHAR(16) NOT NULL, -- llm / tool / mcp / skill name VARCHAR(128) NOT NULL, config JSONB NOT NULL, tags TEXT[], enabled BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT NOW() );config字段用 JSONB 存类型相关的配置。LLM 存模型名和参数Tool 存 schema 和超时MCP 存 endpoint 和 transportSkill 存步骤定义。这样一张表搞定四类资源查询和权限控制都简单。初始化的时候我写了一个注册脚本从配置文件批量导入def register_capability(cap_type, name, config, tags): cap_id f{cap_type}:{name} db.execute( INSERT INTO capabilities (id, type, name, config, tags) VALUES (%s, %s, %s, %s, %s) ON CONFLICT (id) DO UPDATE SET config %s, (cap_id, cap_type, name, json.dumps(config), tags, json.dumps(config)) )用ON CONFLICT DO UPDATE是为了支持幂等注册重启服务不会重复插入。4.3 MCP Server 的接入与工具发现MCP 接入是实操里最容易卡住的地方。核心流程是网关作为 MCP 客户端连接 MCP Server调用tools/list发现可用工具然后把它们注册成网关的 Tool 能力。async def connect_mcp_server(endpoint, transportstdio): if transport stdio: server_params StdioServerParameters(commandendpoint) else: server_params {url: endpoint} async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools await session.list_tools() for tool in tools.tools: register_capability( tool, fmcp_{tool.name}, {schema: tool.inputSchema, source: endpoint}, [mcp, endpoint] )这里有个关键点MCP 工具发现应该是启动时做一次还是每次调用时做我的经验是启动时发现并缓存同时提供一个手动刷新接口。每次调用都去发现会显著增加延迟而且大部分场景下工具列表是稳定的。4.4 Skill 的定义与执行编排Skill 的定义我用 YAML因为可读性好非开发人员也能看懂name: file_summary description: 读取文件并生成摘要 steps: - id: read capability: tool:file_reader input: path: {{ input.path }} - id: summarize capability: llm:long_context depends_on: [read] input: prompt: 请总结以下内容{{ steps.read.output }} output: {{ steps.summarize.output }}执行引擎按depends_on做拓扑排序逐步执行。变量替换用简单的模板语法{{ input.xxx }}引用请求参数{{ steps.xxx.output }}引用前序步骤结果。def execute_skill(skill, request_input): results {} for step in topological_sort(skill[steps]): resolved_input resolve_template(step[input], request_input, results) results[step[id]] call_capability(step[capability], resolved_input) return resolve_template(skill[output], request_input, results)这套编排逻辑不复杂但足够覆盖大部分场景。真需要复杂分支和循环的时候我会在 Skill 里加一个condition字段做简单判断而不是引入完整的工作流引擎。4.5 统一响应格式与错误处理网关对外必须返回统一格式否则调用方要写一堆适配代码{ trace_id: abc123, success: true, data: { ... }, usage: { tokens: 1024, cost: 0.003 }, error: null }错误处理我分了三类调用方错误参数不对、无权限返回 4xx下游错误模型超时、MCP 断连返回 502 并附带可重试标记网关内部错误返回 500 并记录完整堆栈。关键是错误信息要对调用方友好、对排查有用不能只返回一个“internal error”。5. 常见问题与排查技巧实录5.1 MCP 连接失败怎么定位MCP 连接问题占了实操问题的一半以上。我整理了一个排查顺序现象可能原因排查方法启动即失败命令路径错误手动执行 stdio 命令看报错连接超时网络不通或端口错telnet 测端口检查防火墙握手失败协议版本不匹配对比 SDK 版本和 Server 版本工具列表为空Server 未正确注册工具单独用 MCP 客户端连一次我的经验是先用官方 MCP 客户端单独连一次 Server确认 Server 本身没问题再排查网关侧。这样能把问题范围缩小一半。5.2 模型路由选错导致效果差有次线上反馈说回答质量突然下降排查发现是策略路由把请求切到了备用模型而备用模型能力弱很多。教训是降级策略要有明确的触发条件和告警不能静默切换。后来我加了规则主模型失败才降级且降级时打 warn 日志并触发告警运维能第一时间知道。5.3 Skill 执行超时与部分失败Skill 多步执行时某一步失败怎么办我的处理原则是关键步骤失败整个 Skill 失败返回错误。非关键步骤失败记录警告继续执行输出里标注缺失部分。超时每个步骤单独设超时总超时取各步骤之和加缓冲。这里有个坑不要用全局超时一刀切。一个 Skill 里读文件和调模型耗时差很多全局超时要么误杀慢步骤要么让快步骤等太久。5.4 配额与限流的实现细节限流我用 Redis 的滑动窗口按租户和 Capability 两个维度计数。这里要注意计数和实际调用的原子性——先计数再调用如果调用失败要回滚计数否则会误伤正常请求。我的做法是用 Lua 脚本保证原子性失败时显式减回去。提示限流阈值不要拍脑袋定先跑一段时间收集真实 QPS 分布再按 P95 或 P99 设阈值。5.5 日志里敏感信息泄露早期我把完整请求参数写进日志后来发现里面有用户隐私和 MCP 凭证。现在我的做法是日志脱敏在写入前做对已知敏感字段token、password、身份证等做掩码MCP 凭证根本不进日志。这个习惯一定要早养成等出事再改成本很高。6. 一些实操心得与后续扩展方向做 tsm-hub 这类网关我最大的体会是克制。功能上要克制不要什么都往网关里塞抽象上要克制不要为了通用而过度设计依赖上要克制能少一个组件就少一个。网关是基础设施稳定性比功能丰富重要得多。几个我觉得值得继续做的方向一是能力市场把注册的 Capability 做成可发现、可订阅的目录团队之间能复用二是A/B 路由同一能力注册多个实现按比例分流做效果对比三是成本归因把 Token 消耗和工具调用成本精确分摊到租户和业务线这对内部结算很有用。最后分享一个小技巧给每个 Capability 加一个健康检查接口网关定期探活不健康的自动从路由候选里摘除。这个机制上线之后下游抖动导致的失败率明显下降而且不用人工介入。