AI模型测评平台工程化实战十二讲(第一讲:从手工测试到系统化的觉醒)

前言:从"能跑"到"跑得稳",再到"让结果说话"

我至今还记得那天晚上,回望墙上的白板,密密麻麻写着"数据集、口径、阈值、报告、对比、回归"几个词。我们不是第一次做模型评测,但我们第一次直面一个现实:靠手工和临时脚本,已经无法再支撑"频繁评测、多人协作、对外汇报"的需求了。

如果这件事注定会越来越频繁、越来越复杂,为什么不把它变成一条可靠、可复用、可回溯、可协作的流水线?

这就是本文要讲的系统的来历。它不是一个脚本集合,而是一套"评测平台":一处接入、多模型协同、统一评分口径、端到端追踪、结构化历史、可视化报告、权限与分享。如今它已经跑在我们的机器上,它每天在替我们做一件本应自动化的工作:让评测这件事,成为"被系统化管理的工程实践"。

这篇文章是系列第一篇,讲清"为什么要做"与"我们到底做了什么"。后续文章会逐步深入架构、数据、引擎、模型接入、后台、可观测性、上线与生态等方面。

我们曾经是怎么做的:手工测试与"脚本地狱"

先承认一个事实:手工测试在早期"够用"。它有三个优点:

  • 上手快:临时想法立刻验证。
  • 反馈直观:人能看见模型"说了啥"。
  • 协作简单:两个人讨论就能对齐。

但手工测试的上限也很明显:

  • 成本高且难复用:同一套题,不同人不同天做,口径不一致,记录也难以沉淀;
  • 报告负担大:散落在表格和截图里的信息,需要人工再拼成 PPT;
  • 不可追溯:你很难在半年后重现"当时为什么判 A 比 B 好"。

于是我们踏入了脚本化阶段。脚本当然是进步:

  • 批量处理:能一次性跑大量题;
  • 自动记录:输出 CSV/Excel,便于后续分析;
  • 易扩展:可以接入更多模型、添加更多指标。

但脚本也会快速陷入"脚本地狱":

  • 多版本分叉:不同人维护不同脚本,参数与输出格式各不相同;
  • 逻辑拷贝膨胀:临时兼容逐步堆积,异常处理难以沉淀为"系统规则";
  • 口径不透明:评分口径藏在代码里、commit 里,协作讨论成本高;
  • 对外不可用:非工程同事不愿(也没时间)安装环境或读命令帮助。

总之,脚本可以"让事情跑起来",但它很难"让事情跑得稳,还能讲清楚"。

我们想要的是什么:一处接入、统一口径、可回溯与可协作

当我们决定"做系统"时,我们其实是在回答四个问题:

  1. 怎样让"评测流程"成为一个可复用的"流水线"?
  2. 怎样让"评分口径"成为被系统所治理的"第一公民"?
  3. 怎样让"结果"天然可视化、可导出、可分享与可追溯?
  4. 怎样让"并发、限流、重试、幂等"这些工程问题被妥善治理?

对应到能力清单,就是:

  • 统一模型接入层:不同模型的认证、限流、返回格式各不同,但系统只暴露统一接口;
  • 一致评分协议:无论客观题还是主观题,评分路径清晰、JSON 协议统一、异常可恢复;
  • 并发与速率治理:能批量跑、稳运行,可控的上限与重试退避;
  • 数据结构与迁移策略:表结构支持长期演进,软删除、共享与可见性内建;
  • 管理后台:模型、评分标准、历史、权限与分享都在同一界面;
  • 可视化与导出:一键导出、图表对比、历史趋势;
  • 可观测性:日志、链路 ID、限流与重试、压测与开关;
  • 安全与成本:API Key 安全保存、用量治理与预算可控。

我们做了什么:从白纸到"能跑且跑得稳"的平台

分层与目录

系统采用 Flask + Jinja2 作为 Web 层,代码分层清晰:

  • routes/:蓝图与路由,处理评测发起、任务状态、结果查看、导出、评分编辑等;
  • models/:模型客户端与工厂,统一外部模型接入;
  • utils/evaluation_engine.py:评测引擎,负责并发执行、提示词治理与 JSON 协议输出;
  • utils/task_manager.py:任务状态、异步工具;
  • services/model_api_service.py:API Key 管理与校验;
  • templates/:页面模板(主页、结果、历史、共享等);
  • database/database.py:表结构、迁移与持久化;
  • app.py:应用入口、全局蓝图与开发端口。

