ReWOO:先排工具调用,再批量执行
ReAct 每调完一个工具就问模型:接下来干什么?
灵活,但慢。旅游助手需要查天气、查汇率、查开放时间、查地铁路线——这四个调用互相不依赖,ReAct 却要串行跑四轮模型调用。
ReWOO 的做法:先让模型把所有工具调用排出来,Python 批量执行,然后模型读完所有结果一次回答。
一句话说清楚
ReWOO 把"观察一次、决定一次"改成"先排好所有工具调用、批量执行、再一次回答",减少不必要的模型轮次。
解决什么问题
| 问题 | 表面看起来 | 实际风险 |
|---|---|---|
| ReAct 每次工具调用后都问模型 | 灵活 | 独立调用白白串行,延迟翻倍 |
| 独立工具调用串行跑 | 简单 | 延迟随工具数线性增长 |
| 每次观察后没有明确目的 | 模型能回答 | 不知道每次工具调用到底为了什么 |
旅游助手要查 4 样东西,ReAct 要跑 4 轮模型调用(每轮决定下一步)+ 4 次工具调用 = 8 次。ReWOO 只要 1 次模型调用(排计划)+ 4 次工具调用 + 1 次模型调用(综合)= 6 次,而且工具调用可以并行。
引入什么复杂度
- 计划是一次性排的——如果第一个工具的结果会影响后续工具的参数,ReWOO 处理不了。
- 工具调用失败时,没有中途补救机制。
- Solver 一次看完所有 observation,上下文可能膨胀。
和其他模式的关系
| 模式 | 谁决定下一步 | 什么时候用 |
|---|---|---|
| ReAct | 每次观察后模型选动作 | 调用之间有强依赖 |
| ReWOO | 提前排好所有调用 | 调用大部分独立 |
| LLM Compiler | 模型建依赖图 | 有依赖也有并行 |
| Workflow | Python 写死步骤 | 步骤完全已知 |
ReWOO 比 ReAct 省轮次,但比 LLM Compiler 简单——不处理依赖图,只处理"一批独立调用"。
这个模式改变了什么
| 谁 | 负责什么 |
|---|---|
| Planner 模型 | 输出工具调用列表(含 tool、args、purpose) |
| Python | 批量执行工具、处理失败、收集结果 |
| Solver 模型 | 读所有 observation,写最终答案 |
- 谁决定下一步:没有"下一步"。计划一次排完,执行一次做完。
- 谁拥有状态:Python 持有调用列表和结果列表。
- 什么时候停止:所有工具执行完 + Solver 给出答案。
走一遍真实轨迹
用户请求:"杭州三天,查一下天气、西湖门票、从上海到杭州的高铁时间。"
| 阶段 | 内容 | 输出 |
|---|---|---|
| 排计划 | 模型列出 3 个工具调用 | [weather("杭州"), ticket("西湖"), train("上海","杭州")] |
| 批量执行 | Python 并行跑 3 个工具 | 天气:"Day1 晴...";门票:"免费";高铁:"50 分钟" |
| 综合 | Solver 读完 3 个结果 | "杭州三天天气晴好,西湖免费,上海出发高铁 50 分钟..." |
关键:3 个工具调用之间没有依赖,ReWOO 一批搞定。如果"门票价格取决于天气(雨天免费)",那就不能用 ReWOO,要用 ReAct 或 LLM Compiler。
流程图
flowchart TD
T["用户任务"] --> P["模型排工具调用列表"]
P --> E["Python 批量执行工具"]
E --> O["收集所有 observation"]
O --> S["Solver 模型写最终答案"]
完整实现
from __future__ import annotations
from pathlib import Path
from agent_patterns_lab.patterns.rewoo import rewoo
from agent_patterns_lab.runtime import MockLLM, Tool, ToolRegistry, Tracer
def main() -> None:
tracer = Tracer()
# 模拟工具
def weather(args: dict) -> str:
return f"{args['city']}未来三天:Day1 晴 28°C,Day2 多云 26°C,Day3 晴 29°C。"
def ticket(args: dict) -> str:
return f"{args['attraction']}门票:免费(需预约)。"
def train(args: dict) -> str:
return f"{args['from']}→{args['to']}高铁:最快 50 分钟,二等座 73 元。"
tools = ToolRegistry(
[
Tool(name="weather", description="查询城市天气", handler=weather),
Tool(name="ticket", description="查询景点门票", handler=ticket),
Tool(name="train", description="查询高铁信息", handler=train),
]
)
model = MockLLM(
[
# 第一次调用:排工具计划
'{"tool_calls":['
'{"tool":"weather","args":{"city":"杭州"},"purpose":"查天气"},'
'{"tool":"ticket","args":{"attraction":"西湖"},"purpose":"查门票"},'
'{"tool":"train","args":{"from":"上海","to":"杭州"},"purpose":"查高铁"}'
"]}",
# 第二次调用:综合答案
(
"杭州未来三天天气好(晴/多云),适合出行。"
"西湖免费但需预约。"
"上海到杭州高铁 50 分钟,二等座 73 元。"
"建议提前预约西湖,选早班高铁避开人流。"
),
]
)
result = rewoo(model, task="杭州三天,查天气、西湖门票、上海到杭州高铁。", tools=tools, tracer=tracer)
print(result.answer)
print("\n--- 工具调用记录 ---")
for i, (call, res) in enumerate(zip(result.tool_calls, result.tool_results)):
print(f" {i+1}. {call.tool}({call.args}) -> {res}")
trace_path = tracer.export_jsonl(Path(".traces") / "52_rewoo_zh.jsonl")
print(f"\n[trace] {trace_path}")
if __name__ == "__main__":
main()
运行:
PYTHONPATH=src python examples/52_rewoo.py
完整运行记录
杭州未来三天天气好(晴/多云),适合出行。西湖免费但需预约。上海到杭州高铁 50 分钟,二等座 73 元。建议提前预约西湖,选早班高铁避开人流。
--- 工具调用记录 ---
1. weather({'city': '杭州'}) -> 杭州未来三天:Day1 晴 28°C,Day2 多云 26°C,Day3 晴 29°C。
2. ticket({'attraction': '西湖'}) -> 西湖门票:免费(需预约)。
3. train({'from': '上海', 'to': '杭州'}) -> 上海→杭州高铁:最快 50 分钟,二等座 73 元。
[trace] .traces/52_rewoo_zh.jsonl
trace 中的事件:
rewoo.plan tool_calls=3
tool.call name=weather
tool.call name=ticket
tool.call name=train
踩坑与诊断
| 坑 | 现象 | trace 信号 | 修法 |
|---|---|---|---|
| 排了不相关的工具 | 调用了和任务无关的工具 | tool.call 里有任务未提及的工具名 | 缩短计划、允许第二批补调 |
| 无法中途调整 | 第一个工具返回"闭馆",后续工具还在查门票 | 结果里包含矛盾信息 | 回退到 ReAct |
| 工具失败污染 | 一个失败导致 Solver 乱写 | tool.call 有 error 字段 | 逐工具重试 + 把失败标记传给 Solver |
| observation 膨胀 | Solver prompt 太长 | Solver 调用 token 数异常高 | 按 purpose 摘要 observation |
关键诊断技巧:如果 Solver 的回答遗漏了某个工具的结果,检查 _synthesis_prompt 里是不是所有 (call, result) 对都传进去了。
工程备忘
成本公式:
模型调用 = 2(计划 + 综合)
工具调用 = N
总延迟 ≈ 2 × 模型延迟 + max(工具延迟)(如果工具并行)
对比 ReAct:模型调用 = N + 1,总延迟 = (N+1) × 模型延迟 + sum(工具延迟)。
工具越多、越独立,ReWOO 的优势越明显。3 个独立工具时 ReWOO 省约 50% 模型调用。
生产注意事项:
max_tool_calls建议 3-6。超过 6 个的计划容易出错。- 工具调用可以真正并行执行(
asyncio.gather),但当前实现是串行的。生产环境建议改成并行。 - 如果超过 30% 的任务需要"看到第一个结果才能决定下一步",ReWOO 不适合,用 ReAct。
读完以后
ReWOO 适合"多个独立工具调用"的场景。
- 如果调用之间有依赖关系,读 LLM Compiler。
- 如果每一步都需要看观察再决定,读 ReAct。