我用 Java 21 虚拟线程重写了一个 RAG 平台:从架构设计到踩坑实录

我用 Java 21 虚拟线程重写了一个 RAG 平台:从架构设计到踩坑实录

项目地址:gitee.com/taisan/MaxK... | Gitee GVP 认证 | GPL v3

为什么 Java 需要 RAG 平台

先说个现实:2025 年了,你去搜「RAG 平台选型」,出来的结果清一色是 Dify(Python)、FastGPT(Node.js)、MaxKB(Python)、Coze(闭源)。Java 在这里几乎不存在。

但另一面是:国内企业级后端,Java 仍然是绝对主力。一个典型的 Java 团队要做知识库问答,面临的不是「选哪个 RAG 平台」的问题,而是「要不要为此引入一整套 Python/Node.js 技术栈」的问题。

这背后是真实的成本:

  • 人力成本:招一个能搞定 LangChain + FastAPI 的 Python 工程师,或者一个熟悉 Node.js 的全栈,在二线城市月薪 15-25k。而你的 Java 团队本来就能干这事。
  • 运维成本:多一套语言就多一套 CI/CD、多一套监控、多一套部署流水线。
  • 集成成本:RAG 平台要和现有的 Spring Cloud 微服务、MyBatis 数据层、Redis 缓存打通,跨语言调用要么走 HTTP,要么走 gRPC,要么走消息队列------每多一跳就多一个故障点。
  • 定制成本:想改 RAG 的检索策略?想加一个自定义的文档解析器?Python 代码对 Java 团队来说是黑盒。

MaxKB4j 就是冲着这个问题来的。 用 Java 生态原生的技术栈,把 RAG + 工作流 + Agent 这套东西重新实现一遍。

整体架构

先看架构全景:

scss 复制代码
┌─────────────────────────────────────────────────────────┐
│                    前端 (Vue 3 + Element Plus)            │
│         工作流编排器 │ 知识库管理 │ 模型管理 │ 应用管理      │
└──────────────────────────┬──────────────────────────────┘
                           │ HTTP / SSE
┌──────────────────────────▼──────────────────────────────┐
│              API Gateway (Spring Boot 3.2)               │
│         Spring Security │ 权限控制 │ API 鉴权             │
└──────────────────────────┬──────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────┐
│                   核心服务层                              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐ │
│  │ 工作流引擎 │  │ RAG 引擎  │  │ Agent引擎 │  │ 模型管理 │ │
│  │(DAG 调度) │  │(检索+生成) │  │(意图+工具) │  │(多模型)  │ │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬────┘ │
│       │              │              │              │      │
│  ┌────▼──────────────▼──────────────▼──────────────▼────┐ │
│  │           LangChain4j 集成层                          │ │
│  │    Embedding │ ChatModel │ ToolExecution │ RAG       │ │
│  └──────────────────────┬──────────────────────────────┘ │
└─────────────────────────┼────────────────────────────────┘
                          │
┌─────────────────────────▼────────────────────────────────┐
│                    基础设施层                              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐ │
│  │PostgreSQL│  │ pgvector │  │  Redis   │  │  MinIO  │ │
│  │(业务数据) │  │(向量存储) │  │(缓存/会话)│  │(文件存储)│ │
│  └──────────┘  └──────────┘  └──────────┘  └─────────┘ │
└─────────────────────────────────────────────────────────┘

几个关键设计决策:

1. 为什么选 LangChain4j 而不是自己造轮子?

LangChain4j 是 Java 生态里目前最成熟的 LLM 应用开发框架,提供了 ChatLanguageModelEmbeddingModelContentRetrieverToolProvider 等核心抽象。它做的事情和 Python 的 LangChain 类似,但 API 设计更符合 Java 的习惯------强类型、接口驱动、依赖注入友好。

