用户发送的消息,如何路由到正确的 Agent?
本文目录 · 30 节
星球:Java进阶之路&二哥编程星球 | 专栏:求职派 OpenClaw 架构 | 作者:沉默王二 | 发布:2026-08-04
用户发送的消息,如何路由到正确的 Agent?
用户发送的消息,如何路由到正确的 Agent?
用户在微信上发了一句“帮我看看有没有合适的 Java 岗位”,求职派需要快速做出判断:这条消息应该交给岗位推荐 Agent,还是岗位检索 Agent,还是身份采集 Agent? https://paicoding.com/jobclaw-routing-mechanism

一个多 Agent 系统的核心难题不是“每个 Agent 能做什么”,而是“用户发了一条消息,系统怎么知道该派谁上场”。
这个判断做错了,后面的一切都是白费功夫。
上一篇我们拆解了 LangGraph4j 在工作流内部的条件边路由,那是数据驱动的确定性分支。但在对话场景下,用户说的是自然语言,路由的第一步是理解意图。
这篇文章会拆解求职派的消息路由全流程。
一条 IM 消息从到达系统到找到正确的 Agent,中间经过了哪些判断?为什么要分三层做意图分类?会话绑定又省掉了多少次不必要的分类调用?
01、消息从 IM 到 Agent
用户在微信上发了一条消息,消息先经过渠道适配层转成了统一格式,然后通过 Spring 事件总线发送到消息路由器。
消息路由器是整个系统的调度中枢,收到消息后会依次执行七个判断步骤。

第一步,身份检查。 如果用户的画像信息不完整(没有技能、教育背景等),系统会拦截消息,把用户转到身份采集 Agent,先把画像补齐了再说。
第二步,系统命令检查。 消息是不是以 / 开头?如果是 /help、 /reset、 /agents 这类系统命令,直接交给命令分发器处理,不走后续的意图分类流程。
第三步,显式 Agent 切换。 用户如果发的是 /job-recommend 这样的命令,直接路由到对应的 Agent,跳过意图分类。
第四步,会话绑定检查。 当前会话是否已经绑定了一个 Agent?如果绑定了,而且没有过期,就继续用这个 Agent 处理消息,不需要重新分类。
第五步,意图分类。 前面四步都没有命中的消息,才会进入意图分类器。分类器会判断用户想做什么,是推荐岗位、检索岗位、查询进度,还是别的什么。
第六步,Agent 匹配。 拿到意图分类结果后,从 Agent 注册表里找到支持这个意图的 Agent,按优先级选一个。
第七步,绑定并执行。 把选中的 Agent 绑定到当前会话,然后执行 Agent 的处理逻辑,把结果返回给用户。
七步里有四步是“短路”操作,命中了就直接返回,不走后续流程。
这个设计的核心目标是尽量少调用大模型。
Java
// 身份不完整 → 拦截到身份采集
if (identityAgent.triggerToCollectIdentity(conversationInfo, msg)) {
return;
}
// 系统命令 → 直接执行
if (commandDispatcher.isSystemCommand(userMessage)) {
commandDispatcher.executeCommand(msg, conversationInfo, ...);
return;
}
core/.../router/MsgRouter.java,第 93-105 行
每一步的判断条件都是独立的,互不干扰。这让路由逻辑很容易测试,不需要跑完整个七步流程,单独测试任何一步都能拿到确定的结果。
02、意图分类
只有当会话没有绑定 Agent、或者绑定已经过期的时候,消息才会进入意图分类器。
意图分类器采用了三层递进策略,从快到慢、从免费到付费。
第零层,系统命令匹配
最高优先级。如果消息以 / 开头并且匹配到了已注册的系统命令,直接返回对应的系统意图,置信度为 1.0。
第一层,关键词加权匹配
系统预设了一张关键词表,每个关键词都有一个权重值。消息到达后,分类器会扫描消息里有没有命中关键词,取权重最高的那个作为分类结果。

举个例子,用户发了“投简历”,命中了“投简历”关键词(权重 1.0),分类器直接返回“岗位检索”意图,置信度 1.0。但如果用户发的是“实习”,虽然也命中了关键词,权重只有 0.5,分类器会标记为低置信度结果。
权重设计的思路很直观。越明确指向某个意图的词,权重越高。“投简历”只能是岗位检索,给 1.0。“实习”可能是在问实习岗位,也可能是在聊实习经历,给 0.5。
分类器根据权重返回不同的置信度等级。
-
0.9 以上,高置信度,直接采用
-
0.7 到 0.9,中置信度,也能用
-
0.7 以下,低置信度,交给下一层处理
Java
if (maxScore >= 0.9) {
return IntentClassificationRes.highConfidence(bestType, "关键词匹配");
} else if (maxScore >= 0.7) {
return new IntentClassificationRes(bestType, maxScore, "关键词匹配");
}
core/.../intent/classifier/KeywordIntentClassifier.java,第 131-138 行
关键词匹配不消耗任何 API 额度。求职派的实际运行数据里,超过 60% 的消息在这一层就完成了分类。
第二层,大模型分类
关键词匹配的置信度低于 0.7 时,才会触发大模型分类。
大模型分类器会把所有可用的 Agent 列表和用户消息一起发给大模型,让它判断用户的意图属于哪一类。返回的结果是一个结构化的 JSON,包含意图类型、置信度和推理过程。
Java
String prompt = PROMPT_TEMPLATE
.replace("{{BizAgent}}", agentListText)
.replace("{{message}}", message);
var response = simpleLlmCaller.call(
conversationInfo, new Prompt(prompt), IntentClassificationRes.class);
core/.../intent/classifier/LLMIntentClassifier.java,第 67-73 行
这里有一个安全约束。
大模型返回的意图类型不能是系统命令( /help、 /reset 这些)。如果大模型误判了,分类器会把结果降级为“未知”,防止大模型的分类结果和系统命令产生冲突。

