rmonteiro-pereira/rag-eval

GitHub: rmonteiro-pereira/rag-eval

基于巴西央行文档构建的 RAG 评估测试框架,通过受控消融实验和对抗性测试套件系统性地量化检索增强生成管线中各组件的真实效果与安全护栏表现。

Stars: 0 | Forks: 0

# rag-eval **一个基于巴西央行文档的 RAG 系统,其核心围绕评估测试工具而非演示构建。** 检索增强生成(RAG)易于构建却难以信任。本仓库是 后半部分:一个针对 **Copom**(巴西中央银行货币政策委员会)公开会议纪要 的受控 RAG pipeline —— 包含 30 份葡萄牙语 文档,涵盖了从 2022 年 10 月到 2026 年 6 月的 Selic 利率决策 —— 与此同时, 还配备了相应的测试工具来验证它是否真正有效。一个带有版本控制的 gold set。一项 将每个组件与其自身缺失状态进行对比的消融实验。带有 非受控对照组的对抗性测试套件。一个被证明会失效的回归门禁。一切都是本地且免费的: 本地 embedding,本地 vector store,本地 LLM,自托管的 tracing,没有任何付费 API 且本仓库中 任何地方都没有密钥。 有趣的结果恰恰是那些与通常说法相悖的结论 —— 见下文。 ## 核心结论 最初的简单 dense-retrieval 基线存在一个具体的、可诊断的缺陷:**它在错误的 Copom 会议中 找到了正确的段落。** 每份纪要以几乎相同的方式表述其决策, 因此当被问及 2026 年 6 月时,它会返回 2025 年 3 月的副本。在 41 个 直接指明会议名称的 gold 问题中,它在排名第 1 位猜对正确会议的次数**仅为 41 次中的 4 次**。 | | MRR | hit@5 | nDCG@10 | recall@5 | rank-1 correct meeting | |---|--:|--:|--:|--:|--:| | `dense` — M1 基线 | 0.191 | 0.367 | 0.161 | 0.149 | 0.098 | | **`hybrid+rerank+metadata`** — M4 获胜方案 | **0.741** | **0.959** | **0.623** | **0.467** | **1.000** | | Δ | **+0.550** | **+0.592** | **+0.462** | **+0.318** | **+0.902** | 七个分支,以**仅在一个组件上存在差异的受控配对**形式展示,而非 作为累积式的阶梯 —— 因为中间的两个层级比它们下面的层级*表现更差*。排行榜会隐藏的三个发现: 1. **最廉价的组件以数量级的优势胜出。** 使用正则表达式从 问题中提取会议日期并将其作为 Qdrant payload filter 应用,仅需约 120 行代码。 它在纯基线上使 MRR 提高了 **+0.498**,且具有*负*的延迟成本,并实现了 **41/41 的提示精确率** —— 零误报,这正是允许将其作为 硬过滤而非软提升的底气。 2. **昂贵且时髦的组件并不划算。** cross-encoder reranker 带来了 **+2.2 秒的 p95 延迟**,并且在没有 metadata filter 的情况下是* actively harmful* 的(MRR −0.039,rank-1 meeting −0.146)。它根据语义契合度重新排序,而 语义契合度恰恰是无法区分两次 Copom 会议的信号。加上 过滤条件后,它仅带来了 +0.005 的 MRR 提升。在延迟预算限制下,它会被裁掉。 3. **将强分支与弱分支融合会拖累强分支。** 单独使用 BM25 在 rank-1 meeting 上的得分为 0.927;与 dense (0.098) 融合后,该配对的得分为 0.341。RRF 权重 对所有分支一视同仁,当其中一个在决定性轴上接近随机时,这是一个错误的前提。 完整的数值、各指标的差值、延迟和探测定义:**[`docs/ablation.md`](docs/ablation.md)**。 ### 护栏,每一项均与非受控对照组进行对比测量 | 指标 | 受控 | 非受控 | |---|--:|--:| | **注入攻击成功率** (24次攻击) | **8.3%** | 16.7% | | — 直接层面 | 11.1% | 16.7% | | — **间接层面** (被污染的检索段落) | **0.0%** | 16.7% | | **PII 输出泄露** (语料库提供) | **0.0%** | **100.0%** | | 针对干净领域查询的 PII 误报 | 0.0% | — | | 拒绝回答正确率 / 错误拒绝率 | 100% / 4.1% | — | | **未授权用户检索到的受限分块** | **0** | — | **8.3%,而不是 0%。** 二十四次攻击中有两次击败了整个技术栈,这两次都在 [`docs/governance.md`](docs/governance.md) 中有说明。一个以 0% 开头的安全性章节 要么是在测试弱攻击,要么是不够坦诚。 ### 生成:三个后端,一次共享的检索过程 | 分支 | 数值召回率 | 扎实度 | 幻觉数字 | 正常拒绝 | 错误拒绝 | |---|--:|--:|--:|--:|--:| | `extractive` | **0.913** | **0.988** | **0.000** | **0.000** | 0.000 | | `qwen2.5:3b` | 0.777 | 0.838 | **0.000** | 1.000 | 0.082 | | `llama3.1` | 0.837 | 0.887 | **0.000** | 1.000 | 0.041 | **在所有 168 个回答中幻觉数字为零** —— 而扎实度得分最高的 分支在正常拒绝上的得分为 **0.000**:`extractive` 无法说“我不知道”,因此 对于所有七个超出范围的问题,它返回了 Copom 段落,就好像它们回答了 这些问题一样。一个仅根据扎实度对后端进行排名的仪表盘恰恰选错了对象。 而对于一个关于评估的项目来说,最重要的发现是: ## 快速开始 前置条件:Docker、[uv](https://docs.astral.sh/uv/)、约 4 GB 用于模型权重的磁盘空间。 可选 [Ollama](https://ollama.com) —— 没有它,pipeline 将以 extractive 模式 运行,以下所有内容仍然有效。 ``` # 1. infrastructure — Qdrant、Langfuse、Postgres docker compose up -d docker compose ps # all three must report (healthy) # 2. python 环境(--extra dev 会添加 ruff + pytest;Presidio 所需的 spaCy PT 模型 # 是常规依赖项,两种形式都会包含) uv sync --extra dev # 3. corpus:从 bcb.gov.br 下载 30 份 atas,分块、embed(bge-m3、CPU)、upsert uv run python -m ingest.pipeline --download 30 # ~5 min + one-time weight download # 4. ask uv run python -m rag.ask "qual foi a decisao do Copom sobre a Selic em junho de 2026?" # 5. measure uv run python -m eval.ablation # the 7-arm ablation above uv run python -m eval.run_eval --suite adversarial # injection / PII / abstention / ACL uv run pytest -q && uv run ruff check . ``` 然后运行 `uv run uvicorn serving.api:app --port 8000` 并打开 。 关于此运行的逐步记录(从干净状态执行并带有真实输出)位于 **[`docs/REPRODUCE.md`](docs/REPRODUCE.md)** 中。 ## 局限性 按其对您对上述数字信心的可能影响程度排序。附带推理的完整 列表位于 [`docs/writeup.md`](docs/writeup.md#10-honest-limits) 中。 1. **gold set 是 56 行未经人类验证的草稿数据。** 每一个文本片段都经过 程序化验证,确认其存在于它所引用的文档和页面的摄入文本中 —— 因此这些行是 *grounded* 的。它们并未经过 *validated*:一个片段可能真实存在, 而基于它构建的问题仍然可能存在歧义、范围界定错误,或者可以通过其他 三份纪要来回答。在经过人工检查之前,这里的每一个数字都是一个穿着白大褂的 测试工具的冒烟测试。 2. **LLM 评判器在忠实度上接近随机猜测**(对比另一位评判者的 κ = 0.109),并且 在第一次运行时对自身的输出进行了评分。它的忠实度列不应 用于任何事情。`eval/datasets/judge_calibration_sheet.jsonl` 包含 30 行 数据,其中人类评分的列被故意留空。 3. **n = 49 个可回答的问题。** 分支之间几个百分点的差异在 误差范围内;0.55 的 MRR 差距并非如此,但不声称有任何置信区间。 4. **单一语料库、单一语言、单一文档体裁。** 这里没有任何内容表明会议日期 过滤能够泛化到非按时间归档的文档上。 5. **ACL 分类是合成的。** 这些是公开的 BACEN 文件;最近 五次会议代表了发布禁期的虚拟设定。*enforcement* 是真实的,并且 已针对运行中的 Qdrant 进行了测试;*policy* 则是测试固件。 6. **注入防御是基于模式的**,并且测得 ASR 为 8.3%。残留的成功率是该方法的 精确率/召回率特性,而不是一个更长的正则表达式就能修复的 bug。 7. **Agent 模式负责路由但不负责组合。** 它通过在实际数据集市上执行 SQL 回答了 10 个演示问题中的 6 个;它无法将 SQL 结果链接到后续的 检索中,并且它没有自己的评估测试工具 —— 演示记录只是演示,而非测量结果。 8. **分块、提示词和 RRF 权重特意采用了最基础的设计**,并且在所有 七个分支中保持固定,因此它们仍可作为未来的消融分支使用,而不是 作为未测量的变量。 9. **延迟是在单机 CPU 上测出的**,没有成本或 token 经济学的测试分支。 `uv.lock` 固定了 `torch 2.13.0+cpu`,因此 reranker 的 +2.2 秒接近最坏 情况;设备只需修改一行设置(`RERANKER_DEVICE`),但发布 CUDA wheels 将会牺牲 `docs/REPRODUCE.md` 所证明的仅 CPU 的可复现性。 准确性发现与设备无关 —— 只有延迟列会发生变化。 (Ollama,以及随之而来的所有生成和评判数字,在存在 GPU 的 情况下已经使用了 GPU。) 10. **CI 目前刚好只运行过一次。** `.github/workflows/eval.yml` 在提交时 尚未运行(因为仓库当时还没有配置远端),并在发布时未加修改地通过了 (运行编号 `30599034168`,1分03秒:ruff 检查通过,278 个通过 / 3 个 被取消选择,并且 gate-selfcheck 任务在*双向*测试中均显示为绿色)。一次绿色 运行证明该工作流是有效的,但并不代表它能承受重负 —— 目前尚无任何尝试 将一个回归测试合并并通过它。 ## 优先阅读内容 | 如果你有 | 请阅读 | |---|---| | 2 分钟时间 | 上方的本页面内容 | | 15 分钟时间 | [`docs/writeup.md`](docs/writeup.md) — 架构、每一个数字及其重新生成的命令、失败模式、限制 | | 对检索结果感兴趣 | [`docs/ablation.md`](docs/ablation.md) — 7 个分支、受控对比、探测 | | 对安全性感兴趣 | [`docs/governance.md`](docs/governance.md) — 攻击、ASR、两次成功的攻击 | | 对评估方法感兴趣 | [`eval/probes.py`](eval/probes.py), [`eval/calibration.py`](eval/calibration.py), [`eval/regression_gate.py`](eval/regression_gate.py) | | **关注“为什么”,而不是“是什么”** | **[`docs/adr/`](docs/adr/)** — 九项决策,每项都包含被否决的替代方案以及推翻它的条件 | | 对发布的内容存疑 | [`docs/REPRODUCE.md`](docs/REPRODUCE.md), [`docs/PUBLICATION-SCAN.md`](docs/PUBLICATION-SCAN.md) | | 想要贡献或探究威胁模型 | [`CONTRIBUTING.md`](CONTRIBUTING.md), [`SECURITY.md`](SECURITY.md) | ## 架构 ``` flowchart LR subgraph ingest["ingest/ — offline"] A["bcb.gov.br
Copom atas (PDF)"] --> B["loading.py
pypdf, page-aware"] B --> C["chunking.py
fixed-size 1200 / 200 overlap"] C --> D["embedding.py
bge-m3, local CPU"] end D --> Q[("Qdrant
cosine, 1024-d")] subgraph ask["rag/ + retrieval/ — online"] E["question"] --> GI["guardrails/
PII mask + injection scan"] GI --> M1["metadata.py
which meeting?"] M1 --> F["dense (bge-m3)
+ payload filter + ACL"] M1 --> S["sparse.py
BM25"] F --> Q Q --> RRF["fusion.py
RRF k=60"] S --> RRF RRF --> RR["rerank.py
bge-reranker cross-encoder"] RR --> G["generation/prompt.py
stuff top-k"] G --> H{"generation/llm.py"} H -->|ollama| I["qwen2.5:3b / llama3.1"] H -->|extractive| J["verbatim passages"] I --> K["answer + citations"] J --> K K --> GO["guardrails/
output PII mask + audit"] end ask -.->|traces| L[("Langfuse
self-hosted")] subgraph evalsg["eval/"] M["gold set
56 draft Q/A + spans"] --> N["run_eval.py + ablation.py
recall / hit_rate / nDCG / MRR / probes"] Q -.->|complete qrels| N GO -.-> N2["run_generation.py
numeric recall / groundedness /
hallucinated numbers / abstention"] N2 --> JD["judge.py
LLM-as-judge, uncalibrated"] JD --> CAL["judge_calibration_sheet.jsonl
30 items, human column EMPTY"] N --> GATE["regression_gate.py
gates aggregates AND probes"] end ``` | 路径 | 作用 | |---|---| | `ingest/` | 语料库下载、PDF 加载、分块、embedding | | `retrieval/` | Qdrant 访问、BM25、RRF 融合、会议元数据解析、cross-encoder 重排序以及指定的消融分支 | | `generation/` | 提示词、LLM 后端、引用回答、LLM 评判器 | | `guardrails/` | PII 检测 + 掩码(带有巴西专用识别器)、注入检测、受控查询路径 | | `governance/` | 作为 Qdrant payload filter 的文档 ACL、追加型审计日志 | | `agent/` | text-to-SQL 工具、SQL 验证、HITL 确认门禁、agent 循环 | | `rag/` | 配置、tracing、pipeline、CLI(`rag.ask`, `rag.agent`) | | `eval/` | 数据集、指标、测试工具(`run_eval`, `ablation`, `run_generation`, `run_adversarial`, `calibration`)、回归门禁、报告 | | `serving/` | FastAPI `/ask` + 基于受控 pipeline 的最小化 UI | | `docs/` | 记录规范,以及每个测量结果各对应一份文档 | ### 技术栈 | 层级 | 选择 | 原因 | |---|---|---| | Vector store | **Qdrant** (Docker) | 开源、自托管;它的 payload filter 正是会议过滤器和 ACL 最终编译成的形式 | | Embeddings | **bge-m3** 通过 `sentence-transformers` | 多语言、在葡萄牙语上表现强劲、可在 CPU 上运行 | | 稀疏检索 | **BM25,仓库内自研** | 四十行算术代码;当对比的对象清晰可见而非隐藏在依赖锁定背后时,消融实验更具说服力 | | Reranker | **bge-reranker-base** (本地 cross-encoder) | 多语言(基于 XLM-R)、大小仅为 `v2-m3` 的三分之一、可在 CPU 上运行 | | LLM | **Ollama** (`qwen2.5:3b`, `llama3.1`),带有 extractive 回退 | 免费、本地、无供应商锁定 | | 评判器 | **Ollama**,使用与生成器*不同*的模型 | 给自己的作业打分存在已知的偏差方向 | | PII | **Presidio** + spaCy `pt_core_news_sm` + 仓库内的巴西识别器 | 原生 Presidio 漏掉了 CPF;我们的识别器会验证校验位 | | Tracing | **Langfuse v2** (Docker) | 开源,自托管;v2 仅需 Postgres | | 配置 | `pydantic-settings` | 统一通过 `.env`,无硬编码主机 | 以上内容均为免费,且可在笔记本电脑上运行。 ## 完整运行 ### 1. 基础设施 ``` docker compose up -d docker compose ps # all three must report (healthy) ``` - Qdrant 仪表盘 → - Langfuse → (登录 `local@rag-eval.dev` / `ragevallocal123`) Langfuse 项目及其 API keys 在首次启动时通过 `LANGFUSE_INIT_*` 自动配置,因此无需点击 UI 即可进行 tracing。`docker-compose.yml` 中的每一个凭证都是仅限本地使用的常量,并在文件中对此进行了说明。 ### 2. Python 环境 ``` uv sync --extra dev # plain `uv sync` is enough to run the pipeline; the # extra adds ruff and pytest cp .env.example .env # optional; every default already points at the local stack ``` Presidio 的葡萄牙语 NLP 模型 (`pt_core_news_sm`) 是一个**声明的依赖项**, 通过 URL 锁定,因为 spaCy 将其作为 GitHub release wheel 发布,而不是在 PyPI 上发布。没有它,`guardrails/pii.py` 会静默降级为正则表达式后端, PII 相关的数字将不再是实际测量的结果 —— 因此它被显式锁定,而不是 留待用户在 README 中的某处通过 `python -m spacy download` 步骤去手动安装(这往往会被跳过)。 ### 3. 语料库 + 数据摄入 ``` uv run python -m ingest.pipeline --download 30 ``` 将 30 份 Copom 会议纪要下载到 `data/raw/`(已在 gitignore 中忽略),写入已提交的 `data/manifest.json`,进行分块,使用 CPU 上的 bge-m3 生成 embedding,并将其 upsert 到 Qdrant 中。首次 运行还会从 HuggingFace 拉取约 2 GB 的模型权重。重复运行是幂等的 —— 已下载的 PDF 会被跳过。 只有清单文件被提交。语料库可以从中复现,因此不会有二进制文件进入 git。 ### 4. 提问 ``` uv run python -m rag.ask "qual a decisao do Copom sobre a Selic?" uv run python -m rag.ask --top-k 8 --show-passages "quais as expectativas do Focus para 2026?" uv run python -m rag.ask --mode extractive --json "quem votou pela decisao da 279a reuniao?" ``` Flags:`--top-k`, `--mode {auto,ollama,extractive}`, `--show-passages`, `--json`, `--no-trace`。 ### 5. 测量 ``` # retrieval,一个命名的 arm(默认为 `dense` —— 已提交的 baseline) uv run python -m eval.run_eval --min-status draft --out eval/reports/baseline_dense.json uv run python -m eval.run_eval --config hybrid+rerank+metadata # 完整的 ablation:七个 arm、受控对比、wrong-meeting 探针 uv run python -m eval.ablation --out eval/reports/ablation.json # generation:三个 backend、确定性指标 + LLM judge + 校准表 uv run python -m eval.run_generation --out eval/reports/generation.json # guardrails:injection ASR、PII 泄漏、abstention、ACL —— 每个对比一个 ungoverned arm uv run python -m eval.run_eval --suite adversarial # judge 与人工的一致性,在表格被标注后 uv run python -m eval.calibration # regression gate:在提交的 report 上 exit 0,在退化的 fixture 上 exit 1 uv run python -m eval.regression_gate \ --baseline eval/reports/ablation.json --candidate eval/reports/ablation.json uv run python -m eval.regression_gate \ --baseline tests/fixtures/gate_baseline.json --candidate tests/fixtures/gate_degraded.json uv run pytest -q ``` `run_eval` flags:`--gold`, `--min-status {draft,validated}`, `--config`, `--k 1,3,5,10`, `--out`, `--label`, `--quiet`。 有两个默认值被故意设置为不同,如果弄反了,会悄无声息地破坏 本仓库中所有的前后对比数值: - **`eval.run_eval` 默认使用 `--config dense`。** 该命令生成了已提交的 M1/M2 基线,并且必须继续生成它。如果它悄无声息地升级到最佳分支, 每次对比中的“之前”那一半数据就会发生偏移。 - **`rag.ask` 和 serving 路径默认使用 `hybrid+rerank+metadata`** (`settings.retrieval_config`),这是经过测试的获胜方案。对外服务应使用经过 测试的最佳方案。 `--min-status` 是最重要的 flag。目前所有内容都是 `draft`,因此 `--min-status draft` 会运行测试工具,而 `--min-status validated` 不会返回任何内容 (这是设计使然,退出码为 3)。在经过人工验证之后,加上 `--min-status validated` 的相同命令将产生真正有效的数字 —— 验证是一个**重新运行的过程,而非重写**。 每次调用都会发出一个名为 `rag.ask` 的 Langfuse trace,包含一个 `retrieve` span(排序后的命中 结果)和一个 `generate` span(答案和 token 使用情况),并标有提示词版本、 embedding 模型、分块设置和 LLM 后端 —— 因此数字始终可以追溯到 生成它的配置。 ### 6. 服务 ``` uv run uvicorn serving.api:app --port 8000 ``` `/` 是一个最小化的 UI,`/ask` 是 JSON endpoint,`/health` 和 `/config` 是 状态检查 endpoint。一切都会经过**受控的** pipeline —— 不存在任何能够绕过 PII 掩码、注入检测、ACL 或审计日志而直接进行检索的代码路径。 ``` curl -s localhost:8000/ask -H 'content-type: application/json' \ -d '{"question":"Qual foi a decisao do Copom em junho de 2026?","user":"supervisor"}' ``` 在浏览器标签页中切换 `analyst` 和 `supervisor` 用户可以观察 ACL 的工作情况:分析师在受限制的会议上拒绝回答且无任何来源,而主管则获得 答案及其标记为 `restricted` 的来源。`user` 字段来自请求体 **仅限此演示使用**,endpoint 的文档字符串和响应都说明了这一点 —— 由 调用者决定主体的 ACL 根本算不上是 ACL。 ### 7. Agent 模式 ``` uv run python -m rag.agent --demo # regenerates docs/agent_demo.md uv run python -m rag.agent --gate interactive "..." # confirm each SQL by hand ``` 需要 `_artifacts/ofl_gold.duckdb`,这是来自 Open-Finance-LakeHouse 项目的只读数据集市导出。它位于此仓库之外,且从不提交至此。 包含两个工具(`sql_query`, `rag_search`)、分层的 SQL 验证,以及一个经过风险分类的 human-in-the-loop 门禁,该门禁在录制的演示中拒绝了 24 条陈述中的 6 条。 ## LLM 模式:发布的是哪一个 在一个接口(`generation/llm.py`)背后有两个后端: - **`ollama`** — 一个本地的 Ollama 服务器。免费,无密钥,无云端调用。 - **`extractive`** — 完全不需要语言模型。原样返回检索到的段落,每个都带有 其引用。 `--mode auto`(默认值)会探测一次 Ollama,如果无法连接则降级为 extractive 模式,因此 相同的命令可以在任何机器上运行。 M1 发布时仅包含 extractive 后端,因为在原本网络连接健康的情况下,两个模型的下载都在 ~95–97% 时停滞了。后来重试时,两者都完成了下载 —— `qwen2.5:3b` (1.9 GB) 和 `llama3.1:8b` (4.9 GB) 现在都在本地了。没有更改任何代码:`build_llm("auto")` 探测 `:11434/api/tags` 并自行切换后端,完全如 M1 所承诺的那样。 extractive 后端被保留了下来,而且不是作为权宜之计 —— 它是 **groundedness 的底线**。逐字 引用不会产生幻觉,因此它限定了 generation 可能承担的责任范围,并 将*检索*质量与*生成*质量隔离开来,这正是本项目旨在衡量的区别。 ## 状态 | 里程碑 | 状态 | 证据 | |---|---|---| | **M0 — 脚手架** | 完成 | `docker compose ps` 报告 Qdrant、Langfuse 和 Postgres `(healthy)` | | **M0 — 语料库** | 完成 | 30 份 Copom 会议纪要(2022年10月 → 2026年6月),194 页文本,位于 `data/manifest.json` 中 | | **M0 — 数据摄入** | 完成 | **636 个分块**,bge-m3 1024 维,在 CPU 上耗时约 135 秒进行 embedding(4.7 个分块/秒) | | **M1 — 基线 + tracing** | 完成 | `rag.ask` 带引用端到端回答了问题;trace 可在 Langfuse 中查看 | | **M2 — gold set** | 草稿 | **56 个 Q/A 对**,引用了 24 份文档,**等待人工验证** | | **M2 — 评估测试工具** | 完成 | `eval/run_eval.py`;`eval/reports/baseline_dense.json` — MRR 0.191 | | **M3 — 生成模式** | 完成 | `qwen2.5:3b` + `llama3.1` 本地运行;`eval/reports/generation.json` | | **M4 — 检索消融** | 完成 | `eval/reports/ablation.json`,[`docs/ablation.md`](docs/ablation.md) — 7 个分支,MRR 0.191 → 0.741 | | **M5 — 护栏与治理** | 完成 | `eval/reports/adversarial.json`,[`docs/governance.md`](docs/governance.md) | | **M6 — Agent 模式** | 完成 | [`docs/agent_demo.md`](docs/agent_demo.md) — 10 个问题,其中 6 个通过对真实数据集市执行 SQL 解决 | | **M6 — CI 回归门禁** | 完成 | `eval/regression_gate.py`;通过干净测试,**在降级的测试固件上失败**(两者均被断言) | | **M7 — 对外服务** | 完成 | `serving/api.py` — FastAPI `/ask` + 基于*受控* pipeline 的 UI | | **M8 — 总结报告** | 完成 | [`docs/writeup.md`](docs/writeup.md) | | **gold-set 验证** | **等待人工** | `eval/datasets/gold_seed.jsonl` — 56 行,全部为 `draft` | | **评判器校准** | **等待人工** | `eval/datasets/judge_calibration_sheet.jsonl` — 30 项,人类评分列留空 | ### M1 基线详情 纯 dense 检索,bge-m3,636 个分块,49 个可回答的 gold 行,宏平均: | 指标 | @1 | @3 | @5 | @10 | |---|---|---|---|---| | `recall` | 0.053 | 0.085 | 0.149 | 0.194 | | `hit_rate` | 0.082 | 0.204 | 0.367 | **0.531** | | `nDCG` | 0.071 | 0.098 | 0.138 | 0.161 | **MRR = 0.191。** 对于 47% 的 gold 问题,检索到的前十个分块中根本不包含任何 相关内容。 `recall@1` 看起来像是出错了,但其实没有:完整的 qrels 加上多分块的 gold 跨度将其上限限制为 `1/|relevant|`。请在低 k 值时阅读 `hit_rate@k`。 ### 全部七个分支 | 分支 | MRR | hit@5 | nDCG@10 | rank-1 correct meeting | reverse lookup | p95 毫秒 | |---|--:|--:|--:|--:|--:|--:| | `dense` (M1 基线) | 0.191 | 0.367 | 0.161 | 0.098 | 0.000 | 7 | | `bm25` | 0.382 | 0.592 | 0.271 | 0.927 | 0.375 | 0.5 | | `hybrid` | 0.381 | 0.510 | 0.261 | 0.341 | 0.375 | 7 | | `hybrid+rerank` | .342 | 0.510 | 0.267 | 0.195 | 0.500 | 2553 | | `dense+metadata` | 0.689 | 0.878 | 0.603 | 1.000 | 0.000 | 7 | | `hybrid+metadata` | 0.736 | 0.898 | 0.619 | 1.000 | 0.375 | 7 | | **`hybrid+rerank+metadata`** | **0.741** | **0.959** | **0.623** | **1.000** | **0.500** | 2217 | 剩下的诚实差距在于 **reverse lookup** —— 即那些未指明会议而必须 通过内容确定会议的问题(“*Em qual reuniao a Selic foi reduzida para 12,75% a.a.?*”)。 元数据过滤在结构上无法提供帮助,并且 8 个问题中仍有 4 个将错误的纪要排在首位。 这 4 个问题中,正确的文档都在前 5 名内,因此这是一个排序失败而非 检索失败。 ### 依然特意保持最基础设计的部分 调整过的旋钮都经过了测量;这些部分是刻意未作改动的,因此它们仍可作为未来的 消融分支,而不是未测量的变量: - **固定大小的字符分块**(1200 个字符,200 重叠),按页拆分,以便每个 分块都能引用具体的页码。没有语义或结构拆分,并且分块大小在所有 七个分支中保持不变。 - **Stuff-the-context 提示词。** 将 Top-k 段落拼接起来,单次执行,temperature 设为 0。 没有查询重写,没有多步检索。 - **k=60 时的无权重 RRF**,这是原论文中的常数。在只有 49 行的 草稿 gold set 上对其进行调优属于拟合噪声,不能将其称为结果。 ## gold set,以及 agent 绝对不能做的一件事 `eval/datasets/gold_seed.jsonl` 包含 **56 个草稿 Q/A 对**,引用了 30 份文档中的 24 份:单跳查询、数字提取(Focus 预期、Copom 自身的 预测、参考情景假设)、列表提取(包括 5–4 票数对半开的投票情况)、同一文档内的多跳查询、8 个 reverse-lookup 探针和 7 个超出范围的 负样本。 每一个跨度都是从实际摄入的文本中提取的,并在写入前以程序化方式重新验证 :该跨度必须能在其文档的对应页面的分块中找到,`source_doc_id` 必须存在于 `data/manifest.json` 中,标题 必须与清单逐字匹配。因此这些行是 **grounded** 的。 它们并未经过 **validated**,而这种区别正是核心所在。一个跨度可能是真实的,但基于它 构建的问题可能仍然存在歧义、范围界定错误,或者可以通过其他三份会议纪要来回答。检查这些 是人工的工作,也是这个项目的科学核心资产所在。 一个由 agent 编写、由 agent 评分的 gold set 测不出任何东西,因此 `"status": "validated"` 是 一个任何 agent 都不能设置的 flag;如果出现了这种情况,`tests/test_gold.py` 将会报错失败。 相关协议位于 `eval/datasets/README.md`。 ### 第二道人工门禁:评判器校准 `eval/datasets/judge_calibration_sheet.jsonl` 包含 **30 个已评判的答案,其中 `human_faithfulness` 和 `human_answer_relevance` 被留空。** 原则相同,目标 不同:`generation/judge.py` 中的 LLM 评判器是一个本地的 3B/8B 模型,对由本地 3B/8B 模型生成的答案进行 评分,这种设定下产生的数字是 不可靠的。 因此报告显示 **`agreement: null`** —— 是*未知*,而不是*良好*。该表格经过分层,特意过度抽取了具有区分度的行 (首先是评判器/算术冲突,然后是 负样本,接着是低分),并且重新运行生成测试套件**会保留已输入的 标签** —— 我们对此有专门的测试,因为悄悄抹去一个下午的人工标注是 这个文件无法承受的 bug。 每一行都**逐字携带了检索到的段落** —— 仅凭文档 ID 列表是无法回答“每个声明是否都有证据支持”的,因此证据必须跟随行数据一起保存。 一旦填完,运行 `uv run python -m eval.calibration` 就会报告 Cohen's kappa 和混淆 矩阵。特意使用 Kappa 而非原始一致率:在一个大多数答案 其实都没问题的 3 分制量表中,一个学会了只说“2”的评判器将获得 100% 的 原始一致率,但 kappa 为 0。 ## 护栏与治理详情 四项控制措施,每一项都有一个运行相同攻击的非受控对照组 ([`docs/governance.md`](docs/governance.md)): - **间接接触面是护栏立足的根本** —— 0.0% 对比 16.7%。 由*检索到的文档*携带的注入是 RAG 特有的攻击,如果一个系统 只检查用户的问题,对此将毫无防备。 - **Presidio 完全漏掉了 CPF。** 这是实际测量出来的,而不是假设的 —— 因此 `guardrails/brazilian.py` 添加了 CPF/CNPJ/CEP/电话识别器,它们会验证**校验位**, 而不是形状。形状匹配会将 `123.456.789-00` 进行脱敏,但这并不是一个合法的 CPF, 而且这里的问题中密集包含各种数字。 - **ACL 是查询内部的 Qdrant payload filter,而不是后置过滤。** 后置过滤 意味着系统已经读取了受限文档;它还会通过结果列表的长度泄露 有多少文档匹配。这一点已通过 live-Qdrant 测试得到证明,该测试请求 200 个结果, 返回的受限文档数量为零 —— 即使当查询直接针对一个受限会议时依然为零。 - **审计日志刻意只存储掩码后的查询和原始查询的 SHA-256** — 绝不存储原始查询、答案或匹配到的 PII 子字符串。一个存储这些内容的日志 本质上是掩码工具本应控制内容的第二个副本,并且拥有更广泛的读取权限。 ACL 分类是**合成的**(这些是公开的 BACEN 文件;最近五次会议代表了发布禁期),每一份 报告都对此予以了说明。 ## 数据与许可 语料库:*Atas do Copom*,由巴西中央银行发布于 [bcb.gov.br](https://www.bcb.gov.br/publicacoes/atascopom) — 公开信息,免费 使用。`data/manifest.json` 记录了每个文档的 URL、标题、参考日期和 SHA-256,因此 无需重新分发即可复现该语料库。 代码:MIT — 见 [`LICENSE`](LICENSE)。第三方数据、模型权重及其 许可,以及仓库内每一项**合成**数据的清单及其标注位置:[`NOTICE`](NOTICE)。 本仓库是借助 AI 编程助手构建的,提交记录尾注中对此有明确说明。上述的 两道人工门禁特意保持开放:它们是 agent 绝不能代劳的部分。
标签:AI风险缓解, DLL 劫持, RAG系统, 大语言模型, 安全规则引擎, 检索增强生成, 评估测试框架, 请求拦截, 逆向工具, 金融文档分析