feat(backend): RAG Benchmark 检索评测(含 PR #9 评审修复) #11

Closed
yxx wants to merge 0 commits from feat/knowledge-retrieval-core into main
Collaborator

概述

为 Knowledge Core / Retrieval Core 增加一套可复现的 RAG 检索评测能力,量化检索质量并提供可复现基线,为后续检索调优(embedding / reranker / RRF 参数)提供依据。

本 PR 是对已关闭 #9 的重新提交,已落实 #9 审阅中的 3 项 P1 + 3 项 P2 问题。

评审修复(对应 #9 审阅意见)

  1. 检索配置真正进入执行链路(P1)rrf_k / rerank / rerank_candidates / score_threshold 已透传到 SearchRequest 并由 RetrievalEngine 实际执行,不再只写快照;rerank_candidates=None 表示精排全部候选(保留原有检索行为)。配置快照同时补充 embedding / reranker 版本与索引元信息(index_meta)。
  2. Recall@K 去重(P1):改为 len(set(retrieved[:k]) & expected) / len(expected),并补充重复 note_id 的单元测试。
  3. 后台异步运行(P1)POST /api/benchmarks/rag/runs 创建后立即返回 202 queued,由受管 asyncio.Task 后台执行;Case 间检查取消标志;SSE 实时推送进度;取消接口真正生效。
  4. 数据集元数据校验(P2):新增 _DatasetMeta 模型逐文件校验,损坏文件隔离跳过,列表接口不再整体 500。
  5. citation_required 语义(P2):仅 citation_required=true 的样本计入 Citation Hit Rate;citation_required=true 时强制要求存在 expected_block_ids
  6. modes 校验(P2)modesmin_length=1,并拒绝重复模式。

功能点

  • 受控数据集backend/data/benchmarks/*.json 目录注册(dataset_id / kind / version / cases),加载时校验,内容 sha256 哈希保证可复现。
  • 纯函数指标:Hit@1 / Hit@5、Recall@K、MRR、CitationHit、P50/P95 延迟分位数,无副作用、可独立单测。
  • RAG Runner:复用 app.retrieval.engine.search(),不旁路检索链路;逐 (mode, case, repeat) 采样,按 mode 聚合;单样本失败不中断整体。
  • 运行注册表:内存态 run 注册、进度推进、SSE 事件流(RunStarted / CaseCompleted / RunCompleted / RunFailed)、报告组装与取消接口。
  • 配置快照:记录 embedding / reranker 模型(含版本)、检索参数、索引元信息、环境信息,报告可解释、可复现。
  • REST 端点/api/benchmarks/* 共 8 个(数据集列表、RAG 运行、Agent 运行占位、运行列表/详情/取消/事件/报告)。

文件清单

  • 新增 backend/app/benchmarks/metrics.pydatasets.pyrag.pyservice.py
  • 修改:backend/app/contracts.py(Benchmark 契约 + SearchRequest 检索参数)、backend/app/routes.py(端点 + SSE + 取消)、backend/app/retrieval/engine.py(参数透传)、backend/app/retrieval/embedding.pybackend/app/retrieval/reranker.py(版本字段)
  • 新增种子数据集 backend/data/benchmarks/rag-core-v1.json(5 个中文检索 case)
  • 测试 backend/tests/test_benchmark.py(21 个测试,覆盖指标去重 / 数据集校验 / 运行生命周期 / 取消)

测试

cd backend
pytest -q                          # 113 passed
pytest tests/test_benchmark.py -v  # 21 passed

可复现验证(http://127.0.0.1:8000/docs)

  1. POST /api/benchmarks/rag/runs → 立即返回 status: queued
  2. GET /api/benchmarks/rag/runs/{run_id} 轮询 → runningcompletedmetrics 含 hit_at_1 / recall_at_k / mrr / citation_hit_rate / p50 / p95。
  3. POST /api/benchmarks/rag/runs/{run_id}/cancelstatus: accepted,运行转 cancelled
  4. GET /api/benchmarks/rag/runs/{run_id}/events/stream 观察 SSE 实时推送。
## 概述 为 Knowledge Core / Retrieval Core 增加一套可复现的 RAG 检索评测能力,量化检索质量并提供可复现基线,为后续检索调优(embedding / reranker / RRF 参数)提供依据。 > 本 PR 是对已关闭 #9 的重新提交,已落实 #9 审阅中的 3 项 P1 + 3 项 P2 问题。 ## 评审修复(对应 #9 审阅意见) 1. **检索配置真正进入执行链路(P1)**:`rrf_k` / `rerank` / `rerank_candidates` / `score_threshold` 已透传到 `SearchRequest` 并由 `RetrievalEngine` 实际执行,不再只写快照;`rerank_candidates=None` 表示精排全部候选(保留原有检索行为)。配置快照同时补充 embedding / reranker 版本与索引元信息(`index_meta`)。 2. **Recall@K 去重(P1)**:改为 `len(set(retrieved[:k]) & expected) / len(expected)`,并补充重复 `note_id` 的单元测试。 3. **后台异步运行(P1)**:`POST /api/benchmarks/rag/runs` 创建后立即返回 `202 queued`,由受管 `asyncio.Task` 后台执行;Case 间检查取消标志;SSE 实时推送进度;取消接口真正生效。 4. **数据集元数据校验(P2)**:新增 `_DatasetMeta` 模型逐文件校验,损坏文件隔离跳过,列表接口不再整体 500。 5. **`citation_required` 语义(P2)**:仅 `citation_required=true` 的样本计入 Citation Hit Rate;`citation_required=true` 时强制要求存在 `expected_block_ids`。 6. **`modes` 校验(P2)**:`modes` 加 `min_length=1`,并拒绝重复模式。 ## 功能点 - **受控数据集**:`backend/data/benchmarks/*.json` 目录注册(dataset_id / kind / version / cases),加载时校验,内容 sha256 哈希保证可复现。 - **纯函数指标**:Hit@1 / Hit@5、Recall@K、MRR、CitationHit、P50/P95 延迟分位数,无副作用、可独立单测。 - **RAG Runner**:复用 `app.retrieval.engine.search()`,不旁路检索链路;逐 (mode, case, repeat) 采样,按 mode 聚合;单样本失败不中断整体。 - **运行注册表**:内存态 run 注册、进度推进、SSE 事件流(RunStarted / CaseCompleted / RunCompleted / RunFailed)、报告组装与取消接口。 - **配置快照**:记录 embedding / reranker 模型(含版本)、检索参数、索引元信息、环境信息,报告可解释、可复现。 - **REST 端点**:`/api/benchmarks/*` 共 8 个(数据集列表、RAG 运行、Agent 运行占位、运行列表/详情/取消/事件/报告)。 ## 文件清单 - 新增 `backend/app/benchmarks/`:`metrics.py`、`datasets.py`、`rag.py`、`service.py` - 修改:`backend/app/contracts.py`(Benchmark 契约 + SearchRequest 检索参数)、`backend/app/routes.py`(端点 + SSE + 取消)、`backend/app/retrieval/engine.py`(参数透传)、`backend/app/retrieval/embedding.py`、`backend/app/retrieval/reranker.py`(版本字段) - 新增种子数据集 `backend/data/benchmarks/rag-core-v1.json`(5 个中文检索 case) - 测试 `backend/tests/test_benchmark.py`(21 个测试,覆盖指标去重 / 数据集校验 / 运行生命周期 / 取消) ## 测试 ```bash cd backend pytest -q # 113 passed pytest tests/test_benchmark.py -v # 21 passed ``` ## 可复现验证(http://127.0.0.1:8000/docs) 1. `POST /api/benchmarks/rag/runs` → 立即返回 `status: queued`。 2. `GET /api/benchmarks/rag/runs/{run_id}` 轮询 → `running` → `completed`,`metrics` 含 hit_at_1 / recall_at_k / mrr / citation_hit_rate / p50 / p95。 3. `POST /api/benchmarks/rag/runs/{run_id}/cancel` → `status: accepted`,运行转 `cancelled`。 4. `GET /api/benchmarks/rag/runs/{run_id}/events/stream` 观察 SSE 实时推送。
yxx added 4 commits 2026-09-02 23:27:23 +08:00
验收笔记此前被误纳入 benchmark 提交,现摘除跟踪,文件保留在本地磁盘。

Co-Authored-By: Claude Code <noreply@anthropic.com>
- 检索调优参数(rrf_k/rerank/rerank_candidates/score_threshold)透传到引擎实际执行
- Recall 去重,避免同一 Note 多 Block 重复导致 Recall 超 1
- RAG 运行改为后台异步执行:创建即 queued + 202,支持取消与 SSE 实时事件
- 数据集元数据校验,坏文件隔离跳过;citation_required 语义修正
- modes 空/重复校验;配置快照记录模型版本与索引元信息

Co-Authored-By: Claude Code <noreply@anthropic.com>
Owner

审阅结论

暂不建议合并。

该分支能够与最新 main 自动合并,合并态后端 157 项测试、前端 29 项测试、TypeScript 类型检查和生产构建均通过。但针对性验证发现以下问题尚未被现有测试覆盖。

必须修复

1. 活动 Benchmark Run 会被容量裁剪

_remember() 超过 100 条记录时直接删除最旧 Run,没有判断其是否处于终态,同时删除对应的 Task、Event、Subscriber 和 Cancel Flag。

复现时将上限设为 1,创建第二个 Run 后,第一个后台任务会因 _cancel_flags[run_id] 已被删除而抛出 KeyError

建议:

  • 只淘汰 completedfailedcancelled 等终态 Run;
  • 如果容量已满且全部是活动 Run,返回明确的容量错误;
  • 不得直接删除仍在运行的 Task 状态。

2. 取消 Run 没有产生 SSE 终止事件

取消分支只更新 Run 和 Report,随后直接清除订阅者,没有发送 RunCancelled

后果:

  • 已连接客户端会一直阻塞在 queue.get()
  • 重连后只能收到 RunStarted,随后连接正常结束;
  • 前端无法区分“运行已取消”和“网络异常断流”。

建议增加 RunCancelled 事件,在移除订阅前发布,并将其加入 SSE 终止事件判断。

3. 空索引或不兼容索引仍会生成 completed 报告

创建 Benchmark 时只把 index_meta 写入配置快照,没有验证:

  • 索引是否已经建立;
  • Embedding Model 是否一致;
  • Embedding Dimension 是否一致;
  • 索引是否包含有效数据。

实测空数据库仍会返回 completed,并生成全部为 0 的指标。这会把环境或索引错误误判成检索质量差。

建议在创建 Run 前验证索引兼容性,不满足时返回已有的:

BENCHMARK_INDEX_INCOMPATIBLE

4. 失败样本被排除,汇总指标会虚高

聚合指标时只保留 error is None 的 Case。一个满分样本和一个执行失败样本最终仍会得到:

Hit@1 = 1.0
Recall@K = 1.0
MRR = 1.0

这会掩盖 Benchmark 的实际失败率。

建议:

  • 失败样本按零分进入检索质量指标;或
  • 明确增加 total_casessuccessful_casesfailed_casesfailure_rate
  • 报告中标明各指标的实际分母。

5. 损坏的数据集文件会阻断其他合法 Dataset

load_dataset() 遍历目录时直接解析每个文件。只要排序靠前的无关文件存在以下问题:

  • JSON 语法错误;
  • UTF-8 解码错误;
  • 顶层结构是数组而非对象;

就会提前抛出 422 或未映射的 AttributeError,导致后面的合法 Dataset 无法加载。

建议隔离无关损坏文件,并对 JSON 顶层对象类型进行稳定校验。只有与请求 ID 对应的数据集损坏时才返回 BENCHMARK_DATASET_INVALID

6. 原始异常直接进入公开响应

后台异常通过 str(exc) 原样写入:

  • BenchmarkRun.error
  • RunFailed SSE Event
  • BenchmarkReport.error
  • 单个 Case Result

数据库、文件和 Adapter 异常可能包含绝对路径、SQL 信息或敏感参数。

建议统一转换为项目错误码和安全消息,详细异常只进入受控内部日志,不直接通过 HTTP 或 SSE 返回。

需要修复

7. FTS 阈值过滤与分页 total 不一致

FTS 在数据库已经分页和计数后,才对当前页执行归一化和 score_threshold 过滤,但响应仍返回过滤前的 page.total

实测可以出现:

items = []
page.total = 1

而且每页独立归一化会导致同一个阈值在不同页面具有不同含义。

建议在计数和分页前应用阈值,或重新定义阈值计算方式并返回过滤后的 total。

8. Benchmark SSE 不支持 Last-Event-ID

当前只接受:

?after_sequence=

没有像 Agent SSE 一样读取:

Last-Event-ID

这与第二阶段统一 SSE Contract 不一致,浏览器或通用客户端自动重连时无法恢复游标。

建议复用 Agent SSE 的游标解析逻辑,并明确查询参数优先级和非法游标错误码。

9. Agent Benchmark 接口仍固定返回 501

POST /api/benchmarks/agent/runs 已进入 OpenAPI,但始终调用 not_implemented()

如果本 PR 只交付 RAG Benchmark,建议移除占位接口并在 PR 标题和说明中明确范围;如果 PR 声明 Benchmark 功能完成,则仍需补齐 Agent Benchmark Request、Runner 和测试。

文档问题

本 PR 没有同步:

  • 第二阶段接口契约中的 Benchmark 实现状态;
  • 技术栈说明中的当前开发进度;
  • Benchmark 开发说明;
  • README 和测试数量基线;
  • 新增错误码、SSE 事件及运行边界。

按照当前项目合并规范,代码、OpenAPI、接口文档和开发说明应在同一个 PR 中保持一致。

验证结果

与最新 main 自动合并:通过
后端合并态测试:157 passed
前端测试:29 passed
TypeScript 类型检查:通过
前端生产构建:通过
git diff --check:通过

生产构建只有项目既有的大 Chunk 警告。

请先修复上述问题并补充对应回归测试、契约和开发文档,再进行下一轮审阅。

## 审阅结论 暂不建议合并。 该分支能够与最新 `main` 自动合并,合并态后端 157 项测试、前端 29 项测试、TypeScript 类型检查和生产构建均通过。但针对性验证发现以下问题尚未被现有测试覆盖。 ## 必须修复 ### 1. 活动 Benchmark Run 会被容量裁剪 `_remember()` 超过 100 条记录时直接删除最旧 Run,没有判断其是否处于终态,同时删除对应的 Task、Event、Subscriber 和 Cancel Flag。 复现时将上限设为 1,创建第二个 Run 后,第一个后台任务会因 `_cancel_flags[run_id]` 已被删除而抛出 `KeyError`。 建议: - 只淘汰 `completed`、`failed`、`cancelled` 等终态 Run; - 如果容量已满且全部是活动 Run,返回明确的容量错误; - 不得直接删除仍在运行的 Task 状态。 ### 2. 取消 Run 没有产生 SSE 终止事件 取消分支只更新 Run 和 Report,随后直接清除订阅者,没有发送 `RunCancelled`。 后果: - 已连接客户端会一直阻塞在 `queue.get()`; - 重连后只能收到 `RunStarted`,随后连接正常结束; - 前端无法区分“运行已取消”和“网络异常断流”。 建议增加 `RunCancelled` 事件,在移除订阅前发布,并将其加入 SSE 终止事件判断。 ### 3. 空索引或不兼容索引仍会生成 completed 报告 创建 Benchmark 时只把 `index_meta` 写入配置快照,没有验证: - 索引是否已经建立; - Embedding Model 是否一致; - Embedding Dimension 是否一致; - 索引是否包含有效数据。 实测空数据库仍会返回 `completed`,并生成全部为 0 的指标。这会把环境或索引错误误判成检索质量差。 建议在创建 Run 前验证索引兼容性,不满足时返回已有的: ```text BENCHMARK_INDEX_INCOMPATIBLE ``` ### 4. 失败样本被排除,汇总指标会虚高 聚合指标时只保留 `error is None` 的 Case。一个满分样本和一个执行失败样本最终仍会得到: ```text Hit@1 = 1.0 Recall@K = 1.0 MRR = 1.0 ``` 这会掩盖 Benchmark 的实际失败率。 建议: - 失败样本按零分进入检索质量指标;或 - 明确增加 `total_cases`、`successful_cases`、`failed_cases` 和 `failure_rate`; - 报告中标明各指标的实际分母。 ### 5. 损坏的数据集文件会阻断其他合法 Dataset `load_dataset()` 遍历目录时直接解析每个文件。只要排序靠前的无关文件存在以下问题: - JSON 语法错误; - UTF-8 解码错误; - 顶层结构是数组而非对象; 就会提前抛出 422 或未映射的 `AttributeError`,导致后面的合法 Dataset 无法加载。 建议隔离无关损坏文件,并对 JSON 顶层对象类型进行稳定校验。只有与请求 ID 对应的数据集损坏时才返回 `BENCHMARK_DATASET_INVALID`。 ### 6. 原始异常直接进入公开响应 后台异常通过 `str(exc)` 原样写入: - `BenchmarkRun.error` - `RunFailed` SSE Event - `BenchmarkReport.error` - 单个 Case Result 数据库、文件和 Adapter 异常可能包含绝对路径、SQL 信息或敏感参数。 建议统一转换为项目错误码和安全消息,详细异常只进入受控内部日志,不直接通过 HTTP 或 SSE 返回。 ## 需要修复 ### 7. FTS 阈值过滤与分页 total 不一致 FTS 在数据库已经分页和计数后,才对当前页执行归一化和 `score_threshold` 过滤,但响应仍返回过滤前的 `page.total`。 实测可以出现: ```text items = [] page.total = 1 ``` 而且每页独立归一化会导致同一个阈值在不同页面具有不同含义。 建议在计数和分页前应用阈值,或重新定义阈值计算方式并返回过滤后的 total。 ### 8. Benchmark SSE 不支持 Last-Event-ID 当前只接受: ```text ?after_sequence= ``` 没有像 Agent SSE 一样读取: ```http Last-Event-ID ``` 这与第二阶段统一 SSE Contract 不一致,浏览器或通用客户端自动重连时无法恢复游标。 建议复用 Agent SSE 的游标解析逻辑,并明确查询参数优先级和非法游标错误码。 ### 9. Agent Benchmark 接口仍固定返回 501 `POST /api/benchmarks/agent/runs` 已进入 OpenAPI,但始终调用 `not_implemented()`。 如果本 PR 只交付 RAG Benchmark,建议移除占位接口并在 PR 标题和说明中明确范围;如果 PR 声明 Benchmark 功能完成,则仍需补齐 Agent Benchmark Request、Runner 和测试。 ## 文档问题 本 PR 没有同步: - 第二阶段接口契约中的 Benchmark 实现状态; - 技术栈说明中的当前开发进度; - Benchmark 开发说明; - README 和测试数量基线; - 新增错误码、SSE 事件及运行边界。 按照当前项目合并规范,代码、OpenAPI、接口文档和开发说明应在同一个 PR 中保持一致。 ## 验证结果 ```text 与最新 main 自动合并:通过 后端合并态测试:157 passed 前端测试:29 passed TypeScript 类型检查:通过 前端生产构建:通过 git diff --check:通过 ``` 生产构建只有项目既有的大 Chunk 警告。 请先修复上述问题并补充对应回归测试、契约和开发文档,再进行下一轮审阅。
Kronecker closed this pull request 2026-09-03 01:26:54 +08:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Kronecker/NotesAgentic#11