从 0 到 1 实现 LLM/RAG 自动化评测平台:FastAPI + Qwen + DeepSeek 实战
本文记录一个面向 AI 测试开发方向的工程项目:如何把“不稳定的大模型回答”转换成可批量执行、可量化比较、可追踪复现的测试结果。
一、为什么要做 AI 评测平台
传统接口测试通常可以直接断言:
assert response.status_code == 200 assert response.json()["code"] == 0
但大模型可能用不同措辞表达同一个意思。例如参考答案是“退款预计 1 至 3 个工作日到账”,模型可能回答“退款通常三个工作日内原路返回”。如果只做字符串相等判断,正确回答也会失败。
这个项目需要解决四个问题:
-
如何批量调用被测模型并保存问题、回答、耗时和 Token?
-
如何组合确定性规则、语义相似度和 LLM Judge?
-
如何评价 RAG 的召回、排序和答案忠实度?
-
如何证明 Judge 自己的评分是可信的?
最终技术栈如下:
Python 3.11 + FastAPI + Pydantic + SQLAlchemy pytest + httpx + SQLite/MySQL Qwen Target Model + DeepSeek Judge + text-embedding-v4 Docker + Jenkins Pipeline
项目代码仓库地址:发布到 Gitee 后在这里补充链接。
二、系统架构
平台把目标模型和评测模型分开:Qwen 负责生成回答,DeepSeek 负责评分,百炼 Embedding 负责语义相似度和向量检索。
JSON Dataset / FastAPI Request | v EvaluationRunner | Qwen Target Model | actual_answer | +--------+---------+ | | | Rule Semantic LLM Judge | | Embedding DeepSeek +--------+---------+ | SQLAlchemy Store | JSON / HTML Report
RAG 专项链路如下:
业务文档 -> Chunk -> Embedding -> Vector Index | 问题 -> Query Embedding -> Top-K ------+ -> Rerank -> Qwen Answer -> Recall/MRR/Faithfulness
这种设计有两个好处:
-
模型厂商不会侵入业务编排,替换 Base URL、模型名和 Key 即可切换模型。
-
每种 Evaluator 只处理一种指标,新增安全性或指令遵循评测时不需要修改 Runner。
三、统一 OpenAI-compatible Client
Qwen 和 DeepSeek 都可以通过 OpenAI-compatible 接口接入,因此项目没有在业务层写死厂商 SDK。
配置文件只保存模型角色:
TARGET_MODEL=qwen-plus JUDGE_MODEL=deepseek-v4-flash EMBEDDING_MODEL=text-embedding-v4
真实 Key 只保存在本地 .env:
DASHSCOPE_API_KEY=本地填写 DEEPSEEK_API_KEY=本地填写
.env、SQLite 数据库、虚拟环境和真实报告都通过 .gitignore 排除,避免上传 Gitee 时泄露。
Client 统一返回以下元数据:
@dataclass(frozen=True) class ModelResponse: content: str model: str | None latency_ms: float attempts: int = 1 prompt_tokens: int | None = None completion_tokens: int | None = None total_tokens: int | None = None
这样报告不仅展示回答质量,也能分析延迟、重试和 Token 成本。
四、三种 Evaluator 如何组合
1. Rule Evaluator
Rule 适合必须出现的确定性内容,例如退款回答必须包含“退款”和“到账”。它速度快、成本低、解释性强,但无法识别同义表达。
2. Semantic Evaluator
Semantic Evaluator 对参考答案和实际答案分别计算 Embedding,再计算余弦相似度:
reference_answer -> embedding A actual_answer -> embedding B cosine(A, B) -> similarity score
它能容忍措辞差异,但语义相似不等于事实正确,因此不能单独作为最终结论。
3. LLM-as-a-Judge
项目把问题、参考答案和实际答案交给 DeepSeek,要求只返回结构化 JSON:
{
"score": 4,
"passed": true,
"reason": "核心结论正确,但遗漏退款发起时间"
}
Pydantic 会检查:
-
score必须在 1 至 5 之间; -
只有 4 分和 5 分允许
passed=true; -
Judge 返回 Markdown 或非法 JSON 时不能被当作有效分数。
三种指标组合后,可以同时获得确定性、语义和开放式质量证据。
五、批量执行为什么需要并发、重试和故障隔离
模型请求属于 I/O 密集任务,批量串行执行速度很慢。项目使用受限线程池并发调用模型,并通过 executor.map 保持结果与输入 Case 的顺序一致。
但并发不能无限提高,否则会触发限流或快速消耗额度。因此并发数可以通过环境变量控制:
EVALUATION_MAX_WORKERS=4
重试策略只处理可能恢复的错误:
408 / 429 / 5xx / 网络异常 -> 指数退避重试 400 等参数错误 -> 立即失败
如果目标模型失败,当前 Case 没有答案,任务应失败;如果某个 Evaluator 失败,则只把该指标标为失败,保留其他有效指标。这就是故障隔离。
模型调用记录和 Evaluator 结果也分别存储。一个 Case 虽然产生三条评测结果,但 Qwen 实际只调用一次,延迟和 Token 不能重复累计三次。
六、RAG 质量如何评测
项目构造了 24 篇模拟电商客服规则和 60 条带期望文档 ID 的 Benchmark,覆盖:
-
订单
-
支付
-
退款
-
物流
-
发票
-
账户安全
-
14 条跨文档问题
主要检索指标如下。
Recall@K
应召回的相关文档中,Top-K 实际找回了多少:
Recall@K = 命中的唯一相关文档数 / 全部期望文档数
Precision@K
Top-K 结果中有多少是相关文档:
Precision@K = 命中的唯一相关文档数 / K
MRR
第一个相关文档排名的倒数:
第1名命中 -> 1.0 第2名命中 -> 0.5 未命中 -> 0
Faithfulness
Faithfulness 判断回答中的事实是否都能从检索上下文中获得支持。它解决的是“模型引用了知识库,但仍然编造额外信息”的问题。
七、一次真实的 400 问题排查
最开始 Demo 只有少量文本,Embedding 调用正常。扩充到 60 条 Query 后,参数实验突然返回:
400 Bad Request Embedding request failed or returned invalid data
Key 和模型名都没有变化,因此问题更可能与批量规模有关。查询百炼官方文档后确认,text-embedding-v4 同步接口一次最多接受 10 条文本。
修复方式不是重试 400,而是在 Client 内自动分批:
for start in range(0, len(texts), self.batch_size):
batch = texts[start:start + self.batch_size]
# 请求当前 batch,并按 index 排序后追加到结果
随后增加 Mock HTTP 测试,验证 5 条输入、批大小为 2 时,请求会被拆成:
[text1, text2] [text3, text4] [text5]
这个问题说明:真实 API 约束必须在批量数据下验证,少量 Smoke Case 无法发现所有工程问题。
八、三组真实 RAG 参数实验
使用 text-embedding-v4 对 60 条 Query 运行三套配置,结果如下:
| 配置 | Chunk 数 | Recall@K | Precision@K | MRR | Hit Rate |
|---|---|---|---|---|---|
| Chunk40 / Overlap8 / K2 | 48 | 0.91 | 0.53 | 0.99 | 1.00 |
| Chunk80 / Overlap16 / K3 | 24 | 0.98 | 0.41 | 0.99 | 1.00 |
| Chunk80 / Overlap16 / K3 / Rerank | 24 | 0.98 | 0.41 | 1.00 | 1.00 |
实验结论:
-
K 从 2 增加到 3 后,Recall 从 0.91 提升到 0.98,但 Precision 从 0.53 降到 0.41。
-
更大的 Chunk 减少了索引数量,并在当前规则文档上取得更高召回。
-
轻量 Rerank 保持 Recall 不变,把 MRR 从 0.99 提升到 1.00。
这里不能简单说“Top-K 越大越好”。更高 K 会给生成模型更多上下文,也可能引入噪声并增加 Token 成本。
九、LLM Judge 自己可信吗
如果完全相信 Judge,评测系统可能只是把一个模型的偏差换成另一个模型的偏差。因此项目构造了 20 条覆盖 1 至 5 分的规则金标 Case,再与 DeepSeek Judge 比较。
真实结果:
通过/失败一致率:85% 精确分数一致率:70% 平均绝对误差:0.30
其中 3 条通过边界分歧都是:金标为 4 分,Judge 给出 3 分。共同特点是核心答案正确,但遗漏了时间、限制条件或安全操作。
这说明当前 Judge 对“基本正确但不完整”的回答偏严格。正确做法不是为了让数字好看而修改标签,而是:
-
保留分歧 Case;
-
人工复核 3/4 分边界;
-
在报告中说明 Judge 的偏差;
-
重要业务不能只依赖一次模型评分。
十、如何运行项目
1. 安装依赖
python -m venv .venv .\.venv\Scripts\Activate.ps1 python -m pip install -r requirements.txt
2. 无 Key 验收
python -m scripts.run_demo_acceptance python -m scripts.validate_benchmark
3. 配置真实模型
Copy-Item .env.example .env
在本地 .env 填写 Key,不要提交该文件。
4. 真实模型验收
python -m scripts.run_real_acceptance --check-config python -m scripts.run_real_acceptance --confirm-cost
5. RAG 参数实验与 Judge 校准
python -m scripts.run_rag_experiment python -m scripts.run_judge_calibration --check-dataset python -m scripts.run_judge_calibration --confirm-cost
6. 运行自动化测试
python -m pytest -q
当前共 82 项测试,覆盖 Client、重试、Evaluator、数据库、API、RAG、报告、Benchmark 和 Judge 校准。
十一、项目当前边界
这个项目是校招级工程项目,不是生产级商业平台。当前边界包括:
-
向量索引保存在进程内,服务重启后需要重建;
-
Rerank 是向量分与词项重合度组合的轻量基线,不是 Cross-Encoder;
-
批量任务当前同步执行,尚未接入 Celery/Redis;
-
Benchmark 是模拟电商业务规则,不是真实企业生产数据;
-
Dockerfile 和 Jenkinsfile 已提供,但仍应在目标机器完成实际部署验证;
-
Judge 校准数据是基于明确规则构造的项目金标,不应冒充企业人工标注数据。
明确项目边界不是减分项。相比堆叠一串没有验证的技术名词,能够说明“实现了什么、验证了什么、哪里仍有限制”更符合测试开发思维。
十二、总结
这个项目最终形成了以下闭环:
业务规则与测试集
-> 真实模型批量执行
-> Rule / Semantic / Judge
-> RAG Recall / MRR / Faithfulness
-> 参数对比与 Judge 校准
-> JSON / HTML Badcase 报告
-> 自动化回归
最大的收获不是接入了多少模型,而是把原本不确定的自然语言输出转换成了可量化、可复现、可分析的质量证据。
更多推荐



所有评论(0)