二哥项目资料

✅求职派多Agent架构总览,借鉴OpenClaw

正文 16,094 字 #01 原文
本文目录 · 21 节

星球:Java进阶之路&二哥编程星球 | 专栏:求职派 OpenClaw 架构 | 作者:沉默王二 | 发布:2026-06-29

✅求职派多Agent架构总览,借鉴OpenClaw

✅求职派多Agent架构总览,借鉴OpenClaw

技术派: https://paicoding.com/jobclaw-openclaw-agent

求职派其实已经写了4篇内容到技术派上了,有球友想要尝鲜的话,可以先搞起来,代码已经上传到gitcode,目前正在大力修改文档。基本上做到图文并茂。争取把内容给大家讲清楚。

求职派第二期重构成了一个类似 OpenClaw 的多 Agent 架构,8 个分层,每层职责独立。

图片.png

层级

作用

IM 渠道层

接入微信、飞书、钉钉

消息网关

统一消息格式、签名校验、用户 ID 映射

对话管理

意图识别、会话状态、消息路由

业务 Agent

各个求职业务能力

工具调用

MCP、搜索、文件读写、浏览器

模型路由

多模型切换、用户偏好

任务调度

定时任务、心跳、推送

消息推送

Agent 结果回送 IM

消息从 IM 进来,经过渠道适配、事件发布、意图识别、Agent 路由,最终由业务 Agent 处理后原路返回。

这篇内容就是帮大家拆解这套架构,消息怎么流转、意图怎么识别、Agent 怎么注册和路由、不同用户怎么用不同模型。

目标

落地方式

多渠道

渠道抽象层 + 微信/钉钉/飞书三个独立模块

多模型

模型供应商抽象 + OpenAI/智谱/阿里/Anthropic 四家

多 Agent

统一 Agent 接口 + 身份采集/岗位抓取/岗位推荐等独立模块

多工具

可插拔工具:浏览器自动化、岗位检索等

多任务

任务调度 + 一次性/周期/延时任务

用户隔离

会话状态、任务、偏好按用户 ID 隔离

分层逻辑直接借鉴了 OpenClaw 的设计思路。

图片.png

01、消息从 IM 到 Agent 的流转路径

消息从 IM 到达后,先经过渠道适配层。

这一层只做一件事:把微信、钉钉、飞书各自不同的消息格式统一成一个标准结构,包含消息内容、用户 ID、媒体文件等字段,然后通过事件总线发布出去。

下游所有模块看到的都是同一个结构,完全不需要关心消息来自哪个平台。

图片.png

事件总线收到消息后触发消息路由器,这是整条路径的中枢。

消息会在这里走 5 步短路逻辑,任何一步处理成功就直接返回:

  1. 身份采集:新用户第一次进来,必须先采集身份信息(姓名、求职意向等),采集完才能进入正常对话

  2. 系统命令: /help、 /agents、 /reset 这类指令不需要走意图识别,直接执行

  3. 会话绑定检查:如果用户已经在和某个 Agent 对话,且 6 小时内没过期,就直接沿用,跳过意图识别

  4. 意图识别:判断用户消息属于哪个意图(下一节展开)

  5. **路由到 Agent:**按意图找到对应的业务 Agent,绑定会话,执行

第 3 步是一个容易忽略但很重要的步骤。

用户连续问同一个 Agent 问题时,每次都做意图识别既浪费 Token,又可能因为上下文不足误判。

会话绑定解决了这个问题:一旦绑定,6 小时内同一会话的消息直接走已绑定的 Agent,不重复识别。会话绑定信息持久化到文件系统,服务重启也不会丢。

Agent 执行完把结果通过事件总线发回去,渠道层收到后原路回送给用户。如果 Agent 执行失败,系统会兜底到纯大模型对话,保证用户消息不会没有回应。

消息走到第 4 步时,系统是怎么判断意图呢?

02、为什么不直接用大模型做意图分类?

因为求职场景的用户消息模式相对固定,"推荐岗位""投递""查询"这类高频词汇覆盖率够高,每条消息都调一次大模型既慢又贵。

图片.png

