First_agent_template / rag_eval_interview_notes.md
mathidot's picture
build option trading agent modules
8f1601b

A newer version of the Gradio SDK is available: 6.15.2

Upgrade

RAG 评测模块构建总结

本文档用于面试时说明:为什么需要 RAG 评测、如何设计 retrieval eval、如何接入公开数据集和自建 PDF 测试集,以及如何判断 RAG 优化是否真的有效。

背景问题

在优化 RAG 系统时,仅靠主观查看回答效果不稳定,也很难判断 PDF 解析、chunk 切分、embedding、reranker 或检索参数的改动是否真的带来提升。

因此我先搭建了一个独立的 RAG retrieval evaluation 模块,用固定测试集和固定指标来做 before/after 对比。

目标是:

  • 能快速验证检索链路是否跑通。
  • 能用公开 benchmark 做横向参考。
  • 能用金融相关数据集贴近业务场景。
  • 能用自己的期权 PDF 测试集验证 PDF 解析、公式抽取和章节切分是否有效。
  • 每次改动后可以一条命令自动跑评测并生成报告。

数据集接入顺序

我按照由易到难、由通用到业务的顺序接入了 4 类测试集。

1. BEIR/scifact

scifact 是 BEIR 中比较小的科学事实检索数据集,适合快速跑通 retrieval eval。

接入它的目的不是追求业务贴合,而是验证:

  • 数据下载和解析是否正常。
  • corpus、query、qrels 能否正确对齐。
  • 向量索引是否能构建。
  • 检索指标是否能稳定输出。

2. BEIR/fiqa

fiqa 是金融问答相关数据集,比 scifact 更贴近金融场景。

接入它的目的:

  • 验证金融语义检索能力。
  • 检查 embedding 对金融术语、问答表达的适配情况。
  • 作为后续期权 PDF 场景前的公开金融 benchmark。

3. Open RAGBench

Open RAGBench 更接近长文档、PDF、报告类 RAG 场景。

我选择了其中的 pdf/arxiv 子集,用来验证:

  • 长文档解析后的检索效果。
  • 多章节、多段落文档下的 chunk 检索表现。
  • RAG 系统在 PDF-like 文档上的泛化能力。

4. 自建期权 PDF 测试集

最后补充自己的期权 PDF 测试集,因为公开 benchmark 无法完全覆盖当前项目中的业务难点。

自建测试集重点覆盖:

  • 期权定价概念。
  • PDF 中的公式内容。
  • 章节标题和上下文定位。
  • 公式编号、页码、章节等 metadata 是否能帮助检索。

模块设计

评测模块放在 eval/ 目录下,核心文件包括:

  • eval/rag_eval.py:单数据集 retrieval eval 入口。
  • eval/run_eval_suite.py:批量评测多个数据集的 suite runner。
  • eval/local_options_eval.jsonl:自建期权 PDF 测试集。
  • eval/README.md:调用示例和使用说明。

整体流程如下:

加载数据集
  -> 构造 documents / queries / qrels
  -> 构建 Chroma 向量索引
  -> 执行 top-k retrieval
  -> 按 doc_id 去重
  -> 计算 hit@k / MRR / NDCG@K
  -> 生成 JSON 和 Markdown 报告

为什么只先做 retrieval eval

RAG 的最终效果由两部分组成:

RAG = Retrieval + Generation

如果检索阶段没有找到正确上下文,后面的 LLM 生成很容易幻觉。因此我先评估 retrieval:

  • 问题对应的正确文档有没有被找回来。
  • 正确文档排在第几名。
  • top-k 结果排序是否合理。

这样可以先把问题定位在“检索是否正确”,再进一步评估生成答案。

指标设计

Hit@K

Hit@K 表示前 K 个结果里是否包含正确文档。

例如 Hit@5 = 1,表示正确文档出现在前 5 个检索结果中。

它适合判断:

  • 正确上下文有没有被召回。
  • top-k 设大以后召回是否提升。

