切换深色模式
09 · 真实资料检索:从常量数组走到项目文件
前置:03、L3,建议先跑 08 的默认模式。本课预计 3–4 小时。交付:真实 Markdown 索引、召回记录、一组权限与版本测试。
原来的固定笔记只能检查控制流。真实文章有标题、前置元数据、代码块和不同章节,直接 split('\n\n') 会把条件和结论分开。本课提供能看懂每一步的词项检索基线,先建立可量化的参照,再考虑向量检索。
1. 资料入口必须是可信配置
cli.mjs 的 MANIFEST 只列出本站两篇公开学习文章:延时任务笔记与 JVM 堆笔记。路径相对 docs 根目录,owner 由程序配置。本实现不会递归扫描工作区、跟随外部网页或读取任意模型提供的路径。
模型能传入的是片段 ID 与版本,不是文件系统路径。搜索先过滤 owner,读取再独立检查。真实登录系统仍未实现,alice 是本地模拟身份;授权测试通过不能转述为完成了 SSO。
2. 切分规则能解释,错误也应能定位
解析器支持 ATX 标题,也就是 # 到 ######,并保存标题层级。识别以反引号或波浪号围起的代码块,代码中的 # 不作为新章节。开头的 YAML 元数据不进入正文。源码:knowledge.mjs。
text
原始 Markdown → 去掉首部元数据 → 识别标题与代码围栏
→ 章节片段 → 原文件行号 + 标题路径 + 内容摘要版本这里是有边界的教学解析器,不是完整 CommonMark:不支持所有嵌套列表里的代码、Setext 标题、MDX 或 PDF。遇到未闭合围栏或超过 6000 字符的章节直接拒绝,不静默截断。学生应先拆分长章节或引入正式解析器,再比较结果。
每个片段结构如下,字段由解析器生成,模型不能自行声明:
json
{
"id": "notes/delayed-tasks.md:L25",
"revision": "整篇文件原始内容的 SHA-256",
"file": "notes/delayed-tasks.md",
"heading": "延时任务:选型与复盘 / 设计一次故障演练",
"lineStart": 25,
"lineEnd": 30,
"text": "该章节原文"
}行号从 1 开始,区间保留原文件的位置,可能包含空行。ID 不是跨编辑的永久身份:在前面插入一行就可能改变后续 ID,因此必须与 revision 一起使用。原始文件的换行变化也会改变摘要,本课有意选择保守失效策略。
3. 搜索分数到底是什么意思
中文连续文本拆成相邻双字组合,英文按词项处理。查询、正文与标题分别生成去重词项集合。对每个查询词项:正文命中加 1,标题路径命中再加 1,按总分排序;同分按 ID 排序。分数只用于排序,不是相关概率。
例如查询“故障恢复”,词项为“故障、障恢、恢复”。“故障”出现在正文会命中,但同义词“容灾”不一定命中。这解释了为什么基线能处理精确术语,却可能漏掉语义相近的表达。
这个算法没有声称实现 BM25。它没有逆文档频率,也没有长度归一化;长标题和重复的公共主题词可能让多个章节同分。先让这种局限在开发查询集里显现,再决定是否换算法。
4. 搜索只返回候选,读取才产生证据
搜索结果含 ID、revision、行号、标题与 score,不含正文。截断时返回 truncated:true。当前没有分页游标:模型可以缩小查询,不能要求未实现的“下一页”。
读取时再次打开源文件,核对整篇摘要是否仍等于索引版本。若文件改变,返回 stale_revision;若文件删除或变得不可读,返回 source_unavailable。这阻止“旧候选 ID + 新正文”被拼成同一来源。
索引是启动时快照,本版本不会自动热更新。遇到过期可停止并重新建库,也可以在后续扩展显式刷新工具;不要让模型反复读取相同旧版本直到耗尽预算。
5. 路径控制的实际范围
清单拒绝绝对路径、..、重复文件和不支持的后缀。每次读取先解析真实路径,再验证它位于 docs 根目录。这也能拒绝最终目标逃出根目录的符号链接。
本地教学环境假定清单和文件目录由可信开发者控制。真实路径检查与打开之间仍存在并发替换窗口,本实现不是对抗恶意本机写入者的文件沙箱。生产环境应使用受控文档存储和对象标识,不把工作区文件权限当租户隔离。
6. 运行真实资料的召回实验
powershell
node docs/public/examples/research-project/retrieval-eval.mjs程序使用预先标注的章节标题来定位当前 gold 片段,输出每题命中 ID、Recall@3 和无答案题的空结果行为。如果 gold 章节不存在,直接报错,避免文档改名后报告仍然看似正常。
输出的 macroRecallAt3 只平均有答案的任务;无答案题单独检查。开发题目和资料都公开,不能当成保留测试集。Recall 提升也不等于最终答案更正确;这个脚本不生成答案、不调用模型、不检查语义支持。
7. 三次实验,只改一个因素
实验 A:标题权重。 先保留原始报告,再在副本中取消标题加分,比较每题排序与 Recall@3。若某题只因标题有查询词而上升,打开原文检查它是否确实有回答内容。
实验 B:问题改写。 保持索引不变,将“故障恢复”改为自己的同义表达。检查词项变化导致的召回差异。不要把新问法的低召回误归因为生成模型。
实验 C:文档更新。 搜索一次,编辑测试副本对应文章,再读取旧引用。必须得到 stale_revision。重建索引后取得新引用再读,才能成功。
参考解答与合格标准
不能预设标题加权一定更好:有利于主题定位,也可能把只有概述的片段排到答案前。实验报告必须保留逐题结果,而不是只报平均值。更新实验在 project.test.mjs 中有参考:先索引,再改文件,然后断言旧版本不进入证据集合。
合格交付至少包含一题真正相关但词项不匹配的失败题、一题重复主题词导致错误排序的题,以及一次版本变更。修复必须说明解决哪一类失败,没有测到的情况不得写成“全面提升”。
8. 什么时候再引入向量检索
若失败主要来自同义表达,可以增加语义召回作为候选来源;若失败来自参数名和代码标识丢失,应保留精确词项通道;若失败来自段落条件丢失,应先改解析。替换数据库通常解决不了切分时就丢失的信息。
任何新检索器仍要输出相同来源和版本契约,执行相同资源授权,并与当前基线使用相同查询、资料快照与 k。再评估额外延迟、成本和删除更新链。这些是后续扩展任务,本项目未实现向量索引或 reranker。
继续:10 项目交付与运行复盘