ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LLM、Tools、MCP、Skills 统一网关:Agent 系统资源治理与工程实践

LLM、Tools、MCP、Skills 统一网关:Agent 系统资源治理与工程实践 1. 为什么要把 LLM、Tools、MCP、Skills 塞进同一个网关第一次听到“tsm-hub”这个名字很多人会以为又是一个套壳的模型转发服务。但真正上手搭过 Agent 系统的人会立刻意识到它要解决的是一个非常具体的工程痛点当你的系统里同时存在多个大模型供应商、一堆本地工具函数、若干 MCP Server以及不断迭代的 Skills 提示词包时调用链会迅速变成一团乱麻。我最早做 Agent 项目时采用的是最朴素的做法业务代码里直接 import 各家 SDK工具函数写在一个 utils 目录里MCP 客户端单独起一个进程Skills 则以 markdown 文件的形式散落在仓库各处。项目小的时候没问题一旦要接入第二个模型供应商、或者要把某个工具同时暴露给三个不同的 Agent代码就开始失控。改一处工具签名要翻遍五六个调用点想给某个模型单独配置超时和重试策略发现配置项散落在三个配置文件里。tsm-hub 的核心思路就是把这四类东西抽象成统一的“资源”通过一个网关层统一注册、统一路由、统一鉴权、统一观测。LLM 是推理资源Tools 是本地可执行能力MCP 是外部协议化的能力来源Skills 是面向任务的能力编排单元。它们本质上都是 Agent 在完成任务时需要调用的东西只是形态和调用方式不同。把它们收进一个网关意味着上层业务只需要面对一套接口底层的差异由网关消化。这个设计带来的直接好处有三个。第一是可替换性今天用 A 模型明天想换成 B 模型做 A/B 测试只需要在网关配置里改一行业务代码零改动。第二是可观测性所有调用都经过同一个入口日志、耗时、token 消耗、错误率可以统一采集不用在每个 SDK 外面包一层埋点。第三是权限收敛哪些 Agent 能调用哪些工具、哪些 Skills 能访问哪些 MCP Server全部在网关层做策略而不是散落在业务逻辑里做 if-else。适合读这篇内容的人是那些已经过了“跑通 demo”阶段、开始认真考虑 Agent 系统可维护性的开发者。如果你还在纠结怎么让模型返回 JSON这篇可能偏深但如果你已经在为“工具越来越多、模型越接越杂、Skills 版本管理混乱”而头疼那接下来的拆解应该能帮你省下不少重构时间。2. 四类资源的抽象设计与选型考量2.1 LLM 资源为什么不做成简单的代理转发很多人对“LLM 网关”的第一反应是反向代理把请求转发给 OpenAI 或 Anthropic 的接口顺便做个 key 管理。但 tsm-hub 里的 LLM 资源抽象要更深一层。它需要处理的不只是 HTTP 转发还包括模型能力描述、参数归一化和降级策略。模型能力描述指的是网关需要知道每个注册的模型支持什么是否支持 function calling、是否支持流式输出、上下文窗口多大、是否支持图片输入。这些信息在路由时至关重要。比如一个请求带了 tools 参数网关就应该只路由到支持 function calling 的模型如果所有候选模型都不支持网关要能提前返回明确的错误而不是把请求发出去再等一个 400。参数归一化解决的是不同供应商 API 差异的问题。同样是“最大输出 token”有的叫max_tokens有的叫max_output_tokens有的放在顶层有的放在 generation config 里。如果让业务代码去适配这些差异那网关就白做了。tsm-hub 的做法是定义一套内部标准参数在适配层做双向映射。这样新增一个供应商只需要写一个适配器业务侧完全无感。降级策略是实际生产里最容易被忽略、但最不能省的部分。我踩过的坑是某个模型供应商在高峰期频繁超时但业务代码里没有做任何 fallback导致整个 Agent 链路卡死。在网关层做降级就优雅得多——配置一条规则主模型连续失败 N 次或响应超过 T 毫秒自动切到备用模型同时打点告警。业务代码完全不知道背后换了模型它只关心拿到结果。注意降级不是无脑切换。如果主模型和备用模型的能力差异较大比如一个支持 tools 一个不支持降级前必须做能力校验否则会出现“切过去之后请求直接失败”的尴尬情况。2.2 Tools 资源本地能力的注册与生命周期管理Tools 在 tsm-hub 里指的是本地可执行的能力单元通常是一个函数接收结构化参数返回结构化结果。听起来简单但实际管理起来有几个绕不开的问题。第一个问题是注册方式。最直接的做法是装饰器注册在函数定义处打标记启动时扫描收集。这种方式的好处是工具和实现在一起不容易漏坏处是工具的定义散落在代码各处想统一查看或批量修改很麻烦。另一种做法是配置文件注册工具实现和注册分离网关启动时根据配置去加载对应的模块。tsm-hub 采用的是混合模式装饰器负责标记元信息名称、描述、参数 schema配置文件负责控制启用状态和权限策略。这样既保留了开发时的便利又给了运维时的灵活。第二个问题是参数校验。LLM 生成的工具调用参数经常是“看起来对但实际有问题”的。比如一个查询天气的工具需要city和date两个参数模型可能返回{city: 北京, date: 明天}而你的实现期望的是 ISO 日期格式。如果不在网关层做校验错误会一直传到工具函数内部才爆出来排查成本很高。tsm-hub 在工具注册时要求提供 JSON Schema调用前先做 schema 校验不通过直接返回结构化错误给模型让模型有机会自我修正。第三个问题是超时与隔离。本地工具函数如果执行时间过长会阻塞整个调用链。更严重的是如果工具函数里有死循环或者内存泄漏可能拖垮整个网关进程。我的做法是给每个工具调用设置独立的超时并且对于不可信的工具比如用户自定义的 Skills 里引用的工具放到独立的 worker 里执行避免主进程被拖死。2.3 MCP 资源协议适配与连接池管理MCP 是这几年的热词但很多人对它的理解停留在“又一个工具调用协议”。实际上 MCP 的价值在于它把能力的提供方和消费方解耦了工具的实现可以是一个独立进程、一个远程服务甚至是一个完全不同的语言写的程序只要它说 MCP就能被 Agent 调用。tsm-hub 把 MCP 作为一种独立的资源类型来管理而不是简单地当成 Tools 的一种。原因是 MCP 的连接是有状态的。一个 MCP Server 可能维护着会话上下文、缓存、甚至长连接。如果每次调用都新建连接性能会很差但如果复用连接就要处理连接失效、重连、并发控制等问题。我在实际项目里遇到过 MCP Server 在空闲一段时间后连接被对端关闭的情况而客户端没有感知下一次调用直接报错。后来在网关层加了心跳检测和连接池健康检查才把这个坑填上。具体做法是每个 MCP Server 维护一个最小连接数和最大连接数空闲连接超过一定时间就主动探活探活失败则重建。同时对于同一个 Server 的并发调用网关要控制并发度避免把对端打挂。另一个容易被忽略的点是MCP 工具的动态发现。MCP Server 启动后它的工具列表可能不是固定的可能根据配置或运行时状态变化。网关需要在连接建立后拉取工具列表并缓存同时提供刷新机制。如果工具列表变了但网关没更新就会出现“模型调用了不存在的工具”这种低级错误。2.4 Skills 资源从提示词包到可编排单元Skills 是最容易被低估的一类资源。很多人把它等同于“一段系统提示词”但实际上一个成熟的 Skill 应该包含触发条件、所需工具、执行步骤、输出格式约束、以及失败处理逻辑。tsm-hub 把 Skills 设计成一种可编排单元它本身不直接执行而是描述“完成某类任务需要哪些资源、按什么顺序调用”。比如一个“查天气并生成出行建议”的 Skill会声明它需要天气查询工具和 LLM 推理能力执行时先调工具拿数据再把数据喂给 LLM 生成建议。这种设计的好处是 Skills 可以复用底层资源而不是每个 Skill 都自己实现一套工具调用逻辑。同时Skills 的版本管理也变得清晰每个 Skill 有独立的版本号网关可以根据请求上下文路由到不同版本的 Skill方便做灰度发布和回滚。提示Skills 的粒度控制很关键。太粗会导致复用性差太细会导致编排复杂。我的经验是按“用户可感知的完整任务”来划分比如“订机票”是一个 Skill“查询航班”是它内部调用的工具而不是另一个 Skill。3. 网关核心层的实现细节与关键代码3.1 统一资源注册表的设计网关的核心是一个资源注册表它需要同时管理 LLM、Tools、MCP、Skills 四类资源并且支持按名称、类型、标签等多种维度查询。我采用的是“类型 名称”作为唯一键每个资源注册时生成一个内部 ID外部调用通过名称或 ID 引用。注册表的数据结构大致如下class ResourceRegistry: def __init__(self): self._resources {} # {type: {name: ResourceMeta}} self._lock threading.RLock() def register(self, resource_type, name, meta): with self._lock: if resource_type not in self._resources: self._resources[resource_type] {} if name in self._resources[resource_type]: raise DuplicateResourceError(f{resource_type}/{name} already registered) self._resources[resource_type][name] meta def get(self, resource_type, name): with self._lock: return self._resources.get(resource_type, {}).get(name)这里用读写锁而不是普通锁是因为查询频率远高于注册频率。注册通常只在启动时发生而查询在每次请求都会触发。用RLock虽然简单但在高并发下会有性能问题。实际生产里我换成了读写锁读操作可以并发写操作互斥。资源元信息里除了基本的名称、描述、参数 schema还要包含健康状态和统计信息。健康状态用于路由时过滤不可用资源统计信息用于监控和告警。这些信息需要定期更新但不能每次查询都去探测所以采用“缓存 后台刷新”的模式。3.2 请求路由与能力匹配算法当一个请求进来时网关需要决定用哪个 LLM、哪些 Tools、哪个 Skill 版本。这个决策过程我称之为“能力匹配”。以 LLM 路由为例请求可能带有以下约束需要支持 function calling、上下文窗口至少 8k、首选供应商是 A、如果 A 不可用则用 B。网关的匹配逻辑是从注册表取出所有 LLM 资源按硬性约束过滤能力、窗口大小按优先级排序首选供应商优先检查健康状态剔除不可用资源返回第一个可用资源如果没有则返回明确错误这个过程看起来简单但实际实现时要考虑权重和负载。比如两个同等优先级的模型应该按当前负载做均衡而不是永远选第一个。我加了一个简单的加权轮询权重可以配置也可以根据实时延迟动态调整。Tools 的路由相对简单因为工具通常是按名称精确调用的。但有一种情况需要处理模型返回的工具名称可能带有命名空间前缀比如weather.get_current而注册表里存的是get_current。网关需要做名称归一化支持多种命名风格。Skills 的路由最复杂因为一个请求可能匹配多个 Skill。我的做法是给每个 Skill 定义触发条件关键词、正则、或者语义匹配请求进来后先做粗筛再用 LLM 做精排。粗筛用规则引擎快但不够准精排用模型准但慢。两者结合在延迟和准确率之间取平衡。3.3 鉴权、限流与可观测性埋点网关作为统一入口天然适合做鉴权和限流。tsm-hub 的鉴权分两层调用方鉴权和资源访问鉴权。调用方鉴权解决“谁在调用网关”的问题通常用 API Key 或 JWT。资源访问鉴权解决“这个调用方能不能用这个资源”的问题用策略表配置。比如 Agent A 只能调用工具 X 和 Y不能调用 ZSkill B 只能访问 MCP Server C。限流我采用的是令牌桶算法按调用方和资源两个维度分别限流。按调用方限流防止单个客户端打爆网关按资源限流防止某个热门工具被过度调用。令牌桶的参数速率、桶大小可以通过配置热更新不用重启网关。可观测性方面每个请求都会生成一个 trace ID贯穿 LLM 调用、工具执行、MCP 通信全过程。日志里记录 trace ID、资源名称、耗时、token 消耗、错误码。这些数据汇总到监控系统后可以画出调用链路图快速定位瓶颈。def handle_request(request): trace_id generate_trace_id() start time.time() try: resource route(request) result resource.invoke(request, trace_idtrace_id) log_success(trace_id, resource.name, time.time() - start) return result except Exception as e: log_error(trace_id, request.resource_name, time.time() - start, e) raise这段代码看起来简单但实际生产里要考虑异步、超时、取消等复杂情况。我的建议是初期先用同步模型跑通等稳定后再逐步引入异步。4. 实操从零搭一个最小可用的 tsm-hub4.1 环境准备与依赖安装先说明一下这里演示的是最小可用版本目的是让你理解核心机制而不是直接上生产。生产环境还需要考虑持久化、集群、高可用等那些后面再说。基础环境需要 Python 3.10 以上因为用到了match语法和一些新的类型标注特性。依赖方面核心的只有几个pip install fastapi uvicorn pydantic httpxFastAPI 用来做 HTTP 层Pydantic 用来做参数校验和 schema 定义httpx 用来做异步 HTTP 调用。如果你要接 MCP还需要安装对应的 MCP 客户端库具体看你的 MCP Server 实现。目录结构建议这样组织tsm-hub/ config/ resources.yaml policies.yaml src/ registry/ router/ adapters/ llm/ tools/ mcp/ skills/ server.py tests/配置和代码分离方便不同环境用不同配置。resources.yaml里声明有哪些资源policies.yaml里声明访问策略。4.2 注册第一个 LLM 资源假设我们要接入两个模型一个本地的 Ollama 模型一个远程的兼容 OpenAI 接口的服务。先定义适配器接口class LLMAdapter(ABC): abstractmethod async def chat(self, messages, toolsNone, **kwargs): pass abstractmethod def capabilities(self): pass然后实现 Ollama 适配器class OllamaAdapter(LLMAdapter): def __init__(self, base_url, model_name): self.base_url base_url self.model_name model_name async def chat(self, messages, toolsNone, **kwargs): async with httpx.AsyncClient() as client: resp await client.post( f{self.base_url}/api/chat, json{model: self.model_name, messages: messages, stream: False} ) return resp.json() def capabilities(self): return {function_calling: False, streaming: True, context_window: 8192}注册到网关registry.register(llm, local-ollama, { adapter: OllamaAdapter(http://localhost:11434, qwen2.5), capabilities: {function_calling: False, context_window: 8192}, priority: 10, tags: [local, free] })这里priority数字越小优先级越高tags用于策略匹配。注册完成后网关就知道有这个模型可用。4.3 接入一个 MCP Server 并暴露为工具MCP Server 的接入分三步建立连接、拉取工具列表、注册为网关工具。class MCPClient: def __init__(self, server_url): self.server_url server_url self.session None async def connect(self): # 实际实现取决于 MCP 传输方式stdio / SSE / WebSocket self.session await create_mcp_session(self.server_url) tools await self.session.list_tools() return tools async def call_tool(self, name, arguments): return await self.session.call_tool(name, arguments)拉取到工具列表后为每个工具生成一个网关侧的代理async def register_mcp_tools(registry, mcp_client, server_name): tools await mcp_client.connect() for tool in tools: registry.register(tool, f{server_name}.{tool.name}, { handler: lambda args, ttool: mcp_client.call_tool(t.name, args), schema: tool.inputSchema, source: mcp, server: server_name })这样 MCP 工具就和本地工具一样可以通过统一接口调用了。上层业务不需要知道这个工具是本地实现的还是远程 MCP 提供的。4.4 定义一个可复用的 SkillSkill 的定义我用 YAML 描述因为它比代码更直观也更容易做版本管理。name: weather_advice version: 1.0.0 description: 查询天气并生成出行建议 triggers: - keywords: [天气, 出行, 穿什么] steps: - id: get_weather type: tool tool: weather.get_current params: city: {{ input.city }} - id: generate_advice type: llm model: local-ollama prompt: | 根据以下天气数据给出一段简短的出行建议 {{ steps.get_weather.output }} output: {{ steps.generate_advice.output }}网关加载这个 Skill 后当用户输入包含“天气”关键词时就会触发这个 Skill。执行时按步骤调用工具和模型最后返回结果。注意Skill 里的{{ }}模板语法要小心注入问题。如果用户输入直接拼进 prompt可能被恶意利用。我的做法是对所有插值做转义并且限制插值长度。5. 踩坑记录与常见问题排查5.1 模型返回的工具调用参数格式不对怎么办这是最高频的问题。模型可能返回字符串形式的 JSON、可能多包了一层、可能字段名大小写不对。我的处理策略是三级容错第一级在 prompt 里明确要求 JSON 格式并给出示例。第二级解析时先尝试标准 JSON 解析失败则尝试提取代码块、修复常见错误比如单引号转双引号。第三级如果还是失败把错误信息返回给模型让它重新生成最多重试两次。实测下来加了三级容错后工具调用成功率从 85% 左右提升到 97% 以上。剩下 3% 通常是模型能力问题换更强的模型或者简化工具 schema 可以解决。5.2 MCP 连接频繁断开怎么排查先确认是网络问题还是对端问题。在网关侧加日志记录每次连接建立和断开的时间、原因。如果是对端主动断开看对端的日志和配置如果是网络问题检查是否有中间设备做了空闲超时。我遇到过一次是 MCP Server 部署在容器里容器有 60 秒空闲超时而网关的心跳间隔是 90 秒导致每次心跳前连接就被杀了。把心跳间隔改成 30 秒后问题消失。所以心跳间隔一定要小于对端的空闲超时最好留一半的余量。5.3 Skills 版本升级后行为不一致这通常是缓存导致的。网关为了性能会缓存 Skill 的定义如果升级后没有清缓存就会用旧版本。我的做法是给每个 Skill 定义加一个内容哈希请求时带上哈希哈希不匹配就重新加载。同时提供手动刷新接口升级后可以主动触发。另一个可能的原因是 Skill 依赖的工具或模型变了。比如 Skill 里引用了weather.get_current但这个工具升级后参数 schema 变了Skill 没同步更新。所以 Skill 升级时要检查依赖的资源是否兼容最好在 CI 里加一个依赖校验步骤。5.4 常见问题速查表问题现象可能原因排查方向解决方式请求超时模型响应慢 / 工具阻塞看 trace 各阶段耗时加超时、降级、异步化工具调用失败参数 schema 不匹配对比模型输出和 schema加校验、优化 promptMCP 连接断开心跳间隔大于对端超时看连接日志时间戳缩短心跳间隔Skill 不触发触发条件不匹配看粗筛和精排日志调整关键词或阈值鉴权失败策略配置错误看策略匹配日志修正策略表限流误伤令牌桶参数太小看限流统计调大速率或桶大小6. 生产化之前还需要补的几块最小可用版本跑通后离生产还有距离。我列几个必须补的模块按优先级排序。持久化。注册表目前是内存态重启就丢。生产环境需要把资源定义、策略、Skill 版本持久化到数据库或配置中心。但注意运行时状态健康状态、统计信息不需要持久化重启后重新探测即可。集群与一致性。单机网关有单点问题。多机部署时资源注册和策略变更需要同步。简单的做法是用配置中心推送复杂的做法是引入协调服务。我的建议是初期用配置中心 定期拉取够用且简单。灰度与回滚。Skills 和模型的变更要支持灰度。按调用方、按流量比例、按请求特征都可以做灰度维度。回滚要快最好能做到秒级。这要求版本管理足够清晰每个版本可独立寻址。成本控制。LLM 调用是花钱的网关要能统计每个调用方、每个 Skill、每个模型的 token 消耗和费用。更进一步可以设置预算和告警超预算自动降级到便宜模型或拒绝服务。安全审计。所有资源调用要有审计日志记录谁在什么时候调用了什么、传了什么参数、返回了什么。敏感参数要脱敏。审计日志保留时间根据合规要求定通常至少 90 天。这几块里我认为持久化和成本控制是最优先的。前者影响可用性后者影响钱包。其他的可以按业务节奏逐步补。7. 一些个人体会搭这套网关的过程中我最大的体会是抽象要适度不要为了统一而统一。LLM、Tools、MCP、Skills 确实有共性但它们的差异也很大。如果强行用一套接口抹平所有差异最后会得到一个又大又难用的抽象层。tsm-hub 的做法是统一注册和路由但保留各自的调用语义这个平衡点我觉得找得不错。另一个体会是可观测性要前置。我一开始觉得日志和监控是后期的事结果排查问题时两眼一抹黑。后来把 trace ID 贯穿全链路每个环节都打点排查效率提升了一个数量级。建议从第一天就把埋点加上成本很低收益很高。最后说一个具体的技巧给每个资源加一个“熔断开关”。当某个模型或工具出问题时能一键禁用而不是改配置重启。这个开关在事故处理时特别有用能快速止血争取排查时间。实现上就是一个布尔标志路由时检查一下成本几乎为零但关键时刻能救命。
返回列表