news 2026/10/18 8:25:04

ReWOO实战:解耦推理与观察,让LLM Agent告别token焦虑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ReWOO实战:解耦推理与观察,让LLM Agent告别token焦虑

做 LLM Agent 的朋友,大概都遇到过这种尴尬:任务明明不复杂,但跑着跑着上下文就爆了。尤其是用 ReAct 那套“边想边做”的循环,每调一次工具,整个对话历史都要重新喂给模型一遍。当时我算了一笔账,一个带搜索的简单问答,可能烧掉几十万 token。后来我看到 ReWOO 这个方案,才算是找到一个顺手的方向。ReWOO 的全称是 Reasoning WithOut Observation,核心就一句话:把“推理”和“观察”解耦,先让模型一次性规划,再让工具去执行,最后把结果汇总成答案。这篇文章就聊聊它的模块设计、token 节省原理、代码实现,以及我实际踩过的坑。

1. ReWOO 到底改了什么:把“边做边想”换成“先想后做”

1.1 从 ReAct 的循环说起

ReAct 是目前最常见的 Agent 范式之一。流程大概是:模型先输出一个 Thought(思考),再输出一个 Action(动作),然后环境返回 Observation(观察结果),模型看到结果后再次进入 Thought…… 这个过程本身很符合人类解决问题的直觉,但落在工程上有个很现实的问题:每一步调用大模型,都需要把之前的全部历史记录重新发送一遍。假设工具返回了 300 token 的观察结果,那么这个 300 token 会被后续每一次调用重复携带,而且随着轮数增加,历史越来越长,最终形成一种近似平方级的 token 膨胀。

所以 ReAct 的“观察”是有代价的。为了看一眼工具返回的小结果,模型要把一长串旧对话重新读一遍。对 API 调用来说,这是实打实的金钱成本,对本地模型来说,是显存和时延的煎熬。尤其当任务需要连续调用五六个工具时,后面几轮请求的体积几乎是前面的两倍甚至三倍,整个 Agent 就像拖着一条越来越重的尾巴在跑。

1.2 ReWOO 的核心思路:让模型不要观察

ReWOO 想解决的问题就是上面这个。它把整个过程拆成三个阶段:先规划,再执行,最后汇总。规划阶段,模型只负责输出一个完整的方案,方案里用 #E1#、#E2# 这样的占位符标记未来要调用的工具结果。执行阶段,一个叫 Worker 的模块按方案调用工具,把结果填到对应占位符。汇总阶段,另一个叫 Solver 的模块拿到填好的方案,生成最终回答。

这样做的直接好处是:调用大模型的次数从“工具轮数”变成了“固定两三次”。无论你调用 5 个工具还是 10 个工具,规划阶段只调一次,汇总阶段也只调一次,中间的 Worker 完全在大模型之外运行。省掉的 token 不是一点半点,尤其是当工具结果又长又多的时候,优势非常明显。我最初看到这个思路时挺惊讶的,因为大家都在卷“更复杂的推理循环”,ReWOO 却反着来:让模型少看几眼,反而更快更省。

2. 三个模块的分工:Planner、Worker、Solver

2.1 Planner:只负责“想”,不负责“看”

Planner 的职责很简单:给定用户问题,输出一个步骤清单。这个清单可以包含两种内容:一种是纯思考步骤,比如“根据上述信息进行判断”;另一种是工具调用步骤,比如“查询某地的天气:#E1#”。所有工具调用都用占位符标记,Planner 不需要知道工具实际返回什么,它只需要相信自己未来会拿到这些结果。

这里有个容易忽略的细节:Planner 的提示词必须写清楚“不要猜测工具结果”。我之前见过一个初版实现,Planner 在计划里直接把“结果应该是什么”给编出来了,后面 Worker 执行的时候和计划冲突,Solver 都不知道该信谁。后来我在提示词里强调“所有工具结果只能来自 Worker 的执行,不允许在计划阶段假设”,情况才稳定下来。