三层策略的核心思路是能用规则解决的不用大模型,能用关键词解决的不用 Prompt。
03、会话绑定
意图分类解决的是“第一次不知道该找谁”的问题。但用户和 Agent 的对话往往是多轮的,推荐了一个岗位,用户追问“薪资多少”,接着又问“工作地点在哪”。
如果每条消息都重新做意图分类,不仅浪费时间和 API 额度,还会出现一个更严重的问题,上下文断裂。用户追问“薪资多少”的时候,如果系统把这条消息分类到了一个新的 Agent,新 Agent 根本不知道用户之前在聊哪个岗位。
会话绑定机制就是用来解决这个问题的。

当消息路由器第一次把某个 Agent 和一个会话绑定后,后续消息到达时,路由器会先检查绑定状态。如果绑定还没过期,就直接把消息交给绑定的 Agent 处理,跳过整个意图分类流程。
绑定的判断逻辑分四种情况。
-
用户发了
/reset→ 强制重新分类 -
当前会话没有绑定任何 Agent → 需要分类
-
绑定已过期(默认 6 小时) → 需要分类
-
以上都不满足 → 继续使用绑定的 Agent
绑定状态持久化在文件系统里,路径是 workspace/sessions/{userId}/session-{sessionId}.yaml,格式大概长这样。
YAML
sessionId: WeChat_123456789_user_id
boundAgentId: JobRecommendAgent
boundAt: 2026-08-03T10:30:45Z
expiresAt: 2026-08-03T16:30:45Z
6 小时的过期时间是一个经验值。太短了,用户中途去吃个饭回来就要重新绑定。太长了,用户第二天打开对话可能已经换了话题,还绑着昨天的 Agent 就不合理了。
系统还会记录每次意图分类的历史,最多保留最近 10 条。这些数据不参与路由决策,但在排查问题的时候非常有用。能看到用户这个会话里意图是怎么变化的,分类器每次给出的置信度是多少。

大部分多轮对话里,用户连续发的消息都属于同一个意图。会话绑定把这些消息的路由开销从“意图分类 + Agent 匹配”降到了“读一个 YAML 文件”。对于高频对话的用户来说,这能省掉 80% 以上的意图分类调用。
04、新 Agent 接入路由需要做什么?
路由机制再精巧,如果每次加一个新 Agent 都要改路由器的代码,那就不是一个好的架构。
求职派用的是自动注册模式。一个新的业务 Agent 只需要做两件事就能接入路由系统。
**第一件事,实现业务 Agent 接口并标注 @Component。**
接口约定了三个核心方法,分别是返回 Agent 的身份信息(ID、名称、描述)、处理用户消息、以流式方式处理用户消息。Spring 启动时会自动扫描到这个 Bean,注入到 Agent 注册表里。
Java
public interface BizAgent {
AgentIntro getAgentIntro();
String process(UserConversationInfo info, ChannelReceiveMessage msg);
Flux stream(UserConversationInfo info, ChannelReceiveMessage msg);
}
core/.../agent/BizAgent.java,第 22-75 行
第二件事,声明支持的意图类型。
接口有一个默认方法,用来返回这个 Agent 能处理的意图列表。路由器在做 Agent 匹配的时候,就是根据这个列表来筛选候选者。
如果多个 Agent 都支持同一个意图怎么办?
用优先级。
每个 Agent 可以覆盖优先级方法,路由器会选优先级最高的那个。默认 Agent(兜底用的)把自己的优先级设成了最低值,确保它只在没有其他候选者的时候才会被选中。

注册表的 Agent 查询还有一层权限过滤。不同角色的用户看到的 Agent 列表是不同的。普通用户只能用基础的推荐和检索 Agent,付费用户可以使用高级的订阅和计划 Agent。
Java
public List getAllAgents(String jobClawUserId) {
UserRoleEnum role = userService.getRole(jobClawUserId);
return agents.values().stream()
.filter(agent -> agent.permission().enabled(role))
.collect(Collectors.toList());
}
core/.../intent/impl/DefaultAgentRegistry.java,第 109-115 行
这个设计让路由系统变成了一个“即插即用”的插件架构。写一个类、标一个注解,重启之后新 Agent 就能被意图分类器发现、被路由器匹配。不需要改路由器的代码,不需要改配置文件,不需要在任何地方“注册”。
05、两套路由机制怎么分工?
求职派同时运行着两套路由机制,服务于完全不同的场景。
第一套是对话路由,处理用户从 IM 渠道发来的自然语言消息。路由的依据是意图分类,用关键词匹配和大模型来判断用户想做什么,然后把消息交给对应的 Agent。这套路由的结果带有概率性,因为自然语言本身就是模糊的。“实习”这个词,用户可能在问实习岗位,也可能在聊实习经历。
第二套是工作流路由,处理管理后台提交的岗位数据。路由的依据是业务规则,LangGraph4j 的条件边检查状态对象里的数据字段,字段为空就终止,不为空就流转到下一步。这套路由是完全确定性的,不存在“可能走 A 也可能走 B”的歧义。
上一篇文章详细拆解了工作流路由的实现,这里做一个对比。

