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"等主题。你会看到每一个设计,如何在代码里落地,又如何在页面上回应真实的协作需求。

相关推荐
2501_915921433 小时前
小团队如何高效完成 uni-app iOS 上架,从分工到工具组合的实战经验
android·ios·小程序·uni-app·cocoa·iphone·webview
幂简集成4 小时前
通义灵码 AI 程序员低代码 API 课程实战教程
android·人工智能·深度学习·神经网络·低代码·rxjava
2501_916008895 小时前
uni-app iOS 文件管理与 itools 配合实战,多工具协作的完整流程
android·ios·小程序·https·uni-app·iphone·webview
Android-Flutter5 小时前
kotlin - 2个Fragment实现左右显示,左边列表,右边详情,平板横、竖屏切换
android·kotlin
charlie1145141916 小时前
Android开发——初步了解AndroidManifest.xml
android·xml·开发语言·学习·安卓·安全架构
黎宇幻生6 小时前
Java全栈学习笔记30
android·adb
一个尚在学习的计算机小白6 小时前
spring
android·java·spring
时光少年6 小时前
Android 喷雾效果实现
android·前端
tangweiguo030519877 小时前
Android应用完全重启指南:从任务重置到进程重生
android