跳到主要内容

01 · 回合制主循环与双控环境

本章讲 tau2-bench 的「引擎」:一通电话是怎么被一步步跑出来的,以及 τ² 相对老 τ-bench 的核心升级——双控(dual control)

1.1 三个角色,一个传声筒

对话里只有三种角色,定义在 Role(orchestrator.py:41):

角色是谁能干什么
AGENT被考的客服 LLM对 User 说话,或对 Env 调工具(二选一,不能同时)
USER扮顾客的 LLM 模拟器对 Agent 说话,或对 Env 调工具(部分领域有用户工具)
ENV环境(工具+数据库)只被动响应:谁调工具就把结果还给谁

Orchestrator 本身不产生内容,它只是个带状态的传声筒:记住「当前这条消息 message 是从 from_role 发出、要给 to_role」,每走一步就推进一次(orchestrator.py:452-455)。

关键约束(协议规则,orchestrator.py:366-377): 一条消息要么是文字、要么是工具调用,绝不能又是文字又是工具调用,也不能为空。这条规则让「该找谁说话」变得确定:看消息是不是工具调用即可。

1.2 主循环长什么样

run() 是标准的模板方法(orchestrator.py:260):初始化 → 反复 step 直到 done → 收尾。伪代码直觉:

# 示意,非源码。重点看:done 之前不停 step,每步后查是否该终止
def run(self):
self.initialize() # 用 task 开局:设环境、设对话历史、发第一句
while not self.done:
self.step() # 传一次话(可能触发一次工具执行)
self._check_termination() # 步数/错误/超时到顶了吗?
return self._finalize() # 打包成 SimulationRun

真实实现里 run 还包了 try/finally 做「异常时应急清理」(关 WebSocket 等),见 orchestrator.py:277-291。

step 的路由逻辑(这是引擎的心脏)

step()(orchestrator.py:823)就是一张三分支的路由表。怎么读下面这张图:看当前消息从谁来、要给谁,决定这一步做什么

from → to 这一步干的事
───────────────── ─────────────────────────────────────────────
AGENT/ENV → USER ─▶ 叫 UserSim 生成下一句;若含 ###STOP### 则 done
USER/ENV → AGENT ─▶ 叫 Agent 生成下一句;若是工具调用 → 下一步转 ENV
AGENT/USER→ ENV ─▶ 真去执行工具,结果打包回发起方(to=from, from=ENV)

每分支做完都会:把新消息塞进 trajectory、翻转 from_role/to_rolestep_count += 1、再 environment.sync_tools()(orchestrator.py:897-900)。最后这步 sync_tools 是双控的关键,1.4 节细讲。

注意一个巧妙的时序:当 to_role == ENV(正等环境响应)时,_check_termination 直接跳过步数检查(orchestrator.py:741)——不能让「一次工具调用还没拿到结果」就被步数上限硬砍断,否则轨迹是残缺的。

1.3 什么时候停、怎么算「正常停」

终止原因是一个枚举 TerminationReason(simulation.py:1234)。分两类,这个分类直接决定能不能得分

类别具体原因评测后果
正常结束USER_STOP(顾客满意挂电话)、AGENT_STOP(solo agent 主动收工)允许进入打分
非正常结束MAX_STEPSTOO_MANY_ERRORSTIMEOUTAGENT_ERRORUSER_ERRORINFRASTRUCTURE_ERROR直接 0 分(见 03 章)

顾客怎么「挂电话」?UserSim 在回复里吐出三个哨兵串之一:###STOP### / ###TRANSFER### / ###OUT-OF-SCOPE###(user_simulator_base.py:51-53,判定在 UserSimulator.is_stop,user_simulator.py:183)。裁判把「达成任务后满意挂断」当成成功信号,所以顾客愿不愿意挂电话,本身就是任务完成度的一部分

错误计数:每次工具返回 error=Truenum_errors += 1(orchestrator.py:326-328),累计到 max_errors(默认 10)就以 TOO_MANY_ERRORS 收场。

