这次我们来看一个 Hacker News 上以 "Show HN" 形式发布的 LLM 评测项目。它做的事情非常直接:拿 8 个大语言模型,对着 38 个柏林选举相关的问题分别作答,然后把结果放在一起做横向对比。这不是一个聊天机器人 Demo,也不是一张简单的模型排行榜,而是一套可以复用的“垂直领域 LLM 问答评测流程”。在信息准确性要求较高的公共事务场景里,这种评测比通用 benchmark 更能反映模型的实际可用性。
这个项目值得关注的点有几个方面。第一,评测对象覆盖 8 个模型,通常意味着会横跨不同公司和不同参数规模,天然具备横向对比价值;第二,评测问题来自真实选举议题,而不是 MMLU 那种标准化题库,更贴近实际使用场景;第三,输出是结构化对比结果,便于继续计算事实准确率、完整性、中立性等指标;第四,方法论可以迁移。把“柏林选举问题”换成“医保政策问题”“产品售后问题”,这套流程就变成了一个垂直领域问答评测工具。
如果你关心 LLM 选型、RAG 助手上线前的质量验证,或者想把内部知识库问答做一次横向评测,这篇文章建议收藏。我会按这个顺序展开:先给项目规格速览,再讲评测集怎么构建、API 怎么调用、批量任务怎么设计、打分怎么定、结果怎么分析,最后补充资源消耗观察、常见坑和最佳实践。文章里的代码都是通用模板,实际使用时需要替换模型名称、API 地址和评测问题。
1. 项目核心能力速览
从标题可以确认的信息是:8 个 LLM、38 个问题、评测领域是柏林选举相关议题。更具体的实现方式,比如是否开源、用什么模型做裁判、结果如何展示,这个标题里没有写明,需要去项目主页确认。所以下面的规格表会把不确定的内容单独标记出来,避免误导。
| 能力项 | 说明 |
|---|---|
| 项目类型 | LLM 问答评测与横向对比 |
| 来源 | Hacker News “Show HN” 展示项目 |
| 评测规模 | 8 个 LLM × 38 个问题 |
| 评测领域 | 柏林地方选举相关公共议题 |
| 输出形式 | 各模型回答的结构化对比结果,具体可视化方式未在标题中说明 |
| 是否开源 | 未在标题中说明,需查看项目主页 |
| 是否支持本地部署 | 未在标题中说明;如果方案是调用云端 API,本机没有 GPU 压力 |
| 硬件要求 | 纯 API 方案普通电脑足够;本地模型方案取决于模型尺寸 |
| 是否支持批量任务 | 评测本身天然是批量任务,按“问题 × 模型”组合执行 |
| 适合场景 | 模型选型、垂直领域问答验证、RAG 助手前置评测 |
这里要特别说明一个判断:这类评测项目对显存占用不敏感。如果 8 个模型全部走云端 API,本机只负责发起 HTTP 请求和保存结果,显存占用基本可以忽略;只有把部分模型替换成本地部署的开源模型之后,才需要认真考虑显存。实际占用以你的评测方案为准,不要凭感觉做配置。这也意味着,这个项目的门槛不在于硬件,而在于评测集质量和调用费用控制。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 适用场景与使用边界
什么样的团队会用这类项目?
第一类是正在做 LLM 选型的应用开发者。选模型不能只看榜单分数,最有效的办法是把真实业务问题整理成评测集,用同一套问题让候选模型回答,再按自己关心的维度打分。8 个模型 × 38 个问题正好是一个实用的迷你评测集,既能看出模型差异,又不会把评测周期拖得太长。
第二类是 RAG / 知识库助手的开发者。知识库问答最怕模型一本正经地输出过时或错误信息。把领域内高频问题抽出来做成评测集,每隔一段时间跑一遍,可以及时发现模型更新、知识库变更带来的质量波动。这里的评测对象从“8 个模型”变成“同一个模型的不同版本”,方法论是一样的。
第三类是关注 AI 可信度的研究人员和数据产品经理。他们关心的不是单个回答好不好,而是不同模型在同一批问题上的系统性差异:哪些问题普遍回答更完整,哪些模型在争议议题上表现出明显倾向,哪些回答存在知识过时。
第四类是公共事务、媒体或政务数字化团队。面向公众的问答产品上线前需要一套可复现的评测流程来验证回答的中立性和事实准确度,这类对比项目可以作为一个现成模板。
使用边界也很清楚。不要把这个项目当成民意调查工具——38 个问题的模型回答不反映真实选民意向;也不要当成学术级 benchmark——样本量偏小,且问题集中在单一地区议题,结论不能泛化到模型整体能力。评测过程要保持技术中立,只对比“回答质量”,不对政治立场做价值判断。
合规方面需要提醒三点。第一,调用任何模型都要遵守服务提供方的使用条款,尤其是用公共 API 做批量评测时,要确认用途是否在允许范围内。第二,如果评测集中的问题或参考答案来自公开资料,注意保留来源并遵守版权要求。第三,涉及选举、公共政策等话题时,评测结果应以客观对比形式呈现,避免给读者造成引导或误导。无论原项目是否做了合规封装,复用时都要自己把关。
3. LLM 评测集构建与数据准备
这类评测项目的核心资产不是代码,而是那 38 个问题。问题质量直接决定评测结论是否可信。
3.1 评测问题怎么设计
从“柏林选举问题”这个标题可以合理推断,评测集围绕柏林地方选举的关键公共议题展开。通常这类问题会覆盖以下方向:
| 问题方向 | 可能的考察点 |
|---|---|
| 城市基础数据 | 人口、面积、行政区划等事实型信息 |
| 住房与租赁政策 | 租金管控、住房供给等 |
| 交通规划 | 公共交通、自行车道建设等 |
| 气候与能源 | 减排目标、能源转型措施 |
| 教育与文化 | 学校建设、文化设施投入 |
| 选举制度 | 选举周期、投票方式、议会机制 |
设计评测问题的原则,我建议遵循“封闭、可验证、有来源”三条。
封闭是指问题应该有一个相对确定的答案范围,而不是“你觉得柏林的交通怎么样”这种开放式提问。封闭问题更容易打分,模型之间的差异也更容易解释。可验证是指每个问题都要能对应到一份公开资料或官方信息,否则无法判断模型回答的对错。有来源是指在评测集里给每个问题附上参考来源链接,方便复核。38 个问题不算多,所以覆盖面要均衡。如果 38 个问题里 30 个都是人口数据类,那评测结果只能说明“模型对柏林人口数据的掌握程度”。合理做法是把问题均匀分布到不同政策领域和问题类型上。
3.2 问题类型与标签体系
建议给每个问题打标签,方便后续按子集分析。常见的标签维度包括:
| 标签维度 | 取值示例 |
|---|---|
| 难度 | 容易、中等、困难 |
| 类型 | 事实型、政策型、程序型、开放型 |
| 领域 | 住房、交通、气候、教育、财政 |
| 时效敏感度 | 静态信息、政策更新、长期趋势 |
有了标签,后面就能回答“模型在困难事实题上的准确率是多少”“模型在住房政策题上的回答是否完整”这类细分问题。这个环节对评测结论的深度影响很大,不要略过。
3.3 参考答案与评分标准
评分前先写参考答案。参考答案不需要是唯一标准答案,但至少要包含“必答点”。举个例子,如果问题是“柏林市议会多久选举一次”,参考答案可以写“柏林市议会选举周期为五年,具体日期由相关法律规定”,那么“五年”和“法律规定”就是两个采分点。模型回答如果漏掉其中一个,完整性就要扣分。
打分维度建议从四个角度观察:
- 事实准确性:回答中的关键事实是否与参考答案一致。
- 完整性:必答点是否遗漏,是不是只回答了问题的一部分。
- 中立性:对争议议题是否保持中立,有没有明显立场表达。
- 可读性:回答结构是否清晰,是否直接回应问题本身。
这四个维度不需要复杂的提示词,也能覆盖大多数评测场景。具体打分方式在第六章展开。
4. 环境准备与依赖安装
先明确前提:如果评测方案是全部调用云端 API,环境准备非常轻。安装 Python、创建虚拟环境、安装 SDK 和基础库就够了。如果打算评测本地开源模型,则需要额外配置 GPU 或量化推理环境,这部分门槛会高不少。
4.1 环境清单
- 操作系统:Windows / macOS / Linux 都可以,纯 API 调用没有平台限制
- Python:3.10 或更高版本
- 依赖:openai、python-dotenv、requests、tenacity(可选)
- API 密钥:准备好各家模型的密钥
- 磁盘空间:几乎没有要求,依赖包只有几十 MB
创建项目目录:
bash复制mkdir llm-election-eval
cd llm-election-eval
python -m venv .venv
source .venv/bin/activate # Windows 使用 .venv\Scripts\activate
pip install openai python-dotenv requests tenacity
密钥统一放在 .env 文件里,不要写进代码:
properties复制MODEL_A_API_KEY=sk-xxxxxxxx
MODEL_B_API_KEY=sk-xxxxxxxx
MODEL_C_API_KEY=sk-xxxxxxxx
读取方式:
python复制import os
from dotenv import load_dotenv
load_dotenv()
api_key = os.getenv("MODEL_A_API_KEY")
这里有个细节:不同模型的 API 格式未必完全一致。有的提供 OpenAI 兼容接口,有的使用自家 SDK。更稳妥的做法是在第一版脚本里优先适配 OpenAI 兼容接口,把各家的 base_url 和 model_name 写进配置文件,后续再逐步适配特殊接口。
4.2 启动评测的三种方式
根据项目实际情况,启动方式主要有三种。
第一种是全量评测。运行完整脚本,遍历所有模型和全部问题,输出汇总结果。这种方式适合在评测集和评分标准都比较稳定时执行,通常会配合断点续跑。
第二种是单模型联调。先把模型列表缩小成一个模型,把问题集缩小成 5 个,确认 API 连通性、限流情况和结果存储都没有问题,再扩展到全量。这一步能省下大量返工时间。
第三种是本地模型替换。如果要评测本地部署的开源模型,用一个兼容 OpenAI 接口的本地推理服务,把 base_url 指向本机地址,例如 http://127.0.0.1:8000/v1,就可以复用同一套评测脚本。这也是比较推荐的做法,不会因为切换模型而重写代码。
5. API 调用与批量任务实现
评测主流程其实不复杂:读问题集 → 遍历模型列表 → 发送请求 → 保存结果 → 打分 → 汇总。
5.1 评测主流程
python复制# 伪代码:评测流程
for model in models:
for question in questions:
answer = call_llm(model, question)
save_result(model, question, answer)
这个两层循环是最直观的逻辑,但有两个问题:一是串行太慢,8 个模型 × 38 个问题可能要跑很久;二是某个请求失败后容易中断整个流程。下面的并发和重试方案用来解决这两个问题。
5.2 批量并发请求
对 API 评测来说,并发度不需要太高,一般 2 到 4 个并发请求就足够。要特别小心限流,很多 API 对单账号有 RPM(每分钟请求数)限制。下面是一个用 asyncio + OpenAI 兼容接口组织的批量评测脚本模板:
python复制import asyncio
import json
from openai import AsyncOpenAI
# 评测集,按实际项目替换
QUESTIONS = [
{"id": "berlin-001", "question": "柏林市议会多久选举一次?"},
{"id": "berlin-002", "question": "柏林当前的租金管控政策主要包含哪些内容?"},
]
# 模型配置,按实际项目替换
MODEL_CONFIG = {
"model-a": {
"base_url": "https://api.example.com/v1",
"model_name": "model-a",
"api_key_env": "MODEL_A_API_KEY",
},
"model-b": {
"base_url": "https://api.example.com/v1",
"model_name": "model-b",
"api_key_env": "MODEL_B_API_KEY",
},
}
async def ask_once(client, model_name, question):
resp = await client.chat.completions.create(
model=model_name,
messages=[{"role": "user", "content": question["question"]}],
temperature=0.2,
max_tokens=600,
)
return {
"id": question["id"],
"question": question["question"],
"answer": resp.choices[0].message.content,
}
async def eval_model(model_key, cfg):
import os
client = AsyncOpenAI(
api_key=os.getenv(cfg["api_key_env"]),
base_url=cfg["base_url"],
)
tasks = [ask_once(client, cfg["model_name"], q) for q in QUESTIONS]
results = await asyncio.gather(*tasks)
return {"model": model_key, "results": results}
async def main():
all_results = {}
for model_key, cfg in MODEL_CONFIG.items():
all_results[model_key] = await eval_model(model_key, cfg)
with open("eval_results.json", "w", encoding="utf-8") as f:
json.dump(all_results, f, ensure_ascii=False, indent=2)
if __name__ == "__main__":
asyncio.run(main())
这段代码有几个需要替换的位置:QUESTIONS 换成你的评测集;MODEL_CONFIG 换成你的模型列表和 base_url;temperature、max_tokens 按评测需求调整。base_url 只是通用写法,实际项目应以模型服务商文档为准。
5.3 重试与限流
批量跑评测最怕遇到限流。一个 429 响应中断整个评测会很浪费,建议给请求包一层重试逻辑:
python复制import time
from openai import RateLimitError
def call_with_retry(client, model_name, question, retries=3):
for attempt in range(retries):
try:
resp = client.chat.completions.create(
model=model_name,
messages=[{"role": "user", "content": question["question"]}],
temperature=0.2,
max_tokens=600,
)
return resp.choices[0].message.content
except RateLimitError:
if attempt == retries - 1:
raise
time.sleep(2 ** attempt)
这个函数只演示了重试思路,实际使用还要考虑其他异常类型,比如超时和连接错误,可以按需扩展异常捕获列表。重试次数不建议太大,否则会把限流窗口拉满,反而增加整体耗时。
5.4 结果存储
评测结果建议同时存两个文件:一个原始 JSON,记录每个模型对每个问题的完整回答;一个 CSV,记录模型、问题 ID、回答等字段,方便快速透视。原始 JSON 方便后续重新打分和分析,CSV 方便做表格汇总。
python复制import csv
with open("eval_results.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["model", "question_id", "question", "answer"])
for model, data in all_results.items():
for item in data["results"]:
writer.writerow([model, item["id"], item["question"], item["answer"]])
这里有一个容易被忽略的细节:在保存结果时,把模型版本号、评测脚本版本、运行日期一起写入结果文件。模型提供商会定期更新版本,没有版本信息的评测报告,几周后就很难复现。
6. 打分与对比分析
评分环节是整个评测流程里最容易引起争议的地方。建议采用“规则打分 + 量化对比”的双轨策略。
6.1 定义打分维度
按第 3 章的四个维度打分:factual_accuracy(事实准确性)、completeness(完整性)、neutrality(中立性)、readability(可读性)。每个维度用 1 到 5 分制。
要不要用 LLM 当裁判?可以,但不要只依赖 LLM 裁判。更稳妥的方案是:先用一个固定的裁判模型对所有回答打分,然后人工抽查 10% 到 20%,重点检查裁判模型在争议题上的判断是否合理。裁判模型的选择、版本和温度参数都要在评测报告中写明。
评分提示词模板:
json复制{
"system_prompt": "你是评测系统的评分器。针对用户的回答,分别从事实准确性、完整性、中立性、可读性四个维度打分,每项1到5分,并给出一句评分理由。不要修改用户的回答。",
"answer_to_score": "<模型回答原文>"
}
6.2 汇总分析
把打分结果按模型汇总,先做一张最简单的表:
| 模型 | 平均事实性 | 平均完整性 | 平均中立性 | 平均可读性 |
|---|---|---|---|---|
| model-a | 0.0 | 0.0 | 0.0 | 0.0 |
| model-b | 0.0 | 0.0 | 0.0 | 0.0 |
表格里的数字只是格式示例,实际数值以你的评测结果为准。看过整体均值之后,下一步是细分分析:只看“困难题”、只看“住房政策”、只看“选举制度”等子集。这一步能明显提升评测结论的价值。
6.3 失败案例分析
评测的终点不是排名,而是找出失败原因。把得分最低的 10 个回答单独拉出来做一次错误聚类。常见错误类型包括:
- 知识过时:模型给出的是旧政策内容
- 混淆概念:把联邦层面和州层面的机构或机制混在一起
- 回答空洞:输出格式规范,但没有实际信息量
- 倾向性表达:在争议议题上明显偏向某一方
对错误类型做归类之后,评测才算真正有了落地价值。如果你正在做 RAG 助手,这些失败案例就是优化检索器或提示词的最佳素材。
7. 资源占用与成本观察
这个项目对资源的消耗主要体现在三个方面:请求耗时、token 消耗和本地模型推理占用。
先说请求耗时。如果走云端 API,耗时由模型推理速度和网络延迟共同决定。8 个模型 × 38 个问题,串行跑可能很慢,加入 2 到 4 并发后可以明显缩短。记录每次请求的耗时,有助于定位是某个模型服务响应慢,还是评测脚本本身有瓶颈。
python复制import time
start = time.time()
# 在这里发起请求
elapsed = time.time() - start
print(f"question={qid} model={model} elapsed={elapsed:.2f}s")
再说 token 消耗。每次调用消耗的 token = 问题长度 + 回答长度 + 系统提示词长度。38 个问题 × 8 个模型,假设平均每次 500 token,总量大约 15 万 token。这个数量级在商业 API 定价下通常可控,但不同模型定价差距很大,建议先跑 1 个模型 + 5 个问题小批量估一下成本,再决定是否全量跑。
最后说本地模型。如果评测列表里包含本地部署的开源模型,显存占用就要按模型尺寸和推理框架估算。FP16、FP8、INT4 量化版本的显存占用差别很大。遇到显存不足时,优先试用量化版本或减小 batch size。这里不给出具体数字,因为不同模型和框架的占用差异很大,必须以实际运行监控为准。
建议在评测报告的环境信息里记录模型服务、版本日期、评测脚本版本。这样后续复现或对比时,才不会出现“模型版本换了我都不知道”的情况。
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| API 返回 401 Unauthorized | API Key 错误或没有权限 | 检查 .env 是否加载成功,打印 key 前几位 | 修正 Key,确认模型权限 |
| API 返回 429 Rate Limit | 请求频率超过限制 | 查看响应头中的 retry-after | 降低并发度,加重试退避 |
| 请求超时 | 模型推理慢或网络不稳 | 检查响应耗时和重试日志 | 增大 timeout 参数,对慢模型单独放宽 |
| 部分模型返回空字符串 | max_tokens 过小或安全过滤 | 单独复现该问题,查看原始响应 | 调大 max_tokens,调整提示词 |
| 不同模型输出格式差异大 | 各家指令遵循能力不同 | 对比系统提示词和回答长度 | 在评测提示词中要求固定格式,或在评分阶段归一化 |
| 评分结果不稳定 | 温度参数过高或裁判模型换版本 | 检查评分脚本版本和温度 | 温度设为 0 或 0.2,锁定裁判模型版本 |
| 评测中途中断 | 某个请求异常导致进程退出 | 查看异常堆栈 | 增加异常捕获和断点续跑,每完成一个模型就保存 |
| 成本明显超预期 | 没有限制回答长度,重试次数过多 | 统计 token 消耗日志 | 设置 max_tokens,限制重试次数,先小批量测试 |
这里有三个细节要重点提示。第一个是断点续跑。批量评测一旦中断,最不希望的是从头再跑。每完成一个模型就保存一次结果,是简单但有效的设计。第二个是版本记录。模型提供商经常更新版本,同名的 model-a 可能在两周后就变了,报告中要记录调用时的模型版本号和日期。第三个是评测污染问题。如果评测集是公开的,模型可能在预训练阶段见过类似内容,尤其是热门问题更常见。在结论里声明这一限制即可,不需要过度担心。
9. 最佳实践与使用建议
结合这类 LLM 评测项目的通用特点,我给出一组工程化建议。
第一,先小规模跑通再全量跑。建议先用 1 个模型加 5 个问题,验证 API 连通、参数配置、结果存储都没问题,再扩展到 8 个模型 × 38 个问题。全量评测中途发现脚本 bug,返工成本会很高。
第二,把评测问题、参考答案、模型列表、打分标准放在一个配置文件里。这个文件本身就是评测的一部分,方便后续修改和审计。
json复制{
"eval_config": {
"questions_file": "./data/questions.json",
"reference_file": "./data/reference.json",
"models": [
{"key": "model-a", "base_url": "https://api.example.com/v1", "model_name": "model-a"},
{"key": "model-b", "base_url": "https://api.example.com/v1", "model_name": "model-b"}
],
"temperature": 0.2,
"max_tokens": 600,
"concurrency": 2,
"output_dir": "./output"
}
}
第三,同一组问题不要在太短时间内反复跑。模型服务有随机性,每次结果都会有一点差异。一次评测周期内,所有调用尽量使用相同的 temperature、max_tokens 和提示词。跨周期评测时,要固定问题版本,避免问题被无意识改动。
第四,事实型问题要做引用级别验证。检查模型回答中是否包含具体的数字、法案名称、机构名称,这些关键信息最容易出错,也最容易验证。
第五,合规意识不能少。使用公共 API 做批量评测,要确认服务商条款是否允许;结果对外发布时,要保留评测集来源、模型版本、评分标准等可追溯信息。涉及选举、政务等敏感话题,只呈现中立的评测对比,不输出政治建议。
第六,做 RAG 场景评测时,要区分“模型知识”和“检索增强”的影响。如果只是单纯调 LLM,评测的是模型内部知识;如果接入了检索,评测的是整套 RAG 链路的效果。二者不要混在一起排名。
10. 总结与下一步
这类项目最有价值的部分,不是某个模型在柏林选举问题上得分最高,而是它示范了一套低成本、可复现的垂类 LLM 评测流程。对多数开发者来说,一次性拉来 8 个模型做对比的机会并不多,但这个流程完全可以复制到自己的领域。
建议拿到这个项目后先做三件事。第一,把 38 个柏林选举问题替换成自己业务域里最关心的 30 到 40 个问题;第二,先用两个模型加小批量问题跑通脚本,确认结果保存和评分流程没问题;第三,跑完一次全量评测后,把得分最低的失败案例聚类,这些失败案例就是后续优化最明确的切入点。
最容易踩的坑有三类:不锁模型版本导致结论无法复现;不限流重试导致批量任务频繁中断;评分只用 LLM 裁判导致结果偏差没有兜底。这三条在写脚本之前就要想清楚。
后续可以扩展的方向也不少:把评测集接到 CI 里做自动化回归;增加 RAG 与非 RAG 的对照实验;引入多轮对话评测;把打分结果做成定时报告。如果要做模型选型,也可以把这个评测集和成本数据结合,做一个“质量 + 成本”的二维对比。这个项目很适合作为评测体系的起点,值得收藏备用。
