# -*- 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() # 水印字体(支持中文) _WATERMARK_FONT = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc" def _watermark_text(): return db.get_setting("watermark_text", config.DEFAULT_WATERMARK) def _watermark_style(): st = db.get_setting("watermark_style", config.DEFAULT_WATERMARK_STYLE) return st if st in config.WATERMARK_STYLES else config.DEFAULT_WATERMARK_STYLE def _top_n(): try: return max(1, int(db.get_setting("top_n", str(config.DEFAULT_TOP_N)))) except Exception: return config.DEFAULT_TOP_N def _column_config(): out = {} for k, default in config.COLUMN_DEFAULTS.items(): v = db.get_setting(k, default) out[k] = 1 if str(v).strip() in ("1", "true", "on") else 0 return out def apply_watermark(img_bytes: bytes, text: str, style: str = "") -> bytes: """给图片叠加水印,5 种风格可选:corner右下角标注 / diagonal全图斜纹 / bottom底部横条 / corners四角标注 / diag-big对角大字 颜色自适应:浅底深字 / 深底浅字""" if not text: return img_bytes style = style or config.DEFAULT_WATERMARK_STYLE if style not in config.WATERMARK_STYLES: style = config.DEFAULT_WATERMARK_STYLE try: from PIL import Image, ImageDraw, ImageFont except Exception: return img_bytes try: img = Image.open(io.BytesIO(img_bytes)).convert("RGBA") except Exception: return img_bytes w, h = img.size bg = img.getpixel((4, 4))[:3] bg_luma = 0.299 * bg[0] + 0.587 * bg[1] + 0.114 * bg[2] dark_bg = bg_luma < 140 fill = (50, 50, 60) if not dark_bg else (255, 255, 255) a_diag = 30 if not dark_bg else 26 a_main = 130 if not dark_bg else 135 overlay = Image.new("RGBA", (w, h), (0, 0, 0, 0)) d = ImageDraw.Draw(overlay) try: font = ImageFont.truetype(_WATERMARK_FONT, 16) font_big = ImageFont.truetype(_WATERMARK_FONT, 24) font_huge = ImageFont.truetype(_WATERMARK_FONT, 60) except Exception: font = ImageFont.load_default() font_big = ImageFont.load_default() font_huge = ImageFont.load_default() def _corner_label(): box = d.textbbox((0, 0), text, font=font_big) tw = box[2] - box[0]; th = box[3] - box[1] d.text((w - tw - 16, h - th - 12), text, font=font_big, fill=fill + (a_main,)) if style == "corner": _corner_label() elif style == "diagonal": step = 170 for y in range(-h, h, step): for x in range(-w, w, step): d.text((x + y * 0.4, y), text, font=font, fill=fill + (a_diag,)) elif style == "bottom": # 底部半透明横条 box = d.textbbox((0, 0), text, font=font_big) tw = box[2] - box[0]; th = box[3] - box[1] bh = th + 24 d.rectangle([0, h - bh, w, h], fill=(0, 0, 0, 90) if not dark_bg else (255, 255, 255, 70)) d.text((16, h - bh + 10), text, font=font_big, fill=fill + (a_main,)) # 底部中央小字 cx = (w - tw) // 2 d.text((cx, h - 20), text, font=font_big, fill=fill + (110,)) elif style == "corners": box = d.textbbox((0, 0), text, font=font_big) tw = box[2] - box[0]; th = box[3] - box[1] for px, py in [(12, 12), (w - tw - 12, 12), (12, h - th - 12), (w - tw - 12, h - th - 12)]: d.text((px, py), text, font=font_big, fill=fill + (a_main,)) elif style == "diag-big": box = d.textbbox((0, 0), text, font=font_huge) tw = box[2] - box[0]; th = box[3] - box[1] for i in range(-1, 2): d.text(((w - tw) // 2 + i * 14, (h - th) // 2 + i * 28), text, font=font_huge, fill=fill + (38,)) d.text(((w - tw) // 2, (h - th) // 2), text, font=font_huge, fill=fill + (80,)) else: _corner_label() out = Image.alpha_composite(img, overlay) buf = io.BytesIO() out.convert("RGB").save(buf, format="PNG") buf.seek(0) return buf.getvalue() @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) db.touch_speed_item(body.get("provider") or "", model) # 速度项热度/个数自动关联 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", "heat") # 默认按热度 order = request.args.get("order", "desc") limit = min(int(request.args.get("limit", 500) or 500), 1000) 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] # 可选:搜索过滤(模型/提供商/配置简介) q = (request.args.get("q") or "").strip().lower() if q: rows = [r for r in rows if q in (r["model"] or "").lower() or q in (r["provider"] or "").lower() or q in (r.get("intro") or "").lower()] chart_rows = rows[: int(request.args.get("chart_top", 30) or 30)] chart_csv = _speed_csv(chart_rows) cols = _column_config() return jsonify({"ok": True, "rows": rows, "chart_csv": chart_csv, "top_n": _top_n(), "watermark": _watermark_text(), "watermark_style": _watermark_style(), "columns": cols}) def _speed_csv(chart_rows): """模型 运行速度 CSV:预填充 / 解码 / 首字 三列(前端据此配置柱/折线/双轴/颜色)""" 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_prefill_speed"]), _fmt(r["avg_decode_speed"]), _fmt(r["avg_ttft_ms"]))) return "\n".join(csv_lines) # ───────────────────────── 模型对比(硬件 + 可去留对比项) ───────────────────────── @app.route("/api/compare") def compare(): """返回指定模型(默认全部)的聚合对比行 + 运行速度 CSV。 models: 逗号分隔 provider|model(保持传入顺序=对比车添加顺序); 不传时默认全部(按解码速度排序,前端默认取前5)。 """ 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)) rows = db.compare_models(keys) # 保持传入顺序(对比车顺序) 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) chart_csv = _speed_csv(rows[: int(request.args.get("chart_top", 60) or 60)]) return jsonify({"ok": True, "rows": rows, "chart_csv": chart_csv}) @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": "dark", "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/") 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/", methods=["DELETE"]) @admin_required def submission_delete(sid): db.delete_submission(sid) return jsonify({"ok": True}) @app.route("/api/submissions//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//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": "dark", "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/", 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/", methods=["DELETE"]) @admin_required def account_delete(aid): db.delete_account(aid) return jsonify({"ok": True}) # ───────────────────────── 速度项管理(后台) / 设置 ───────────────────────── @app.route("/api/settings") def public_settings(): return jsonify({"ok": True, "watermark": _watermark_text(), "watermark_style": _watermark_style(), "top_n": _top_n(), "columns": _column_config()}) @app.route("/api/admin/speed-items") @admin_required def admin_speed_items(): return jsonify(db.list_speed_items()) @app.route("/api/admin/speed-items", methods=["POST"]) @admin_required def admin_speed_item_add(): body = request.get_json(force=True) or {} try: sid = db.add_speed_item(body.get("provider", ""), body.get("model", ""), body.get("heat", 0), body.get("intro", ""), body.get("count", 0), body.get("remark", "")) return jsonify({"ok": True, "id": sid}) except ValueError as e: return jsonify({"ok": False, "error": str(e)}), 400 @app.route("/api/admin/speed-items/", methods=["PUT"]) @admin_required def admin_speed_item_update(sid): body = request.get_json(force=True) or {} db.update_speed_item(sid, body) return jsonify({"ok": True}) @app.route("/api/admin/speed-items/", methods=["DELETE"]) @admin_required def admin_speed_item_delete(sid): db.delete_speed_item(sid) return jsonify({"ok": True}) @app.route("/api/admin/speed-items/sync", methods=["POST"]) @admin_required def admin_speed_item_sync(): added = db.sync_speed_items() return jsonify({"ok": True, "added": added}) @app.route("/api/admin/settings", methods=["GET"]) @admin_required def admin_settings_get(): return jsonify({"ok": True, "settings": db.get_all_settings(), "watermark": _watermark_text(), "watermark_style": _watermark_style(), "top_n": _top_n(), "columns": _column_config()}) @app.route("/api/admin/settings", methods=["PUT"]) @admin_required def admin_settings_put(): body = request.get_json(force=True) or {} allow = {"watermark_text", "watermark_style", "top_n"} | set(config.COLUMN_DEFAULTS.keys()) for k, v in body.items(): if k in allow: db.set_setting(k, v) 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/", 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/", 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 # 叠加水印(默认首页网址,后台可配置文字+5种风格) data = apply_watermark(resp.content, _watermark_text(), _watermark_style()) return send_file(io.BytesIO(data), 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) db.touch_speed_item(provider, model) 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)