529 lines
23 KiB
Python
529 lines
23 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""模型评测网站 - Flask 主应用
|
||
功能:
|
||
1. 聚合展示各种模型的运行速度(解码/预填充/首字延迟 排行 + 图表)
|
||
2. 模型对比(支持硬件显示 + 对比项可去留)
|
||
3. 模型能力测试模块(内容由后台管理编辑)
|
||
4. 账号体系:每个提交挂到对应账号下
|
||
5. POST /api/submit 接收 llm-speed-tester 一键发送的测试结果
|
||
6. 后台管理(/admin.html):提交/账号/能力测试/硬件 管理
|
||
"""
|
||
import io
|
||
from functools import wraps
|
||
|
||
import requests
|
||
from flask import Flask, jsonify, request, send_file, send_from_directory, session
|
||
|
||
import config
|
||
import database as db
|
||
|
||
app = Flask(__name__, static_folder="static", static_url_path="")
|
||
app.json.ensure_ascii = False
|
||
app.secret_key = config.SESSION_SECRET
|
||
|
||
db.init_db()
|
||
|
||
|
||
@app.after_request
|
||
def _cors(resp):
|
||
resp.headers["Access-Control-Allow-Origin"] = "*"
|
||
resp.headers["Access-Control-Allow-Headers"] = "Content-Type, X-Token"
|
||
resp.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
|
||
return resp
|
||
|
||
|
||
def admin_required(f):
|
||
@wraps(f)
|
||
def wrapper(*a, **kw):
|
||
if not session.get("admin"):
|
||
return jsonify({"ok": False, "error": "未登录或会话过期,请先登录后台"}), 401
|
||
return f(*a, **kw)
|
||
return wrapper
|
||
|
||
|
||
@app.route("/")
|
||
def index():
|
||
return send_from_directory(app.static_folder, "index.html")
|
||
|
||
|
||
@app.route("/health")
|
||
@app.route("/api/health")
|
||
def health():
|
||
return jsonify({"ok": True, "port": config.PORT, "service": "model-eval-site",
|
||
"stats": db.get_stats()})
|
||
|
||
|
||
# ───────────────────────── 后台登录 ─────────────────────────
|
||
|
||
@app.route("/api/admin/login", methods=["POST"])
|
||
def admin_login():
|
||
body = request.get_json(force=True) or {}
|
||
if body.get("username") == config.ADMIN_USER and body.get("password") == config.ADMIN_PASSWORD:
|
||
session["admin"] = True
|
||
return jsonify({"ok": True})
|
||
return jsonify({"ok": False, "error": "账号或密码错误"}), 401
|
||
|
||
|
||
@app.route("/api/admin/logout", methods=["POST"])
|
||
def admin_logout():
|
||
session.clear()
|
||
return jsonify({"ok": True})
|
||
|
||
|
||
@app.route("/api/admin/session")
|
||
def admin_session():
|
||
return jsonify({"ok": True, "admin": bool(session.get("admin"))})
|
||
|
||
|
||
# ───────────────────────── 接收 llm-speed-tester 提交 ─────────────────────────
|
||
|
||
@app.route("/api/submit", methods=["POST"])
|
||
def submit():
|
||
"""接收速度测试结果,挂到对应账号下(账号不存在自动创建)"""
|
||
body = request.get_json(force=True) or {}
|
||
token = request.headers.get("X-Token") or body.get("token") or ""
|
||
if token != config.SUBMIT_TOKEN:
|
||
return jsonify({"ok": False, "error": "提交密钥错误"}), 403
|
||
model = (body.get("model") or "").strip()
|
||
if not model:
|
||
return jsonify({"ok": False, "error": "缺少模型名称"}), 400
|
||
summary = body.get("summary") or {}
|
||
if not (summary.get("samples_ok") or 0):
|
||
return jsonify({"ok": False, "error": "该测试没有成功采样数据,无法发布到评测站"}), 400
|
||
|
||
account_name = (body.get("account") or "").strip() or "默认账号"
|
||
aid = db.get_or_create_account(account_name, remark="来自 llm-speed-tester")
|
||
sid = db.add_submission(aid, body)
|
||
account = db.list_accounts()
|
||
acc = next((a for a in account if a["id"] == aid), None)
|
||
return jsonify({
|
||
"ok": True, "id": sid, "account_id": aid,
|
||
"account": acc["name"] if acc else account_name,
|
||
"model": model,
|
||
"url": "/model.html?provider=%s&model=%s" % (
|
||
requests.utils.quote(body.get("provider") or ""),
|
||
requests.utils.quote(model)),
|
||
})
|
||
|
||
|
||
# ───────────────────────── 汇总 / 排行 ─────────────────────────
|
||
|
||
@app.route("/api/stats")
|
||
def stats():
|
||
return jsonify(db.get_stats())
|
||
|
||
|
||
@app.route("/api/leaderboard")
|
||
def leaderboard():
|
||
sort = request.args.get("sort", "avg_decode_speed")
|
||
order = request.args.get("order", "desc")
|
||
limit = min(int(request.args.get("limit", 200) or 200), 500)
|
||
rows = db.leaderboard(sort=sort, order=order, limit=limit)
|
||
|
||
# 可选:指定对比哪些模型(provider|model 或 仅 model,逗号分隔)
|
||
sel = request.args.get("models", "")
|
||
if sel:
|
||
keys = set()
|
||
for item in sel.split(","):
|
||
item = item.strip()
|
||
if not item:
|
||
continue
|
||
if "|" in item:
|
||
p, m = item.split("|", 1)
|
||
keys.add((p.strip(), m.strip()))
|
||
else:
|
||
keys.add(("", item))
|
||
rows = [r for r in rows if (r["provider"], r["model"]) in keys]
|
||
|
||
# 按排行榜生成柱状图 CSV(解码速度 TOP,默认全部模型,可指定)
|
||
chart_rows = rows[: int(request.args.get("chart_top", 20) or 20)]
|
||
csv_lines = ["模型, 解码速度(tok/s), 预填充速度(tok/s), 首字延迟(ms)"]
|
||
for r in chart_rows:
|
||
label = "%s %s" % (r["provider"], r["model"])
|
||
csv_lines.append("%s, %s, %s, %s" % (
|
||
label.replace(",", " "), _fmt(r["avg_decode_speed"]),
|
||
_fmt(r["avg_prefill_speed"]), _fmt(r["avg_ttft_ms"])))
|
||
chart_csv = "\n".join(csv_lines)
|
||
bar_payload = {
|
||
"data": chart_csv, "chartType": "bar",
|
||
"title": "模型解码速度对比(%d 个模型)" % len(chart_rows),
|
||
"theme": "default", "showLegend": True, "showGrid": True, "showLabel": True,
|
||
"seriesTypes": ["bar", "bar", "line"],
|
||
"seriesAxis": [0, 0, 1],
|
||
"seriesStyles": ["solid", "hollow", "dashed"],
|
||
"width": 1100, "height": 560, "pixelRatio": 2,
|
||
}
|
||
return jsonify({"ok": True, "rows": rows, "chart_csv": chart_csv,
|
||
"bar_payload": bar_payload})
|
||
|
||
|
||
# ───────────────────────── 模型对比(硬件 + 可去留对比项) ─────────────────────────
|
||
|
||
@app.route("/api/compare")
|
||
def compare():
|
||
"""返回指定模型(默认全部)的聚合对比行 + 图表数据。
|
||
models: 逗号分隔 provider|model;chart_metric: 解码/预填充/首字;chart_type: bar/line
|
||
"""
|
||
sel = request.args.get("models", "").strip()
|
||
keys = []
|
||
if sel:
|
||
for item in sel.split(","):
|
||
item = item.strip()
|
||
if not item:
|
||
continue
|
||
if "|" in item:
|
||
p, m = item.split("|", 1)
|
||
keys.append((p.strip(), m.strip()))
|
||
else:
|
||
keys.append(("", item))
|
||
else:
|
||
lb = db.leaderboard(limit=500)
|
||
keys = [(r["provider"], r["model"]) for r in lb]
|
||
rows = db.compare_models(keys)
|
||
# 有硬件优先展示
|
||
rows.sort(key=lambda r: (r.get("avg_decode_speed") or 0), reverse=True)
|
||
|
||
# 图表数据(按所选指标)
|
||
metric = request.args.get("chart_metric", "avg_decode_speed")
|
||
ctype = request.args.get("chart_type", "bar")
|
||
metric_name = {"avg_decode_speed": "解码速度", "avg_prefill_speed": "预填充速度",
|
||
"avg_ttft_ms": "首字延迟", "best_decode": "最佳解码"}.get(metric, "解码速度")
|
||
unit = "tok/s" if metric in ("avg_decode_speed", "avg_prefill_speed", "best_decode") else "ms"
|
||
chart_rows = rows[: int(request.args.get("chart_top", 50) or 50)]
|
||
csv_lines = ["模型, %s(%s)" % (metric_name, unit)]
|
||
for r in chart_rows:
|
||
label = "%s %s" % (r["provider"], r["model"])
|
||
csv_lines.append("%s, %s" % (label.replace(",", " "), _fmt(r.get(metric))))
|
||
chart_csv = "\n".join(csv_lines)
|
||
chart_payload = {
|
||
"data": chart_csv, "chartType": "bar" if ctype != "line" else "line",
|
||
"title": "模型%s对比(%d 个模型)" % (metric_name, len(chart_rows)),
|
||
"theme": "default", "showLegend": False, "showGrid": True, "showLabel": True,
|
||
"smoothLine": True,
|
||
"seriesTypes": ["bar"] if ctype != "line" else ["line"],
|
||
"width": 1100, "height": 520, "pixelRatio": 2,
|
||
}
|
||
return jsonify({"ok": True, "rows": rows, "chart_csv": chart_csv,
|
||
"chart_payload": chart_payload, "metric": metric})
|
||
|
||
|
||
@app.route("/api/model")
|
||
def model_detail():
|
||
provider = request.args.get("provider", "")
|
||
model = request.args.get("model", "")
|
||
if not model:
|
||
return jsonify({"ok": False, "error": "缺少模型名称"}), 400
|
||
d = db.get_model_detail(provider, model)
|
||
if not d["submissions"]:
|
||
return jsonify({"ok": False, "error": "该模型暂无评测数据"}), 404
|
||
csv_lines = ["上下文长度(tok), 解码速度(tok/s), 预填充速度(tok/s)"]
|
||
for r in d["by_length"]:
|
||
csv_lines.append("%d, %s, %s" % (r["length"], r["avg_decode_speed"], r["avg_prefill_speed"]))
|
||
line_csv = "\n".join(csv_lines)
|
||
line_payload = {
|
||
"data": line_csv, "chartType": "line",
|
||
"title": "%s %s · 解码速度随上下文长度变化" % (provider or "", model),
|
||
"theme": "default", "showLegend": True, "showGrid": True, "showLabel": False,
|
||
"smoothLine": True, "dualYAxis": True,
|
||
"leftAxisName": "预填充速度(tok/s)", "rightAxisName": "解码速度(tok/s)",
|
||
"seriesTypes": ["line", "line"], "seriesAxis": [0, 1],
|
||
"seriesStyles": ["dashed", "solid"],
|
||
"width": 1000, "height": 520, "pixelRatio": 2,
|
||
}
|
||
d["line_csv"] = line_csv
|
||
d["line_payload"] = line_payload
|
||
return jsonify({"ok": True, **d})
|
||
|
||
|
||
# ───────────────────────── 提交(前台只读,管理走后端) ─────────────────────────
|
||
|
||
@app.route("/api/submissions")
|
||
def submissions():
|
||
page = max(1, int(request.args.get("page", 1)))
|
||
page_size = min(max(1, int(request.args.get("page_size", 20))), 100)
|
||
q = request.args.get("q", "")
|
||
account_id = request.args.get("account_id") or None
|
||
model = request.args.get("model", "")
|
||
provider = request.args.get("provider", "")
|
||
return jsonify(db.list_submissions(page=page, page_size=page_size, q=q,
|
||
account_id=int(account_id) if account_id else None,
|
||
model=model, provider=provider))
|
||
|
||
|
||
@app.route("/api/submissions/<int:sid>")
|
||
def submission_detail(sid):
|
||
s = db.get_submission(sid)
|
||
if not s:
|
||
return jsonify({"ok": False, "error": "提交不存在"}), 404
|
||
return jsonify(s)
|
||
|
||
|
||
@app.route("/api/submissions/<int:sid>", methods=["DELETE"])
|
||
@admin_required
|
||
def submission_delete(sid):
|
||
db.delete_submission(sid)
|
||
return jsonify({"ok": True})
|
||
|
||
|
||
@app.route("/api/submissions/<int:sid>/hardware", methods=["PUT"])
|
||
@admin_required
|
||
def submission_hardware(sid):
|
||
body = request.get_json(force=True) or {}
|
||
db.update_submission_hardware(sid, body.get("hardware", ""))
|
||
return jsonify({"ok": True})
|
||
|
||
|
||
@app.route("/api/submissions/<int:sid>/chart")
|
||
def submission_chart(sid):
|
||
"""单条提交:上下文长度 → 解码/预填充速度 折线图"""
|
||
s = db.get_submission(sid)
|
||
if not s:
|
||
return jsonify({"ok": False, "error": "提交不存在"}), 404
|
||
by = s["by_length"]
|
||
csv_lines = ["上下文长度(tok), 预填充速度(tok/s), 解码速度(tok/s)"]
|
||
for L in sorted(by, key=int):
|
||
bl = by[L]
|
||
pre = bl.get("avg_prefill_speed")
|
||
dec = bl.get("avg_decode_speed")
|
||
if pre is None or dec is None:
|
||
continue
|
||
csv_lines.append("%s, %.2f, %.2f" % (L, pre, dec))
|
||
if len(csv_lines) < 2:
|
||
return jsonify({"ok": False, "error": "该提交无可画图的长度分组数据"}), 400
|
||
payload = {
|
||
"data": "\n".join(csv_lines), "chartType": "line",
|
||
"title": "%s %s · 速度随上下文长度(提交#%d)" % (s["provider"], s["model"], sid),
|
||
"theme": "default", "showLegend": True, "showGrid": True, "showLabel": False,
|
||
"smoothLine": True, "dualYAxis": True,
|
||
"leftAxisName": "预填充速度(tok/s)", "rightAxisName": "解码速度(tok/s)",
|
||
"seriesTypes": ["line", "line"], "seriesAxis": [0, 1],
|
||
"seriesStyles": ["dashed", "solid"],
|
||
"width": 1000, "height": 480, "pixelRatio": 2,
|
||
}
|
||
return _chart_proxy(payload)
|
||
|
||
|
||
# ───────────────────────── 账号(后台管理) ─────────────────────────
|
||
|
||
@app.route("/api/accounts")
|
||
@admin_required
|
||
def accounts():
|
||
return jsonify(db.list_accounts())
|
||
|
||
|
||
@app.route("/api/accounts", methods=["POST"])
|
||
@admin_required
|
||
def account_add():
|
||
body = request.get_json(force=True) or {}
|
||
try:
|
||
aid = db.add_account(body.get("name", ""), body.get("remark", ""))
|
||
return jsonify({"ok": True, "id": aid})
|
||
except ValueError as e:
|
||
return jsonify({"ok": False, "error": str(e)}), 400
|
||
|
||
|
||
@app.route("/api/accounts/<int:aid>", methods=["PUT"])
|
||
@admin_required
|
||
def account_update(aid):
|
||
body = request.get_json(force=True) or {}
|
||
db.rename_account(aid, body.get("name", ""), body.get("remark", ""))
|
||
return jsonify({"ok": True})
|
||
|
||
|
||
@app.route("/api/accounts/<int:aid>", methods=["DELETE"])
|
||
@admin_required
|
||
def account_delete(aid):
|
||
db.delete_account(aid)
|
||
return jsonify({"ok": True})
|
||
|
||
|
||
# ───────────────────────── 模型能力测试模块(前台只读,内容后台编辑) ─────────────────────────
|
||
|
||
@app.route("/api/capabilities")
|
||
def capabilities_list():
|
||
return jsonify(db.list_capabilities(status_only=True))
|
||
|
||
|
||
@app.route("/api/admin/capabilities")
|
||
@admin_required
|
||
def admin_capabilities_list():
|
||
return jsonify(db.list_capabilities(status_only=False))
|
||
|
||
|
||
@app.route("/api/admin/capabilities", methods=["POST"])
|
||
@admin_required
|
||
def admin_capability_add():
|
||
body = request.get_json(force=True) or {}
|
||
if not (body.get("name") or "").strip():
|
||
return jsonify({"ok": False, "error": "请填写能力测试名称"}), 400
|
||
cid = db.add_capability(body)
|
||
return jsonify({"ok": True, "id": cid})
|
||
|
||
|
||
@app.route("/api/admin/capabilities/<int:cid>", methods=["PUT"])
|
||
@admin_required
|
||
def admin_capability_update(cid):
|
||
body = request.get_json(force=True) or {}
|
||
if not (body.get("name") or "").strip():
|
||
return jsonify({"ok": False, "error": "请填写能力测试名称"}), 400
|
||
db.update_capability(cid, body)
|
||
return jsonify({"ok": True})
|
||
|
||
|
||
@app.route("/api/admin/capabilities/<int:cid>", methods=["DELETE"])
|
||
@admin_required
|
||
def admin_capability_delete(cid):
|
||
db.delete_capability(cid)
|
||
return jsonify({"ok": True})
|
||
|
||
|
||
# ───────────────────────── 图表代理(data-chart-tool) ─────────────────────────
|
||
|
||
def _chart_proxy(payload):
|
||
try:
|
||
resp = requests.post(config.CHART_API_BASE + "/api/chart", json=payload, timeout=60)
|
||
except requests.RequestException as e:
|
||
return jsonify({"ok": False, "error": "图表服务不可用: %s" % e}), 502
|
||
if resp.status_code != 200:
|
||
return jsonify({"ok": False, "error": "图表生成失败(%d): %s" % (resp.status_code, resp.text[:300])}), 502
|
||
return send_file(io.BytesIO(resp.content), mimetype="image/png")
|
||
|
||
|
||
@app.route("/api/chart", methods=["POST"])
|
||
def chart_proxy():
|
||
payload = request.get_json(force=True) or {}
|
||
return _chart_proxy(payload)
|
||
|
||
|
||
# ───────────────────────── 演示数据(后台) ─────────────────────────
|
||
|
||
DEMO_MODELS = [
|
||
("autodl", "qwen3.5-plus", [512, 2048, 4096, 8192, 16384]),
|
||
("autodl", "glm-5.3-flash", [512, 2048, 4096, 8192]),
|
||
("siliconflow", "deepseek-v4-flash", [512, 2048, 4096, 8192, 16384, 32768]),
|
||
("siliconflow", "longcat", [512, 2048, 4096, 8192]),
|
||
("local-qwen", "qwen3.5-plus", [512, 2048, 4096, 8192, 16384]),
|
||
("openai", "gpt-4o-mini", [512, 2048, 4096, 8192, 16384, 32768]),
|
||
("anthropic", "claude-3.5-haiku", [512, 2048, 4096, 8192, 16384]),
|
||
("google", "gemini-2.0-flash", [512, 2048, 4096, 8192, 16384, 32768]),
|
||
]
|
||
|
||
|
||
@app.route("/api/seed-demo", methods=["POST"])
|
||
@admin_required
|
||
def seed_demo():
|
||
"""生成一批演示评测数据(重复调用会追加)"""
|
||
import random
|
||
random.seed()
|
||
accounts = ["测试小组A", "性能评测组", "模型研究所"]
|
||
gpus = ["NVIDIA A100 80G", "NVIDIA L20 48G", "NVIDIA RTX 4090 24G"]
|
||
cnt = 0
|
||
for ai, acc_name in enumerate(accounts):
|
||
aid = db.get_or_create_account(acc_name, remark="演示账号")
|
||
for mi, (provider, model, lens) in enumerate(DEMO_MODELS):
|
||
if (mi + ai) % 3 == 0 and ai != 0:
|
||
continue # 让数据分布有点差异
|
||
base_decode = 40 + mi * 12 + random.uniform(-5, 12)
|
||
base_prefill = base_decode * random.uniform(0.55, 0.9)
|
||
base_ttft = 200 + mi * 15 + random.uniform(-40, 80)
|
||
by_length = {}
|
||
for L in lens:
|
||
k = 1 - (L / 65536) * 0.3
|
||
by_length[str(L)] = {
|
||
"samples_ok": 2, "samples_total": 2,
|
||
"avg_decode_speed": round(base_decode * k, 1),
|
||
"avg_prefill_speed": round(base_prefill * k, 1),
|
||
"avg_ttft_ms": round(base_ttft + L * 0.02, 0),
|
||
"avg_prompt_tokens": int(L * 0.75), "avg_output_tokens": 128,
|
||
"avg_total_ms": round((L * 0.75 / base_prefill + 128 / base_decode) * 1000, 0),
|
||
}
|
||
speeds = [v["avg_decode_speed"] for v in by_length.values()]
|
||
payload = {
|
||
"token": config.SUBMIT_TOKEN,
|
||
"account": acc_name,
|
||
"source_test_id": 9000 + cnt, "source_site": "llm-speed-tester(演示)",
|
||
"provider": provider, "model": model,
|
||
"hardware": gpus[(mi + ai) % len(gpus)],
|
||
"test_name": "%s 基准评测" % model,
|
||
"summary": {
|
||
"samples_ok": 2, "samples_total": 2,
|
||
"avg_decode_speed": round(sum(speeds) / len(speeds), 1),
|
||
"avg_prefill_speed": round(sum(v["avg_prefill_speed"] for v in by_length.values()) / len(by_length), 1),
|
||
"avg_ttft_ms": round(sum(v["avg_ttft_ms"] for v in by_length.values()) / len(by_length), 0),
|
||
"avg_output_tokens": 128, "avg_total_ms": 3200,
|
||
"min_decode_speed": min(speeds), "max_decode_speed": max(speeds),
|
||
"concurrency_levels": [1],
|
||
"by_length": by_length, "by_concurrency": {},
|
||
},
|
||
"gen": {"context_lengths": lens, "max_tokens": 128,
|
||
"samples": 2, "concurrency_levels": [1]},
|
||
"runs": [],
|
||
}
|
||
db.add_submission(aid, payload)
|
||
cnt += 1
|
||
return jsonify({"ok": True, "seeded": cnt, "stats": db.get_stats()})
|
||
|
||
|
||
# ───────────────────────── 能力测试演示数据(后台) ─────────────────────────
|
||
|
||
@app.route("/api/seed-capabilities", methods=["POST"])
|
||
@admin_required
|
||
def seed_capabilities():
|
||
"""生成一批演示能力测试模块(便于展示)"""
|
||
demos = [
|
||
{"name": "数学推理", "category": "推理", "icon": "🧮",
|
||
"description": "考察基础运算、代数、逻辑推导与数学应用题能力。",
|
||
"items": [
|
||
{"title": "四则运算", "prompt": "计算 17 × 23 - 45 ÷ 5 的结果", "expect": "能正确按运算优先级给出结果"},
|
||
{"title": "一元一次方程", "prompt": "解方程 3x + 7 = 28", "expect": "给出 x=7 并附简要过程"},
|
||
{"title": "鸡兔同笼", "prompt": "笼中共35个头、94只脚,问鸡兔各几只", "expect": "正确列出方程并求解"},
|
||
]},
|
||
{"name": "代码生成", "category": "编程", "icon": "💻",
|
||
"description": "考察代码生成、算法实现、Bug 修复与代码理解。",
|
||
"items": [
|
||
{"title": "冒泡排序", "prompt": "用 Python 写一个冒泡排序", "expect": "代码正确、缩进规范"},
|
||
{"title": "反转链表", "prompt": "实现单链表反转函数", "expect": "时间复杂度 O(n)"},
|
||
{"title": "找 Bug", "prompt": "以下代码输出错误,请找出问题并修复:...", "expect": "准确定位并给出修复"},
|
||
]},
|
||
{"name": "中文理解", "category": "语言", "icon": "📖",
|
||
"description": "考察中文阅读理解、概括、润色与改写能力。",
|
||
"items": [
|
||
{"title": "段落概括", "prompt": "用一句话概括以下段落大意:...", "expect": "抓住主旨,表述通顺"},
|
||
{"title": "润色改写", "prompt": "把这段口语化的介绍改写成正式书面语", "expect": "用词准确、逻辑清晰"},
|
||
]},
|
||
{"name": "长文本处理", "category": "长文本", "icon": "📚",
|
||
"description": "考察超长上下文的记忆、定位与跨段归纳能力。",
|
||
"items": [
|
||
{"title": "长文定位", "prompt": "在给定的 8K 文档中找出第 3 段提到的日期", "expect": "准确命中原文内容"},
|
||
{"title": "跨段归纳", "prompt": "综合全文 5 个要点做总结", "expect": "要点无遗漏、顺序合理"},
|
||
]},
|
||
{"name": "多模态理解", "category": "多模态", "icon": "🖼️",
|
||
"description": "考察图片识别、OCR 与图文关联能力。",
|
||
"items": [
|
||
{"title": "OCR 识别", "prompt": "识别图片中的文字", "expect": "识别准确、无错漏"},
|
||
{"title": "图文问答", "prompt": "根据图片回答其中的物品/场景", "expect": "描述准确、贴合图片"},
|
||
]},
|
||
{"name": "对话与指令遵循", "category": "对话", "icon": "💬",
|
||
"description": "考察多轮对话、指令理解与约束遵循能力。",
|
||
"items": [
|
||
{"title": "指令遵循", "prompt": "请只回答‘是’或‘否’:1+1=2 对吗?", "expect": "严格遵守格式约束"},
|
||
{"title": "多轮记忆", "prompt": "记住我喜欢的颜色是蓝色,并在下一轮回答中引用", "expect": "跨轮记忆正确"},
|
||
]},
|
||
]
|
||
cnt = 0
|
||
for i, c in enumerate(demos):
|
||
db.add_capability({**c, "sort": i})
|
||
cnt += 1
|
||
return jsonify({"ok": True, "seeded": cnt, "stats": db.get_stats()})
|
||
|
||
|
||
def _fmt(v):
|
||
try:
|
||
return "%.2f" % float(v)
|
||
except Exception:
|
||
return ""
|
||
|
||
|
||
if __name__ == "__main__":
|
||
app.run(host=config.HOST, port=config.PORT, threaded=True, debug=False)
|