☰
从 0 到 1 实现 LLM/RAG 自动化评测平台:FastAPI + Qwen + DeepSeek 实战(TaoToken 统一 Key 接入篇)
2026/10/2 6:45:14 网站建设 项目流程

1. 多模型 Key 分散,评测平台还没跑起来就先被配置拖垮

做 LLM/RAG 自动化评测平台,绕不开一个很现实的问题:你不可能只用一个模型。目标模型要生成回答,Judge 模型要打分,Embedding 模型要算语义相似度,这三类角色往往来自不同厂商。Qwen 擅长中文生成和指令遵循,DeepSeek 在推理和评分上性价比高,Embedding 又常常用另一家的接口。于是项目还没写几行评测逻辑,.env里已经躺了三四组 Key,每组 Key 对应不同的 Base URL、不同的模型名、不同的限流策略。

我见过不少团队的做法是:在代码里写死厂商 SDK,Qwen 用 dashscope 的包,DeepSeek 用 openai 的包,Embedding 再引一个。结果就是每换一个模型,业务编排层要跟着改;每加一个评测维度,配置文件要重新梳理一遍。更麻烦的是,当你想把 Qwen 换成另一个同级别模型做对比实验时,发现调用方式、返回结构、错误码全都不一样,评测脚本得重写。

这个场景的核心痛点其实就三个:Key 分散导致管理成本高,模型切换导致代码侵入性强,多厂商接口差异导致评测结果难以横向对比。而 FastAPI + TaoToken 统一 Key 接入的组合,恰好能把这三个问题一次性收拢。TaoToken 提供的是 OpenAI-compatible 的统一 API 通道,Qwen 和 DeepSeek 都通过同一个 Base URL 和同一套请求格式调用,你只需要在配置里改模型名,业务代码完全不用动。

这篇文章要带你跑通的,是一个最小可用的 LLM/RAG 自动化评测平台:用 FastAPI 搭服务,用 TaoToken 统一 Key 接入 Qwen 和 DeepSeek 双模型,用一套可复制的目录结构和.env配置,完成一次从请求到评分的 RAG 问答评测闭环。适合谁?适合正在做 AI 测试开发、想给大模型输出建立可量化质量证据的工程师,也适合想把评测流程工程化、但不想被多厂商配置拖住的后端同学。

整个平台的设计思路是:目标模型和评测模型分离,Qwen 负责生成回答,DeepSeek 负责 LLM Judge 评分,Embedding 负责语义相似度。三者都走 TaoToken 的统一通道,配置只保存模型角色,真实 Key 只放在本地.env。这样替换模型时,改一行环境变量就能切换,评测逻辑和报告结构保持不变。

2. TaoToken 统一 Key 接入:把多厂商配置收拢成一个 Base URL

在动手写 FastAPI 服务之前,先把接入层理清楚。传统做法里,Qwen 和 DeepSeek 各自有独立的控制台、独立的 Key、独立的计费,你在代码里要维护两套客户端。TaoToken 的思路是提供一个统一的 OpenAI-compatible 网关,你拿一个 Key,就能调用包括 Qwen、DeepSeek 在内的多个模型,请求格式和 OpenAI 的/v1/chat/completions一致。

这意味着你的评测平台只需要一个 HTTP 客户端,不需要为每个厂商装不同的 SDK。对于自动化评测来说,这一点特别重要:评测平台的价值在于批量执行、可复现、可对比,如果接入层就充满了厂商差异,后面的指标计算和报告生成都会被污染。

先看接入需要准备什么。你需要一个 TaoToken 的 API Key,在控制台的 API Keys 页面创建。创建之后,Base URL 统一用https://taotoken.net/api,注意这个地址不带任何查询参数,是纯粹的 API 端点。模型名则根据你要调用的角色填写,比如 Qwen 系列填qwen-plus,DeepSeek 系列填deepseek-v4-flash,Embedding 填text-embedding-v4。这些模型名会作为请求体里的model字段传给网关,由网关路由到对应的后端。

这里有个容易踩的坑:很多人会把 Base URL 写成带/v1的完整路径,然后在代码里又拼一次/v1/chat/completions,结果变成/v1/v1/chat/completions,直接 404。正确的做法是 Base URL 只写到https://taotoken.net/api,具体的路径由 OpenAI SDK 或 httpx 客户端自己拼接。如果你用的是 openai 这个 Python 包,初始化时传base_url="https://taotoken.net/api"即可,SDK 会自动补上/chat/completions。

