第十章 基础检索服务 - 核心知识点速记
第十章 基础检索服务 - 完整总结本章核心实现向量检索基础流程从用户问题到返回可引用的证据一、本章要解决的核心问题前面章节已经把文档存入 MySQL子块向量存入 Milvus但系统还不知道怎么根据用户问题找资料。用户提问时不能直接把所有文档都塞给大模型文档太多超过模型上下文长度无关内容干扰回答没有证据筛选模型容易编造幻觉检索服务的定位用户问题 → 检索服务 → 少量相关证据 → 回答服务 → 最终答案二、核心概念详解候选candidatesvs 证据evidence候选candidates向量检索召回的原始结果可能包含低分或不相关内容数量较多如 top_k8证据evidence经过过滤、分数筛选、父块扩展后的最终结果直接交给大模型使用数量较少如 evidence_top_k3为什么要区分方便调试可以看召回阶段找到什么最终用了什么方便评估分别评估召回率和精确率子块 vs 父块子块短200字信息聚焦适合向量检索精确匹配向量检索命中子块父块长完整段落/章节上下文完整不适合直接检索太长命中子块后回查父块作为最终证据设计思路子块负责找得准父块负责读得懂过滤条件RetrievalFilters字段类型作用document_idslist[str]限制只从指定文档中检索topicstr限制只检索某个主题effective_beforedate限制只检索某个日期之前生效的政策注意document_ids 必须用 default_factorylist不能用 []防止多个请求共享同一个列表对象。trace 调试信息trace 是检索链路追踪信息记录每一步的关键数据original_query原始用户问题vector_results向量检索原始召回结果final_evidence最终证据用途开发调试排查为什么没召回或为什么召回太多的问题。三、代码详解RetrievalFilters 定义from datetime import datefrom pydantic import Fieldclass RetrievalFilters(APIModel):document_ids: list[str] Field(default_factorylist)topic: str | None Noneeffective_before: date | None None【代码解释】为什么用 default_factorylist保证每个请求有独立的列表避免多个请求共享同一个可变对象为什么不用 []虽然 Pydantic 会特殊处理但可读性差复制到 dataclass 会出 bugRetrievalResult 定义dataclassclass RetrievalResult:query: strcandidates: list[dict[str, Any]]evidence: list[dict[str, Any]]refused: boolrefusal_reason: str | Nonetrace: dict[str, Any]【代码解释】为什么用 dataclass自动生成init和repr方便打印和调试为什么区分 candidates 和 evidencecandidates 是召回结果evidence 是最终证据方便分别评估元数据过滤函数 apply_metadata_filtersdef apply_metadata_filters(rows: list[dict[str, Any]],filters: RetrievalFilters) - list[dict[str, Any]]:filtered rowsif filters.topic:filtered [item for item in filteredif item[“metadata”].get(“topic”) filters.topic]if filters.effective_before:boundary filters.effective_before.isoformat()filtered [item for item in filteredif item[“metadata”].get(“effective_date”)and item[“metadata”][“effective_date”] boundary]return filtered【代码解释】为什么 topic 和 effective_before 在向量结果上过滤因为 Milvus 只支持 document_ids 过滤其他元数据过滤需要在结果上做为什么 effective_date 用字符串比较Milvus 中日期存为字符串直接用 ISO 格式比较即可父块扩展函数 expand_parent_contextasync def expand_parent_context(session: Session, evidence: list[dict[str, Any]]) - list[dict[str, Any]]:# 收集所有父块 IDparent_ids {item[“metadata”].get(“parent_id”)for item in evidenceif item[“metadata”].get(“parent_id”)}if not parent_ids:return evidence# 查询父块 parents { chunk.id: chunk for chunk in ( session.scalars( select(DocumentChunk).where(DocumentChunk.id.in_(parent_ids)) ) ).all() } # 替换为父块内容并去重 expanded: list[dict[str, Any]] [] seen_parents: set[str] set() for item in evidence: parent_id item[metadata].get(parent_id) parent parents.get(parent_id) if parent and parent.id not in seen_parents: item { **item, content: parent.content, metadata: { **item[metadata], section: parent.section or item[metadata].get(section, ), page: parent.page or item[metadata].get(page, 0), context_chunk_id: parent.id, }, } seen_parents.add(parent.id) expanded.append(item) elif not parent: expanded.append(item) return expanded【代码解释】为什么用 set 收集 parent_ids自动去重为什么用字典 parents通过 id 快速查找父块为什么用 seen_parents 去重多个子块命中同一个父块时只保留一份为什么 context_chunk_id 存父块 ID方便追溯最终证据的来源主检索函数 retrieveasync def retrieve(session: Session,question: str,filters: RetrievalFilters,top_k: int 8,evidence_top_k: int 4,min_score: float 0.25,) - RetrievalResult:# 1. 向量检索vector_results await VectorStoreService().search(question,top_k,filters.document_ids or None,)# 2. 元数据过滤 vector_results apply_metadata_filters(vector_results, filters) candidates vector_results[:top_k] # 3. 分数筛选 evidence [] for item in candidates[:evidence_top_k]: score max(0.0, min(1.0, float(item.get(score, 0)))) if score min_score: item[evidence_score] score evidence.append(item) # 4. 父块扩展 evidence await expand_parent_context(session, evidence) # 5. 拒答判断 refused not evidence # 6. 构建 trace trace { original_query: question, vector_results: vector_results, final_evidence: evidence, } return RetrievalResult( queryquestion, candidatescandidates, evidenceevidence, refusedrefused, refusal_reason检索结果未达到证据阈值 if refused else None, tracetrace, )【代码解释】为什么向量检索取 top_kevidence 取 evidence_top_k检索阶段多取一些筛选阶段再截断为什么用 max(0, min(1, score))确保分数在 0-1 范围内为什么判断 refused not evidence证据为空时不回答减少幻觉为什么 trace 要保存 vector_results 和 final_evidence方便调试对比召回和最终结果四、配置说明本章没有新增配置使用已有配置配置项默认值作用top_k8向量召回数量evidence_top_k4最终证据数量min_score0.25最低证据分数阈值五、重点难点总结重点必须掌握检索服务的作用从知识库找相关证据不是直接回答candidates 和 evidence 的区别子块召回 父块扩展的设计思路RetrievalFilters 的三个过滤条件trace 的作用和内容难点理解原理为什么用子块检索、父块回答Pydantic 中 default_factorylist 和 [] 的区别父块扩展中去重的原因生产实践建议min_score 阈值调优建议用测试集调整找到精确率和召回率的平衡点trace 日志生产环境建议把 trace 写入日志方便排查问题监控指标关注拒答率太高说明知识库覆盖不足六、面试题预测Q1: 检索服务在整个 RAG 系统中扮演什么角色参考答案检索服务是 RAG 系统的第一步负责从知识库中找到与用户问题相关的证据。它把检索结果交给后续的回答服务生成答案。如果检索找不到证据系统应该拒答而不是强行编造。Q2: 为什么用子块做检索却用父块作为最终证据参考答案子块更短、更聚焦适合向量检索召回精度高。父块更长、上下文完整适合给大模型生成回答。子块负责找得准父块负责读得懂。Q3: candidates 和 evidence 有什么区别参考答案candidates 是向量检索召回的原始候选结果数量较多可能包含低分项。evidence 是经过分数筛选和父块扩展后的最终证据数量较少直接给大模型使用。Q4: trace 字段的作用是什么参考答案trace 是检索链路的调试信息记录原始问题、向量召回结果、最终证据等。方便开发人员排查为什么没召回或为什么召回太多的问题。Q5: 证据为空时为什么要拒答参考答案如果证据为空说明知识库中没有与问题相关的内容。强行让大模型回答容易编造内容幻觉。拒答并提示根据现有资料无法回答该问题更安全。七、自测清单能说出检索服务的作用能说出 candidates 和 evidence 的区别能解释为什么用子块检索、父块回答能说出 RetrievalFilters 的三个过滤条件能解释 default_factorylist 和 [] 的区别能说出 trace 包含哪些信息能解释证据为空时为什么要拒答能画出检索服务的完整流程八、完整流程图用户问题↓① 生成查询向量VectorStoreService.search↓② Milvus 向量召回top_k 个候选↓③ 元数据过滤topic / effective_before↓④ 分数阈值过滤score min_score↓⑤ 回查父块上下文expand_parent_context↓⑥ 证据为空→ 拒答refused True↓⑦ 返回 RetrievalResult