MaxKB4j 在 LangChain4j 之上构建了三层能力:

  • 模型适配层 :统一封装 OpenAI、通义千问、文心一言、智谱 GLM、DeepSeek 等 20+ 模型的调用差异,上层业务代码只面对一个 ChatLanguageModel 接口
  • RAG 管道层 :基于 LangChain4j 的 RetrievalAugmentor,实现了文档分段、向量化、检索、重排序的完整管道
  • 工作流编排层:在 LangChain4j 的 AI Service 之上,自研了 DAG 工作流引擎

2. 为什么用 pgvector 而不是 Milvus/Weaviate?

早期版本支持多种向量数据库,但实际生产中我们发现:大多数 Java 团队的 RAG 场景,数据量在百万级文档段以内,pgvector 完全够用。而且它跑在 PostgreSQL 里,不需要额外部署一个独立的向量数据库服务,运维成本直接降一个量级。

当然,如果需要更专业的向量检索能力(十亿级、多模态向量),架构上也预留了切换到 Milvus 的接口。

虚拟线程 + Reactor:LLM 场景下的并发模型选择

这是整个项目里技术含量最高的部分,也是我踩坑最多的地方。

LLM 应用的并发特征

先分析一下 LLM 应用的 I/O 模型:

css 复制代码
用户请求 → [意图识别 200ms] → [知识库检索 100ms] → [LLM 生成 2-10s] → 响应
                                    ↓
                              [向量检索 50ms]
                              [重排序 100ms]

特征很明显:

  1. 长连接 + 流式输出:LLM 生成一个回答可能要 2-10 秒,但用户期望看到逐字输出(SSE),不是等 10 秒后一次性返回
  2. 串行依赖链:意图识别 → 检索 → 生成,有严格的先后顺序
  3. 高并发 + 长等待:每个请求占用资源的时间很长(秒级),但大部分时间在等 I/O
  4. 混合 I/O 模型:既有阻塞式调用(JDBC 查数据库),也有非阻塞式调用(HTTP 调 LLM API + SSE 流式读取)

为什么不全用 WebFlux?

很多人第一反应是:既然 I/O 密集,直接上 WebFlux + Reactor 不就完了?

理论上可以,实际上坑很多:

坑 1:LangChain4j 和 JDBC 都是阻塞的

LangChain4j 的 ChatLanguageModel.call() 是同步阻塞的。pgvector 的查询走的是 JDBC,也是同步阻塞的。如果你在 Reactor 的 event loop 里调用这些,直接把线程池堵死。

java 复制代码
// ❌ 错误示范:在 Reactor event loop 里做阻塞调用
webClient.post()
    .uri("/chat")
    .bodyValue(request)
    .retrieve()
    .bodyToFlux(String.class)
    .flatMap(chunk -> {
        // 这里调用 LangChain4j 的同步 API → 阻塞 event loop
        String result = chatModel.call(prompt);  // 💀
        return Flux.just(result);
    });

要解决这个问题,你得把所有阻塞调用都包一层 subscribeOn(Schedulers.boundedElastic()),代码复杂度直线上升,而且调试困难。

坑 2:调试体验差

Reactor 的调用栈是异步的,出了问题看堆栈跟看天书一样。对于 LLM 应用这种需要频繁调试 Prompt、检索策略、模型参数的场景,开发效率会大幅下降。

坑 3:团队学习成本

WebFlux 的编程模型(Mono/Flux/背压/调度器)对习惯了 Spring MVC 的 Java 开发者来说,学习曲线陡峭。一个 RAG 平台如果只有核心开发者能维护,开源社区就活不起来。

虚拟线程 + Reactor 的混合方案

MaxKB4j 最终采用的是 虚拟线程处理业务逻辑 + Reactor 处理流式输出 的混合模型:

java 复制代码
// 虚拟线程处理完整的 RAG 请求链(阻塞式,但虚拟线程不消耗平台线程)
@Service
public class ChatService {
    
    private final ExecutorService virtualExecutor = 
        Executors.newVirtualThreadPerTaskExecutor();
    