两套路由机制的分工可以用一个问题来判断: 下一步该由谁来决定?
如果答案是“由用户的自然语言意图来决定”,用对话路由。消息路由器通过三层意图分类来理解用户想要什么,然后把消息分配给对应的 Agent。
如果答案是“由数据本身的状态来决定”,用工作流路由。LangGraph4j 的条件边检查上一步的输出是否合格,合格就往下走,不合格就提前终止。
两套机制在代码层面完全独立。
对话路由走消息路由器和意图分类器,工作流路由走 LangGraph4j 的图执行器。
它们共享的只有底层的数据层。工作流发布的岗位数据,最终会被对话路由里的岗位推荐 Agent 检索到并推荐给用户。
对话场景需要容忍模糊性,允许大模型猜测用户意图,猜错了还能通过会话绑定和 /reset 命令来修正。数据处理场景不能有任何模糊性,一条岗位信息该不该发布,不能让大模型来“猜”。
ending
【求职派的消息路由器用七步判断链来调度消息,三层意图分类(命令匹配→关键词加权→大模型兜底)平衡了速度和准确率,会话绑定机制省掉了 80% 以上的重复分类调用。】
路由做得好不好,用户感知不到。
用户只会觉得“我说什么它都能理解”,不会关心背后是关键词匹配还是大模型在分类。但路由做得差,用户每一条消息都能感知到。问推荐岗位却跳到了身份采集,追问薪资却丢掉了上下文。
如何把求职派写到简历上?
项目名称:求职派(JobClaw)—— 多 Agent 智能求职系统
项目简介:基于 Spring Boot 4 + Spring AI 2.0 + LangGraph4j 的多 Agent 求职平台,支持微信/钉钉/飞书三渠道接入,具备意图识别、岗位推荐、身份采集等对话型 Agent,以及基于工作流引擎的岗位数据自动采集流水线。
技术栈:Java 21、Spring Boot 4、Spring AI 2.0、LangGraph4j 1.6、JPA/Hibernate、H2/MySQL、React 19 / Next.js 15
核心职责:
-
设计并实现了多 Agent 系统的意图分类机制(系统命令精确匹配→关键词加权评分→大模型结构化分类)
-
基于文件系统实现了会话级 Agent 绑定机制,支持 6 小时 TTL 自动过期和手动重置,多轮对话场景下减少 80% 以上的重复意图分类调用
-
基于 Spring 组件扫描的 Agent 自动注册机制,新 Agent 只需实现接口并标注 @Component 即可接入路由系统,支持基于用户角色的权限过滤和优先级排序
-
基于 Spring 事件总线实现了异步消息路由器,通过七步判断(身份检查→命令匹配→显式切换→会话绑定→意图分类→Agent匹配→执行)调度消息
-
实现了对话路由与工作流路由的双架构方案,对话路由走意图分类处理用户自然语言,工作流路由走 LangGraph4j 条件边处理结构化数据,两套系统通过共享数据层协同
用户在微信上发了一句“帮我看看有没有合适的 Java 岗位”,求职派需要快速做出判断:这条消息应该交给岗位推荐 Agent,还是岗位检索 Agent,还是身份采集 Agent? https://paicoding.com/jobclaw-routing-mechanism

一个多 Agent 系统的核心难题不是“每个 Agent 能做什么”,而是“用户发了一条消息,系统怎么知道该派谁上场”。
这个判断做错了,后面的一切都是白费功夫。
上一篇我们拆解了 LangGraph4j 在工作流内部的条件边路由,那是数据驱动的确定性分支。但在对话场景下,用户说的是自然语言,路由的第一步是理解意图。
这篇文章会拆解求职派的消息路由全流程。
一条 IM 消息从到达系统到找到正确的 Agent,中间经过了哪些判断?为什么要分三层做意图分类?会话绑定又省掉了多少次不必要的分类调用?
01、消息从 IM 到 Agent
用户在微信上发了一条消息,消息先经过渠道适配层转成了统一格式,然后通过 Spring 事件总线发送到消息路由器。
消息路由器是整个系统的调度中枢,收到消息后会依次执行七个判断步骤。

