qwen-agentworld7 域 LWM 评测
§ · Nested detail page

AgentWorldBench 三段流水线 深度解析

本页只展开一个模块,把导入契约、核心实现、流程和失败模式拆成可回溯行号。

§ 00 · IMPORT CHAIN

导入链与契约

入口代码说明该模块接受什么输入、输出什么结构,以及它依赖哪些相邻模块。

▸入口片段

src/eval/eval.py · L78-113 repo: eval/eval.pyPython
  78def run_inference(
  79    jobs: List[Dict[str, Any]],
  80    client: OpenAI,
  81    model_name: str,
  82    max_tokens: int = 32768,
  83    temperature: float = 0.6,
  84) -> List[Dict[str, Any]]:
  85    """Run world model inference on all jobs."""
  86    total = len(jobs)
  87    for i, job in enumerate(jobs):
  88        subtask = get_subtask(job)
  89
  90        system_prompt = job.get("system_str", "")
  91        current_prompt = job.get("current_prompt", "")
  92        if not current_prompt:
  93            prompts = job.get("prompt", [])
  94            turn_idx = job.get("turn_idx", 1) - 1
  95            if isinstance(prompts, list) and turn_idx < len(prompts):
  96                current_prompt = prompts[turn_idx]
  97
  98        messages = []
  99        if system_prompt:
 100            messages.append({"role": "system", "content": system_prompt})
 101        messages.append({"role": "user", "content": current_prompt})
 102
 103        try:
 104            response = client.chat.completions.create(
 105                model=model_name,
 106                messages=messages,
 107                max_tokens=max_tokens,
 108                temperature=temperature,
 109            )
 110            gen = response.choices[0].message.content or ""
 111        except Exception as e:
 112            logger.warning(f"[{i+1}/{total}] Inference failed for {job.get('id')}: {e}")
 113            gen = ""

这段不是普通“调用模型”包装。src/eval/eval.py:78 让 inference 接收 job 列表而不是单条样本,说明评测默认是批处理;src/eval/eval.py:82 和 src/eval/eval.py:83 把输出长度、采样温度显式暴露,便于复现实验条件;src/eval/eval.py:87 开始逐条样本处理,所以失败会被写回对应 job,而不是中断整批。

输入选择的关键在 src/eval/eval.py:90 到 src/eval/eval.py:96:如果样本已经有 current_prompt 就直接使用,否则根据 turn_idx 从 prompt 历史里取当前轮。这解释了 AgentWorldBench 为什么能评估多轮环境模拟:src/eval/eval.py:94 把一基索引转成零基索引,src/eval/eval.py:95 又避免越界读取历史轮次。

消息组装保持了 OpenAI-compatible API 的最小契约。src/eval/eval.py:98 初始化 messages,src/eval/eval.py:99 到 src/eval/eval.py:101 只放 system 与 user 两类消息;真正的服务调用在 src/eval/eval.py:104,模型名来自 src/eval/eval.py:105,消息体来自 src/eval/eval.py:106,token 和 temperature 则分别在 src/eval/eval.py:107、src/eval/eval.py:108 进入请求。异常路径也有明确落点:src/eval/eval.py:112 记录失败,src/eval/eval.py:113 把生成置空,后面的 judge 阶段会识别这个空输出。

§ 01 · CORE IMPL

核心实现,逐段拆

▸中段状态转换

这一段通常体现最关键的状态转换:解析、选择、打分、写入或安全检查。

src/eval/eval.py · L114-153 repo: eval/eval.pyPython
 114
 115        job["gen"] = gen
 116        if (i + 1) % 10 == 0 or (i + 1) == total:
 117            logger.info(f"[{i+1}/{total}] Inference progress")
 118
 119    return jobs
 120
 121
 122# ─── Judge ────────────────────────────────────────────────────────────────────
 123
 124def build_judge_messages(
 125    job: Dict[str, Any],
 126    model_output: str,
 127    judge_system_prompts: Dict[str, str],
 128) -> List[Dict[str, str]]:
 129    """Build LLM Judge evaluation messages."""
 130    subtask = get_subtask(job)
 131    prompts = job.get("prompt", [])
 132    responses = job.get("response", [])
 133    if isinstance(prompts, str):
 134        prompts = [prompts]
 135    if isinstance(responses, str):
 136        responses = [responses]
 137
 138    turn_idx = max(job.get("turn_idx", 1) - 1, 0)
 139
 140    context = ""
 141    for i in range(turn_idx):
 142        if i < len(prompts) and i < len(responses):
 143            context += prompts[i] + "\n" + responses[i] + "\n\n"
 144    if context:
 145        context = f"# Context (Historical Interactions):\n\n{context}"
 146
 147    current_prompt = job.get("current_prompt", "")
 148    if not current_prompt and turn_idx < len(prompts):
 149        current_prompt = prompts[turn_idx]
 150
 151    ground_truth_raw = responses[turn_idx] if turn_idx < len(responses) else ""
 152
 153    model_output_clean = clean_response_marker(model_output, subtask)