2.2 Worker:纯执行器,不消耗心理令牌

Worker 在工程上通常是一个普通函数或服务,它根据 Planner 给出的工具名和参数,去调用外部工具或内部函数,然后返回执行结果。Worker 不需要调用 LLM,所以它的运行成本可以忽略不计。这也是 ReWOO 省 token 的关键:工具返回的结果只在 Solver 阶段被看到一次,而不是像 ReAct 那样每轮都被重复消费。

Worker 的设计要注意两点。第一,返回值尽量结构化,比如 JSON 或者带字段的文本,方便 Solver 解析。第二,要给 Worker 设置超时和异常捕获,工具失败时不要直接让整个流程崩溃,而是返回一个错误码,由 Solver 决定怎么处理。如果你用了多个工具,Worker 还可以内置一个工具注册表,按名字分发执行,这样新增工具时不用改主流程。

2.3 Solver:把信息汇总成答案

Solver 是最后一个调用 LLM 的环节。它拿到的是“填充了工具结果的完整计划”,也就是 Planning + Observations 的组合。Solver 的任务是综合分析这些信息,生成用户最终要的答案。因为所有证据都已经摆在眼前,Solver 只需要做一次简单的总结和推理,不需要再频繁决策。

模块是否调用LLM核心职责主要输入主要输出
Planner是产出完整执行计划用户问题含占位符的计划
Worker否执行工具调用计划中的工具指令工具执行结果
Solver是生成最终回答计划 + 工具结果最终答案

这三个模块的关系,有点像项目经理、外包团队和写报告的人。项目经理不关心外包具体怎么干,只把任务拆好;外包把结果交回来;写报告的人拿着所有材料一次性输出。正因为中间不再反复请示“下一步怎么办”,整个流程才变得轻快。

3. Planner 的输出设计:占位符与 token 节省的数学

3.1 为什么用 #E# 占位符

在 ReWOO 中,占位符是一个很关键的约定。使用 #E1#、#E2# 这样的格式,可以避免把中间输出直接混入计划文本,也方便程序用正则表达式解析。实际使用中,我见过有人用 [E1] 或者 {{E1}},都能跑,但关键是格式必须唯一,且不能和自然语言里的内容冲突。比如不要用“#1”这种太宽泛的标记,容易误匹配。

E 可以理解为 Evidence(证据),也可以理解为 Execution(执行结果)。它表示“这个位置将来会被工具执行结果替换”。这样做的好处是规划文本是稳定的,不依赖具体执行结果,可以提前生成、缓存,甚至在多轮对话中复用同一份计划。

3.2 一个可读的计划示例

光说概念容易晕,我放一个具体的例子。用户提问:某城市明天适合穿什么衣服出门?请结合天气情况。

Planner 输出可能是这样的:

1. 用天气查询工具查询某城市明天的最高气温和最低气温: #E1# 2. 用天气查询工具查询某城市明天是否有降水: #E2# 3. 根据 #E1# 和 #E2# 中的信息,给出穿衣建议。

Worker 执行后,两个占位符被填充为:

#E1# -> {"city": "某城市", "date": "明天", "high": 18, "low": 8, "condition": "多云"}

#E2# -> {"city": "某城市", "date": "明天", "rain": false}

Solver 看到的是完整信息,然后生成:“明天某城市多云,气温 8 到 18 摄氏度,早晚偏凉,建议穿薄外套,不需要带伞。”

这个例子里,规划阶段没有真实数据,但占位符保证了结构完整。Solver 阶段才看到真实数据,而且只看一次,不会反复把同一批天气信息塞给模型。

3.3 token 消耗的量化对比

我们可以做一个简化计算。设系统提示固定为 S token,每次 Thought/Action 输出约为 T token,每次 Observation 长度约为 O token。ReAct 第 k 轮调用时,输入包含系统提示 + 前面所有轮次的输出和观察结果,所以第 k 轮的输入长度约为 S + kT + (k-1)O。如果一共运行 n 轮,总输入 token 约为 nS + Tn(n+1)/2 + O*n(n-1)/2。工具调用轮数越多,n 的平方项越恐怖。