第一步,身份检查。 如果用户的画像信息不完整(没有技能、教育背景等),系统会拦截消息,把用户转到身份采集 Agent,先把画像补齐了再说。
第二步,系统命令检查。 消息是不是以 / 开头?如果是 /help、 /reset、 /agents 这类系统命令,直接交给命令分发器处理,不走后续的意图分类流程。
第三步,显式 Agent 切换。 用户如果发的是 /job-recommend 这样的命令,直接路由到对应的 Agent,跳过意图分类。
第四步,会话绑定检查。 当前会话是否已经绑定了一个 Agent?如果绑定了,而且没有过期,就继续用这个 Agent 处理消息,不需要重新分类。
第五步,意图分类。 前面四步都没有命中的消息,才会进入意图分类器。分类器会判断用户想做什么,是推荐岗位、检索岗位、查询进度,还是别的什么。
第六步,Agent 匹配。 拿到意图分类结果后,从 Agent 注册表里找到支持这个意图的 Agent,按优先级选一个。
第七步,绑定并执行。 把选中的 Agent 绑定到当前会话,然后执行 Agent 的处理逻辑,把结果返回给用户。
七步里有四步是“短路”操作,命中了就直接返回,不走后续流程。
这个设计的核心目标是尽量少调用大模型。
Java
// 身份不完整 → 拦截到身份采集
if (identityAgent.triggerToCollectIdentity(conversationInfo, msg)) {
return;
}
// 系统命令 → 直接执行
if (commandDispatcher.isSystemCommand(userMessage)) {
commandDispatcher.executeCommand(msg, conversationInfo, ...);
return;
}
core/.../router/MsgRouter.java,第 93-105 行
每一步的判断条件都是独立的,互不干扰。这让路由逻辑很容易测试,不需要跑完整个七步流程,单独测试任何一步都能拿到确定的结果。
02、意图分类
只有当会话没有绑定 Agent、或者绑定已经过期的时候,消息才会进入意图分类器。
意图分类器采用了三层递进策略,从快到慢、从免费到付费。
第零层,系统命令匹配
最高优先级。如果消息以 / 开头并且匹配到了已注册的系统命令,直接返回对应的系统意图,置信度为 1.0。
第一层,关键词加权匹配
系统预设了一张关键词表,每个关键词都有一个权重值。消息到达后,分类器会扫描消息里有没有命中关键词,取权重最高的那个作为分类结果。

举个例子,用户发了“投简历”,命中了“投简历”关键词(权重 1.0),分类器直接返回“岗位检索”意图,置信度 1.0。但如果用户发的是“实习”,虽然也命中了关键词,权重只有 0.5,分类器会标记为低置信度结果。
权重设计的思路很直观。越明确指向某个意图的词,权重越高。“投简历”只能是岗位检索,给 1.0。“实习”可能是在问实习岗位,也可能是在聊实习经历,给 0.5。
分类器根据权重返回不同的置信度等级。
-
0.9 以上,高置信度,直接采用
-
0.7 到 0.9,中置信度,也能用
-
0.7 以下,低置信度,交给下一层处理
Java
if (maxScore >= 0.9) {
return IntentClassificationRes.highConfidence(bestType, "关键词匹配");
} else if (maxScore >= 0.7) {
return new IntentClassificationRes(bestType, maxScore, "关键词匹配");
}
core/.../intent/classifier/KeywordIntentClassifier.java,第 131-138 行
关键词匹配不消耗任何 API 额度。求职派的实际运行数据里,超过 60% 的消息在这一层就完成了分类。
第二层,大模型分类
关键词匹配的置信度低于 0.7 时,才会触发大模型分类。
大模型分类器会把所有可用的 Agent 列表和用户消息一起发给大模型,让它判断用户的意图属于哪一类。返回的结果是一个结构化的 JSON,包含意图类型、置信度和推理过程。
Java
String prompt = PROMPT_TEMPLATE
.replace("{{BizAgent}}", agentListText)
.replace("{{message}}", message);
var response = simpleLlmCaller.call(
conversationInfo, new Prompt(prompt), IntentClassificationRes.class);
core/.../intent/classifier/LLMIntentClassifier.java,第 67-73 行
这里有一个安全约束。
大模型返回的意图类型不能是系统命令( /help、 /reset 这些)。如果大模型误判了,分类器会把结果降级为“未知”,防止大模型的分类结果和系统命令产生冲突。

三层策略的核心思路是能用规则解决的不用大模型,能用关键词解决的不用 Prompt。
03、会话绑定
意图分类解决的是“第一次不知道该找谁”的问题。但用户和 Agent 的对话往往是多轮的,推荐了一个岗位,用户追问“薪资多少”,接着又问“工作地点在哪”。
如果每条消息都重新做意图分类,不仅浪费时间和 API 额度,还会出现一个更严重的问题,上下文断裂。用户追问“薪资多少”的时候,如果系统把这条消息分类到了一个新的 Agent,新 Agent 根本不知道用户之前在聊哪个岗位。
会话绑定机制就是用来解决这个问题的。