在本地开发时,运行 python app.py --port 8080 即可访问 http://127.0.0.1:8080/

端到端流程(上传→获取答案→裁判打分→结果落库→可视化→导出/分享)

  1. 上传数据集与配置评测:
  • 支持 CSV/Excel,主观题至少包含 query,客观题需要 query + answer
  • 可选择多个"被测模型",并指定一个"裁判模型";
  • 模式支持自动/主观/客观(自动模式会根据列结构智能判断)。
  1. 并发获取被测模型答案:
  • 通过 get_multiple_model_answers 并发调用多个外部模型;
  • 信号量限制并发,避免击穿限流;
  • 错误与超时有兜底与重试策略(后续将持续增强)。
  1. 裁判模型统一打分(严格 JSON 协议):
  • 主观题与客观题分别使用 build_subjective_eval_promptbuild_objective_eval_prompt 构建提示词;
  • 提示词获取优先级:文件级自定义 > 系统默认;缺失时抛出明确错误;
  • 强制裁判模型输出严格 JSON,不允许前后附加说明或 markdown 代码块;
  • 若解析失败,使用最小可用结构兜底,保证整批任务不会被"坏样本"卡死。
  1. 结果落库与可视化:
  • 结果以 CSV 形式写入 results/,列结构稳定(序号、类型、query、标准答案(客观题)、每模型的答案/评分/理由/准确性(客观题));
  • 历史记录写入数据库,包含开始/结束时间、题量、文件大小、模型列表与标签;
  • 页面 /results/<result_id>/view_results/<filename> 自动加载 CSV 与统计数据,支持筛选、导出与分享。
  1. 导出与分享:
  • 支持 CSV/Excel 导出,便于汇报或二次分析;
  • 分享链接与可见性策略在数据库中管理,后续支持密码与有效期。

关键代码锚点

应用入口与端口:

271:283:app.py 复制代码
if __name__ == '__main__':
    # 处理命令行参数
    import sys
    port = 8080
    if len(sys.argv) >= 3 and sys.argv[1] == '--port':
        try:
            port = int(sys.argv[2])
        except ValueError:
            print("❌ 无效的端口号,使用默认端口8080")

    print(f"\n🌐 访问地址: http://localhost:{port}")
    print("📖 配置帮助: python3 test_config.py")
    app.run(debug=True, host='0.0.0.0', port=port)

评测蓝图的发起与进度回填:

53:76:routes/evaluation.py 复制代码
@evaluation_bp.route('/start_evaluation', methods=['POST'])
@login_required
def start_evaluation():
    """开始评测"""
    data = request.get_json()
    filename = data.get('filename')
    selected_models = data.get('selected_models', [])
    judge_model = data.get('judge_model')  # 裁判模型
    force_mode = data.get('force_mode')  # 'auto', 'subjective', 'objective'
    custom_name = data.get('custom_name', '').strip()  # 自定义结果名称
    save_to_history = data.get('save_to_history', True)  # 是否保存到历史记录
    ...

评测引擎并发执行与 JSON 映射:

94:112:utils/evaluation_engine.py 复制代码
# 创建并发任务来评测所有问题,添加实时进度更新
print(f"🚀 开始并发评测,并发数: {GEMINI_CONCURRENT_REQUESTS}")
semaphore = asyncio.Semaphore(GEMINI_CONCURRENT_REQUESTS)
...
async def evaluate_single_question(i: int, row: Dict) -> Tuple[int, List]:
    async with semaphore:
        ...
        # 使用选定的裁判模型进行评测
        judge_raw = await call_judge_model(judge_model, prompt)
        result_json = parse_json_str(judge_raw)
        ...
        # 映射为 CSV 列
        for j, model_name in enumerate(model_names, 1):
            model_key = f"模型{j}"
            row_data.append(current_answers[model_name])  # 模型答案
            if model_key in result_json:
                row_data.append(result_json[model_key].get("评分", ""))
                row_data.append(result_json[model_key].get("理由", ""))
                if mode == 'objective':
                    row_data.append(result_json[model_key].get("准确性", ""))