求职派用了 3 层复合分类器,逐层兜底:

  • 第一层:系统命令匹配。 /help、 /agents 这些指令直接字符串精确匹配,最快,不需要任何分类逻辑

  • 第二层:关键词权重匹配。每个意图预配了一组关键词和置信度分数,比如"投递"权重 1.0、"面试"权重 0.7。用户消息里命中的关键词加权求和,过了阈值就直接返回结果

  • 第三层:大模型兜底。只有关键词分类器的置信度低于阈值时才调用,会带上对话历史和 Agent 列表让大模型判断

第二层是整个设计的关键。实际运行中大部分消息在这一层就能判断出来,第三层大模型分类器只在遇到模糊表述(比如"我想看看有没有合适的机会")时才触发。

意图识别完成后,路由模块负责把意图映射到具体的 Agent。

路由也有优先级:先看当前会话绑定的 Agent 能不能处理这个意图,能就保持(避免频繁切换打断用户对话),不能才从注册表里重新找,都找不到就用默认 Agent 兜底。

03、Agent 是怎么注册和管理的?

意图和路由都有了,那被路由到的业务 Agent 是怎么注册和管理的?

所有业务 Agent 统一实现了同一个接口,核心能力就四项:权限控制(哪些用户能用)、Agent 标识(名称和描述)、同步处理、流式处理。

但开发一个新 Agent 不需要关心这些底层方法。

框架提供了一个抽象基类,把模型调用、对话记忆、用户画像这些基础能力全注入好了,启动时自动构建大模型调用器。

业务 Agent 继承它,只需要做两件事:写一段系统提示词(告诉大模型"你是谁、你能做什么"),给 Agent 起个名字和描述。需要工具能力时,额外挂载工具就行。

注册机制是全自动的。

图片.png

Spring 启动时扫描所有 Agent 实现类,统一注册到一个注册表里。注册表内部维护了一张意图到 Agent 的映射关系:每个意图对应一组 Agent,排在前面的优先级高,最后兜底到默认 Agent。

这个设计的好处是扩展完全是增量的。

加一个求职推荐 Agent、加一个面试模拟 Agent,只是在 agents/ 目录下多一个文件的事,不需要改消息路由器,不需要动路由逻辑,不需要碰任何已有代码。

04、模型能按用户自定义吗?

Agent 有了,但 Agent 背后调的大模型能不能按用户自定义?

能。

模型解析模块按用户 ID 解析模型偏好。

用户偏好的格式是 厂商#模型名,比如 zhipu#glm-4.7-flash。

系统拿到这个字符串后,用 # 拆成厂商和模型名两部分,再从用户配置的供应商列表里找到对应的 API Key 和接口地址。

为什么用 # 做分隔符?因为模型名本身可能带 /(比如 openai/gpt-4o),用 # 就不会冲突。

用户没配供应商时自动回退到全局配置,保证总能拿到可用模型。

拿到供应商信息后,系统按接口风格找到对应的供应商实现来实例化模型。

当前支持 OpenAI、智谱、阿里、Anthropic 四家。新增一个模型厂商只需要在 providers/ 下新建模块实现供应商接口,Spring 自动发现并注册。

模型实例做了缓存。同一组参数(厂商 + 模型名 + API Key)只构建一次,不同用户配了相同偏好就共用一份。偏好变更时通过事件清空缓存。

图片.png

大模型调用器分了三层。

  • 基础调用器只做最简单的大模型交互;

  • 业务 Agent 调用器加了用户画像和推理循环(ReAct);

  • 全能调用器再叠加全套工具——Skills、MCP、命令行、文件系统、网页抓取。

为什么不统一用一个全能调用器?

因为上下文窗口是有限的,做岗位信息提取的 Agent 只需要调大模型做文本处理,把命令行、网页抓取这些工具塞进它的上下文纯属浪费 token。分层让每个 Agent 只加载它实际需要的能力。

Agent 能独立扩展,模型能按用户切换,但渠道和 Agent 之间是怎么做到互不感知的?

05、渠道和 Agent 之间怎么解耦的?

靠 Spring 的事件机制做双向事件总线。

收消息的方向:渠道层收到 IM 消息后不直接调消息路由器,而是往事件总线发一个"收到消息"的事件,消息路由器监听这个事件并处理。