另一个要注意的是模型角色的划分。评测平台里至少有三类调用:目标模型生成回答、Judge 模型评分、Embedding 模型算相似度。这三类调用虽然都走同一个 Key,但模型名不同,超时和重试策略也可能不同。所以配置里不要只写一个MODEL,而要按角色分开写。这样后面做参数实验时,你可以只改 Judge 模型,不影响目标模型,评测结果的可比性更强。

TaoToken 的接入文档里有完整的模型列表和参数说明,建议在配置前先扫一眼,确认你要用的模型名拼写正确。模型名写错是最常见的 400 来源之一,而且报错信息往往只告诉你model not found,不会提示你正确的名字。把模型名集中放在.env里,而不是散落在代码各处,就是为了改的时候只改一个地方。

从工程角度看,统一 Key 接入带来的最大好处是:你的评测平台和具体厂商解耦了。今天用 Qwen 做目标模型、DeepSeek 做 Judge,明天想换成两个 DeepSeek 模型做对比,只需要改.env里的两行,FastAPI 服务和评测逻辑一行都不用动。这种解耦对于需要频繁做模型对比实验的评测场景来说,是刚需。

3. 可复制配置:目录结构、模型路由与 .env 示例

这一节给出可以直接抄的工程配置。先看目录结构,这是我在多个评测项目里收敛出来的最小布局,既够用又不会过度设计:

llm-eval-platform/ ├── app/ │ ├── main.py # FastAPI 入口 │ ├── config.py # 读取 .env,定义模型角色 │ ├── clients/ │ │ └── llm_client.py # 统一 OpenAI-compatible 客户端 │ ├── evaluators/ │ │ ├── rule_evaluator.py # 确定性规则 │ │ ├── semantic_evaluator.py │ │ └── judge_evaluator.py # DeepSeek LLM Judge │ ├── rag/ │ │ ├── retriever.py # 向量检索 │ │ └── metrics.py # Recall / MRR / Faithfulness │ ├── runner.py # 批量执行 + 并发 + 重试 │ └── models.py # Pydantic 数据模型 ├── data/ │ ├── benchmark.jsonl # 评测集 │ └── docs/ # RAG 知识库文档 ├── scripts/ │ ├── run_acceptance.py # 验收脚本 │ └── run_rag_experiment.py # RAG 参数实验 ├── tests/ ├── .env.example ├── .gitignore └── requirements.txt

关键点在于clients/llm_client.py只依赖一个 Base URL 和一个 Key,模型名从配置里读。evaluators/下每个评测器只负责一种指标,新增评测维度时不用改 Runner。rag/单独放检索和指标计算,和模型调用解耦。

接下来是.env.example,这是接入的核心配置,直接复制改 Key 就能用:

# TaoToken 统一接入 TAOTOKEN_API_KEY=sk-your-key-here TAOTOKEN_BASE_URL=https://taotoken.net/api # 模型角色划分 TARGET_MODEL=qwen-plus JUDGE_MODEL=deepseek-v4-flash EMBEDDING_MODEL=text-embedding-v4 # 评测执行参数 EVALUATION_MAX_WORKERS=4 REQUEST_TIMEOUT=60 MAX_RETRIES=3 # 存储 DATABASE_URL=sqlite:///./eval.db

注意.env本身要写进.gitignore,只提交.env.example。真实 Key 永远不进版本库,这是底线。config.py里用 Pydantic Settings 读取这些变量,模型角色作为独立字段暴露给业务层:

from pydantic_settings import BaseSettings class Settings(BaseSettings): taotoken_api_key: str taotoken_base_url: str = "https://taotoken.net/api" target_model: str = "qwen-plus" judge_model: str = "deepseek-v4-flash" embedding_model: str = "text-embedding-v4" evaluation_max_workers: int = 4 request_timeout: int = 60 max_retries: int = 3 class Config: env_file = ".env" settings = Settings()

统一客户端用 openai 包初始化,注意base_url只写到/api:

from openai import OpenAI from app.config import settings client = OpenAI( api_key=settings.taotoken_api_key, base_url=settings.taotoken_base_url, timeout=settings.request_timeout, ) def chat(model_role: str, messages: list[dict]) -> dict: model = { "target": settings.target_model, "judge": settings.judge_model, }[model_role] resp = client.chat.completions.create( model=model, messages=messages, temperature=0.0, ) return { "content": resp.choices[0].message.content, "model": resp.model, "prompt_tokens": resp.usage.prompt_tokens, "completion_tokens": resp.usage.completion_tokens, }

这段代码里,model_role是业务层唯一需要关心的东西,它不关心背后是 Qwen 还是 DeepSeek。要切换模型,改.env里的TARGET_MODEL就行。temperature=0.0是评测场景的推荐值,减少随机性,让结果更可复现。