当消息路由器第一次把某个 Agent 和一个会话绑定后,后续消息到达时,路由器会先检查绑定状态。如果绑定还没过期,就直接把消息交给绑定的 Agent 处理,跳过整个意图分类流程。
绑定的判断逻辑分四种情况。
-
用户发了
/reset→ 强制重新分类 -
当前会话没有绑定任何 Agent → 需要分类
-
绑定已过期(默认 6 小时) → 需要分类
-
以上都不满足 → 继续使用绑定的 Agent
绑定状态持久化在文件系统里,路径是 workspace/sessions/{userId}/session-{sessionId}.yaml,格式大概长这样。
YAML
sessionId: WeChat_123456789_user_id
boundAgentId: JobRecommendAgent
boundAt: 2026-08-03T10:30:45Z
expiresAt: 2026-08-03T16:30:45Z
6 小时的过期时间是一个经验值。太短了,用户中途去吃个饭回来就要重新绑定。太长了,用户第二天打开对话可能已经换了话题,还绑着昨天的 Agent 就不合理了。
系统还会记录每次意图分类的历史,最多保留最近 10 条。这些数据不参与路由决策,但在排查问题的时候非常有用。能看到用户这个会话里意图是怎么变化的,分类器每次给出的置信度是多少。

大部分多轮对话里,用户连续发的消息都属于同一个意图。会话绑定把这些消息的路由开销从“意图分类 + Agent 匹配”降到了“读一个 YAML 文件”。对于高频对话的用户来说,这能省掉 80% 以上的意图分类调用。
04、新 Agent 接入路由需要做什么?
路由机制再精巧,如果每次加一个新 Agent 都要改路由器的代码,那就不是一个好的架构。
求职派用的是自动注册模式。一个新的业务 Agent 只需要做两件事就能接入路由系统。
**第一件事,实现业务 Agent 接口并标注 @Component。**
接口约定了三个核心方法,分别是返回 Agent 的身份信息(ID、名称、描述)、处理用户消息、以流式方式处理用户消息。Spring 启动时会自动扫描到这个 Bean,注入到 Agent 注册表里。
Java
public interface BizAgent {
AgentIntro getAgentIntro();
String process(UserConversationInfo info, ChannelReceiveMessage msg);
Flux stream(UserConversationInfo info, ChannelReceiveMessage msg);
}
core/.../agent/BizAgent.java,第 22-75 行
第二件事,声明支持的意图类型。
接口有一个默认方法,用来返回这个 Agent 能处理的意图列表。路由器在做 Agent 匹配的时候,就是根据这个列表来筛选候选者。
如果多个 Agent 都支持同一个意图怎么办?
用优先级。
每个 Agent 可以覆盖优先级方法,路由器会选优先级最高的那个。默认 Agent(兜底用的)把自己的优先级设成了最低值,确保它只在没有其他候选者的时候才会被选中。

注册表的 Agent 查询还有一层权限过滤。不同角色的用户看到的 Agent 列表是不同的。普通用户只能用基础的推荐和检索 Agent,付费用户可以使用高级的订阅和计划 Agent。
Java
public List getAllAgents(String jobClawUserId) {
UserRoleEnum role = userService.getRole(jobClawUserId);
return agents.values().stream()
.filter(agent -> agent.permission().enabled(role))
.collect(Collectors.toList());
}
core/.../intent/impl/DefaultAgentRegistry.java,第 109-115 行
这个设计让路由系统变成了一个“即插即用”的插件架构。写一个类、标一个注解,重启之后新 Agent 就能被意图分类器发现、被路由器匹配。不需要改路由器的代码,不需要改配置文件,不需要在任何地方“注册”。
05、两套路由机制怎么分工?
求职派同时运行着两套路由机制,服务于完全不同的场景。
第一套是对话路由,处理用户从 IM 渠道发来的自然语言消息。路由的依据是意图分类,用关键词匹配和大模型来判断用户想做什么,然后把消息交给对应的 Agent。这套路由的结果带有概率性,因为自然语言本身就是模糊的。“实习”这个词,用户可能在问实习岗位,也可能在聊实习经历。
第二套是工作流路由,处理管理后台提交的岗位数据。路由的依据是业务规则,LangGraph4j 的条件边检查状态对象里的数据字段,字段为空就终止,不为空就流转到下一步。这套路由是完全确定性的,不存在“可能走 A 也可能走 B”的歧义。
上一篇文章详细拆解了工作流路由的实现,这里做一个对比。