ReWOO 这边,固定是两次 LLM 调用:Planner 一次,Solver 一次。Planner 输入长度约为 S + 用户问题长度,输出是计划长度 P。Solver 输入长度约为 S + P + 所有工具结果总长度 R,输出是最终答案长度 A。总 token 约为 2S + 问题长度 + P + R + A。工具结果 R 只出现一次。

拿具体数字说话:设 S=800,T=120,O=300,n=5,工具结果总长度 R=1500,计划长度 P=400,问题长度=50。ReAct 总输入约为 5800 + 12015 + 300*10 = 4000 + 1800 + 3000 = 8800,加上输出约 600,总 9400。ReWOO 总输入约为 1600+50+400+1500 = 3550,加上输出约 300,总 3850。省了一半多。如果 n 更大、工具结果更长,差距会非常夸张。

注意,ReWOO 的核心优势不是“绝对少”,而是“不随工具轮数线性膨胀”。它的成本主要由工具结果总长度决定,而这个长度是固定的,不会被反复累加。

4. 手写一个最小可用的 ReWOO Agent

4.1 框架设计

为了讲清楚实现,我写一个极简版本,不依赖任何 Agent 框架。核心只有三个函数和一个流程控制器。你可以把它塞进自己的项目里,或者替换成更成熟的框架实现。这里全部用 Python 演示,里面调用 LLM 的部分我用一个 mock 函数代替,实际使用时换成你自己的大模型 SDK 就行。

整体流程是:用户输入 -> Planner 生成计划 -> Worker 执行工具并填充占位符 -> Solver 生成最终答案。我习惯把流程控制写成一个run_rewoo函数,这样外部只需要调用它一个入口。

4.2 先定义 LLM 调用和工具

我们需要一个call_llm占位函数。真实项目中,这里就是请求大模型接口,但为了演示,我直接返回写死的字符串。工具部分,我模拟一个天气查询工具和一个计算器。

import json import re def call_llm(system_prompt, user_prompt): # 实际使用时替换为真实的大模型调用 # 这里只做演示 return "mock_llm_response" def execute_tool(tool_name, args): """模拟工具执行。""" if tool_name == "weather": city = args.get("city", "某城市") return {"city": city, "high": 18, "low": 8, "condition": "多云"} if tool_name == "calculator": expr = args.get("expression", "0") try: return {"result": eval(expr)} except Exception: return {"error": "invalid expression"} return {"error": "unknown tool"}

这里eval只是为了演示,真实项目中千万不要对用户输入用eval,会有严重安全隐患。可以换成解析器或者预定义的计算逻辑。

4.3 Planner 实现:生成计划并解析占位符

Planner 的提示词里,我会明确要求模型按固定格式输出。为了让解析简单,这里要求计划中用#E1#这样的占位符标记工具结果,同时用一行 JSON 描述工具调用。

def generate_plan(user_query): system_prompt = ( "你是一个任务规划器。把用户问题拆解成步骤。" "工具调用步骤必须写成 JSON,格式为 {\"tool\": \"工具名\", \"args\": {...}}," "并在同一行末尾用 #E1#、#E2# 等占位符标记结果。" "不要猜测工具结果,工具结果由之后的Worker提供。" ) # 真实项目里这里会调用大模型 # 这里返回一个固定示例计划 plan = ( "1. 用天气工具查询某城市天气:{\"tool\": \"weather\", \"args\": {\"city\": \"某城市\"}} -> #E1#\n" "2. 根据 #E1# 中的天气信息给出穿衣建议。" ) return plan

实际项目中,你应该用call_llm(system_prompt, user_query)替换固定返回。解析占位符可以用正则:

def extract_tool_calls(plan): pattern = r'\{[^{}]*"tool"[^{}]*\}' matches = re.findall(pattern, plan) return [json.loads(m) for m in matches]