提示词治理(主观/客观):

235:268:utils/evaluation_engine.py 复制代码
def build_subjective_eval_prompt(..., filename: str = None) -> str:
    ...
    file_prompt = db.get_file_prompt(filename)
    if file_prompt:
        custom_prompt = file_prompt
        score_instruction = "请严格按照上述自定义提示词中定义的评分标准进行评分"
        ...
    else:
        default_prompt = db.get_default_prompt('subjective')
        if default_prompt:
            custom_prompt = default_prompt
        else:
            raise ValueError(...)

并发、限流、退避与幂等:工程问题的"地基"

在评测系统里,并发不是"越高越好",而是"可控、可解释、可稳定"。我们采用以下策略:

  • 信号量限制:集中控制并发上限,避免瞬时洪峰击穿第三方限流;
  • 分阶段并发:被测模型答案获取与裁判评分分两个阶段,避免耦合导致问题放大;
  • 失败可恢复:解析失败与异常都有兜底策略,保证任务整体推进;
  • 进度可观测:每题完成时更新内存与数据库,页面实时可见。

这四点听起来朴素,但是真正让系统"跑得稳"的关键。

可视化蓝图(Mermaid 时序/组件图)

评测主流程(时序图):
用户 Web前端 路由层(routes) 评测引擎(engine) 模型工厂(models) 裁判模型 数据库/文件 上传数据集/选择模型/裁判/模式 1 POST /start_evaluation 2 创建任务记录(task_id) 3 并发获取被测模型答案 4 各模型答案列表 5 并发评测(data, mode, answers, judge) 6 发送评分提示词(JSON强约束) 7 返回JSON评分/理由/(准确性) 8 写入结果CSV与进度 9 返回task_id 10 轮询 /task_status/<task_id> 11 进度/当前步骤/耗时/文件名 12 打开结果页 13 GET /results/<result_id> 14 读取CSV/统计 15 渲染表格/图表/导出/分享 16 用户 Web前端 路由层(routes) 评测引擎(engine) 模型工厂(models) 裁判模型 数据库/文件

