无观察推理¶
在ReWOO中,Xu等人提出了一种结合多步骤规划器和变量替换以有效使用工具的代理。其设计旨在通过以下方式改进ReACT风格的代理架构:
- 通过一次性生成完整的工具使用链,减少token消耗和执行时间。(ReACT风格的代理架构需要多次LLM调用,且带有冗余前缀(因为系统提示和先前步骤会提供给LLM用于每个推理步骤))
- 简化微调过程。由于规划数据不依赖于工具的输出,模型可以在不实际调用工具的情况下进行微调(理论上)。
以下图示概述了 ReWOO 的整体计算图

ReWOO 由 3 个模块组成
- 🧠规划器 (Planner):按以下格式生成计划
Plan: <reasoning>
#E1 = Tool[argument for tool]
Plan: <reasoning>
#E2 = Tool[argument for tool with #E1 variable substitution]
...
- 执行器 (Worker):使用提供的参数执行工具。
- 🧠求解器 (Solver):根据工具观察结果,为初始任务生成答案。
带有🧠表情符号的模块依赖于LLM调用。请注意,我们通过使用变量替换来避免对规划器LLM的冗余调用。
在此示例中,每个模块都由一个 LangGraph 节点表示。最终结果将生成一个这样的追踪。让我们开始吧!
0. 先决条件¶
在此示例中,我们将为代理提供 Tavily 搜索引擎工具。您可以在此处获取 API 密钥,或替换为免费工具选项(例如,DuckDuckGo 搜索)。
对于此notebook,您应该在 repo 的根目录下添加一个包含 TAVILY_API_KEY 的 .env 文件。
安装依赖项¶
图状态:在 LangGraph 中,每个节点都会更新一个共享的图状态。当任何节点被调用时,该状态就是其输入。
下面,我们将定义一个状态对象来包含任务、计划、步骤和其他变量。
import { Annotation } from "@langchain/langgraph";
const GraphState = Annotation.Root({
task: Annotation<string>({
reducer: (x, y) => (y ?? x),
default: () => "",
}),
planString: Annotation<string>({
reducer: (x, y) => (y ?? x),
default: () => "",
}),
steps: Annotation<string[][]>({
reducer: (x, y) => x.concat(y),
default: () => [],
}),
results: Annotation<Record<string, any>>({
reducer: (x, y) => ({ ...x, ...y }),
default: () => ({}),
}),
result: Annotation<string>({
reducer: (x, y) => (y ?? x),
default: () => "",
}),
})
1. 规划器¶
规划器会提示LLM以任务列表的形式生成计划。每个任务的参数都是字符串,其中可能包含特殊变量(#E{{0-9}}+),这些变量用于从其他任务结果中进行变量替换。

我们的示例代理将有两个工具
- Google - 一个搜索引擎(此处为 Tavily)
- LLM - 一个LLM调用,用于对先前的输出进行推理。
LLM工具接收的提示上下文较少,因此比ReACT范式更节省token。
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({
model: "gpt-4o",
temperature: 0,
});
import { ChatPromptTemplate } from "@langchain/core/prompts";
const template =
`For the following task, make plans that can solve the problem step by step. For each plan, indicate
which external tool together with tool input to retrieve evidence. You can store the evidence into a
variable #E that can be called by later tools. (Plan, #E1, Plan, #E2, Plan, ...)
Tools can be one of the following:
(1) Google[input]: Worker that searches results from Google. Useful when you need to find short
and succinct answers about a specific topic. The input should be a search query.
(2) LLM[input]: A pre-trained LLM like yourself. Useful when you need to act with general
world knowledge and common sense. Prioritize it when you are confident in solving the problem
yourself. Input can be any instruction.
For example,
Task: Thomas, Toby, and Rebecca worked a total of 157 hours in one week. Thomas worked x
hours. Toby worked 10 hours less than twice what Thomas worked, and Rebecca worked 8 hours
less than Toby. How many hours did Rebecca work?
Plan: Given Thomas worked x hours, translate the problem into algebraic expressions and solve with Wolfram Alpha.
#E1 = WolframAlpha[Solve x + (2x - 10) + ((2x - 10) - 8) = 157]
Plan: Find out the number of hours Thomas worked.
#E2 = LLM[What is x, given #E1]
Plan: Calculate the number of hours Rebecca worked.
#E3 = Calculator[(2 * #E2 - 10) - 8]
Important!
Variables/results MUST be referenced using the # symbol!
The plan will be executed as a program, so no coreference resolution apart from naive variable replacement is allowed.
The ONLY way for steps to share context is by including #E<step> within the arguments of the tool.
Begin!
Describe your plans with rich details. Each Plan should be followed by only one #E.
Task: {task}`;
const promptTemplate = ChatPromptTemplate.fromMessages([["human", template]]);
const planner = promptTemplate.pipe(model);
const task = "what is the hometown of the winner of the 2023 australian open?";
await planner.invoke({ task });
AIMessage {
"id": "chatcmpl-9z88bDgCFkpWbYitlBSkuEaUU0YA2",
"content": "Plan: Identify the winner of the 2023 Australian Open.\n#E1 = Google[\"winner of the 2023 Australian Open\"]\n\nPlan: Find the hometown of the winner identified in #E1.\n#E2 = Google[\"hometown of #E1\"]",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 55,
"promptTokens": 438,
"totalTokens": 493
},
"finish_reason": "stop",
"system_fingerprint": "fp_3aa7262c27"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 438,
"output_tokens": 55,
"total_tokens": 493
}
}
规划器节点¶
为了将规划器连接到我们的图,我们将创建一个 getPlan 节点,该节点接受 ReWOO 状态并返回包含 steps 和 planString 字段的状态更新。
import { RunnableConfig } from "@langchain/core/runnables";
const regexPattern = new RegExp(
"Plan\\s*\\d*:\\s*([^#]+)\\s*(#E\\d+)\\s*=\\s*(\\w+)\\s*\\[([^\\]]+)\\]",
"g",
);
async function getPlan(state: typeof GraphState.State, config?: RunnableConfig) {
console.log("---GET PLAN---");
const task = state.task;
const result = await planner.invoke({ task }, config);
// Find all matches in the sample text.
const matches = result.content.toString().matchAll(regexPattern);
let steps: string[][] = [];
for (const match of matches) {
const item = [match[1], match[2], match[3], match[4], match[0]];
if (item.some((i) => i === undefined)) {
throw new Error("Invalid match");
}
steps.push(item as string[]);
}
return {
steps,
planString: result.content.toString(),
};
}
2. 执行器¶
执行器接收计划并按顺序执行工具。
下面,实例化搜索引擎并定义工具执行节点。
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
const search = new TavilySearchResults();
const _getCurrentTask = (state: typeof GraphState.State) => {
console.log("_getCurrentTask", state);
if (!state.results) {
return 1;
}
if (Object.entries(state.results).length === state.steps.length) {
return null;
}
return Object.entries(state.results).length + 1;
};
const _parseResult = (input: unknown) => {
if (typeof input === "string") {
const parsedInput = JSON.parse(input);
if (Array.isArray(parsedInput) && "content" in parsedInput[0]) {
// This means it is a tool result.
return parsedInput.map(({ content }) => content).join("\n");
}
}
if (input && typeof input === "object" && "content" in input) {
// If it's not a tool, we know it's an LLM result.
const { content } = input;
return content;
}
throw new Error("Invalid input received");
};
async function toolExecution(state: typeof GraphState.State, config?: RunnableConfig) {
console.log("---EXECUTE TOOL---");
const _step = _getCurrentTask(state);
if (_step === null) {
throw new Error("No current task found");
}
const [_, stepName, tool, toolInputTemplate] = state.steps[_step - 1];
let toolInput = toolInputTemplate;
const _results = state.results || {};
for (const [k, v] of Object.entries(_results)) {
toolInput = toolInput.replace(k, v);
}
let result;
if (tool === "Google") {
result = await search.invoke(toolInput, config);
} else if (tool === "LLM") {
result = await model.invoke(toolInput, config);
} else {
throw new Error("Invalid tool specified");
}
_results[stepName] = JSON.stringify(_parseResult(result), null, 2);
return { results: _results };
}
3. 求解器¶
求解器接收完整的计划,并根据执行器(worker)的工具调用响应生成最终答案。
const solvePrompt = ChatPromptTemplate.fromTemplate(
`Solve the following task or problem. To solve the problem, we have made step-by-step Plan and
retrieved corresponding Evidence to each Plan. Use them with caution since long evidence might
contain irrelevant information.
{plan}
Now solve the question or task according to provided Evidence above. Respond with the answer
directly with no extra words.
Task: {task}
Response:`,
);
async function solve(state: typeof GraphState.State, config?: RunnableConfig) {
console.log("---SOLVE---");
let plan = "";
const _results = state.results || {};
for (let [_plan, stepName, tool, toolInput] of state.steps) {
for (const [k, v] of Object.entries(_results)) {
toolInput = toolInput.replace(k, v);
}
plan += `Plan: ${_plan}\n${stepName} = ${tool}[${toolInput}]\n`;
}
const model = new ChatOpenAI({
temperature: 0,
model: "gpt-4o",
});
const result = await solvePrompt
.pipe(model)
.invoke({ plan, task: state.task }, config);
return {
result: result.content.toString(),
};
}
4. 定义图¶
我们的图定义了工作流程。规划器、工具执行器和求解器模块都作为节点添加。
import { END, START, StateGraph } from "@langchain/langgraph";
import { MemorySaver } from "@langchain/langgraph";
const _route = (state: typeof GraphState.State) => {
console.log("---ROUTE TASK---");
const _step = _getCurrentTask(state);
if (_step === null) {
// We have executed all tasks
return "solve";
}
// We are still executing tasks, loop back to the "tool" node
return "tool";
};
const workflow = new StateGraph(GraphState)
.addNode("plan", getPlan)
.addNode("tool", toolExecution)
.addNode("solve", solve)
.addEdge("plan", "tool")
.addEdge("solve", END)
.addConditionalEdges("tool", _route)
.addEdge(START, "plan");
// Compile
const app = workflow.compile({ checkpointer: new MemorySaver() });
const threadConfig = { configurable: { thread_id: "123" } };
let finalResult;
const stream = await app.stream(
{
task: "what is the hometown of the winner of the 2023 australian open?",
},
threadConfig,
);
for await (const item of stream) {
console.log(item);
console.log("-----");
finalResult = item;
}
---GET PLAN---
{
plan: {
planString: 'Plan: Identify the winner of the 2023 Australian Open.\n' +
'#E1 = Google["winner of the 2023 Australian Open"]\n' +
'\n' +
'Plan: Find the hometown of the winner identified in #E1.\n' +
'#E2 = Google["hometown of #E1"]',
steps: [ [Array] ]
}
}
-----
---EXECUTE TOOL---
_getCurrentTask {
task: 'what is the hometown of the winner of the 2023 australian open?',
planString: 'Plan: Identify the winner of the 2023 Australian Open.\n' +
'#E1 = Google["winner of the 2023 Australian Open"]\n' +
'\n' +
'Plan: Find the hometown of the winner identified in #E1.\n' +
'#E2 = Google["hometown of #E1"]',
steps: [
[
'Identify the winner of the 2023 Australian Open.\n',
'#E1',
'Google',
'"winner of the 2023 Australian Open"',
'Plan: Identify the winner of the 2023 Australian Open.\n' +
'#E1 = Google["winner of the 2023 Australian Open"]'
]
],
results: {},
result: ''
}
---ROUTE TASK---
_getCurrentTask {
task: 'what is the hometown of the winner of the 2023 australian open?',
planString: 'Plan: Identify the winner of the 2023 Australian Open.\n' +
'#E1 = Google["winner of the 2023 Australian Open"]\n' +
'\n' +
'Plan: Find the hometown of the winner identified in #E1.\n' +
'#E2 = Google["hometown of #E1"]',
steps: [
[
'Identify the winner of the 2023 Australian Open.\n',
'#E1',
'Google',
'"winner of the 2023 Australian Open"',
'Plan: Identify the winner of the 2023 Australian Open.\n' +
'#E1 = Google["winner of the 2023 Australian Open"]'
]
],
results: {
'#E1': `"A one-set shoot-off to decide the winner of the 2023 Australian Open. There could not have been a better script. SECOND SET (* denotes server) Sabalenka* 6-3 Rybakina - Wide second serve into the deuce court from Sabalenka and forehand return from Rybakina is long. Deep backhand crosscourt return from Rybakina draws a shot ball from Sabalenka ...\\nThe Crossword Solver found 30 answers to \\"Tennis player Sabalenka, winner of the 2023 Australian Open\\", 5 letters crossword clue. The Crossword Solver finds answers to classic crosswords and cryptic crossword puzzles. Enter the length or pattern for better results. Click the answer to find similar crossword clues .\\nAccording to bet365, Djokovic has even odds of winning the title at Melbourne Park -- meaning that the 35-year-old has a 50% chance of being the winner of the 2023 Australian Open men's singles ...\\nWe found 40 solutions for Tennis player Sabalenka, winner of the 2023 Australian Open. The top solutions are determined by popularity, ratings and frequency of searches. The most likely answer for the clue is ARYNA. How many solutions does Tennis player Sabalenka, winner of the 2023 Australian Open have?"`
},
result: ''
}
{
tool: {
results: {
'#E1': `"A one-set shoot-off to decide the winner of the 2023 Australian Open. There could not have been a better script. SECOND SET (* denotes server) Sabalenka* 6-3 Rybakina - Wide second serve into the deuce court from Sabalenka and forehand return from Rybakina is long. Deep backhand crosscourt return from Rybakina draws a shot ball from Sabalenka ...\\nThe Crossword Solver found 30 answers to \\"Tennis player Sabalenka, winner of the 2023 Australian Open\\", 5 letters crossword clue. The Crossword Solver finds answers to classic crosswords and cryptic crossword puzzles. Enter the length or pattern for better results. Click the answer to find similar crossword clues .\\nAccording to bet365, Djokovic has even odds of winning the title at Melbourne Park -- meaning that the 35-year-old has a 50% chance of being the winner of the 2023 Australian Open men's singles ...\\nWe found 40 solutions for Tennis player Sabalenka, winner of the 2023 Australian Open. The top solutions are determined by popularity, ratings and frequency of searches. The most likely answer for the clue is ARYNA. How many solutions does Tennis player Sabalenka, winner of the 2023 Australian Open have?"`
}
}
}
-----
---SOLVE---
{ solve: { result: 'Belgrade, Serbia' } }
-----
在此处查看 LangSmith 追踪¶
结论¶
恭喜您实现了 ReWOO!在您离开之前,我将向您介绍该论文中当前实现的几个限制
- 如果环境上下文信息很少,规划器在工具使用方面将效率低下。这通常可以通过少样本提示和/或微调来改善。
- 任务仍然是顺序执行的,这意味着总执行时间受到每个工具调用的影响,而不仅仅是给定步骤中运行时间最长的工具。