📌 系列说明:一个 Java 后端视角的 Spring AI 渐进式实战教程,载体为开源项目「劳小司 · 智能法律助手」。
- 序章:技术栈全景与 AI 学习指南
- 阶段一 · 流式对话内核:篇1 SSE 流式·停止·思考可见化 / 篇2 会话记忆压缩与滚动体验
- 阶段二 · 工具调用:篇1 Function Calling 与法律计算器 / 篇2 联网搜索与工具预算
- 阶段三 · RAG 知识库:篇1 起步与底账化 / 篇2 Agentic RAG 与引用可信度(本文) / 篇3 检索质量与体验
- 阶段四 · 多模型路由:篇1 五路级联路由
- 阶段五 · 安全与质量门:篇1 安全层与强制检索 / 篇2 质量门与评估门禁 / 篇3 指代消解与阻塞隔离
- 阶段六 · 产品化与用户体系:篇1 认证·配额·门禁 / 篇2 前端·移动端·身份 / 篇3 劳动法专精与多模态
- 阶段七 · 存储演进与部署:篇1 存储迁移 / 篇2 部署契约
📦 本篇涉及:tool/SearchLawTool.java、rag/LawCitation.java、rag/CitationRegistry.java。
一、Naive RAG 的三个问题
篇1 的"每轮无条件前置检索 Top-K 拼 Prompt"虽然能跑,但有三个实打实的问题:
根因是:检索的时机、检索词、次数都被代码写死了。能不能交给模型自己判断?
二、检索工具化:从 Workflow 到 Agent
把检索做成一个 @Tool——searchLaw,让模型自主决定:
@Tool(description = "检索与法律问题相关的法律条文。当用户咨询劳动合同、经济补偿、加班费、"
+ "试用期、诉讼时效等法律问题时,回答前必须先调用本工具获取条文依据,"
+ "并在回答中注明法律名称与条文编号。闲聊、问候、通用知识问题不需要调用。")
public String searchLaw(
@ToolParam(description = "检索关键词。不要照抄用户原话,请提炼为法律术语,"
+ "例如用户问'被公司辞退了能拿多少钱'应检索'解除劳动合同 经济补偿 标准'") String query,
@ToolParam(description = "可选业务领域过滤:劳动/民事/商事/刑事…;不限制时不传", required = false) String category,
ToolContext toolContext) {
ToolBudget budget = (ToolBudget) toolContext.getContext().get(ToolBudget.KEY);
if (budget != null && !budget.tryAcquire()) { /* 预算耗尽,劝退 */ }
List<LawCitation> cites = retrievalService.retrieveCitations(query, category);
CitationRegistry registry = (CitationRegistry) toolContext.getContext().get(CitationRegistry.KEY);
if (registry != null) registry.register(cites); // 命中登记
return cites.stream().map(LawCitation::text).reduce((a, b) -> a + "\\n\\n" + b).orElse("");
}
这一步之后,三个问题一次性解决:闲聊不调(模型判断不需要就不调,省 token)、检索词由模型改写成法律术语、多问题可多次调用(多跳)。
架构上,这是一个质变:从"流程写死的 Workflow"跨入"模型自主决策步骤的 Agent"。
但要注意:工具化后,检索"要不要做"变成了模型的决策——模型可以不听。它可能觉得自己知道就跳过检索,直接凭记忆作答,幻觉又回来了。这个隐患本篇先埋下,阶段五·篇1 用"强制检索打底(Grounded ReAct)"来实锤回收。
三、引用可信度:别让 UI 给幻觉盖权威章