发消息的方向反过来,路由器处理完后往事件总线发一个"响应消息"的事件,渠道层监听到后把结果回送给用户。

两边都只依赖事件总线这一个抽象。

渠道层不知道消息路由器存在,消息路由器不知道是哪个渠道在监听。加一个新渠道,只要实现渠道接口、往事件总线发消息就行,不用动路由层一行代码。

图片.png

主动推送也走同一条总线。

响应事件有一个字段区分是回复还是主动推送,后台定时任务要给用户推消息时,发一个标记为"主动推送"的事件,消息路由器照常接收并路由到对应渠道。

主动推送能跑通有一个前提:系统得知道用户在哪个渠道。渠道适配层每次收到用户消息时,会把用户的渠道信息和回调参数(钉钉的 webhook 地址、飞书的 chatId)存起来。

后台任务推送时直接取出来用,用户即使离线也不影响,因为这些信息在用户最后一次活跃时就保存好了。

06、和 OpenClaw 相比,求职派做了哪些取舍?

借鉴的是分层解耦的设计思路:

  • 求职派的事件发布器对应 OpenClaw 的事件总线,

  • 业务 Agent 抽象对应 OpenClaw 的 Agent 接口,

  • 模型供应商抽象对应 OpenClaw 的 Provider 体系。

图片.png

但求职派是求职场景的实战项目,不是通用框架,做了几个明确的取舍:

维度

OpenClaw 的做法

求职派的选择

为什么

意图识别

统一大模型分类

3 层复合(命令→关键词→大模型)

求职场景高频词覆盖率高,关键词层能拦截大部分消息,省调用费

会话存储

Redis

文件系统

低并发场景不需要 Redis,少一个中间件少一份运维

大模型调用

统一调用器

3 层分级调用器

不同 Agent 能力需求不同,避免无关工具挤占上下文窗口

工具注册

全局工具池

按 Agent 分层挂载

岗位抓取 Agent 不需要命令行,自由聊天 Agent 需要全套

07、模块边界与扩展路径

整个项目按职责拆成了 7 个模块:

  • 渠道层(微信/钉钉/飞书)、

  • 核心层(对话管理、Agent 抽象、路由、模型、记忆)、

  • 业务 Agent 层、

  • 模型供应商层、

  • 工具插件层、

  • 主应用(Web 管理后台、职位数据、用户、支付)、

  • 前端。

核心层是共享基础,所有其他模块都依赖它,但它不依赖任何业务模块。主应用是组装模块,把所有模块拉到一起运行。

图片.png

新增能力时按固定路径落点:

需求

落在哪里

新接一个 IM 或 Webhook 入口

渠道层,新建一个渠道模块

新增一个可对话的业务角色

Agent 层,新建一个 Agent 模块

新接一个模型厂商

供应商层,新建一个供应商模块

新增一个可被模型调用的工具

工具层,新建一个插件模块

新增后台职位数据流程

主应用内部

这样保持的主线是,入口归入口,网关归网关,对话管理归核心层,业务 Agent 只处理业务,工具和模型能力独立扩展。

技术派: https://paicoding.com/jobclaw-openclaw-agent

求职派其实已经写了4篇内容到技术派上了,有球友想要尝鲜的话,可以先搞起来,代码已经上传到gitcode,目前正在大力修改文档。基本上做到图文并茂。争取把内容给大家讲清楚。

求职派第二期重构成了一个类似 OpenClaw 的多 Agent 架构,8 个分层,每层职责独立。

图片.png

层级

作用

IM 渠道层

接入微信、飞书、钉钉

消息网关

统一消息格式、签名校验、用户 ID 映射

对话管理

意图识别、会话状态、消息路由

业务 Agent

各个求职业务能力

工具调用

MCP、搜索、文件读写、浏览器

模型路由

多模型切换、用户偏好

任务调度

定时任务、心跳、推送

消息推送

Agent 结果回送 IM

消息从 IM 进来,经过渠道适配、事件发布、意图识别、Agent 路由,最终由业务 Agent 处理后原路返回。

这篇内容就是帮大家拆解这套架构,消息怎么流转、意图怎么识别、Agent 怎么注册和路由、不同用户怎么用不同模型。

