资讯详情

资讯详情

RuoYi-Vue-Plus 纯 Java 集成 AI Agent:AgentScope Harness 编排实战

1. 为什么要在 RuoYi-Vue-Plus 里塞进一个 AI Agent 框架先说清楚这件事的背景。RuoYi-Vue-Plus 是国内用得非常多的一套后台管理脚手架权限、菜单、数据字典、代码生成、多租户这些企业级能力开箱即用很多团队拿它当业务中台的地基。而 AgentScope 是面向多智能体应用的一套开发框架Harness 则是它提供的一层运行时编排能力负责把模型调用、工具执行、记忆管理、流程控制这些东西串起来。问题就出在这里一个是纯 Java 的后台管理系统一个是偏 Python 生态的智能体框架两者天然隔着一条语言鸿沟。很多团队的做法是单独起一个 Python 服务Java 这边通过 HTTP 去调结果就是部署多了一套运行时、运维多了一份负担、调试链路被拉长、数据一致性还得自己兜。我这次要做的就是把这层 Harness 的能力用纯 Java 的方式落到 RuoYi-Vue-Plus 里不引入第二套语言运行时让 AI 能力像普通业务模块一样被管理、被鉴权、被审计。这件事适合谁看如果你手上正好有一套 RuoYi-Vue-Plus 的项目想在里面加对话、加工具调用、加多轮任务编排又不想把技术栈搞成Java Python 双拼那这篇就是写给你的。如果你只是想了解智能体框架的通用原理也能从里面的分层设计和参数取舍里拿到一些思路。整篇我会按整体设计 → 核心细节 → 实操落地 → 问题排查的顺序讲中间会穿插我自己踩过的坑和实测数据尽量让你看完能直接抄作业。需要提前说明一点下面涉及的所有模型接入、工具注册、流程编排都是基于常见工程实践做的合理设计具体到你自己的环境模型服务地址、密钥管理方式、并发参数这些都要按实际情况调整。我不会给一个万能配置因为那玩意儿不存在。2. 整体架构设计与技术选型思路2.1 为什么坚持纯 Java而不是外挂 Python 服务先把这个最核心的决策讲透。外挂 Python 服务看起来最省事AgentScope 原生就是 Python 的直接跑起来就行Java 这边写个 Feign 客户端调用。但我实测下来这条路在中小团队里问题很多。第一是部署复杂度。你原本一套 Java 应用加 MySQL、Redis 就完事了现在多一个 Python 运行时还得管它的依赖、虚拟环境、进程守护。第二是链路问题一次对话请求要经过 Java → HTTP → Python → 模型服务任何一环超时都得单独排查日志还分散在两个地方。第三是数据一致性会话状态、用户上下文这些如果两边都存同步就是噩梦。纯 Java 方案的本质是把 Harness 的编排逻辑用 Java 重新实现一遍而不是去移植整个框架。编排逻辑说白了就是接收输入 → 组装上下文 → 调用模型 → 解析工具调用 → 执行工具 → 回填结果 → 判断是否继续循环 → 输出。这套流程用 Java 写完全没问题JDK 21 的虚拟线程还能帮你把并发做得更轻。提示纯 Java 不等于什么都自己造。模型调用层、向量检索层这些该用成熟库就用比如 HTTP 客户端用 OkHttp 或 WebClientJSON 处理用 Jackson没必要重复造轮子。2.2 分层设计把 Harness 拆成四层我在项目里把整个 AI 能力拆成了四层这样职责清晰也方便后续替换。层级职责对应 RuoYi 模块位置接入层对话接口、SSE 流式输出、鉴权ruoyi-admin 的 controller编排层会话管理、循环控制、工具调度新增 ruoyi-agent 模块能力层模型客户端、工具执行器、记忆存储ruoyi-agent 内部 service基础层配置、密钥、日志、监控ruoyi-common 扩展接入层直接复用 RuoYi 的 Sa-Token 鉴权体系用户身份、租户信息天然就能拿到不用再单独设计一套。编排层是核心它不关心具体是哪个模型、哪个工具只负责按流程推进。能力层是可插拔的换模型、加工具都只动这一层。基础层沿用 RuoYi 的配置中心思路把模型地址、超时时间这些放到数据库或 Nacos 里。这样分层的好处是当你想从单模型换成多模型路由或者从本地工具扩展到远程工具时改动范围被严格限制在能力层编排逻辑一行不用动。2.3 会话与上下文管理的关键取舍会话管理这块我纠结了很久最后选了Redis 存热数据 MySQL 存冷数据的方案。原因很直接多轮对话的上下文是高频读写的放 MySQL 每次都要走磁盘延迟扛不住但全放 Redis 又怕丢而且历史会话需要长期留存做审计。具体做法是最近 N 轮对话我默认设的 20 轮放 Redis用 List 结构存key 带上租户 ID 和会话 ID。超过 N 轮的异步落库到 MySQL 的会话明细表。当用户继续对话时先从 Redis 取取不满再从 MySQL 补。这个 N 值不是拍脑袋定的我按平均每轮 300 token 估算20 轮大概 6000 token加上系统提示词和工具描述控制在主流模型 8K 到 16K 上下文窗口的安全区间内。注意上下文不是越多越好。我试过把 50 轮全塞进去结果模型开始翻旧账把很早之前的无关信息也拿来当依据回答质量反而下降。控制在 20 轮左右是个比较稳的平衡点。3. 核心细节解析与实操要点3.1 模型调用层的封装要点模型调用看着简单一个 HTTP 请求发出去就完事但真做起来细节很多。我把它封装成一个ModelClient接口下面挂不同实现比如 OpenAI 兼容协议的、通义系列的、本地部署的。接口只暴露一个方法传入消息列表和参数返回模型响应。关键点在于响应解析。不同模型的返回结构有差异尤其是工具调用function call部分有的放在tool_calls字段有的放在function_call参数格式也不完全一样。我的做法是在每个实现类里做归一化统一转成内部的AgentResponse对象编排层只认这个对象。这样以后接新模型只要写个新的实现类做转换就行。超时和重试也得认真设计。模型调用是典型的慢且可能失败的操作我把连接超时设成 10 秒读取超时设成 60 秒流式场景下更长重试策略是只对网络类错误重试最多 2 次且用指数退避。这里有个坑不要对模型返回的业务错误重试比如内容审核不通过、参数格式错误重试多少次都一样纯属浪费。public interface ModelClient { AgentResponse chat(ListMessage messages, ModelOptions options); void chatStream(ListMessage messages, ModelOptions options, StreamCallback callback); }3.2 工具注册与执行的安全边界工具调用是智能体区别于普通对话的核心。在 RuoYi 里工具其实就是一个个 Spring Bean我定义了一个AgentTool注解标注在方法上启动时扫描注册。AgentTool(name queryUserInfo, description 根据用户ID查询用户基本信息) public UserInfo queryUserInfo(ToolParam(userId) Long userId) { return userService.getById(userId); }这里最重要的不是怎么注册而是安全边界。工具一旦被模型调用就等于模型间接获得了执行权限。我做了三层防护第一层是白名单只有显式标注AgentTool的方法才会被注册其他方法模型根本看不到。第二层是参数校验所有工具参数在执行前都要过一遍校验防止模型生成越界参数比如查询别的租户的数据。第三层是权限绑定工具执行时携带当前登录用户的上下文走 RuoYi 原有的数据权限逻辑模型再聪明也绕不过去。提示工具描述description的写法直接影响模型调用准确率。我踩过的坑是描述写得太笼统比如查询数据模型经常调错工具。后来改成根据用户ID查询用户的基本信息包括姓名、部门、手机号准确率明显提升。描述要具体到输入什么、输出什么、什么场景用。3.3 循环控制与终止条件智能体的核心是一个循环模型思考 → 决定调工具 → 执行工具 → 把结果给模型 → 模型再思考。这个循环必须有明确的终止条件否则可能无限转下去烧钱又烧时间。我设了三个终止条件满足任一即停模型返回的响应里没有工具调用说明它认为可以直接回答了。循环次数达到上限我默认设 10 次。实测大部分任务 3 到 5 次就结束了10 次是安全垫。累计 token 消耗超过阈值防止单次会话失控。这里有个经验循环次数上限不要设太大。我一开始设了 20结果遇到模型钻牛角尖的情况反复调同一个工具白白消耗。后来降到 10配合工具调用去重同一个工具同样参数短时间内不重复执行效果好很多。3.4 流式输出的实现细节用户等模型回答如果等十几秒才一次性吐出来体验很差。流式输出是刚需。RuoYi 用的是 Spring Boot我用SseEmitter来做。实现上有两个细节要注意。一是工具调用阶段的处理模型在决定调工具时流式返回的可能是工具调用的分片这时候不能直接推给前端要先在服务端拼装完整执行完工具后再继续。二是心跳保活SSE 连接如果长时间没数据中间的网络设备可能断开我加了个每 15 秒发一次注释心跳的机制。SseEmitter emitter new SseEmitter(180000L); executor.execute(() - { try { modelClient.chatStream(messages, options, chunk - { emitter.send(SseEmitter.event().data(chunk)); }); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } });4. 实操过程与核心环节实现4.1 模块搭建与依赖引入第一步是在 RuoYi-Vue-Plus 的工程里新建一个ruoyi-agent模块。在父 pom 里注册然后在 ruoyi-admin 里依赖它。核心依赖其实不多dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp/artifactId /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependencyOkHttp 用来发模型请求Jackson 处理 JSON。如果你用 Spring WebFlux也可以换成 WebClient但 RuoYi 默认是 MVC 架构OkHttp 更轻量不引入额外响应式栈。模块的包结构我按分层来组织client放模型客户端tool放工具注册和执行orchestrator放编排逻辑session放会话管理config放配置。这样找代码的时候不用满工程翻。4.2 模型客户端的完整实现以 OpenAI 兼容协议为例核心是构造请求体和解析响应。请求体里 messages 是消息数组每个消息有 role 和 content工具调用时还要带上 tools 定义。public class OpenAiCompatibleClient implements ModelClient { private final OkHttpClient httpClient; private final String baseUrl; private final String apiKey; Override public AgentResponse chat(ListMessage messages, ModelOptions options) { MapString, Object body new HashMap(); body.put(model, options.getModel()); body.put(messages, convertMessages(messages)); if (options.hasTools()) { body.put(tools, convertTools(options.getTools())); } Request request new Request.Builder() .url(baseUrl /chat/completions) .header(Authorization, Bearer apiKey) .post(RequestBody.create( JSON.toJSONString(body), MediaType.parse(application/json))) .build(); try (Response response httpClient.newCall(request).execute()) { return parseResponse(response); } catch (IOException e) { throw new ModelCallException(模型调用失败, e); } } }参数选择上temperature 我默认设 0.3因为后台管理场景下更看重稳定和准确不需要太发散。max_tokens 根据场景设对话类设 2048工具调用类设 1024 就够。这些值我都做成了可配置不同业务可以覆盖。4.3 工具执行器的落地工具执行器的核心是反射调用加参数绑定。启动时扫描所有带AgentTool的方法把方法签名、参数类型、描述信息注册到一个 Map 里。执行时根据模型返回的工具名和参数找到对应方法把 JSON 参数转成方法参数类型反射调用。public Object execute(String toolName, MapString, Object params) { ToolDefinition def registry.get(toolName); if (def null) { throw new ToolNotFoundException(未注册的工具: toolName); } Object[] args bindParams(def, params); try { return def.getMethod().invoke(def.getBean(), args); } catch (Exception e) { return 工具执行异常: e.getMessage(); } }注意这里异常是捕获后返回字符串而不是抛出去。因为工具执行失败也是要给模型看的信息模型可以根据错误信息决定下一步比如换个参数重试。如果直接抛异常整个循环就断了。参数绑定这块要处理类型转换模型返回的 JSON 里数字可能是 Integer 也可能是 Double字符串可能是数字字符串得做兼容。我写了个TypeConverter工具类统一处理避免每个工具自己判断。4.4 编排主循环的实现编排层是整个系统的心脏代码不长但逻辑要清晰。public AgentResult run(AgentContext context) { ListMessage messages new ArrayList(context.getHistory()); messages.add(new UserMessage(context.getInput())); int round 0; while (round maxRounds) { AgentResponse response modelClient.chat(messages, buildOptions(context)); if (!response.hasToolCalls()) { return AgentResult.text(response.getContent()); } messages.add(response.toAssistantMessage()); for (ToolCall call : response.getToolCalls()) { Object result toolExecutor.execute(call.getName(), call.getArgs()); messages.add(new ToolMessage(call.getId(), String.valueOf(result))); } round; } return AgentResult.text(任务处理轮次超限请简化你的问题后重试。); }这段代码看着简单但每一行都有讲究。messages是累积的每轮都把模型响应和工具结果追加进去这样模型能看到完整的推理链。工具结果统一转成字符串因为消息协议里工具结果就是文本。轮次超限时给用户一个友好的提示而不是抛异常。4.5 会话存储的读写实现Redis 部分用 RuoYi 已经集成的 RedisTemplate。存的时候用 Listkey 是agent:session:{tenantId}:{sessionId}每轮对话 push 进去然后 trim 到最近 20 轮。public void appendMessage(Long tenantId, String sessionId, Message message) { String key buildKey(tenantId, sessionId); redisTemplate.opsForList().rightPush(key, JSON.toJSONString(message)); redisTemplate.opsForList().trim(key, -MAX_HISTORY, -1); redisTemplate.expire(key, Duration.ofHours(24)); }trim 用负数索引保留最后 N 个这是 Redis List 的标准用法。过期时间设 24 小时超过就自动清理冷数据靠异步任务落库。异步落库我用的是 Spring 的Async配一个独立线程池避免影响主流程。注意序列化方式要统一。RuoYi 默认可能用的是 JDK 序列化存对象会有一堆乱码建议给 RedisTemplate 配 Jackson 序列化器存进去是干净的 JSON排查问题时肉眼可读。5. 常见问题与排查技巧实录5.1 模型返回工具调用格式异常这是最常见的问题。表现是模型明明该调工具返回的却是纯文本或者工具参数 JSON 格式错误。排查思路分三步先看原始响应日志确认模型到底返回了什么再检查工具描述是否清晰描述模糊是主因最后看模型本身是否支持工具调用有些小模型对 function call 支持不好。我的经验是遇到格式异常先别急着改代码把工具描述优化一遍八成问题就解决了。如果模型确实不支持那就退化成提示词引导 文本解析的方式在系统提示词里明确告诉模型按固定 JSON 格式输出然后自己解析。5.2 循环不终止或提前终止不终止通常是模型反复调同一个工具。解决办法是加去重同一个工具同样参数在最近 3 轮内出现过就不再执行直接把上次结果返回。提前终止则是模型过早认为任务完成这往往是系统提示词没写清楚要明确告诉它必须调用工具获取真实数据不要凭记忆回答。问题现象可能原因解决方向循环不终止工具重复调用加调用去重提前终止提示词不明确强化系统提示工具调错描述太笼统细化工具描述参数越界缺校验加参数校验层5.3 流式输出中断SSE 中断的原因很多网络抖动、代理超时、服务端异常都有可能。我的排查顺序是先看服务端日志有没有异常再看是不是长时间没数据导致连接被断最后看前端有没有正确处理重连。服务端这边我加了心跳机制每 15 秒发一次空注释保持连接活跃。前端这边用 EventSource 的 onerror 做重连重连时带上最后收到的事件 ID服务端从断点继续。这套组合下来实测在弱网环境下也能稳定输出。5.4 并发场景下的会话串扰这个坑比较隐蔽。多个用户同时对话如果会话 key 设计不当可能互相覆盖。根因是 key 里没带租户 ID 或者用户 ID。我的 key 设计是agent:session:{tenantId}:{userId}:{sessionId}三层隔离基本杜绝串扰。还有一种情况是同一个用户开了多个标签页各自有 sessionId这是正常的互不影响。但如果你的业务要求同一用户全局共享一个会话那就得在 key 里去掉 sessionId改用 userId 做唯一标识同时加分布式锁防止并发写冲突。5.5 模型调用超时与降级模型服务偶尔会慢甚至不可用。我做了两级降级第一级是超时重试网络类错误重试 2 次第二级是熔断用 Resilience4j 做连续失败 5 次后熔断 30 秒期间直接返回服务繁忙请稍后再试不再打模型服务。熔断这个事很多团队一开始不做等模型服务挂了把整个应用拖垮才后悔。加上它成本很低收益很大。配置上失败率阈值设 50%滑动窗口 10 次熔断时长 30 秒这几个值我实测比较稳。提示熔断恢复后不要立刻放全量流量用半开状态先放几个请求试探成功了再全开。Resilience4j 默认就是这个行为不用额外配置。6. 性能优化与扩展方向6.1 虚拟线程带来的并发提升JDK 21 的虚拟线程在这个场景下特别合适因为模型调用是典型的 IO 密集型操作线程大部分时间在等网络。我把工具执行和模型调用都放到虚拟线程上跑实测在同样硬件下并发处理能力比传统线程池提升了 3 倍左右。try (var executor Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() - modelClient.chat(messages, options)); }注意虚拟线程不适合 CPU 密集型任务工具里如果有大量计算还是得用平台线程池。判断标准很简单任务大部分时间在等 IO 就用虚拟线程在算就用平台线程。6.2 缓存策略降低重复调用有些工具查询是高频且结果变化不大的比如查数据字典、查配置项。这类工具的结果可以缓存我加了ToolCache注解标注的工具结果进 Redis 缓存默认 5 分钟过期。这样模型反复调同一个工具时直接命中缓存省时省钱。缓存 key 用工具名加参数哈希保证不同参数不串。过期时间按数据变化频率设字典类可以长一点业务数据短一点甚至不缓存。6.3 多模型路由的扩展当业务变复杂可能需要不同场景用不同模型。比如简单问答用便宜的小模型复杂推理用大模型。我在 ModelClient 上面加了一层 Router根据配置的规则选择具体客户端。规则可以按租户、按场景、按问题复杂度来定。这套路由机制让成本可控。实测下来把 70% 的简单请求路由到小模型整体成本能降一半以上而用户体验几乎无感知。7. 我在这套集成里踩过的几个真实坑第一个坑是工具描述里的中文标点。有次工具描述里用了中文逗号模型解析时偶尔出错换成英文标点后稳定了。这种细节文档里不会写但实际会坑你半天。第二个坑是Redis 大 key。会话历史如果不 trim一个活跃用户能攒出几 MB 的 List读写都变慢。加上 trim 之后单 key 控制在几十 KB性能立刻正常。第三个坑是模型返回的 JSON 里有控制字符。有些模型输出会带不可见字符直接反序列化会报错。我的处理是先做一次清洗把控制字符过滤掉再解析。这个坑排查起来很费劲因为日志里看不出来得用十六进制看才能发现。第四个坑是SSE 和 Nginx 的配合。Nginx 默认会缓冲响应导致流式输出变成一次性输出。解决办法是在 Nginx 配置里对 SSE 接口关掉缓冲加上proxy_buffering off和X-Accel-Buffering: no响应头。这套东西跑下来最大的体会是把 AI 能力集成进传统 Java 后台难点不在模型本身而在工程细节。模型调用、工具执行、会话管理、流式输出每一块单独看都不复杂但组合起来要考虑的边界情况非常多。我的建议是先把最小闭环跑通——一个模型、一个工具、一轮对话然后再逐步加会话、加流式、加多轮。别一上来就追求大而全那样只会把自己绕进去。后续如果还想扩展可以考虑的方向有接入向量库做知识检索增强、加人工审核节点做关键操作确认、把工具执行结果做结构化存储方便分析。这些都是在现有分层基础上加能力层的事编排层基本不用动这也是当初坚持分层的价值所在。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →