前言:从“能跑”到“跑得稳”,再到“让结果说话”
我至今还记得那天晚上,回望墙上的白板,密密麻麻写着“数据集、口径、阈值、报告、对比、回归”几个词。我们不是第一次做模型评测,但我们第一次直面一个现实:靠手工和临时脚本,已经无法再支撑“频繁评测、多人协作、对外汇报”的需求了。
如果这件事注定会越来越频繁、越来越复杂,为什么不把它变成一条可靠、可复用、可回溯、可协作的流水线?
这就是本文要讲的系统的来历。它不是一个脚本集合,而是一套“评测平台”:一处接入、多模型协同、统一评分口径、端到端追踪、结构化历史、可视化报告、权限与分享。如今它已经跑在我们的机器上,它每天在替我们做一件本应自动化的工作:让评测这件事,成为“被系统化管理的工程实践”。
这篇文章是系列第一篇,讲清“为什么要做”与“我们到底做了什么”。后续文章会逐步深入架构、数据、引擎、模型接入、后台、可观测性、上线与生态等方面。
我们曾经是怎么做的:手工测试与“脚本地狱”
先承认一个事实:手工测试在早期“够用”。它有三个优点:
- 上手快:临时想法立刻验证。
- 反馈直观:人能看见模型“说了啥”。
- 协作简单:两个人讨论就能对齐。
但手工测试的上限也很明显:
- 成本高且难复用:同一套题,不同人不同天做,口径不一致,记录也难以沉淀;
- 报告负担大:散落在表格和截图里的信息,需要人工再拼成 PPT;
- 不可追溯:你很难在半年后重现“当时为什么判 A 比 B 好”。
于是我们踏入了脚本化阶段。脚本当然是进步:
- 批量处理:能一次性跑大量题;
- 自动记录:输出 CSV/Excel,便于后续分析;
- 易扩展:可以接入更多模型、添加更多指标。
但脚本也会快速陷入“脚本地狱”:
- 多版本分叉:不同人维护不同脚本,参数与输出格式各不相同;
- 逻辑拷贝膨胀:临时兼容逐步堆积,异常处理难以沉淀为“系统规则”;
- 口径不透明:评分口径藏在代码里、commit 里,协作讨论成本高;
- 对外不可用:非工程同事不愿(也没时间)安装环境或读命令帮助。
总之,脚本可以“让事情跑起来”,但它很难“让事情跑得稳,还能讲清楚”。
我们想要的是什么:一处接入、统一口径、可回溯与可协作
当我们决定“做系统”时,我们其实是在回答四个问题:
- 怎样让“评测流程”成为一个可复用的“流水线”?
- 怎样让“评分口径”成为被系统所治理的“第一公民”?
- 怎样让“结果”天然可视化、可导出、可分享与可追溯?
- 怎样让“并发、限流、重试、幂等”这些工程问题被妥善治理?
对应到能力清单,就是:
- 统一模型接入层:不同模型的认证、限流、返回格式各不同,但系统只暴露统一接口;
- 一致评分协议:无论客观题还是主观题,评分路径清晰、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/
。
端到端流程(上传→获取答案→裁判打分→结果落库→可视化→导出/分享)
- 上传数据集与配置评测:
- 支持 CSV/Excel,主观题至少包含
query
,客观题需要query
+answer
; - 可选择多个“被测模型”,并指定一个“裁判模型”;
- 模式支持自动/主观/客观(自动模式会根据列结构智能判断)。
- 并发获取被测模型答案:
- 通过
get_multiple_model_answers
并发调用多个外部模型; - 信号量限制并发,避免击穿限流;
- 错误与超时有兜底与重试策略(后续将持续增强)。
- 裁判模型统一打分(严格 JSON 协议):
- 主观题与客观题分别使用
build_subjective_eval_prompt
与build_objective_eval_prompt
构建提示词; - 提示词获取优先级:文件级自定义 > 系统默认;缺失时抛出明确错误;
- 强制裁判模型输出严格 JSON,不允许前后附加说明或 markdown 代码块;
- 若解析失败,使用最小可用结构兜底,保证整批任务不会被“坏样本”卡死。
- 结果落库与可视化:
- 结果以 CSV 形式写入
results/
,列结构稳定(序号、类型、query、标准答案(客观题)、每模型的答案/评分/理由/准确性(客观题)); - 历史记录写入数据库,包含开始/结束时间、题量、文件大小、模型列表与标签;
- 页面
/results/<result_id>
与/view_results/<filename>
自动加载 CSV 与统计数据,支持筛选、导出与分享。
- 导出与分享:
- 支持 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(...)
并发、限流、退避与幂等:工程问题的“地基”
在评测系统里,并发不是“越高越好”,而是“可控、可解释、可稳定”。我们采用以下策略:
- 信号量限制:集中控制并发上限,避免瞬时洪峰击穿第三方限流;
- 分阶段并发:被测模型答案获取与裁判评分分两个阶段,避免耦合导致问题放大;
- 失败可恢复:解析失败与异常都有兜底策略,保证任务整体推进;
- 进度可观测:每题完成时更新内存与数据库,页面实时可见。
这四点听起来朴素,但是真正让系统“跑得稳”的关键。
可视化蓝图(Mermaid 时序/组件图)
评测主流程(时序图):
系统组件图:
以上两张图可直接在 Markdown 渲染(Mermaid 支持)或导出为图片,用于报告展示。
JSON 协议与“强约束+容错兜底”
裁判模型输出 JSON,看似简单,实际是一个“强约束+容错兜底”的工程问题:
- 强约束:提示词中明确“仅输出 JSON,不要任何说明、不要 markdown 代码块”;
- 解析严格:结果进入引擎后,必须能被解析到各模型的评分/理由/准确性字段;
- 容错兜底:对非标输出生成“最小可用结构”,保证不会阻塞整批任务;
- 可追溯:异常输出与兜底发生率可被记录,用于后续评估裁判模型的稳定性。
这套策略让我们在“模型偶尔不听话”的现实世界里,依然能把任务跑完,并把问题点留痕。
数据长期主义:软删除、历史、可见性与分享
“数据是资产”的一个直接含义是:你不应该因为一次误删、一次口径变化,就丢失评测历史。于是我们:
- 对关键实体采用软删除(
deleted_at
),默认查询过滤; - 结果历史持久化,记录模型列表、时间窗口、题量与文件大小;
- 可见性与分享:支持私有/团队/公开的权限策略,分享链接(后续支持密码和有效期);
- 审计与回溯:谁创建、谁查看、谁导出,逐步纳入日志与审计。
这些设计让“半年后能复现当初的判断”不再是一句口号。
管理后台:把“口径与密钥”变成“系统配置”
我们在后台提供了两类关键能力:
- API Key 管理:通过页面保存到
.env
,并集成对常见服务商 Key 的有效性校验; - 提示词管理:文件级自定义与系统默认,成为“第一公民”。
它们的共同点是:把口径与密钥从“某个人的电脑/某个脚本”里,移到“系统配置”里,让协作真正发生。
我们学到的:原则与反模式
做完这个系统之后,我们总结了几条“原则”,也踩过一些“反模式”。
原则
- 先定义目标与指标,再决定怎么跑。避免“跑完才想指标”。
- 评分口径是第一公民,必须可配置、可追溯、可审计。
- JSON 是硬约束,容错是兜底,不应以容错代替约束。
- 并发有边界,失败可恢复,进度可观测。
- 数据长期主义:历史、软删除、可见性与分享,从第一天就要有。
- 工程可解释:日志与链路让每一次失败都有“可以复盘的证据”。
反模式
- 让提示词藏在代码里:这会让口径不可讨论、不可版本化。
- 结果结构随意变:导出与可视化会变得脆弱,历史也难以对齐。
- 并发“拉满”:限流与失败率会用血的事实告诉你什么叫“不可控”。
- 进度不可见:用户会误以为“系统卡住了”。
- 单纯堆功能:没有“系统能力”的治理,功能越多越难用。
建议截图位(用于本篇与后续报告)
- 首页:上传区域、模型选择、裁判模型、评测模式、开始按钮。
- 任务状态:发起后查看
/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”等主题。你会看到每一个设计,如何在代码里落地,又如何在页面上回应真实的协作需求。