Files

693 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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/<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": "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/<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/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/<int:sid>", 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/<int:sid>", 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/<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
# 叠加水印(默认首页网址,后台可配置文字+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)