我至今还记得那天晚上,回望墙上的白板,密密麻麻写着 “数据集、口径、阈值、报告、对比、回归” 几个词。我们不是第一次做模型其实仅仅是模型,而是负责的对外 api 的评测,但我们第一次直面一个现实:靠手工和临时脚本,已经无法再支撑 “频繁评测、多人协作、对外汇报” 的需求了。
如果这件事注定会越来越频繁、越来越复杂,为什么不把它变成一条可靠、可复用、可回溯、可协作的流水线?
这就是本文要讲的系统的来历。它不是一个脚本集合,而是一套 “评测平台”:一处接入、多模型协同、统一评分口径、端到端追踪、结构化历史、可视化报告、权限与分享。如今它已经跑在我们的机器上,它每天在替我们做一件本应自动化的工作:让评测这件事,成为 “被系统化管理的工程实践”。
这篇文章是系列第一篇,讲清 “为什么要做” 与 “我们到底做了什么”。后续文章会逐步深入架构、数据、引擎、模型接入、后台、可观测性、上线与生态等方面。
先承认一个事实:手工测试在早期 “够用”。它有三个优点:
但手工测试的上限也很明显:
于是我们踏入了脚本化阶段。脚本当然是进步:
但脚本也会快速陷入 “脚本地狱”:
总之,脚本可以 “让事情跑起来”,但它很难 “让事情跑得稳,还能讲清楚”。
当我们决定 “做系统” 时,我们其实是在回答四个问题:
1) 怎样让 “评测流程” 成为一个可复用的 “流水线”?
2) 怎样让 “评分口径” 成为被系统所治理的 “第一公民”?
3) 怎样让 “结果” 天然可视化、可导出、可分享与可追溯?
4) 怎样让 “并发、限流、重试、幂等” 这些工程问题被妥善治理?
对应到能力清单,就是:
系统采用 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) 上传数据集与配置评测:
query
,客观题需要 query
+ answer
;2) 并发获取被测模型答案:
get_multiple_model_answers
并发调用多个外部模型;3) 裁判模型统一打分(严格 JSON 协议):
build_subjective_eval_prompt
与 build_objective_eval_prompt
构建提示词;4) 结果落库与可视化:
results/
,列结构稳定(序号、类型、query、标准答案 (客观题)、每模型的答案/评分/理由/准确性 (客观题));/results/<result_id>
与 /view_results/<filename>
自动加载 CSV 与统计数据,支持筛选、导出与分享。5) 导出与分享:
应用入口与端口:
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,看似简单,实际是一个 “强约束 + 容错兜底” 的工程问题:
这套策略让我们在 “模型偶尔不听话” 的现实世界里,依然能把任务跑完,并把问题点留痕。
“数据是资产” 的一个直接含义是:你不应该因为一次误删、一次口径变化,就丢失评测历史。于是我们:
deleted_at
),默认查询过滤;这些设计让 “半年后能复现当初的判断” 不再是一句口号。
我们在后台提供了两类关键能力:
.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” 等主题。你会看到每一个设计,如何在代码里落地,又如何在页面上回应真实的协作需求。