1. 为什么要在 RuoYi-Vue-Plus 里塞进一个 AI 编排引擎
1.1 从"后台管理系统"到"AI 应用底座"的定位跃迁
RuoYi-Vue-Plus 这套脚手架在国内 Java 圈子里有多普及,不用我多说。权限、菜单、数据权限、多租户、代码生成、定时任务、工作流,基本上一个中后台项目该有的骨架它都给你搭好了。很多团队拿它当公司内部系统的起点,改改包名、换换主题色,两三天就能跑出一个能交付的雏形。
但这两年需求变了。以前客户要的是"能录入、能查询、能导出 Excel",现在张口就是"能不能接个大模型""能不能让系统自己分析这批工单""能不能做个知识库问答"。你如果还拿传统的 Controller-Service-Mapper 三层去硬扛,会发现两个致命问题:第一,AI 调用是长耗时、流式、有状态的,跟传统同步请求模型天然冲突;第二,AI 应用的核心不是"接口",而是"编排"——一次对话可能要串起检索、工具调用、多轮推理、结果落库,这套东西用 if-else 写出来就是灾难。
AgentScope 的 Harness 模块,恰好就是冲着"编排"这件事去的。它把 Agent 的执行过程抽象成可观测、可干预、可复用的运行时,让多智能体协作、工具注册、消息流转这些事有了统一的容器。把它装进 RuoYi-Vue-Plus,本质上是给这套成熟的后台骨架接上一颗"会思考的大脑",让原本只会 CRUD 的系统具备任务分解和自主执行的能力。
这篇文章面向的是有 Java 后端基础、正在考虑给现有系统加 AI 能力的开发者。我会把整个集成过程拆开讲:为什么这么选型、核心模块怎么对接、踩过哪些坑、参数怎么调。你不需要事先懂 AgentScope,但最好对 Spring Boot 的自动装配和线程模型有点概念。
1.2 选型对比:为什么不是直接调 SDK,也不是换框架
在动手之前,我先说清楚为什么走"集成"这条路,而不是"重写"或者"裸调"。
最省事的做法当然是直接在 Service 里 new 一个 HTTP 客户端,往大模型 API 发请求。小 Demo 这么干没问题,但一旦要支持多轮对话、工具调用、上下文管理,你就会发现自己在一遍遍造轮子:会话状态存哪?工具怎么注册?流式响应怎么和前端 SSE 对接?并发上来之后线程池怎么隔离?这些问题 AgentScope Harness 已经替你解决了一大半。
另一个极端是干脆换一套 AI 原生的框架重写整个后端。代价太大——RuoYi-Vue-Plus 里积累的权限体系、数据权限、审计日志、多租户隔离,全部要重新实现。而且团队的学习成本、运维成本都会陡增。对于绝大多数已经在用 RuoYi-Vue-Plus 的团队来说,渐进式集成才是性价比最高的路径。
所以我的判断是:保留 RuoYi-Vue-Plus 作为业务底座和权限中枢,把 AgentScope Harness 作为 AI 能力的运行时层嵌进去,两者通过清晰的服务边界通信。这样既复用了成熟的后台能力,又拿到了 AI 编排的灵活性。
| 方案 | 开发成本 | 复用现有能力 | 可维护性 | 适用场景 |
|---|---|---|---|---|
| 裸调大模型 API | 低 | 差 | 差 | 一次性 Demo |
| 换 AI 原生框架重写 | 极高 | 无 | 中 | 全新项目 |
| 集成 AgentScope Harness | 中 | 好 | 好 | 存量系统加 AI |
2. 集成前的整体架构设计与模块划分
2.1 分层思路:让 AI 层"可插拔"
我在设计这套集成方案时,给自己定了一条铁律:AI 相关代码必须能整体拔掉而不影响原有业务。这不是洁癖,是工程现实——AI 能力上线初期往往只对部分租户或部分角色开放,如果耦合太深,灰度、回滚都会很痛苦。
具体做法是新增一个独立的 Maven 模块,比如叫ruoyi-ai,它依赖ruoyi-common拿到工具类和统一返回体,但反过来ruoyi-system、ruoyi-admin不直接依赖它的内部实现,只通过一个门面接口交互。这样 AI 模块可以独立打包、独立配置、独立开关。
分层大致是这样:
- 接入层:放在
ruoyi-admin里,提供 REST 接口和 SSE 流式端点,负责鉴权、限流、参数校验。 - 门面层:定义
AiAgentFacade接口,屏蔽 AgentScope 的具体类型,业务侧只认这个接口。 - 运行时层:
ruoyi-ai模块内部,封装 Harness 的初始化、Agent 注册、会话管理、工具桥接。 - 适配层:把 RuoYi 的用户上下文、权限、数据源适配成 Agent 能理解的形式。
这么分的好处是,将来 AgentScope 升级或者换别的编排引擎,改动被限制在运行时层,门面以上的代码基本不动。
2.2 会话状态该放哪:内存、Redis 还是数据库
这是集成时第一个要拍板的问题。AgentScope Harness 本身对会话状态的管理是抽象的,但落到 RuoYi-Vue-Plus 这个环境里,你得选一个存储。
我的建议是分层存储:热会话放 Redis,冷会话和审计记录落 MySQL。理由很直接——多轮对话的上下文是高频读写的,放数据库每次都要序列化反序列化,延迟扛不住;但对话记录作为审计和回溯依据,又必须持久化。Redis 负责"当前正在进行的对话",MySQL 负责"对话历史"。
RuoYi-Vue-Plus 本身就集成了 Redis(Redisson),直接复用它的连接工厂即可,不用另起一套。这里有个细节:会话的 key 命名要带上租户 ID 和用户 ID,比如ai:session:{tenantId}:{userId}:{sessionId},否则多租户环境下会串数据。这个坑我在测试环境踩过一次,两个租户的对话上下文混在一起,排查了半天。
2.3 线程模型:别让 AI 请求拖垮业务线程池
传统 Spring MVC 是"一个请求一个线程",Tomcat 默认 200 个工作线程。AI 调用动辄十几秒甚至几分钟,如果直接占用 Tomcat 线程,并发一上来整个应用就卡死了,连正常的登录接口都响应不了。
所以必须做线程隔离。我的做法是:
- 接入层的 AI 接口全部改成异步,用
DeferredResult或SseEmitter返回,Tomcat 线程立刻释放。 - AI 的实际执行放到一个独立的、有界队列的线程池里,核心线程数和最大线程数根据模型响应速度调,队列满了直接拒绝并返回友好提示,而不是无限堆积。
- 流式响应通过 SSE 推送,前端用 EventSource 接收。
线程池参数我一般这么起步:核心 8、最大 16、队列 100、拒绝策略用CallerRunsPolicy之外的——这里千万别用 CallerRuns,否则会退化成占用 Tomcat 线程。用AbortPolicy配合全局异常处理,给用户返回"当前 AI 服务繁忙,请稍后再试"。
3. 核心模块对接的实操细节
3.1 依赖引入与版本对齐
第一步是把 AgentScope 的 Java 相关依赖引进来。这里要注意版本对齐——AgentScope 的迭代速度不慢,不同版本之间 API 可能有破坏性变更。我的经验是锁定一个经过验证的版本,写死在父 POM 的dependencyManagement里,别用动态版本。
<dependencyManagement> <dependencies> <dependency> <groupId>io.agentscope</groupId> <artifactId>agentscope-core</artifactId> <version>${agentscope.version}</version> </dependency> </dependencies> </dependencyManagement>引入之后先别急着写业务,跑一个最小的连通性测试:初始化一个最简单的 Agent,发一句"你好",看能不能拿到回复。这一步能帮你提前发现依赖冲突、类加载问题。我遇到过 AgentScope 依赖的某个 JSON 库版本和 RuoYi 里已有的冲突,导致序列化异常,最后靠maven-enforcer-plugin排查出来的。
提示:引入新依赖后,务必用
mvn dependency:tree检查是否有版本冲突,尤其是 JSON、HTTP 客户端、日志这几类库。
3.2 Agent 的注册与生命周期管理
AgentScope Harness 的核心是 Agent 的注册和调度。在 RuoYi 环境里,我倾向于把 Agent 定义做成"配置驱动"而不是"硬编码"。也就是说,Agent 的名称、系统提示词、可用工具列表、绑定的模型,都从数据库或配置文件读取,启动时批量注册到 Harness 里。
这么做的好处是运营人员可以在后台调整提示词、增减工具,不用改代码重新发版。实现上,我定义了一张ai_agent_config表,字段包括 agent_key、display_name、system_prompt、model_name、tool_keys、enabled 等。应用启动时用一个ApplicationRunner扫描这张表,逐个构建 Agent 实例并注册。
生命周期上要注意:Agent 实例本身应该是无状态的(或者说状态外置到会话存储里),这样才能被多线程安全复用。如果你把对话历史直接挂在 Agent 对象上,并发场景下必然出问题。这一点 AgentScope 的设计是支持的,但需要你在构建时明确指定状态管理策略。
3.3 工具桥接:把 RuoYi 的 Service 暴露给 Agent
这是整个集成里最有意思也最容易出问题的部分。Agent 要能"干活",就得能调用你系统里的能力——查订单、发通知、生成报表。这些能力在 RuoYi 里都是现成的 Service,怎么让 Agent 用上?
我的方案是定义一个注解@AiTool,标注在需要暴露的方法上,然后用一个扫描器在启动时把这些方法注册成 AgentScope 的工具。注解里可以配置工具名、描述、参数说明。描述特别重要——大模型是靠描述来判断什么时候调用哪个工具的,描述写得含糊,模型就会乱调或者不调。
@AiTool(name = "queryOrderStatus", description = "根据订单号查询订单当前状态,参数为订单号字符串") public String queryOrderStatus(String orderNo) { // 复用原有 Service 逻辑 return orderService.getStatusByNo(orderNo); }这里有几个坑必须提醒:
- 参数类型要简单。工具方法的入参尽量用 String、Integer 这类基础类型,复杂对象模型很难准确生成。如果确实需要多参数,用 JSON 字符串传入再在方法内解析。
- 返回值要控制长度。模型上下文有限,工具返回一大坨数据会挤占对话空间。建议在工具方法里就做好裁剪,只返回关键字段。
- 权限要校验。Agent 调用工具时,用的是哪个用户的权限?我的做法是把当前登录用户 ID 通过 ThreadLocal 透传到工具执行上下文,工具方法内部再走一遍 RuoYi 的数据权限校验,防止越权。
4. 流式响应与前端对接的完整链路
4.1 SSE 端点的实现要点
前端要看到"打字机"效果,就得用流式响应。RuoYi-Vue-Plus 默认是同步返回,加 SSE 需要额外配置。核心是返回SseEmitter,然后在 AI 执行过程中不断往 emitter 里 send 数据。
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter streamChat(@RequestParam String sessionId, @RequestParam String message) { SseEmitter emitter = new SseEmitter(180_000L); aiAgentFacade.streamChat(sessionId, message, emitter); return emitter; }超时时间要设够,AI 生成慢的时候 30 秒根本不够,我一般设 180 秒。另外要处理客户端断开的情况——用户关掉页面,后端还在傻傻地生成,浪费资源。给 emitter 注册onCompletion和onTimeout回调,在里面取消正在执行的 Agent 任务。
4.2 消息协议设计:别只发纯文本
流式推送如果只发纯文本,前端没法区分"这是正文"还是"这是工具调用提示"。我建议设计一个简单的 JSON 协议,每条 SSE 消息带一个 type 字段:
| type | 含义 | data 内容 |
|---|---|---|
| text | 正文增量 | 文本片段 |
| tool_call | 工具调用开始 | 工具名、参数 |
| tool_result | 工具返回 | 结果摘要 |
| done | 生成结束 | 完整消息 ID |
| error | 出错 | 错误码、提示 |
前端根据 type 决定怎么渲染。工具调用可以显示成一个小卡片"正在查询订单…",用户体验会好很多,也让整个推理过程透明可观测。
4.3 前端 EventSource 的注意事项
前端用原生 EventSource 就行,但要注意它不支持自定义请求头,所以鉴权不能靠 Header 里的 Token。我的做法是鉴权信息放在 URL 参数里传一个短期有效的 ticket,后端校验 ticket 而不是直接校验 JWT。或者用 fetch + ReadableStream 自己解析 SSE,这样能带 Header,但代码稍复杂。
另外 EventSource 默认会自动重连,AI 场景下这个行为往往不是你想要的——重连会导致重复请求。记得在收到 done 或 error 后主动close()。
5. 常见问题排查与避坑经验
5.1 典型问题速查表
集成过程中我整理了一份问题清单,基本都是实际踩过的:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 启动报类冲突 | 依赖版本不一致 | mvn dependency:tree |
| 对话上下文丢失 | 会话 key 未带租户 | 检查 Redis key 命名 |
| 工具不被调用 | 描述太模糊 | 优化工具 description |
| 流式响应卡住 | 线程池队列满 | 调大队列或加机器 |
| 并发下数据串号 | ThreadLocal 未清理 | finally 中 remove |
| 响应超时 | SSE 超时太短 | 调大 timeout |
5.2 几个容易被忽视的细节
ThreadLocal 泄漏:把用户上下文透传到工具执行线程时,一定要在 finally 里清理,否则线程池复用时会串用户。这个 bug 极其隐蔽,测试环境单线程跑永远发现不了。
模型返回格式不稳定:大模型有时候不按你要求的 JSON 格式返回,工具调用的参数解析会失败。我的做法是加一层容错解析,解析失败时把原始返回喂回给模型让它重试,而不是直接抛异常给用户。
上下文长度爆炸:多轮对话聊久了,历史消息越堆越多,最后超出模型上下文窗口。必须做截断或摘要。我一般保留最近 N 轮完整对话,更早的用模型压缩成一段摘要。
成本失控:AI 调用是花钱的,没有配额控制很容易被刷爆。建议在门面层加一层配额校验,按用户、按租户限制每日调用次数,超了直接拒绝。
5.3 灰度与回滚策略
AI 功能上线千万别全量。我的做法是用 RuoYi 已有的租户和角色体系做灰度:先只对内部测试租户开放,观察一周稳定后再逐步放开。配置开关放在数据库里,出问题一键关闭,AI 模块整体降级为不可用,但不影响原有业务。
回滚就更简单了——因为 AI 模块是独立可插拔的,最坏情况直接把ruoyi-ai依赖从ruoyi-admin里去掉,重新打包即可,业务代码一行不用改。这就是前面强调"可插拔"的价值所在。
6. 性能调优与参数选择
6.1 线程池参数的推算过程
线程池大小不是拍脑袋定的,得算。假设单个 AI 请求平均耗时 T 秒,系统要支撑 QPS 为 Q 的并发,那么需要的线程数大约是 Q × T。比如平均 15 秒、目标 2 QPS,那就是 30 个线程。但这是理论值,实际要考虑模型服务端的承载能力,别把压力全转嫁过去。
我的经验值是:核心线程数设为 CPU 核数的 2 倍起步,最大线程数不超过 32,队列长度 100 到 200。超过这个量级,说明该考虑水平扩容或者上消息队列做削峰了。
6.2 会话缓存的过期策略
Redis 里的会话不能永久保存,否则内存迟早爆。我一般设 2 小时过期,用户活跃对话会不断续期,闲置 2 小时自动清理。同时落库的对话历史保留 30 天,超期归档或删除。这两个时间可以根据业务调整,但一定要有,别让缓存无限增长。
6.3 模型调用的超时与重试
模型服务偶尔会抖动,超时和重试策略要配好。超时我设 60 秒(流式场景下是首字节超时),重试最多 2 次,且只对网络类错误重试,对模型返回的业务错误不重试。重试要加退避,别一失败就立刻重发,那样只会加剧服务端压力。
7. 这套集成方案还能怎么扩展
跑通基础对话之后,能玩的花样就多了。我列几个已经在规划或小范围验证的方向。
多 Agent 协作:Harness 本身支持多 Agent,可以搞一个"调度 Agent + 若干专业 Agent"的结构。调度 Agent 负责理解用户意图,分派给查数据的、写报告的、发通知的专业 Agent。这在复杂业务流程里特别有用。
RAG 知识库:把企业内部的文档、手册向量化存起来,Agent 回答前先检索。RuoYi 里可以加一个知识库管理模块,文档上传、切片、向量化一条龙。检索到的片段作为上下文喂给模型,回答准确率能提升一大截。
工作流集成:RuoYi-Vue-Plus 自带工作流引擎,可以让 Agent 在特定节点触发审批流,或者反过来,审批通过后自动触发 Agent 执行后续动作。这种"人机协同"的流程在 OA 场景里想象空间很大。
可观测性建设:给每次 Agent 执行打上 traceId,把推理步骤、工具调用、耗时都记录下来,做一个可视化的执行链路面板。排查问题、优化提示词、分析成本都靠它。
我个人在实际操作中的体会是,集成 AI 能力最难的不是技术本身,而是想清楚"哪些事该交给 AI,哪些事必须人来兜底"。把边界划清楚,系统才稳。至于 AgentScope Harness 和 RuoYi-Vue-Plus 这套组合,跑通之后你会发现,给一个成熟的后台系统装上 AI 大脑,比从零搭一个 AI 应用要踏实得多——毕竟权限、审计、多租户这些脏活累活,RuoYi 早就替你干完了。