这半年我们团队被反复问到同一个问题:能不能在现有的 Java 后台管理系统里直接跑 Agent 评估任务?以前的做法是把 AI 能力单独部署成 Python 服务,再给 Java 侧留一套 HTTP 接口,结果运维上多了一堆要盯的进程和端口。这次我们换了一个思路——把 AgentScope Harness 作为子进程,直接交给 RuoYi-Vue-Plus 来拉起、监控和回收,让它成为后台里的一个任务模块。整个过程走下来,才发现"纯 Java AI 平台"并不是让 Java 去跑模型,而是让 Java 去管 Agent 的运行生命周期。这篇就完整记录这次集成实践:任务状态机、进程生命周期、日志回传、权限绑定,以及那些文档里不会写的坑,给同样想在传统 Java 后台里接入多智能体能力的团队做个参考。
1. 需求源头:管理后台要的是"任务工厂",不是"聊天框"
1.1 传统 Java 后台接入 AI 的第一反应,往往是加一个对话页面
大多数 Java 团队接到 AI 需求,第一反应就是在 RuoYi-Vue-Plus 里加一个菜单,前端塞一个聊天框,后端对接 LLM API。对"单轮问答"这种场景,这条路确实最快。但一旦需求变成"让多个 Agent 协同完成一个目标""跑一批测试用例看评估结果""把每次执行记录留档审计",单纯聊天框就完全不合适了。聊天框是人来发起、人来看结果,而这类需求要的是系统自动发起、自动汇聚结果、可追溯可重跑——本质上是把 Agent 变成"后台批处理任务"。
RuoYi-Vue-Plus 这类脚手架本身已经解决了权限、数据隔离、操作日志、定时任务这些通用能力。我们要做的,不是另造一套 AI 调度平台,而是把 Agent 任务嵌进现有的管理后台流程里。用户打开菜单,能看到任务的创建时间、执行状态、日志、结果文件;管理员能按部门或角色限制谁能启动任务;审计员能在操作日志里看到谁在什么时间动了哪些任务。这些能力恰恰是 Java 后台的强项。
1.2 为什么选中 AgentScope Harness,而不是自己写一套 Agent 编排
自己写 Agent 编排最大的问题是:你以为你只需要调模型,实际上还要处理多轮上下文、工具注册、结果解析、并发压测、评估指标归一化。Harness 这个词在 AgentScope 里更像是"装载与运行底座",它把 Agent 的启动、协作流程、结果收集、压测与评估封装成一套可重复执行的结构。直接使用 Harness,我们可以把精力放在"什么时候跑、跑完怎么归档"上,而不是重新发明"怎么跑"。
我们的场景里还有一个硬指标:同一套多智能体应用要在不同配置下反复执行,用来做回归对比。Harness 天然支持配置化输入和结构化输出,跑完一轮就是一份结果文件,非常适合接任务系统。如果从零自己开发编排,至少要多花两到三周去处理并发、超时、结果标准化,而这些 Harness 已经替你兜底了。
1.3 三种集成方案对比:旁路服务、内嵌解释器、子进程
真正动手前,我们对比了三条路:
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| Python 独立服务 + Java HTTP 调用 | 边界清晰,Python 生态自由 | 多一套进程要运维,接口协议要自己设计,部署成本高 | 不差钱不差人可选,我们不选 |
| JVM 内嵌 Python 解释器 | 看起来"纯 Java"最彻底 | 解释器与 Java 版本兼容风险高,调试和内存管理是灾难 | 直接否决 |
| Java 拉取 Python 子进程 | 部署整体仍在一个应用体系里,生命周期可控,失败可回收 | Java 侧要处理进程监、日志、超时 | 最终选择 |
子进程方案看起来"不够高级",但生产环境里最稳。我们把 AgentScope 的依赖和虚拟环境固定在一个目录,RuoYi-Vue-Plus 通过进程管理模块去启动和回收它。对团队来说,仍然只需要部署一套主程序;对运维来说,不需要额外开着另一个 service;对审计来说,所有进程启动记录都能落在 Java 侧的任务表里。
2. 动手前的三个关键设计:路径、契约、生命周期
2.1 依赖目录与 Python 虚拟环境:不要把 Harness 塞进 JAR 包
第一个容易踩的坑,就是想着把 AgentScope 的 Python 代码打包进 Spring Boot 的 resources 里。千万不要这样做。JAR 包一旦被重新构建,Python 依赖就得重新装,路径还容易乱。我们采用独立目录方案:
/opt/agent-platform/ ├── harness/ │ ├── venv/ # Python 虚拟环境 │ ├── requirements.txt # 固定版本依赖 │ ├── task_templates/ # 各类 Harness 配置文件 │ └── runtime/ │ ├── {taskId}/ # 每个任务独立运行目录 │ │ ├── input.json │ │ ├── logs/ │ │ └── results/ │ └── .lock创建虚拟环境时一定用python3 -m venv venv,然后venv/bin/pip install -r requirements.txt。Java 侧启动进程时,可执行文件写死为venv/bin/python,不要写python3,避免踩到系统 Python 版本。这属于常见的生产环境加固手段,不是花活。
2.2 任务输入输出契约:不要用"控制台文本"当 API
Java 和 Python 子进程之间最忌讳把 stdout 当通信协议。Harness 跑起来后往控制台打一堆进度,文本解析既脆弱又不好扩展。我们的约定是:Java 侧把任务参数写成 JSON 文件,Harness 读取该文件执行,执行结果也写在 JSON 文件里;Java 侧通过"退出码 + 结果文件是否存在 + JSON 是否合法"来判断成功。这样解析逻辑只跟文件打交道,跟打印内容无关。
一个最小化的input.json长这样:
{ "task_id": "task_20250921_001", "agent_pipeline": "multi_agent_writing", "goal": "基于给定材料生成一份产品发布说明", "max_rounds": 10, "timeout_seconds": 600, "output_dir": "/opt/agent-platform/harness/runtime/task_20250921_001/results", "extra_params": { "temperature": 0.7 } }这个设计坚持下来以后,收益非常大。后面无论前端怎么改,Java 侧处理逻辑都不用动,因为契约是文件,不是接口。
2.3 状态机:后台任务必须异步,HTTP 请求不能等 Agent 跑完
管理后台里的任务本质上都是异步的。用户点击"启动任务",HTTP 接口只负责落库和投递,真正的执行逻辑要放到线程池或者消息队列里。否则一个 Agent 跑 5 分钟,HTTP 连接就挂 5 分钟,网关层早就超时了。
我们定义了五态任务状态机:
PENDING -> RUNNING -> SUCCESS |-> FAILED |-> TIMEOUT状态流转完全由 Java 侧驱动。进程启动后置 RUNNING,正常退出且结果合法置 SUCCESS,退出码非零置 FAILED,超过最大执行时间就强制杀进程并置 TIMEOUT。这个状态机虽然简单,但足以覆盖后台管理场景。后续要加 CANCELED 或 PAUSED,也只是在这个基础上扩展。
3. Spring Boot 侧落地:从 ProcessBuilder 到任务状态机
3.1 最容易被忽视的细节:进程输出流必须异步消费
用 ProcessBuilder 启动外部进程时,新手最常犯的错误是启动后不读输出流。子进程写满 stdout 缓冲区后会被阻塞,Java 侧看到的假象是"进程卡死",其实是没人消费输出。正确的做法是启动后立刻用独立线程消费 stdin/stdout/stderr。我们用的是CompletableFuture.runAsync,把输出逐行写入任务日志文件。
核心启动代码如下:
public class HarnessProcessRunner { private final ThreadPoolTaskExecutor taskExecutor; public StartedProcess start(AgentTask task) throws IOException { ProcessBuilder pb = new ProcessBuilder( "/opt/agent-platform/harness/venv/bin/python", "-m", "agentscope.harness", "--config", task.getInputFile().toAbsolutePath().toString() ); pb.directory(task.getRuntimeDir().toFile()); pb.environment().put("PYTHONIOENCODING", "utf-8"); pb.redirectErrorStream(true); Process process = pb.start(); Path logFile = task.getLogFile(); CompletableFuture<Void> ioFuture = CompletableFuture.runAsync(() -> { try (BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { Files.writeString(logFile, line + System.lineSeparator(), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND); } } catch (IOException ignored) { // 进程退出时中断读取,属正常情况 } }, taskExecutor); return new StartedProcess(process, ioFuture); } }3.2 状态轮询与超时回收:Java 侧要当"监工"
进程启动只是第一步,后台还要有守护逻辑。我们用一个ScheduledExecutorService定时轮询每个 RUNNING 任务,每次检查process.isAlive()。如果存活时间超过任务的timeout_seconds,执行process.destroyForcibly(),并把状态置为 TIMEOUT。如果进程已退出,则根据退出码做最终状态判定。
这里有一个很重要的细节:destroyForcibly()杀掉的是当前 Process 对象对应的进程,但如果 Harness 内部又 fork 了子进程,那些子进程可能变成孤儿进程。我们的临时方案是写一个包装脚本,用exec替换当前进程,然后在 Java 侧按进程组处理:
#!/usr/bin/env bash # run_harness.sh cd /opt/agent-platform/harness exec ./venv/bin/python -m agentscope.harness --config "$1"exec是关键。它让 Harness 进程直接接管脚本的 PID,这样 Java 侧拿到的process.pid()就是真正要监控的 Python 进程。配合kill -- -PID杀整个进程组,能解决大部分孤儿进程问题。这是实际生产环境验证过的做法。
3.3 任务表设计:把进程信息落库,才能做审计
任务表要同时承担业务和运维两类信息。我们最终设计成如下核心字段:
| 字段 | 说明 |
|---|---|
| id | 任务ID |
| task_code | 业务编号,如 AGT-20250921-001 |
| task_name | 任务名称,前端展示用 |
| pipeline_code | 对应 Harness 配置模板 |
| status | PENDING / RUNNING / SUCCESS / FAILED / TIMEOUT |
| pid | Java 侧记录的子进程 PID |
| exit_code | 进程退出码 |
| input_path | 输入 JSON 路径 |
| result_path | 结果 JSON 路径 |
| log_path | 日志文件路径 |
| error_msg | 失败原因摘要 |
| creator_id | 创建人 |
| create_time / finish_time | 时间记录 |
落库的意义不只是列表展示,更重要的是事故追溯。某天线上出问题,运营说"我没跑过这个任务",后台一查 creator_id、启动时间、进程 PID、日志路径,整条链路都摆在眼前。这正是 RuoYi-Vue-Plus 这类管理平台的价值。
3.4 权限接入:菜单、按钮和数据权限三件套
RuoYi-Vue-Plus 的权限模型是现成的:菜单决定能不能看到页面,按钮权限决定能不能操作,数据权限决定能看到哪些数据。我们新增了一个"Agent 任务管理"菜单,下面挂启动、取消、删除三个按钮权限,权限标识类似agent:task:add、agent:task:cancel、agent:task:delete。所有接口在 Controller 层加@PreAuthorize注解。
数据权限要特别提一下。任务不能是全局共享的,否则某部门创建的任务,其他部门也能看到。我们在任务表里加上部门字段,并接入 RuoYi 的DataScope注解,让列表接口按当前用户所属部门过滤。这是把 AI 能力接入管理后台时最容易被忽略,但最不能省的设计。
4. 跑通一个最小示例:多智能体任务从创建到结果归档
4.1 准备最小 Harness 配置
我们以一个"检索 + 写作"的双 Agent 协作为例。Harness 配置文件里定义两个 agent,一个负责检索材料,一个负责汇总成文。配置模板放在task_templates/目录,Java 侧启动任务时用实际参数替换模板变量。
# multi_agent_writing.yaml harness: name: multi_agent_writing agents: - name: researcher role: 信息检索与材料整理 model: ${RESEARCHER_MODEL} - name: writer role: 根据材料撰写最终稿件 model: ${WRITER_MODEL} pipeline: - step: researcher.run output: research_notes - step: writer.run input: research_notes output: final_document output: format: json path: ${OUTPUT_DIR}/result.json这里的变量RESEARCHER_MODEL、WRITER_MODEL、OUTPUT_DIR通过 Java 侧启动任务时写入环境变量来注入。写死在 YAML 里的只有流程结构,具体跑哪次任务,由运行时决定。
4.2 Java 侧启动任务的完整过程
整个启动动作在用户点击"启动"按钮后发生,链路如下:
- 前端提交任务表单,后端把参数写入
input.json,创建任务记录,状态置 PENDING。 - 线程池从
PENDING队列取出任务,构建 ProcessBuilder,调用start()。 - 记录
process.pid()到任务表,状态置 RUNNING。 - 启动独立线程消费输出流,写入日志文件。
- 调度线程每 2 秒检查一次状态,超时则回收进程。
代码实现上,我们把这个启动方法放在AgentTaskService里,用@Async注解异步执行:
@Async("agentTaskExecutor") public void runTaskAsync(AgentTask task) { try { StartedProcess started = runner.start(task); task.setStatus(RUNNING); task.setPid(started.getPid()); taskService.updateById(task); boolean finished = started.getProcess().waitFor( task.getTimeoutSeconds(), TimeUnit.SECONDS ); if (!finished) { started.getProcess().destroyForcibly(); task.setStatus(TIMEOUT); } else if (started.getProcess().exitValue() == 0 && resultFileExists(task)) { task.setStatus(SUCCESS); parseAndStoreResult(task); } else { task.setStatus(FAILED); task.setErrorMsg(lastLines(task.getLogFile(), 20)); } task.setFinishTime(LocalDateTime.now()); taskService.updateById(task); } catch (Exception e) { task.setStatus(FAILED); task.setErrorMsg(e.getMessage()); taskService.updateById(task); } }4.3 用户在页面看到的完整闭环
用户在 RuoYi-Vue-Plus 的"Agent 任务管理"页面新建任务,选择模板,填写参数,点击启动。列表里状态从"待启动"变成"运行中",再变成"成功"或"失败"。点击"查看日志"能看到实时追加的 Harness 输出;点击"查看结果"能看到结构化 JSON 结果文件。整个交互和平时跑报表任务完全一样,不需要用户理解底层是 Python 还是 Java。
这一点在生产中极其重要。业务方不关心技术栈,他们只关心任务能不能看到、能不能查、能不能撤销。AgentScope Harness 提供了 Agent 运行能力,RuoYi-Vue-Plus 提供了任务管理能力,两者各司其职。
5. 踩坑清单:从路径乱码到孤儿进程的实战记录
5.1 虚拟环境路径问题:可执行文件必须指向 venv 内 Python
有同事图省事,启动命令写python3 -m agentscope.harness,结果子进程用的是系统 Python,依赖全部丢失。更隐蔽的问题是 cron 或 systemd 环境里 PATH 和手动终端不一样,可能意外切到另一个解释器。后来我们统一改为绝对路径venv/bin/python,配置里写死,并在启动脚本第一行做环境检查。
这个坑的根因是"你以为的 Python 和实际运行的 Python 不是同一个"。凡是涉及子进程调用的系统,都建议把可执行文件路径写成绝对路径,不要依赖 PATH。
5.2 中文日志乱码:必须同时设置环境变量与读取编码
Agent 的输出经常带中文,第一次集成时日志文件里全是乱码。问题出在两个地方:一是 Python 侧输出编码不是 UTF-8,二是 Java 侧读取流时用了系统默认编码。解决方案是双管齐下:
pb.environment().put("PYTHONIOENCODING", "utf-8"); pb.environment().put("PYTHONUNBUFFERED", "1");读取端统一用StandardCharsets.UTF_8构造InputStreamReader。设置PYTHONUNBUFFERED=1也很关键,否则 Python 的 print 会走块缓冲,Java 侧看日志总感觉延迟好几秒。
5.3 超时杀进程后仍有残留:用进程组兜底
线上第一次遇到超时任务时,Java 侧执行了destroyForcibly(),状态置为 TIMEOUT。结果 5 分钟后发现机器上还挂着孤儿进程在跑,继续消耗 GPU 显存。原因就是 Harness 内部可能拉起子进程,destroyForcibly()只杀了父进程。
我们最终用进程组方式兜底:启动进程时让它成为独立的进程组组长,回收时执行kill -- -PID,把它和所有后代进程一起结束。Java 侧代码如下:
private void killProcessGroup(long pid) { try { Runtime.getRuntime().exec(new String[]{"kill", "--", "-" + pid}); } catch (IOException e) { log.error("kill process group failed, pid={}", pid, e); } }这里补充说明:先destroy()发送 SIGTERM,等 5 秒,再destroyForcibly()发送 SIGKILL,最后如果进程组还存在再用kill -- -PID。多一级优雅退出,结果文件更完整。
5.4 并发任务资源竞争:每个任务必须独立运行目录
多个任务同时跑,如果共用同一个工作目录,日志和结果文件会互相覆盖。我们规定每个任务运行时目录为/runtime/{taskId}/,所有中间文件都在该目录下。另外还要在 Java 侧限制最大并发数,我们用的是线程池大小为 4,配合信号量做任务级排队。AgentScope Harness 本身可能有自己的并发控制,但 Java 侧这一层兜底仍然需要,因为进程数和资源占用必须由平台掌握。
5.5 命令注入风险:用户入参永远不能直接拼进命令行
RuoYi-Vue-Plus 这类平台通常有多个租户、多个角色。如果用户输入的任务 ID 或者其他参数直接拼进 ProcessBuilder 命令,理论上存在命令注入风险。我们的做法是:进程启动命令固定只有一条,用户能配置的只有 JSON 文件里的参数,绝不把前端传参拼到new ProcessBuilder(...)里。具体来说,启动命令永远是:
new ProcessBuilder(harnessPythonPath, "-m", "agentscope.harness", "--config", configFilePath)configFilePath是后端根据任务 ID 生成的固定路径,不是用户传参。这算是最基础的输入校验,但每次接新的外部进程时都要重新检查一遍。
6. 从"跑任务"到"沉淀能力":下一步的扩展思考
6.1 别丢掉 Harness 本身的评估语义
集成过程中最容易犯的错误,是把 Harness 当成"万能执行器",然后在 Java 侧重新实现一堆评估逻辑。实际上 Harness 的价值在于它自带可复用的评估、压测和结果归一化。我们后续要做回归对比时,直接复用 Harness 的评测能力,Java 侧只负责把历史结果拉出来做交叉比对。自己的业务系统永远只解决"什么时候跑、结果怎么归档"这两个问题,至于"怎么评估"交给 Harness。
6.2 任务模板中心:让业务人员自己配模板
现在每个任务模板都写在task_templates/目录里,新增模板需要后端改代码。下一步我们计划做成数据库里的"任务模板表",前端可视化管理模板字段。业务人员选择模板、填参数、保存为模板,再基于模板批量创建任务。这样 Agent 的运行能力就能逐步下沉给业务侧,Java 后台退化为纯粹的流程编排与权限管理。
6.3 结果回调与业务联动:从异步任务到自动化流程
现在的流程在"结果归档"就结束了。更进一步的玩法是:Harness 跑完的结果写入某个业务表,触发后续的人工审批或自动通知。比如多智能体生成了一份分析报告,跑完后自动给负责人推一条待办,负责人审阅后可以一键发布。这是把 AI 能力真正嵌入企业流程的关键一步,也是 RuoYi-Vue-Plus 这类平台的天然主场。
最后再分享一个我们后期一定会保留的设计习惯:任务结果文件里除了最终的评估输出,尽量把 Agent 的运行轨迹原始数据也一起落盘。这些原始轨迹现在看没什么用,等你想做任务回放、异常分析或者换模型对比时,就知道当时的决策有多划算了。集成 AgentScope Harness 到 RuoYi-Vue-Plus 这个过程本身不难,难的是把进程、状态机、权限和安全边界都想清楚。这套链路跑通之后,"Java 平台管理 AI 能力"就不再是一个别扭的伪命题,而是一条非常务实的落地路径。