两套路由机制的分工可以用一个问题来判断: 下一步该由谁来决定?
如果答案是“由用户的自然语言意图来决定”,用对话路由。消息路由器通过三层意图分类来理解用户想要什么,然后把消息分配给对应的 Agent。
如果答案是“由数据本身的状态来决定”,用工作流路由。LangGraph4j 的条件边检查上一步的输出是否合格,合格就往下走,不合格就提前终止。
两套机制在代码层面完全独立。
对话路由走消息路由器和意图分类器,工作流路由走 LangGraph4j 的图执行器。
它们共享的只有底层的数据层。工作流发布的岗位数据,最终会被对话路由里的岗位推荐 Agent 检索到并推荐给用户。
对话场景需要容忍模糊性,允许大模型猜测用户意图,猜错了还能通过会话绑定和 /reset 命令来修正。数据处理场景不能有任何模糊性,一条岗位信息该不该发布,不能让大模型来“猜”。
ending
【求职派的消息路由器用七步判断链来调度消息,三层意图分类(命令匹配→关键词加权→大模型兜底)平衡了速度和准确率,会话绑定机制省掉了 80% 以上的重复分类调用。】
路由做得好不好,用户感知不到。
用户只会觉得“我说什么它都能理解”,不会关心背后是关键词匹配还是大模型在分类。但路由做得差,用户每一条消息都能感知到。问推荐岗位却跳到了身份采集,追问薪资却丢掉了上下文。
如何把求职派写到简历上?
项目名称:求职派(JobClaw)—— 多 Agent 智能求职系统
项目简介:基于 Spring Boot 4 + Spring AI 2.0 + LangGraph4j 的多 Agent 求职平台,支持微信/钉钉/飞书三渠道接入,具备意图识别、岗位推荐、身份采集等对话型 Agent,以及基于工作流引擎的岗位数据自动采集流水线。
技术栈:Java 21、Spring Boot 4、Spring AI 2.0、LangGraph4j 1.6、JPA/Hibernate、H2/MySQL、React 19 / Next.js 15
核心职责:
-
设计并实现了多 Agent 系统的意图分类机制(系统命令精确匹配→关键词加权评分→大模型结构化分类)
-
基于文件系统实现了会话级 Agent 绑定机制,支持 6 小时 TTL 自动过期和手动重置,多轮对话场景下减少 80% 以上的重复意图分类调用
-
基于 Spring 组件扫描的 Agent 自动注册机制,新 Agent 只需实现接口并标注 @Component 即可接入路由系统,支持基于用户角色的权限过滤和优先级排序
-
基于 Spring 事件总线实现了异步消息路由器,通过七步判断(身份检查→命令匹配→显式切换→会话绑定→意图分类→Agent匹配→执行)调度消息
-
实现了对话路由与工作流路由的双架构方案,对话路由走意图分类处理用户自然语言,工作流路由走 LangGraph4j 条件边处理结构化数据,两套系统通过共享数据层协同
用户在微信上发了一句“帮我看看有没有合适的 Java 岗位”,求职派需要快速做出判断:这条消息应该交给岗位推荐 Agent,还是岗位检索 Agent,还是身份采集 Agent? https://paicoding.com/jobclaw-routing-mechanism

一个多 Agent 系统的核心难题不是“每个 Agent 能做什么”,而是“用户发了一条消息,系统怎么知道该派谁上场”。
这个判断做错了,后面的一切都是白费功夫。
上一篇我们拆解了 LangGraph4j 在工作流内部的条件边路由,那是数据驱动的确定性分支。但在对话场景下,用户说的是自然语言,路由的第一步是理解意图。
这篇文章会拆解求职派的消息路由全流程。
一条 IM 消息从到达系统到找到正确的 Agent,中间经过了哪些判断?为什么要分三层做意图分类?会话绑定又省掉了多少次不必要的分类调用?
01、消息从 IM 到 Agent
用户在微信上发了一条消息,消息先经过渠道适配层转成了统一格式,然后通过 Spring 事件总线发送到消息路由器。
消息路由器是整个系统的调度中枢,收到消息后会依次执行七个判断步骤。

第一步,身份检查。 如果用户的画像信息不完整(没有技能、教育背景等),系统会拦截消息,把用户转到身份采集 Agent,先把画像补齐了再说。
第二步,系统命令检查。 消息是不是以 / 开头?如果是 /help、 /reset、 /agents 这类系统命令,直接交给命令分发器处理,不走后续的意图分类流程。
第三步,显式 Agent 切换。 用户如果发的是 /job-recommend 这样的命令,直接路由到对应的 Agent,跳过意图分类。
第四步,会话绑定检查。 当前会话是否已经绑定了一个 Agent?如果绑定了,而且没有过期,就继续用这个 Agent 处理消息,不需要重新分类。
第五步,意图分类。 前面四步都没有命中的消息,才会进入意图分类器。分类器会判断用户想做什么,是推荐岗位、检索岗位、查询进度,还是别的什么。
第六步,Agent 匹配。 拿到意图分类结果后,从 Agent 注册表里找到支持这个意图的 Agent,按优先级选一个。
第七步,绑定并执行。 把选中的 Agent 绑定到当前会话,然后执行 Agent 的处理逻辑,把结果返回给用户。
七步里有四步是“短路”操作,命中了就直接返回,不走后续流程。
这个设计的核心目标是尽量少调用大模型。
Java
// 身份不完整 → 拦截到身份采集
if (identityAgent.triggerToCollectIdentity(conversationInfo, msg)) {
return;
}
// 系统命令 → 直接执行
if (commandDispatcher.isSystemCommand(userMessage)) {
commandDispatcher.executeCommand(msg, conversationInfo, ...);
return;
}
core/.../router/MsgRouter.java,第 93-105 行
每一步的判断条件都是独立的,互不干扰。这让路由逻辑很容易测试,不需要跑完整个七步流程,单独测试任何一步都能拿到确定的结果。
02、意图分类
只有当会话没有绑定 Agent、或者绑定已经过期的时候,消息才会进入意图分类器。
意图分类器采用了三层递进策略,从快到慢、从免费到付费。
第零层,系统命令匹配
最高优先级。如果消息以 / 开头并且匹配到了已注册的系统命令,直接返回对应的系统意图,置信度为 1.0。
第一层,关键词加权匹配
系统预设了一张关键词表,每个关键词都有一个权重值。消息到达后,分类器会扫描消息里有没有命中关键词,取权重最高的那个作为分类结果。