这个正则比较粗,只是演示思路。如果让 Planner 输出严格的 JSON 数组,解析会稳定得多。

4.4 Worker 实现:按序执行并填充

Worker 要做两件事:找出所有占位符对应的工具调用,执行工具,并把结果替换回计划文本。这里我按顺序执行,真实场景中互相独立的调用可以并行。

def run_worker(plan): tool_calls = extract_tool_calls(plan) filled_plan = plan for idx, tool_call in enumerate(tool_calls, start=1): result = execute_tool(tool_call["tool"], tool_call["args"]) result_text = json.dumps(result, ensure_ascii=False) filled_plan = filled_plan.replace(f"#E{idx}#", result_text, 1) return filled_plan

如果工具调用在计划中出现的顺序和编号不一致,最好先统一编号,再按编号替换。上面的代码假设 Planner 生成的第一个工具调用对应 #E1#,第二个对应 #E2#,实际使用时建议用更严格的编号约束。

4.5 Solver 实现:汇总生成最终答案

Solver 拿到填充好的计划后,调用一次 LLM,让模型基于计划中的信息回答用户问题。它不需要再考虑调用什么工具,只需要做总结。

def run_solver(user_query, filled_plan): system_prompt = "你是一个助手。根据给定的计划步骤和工具结果,回答用户问题。" final_answer = call_llm(system_prompt, f"用户问题:{user_query}\n计划及结果:\n{filled_plan}") return final_answer

为了演示,我让call_llm返回固定文本,所以这里不会真的得到一个智能回答。你把call_llm换成真实模型后,流程就是完整的。

4.6 完整流程与输出效果

核心控制器只需要几行:

def run_rewoo(user_query): plan = generate_plan(user_query) filled_plan = run_worker(plan) return run_solver(user_query, filled_plan) if __name__ == "__main__": result = run_rewoo("某城市明天适合穿什么衣服?") print(result)

这个最小实现已经体现了 ReWOO 的全部核心:Planner 一次性规划,Worker 不调用 LLM 直接执行,Solver 汇总。把call_llm替换成真实大模型接口,再增加一些异常处理,就可以用在真实项目里了。

5. 和 ReAct、Plan-and-Execute 对比,怎么选

5.1 三种范式放在一起看

ReWOO 不是唯一把“规划”和“执行”分开的范式,Plan-and-Execute 也做类似的事。两者区别在于:ReWOO 强调“不观察”,Planner 制定计划后,执行阶段不会因为中间结果而调整计划;Plan-and-Execute 则允许在阶段之间重新规划,执行一步、看一步、再规划下一步。

范式规划时机是否观察中间结果LLM调用次数典型场景
ReAct逐步是与工具轮数成正比动态探索、试错型任务
ReWOO一次性前置否固定2-3次步骤明确、结果独立
Plan-and-Execute阶段性前置部分步骤间可重新规划规划多次 + 执行多次复杂任务、分阶段执行

ReAct 的优势是灵活,能根据错误调整策略,但代价是 token 高、耗时长。ReWOO 的优势是高效稳定,代价是缺乏动态性。Plan-and-Execute 折中,适合“既要拆分步骤,又需要阶段性检查”的场景。

5.2 什么时候优先用 ReWOO

我的经验是,满足下面任意两条,ReWOO 都会有明显收益:

  • 任务可以离线拆分,不依赖中间结果,比如“查天气、查汇率、查新闻”这种并列查询;
  • 工具数量多但相互独立,并行执行也不会冲突;
  • 工具返回结果很长,比如搜索摘要、数据库查询结果,token 成本敏感;
  • 对响应时延有要求,希望 LLM 调用次数稳定,不让用户等太久。

我手里有几个知识库问答的 Agent,流程就是检索、阅读、总结,步骤完全固定。改用 ReWOO 之后,成本降了大概一半,响应也快了不少。最关键的是稳定,不会像 ReAct 那样跑到一半上下文乱了。

