我用 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 应用开发框架,提供了 ChatLanguageModel、EmbeddingModel、ContentRetriever、ToolProvider 等核心抽象。它做的事情和 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...

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

相关推荐
承渊政道3 分钟前
HIL-UMI论文解读:用手持示范补齐机器人操作短板——对启元Q1机器人开发的启发
人工智能·深度学习·机器人·论文解读·具身智能·启元q1
解决问题no解决代码问题7 分钟前
AI 研发:一致性漂移的加速陷阱
人工智能
Shaoshing7 分钟前
缓存一致问题及常见解决方案
java·缓存
启信慧眼.8 分钟前
怎么用 AI 做供应商尽调
人工智能
聚美智数13 分钟前
文档转换 API‑文档转 PDF‑PDF 转文档‑异步文档处理‑文件格式迁移‑转换结果回调推送 API 接口介绍
android·java·pdf
视图猿人17 分钟前
ROS2 nav2全局规划器和局部规划器参数说明
人工智能
俊哥V18 分钟前
每日 AI 研究简报 · 2026-10-23
人工智能·ai
2401_8322981020 分钟前
与人工智能同行:机遇、挑战与未来选择
人工智能
从零数据分析23 分钟前
零基础学SQL 23:和上月比总对不上?LAG/LEAD 一步取出上一次(附 4 个模板)
java·数据库·sql·mysql·oracle·sqlite·database
lambo mercy25 分钟前
什么是LangGraph?
人工智能