目标

落地方式

多渠道

渠道抽象层 + 微信/钉钉/飞书三个独立模块

多模型

模型供应商抽象 + OpenAI/智谱/阿里/Anthropic 四家

多 Agent

统一 Agent 接口 + 身份采集/岗位抓取/岗位推荐等独立模块

多工具

可插拔工具:浏览器自动化、岗位检索等

多任务

任务调度 + 一次性/周期/延时任务

用户隔离

会话状态、任务、偏好按用户 ID 隔离

分层逻辑直接借鉴了 OpenClaw 的设计思路。

图片.png

01、消息从 IM 到 Agent 的流转路径

消息从 IM 到达后,先经过渠道适配层。

这一层只做一件事:把微信、钉钉、飞书各自不同的消息格式统一成一个标准结构,包含消息内容、用户 ID、媒体文件等字段,然后通过事件总线发布出去。

下游所有模块看到的都是同一个结构,完全不需要关心消息来自哪个平台。

图片.png

事件总线收到消息后触发消息路由器,这是整条路径的中枢。

消息会在这里走 5 步短路逻辑,任何一步处理成功就直接返回:

  1. 身份采集:新用户第一次进来,必须先采集身份信息(姓名、求职意向等),采集完才能进入正常对话

  2. 系统命令: /help、 /agents、 /reset 这类指令不需要走意图识别,直接执行

  3. 会话绑定检查:如果用户已经在和某个 Agent 对话,且 6 小时内没过期,就直接沿用,跳过意图识别

  4. 意图识别:判断用户消息属于哪个意图(下一节展开)

  5. **路由到 Agent:**按意图找到对应的业务 Agent,绑定会话,执行

第 3 步是一个容易忽略但很重要的步骤。

用户连续问同一个 Agent 问题时,每次都做意图识别既浪费 Token,又可能因为上下文不足误判。

会话绑定解决了这个问题:一旦绑定,6 小时内同一会话的消息直接走已绑定的 Agent,不重复识别。会话绑定信息持久化到文件系统,服务重启也不会丢。

Agent 执行完把结果通过事件总线发回去,渠道层收到后原路回送给用户。如果 Agent 执行失败,系统会兜底到纯大模型对话,保证用户消息不会没有回应。

消息走到第 4 步时,系统是怎么判断意图呢?

02、为什么不直接用大模型做意图分类?

因为求职场景的用户消息模式相对固定,"推荐岗位""投递""查询"这类高频词汇覆盖率够高,每条消息都调一次大模型既慢又贵。

图片.png

求职派用了 3 层复合分类器,逐层兜底:

  • 第一层:系统命令匹配。 /help、 /agents 这些指令直接字符串精确匹配,最快,不需要任何分类逻辑

  • 第二层:关键词权重匹配。每个意图预配了一组关键词和置信度分数,比如"投递"权重 1.0、"面试"权重 0.7。用户消息里命中的关键词加权求和,过了阈值就直接返回结果

  • 第三层:大模型兜底。只有关键词分类器的置信度低于阈值时才调用,会带上对话历史和 Agent 列表让大模型判断

第二层是整个设计的关键。实际运行中大部分消息在这一层就能判断出来,第三层大模型分类器只在遇到模糊表述(比如"我想看看有没有合适的机会")时才触发。

意图识别完成后,路由模块负责把意图映射到具体的 Agent。

路由也有优先级:先看当前会话绑定的 Agent 能不能处理这个意图,能就保持(避免频繁切换打断用户对话),不能才从注册表里重新找,都找不到就用默认 Agent 兜底。

03、Agent 是怎么注册和管理的?

意图和路由都有了,那被路由到的业务 Agent 是怎么注册和管理的?

所有业务 Agent 统一实现了同一个接口,核心能力就四项:权限控制(哪些用户能用)、Agent 标识(名称和描述)、同步处理、流式处理。

但开发一个新 Agent 不需要关心这些底层方法。

框架提供了一个抽象基类,把模型调用、对话记忆、用户画像这些基础能力全注入好了,启动时自动构建大模型调用器。