举个例子,用户发了“投简历”,命中了“投简历”关键词(权重 1.0),分类器直接返回“岗位检索”意图,置信度 1.0。但如果用户发的是“实习”,虽然也命中了关键词,权重只有 0.5,分类器会标记为低置信度结果。
权重设计的思路很直观。越明确指向某个意图的词,权重越高。“投简历”只能是岗位检索,给 1.0。“实习”可能是在问实习岗位,也可能是在聊实习经历,给 0.5。
分类器根据权重返回不同的置信度等级。
-
0.9 以上,高置信度,直接采用
-
0.7 到 0.9,中置信度,也能用
-
0.7 以下,低置信度,交给下一层处理
Java
if (maxScore >= 0.9) {
return IntentClassificationRes.highConfidence(bestType, "关键词匹配");
} else if (maxScore >= 0.7) {
return new IntentClassificationRes(bestType, maxScore, "关键词匹配");
}
core/.../intent/classifier/KeywordIntentClassifier.java,第 131-138 行
关键词匹配不消耗任何 API 额度。求职派的实际运行数据里,超过 60% 的消息在这一层就完成了分类。
第二层,大模型分类
关键词匹配的置信度低于 0.7 时,才会触发大模型分类。
大模型分类器会把所有可用的 Agent 列表和用户消息一起发给大模型,让它判断用户的意图属于哪一类。返回的结果是一个结构化的 JSON,包含意图类型、置信度和推理过程。
Java
String prompt = PROMPT_TEMPLATE
.replace("{{BizAgent}}", agentListText)
.replace("{{message}}", message);
var response = simpleLlmCaller.call(
conversationInfo, new Prompt(prompt), IntentClassificationRes.class);
core/.../intent/classifier/LLMIntentClassifier.java,第 67-73 行
这里有一个安全约束。
大模型返回的意图类型不能是系统命令( /help、 /reset 这些)。如果大模型误判了,分类器会把结果降级为“未知”,防止大模型的分类结果和系统命令产生冲突。

三层策略的核心思路是能用规则解决的不用大模型,能用关键词解决的不用 Prompt。
03、会话绑定
意图分类解决的是“第一次不知道该找谁”的问题。但用户和 Agent 的对话往往是多轮的,推荐了一个岗位,用户追问“薪资多少”,接着又问“工作地点在哪”。
如果每条消息都重新做意图分类,不仅浪费时间和 API 额度,还会出现一个更严重的问题,上下文断裂。用户追问“薪资多少”的时候,如果系统把这条消息分类到了一个新的 Agent,新 Agent 根本不知道用户之前在聊哪个岗位。
会话绑定机制就是用来解决这个问题的。

当消息路由器第一次把某个 Agent 和一个会话绑定后,后续消息到达时,路由器会先检查绑定状态。如果绑定还没过期,就直接把消息交给绑定的 Agent 处理,跳过整个意图分类流程。
绑定的判断逻辑分四种情况。
-
用户发了
/reset→ 强制重新分类 -
当前会话没有绑定任何 Agent → 需要分类
-
绑定已过期(默认 6 小时) → 需要分类
-
以上都不满足 → 继续使用绑定的 Agent
绑定状态持久化在文件系统里,路径是 workspace/sessions/{userId}/session-{sessionId}.yaml,格式大概长这样。
YAML
sessionId: WeChat_123456789_user_id
boundAgentId: JobRecommendAgent
boundAt: 2026-08-03T10:30:45Z
expiresAt: 2026-08-03T16:30:45Z
6 小时的过期时间是一个经验值。太短了,用户中途去吃个饭回来就要重新绑定。太长了,用户第二天打开对话可能已经换了话题,还绑着昨天的 Agent 就不合理了。
系统还会记录每次意图分类的历史,最多保留最近 10 条。这些数据不参与路由决策,但在排查问题的时候非常有用。能看到用户这个会话里意图是怎么变化的,分类器每次给出的置信度是多少。

大部分多轮对话里,用户连续发的消息都属于同一个意图。会话绑定把这些消息的路由开销从“意图分类 + Agent 匹配”降到了“读一个 YAML 文件”。对于高频对话的用户来说,这能省掉 80% 以上的意图分类调用。
04、新 Agent 接入路由需要做什么?
路由机制再精巧,如果每次加一个新 Agent 都要改路由器的代码,那就不是一个好的架构。
求职派用的是自动注册模式。一个新的业务 Agent 只需要做两件事就能接入路由系统。
**第一件事,实现业务 Agent 接口并标注 @Component。**
接口约定了三个核心方法,分别是返回 Agent 的身份信息(ID、名称、描述)、处理用户消息、以流式方式处理用户消息。Spring 启动时会自动扫描到这个 Bean,注入到 Agent 注册表里。
Java
public interface BizAgent {
AgentIntro getAgentIntro();
String process(UserConversationInfo info, ChannelReceiveMessage msg);
Flux stream(UserConversationInfo info, ChannelReceiveMessage msg);
}
core/.../agent/BizAgent.java,第 22-75 行
第二件事,声明支持的意图类型。
接口有一个默认方法,用来返回这个 Agent 能处理的意图列表。路由器在做 Agent 匹配的时候,就是根据这个列表来筛选候选者。
如果多个 Agent 都支持同一个意图怎么办?
用优先级。
每个 Agent 可以覆盖优先级方法,路由器会选优先级最高的那个。默认 Agent(兜底用的)把自己的优先级设成了最低值,确保它只在没有其他候选者的时候才会被选中。