MRR

MRR 是 Mean Reciprocal Rank,关注第一个正确结果出现的位置。

如果正确结果排第 1,得分是 1。 如果正确结果排第 2,得分是 1/2。 如果正确结果排第 5,得分是 1/5

它适合判断:

  • 正确文档是否排得足够靠前。
  • 检索排序质量是否提升。

NDCG@K

NDCG@K 衡量前 K 个结果的排序质量。

计算方式是:

DCG@K = rel_1 / log2(2) + rel_2 / log2(3) + ... + rel_K / log2(K + 1)
NDCG@K = DCG@K / IDCG@K

其中 rel_i = 1 表示第 i 个结果相关,rel_i = 0 表示不相关。

NDCG 越接近 1,说明相关结果越靠前。

关键实现细节

1. 统一数据格式

不同数据集格式不同,因此我统一抽象成:

documents = [
    {
        "doc_id": "...",
        "title": "...",
        "text": "...",
        "metadata": {...}
    }
]

queries = [
    {
        "query_id": "...",
        "question": "...",
        "relevant_doc_ids": [...]
    }
]

qrels = {
    "query_id": {"doc_id"}
}

这样后续索引构建和指标计算可以复用同一套逻辑。

2. 小样本评测必须包含 gold 文档

在做 smoke test 时,如果只取 corpus 前 N 篇文档,可能会出现 query 的正确文档不在测试 corpus 里,导致评测不公平。

所以我在加载 BEIR 和 Open RAGBench 时,会先读取 qrels,确定当前 query 需要哪些 gold documents,再优先把这些文档纳入 corpus。

这样小样本测试可以稳定评估检索能力,而不是被采样问题干扰。

3. 检索结果按 doc_id 去重

一个文档会被切成多个 chunk,检索时可能同一篇文档的多个 chunk 同时出现在 top-k 中。

如果不去重,会导致:

  • 指标被重复 chunk 影响。
  • NDCG 可能异常偏高。
  • top-k 实际上不是 top-k documents,而是 top-k chunks。

因此评测时内部会多取一些 chunk,然后按 doc_id 去重,再计算 top-k 文档级指标。

4. 支持 rebuild

如果修改了:

  • PDF 解析逻辑
  • chunk 切分方式
  • embedding 模型
  • metadata 构造
  • reranker 或检索参数

必须使用 --rebuild 重建索引,否则会复用旧索引,评测结果不能代表最新代码。

自动化评测脚本

单数据集评测:

uv --cache-dir .uv-cache run python -m eval.rag_eval \
  --dataset local-options \
  --max-queries 3 \
  --top-k 5 \
  --rebuild

批量评测:

uv --cache-dir .uv-cache run python -m eval.run_eval_suite --rebuild

只跑指定数据集:

uv --cache-dir .uv-cache run python -m eval.run_eval_suite \
  --datasets local-options,beir/fiqa \
  --top-k 5 \
  --max-queries 20 \
  --rebuild

对比不同 chunk 设置:

uv --cache-dir .uv-cache run python -m eval.run_eval_suite \
  --datasets local-options \
  --chunk-size 384 \
  --chunk-overlap 64 \
  --output-name local_options_chunk384 \
  --rebuild

报告会输出到:

eval/reports/

包括:

  • 每个数据集的 JSON 报告。
  • 每个数据集的 Markdown 报告。
  • suite 级别的汇总报告。

遇到的问题和解决方案

问题 1:公开数据集需要联网下载

BEIR 和 Open RAGBench 都需要从公网下载数据。

解决方法:

  • 第一次运行时下载并缓存到 eval/data/
  • 后续运行直接复用本地数据。
  • 数据和索引分开存放,便于排查问题。

问题 2:Open RAGBench 实际目录结构和预期不一致

最开始预设路径是 official/pdf/arxiv,但实际下载后路径是 pdf/arxiv

