☰
从一句用户问题到 MCP Tool:商城 Agent 的 Java 路由治理
2026/10/5 4:35:03 网站建设 项目流程

很多 Agent 项目最容易跑通的做法,是把一组工具直接交给大模型:模型判断要不要调用工具、调用哪个工具,再生成参数交给程序执行。

做 Demo 时,这条链路很顺。但当工具后面连接的是真实订单、物流、设备和售后数据时,问题就不再只是“模型能不能选对工具”,而是:

这次调用究竟该不该发生?模型判断错了以后,系统有没有机会把它拦下来?

我在改造商城问答系统时,逐渐把这个问题落到了 Java 路由治理上。模型可以参与理解用户意图,但模型输出不能直接变成业务执行指令。在自然语言和真实业务能力之间,需要有一层确定性的 Java 代码,负责判断请求能去哪里、能访问什么资源,以及什么时候应该停下来。

不过,真正做完以后我发现,“路由”并不是一次简单的 Intent 分类。用户回复“第二个”时,要先恢复上一轮待办;“你能做什么”没必要调用模型;一个复合问题需要先拆分;模型给出的 Intent 还要经过置信度、白名单和资源契约检查;最后进入知识库或 MCP Tool 后,又有各自的执行路径。

因此,这个项目里的路由更像一条分层的决策链:

待办状态恢复 → 预路由 → 问题改写与拆分 → Intent候选识别 → Java路由治理 →资源契约授权→ 知识库或MCP Tool执行

这篇文章就沿着这条链路展开。参数补全、实体消歧、身份绑定和权限控制虽然会在架构中出现,但只说明它们与路由的边界,具体实现留到下一篇。

一、路由治理为什么会出现

这个商城项目最初是一个 RAG 问答系统,擅长回答商品介绍、退换货政策、配送规则等稳定知识。但实际用户还会问:

我上个月买的手表还在保吗?换屏多少钱?

要回答这个问题,系统需要找到用户购买的设备,再查询实时保修状态和备件价格。这些数据不会长期静态地放在知识库里,也不能让模型根据训练记忆自由生成。

于是系统中出现了两类数据来源:

  • 知识库负责稳定、公开的非结构化知识;

  • 业务工具负责实时、结构化或者与用户身份绑定的权威数据。

更准确的说法是:

RAG 负责稳定、公开的非结构化知识;Tool 负责实时、结构化或与用户身份绑定的权威数据。

当两种数据源同时存在,系统首先要解决的就不是“怎么检索”,而是“这个问题应该交给谁”。

二、先分清四个容易混淆的概念

在看架构之前,需要先分清 Intent、Route、Tool 和 Resource Contract。

1. Intent:用户想完成什么

Intent 属于业务语义层,例如:

ORDER_QUERY 查询订单 LOGISTICS_QUERY 查询物流 WARRANTY_STATUS 查询保修状态 RETURN_POLICY 了解退货政策

Intent 不是为了给句子贴一个好看的标签,而是为了给后面的业务分发提供依据。

2. Route:系统准备走哪类处理链路

Route 属于流程决策层。同一个用户问题最终可能进入:

  • 指定知识库检索;

  • MCP Tool 调用;

  • 系统固定处理;

  • 补充信息;

  • 公共知识库兜底;

  • 超出范围拒绝。

3. Tool:系统真正具备的执行能力

Tool 属于执行层,例如:

getOrder getLogistics getWarrantyStatus getSparePartPrice

4. Resource Contract:允许访问什么资源

资源契约就是这层映射关系。它把某个 Intent 转换成系统允许访问的资源:

Intent → ResourceKind:KB / MCP / SYSTEM → ResourceName:collectionName / mcpToolId / systemAction

例如:

Intent资源类型授权资源
RETURN_POLICYKBshopping-policy-kb
ORDER_QUERYMCPgetOrder
WARRANTY_STATUSMCPgetWarrantyStatus
ORDER_CHANGESYSTEM官方订单修改指引

