Citations and Trace
AI4J 这一章里,citation 和 trace 很容易被一起写成“可解释性能力”。
这个说法不算错,但太粗。源码里这两者其实解决的是两件不同的事:
citation解决“最终上下文和回答能引用到哪里”trace解决“RAG 管线里命中是怎么变化的”
这页把两条链分开讲。
1. 源码入口在哪里
关键类很少,但非常集中:
rag/DefaultRagService.javarag/DefaultRagContextAssembler.javarag/RagContext.javarag/RagCitation.javarag/RagTrace.javarag/RagResult.java
从这组类就能看出,AI4J 当前的 citation/trace 都属于 RAG 结果组装层,不是 provider 层能力。
2. citation 是怎么生成出来的
真正生成 citation 的不是 retriever,也不是 reranker,而是:
DefaultRagContextAssembler.assemble(query, hits)
它会按最终 hits 顺序逐条处理:
- 为每个 hit 分配
citationId,格式是S1、S2、S3 - 从 hit 里提取:
sourceNamesourcePathsourceUripageNumbersectionTitlecontent
- 生成
RagCitation - 同时拼接给模型的
context.text
所以 citation 不是单独的一张表,它和最终给模型的上下文文本,是在同一个 assembler 里一起产生的。
3. RagCitation 里到底保存什么
当前 RagCitation 很轻:
citationIdsourceNamesourcePathsourceUripageNumbersectionTitlesnippet
这里没有:
- 精确字符 offset
- chunkId
- dataset
- 命中分数
这说明 AI4J 当前 citation 的设计目标更偏:
- 给模型和用户一个可读来源标识
- 给排障提供基本来源线索
而不是做细粒度审计级引用定位。
4. context 文本里的引用前缀是怎么拼的
DefaultRagContextAssembler 默认会在每段命中内容前面加类似:
[S1] source label
其中 source label 的拼接优先级大致是:
sourceNamesourcePath- 否则退化成
source
如果有:
pageNumbersectionTitle
也会继续附加到 label 上。
然后才把 hit.content 接到后面。
这说明 citation 在 AI4J 当前实现里,不只是返回结构化字段,而是 直接写进了给模型看的上下文文本。
5. includeCitations 控制的到底是什么
RagQuery.includeCitations 默认是 true。
但从 DefaultRagContextAssembler 的实现看,它控制的是:
- 是否把
[S1] source label这种前缀写进context.text
它不控制 RagCitation 列表是否生成。
也就是说,即使你把 includeCitations = false:
RagResult.citations仍然会有内容- 只是给模型的上下文不再内嵌 citation 标签
这个细节很容易被误解成“关掉后就没有 citations 了”,实际上不是。
6. trace 是怎么生成出来的
trace 由 DefaultRagService.search(...) 生成。
只有当:
query != null && query.isIncludeTrace()
时,结果里才会带:
RagTrace.builder()
.retrievedHits(hits)
.rerankedHits(reranked)
.build()
也就是说,当前 trace 只记录两段:
- 检索后命中列表
- rerank 后命中列表
它不记录:
- 最终模型回答
- prompt 拼装细节
- token 使用
- provider reasoning
所以别把它误写成“全链路 trace”。
7. 为什么 trace 不能只靠最终回答替代
最终回答告诉你的只是模型输出。
但 RAG 排障真正常见的问题是:
- 是没召回到?
- 还是召回到了但排序不对?
- 还是排序对了但 context 太长被噪音稀释?
当前 RagTrace 虽然很轻,但至少能帮你区分前两类问题:
retrievedHits看召回集合rerankedHits看排序变化
这对于调:
- dense topK
- hybrid 融合
- rerank topN
- finalTopK
都非常重要。
8. citation 和 trace 的数据来源为什么不同
这两者的来源阶段并不相同。
citation 来自:
- 最终传给
contextAssembler的finalHits
trace 来自:
retrievedHitsrerankedHits
这意味着 citation 是 后状态,trace 更偏 中间状态记录。
一个直接后果是:
- 被
finalTopK裁掉的 hit 可能出现在 trace 里 - 但不会出现在 citations 里
所以看到某条命中“trace 有,citation 没有”,不一定是 bug,很可能只是因为它没进最终上下文。
9. citation 质量真正受什么影响
从源码看,citation 的内容几乎直接取自 RagHit。
因此 citation 质量首先受前面几层影响:
- chunking 是否合理
- metadata 是否完整
- retriever 是否把
sourceName/sourcePath/pageNumber/sectionTitle带回来了 - rerank 后是否替换过 content
尤其 DenseRetriever,它会尽量从 metadata 恢复:
documentIdsourceNamesourcePathsourceUripageNumbersectionTitlechunkIndex
如果 ingest 时这些字段没有入库,citation 再怎么写也只能很弱。
10. 当前设计最真实的边界
AI4J 这层 citation/trace 很有用,但边界要说透。
它当前没有直接提供:
- chunk 级精确偏移定位
- 最终回答中的 span 到 citation 的自动绑定
- prompt 版本记录
- 模型使用了哪条 citation 的因果证明
- provider 级可追踪性
所以它更像是:
- 一个可读引用机制
- 一个轻量 RAG 过程快照
而不是法务级、审计级、科研级引用系统。
11. 最容易踩坑的 5 个点
11.1 以为 citation 来自原始文档
当前 citation 实际来自最终 RagHit,不是直接来自原始文档对象。
11.2 以为关闭 includeCitations 就没有 citation 结构
关掉的只是上下文文本里的标签前缀,不是 RagCitation 列表本身。
11.3 以为 trace 是完整调用链
当前 trace 只覆盖 retrieval 和 rerank 两段,不覆盖最终生成。
11.4 忽略 metadata 质量
没有 sourceName、pageNumber、sectionTitle 的 hit,citation 可读性会明显下降。
11.5 把 citation 和 answer grounding 画等号
AI4J 能提供引用材料,不等于模型最终一定严格按这些引用作答。
12. 最稳的扩展位置在哪里
如果你想增强 citation/trace,当前最稳的扩展位点是:
- 自定义
RagContextAssembler - 改进 ingest metadata
- 在上层 runtime 外挂更完整的 trace 记录
不要把所有事情都压到 retriever 上。
retriever 负责找回 chunk,不负责决定最终 citation 呈现格式。
13. 这页最该记住的结论
AI4J 当前的 citation 和 trace 不是同一件事:
- citation 由
DefaultRagContextAssembler基于最终 hits 生成 - trace 由
DefaultRagService记录 retrieval / rerank 两个阶段
前者面向“最终引用与上下文呈现”,后者面向“中间过程排障”。
把这两层分清,RAG 的可解释性分析才不会混。