业务 Agent 继承它,只需要做两件事:写一段系统提示词(告诉大模型"你是谁、你能做什么"),给 Agent 起个名字和描述。需要工具能力时,额外挂载工具就行。

注册机制是全自动的。

图片.png

Spring 启动时扫描所有 Agent 实现类,统一注册到一个注册表里。注册表内部维护了一张意图到 Agent 的映射关系:每个意图对应一组 Agent,排在前面的优先级高,最后兜底到默认 Agent。

这个设计的好处是扩展完全是增量的。

加一个求职推荐 Agent、加一个面试模拟 Agent,只是在 agents/ 目录下多一个文件的事,不需要改消息路由器,不需要动路由逻辑,不需要碰任何已有代码。

04、模型能按用户自定义吗?

Agent 有了,但 Agent 背后调的大模型能不能按用户自定义?

能。

模型解析模块按用户 ID 解析模型偏好。

用户偏好的格式是 厂商#模型名,比如 zhipu#glm-4.7-flash。

系统拿到这个字符串后,用 # 拆成厂商和模型名两部分,再从用户配置的供应商列表里找到对应的 API Key 和接口地址。

为什么用 # 做分隔符?因为模型名本身可能带 /(比如 openai/gpt-4o),用 # 就不会冲突。

用户没配供应商时自动回退到全局配置,保证总能拿到可用模型。

拿到供应商信息后,系统按接口风格找到对应的供应商实现来实例化模型。

当前支持 OpenAI、智谱、阿里、Anthropic 四家。新增一个模型厂商只需要在 providers/ 下新建模块实现供应商接口,Spring 自动发现并注册。

模型实例做了缓存。同一组参数(厂商 + 模型名 + API Key)只构建一次,不同用户配了相同偏好就共用一份。偏好变更时通过事件清空缓存。

图片.png

大模型调用器分了三层。

  • 基础调用器只做最简单的大模型交互;

  • 业务 Agent 调用器加了用户画像和推理循环(ReAct);

  • 全能调用器再叠加全套工具——Skills、MCP、命令行、文件系统、网页抓取。

为什么不统一用一个全能调用器?

因为上下文窗口是有限的,做岗位信息提取的 Agent 只需要调大模型做文本处理,把命令行、网页抓取这些工具塞进它的上下文纯属浪费 token。分层让每个 Agent 只加载它实际需要的能力。

Agent 能独立扩展,模型能按用户切换,但渠道和 Agent 之间是怎么做到互不感知的?

05、渠道和 Agent 之间怎么解耦的?

靠 Spring 的事件机制做双向事件总线。

收消息的方向:渠道层收到 IM 消息后不直接调消息路由器,而是往事件总线发一个"收到消息"的事件,消息路由器监听这个事件并处理。

发消息的方向反过来,路由器处理完后往事件总线发一个"响应消息"的事件,渠道层监听到后把结果回送给用户。

两边都只依赖事件总线这一个抽象。

渠道层不知道消息路由器存在,消息路由器不知道是哪个渠道在监听。加一个新渠道,只要实现渠道接口、往事件总线发消息就行,不用动路由层一行代码。

图片.png

主动推送也走同一条总线。

响应事件有一个字段区分是回复还是主动推送,后台定时任务要给用户推消息时,发一个标记为"主动推送"的事件,消息路由器照常接收并路由到对应渠道。

主动推送能跑通有一个前提:系统得知道用户在哪个渠道。渠道适配层每次收到用户消息时,会把用户的渠道信息和回调参数(钉钉的 webhook 地址、飞书的 chatId)存起来。

后台任务推送时直接取出来用,用户即使离线也不影响,因为这些信息在用户最后一次活跃时就保存好了。

06、和 OpenClaw 相比,求职派做了哪些取舍?

借鉴的是分层解耦的设计思路:

  • 求职派的事件发布器对应 OpenClaw 的事件总线,

  • 业务 Agent 抽象对应 OpenClaw 的 Agent 接口,

  • 模型供应商抽象对应 OpenClaw 的 Provider 体系。

图片.png

但求职派是求职场景的实战项目,不是通用框架,做了几个明确的取舍:

维度

OpenClaw 的做法

求职派的选择

为什么

意图识别

统一大模型分类

3 层复合(命令→关键词→大模型)

