把 AgentScope Harness 装进 RuoYi-Vue-Plus 这件事乍听上去像是硬要把两条生态扭在一起。但真的做下来你会发现问题不在技术能不能通而在你用哪一层去通。AgentScope 是 Python 生态里的多智能体编排框架它的 harness 会拉起一个交互式管理服务用于查看智能体状态、发起对话、观察消息流RuoYi-Vue-Plus 则是 Java 团队最常用的后台管理系统脚手架围绕它你会有一套现成的用户、菜单、权限、部门体系。这篇记录的是一个真实落地过程我把 harness 作为一个独立的 Python 服务跑起来再在 RuoYi 侧写一层 REST 网关把创建智能体、对话、日志查询全部接回 Java 后台并用 SSE 把流式回复转发到前端。适合的人群很明确后台是 Java但想用 AgentScope 做多智能体应用不想让 Python 服务各自为政的团队。先说结论这套方案跑通之后团队里真正接触 Python 代码的可能只有两三个人其他所有功能都还是在熟悉的 Java 后台里完成。所谓纯 Java AI 平台并不是真的用 Java 去重写智能体内核而是让 Java 侧拿到完整的控制权和集成边界AI 引擎则作为被隔离调度的运行时。这篇文章我会从架构选型讲起一直落到核心表设计、Java 代理层写法、Python 侧适配和常见踩坑点尽量把它讲成一份能直接抄作业的实践记录。1. 为什么要在 Java 后台里塞进 AgentScope Harness1.1 先理清两边的定位这事儿才不拧巴AgentScope 在多智能体应用开发里干的事是用声明式的方式组织多个智能体一个负责接收用户输入一个负责调用外部工具一个负责汇总答案再用消息流把这些节点串起来。它的运行形态通常是 Python 服务启动后由 harness 提供一个基于 Web 的交互界面在界面上可以看到每个智能体的输入、输出、中间状态也能手动触发下一轮对话。对于 AI 研发来说这个界面是调试的好帮手但对于一个企业的统一管理系统来说它完全是一个孤岛。RuoYi-Vue-Plus 是另一套东西。它基于 Spring Boot 和 Vue3自带用户、角色、菜单、操作日志、代码生成这些企业后台几乎人人都要用的能力。团队里的 Java 工程师已经非常习惯Controller–Service–Mapper这套开发方式也有一套成熟的发布和运维流程。当 Python 侧的 harness 要进入这个体系时如果直接把它的网页丢给用户身份对不上、权限对不上、日志对不上更别提跟工单、订单这类业务数据做关联。1.2 集成之后业务上到底改变了什么集成以后用户看到的不再是去另一个地址打开 harness而是后台菜单里多了一项智能体管理。在这个菜单下运营人员可以创建智能体实例、配置运行参数、查看当前状态、发起一轮对话、翻历史消息。会话的创建人就是后台用户表里的那个人消息记录落在自己的业务数据库里可以做审计也可以跟客户档案、工单系统打通。其中一个很容易被低估的价值是可管理性。harness 自带界面虽然方便但它不会把操作记录写到你的审计表也不会触发你公司的告警。通过 RuoYi 网关接入后所有对智能体的操作都能被拦截、记录和授权校验。多个智能体项目不再需要各自维护入口统一在一个后台里就能看到所有服务实例的健康状态。说白了这套集成的价值不是AI 本身多复杂而是让 AI 能力变成了公司基础设施的一部分而不是某个 Python 脚本的附属品。1.3 什么团队最适合做这件事我接触过的场景里收益最大的团队是那种已经有成熟 Java 后台AI 侧还处于小规模验证阶段的团队。这种团队往往不缺想法缺的是把 Python AI 服务纳入正规研发体系的工程化手段。把 AgentScope harness 当做一个可被 HTTP 调用的智能体引擎接入Java 团队不需要去学 Python 异步编程也不需要维护一套新的前端所有工程规范都还能沿用下来。如果你所在团队的背景类似这篇文章的方法论可以直接复用到另一个多智能体框架上思路是通用的。2. 整体架构设计中间夹一层网关2.1 为什么不用消息队列解耦第一版方案里我一度想用消息队列做异步解耦Java 后端把用户输入写到队列Python 侧消费后执行推理再写回结果队列。听起来很干净可一联调就发现它不适合会话型交互。多智能体对话是双向、实时、多轮的用户每说一句话就在等推理结果。走写入队列–消费–回写–通知这条链路任何一环有延迟都会让体验断掉还得额外处理消息幂等和顺序问题。会话本质上不是事件流而是一组带上下文状态的请求-响应用 HTTP 同步模型反而顺理成章。于是最终选了网关代理模式RuoYi 后端作为唯一入口把用户请求翻译成对 harness 的调用。前端只跟 RuoYi 打交道RuoYi 内部通过一个带内部密钥的客户端去调用 Python 服务。这个模式的优点是链路短、排障直观还能直接复用 Java 的权限认证、参数校验能力。对 Python 侧而言它只需面对 RuoYi 一个可靠调用方不必暴露公网、处理 CORS、纠结用户鉴权。把复杂的状态留在 Python把业务边界留在 Java这是整套设计的一条主线。2.2 三类核心链路的设计落地时我把交互链路分成了三类各自选择不同技术方案。同步查询类处理 harness 版本、智能体实例列表、运行状态这类短请求直接由 Java 转发 GET 请求把结果包装为统一响应返回。实时交互类处理发起对话、发送消息等需要持续接收 token 流或事件流的场景我用 SSE 来做。Java 先向 Python 侧发起 POST 请求Python 返回text/event-streamJava 再用 Spring 的 SseEmitter 把流数据转发给浏览器。状态回写类处理多智能体后台任务比如一个需要轮询外部接口的智能体在任务中途会改变自身状态这里由 Python 主动向 RuoYi 注册的回调地址推送RuoYi 收到后更新本地表并通知在线用户。前端 Vue3 ── RuoYi 网关Java ── Python Harness 服务 ── AgentScope 智能体 ^ | |────────── SSE 流式数据转发 ←───────────三类链路的本质区别在于同步性短请求适合同步长回复适合流式后台任务适合回调。我先固定了接口协议让前端统一认为所有交互都经过 RuoYi 网关这样后续即便把 harness 换成其他引擎Java 和前端的大部分代码都不用大改。2.3 认证与权限是如何打通的RuoYi 本身用 JWT 做身份认证后端拦截器会校验请求头里的 token 并解析用户信息。Python 的 harness 并不知道这套体系我没有选择把 JWT 原样透传过去原因有二一是 harness 不验签传过去也不会执行同一套校验逻辑二是万一接口被绕过等于把内部身份能力交给了另一个进程。最终采用内部密钥RuoYi 调用 Python 接口时携带X-Harness-TokenPython 侧用环境变量配置同一个密钥校验通过才放行。这样就有两个隔离层对外由 RuoYi 负责用户身份对内由内部密钥保护 Python 服务。权限细粒度控制在 Java 侧完成比如这个用户能不能创建智能体能不能查看某个会话。Python 侧只关心一件事这是不是 RuoYi 发来的合法请求。提示内部密钥不要硬编码到配置里放在环境变量或配置中心。一旦泄露Python 侧的服务就完全暴露给了内网任意调用方。3. Java 侧落地数据模型、代理服务与 SSE 转发3.1 先把数据表设计好再加业务耦合点智能体数据要分定义和实例两层。定义层是模板描述智能体用什么配置、运行在哪个 harness 地址实例层是用户真正创建出来的运行单元状态独立可能随时启停。这样同一个智能体模板可以被复制出多个隔离实例互不影响。CREATE TABLE biz_agent_definition ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, agent_key VARCHAR(64) NOT NULL, harness_url VARCHAR(255) NOT NULL, default_config JSON, status CHAR(1) DEFAULT 1, create_time DATETIME ); CREATE TABLE biz_agent_instance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, definition_id BIGINT NOT NULL, owner_user_id BIGINT NOT NULL, config_json JSON, current_status VARCHAR(16) DEFAULT stopped, last_heartbeat DATETIME, create_time DATETIME );紧接着是会话表和消息表。会话表记录一次完整对话上下文字段包括agent_instance_id、user_id、title、status消息表记录每轮消息字段有conversation_id、sender_typeuser/agent、content、msg_type、sequence。这里的sequence很关键因为多智能体的回复可能有多个中间结果需要按照序号组织显示顺序。把表设计好以后业务耦合点就可以放在服务层里处理。比如创建会话前先校验用户是否有该智能体实例的使用权限发送消息前判断实例状态是否为 running回声写逻辑时统一记录审计日志。3.2 控制层与门面 API 的实现我把门面 API 放在AgentController里统一以/agent开头。所有接口的第一件事都是解析当前用户第二件事是记录审计日志第三件事才是真正调用代理服务。核心接口大概有这几个创建智能体实例、查询实例状态、创建会话、发送消息、获取会话历史、删除会话。发送消息最常用签名大致如下PostMapping(/instances/{instanceId}/chat) public SseEmitter chat( PathVariable Long instanceId, RequestBody ChatRequest request, RequestHeader(Authorization) String token) { Long userId JwtUtil.getUserId(token); AgentInstance instance agentInstanceService.getById(instanceId); if (!agentInstanceService.canUse(userId, instance)) { throw new ServiceException(无该实例的使用权限); } SseEmitter emitter new SseEmitter(0L); agentProxy.forwardChat(instance, request, userId, emitter); return emitter; }SseEmitter(0L)里的 0L 表示不设置超时因为多智能体推理可能长达几十秒甚至更久。要特别注意必须保证方法正常返回 SseEmitter 对象Spring 才会把响应切到异步模式。如果你在中途直接抛异常前端拿到的就是一个 500而不是流式的错误事件。实际项目里我通常还会在连接建立后立刻发送一个connected事件让前端明确知道链路已通。3.3 代理服务层与 SSE 转发细节代理服务层是这套集成的心脏。我选择用 Spring 的 WebClient 而不是 RestTemplate原因是 WebClient 能更自然地处理流式响应。public void forwardChat(AgentInstance instance, ChatRequest request, Long userId, SseEmitter emitter) { String harnessUrl instance.getHarnessUrl(); webClient.post() .uri(harnessUrl /api/agent/chat) .header(X-Harness-Token, harnessToken) .bodyValue(buildHarnessPayload(instance, request, userId)) .accept(MediaType.TEXT_EVENT_STREAM) .exchangeToFlux(response - response.bodyToFlux(ServerSentEvent.class)) .doOnNext(event - safeSend(emitter, event)) .doOnComplete(emitter::complete) .doOnError(emitter::completeWithError) .subscribe(); }这里有个容易忽略的点exchangeToFlux是响应式的方法内部不会阻塞当前线程所以代理方法返回后执行链还能继续SSE 事件会在异步线程里逐条写给 SseEmitter。我在safeSend里做了异常兜底防止长连接已经被前端断开后继续写导致的空指针。doOnError里不要把堆栈直接打到用户端而是转成一个约定好的错误事件结构前端解析后提示智能体运行异常即可。SSE 转发还有一个性能注意点如果前端没有指定Last-Event-ID千万不要自动补发历史事件否则会因为消息表里历史记录太多拖垮内存。我们约定只在重连时由前端显式带上断点事件 ID后端根据这个 ID 从数据库补发未消费的消息。4. Python 侧的准备把 harness 变成可被调度的服务4.1 标准启动流程与配置调整Python 侧我用一个独立的项目目录来管理里面包含 AgentScope 应用配置文件、智能体脚本、自定义工具函数和入口启动文件。AgentScope 本身启动 harness 的方式并不复杂关键是要让它跑在受控的端口和网卡上。import os import agentscope config { multi_agent: ..., port: 8765, host: 127.0.0.1, logging_level: INFO } agentscope.init(config) # 启动 harness 服务监听 127.0.0.1:8765在容器化部署时这个端口只在 Docker 内部网络开放给 RuoYi 网关不直接对外映射。这样即便内部密钥泄露外网也无法直接访问。需要说明的是harness 服务本身是单进程的同一时间能承载的会话数有限所以在多团队共用时要特别注意资源上限。4.2 保留内置 UI但加一道内部保护我看到有些集成方案会直接把 harness 的 UI 去掉做成纯 API 服务。我个人不太推荐这么做因为 harness 自带的网页在多智能体调试时太好用了能直观看到每个智能体的中间消息和运行状态。正确做法是保留 UI只是让它不可被随意访问。我习惯在 harness 应用前面再包一层轻量网关程序用 FastAPI 实现专门负责三件事校验X-Harness-Token、把白名单内的请求转发给 harness、统一处理编码和异常格式。from fastapi import FastAPI, Request, HTTPException, Header app FastAPI(titleagent-harness-gateway) app.post(/api/agent/chat) async def forward_chat(request: Request, x_harness_token: str Header(None)): if x_harness_token ! os.getenv(HARNESS_TOKEN): raise HTTPException(status_code401, detailinternal token mismatch) payload await request.json() return await dispatch_to_harness(payload)这样做的额外好处是未来如果想在同一台机器上跑多个智能体项目可以由这个 FastAPI 网关做路由分流按路径把请求分给不同的 harness 实例避免端口冲突时反复改配置。4.3 环境隔离和多项目共存多智能体项目之间依赖冲突是很日常的事情。一个项目需要某个特定版本的工具库另一个项目可能完全不需要。我的建议是给每个智能体项目建独立的虚拟环境并且使用 requirements.txt 锁定版本。在部署阶段用 systemd 或 supervisor 管理每个进程环境变量里放各自不同的HARNESS_TOKEN这样既避免了依赖冲突也能独立重启某个项目而不影响其他服务。这一层的稳定性直接决定整套系统的可靠性。因为 Java 侧只是转发真正的智能体逻辑和并发上限都在 Python 侧。我见过不少集成后来出问题都出在 Python 容器里内存持续增长最后触发 OOM。解决办法是为每个进程设置显式内存上限并加上周期重启机制这些可以在部署阶段补齐。5. 前端页面的接入与交互状态管理5.1 在 RuoYi 前端新增独立菜单RuoYi-Vue-Plus 前端是 Vue3 加 Element Plus。我新增了一个ai-agent目录包含三个核心视图智能体管理页、会话中心页、运行监控页。路由注册在后台菜单表里这样菜单权限可以跟角色绑定只有授权用户能看到和操作。智能体管理页是一个表格加表单弹窗。表格列包括实例名称、所属模板、运行状态、最近心跳时间、所属用户、操作按钮。操作按钮绑定了启动、停止、删除、进入会话四个动作。进入会话时把实例 ID 通过路由参数传过去。会话中心页是核心交互页面。左侧是会话列表右侧是消息展示区和输入框。消息区按发送者不同渲染不同气泡智能体信息里面同时展示两样东西最终回复和中间思考摘要。中间思考摘要不是强制项但多智能体场景里用户很关心这个回答哪里来的有摘要能显著降低疑惑感。运行监控页主要展示各实例的 CPU、内存、最近调用耗时曲线。这些数据由 Python 侧周期性上报到 Java 接口存到监控表后由前端定时刷新。5.2 状态管理三层数据的同步策略前端的状态管理要分三层来看。第一层是智能体实例列表创建和删除后需要整体刷新这个比较简单直接重新拉接口。第二层是会话列表每次进入页面或会话结束时刷新。第三层是消息流这是最容易出问题的因为它要同时处理 SSE 推送、本地临时输入、失败重试三个来源。我的做法是给每条消息一个前端生成的临时 ID发送时先以发送中状态插入本地列表等 SSE 推送来第一块内容时用服务端返回的消息 ID 替换临时 ID后续增量内容按同一 ID append。这个策略避免了 SSE 顺序错乱导致的文本拼接错误也让前端重连后能正确收到补发消息。SSE 断线重连同样要在前端处理。浏览器原生 EventSource 在断线时会自动重连但它默认不携带认证 header所以我在 RuoYi 集成里换成了浏览器标准的方式用fetch读取流式响应自己解析text/event-stream自己管理重连逻辑。前端组件在 onUnmounted 时要主动 abort 当前请求否则页面切走后连接还在浪费连接资源。6. 踩坑实录与排查速查表6.1 七个高频问题与排查思路我把实际踩过的坑整理成一张速查表方便遇到问题时快速定位现象可能原因处理方式SSE 消息积压在一个点后一次性返回Nginx 开启了代理缓冲对 SSE 路径设置proxy_buffering off;页面偶发出现乱码Python 侧输出非 UTF-8 编码在 FastAPI 网关统一处理响应编码SseEmitter 报异步超时推理时间超过 Spring MVC 默认超时使用SseEmitter(0L)禁用超时并配合心跳用户权限错乱会话接口没有校验实例归属在 Controller 里统一走canUse校验Python 侧进程内存持续增长多轮对话历史全部放内存设置上下文窗口裁剪策略Python 侧重启后连接报错WebClient 复用已关闭连接开启连接池重连机制使用连接超时与空闲回收前端收到重复消息SSE 重连时补发了全部历史事件由前端携带断点 ID后端只补发增量第三行那个异步超时问题我最初没加心跳结果长对话跑到一半连接被服务端断开。解决办法是让 Python 网关每 15 秒发一个注释行心跳SSE 规范里注释行不会触发前端 message 事件但能防止中间链路空闲超时。6.2 让这套集成跑得更稳的几条心得集成做了几轮之后我最大的体会是跨语言集成的难点从来不在跨语言本身而在两面各自的边界假设。Java 侧容易默认 Python 服务是内存态、不可重启的Python 侧容易默认 Java 调用方会一直在线、不会超时。把这两边的假设都打破系统才稳定。几个具体的建议Java 侧的消息写入用异步队列避免 SSE 推送和数据库写入强绑定Python 侧给每个后端任务都加上超时和重试上限宁可失败也不要无限等待日志格式两边统一成 JSON并带上 request_id 串起完整链路监控上不只盯 Java 网关的平均响应时间还要看 Python 侧推理耗时分布因为多智能体的耗时方差往往很大。最后再分享一个小技巧我在biz_agent_instance表里加了last_heartbeat字段Python 侧每 30 秒上报一次心跳Java 定时任务检查超过 90 秒未上报的就自动标记为异常并告警。这个机制看起来不起眼但它最早发现问题的次数远超过各类复杂监控。做集成别急着铺庞大的监控平台先把心跳和状态标记这两个基础动作做好整个系统就不会失控。