这张表实际上就是商城 Agent 的能力边界。模型可以理解用户“像是想查物流”,但只有资源契约中存在且启用的 Intent,才能被转换成可执行路径。

三、完整路由架构

整个请求进入系统后,大致经过下面这些层次:

客户端请求 ↓ Sa-Token登录校验并建立UserContext ↓ 待办状态路由 ├─ 恢复工具依赖选择 ├─ 恢复实体候选选择 └─ 恢复缺失参数补充 ↓ 没有待办任务 PreRoutingPolicy预路由 ├─ AUTH_CONTEXT ├─ SYSTEM └─ 普通商城问题继续 ↓ 会话记忆 + Query Rewrite + 多问题拆分 ↓ IntentResolver输出候选Intent与置信度 ↓ RoutingGovernanceService进行最终治理 ↓ ResourceContractGuard生成AllowedRoute ↓ RetrievalEngine执行授权资源 ├─ 指定知识库 ├─ 公共知识库兜底 ├─ MCP Tool └─ SYSTEM处理 ↓ 组合KB Evidence与MCP Result ↓ 生成最终回答并记录Trace

这条链路里最值得注意的不是类名,而是每一层只回答一个问题:

层次回答的问题
Pending-State Routing这句话是不是在接着完成上一轮任务?
PreRoutingPolicy这个问题是否可以不经过模型直接处理?
IntentResolver这句话可能属于哪些业务意图?
RoutingGovernanceService这些候选是否可信,应该继续、兜底还是拒绝?
ResourceContractGuard最终允许访问哪个具体资源?
RetrievalEngine已经授权以后,具体怎么检索或调用?

把这些职责拆开以后,路由错误才有可能被定位,而不是所有问题最后都归结为一句“模型选错工具了”。

四、第一层不是 Intent,而是待办状态路由

正常情况下,我们会习惯性地把新输入送去意图识别。但在多轮任务中,有些输入根本不是一个完整的新问题。

例如上一轮系统说:

找到两台符合条件的手表,请选择第一台或第二台。

用户下一轮只回复:

第二个。

如果把“第二个”重新送去做 Intent 分类,模型很难知道它在说什么。它真正的含义保存在上一轮的待确认任务里。

因此,StreamChatPipeline在正常路由之前,会先尝试恢复几类 Pending State:

DependencyExecutionCoordinator.resume() → 是否正在等待工具依赖候选确认 ​ EntitySelectionClosedLoopService.resume() → 是否正在等待实体选择 ​ MissingParameterClosedLoopService.resume() → 是否正在等待缺失参数

只有不存在待处理状态时,请求才会继续进入普通问题路由。

这一步可以称为“会话状态路由”:它判断的不是用户属于哪个业务 Intent,而是当前输入属于一个新任务,还是上一轮任务的后续动作。

五、预路由:有些问题不值得调用模型

通过待办状态检查以后,系统进入PreRoutingPolicy。这一层使用纯 Java 规则,处理少量明确而稳定的问题。

例如:

“我的业务用户编号是什么” → AUTH_CONTEXT → 从服务端登录上下文回答 ​ “你是谁”“你能做什么” → SYSTEM → 返回固定能力说明

这类问题的答案已经存在于系统上下文中,让模型重新分类、检索甚至调用工具,不仅浪费资源,还可能让模型生成错误身份信息。

不过,预路由不应该无限扩张成一套关键词系统。它适合处理数量少、规则稳定、结果确定的特殊场景。普通的商城语义仍然交给后面的 IntentResolver。

六、Query Rewrite 的任务是整理问题,不是决定资源

进入正常路由以后,系统会结合会话记忆做 Query Rewrite,并把复合问题拆成相对独立的子问题。

例如:

查一下我的订单,再告诉我怎么退货。

可以拆成:

子问题1:查询我的订单 子问题2:退货政策是什么

子问题 1 最终可能进入getOrder,子问题 2 则进入shopping-policy-kb。如果不拆分,只给整句话分配一个 Intent,系统往往只能满足其中一个目标。