1.4 双控:τ² 相对 τ-bench 的核心升级

要解决的小问题: 老 τ-bench 里,只有 Agent 能调工具,用户只会说话。但现实客服里大量事情必须用户自己动手——「请您在手机设置里打开飞行模式再关掉」「请您点邮件里的验证链接」。Agent 不能替用户按手机。

τ² 的做法: 环境同时持有两套工具——tools(给 Agent)和 user_tools(给 User),见 Environment.__init__(environment.py:39-63)。make_tool_callrequestor 字段路由到对应工具集(environment.py:158-185)。于是 User 也能调工具改环境。

为什么这让任务变难: Agent 现在要在对话里精确指挥用户完成一串操作,还要根据用户工具返回的结果判断下一步。这考的不是「会不会调 API」,而是「会不会带着人把流程走对」。

双控最精彩的例子:telecom 的 sync_tools

光有两套工具还不够——两套工具改的必须是「同一个耦合的世界」。telecom 领域把这点做到了极致,看 TelecomEnvironment.sync_tools(telecom/environment.py:39)。每步之后它都重新算一遍「用户手机现在的处境」,让 Agent 侧数据库和 User 侧设备状态保持物理一致:

Agent 改了账户 DB sync_tools 每步重算 User 侧「手机处境」
───────────────── ────────────────── ──────────────────
给某条线开了 roaming ─▶ 读线路 roaming_enabled ─▶ surroundings.roaming_allowed=True
线路欠费被停机 ─▶ 读线路 status ─▶ surroundings.line_active=False
数据用量超套餐 ─▶ 比 data_used vs 套餐上限 ─▶ mobile_data_usage_exceeded=True
用户在自己手机上付了款 ─▶ 读 payment_request.paid ─▶ 反向把账单标记为已付(回写 Agent DB)

最后一条是双向的:用户的动作反过来改了 Agent 的数据库(telecom/environment.py:76-80)。这就是「双控」的字面意思——两个控制者、一个共享现实。要读双控怎么落到具体领域,sync_tools 是最该看的一段。

1.5 solo 模式:把用户抽掉

有时你只想测「给足信息后,Agent 能不能自己把事办对」,不想让 user 模拟器的噪声干扰。这就是 solo 模式:User 换成 DummyUser(一个会对任何调用抛异常的占位,user_simulator.py:269),Agent 换成 LLMSoloAgent

此时规则收紧(orchestrator.py:379-384、728-732):Agent 只能调工具,不能对用户说话(因为没有用户),唯一例外是发停止信号。任务改用 ticket 字段(一段工单文本)替代对话式的 user_scenario。solo agent 干完活要显式调 done 工具收工,触发 ###STOP###(llm_agent.py:329-364)。

边界提醒:solo 模式下环境也要进 solo(env.solo_mode=True),且要求 Agent 工具和 User 工具名字不能重叠,否则路由会歧义(validate_solo_mode,environment.py:432-444)。

1.6 代码地图

主题文件符号名
主循环模板src/tau2/orchestrator/orchestrator.pyBaseOrchestrator.run
回合路由src/tau2/orchestrator/orchestrator.pyOrchestrator.step
开局设置src/tau2/orchestrator/orchestrator.pyOrchestrator.initialize
角色枚举src/tau2/orchestrator/orchestrator.pyRole
协议校验src/tau2/orchestrator/orchestrator.pyOrchestrator._check_communication_error
终止判定src/tau2/orchestrator/orchestrator.pyOrchestrator._check_termination
终止原因src/tau2/data_model/simulation.pyTerminationReason
顾客挂电话src/tau2/user/user_simulator.pyUserSimulator.is_stop
双控工具路由src/tau2/environment/environment.pyEnvironment.make_tool_call
双控状态耦合(范例)src/tau2/domains/telecom/environment.pyTelecomEnvironment.sync_tools