别卷 Python 了:我用 Java 21 + Spring Boot 3 打造了一个企业级 RAG + 智能体工作流引擎(附架构与源码解析)
声明:本文介绍的是笔者本人的开源项目 MaxKB4j,重点聊架构设计与技术选型,代码均节选自真实源码,欢迎拍砖。
先交结果:MaxKB4j(Max Knowledge Brain for Java),一个纯 Java 原生的大模型应用平台------RAG 知识库 + 可视化工作流 + 多 Agent 协作,对标 Dify / MaxKB / FastGPT 这一品类,但技术栈是 Java 21 + Spring Boot 3.5 + LangChain4j 1.20。
几个硬数据:
- 21 个月、2270 次提交、887 个 Java 文件、约 5.6 万行代码,12 个发布版本(最新 v2.9.0)
- 32 种工作流节点 、18 家模型提供商 (OpenAI / DeepSeek / 通义 / Kimi / GLM / Ollama......)、7 种模型类型
- 仓库:Gitee(主) · GitHub(镜像) · 在线 Demo(demo / demo@123456)
这篇文章不复述 README,专注回答四个工程师视角的问题:整体架构长什么样?虚拟线程在 LLM 场景里怎么用?工作流引擎怎么抽象?混合检索怎么落地?
一、整体架构:五层 Maven 模块,契约与实现分离
sql
┌────────────────────────────────────────────────┐
│ maxkb4j-start 启动模块 / Flyway / Dockerfile │
└───────────────────────┬────────────────────────┘
┌───────────────────────▼────────────────────────┐
│ maxkb4j-service 业务实现(9 个子模块) │
│ application · chat · knowledge · model · oss │
│ system · tool · trigger · workflow │
└───────────────────────┬────────────────────────┘
┌───────────────────────▼────────────────────────┐
│ maxkb4j-service-api 对外契约 DTO/VO(8 个子模块) │
└───────────────────────┬────────────────────────┘
┌───────────────────────▼────────────────────────┐
│ maxkb4j-core 助手抽象 · 护栏 · LC4J 扩展 │
└───────────────────────┬────────────────────────┘
┌───────────────────────▼────────────────────────┐
│ maxkb4j-common 通用工具 │
└────────────────────────────────────────────────┘
技术选型没有一个冷门组件,全是 Java 圈存量设施:
| 维度 | 技术 |
|---|---|
| 运行时 | Java 21(LTS)+ 虚拟线程 |
| 框架 | Spring Boot 3.5 + Undertow |
| LLM 编排 | LangChain4j 1.20(agentic / mcp / pgvector / reactor) |
| ORM / 迁移 | MyBatis-Plus 3.5 + Flyway |
| 鉴权 | Sa-Token + JWT |
| 存储 | PostgreSQL + pgvector(向量)/ MongoDB(全文、文件、图谱) |
| 解析 | Apache Tika、Docling、EasyExcel、OFDRW、RapidOCR |
| 沙箱 | Groovy + groovy-sandbox |
这个选型的动机很直接:中国企业市场的主力存量栈是 Java。大模型应用不是孤立系统,它要嵌进已有的权限体系、审批流和数据中台------让现有 Java 工程师直接读、直接改、直接上线,比"团队集体转 Python"现实得多。
二、虚拟线程为什么是 LLM 应用的"版本答案"
先看配置(节选自 maxkb4j-start/src/main/resources/application.yml):
yaml
server:
undertow:
# 与 multipart 上限保持一致,防止超大请求体耗尽内存
max-http-post-size: 300MB
spring:
threads:
virtual:
enabled: true # Java 21 虚拟线程
一行配置,但它背后是对 LLM 应用负载特性的判断。
大模型应用的 IO 画像很极端:一次模型调用阻塞几秒到几十秒,一路用户请求里还串着向量检索、全文检索、Reranker 重排、工具 HTTP 调用、多 Agent 并行调度------全是长阻塞 IO,CPU 几乎闲着。
传统平台线程模型下,200 个并发用户就能把 Tomcat 默认线程池打满,剩下的请求排队;上 Reactor 响应式又意味着全团队陪葬一套心智负担。虚拟线程正好卡在中间:保持"一个请求一个线程"的朴素编程模型,阻塞代价却趋近于零,官方口径单机可支撑数千级并发。
两个实践细节,掘金的老哥们大概率会问,先说了:
- 虚拟线程只解决 IO 并发,流式输出另算 ------流式响应用的是 LangChain4j 的 reactor 模块(
langchain4j-reactor),SSE 推流不占请求线程; - JDK 21 上要注意 pinning :虚拟线程在
synchronized块内阻塞会钉住载体线程,JDK 24 的 JEP 491 才彻底解决------项目里长 IO 段一律避免synchronized,用ReentrantLock替代。
三、工作流引擎:32 种节点,一个接口
可视化工作流是这类平台的核心,MaxKB4j 有 32 种节点:条件分支、循环(Loop / Break / Continue)、变量聚合 / 赋值 / 拆分、LLM 对话、意图分类、参数提取、NL2SQL、知识库搜索 / 写入、文档抽取 / 分割、图像生成 / 理解、语音转文本 / 文本转语音、HTTP 请求、MCP、工具、应用子流程、表单、用户选择......
听起来吓人,但引擎侧的抽象只有一个接口(节选自 workflow/handler/node/INodeHandler.java):
java
public interface INodeHandler {
/**
* Executes the node.
* 同步 Handler 返回已完成的 future;
* 流式 Handler 返回的 future 在流结束时完成。
*/
CompletableFuture<NodeResult> execute(IWorkflow workflow, AbsNode node) throws Exception;
/**
* 节点执行后是否暂停工作流(例如表单节点等待用户输入)。
*/
default boolean shouldInterrupt(INode node) {
return false;
}
}
三个设计决策值得展开:
1. 返回 CompletableFuture 而不是同步结果。 同步节点直接返回已完成的 future,流式节点(LLM 对话、TTS)在流结束时完成 future------统一了同步/流式两种节点形态,引擎不用写 if-else 分叉。
2. 生命周期在抽象模板里。 计时、起始消息发射、失败传播这些公共逻辑收在 AbsNodeHandler 模板类中,32 个实现类只写节点本身的业务逻辑。加一种新节点 = 加一个类,不碰引擎内核。
3. shouldInterrupt 默认方法支持"暂停"。 表单节点、用户选择节点执行完后工作流挂起等待外部输入,恢复执行时上下文不丢------这是很多开源工作流缺的"人机协同"能力,靠这个 default 方法接住。
多 Agent 协作(多角色并行/串行、任务拆解汇总、共享记忆总线)构建在这套节点机制之上,底层用 LangChain4j 的 langchain4j-agentic 模块,core 模块里预置了 Router、IntentClassify、QueryCompress 等助手抽象。
四、混合检索:双写、并发召回、单路降级
RAG 的效果上限由检索决定。MaxKB4j 的检索链路是:向量 + 全文双路召回 → Reranker 精排 → 溯源返回 ,存储层抽象成 IDataStore,有三个实现:pgvector(向量)、MongoDB(全文)、以及把两者组合起来的 CompositeStoreImpl。
CompositeStoreImpl 的设计比较有味道,直接看源码(节选):
java
/**
* 组合 store:同时写入向量与全文两路,搜索时按来源类型混合两路结果。
* 写操作通过 dualWrite 串行委托并对失败做统一日志/异常包装;
* 搜索通过 safeSearchAsync 并发执行并支持单路降级(一路故障仍返回另一路结果)。
* 段落路与问题路的编排、问题到段落的映射以及去重排序由 SearchOrchestrator 负责,
* 本类只做同源两后端融合。
*/
@Component("compositeStore")
public class CompositeStoreImpl extends BaseStoreImpl {
private final IDataStore vectorStore;
private final IDataStore fullTextStore;
public CompositeStoreImpl(@Qualifier("vectorStore") IDataStore vectorStore,
@Qualifier("fullTextStore") IDataStore fullTextStore) {
this.vectorStore = vectorStore;
this.fullTextStore = fullTextStore;
}
@Override
public void upsert(EmbeddingModel model, List<EmbeddingEntity> entities) {
// 双写:同一份数据同时落 pgvector 与 MongoDB 全文索引
dualWrite("upsert", s -> s.upsert(model, entities));
}
// search:safeSearchAsync 并发两路召回,按 sourceId 取较高分融合,排序截断 topK
}
注意几个点:
- 写入双路、读取并发 :写路径
dualWrite串行委托并统一异常包装;读路径并发打两个后端,互不等待; - 单路降级:向量库抖了,全文检索的结果照样返回------检索可用性 > 检索完备性,这是生产环境和 Demo 的分水岭;
- 职责切分干净 :段落路/问题路的编排、问题到段落的映射、去重排序在上层
SearchOrchestrator,这个类只做"同源两后端融合",单一职责。
为什么不用 Milvus / 专用向量库?运维经济学:pgvector 对企业是"存量 PG 加一个 extension",百万级段落 + HNSW 索引完全够用,少一个分布式组件就少一分专人运维成本------这个取舍见仁见智,欢迎评论区battle。
五、18 家模型提供商:注解驱动,加一家不改内核
模型接入最容易写烂------初版代码里往往是一个巨型 switch (provider)。MaxKB4j 用注解 + 自动注册解决了这个问题(节选自 model/annotation/ModelProviderType.java):
java
/**
* 标注一个 AbsModelProvider 实现为可被自动发现的模型供应商。
* 由 ModelProviderAutoRegistrar 在所有单例就绪后扫描,
* 注册到 ModelProviderRegistry,替代原先枚举中硬编码的 switch 静态工厂。
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModelProviderType {
/** 供应商唯一标识 */
String provider();
/** 展示名称 */
String name();
/** 图标文件名(classpath:model-icons/ 下) */
String icon();
}
新增一家模型提供商 = 实现一个 AbsModelProvider 子类 + 打一个注解,注册器启动时自动扫描装配。目前预置 18 家:通义千问、DeepSeek、Kimi、GLM、豆包、星火、混元、文心、SiliconFlow、MiniMax、OpenAI、Azure、Claude、Gemini、Ollama、XInference、LocalAI、本地模型。
模型类型覆盖七种------不只是一个"ChatModel 平台":
java
public enum ModelType {
LLM("LLM", "大语言模型"),
EMBEDDING("EMBEDDING", "向量模型"),
STT("STT", "语音识别"),
TTS("TTS", "语音合成"),
VISION("VISION", "图片理解"),
TTI("TTI", "图片生成"),
RERANKER("RERANKER", "重排模型");
}
六、LangChain4j 实际用了哪些模块
项目启动时(2024 年底)Spring AI 还在里程碑版本,LangChain4j 的模块完整度更高,于是选了它,从 1.0 之前一路升到 1.20。实际引入的模块:
| 模块 | 用途 |
|---|---|
langchain4j-core / langchain4j |
基础抽象与模型接入 |
langchain4j-reactor |
流式输出(SSE) |
langchain4j-agentic |
多 Agent 编排 |
langchain4j-mcp |
MCP 协议(SSE / stream_http) |
langchain4j-pgvector |
pgvector 向量存储适配 |
langchain4j-experimental-sql |
NL2SQL 节点 |
langchain4j-document-parser-docling |
Docling 文档解析 |
顺带说句公道话:Spring AI 这两年迭代很快,两者竞品式发展对 Java 生态是好事。选型这事没有标准答案,只有"你的场景答案"。
七、5 分钟跑起来
bash
git clone https://gitee.com/taisan/MaxKB4j.git
cd MaxKB4j
docker-compose up -d
访问 http://localhost:8080/admin/login(admin / tarzan@123456),Compose 会拉起 pgvector + MongoDB + 应用三件套,首启自动跑 Flyway 初始化。也支持单容器、一键安装脚本(Linux/macOS/Windows)、源码构建(mvn clean package -DskipTests)四种姿势,仓库里还有 29 个开箱应用模板。
嫌麻烦就直接逛在线 Demo(demo / demo@123456)。
八、客观说明(防杠声明)
- 不是要"全面替代" Dify。 Dify 的社区、插件市场、贡献者规模是客观优势。MaxKB4j 的生态位是:Java 栈、要私有化、要深度二开的团队------这个细分下它可能是唯一不需要跨语言的选项;
- 前端源码未开源:后端 GPLv3 完全开放,前端以构建产物形式内置,源码属于赞助权益------独立开发者要维持 5 万行项目 21 个月不断更,总得有不靠情怀发电的路子;
- README 里与 Dify / MaxKB / FastGPT / RAGFlow 的对比表是能力取向对比不是跑分,选型请以自己场景实测为准;
- 一个人维护,issue 都会看,SLA 没法承诺。
写在最后
大模型应用的上半场拼模型,下半场拼工程化------而工程化是 Java 生态深耕二十年的主场。MaxKB4j 做的事一句话说完:把 LLM 编排、RAG、工作流、多 Agent 装进 Java 工程师最熟悉的技术栈里。
如果你正好在用 Java 落地 AI 应用,或者想研究工作流引擎抽象 / 虚拟线程落地 / RAG 链路设计的真实工程实现,欢迎来读源码、提 issue、拍砖:
- Gitee :gitee.com/taisan/MaxK...
- GitHub :github.com/taishan666/...
觉得有帮助的话,点个 Star 就当请笔者喝了杯咖啡 ☕。