早期 retrieve() 返回 List<String>,Document 的 metadata(articleId/lawName/score)全丢了。前端引用卡怎么办?只能用正则从模型输出里猜——模型写了"《劳动合同法》第四十七条"就渲染成一张 § 引用卡。
问题很严重:引用卡展示的是"模型说了什么",不是"检索命中了什么"。模型要是编造一个不存在的条文号,UI 会把它包装成看起来权威的引用卡——等于给幻觉盖了个权威章。
解法:让引用以结构化形态贯穿全链路。 定义 LawCitation record:
public record LawCitation(Long articleId, String lawName, String articleNo,
String category, double score, String text, boolean chunked) {
public String key() { return lawName + "|" + articleNo; } // 去重与真伪校验的键
}
它一路带着走:检索命中 → System Prompt 里以 [1][2][3] 编号注入 → SSE citation 事件推前端 → 审校校验真伪,全链路同一份数据。前端引用卡展示的是"检索真实命中了什么",而不是"模型嘴上说了什么"。引用卡上的 TOP 0.xx 是真实相关度分。
四、CitationRegistry:请求级引用登记器
有个现实问题:命中产生在两处——一是 ChatService 的强制打底检索(进模型前),二是 ReAct 循环里模型调 searchLaw 的多跳检索(发生在 Spring AI 工具调用内部,ChatService 拿不到)。审校时要把两处的命中合起来校验答案引用的真伪。
解法是一个请求级登记器 CitationRegistry,经 ToolContext 注入,两处命中都 register 进来:
public class CitationRegistry {
public static final String KEY = "citationRegistry";
private final Set<String> keys = ConcurrentHashMap.newKeySet(); // lawName|articleNo
private final List<LawCitation> citations = Collections.synchronizedList(new ArrayList<>());
public void register(List<LawCitation> hits) {
for (LawCitation c : hits) {
if (keys.add(c.key())) citations.add(c); // 去重:同键只留一条
}
}
public boolean contains(String lawName, String articleNo) { // 审校:引用是否真实命中过
return keys.contains(lawName + "|" + articleNo);
}
}
生命周期 = 单次请求(每轮 new 一个),天然请求隔离;内部用并发容器,因为 Reactor 链和工具调用可能跨线程。它同时是 SSE citation 事件的数据源(前端展示)和 AnswerReviewer 的校验数据源(阶段五·篇2)。
五、踩坑备忘
① RedisVectorStore 读回时自定义 metadata 可能丢失。 因为 metadata 没声明进索引 schema,读回来是空的。兜底链:articleId 从文档 id(law:article:{id}#cN)解析,法名/条号从固定文本格式 法律名称:《X》。条文编号:Y 正则解析。结构化引用要设计好"metadata 丢了也能还原"的兜底。
② 双路命中会重复,前端 v-for key 冲突。 稠密路和稀疏路可能命中同一条文,citation 推给前端前必须按 lawName+articleNo 去重(保留相关度更高的一条),否则 CitationCard 的 :key 冲突导致 Vue patch 错乱、引用卡子树渲染冻结。
③ 工具化不等于可靠。 Agentic RAG 让检索更聪明,但把"要不要检索"交给了模型——这是灵活性的代价。生产级系统必须在架构层补一道确定性兜底(就是阶段五的强制检索打底),不能纯指望模型自觉。
六、小结
| Agentic RAG | 检索做成工具,模型自主决定时机/检索词/次数 |
| Workflow → Agent | 检索决策从代码写死变为模型自主 |
| LawCitation | 结构化引用贯穿全链路,展示"命中了什么" |
| CitationRegistry | 请求级登记器,打底+多跳命中统一供审校 |
| 埋的隐患 | 模型可以不听(阶段五·篇1 回收) |
七、下篇预告
检索交给模型、引用可溯源了,但还差临门一脚:检索本身准不准。单纯向量 Top-K 召回质量有限,"第四十七条"这种离散编号更是命中不了。下一篇把检索管线升级到工业级五阶段。
🌐 源码与体验:Gitee(国内快)https://gitee.com/spaserby/laoxiaosi.git | GitHub https://github.com/spaserby/laoxiaosi.git 🖥 在线演示:https://laoxiaosi.noctisblue.com 本系列全套代码皆开源,觉得这篇有帮助,欢迎顺手点颗 ⭐
网硕互联帮助中心




评论前必须登录!
注册