但 Query Rewrite 也有风险:改写后的句子可能偏离用户原始表达。为此,后面的资源治理不能只看改写结果,还需要保留原始问题作为纠偏依据。

七、IntentResolver:只负责寻找候选

商城 Agent 先根据商品、订单、物流、售后和设备等业务领域梳理能力,再细化成 24 个叶子 Intent。Intent 的划分依据不是表面词义,而是问题背后的数据来源和处理链路。

例如,“支持哪些配送方式”和“我的订单到哪里了”都与物流有关,但前者查询稳定政策,后者查询实时物流,因此会落到不同 Intent 和资源中。

运行时,IntentResolver把启用的叶子 Intent 提供给大模型,让模型针对每个子问题输出候选 Intent 和置信度:

[ {"id": "LOGISTICS_QUERY", "score": 0.93}, {"id": "ORDER_QUERY", "score": 0.42} ]

模型结果回来以后,Java 还会执行几项基础检查:

  • 丢弃系统中不存在的 Intent ID;

  • 丢弃低于配置阈值的候选;

  • 限制单个子问题保留的候选数量;

  • 区分正常解析、没有可信候选和分类异常。

最终得到的IntentResolutionResult大致有三类状态:

RESOLVED 得到可信候选 NO_TRUSTED 没有候选通过门槛 ERROR 分类过程发生异常

到这里仍然只是“候选识别”。IntentResolver 回答的是“这句话像什么”,并没有给模型最终工具调用权。

八、RoutingGovernanceService:不是重新分类,而是最终裁决

RoutingGovernanceService接收 IntentResolver 的候选结果,回答另一个问题:

当前候选是否可信,系统最终应该继续执行、进入公共知识库兜底,还是明确拒绝?

它首先处理几类确定性情况。

1. 表达明显不完整

例如用户只说“查一下”“看一下”,系统虽然知道他可能想办理某项业务,却无法判断具体目标。此时应该进入NEED_CLARIFICATION,而不是选择分数最高的工具碰运气。

2. 分类模型异常

模型调用失败、输出无法解析等情况会进入ROUTING_ERROR。技术异常不能伪装成用户问题,也不应该自动放宽路由条件。

3. 候选进入资源契约检查

剩余候选会交给ResourceContractGuard,转换成系统真正允许执行的AllowedRoute。

治理结束后,业务上主要有三种结果:

ROUTED → 存在可信且已授权的资源 GLOBAL_KB_FALLBACK → 没有叶子Intent,但仍可能是商城公共知识 OUT_OF_SCOPE → 超出系统承诺的能力范围

这里体现了一个很重要的设计原则:低置信度时,系统宁愿补问、受限兜底或拒绝,也不会为了给出答案而强行匹配一个业务工具。

九、ResourceContractGuard:把语义候选收敛成授权路由

ResourceContractGuard负责把最终 Intent 转换为AllowedRoute:

public record AllowedRoute( String subQuestion, String intentId, ResourceKind kind, String resourceName ) {}

其中:

kind → KB / MCP / SYSTEM resourceName → collectionName / mcpToolId / systemAction

对于同一个子问题,它会从候选中收敛出一个授权 Intent 和一个资源,而不是把所有候选都交给后续执行器。

这一层还保留少量确定性纠偏规则,用原始问题修正模型分类或 Query Rewrite 的漂移。例如:

“订单到哪了” → LOGISTICS_QUERY “订单详情、金额” → ORDER_QUERY “搜索不到、配对失败” → CONNECTION_NETWORK “单耳无声、频繁重启” → TROUBLESHOOTING “怎么设置、怎么开启” → USAGE_GUIDE

这些规则不是为了取代大模型重新实现一套分类器,而是针对业务中已经确认的高风险混淆点做确定性修正。

因此,几个核心组件可以这样概括:

IntentResolver = 找候选:这个问题像什么 RoutingGovernanceService = 做决策:候选是否可信,系统要不要继续 ResourceContractGuard = 做授权:最终允许访问哪个资源 RetrievalEngine = 做执行:具体怎么查知识库或调用工具