注册表的 Agent 查询还有一层权限过滤。不同角色的用户看到的 Agent 列表是不同的。普通用户只能用基础的推荐和检索 Agent,付费用户可以使用高级的订阅和计划 Agent。
Java
public List getAllAgents(String jobClawUserId) {
UserRoleEnum role = userService.getRole(jobClawUserId);
return agents.values().stream()
.filter(agent -> agent.permission().enabled(role))
.collect(Collectors.toList());
}
core/.../intent/impl/DefaultAgentRegistry.java,第 109-115 行
这个设计让路由系统变成了一个“即插即用”的插件架构。写一个类、标一个注解,重启之后新 Agent 就能被意图分类器发现、被路由器匹配。不需要改路由器的代码,不需要改配置文件,不需要在任何地方“注册”。
05、两套路由机制怎么分工?
求职派同时运行着两套路由机制,服务于完全不同的场景。
第一套是对话路由,处理用户从 IM 渠道发来的自然语言消息。路由的依据是意图分类,用关键词匹配和大模型来判断用户想做什么,然后把消息交给对应的 Agent。这套路由的结果带有概率性,因为自然语言本身就是模糊的。“实习”这个词,用户可能在问实习岗位,也可能在聊实习经历。
第二套是工作流路由,处理管理后台提交的岗位数据。路由的依据是业务规则,LangGraph4j 的条件边检查状态对象里的数据字段,字段为空就终止,不为空就流转到下一步。这套路由是完全确定性的,不存在“可能走 A 也可能走 B”的歧义。
上一篇文章详细拆解了工作流路由的实现,这里做一个对比。

两套路由机制的分工可以用一个问题来判断: 下一步该由谁来决定?
如果答案是“由用户的自然语言意图来决定”,用对话路由。消息路由器通过三层意图分类来理解用户想要什么,然后把消息分配给对应的 Agent。
如果答案是“由数据本身的状态来决定”,用工作流路由。LangGraph4j 的条件边检查上一步的输出是否合格,合格就往下走,不合格就提前终止。
两套机制在代码层面完全独立。
对话路由走消息路由器和意图分类器,工作流路由走 LangGraph4j 的图执行器。
它们共享的只有底层的数据层。工作流发布的岗位数据,最终会被对话路由里的岗位推荐 Agent 检索到并推荐给用户。
对话场景需要容忍模糊性,允许大模型猜测用户意图,猜错了还能通过会话绑定和 /reset 命令来修正。数据处理场景不能有任何模糊性,一条岗位信息该不该发布,不能让大模型来“猜”。
ending
【求职派的消息路由器用七步判断链来调度消息,三层意图分类(命令匹配→关键词加权→大模型兜底)平衡了速度和准确率,会话绑定机制省掉了 80% 以上的重复分类调用。】
路由做得好不好,用户感知不到。
用户只会觉得“我说什么它都能理解”,不会关心背后是关键词匹配还是大模型在分类。但路由做得差,用户每一条消息都能感知到。问推荐岗位却跳到了身份采集,追问薪资却丢掉了上下文。
如何把求职派写到简历上?
项目名称:求职派(JobClaw)—— 多 Agent 智能求职系统
项目简介:基于 Spring Boot 4 + Spring AI 2.0 + LangGraph4j 的多 Agent 求职平台,支持微信/钉钉/飞书三渠道接入,具备意图识别、岗位推荐、身份采集等对话型 Agent,以及基于工作流引擎的岗位数据自动采集流水线。
技术栈:Java 21、Spring Boot 4、Spring AI 2.0、LangGraph4j 1.6、JPA/Hibernate、H2/MySQL、React 19 / Next.js 15
核心职责:
-
设计并实现了多 Agent 系统的意图分类机制(系统命令精确匹配→关键词加权评分→大模型结构化分类)
-
基于文件系统实现了会话级 Agent 绑定机制,支持 6 小时 TTL 自动过期和手动重置,多轮对话场景下减少 80% 以上的重复意图分类调用
-
基于 Spring 组件扫描的 Agent 自动注册机制,新 Agent 只需实现接口并标注 @Component 即可接入路由系统,支持基于用户角色的权限过滤和优先级排序
-
基于 Spring 事件总线实现了异步消息路由器,通过七步判断(身份检查→命令匹配→显式切换→会话绑定→意图分类→Agent匹配→执行)调度消息
-
实现了对话路由与工作流路由的双架构方案,对话路由走意图分类处理用户自然语言,工作流路由走 LangGraph4j 条件边处理结构化数据,两套系统通过共享数据层协同