推理结果不是立刻评分,而是先写回同一个 job。src/eval/eval.py:115 把生成文本保存为 gen 字段,src/eval/eval.py:116 和 src/eval/eval.py:117 只做进度日志,不改变数据;因此 src/eval/eval.py:119 返回的仍是一组扩展后的 job。judge 构造从 src/eval/eval.py:124 开始,参数里的 src/eval/eval.py:126 表示它吃的是已解析的模型输出,src/eval/eval.py:127 表示系统 prompt 已经按 domain 预加载。

▸末段收束

收束段决定失败是否显式返回、结果是否稳定落盘,以及调用者能否继续恢复。

src/eval/eval.py · L132-167 repo: eval/eval.pyPython
 132    responses = job.get("response", [])
 133    if isinstance(prompts, str):
 134        prompts = [prompts]
 135    if isinstance(responses, str):
 136        responses = [responses]
 137
 138    turn_idx = max(job.get("turn_idx", 1) - 1, 0)
 139
 140    context = ""
 141    for i in range(turn_idx):
 142        if i < len(prompts) and i < len(responses):
 143            context += prompts[i] + "\n" + responses[i] + "\n\n"
 144    if context:
 145        context = f"# Context (Historical Interactions):\n\n{context}"
 146
 147    current_prompt = job.get("current_prompt", "")
 148    if not current_prompt and turn_idx < len(prompts):
 149        current_prompt = prompts[turn_idx]
 150
 151    ground_truth_raw = responses[turn_idx] if turn_idx < len(responses) else ""
 152
 153    model_output_clean = clean_response_marker(model_output, subtask)
 154    ground_truth_clean = clean_response_marker(ground_truth_raw, subtask)
 155
 156    user_prompt = JUDGE_USER_PROMPT.format(
 157        context=context,
 158        world_model_input=f"# Current Turn:\n\n{current_prompt}",
 159        predicted_observation=f"**World Model Output (Simulated):**\n```\n{model_output_clean}\n```",
 160        ground_truth=f"**Ground Truth (Real Output):**\n```\n{ground_truth_clean}\n```",
 161    ).strip()
 162
 163    return [
 164        {"role": "system", "content": judge_system_prompts.get(subtask, "")},
 165        {"role": "user", "content": user_prompt},
 166    ]
 167

judge prompt 的核心是把历史、当前输入、预测输出和真实输出合成一个评审请求。src/eval/eval.py:132 读取真实 response,src/eval/eval.py:133 到 src/eval/eval.py:136 把字符串统一包成列表;src/eval/eval.py:138 决定当前轮,src/eval/eval.py:141 到 src/eval/eval.py:145 把当前轮之前的交互拼成历史上下文。这样做的原因是 judge 需要比较状态连续性,而不是只看单轮文本。

当前轮材料在 src/eval/eval.py:147 和 src/eval/eval.py:149 间被兜底选择,真实答案在 src/eval/eval.py:151 取出。然后 src/eval/eval.py:153 清理模型输出 marker,src/eval/eval.py:154 对 ground truth 做同样清理,避免格式前缀污染评分。最终 src/eval/eval.py:156 到 src/eval/eval.py:161 填充统一模板,src/eval/eval.py:163 到 src/eval/eval.py:166 返回 system/user 两段消息。

§ 02 · KEY FLOW

关键流程

▸流程拆解

input → normalize → validate → output
failure → explicit fallback / error record → caller decision

CLI 把三段流水线拆成可单独复跑的命令。src/eval/eval.py:302 创建 parser,src/eval/eval.py:304 要求 subcommand;src/eval/eval.py:307 定义 infer,src/eval/eval.py:317 定义 judge,src/eval/eval.py:328 定义 score。infer 分支在 src/eval/eval.py:333 进入,src/eval/eval.py:334 读数据,src/eval/eval.py:336 运行推理,src/eval/eval.py:340 固定输出到 predictions.jsonl,src/eval/eval.py:343 一行一个 job 写回,便于 judge 阶段继续读取。

如果后续改动触碰这些行,主页面对应的风险判断也要重跑。judge 分支从 src/eval/eval.py:346 开始,src/eval/eval.py:347 到 src/eval/eval.py:348 读取 predictions,src/eval/eval.py:350 处理 judge API key,src/eval/eval.py:352 写入评分结果,src/eval/eval.py:356 固定 judged.jsonl,src/eval/eval.py:361 立即聚合。score 分支在 src/eval/eval.py:363 到 src/eval/eval.py:366 只读已评分文件,说明聚合与模型调用解耦。

§ 03 · EDGE CASES

边界 / 失败模式

▸边界列表

  • 空输入或缺失字段必须被显式处理。
  • 外部命令、模型调用或文件写入失败不能覆盖原始上下文。
  • 路径别名仅用于索引兼容;实际源码位置见每个代码块的 repo 标注。