5.3 什么时候不要用 ReWOO

反过来,这些情况就别硬套 ReWOO:

  • 任务无法预估步骤,比如“帮我调试这段代码,根据报错修改再运行再看结果”,这种必须依赖逐步观察;
  • 工具之间存在强依赖,后面的调用参数取决于前面工具的输出;
  • 用户问题非常开放,目标不明确,需要模型一步步探索。

我之前试过用一个 ReWOO Agent 处理代码调试,结果 Planner 根本不知道第一步该查什么,生成的计划要么太泛,要么直接错。后来老老实实换回 ReAct,问题才解决。工具链设计要匹配任务类型,不要为了省 token 把答案质量搞砸。

6. 实战中遇到的坑和排查方法

6.1 占位符解析失败

最常见的问题就是占位符格式不统一。Planner 可能输出#E1#、#E 1#、E1、#1E#,正则匹配时一脸懵。解决办法是不要在自由文本里解析,而是让 Planner 输出结构化 JSON。比如要求{"plan": [...]},其中每个工具步骤就是一个带placeholder字段的对象。这样解析稳定,后续填充也方便。

6.2 工具调用参数缺失

第二条高频坑:Planner 只写“调用搜索工具查询相关资料:#E1#”,但没给搜索关键词。Worker 拿到一个空参数,完全没法执行。解决方法是把工具参数 schema 写进 Planner 的提示词里,明确要求“每个工具调用必须附带完整 JSON 参数”。最好给一个 one-shot 示例,让模型模仿格式。

6.3 工具结果太长导致 Solver 溢出

搜索引擎或数据库返回的内容经常几千字,全塞给 Solver 可能直接超出上下文。我踩过这个坑后,在 Worker 里加了一步“预处理”:对结果做截断、摘要或字段抽取,只把关键信息填回占位符。你也可以用一个小模型先对长文本做摘要,虽然多花一次调用,但整体仍然比 ReAct 省很多。

6.4 规划与实际执行结果脱节

Planner 假设某个工具会返回“温度 20 度”,但实际工具返回的是 JSON 里嵌套了一层{"data": {"temp": 20}}。Solver 如果直接从文本里找信息,容易漏掉。解决方法是让 Worker 统一包装返回结果,并告诉 Solver“工具返回的是 JSON 格式,请以字段为准”。如果工具结构差异大,还可以在 Worker 里做一层格式转换。

6.5 工具失败后的错误处理

工具调用失败时,不能直接让整个流程中断。我会让 Worker 捕获异常,返回一个固定的错误占位符,比如#E1# = {"error": "timeout"},同时把错误信息传给 Solver。Solver 看到错误后,会基于其他可用信息给出回答,或者明确告诉用户“这项信息暂时无法获取”。这样 Agent 至少是优雅降级,而不是直接崩溃。

6.6 并发执行时的顺序问题

如果多个工具互相独立,顺序执行会浪费大量时间。我在实际项目中用线程池并发执行所有工具,但要注意一个问题:占位符的填充必须按编号精确对应,不能因为并发返回顺序不同就错位。我一般先把所有工具结果收集到一个字典里,再统一填充计划文本,避免竞态。

问题常见原因建议对策
占位符解析失败格式不统一改用结构化 JSON 输出
参数缺失Planner 提示词未约束 schema在提示词中给出工具参数示例
结果过长工具返回大段文本Worker 里做截断或摘要
计划与结果脱节工具返回结构与预期不一致Worker 统一包装格式
工具失败外部服务异常返回错误占位符,优雅降级
并发错位多线程填充占位符先收集结果,再统一替换

7. 一些个人使用体会和后续扩展

7.1 我为什么在部分项目里彻底换了方案