解决方法:

  • loader 中兼容两种路径。
  • 优先尝试 pdf/arxiv,不存在时再回退到 official/pdf/arxiv

问题 3:小样本采样会漏掉 gold document

如果 max_corpus_docs 很小,直接截取 corpus 前 N 条可能不包含 qrels 中的正确文档。

解决方法:

  • 先根据 qrels 选择 query。
  • 再把对应 gold documents 强制纳入 corpus。
  • 最后补充其他文档作为干扰项。

问题 4:chunk 重复导致指标异常

同一篇文档的多个 chunk 可能同时命中,导致 NDCG 等指标不合理。

解决方法:

  • 检索时多取一些 chunk。
  • 评估时按 doc_id 去重。
  • 最终以 document-level top-k 计算指标。

问题 5:不重建索引可能复用旧结果

如果代码改了但没有 --rebuild,Chroma 可能复用旧索引。

解决方法:

  • 文档中明确说明改动后必须加 --rebuild
  • suite runner 支持统一传入 --rebuild
  • --output-name 固定报告名,方便 before/after 对比。

问题 6:RAG 只是独立模块,没有真正接入 Agent

最开始 RAG 已经能单独查询知识库,但主 CodeAgent 的 tools 里没有注册知识库工具。这样在真实对话里,agent 实际只能查行情和时间,不能主动调用本地期权知识库。

解决方法:

  • QueryKnowledgeTool 注册进主 agent。
  • 优化 tool description,让模型知道它应该在期权概念、波动率、Greeks、策略、公式编号和书籍引用问题上调用该工具。
  • 控制 tool 输出长度,只返回来源、页码、section、分数和截断后的片段,避免检索结果占满上下文。

面试可以强调:

RAG 不是只要能单独跑 query 就算完成,必须作为 agent 的一个可调用工具接入主工作流。否则用户问期权概念时,agent 不一定会查知识库,仍然可能凭模型参数记忆回答。

问题 7:知识库目录和代码目录耦合

早期知识库放在 tools/knowledge_base 下,代码、原始资料和 Chroma 数据库混在一起。随着知识库变大,这种结构不利于维护,也不利于后续把工具代码、数据和缓存分开管理。

解决方法:

  • 将知识库统一到项目根目录:
OptionAgent/knowledge_base/
  raw/
  chroma_db/
  • 工具代码中使用 PROJECT_ROOT / "knowledge_base" 作为主路径。
  • 保留旧路径 fallback,避免迁移时旧数据立刻失效。

面试可以强调:

我把知识库从工具目录迁到项目根目录,并保留 legacy fallback。这样既完成了结构治理,也避免了迁移时破坏已有索引和原始文档。

问题 8:全量 rebuild 成本高

只要文档、解析方法或 embedding 模型变化,就全量重建索引。书籍变多后,这会浪费大量时间,而且不方便频繁更新笔记。

解决方法:

  • 每个 chunk metadata 中保留:
source_file
file_hash
embedding_model
extraction_method
  • 启动时扫描当前 raw 文件,和 Chroma 中已有 metadata 对比:
新增文件 -> 只入库新增文件
修改文件 -> 删除该文件旧 chunks,再重新入库
删除文件 -> 删除该文件对应 chunks
embedding/extraction 版本变化 -> 触发对应文件更新

面试可以强调:

我没有只依赖 collection 是否为空,而是基于 source_file、file_hash、embedding_model 和 extraction_method 做增量更新。这样文档更新后索引不会脏,也不用每次全量 rebuild。

问题 9:纯向量检索对公式编号和专有名词不稳定

期权书里有很多精确查询,例如:

Equation 21.23
WITH ZERO CORRELATION
Black-Scholes-Merton
vega
gamma

这类问题不只是语义相似,还需要字面命中。纯 dense embedding 对概念解释很强,但对公式编号、章节标题、专有名词有时不如关键词检索稳定。

解决方法:

  • 增加轻量 BM25 检索。
  • 查询时同时跑:
dense vector retrieval
BM25 keyword retrieval
  • 使用 reciprocal-rank merge 合并结果。
  • 再交给 cross-encoder reranker 做最终排序。

最终链路:

query
  -> dense top-k
  -> BM25 top-k
  -> merge / deduplicate
  -> reranker
  -> top results with citations

面试可以强调:

我做 hybrid search 是因为金融和期权文档里存在大量公式编号、章节名、ticker-like token 和专有名词。Dense retrieval 负责语义召回,BM25 负责精确词命中,reranker 负责最终排序。

问题 10:本地评测集太小

最初 local-options 只有 3 条 case,容易出现指标过高但不可泛化的问题。比如小样本里 Hit@5 为 1,并不代表系统在真实问题上稳定。

解决方法:

  • 新增 eval/generate_local_options_eval.py
  • 从已解析的 PDF/MD 文档中随机抽样 chunk。
  • 优先覆盖:
    • 公式问题。
    • 章节定位问题。
    • 期权关键词问题。
    • 波动率、Greeks、风险中性、策略等业务术语。
  • 过滤前言、索引页、表格/图注噪声,避免生成低质量 query。
  • 将本地 eval 扩充到 40 条。

面试可以强调:

我没有只手写少量 happy path case,而是做了一个本地 eval case generator,从真实 chunk 中抽样生成问题,并对噪声标题做过滤。这样可以更稳定地评估 PDF 解析和检索策略的变化。

Hybrid Search 和 Reranker 对比实验

扩充到 40 条 local-options case 后,我做了三组对比:

dense-only:
MRR      0.4708
NDCG@5   0.3468
Hit@1    0.4250
Hit@3    0.5250
Hit@5    0.5250

hybrid:
MRR      0.4833
NDCG@5   0.3190
Hit@1    0.4250
Hit@3    0.5250
Hit@5    0.5750

hybrid + reranker:
MRR      0.7125
NDCG@5   0.4717
Hit@1    0.7000
Hit@3    0.7250
Hit@5    0.7250

结果解释:

  • Hybrid search 单独提升了 Hit@5,说明 BM25 补充了召回,尤其对精确术语和公式编号有帮助。
  • Hybrid 的 NDCG 略降,说明召回增加后排序还不够好。
  • 加上 reranker 后,MRR、NDCG、Hit@1、Hit@5 都明显提升,说明 reranker 有效改善了排序质量。

面试可以这样总结:

单独加 BM25 后,召回有提升但排序不一定更好;这符合预期,因为 BM25 会把更多字面相关结果拉进候选集。最终效果最好的是 dense + BM25 扩召回,再用 cross-encoder reranker 排序。这个实验也说明我不是凭感觉加组件,而是用 Hit@K、MRR 和 NDCG 验证每一步是否真的有效。

当前评测结果示例

早期小规模 smoke test 的结果示例:

BEIR/scifact:
MRR = 0.9000
NDCG@5 = 0.9262
Hit@1 = 0.8000
Hit@5 = 1.0000

BEIR/fiqa:
MRR = 0.8000
NDCG@5 = 0.6582
Hit@1 = 0.8000
Hit@5 = 0.8000

local-options:
MRR = 1.0000
NDCG@5 = 0.7162
Hit@1 = 1.0000
Hit@5 = 1.0000

这些结果主要用于验证评测流程和小样本趋势,不能直接代表完整 benchmark 成绩。正式对比时需要扩大 max_queriesmax_corpus_docs

面试回答话术

可以这样回答:

我在优化 RAG 系统时发现,单纯看回答效果很难判断改动是否真的有效,所以先搭了一个 retrieval evaluation 模块。我的思路是先用 BEIR/scifact 快速跑通标准检索评测,再接 BEIR/fiqa 贴近金融场景,然后接 Open RAGBench 验证长文档和 PDF-like 场景,最后补自己的期权 PDF 测试集,用来覆盖项目里公式、章节和金融术语这些业务难点。

