尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

LLM、Tools、MCP、Skills 统一网关架构设计与落地实践

LLM、Tools、MCP、Skills 统一网关架构设计与落地实践 1. 为什么要把 LLM、Tools、MCP、Skills 塞进同一个网关第一次看到 tsm-hub 这个命名的时候我脑子里冒出来的第一个问题是这东西到底解决的是调用麻烦还是治理混乱后来自己动手把几个 Agent 项目从散装脚本重构成统一入口之后才真正理解这类网关的价值——它解决的核心矛盾是能力供给侧的碎片化和调用侧的标准化诉求之间的冲突。先说清楚这四个词各自代表什么不然后面全是空中楼阁。LLM是大语言模型本身负责理解意图、生成内容、做推理决策。它是大脑但大脑单独存在没有意义得有手有脚有记忆。Tools是工具是模型可以调用的外部函数或接口。查天气、读数据库、发邮件、跑代码这些都是 Tool。传统做法是每个 Tool 写一套适配代码模型换一个适配层就得重写一遍。MCP是 Model Context Protocol一套让模型和外部资源之间用统一协议对话的规范。你可以把它理解成AI 世界的 USB-C 接口——以前每个设备一个专用口现在统一了插上就能用。MCP Server 负责暴露资源MCP Client 负责消费资源。Skills是技能是比 Tool 更高一层的封装。一个 Skill 可能内部编排了多个 Tool、多轮 LLM 调用、若干条件分支对外只暴露一个能力单元。比如生成周报这个 Skill内部可能要读日历、拉任务列表、调 LLM 总结、再格式化输出。把这四样东西收进一个网关本质上是做三件事统一注册、统一路由、统一治理。统一注册解决东西散落在各处找不到的问题统一路由解决调用方不知道该找谁的问题统一治理解决谁在用、用了多少、出错了怎么办的问题。我踩过的最典型的坑是在一个项目里同时接了三个不同的模型供应商、七八个自研 Tool、两套 MCP Server结果配置文件散在五个地方改一个超时参数要翻半天。tsm-hub 这类网关要做的就是把这些配置收敛到一个地方让加一个能力变成改一段配置而不是改五处代码。提示网关不是万能胶。如果你的项目只有一两个 Tool、一个模型硬上网关只会增加复杂度。判断标准很简单——当你开始需要记住某个能力在哪注册的时候就该考虑收敛了。2. tsm-hub 的分层架构与核心模块拆解2.1 接入层请求进来之后先过哪几道关接入层是网关的门面所有外部请求第一站到这里。它要干的事比想象中多协议适配、鉴权、限流、请求归一化。协议适配这块因为 LLM 调用可能是 OpenAI 兼容格式也可能是 Anthropic 格式还可能是自定义的 HTTP 接口接入层得把这些差异抹平转成内部统一的数据结构。我一般会定义一个NormalizedRequest结构把 model、messages、tools、temperature 这些字段固定下来后面所有模块只认这个结构。鉴权不是简单的 API Key 校验。在多租户场景下你得知道这个请求属于哪个用户、哪个应用、有什么权限。我见过太多项目把鉴权做成一个全局 Key结果一个泄露全盘皆输。合理的做法是每个调用方一个凭证凭证绑定权限范围。限流要分维度。按用户限、按模型限、按 Tool 限粒度不同策略不同。LLM 调用贵且慢通常按 token 配额限Tool 调用快但可能打爆下游通常按 QPS 限。# 接入层请求归一化的简化示意 class NormalizedRequest: def __init__(self, raw): self.model raw.get(model) self.messages self._normalize_messages(raw) self.tools raw.get(tools, []) self.trace_id generate_trace_id() self.tenant resolve_tenant(raw) def _normalize_messages(self, raw): # 把不同供应商的消息格式统一成内部格式 # 这里是最容易出兼容问题的地方 ...2.2 路由层一个请求怎么找到它该去的地方路由层是网关的大脑。它要回答的问题是这个请求应该走哪个模型、调哪个 Tool、触发哪个 Skill。路由策略我一般分三级。第一级是显式路由请求里明确指定了目标直接转发。第二级是规则路由根据请求特征匹配规则比如包含代码相关关键词的走代码模型。第三级是智能路由让一个轻量模型来判断意图再决定去向。显式路由最稳但要求调用方知道目标。规则路由灵活但规则维护成本高。智能路由最省心但引入了额外延迟和不确定性。我的经验是核心链路用显式边缘场景用规则实验性功能才上智能路由。路由层还要处理降级。主模型挂了怎么办切备用模型。备用也挂了怎么办返回缓存结果或友好错误。这套降级链要在路由层配置好不能等到出事再临时加。2.3 能力注册中心LLM、Tool、MCP、Skill 怎么统一描述这是 tsm-hub 最核心的设计。四种能力形态不同但需要一套统一的描述语言来注册。我给每个能力定义一个CapabilityDescriptor包含唯一标识、类型llm/tool/mcp/skill、输入 schema、输出 schema、调用方式、依赖关系、配额策略。能力类型标识示例输入特征调用方式典型依赖LLMllm.gpt4omessages paramsHTTP 流式无Tooltool.weather结构化参数函数调用无MCPmcp.filesystem协议消息长连接MCP ServerSkillskill.weekly_report高层意图编排执行多个 Tool LLM统一描述的好处是上层调用方不需要知道底层是哪种类型只管按标识调用。加一个新能力注册一个描述符就行调用方代码不用动。2.4 执行引擎Skill 编排与 Tool 调用的调度逻辑执行引擎负责真正把请求跑起来。对于单个 Tool 调用就是转发加结果处理。对于 Skill就复杂了——它要按预定义的流程依次或并行地调用多个子能力。我设计 Skill 编排时用的是 DAG有向无环图思路。每个 Skill 定义成一个节点图节点是 Tool 调用或 LLM 调用边是数据依赖。执行引擎做拓扑排序能并行的并行有依赖的串行。这里有个容易忽略的点中间结果的传递。节点 A 的输出怎么变成节点 B 的输入我一般用变量绑定A 的输出存到一个命名变量B 的输入从这个变量取。这样 Skill 定义就是声明式的不写命令式代码。# Skill 编排的声明式定义示意 skill_weekly_report { name: skill.weekly_report, nodes: [ {id: fetch_tasks, type: tool, ref: tool.task_list}, {id: fetch_calendar, type: tool, ref: tool.calendar}, {id: summarize, type: llm, ref: llm.gpt4o, input: {messages: {{fetch_tasks.output}} {{fetch_calendar.output}}}}, {id: format, type: tool, ref: tool.markdown_format} ], edges: [ (fetch_tasks, summarize), (fetch_calendar, summarize), (summarize, format) ] }3. MCP 协议在网关里的落地细节3.1 MCP Server 的接入与生命周期管理MCP 的接入方式和普通 Tool 不一样。普通 Tool 是无状态的函数调用MCP Server 往往是有状态的长连接。这就带来生命周期管理的问题什么时候建立连接、什么时候断开、连接池怎么维护。我的做法是懒加载加心跳保活。第一次用到某个 MCP Server 时才建立连接之后维持一个连接池。定期发心跳确认存活连续几次失败就标记为不可用触发重连。MCP Server 的启动方式也分几种本地进程、远程服务、容器化部署。本地进程最简单但隔离性差远程服务隔离好但网络开销大容器化介于两者之间。我一般根据 MCP Server 的资源需求来选——轻量的本地跑重量的容器化。注意MCP Server 崩溃不会自动通知网关。必须有心跳机制否则网关会一直往一个死连接发请求直到超时才发现。这个坑我在生产环境踩过表现为偶发的请求卡顿排查了很久才定位到是 MCP 连接假死。3.2 工具发现与动态注册MCP 的 tools/list 怎么用MCP 协议有个很好的设计Server 可以声明自己提供哪些工具。网关启动时调一次tools/list就能拿到这个 Server 的所有能力自动注册到能力中心。这比手动配置强太多。手动配置的问题是Server 升级加了新工具网关不知道得人工同步。动态发现解决了这个问题——Server 说有什么网关就注册什么。但动态发现也有风险。如果 Server 返回了格式不对的描述或者工具名冲突网关得有容错。我的做法是发现阶段做校验不合规的工具跳过并告警不阻塞其他工具注册。# MCP 工具发现的简化流程 async def discover_mcp_tools(server): try: response await server.call(tools/list) for tool in response[tools]: if not validate_tool_schema(tool): log_warning(f跳过不合规工具: {tool.get(name)}) continue register_capability( typemcp, namefmcp.{server.name}.{tool[name]}, schematool[inputSchema] ) except Exception as e: log_error(fMCP 工具发现失败: {server.name}, {e})3.3 MCP 与原生 Tool 的差异处理MCP Tool 和原生 Tool 在调用方式上有本质区别。原生 Tool 是进程内函数调用快但耦合MCP Tool 是跨进程协议调用慢但解耦。网关要抹平这个差异让调用方感觉不到区别。我的做法是在执行引擎里加一层适配器原生 Tool 直接调MCP Tool 走协议。对上层来说都是调一个能力拿一个结果。性能上要有预期。MCP 调用比原生调用慢一个数量级是正常的因为多了序列化和网络往返。如果某个 MCP Tool 在热路径上得考虑缓存或者本地化。3.4 连接池与超时策略的实战配置MCP 连接池的配置我一般这么定最小连接数 1最大连接数按并发量估空闲超时 5 分钟请求超时 30 秒。请求超时这个值要慎重。太短正常请求被误杀太长故障时请求堆积。我的经验是设成 P99 延迟的 2 到 3 倍。如果 P99 是 10 秒超时设 20 到 30 秒比较合理。重试策略也要配。MCP 调用失败分两种可重试的网络抖动、临时过载和不可重试的参数错误、权限不足。可重试的做指数退避重试最多 3 次不可重试的直接返回错误。4. Skills 的设计哲学与编排实践4.1 Skill 和 Tool 的边界到底在哪这个问题我被问过很多次。我的回答是Tool 是原子能力Skill 是业务能力。Tool 是查天气这种输入城市输出天气逻辑单一。Skill 是帮我安排明天出行这种内部要查天气、查日程、查交通、综合判断、给出建议涉及多个 Tool 和多轮推理。判断标准如果一个能力需要多次 LLM 调用或者多个 Tool 协作才能完成它就是 Skill。如果一次调用就能搞定它就是 Tool。这个边界不是绝对的。有些 Skill 用久了发现逻辑固定可以下沉成 Tool。有些 Tool 用着用着发现需要加推理就升级成 Skill。架构上要允许这种演进。4.2 用声明式方式定义 Skill 的输入输出Skill 的定义我坚持声明式。为什么因为声明式的 Skill 可以被网关理解、校验、编排命令式的 Skill 就是一段黑盒代码网关插不上手。声明式 Skill 的核心是输入输出 schema。输入定义清楚需要哪些参数输出定义清楚会返回什么结构。这样网关可以在调用前校验参数调用后校验结果中间还能做类型转换。# Skill 声明式定义示例 name: skill.trip_planner description: 根据目的地和日期规划出行安排 input: type: object properties: destination: type: string description: 目的地城市 date: type: string format: date required: [destination, date] output: type: object properties: itinerary: type: array items: type: object properties: time: {type: string} activity: {type: string} steps: - id: weather ref: tool.weather input: {city: {{input.destination}}} - id: plan ref: llm.gpt4o input: messages: 根据天气 {{weather.output}} 规划 {{input.date}} 的行程4.3 Skill 内部的错误传播与部分失败处理Skill 编排最麻烦的是错误处理。一个 Skill 有五个步骤第三步失败了前面两步的结果怎么办整个 Skill 回滚还是部分返回我的策略是分级处理。关键步骤失败整个 Skill 失败返回明确错误。非关键步骤失败记录警告继续执行最后在结果里标注哪些部分降级了。比如出行规划 Skill查天气失败不是致命的可以跳过天气建议其他照常。但查日程失败就是致命的因为整个规划依赖日程。这个分级要在 Skill 定义里声明哪个步骤是关键、哪个是可选的。执行引擎按声明来处理。4.4 Skill 版本管理与灰度发布Skill 是会迭代的。今天 v1 的逻辑明天可能要改成 v2。直接改线上 Skill 风险很大得有版本管理。我的做法是 Skill 标识带版本号skill.trip_plannerv1和skill.trip_plannerv2并存。调用方指定版本不指定就用默认版本。新版本先给小流量灰度观察指标正常再全量。灰度怎么做在路由层按比例分流。10% 流量走 v290% 走 v1。对比两边的成功率、延迟、用户反馈没问题再调比例。5. 统一网关带来的治理能力5.1 全链路追踪一次请求到底经过了哪些环节网关最大的隐性价值是可观测性。所有请求都过网关意味着所有调用都有记录。这在排查问题时是救命稻草。我给每个请求分配一个 trace_id从接入层一路传到执行引擎、到每个 Tool 调用、到每次 LLM 请求。出问题时拿 trace_id 一查整条链路清清楚楚。追踪要记录什么请求参数、响应结果、耗时、状态、依赖关系。参数和结果要注意脱敏别把敏感信息记进去。耗时按环节拆能看出瓶颈在哪。# 全链路追踪的埋点示意 with trace(trace_id) as t: t.span(route, lambda: route_request(req)) t.span(execute, lambda: execute_capability(cap, req)) t.span(llm_call, lambda: call_llm(model, messages))5.2 配额与成本控制按用户、按模型、按 Skill 计量LLM 调用是要花钱的Tool 调用可能触发下游计费。网关得能算清楚每个维度花了多少。我一般按三个维度计量用户、模型、Skill。用户维度看谁在用模型维度看哪个模型烧钱Skill 维度看哪个业务成本高。配额策略分硬限和软限。硬限到了直接拒绝软限到了告警但放行。生产环境我建议关键资源用硬限防止单个用户打爆全局。成本控制还有个技巧是缓存。相同或相似的请求结果可以复用。LLM 调用尤其适合缓存很多请求其实是重复的。缓存命中率上去成本直接下来。5.3 熔断降级某个能力挂了怎么保证整体可用分布式系统里局部故障是常态。某个 MCP Server 挂了、某个模型供应商限流了网关不能让整个系统跟着挂。熔断器是标配。某个能力连续失败 N 次熔断器打开后续请求直接走降级逻辑不再尝试调用。过一段时间进入半开状态放少量请求试探成功就关闭熔断失败就继续打开。降级逻辑要提前设计。模型挂了降级到备用模型备用也挂了返回缓存或默认值。Tool 挂了返回空结果并标注。关键是让调用方知道结果是降级的别把降级结果当正常结果用。5.4 审计日志谁在什么时候调了什么合规场景下审计日志是刚需。谁、什么时候、调了什么能力、传了什么参数、拿到什么结果都得有记录。审计日志和追踪日志不一样。追踪日志偏技术用于排查问题审计日志偏业务用于合规审查。审计日志要长期保存追踪日志可以定期清理。审计日志的存储要注意隐私。参数和结果里的敏感字段要脱敏用户标识要可追溯但不能直接暴露。我一般存用户 ID 的哈希需要时通过映射表反查。6. 落地过程中的坑与调优经验6.1 冷启动延迟第一次调用为什么特别慢网关刚启动时第一次调用某个能力往往特别慢。原因可能是 MCP 连接还没建立、模型客户端还没初始化、缓存还是空的。解决办法是预热。网关启动后主动调用一遍所有注册的能力把连接建起来、客户端初始化好、缓存填上。这样第一个真实请求来的时候已经是热状态了。预热要注意别把下游打挂。启动时并发调所有能力可能瞬间给下游很大压力。我一般串行预热或者限制并发数。6.2 配置热更新改配置不重启怎么做生产环境不能随便重启。改个超时参数就重启网关影响太大。配置得支持热更新。我的做法是配置中心加监听。配置存在中心里网关监听变更收到通知就重新加载。加载时要注意原子性别加载到一半配置不一致。热更新还有个坑是状态。有些配置改了需要重建连接有些只需要改内存变量。得区分对待不能一刀切。6.3 多租户隔离不同团队的请求怎么互不干扰一个网关服务多个团队时隔离很重要。A 团队的请求不能影响 B 团队A 团队的配额不能占用 B 团队的。隔离分几个层面。资源隔离每个租户独立的连接池和线程池。配额隔离每个租户独立的配额。数据隔离每个租户只能看到自己的日志和指标。隔离的代价是资源利用率下降。完全隔离意味着每个租户都要预留资源空闲时浪费。折中方案是逻辑隔离加动态分配平时共享紧张时按优先级分配。6.4 性能压测网关本身的瓶颈在哪网关是中间层它的性能直接影响整体。压测是必须的。压测要分场景。纯转发场景测网关本身的开销带 LLM 调用场景测端到端延迟高并发场景测稳定性。我压测时发现的典型瓶颈是序列化。请求和结果都要序列化反序列化数据量大时这块开销不小。优化手段是换更快的序列化库或者减少不必要的数据拷贝。另一个瓶颈是日志。每个请求都写详细日志高并发时 IO 扛不住。解法是异步写日志或者采样写。7. 从零搭一个最小可用网关的实操路径7.1 技术选型为什么我选了这套组合搭网关的技术选型我最终选了 Python FastAPI Redis PostgreSQL 这套组合。Python 是因为 LLM 生态好各种 SDK 都是 Python 优先。FastAPI 是因为异步支持好适合 IO 密集的网关场景。Redis 做缓存和限流计数PostgreSQL 做配置存储和审计日志。为什么不用 GoGo 性能确实好但 LLM 生态弱很多 SDK 得自己封装。网关的瓶颈通常在 LLM 调用而不是网关本身用 Go 省下的那点性能意义不大。为什么不用现成的网关现成网关如各种 API Gateway解决的是通用转发问题不理解 LLM、MCP、Skill 这些概念。要支持这些得做大量定制不如自己搭。7.2 核心代码骨架注册、路由、执行三段式最小可用网关的核心就三段注册、路由、执行。注册是把能力登记到中心。路由是根据请求找到能力。执行是调用能力拿结果。# 最小网关骨架 class Gateway: def __init__(self): self.registry CapabilityRegistry() self.router Router(self.registry) self.executor Executor() async def handle(self, request): # 1. 归一化 req normalize(request) # 2. 路由 capability self.router.route(req) # 3. 执行 result await self.executor.execute(capability, req) return result这三段每段都可以独立扩展。注册支持更多能力类型路由支持更多策略执行支持更多编排模式。7.3 最小闭环验证跑通一个 Skill 调用搭好骨架后先跑通一个最简单的 Skill验证闭环。我一般用一个echo Skill 做验证输入什么经过一次 LLM 调用输出什么。这个 Skill 简单但覆盖了注册、路由、执行、LLM 调用全链路。跑通之后再加 Tool 调用、加 MCP 接入、加多步骤编排逐步复杂化。每加一个能力都验证一遍确保没破坏已有功能。7.4 上线前的检查清单上线前我会过一遍这个清单所有能力都注册了吗注册信息准确吗路由规则覆盖所有场景了吗默认路由是什么超时、重试、熔断都配了吗参数合理吗追踪和审计日志都开了吗脱敏做了吗配额和限流都生效了吗测试过吗降级逻辑都验证了吗降级结果能识别吗压测做了吗瓶颈在哪能扛多少 QPS监控告警配了吗关键指标有哪些这个清单看着简单但每一条背后都是踩过的坑。少一条上线后就可能出问题。8. 我对这类网关未来演进的一些个人判断网关这个形态我觉得会往两个方向走。一个是更薄。随着 MCP 这类协议标准化很多适配工作会被协议本身消化。网关可能退化成一个纯路由加治理层能力接入变得极其简单。另一个是更厚。网关会承担更多智能调度职责比如根据成本、延迟、质量自动选择模型根据上下文自动编排 Skill根据反馈自动优化路由策略。这两个方向不矛盾。薄的是接入厚的是调度。接入越简单调度能做的事情越多。我个人最期待的是 Skill 的生态化。现在 Skill 都是各写各的未来如果有一套标准Skill 能像 npm 包一样共享、组合、复用那整个 Agent 开发的效率会再上一个台阶。tsm-hub 这类网关很可能就是承载这个生态的基础设施。不过话说回来工具再好核心还是解决实际问题。我见过太多项目沉迷于搭框架最后业务没跑起来。网关是手段不是目的能解决问题、能降本增效才是它存在的意义。
返回列表