相关新闻

网页视频下载终极指南:猫抓浏览器扩展资源嗅探与M3U8解析一文搞定

网页视频下载终极指南:猫抓浏览器扩展资源嗅探与M3U8解析一文搞定

网页视频下载终极指南:猫抓浏览器扩展资源嗅探与M3U8解析一文搞定 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 打开某个网页&#x…

2026/9/24 22:00:26 阅读更多 →
当 AI Agent 开始写生产代码,初级程序员的窗口期还剩多久

当 AI Agent 开始写生产代码,初级程序员的窗口期还剩多久

上周有个前同事问我,现在招 1-3 年经验的 Java 开发,简历投递量比上半年跌了一半。我以为是错觉,去脉脉、V2EX、HackerNews 刷了一圈,发现不是一半,是断崖。 这不是个别公司的招聘偏好变了,是整个行业的供需…

2026/10/15 11:35:04 阅读更多 →
具身智能的注意力机制与眼脑协同:从生物视觉到主动感知的范式重构

具身智能的注意力机制与眼脑协同:从生物视觉到主动感知的范式重构

引言 当前主流具身智能研究存在一个根本性的方向误区:试图给机器装上一个“完美的摄像头”,再配一个“全知的模型”,仿佛只要算力够强、数据够多,机器就能看懂世界。但这条路忽略了一个最根本的事实——人眼的伟大,不在…

