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

andyguo · 2025年10月03日 · 33 次阅读

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

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

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

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

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

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

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

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

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

  • 成本高且难复用:同一套题,不同人不同天做,口径不一致,记录也难以沉淀;
  • 报告负担大:散落在表格和截图里的信息,需要人工再拼成 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
  • 可选择多个 “被测模型”,并指定一个 “裁判模型”;
  • 模式支持自动/主观/客观(自动模式会根据列结构智能判断)。

2) 并发获取被测模型答案:

  • 通过 get_multiple_model_answers 并发调用多个外部模型;
  • 信号量限制并发,避免击穿限流;
  • 错误与超时有兜底与重试策略(后续将持续增强)。

3) 裁判模型统一打分(严格 JSON 协议):

  • 主观题与客观题分别使用 build_subjective_eval_promptbuild_objective_eval_prompt 构建提示词;
  • 提示词获取优先级:文件级自定义 > 系统默认;缺失时抛出明确错误;
  • 强制裁判模型输出严格 JSON,不允许前后附加说明或 markdown 代码块;
  • 若解析失败,使用最小可用结构兜底,保证整批任务不会被 “坏样本” 卡死。

4) 结果落库与可视化:

  • 结果以 CSV 形式写入 results/,列结构稳定(序号、类型、query、标准答案 (客观题)、每模型的答案/评分/理由/准确性 (客观题));
  • 历史记录写入数据库,包含开始/结束时间、题量、文件大小、模型列表与标签;
  • 页面 /results/<result_id>/view_results/<filename> 自动加载 CSV 与统计数据,支持筛选、导出与分享。

5) 导出与分享:

  • 支持 CSV/Excel 导出,便于汇报或二次分析;
  • 分享链接与可见性策略在数据库中管理,后续支持密码与有效期。

关键代码锚点

应用入口与端口:

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)

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

@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 映射:

# 创建并发任务来评测所有问题,添加实时进度更新
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("准确性", ""))

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

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(...)

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

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

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

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

裁判模型输出 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 与提示词管理界面,说明 “口径系统化”。

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

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

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

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

暫無回覆。
需要 登录 後方可回應,如果你還沒有帳號按這裡 注册