AgentWorldBench 三段流水线 深度解析
本页只展开一个模块,把导入契约、核心实现、流程和失败模式拆成可回溯行号。
导入链与契约
入口代码说明该模块接受什么输入、输出什么结构,以及它依赖哪些相邻模块。
▸入口片段
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 阶段会识别这个空输出。
核心实现,逐段拆
▸中段状态转换
这一段通常体现最关键的状态转换:解析、选择、打分、写入或安全检查。
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 预加载。
▸末段收束
收束段决定失败是否显式返回、结果是否稳定落盘,以及调用者能否继续恢复。
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 两段消息。
关键流程
▸流程拆解
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 只读已评分文件,说明聚合与模型调用解耦。
边界 / 失败模式
▸边界列表
- 空输入或缺失字段必须被显式处理。
- 外部命令、模型调用或文件写入失败不能覆盖原始上下文。
- 路径别名仅用于索引兼容;实际源码位置见每个代码块的 repo 标注。