用MLflow Tracking构建可复现的大模型评估工作流
1. 项目概述:为什么用 MLflow 来评估大模型,而不是写一堆零散脚本?
“Evaluating LLMs with MLflow: A Practical Beginner’s Guide”这个标题一上来就划清了边界——它不讲怎么训练大模型,不聊模型架构选型,也不堆砌 BLEU、ROUGE、BERTScore 这些指标的数学定义。它直奔一个非常具体、也非常痛的现实问题: 当你手头有 3 个微调后的 Llama-3-8B、2 个量化版 Qwen2-7B、还有 1 个刚从 Hugging Face 拉下来的 Phi-3-mini,你如何在 2 天内说清楚‘哪个模型在客服问答任务上真正更稳’?
我做过不下 12 个 LLM 应用落地项目,最常被业务方拍桌子问的一句话是:“你昨天说模型 A 的准确率高 0.8%,那它在真实对话里会不会突然胡说八道?上线后出错谁兜底?”——这种问题,光靠 Jupyter Notebook 里跑一次 accuracy_score() 是完全答不上来的。你得能回溯:用的是哪次推理的 prompt 模板?温度值设为 0.3 还是 0.7?测试集是不是混进了上周新上线的 500 条用户投诉语料?错误样本长什么样?人工复核过没?
MLflow 在这里不是“又一个要学的新工具”,而是 把模型评估这件事从‘临时性实验’升级为‘可审计、可复现、可协作的工程动作’的关键枢纽 。它天然解决三个新手最容易踩的坑:第一,指标散落在不同 .py 文件、不同 notebook 单元格里,改个参数就得手动改 5 处;第二,没人记得清“v2.3 版本的评估结果”对应的是哪次 git commit、哪个 GPU 节点、什么 PyTorch 版本;第三,当算法同学说“模型 B 的 F1 高”,而产品同学质疑“但它把‘退款’全判成‘物流查询’”,双方根本不在同一份带原始输入输出的评估报告上对齐。
所以这个指南的核心价值,不是教你“怎么装 MLflow”,而是帮你建立一套 带上下文的评估工作流 :每次运行评估,自动存下 prompt、样本、预测、标注、指标、硬件环境、甚至人工复核标记。这样下次有人问“为什么选模型 C?”,你直接甩出一个 MLflow UI 链接,点开就能看到它在 200 条模糊问法(比如“东西还没到,急死了”)上的召回率是 91.2%,而模型 A 只有 73.5%,且附带 12 条典型失败案例截图。这才是技术人该有的交付姿势。
关键词“LLMs”“MLflow”“Beginner’s Guide”已经框定了范围:面向刚跑通第一个 pipeline("text-generation") 的开发者,不预设 MLOps 经验,但默认你熟悉 Python 和命令行。接下来所有内容,都围绕“如何让评估这件事,从‘能跑通’变成‘敢签字’”来展开。
2. 整体设计思路:为什么是 MLflow Tracking + Custom Evaluation,而不是 MLflow Evaluate 或第三方库?
很多新手看到标题,第一反应是:“MLflow 不是有现成的 mlflow.evaluate() 吗?直接调用不就行了?”——这恰恰是我要先掰开揉碎讲透的关键决策点。我在实际项目中试过三种路径:纯自定义脚本、MLflow 官方 evaluate() 、以及本文采用的 Tracking API + 手动 log_metrics/log_table/log_artifact 组合拳 。结论很明确:对 LLM 评估,官方 evaluate() 在 2024 年仍处于“可用但不够用”的状态,而 Tracking API 是目前最可控、最透明、也最容易 debug 的方案。
2.1 官方 mlflow.evaluate() 的三大硬伤
先说结论: 它适合结构化数据(如表格分类)的快速 baseline 对比,但不适合 LLM 的多维、非确定性、强上下文评估 。原因有三:
第一, 指标固化,无法表达 LLM 的核心痛点 。 mlflow.evaluate() 内置的 toxicity , flesch_kincaid_grade 等指标,本质是调用 Hugging Face datasets 的预设 pipeline。但你在做客服场景时,真正关心的可能是:“模型是否在用户明确说‘我要投诉’时,依然返回标准话术?”——这需要自定义规则匹配,比如正则检测 r'(?i)投诉|不满|要告|12315' 是否出现在输入中,且模型输出未包含 ['已记录','将反馈','为您转接'] 中任一短语。官方接口不支持注入这种业务逻辑。
第二, 样本级分析能力薄弱 。它的 eval_result.tables 只提供 predictions 和 targets 两列,而 LLM 评估必须看到更多维度:原始 prompt、模型生成的完整文本、截断前的 logits 分布、temperature/top_p 参数、甚至 token-level 的 attention 可视化(用于 debug 为什么模型总在第 3 句开始胡编)。这些信息, mlflow.evaluate() 默认不采集,也无法通过参数扩展。
第三, 版本与环境耦合松散 。它会自动 log model 和 dataset,但不会强制绑定 Python 环境(如 transformers==4.41.2 )、GPU 驱动版本( nvidia-driver=535.129.03 ),更不会记录 prompt template 的 git hash。而我们在某银行项目中就遇到过:同一份评估脚本,在 A 服务器上 F1=0.87,在 B 服务器上掉到 0.79,最后发现是 B 服务器的 tokenizers 库版本低了 0.2,导致中文分词边界偏移——这种坑,必须靠 Tracking API 的 log_params 和 set_tag 显式固化。
2.2 为什么 Tracking API 是更优解?
Tracking API 的核心优势在于: 它不假设你的评估逻辑,只提供一个标准化的“记账本” 。你负责定义“什么是重要的”,它负责确保“每笔账都记得清、查得到、对得上”。具体到 LLM 评估,我们把它拆成四个必录层:
-
Layer 1:执行上下文(Context)
用log_params()记录所有影响结果的变量:model_name="Qwen2-7B-Instruct",quantization="awq",max_new_tokens=256,temperature=0.2。特别注意prompt_template_version="v2.4-20240521"——我们把 prompt 模板存在 Git 仓库,每次更新都打 tag,这里直接 log tag 名,确保可追溯。 -
Layer 2:数据快照(Data)
用log_table()存评估集的结构化摘要:{"dataset_name":"customer_service_v3","size":1247,"avg_input_length":42.3,"label_distribution":{"query":0.62,"complaint":0.28,"refund":0.10}}。同时用log_artifact()上传原始 JSONL 文件(压缩后),文件名带 hash,比如test_set_8a3f2d.jsonl.gz。 -
Layer 3:逐样本洞察(Sample-level Insight)
这是 LLM 评估的灵魂。我们构建一个 Pandas DataFrame,每行对应一个测试样本,列包括:input_text,gold_label,model_output,is_correct,confidence_score,error_type(人工标注:hallucination/off-topic/incomplete),latency_ms。然后用log_table("detailed_results", df)一次性存入。MLflow UI 会自动生成可排序、可筛选的表格,点击任意行还能展开看完整文本。 -
Layer 4:聚合指标(Aggregation)
用log_metrics()记录关键数字:{"accuracy":0.842,"complaint_recall":0.791,"avg_latency_ms":1247.3,"hallucination_rate":0.083}。注意,这些数字必须是从 Layer 3 的 DataFrame 计算而来,而非独立计算——保证指标与样本数据严格一致。
提示:不要试图用
log_metric()一条条记录 20 个指标。先在内存中算好metrics_dict = {"acc":..., "f1":..., "toxicity_avg":...},再用log_metrics(metrics_dict, step=0)一次性提交。多次调用log_metric()会产生时间戳错乱,UI 上显示为“同一实验下多个时间点的指标”,干扰对比。
这套设计看似多写几行代码,但换来的是 绝对的掌控力 。当业务方质疑某个指标时,你可以立刻在 UI 里按 error_type="hallucination" 筛选,导出全部 97 条失败样本,发给 QA 团队人工复核——而不是对着一个黑盒函数的结果干瞪眼。
3. 核心细节解析:从零搭建可复现的 LLM 评估流水线
现在进入实操环节。我会以一个真实的客服问答评估任务为例,带你一步步搭起整套流程。假设你已有:一个微调好的 Qwen2-7B-Instruct 模型(Hugging Face 格式),一份含 1200 条样本的 test.jsonl (每行是 {"input": "用户问题", "output": "标准答案", "category": "咨询/投诉/退款"} ),以及一台装有 CUDA 12.1 的服务器。整个过程不依赖 Docker 或 Kubernetes,纯 Python 脚本驱动,确保新手能 100% 复现。
3.1 环境准备与 MLflow 初始化:轻量但严谨
首先明确一点: MLflow Tracking Server 不是必须的 。对于单机评估,我们直接用 file:// 后端,既简单又可靠。但“简单”不等于“随意”——环境隔离是复现性的第一道防线。
# 创建专用虚拟环境(Python 3.10+)
python -m venv llm-eval-env
source llm-eval-env/bin/activate # Linux/Mac
# llm-eval-env\Scripts\activate # Windows
# 安装核心依赖(版本锁定!)
pip install mlflow==2.14.0 \
transformers==4.41.2 \
torch==2.3.0+cu121 \
pandas==2.2.2 \
scikit-learn==1.4.2 \
accelerate==0.30.1 \
bitsandbytes==0.43.3 # 如需 4-bit 量化
关键点在于版本锁定。 transformers==4.41.2 是因为我们的模型是在此版本下微调的, torch==2.3.0+cu121 对应 CUDA 12.1 驱动。如果用 pip install mlflow 不加版本,可能装到 2.15,而它默认启用的新 tracking backend 在某些旧系统上会报 sqlite3 兼容问题——这是我在线上环境踩过的坑,务必规避。
初始化 Tracking 时,采用显式配置而非环境变量:
import mlflow
from mlflow import MlflowClient
# 显式设置 tracking URI,避免读取环境变量导致混乱
mlflow.set_tracking_uri("file:///path/to/your/mlruns") # 替换为你的绝对路径
# 创建实验(相当于一个项目文件夹)
experiment_name = "llm-customer-service-eval"
experiment = mlflow.get_experiment_by_name(experiment_name)
if experiment is None:
experiment_id = mlflow.create_experiment(experiment_name)
else:
experiment_id = experiment.experiment_id
# 启动 run(一次评估即一个 run)
with mlflow.start_run(
experiment_id=experiment_id,
run_name=f"qwen2-7b-instruct-v1.2-{int(time.time())}" # 带时间戳,避免重名
) as run:
# 后续所有 log_xxx 都在此 run 下
pass
注意:
mlruns目录必须是 绝对路径 。很多人用相对路径file://./mlruns,结果在不同目录下运行脚本时,MLflow 会创建多个分散的mlruns文件夹,导致实验数据丢失。这是新手最高频的失误,建议直接写死/home/user/llm-eval/mlruns。
3.2 Prompt 构建与模型加载:确保推理一致性
LLM 评估的第一步,永远是“让模型说人话”。这里有两个隐形雷区:prompt 模板不统一、模型加载方式影响输出稳定性。
Prompt 模板必须参数化且版本化 。我们不用字符串拼接,而是用 Jinja2 模板引擎:
{# templates/qwen2_instruct_v2.4.j2 #}
<|im_start|>system
你是一名专业的客服助手,请根据用户问题提供准确、简洁、友好的回答。禁止编造信息,不确定时请回答“暂未获取相关信息”。<|im_end|>
<|im_start|>user
{{ input_text }}<|im_end|>
<|im_start|>assistant
在 Python 中加载并渲染:
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("templates"))
template = env.get_template("qwen2_instruct_v2.4.j2")
# 渲染 prompt
prompt = template.render(input_text="我的订单还没发货,能查一下吗?")
# 输出:<|im_start|>system\n...\n<|im_start|>user\n我的订单还没发货,能查一下吗?<|im_end|>\n<|im_start|>assistant
为什么用 Jinja2?因为它支持条件逻辑(如 {{ "请稍候" if category == 'query' else "已记录您的投诉" }} ),且模板文件可独立 git 管理。我们在 log_params() 中记录 prompt_template_path="templates/qwen2_instruct_v2.4.j2" 和 prompt_template_hash="sha256:8a3f2d..." (用 hashlib.sha256(open(...).read().encode()).hexdigest() 计算),确保任何 prompt 变更都可追溯。
模型加载必须禁用随机性 。LLM 推理的非确定性主要来自 torch.backends.cudnn.benchmark 和 torch.use_deterministic_algorithms 。我们在加载前强制设置:
import torch
# 关键:禁用 cuDNN benchmark(它会为不同输入选择不同算法,影响输出一致性)
torch.backends.cudnn.benchmark = False
torch.use_deterministic_algorithms(True)
# 加载模型(以 AWQ 量化为例)
from transformers import AutoModelForCausalLM, AutoTokenizer
import awq_cpp # 确保已编译
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2-7B-Instruct-AWQ",
device_map="auto",
torch_dtype=torch.float16,
# 关键参数:禁用 dropout 和 layer norm 的随机性
attn_implementation="eager" # 避免 flash-attn 的非确定性
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-7B-Instruct-AWQ")
实操心得:
attn_implementation="eager"是必须的。默认的"flash_attention_2"在某些序列长度下会因 CUDA kernel 优化导致微小浮点差异,虽然不影响业务,但会让model_output字符串在多次运行间不完全一致,破坏is_correct判断的稳定性。宁可慢 15%,也要结果可复现。
3.3 评估逻辑实现:从 raw output 到可解释指标
现在进入核心——如何把模型输出转化为业务可理解的指标。我们以“投诉识别准确率”为例,展示完整链条。
Step 1:定义评估任务与标签映射
客服场景中,“投诉”不是单一标签,而是由多个信号构成:
- 输入信号:用户语句含
投诉|不满|要告|12315|工商局(正则匹配) - 输出信号:模型回复含
已记录|将反馈|为您转接|主管(正则匹配) - 黄金标准:
test.jsonl中的"category": "complaint"
我们构建一个 ComplaintEvaluator 类:
import re
from typing import Dict, List, Tuple
class ComplaintEvaluator:
def __init__(self):
self.input_patterns = [r'(?i)投诉|不满|要告|12315|工商局|消协']
self.output_patterns = [r'(?i)已记录|将反馈|为您转接|主管|负责人']
def is_complaint_input(self, text: str) -> bool:
return any(re.search(p, text) for p in self.input_patterns)
def is_complaint_response(self, text: str) -> bool:
return any(re.search(p, text) for p in self.output_patterns)
def evaluate_sample(self, input_text: str, model_output: str, gold_label: str) -> Dict:
pred_label = "complaint" if self.is_complaint_response(model_output) else "other"
is_correct = (pred_label == gold_label)
# 深度分析:为什么错?
error_type = "none"
if not is_correct:
if gold_label == "complaint" and not self.is_complaint_response(model_output):
error_type = "missed_complaint" # 漏判
elif gold_label != "complaint" and self.is_complaint_response(model_output):
error_type = "false_complaint" # 误判
return {
"input_text": input_text[:100] + "..." if len(input_text) > 100 else input_text,
"gold_label": gold_label,
"model_output": model_output[:200] + "..." if len(model_output) > 200 else model_output,
"pred_label": pred_label,
"is_correct": is_correct,
"error_type": error_type,
"latency_ms": 0, # 后续填充
}
Step 2:批量推理与结果收集
重点在于 控制 batch size 和 memory 。LLM 推理容易 OOM,我们采用动态 batch:
def batch_inference(model, tokenizer, prompts: List[str], batch_size: int = 4) -> List[str]:
results = []
for i in range(0, len(prompts), batch_size):
batch = prompts[i:i+batch_size]
# Tokenize with padding
inputs = tokenizer(
batch,
return_tensors="pt",
padding=True,
truncation=True,
max_length=2048
).to(model.device)
# 关键:设置 deterministic seed
torch.manual_seed(42) # 固定种子,确保相同输入输出一致
start_time = time.time()
outputs = model.generate(
**inputs,
max_new_tokens=256,
temperature=0.2,
top_p=0.95,
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
eos_token_id=tokenizer.eos_token_id
)
end_time = time.time()
# Decode only new tokens
decoded = tokenizer.batch_decode(
outputs[:, inputs.input_ids.shape[1]:],
skip_special_tokens=True
)
results.extend(decoded)
print(f"Batch {i//batch_size+1}/{(len(prompts)-1)//batch_size+1} done. Avg latency: {(end_time-start_time)/len(batch)*1000:.1f}ms")
return results
# 主评估循环
evaluator = ComplaintEvaluator()
all_results = []
for sample in tqdm(test_data, desc="Evaluating"):
prompt = template.render(input_text=sample["input"])
start_time = time.time()
output = model.generate_one(prompt) # 封装好的单样本生成
latency = (time.time() - start_time) * 1000
result = evaluator.evaluate_sample(
input_text=sample["input"],
model_output=output,
gold_label=sample["category"]
)
result["latency_ms"] = latency
all_results.append(result)
Step 3:计算指标并结构化存储
所有样本处理完后,汇总成 DataFrame 并 log:
import pandas as pd
from sklearn.metrics import classification_report
df_results = pd.DataFrame(all_results)
# 计算宏观指标
metrics = {
"accuracy": (df_results["is_correct"] == True).mean(),
"complaint_recall": df_results[df_results["gold_label"]=="complaint"]["is_correct"].mean(),
"avg_latency_ms": df_results["latency_ms"].mean(),
"hallucination_rate": (df_results["error_type"] == "false_complaint").mean(),
}
# log metrics
mlflow.log_metrics(metrics)
# log detailed table
mlflow.log_table("detailed_results", df_results)
# log classification report as text artifact
report_str = classification_report(
df_results["gold_label"],
df_results["pred_label"],
output_dict=False
)
with open("classification_report.txt", "w") as f:
f.write(report_str)
mlflow.log_artifact("classification_report.txt")
注意:
classification_report必须保存为文本文件再log_artifact,不能直接log_text,因为 MLflow UI 对文本文件的支持更稳定,支持在线预览。
3.4 人工复核机制:让评估结果经得起质疑
自动化指标只是起点,真正的可信度来自人工校验。我们在流程中嵌入轻量级复核:
- 自动抽样 :对
error_type != "none"的所有样本,以及latency_ms > 2000的慢响应样本,自动生成review_candidates.csv。 - 复核模板 :提供 CSV 文件,含
input_text,model_output,gold_label,pred_label,error_type,confidence_score(可选),QA 人员只需填review_result: "correct"/"incorrect"/"ambiguous"和review_note。 - 结果融合 :复核完成后,用新标签重新计算指标,并
log_table("detailed_results_reviewed", reviewed_df),同时set_tag("review_status", "completed")。
这一步让评估从“算法说了算”变成“人机协同验证”,极大提升业务方信任度。在某电商项目中,人工复核发现模型将 12% 的“物流查询”误判为“投诉”,原因是 prompt 中 system 指令过于强调“投诉”关键词——这个洞察,纯自动化指标永远给不出。
4. 实操全流程:从启动到生成可交付报告的完整 walkthrough
现在把前面所有模块串起来,给出一个可直接复制粘贴运行的完整脚本框架。这个脚本设计为“一次运行,全程留痕”,目标是生成一份能让产品经理、算法总监、运维同事三方都认可的评估报告。
4.1 脚本结构与执行入口
我们采用模块化设计,主脚本 run_evaluation.py 只负责调度:
# run_evaluation.py
import argparse
import time
from datetime import datetime
import mlflow
from mlflow import MlflowClient
from evaluation.pipeline import LLMClassificationPipeline
from evaluation.evaluators import ComplaintEvaluator
from evaluation.utils import load_test_data, setup_mlflow
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--model-path", type=str, required=True, help="Path to model (HF format)")
parser.add_argument("--test-data", type=str, required=True, help="Path to test.jsonl")
parser.add_argument("--prompt-template", type=str, required=True, help="Path to .j2 template")
parser.add_argument("--experiment-name", type=str, default="llm-customer-service-eval")
args = parser.parse_args()
# Step 1: Setup MLflow
setup_mlflow(args.experiment_name)
# Step 2: Load data and config
test_data = load_test_data(args.test_data)
evaluator = ComplaintEvaluator()
# Step 3: Run full pipeline
pipeline = LLMClassificationPipeline(
model_path=args.model_path,
prompt_template_path=args.prompt_template,
evaluator=evaluator,
batch_size=4
)
# Execute
results_df, metrics_dict = pipeline.run(test_data)
# Step 4: Log everything
with mlflow.start_run(run_name=f"eval-{int(time.time())}") as run:
# Log params
mlflow.log_params({
"model_path": args.model_path,
"test_data": args.test_data,
"prompt_template": args.prompt_template,
"batch_size": 4,
"timestamp": datetime.now().isoformat(),
})
# Log metrics
mlflow.log_metrics(metrics_dict)
# Log tables
mlflow.log_table("detailed_results", results_df)
# Log artifacts
mlflow.log_artifact("config.yaml") # 存放所有超参
mlflow.log_artifact("prompt_template.j2") # 当前使用的模板副本
# Set tags for quick filtering
mlflow.set_tag("task", "customer_service_classification")
mlflow.set_tag("model_family", "qwen2")
mlflow.set_tag("quantization", "awq")
if __name__ == "__main__":
main()
执行命令极其简单:
python run_evaluation.py \
--model-path "/models/Qwen2-7B-Instruct-AWQ" \
--test-data "/data/test_v3.jsonl" \
--prompt-template "templates/qwen2_instruct_v2.4.j2" \
--experiment-name "llm-customer-service-eval"
4.2 运行过程详解:每一秒发生了什么?
假设你执行上述命令,以下是真实运行时的详细日志解读(模拟):
[INFO] Setting up MLflow experiment 'llm-customer-service-eval'
[INFO] Experiment ID: 327
[INFO] Loading test data from /data/test_v3.jsonl (1247 samples)
[INFO] Initializing Qwen2-7B-Instruct-AWQ model on GPU...
[INFO] Model loaded. Memory usage: 12.4 GB
[INFO] Compiling prompt template...
[INFO] Starting evaluation run: eval-1716528942
[PROGRESS] Batch 1/312: Processing 4 samples...
[INFO] Batch 1/312 done. Avg latency: 1247.3ms. GPU memory: 13.1 GB
[PROGRESS] Batch 2/312: Processing 4 samples...
...
[INFO] All 1247 samples processed. Total time: 24.7 minutes.
[INFO] Calculating metrics...
[INFO] Accuracy: 0.842 | Complaint Recall: 0.791 | Avg Latency: 1247.3ms
[INFO] Saving detailed_results table (1247 rows x 8 cols)...
[INFO] Logging artifacts...
[INFO] Run completed. Run ID: 8a3f2d1e4c7b8a9f0e1d2c3b4a5f6e7d
[INFO] View results at: http://localhost:5000/#/experiments/327/runs/8a3f2d1e4c7b8a9f0e1d2c3b4a5f6e7d
关键观察点:
- 内存监控 :日志中
GPU memory: 13.1 GB是重要线索。如果后续 run 出现 OOM,对比这个基线值即可判断是否模型加载异常。 - 时间分布 :
Total time: 24.7 minutes包含模型加载(约 2 分钟)、prompt 渲染(可忽略)、推理(22 分钟)、后处理(30 秒)。如果某次 run 时间翻倍,优先检查latency_ms分布,定位是整体变慢还是个别样本卡死。 - Run ID 输出 :
8a3f2d1e4c7b8a9f0e1d2c3b4a5f6e7d是唯一标识,可直接粘贴到 MLflow UI URL 中,无需在 UI 里翻找。
4.3 MLflow UI 中的成果呈现:如何向非技术人员解释结果?
打开 http://localhost:5000 (默认 UI 地址),你会看到清晰的三层信息:
第一层:Run 摘要页(Overview)
- 左侧
Parameters:列出所有log_params,如model_path,prompt_template,点击值可跳转到对应文件(如果log_artifact过)。 - 中间
Metrics:所有log_metrics,支持按时间排序(虽然我们只 log 一次,但 UI 仍显示为时间序列)。 - 右侧
Tags:task,model_family等,支持点击筛选,比如点model_family=qwen2,立即过滤出所有 Qwen2 模型的评估 run。
第二层:详细结果表(detailed_results)
点击 Artifacts → detailed_results ,进入交互式表格:
- 列可排序:点击
latency_ms降序,立刻看到最慢的 10 个样本。 - 列可筛选:在
error_type列输入missed_complaint,筛选出所有漏判投诉的样本。 - 行可展开:点击任意行右侧
>,展开input_text和model_output的完整内容,支持复制。 - 导出便捷:右上角
Export按钮,一键下载 CSV,供 QA 团队复核。
第三层:对比分析(Compare Runs)
这是 MLflow 最强大的功能。勾选两个 run(如 qwen2-7b 和 llama3-8b ),UI 自动生成对比表格:
| Metric | qwen2-7b | llama3-8b | Delta |
|---|---|---|---|
| accuracy | 0.842 | 0.831 | +0.011 |
| complaint_recall | 0.791 | 0.823 | -0.032 |
| avg_latency_ms | 1247.3 | 2156.7 | -909.4 |
更关键的是,点击 detailed_results 列,可并排查看两个 run 的同一样本输出。例如,对样本 ID=882 (输入:“我要投诉你们虚假宣传!”),你能直观看到:
- Qwen2 输出:“已记录您的投诉,将尽快反馈。” ✅
- Llama3 输出:“感谢您的反馈,我们会持续改进。” ❌(未识别投诉意图)
这种粒度的对比,是任何 PPT 报告都无法替代的证据链。
4.4 生成可交付报告:从 UI 到 PDF 的一键导出
MLflow UI 本身不支持 PDF 导出,但我们用一个轻量技巧解决:
-
方案 A(推荐):MLflow Export API
MLflow 提供mlflow export命令行工具,可导出整个 experiment 为 ZIP:mlflow experiments export --experiment-id 327 --output-dir ./export_qwen2_eval导出的 ZIP 包含所有
params,metrics,artifacts的 JSON 文件,用 Python 脚本解析后生成 Markdown 报告,再用pandoc转 PDF。我们提供了一个generate_report.py脚本(见 GitHub 仓库),3 行命令搞定:python generate_report.py --run-id 8a3f2d1e4c7b8a9f0e1d2c3b4a5f6e7d --output report_qwen2.pdf -
方案 B(极简):浏览器打印
在 MLflow UI 的 Run 页,按Ctrl+P(MacCmd+P),选择“保存为 PDF”。虽然丢失交互性,但保留所有表格和图表,足够应付日常汇报。
最终生成的 PDF 报告包含:
- 封面:模型名称、评估日期、负责人
- 执行摘要:关键指标卡片(Accuracy, Recall, Latency)
- 错误分析:
error_type分布饼图 + 前 5 类错误样本 - 性能对比:与基线模型(如
Qwen1.5-4B)的指标雷达图 - 附录:完整
detailed_results表(前 100 行)
这份 PDF,就是你向业务方交付的“评估签证”,签字即生效。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
在 12 个 LLM 评估项目中,我整理出 7 个最高频、最隐蔽、最让人抓狂的问题。每个问题都附带真实日志、根因分析和一行修复命令。这不是理论,是血泪经验。
5.1 问题 1: mlflow.log_table() 报错 ValueError: Table must be a pandas DataFrame or dict
现象 :
脚本运行到 mlflow.log_table("results", df) 时崩溃,报错:
ValueError: Table must be a pandas DataFrame or dict
根因 :
你以为 df 是 DataFrame,其实它是 None 。常见于 pandas.read_json() 读取损坏的 JSONL 文件时静默失败,返回 None ,而你没加 if df is None: raise ValueError("Failed to load data") 检查。
排查 :
在 log_table 前加调试:
print(f"Type of df_results: {type(df_results)}")
print(f"Shape of df_results: {getattr(df_results, 'shape', 'no shape')}")
print(f"First 2 rows:\n{df_results.head(2) if hasattr(df_results, 'head') else 'no head'}")
修复 :
确保 JSONL 文件格式正确。用 jq 验证:
# 检查是否每行都是合法 JSON
jq -s '.' test.jsonl # 成功则输出合并后的数组,失败则报错行号
# 检查关键字段是否存在
jq 'select(.input == null or .output == null)' test.jsonl # 输出为空则安全
5.2 问题 2:MLflow UI
更多推荐
所有评论(0)