数据截至 (上游 commit 9ad3e4e49aa3)
生成答案与引用:证据组装、带引证问答、附加物
30 秒导读: 上一章 混合检索与重排 把问题变成了一堆排好序的文档片段。这一章讲这些片段如何变成一个带引用的答案:先拼成一段证据文本,喂给 LLM 一边流式吐答案、一边并行抽出"哪几句话支持了答案",再把这些句子回锚到原文做高亮,顺带算个可信度分、画个思维导图。
本章聚焦默认(非 agent)的问答链。可插拔的 ReAct / ReWOO 推理留给 05 可插拔推理。想先看整体地图,回 index。
1. 这是什么(零基础也能懂)
一句话定义: 把检索到的文档片段,变成一段有出处、可核对的自然语言答案。
它解决什么问题。 普通的 RAG 只是"把文档塞给模型,让它回答"。但用户会追问两件事:
- 这个答案从哪来的?哪段原文支持它?
- 这个答案可信吗?模型是不是在编?
Kotaemon 的问答链就是围绕这两个追问设计的。它不只给答案,还给出引用高亮(答案的每句话对应到原文哪一段)、可信度分数、以及低相关警告。
用起来什么样。 用户在聊天框问一句,右侧信息栏会陆续出现:
[聊天区] Fixed-size chunking 把文档切成固定大小的块,计算高效【1】,
但可能割裂语义相关内容【1】【2】。
[信息栏] Answer confidence: 0.87
▸ Content from paper.pdf (Page 3) ← 命中的原文,相关句被高亮
▸ Content from paper.pdf (Page 5)
Mindmap [Expand] [Export]
一句话直觉。 把它想成一个认真的助教:不光答题,还在答案边上用荧光笔划出课本里的依据,并在角落写一句"我大概有多确定"。
2. 顶层全景(它大概怎么转)
整条链的入口是 FullQAPipeline(继承 BaseReasoning),真源码在 libs/ktem/ktem/reasoning/simple.py:86。它的 stream() 方法(simple.py:281)是主控制流:
用户消息 message
│
▼
① retrieve() 遍历 retrievers,取回文档;按 metadata.type 分出 text / plot
│ simple.py:108
▼
② PrepareEvidencePipeline 把文档列表拼成一段带 HTML 标记的 evidence 文本
│ format_context.py:15 顺带定出 evidence_mode(纯文本/表格/图片)
▼
③ answering_pipeline.stream() 证据+问题喂 LLM,边流式吐答案,边并行抽引用
│ citation_qa.py:190 (另有并行线程算 relevance score / mindmap)
▼
④ show_citations_and_addons() 引用回锚到原文→高亮;算 qa_score;低相关警告;思维导图
simple.py:223
各部件的职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
FullQAPipeline | 问答总编排:检索→组证据→答题→展示引用 | libs/ktem/ktem/reasoning/simple.py:86 |
AddQueryContextPipeline | 用对话历史改写查询(当前默认关闭) | simple.py:42 |
PrepareEvidencePipeline | 把文档拼成证据文本 + 定证据模式 + 裁长度 | libs/kotaemon/kotaemon/indices/qa/format_context.py:15 |
AnswerWithContextPipeline | 默认"高亮"引用模式的答题器 | libs/kotaemon/kotaemon/indices/qa/citation_qa.py:83 |
AnswerWithInlineCitation | "内联"引用模式的答题器 | libs/kotaemon/kotaemon/indices/qa/citation_qa_inline.py:86 |
CitationPipeline / CiteEvidence | 用 function-calling 结构化抽引用片段 | libs/kotaemon/kotaemon/indices/qa/citation.py:22 |
一句话读图:证据在②被"拼装+裁剪",答案在③被"生成+同时抽 引用",可信度与高亮在④被"回锚+展示"。 检索本身是上一章的事,这里只消费它的输出。
3. 核心原理(逐个机制,由浅入深)
3.1 retrieve:取回文档,并把"图"和"文"分开
retrieve()(simple.py:108)做三件事:遍历所有 retriever、按 metadata.type 分流、跨 retriever 去重。
思路。 一个库可能挂多个 retriever(不同索引)。有的片段是普通文本,有的是绘图数据(type == "plot")。文本要送去答题,绘图数据只用来在界面上画图,不能当证据喂给 LLM。所以要先分流。
真实实现里,循环遍历 self.retrievers,对每个 retriever 的返回按 type 分桶,文本片段再用 doc_id 去重:
# 真源码节选 simple.py:137-148
for doc in retriever_docs:
if doc.metadata.get("type", "") == "plot":
retriever_docs_plot.append(doc)
else:
retriever_docs_text.append(doc)
for doc in retriever_docs_text:
if doc.doc_id not in doc_ids: # 跨 retriever 去重
docs.append(doc)
doc_ids.append(doc.doc_id)
返回两样东西:docs(纯文本片段列表,后续用于答题与引用)和 info(给界面的 Document,channel 分 "info" / "plot",simple.py:150-162)。info 在 stream() 里被立刻 yield(simple.py:293),所以界面能先看到"检索到了哪些文档",再等答案生成。
一个当前状态的小坑:
retrieve()顶部有一大段被注释掉的add_query_context逻辑(simple.py:112-120),现在硬编码query = message(simple.py:121-125)。也就是说默认不做历史改写查询——AddQueryContextPipeline虽然还在(见 3.5),但不在检索路径里被调用。
3.2 PrepareEvidencePipeline:把文档拼成一段"带标记"的证据
它要解决的小问题。 LLM 只能读一段文本。检索给的是结构化的文档对象(有正文、有表格、有图片、有 metadata),得压平成一段字符串,同时保留"这是表格/这是图/来自哪个文件"这些线索。
思路。 按每个片段的 metadata.type 走不同分支,拼成一段夹带 HTML 小标签的证据文本;同时记录出现过哪些"证据模式"。真源码在 format_context.py:28 的 run()。
四种证据模式是常量(format_context.py:9-12):
| 常量 | 值 | 触发条件 | 拼进证据的内容 |
|---|---|---|---|
EVIDENCE_MODE_TEXT | 0 | 默认 | Content from {source}: … 正文 |
EVIDENCE_MODE_TABLE | 1 | type == "table" | Table from {source} + 表格原文(最多 5 张) |
EVIDENCE_MODE_CHATBOT | 2 | type == "chatbot" | Excel 每行一个预设问答场景 |
EVIDENCE_MODE_FIGURE | 3 | type == "image" | Figure from {source} + <img> 占位 + 图片进 images 列表 |
拼的时候有两个去重细节:表格片段拼前先查 if retrieved_content not in evidence(format_context.py:61),纯文本同样查(format_context.py:93),避免重复内容占满上下文。
多模式如何收敛成一个。 一次检索可能同时命中文本和图。最终 evidence_mode 按优先级归一(format_context.py:101-105):图 > 表 > 文。这个模式决定 3.3 里用哪个 prompt 模板。
最后一步:裁长度。 全部拼完后,用 TokenSplitter 按 max_context_length(默认 32000,format_context.py:25)切一刀,只保留第一块(format_context.py:109-112)。超出上下文窗口的证据被直接丢弃——简单粗暴但可靠。
返回值是打包成 Document.content 的三元组 (evidence_mode, evidence, images)(format_context.py:114),在 stream() 里被解构(simple.py:295)。
3.3 AnswerWithContextPipeline:边流式答题,边并行抽引用
这是默认答题器(citation_qa.py:83),对应界面上的 "citation: highlight" 模式。它的 stream()(citation_qa.py:190)是本章最核心的一段。
第一步:按证据模式选 prompt。 get_prompt()(citation_qa.py:121)根据 evidence_mode 从四个模板里挑一个。这个默认模板族(citation_qa.py:40-80)共同的骨架是"用下面的上下文回答问题,不知道就说不知道,用 {lang} 回答":
| 模板常量 | 用于 | 特点 |
|---|---|---|
DEFAULT_QA_TEXT_PROMPT | 纯文本 | 要求"详细+清晰解释" |
DEFAULT_QA_TABLE_PROMPT | 表格 | 明示上下文含"文本/表格/图" |
DEFAULT_QA_CHATBOT_PROMPT |