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_role、step_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_STEPS、TOO_MANY_ERRORS、TIMEOUT、AGENT_ERROR、USER_ERROR、INFRASTRUCTURE_ERROR 等 | 直接 0 分(见 03 章) |
顾客怎么「挂电话」?UserSim 在回复里吐出三个哨兵串之一:###STOP### / ###TRANSFER### / ###OUT-OF-SCOPE###(user_simulator_base.py:51-53,判定在 UserSimulator.is_stop,user_simulator.py:183)。裁判把「达成任务后满意挂断」当成成功信号,所以顾客愿不愿意挂电话,本身就是任务完成度的一部分。
错误计数:每次工具返回 error=True 就 num_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_call 按 requestor 字段路由到对应工具集(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.py | BaseOrchestrator.run |
| 回合路由 | src/tau2/orchestrator/orchestrator.py | Orchestrator.step |
| 开局设置 | src/tau2/orchestrator/orchestrator.py | Orchestrator.initialize |
| 角色枚举 | src/tau2/orchestrator/orchestrator.py | Role |
| 协议校验 | src/tau2/orchestrator/orchestrator.py | Orchestrator._check_communication_error |
| 终止判定 | src/tau2/orchestrator/orchestrator.py | Orchestrator._check_termination |
| 终止原因 | src/tau2/data_model/simulation.py | TerminationReason |
| 顾客挂电话 | src/tau2/user/user_simulator.py | UserSimulator.is_stop |
| 双控工具路由 | src/tau2/environment/environment.py | Environment.make_tool_call |
| 双控状态耦合(范例) | src/tau2/domains/telecom/environment.py | TelecomEnvironment.sync_tools |