大家好,我是一言,继续更新 Spring AI 2.0 系列。
前几篇解决了怎么找资料。到了实际使用,用户还会追问:这句话依据哪份文档?
微软的开源 RAG 示例把这种体验做得很直观:答案旁有引用编号,下方的Citations列出 PDF 文件和页码。看图中第一条回答,依据指向Benefit_Options.pdf第 3 页。
这篇把答案和来源一起返回:模型选择引用编号,Java 校验编号,再补上文件名、章节和原文。用户就能顺着引用核对回答。
我们先实现后端的来源回填,暂不做图中的 PDF 跳页和逐句引用。
先看实际返回什么
问“电子发票的抬头写错了怎么办?”,本地模型返回:
{"answer":"电子发票的抬头写错了,应先申请红冲原发票,再按正确抬头重新开具。","citationIds":["S1"]}Java 根据S1找到本次检索的资料,补出:
{"id":"S1","file":"invoice.md","section":"修改抬头","text":"电子发票抬头写错后,应先申请红冲原发票,再按正确抬头重新开具。"}文件名和原文取自后端保存的资料,模型只负责选编号。页面可以把来源做成展开项,查看对应章节和原文。
给本次检索结果编号
准备四段演示资料,分别记录银行卡退款、银行卡注销、定制退货和发票抬头修改。每段保留来源标签:
Document.builder().id("invoice-title").text("电子发票抬头写错后,应先申请红冲原发票,再按正确抬头重新开具。").metadata("source","invoice.md").metadata("section","修改抬头").build();检索取 Top 2,再给它们分配当前请求的编号:
varsources=newLinkedHashMap<String,Document>();for(inti=0;i<retrieved.size();i++){sources.put("S"+(i+1),retrieved.get(i));}发给模型的上下文按下面的格式组织:
[S1] 第一份检索资料的原文 [S2] 第二份检索资料的原文S1只在当前请求里有效。下一次提问的S1可能对应另一份资料,校验时要保留本次映射。
用 Java 对象接住引用
复用前面讲过的 Structured Output:
图左侧对应getFormat(),告诉模型按什么格式返回;右侧对应convert(),把返回文本接成下面的Draft对象。
recordDraft(Stringanswer,List<String>citationIds){}varconverter=newBeanOutputConverter<>(Draft.class);Stringraw=client.prompt().system(system).user("资料:\n"+context(sources)+"\n问题:"+question+"\n"+converter.getFormat()).call().content();Draftdraft=converter.convert(raw);提示词要求:只引用直接支持回答的资料,citationIds填S1这样的编号,不生成文件名或网址。
Spring AI 提供模型调用和对象转换。引用协议、编号映射和校验由我们实现,转换成功也不代表编号合法。
编号先校验,再返回
校验的核心是:引用必须出现在本次候选中。
for(Stringid:draft.citationIds()){Documentdoc=sources.get(id);if(doc==null){thrownewIllegalArgumentException("UNKNOWN_CITATION: "+id);}// 从 doc 取文件名、章节和原文,组装 sources 返回}完整实现还检查空答案、空引用,并去重。资料不足时模型可以返回空引用,这版示例会拦下该结果;产品中可另设“资料不足”状态。
我用固定输入验证了三个异常场景:
NEGATIVE invented=S99 rejected=true NEGATIVE outsideRequest=invoice-title rejected=true NEGATIVE missingCitation rejected=true即使invoice-title是知识库中已有的文档 ID,也不能绕过本次S1/S2映射直接引用。这三条是 Java 校验测试,没有让模型故意生成错误编号。
有引用,仍要看原文
真实模型测试用了 Java 21、Spring AI 2.0.1,Qwen3-Embedding 0.6B 和 Qwen2.5 3B。向量在内存中按余弦相似度排序,三道题各调用一次回答模型。
银行卡注销这题,回答带出了人工处理要求,也返回了对应章节和原文:
| 问题 | 返回来源 | 原文核对 |
|---|---|---|
| 银行卡注销后退款多久到 | 银行卡注销章节 | 回答包含人工处理和到账限制 |
| 定制商品不想要了能否退 | 定制商品章节 | 引用有效,回答漏掉质量问题条件 |
| 发票抬头写错怎么办 | 修改抬头章节 | 回答与红冲重开规则一致 |
第二题尤其值得看:
S1确实存在,校验会通过;原文中的“存在质量问题时”,却被回答省略了。编号校验负责拦住无效引用,原文核对才能发现这种条件遗漏。
本地执行RUN_REAL_MODEL_TEST=true mvn test,结果为:
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS其中四项检查引用解析与异常,另一项执行三道题的真实模型对照。引用让答案有据可查,无法自动保证每句话都正确。
作者:一言。