    // 业务入口:在虚拟线程中执行完整的 RAG 管道
    public SseEmitter chat(ChatRequest request) {
        SseEmitter emitter = new SseEmitter(300_000L); // 5分钟超时
        
        virtualExecutor.submit(() -> {
            try {
                // 1. 意图识别(同步调用 LLM API,虚拟线程自动挂起)
                Intent intent = intentRecognizer.recognize(request.getMessage());
                
                // 2. 知识库检索(同步 JDBC 查询 pgvector,虚拟线程自动挂起)
                List<Document> docs = retriever.retrieve(
                    EmbeddingRequest.builder()
                        .text(request.getMessage())
                        .build()
                );
                
                // 3. 构建 Prompt + 调用 LLM 流式生成
                Prompt prompt = promptBuilder.build(request, docs, intent);
                
                // 4. 流式输出:这里切换到 Reactor 处理 SSE
                chatModel.generateStream(prompt)
                    .doOnNext(token -> {
                        try {
                            emitter.send(SseEmitter.event()
                                .data(token));
                        } catch (IOException e) {
                            emitter.completeWithError(e);
                        }
                    })
                    .doOnComplete(emitter::complete)
                    .subscribe();
                    
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        });
        
        return emitter;
    }
}

这个设计的核心思路是:

  1. 业务编排用虚拟线程:意图识别、检索、Prompt 构建、会话管理等业务逻辑,全部在虚拟线程中以同步方式编写。代码可读性高,调试方便,和传统 Spring MVC 开发体验一致。

  2. 流式输出用 Reactor :LLM 的 SSE 流式响应是天然的响应式场景,用 Reactor 的 Flux<String> 处理最合适。背压控制、错误传播、超时处理都是现成的。

  3. 虚拟线程解决并发瓶颈:一个 4 核 8G 的服务器,传统线程池可能只能开 200 个线程,而虚拟线程可以轻松开到几万个。每个用户请求占一个虚拟线程,等 LLM API 返回时自动让出 CPU,不浪费任何平台线程。

性能数据

在我们的压测场景下(100 并发用户,每个请求触发一次 RAG 检索 + LLM 生成):

模型 平台线程数 内存占用 P99 响应时间 吞吐量
传统线程池 (200线程) 200 2.1 GB 12.3s 8 req/s
虚拟线程 4 (carrier) 1.4 GB 8.7s 23 req/s
WebFlux (全响应式) 4 (event loop) 1.2 GB 7.9s 26 req/s

虚拟线程方案比传统线程池吞吐量提升 ~3 倍,和全响应式方案接近,但代码复杂度只有后者的 1/3。

RAG 引擎:不只是「向量检索 + 拼接 Prompt」

很多人对 RAG 的理解停留在「把文档向量化 → 检索相关段落 → 拼到 Prompt 里 → 让 LLM 回答」。这个理解没错,但生产环境的 RAG 远比这复杂。

文档处理管道

scss 复制代码
原始文档 (PDF/Word/Markdown/HTML)
    ↓
文档解析器 (Apache Tika / 自定义解析)
    ↓
文本清洗 (去噪、去重、格式标准化)
    ↓
智能分段 (按语义边界切分,不是简单按字数切)
    ↓
向量化 (Embedding Model)
    ↓
存入 pgvector (带元数据:来源文档、页码、章节)

分段策略是 RAG 效果的关键。 简单的按 500 字切一刀,很可能把一个完整的逻辑段落切成两半,导致检索到的片段缺乏上下文。

MaxKB4j 实现了基于语义的分段策略:

  • 优先按标题层级(H1/H2/H3)分段
  • 其次按段落边界分段
  • 超长段落按句子边界切分,保留上下文重叠区(overlap)
  • 支持自定义分段规则(正则、分隔符、最大/最小长度)

混合检索策略

scss 复制代码
用户查询
    ↓
┌─────────────┐
│  查询改写    │ ← LLM 将用户口语化的问题改写为更适合检索的形式
└──────┬──────┘
       ↓
┌──────────────┬──────────────┐
│  向量检索     │  全文检索     │ ← pgvector + PostgreSQL tsvector
│ (语义相似度)  │ (关键词匹配)  │
└──────┬───────┴──────┬───────┘
       ↓              ↓
┌─────────────────────────────┐
│  RRF (Reciprocal Rank Fusion)│ ← 融合两路检索结果
└──────────────┬──────────────┘
               ↓
┌──────────────┐
│  重排序       │ ← 用 Cross-Encoder 模型对 Top-K 结果精排
└──────────────┘
               ↓
          最终结果

为什么需要混合检索?

纯向量检索有个经典问题:用户问「Java 21 的虚拟线程怎么用?」,向量检索可能返回一篇讲「Java 21 新特性概览」的文章(语义相似),但漏掉一篇标题就叫「Java 21 虚拟线程使用指南」的文章(关键词完全匹配但 embedding 距离稍远)。

全文检索恰好相反,擅长精确关键词匹配,但不理解语义。

MaxKB4j 的混合检索用 RRF(Reciprocal Rank Fusion)算法融合两路结果:

java 复制代码
// RRF 融合算法核心逻辑
public List<SearchResult> hybridSearch(String query, int topK) {
    // 向量检索
    List<SearchResult> vectorResults = vectorSearch.search(
        embeddingModel.embed(query), topK * 2
    );
    
    // 全文检索
    List<SearchResult> fullTextResults = fullTextSearch.search(query, topK * 2);
    
    // RRF 融合:score = Σ 1/(k + rank_i),k 通常取 60
    Map<String, Double> rrfScores = new HashMap<>();
    int k = 60;
    
    for (int i = 0; i < vectorResults.size(); i++) {
        String docId = vectorResults.get(i).getDocId();
        rrfScores.merge(docId, 1.0 / (k + i + 1), Double::sum);
    }
    
    for (int i = 0; i < fullTextResults.size(); i++) {
        String docId = fullTextResults.get(i).getDocId();
        rrfScores.merge(docId, 1.0 / (k + i + 1), Double::sum);
    }
    
    return rrfScores.entrySet().stream()
        .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
        .limit(topK)
        .map(entry -> getDocument(entry.getKey()))
        .collect(Collectors.toList());
}

工作流引擎:DAG 调度 + 节点可扩展

MaxKB4j 的工作流引擎是一个基于 DAG(有向无环图)的可视化编排系统。用户在前端拖拽节点、连线,定义 LLM 应用的执行流程。

节点类型

节点类型 功能 输入 输出
LLM 节点 调用大模型生成文本 Prompt 模板 + 变量 生成文本
知识库检索节点 RAG 检索 查询文本 检索到的文档片段
条件判断节点 if/else 分支 判断表达式 走不同分支
代码执行节点 运行自定义 Java/JS 代码 上下文变量 代码返回值
HTTP 请求节点 调用外部 API URL + 参数 API 响应
变量赋值节点 设置/修改变量 键值对 更新后的上下文
开始/结束节点 流程控制 - -

执行引擎设计

java 复制代码
public class WorkflowExecutor {
    
    private final Map<String, NodeHandler> nodeHandlers;
    private final ExecutorService virtualExecutor;
    
    /**
     * 执行工作流
     * 核心思路:拓扑排序 → 按层并行执行
     */
    public WorkflowResult execute(Workflow workflow, Map<String, Object> input) {
        // 1. 拓扑排序,确定执行层级
        List<List<Node>> layers = topologicalSort(workflow.getNodes(), workflow.getEdges());
        
        // 2. 共享上下文(节点间传递数据)
        WorkflowContext context = new WorkflowContext(input);
        
        // 3. 逐层执行,同层节点并行
        for (List<Node> layer : layers) {
            List<Future<?>> futures = layer.stream()
                .map(node -> virtualExecutor.submit(() -> {
                    NodeHandler handler = nodeHandlers.get(node.getType());
                    NodeResult result = handler.execute(node, context);
                    context.setVariable(node.getId(), result);
                }))
                .collect(Collectors.toList());
            
            // 等待当前层所有节点完成
            awaitAll(futures);
        }
        
        return context.buildResult();
    }
}

关键设计点:

  1. 同层并行:DAG 中没有依赖关系的节点在同一层并行执行(虚拟线程让并行成本几乎为零)
  2. 上下文传递 :每个节点的输出自动注入到工作流上下文,下游节点通过 ${nodeId.output} 引用
  3. 节点可扩展 :实现 NodeHandler 接口就能自定义节点类型,用户可以通过代码执行节点写自己的逻辑

一个实际的工作流示例

css 复制代码
[开始] → [意图识别(LLM)] → [条件判断]
                              ├─ 知识问答 → [知识库检索] → [LLM生成] → [结束]
                              ├─ 工具调用 → [HTTP请求] → [结果处理] → [结束]
                              └─ 闲聊 → [LLM闲聊回复] → [结束]

这个工作流实现了一个简单的智能路由:先让 LLM 判断用户意图,然后根据意图走不同的处理分支。在传统代码里这要写一堆 if-else,在工作流里就是拖几个节点、连几条线。

Agent 引擎:从 RAG 到自主决策

RAG 解决的是「基于知识回答问题」,Agent 解决的是「理解意图 + 选择工具 + 执行任务」。

MaxKB4j 的 Agent 引擎基于 LangChain4j 的 @Tool 注解和 AiServices 构建:

java 复制代码
// 定义一个可以查询订单的 Agent
public interface OrderAgent {
    
    @SystemMessage("你是一个订单查询助手,可以帮用户查询订单状态。")
    String chat(@UserMessage String message);
}

// 定义工具
public class OrderTools {
    
    @Tool("根据订单号查询订单状态")
    public OrderStatus queryOrder(@P("订单号") String orderId) {
        return orderService.getOrderStatus(orderId);
    }
    
    @Tool("根据用户ID查询该用户的所有订单")
    public List<Order> queryUserOrders(@P("用户ID") String userId) {
        return orderService.getOrdersByUserId(userId);
    }
    
    @Tool("申请退款")
    public RefundResult applyRefund(
        @P("订单号") String orderId, 
        @P("退款原因") String reason
    ) {
        return refundService.apply(orderId, reason);
    }
}

// 组装 Agent
OrderAgent agent = AiServices.builder(OrderAgent.class)
    .chatLanguageModel(chatModel)
    .tools(new OrderTools())
    .contentRetriever(knowledgeBaseRetriever)  // 同时具备 RAG 能力
    .build();

用户说「帮我查一下订单 20250315001 的物流到哪了」,Agent 会:

  1. 理解意图 → 需要调用查询工具
  2. 提取参数 → orderId = "20250315001"
  3. 调用 queryOrder 工具
  4. 将工具返回的结果组织成自然语言回复

Agent + RAG + 工具的三合一是 MaxKB4j 的核心卖点。很多平台要么只做 RAG,要么只做 Agent,能同时支持并且通过工作流编排组合的,Java 生态里目前只有 MaxKB4j。

MCP 协议支持

MaxKB4j 还支持了 MCP(Model Context Protocol),这是 Anthropic 提出的模型上下文协议,让 AI 能够感知代码仓库、文件系统等开发环境上下文。

这意味着 MaxKB4j 的 Agent 不只是一个聊天机器人,它可以:

  • 读取和分析代码仓库
  • 理解项目的上下文结构
  • 基于代码上下文给出更精准的回答

对于「把 AI 集成到开发工具链」这个场景,MCP 协议的支持是一个重要的差异化能力。

踩坑实录

坑 1:虚拟线程 + synchronized 的死锁陷阱

虚拟线程遇到 synchronized 块时会「钉住」(pin)载体线程(carrier thread),不会像普通阻塞那样让出 CPU。如果大量虚拟线程同时竞争同一个锁,载体线程会被耗尽。

解决方案 :把所有 synchronized 替换为 ReentrantLock

java 复制代码
// ❌ 虚拟线程中避免使用
synchronized (lock) {
    // 如果这里阻塞,载体线程被钉住
    someBlockingOperation();
}

// ✅ 使用 ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    someBlockingOperation();
} finally {
    lock.unlock();
}

