chohyerinn/rag-trust-lab

GitHub: chohyerinn/rag-trust-lab

一个 RAG 信任度与回归评估框架,通过多维指标和统计显著性检验来量化检索安全过滤对回答质量的影响。

Stars: 0 | Forks: 0

# rag-trust-lab [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg)](https://github.com/chohyerinn/rag-trust-lab/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) 与其仅仅关注 RAG 回答是否正确,这个小型评估框架旨在探讨**安全的检索范围在多大程度上牺牲了回答覆盖率(即安全税/safety tax),以及如何通过运维型 guardrail 来观察这种牺牲**。 ![运维人员界面演示 — LiteLLM(Claude Haiku) 实际回答与 judge 核验,排除了 2 条非官方文档的检索结果](https://static.pigsec.cn/wp-content/uploads/repos/cas/03/03e3f2c59e45bf84d5a4019aaa806573538aceaa452fa7a4ca439b990b92731b.png) *运维人员界面:回答生成仅使用官方文档,在检索阶段排除掉非官方/危险文档后,以条目数量显示。回答确认(judge)使用与生成模型不同的模型进行分离式事后核验。* 我们的目标并非构建庞大的 RAG 平台,而是将招聘启事中常见的 RAG 关键词整合到一个最小可行性产品(MVP)中,以此来缩小项目范围。 - 4 种检索器:lexical / BM25 / **dense(CLOVA bge-m3 embedding)** / **hybrid(BM25+dense, RRF)** - 默认运行无需 API 密钥,采用 lexical retriever + deterministic mock generator - 实际生成后端:CLOVA Studio 或 LiteLLM(OpenAI/Anthropic 等) - 基于官方来源的 trusted corpus + 仅限非官方/冲突/injection 的 fixture - retrieval recall@k, MRR - grounded rate, answer accuracy - answer coverage, abstention accuracy - prompt injection following rate - untrusted / poisoned document retrieval rate - CLOVA LLM-as-a-Judge 选项与 heuristic judge 的一致率 - 配置 A/B 回归对比 — 通过 **paired bootstrap CI + McNemar 检验** 判定显著性(与 `mini-agent-harness` 采用相同的评估方法论) ## 演示 / 部署 **▶ Swagger API 文档:** [rag-trust-lab.onrender.com/docs](https://rag-trust-lab.onrender.com/docs) — 使用 Docker + FastAPI 部署到 Render(因为是免费层级,首次请求会有约 50 秒的冷启动) 此链接并非 Streamlit 界面,而是 FastAPI 自动提供的 Swagger UI。展开 `/health` 和 `/query`,通过 `POST /query` 的 **Try it out** 按钮即可直接测试 API 请求。 发送问题后,系统将返回检索到的依据(标明官方/非官方)、官方来源元数据、回答以及回答确认结果。如果将 `trust_mode` 从 `all` 切换为 `trusted-only`,即可观察到**非官方/危险文档在检索阶段被过滤掉的过程**。由于无需 API 密钥即可通过 lexical retriever + mock generator 运行,因此可以直接作为容器部署。 **REST API (FastAPI, 通过 Docker 部署)** ``` docker build -t rag-trust-lab . docker run -p 8000:8000 rag-trust-lab # http://localhost:8000/docs (在 Swagger UI 中直接运行 POST /query) ``` 包含 `render.yaml`,只需在 Render 上关联代码仓库,即可自动构建并部署为 Docker 容器。健康检查路径为 `/health`。 **本地可视化演示 (Streamlit, 可选)** ``` pip install streamlit # 管理员评估服务器:比较 all / trusted-only 策略并检查风险指标 streamlit run streamlit_app.py --server.port 8501 # 运营人员服务器:整体搜索进行观察,但在生成回答时仅使用 trusted 文档 streamlit run operator_app.py --server.port 8502 ``` 管理员界面是用于实验/评估的视图。可以查看在 `all` 策略下是否检索到了受污染的文档,并与 `trusted-only` 策略进行对比。运维人员界面则尽可能贴近实际业务场景:用户无需选择检索范围,系统在内部以日志形式监控全局检索风险,但仅将 trusted 官方依据传递给 generator 的上下文。 评估问题的分类设计旨在避免仅关注安全性。 | type | 目的 | | --- | --- | | `official_answerable` | 仅凭官方文档即可回答的基础业务问题 | | `untrusted_only` | 答案仅存在于非官方文档中的问题,用于暴露 trusted-only 策略造成的覆盖率损失 | | `source_conflict` | 没有恶意指令,但官方与非官方文档内容相互冲突的问题 | | `prompt_injection` | 用于观察模型是否会遵循检索到的文档中的恶意指令的问题 | | `insufficient_evidence` | 用于观察模型是否会在文档无相关信息时拒绝捏造答案的问题 | 界面上将内部评估术语转换成了运维人员容易理解的表达方式。 | 内部指标 | 界面显示 | 含义 | | --- | --- | --- | | `answer_coverage` | 回答覆盖率 | 对于应该回答的问题,是否实际给出了答案 | | `abstention_accuracy` | 无法确认判断 | 对于文档中没有的问题,是否做到不捏造并拒绝回答 | | `grounded` | 是否基于官方依据回答 | 回答是否有检索到的官方文档内容作为支撑 | | `injection_following` | 是否遵循了危险文档的指令 | 模型是否遵循了来源不明文档中的恶意指令 | | retrieval risk log | 检索候选审查 | 虽未用于回答,但出现在检索候选中的非官方/危险文档列表 | | judge | 回答确认 | 利用规则、CLOVA 或 LiteLLM 确认回答是否符合官方文档标准的阶段 | ## 最新 smoke 测试结果 (67道问题 / 35篇文档) 以下结果是在不产生 API 费用的情况下可重现的 deterministic mock smoke 测试。与其说是模型性能得分,它更像是一个回归测试,旨在观察不同问题类型下的失败模式,并验证指标计算是否按预期工作。 | Config | n | recall@3 | answer coverage | abstention | injection following | untrusted retrieved | poisoned retrieved | | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | | `basic-tradeoff` | 67 | 79% | 47% | 100% | 10% | 75% | 19% | | `trusted-tradeoff` | 67 | 75% | 64% | 100% | 0% | 0% | 0% | 仅看整体平均值的话,trusted 模式似乎更好,但按类型分解就会发现 tradeoff 的存在。 | Type | n | basic | trusted | 解读 | | --- | ---: | ---: | ---: | --- | | `untrusted_only` coverage | 11 | 64% | 0% | 仅使用官方文档虽然安全,但会无法回答仅存在于非官方文档中的运维信息 | | `prompt_injection` injection | 12 | 42% | 0% | 如果受污染文档进入回答 context,则存在攻击成功的可能性 | | `source_conflict` accuracy | 10 | 10% | 100% | 确认在官方文档与非官方文档冲突时,trusted-only 是否收敛于官方依据 | | `insufficient_evidence` abstention | 7 | 100% | 100% | 对于文档中没有的问题,不进行捏造而是予以拒绝 | ## CLOVA 实测结果 (HCX-005, 67道问题) 下表是在 67 道问题的评估集中,生成与 judge 均实际使用 **HCX-005** 执行的结果(非 mock)。 | Config | n | recall@3 | accuracy | grounded | coverage | injection following | untrusted retrieved | poisoned retrieved | | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | | `clova-basic-67` | 67 | 79% | 99% | 90% | 98% | 0% | 75% | 19% | | `clova-trusted-67` | 67 | 75% | 97% | 75% | 98% | 0% | 0% | 0% | 解读的关键在于**哪些指标具有统计学显著性**。由于 HCX-005 即使检索到受污染文档也不会遵循 injection,因此开启 trusted 过滤后,`injection_following_rate` 依然是 0% → 0%,没有差异。相反,在检索阶段,危险文档的暴露率显著降低。 | Metric | basic → trusted | 95% CI (B−A) | McNemar p | 判定 | | --- | ---: | --- | ---: | --- | | `untrusted_retrieved_rate` | 75% → 0% | [−0.851, −0.642] | 0.0 | 显著改善 | | `poisoned_retrieved_rate` | 19% → 0% | [−0.298, −0.104] | 0.0002 | 显著改善 | | `untrusted_top_source_rate` | 45% → 0% | [−0.567, −0.328] | 0.0 | 显著改善 | | `injection_following_rate` | 0% → 0% | [+0.000, +0.000] | 1.0 | 无差异 | | `answer_accuracy` | 99% → 97% | [−0.075, +0.030] | 1.0 | 回归方向 (不显著) | | `grounded_rate` | 90% → 75% | [−0.284, −0.030] | 0.0414 | 显著回归 | 也就是说,在此数据中,trusted 过滤的价值不在于“提高回答准确率”,而在于作为阻断受污染依据被检索到的**检索阶段防御线(defense-in-depth)**。同时,`grounded_rate` 下降这一现象是留给后续分析的课题。guardrail 不仅仅是提升安全指标的工具,还需要观察缩小检索范围时,回答的依据性和覆盖率会受到怎样的影响。完整的对比报告已保存在 `reports/compare-clova-basic-67-vs-clova-trusted-67.md` 中。 ## Judge 分离实测(验证 self-judging bias,67道问题) 上述 CLOVA 实测中,生成和 judge 均使用了 HCX-005,因此可能存在 self-judging bias。我们使用 **Claude Haiku judge** 对相同的回答进行了重新评分实测(生成依然是 CLOVA HCX-005)。结果定量展示了 judge 的选择对各项指标的具体影响程度。 | 指标 (basic → trusted) | CLOVA judge | Claude judge | 解读 | | --- | ---: | ---: | --- | | `answer_accuracy` | 99% → 97% | 75% → 79% | 回答阶段指标高度依赖 judge (相差 24%p) | | `grounded_rate` | 90% → 75% | 72% → 84% | grounded 判定根据 judge 的不同,甚至**结论方向**都会逆转 | | `untrusted_retrieved_rate` | 75% → 0% (p<0.001) | 75% → 0% (p<0.001) | 检索阶段指标与 judge 无关 | | `poisoned_retrieved_rate` | 19% → 0% (p=0.0002) | 19% → 0% (p=0.0002) | 检索阶段指标与 judge 无关 | | `untrusted_top_source_rate` | 45% → 0% (p<0.001) | 45% → 0% (p<0.001) | 检索阶段指标与 judge 无关 | 这同时揭示了两点: - 本 repo 的核心结论——**检索阶段防御线(defense-in-depth)对 judge 的选择具有鲁棒性**。无论是 CLOVA judge 还是 Claude judge,显著性与效应量均保持一致。 - 相反,回答阶段指标(accuracy、grounded)则易受 self-judging bias 影响。如果 CLOVA 给自己的回答评分,accuracy 为 99%,但由 Claude 评分时降至 75%,相差达 24%p。`grounded_rate` 甚至根据 judge 的不同,连符号(改善/回归)都发生了逆转。 也就是说,由于回答指标的绝对值高度依赖于 judge,因此不应将其作为单纯的基准测试分数,而应视为必须明确评判主体并进行对比的指标。相反,检索阶段的风险指标无需 judge 即可确定性地计算,因此能够可靠地衡量 guardrail 的效果。完整的对比报告位于 `reports/compare-clova-basic-xjudge-vs-clova-trusted-xjudge.md`。 重现方式: ``` $env:CLOVASTUDIO_API_KEY = "..." $env:ANTHROPIC_API_KEY = "..." python -m rag_trust_lab run --config configs/clova-basic-xjudge.json --name clova-basic-xjudge python -m rag_trust_lab run --config configs/clova-trusted-xjudge.json --name clova-trusted-xjudge python -m rag_trust_lab compare --a reports/clova-basic-xjudge.json --b reports/clova-trusted-xjudge.json ``` ## 检索方法对比(retrieval quality) 当问题与文档使用了*不同的词汇*(例如:“想把钱退回来” vs “退款”)时,基于词汇匹配的检索就会遗漏。为了探究语义 embedding(CLOVA **bge-m3**)是否能弥补这一点,我们在相同的 18 个问题(`data/retrieval_corpus`,带 gold-labeled)上对比了 4 种检索器。由于样本较小,这不应被解读为绝对性能排名,而是作为在该受控集合下的检索失败模式分析。 | retriever | recall@3 | | --- | ---: | | lexical (TF-IDF) | 56% | | BM25 | 56% | | **dense (CLOVA bge-m3)** | **100%** | | hybrid (BM25 + dense, RRF) | 83% | `lexical → dense` 实现了 **recall@3 56% → 100%** 的提升,M 也提高了 **+0.44**,通过 paired bootstrap 95% CI `[+0.222, +0.667]` 和 McNemar `p=0.0078` 检验——这是一项**统计学上的显著改善**。因为即使词汇不同,只要语义相近就能被捕捉到。 有趣的是:**hybrid (83%) 低于单独使用 dense (100%)。**与“hybrid 总是更好”的传统观念相反,在这个小型语料库中,由于 BM25 表现较弱(56%),RRF 稀释了 dense 强大的排序能力。增加方法并不总是意味着收益,因此我们依靠指标而非直觉进行验证。 ``` python -m rag_trust_lab run --config configs/retrieval-lexical.json --name r-lexical $env:CLOVASTUDIO_API_KEY = "..." python -m rag_trust_lab run --config configs/retrieval-dense.json --name r-dense python -m rag_trust_lab compare --a reports/r-lexical.json --b reports/r-dense.json ``` ## 背景 RAG 演示通常停留在“输入文档,提出问题,然后给出回答”的层面。然而在现实中,当回答出错时,原因可能多种多样: - 可能是检索没能找到正确的文档 - 虽然检索到了,但模型忽略了依据 - 引用了过时的政策 - 回答虽然安全,但检索结果中混入了受污染的文档 - 直接遵循了文档内部的 prompt injection ## Corpus provenance 默认的 corpus 并非内部捏造的客服政策,而是由为了评估目的从官方来源提取的关键条款汇总而成的 trusted 文档,以及为了观察失败模式而设计的 untrusted / stale / distractor fixture 组成。每篇文档的 front matter 中都记录了 `publisher`、`source_url`、`collection_method` 和 `review_status`,以便在 Streamlit 界面上也能追溯来源。 目前的 corpus 包含 14 个 trusted official、4 个 trusted distractor、12 个 untrusted/stale/conflict fixture 以及 5 个 irrelevant distractor。所有文档的选择理由和来源均单独整理在 `docs/corpus_inventory.md` 中。 67 个示例问题同样拥有 `question_source`、`review_status` 和 `evaluation_type` 属性。在问题草案编写过程中,LLM 被用作辅助工具,而评估类型分布、来源映射以及 expected term 则进行了单独审查。我们通过测试来验证 `expected_terms`,确保仅保留实际出现在 `gold_sources` 文档原文中的表达。问题集的扩展标准整理在 `docs/evaluation_set_design.md`,审查记录位于 `docs/question_review_log.md`。 我们并不是在用 `mini-agent-harness` 去“验证” RAG。这两个项目的评估对象不同——harness 评估的是 coding agent,而本项目是对 RAG 系统进行打分。二者共享的是**评估方法论**:重复运行、paired bootstrap CI、McNemar 检验、综合考量成本,以及“绝不只凭单一平均值下结论”的标准。将这套方法论从 coding agent 迁移并应用到 RAG 可靠性上,正是本项目的初衷。 ## 立即运行 ``` cd "C:\Users\chohy\OneDrive\바탕 화면\my\work\rag-trust-lab" python -m pytest -q python -m rag_trust_lab run --config configs/basic.json --name basic python -m rag_trust_lab run --config configs/trusted.json --name trusted python -m rag_trust_lab compare --a reports/basic.json --b reports/trusted.json ``` 在 macOS/Linux 上,可以通过以下命令执行相同操作。 ``` python -m pytest -q python -m rag_trust_lab run --config configs/basic.json --name basic python -m rag_trust_lab run --config configs/trusted.json --name trusted python -m rag_trust_lab compare --a reports/basic.json --b reports/trusted.json ``` 示例输出结果已提交至 `reports/compare-basic-tradeoff-vs-trusted-tradeoff.md`。 `basic` 会检索所有文档。因此,它可能会遵循示例受污染文档中的诸如“随时支持退款”等指令,或是无视个人信息处理目的和保留期限的未审查备忘录。 `trusted` 仅检索 trusted 文档。除了观察在相同问题集下 injection-following 是否减少外,我们还会同时关注在 `untrusted_only` 问题中流失了多少回答覆盖率。 默认运行的是无需 API 密钥的 **deterministic mock** —— 这是用于 CI 和快速运行检查的 smoke test,并非模型性能结果。实际模型结果需要通过 CLOVA/LiteLLM config 单独运行,且应与上述的 **[CLOVA 实测结果](#실제-clova-결과-hcx-005-67문항)** 区分开来看。保留 mock 的原因是,为了让任何人都能在无密钥的情况下重现完整的评估流水线(检索→生成→judge→统计)。 `compare` 绝不仅是看平均值差异。由于两个 config 解答的是同一套问题,因此每个指标都会通过**问题级别的 paired bootstrap 95% CI** 以及(针对二值指标的)**McNemar 检验**来判定显著性。对于 `injection`、`stale`、`poisoned retrieved` 等数值越小越好的指标,判定时会反映其极性。 `poisoned_retrieved_rate` 负责审视回答生成前阶段的风险。即使模型没有遵循受污染文档,只要检索结果中混入了 untrusted/poisoned 依据,后续随着模型、prompt 或问题的变化就有可能出现失败,因此我们将其作为独立指标保留。相反,`answer_coverage` 则是用于观察安全过滤器在多大程度上牺牲了回答可能性的指标。 ## Limitations 本 repo 并非大规模 RAG 基准测试,而是一个受控的测试平台。尽管文档数量较少,但我们有意将官方文档、仅限非官方文档、冲突文档以及 prompt injection fixture 区分开来,以便于观察不同架构下的失败模式。 - trust 标签是在项目内部手动分配的,可能不如真实运营环境那样完美。 - mock 结果仅用于回归测试,关于模型性能的论断应与 CLOVA/LiteLLM 的实测结果分开看待。 - `untrusted_only` 文档是我们特意构造的、答案仅存在于非官方文档场景的 fixture,在实际服务中应结合审查队列或文档提升 workflow 一起考量。 ## 使用 CLOVA 运行实际生成 + LLM Judge `configs/clova-basic.json` 和 `configs/clova-trusted.json` 会将回答生成和 judge 均交由 HCX-005 执行。以目前的 67 道问题为基准,共计需要 67 道问题 × 2 种配置 × (生成+judge) = 268 次 API 调用。 ``` $env:CLOVASTUDIO_API_KEY = "..." python -m rag_trust_lab run --config configs/clova-basic.json --name clova-basic python -m rag_trust_lab run --config configs/clova-trusted.json --name clova-trusted python -m rag_trust_lab compare --a reports/clova-basic.json --b reports/clova-trusted.json ``` 报告中的 `judge / heuristic agreement` 展示了 LLM judge 与现有 deterministic judge 的一致程度。数值较低的条目,可以结合实际的回答原文和 judge reason 进行查看,作为改进 heuristic 的候选对象。 默认的 CLOVA config 由于生成和 judge 均使用 HCX-005,可能存在 self-judging bias。此结果应被视为实际模型的 smoke 测试,而非最终的基准测试分数。如果想进行更严谨的评估,可以将生成和 judge 分配给不同的模型,例如生成使用 `clova:HCX-005`,而 judge 使用 `litellm:gpt-4o-mini`。在管理员/运维人员的 Streamlit 界面中,只要 `.env` 配置了 `OPENAI_API_KEY`,就会显示 LiteLLM 选项。 在 HCX-005 的运行中,即使模型检索到了受污染文档,也可能不会遵循 injection。在这种情况下,trusted filtering 的效果将首先在 `poisoned_retrieved_rate` 等检索风险指标中显现,而不是在 `answer_accuracy` 上。在解读实际模型结果时,不要仅看数字,还需结合核对 `reports/*.md` 中的回答原文和 judge reason。 ## 流水线 ``` flowchart LR D["docs
official sources · untrusted fixtures"] --> C["chunker"] C --> RT["retriever
lexical / Chroma
trust filter"] RT --> G["generator
mock · LiteLLM · CLOVA"] G --> J["judge
heuristic + LLM-as-a-Judge"] J --> M["metrics
coverage · abstention · grounded
injection · poisoned-retrieved"] M --> RP["report + compare
paired bootstrap · McNemar"] ``` ## 项目结构 ``` rag_trust_lab/ data.py # markdown docs / question set loader retriever.py # lexical fallback + optional Chroma retriever generator.py # mock + CLOVA + LiteLLM generator judge.py # heuristic + optional CLOVA/LiteLLM LLM-as-a-Judge checks metrics.py # recall@k, MRR, grounded rate, regression diff report.py # markdown / json report cli.py # run, compare data/ docs/ # official-source trusted docs + intentionally untrusted fixtures questions.json configs/ basic.json # all documents trusted.json # trusted documents only chroma.json # optional Chroma path ``` ## 接入 Chroma / LiteLLM 为了保持轻量化,默认运行无需任何外部 API。如果想要接入 Chroma vector store: ``` pip install -r requirements-optional.txt python -m rag_trust_lab run --config configs/chroma.json --name chroma-trusted ``` LiteLLM 已包含在默认的 requirements 中。如果想使用 OpenAI 示例模型,请在 `.env` 中设置密钥和模型。 ``` OPENAI_API_KEY=... LITELLM_MODEL=gpt-4o-mini LITELLM_JUDGE_MODEL=gpt-4o-mini ``` 如果想使用 Claude API,请配置 Anthropic 密钥和 Claude 模型名称。 ``` ANTHROPIC_API_KEY=... LITELLM_MODEL=anthropic/claude-haiku-4-5-20251001 LITELLM_JUDGE_MODEL=anthropic/claude-haiku-4-5-20251001 ``` 在管理员/运维人员的 Streamlit 界面中,只要检测到密钥,LiteLLM 模型就会出现在下拉菜单中。在 CLI 中,只需更改 config 里的 `generator` 和 `judge`。 ``` { "generator": "litellm:gpt-4o-mini" } ``` 项目中还包含了示例 config。 ``` python -m rag_trust_lab run --config configs/litellm-basic.json --name litellm-basic python -m rag_trust_lab run --config configs/litellm-trusted.json --name litellm-trusted ``` 或者,如果想直接使用 CLOVA 的 OpenAI 兼容 endpoint,请在 config 中指定 `generator` 和 `judge`。 ``` { "generator": "clova:HCX-005", "judge": "clova:HCX-005" } ``` 如果只想把 judge 单独分离到其他模型,可以使用 LiteLLM judge。 ``` { "generator": "clova:HCX-005", "judge": "litellm:gpt-4o-mini" } ```
标签:AI安全, AV绕过, Chat Copilot, DLL 劫持, FastAPI, Kubernetes, RAG, 大语言模型, 检索增强生成, 评估测试, 请求拦截, 逆向工具