十、24 类 Intent 最终被路由到哪里

当前 24 个叶子 Intent 并不是 24 个工具,而是分成 15 个知识库 Intent、8 个 MCP Tool Intent 和 1 个 SYSTEM Intent。

1. 15 个知识库 Intent

知识库Intent
product-kbPRODUCT_INFO、PRODUCT_COMPARE、PRODUCT_RECOMMEND_KB、PRODUCT_COMPATIBILITY
shopping-policy-kbCOUPON_POLICY、INVOICE_POLICY、DELIVERY_POLICY、RETURN_POLICY
after-sales-kbWARRANTY_POLICY、REPAIR_SERVICE、SERVICE_CENTER
support-kbUSAGE_GUIDE、TROUBLESHOOTING、CONNECTION_NETWORK、CHARGING_BATTERY

这些 Intent 处理的是稳定、公开、适合通过文档检索的知识。

2. 8 个 MCP Tool Intent

IntentMCP Tool数据特点
PRODUCT_RECOMMEND_TOOLsearchProducts动态、结构化
PRODUCT_AVAILABILITYgetProductAvailability实时库存
COUPON_ELIGIBILITYgetCouponEligibility用户私有数据
ORDER_QUERYgetOrder用户私有数据
LOGISTICS_QUERYgetLogistics实时且与用户绑定
WARRANTY_STATUSgetWarrantyStatus设备实时状态
SPARE_PART_PRICEgetSparePartPrice动态、结构化
REPAIR_PROGRESSgetRepairProgress用户私有数据

这 8 个工具又可以分成两类:

公开但动态、结构化的数据 ├─ searchProducts ├─ getProductAvailability └─ getSparePartPrice 与当前用户身份绑定的私有数据 ├─ getCouponEligibility ├─ getOrder ├─ getLogistics ├─ getWarrantyStatus └─ getRepairProgress

3. 1 个 SYSTEM Intent

ORDER_CHANGE → 不调用修改订单工具 → 引导用户进入官方订单修改入口

这个设计明确表达了系统边界:当前 Agent 可以提供订单修改指引,但并没有获得修改真实订单的能力。

需要注意的是,这里的 SYSTEM Intent 和前面PreRoutingPolicy处理的“你是谁”并不完全相同。前者是 24 类业务能力中的一个正式资源路由,后者则是在正常分类之前处理的系统快捷路径。

十一、RetrievalEngine:获得授权以后才真正执行

完成路由治理后,RetrievalEngine根据AllowedRoute执行具体资源。

1. 指定知识库路由

如果某个子问题被授权为 KB 资源,会进入 Intent Directed Search,只检索当前 Intent 绑定的 Collection。

RETURN_POLICY → shopping-policy-kb → 向量召回 → 去重与重排 → 返回KBEvidence

先做 Intent 定向,再检索指定知识库,可以减少商品知识、售后政策和设备操作文档之间的相互干扰。

2. 公共知识库兜底

如果治理结果是GLOBAL_KB_FALLBACK,系统不会访问任意资源,而只在四个公共知识库白名单内并行检索:

product-kb shopping-policy-kb after-sales-kb support-kb

兜底的含义不是取消边界,而是在受限公共知识范围内给系统一次检索机会。订单、优惠券、维修进度等私有工具不会因为 Intent 没识别出来就被兜底调用。

3. MCP Tool 路由

如果AllowedRoute的类型为 MCP,执行器会根据 Intent 节点中固定的mcpToolId,到本地McpToolRegistry中精确查找工具。

WARRANTY_STATUS → mcpToolId = getWarrantyStatus → McpToolRegistry精确查找 → 获取该工具的输入Schema → 进入参数抽取与执行前门禁 → MCP Client发起tools/call

模型不能临时发明一个工具名,也不会在所有 MCP 工具中自由探索。Java 在进入执行层之前,已经把可见工具限制为当前授权资源。

这也解释了 Java 路由和 MCP 的关系:

Java 路由负责决定这次请求能不能调用工具、允许调用哪个工具;MCP 负责工具的描述、发现和远程调用。