如果面试官问为什么先评估 retrieval:

因为 RAG 的生成质量高度依赖检索质量。如果检索阶段没有召回正确上下文,后面 LLM 很容易幻觉。所以我先用 Hit@K、MRR、NDCG@K 衡量正确文档是否被召回以及排序是否靠前,把 retrieval 问题和 generation 问题分开定位。

如果面试官问如何保证评测可靠:

我做了几个处理。第一,所有数据集统一成 documents、queries、qrels 三类结构。第二,小样本 smoke test 会优先把 qrels 需要的 gold document 放进 corpus,避免因为采样漏掉正确文档导致评测不公平。第三,检索结果按 doc_id 去重,避免同一篇文档多个 chunk 重复命中导致指标虚高。第四,修改解析、chunk、embedding 或检索逻辑后必须 rebuild 索引,保证评测对应的是最新系统。

如果面试官问这个模块怎么用:

我提供了单数据集入口和 suite 入口。单数据集可以用 python -m eval.rag_eval --dataset local-options --rebuild,批量评测可以用 python -m eval.run_eval_suite --rebuild。它会自动跑多个数据集,输出 JSON 和 Markdown 报告,便于做 before/after 对比。

如果面试官问为什么要做 hybrid search:

因为期权和金融文档里有两类查询。一类是语义型,比如“为什么临近到期 gamma 风险变大”,dense embedding 很适合;另一类是精确匹配型,比如 Equation 21.23WITH ZERO CORRELATIONBlack-Scholes-Merton,这些 BM25 更稳定。所以我用 dense retrieval 负责语义召回,BM25 负责关键词召回,然后合并候选,再用 cross-encoder reranker 排序。

如果面试官问 hybrid 是否真的提升了:

我用扩充后的 40 条 local-options eval 做了对比。Dense-only 的 Hit@5 是 0.525,MRR 是 0.471;加入 hybrid 后 Hit@5 提升到 0.575,说明召回变好,但 NDCG 有一点下降,说明排序还不够好;再加 reranker 后 Hit@5 到 0.725,MRR 到 0.713,Hit@1 到 0.700,说明 dense + BM25 + reranker 的组合最稳。

如果面试官问为什么不能每次全量 rebuild:

全量 rebuild 在文档少的时候可以,但参考书和笔记变多后成本会越来越高。我在 metadata 里记录 source_file、file_hash、embedding_model 和 extraction_method,启动时对比当前文件状态和 Chroma 中已有 metadata。新增文件只入库新增部分,修改文件只删除并重建该文件对应 chunks,删除文件同步清理旧 chunks。这样既保证索引新鲜,也避免无意义的全量重建。

如果面试官问 RAG 和 agent 怎么结合:

我把 RAG 封装成 QueryKnowledgeTool 注册到主 CodeAgent,而不是只做一个独立脚本。tool description 明确告诉模型在期权概念、波动率、Greeks、策略和公式编号问题上调用它。返回结果包含 source、page、section、content_type、score 和 excerpt,方便 agent 带引用地回答,而不是凭模型记忆回答。

如果面试官问如何避免本地 eval 过拟合:

早期我只有几条手写 case,很容易高估效果。后来我写了 local eval generator,从真实 PDF chunks 中抽样生成问题,同时过滤前言、索引、表格和图注噪声。这样测试集覆盖公式、章节、概念和金融术语,能更真实地暴露 retrieval 的召回和排序问题。

后续可扩展方向

后续还可以继续扩展:

  • 增加 reranker 前后的对比实验。
  • 增加 answer-level evaluation,评估最终回答是否正确。
  • 增加 citation accuracy,判断引用来源是否准确。
  • 增加公式检索专门测试集。
  • 增加表格类 query 测试集。
  • 对不同 chunk 策略、embedding 模型、top-k 参数做批量实验。
  • 将报告接入 CI 或定期任务,防止 RAG 效果回退。