用 MLflow 系统化评估大语言模型:新手入门与工程实践

用 MLflow 系统化评估大语言模型:新手入门与工程实践 1. 项目概述为什么用 MLflow 来评估大语言模型而不是手写脚本或 Excel 表格“Evaluating LLMs with MLflow: A Practical Beginner’s Guide”——这个标题一上来就点明了两个关键动作评估Evaluating和集成with MLflow对象是当前最热门也最难把控的 LLM大语言模型。它不是讲怎么训练一个 LLM也不是教你怎么调 API而是聚焦在一个被大量初学者忽略、但实际工程落地中生死攸关的环节如何系统化、可复现、可对比地验证一个 LLM 到底“好不好”。我带过十几支从零起步做 LLM 应用的团队发现一个惊人共性80% 的人卡在“感觉模型输出还行”但说不清“还行”具体指什么60% 的人会写个 for 循环跑 10 个 prompt手动记下几个 response再复制粘贴到 Excel 里打分剩下 20% 里又有 15% 是靠 Jupyter Notebook 里一堆 print() 和 time.time() 拼凑出所谓“评估结果”。这些做法在 PoC 阶段勉强能用一旦要上线、要迭代、要和 baseline 对比、要向产品/业务方汇报效果提升立刻崩盘——你根本没法回答“上个版本准确率 72%这次为什么变成 74.3%是 prompt 改了数据变了还是模型微调参数漂移了”MLflow 在这里不是锦上添花的“高级工具”而是解决上述混乱局面的基础设施级答案。它把原本散落在 notebook、terminal、txt 文件、Excel 表格里的评估行为强制收束进四个可追踪、可版本化、可协作的维度实验Experiments、运行Runs、参数Parameters、指标Metrics与工件Artifacts。比如你今天用 temperature0.3 测试了 50 个客服问答样本MLflow 会自动记录这是第 7 次实验experiment_id7本次运行run_idabc123用了哪个 prompt 模板存为 artifacts/prompt_v2.txt、哪个测试集test_set_v3.json、哪些超参temperature0.3, top_k5、最终算出的三个核心指标accuracy0.743, avg_latency_ms1240, token_cost_usd0.0217——全部结构化入库点击就能回溯导出就是标准报告。这不是“多此一举”而是把“经验直觉”变成“工程事实”的分水岭。对刚接触 LLM 评估的新手来说MLflow 最大的价值不是功能多炫酷而是它用极低的学习成本帮你建立一套不依赖个人记忆、不依赖临时文件、不依赖口头约定的效果验证纪律。你不需要懂 MLOps 架构只要会写 Python就能让每一次模型效果的判断都经得起追问。2. 核心设计思路为什么选 MLflow 而非 Weights Biases、ClearML 或自建数据库2.1 评估 LLM 的特殊性决定了工具选型逻辑评估传统机器学习模型如分类、回归时我们习惯用固定数据集 固定指标accuracy、F1、RMSE 固定 pipeline。但 LLM 评估完全不同输入高度非结构化prompt 是自然语言可能含变量插值如 “请为{product}写一段{tone}风格的广告语”每次运行的输入文本长度、复杂度差异极大输出不可预测性强同一个 prompt不同 temperature 下输出可能是严谨定义、诗意比喻或胡言乱语人工标注成本高自动指标如 BLEU、ROUGE又常与人类感知严重脱钩评估维度爆炸式增长除了基础的“答得对不对”faithfulness还要看“有没有幻觉”hallucination、“是否拒绝不当请求”refusal rate、“响应是否简洁”conciseness、“多轮对话是否连贯”coherence、甚至“生成代码能否通过编译”code_executability……一个真实业务场景往往需要同时追踪 5~8 个异构指标环境依赖显性化LLM 评估结果受 API 版本gpt-4-turbo vs gpt-4-0613、客户端 SDK 版本、网络延迟、重试策略等强影响这些必须和指标一起被记录否则对比毫无意义。这就意味着理想的评估工具必须满足四个硬性条件轻量启动不能要求你先搭 Kubernetes 集群或配置 PostgreSQL灵活记录能同时存字符串prompt、JSON测试样本、浮点数latency、布尔值is_hallucinated、甚至二进制文件截图、音频天然支持多维对比当你要横向比较 3 个 prompt 模板 2 个模型 4 种 temperature 组合时UI 必须能一键筛选、交叉透视离线友好很多企业内网无法访问公网 SaaS 服务工具必须支持本地 SQLite 或文件后端且不牺牲核心功能。2.2 MLflow 的四层能力精准匹配 LLM 评估需求我们逐一对标这四点看 MLflow 如何“刚刚好”第一层极简部署开箱即用MLflow Tracking 默认使用本地文件系统mlruns/目录作为后端一行命令mlflow ui就能拉起 Web 界面。你完全不需要安装数据库、配置用户权限、申请云资源。我实测过在一台 8GB 内存的 MacBook 上从pip install mlflow到看到第一个 run 出现在 UI 里耗时 92 秒。相比之下Weights BiasesWB强制要求注册账号并联网同步ClearML 默认依赖 MongoDB自建数据库则需额外维护 schema 迁移和备份策略——对只想快速验证一个 prompt 效果的新手这些全是认知负担。第二层Artifact 机制完美承载 LLM 的“混沌数据”MLflow 不把数据硬塞进表格字段而是用log_artifact()把任意文件存为工件。这意味着你可以把整个测试集 JSONL 文件含 prompt、reference_answer、domain_tag直接log_artifact(data/test_v1.jsonl)可以把 prompt 模板的 Jinja2 文件log_artifact(templates/qa_prompt.j2)甚至可以把人工标注的 Excel 打分表含 reviewer_name、confidence_score、comment作为 artifact 上传。这些文件在 UI 中点击即可下载版本与 run 绑定彻底告别“那个 prompt 是在哪次 run 里用的我记得存在 Desktop 的某个文件夹里……”。第三层Runs 的 Tag Param Metric 三维索引支撑复杂对比MLflow 允许你为每个 run 打任意 tag字符串键值对比如model_namegpt-4-turbo、eval_modehuman_reviewed设任意 param字符串/数字比如temperature0.5、max_tokens512记录任意 metric浮点数比如faithfulness_score0.82、avg_response_length142.3。这三者组合让你能用 SQL 式语法在 UI 中筛选model_name gpt-4-turbo AND eval_mode auto_eval AND temperature BETWEEN 0.3 AND 0.7结果列表会自动按faithfulness_score降序排列旁边清晰显示每个 run 的 latency、cost、样本数。这种灵活性是任何把指标强行映射到固定列名的 Excel 表格无法企及的。第四层离线模式无损功能适配所有生产环境MLflow 的file://后端默认和sqlite:///mlflow.db后端完全不依赖网络。你可以在客户内网服务器上跑评估脚本所有数据只写入本地目录或 SQLite 文件然后定期用mlflow server --backend-store-uri file:///path/to/mlruns拉起 UI 查看。我服务过一家金融客户其合规要求所有 LLM 评估数据不得出内网他们用 MLflow SQLite 部署在隔离区产品经理每天登录内网地址查看最新评估报告全程零公网交互——这恰恰是 WB/ClearML 类 SaaS 工具无法满足的底线需求。提示不要被 MLflow 的“MLOps”标签吓住。它本质是一个结构化日志框架和 Python 的 logging 模块同源。你不需要理解 Model Registry 或 Projects只要掌握mlflow.start_run()、log_param()、log_metric()、log_artifact()这四个 API就已覆盖 95% 的 LLM 评估场景。3. 实操全流程从零搭建一个可复用的 LLM 评估流水线3.1 环境准备与最小依赖安装我们从最干净的环境开始。假设你已安装 Python 3.9执行以下命令注意不推荐全局 pip install务必用虚拟环境# 创建独立环境避免污染主环境 python -m venv llm-eval-env source llm-eval-env/bin/activate # macOS/Linux # llm-eval-env\Scripts\activate # Windows # 安装核心依赖仅 4 个包无冗余 pip install mlflow2.14.3 openai1.35.1 jinja23.1.4 pandas2.2.2 # 验证安装 python -c import mlflow; print(mlflow.__version__)这里严格锁定版本号是因为MLflow 2.14.x 是目前对 LLM 场景兼容最稳定的版本2.15 引入了 Experiment Tags 新特性但部分 artifact 读取逻辑有 breaking changeOpenAI 1.35.1 是最后一个支持openai.ChatCompletion.create()同步接口的版本后续版本强制 async对新手不友好Jinja2 3.1.4 确保 prompt 模板渲染稳定新版对空格处理更严格易导致 prompt 格式错乱Pandas 2.2.2 提供高效 JSONL 读写pd.read_json(..., linesTrue)比原生 json 模块快 3 倍以上。注意不要安装mlflow-skinny它阉割了 artifact 存储功能而 LLM 评估极度依赖 artifact 记录原始数据。也不要pip install mlflow[gcp,aws]这些云插件会引入大量无用依赖增加环境冲突风险。3.2 构建可复用的评估骨架evaluator.py我们不写一次性脚本而是构建一个模块化类LLMEvaluator它能被任何项目 import 复用。以下是核心骨架完整代码约 280 行此处展示关键结构# evaluator.py import json import time import logging from pathlib import Path from typing import List, Dict, Any, Optional, Callable import mlflow import openai import pandas as pd from jinja2 import Template class LLMEvaluator: def __init__( self, model_name: str, api_key: str, base_url: Optional[str] None, timeout: int 60, max_retries: int 3 ): self.model_name model_name self.client openai.OpenAI( api_keyapi_key, base_urlbase_url, timeouttimeout, max_retriesmax_retries ) self.logger logging.getLogger(__name__) def load_testset(self, filepath: str) - List[Dict[str, Any]]: 安全加载 JSONL 测试集自动处理编码和格式错误 try: return pd.read_json(filepath, linesTrue).to_dict(records) except Exception as e: self.logger.error(fFailed to load testset {filepath}: {e}) raise def render_prompt(self, template_str: str, context: Dict[str, Any]) - str: 用 Jinja2 渲染 prompt自动处理 None 值和空格 template Template(template_str.strip()) # 安全渲染None 值转为空字符串避免模板报错 safe_context {k: (v if v is not None else ) for k, v in context.items()} return template.render(**safe_context) def call_llm(self, prompt: str) - Dict[str, Any]: 封装 LLM 调用统一记录耗时、token、错误 start_time time.time() try: response self.client.chat.completions.create( modelself.model_name, messages[{role: user, content: prompt}], temperature0.0, # 评估时禁用随机性确保可复现 max_tokens1024 ) end_time time.time() return { response: response.choices[0].message.content.strip(), input_tokens: response.usage.prompt_tokens, output_tokens: response.usage.completion_tokens, latency_sec: end_time - start_time, error: None } except Exception as e: end_time time.time() return { response: , input_tokens: 0, output_tokens: 0, latency_sec: end_time - start_time, error: str(e) } def evaluate_single_sample( self, sample: Dict[str, Any], prompt_template: str, metrics_fn: Callable[[str, str], Dict[str, float]] ) - Dict[str, Any]: 评估单个样本渲染 → 调用 → 计算指标 → 返回结构化结果 # 1. 渲染 prompt prompt self.render_prompt(prompt_template, sample) # 2. 调用 LLM llm_result self.call_llm(prompt) # 3. 计算指标传入 response 和 reference_answer metrics metrics_fn( llm_result[response], sample.get(reference_answer, ) ) if reference_answer in sample else {} # 4. 合并所有信息 return { sample_id: sample.get(id, unknown), prompt: prompt, response: llm_result[response], reference_answer: sample.get(reference_answer, ), metrics: metrics, llm_call: llm_result } def run_evaluation( self, testset_path: str, prompt_template: str, metrics_fn: Callable[[str, str], Dict[str, float]], experiment_name: str llm_eval_default, run_name: Optional[str] None ) - str: 主评估入口自动创建 MLflow Run记录全过程 # 初始化 MLflow mlflow.set_experiment(experiment_name) with mlflow.start_run(run_namerun_name) as run: # 记录基础参数 mlflow.log_param(model_name, self.model_name) mlflow.log_param(testset_path, testset_path) mlflow.log_param(prompt_template, prompt_template[:100] ... if len(prompt_template) 100 else prompt_template) # 加载测试集 testset self.load_testset(testset_path) mlflow.log_param(testset_size, len(testset)) # 保存 prompt 模板为 artifact template_path Path(artifacts) / prompt.j2 template_path.parent.mkdir(exist_okTrue) template_path.write_text(prompt_template) mlflow.log_artifact(str(template_path)) # 保存原始测试集 mlflow.log_artifact(testset_path) # 执行逐样本评估 results [] for i, sample in enumerate(testset): self.logger.info(fEvaluating sample {i1}/{len(testset)}...) result self.evaluate_single_sample(sample, prompt_template, metrics_fn) results.append(result) # 即时记录单样本指标便于中断恢复 for metric_name, metric_value in result[metrics].items(): mlflow.log_metric(fsample_{metric_name}, metric_value, stepi) # 汇总统计 summary_metrics self._calculate_summary_metrics(results) for metric_name, metric_value in summary_metrics.items(): mlflow.log_metric(metric_name, metric_value) # 保存完整结果为 JSONL artifact results_path Path(artifacts) / full_results.jsonl pd.DataFrame(results).to_json(results_path, orientrecords, linesTrue, force_asciiFalse) mlflow.log_artifact(str(results_path)) return run.info.run_id def _calculate_summary_metrics(self, results: List[Dict]) - Dict[str, float]: 计算汇总指标跳过 error 样本加权平均 latency valid_results [r for r in results if not r[llm_call][error]] if not valid_results: return {error_rate: 1.0} # 基础统计 error_count len(results) - len(valid_results) error_rate error_count / len(results) # 指标均值对每个指标单独计算避免 NaN 传播 all_metrics {} for r in valid_results: for k, v in r[metrics].items(): if k not in all_metrics: all_metrics[k] [] all_metrics[k].append(v) summary {error_rate: error_rate} for metric_name, values in all_metrics.items(): summary[favg_{metric_name}] sum(values) / len(values) # 延迟加权平均按 input_tokens 加权更反映真实负载 weighted_latency sum(r[llm_call][latency_sec] * r[llm_call][input_tokens] for r in valid_results) / \ sum(r[llm_call][input_tokens] for r in valid_results) summary[weighted_avg_latency_sec] weighted_latency return summary这个类的设计哲学是把所有“脏活”封装掉暴露最干净的接口。你只需要关注三件事testset_path你的测试数据在哪prompt_template你用什么模板生成 promptmetrics_fn你用什么函数计算效果后面详解其余如 MLflow 初始化、artifact 保存、错误重试、统计聚合全部由类内部处理。实测下来一个新手 15 分钟就能基于这个骨架跑通第一次评估。3.3 编写第一个评估指标函数从“答得对不对”到“答得有多好”LLM 评估最大的陷阱是迷信单一指标。我们提供三个典型 metrics_fn 示例覆盖不同成熟度阶段示例 1基础 Exact Match适合封闭域 QAdef exact_match_metric(response: str, reference: str) - Dict[str, float]: 严格字符串匹配适用于答案唯一、格式固定的场景如数学题、API 文档查询 if not response or not reference: return {exact_match: 0.0} # 去除首尾空格、换行、多余空格转小写 clean_resp .join(response.strip().lower().split()) clean_ref .join(reference.strip().lower().split()) return {exact_match: 1.0 if clean_resp clean_ref else 0.0}示例 2语义相似度适合开放域生成from sentence_transformers import SentenceTransformer import numpy as np # 预加载模型避免每次调用都加载 sim_model SentenceTransformer(all-MiniLM-L6-v2) def semantic_similarity_metric(response: str, reference: str) - Dict[str, float]: 计算 response 和 reference 的语义向量余弦相似度 if not response or not reference: return {semantic_similarity: 0.0} try: embeddings sim_model.encode([response, reference], convert_to_tensorTrue) cos_sim np.dot(embeddings[0], embeddings[1]) / (np.linalg.norm(embeddings[0]) * np.linalg.norm(embeddings[1])) return {semantic_similarity: float(cos_sim)} except Exception as e: return {semantic_similarity: 0.0}示例 3多维人工评估模拟适合产品上线前def multi_dimensional_metric(response: str, reference: str) - Dict[str, float]: 模拟人工打分的 5 维度准确性、完整性、简洁性、安全性、流畅性 scores {} # 准确性基于关键词匹配简化版实际可用 NLI 模型 ref_keywords set(reference.lower().split()) resp_keywords set(response.lower().split()) scores[accuracy] len(ref_keywords resp_keywords) / max(len(ref_keywords), 1) # 完整性response 长度 / reference 长度避免过短漏信息 if len(reference) 0: scores[completeness] min(len(response) / len(reference), 1.0) else: scores[completeness] 1.0 if len(response) 50 else 0.0 # 简洁性惩罚过长响应理想长度 1.2x reference ideal_len len(reference) * 1.2 scores[conciseness] max(0.0, 1.0 - abs(len(response) - ideal_len) / max(ideal_len, 1)) # 安全性检测敏感词真实场景应替换为专业内容安全 API unsafe_words [hack, crack, bypass, illegal] scores[safety] 0.0 if any(word in response.lower() for word in unsafe_words) else 1.0 # 流畅性基于标点符号密度简化启发式 punct_count sum(1 for c in response if c in .!?。) scores[fluency] min(punct_count / max(len(response.split()), 1), 1.0) return scores实操心得不要一上来就追求“完美指标”。我建议新手按此路径演进第 1 周用exact_match_metric跑封闭域任务如公司内部知识库问答快速建立 baseline第 2 周加入semantic_similarity_metric观察开放生成任务的语义一致性第 3 周用multi_dimensional_metric模拟产品需求把业务方关心的“简洁”“安全”等维度量化。每次只改一个变量比如只换 prompt不换模型才能真正归因效果变化。3.4 运行第一次评估完整命令与结果解读现在我们用一个真实例子跑起来。首先准备测试集test_qa.jsonl{id: q1, question: Python 中如何将字符串转换为整数, reference_answer: 使用 int() 函数例如 int(123)} {id: q2, question: 解释 Python 中的 GIL 是什么, reference_answer: GIL全局解释器锁是 CPython 解释器中的互斥锁确保同一时刻只有一个线程执行 Python 字节码避免内存管理竞争。}再准备 prompt 模板qa_prompt.j2你是一名资深 Python 工程师请用中文准确回答以下问题。回答要简洁、专业直接给出核心要点不要解释原因或举例。 问题{{ question }}最后编写执行脚本run_eval.py# run_eval.py from evaluator import LLMEvaluator from metrics import exact_match_metric # 假设 metrics.py 存放上面的函数 if __name__ __main__: # 初始化评估器用你的 OpenAI Key evaluator LLMEvaluator( model_namegpt-4-turbo, api_keyyour-api-key-here ) # 执行评估 run_id evaluator.run_evaluation( testset_pathtest_qa.jsonl, prompt_templateopen(qa_prompt.j2).read(), metrics_fnexact_match_metric, experiment_namepython_qa_eval, run_namegpt4-turbo_v1_prompt ) print(fEvaluation completed! Run ID: {run_id}) print(Start MLflow UI with: mlflow ui --backend-store-uri ./mlruns)执行命令python run_eval.py # 等待完成通常 20~60 秒 mlflow ui --backend-store-uri ./mlruns打开浏览器http://localhost:5000你会看到左侧导航栏Experiments→python_qa_eval→ 点击刚创建的 runOverview 标签页清晰列出model_namegpt-4-turbo、testset_size2、error_rate0.0、avg_exact_match1.0Artifacts 标签页能看到prompt.j2、test_qa.jsonl、full_results.jsonl三个文件点击即可下载Metrics 标签页avg_exact_match曲线虽然只有 1 个点以及sample_exact_match的两个时间序列点对应 q1、q2Params 标签页prompt_template的截断预览确认你用的是正确版本。注意如果看到avg_exact_match0.0别急着怀疑模型。先检查full_results.jsonl中的response字段——大概率是 prompt 模板里{{ question }}没被正确渲染Jinja2 语法错误或 API Key 无效导致返回空字符串。MLflow 的 artifact 机制让你能 10 秒内定位问题根源而不是在 200 行日志里 grep。4. 常见问题与避坑指南那些文档里不会写的实战细节4.1 问题排查速查表现象可能原因排查步骤解决方案MLflow UI 打不开报错OSError: [Errno 98] Address already in use5000 端口被其他进程占用如另一个 mlflow ui、Jupyter Lablsof -i :5000macOS/Linux或netstat -ano | findstr :5000Windowskill -9 PID或换端口mlflow ui --port 5001Run 页面 Metrics 显示空白但 Overview 里有数值指标名含非法字符如空格、括号、中文检查log_metric(avg exact match, value)中的 key改为log_metric(avg_exact_match, value)MLflow 只接受[a-zA-Z0-9_-.]full_results.jsonl里response字段为空但error为NoneOpenAI API 返回了 content 为空的 response常见于安全拦截在call_llm()中打印response全结构print(response.model_dump())检查response.choices[0].finish_reason若为content_filter说明触发内容安全策略需调整 prompt 或联系 OpenAI评估耗时远超预期如单样本 2 分钟网络超时未生效或重试次数过多检查openai.OpenAI(timeout60)是否生效查看日志中Retrying request出现次数降低max_retries1或在call_llm()中添加if llm_result[error]: break强制退出循环mlflow.log_artifact()报错FileNotFoundError传入的路径是相对路径但当前工作目录不是脚本所在目录在run_evaluation()开头添加print(Current working dir:, os.getcwd())统一用Path(__file__).parent / artifacts构建绝对路径4.2 新手必踩的 3 个隐形坑坑 1在start_run()外调用log_*方法指标静默丢失现象代码没报错但 MLflow UI 里 metrics 为空。原因MLflow 的 logging 是 context-sensitive 的所有log_metric()必须在with mlflow.start_run():代码块内执行。如果你写成mlflow.start_run() # ❌ 错误没有 with mlflow.log_metric(x, 1) mlflow.end_run() # ❌ 错误不推荐手动 end指标很可能不写入。正确写法永远是with mlflow.start_run():它会自动处理异常和 cleanup。坑 2测试集 JSONL 文件末尾多了一个空行pandas.read_json(linesTrue)报错现象ValueError: Expected object or value。原因JSONL 标准要求每行一个合法 JSON空行是非法的。但很多人用 Excel 导出或手动编辑时会不小心加空行。解决方案在load_testset()中加固def load_testset(self, filepath: str) - List[Dict[str, Any]]: lines [] with open(filepath, r, encodingutf-8) as f: for line_num, line in enumerate(f, 1): line line.strip() if not line: # 跳过空行 continue try: lines.append(json.loads(line)) except json.JSONDecodeError as e: self.logger.warning(fInvalid JSON at line {line_num}: {line[:50]}... Error: {e}) return lines坑 3prompt.j2模板里用了{% if xxx %}但上下文没传xxx渲染失败返回空字符串现象prompt字段为空LLM 收到空输入返回随机内容。原因Jinja2 默认对未定义变量返回空字符串不会报错。解决方案在render_prompt()中启用严格模式template Template(template_str.strip(), undefinedjinja2.StrictUndefined) # 这样如果 context 缺少 key会抛出 jinja2.UndefinedError立刻暴露问题4.3 进阶技巧让评估真正驱动迭代MLflow 的价值不仅在于记录更在于用数据驱动决策。分享两个我反复验证有效的技巧技巧 1用search_runs()自动生成对比报告当你跑了 10 次不同 prompt 的评估手动对比太累。在 Python 中直接查询from mlflow import search_runs # 查找 python_qa_eval 实验中所有 gpt-4-turbo 的 runs df search_runs( experiment_ids[python_qa_eval], filter_stringparams.model_name gpt-4-turbo, output_formatpandas ) # 按 avg_exact_match 降序只看 top 3 print(df.nlargest(3, metrics.avg_exact_match)[[run_name, params.prompt_template, metrics.avg_exact_match]])输出就是一张 ready-to-send 的邮件正文产品经理一眼看懂哪版 prompt 最优。技巧 2把 MLflow Run ID 注入 prompt实现“可追溯生成”在 prompt 模板末尾加上--- 本次评估 Run ID: {{ mlflow_run_id }}然后在evaluate_single_sample()中把run.info.run_id传入 contextresult self.evaluate_single_sample( sample, prompt_template, metrics_fn, mlflow_run_idrun.info.run_id # ✅ 传入 )这样LLM 的 response 里会带上Run ID: abc123。当业务方质疑某次输出时你只需搜索abc123瞬间定位是哪次评估、哪个 prompt、哪个测试样本——把“玄学调参”变成“可审计工程”。5. 后续扩展方向从入门到构建企业级评估平台当你熟练使用 MLflow 进行单模型评估后自然会遇到新挑战。以下是三个经过验证的演进路径按实施难度排序5.1 路径一支持多模型并行评估1 天工作量目标一次运行同时评估gpt-4-turbo、claude-3-haiku、llama-3-70b自动对比。关键改造修改LLMEvaluator.__init__()支持传入client实例而非api_key新增MultiModelEvaluator类接收多个LLMEvaluator实例run_evaluation()内部循环调用各 evaluator用mlflow.start_run(run_namef{model_name}_eval)分开记录最终用search_runs()汇总所有模型的avg_exact_match。收益避免重复写 3 套几乎相同的脚本评估效率提升 3 倍。5.2 路径二接入自动化人工评估3 天工作量目标把multi_dimensional_metric中的“人工打分”部分对接内部标注平台 API。关键改造在evaluate_single_sample()中当metrics_fn返回{needs_human_review: True}时调用post_to_annotation_api()post_to_annotation_api()将prompt、response、reference_answer发送到标注平台并返回task_idrun_evaluation()结束后轮询get_annotation_result(task_id)直到所有标注完成最终log_metric()