MCP 是工具协议,不是业务路由器。

十二、复合问题怎样组合 KB 和 Tool

当前实现中,一个子问题基本只会对应一个资源:一个知识库、一个工具或一个 SYSTEM 处理器。

系统之所以能够处理“既需要知识库又需要工具”的问题,主要依赖 Query Rewrite 先把复合问题拆开。

例如:

“查一下我的订单,并告诉我怎么退货” ↓ 子问题1:查询我的订单 → ORDER_QUERY → getOrder 子问题2:退货政策是什么 → RETURN_POLICY → shopping-policy-kb ↓ 组合MCP Result和KB Evidence生成最终回答

这是一种可控的混合路由,但它不是让一个不可拆分的子问题同时自由访问多个知识库和工具。如果将来出现真正需要同一子问题多资源协同的场景,还需要显式扩展路由模型,而不能把当前能力说成通用的自由混合路由。

十三、工具依赖路由不是重新做 Intent 分类

有些工具在执行时缺少的参数,可以由另一个工具提供。例如:

“上个月买的手表还在保吗?” ↓ getOrder → 找到购买记录和设备SN ↓ getWarrantyStatus → SN只从上游可信结果绑定

这时的DependencyExecutionCoordinator做的是工具依赖规划:根据已经确定的业务目标,决定先调用哪个上游工具,再把可信结果传给下游工具。

它并没有重新判断用户属于什么 Intent,因此更准确地说,这是执行阶段的“依赖路由”,而不是语义分类路由。

如果上游找到多条可能的购买记录,系统会进入待确认状态;用户下一轮回复“第二个”时,又会回到最前面的 Pending-State Routing 恢复任务。这样,整条链路形成了一个闭环,而不是一次性的工具调用。

十四、哪些东西不应该都叫“路由”

项目中还有一批组件经常和路由混在一起:

ExactBusinessParameterFilter → 过滤不符合格式或来源要求的业务参数 MissingParameterGate → 检查必填参数是否完整 EntityResolver → 从可信业务数据中绑定实体并处理歧义 HuaweiMallRuntimeArguments → 删除模型生成的身份字段,注入服务端用户身份 McpToolRegistry → 只允许执行已经注册的工具

这些组件发生在工具已经选定以后,解决的是“这次调用是否具备安全执行条件”,因此更适合统称为工具执行门禁或参数与权限治理,而不是意图路由。

也就是说,完整链路可以被切成两个问题:

路由治理 → 该走知识库、工具、系统处理还是拒绝? 工具执行治理 → 工具已经确定后,参数和身份是否允许执行?

本文讨论的是前半段,后半段会单独展开。

十五、模型供应商路由也不是业务路由

项目中还存在RoutingLLMService、RoutingEmbeddingService和RoutingRerankService。它们名字里同样带有 Routing,但解决的是另一个层次的问题:

Chat / Embedding / Rerank请求 → 选择首选Provider和Model → 检查健康状态 → 失败时切换备用模型

这是基础设施层的模型容灾路由,不负责判断用户要查订单、物流还是知识库。

区分这两种 Routing 很重要:业务路由决定请求访问什么业务资源;模型路由决定某次 AI 能力由哪个供应商或模型承载。

十六、为什么不直接让模型 Function Calling

Function Calling 解决的是模型如何用结构化格式表达“我想调用这个函数”。它非常有价值,但它本身不等于完整的业务治理。

如果把全部工具直接交给模型,模型通常同时负责:

理解用户意图 → 选择工具 → 生成参数

开发速度很快,但三类错误也会耦合在一起。一旦调用失败,很难判断究竟是意图识别错了、工具描述不清楚,还是参数生成出了问题。

当前项目把链路拆成:

模型识别候选Intent → Java判断候选是否可信 → 资源契约确定唯一授权资源 → 模型针对指定Schema抽取普通参数 → Java完成参数与权限门禁 → MCP执行tools/call