求职场景高频词覆盖率高,关键词层能拦截大部分消息,省调用费

会话存储

Redis

文件系统

低并发场景不需要 Redis,少一个中间件少一份运维

大模型调用

统一调用器

3 层分级调用器

不同 Agent 能力需求不同,避免无关工具挤占上下文窗口

工具注册

全局工具池

按 Agent 分层挂载

岗位抓取 Agent 不需要命令行,自由聊天 Agent 需要全套

07、模块边界与扩展路径

整个项目按职责拆成了 7 个模块:

  • 渠道层(微信/钉钉/飞书)、

  • 核心层(对话管理、Agent 抽象、路由、模型、记忆)、

  • 业务 Agent 层、

  • 模型供应商层、

  • 工具插件层、

  • 主应用(Web 管理后台、职位数据、用户、支付)、

  • 前端。

核心层是共享基础,所有其他模块都依赖它,但它不依赖任何业务模块。主应用是组装模块,把所有模块拉到一起运行。

图片.png

新增能力时按固定路径落点:

需求

落在哪里

新接一个 IM 或 Webhook 入口

渠道层,新建一个渠道模块

新增一个可对话的业务角色

Agent 层,新建一个 Agent 模块

新接一个模型厂商

供应商层,新建一个供应商模块

新增一个可被模型调用的工具

工具层,新建一个插件模块

新增后台职位数据流程

主应用内部

这样保持的主线是,入口归入口,网关归网关,对话管理归核心层,业务 Agent 只处理业务,工具和模型能力独立扩展。

技术派: https://paicoding.com/jobclaw-openclaw-agent

求职派其实已经写了4篇内容到技术派上了,有球友想要尝鲜的话,可以先搞起来,代码已经上传到gitcode,目前正在大力修改文档。基本上做到图文并茂。争取把内容给大家讲清楚。

求职派第二期重构成了一个类似 OpenClaw 的多 Agent 架构,8 个分层,每层职责独立。

图片.png

层级

作用

IM 渠道层

接入微信、飞书、钉钉

消息网关

统一消息格式、签名校验、用户 ID 映射

对话管理

意图识别、会话状态、消息路由

业务 Agent

各个求职业务能力

工具调用

MCP、搜索、文件读写、浏览器

模型路由

多模型切换、用户偏好

任务调度

定时任务、心跳、推送

消息推送

Agent 结果回送 IM

消息从 IM 进来,经过渠道适配、事件发布、意图识别、Agent 路由,最终由业务 Agent 处理后原路返回。

这篇内容就是帮大家拆解这套架构,消息怎么流转、意图怎么识别、Agent 怎么注册和路由、不同用户怎么用不同模型。

目标

落地方式

多渠道

渠道抽象层 + 微信/钉钉/飞书三个独立模块

多模型

模型供应商抽象 + OpenAI/智谱/阿里/Anthropic 四家

多 Agent

统一 Agent 接口 + 身份采集/岗位抓取/岗位推荐等独立模块

多工具

可插拔工具:浏览器自动化、岗位检索等

多任务

任务调度 + 一次性/周期/延时任务

用户隔离

会话状态、任务、偏好按用户 ID 隔离

分层逻辑直接借鉴了 OpenClaw 的设计思路。

图片.png

01、消息从 IM 到 Agent 的流转路径

消息从 IM 到达后,先经过渠道适配层。

这一层只做一件事:把微信、钉钉、飞书各自不同的消息格式统一成一个标准结构,包含消息内容、用户 ID、媒体文件等字段,然后通过事件总线发布出去。

下游所有模块看到的都是同一个结构,完全不需要关心消息来自哪个平台。

图片.png

事件总线收到消息后触发消息路由器,这是整条路径的中枢。

消息会在这里走 5 步短路逻辑,任何一步处理成功就直接返回:

  1. 身份采集:新用户第一次进来,必须先采集身份信息(姓名、求职意向等),采集完才能进入正常对话

  2. 系统命令: /help、 /agents、 /reset 这类指令不需要走意图识别,直接执行

  3. 会话绑定检查:如果用户已经在和某个 Agent 对话,且 6 小时内没过期,就直接沿用,跳过意图识别

  4. 意图识别:判断用户消息属于哪个意图(下一节展开)

  5. **路由到 Agent:**按意图找到对应的业务 Agent,绑定会话,执行

