1. 从被动看图到主动探索:ESI-BENCH 到底在测什么
如果你最近在关注具身智能,大概率刷到过李飞飞团队的新作 ESI-BENCH。它的全称是 Embodied Spatial Intelligence Benchmark,中文可以理解为“具身空间智能基准”。和以往那些只让模型看一张图、回答一个空间问题的数据集不同,ESI-BENCH 的核心命题是:只有任务导向的主动观测,才是空间推理的关键。换句话说,它测的不是模型“视力”好不好,而是模型会不会为了拿到证据去主动转头、移动、拾取、倾倒。
这件事为什么重要?因为过去两年多模态大模型在 VSR、BLINK、3DSRBench 这类基准上分数越刷越高,但它们几乎都建立在一个隐含前提上:模型拿到的输入已经是“最优视角”。真实世界里没有这种好事。一个杯子是不是在柜子里,你得走过去、换个角度、甚至打开柜门才能确认;一个物体能不能放进另一个物体,你得比较容积、看遮挡关系。ESI-BENCH 就是要把这个“感知-行动回路”闭合起来,用 10 大类、29 子类、3081 个任务实例,逼着智能体自己决定下一步看哪里、动哪里。
它适合谁?我认为有三类开发者值得关注。第一类是做空间推理、VLM 评测的研究者,想找一个比传统静态基准更接近真实交互的测试集;第二类是做机器人、具身 Agent 的工程师,想看看当前模型在“选动作”这件事上到底有多弱;第三类是想复现论文结果、跑通评测流程的开发者,尤其是需要统一管理多个模型 Key、避免在环境配置上反复踩坑的人。我自己在复现这类基准时,最头疼的往往不是算法,而是不同模型供应商的 Base URL、鉴权方式、模型 ID 各不相同,脚本里到处硬编码,换一个模型就要改一堆配置。这篇就围绕“怎么把 ESI-BENCH 的评测流程跑通”来写,顺带把 TaoToken 作为统一 Key 网关的配置方式讲清楚,让你能把精力放在空间推理指标本身,而不是环境变量上。
需要先说明的是,ESI-BENCH 本身依赖 OmniGibson 仿真器和 BEHAVIOR-1K 场景,完整跑通需要 GPU 和 Isaac Sim 环境。本文的重点不是教你装仿真器,而是演示评测侧的调用链路:如何用统一的 Base URL 和 Key 去请求模型,如何构造一次基准任务请求,以及如何校验返回结果里的空间推理字段。这样即使你暂时没有仿真环境,也能先把模型接入和结果解析这一段跑通。
2. TaoToken 前置:统一 Key 与 Base URL 的接入准备
在复现 ESI-BENCH 这类基准时,评测脚本通常要调用多个 MLLM 做零样本测试,比如 GPT 系列、Gemini 系列,甚至 3D 增强模型。每个供应商的接入方式都不一样:有的用Authorization: Bearer,有的用x-api-key,有的还要额外的anthropic-version头。如果你在脚本里为每个模型写一套请求逻辑,维护成本会非常高,而且一旦 Key 泄露或额度用尽,排查起来很麻烦。
TaoToken 在这里扮演的角色是一个统一的模型接入网关。你只需要在官网注册后拿到一个 API Key,然后把所有模型的请求都指向同一个 Base URL,就能用同一套鉴权方式调用不同模型。对复现基准来说,这带来的直接好处是:评测脚本里的模型配置可以抽成一个字典,切换模型只改model字段,不用动请求代码。
先把关键地址列清楚,方便你复制:
- 官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- API Base URL:https://taotoken.net/api
- 模型对话(用于快速验证模型是否可用):https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
- API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
- Coding Plan(长期编码/Agent 场景):https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
这里要强调一点:TaoToken 是合规的模型接入服务,不是所谓的“中转”或灰色通道。它的接口遵循 OpenAI 兼容格式,所以你用openai这个 Python 包就能直接请求,不需要额外装奇怪的 SDK。对于 ESI-BENCH 这种需要批量跑任务的场景,OpenAI 兼容格式意味着你可以复用大量现成的评测代码。
接下来是环境变量配置。我建议把 Base URL 和 Key 都放在环境变量里,不要写死在脚本中。这样在 CI 或不同机器上跑评测时,只需要改环境变量,不用改代码。下面是我实际用的.env片段,你可以直接复制:
# TaoToken 统一接入配置 export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_API_KEY="sk-你的实际Key" # 评测用的模型 ID,按需替换 export ESI_MODEL_ID="gpt-4o"如果你用的是python-dotenv,可以在脚本开头load_dotenv(),然后通过os.getenv读取。注意 API Key 不要提交到 Git,建议把.env加进.gitignore。
对于 Claude Code 这类需要 Anthropic 协议的工具,TaoToken 也提供了对应的接入方式。如果你要在 Claude Code 里做代码润色或辅助分析,Base URL 同样指向https://taotoken.net/api,Key 用同一个,模型 ID 填 Claude 系列即可。具体路径可以参考接入文档里的 ClaudeCodeAnthropic 章节。这里不展开,因为本文主线是 ESI-BENCH 评测。
还有一个容易被忽略的点:模型 ID 的命名。不同供应商对同一个模型的命名可能不同,比如gpt-4o、gpt-4o-2024-08-06、gemini-1.5-pro等。在 TaoToken 的模型对话页面可以查到当前支持的模型列表,建议先在那里确认你要用的模型 ID,再写进评测脚本。否则很容易遇到model not found这类报错。
3. 可复制配置:把 ESI-BENCH 评测脚本接到统一网关
这一节是全文的核心,我会给出一个可以直接跑的 Python 配置片段,把 ESI-BENCH 的评测请求接到 TaoToken 上。为了让配置更清晰,我分成三部分:客户端初始化、任务请求构造、结果解析。你可以把这三段拼成一个完整的脚本。
先看客户端初始化。因为 TaoToken 兼容 OpenAI 格式,所以直接用openai包:
import os from openai import OpenAI client = OpenAI( base_url=os.getenv("TAOTOKEN_BASE_URL", "https://taotoken.net/api"), api_key=os.getenv("TAOTOKEN_API_KEY"), ) MODEL_ID = os.getenv("ESI_MODEL_ID", "gpt-4o")这段代码的关键是base_url指向 TaoToken 的 API 地址,api_key从环境变量读取。这样你切换模型时只需要改ESI_MODEL_ID,不用动客户端。
接下来是任务请求构造。ESI-BENCH 的任务本质上是给模型一段观测描述(或图像)加一个问题,要求模型输出答案和置信度。在真实评测中,观测来自 OmniGibson 渲染的图像;在没有仿真环境时,我们可以用文本描述模拟一次“感知-行动回路”的请求,验证调用链路是否通。下面是一个模拟请求:
def build_esi_prompt(task_type, observation, question): system_prompt = ( "你是一个具身空间智能评测助手。" "你需要根据当前观测回答空间推理问题," "并给出 0 到 1 之间的置信度。" "如果证据不足,请明确说明需要哪种动作来获取更多信息。" ) user_prompt = f""" 任务类型:{task_type} 当前观测:{observation} 问题:{question} 请按以下 JSON 格式输出: {{ "answer": "你的答案", "confidence": 0.0, "needed_action": "如果需要更多信息,填写建议动作,否则填 none" }} """ return system_prompt, user_prompt system_prompt, user_prompt = build_esi_prompt( task_type="occlusion_counting", observation="你看到一个架子,上层有3个盒子,下层被一个挡板遮住,只能看到部分区域。", question="架子上总共有多少个盒子?", ) response = client.chat.completions.create( model=MODEL_ID, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], temperature=0, ) print(response.choices[0].message.content)这段代码里,task_type对应 ESI-BENCH 的任务大类,比如occlusion_counting(遮挡计数)、rigid_containment(刚性容纳)、spatial_distance(空间距离)等。observation是当前视角能看到的信息,question是任务问题。要求模型输出 JSON,是为了后续能程序化解析answer和confidence,方便和 Ground Truth 对比。
如果你要跑真实图像输入,可以把messages里的content改成多模态格式:
messages = [ {"role": "system", "content": system_prompt}, { "role": "user", "content": [ {"type": "text", "text": user_prompt}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,你的图像base64"}}, ], }, ]注意图像 base64 不要太大,否则请求体可能超限。实测下来,OmniGibson 渲染的 512x512 图像转 base64 后大约几百 KB,是可以接受的。
再给一个 TOML 配置片段,方便你把评测参数外置。如果你用tomllib或toml包读取,可以这样写:
[taotoken] base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" [esi_bench] model_id = "gpt-4o" temperature = 0 max_tokens = 512 task_types = ["occlusion_counting", "rigid_containment", "spatial_distance"]这样评测脚本读 TOML 就能拿到所有配置,切换模型或任务类型不用改代码。对于需要跑多个模型对比的实验,这个结构会省很多事。
最后提醒一点:如果你在脚本里用了requests而不是openai包,记得请求头是Authorization: Bearer <key>,路径是/v1/chat/completions。TaoToken 的 Base URL 是https://taotoken.net/api,所以完整地址是https://taotoken.net/api/v1/chat/completions。这个路径在接入文档里有说明,建议对照确认。
4. 验证请求与结果校验:一次基准任务的完整调用
配置写好后,下一步是实际发一次请求,确认返回结果符合预期。这一节我会演示完整的调用过程,并给出结果校验的代码。校验的重点是:模型是否返回了合法 JSON、answer字段是否可解析、confidence是否在 0 到 1 之间、needed_action是否合理。
先跑一次请求。假设你已经设置好环境变量,执行上面的脚本,你会看到类似下面的输出:
{ "answer": "无法确定,因为下层被挡板遮挡,需要移动视角或移除挡板才能计数。", "confidence": 0.35, "needed_action": "move_backward" }这个结果说明调用链路是通的。模型没有瞎猜一个数字,而是识别出遮挡导致信息不足,并建议了一个动作move_backward。这正好对应 ESI-BENCH 强调的“主动探索”逻辑:模型应该知道自己缺什么证据,而不是过早承诺答案。
接下来做结果校验。我写了一个validate_response函数,把返回内容解析成字典并检查字段:
import json def validate_response(content): try: data = json.loads(content) except json.JSONDecodeError: return False, "返回内容不是合法 JSON" required_fields = ["answer", "confidence", "needed_action"] for field in required_fields: if field not in data: return False, f"缺少字段:{field}" confidence = data["confidence"] if not isinstance(confidence, (int, float)) or not (0 <= confidence <= 1): return False, f"confidence 不合法:{confidence}" if not isinstance(data["answer"], str) or not data["answer"].strip(): return False, "answer 为空" return True, data ok, result = validate_response(response.choices[0].message.content) if ok: print("校验通过:", result) else: print("校验失败:", result)这段代码会检查 JSON 合法性、字段完整性、置信度范围、答案非空。对于 ESI-BENCH 的批量评测,你可以把每个任务的返回都过一遍这个校验,统计通过率。如果某个模型经常返回非法 JSON,说明它的指令遵循能力有问题,这本身就是一个值得记录的指标。
再进一步,你可以把校验结果和 Ground Truth 对比,计算准确率。ESI-BENCH 的任务大多有明确的正确答案,比如遮挡计数是整数、刚性容纳是布尔值、空间距离是选项。下面是一个简单的对比逻辑:
def compare_with_ground_truth(prediction, ground_truth): pred_answer = prediction["answer"].strip().lower() gt_answer = str(ground_truth).strip().lower() return pred_answer == gt_answer对于数值型答案,你可能需要先做归一化,比如把“三个”转成“3”。对于选项型答案,直接字符串匹配即可。实测下来,模型在计数任务上经常输出“无法确定”,这时候准确率会很低,但这恰恰反映了 ESI-BENCH 论文里提到的“行动失明”问题:模型不知道该怎么动,只能保守回答。
还有一个校验动作是检查needed_action是否在合法动作空间内。ESI-BENCH 的动作空间包括移动(前后左右、上下)、感知(左右转头、上下俯仰)、操作(拾取、放置、注水、倾倒)、终止(提交答案)。你可以定义一个合法动作集合,检查模型建议的动作是否在其中:
VALID_ACTIONS = { "move_forward", "move_backward", "move_left", "move_right", "move_up", "move_down", "turn_left", "turn_right", "look_up", "look_down", "pick_up", "place_on", "place_in", "pour", "fill", "submit" } def validate_action(action): return action in VALID_ACTIONS or action == "none"如果模型频繁输出不在集合里的动作,说明它对动作空间的理解有偏差。这个指标在论文里对应“行动失明”的量化分析,你可以自己跑一批任务统计一下。
最后,把一次完整调用的日志记下来,包括请求时间、模型 ID、任务类型、返回内容、校验结果。这样在排查问题时能快速定位是模型问题还是配置问题。我习惯把日志写成 JSONL,每行一条记录,方便后续用 pandas 分析。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
复现评测时,报错是难免的。这一节我整理了几个高频错误,对照真实报错信息给出排查路径。这些错误大多和接入配置有关,而不是 ESI-BENCH 本身的问题。
401 Unauthorized。这是最常见的错误,通常有三种原因。第一,API Key 没设置或设置错了。检查TAOTOKEN_API_KEY环境变量是否为空,或者 Key 是否复制时多了空格。第二,请求头格式不对。如果你用requests,确认是Authorization: Bearer sk-xxx,注意Bearer后面有一个空格。第三,Key 被禁用或额度耗尽。这时候去 API Keys 页面确认 Key 状态。我踩过的坑是:在.env里写了export,但用python-dotenv读取时export会被当成值的一部分,导致 Key 变成export sk-xxx。解决办法是.env里不要写export,直接写TAOTOKEN_API_KEY=sk-xxx。
local proxy failed。这个报错通常出现在你本地设置了 HTTP 代理,但代理不可用或配置冲突。注意,这里说的代理是系统层面的网络代理设置,不是让你去用什么特殊工具。排查方法是检查环境变量HTTP_PROXY、HTTPS_PROXY是否指向了一个不可用的地址。如果你在公司内网,可能需要走内网代理,那就确认代理地址和端口是否正确。如果不需要代理,直接unset HTTP_PROXY HTTPS_PROXY再跑一次。另外,有些 Python 库会读取ALL_PROXY,也一并检查。
reading choices 报错。这个错误通常表现为AttributeError: 'NoneType' object has no attribute 'choices'或者KeyError: 'choices'。原因是 API 返回的结构和你预期的不一样。可能的情况有:请求失败但没抛异常,返回体里是error字段而不是choices;或者你用的 SDK 版本和接口不兼容。排查方法是先把原始返回打印出来:
print(response.model_dump_json(indent=2))如果是error,里面会有具体错误信息,比如model not found、invalid api key、rate limit exceeded。如果是model not found,去模型对话页面确认模型 ID 是否正确。如果是rate limit exceeded,说明请求太频繁,加个time.sleep(1)或降低并发。
OAuth 相关报错。如果你在用 Claude Code 或某些需要 OAuth 的工具,可能会遇到OAuth token expired或invalid_grant。这类错误通常和工具本身的登录态有关,不是 TaoToken 的问题。解决办法是重新走一遍工具的登录流程,或者在配置里改用 API Key 方式而不是 OAuth。对于 Claude Code,接入文档里有专门的 ClaudeCodeAnthropic 配置说明,建议对照检查 Base URL 和 Key 是否填对。
除了这些,还有一个容易忽略的问题:模型返回的 JSON 被 Markdown 代码块包裹。比如模型输出:
```json {"answer": "3", "confidence": 0.8, "needed_action": "none"}这时候 `json.loads` 会失败,因为前面有 ```json。解决办法是在解析前先剥离代码块标记: ```python def strip_code_block(content): content = content.strip() if content.startswith("```"): lines = content.split("\n") lines = lines[1:] if lines and lines[-1].strip() == "```": lines = lines[:-1] content = "\n".join(lines) return content这个坑我在跑批量评测时遇到过,模型有时候会加代码块,有时候不加,导致解析逻辑要兼容两种情况。建议在validate_response里先调用strip_code_block。
最后,如果你在 ESI-BENCH 的仿真环境里跑,还可能遇到 OmniGibson 渲染失败、GPU 显存不足等问题。这些和模型接入无关,建议先单独跑通仿真器的示例脚本,再接入模型评测。分阶段排查,比一上来就端到端跑要高效得多。
6. 把评测流程跑通之后:统一 Key 带来的复现效率
写到这里,评测链路基本讲完了。从环境变量配置、客户端初始化、任务请求构造,到结果校验和报错排查,这套流程可以让你在没有完整仿真环境的情况下,先把模型接入和结果解析这一段跑通。等你有了 OmniGibson 环境,只需要把文本观测替换成渲染图像,其余逻辑不用大改。
回到 ESI-BENCH 本身,它最有价值的地方不是又刷了一个榜,而是把“感知-行动回路”这个命题摆到了台面上。论文里的几个结论——主动探索有效、行动失明大于感知失明、瑕疵 3D 比 2D 更坑、元认知是核心差距——都在提醒我们:空间智能不是单纯的视觉问题,而是一个决策问题。模型需要知道什么时候该动、动哪里、动完之后怎么修正判断。这个思路对做 Agent 的开发者同样有启发:不要只优化单步感知,要把动作选择纳入评测。
对于想复现这个基准的开发者,我的建议是先把评测脚本的模型接入层抽象出来。用 TaoToken 这样的统一网关,把 Base URL 和 Key 收敛到一处,模型 ID 做成配置项。这样你在对比 GPT、Gemini、Claude 时,只需要改一个字段,不用重写请求逻辑。实测下来,这种结构能省掉大量重复劳动,尤其是在跑几十个任务、多个模型的批量评测时。
如果你还没有 Key,可以去官网注册一个,然后在 API Keys 页面生成。生成后先别急着跑批量任务,用模型对话页面发一条简单请求,确认 Key 和 Base URL 都通。这一步花两分钟,能避免后面很多 401 报错。接入文档里有完整的请求示例,包括 curl 和 Python 两种方式,建议对照跑一遍。
最后留一个实用技巧:在批量评测时,给每个请求加一个唯一 ID,把请求参数、返回内容、校验结果、耗时都记到 JSONL 里。这样当某个任务失败时,你可以直接根据 ID 定位到原始请求和返回,不用在一堆日志里翻。这个习惯在复现任何基准时都很有用,尤其是当你要对比不同模型在同一任务上的表现时,结构化日志能让分析效率提升一个量级。