跳到主要内容

检索到答案:从问题到带引用的回答

本章讲在线问答:query 进来后,怎么捞 chunk、怎么重排、怎么把图片喂给 LLM。

1. 总编排:query 一步步做什么

DocumentService.query(core/services/document_service.py:1032)是问答总入口,流程很直白:

query(问题, k=20, use_colpali?, use_reranking?, padding?, schema?, ...)

├─▶ retrieve_chunks(...) 捞候选 chunk(核心,见 §2)
├─▶ _create_document_results 补齐 chunk 所属文档的元信息
├─▶ augmented_content(...) 每个 chunk 拼上文档级上下文(:1122)
├─▶ 收集 sources / 可选 inline 引用元数据(:1124)
└─▶ completion_model.complete(CompletionRequest) 让 LLM 生成答案(:1201)

k 默认 20(注释 from contextual embedding paper,:1037)。支持流式(stream_response)和结构化输出(schema)。

2. retrieve_chunks:四配置矩阵

retrieve_chunks(core/services/document_service.py:182)是检索心脏。它开头的注释(:217-221)点明了**「是否 rerank」×「是否 colpali」= 4 种配置**:

配置rerankcolpali行为
1直接返回普通单向量检索的 chunk
2返回 ColPali 多向量库的 chunk(库自带打分,不再另跑小模型)
3单向量捞更多,再用交叉编码器 reranker 重排取 top-k
4走 ColPali 多向量库的打分,直接用它排序

关键开关计算在 :229-233:using_multivector = requested_multivector and settings.ENABLE_COLPALI and multivector_available。若请求了 colpali 但组件不全,会告警并降级到普通检索(:252)。

2.1 并行:embedding 与鉴权同时跑

一个务实的优化:算查询 embedding 和「查有权访问哪些文档」是互不依赖的,所以并行(asyncio.gather(timed_embeddings(), timed_auth()),:353)。鉴权走 find_authorized_and_filtered_documents(:340),它把多租户 ACL、文件夹范围、status=completed 过滤一次做完;没有授权文档就直接返回空(:371)。

2.2 图片查询也支持

查询本身可以是一张图(query_image)。此时必须 use_colpali=True(否则 400,:238),走 generate_embeddings(query_pil_image) 出多向量(:290),实现「以图搜图/以图搜页」。

2.3 标准 reranker(配置 3)

只在「非 colpali + 开了 rerank + reranker 存在」时启用(use_standard_reranker,:387)。它先过量召回(oversample_k = max(k, min(3*k, 20)),:395),再用 FlagReranker.rerank(core/reranker/flag_reranker.py:30,交叉编码器 BAAI/bge-reranker)对 (query, chunk) 对逐个精打分、重排、截断到 k(document_service.py:464-468)。

为什么 colpali 路不用它?因为 ColPali 多向量库的 MaxSim 打分本身已经是强排序信号(:386 注释)。

3. padding:把命中页的前后页也捞回来

对 ColPali 检索,一个 chunk = 一页(page)。答案常横跨相邻页。_apply_padding_to_chunks(core/services/document_service.py:558)在命中页前后各补 padding 页:

命中第 5 页,padding=2
──▶ 额外捞第 3、4、6、7 页(去重、只对图片 chunk 生效)

它按 (doc_id, chunk_number ± i) 生成待补的页号(:620-630),用 colpali_vector_store.get_chunks_by_id 批量取回(:642),命中页保留原分、补的页记 0 分并打上 padding 标记(:679-683),最后按分数+页序排序(:688)。注意:padding 只对图片 chunk 生效(_is_image_chunk 过滤,:582),非图片 chunk 在开 padding 时会被丢掉。

4. 把 chunk 喂给 LLM:多模态拼装

检索回来的 chunk 里,图片是 data URI 字符串(data:image/...)。process_context_chunks(core/completion/litellm_completion.py:78)把它们分成两拨:

# core/completion/litellm_completion.py:94 (简化)
if chunk.startswith("data:image/"):
image_urls.append(chunk) # 图片 → 走多模态 image_url
else:
context_text.append(chunk) # 文本 → 拼进文字 context

然后按 provider 组装消息:非 Ollama 走 LiteLLM,图片以 {"type": "image_url", "image_url": {"url": img_url}} 附到消息里(:367)。总入口 complete(:620)还处理三种模式:普通、流式(_handle_streaming_*)、结构化输出(用 instructor 按 schema 约束,:679)。

4.1 引用来源

每个答案都带 ChunkSource(document_id, chunk_number, score)(document_service.py:1164)。开 inline_citations 时,还会为每个 chunk 记 filename + page number,ColPali chunk 的页号 = chunk_number + 1(0 索引转 1 索引,:1148)——因为对 ColPali,chunk_number 就是页序。

5. 一句话小结

问答 = retrieve_chunks(四配置矩阵,embedding 与鉴权并行) → 可选 reranker/padding → chunk 分文本/图片拼成多模态 context → LiteLLM 生成带引用的答案。检索的灵活性(单向量便宜路 vs ColPali 视觉路)是这一层的设计核心。