数据截至 (上游 commit 5215e9791133)
RSE 相关段落抽取:把碎片拼回长段(查询主算法)
30 秒导读: 普通 RAG 检索返回一堆固定长度的 top-k 碎片,答案却常常横跨连续好几个 chunk。dsRAG 的招牌算法 RSE(Relevant Segment Extraction,相关段落抽取) 换了个思路:先给每个 chunk 打"相关性分",再用一个带约束的优化,动态挑出分数最高的一段连续 chunk——长度不定,能长能短,自动贴合问题的复杂度。项目名里的 "d(ynamic)s(egment)RAG" 精神就落在这一章。
本章讲 dsRAG 的查询侧。摄取侧(把文件切成带上下文头的 chunk)见 01-ingestion-pipeline 与 02-autocontext;组件如何插拔见 04-pluggable-components;更上层的对话与引用见 05-chat-and-citations。
1. 这是什么(先建直觉)
1.1 固定 top-k 的毛病
先看普通 RAG 怎么做检索:把问题嵌入成向量,去向量库里找最相似的前 k 个 chunk(比如 k=5),把这 5 段拼起来喂给 LLM。
问题在于:k 是拍脑袋定死的,而答案的"体量"是不定的。
- 问"公司 2023 年的净利润是多少?"——答案可能就在一个 chunk 里,取 5 个纯属浪费、还塞进噪声。
- 问"详细讲讲这份合同里关于知识产权的全部条款"——答案可能连续横跨 12 个 chunk,取 5 个直接答不全。
更糟的是,固定 top-k 取回的往往是分散的碎片:第 3 段、第 47 段、第 102 段……它们各自相似度高,但彼此不连续,LLM 拿到一堆上下文断裂的片段,很难拼出完整答案。
1.2 RSE 的答案:动态找"连续段"
RSE 的核心主张只有一句:
不要返回 k 个分散的 chunk,而是返回若干条"连续的、最相关的段"(segment),每段的长度由数据自己决定。
一句类比:普通 top-k 像用镊子从书里夹出 5 个词;RSE 像用荧光笔在书上划出几个连续的段落——划多长,取决于这一段有多相关、相关性延续了多久。
一段连续 chunk [start, end) 就叫一个 segment。RSE 要解决的是:在所有 chunk 里,哪几条连续区间的"总相关性"最高,同时不能太长、不能重叠、不能跨到别的文档去。
1.3 用起来什么样
对使用者而言,RSE 是完全隐藏在 query() 后面的:
# 示意:调用方只管发问题,RSE 在内部自动完成
results = kb.query(
search_queries=["知识产权条款有哪些", "IP ownership clauses"], # 可给多个 query
rse_params="balanced", # 预设:balanced / precision / find_all
)
for seg in results:
print(seg["doc_id"], seg["chunk_start"], seg["chunk_end"], seg["score"])
print(seg["content"]) # 已经是拼好的一整段长文本(或 page images)
# 重点看:返回的是「段」,chunk_start~chunk_end 是一段连续区间,不是孤立 chunk
返回的每个 seg 就是一条 segment,content 是这段连续 chunk 拼接起来的整块文本。真实签名见 knowledge_base.py:858 的 KnowledgeBase.query。