2026/10/13 10:28:43 阅读更多 →

最新新闻

Unity Profiler性能分析实战:从真机采集到GC优化完整指南

Unity Profiler性能分析实战:从真机采集到GC优化完整指南

1. 为什么每个Unity开发者都该把Profiler当日常工具做Unity这几年,我见过太多项目在编辑器里跑得飞起,一打包到真机就卡成幻灯片。帧率从90掉到22,发热烫手,玩家骂声一片,团队却找不到瓶颈在哪。这种场景几乎每个中大型…

2026/10/15 22:09:39 阅读更多 →
SolidWorks干涉检查假干涉排查:隐藏实体与压缩的区别及选项设置

SolidWorks干涉检查假干涉排查:隐藏实体与压缩的区别及选项设置

1. 先别急着翻设计树:干涉检查报出的"假干涉"从哪来做整机装配干涉检查的时候,我猜你肯定遇过这种场面:明明把弹簧、密封圈、线束、装饰罩这些零件都隐藏了,图纸看着清爽得很,结果一点"干涉检查"&…

2026/10/15 22:09:39 阅读更多 →
collect.js 严格比较设计指南:全方法默认 `===` 的实现原理与 Laravel 差异对照

collect.js 严格比较设计指南:全方法默认 `===` 的实现原理与 Laravel 差异对照

开发工具 【免费下载链接】collect.js 💎 Convenient and dependency free wrapper for working with arrays and objects 项目地址: https://gitcode.com/gh_mirrors/co/collect.js 点击查看 免费下载 导读 本文围绕 collect.js 的**严格比较&#x…

2026/10/15 22:09:39 阅读更多 →
6D位姿估计综述:RGB-D、点云、RGB三条路线选型与复现避坑指南

6D位姿估计综述:RGB-D、点云、RGB三条路线选型与复现避坑指南

简介:这份资源是面向机器人视觉与三维感知方向学习者、研究者的6D位姿估计综述文献总结,以思维导图形式梳理了Vision-based Robotic Grasping综述的核心脉络,帮助读者快速建立从物体定位、位姿估计到抓取估计的整体认知框架。内容覆盖基于RGB…

2026/10/15 22:09:39 阅读更多 →
memtester内存压力测试实战:从原理到脚本化压测

memtester内存压力测试实战:从原理到脚本化压测

简介:这是一份内存压力测试工具 memtester 4.1.2 的完整源码包,面向 Linux/Unix 系统管理员、运维工程师与底层软硬件开发者,用于检测内存条位翻转、数据丢失、地址错误等不稳定因素,也可作为服务器上线前压力验证和故障排查的有力…

2026/10/15 22:09:39 阅读更多 →
MySQL时间类型选型:DATETIME与TIMESTAMP底层差异与避坑指南

MySQL时间类型选型:DATETIME与TIMESTAMP底层差异与避坑指南

MySQL 的时间类型,尤其是在 DATETIME 和 TIMESTAMP 之间反复纠结的情况,我见过太多了。很多人直到线上出现“时间差8小时”或者“2038年数据写不进去”才发现自己对这两个类型的理解还是靠猜。今天把这俩家伙的底层逻辑、行为差异、性能取舍和选型思路一…

2026/10/15 22:08:39 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →