以 knowledge-base.md 为语料的 Spring AI RAG 集成测试实战指南
【免费下载链接】spring-aiAn Application Framework for AI Engineering项目地址: https://gitcode.com/GitHub_Trending/spr/spring-ai
本指南以 Spring AI 仓库中 knowledge-base.md 为切入点,剖析这份测试知识库在
QuestionAnswerAdvisor、RetrievalAugmentationAdvisor等检索增强生成(RAG)集成测试中的完整生命周期——从 Markdown 文档读取、向量化入库、相似度检索到上下文增强问答,并给出可直接复用的代码示例与断言思路。读完本文,你将掌握如何在 Spring AI 项目中构建一套"文档 → 向量库 → Advisor → 问答"的端到端集成测试。
一、这份文档是什么:一份"会讲故事"的测试语料
knowledge-base.md位于 spring-ai-integration-tests/src/test/resources/documents/,是 Spring AI 集成测试模块(spring-ai-integration-tests)内置的知识库样本。它的正文并非技术说明,而是一篇完整的虚构故事《Anacletus and Birba's Quest for the Loch of the Stars》(《阿纳克莱图斯与比尔巴的星之湖寻宝记》),共 10 个章节,讲述了:
- 主角:一只谨慎聪明的猫头鹰Anacletus和一只活泼好奇的猫Birba;
- 冒险地点:苏格兰高地(Scottish Highlands),最终目的地是传说中夜晚比任何湖泊都明亮的星之湖(Loch of the Stars);
- 关键情节:跨越小溪、遇到长毛高地牛Fergus(指路"沿大石头旁的小路走")、穿过神秘森林、与鹿群相遇、在湖畔过夜后返程回家。
这份语料的设计初衷值得玩味:它包含精确可验证的事实点(如"冒险发生在苏格兰高地")、专有名词(Anacletus、Birba、Fergus、Loch of the Stars)以及前后呼应的叙事线索,非常适合用来校验 RAG 链路是否真的"检索到了正确上下文"。同时它没有任何版权与合规负担,可以被安全地提交进仓库并反复运行。
在正文中,文章提供了知识库文件本身的完整内容:
你可以在 knowledge-base.md 中直接查看全部 10 个章节的原始文本。集成测试通过
@Value("${classpath:documents/knowledge-base.md}")将该资源注入为Resource,例如 QuestionAnswerAdvisorIT.java 与 RetrievalAugmentationAdvisorIT.java 中的用法完全一致。
二、知识库的完整生命周期:从 Markdown 到向量存储
在任一集成测试中,这份知识库都要经历一条标准化的数据处理管线,其核心代码在每个测试类的setUp()中保持一致(见 QuestionAnswerAdvisorIT.java):
@BeforeEach void setUp() { DocumentReader markdownReader = new MarkdownDocumentReader(this.knowledgeBaseResource, MarkdownDocumentReaderConfig.defaultConfig()); this.knowledgeBaseDocuments = markdownReader.read(); this.pgVectorStore.add(this.knowledgeBaseDocuments); }2.1 读取:MarkdownDocumentReader
MarkdownDocumentReader是 Spring AI 提供的 Markdown 专用读取器,实现类位于 spring-ai-markdown-document-reader 模块。- 构造函数接收
Resource(此处为 classpath 下的documents/knowledge-base.md)和MarkdownDocumentReaderConfig。defaultConfig()使用默认配置;MarkdownDocumentReaderConfig支持按需调整分块(chunk)尺寸、是否按标题切分等参数(详见 MarkdownDocumentReaderConfig 源码)。 read()返回List<Document>——一个 10 章故事会被切分为多个Document,每个文档即一个可检索的知识单元。
2.2 入库:PgVectorStore
PgVectorStore是 PostgreSQL + pgvector 扩展的向量存储实现,位于 spring-ai-pgvector-store。pgVectorStore.add(documents)会为每个Document生成嵌入向量并写入数据库,同时保留其文本与元数据。- 测试结束后,
tearDown()通过pgVectorStore.delete(ids)清理数据,保证测试可重复运行:
@AfterEach void tearDown() { this.pgVectorStore.delete(this.knowledgeBaseDocuments.stream().map(Document::getId).toList()); }提示:在
RetrievalAugmentationAdvisorIT与QuestionAnswerAdvisorIT中,PgVectorStore均通过@Autowired注入,并由TestApplication(spring-ai-integration-tests模块)提供自动配置。整个链路依赖真实的 OpenAI API(测试类标注@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+"))与可用的 PostgreSQL/pgvector 实例。
三、QuestionAnswerAdvisor:基于向量检索的问答
3.1 基础用法
QuestionAnswerAdvisorIT.java 中最基础的一条测试如下:
@Test void qaBasic() { String question = "Where does the adventure of Anacletus and Birba take place?"; QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(this.pgVectorStore).build(); ChatResponse chatResponse = ChatClient.builder(this.openAiChatModel) .build() .prompt(question) .advisors(qaAdvisor) .call() .chatResponse(); assertThat(chatResponse).isNotNull(); String response = chatResponse.getResult().getOutput().getText(); assertThat(response).containsIgnoringCase("Highlands"); evaluateRelevancy(question, chatResponse); }关键点:
QuestionAnswerAdvisor.builder(vectorStore).build()一条链式调用即完成 Advisor 装配;- 通过
ChatClient.advisors(qaAdvisor)挂载后,用户的原始问题会先被拿去向量库检索,检索到的文档作为上下文与问题一起交给模型; - 断言
response.containsIgnoringCase("Highlands")直接验证了"模型确实基于检索到的知识库回答,而非依赖自身先验知识"——这正是本题选材的巧妙之处:Anacletus 与 Birba 是虚构角色,模型不可能"知道"这个故事。
3.2 用 RelevancyEvaluator 做相关性评测
测试末尾的evaluateRelevancy使用了 Spring AI 的评测能力(QuestionAnswerAdvisorIT.java):
private void evaluateRelevancy(String question, ChatResponse chatResponse) { EvaluationRequest evaluationRequest = new EvaluationRequest(question, chatResponse.getMetadata().get(QuestionAnswerAdvisor.RETRIEVED_DOCUMENTS), chatResponse.getResult().getOutput().getText()); RelevancyEvaluator evaluator = new RelevancyEvaluator(ChatClient.builder(this.openAiChatModel)); EvaluationResponse evaluationResponse = evaluator.evaluate(evaluationRequest); assertThat(evaluationResponse.isPass()).isTrue(); }QuestionAnswerAdvisor.RETRIEVED_DOCUMENTS是 Advisor 写入响应元数据中的键,携带本次实际检索到的文档,可用来追溯检索质量;RelevancyEvaluator让模型充当"裁判",判断回答与问题的相关性,将结果封装为EvaluationResponse并断言isPass()。这样就把"有没有答对"从人工判断升级为自动化断言。
3.3 流式场景
QuestionAnswerAdvisorStreamIT.java 验证了 Advisor 在流式(streaming)上下文中的行为,它使用OpenAiChatOptions.builder().streamUsage(true)开启流式用量统计,并通过Flux<String>订阅逐块输出:
Flux<String> responseFlux = ChatClient.builder(this.openAiChatModel) .build() .prompt(question) .advisors(qaAdvisor) .options(OpenAiChatOptions.builder().streamUsage(true)) .stream() .content(); String response = responseFlux.collectList().block().stream().collect(Collectors.joining()); assertThat(response).isNotEmpty(); assertThat(response).containsIgnoringCase("Highlands");这保证了对同一个知识库,流式与非流式问答能给出同样准确的检索增强结果。
3.4 自定义提示模板
QuestionAnswerAdvisor默认用一套内置模板把"用户问题 + 检索上下文"拼接后交给模型,但允许通过.promptTemplate()完全替换。官方文档 retrieval-augmented-generation.adoc 与集成测试 qaCustomPromptTemplate 展示了自定义模板的两个硬性要求——必须包含$query$(用户问题)与$question_answer_context$(检索上下文)两个占位符:
PromptTemplate customPromptTemplate = PromptTemplate.builder() .renderer(StTemplateRenderer.builder().startDelimiterToken('$').endDelimiterToken('$').build()) .template(""" $query$ Context information is below, surrounded by --------------------- --------------------- $question_answer_context$ --------------------- Given the context and provided history information and not prior knowledge, reply to the user comment. If the answer is not in the context, inform the user that you can't answer the question. """) .build(); QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(this.pgVectorStore) .promptTemplate(customPromptTemplate) .build();两种常见的占位符定界符用法:
| 定界符 | 配置方式 | 场景 |
|---|---|---|
<query>/<question_answer_context> | StTemplateRenderer配置<、>作为起止符 | 避免与模板内其他尖括号内容冲突 |
$query$/$question_answer_context$ | StTemplateRenderer配置$、$作为起止符 | 默认风格,语义清晰 |
3.5 自定义模板渲染器
qaCustomTemplateRenderer 展示了另一种定制维度:在ChatClient层配置TemplateRenderer,使带占位符的用户消息在进入 Advisor 前先被渲染:
ChatResponse chatResponse = ChatClient.builder(this.openAiChatModel) .build() .prompt() .user(user -> user.text("Where does the adventure of <character1> and <character2> take place?") .param("character1", "Anacletus") .param("character2", "Birba")) .advisors(qaAdvisor) .templateRenderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build()) .call() .chatResponse();注意区分两层渲染:ChatClient.templateRenderer()渲染的是进入 Advisor 之前的初始用户消息;QuestionAnswerAdvisor.Builder.promptTemplate()定制的是Advisor 合并检索上下文时的提示模板。二者职责不同,可独立配置。
四、RetrievalAugmentationAdvisor:模块化 RAG 流水线
与开箱即用的QuestionAnswerAdvisor不同,RetrievalAugmentationAdvisor采用模块化架构,让你显式组装"查询处理 → 检索 → 上下文增强 → 生成"的每一环。RetrievalAugmentationAdvisorIT.java 以同一份knowledge-base.md语料,完整演示了六种典型装配方式。
4.1 朴素 RAG(Naive RAG)
RetrievalAugmentationAdvisor ragAdvisor = RetrievalAugmentationAdvisor.builder() .documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(this.pgVectorStore).build()) .build(); ChatResponse chatResponse = ChatClient.builder(this.openAiChatModel) .build() .prompt("Where does the adventure of Anacletus and Birba take place?") .advisors(ragAdvisor) .call() .chatResponse();VectorStoreDocumentRetriever负责把问题转成向量并查询PgVectorStore。官方文档 retrieval-augmented-generation.adoc 还展示了通过similarityThreshold(0.50)设置相似度阈值等参数。
4.2 请求级过滤:FILTER_EXPRESSION
通过VectorStoreDocumentRetriever.FILTER_EXPRESSION上下文参数,可以在不修改 Advisor 定义的情况下按元数据动态过滤检索范围(RetrievalAugmentationAdvisorIT.java):
ChatResponse chatResponse = ChatClient.builder(this.openAiChatModel) .build() .prompt("Where does the adventure of Anacletus and Birba take place?") .advisors(ragAdvisor) .advisors(a -> a.param(VectorStoreDocumentRetriever.FILTER_EXPRESSION, "location == 'Italy'")) .call() .chatResponse(); // 由于知识库中没有任何文档的 location 元数据等于 Italy, // 检索结果为空,DOCUMENT_CONTEXT 元数据为 null assertThat((String) chatResponse.getResult().getMetadata() .get(RetrievalAugmentationAdvisor.DOCUMENT_CONTEXT)).isNull();这个断言非常巧妙:它验证了过滤表达式确实生效——"Italy" 与故事语料完全无关,因此检索不到任何文档,Advisor 的DOCUMENT_CONTEXT元数据为空。同样的机制在 VectorStoreDocumentRetrieverIT.java 中有更细粒度的验证:四个带location元数据(Whispering Woods、Alfea等)的文档,在location == 'Whispering Woods'过滤下只命中 2 篇。
4.3 高级 RAG:查询变换与扩展
同一个知识库还驱动了三种"查询端"优化策略的测试:
| 策略 | 作用 | 测试示例 | 关键配置 |
|---|---|---|---|
CompressionQueryTransformer | 结合对话历史压缩查询,去除冗余指代 | ragWithCompression(结合MessageChatMemoryAdvisor与MessageWindowChatMemory,追问 "Did they meet any cow?" 断言答出 "Fergus") | .chatClientBuilder(ChatClient.builder(openAiChatModel)) |
RewriteQueryTransformer | 把口语化/模糊问题重写为适合向量检索的表述 | ragWithRewrite(问题 "Where are the main characters going?" 断言答出 "Loch of the Stars") | .targetSearchSystem("vector store") |
TranslationQueryTransformer | 跨语言检索,先翻译再检索 | ragWithTranslation(丹麦语问题 "Hvor finder Anacletus og Birbas eventyr sted?" 断言答案含 "highlands" 或 "højland") | .targetLanguage("english") |
MultiQueryExpander | 把一个问题扩展为多个变体查询,提升召回 | ragWithMultiQuery | .numberOfQueries(2) |
以重写为例:
RetrievalAugmentationAdvisor ragAdvisor = RetrievalAugmentationAdvisor.builder() .queryTransformers(RewriteQueryTransformer.builder() .chatClientBuilder(ChatClient.builder(this.openAiChatModel)) .targetSearchSystem("vector store") .build()) .documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(this.pgVectorStore).build()) .build();这些测试共同说明:语料本身的一致性(人名、地名、情节在全文重复出现)是检验"查询优化后仍能命中正确上下文"的前提。
4.4 文档后处理:替换检索结果
ragWithDocumentPostProcessor展示了documentPostProcessors回调——可以在检索之后、送入模型之前对文档列表做任意变换(RetrievalAugmentationAdvisorIT.java):
RetrievalAugmentationAdvisor ragAdvisor = RetrievalAugmentationAdvisor.builder() .documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(this.pgVectorStore).build()) .documentPostProcessors((query, documents) -> List .of(Document.builder().text("The adventure of Anacletus and Birba takes place in Molise").build())) .build();这里直接把检索结果替换为一段新文本,随后断言模型回答包含 "Molise",用于验证后处理管线确实接管了文档流。
五、从语料设计反推测试要点:如何"复制"这套测试方案
综合以上测试,knowledge-base.md之所以能同时服务多种 RAG 场景,是因为它天然满足了几条测试语料设计原则,这也正是你在自己项目中编写测试知识库时可借鉴的:
- 事实可断言:故事中的地点(Highlands)、角色(Anacletus、Birba、Fergus)、目标(Loch of the Stars)在文中反复出现,任意一次成功检索都可用
containsIgnoringCase精确断言; - 虚构无污染:角色与情节均为原创,模型不具备先验知识,回答必须来自检索上下文,杜绝"蒙对";
- 跨语言/跨形态可测:适合验证翻译、重写、压缩等查询变换器的效果;
- 元数据可扩展:配合
FILTER_EXPRESSION可模拟空结果、精确过滤等边界场景。
若要在自己的 Spring AI 项目中复刻这套方案,最小步骤为:
- 准备一份 Markdown 知识库并放入
src/test/resources/documents/; - 用
MarkdownDocumentReader读取并add进PgVectorStore(或任意 VectorStore); - 在
ChatClient上挂载QuestionAnswerAdvisor或RetrievalAugmentationAdvisor; - 用断言 +
RelevancyEvaluator双保险验证答案正确性与相关性; - 通过
@EnabledIfEnvironmentVariable控制需要真实模型 API 的测试执行。
六、小结
knowledge-base.md表面上是一篇童话故事,实质上是 Spring AI 集成测试体系中设计精良的"事实固定"语料:它以 10 个章节构建了一个可检索、可断言、可变换的知识空间,被QuestionAnswerAdvisor、RetrievalAugmentationAdvisor、VectorStoreDocumentRetriever以及流式问答等多个集成测试共同引用,覆盖了 RAG 链路中的读取、入库、检索、过滤、查询优化、后处理、评测与流式输出等全部关键环节。阅读其对应源码(QuestionAnswerAdvisorIT.java、RetrievalAugmentationAdvisorIT.java、VectorStoreDocumentRetrieverIT.java)与官方 RAG 文档(retrieval-augmented-generation.adoc),即可在自有项目中复现整套"文档 → 向量库 → Advisor → 问答"的工程实践。
【免费下载链接】spring-aiAn Application Framework for AI Engineering项目地址: https://gitcode.com/GitHub_Trending/spr/spring-ai
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考