为什么 Recall@5 不够用
第03篇开了一个先例:用 30 道检索题测向量路径,Recall@5 = 0.958。后来每篇涉及检索实验的文章,都用类似的方式汇报结果。
Recall@5 是个合理的起点指标,但它有三个盲区:
盲区一:精度。 Recall@5 问的是"正确答案有没有出现在前5个结果里",但代码检索的目标经常是"精确定位到那一个函数"。出现在第5位和出现在第1位,用 Recall@5 得分一样,但实际价值差很多——第1位意味着直接命中,第5位意味着还要手动翻找。
盲区二:任务类型。 第08篇确立了三路检索架构,不同路径对应不同任务:向量路径做语义探索,图路径做结构遍历,符号路径做精确匹配。一套 Recall@5 数据集评的是哪条路径?三条混在一起评,还是分开评?混评会掩盖某条路径的具体短板。
盲区三:影响面分析。 代码库知识库的核心价值之一是"改这个函数会影响哪些地方"。这类任务的正确答案是一个集合(所有上游调用方),评测时不能只看"有没有返回相关结果",还要看"有没有漏掉重要的调用方"。这是召回率问题,但不是 Recall@5。
四维评测指标
针对代码库知识库的特点,提出四个专用指标:
指标一:符号定位准确率(Symbol Location Accuracy)
定义: 对一组"找这个功能在哪实现"的查询,正确答案(目标函数)出现在第一位的比例。
为什么用 Top-1 而不是 Top-5: 代码库检索的"成功"是找到那个具体的函数。出现在第3位说明还需要人工筛选。Top-1 准确率衡量的是直接命中能力,这对实际工程使用体验影响最大。
评测数据集构建:
查询示例:
Q1: "如何向 LightRAG 插入新文档"
A1: lightrag/lightrag.py::ainsert (函数,行1428)
Q2: "LightRAG 支持哪些查询模式"
A2: lightrag/base.py::QueryParam (类,行83)
Q3: "文档分块策略在哪里决定的"
A3: lightrag/parser/routing.py::resolve_chunk_options
Q4: "删除一个文档会触发哪些清理操作"
A4: lightrag/lightrag.py::adelete_by_doc_id对应本系列实验数据:第03篇的向量检索 Recall@5 = 0.958,转换为 Top-1 准确率后通常在 0.75~0.85 之间——前5位里有答案,但排在第一的比例更低。
指标二:语义搜索召回率(Semantic Search Recall)
定义: 对一组查询,正确答案出现在前 K 个结果里的比例。K 通常取 5 或 10。
和 Recall@5 的区别: 评测对象不是全量检索,而是专门针对"术语不对齐"场景——用户说"文件解析",代码里叫"document ingestion pipeline";用户说"缓存机制",代码里叫"KV storage with TTL"。
这类查询最能体现向量路径的价值:BM25 在术语不对齐时会失败,向量 embedding 可以跨越词汇鸿沟。
评测数据集构建要点: 查询必须刻意使用与代码不同的术语,否则 BM25 就能答对,测不出向量路径的差异。
好的测试用例(术语不对齐):
Q: "文档去重逻辑" → A: compute_mdhash_id (operate.py)
Q: "LLM 请求速率控制" → A: priority_limit_async_func_call (utils.py)
Q: "知识图谱节点合并" → A: _merge_nodes_then_upsert (operate.py)
弱的测试用例(术语直接对齐,BM25 就能做到):
Q: "priority limit async func" → A: priority_limit_async_func_call指标三:影响面分析完整率(Impact Analysis Completeness)
定义: 对一个目标函数,要求返回"所有直接调用方",评测实际返回结果和真实调用方集合的交集比例。
这是最接近实际工程价值的指标:改一个函数之前,知识库能不能告诉我所有会受影响的地方。
公式:
Completeness = |returned callers ∩ actual callers| / |actual callers|对 LightRAG 的实测数据(第09篇):QueryParam 的真实调用方有 19 个,search_code("QueryParam") 返回了全部 19 个。这一组的 Completeness = 1.0。
但并非所有函数都这么理想。BaseVectorStorage.upsert 的 fan_in = 268,如果工具有返回数量上限(比如 limit=50),就会漏掉 218 个调用方,Completeness 急剧下降。这不是召回算法的问题,而是工具配置的问题——但评测能揭示这个配置缺陷。
指标四:历史查询命中率(History Query Hit Rate)
定义: 对一组"这段代码为什么这样写"的查询,FILE_CHANGES_WITH 边或 detect_changes 历史数据是否提供了有意义的线索。
这是最难量化的指标,因为"有意义的线索"本身是主观的。实践中通常用二值评分:
1 = 查询结果中有直接相关的历史变更信息
0 = 查询结果为空或完全无关对于 priority_limit_async_func_call:查询它所在的 utils.py 在历史上的变更模式,得到"高频变更文件"这一信号——命中,评分 1。
对于 base.py::QueryParam:FILE_CHANGES_WITH 显示 base.py ↔ lightrag.py 有强耦合——命中,评分 1。
评测数据集的三种来源
构建评测数据集时,代码库知识库比普通文档 RAG 有一个天然优势:代码本身就是答案的标注来源。
来源一:从测试文件中提取
测试文件是"这个函数应该做什么"的最佳文档。对每个测试函数,可以反向推导出"这是对哪个生产函数的检索问题":
# tests/test_query.py
def test_hybrid_query():
param = QueryParam(mode="hybrid")
result = rag.query("what is LightRAG", param)
...从这个测试,可以自动生成:
- 查询:'LightRAG 的混合查询模式'
- 期望答案:
lightrag/base.py::QueryParam
LightRAG 有超过 200 个测试文件,每个测试文件都是潜在的评测数据来源。
来源二:从 git commit message 中提取
Git commit message 里经常包含"修复了哪个函数的什么问题"的信息,可以反向构造出查询:
commit: "fix: priority_limit_async_func_call deadlock when worker timeout"
→ 查询:'并发 LLM 调用超时死锁问题在哪里处理'
→ 期望答案:lightrag/utils.py::priority_limit_async_func_call这类数据集的特点是:它考验的是"真实工程师会问的真实问题",比人工构造的问题更贴近实际使用场景。
来源三:人工构造高难度用例
前两种来源倾向于覆盖"代码库里有明确文档的部分"。但工程师最需要知识库帮助的,恰恰是那些没有文档、只能靠读代码理解的部分。这类用例需要人工构造:
高难度用例示例:
Q: "SDK 用户和 REST API 用户,文档处理走的是不同代码路径吗"
A: 是的——SDK 走 ainsert(F-only),REST API 走 apipeline_enqueue_documents
答案要找到两条路径的分叉点
Q: "Kafka 消息格式变更影响哪些消费者"(跨库场景)
A: 需要跨库分析,找 CROSS_ASYNC_CALLS 边这类用例占评测集的比例不需要多(20%左右足够),但它们是区分"够用"和"真的好用"的分水岭。
四维指标的参考基线
基于前十二篇的实验数据,整理一个参考基线:
| 指标 | 弱基线(仅向量检索) | 三路检索基线 | 四路检索基线 |
|---|---|---|---|
| 符号定位准确率(Top-1) | 0.65~0.75 | 0.85~0.92 | 与三路相近(历史路径主要帮助理解,不提升定位精度) |
| 语义搜索召回率(@5) | 0.90~0.96 | 0.95~0.99 | 0.95~0.99 |
| 影响面分析完整率 | N/A(单路径无法做影响分析) | 0.90~1.0(取决于 limit 配置) | 0.90~1.0 |
| 历史查询命中率 | 0(无历史路径) | 0(无历史路径) | 0.70~0.85 |
怎么解读这个表:
语义搜索召回率的弱基线已经相当高(0.90+),因为第03篇就证明了 AST 分块的向量检索本身已经很强。三路检索的提升主要体现在 Top-1 准确率和影响面分析上——这两个场景是单靠向量检索做不好的。
历史查询命中率是四路检索独有的指标,衡量的是知识库能否回答"为什么"类问题,前三路均为 0。
把评测接进 CI/CD
评测不是一次性的任务,而是持续的监控。代码库在演进,知识库也在更新,每次增量更新后都应该跑一遍评测,确认更新没有降低检索质量。
最简单的 CI 集成方案:
# 伪代码 — 非可运行
# 在 CI pipeline 中触发(每次 PR 合并后)
def run_kb_quality_check():
# 从标注集里取 50 道题(随机抽样,覆盖四类指标)
questions = sample(EVAL_DATASET, n=50)
results = {
"symbol_accuracy": [],
"semantic_recall": [],
"impact_completeness": [],
"history_hit": []
}
for q in questions:
result = query_knowledge_base(q.query, path=q.path)
results[q.metric_type].append(evaluate(result, q.expected))
scores = {k: mean(v) for k, v in results.items()}
# 告警阈值
THRESHOLDS = {
"symbol_accuracy": 0.80,
"semantic_recall": 0.90,
"impact_completeness": 0.85,
"history_hit": 0.65,
}
for metric, score in scores.items():
if score < THRESHOLDS[metric]:
alert(f"Knowledge base quality degraded: {metric} = {score:.2f}")
return scores这个方案只需要维护一个标注数据集(50~200 道题足够),CI 运行时间大约在 5~15 分钟,开销可以接受。
何时重建标注数据集: 当代码库有大规模重构时,旧的标注集可能失效——因为目标函数被重命名或移走了。好的做法是在标注集里记录函数的 git hash,如果目标函数在新版本里已经不存在,自动标记这道题为"失效",需要人工更新。
系列复盘:十三篇走过了什么
在终篇做一个完整的回顾。
这个系列从一个具体问题出发:工程师如何对大型代码库建立可检索的知识体系?
Part 1(理论与工具):第01-02篇
建立了两个基本认知:
- 代码库知识库的核心价值不是"生成代码",而是"理解代码"——回答"这在哪里"、"影响什么"、"为什么这样"四类问题
- 现有工具的能力边界:纯文本 embedding、AST 分析、知识图谱各有所长,单独用都有盲区
Part 2(核心技术):第03-08篇
用实验数据驱动结论:
- 第03篇:AST 函数级分块的 Recall@5 = 0.958,这是纯向量路径的天花板
- 第04篇:分块策略的四种选择(F/R/V/P),以及为什么分块粒度直接决定检索质量
- 第05篇:图路径和向量路径不是替代关系——Q8(跨文件结构问题)证明了图路径的不可替代性
- 第06-07篇:结构 embedding 和混合检索是边际改进,核心问题是"路径选错"而非"路径不够强"
- 第08篇:三路架构——向量/图/符号三条正交路径,按查询意图路由
Part 3(工程实践):第09-13篇
从实验室走向生产:
- 第09篇:
codebase-memory-mcp在真实项目(LightRAG, 20674节点)上的三路实战 - 第10篇:增量更新三层决策树——80% 的 commit 可以在第一层直接跳过
- 第11篇:跨库分析——0 条边也是有意义的答案,LightRAG × graphrag = 平行替代品
- 第12篇:Git 历史是第四条路径,465 条 FILE_CHANGES_WITH 边揭示隐式耦合
- 第13篇:评测框架——四维指标 + 数据集构建 + CI 持续监控
一条贯穿始终的主线:
这个系列从始至终在说同一件事——代码理解是多维度的,没有任何单一信号能覆盖所有问题。向量路径在术语对齐时强,图路径在结构遍历时不可替代,符号路径在精确定位时零歧义,历史路径在理解演进背景时独一无二。
好的代码库知识库不是选最强的一条路径,而是把四条路径的能力边界搞清楚,然后按问题类型路由。
总结
代码库知识库系列到此结束。
十三篇写下来,覆盖了从原理到工具、从实验到生产、从单库到跨库、从建库到维护的完整工程周期。
如果要用一句话概括整个系列的核心结论:
代码库知识库的质量,不取决于用了多强的 embedding 模型或多大的图,而取决于每条路径的能力边界是否清晰,以及路由逻辑是否正确。
四路检索、三层决策树、四维评测——这些不是复杂的工程,而是把"代码理解"这个问题的结构想清楚之后,自然导出的答案。
如果这个系列对你有帮助,欢迎关注我的个人主页 dongqi.dev,持续更新 LLM 工程实践内容。
在 PrimeSkills,我们帮助工程团队把代码库知识库从原型做到生产,包括四路检索架构设计、评测体系搭建和 CI/CD 集成。如果你的团队正在考虑为大型代码库建立知识体系,欢迎联系我们。