Spring AI与LangChain4j:相同的RAG技术,不同的隐性设计选择
基于同一份手册的双Java RAG构建显示,与Spring集成后行数差异有所缩小;而一个错误的休假答案则暴露了分块处理的默认设置。
同样的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,因其流程更简化且操作更为便捷。
不要仅凭行数来决定。在构建完两种方案后,真正的筛选标准应是:你愿意在框架的默认设置中保留多少种处理选项?请从这里开始考虑——在有人删掉85行并宣称胜利之前。