系统组件图:
前端/模板 templates 路由 routes/* 评测引擎 utils/evaluation_engine.py 任务管理 utils/task_manager.py 模型工厂 models/*_client + factory 数据库 database.py / database/* 服务 services/model_api_service.py 静态资源 static/* 结果文件 results/*.csv

以上两张图可直接在 Markdown 渲染(Mermaid 支持)或导出为图片,用于报告展示。

JSON 协议与"强约束+容错兜底"

裁判模型输出 JSON,看似简单,实际是一个"强约束+容错兜底"的工程问题:

  • 强约束:提示词中明确"仅输出 JSON,不要任何说明、不要 markdown 代码块";
  • 解析严格:结果进入引擎后,必须能被解析到各模型的评分/理由/准确性字段;
  • 容错兜底:对非标输出生成"最小可用结构",保证不会阻塞整批任务;
  • 可追溯:异常输出与兜底发生率可被记录,用于后续评估裁判模型的稳定性。

这套策略让我们在"模型偶尔不听话"的现实世界里,依然能把任务跑完,并把问题点留痕。

数据长期主义:软删除、历史、可见性与分享

"数据是资产"的一个直接含义是:你不应该因为一次误删、一次口径变化,就丢失评测历史。于是我们:

  • 对关键实体采用软删除(deleted_at),默认查询过滤;
  • 结果历史持久化,记录模型列表、时间窗口、题量与文件大小;
  • 可见性与分享:支持私有/团队/公开的权限策略,分享链接(后续支持密码和有效期);
  • 审计与回溯:谁创建、谁查看、谁导出,逐步纳入日志与审计。

这些设计让"半年后能复现当初的判断"不再是一句口号。

管理后台:把"口径与密钥"变成"系统配置"

我们在后台提供了两类关键能力:

  • API Key 管理:通过页面保存到 .env,并集成对常见服务商 Key 的有效性校验;
  • 提示词管理:文件级自定义与系统默认,成为"第一公民"。

它们的共同点是:把口径与密钥从"某个人的电脑/某个脚本"里,移到"系统配置"里,让协作真正发生。

我们学到的:原则与反模式

做完这个系统之后,我们总结了几条"原则",也踩过一些"反模式"。

原则

  1. 先定义目标与指标,再决定怎么跑。避免"跑完才想指标"。
  2. 评分口径是第一公民,必须可配置、可追溯、可审计。
  3. JSON 是硬约束,容错是兜底,不应以容错代替约束。
  4. 并发有边界,失败可恢复,进度可观测。
  5. 数据长期主义:历史、软删除、可见性与分享,从第一天就要有。
  6. 工程可解释:日志与链路让每一次失败都有"可以复盘的证据"。

反模式

  1. 让提示词藏在代码里:这会让口径不可讨论、不可版本化。
  2. 结果结构随意变:导出与可视化会变得脆弱,历史也难以对齐。
  3. 并发"拉满":限流与失败率会用血的事实告诉你什么叫"不可控"。
  4. 进度不可见:用户会误以为"系统卡住了"。
  5. 单纯堆功能:没有"系统能力"的治理,功能越多越难用。

建议截图位(用于本篇与后续报告)

  • 首页:上传区域、模型选择、裁判模型、评测模式、开始按钮。
  • 任务状态:发起后查看 /task_status/<task_id> 的进度、总题数、当前步骤、耗时。
  • 结果页(/results/<result_id>/view_results/<filename>):表格列(答案/评分/理由/准确性)、统计图表、导出按钮。
  • 历史/分享:历史列表与分享入口,展示"评测如何沉淀为资产"。
  • 后台配置:API Key 与提示词管理界面,说明"口径系统化"。

每一张图都不只是"好看",它们共同讲述一个故事:从"人来背口径",到"系统固化口径";从"脚本跑起来",到"平台跑得稳"。

截图拍摄建议与命名规范

  • 截图命名:blogs/images/01-<模块>-<要点>.png,例如 blogs/images/01-home-upload.png
  • 主页上传区:包含文件选择、模型多选、裁判模型、模式切换、开始按钮
  • 任务状态:展示 task_id、进度条、当前步骤、耗时、已完成题数
  • 结果页:展示表头中"_答案/_评分/_理由/(客观题)_准确性"列
  • 历史列表:展示最近一次结果、创建者、时间与标签
  • 后台配置:展示 Key 输入、保存成功提示、已配置列表(隐藏中段)

结语:系统不是终点,是团队协作方式的起点

我们做这套系统的初衷,不是"省时间",而是"减少不可控"。当评测这件事被系统化之后,我们可以对需求说"半天给你结果",而不是"让我看看有没有空"。系统把"做事"变成"做法",把"结果"变成"资产",把"经验"变成"流程"。

在本系列接下来的文章里,我会继续展开"目标与指标""架构落地""数据与迁移""模型适配与接入""评测引擎""管理后台""可视化与分享""可观测性与稳定性""上线与演进""报告自动化""生态与 API"等主题。你会看到每一个设计,如何在代码里落地,又如何在页面上回应真实的协作需求。

相关推荐
小趴菜82275 小时前
安卓接入Kwai广告源
android·kotlin
2501_916013745 小时前
iOS 混淆与 App Store 审核兼容性 避免被拒的策略与实战流程(iOS 混淆、ipa 加固、上架合规)
android·ios·小程序·https·uni-app·iphone·webview
程序员江同学6 小时前
Kotlin 技术月报 | 2025 年 9 月
android·kotlin
码农的小菜园7 小时前
探究ContentProvider(一)
android
时光少年8 小时前
Compose AnnotatedString实现Html样式解析
android·前端
hnlgzb9 小时前
安卓中,kotlin如何写app界面?
android·开发语言·kotlin
jzlhll12310 小时前
deepseek kotlin flow快生产者和慢消费者解决策略
android·kotlin
火柴就是我10 小时前
Android 事件分发之动态的决定某个View来处理事件
android
一直向钱10 小时前
FileProvider 配置必须针对 Android 7.0+(API 24+)做兼容
android
zh_xuan10 小时前
Android 消息循环机制
android