如果你用的是 Claude Code 或 Cline 这类工具做辅助开发,配置方式类似:Base URL 填https://taotoken.net/api,Key 填 TaoToken 的 Key,Model ID 填你要用的模型名。三件套(Base URL + Key + Model ID)缺一不可,尤其是 Model ID 要和网关支持的模型名完全一致。

4. 验证请求:跑通一次 RAG 问答评测的完整闭环

配置就绪后,用一次真实的 RAG 问答评测来验证整条链路。这个验证动作要覆盖:检索、生成、评分三个环节,最终产出一个可读的评分结果。

先准备一条评测样本。假设知识库里有电商退款规则文档,评测问题是“退款多久到账”,参考答案是“退款预计 1 至 3 个工作日到账”。RAG 链路会先检索相关文档,把上下文喂给 Qwen 生成回答,再用 DeepSeek 做 Judge 评分。

检索部分用 Embedding 模型把问题和文档都转成向量,算余弦相似度取 Top-K。这里要注意 Embedding 接口的批量限制,text-embedding-v4同步接口一次最多接受 10 条文本,超过要分批。这个坑后面排障章节会细说。

生成部分调用目标模型:

def generate_answer(question: str, context: str) -> str: messages = [ {"role": "system", "content": "你是电商客服助手,只根据给定上下文回答。"}, {"role": "user", "content": f"上下文:\n{context}\n\n问题:{question}"}, ] return chat("target", messages)["content"]

Judge 部分要求 DeepSeek 返回结构化 JSON,用 Pydantic 校验:

from pydantic import BaseModel, Field, field_validator class JudgeResult(BaseModel): score: int = Field(ge=1, le=5) passed: bool reason: str @field_validator("passed") @classmethod def check_pass(cls, v, info): score = info.data.get("score") if score is not None and score < 4 and v: raise ValueError("score < 4 不允许 passed=true") return v

Judge 的 prompt 要明确要求只返回 JSON,不要 Markdown 包裹:

def judge_answer(question: str, reference: str, actual: str) -> JudgeResult: messages = [ {"role": "system", "content": "你是严格的评测员,只返回 JSON,格式:{\"score\": int, \"passed\": bool, \"reason\": str}"}, {"role": "user", "content": f"问题:{question}\n参考答案:{reference}\n实际回答:{actual}"}, ] raw = chat("judge", messages)["content"] return JudgeResult.model_validate_json(raw)

跑一次完整验证:

python -m scripts.run_acceptance --question "退款多久到账" --confirm-cost

预期输出类似:

[Retrieve] top_k=3, docs=['refund_policy.md', 'payment_rule.md'] [Generate] model=qwen-plus, latency=1.8s, tokens=156 [Answer] 退款通常三个工作日内原路返回 [Judge] model=deepseek-v4-flash, score=4, passed=true [Reason] 核心结论正确,但遗漏了退款发起时间 [Report] saved to reports/acceptance_20250101.json

看到score=4, passed=true就说明闭环跑通了。注意 Judge 给 4 分而不是 5 分,理由是“遗漏退款发起时间”,这正是 LLM Judge 的价值:它不只看结论对不对,还看信息完整性。这种细粒度反馈是字符串匹配做不到的。

验证时建议先跑一条样本,确认链路通了再批量跑。批量执行用受限线程池,并发数由EVALUATION_MAX_WORKERS控制,默认 4。并发太高会触发限流,太低则批量评测慢。重试策略只处理可恢复错误:408、429、5xx 和网络异常走指数退避,400 这类参数错误立即失败,不浪费重试次数。

批量跑完后,报告里会包含每条样本的问题、回答、耗时、Token 和三个评测器的结果。Rule 看关键词是否命中,Semantic 看余弦相似度,Judge 看结构化评分。三者组合,既有确定性证据,又有语义和开放式质量证据。

5. 常见报错排查:401、local proxy failed、reading choices、OAuth

接入过程中最容易撞上的几类报错,这里逐个对照排查。这些错误信息你在日志里大概率会见到,提前知道原因能省不少时间。

401 Unauthorized / invalid api key:Key 没填对,或者.env没被正确加载。先确认TAOTOKEN_API_KEY的值没有多余空格,再确认config.py里的env_file路径正确。如果你在 Docker 里跑,检查环境变量有没有传进容器。还有一种情况是 Key 被复制时带了换行,用echo $TAOTOKEN_API_KEY | wc -c看长度是否异常。