第 3 步是一个容易忽略但很重要的步骤。

用户连续问同一个 Agent 问题时,每次都做意图识别既浪费 Token,又可能因为上下文不足误判。

会话绑定解决了这个问题:一旦绑定,6 小时内同一会话的消息直接走已绑定的 Agent,不重复识别。会话绑定信息持久化到文件系统,服务重启也不会丢。

Agent 执行完把结果通过事件总线发回去,渠道层收到后原路回送给用户。如果 Agent 执行失败,系统会兜底到纯大模型对话,保证用户消息不会没有回应。

消息走到第 4 步时,系统是怎么判断意图呢?

02、为什么不直接用大模型做意图分类?

因为求职场景的用户消息模式相对固定,"推荐岗位""投递""查询"这类高频词汇覆盖率够高,每条消息都调一次大模型既慢又贵。

图片.png

求职派用了 3 层复合分类器,逐层兜底:

  • 第一层:系统命令匹配。 /help、 /agents 这些指令直接字符串精确匹配,最快,不需要任何分类逻辑

  • 第二层:关键词权重匹配。每个意图预配了一组关键词和置信度分数,比如"投递"权重 1.0、"面试"权重 0.7。用户消息里命中的关键词加权求和,过了阈值就直接返回结果

  • 第三层:大模型兜底。只有关键词分类器的置信度低于阈值时才调用,会带上对话历史和 Agent 列表让大模型判断

第二层是整个设计的关键。实际运行中大部分消息在这一层就能判断出来,第三层大模型分类器只在遇到模糊表述(比如"我想看看有没有合适的机会")时才触发。

意图识别完成后,路由模块负责把意图映射到具体的 Agent。

路由也有优先级:先看当前会话绑定的 Agent 能不能处理这个意图,能就保持(避免频繁切换打断用户对话),不能才从注册表里重新找,都找不到就用默认 Agent 兜底。

03、Agent 是怎么注册和管理的?

意图和路由都有了,那被路由到的业务 Agent 是怎么注册和管理的?

所有业务 Agent 统一实现了同一个接口,核心能力就四项:权限控制(哪些用户能用)、Agent 标识(名称和描述)、同步处理、流式处理。

但开发一个新 Agent 不需要关心这些底层方法。

框架提供了一个抽象基类,把模型调用、对话记忆、用户画像这些基础能力全注入好了,启动时自动构建大模型调用器。

业务 Agent 继承它,只需要做两件事:写一段系统提示词(告诉大模型"你是谁、你能做什么"),给 Agent 起个名字和描述。需要工具能力时,额外挂载工具就行。

注册机制是全自动的。

图片.png

Spring 启动时扫描所有 Agent 实现类,统一注册到一个注册表里。注册表内部维护了一张意图到 Agent 的映射关系:每个意图对应一组 Agent,排在前面的优先级高,最后兜底到默认 Agent。

这个设计的好处是扩展完全是增量的。

加一个求职推荐 Agent、加一个面试模拟 Agent,只是在 agents/ 目录下多一个文件的事,不需要改消息路由器,不需要动路由逻辑,不需要碰任何已有代码。

04、模型能按用户自定义吗?

Agent 有了,但 Agent 背后调的大模型能不能按用户自定义?

能。

模型解析模块按用户 ID 解析模型偏好。

用户偏好的格式是 厂商#模型名,比如 zhipu#glm-4.7-flash。

系统拿到这个字符串后,用 # 拆成厂商和模型名两部分,再从用户配置的供应商列表里找到对应的 API Key 和接口地址。

为什么用 # 做分隔符?因为模型名本身可能带 /(比如 openai/gpt-4o),用 # 就不会冲突。

用户没配供应商时自动回退到全局配置,保证总能拿到可用模型。

拿到供应商信息后,系统按接口风格找到对应的供应商实现来实例化模型。

当前支持 OpenAI、智谱、阿里、Anthropic 四家。新增一个模型厂商只需要在 providers/ 下新建模块实现供应商接口,Spring 自动发现并注册。

