模型路由器与MCP网关融合架构:从工具调用到统一路由的实践

📅 2026/8/27 4:01:58
模型路由器与MCP网关融合架构:从工具调用到统一路由的实践
模型路由器要解决的问题从一开始就不只是“把一个请求转发给某个大模型”这么简单。多个模型、多个渠道、不同的成本和延迟、不同的限流和故障表现都要收在一个入口后面统一处理。而这两年 AI 应用里真正把请求链路拉长的其实是另一个东西工具调用。大模型要查数据库、要读当天文档、要调内部系统都需要外部工具MCP 网关要解决的正是工具这一侧的统一注册、统一授权和统一转发。所以当有人提出“模型路由器应该同时提供一个 MCP 网关”这个判断时我的态度很明确方向是对的而且越早按这个思路设计后面要填的坑越少。下面把我的实测过程、架构取舍和排查顺序完整拆一遍。1. 先把“模型路由”和“MCP 网关”放到同一条请求链路里看1.1 模型路由器原本管什么模型路由器本质上是一个位于客户端和模型服务之间的分发层。常见的职责包括统一入口客户端不用关心背后到底接的是哪家模型。管理多个 API Key按模型或按业务方隔离。根据延迟、成本、任务类型做路由例如简单问答走便宜模型复杂推理走强模型。主模型超时或报错时自动切换备用模型。做限流、审计和调用日志。处理不同厂商 API 格式之间的差异比如把 OpenAI 兼容格式转成其他厂商的格式。这些能力很成熟很多开源项目和个人工具都在做。但注意一个细节模型路由器的核心决策只发生在“用户请求该发给哪个模型”这一步。请求发给模型之后如果模型说“我需要调用一个工具”接下来发生的事就不再属于模型路由器的管辖范围了。1.2 MCP 网关补的是工具调用这一半MCP 的全称是 Model Context Protocol它把大模型需要的外部能力统一成三类东西工具、资源和提示词模板。工具对应函数调用资源对应文件或数据库内容提示词模板对应会话预设。日常用得最多的是工具所以很多人会把 MCP 直接理解成“给大模型接外部工具的协议”这个说法不完整但方向上没错。MCP 网关要管的不是某一次具体的工具调用而是所有工具调用的公共部分注册并发现可用的 MCP 服务端。聚合各服务端提供的工具列表并组装成模型能理解的 JSON Schema。处理客户端和服务端之间的授权、超时、重试。把模型的工具调用请求转发给正确的 MCP 服务端。把执行结果按协议要求返回给模型。如果项目里没有这个网关层每个业务方都得自己处理“连哪个 MCP 服务端、怎么鉴权、工具调用失败了怎么重试”这些问题。一个业务方还好三个业务方、五个 MCP 服务端一起接入就会乱成一团。1.3 为什么非要合成一个入口你可以把模型路由和 MCP 网关做成两个独立服务中间用 HTTP 通信。技术上完全可行但实际使用时会发现几个很别扭的地方。第一一次带工具的请求其实是一个循环模型决定调用工具工具执行完结果要送回模型模型再生成下一段回复。这个循环里模型服务和工具服务是交替出现的。如果模型路由器在 A 系统MCP 网关在 B 系统客户端要先请求 AA 再请求 BB 再把结果还给 AA 再发给模型。多一跳就多一次超时风险也多一层排查难度。第二路由决策依赖上下文。模型路由器决定用哪个模型时通常已经掌握了用户的完整对话历史。而工具路由也需要看上下文比如用户问“帮我查一下昨天订单”此时应该把请求路由到订单 MCP 服务端。如果两个系统分开工具路由只能拿到孤立的调用请求缺少对话上下文很难做准确判断。第三格式转换本来就应该放在路由器里。不同模型厂商的 tool_call 格式不一样OpenAI 兼容格式和 Anthropic 的 tool_use 结构就不同。模型路由器已经有了“转换厂商格式”的职责再让它顺手转换工具调用格式是顺理成章的。反过来如果在 MCP 网关里做转换意味着网关要同时维护多套模型格式的适配逻辑职责会越搞越重。所以我的结论是模型路由和 MCP 网关不是两个并列的系统而是同一条请求链路的两个阶段。放在同一个路由器里实现复杂度是加法而不是乘法。2. 一个合并架构要管住哪些对象2.1 两类注册表和两类路由决策合并之后路由器里至少要有两张注册表。一张是模型注册表记录每个模型的端点、认证方式、支持的功能、成本和延迟。这里要特别关注一个字段这个模型是否支持工具调用以及支持哪种工具调用格式。有些模型根本不支持 function calling如果你把工具定义硬塞给它轻则忽略重则报错。另一张是工具注册表记录每个 MCP 服务端的连接方式、传输类型、工具名称、工具描述、输入 Schema、所需权限和可用范围。有了两张注册表就对应两类路由决策模型路由这个请求该给哪个模型。工具路由模型要调用的工具应该由哪个 MCP 服务端执行。工具路由看起来简单因为工具名称是确定的。但实际多服务端接入后会出现同名工具。比如两个服务端都提供get_order_info一个查订单一个查内部工单。这时候工具注册表里必须给每个工具一个带命名空间的名字比如order_system.get_order_info和ticket_system.get_order_info否则模型调用时根本分不清是哪一边。2.2 一次带工具调用的完整请求长什么样把两条链路合并之后一次成功的请求大致走这些步骤客户端把对话请求发给路由器。路由器根据路由规则选择一个模型。路由器从工具注册表拉取当前业务方有权限使用的工具列表转成目标模型能识别的格式。路由器把用户消息和工具定义一次性发给模型。模型返回结果可能是普通回答也可能是工具调用请求。如果是工具调用请求路由器根据工具名称解析出对应的 MCP 服务端。路由器以 JSON-RPC 形式调用该服务端的tools/call方法并等待执行结果。路由器把工具执行结果组装成模型能理解的消息再次调用模型。模型基于工具结果生成最终回答路由器返回给客户端。这里看起来只是多了两三个步骤但每多一步就多一个出问题的位置。我建议第一次实现时不要着急优化性能而是先用一条能够打印完整日志的最小链路把它跑通。2.3 配置上至少要有这五类信息一个同时承担 MCP 网关职责的模型路由器配置项会比普通路由器多出一层。我的建议是把配置拆成下面五类分开管理配置类别包含内容典型字段模型配置端点、模型名、认证、供应商格式endpoint, api_key, format, supports_toolsMCP 服务端配置传输类型、地址、鉴权、连接数transport, url, auth, pool_size工具注册配置工具归属、命名空间、Schema、可见范围server, tool_name, input_schema, allow_list路由规则模型选择条件、工具选择条件、故障转移model_priority, tool_match, fallback观测配置日志、追踪、指标、审计trace_id, log_level, metrics_enabled模型配置和 MCP 服务端配置是基础设施改动的频率最低。工具注册配置和路由规则是日常要改的建议做成可热更新的配置源而不是写死在代码里。观测配置最好从第一天就打开不要等项目跑起来之后发现出问题没法定位再回头补日志。3. 最小闭环怎么跑通3.1 准备一个最简单的 MCP 服务端先不要接任何生产系统。我建议自己写一个只有一个工具的 MCP 服务端工具就做一件事返回服务器当前时间。这个工具足够简单便于确认链路是否真的通了。用 Python MCP SDK 的 FastMCP 可以这么写from mcp.server.fastmcp import FastMCP mcp FastMCP(demo-tools) mcp.tool() def get_server_time() - str: 返回服务器的当前时间。 from datetime import datetime return datetime.now().isoformat() if __name__ __main__: mcp.run(transportstreamable-http)传输类型用 Streamable HTTP因为生产环境基本不会用 stdio 去跑一个远程服务。stdio 比较适合本地调试后面我会专门说它的问题。SDK 版本不同启动参数可能略有差异实际以你安装的版本为准。3.2 在路由器里注册模型和工具路由器侧先做最小配置一个模型一个 MCP 服务端一个工具一条路由规则。下面是示意配置字段名不要求完全照抄关键是信息要全。router: models: - name: primary-llm endpoint: https://api.example.com/v1/chat/completions format: openai supports_tools: true mcp_servers: - name: demo-tools transport: streamable-http url: http://127.0.0.1:8000/mcp tools: - get_server_time routes: - id: demo-route match: modelprimary-llm tools: [demo-tools.get_server_time]注册完成后路由器可以先把tools/list拉一次确认能拿到工具定义再继续往下测。3.3 验证链路是否真的闭环单条测试这样问模型“现在服务器时间是多少”如果整条链路正常你会看到第一条模型响应里出现工具调用请求而不是直接编一个时间。路由器正确解析出工具名并调用demo-tools服务端。MCP 服务端返回真实时间。路由器把结果送回模型。最终回答里出现的时间与服务器真实时间一致。判断成功有两个标准。第一是结果正确第二是日志完整。如果你能在日志里看到“模型请求-工具调用-工具结果-模型二次请求-最终回答”这五个阶段最小闭环就打通了。3.4 先别急着上的三个功能最小闭环跑通之后很多人会急着加功能。我有三个建议先别上。第一不要马上做多模型自动故障转移。故障转移里的“判断失败”逻辑和工具重试逻辑会互相干扰先把单模型调稳。第二不要马上做工具语义路由。就是让路由器根据用户描述自动判断该用哪个工具看起来很聪明但准确率在你没有足够测试样本时很难保证。先用固定的工具命名空间映射。第三不要马上支持多工具并行调用。模型一次请求里可能声明要连续调用多个工具但同一个 MCP 服务端未必能安全处理并发。先串行执行稳定后再开并行。4. 真正要上生产时分散注意力的都是细节4.1 工具列表缓存和初始化时机MCP 协议的tools/list调用是有代价的。每有一个工具调用请求就实时拉一次工具列表会在高并发下拖垮服务端。生产环境应该做缓存。缓存策略我一般这么定启动时先拉一次作为基线。设置一个合理的 TTL比如 60 到 300 秒。MCP 服务端重启或版本更新时主动失效缓存。如果某个工具调用失败提示“工具不存在”先刷新缓存再判断而不是立刻报错。还有一个容易被忽略的问题工具列表会占用模型请求的上下文窗口。每个工具定义的 JSON Schema 都要随请求发给模型。假设你有 20 个工具每个工具描述很长光工具定义可能就吃掉几千 token。生产环境一定要给每个业务方配工具可见范围不要把所有工具一股脑发给所有模型。4.2 超时、重试和结果大小限制模型调用超时和工具调用超时是两件事必须分别配置。模型超时通常设得长一些因为大模型生成本身就慢。工具超时要根据工具类型单独设比如查数据库可能 2 秒生成报表可能要 30 秒。重试更要谨慎。只读类工具失败后重试是安全的但写操作类工具比如下单、发消息、删数据重试可能导致重复执行。正确做法是只读工具允许超时重试例如 2 次。写工具不允许自动重试只记录失败并返回给模型。如果服务端支持幂等键可以在调用里带上否则不要盲目重试。工具返回结果的大小也要限制。MCP 服务端可能返回一个几 MB 的报表模型上下文根本放不下。路由器要对结果做截断或摘要再接回模型。我一般会在路由器里配一个缓存结果上限超过上限的工具结果直接截断前 N 个字符并在日志里写清楚。4.3 流式响应下的调用顺序问题流式请求在工具调用场景下有个比较麻烦的地方。模型可能在流式输出中先吐出若干文字然后表示需要一个工具接着又继续流式输出。这听起来没问题但很多模型 API 的流式模式下工具调用信息未必会按照人类阅读顺序稳定出现。我的做法是路由器先流式转发普通文本一旦识别到工具调用标记就停止转发进入工具执行阶段。工具结果拿回来后发起第二次模型调用之后再把最终回复以流式方式转发给客户端。这里关键要处理客户端的模式切换。如果客户端正在等一个完整回复结果你中途停下来去调工具它可能会误判为超时。所以路由器需要支持某种形式的中间状态通知或者客户端在发起请求时就明确声明自己知道“可能发生工具调用”。4.4 观测路由决策本身要留痕普通 API 网关记录每次请求模型路由器只记录请求还不够要记录决策。也就是说日志里除了“调用了哪个模型”还要写清楚为什么选这个模型、这个工具调用是哪个规则匹配到的、超时和重试发生在哪一步。我习惯给每次路由决策一个单独的事件类型model_route_selected记录模型名称、路由规则 ID、预算约束。tool_route_selected记录工具全名、MCP 服务端、命名空间。tool_call_started和tool_call_finished记录耗时、结果大小、错误码。tool_call_failed记录失败类型、是否重试、是否幂等。有了这些事件接到用户反馈“某个回答不对”时就能快速定位到底是模型选错了还是工具调用失败但模型自己编了一个结果。后者是最危险的错误因为模型拿到工具失败的提示后可能强行编造一个看似合理的答案。5. 常见问题排查按这个顺序走5.1 模型看不到工具这是最高频的问题现象是模型没有发起工具调用或者把工具名当作普通文本输出了。排查顺序是先确认路由器能不能从 MCP 服务端取到工具列表。单独调用tools/list看返回是否为空。看工具 JSON Schema 是否合法。缺失required字段、description 为空、参数类型用错都可能让模型忽略该工具。看目标模型是否真的支持工具调用。有些模型的兼容接口支持 Chat但不支持 tool_calls。看工具可见范围配置。工具可能在注册表里但不在当前业务方的 allowlist 里。如果这些都没问题再检查格式转换逻辑。OpenAI 格式和 Anthropic 格式之间转换时最容易丢的是工具参数里的嵌套对象。5.2 工具调用超时或返回异常先看是哪种表现请求发出后长时间没有响应还是立即返回错误。长时间无响应优先看 MCP 服务端本身的日志和资源占用。尤其是服务端内部调用了外部依赖比如数据库慢查询或下游 HTTP 接口超时服务端不会立刻把错误返回给路由器。立即报错则看工具参数是否和 Schema 匹配。MCP 服务端是否已经注册了该工具。服务端返回的是协议错误还是工具业务逻辑错误。协议错误说明传输或格式有问题业务逻辑错误是服务端内部问题路由器按业务失败记录即可。5.3 多个 MCP 服务端之间的冲突最常见的冲突是工具重名。两个服务端都有get_balance一个是账户余额一个是库存余额模型选错就出大问题。解决方案是用命名空间伪装工具名例如account.get_balance和inventory.get_balance在路由器侧做映射表。另一个冲突是权限。A 服务端允许查询用户详细信息B 服务端不允许。如果路由器把所有服务端的工具混在一个列表里就要在工具级做权限过滤而不是服务端级。5.4 本地进程型 MCP 服务端的生命周期用 stdio 方式运行 MCP 服务端服务端会被路由器作为子进程拉起。这在本地开发时很快但生产上有几个隐藏坑。子进程崩溃后路由器要负责拉起否则工具列表会持续指向一个死进程。进程退出时没有释放端口和临时文件可能留下僵尸进程。如果路由器是容器化部署每个副本都会拉起一份自己的子进程资源消耗翻倍。所以生产环境我建议优先使用 Streamable HTTP 传输把 MCP 服务端作为独立服务部署。stdio 只保留给本地调试和单机小规模使用。6. 我的建议先当“工具路由问题”来设计6.1 从单模型单工具开始如果让我给一个实施顺序我会先做一个单模型、单工具、串行调用的版本把这个版本跑一个月积累真实的调用日志。然后加入第二个模型观察工具定义格式转换是否稳定。再加入第二个 MCP 服务端验证命名空间和权限隔离。最后才考虑并行调用、语义路由这些高阶能力。这个顺序慢但每一步失败时你都能准确判断问题出在哪一层。反过来一上来就接了五个模型、八个工具出问题根本无从下手。6.2 把工具路由决策也当作一等日志很多项目把模型路由日志做得很好工具路由却只有一句“tool called”。这是不够的。模型路由和工具路由是同一件事的两半日志级别、追踪方式、错误码体系应该完全对齐。前端排查问题时会看到一条完整的链路用户请求进入了路由器路由器选择了模型 A模型 A 请求调用工具 B工具 B 在服务端 C 执行失败重试后成功模型 A 生成了最终回答。只要有这么一条完整日志基本不用问人就能定位问题。6.3 后续值得扩展的方向闭环稳定后有几个方向值得深入研究。第一工具缓存。同一种语义的工具很多比如多个文档库都有全文检索路由器可以在路由规则里配置优先级减少重复开发。第二工具级限流和预算。一个模型调用可能绑定多次工具调用成本要能分摊到工具上才能知道哪个业务方消耗最多。第三工具结果缓存。对于“查询同一个用户基本信息”这类高频重复调用可以在路由器层做短时缓存减少 MCP 服务端压力也减少发送给模型的 token 数。最后说一句这类系统真正磨人的不是“让模型调用一个工具”这个 demo而是你如何保证工具调用失败时不会让模型胡编乱造如何保证高并发下工具调用不会拖垮服务端如何保证出了问题能在一分钟内从日志里看清整条链路。把这些做扎实模型路由器里的 MCP 网关才算真合格。