首页 / 文章 / Spring AI与LangChain4j:相同的RAG技术,不同的隐性设计选择

Spring AI与LangChain4j:相同的RAG技术,不同的隐性设计选择

基于同一份手册的双Java RAG构建显示,与Spring集成后行数差异有所缩小;而一个错误的休假答案则暴露了分块处理的默认设置。

736 词

同样的RAG流程被构建了两次。最初的测试结果看似已定论,但一次实验却改变了这一结论。

从RAG服务中删除85行Java代码并未改变用户能看到的任何结果——同时也结束了团队围绕框架问题持续两周的争论。

Spring AI的支持者和LangChain4j的支持者各自带来了演示文稿,但没人带来双重实现版本。这两个版本都是在完全相同的控制条件下编写的。

控制条件保持不变:一份240页的人力资源PDF文件、一个嵌入模型、Postgres加上pgvector数据库、一个聊天模型,以及在一开始编写任何代码之前就确定的200道题目集。

目标在于:如果行为存在差异,原因应出在框架本身,而非两种手工设计的架构差异。

统一的流程结构

数据获取环节没有任何特别之处:

Question
   |
   v
Embed Query
   |
   v
Vector Search
   |
   v
Top 4 Chunks
   |
   v
Build Context
   |
   v
Chat Model
   |
   v
Answer

有意让数据导入过程也显得十分单调:

PDF
 ↓
Extract Text
 ↓
Split Into Chunks
 ↓
Generate Embeddings
 ↓
Store In pgvector

重点在于框架之间的比较,而非会干扰结果的新型架构。

Spring AI版本

配置文件依然非常简洁:

spring:
  ai:
    openai.api-key: ${OPENAI_KEY}
    vectorstore.pgvector:
      initialize-schema: true

数据接入作为Spring组件实现:

@Component
class Ingest {
  Ingest(VectorStore store) {
    var docs =
        new TikaDocumentReader(
            "classpath:/docs/handbook.pdf").get();

store.add(new TokenTextSplitter().apply(docs));
  }
}

用于处理查询的HTTP接口:

@RestController
class AskApi {
  private final ChatClient ai;

AskApi(ChatClient.Builder b, VectorStore store) {
    this.ai = b.defaultAdvisors(
        new QuestionAnswerAdvisor(store)).build();
  }
  @GetMapping("/ask")
  String ask(@RequestParam String q) {
    return ai.prompt().user(q).call().content();
  }
}

QuestionAnswerAdvisor负责隐藏信息检索、上下文整合以及提示词注入等操作。信息检索部分几乎不需要自定义代码——这看似是个优点,但后来情况有所变化。

LangChain4j版本

显式的实现方式让更多的组件结构一目了然:

var embed =
    OpenAiEmbeddingModel.builder()
        .apiKey(key)
        .build();

var store =
    PgVectorEmbeddingStore.builder()
        .host("localhost")
        .port(5432)
        .database("rag")
        .user("app")
        .password(pw)
        .table("chunks")
        .dimension(1536)
        .build();
EmbeddingStoreIngestor.builder()
    .documentSplitter(
        DocumentSplitters.recursive(500, 60))
    .embeddingModel(embed)
    .embeddingStore(store)
    .build()
    .ingest(
        FileSystemDocumentLoader.loadDocument(path));

信息检索器的构建方式同样透明可见:

var retriever =
    EmbeddingStoreContentRetriever.builder()
        .embeddingStore(store)
        .embeddingModel(embed)
        .maxResults(4)
        .minScore(0.6)
        .build();

确实易于理解。但与Spring AI相比是否更简洁?起初并非如此。该应用在Java代码行数上的差异约为41行对比126行——LangChain4j版本的代码量几乎是Spring AI的三倍。从代码简洁性这一角度来看,这场争论似乎已经有了定论。

接着系统回答了一个关于陪产假的问题。

手册中没有的答案

当被问及手册规定的陪产假天数时,模型自信地回答:

Employees are entitled to 12 days of paternity leave,
subject to the conditions listed in the policy.

手册上写的是15天。该政策文件中从未出现过12这个数字。通过检索到的上下文才得知真相:政策表格被默认的分割器拆分了,相关行分散在不同的数据块中;检索工具返回的是各自看似合理的片段;模型将这些片段拼凑成了一个逻辑通顺但错误的答案。

这个漏洞存在于聊天模型之前。框架的行数统计并未能预测出质量问题。

3倍的差距并非框架所致

最关键的那一行是:

new TokenTextSplitter().apply(docs)

一次调用就决定了诸多未经审查的细节:表格处理方式、数据块边界、重叠问题、保留的元数据,以及检索系统实际看到的内容。Spring AI让这些选择容易被忽视,而LangChain4j则使更多此类决策在应用代码中可见。

最初的对比还将与Spring Boot集成的Spring AI与更为手动化的LangChain4j设置进行了比较。通过为LangChain4j添加Spring集成功能后,代码行数减少到了58行。两者之间的差距从大约3倍缩小到了约1.4倍。复杂性只是转移到了框架内部,并未从系统中消失。

实际应用中该如何选择

  • 现有的Spring Boot服务 → 对于常规的RAG应用,可选择Spring AI,因其流程更简化且操作更为便捷。
  • 独立的 Java 实现或复杂的自定义检索方式 → 当需要明确的处理流程组件以及可调节的分块/检索参数时,应选择 LangChain4j。
  • 不要仅凭行数来决定。在构建完两种方案后,真正的筛选标准应是:你愿意在框架的默认设置中保留多少种处理选项?请从这里开始考虑——在有人删掉85行并宣称胜利之前。