基于LiteLLM的LLM推理能力基准测试:构建“猜数字”游戏评估框架
1. 项目概述一个为LLM设计的“猜数字”基准测试框架最近在折腾大语言模型LLM的推理能力评估发现很多测试要么太“学术”比如一堆数学题要么太“玄学”比如让模型写首诗然后凭感觉打分。正好之前在Telegram上看到有人争论说现有的LLM连一个简单的“猜数字”游戏Bulls and Cows都玩不明白以此证明模型缺乏真正的推理能力。这个说法勾起了我的兴趣——与其空对空地辩论不如动手建个测试场用代码和数据来说话。于是这个名为llm-bulls-and-cows-benchmark的小项目就诞生了。它的核心目标非常直接为不同的大语言模型提供一个标准化的“竞技场”让它们来玩“公牛与母牛”这个经典的数字推理游戏并通过一系列可量化的指标成功率、效率、指令遵循度来客观评估其表现。这个项目适合任何对LLM能力评测、智能体Agent基础逻辑构建或者单纯想找个有趣又带点挑战性的Python项目来练手的朋友。简单来说这个框架会模拟游戏裁判自动生成一个秘密数字例如一个各位不重复的四位数1234然后让LLM扮演猜数字的玩家。模型每猜一次例如1246裁判会返回反馈位置和数字都对的叫“公牛”Bulls此例中数字1和2位置正确计2 Bulls数字对但位置错的叫“母牛”Cows此例中数字4位置不对计1 Cow。模型需要根据这些线索进行推理给出下一个猜测直到猜中为止。理论上一个完美的推理者能在7步内解决任何4位数谜题。我们的框架就负责自动化这个过程并记录下一切。2. 核心设计思路与方案选型当我决定要构建这个基准测试时首要考虑的是如何让它既严谨又灵活。严谨意味着测试条件必须可控、可复现灵活意味着能快速接入各种各样的模型而不被某个特定的API供应商绑死。2.1 为什么选择“公牛与母牛”游戏这并非随意选择。这个游戏是一个检验“序列决策与信息整合”能力的绝佳沙盘。明确的规则与状态游戏规则极其简单清晰没有歧义。每一步的反馈Bulls/Cows是确定性的、信息量明确的。需要多步推理单次反馈不足以解题模型必须结合历史猜测和反馈构建并更新对秘密数字可能性的“心智模型”。检验指令遵循我们要求模型以特定格式GUESS: 1234输出答案。这能顺带测试模型在持续对话中遵守简单结构化指令的能力这是许多实际应用场景的基础。计算成本可控相比让模型生成长篇大论这个游戏的交互以短文本为主使得进行大量对局例如50局、100局以获取统计意义结果的成本变得可以接受。2.2 技术栈与架构选型项目的技术选型围绕着“轻量”、“通用”和“可维护”三个原则展开。核心游戏引擎用纯Python实现。逻辑不复杂自己写能保证完全可控也方便做单元测试。游戏状态机、反馈计算、胜负判定都封装在独立的模块里。LLM交互层这是项目的关键。我选择了LiteLLM作为抽象层。这是一个非常棒的开源库它用一个统一的接口封装了众多LLM提供商OpenAI, Anthropic, Google, Groq, 以及通过OpenRouter接入的数十个模型的API。这意味着我写一套调用代码通过修改配置就能测试几乎任何主流模型极大地提升了框架的扩展性。注意虽然LiteLLM支持很多高级功能如JSON模式、函数调用但在这个基准测试中我刻意避免使用结构化输出。原因在项目说明里提到了一是有些研究表明强制结构化输出可能会影响模型本身的推理质量二是并非所有API都平等地支持这些功能。为了公平对比我们回归最基础的文本补全模式。配置管理使用YAML文件。将模型名称、游戏位数3位或4位、并发数、最大回合数等参数全部外置。这样想要测试不同配置无需修改代码只需改配置文件甚至可以通过命令行参数覆盖非常利于批量实验。并发控制为了压榨API的吞吐量特别是对付那些有每秒请求数限制的供应商框架支持并发运行多个游戏。这里用到了asyncio和aiohttp来实现异步请求避免因为网络IO而让测试跑成“慢动作”。不过并发数需要根据具体API的限流策略谨慎设置。结果可视化原始日志JSON格式虽然完整但不直观。我增加了用Plotly生成交互式HTML报告的功能。它能绘制模型间的对比柱状图、成功率随时间变化曲线等让结果一目了然。2.3 关键设计决策Prompt工程与评估指标如何与模型对话以及如何评判它的表现是基准测试的灵魂。Prompt设计 所有的提示词都集中放在一个prompts.py文件里方便管理和迭代。核心的提示词结构如下系统指令明确告知模型它正在玩Bulls and Cows游戏解释规则并严格规定输出格式必须是先进行推理可选然后在单独一行以GUESS: XXXX的形式给出猜测。上下文管理每次请求都会将之前所有轮次的对话历史用户的猜测和系统的反馈一并发送给模型。这是模拟模型的“工作记忆”是它进行多步推理的基础。纠错机制如果模型没有按照格式输出系统会在下一轮明确指出错误并要求其重试。这考察了模型从错误中学习并调整行为的能力。评估指标 我们主要看三个维度这比单纯一个“是否成功”要丰富得多成功率在规定的最大回合数内猜中秘密数字的游戏比例。这是最核心的指标。格式失败率模型输出不符合GUESS: XXXX格式从而浪费一个回合的比例。这反映了模型对指令的遵循程度和输出稳定性。平均获胜回合数仅针对那些成功的游戏计算模型平均用了多少步猜中。这个指标衡量模型的“效率”——推理能力越强、策略越好用的步数应该越接近理论最优值7步。实操心得在测试中发现像o1-mini这类推理模型有时会“自作聪明”地在输出中使用加粗等Markdown格式尽管指令明确禁止。我们将这种行为果断判为格式错误。这提醒我们在评估时不仅要看模型“能不能做对”还要看它“是否按你要求的方式做对”。3. 环境搭建与详细操作指南好了理论说再多不如动手跑一遍。下面我就带你从零开始把这个基准测试框架搭起来并运行你的第一次模型评测。3.1 基础环境准备首先你需要一个Python环境建议3.9以上版本和基本的命令行操作知识。克隆项目代码git clone https://github.com/stalkermustang/llm-bulls-and-cows-benchmark.git cd llm-bulls-and-cows-benchmark安装依赖 项目使用requirements.txt管理依赖。一键安装即可。pip install -r requirements.txt这将会安装 LiteLLM、pydantic用于配置验证、plotly用于绘图、tqdm进度条等核心库。设置代码质量钩子可选但推荐 项目使用了pre-commit来在提交代码前自动格式化Black和排序导入isort。安装后能保持代码风格统一。pip install pre-commit pre-commit install3.2 配置API密钥与模型框架本身不提供API密钥你需要自己准备。获取API密钥OpenAI前往 OpenAI Platform 创建密钥。Anthropic前往 Anthropic Console 创建密钥。其他模型通过OpenRouter前往 OpenRouter 创建密钥。OpenRouter是一个聚合平台可以用一个密钥调用许多不同公司的模型非常方便进行横向对比。设置环境变量 最安全的方式是使用.env文件。在项目根目录下创建名为.env的文件内容如下# 根据你使用的供应商设置对应的Key OPENAI_API_KEYsk-your-openai-key-here ANTHROPIC_API_KEYyour-anthropic-key-here OPENROUTER_API_KEYyour-openrouter-key-here重要提示确保.env文件已被添加到.gitignore中千万不要将包含真实密钥的文件提交到Git仓库框架的代码会通过os.getenv()读取这些变量。LiteLLM会自动识别它们。修改配置文件 核心的测试参数都在config/default_config.yaml里。我们打开它看看关键部分model: openai/gpt-4o-mini-2024-07-18 # 要测试的模型 target_length: 4 # 秘密数字的位数 (3 或 4) allow_repetitions: false # 数字是否允许重复 num_games: 10 # 总共要玩多少局游戏为了统计显著性建议至少50局 max_turns_per_game: 15 # 每局游戏的最大猜测次数4位数理论最优是7步这里给了一倍多的余量 num_concurrent_games: 5 # 并发游戏数用于提升测试速度 run_id: my_first_test # 本次运行的标识结果会保存在以它命名的文件夹里model字段的格式遵循 LiteLLM 的约定。例如openai/gpt-4oanthropic/claude-3-5-sonnet-20241022openrouter/google/gemini-pro-1.5初次测试建议将num_games设为10num_concurrent_games设为2或3先小规模跑通流程控制成本。3.3 运行基准测试与查看结果配置妥当后运行测试就非常简单了。启动基准测试 在项目根目录下执行python run_benchmark.py程序会开始运行。你会看到一个漂亮的进度条实时显示当前正在进行的游戏、已完成的游戏、总体成功率、平均回合数等信息。所有游戏的历史对话都会实时保存。生成可视化报告 测试完成后运行可视化脚本python scripts/visualize_results.py这个脚本会扫描benchmark_results/目录下的所有历史运行数据并生成一个汇总的交互式HTML报告。报告默认会保存在benchmark_results/visualization.html。用浏览器打开它你可以看到所有测试模型的成功率、平均回合数对比柱状图。点击图例来显示/隐藏特定模型的数据。查看每个模型具体对局的细节。结果文件解读 在benchmark_results/目录下你会找到以run_id或时间戳命名的文件夹例如benchmark_results/4_digits/my_first_test/。里面通常包含config.yaml: 本次运行使用的配置备份。results_summary.json: 核心指标的JSON汇总。full_conversations.json:最重要的文件记录了每一局游戏的完整对话历史。你可以在这里仔细分析模型每一步的推理和猜测。results_table.md: 一个格式美观的Markdown表格方便你直接复制到文档或报告中。4. 深入解析代码结构与核心逻辑理解了怎么用我们再来看看框架是怎么工作的。这对于你想自定义游戏规则、修改评估逻辑或者集成到自己的项目中至关重要。4.1 项目目录结构llm-bulls-and-cows-benchmark/ ├── config/ │ └── default_config.yaml # 主配置文件 ├── src/ │ ├── __init__.py │ ├── game.py # 游戏核心逻辑判断Bulls Cows │ ├── benchmark.py # 基准测试运行器管理多局游戏 │ ├── prompts.py # 所有提示词模板 │ └── utils/ # 工具函数如解析模型响应 ├── scripts/ │ └── visualize_results.py # 结果可视化脚本 ├── tests/ # 单元测试 ├── run_benchmark.py # 主程序入口 └── requirements.txt4.2 核心模块拆解1.src/game.py游戏引擎这是最纯粹的部分不涉及任何LLM。它定义了一个BullsAndCowsGame类。__init__: 初始化游戏生成一个秘密数字。这里有一个细节allow_repetitions参数决定了数字是否可以重复如1123。经典规则是不允许重复的。evaluate_guess: 核心函数。输入一个猜测返回 (bulls, cows) 的元组。其算法是遍历猜测和秘密数字的每一位。如果位置和数字都相同bulls计数加1。否则如果数字相同但位置不同且该数字在秘密数字中出现的次数多于已匹配的次数cows计数加1。 这个逻辑需要仔细实现避免重复计数。2.src/benchmark.py测试协调器这个模块负责“导演”整个测试过程。Benchmark类初始化时加载配置和提示词。_play_single_game_async: 一个异步函数负责和一局游戏。其流程是while not game.is_solved and turn max_turns: 1. 构建本次请求的对话历史包含之前的猜测和反馈。 2. 调用 litellm.acompletion 向LLM发送请求。 3. 从模型回复中尝试解析出 GUESS: XXXX 格式的字符串。 4. 如果解析成功将猜测交给 game.evaluate_guess 获得反馈。 5. 如果解析失败记录一次格式错误并将“格式错误”信息作为反馈加入对话历史让模型下一轮纠正。 6. 记录本轮所有信息。run方法使用asyncio.gather并发运行num_concurrent_games个_play_single_game_async任务并利用tqdm显示总进度。3.src/prompts.py提示词仓库这里定义了系统提示词、用户提示词模板以及解析逻辑。保持提示词的一致性对于公平比较不同模型至关重要。我采用的策略是使用一个固定的系统提示词然后在每轮的用户消息中动态填入历史对话。4.3 异步与并发处理并发是提升测试效率的关键但也带来了复杂性。为什么用异步LLM API调用是网络I/O密集型操作大部分时间在等待响应。使用asyncio可以在等待一个游戏响应的同时处理另一个游戏的请求极大缩短总测试时间。并发数 (num_concurrent_games) 的设置这不是越大越好。OpenAI通常限制是TPM每分钟tokens和RPM每分钟请求数。对于GPT-4o这类模型设置5-10的并发通常比较安全。Anthropic免费层限制较严。根据我的经验并发数设为2比较稳妥否则很容易触发速率限制错误。如果设置过高你会遇到429 Too Many Requests或RateLimitError。框架中通过asyncio.Semaphore或简单的延迟重试机制来处理这些错误但最根本的解决方案是根据API供应商的文档调整并发数。5. 测试结果分析与模型表现解读跑完测试面对一堆数据我们该如何解读这里我结合项目已有的测试结果分享一些观察和分析方法。5.1 理解数据表格我们以项目README中的结果表为例ModelGamesSuccess RateAvg Turns (success only)Format Failures (Turns)openai/o1-mini-2024-09-122560.0%[40.7%; 76.6%]9.1±2.723.1%openrouter/anthropic/claude-3.5-sonnet5036.0%[24.1%; 49.9%]9.8±4.00.0%Success Rate (成功率)这是核心指标。o1-mini以60%领先但注意它只跑了25局而Claude 3.5 Sonnet跑了50局。旁边的[40.7%; 76.6%]是威尔逊置信区间。因为样本量小真实成功率落在这个区间的可能性是95%。o1-mini的区间很宽说明需要更多对局来确认其优势是否稳定。Avg Turns (平均获胜回合数)仅统计成功的对局。o1-mini平均9.1步Sonnet平均9.8步都远高于理论最优的7步。这说明即使能解出来模型的推理策略也远非最优。Format Failures (格式失败率)o1-mini高达23.1%这意味着近四分之一的回合它没有按要求格式输出浪费了机会。这与其强大的推理能力形成鲜明对比凸显了“指令遵循”是一个独立的、需要评估的能力维度。5.2 不同模型的行为模式分析通过查看full_conversations.json我们可以深入洞察模型的“思考”过程强推理模型如o1-mini它们的回复往往包含长篇的、逐步的推理链。例如“上一轮我猜1234得到1B1C。说明数字1和2、3、4中的一个在秘密数字里但位置可能不对。让我考虑所有可能性...” 这种系统性的排除法是人类玩家的典型策略。但有时它们会过度推理或陷入循环。通用对话模型如GPT-4o, Claude 3.5它们的回复相对简洁推理步骤可能隐含在上下文中。它们更依赖从历史反馈中直接学习模式而不是显式地构建所有可能性空间。成功率中等但格式遵循性通常很好。较小或专用模型如一些较小的Gemini或Llama模型成功率显著降低。它们的失败往往表现为a) 无法从反馈中有效更新假设重复猜类似的数字b) 推理出现逻辑断裂c) 偶尔的格式错误。5.3 如何提高测试的可靠性与洞察力单次运行50局游戏对于区分顶尖模型可能还不够。如果你想获得更可靠的结论增加游戏数量将num_games提高到200甚至500。这能显著缩小置信区间让差异更具统计显著性。当然成本也会线性增加。固定随机种子确保所有模型面对的是同一组秘密数字。这样可以完全排除“运气”因素比如某个模型恰好遇到简单的数字。你需要在代码中修改数字生成部分使其可复现。分析失败案例不要只看汇总数据。仔细研究那些失败的游戏日志。模型是在哪一步开始出错的是错误解读了反馈还是做出了明显不合逻辑的猜测这能帮你更精准地定位模型的能力边界。尝试不同难度3位数 vs 4位数3位数0-9不重复的可能性是10*9*8720种4位数是10*9*8*75040种。难度提升显著。可以测试模型在问题空间扩大后的表现衰减。允许数字重复这将可能性空间从排列问题变为组合问题4位数且允许重复有10^410000种并且反馈的解读会更复杂例如猜测1122对秘密1212的反馈计算。这能进一步考验模型的逻辑能力。6. 常见问题与故障排查实录在实际搭建和运行过程中你肯定会遇到各种问题。下面是我踩过的一些坑以及解决办法。6.1 环境与依赖问题问题运行pip install -r requirements.txt时失败提示某些包版本冲突或找不到。解决建议使用虚拟环境venv或conda隔离项目依赖。如果还是冲突可以尝试先安装LiteLLM的核心版本再单独安装其他包。python -m venv venv source venv/bin/activate # Linux/Mac # venv\Scripts\activate # Windows pip install litellm pip install -r requirements.txt问题运行脚本时提示ModuleNotFoundError: No module named src。解决确保你的终端当前目录在项目的根目录即llm-bulls-and-cows-benchmark/下而不是在src/目录里。Python需要正确的路径来导入模块。6.2 API与网络问题问题运行测试时大量出现RateLimitError或429错误。排查首先检查你的API密钥是否有足够的额度或权限。然后大幅降低config.yaml中的num_concurrent_games值。对于Anthropic尝试设为1或2。对于OpenAI可以先从3开始测试。你可以在代码中benchmark.py的请求函数周围添加简单的指数退避重试逻辑来应对临时的速率限制。示例代码片段需集成到你的异步请求中import asyncio from litellm import RateLimitError async def make_request_with_retry(messages, max_retries3): for i in range(max_retries): try: response await litellm.acompletion(modelmodel, messagesmessages) return response except RateLimitError: wait_time (2 ** i) 1 # 指数退避2秒5秒9秒... print(fRate limited. Retrying in {wait_time} seconds...) await asyncio.sleep(wait_time) raise Exception(Max retries exceeded for rate limit.)问题使用OpenRouter时返回错误提示Model not found。解决检查config.yaml中的model字段。OpenRouter的模型名称格式通常是openrouter/provider/model-name。确保你复制的是OpenRouter官网上显示的完整模型ID。也可以先在LiteLLM的文档里查询支持的模型列表。6.3 结果分析与可视化问题问题运行visualize_results.py后没有生成HTML文件或者图表是空的。排查1检查benchmark_results/目录下是否有子文件夹。可视化脚本是读取该目录下所有包含results_summary.json的文件夹来生成汇总报告的。确保你至少成功运行过一次run_benchmark.py。排查2检查生成的results_summary.json文件格式是否正确。可能是测试中途出错导致JSON文件损坏。可以手动打开文件看看是否是有效的JSON格式。问题我想对比两次不同配置的测试结果但它们混在一起了。解决充分利用run_id配置项。每次运行前在config.yaml中设置一个独特的run_id例如gpt4o_4digits_50games。这样结果会保存在独立的文件夹里可视化报告也会区分显示。6.4 自定义与扩展问题我想修改游戏规则比如测试5位数或者允许0开头。解决主要修改两个地方config.yaml中的target_length。src/game.py中BullsAndCowsGame类的_generate_secret方法。你需要根据新规则调整随机数生成逻辑。注意提示词prompts.py中关于游戏规则的文字描述也需要相应更新否则模型会困惑。问题我想测试模型在“求助”或使用“工具”时的表现比如允许它调用一个计算可能性的函数。解决这涉及到将基准测试从单纯的“对话”升级为“智能体Agent”测试。你需要修改提示词告诉模型它可以调用某个工具函数。在benchmark.py的对话循环中增加对模型输出中“工具调用”的解析逻辑。实现对应的工具函数例如一个能根据历史反馈返回所有可能数字组合的函数。将工具的执行结果作为新一轮的“系统”或“工具”消息追加到对话历史中。 这是一个更高级的改造但能极大地丰富评估维度。最后这个项目本身就是一个起点。它的价值不在于给出一个“最强LLM”的终极排名而在于提供了一个透明、可复现、可扩展的测试框架。你可以用它来探究更深入的问题比如思维链Chain-of-Thought提示对成功率有多大影响给模型提供一个“策略提示”能否显著提升其效率不同量化版本的同一模型推理能力是否有差异希望这个工具和这些经验能帮助你更好地理解和评估这些日益强大的语言模型。