我负责的几个 Agent 项目里,凡是流程相对固定的,我都把 ReAct 换成了 ReWOO。换完之后最明显的感受是“可控”。ReAct 在长任务中经常前言不搭后语,模型会忘掉早期步骤的结果,需要我在提示词里反复强调“记住之前的观察”。ReWOO 不会,因为所有结果都在最后一步一次性给到 Solver,不存在“遗忘”的问题。

另一个体会是:ReWOO 的 Planner 提示词值得花时间打磨。它决定整个任务的上限。我建议先手动写几条典型任务的“标准计划”,作为 few-shot 示例放进提示词里。模型模仿能力很强,示例里工具参数写得越具体,Planner 输出越可靠。

7.2 扩展思路:从单轮到多轮、从单智能体到多智能体

ReWOO 本身是一个单轮范式,但可以扩展。比如引入多轮对话时,把用户历史对话并入 Planner 输入;或者让 Solver 在回答后继续追问,从而触发下一轮 ReWOO。也可以把 ReWOO 当作一个“子任务执行器”:外层有个总控 Agent,把复杂问题拆成多个子任务,每个子任务交给一个独立的 ReWOO 流程去跑,最后再汇总。这种组合比单纯用 ReAct 应付所有事情要清晰很多。

另外,工具注册表值得做。Worker 根据工具名动态分发,扩展新工具时只需要注册一个函数,不用改 Planner 和 Solver 的代码。配合 LLM 的 Function Calling 能力,Planner 的 JSON 参数生成会更稳定,解析也不容易出错。

7.3 一点小建议

如果你正在用 ReAct 觉得 token 飙升、上下文爆炸,可以先拿一个固定流程的任务试试 ReWOO。不用一上来就搞复杂框架,像上面那样几十行代码就能跑通。跑通之后,再逐步加并发、错误处理、重新规划机制。我个人实际测下来,只要任务允许“先规划后执行”,ReWOO 基本都能带来肉眼可见的成本下降和响应提速。工具链设计这件事,适合的才是最好的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 8:22:40

轻量级PHP积分商城系统:高并发兑换码生成与核销闭环

简介:这是一套面向PHP开发者与中小型电商项目技术负责人的开源积分商城系统源码,专为构建积分兑换类平台(如会员积分商城、企业福利兑换站、营销活动赠品发放系统)而设计,兼顾功能完整性与二次开发友好性。资源包共973…

作者头像 李华
网站建设 2026/10/18 8:22:33

麻雀搜索算法优化Transformer-GRU的电力负荷预测实战

简介:面向负荷数据回归预测的Matlab实现方案,采用麻雀搜索算法(SSA)优化Transformer-GRU组合模型,覆盖数据预处理、模型训练、预测评估等核心环节,适合计算机、电子信息工程、数学等专业学生完成课程设计、…

作者头像 李华
网站建设 2026/10/18 8:20:24

广州租赁合同没到期房东要求提前收房怎么办 租客的救济途径

广州租客遇到房东在租期内要求提前收房,可以拒绝并要求继续履行合同,也可以同意搬离但主张违约金和实际损失赔付。房屋在这期间被出售的,不影响租赁合同效力,租客可以住到租期届满。应对的关键是留好合同、付款和沟通证据&#xf…

作者头像 李华
网站建设 2026/10/18 8:18:10

Linux预编译包实战:从解压校验到systemd部署与避坑指南

简介:本资源为Oracle数据库11.2.0.4.161018版本的季度补丁包,补丁编号24006111,适用于64位Linux环境,面向需要维护生产库稳定与安全的DBA及数据库运维人员。该补丁属于Oracle季度累积更新策略的一部分,集中修复已知漏洞…

作者头像 李华
网站建设 2026/10/18 8:17:34

原生视觉生成智能体:架构、训练、推理与多轮编辑实战

从立项到正式交付,我们团队用大半年时间把一套“原生视觉生成智能体”从零搭起来。印象最深的是初期推翻重来的那次:当时所有人默认的方案是“语言模型 外部图像生成服务”,把视觉生成当成工具去调用,结果在真实场景里根本跑不通…

作者头像 李华