资讯动态

RuoYi-Vue-Plus 集成 AgentScope Harness 智能体实践与踩坑指南

发布时间:2026/10/26 21:51:48 来源:尧图企业网站定制
最近在给公司做内部运营助手领导一句话别搞独立服务就落到现有的 RuoYi-Vue-Plus 里。于是开始折腾把 AgentScope Harness 集成进 RuoYi-Vue-Plus。折腾完回头看这件事比想象中麻烦不少但只要分清楚 Harness 的职责边界和 RuoYi 的扩展点其实可以做到比较优雅。这篇就把我的集成过程、踩坑经历、还有最后稳定上线的配置一次性写清楚。如果你是 Java 技术栈又想在现有后台框架里加智能体能力这篇应该能帮你少走不少弯路。RuoYi-Vue-Plus 本身是典型的 Spring Boot Vue 后台管理脚手架用户、权限、菜单、日志这些底座都现成。AgentScope Harness 则是负责多智能体任务编排和执行的那一层它不管业务数据也不管用户权限只关心怎么把大模型、工具调用、上下文记忆串起来。所以集成思路很直接让 RuoYi 做业务外观Harness 做智能体执行内核两者通过 Java 接口对接。下面按流程慢慢拆。1. 集成前先搞清两件事Harness 在体系里的位置RuoYi 的扩展边界在哪里动手前我花了一天时间想清楚了一个问题如果不做这次集成另一种方案是什么最常见的做法是单独部署一个 Python 或其他语言写的智能体服务Java 后台通过 HTTP 去调。但我们是纯 Java 团队运维、排障、上线流程都围绕 JVM 展开引入一门新语言等于多养一套系统。所以目标锁定最好能直接把 Harness 以 SDK 方式塞进现有工程实在不行也要用 Java 客户端封装让业务代码完全感知不到远端是谁。1.1 为什么不是起一个 Python 微服务这个问题我得先说因为几乎所有聊过这个方案的人都先想到独立微服务。独立服务的优点当然有语言生态天然贴合 AI、版本独立升级、不会污染业务应用。但实际一算账就发现成本高需要额外维护一套发布流水线需要定 API 约定需要处理跨语言调用时的序列化兼容问题。更重要的是一旦智能体任务里要回写业务数据比如自动创建工单跨服务调用的链路会变得又长又难查。所以我的选择是用 Harness 提供的 Java 客户端没有就自己封装一个在 RuoYi 里以 Spring Bean 的形式运行。RuoYi 作为宿主通过依赖注入把 HarnessSession 交给业务 Service 去用。进程内调用不走网络参数和返回值都是 Java 对象排查问题直接在同一个日志栈里拉非常舒服。1.2 RuoYi-Vue-Plus 的哪些位置适合放智能体能力RuoYi-Vue-Plus 的工程结构是ruoyi-admin、ruoyi-framework、ruoyi-system、ruoyi-common、ruoyi-modules。我一开始想是不是要改ruoyi-framework加拦截器后来发现完全不用。最合适的做法是新建一个独立 module比如ruoyi-modules/agentscope-harness里面放 Harness 相关的配置类、客户端封装、Service 实现和 DTO。这样不会和框架代码纠缠将来 RuoYi 升级打补丁这个 module 基本不受影响。另外菜单和权限这块也不要动框架直接利用 RuoYi 已有的sys_menu表和PreAuthorize注解就能把智能体功能管起来。流式输出可以走 RuoYi-Vue-Plus 自带的 WebSocket 能力。总结下来RuoYi-Vue-Plus 的边界是它提供用户、权限、组织、日志、配置管理的底座Harness 的边界是它提供模型路由、工具调用、上下文记忆、任务编排能力。两者通过一个薄薄的 Service 层粘连就足够干净。2. 依赖入场Maven 坐标、版本选择和第一个报错架构定了接下来就是动手拉依赖。我的 pom 里加入了这样一个坐标版本号是本例中的实际使用值dependency groupIdcom.agentscope/groupId artifactIdagentscope-harness-client/artifactId version2.4.1/version /dependency这里有个坑Harness 客户端传递依赖里带了一个okhttp、一个guava、还有旧的jackson-databind。RuoYi-Vue-Plus 本身用的是HttpClient5而且 jackson 版本已经升到了 2.15 以上。直接引入后跑单元测试第一行就报ClassNotFoundException: reactor.util.context.Context当时人都是懵的。2.1 通过 Nacos 动态控制 Harness 开关RuoYi-Vue-Plus 普遍使用 Nacos 做配置中心我在bootstrap.yml里加了一段动态配置agentscope: harness: enabled: true session-timeout: 30s model: api-key: ${HARNESS_API_KEY} base-url: https://api.internal.example.com/v1 default-timeout: 30s然后在配置类上加了ConditionalOnProperty(prefix agentscope.harness, name enabled, havingValue true)。这样做的原因是Harness 客户端的初始化会创建线程池和连接池如果某个环境比如本地开发不想跑真模型直接设置false就不会加载相关 Bean也不会占资源。这个开关在后续排障时非常有用甚至可以为“降级为 mock 模式”提供切入点。2.2 第一个报错reactor 上下文类加载失败回到刚才那个ClassNotFoundException。原因是 Harness 客户端内部的流式调用依赖reactor-core但我一开始为了控制依赖体积把spring-webflux排除了。后来仔细看 Harness 源码发现它调 WebClient 不一定要完整引入 WebFlux只要reactor-core在 classpath 里就能跑。于是我在 pom 里单独加了 reactor-core并排除了 Harness 传递来的旧版 jacksondependency groupIdcom.agentscope/groupId artifactIdagentscope-harness-client/artifactId version2.4.1/version exclusions exclusion groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /exclusion exclusion groupIdcom.google.guava/groupId artifactIdguava/artifactId /exclusion /exclusions /dependency dependency groupIdio.projectreactor/groupId artifactIdreactor-core/artifactId version3.4.30/version /dependency排除之后 jackson 统一走 RuoYi 的版本guava 用 RuoYi 自带的这个问题彻底解决。这里给后来的朋友一个建议不要盲目加依赖先mvn dependency:tree看一下冲突再针对性排比一股脑升级快得多。3. 核心集成代码把 Agent 调用装进业务 Service依赖通了之后最核心的工作是把 Harness 的能力封装成一个个业务方法。我以“订单售后智能体”作为 demo用户在前台提交售后工单后台自动调用智能体分析退款金额、生成回复话术并更新工单状态。整个过程要求异步执行页面只显示“处理中”等智能体跑完再回调刷新。3.1 面向接口设计“智能体服务”先定义一个业务接口public interface AgentService { /** * 提交一个智能体任务立即返回 */ void submitTask(AgentTaskRequest request); /** * 查询任务结果 */ AgentTaskResult getTaskResult(Long taskId); }实现类里注入 HarnessSessionFactoryService public class HarnessAgentServiceImpl implements AgentService { Resource private HarnessSessionFactory sessionFactory; Resource private IHarnessTaskMapper taskMapper; Override public void submitTask(AgentTaskRequest request) { // 先把任务落库状态为 PROCESSING HarnessTaskDO task new HarnessTaskDO(); task.setTaskId(request.getTaskId()); task.setStatus(PROCESSING); task.setRequestJson(JSON.toJSONString(request)); taskMapper.insert(task); // 异步执行避免阻塞主线程 CompletableFutureAgentExecutionResult future CompletableFuture .supplyAsync(() - executeHarness(request)) .exceptionally(ex - { task.setStatus(FAILED); task.setErrorMsg(ex.getMessage()); taskMapper.updateById(task); return null; }); future.thenAccept(result - { if (result ! null) { task.setStatus(SUCCESS); task.setResultJson(JSON.toJSONString(result)); taskMapper.updateById(task); } }); } }这套逻辑的核心在于任务管理完全走数据库Harness 只负责“思考”业务系统负责“记录”。哪怕 Harness 崩了任务状态还在数据库里可以重跑。3.2 同步改异步让工单后台不卡死一开始我图省事直接在 Controller 里同步调harnessSession.run()结果前端等 15 秒才返回接口超时被网关拦截。后来改成异步才真正符合生产场景。RuoYi-Vue-Plus 自带Async和线程池但直接用默认的ThreadPoolTaskExecutor有一个隐患它的队列默认是无界的大流量下会把内存吃满。这部分问题后面的翻车现场会细讲。改造后的做法是在 Service 层用Async(harnessExecutor)并自定义线程池Bean(harnessExecutor) public ThreadPoolTaskExecutor harnessExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(8); executor.setQueueCapacity(1000); executor.setThreadNamePrefix(harness-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }这里CallerRunsPolicy很关键队列满了之后任务会在提交线程里同步运行虽然会阻塞主线程但至少不会丢失任务。比直接抛RejectedExecutionException要好得多。3.3 上下文记忆在 RuoYi 数据库里存会话Agent 的能力很依赖上下文。Harness 提供了内存会话但应用重启就丢。我选择把会话历史落到 RuoYi 的 MySQL 里每次调用前拉取最近 20 条消息组装成 Harness 认识的格式。表结构大概是CREATE TABLE harness_conversation ( id bigint auto_increment primary key, session_id varchar(64) not null, role varchar(16) not null, content text not null, create_time datetime not null, index idx_session_time(session_id, create_time) );然后写一个简单的组装方法private ListHarnessMessage buildMessages(String sessionId) { ListHarnessConversationDO history conversationMapper.selectRecent(sessionId, 20); return history.stream() .map(item - new HarnessMessage(item.getRole(), item.getContent())) .collect(Collectors.toList()); }注意不要一股脑把所有历史都塞给模型token 限制会打脸。后面会有一个关于 token 超限的兜底方案。4. 与 RuoYi-Vue-Plus 现有体系的融合权限、菜单、日志、WebSocket实现完核心逻辑还只是跑通了功能真正要落地到 RuoYi 这种后台管理系统里还得把用户体系、菜单权限、操作日志和流式推送全部打通。这一节也是“集成”二字的分量所在。4.1 权限继承不同角色有不同的 Agent 可用RuoYi 的用户体系很成熟sys_user、sys_role、sys_user_role、sys_menu层层关联。我不需要另建权限表直接在AgentTaskRequest里带上userId然后在 Harness 的系统提示词里动态注入String systemPrompt String.format( 你是客服助手当前用户ID为%s用户等级%s部门%s。请据此提供个性化答案。, userId, userLevel, deptName );更精细的权限控制可以利用 RuoYi 的菜单权限来控制哪些角色能访问哪些智能体功能。我在后端 Controller 上加了PreAuthorize(ss.hasPermi(harness:session:create)) PostMapping(/session) public AjaxResult createSession(RequestBody HarnessSessionReq req) { ... }然后在sys_menu里添加了一个目录“智能体”下面挂上“会话管理”“任务中心”“工具注册”等按钮权限。这样用户没被授权连按钮都看不到非常省心。4.2 流式输出的 WebSocket 通道大模型流式返回体验比等待整个响应好太多。RuoYi-Vue-Plus 自带 WebSocket 支持我在此基础上加了一个/harness/ws端点。流程是前端页面创建任务后打开 WebSocket 订阅会话ID后端在HarnessAgentServiceImpl的异步方法里通过WebSocketSession推送消息片段。这里有个极易踩的坑RuoYi 的 WebSocket 握手本身是认证过的但长连接期间如果用户 Token 过期服务端并不知道。我增加了定时校验Scheduled(fixedRate 60000) public void checkWebSocketAlive() { for (WebSocketSession session : sessionManager.getSessions()) { Long userId (Long) session.getAttributes().get(userId); if (!loginUserService.isLogin(userId)) { session.close(CloseStatus.POLICY_VIOLATION); } } }60 秒轮询一次虽然不算实时但能有效防止死连接占满连接池。4.3 日志与审计把每次 Agent 调用记录到 sys_oper_logRuoYi 的Log注解很方便但它在异步线程里不好用因为操作员信息拿不到。我干脆不依赖注解直接在 Harness 任务回调里手动记录日志OperLogService.recordOper( SecurityUtils.getUserId(), // 当时通过上下文传入存在 task 字段里 harness, 执行智能体任务, RequestMethod.POST, taskId, JSON.toJSONString(request), JSON.toJSONString(result), costTimeMs );这样审计信息可以追溯到每个用户做了什么操作智能体输出了什么耗时多少。将来做成本分析、合规审计都靠这张表。5. 翻车现场版本冲突、OOM 与模型返回不稳定接下来说三个我在实测阶段真实遇到、且排查过程有点曲折的问题。每个问题我都会按“现象 - 排查 - 解决”的顺序讲希望对你有参考。5.1 jackson-databind 冲突引发的 JsonMappingException现象任务提交后Harness 内部反序列化响应时直接抛JsonMappingException: Unknown property suggestions。排查一开始以为是我的 DTO 少字段加上之后还是报错。于是用mvn dependency:tree -Dincludescom.fasterxml.jackson.core看依赖树发现 Harness 客户端传递进来的 jackson-databind 是 2.13而 RuoYi 已经用到 2.15。2.15 默认关闭了某些允许未知属性的配置导致老库编译产物里写的字段名和我这边不一致。解决按照 2.1 节的 exclusion 把旧 jackson 排除掉统一用 RuoYi 的版本。同时还可以在 ObjectMapper 里配置FAIL_ON_UNKNOWN_PROPERTIESfalse做兜底这样即使字段多了也不至于崩。5.2 智能体线程池导致的内存增长现象上线做了三天压测内存持续上涨GC 之后也回不到稳定水位最终 OOM。排查dump 堆后看到大量LinkedBlockingQueue里的Runnable对象堆积每个对象都引用着整个 HarnessSession。原因就是我前面提到的默认线程池队列无界异步任务全堵在队列里每个 session 又持有大模型上下文内存直接被撑爆。解决换了自定义线程池队列改成有界ArrayBlockingQueue(1000)并且设置了CallerRunsPolicy。同时给每个会话设置最大空闲时间超过 30 分钟自动销毁 session 并释放上下文。改完再压测内存曲线稳定。这里我给个具体估算方法每个 session 平均占 50KB 上下文内存1000 个队列任务就是 50MB看起来不大但如果 session 还持有工具调用的中间结果可能会涨到几百 KB。把队列限制住就等于给内存设了闸门。5.3 模型返回超时与上下文截断现象每天下午高并发时间接口偶尔会等 30 秒以上才返回部分任务直接超时报错。而且越长对话越容易失败。排查先看日志Harness 客户端报的普遍是ReadTimeout。原因是模型服务在高峰时响应变慢而我客户端的 readTimeout 设置成了 10 秒。后来把 readTimeout 提高到 45 秒后超时问题缓解但长对话开始出现 token 超限的 400 错误。解决双管齐下。一是超时参数按照模型服务的 P99 延迟动态调整我取了一个相对保守的值连接超时 10 秒读超时 45 秒。二是做了对话历史的 token 裁剪策略private ListHarnessMessage tokenAwareTrim(ListHarnessMessage messages, int maxTokens) { ListHarnessMessage result new ArrayList(); int total 0; for (int i messages.size() - 1; i 0; i--) { HarnessMessage msg messages.get(i); int tokens estimateTokens(msg.getContent()); if (total tokens maxTokens) { // 超过限制截断当前消息 msg.setContent(msg.getContent().substring(0, Math.min(500, msg.getContent().length()))); result.add(0, msg); break; } result.add(0, msg); total tokens; } return result; }token 估算可以用简单的字符数 / 2作为启发式规则中文场景下比较接近。这样即使模型输入长度有限制最多也只是丢一些较老的消息不会崩。6. 上线后的配置反思与后续扩展集成做完稳定运行了两个月有些心得体会值得单独说。尤其是配置管理千万别一股脑写死在代码里。6.1 哪些配置我建议写进 Nacos 而不是 yml我的经验是与模型行为相关的参数尽量放 Nacos因为调优频繁与系统架构相关的参数尽量放 yml因为变动很少。配置项推荐位置原因模型 API KeyNacos可能随时轮换温度temperatureNacos不同场景需要实时调最大 token 数Nacos模型版本升级后需要调整会话空闲超时Nacos灰度期间可能频繁改线程池核心线程数yml与部署环境强相关一般固定Harness 版本号yml版本升级跟着应用发布走搞清这个边界能避免很多“配置在 Nacos 里找一上午”的情况。6.2 从单机到多节点分布式锁和任务队列的切换思考现在这个实现是单机可用的但如果以后 RuoYi 多节点部署就会遇到两个问题一是异步任务会重复执行同一个工单可能被两个节点同时拉起二是 Harness 会话状态存在本地内存节点切换后上下文丢失。我在真正上线前预留了改造方案用 Redis 分布式锁在submitTask里对taskId加锁保证同一任务只被一个节点处理。把异步任务丢进 MQ比如 RocketMQ消费方从 MQ 里拉任务这样天然保证单任务单消费。Harness 会话状态迁移到 Redis用sessionId为 key消息列表用 List 结构存储。这些改动工程量不小但方向很明确。如果你一开始就要上多节点建议直接在 Service 层用 MQ 异步而不是本地线程池。最后说一点个人感受把 AgentScope Harness 装进 RuoYi-Vue-Plus技术上不是难事但真正吃时间的不是编码而是反复理清“哪些事该 Harness 管哪些事该 RuoYi 管”。我最终选择了一个薄薄的 Service 层来隔离两者加上动态配置和兜底策略运行起来相当省心。如果你的团队也是纯 Java正在为智能体到底怎么落地发愁希望这篇能给你一个清晰的起点。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