dagent_eval/docs/rag-eval-framework-design.md

647 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# RAG 评测框架设计文档
> 版本v1.0
> 日期2026-04-13
> 背景:为 dagent agent 平台设计的独立 RAG 评测框架
---
## 一、背景与目标
### 为什么做成独立框架
dagent 平台已具备完整的 RAG 能力知识库切片、向量检索、ReAct Agent但缺乏系统性的评测手段。将评测能力做成**独立框架**而非嵌入现有 backend原因如下
- **平台无关**:通过标准化 Adapter 接口,可评测任何 RAG 系统,不只是 dagent
- **独立部署**:不影响生产服务,可单独扩缩容,评测任务不占用业务资源
- **技术栈自由**:可选最适合评测场景的工具和模型
- **可复用**:其他项目也能接入使用
### 目标
1. 提供**检索层**和**生成层**的完整评测指标体系
2. 支持通过 **Python SDK** 集成到 CI/CD 流程
3. 提供 **Web UI** 供非技术人员操作和查看报告
4. 对接 dagent 平台,同时保持对其他平台的扩展能力
---
## 二、评测指标体系
### 2.1 检索层评测Retrieval Evaluation
评测知识库切片的召回质量,**不依赖 LLM**,纯计算指标。
| 指标 | 全称 | 说明 | 计算方式 |
|------|------|------|----------|
| **Hit Rate@K** | 命中率 | Top-K 结果中是否包含至少一个相关切片 | 二值判断,对所有样本取均值 |
| **MRR@K** | Mean Reciprocal Rank | 第一个相关切片排名的倒数均值 | `MRR = mean(1 / rank_i)`rank_i 为第一个相关切片的位置 |
| **NDCG@K** | Normalized Discounted Cumulative Gain | 考虑排名权重的相关性得分,最全面的检索指标 | `NDCG = DCG / IDCG`DCG 对高排名相关结果给予更高权重 |
| **Context Precision** | 上下文精确率 | 召回的切片中有多少是真正相关的(信噪比) | LLM-as-judge 判断每个召回切片是否相关 |
| **Context Recall** | 上下文召回率 | 回答所需信息有多少被召回覆盖 | LLM 将参考答案分解为原子声明,检查每条声明是否被召回内容覆盖 |
**指标公式**
```
Hit Rate@K = (1/|Q|) * Σ 1[∃ relevant chunk in top-K results]
MRR@K = (1/|Q|) * Σ (1 / rank_i)
rank_i = position of first relevant chunk for query i
DCG@K = Σ_{i=1}^{K} rel_i / log2(i+1)
NDCG@K = DCG@K / IDCG@K
IDCG = DCG of ideal (perfect) ranking
Context Precision = |relevant ∩ retrieved| / |retrieved|
Context Recall = |ground truth claims covered by context| / |total ground truth claims|
```
### 2.2 生成层评测Generation Evaluation
评测 Agent 基于召回内容的回复质量,**依赖 LLM Judge**。
| 指标 | 说明 | 计算方式 | 是否需要参考答案 |
|------|------|----------|-----------------|
| **Faithfulness忠实度** | 回答中每个声明是否都有召回内容支撑,无幻觉 | LLM 分解答案为原子声明 → 逐条判断是否可从 context 推导 → 支持数/总数 | 否 |
| **Answer Relevance答案相关性** | 回答是否切题,有没有答非所问 | LLM 从答案反向生成问题 → 与原问题做 Embedding 相似度 | 否 |
| **Answer Correctness答案正确性** | 回答与标准答案的事实一致程度 | LLM judge 评分 + Embedding 相似度加权 | 是 |
| **Groundedness可溯源性** | 回答中每个声明是否可追溯到具体切片 | LLM-as-judge带 chain-of-thought | 否 |
**Faithfulness 计算原理(最重要的指标)**
```
1. LLM 将 answer 分解为原子声明列表
例:"答案北京是中国首都人口约2200万"
→ ["北京是中国首都", "北京人口约2200万"]
2. 对每条声明LLM 判断:能否从 retrieved context 中推导出来?
→ [True, False] (第二条无法从 context 推导 = 幻觉)
3. Faithfulness = 支持的声明数 / 总声明数 = 1/2 = 0.5
```
### 2.3 端到端综合指标
| 指标 | 计算方式 | 说明 |
|------|----------|------|
| **RAG Score** | 调和均值(Faithfulness, Answer Relevance, Context Precision, Context Recall) | 综合评分,任一短板都会拉低总分 |
| **Hallucination Rate** | 含幻觉样本数 / 总样本数Faithfulness < 阈值 | 幻觉发生率 |
---
## 三、系统架构
### 3.1 整体架构
```
┌─────────────────────────────────────────────────────────────────┐
│ RAG Eval Framework │
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌───────────────┐ │
│ │ Python SDK │ │ FastAPI Server │ │ React Web UI │ │
│ │ (核心逻辑) │ ←→ │ (REST API) │ ←→ │ (可视化报告) │ │
│ │ CLI 支持 │ │ 任务队列 │ │ 测试集管理 │ │
│ └──────────────┘ └──────────────────┘ └───────────────┘ │
│ ↓ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 核心模块 │ │
│ │ Adapters │ Evaluators │ LLM Judge │ Dataset Gen │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↕ HTTP API标准化 Adapter 接口)
┌─────────────────────────────────────────────────────────────────┐
│ dagent platform / 任何其他 RAG 系统 │
└─────────────────────────────────────────────────────────────────┘
```
### 3.2 数据流
```
测试集 (question + relevant_chunk_ids + reference_answer)
EvalRunner.run(dataset, agent_id, knowledge_hub_id)
┌────────────────────────────────────────────────────┐
│ for each sample: │
│ │
│ Step 1: adapter.retrieve(question) │
│ → 获取 Top-K 召回切片 │
│ → 计算 Hit Rate / MRR / NDCG与标注对比
│ │
│ Step 2: adapter.chat(question) │
│ → 获取 Agent 回复 + 引用切片 │
│ → judge.score_faithfulness(answer, context) │
│ → judge.score_relevance(question, answer) │
│ → judge.score_correctness(answer, reference) │
└────────────────────────────────────────────────────┘
EvalReport每条样本详情 + 汇总统计 + 趋势对比)
```
---
## 四、项目结构
```
rag-eval/
├── sdk/ # Python SDK核心
│ ├── rag_eval/
│ │ ├── __init__.py
│ │ ├── runner.py # 评测任务执行器(入口)
│ │ ├── adapters/ # 平台适配器
│ │ │ ├── base.py # 抽象接口定义
│ │ │ └── dagent.py # dagent 适配器实现
│ │ ├── evaluators/ # 评测器
│ │ │ ├── retrieval.py # 检索层Hit Rate / MRR / NDCG
│ │ │ └── generation.py # 生成层Faithfulness / Relevance / Correctness
│ │ ├── judge/ # LLM Judge
│ │ │ ├── base.py # 抽象接口
│ │ │ └── openai_compatible.py # 兼容 DeepSeek / Qwen / OpenAI
│ │ ├── dataset/ # 测试集管理
│ │ │ ├── schema.py # 数据结构定义Pydantic
│ │ │ └── generator.py # LLM 自动生成测试集
│ │ └── report.py # 报告生成与格式化
│ ├── pyproject.toml
│ └── README.md
├── server/ # FastAPI 后端
│ ├── main.py
│ ├── api/
│ │ ├── dataset.py # 测试集 CRUD
│ │ ├── task.py # 评测任务管理
│ │ ├── report.py # 报告查询
│ │ └── config.py # 平台连接 & Judge 配置
│ ├── service/
│ │ ├── task_service.py
│ │ └── report_service.py
│ ├── models/ # 数据库模型SQLite / PostgreSQL
│ │ └── schema.sql
│ └── requirements.txt
├── frontend/ # React 前端
│ ├── src/
│ │ ├── pages/
│ │ │ ├── Dataset/ # 测试集管理(上传/生成/标注)
│ │ │ ├── Task/ # 评测任务(配置/提交/进度)
│ │ │ └── Report/ # 报告 & 可视化(雷达图/趋势图)
│ │ └── components/
│ └── package.json
└── docker-compose.yml # 一键部署
```
---
## 五、核心接口设计
### 5.1 Adapter 抽象接口
```python
# sdk/rag_eval/adapters/base.py
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class RetrievedChunk:
chunk_id: str
content: str
score: float # 相似度分数
headers: str # 所属章节标题
file_id: str
@dataclass
class AgentResponse:
answer: str
retrieved_chunks: list[RetrievedChunk] # Agent 实际使用的切片
latency_ms: int
class RAGAdapter(ABC):
"""
任何 RAG 平台都需要实现这两个方法。
框架通过此接口与平台交互,不依赖平台内部实现。
"""
@abstractmethod
async def retrieve(
self,
query: str,
knowledge_hub_id: str,
top_k: int = 10,
**kwargs
) -> list[RetrievedChunk]:
"""调用平台检索接口,返回召回的切片列表"""
...
@abstractmethod
async def chat(
self,
query: str,
agent_id: str,
**kwargs
) -> AgentResponse:
"""调用平台 Agent 对话接口,返回回复和引用的切片"""
...
```
### 5.2 dagent 适配器
```python
# sdk/rag_eval/adapters/dagent.py
class DagentAdapter(RAGAdapter):
"""
对接 dagent 平台的适配器。
通过 HTTP API 调用,不依赖 dagent 内部代码。
"""
def __init__(self, base_url: str, org_id: str, token: str):
self.base_url = base_url
self.org_id = org_id
self.headers = {"Authorization": f"Bearer {token}"}
async def retrieve(self, query, knowledge_hub_id, top_k=10, **kwargs):
# 调用 dagent 知识库检索接口
# POST /dagent/knowledge/retrieve
async with aiohttp.ClientSession() as session:
resp = await session.post(
f"{self.base_url}/dagent/knowledge/retrieve",
json={"query": query, "knowledge_hub_id": knowledge_hub_id,
"top_k": top_k, "org_id": self.org_id},
headers=self.headers
)
data = await resp.json()
return [RetrievedChunk(**chunk) for chunk in data["chunks"]]
async def chat(self, query, agent_id, **kwargs):
# 调用 dagent Agent 对话接口SSE 流式,解析完整回复)
# POST /dagent/agent/chat
...
```
### 5.3 LLM Judge
```python
# sdk/rag_eval/judge/openai_compatible.py
class OpenAICompatibleJudge(LLMJudge):
"""
兼容所有 OpenAI 协议的模型DeepSeek / Qwen / OpenAI / Azure OpenAI
评判逻辑使用中文 prompt适合中文 RAG 场景
"""
def __init__(self, base_url: str, api_key: str, model: str):
self.client = AsyncOpenAI(base_url=base_url, api_key=api_key)
self.model = model
async def score_faithfulness(self, answer: str, context: list[str]) -> float:
"""
原理:
1. 让 LLM 把 answer 分解为原子声明列表
2. 对每条声明,判断是否可从 context 推导
3. 返回 支持声明数 / 总声明数
"""
context_text = "\n\n".join(context)
# Step 1: 分解为原子声明
decompose_prompt = f"""
请将以下回答分解为独立的原子声明列表,每条声明是一个不可再分的事实陈述。
回答:{answer}
输出格式JSON 数组,如 ["声明1", "声明2", ...]
"""
claims = await self._call_json(decompose_prompt)
# Step 2: 逐条判断是否有 context 支撑
supported = 0
for claim in claims:
verify_prompt = f"""
参考资料:
{context_text}
声明:{claim}
问题:上述声明是否可以从参考资料中推导出来?
只回答 yes 或 no。
"""
result = await self._call(verify_prompt)
if "yes" in result.lower():
supported += 1
return supported / len(claims) if claims else 0.0
async def score_relevance(self, question: str, answer: str) -> float:
"""
原理:
1. 让 LLM 从 answer 反向生成 N 个问题
2. 计算这些问题与原 question 的 Embedding 相似度
3. 返回均值
"""
...
async def score_correctness(self, answer: str, reference: str) -> float:
"""
原理LLM 对比 answer 和 reference给出 0-1 分数
"""
prompt = f"""
请评估以下回答与参考答案的事实一致程度,给出 0 到 1 之间的分数。
1.0 = 完全一致0.0 = 完全错误或无关。
参考答案:{reference}
待评估回答:{answer}
只输出一个 0 到 1 之间的小数。
"""
result = await self._call(prompt)
return float(result.strip())
```
### 5.4 测试集数据结构
```python
# sdk/rag_eval/dataset/schema.py
@dataclass
class EvalSample:
id: str
question: str # 测试问题
reference_answer: str # 标准参考答案
relevant_chunk_ids: list[str] # 标注的相关切片 ID用于检索层评测
knowledge_hub_id: str # 所属知识库
source_file_id: str | None = None # 来源文件(可选)
metadata: dict = field(default_factory=dict)
@dataclass
class EvalDataset:
id: str
name: str
description: str
samples: list[EvalSample]
created_at: datetime
```
### 5.5 SDK 使用示例
```python
from rag_eval import EvalRunner
from rag_eval.adapters import DagentAdapter
from rag_eval.judge import OpenAICompatibleJudge
# 配置适配器(对接 dagent
adapter = DagentAdapter(
base_url="http://dagent-backend:8000",
org_id="org_xxx",
token="your-token"
)
# 配置 LLM Judge独立于 dagent使用 DeepSeek
judge = OpenAICompatibleJudge(
base_url="https://api.deepseek.com/v1",
api_key="sk-xxx",
model="deepseek-chat"
)
# 运行评测
runner = EvalRunner(adapter=adapter, judge=judge)
report = await runner.run(
dataset="./my_dataset.json",
agent_id="agent_xxx",
knowledge_hub_id="hub_xxx",
top_k=10,
)
# 查看结果
print(report.summary())
# ┌─────────────────────────────────────────┐
# │ 评测报告摘要 │
# ├──────────────────────┬──────────────────┤
# │ 样本数 │ 200 │
# │ Hit Rate@10 │ 0.87 │
# │ MRR@10 │ 0.72 │
# │ NDCG@10 │ 0.81 │
# │ Context Precision │ 0.76 │
# │ Context Recall │ 0.83 │
# │ Faithfulness │ 0.91 │
# │ Answer Relevance │ 0.88 │
# │ Answer Correctness │ 0.79 │
# │ RAG Score │ 0.84 │
# │ Hallucination Rate │ 4.5% │
# └──────────────────────┴──────────────────┘
report.save("./eval_report_20260413.json")
```
---
## 六、测试集构建方案
### 6.1 数据结构
每条测试样本
```json
{
"id": "sample_001",
"question": "什么是向量数据库?",
"reference_answer": "向量数据库是专门存储和检索高维向量的数据库系统...",
"relevant_chunk_ids": ["chunk_abc123", "chunk_def456"],
"knowledge_hub_id": "hub_xxx",
"source_file_id": "file_yyy"
}
```
### 6.2 构建方式
**方式 ALLM 自动生成(推荐先用)**
```python
from rag_eval.dataset import DatasetGenerator
generator = DatasetGenerator(judge=judge, adapter=adapter)
dataset = await generator.generate(
knowledge_hub_id="hub_xxx",
questions_per_chunk=2,
question_types=["factual", "reasoning", "comparison", "unanswerable"]
)
# 自动生成问题 + 参考答案 + 标注 relevant_chunk_ids
```
原理
1. 遍历知识库中所有切片
2. 对每个切片 LLM 生成 2-3 个不同类型的问题
3. LLM 基于切片内容生成参考答案
4. 自动标注 `relevant_chunk_ids`生成来源切片
5. 建议人工抽检 10-20% 过滤低质量样本
**方式 B人工标注质量最高**
通过 Web UI 提供标注界面
- 输入问题
- 搜索并标注相关切片
- 填写参考答案
**问题类型覆盖建议**
| 类型 | 示例 | 占比建议 |
|------|------|----------|
| 事实查询 | "X 是什么" | 40% |
| 多跳推理 | "X Y 的关系是" | 20% |
| 比较 | "X Y 有什么区别" | 20% |
| 不可回答 | 文档中不存在的信息 | 10% |
| 摘要 | "总结 X 的主要内容" | 10% |
推荐测试集规模**200-500 **低于 100 条统计意义不足
---
## 七、Web 端功能规划
| 页面 | 核心功能 |
|------|----------|
| **测试集管理** | 上传 JSON 测试集LLM 自动生成人工标注界面样本预览 |
| **评测任务** | 配置 Adapter平台连接)、配置 Judge 模型提交任务实时进度 |
| **评测报告** | 各指标得分雷达图样本级别明细表多次评测趋势对比问题样本下钻 |
| **配置管理** | 平台连接配置URL/Token)、Judge 模型配置API Key/Model|
---
## 八、数据库设计Server 端)
```sql
-- 平台连接配置
CREATE TABLE platform_config (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL, -- 'dagent' | 'custom'
base_url TEXT NOT NULL,
org_id TEXT,
token TEXT,
created_at DATETIME
);
-- Judge 模型配置
CREATE TABLE judge_config (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
base_url TEXT NOT NULL,
api_key TEXT NOT NULL,
model TEXT NOT NULL,
created_at DATETIME
);
-- 测试集
CREATE TABLE eval_dataset (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
sample_count INTEGER,
created_at DATETIME
);
-- 测试样本
CREATE TABLE eval_sample (
id TEXT PRIMARY KEY,
dataset_id TEXT NOT NULL,
question TEXT NOT NULL,
reference_answer TEXT NOT NULL,
relevant_chunk_ids TEXT NOT NULL, -- JSON array
knowledge_hub_id TEXT NOT NULL,
source_file_id TEXT,
metadata TEXT -- JSON
);
-- 评测任务
CREATE TABLE eval_task (
id TEXT PRIMARY KEY,
name TEXT,
dataset_id TEXT NOT NULL,
platform_config_id TEXT NOT NULL,
judge_config_id TEXT NOT NULL,
agent_id TEXT NOT NULL,
knowledge_hub_id TEXT NOT NULL,
top_k INTEGER DEFAULT 10,
status TEXT NOT NULL, -- pending | running | done | failed
progress INTEGER DEFAULT 0,
created_at DATETIME,
finished_at DATETIME
);
-- 样本级评测结果
CREATE TABLE eval_result (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
sample_id TEXT NOT NULL,
retrieved_chunks TEXT, -- JSON
agent_answer TEXT,
hit_rate REAL,
mrr REAL,
ndcg REAL,
context_precision REAL,
context_recall REAL,
faithfulness REAL,
answer_relevance REAL,
answer_correctness REAL,
judge_detail TEXT -- JSONLLM judge 的推理过程
);
-- 评测汇总报告
CREATE TABLE eval_report (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL UNIQUE,
sample_count INTEGER,
avg_hit_rate REAL,
avg_mrr REAL,
avg_ndcg REAL,
avg_context_precision REAL,
avg_context_recall REAL,
avg_faithfulness REAL,
avg_answer_relevance REAL,
avg_answer_correctness REAL,
rag_score REAL,
hallucination_rate REAL,
created_at DATETIME
);
```
---
## 九、开发优先级
| 阶段 | 内容 | 说明 |
|------|------|------|
| **Phase 1** | SDK 核心Adapter 接口 + 检索评测器 | LLM 依赖最快验证Hit Rate/MRR/NDCG |
| **Phase 2** | dagent Adapter 实现 | 对接现有平台 HTTP API |
| **Phase 3** | LLM Judge 模块 | Faithfulness / Relevance / Correctness |
| **Phase 4** | 测试集自动生成器 | 降低标注成本 |
| **Phase 5** | FastAPI Server | SDK 包成 Web 服务支持异步任务 |
| **Phase 6** | React 前端 | 报告可视化测试集管理 |
---
## 十、技术选型
| 模块 | 技术 | 理由 |
|------|------|------|
| SDK | Python 3.10+, asyncio, Pydantic | dagent 保持一致异步支持并发评测 |
| Server | FastAPI + SQLite开发/ PostgreSQL生产 | 轻量易部署 |
| 任务队列 | asyncio.Queue轻量/ Celery生产 | 评测任务耗时长需异步执行 |
| Frontend | React + TypeScript + Ant Design | dagent 前端技术栈一致 |
| LLM Judge | OpenAI SDK兼容 DeepSeek/Qwen | 统一接口灵活切换模型 |
| 部署 | Docker Compose | 一键启动 server + frontend |
---
## 十一、与 dagent 平台的集成方式
框架通过 **HTTP API** 调用 dagent不依赖 dagent 内部代码
dagent 需要提供或框架调用现有接口
1. **检索接口**`POST /dagent/knowledge/retrieve`
- 输入query, knowledge_hub_id, top_k, org_id
- 输出切片列表chunk_id, content, score, headers, file_id
2. **对话接口**`POST /dagent/agent/chat`现有 SSE 接口
- 输入question, agent_id, org_id
- 输出回复文本 + 引用切片信息
如果 dagent 现有接口不完全满足可在 dagent 侧新增一个**评测专用接口**返回更详细的检索过程信息如每个切片的 cosine distancererank score )。