坑 2:SSE 连接超时

LLM 生成长文本时,一个 SSE 连接可能持续 30 秒以上。Nginx 默认的 proxy_read_timeout 是 60 秒,但某些云负载均衡器的超时更短。

解决方案

  • Nginx 层:proxy_read_timeout 300s;
  • Spring 层:SseEmitter 设置合理的超时时间
  • 前端层:实现自动重连机制

坑 3:Embedding 模型的 Token 限制

文档分段后,每段都要调用 Embedding 模型向量化。大多数 Embedding 模型有 Token 限制(如 text-embedding-3-small 限制 8191 tokens)。超长段落会被截断,导致信息丢失。

解决方案:分段时不仅控制字符数,还要估算 Token 数(中文约 1.5 字/token),超长段落强制二次切分。

和主流平台的客观对比

维度 MaxKB4j Dify FastGPT MaxKB
语言 Java 21 Python Node.js Python
核心框架 Spring Boot 3 + LangChain4j Flask + LangChain Next.js Django
RAG ✅ 混合检索 + RRF
工作流 ✅ DAG 引擎
Agent ✅ Tool + RAG
MCP 协议
虚拟线程
向量数据库 pgvector Weaviate/Qdrant MongoDB+pgvector Milvus
模型支持 20+ 国内外模型 20+ 10+ 10+
Java 团队集成 原生 需跨语言 需跨语言 需跨语言
社区规模 成长中
开源协议 GPL v3 Apache 2.0 Apache 2.0 GPL v3

不吹不黑: Dify 的社区和生态目前最大,FastGPT 的知识库体验最好,MaxKB 的 Gitee 生态最完善。MaxKB4j 的差异化在于 Java 生态原生 + 虚拟线程性能 + MCP 协议支持。如果你的团队是 Java 技术栈,这是目前唯一合理的选择。

写在最后

这个项目从 V1 到 V2.4,写了 1433+ 次提交,踩了无数的坑。最大的感悟是:Java 21 的虚拟线程彻底改变了 Java 在 I/O 密集型场景的竞争力。 以前 Java 做 LLM 应用,要么上 WebFlux 承受复杂的响应式编程,要么用传统线程池忍受低并发。虚拟线程让 Java 可以用最简单的同步代码,获得接近响应式编程的并发性能。

如果你是 Java 开发者,对 RAG / Agent / LLM 应用感兴趣,欢迎来 Gitee 看看代码、提 Issue、提 PR。Java 生态的 AI 工具链需要更多人一起建设。


🔗 项目地址:gitee.com/taisan/MaxK...

如果觉得有收获,欢迎点赞收藏。有问题可以在评论区讨论,我会逐一回复。

相关推荐
永远睡不够的入1 小时前
C++继承详解
java·c++·redis
feasibility.1 小时前
Agent-Reach赋能OpenClaw成为信息管家:实现GitHub/X/b站/小红书等十大平台信息获取(含手动安装)
人工智能·github·微信公众平台·新浪微博·小红书·openclaw·agent-reach
兑生1 小时前
【灵神题单·贪心】1833. 雪糕的最大数量 | 排序贪心 | Java
java·开发语言
冷雨夜中漫步1 小时前
AI入门——什么是知识图谱?
人工智能·知识图谱
实在智能RPA1 小时前
实在 Agent 支持哪些企业业务场景的自动化?全行业智能自动化场景深度拆解
java·运维·自动化
Xpower 171 小时前
Clawith:开启多智能体协作的新纪元
人工智能·python·语言模型·自动化
TsingtaoAI1 小时前
面向工业互操作性与优化的AI驱动数字孪生语义与模块化编排
人工智能·数字孪生
左左右右左右摇晃2 小时前
Java并发——偏向锁
java
moxiaoran57532 小时前
使用springboot+flowable实现一个简单的订单审批工作流
java·spring boot·后端