它牺牲了一部分模型自由度,但换来了清晰的能力边界、可解释的调用路径和更容易回归的业务行为。因此,这个商城系统更准确的定位是受控业务 Agent,或者 Agentic Workflow,而不是强自治的 ReAct Agent。

十七、路由必须留下 Trace

最终回答错误,并不能说明错误一定发生在生成阶段。它可能来自 Query Rewrite 漂移、Intent 判断错误、资源契约配置错误,甚至公共知识库兜底范围不合理。

因此,一次路由至少应该记录:

  • 原始问题和改写结果;

  • 拆分出的子问题;

  • 候选 Intent 与置信度;

  • IntentResolver 的解析状态;

  • RoutingGovernanceService 的最终决策;

  • AllowedRoute 中的资源类型和资源名称;

  • 是否发生公共知识库兜底;

  • 进入澄清、拒绝或错误处理的原因。

例如,用户询问物流却调用了订单详情工具,可以沿着 Trace 逐层定位:

模型是否把它识别为ORDER_QUERY? → ResourceContractGuard是否发生错误纠偏? → LOGISTICS_QUERY是否被错误映射到getOrder? → RetrievalEngine是否取错了toolId?

这也是为什么 Agent 评测不能只看最终回答。对路由层来说,更重要的断言是:Intent 是否可信、资源是否正确、该补问时有没有停止执行,以及超出能力范围的问题有没有误调用工具。

十八、当前实现需要诚实说明的边界

工程设计不能只介绍理想结构,还要说明当前版本实际做到哪里。

1. IntentGuidanceService 当前不在有效主链上

它原本希望在同一子问题存在多个 KB 候选时,引导用户确认具体意图。但当前顺序大致是:

IntentResolver → RoutingGovernanceService → ResourceContractGuard将每个子问题收敛成一个候选 → IntentGuidanceService再检查多候选

等执行到 Guidance 时,多候选已经被收敛,因此这一模块在主链中很难真正触发。若要让它生效,更合理的顺序应该是:

IntentResolver → IntentGuidanceService → 用户确认 → ResourceContractGuard

或者专门保留一份未经裁剪的候选集合。

2. 单个子问题目前基本是单资源路由

当前ResourceContractGuard对一个子问题只保留一个 Intent,因此一个子问题基本对应一个 KB、一个 Tool 或一个 SYSTEM 资源。

系统能够处理 KB 与 Tool 的组合,主要依靠 Query Rewrite 拆分多个子问题,而不是让同一个子问题自由访问多个资源。这种方式更容易控制,但能力边界也需要如实说明。

3. 确定性纠偏规则需要持续维护

“到哪了”映射物流、“详情和金额”映射订单,这类规则对高风险混淆场景很有效,但它们依赖业务经验。如果规则不断膨胀,ResourceContractGuard也可能演变成另一个难以维护的分类器。

更合适的做法是只保留少量稳定、高价值的纠偏规则,并通过 Trace 和业务评测决定哪些规则值得进入主链。

十九、总结

回头看这套路由架构,它并不是在大模型外面简单加一个switch,而是把一次模糊的自然语言请求逐步收敛成一条确定、授权并可追踪的执行路径。

Pending-State Routing → 判断是不是上一轮任务的继续 PreRoutingPolicy → 处理无需模型参与的确定性问题 IntentResolver → 识别候选:用户的问题像什么 RoutingGovernanceService → 最终裁决:候选是否可信,系统是否继续 ResourceContractGuard → 资源授权:允许访问哪个KB、Tool或SYSTEM资源 RetrievalEngine → 执行授权:真正检索或调用

整套设计最终可以收束成一句话:

模型负责理解,Java 负责裁决;资源契约划定能力边界,MCP 负责连接工具,但只有通过路由治理的请求,才有资格进入工具调用链路。

下一篇将从McpToolRegistry继续向下:工具已经确定以后,如何根据 Schema 抽取参数,怎样区分意图澄清和缺参补问,怎样完成实体消歧、上游参数绑定与服务端身份注入,以及为什么模型生成的订单号、SN 和 userId 不能被直接信任。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询