模型实例做了缓存。同一组参数(厂商 + 模型名 + API Key)只构建一次,不同用户配了相同偏好就共用一份。偏好变更时通过事件清空缓存。

图片.png

大模型调用器分了三层。

  • 基础调用器只做最简单的大模型交互;

  • 业务 Agent 调用器加了用户画像和推理循环(ReAct);

  • 全能调用器再叠加全套工具——Skills、MCP、命令行、文件系统、网页抓取。

为什么不统一用一个全能调用器?

因为上下文窗口是有限的,做岗位信息提取的 Agent 只需要调大模型做文本处理,把命令行、网页抓取这些工具塞进它的上下文纯属浪费 token。分层让每个 Agent 只加载它实际需要的能力。

Agent 能独立扩展,模型能按用户切换,但渠道和 Agent 之间是怎么做到互不感知的?

05、渠道和 Agent 之间怎么解耦的?

靠 Spring 的事件机制做双向事件总线。

收消息的方向:渠道层收到 IM 消息后不直接调消息路由器,而是往事件总线发一个"收到消息"的事件,消息路由器监听这个事件并处理。

发消息的方向反过来,路由器处理完后往事件总线发一个"响应消息"的事件,渠道层监听到后把结果回送给用户。

两边都只依赖事件总线这一个抽象。

渠道层不知道消息路由器存在,消息路由器不知道是哪个渠道在监听。加一个新渠道,只要实现渠道接口、往事件总线发消息就行,不用动路由层一行代码。

图片.png

主动推送也走同一条总线。

响应事件有一个字段区分是回复还是主动推送,后台定时任务要给用户推消息时,发一个标记为"主动推送"的事件,消息路由器照常接收并路由到对应渠道。

主动推送能跑通有一个前提:系统得知道用户在哪个渠道。渠道适配层每次收到用户消息时,会把用户的渠道信息和回调参数(钉钉的 webhook 地址、飞书的 chatId)存起来。

后台任务推送时直接取出来用,用户即使离线也不影响,因为这些信息在用户最后一次活跃时就保存好了。

06、和 OpenClaw 相比,求职派做了哪些取舍?

借鉴的是分层解耦的设计思路:

  • 求职派的事件发布器对应 OpenClaw 的事件总线,

  • 业务 Agent 抽象对应 OpenClaw 的 Agent 接口,

  • 模型供应商抽象对应 OpenClaw 的 Provider 体系。

图片.png

但求职派是求职场景的实战项目,不是通用框架,做了几个明确的取舍:

维度

OpenClaw 的做法

求职派的选择

为什么

意图识别

统一大模型分类

3 层复合(命令→关键词→大模型)

求职场景高频词覆盖率高,关键词层能拦截大部分消息,省调用费

会话存储

Redis

文件系统

低并发场景不需要 Redis,少一个中间件少一份运维

大模型调用

统一调用器

3 层分级调用器

不同 Agent 能力需求不同,避免无关工具挤占上下文窗口

工具注册

全局工具池

按 Agent 分层挂载

岗位抓取 Agent 不需要命令行,自由聊天 Agent 需要全套

07、模块边界与扩展路径

整个项目按职责拆成了 7 个模块:

  • 渠道层(微信/钉钉/飞书)、

  • 核心层(对话管理、Agent 抽象、路由、模型、记忆)、

  • 业务 Agent 层、

  • 模型供应商层、

  • 工具插件层、

  • 主应用(Web 管理后台、职位数据、用户、支付)、

  • 前端。

核心层是共享基础,所有其他模块都依赖它,但它不依赖任何业务模块。主应用是组装模块,把所有模块拉到一起运行。

图片.png

新增能力时按固定路径落点:

需求

落在哪里

新接一个 IM 或 Webhook 入口

渠道层,新建一个渠道模块

新增一个可对话的业务角色

Agent 层,新建一个 Agent 模块

新接一个模型厂商

供应商层,新建一个供应商模块

新增一个可被模型调用的工具

工具层,新建一个插件模块

新增后台职位数据流程

主应用内部

这样保持的主线是,入口归入口,网关归网关,对话管理归核心层,业务 Agent 只处理业务,工具和模型能力独立扩展。