local proxy failed / connection refused:这类错误通常和网络环境有关。检查你的 Base URL 是不是写成了https://taotoken.net/api,有没有多写或少写路径。如果本地有 HTTP 代理设置,确认代理没有拦截对 API 端点的请求。在容器里跑的话,确认容器能正常解析域名。

reading choices / KeyError 'choices':这个报错说明请求返回了,但返回结构里没有choices字段。常见原因是模型名写错,网关返回了一个错误对象而不是正常的 completion 结构。先打印完整响应体看error字段,确认模型名拼写。另一个原因是请求体格式不对,比如messages为空或格式错误。用resp.model_dump()打印完整响应,比只看resp.choices更容易定位。

OAuth / authentication failed:如果你用的是 Claude Code 或类似工具,出现 OAuth 相关报错,通常是认证方式选错了。这类工具要选 API Key 认证,而不是 OAuth 登录。Base URL 填https://taotoken.net/api,Key 填 TaoToken 的 Key,Model ID 填对应模型名。三件套齐全后,认证方式选 API Key,不要走 OAuth 流程。

400 Bad Request / Embedding request failed:Embedding 接口对批量大小有限制,text-embedding-v4同步接口一次最多 10 条。如果你的评测集有 60 条 Query,一次性传进去就会 400。修复方式是在客户端内自动分批:

def embed_batch(texts: list[str], batch_size: int = 10) -> list[list[float]]: results = [] for start in range(0, len(texts), batch_size): batch = texts[start:start + batch_size] resp = client.embeddings.create( model=settings.embedding_model, input=batch, ) ordered = sorted(resp.data, key=lambda x: x.index) results.extend([d.embedding for d in ordered]) return results

注意按index排序后再追加,否则并发或分批返回的顺序可能和输入不一致,导致向量和文本对不上。这个 bug 很隐蔽,因为不报错,只是相似度算出来不对。

429 Too Many Requests:并发太高或请求太密集。降低EVALUATION_MAX_WORKERS,或者给重试加指数退避。评测场景不建议把并发开到 10 以上,4 到 6 是比较稳的区间。

Judge 返回非法 JSON:DeepSeek 有时会用 Markdown 代码块包裹 JSON,或者加一句“以下是评分结果”。Pydantic 的model_validate_json会直接失败。处理方式是在解析前先剥离 Markdown 标记,或者用正则提取第一个{...}块。更稳的做法是在 prompt 里强调“只返回 JSON,不要任何其他文字”,并在解析失败时记录原始输出,方便排查。

排查的核心思路是:先确认请求有没有发出去,再看返回结构对不对,最后看业务逻辑有没有处理正确。把完整响应体打日志,比只看异常信息高效得多。

6. 从评测闭环到持续回归:把统一 Key 接入用起来

跑通一次评测只是起点,真正的价值在于把这条链路变成可重复执行的回归流程。当你有了统一 Key 接入,模型切换的成本从“改代码”降到“改配置”,这意味着你可以更频繁地做模型对比实验,而不必担心每次切换都引入新的接入 bug。

一个实用的做法是把评测脚本接进 CI。每次模型配置变更或 prompt 调整后,自动跑一遍基准评测集,对比 Recall、MRR、Judge 一致率等指标。如果指标下降超过阈值,就阻断合并。这样评测平台就不只是一个离线工具,而是质量门禁的一部分。

另一个值得做的方向是 Judge 校准。LLM Judge 本身也有偏差,比如对“基本正确但不完整”的回答偏严格。你可以构造一批人工标注的金标样本,定期跑 Judge 校准,记录一致率和平均绝对误差。当误差超过可接受范围时,调整 Judge 的 prompt 或换模型。这个过程同样受益于统一 Key:换 Judge 模型只需要改.env里的一行。

如果你需要长期跑批量评测或 Agent 任务,可以关注 TaoToken 的 Coding Plan,它在高频调用场景下比按量计费更划算。对于需要快速验证模型效果的场景,模型对话页面可以直接试跑,不用写代码就能对比 Qwen 和 DeepSeek 的输出差异。接入文档里有完整的模型列表和参数说明,配置前扫一眼能避免大部分模型名拼写错误。

回到工程本身,这个评测平台最大的收获不是接入了多少模型,而是把原本不确定的自然语言输出,转换成了可量化、可复现、可分析的质量证据。统一 Key 接入解决的是接入层的复杂度,让你能把精力放在评测逻辑和指标设计上。当接入不再是瓶颈,你才有余力去思考:Rule、Semantic、Judge 三种评测器怎么组合才合理,RAG 的 Recall 和 Precision 怎么权衡,Judge 的偏差怎么量化。这些才是评测平台真正的价值所在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询