规划与执行¶
本笔记本展示了如何创建一个“规划与执行”风格的智能体。这主要受到 Plan-and-Solve 论文以及 Baby-AGI 项目的启发。
其核心思想是首先制定一个多步骤的计划,然后逐一执行该计划中的每个项目。完成特定任务后,您可以重新审视计划并进行适当的修改。
这与典型的 ReAct 风格的智能体不同,后者是每次只思考一步。这种“规划与执行”风格智能体的优势在于:
- 明确的长期规划(即使是非常强大的大语言模型也可能难以处理)
- 能够在执行步骤中使用更小/更弱的模型,仅在规划步骤中使用更大/更好的模型
设置¶
首先,我们需要安装所需的软件包。
接下来,我们需要为 OpenAI(我们将使用的 LLM)和 Tavily(我们将使用的搜索工具)设置 API 密钥。
可选地,我们可以为 LangSmith 跟踪设置 API 密钥,这将为我们提供一流的可观察性。
// process.env.LANGCHAIN_TRACING_V2 = "true"
// process.env.LANGCHAIN_API_KEY = "YOUR_API_KEY"
// process.env.LANGCHAIN_PROJECT = "YOUR_PROJECT_NAME"
定义状态¶
让我们从定义此智能体需要跟踪的状态开始。
首先,我们需要跟踪当前计划。让我们用一个字符串列表来表示它。
接下来,我们应该跟踪先前执行的步骤。让我们用一个元组列表来表示(这些元组将包含步骤及其结果)。
最后,我们需要一些状态来表示最终响应以及原始输入。
import { Annotation } from "@langchain/langgraph";
const PlanExecuteState = Annotation.Root({
input: Annotation<string>({
reducer: (x, y) => y ?? x ?? "",
}),
plan: Annotation<string[]>({
reducer: (x, y) => y ?? x ?? [],
}),
pastSteps: Annotation<[string, string][]>({
reducer: (x, y) => x.concat(y),
}),
response: Annotation<string>({
reducer: (x, y) => y ?? x,
}),
})
定义工具¶
我们首先定义我们想要使用的工具。对于这个简单的例子,我们将通过 Tavily 使用一个内置的搜索工具。然而,创建自己的工具非常容易——请参阅此处的文档了解如何操作。
import { TavilySearch } from "@langchain/tavily";
const tools = [new TavilySearch({ maxResults: 3 })];
定义我们的执行智能体¶
现在我们将创建用于执行任务的执行智能体。请注意,在本例中,我们将为每个任务使用相同的执行智能体,但这并非必须如此。
import { ChatOpenAI } from "@langchain/openai";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
const agentExecutor = createReactAgent({
llm: new ChatOpenAI({ model: "gpt-4o" }),
tools: tools,
});
import { HumanMessage } from "@langchain/core/messages";
await agentExecutor.invoke({
messages: [new HumanMessage("who is the winner of the us open")],
});
{
messages: [
HumanMessage {
"content": "who is the winner of the us open",
"additional_kwargs": {},
"response_metadata": {}
},
AIMessage {
"content": "",
"additional_kwargs": {
"tool_calls": [
{
"id": "call_c2N7Z1RX31qKJaSlpOJ0K7Wm",
"type": "function",
"function": "[Object]"
}
]
},
"response_metadata": {
"tokenUsage": {
"completionTokens": 25,
"promptTokens": 80,
"totalTokens": 105
},
"finish_reason": "tool_calls"
},
"tool_calls": [
{
"name": "tavily_search_results_json",
"args": {
"input": "winner of the US Open 2023"
},
"type": "tool_call",
"id": "call_c2N7Z1RX31qKJaSlpOJ0K7Wm"
}
],
"invalid_tool_calls": []
},
ToolMessage {
"content": "[{\"title\":\"How Wyndham Clark won the 2023 U.S. Open over Rory McIlroy, Scottie ...\",\"url\":\"https://www.nytimes.com/athletic/live-blogs/us-open-leaderboard-live-scores-results-tee-times/mhPUFgLsyFfM/\",\"content\":\"Wyndham Clark is your 2023 U.S. Open champion. Wyndham Clark has won his first major championship, besting some of the best players in the world on Sunday at Los Angeles Country Club to claim the ...\",\"score\":0.9981324,\"raw_content\":null},{\"title\":\"Championship Point | Coco Gauff Wins Women's Singles Title | 2023 US Open\",\"url\":\"https://www.youtube.com/watch?v=rZ0XQWWFIAo\",\"content\":\"The moment Coco Gauff beat Aryna Sabalenka in the final of the 2023 US Open.Don't miss a moment of the US Open! Subscribe now: https://bit.ly/2Pdr81iThe 2023...\",\"score\":0.997459,\"raw_content\":null},{\"title\":\"2023 U.S. Open leaderboard: Wyndham Clark breaks through edging Rory ...\",\"url\":\"https://www.cbssports.com/golf/news/2023-u-s-open-leaderboard-wyndham-clark-breaks-through-edging-rory-mcilroy-for-first-major-championship/live/\",\"content\":\"College Pick'em\\nA Daily SportsLine Betting Podcast\\nNFL Playoff Time!\\n2023 U.S. Open leaderboard: Wyndham Clark breaks through edging Rory McIlroy for first major championship\\nClark beat one of the game's best clinching his second PGA Tour victory, both in the last six weeks\\nWith Rickie Fowler, Rory McIlroy and Scottie Scheffler atop the 2023 U.S. Open leaderboard, it appeared as if Los Angeles Country Club was set to crown a shining star as its national champion. After making birdie on No. 1 to momentarily pull even with the leaders, McIlroy was unable to take advantage of the short par-4 6th before leaving one on the table on the par-5 8th when his birdie putt from less than four feet failed to even touch the hole.\\n The shot on 14 was kind of the shot of the week for me -- to make a birdie there and grind it on the way in. The Champion Golfer of the Year now goes to defend the Claret Jug at Hoylake where he will relish the opportunity to put his creativity and imagination on display again.\\n Instead, the City of Angels saw a breakout performance from perhaps one of the game's rising stars as 29-year-old Wyndham Clark (-10) outlasted the veteran McIlroy (-9) to capture his first major championship and clinch his second professional victory.\\n\",\"score\":0.99586606,\"raw_content\":null}]",
"name": "tavily_search_results_json",
"additional_kwargs": {},
"response_metadata": {},
"tool_call_id": "call_c2N7Z1RX31qKJaSlpOJ0K7Wm"
},
AIMessage {
"content": "The winners of the 2023 US Open are:\n\n- **Men's Singles**: Wyndham Clark, who won his first major championship.\n- **Women's Singles**: Coco Gauff, who defeated Aryna Sabalenka in the final.",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 50,
"promptTokens": 717,
"totalTokens": 767
},
"finish_reason": "stop"
},
"tool_calls": [],
"invalid_tool_calls": []
}
]
}
规划步骤¶
现在让我们考虑创建规划步骤。这将使用函数调用来创建一个计划。
import { z } from "zod";
const planObject = z.object({
steps: z
.array(z.string())
.describe("different steps to follow, should be in sorted order"),
});
import { ChatPromptTemplate } from "@langchain/core/prompts";
const plannerPrompt = ChatPromptTemplate.fromTemplate(
`For the given objective, come up with a simple step by step plan. \
This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \
The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.
{objective}`,
);
const model = new ChatOpenAI({
modelName: "gpt-4-0125-preview",
})
const structuredModel = model.withStructuredOutput(planObject);
const planner = plannerPrompt.pipe(structuredModel);
{
steps: [
[32m"Identify the current Australia Open winner."[39m,
[32m"Research the hometown of the identified Australia Open winner."[39m,
[32m"Report the hometown of the Australia Open winner."[39m
]
}
重新规划步骤¶
现在,让我们创建一个根据上一步结果重新制定计划的步骤。
import { JsonOutputToolsParser } from "@langchain/core/output_parsers/openai_tools";
import { tool } from "@langchain/core/tools";
const responseObject = z.object({
response: z.string().describe("Response to user."),
});
const responseTool = tool(() => {}, {
name: "response",
description: "Respond to the user.",
schema: responseObject,
})
const planTool = tool(() => {}, {
name: "plan",
description: "This tool is used to plan the steps to follow.",
schema: planObject,
})
const replannerPrompt = ChatPromptTemplate.fromTemplate(
`For the given objective, come up with a simple step by step plan.
This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps.
The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.
Your objective was this:
{input}
Your original plan was this:
{plan}
You have currently done the follow steps:
{pastSteps}
Update your plan accordingly. If no more steps are needed and you can return to the user, then respond with that and use the 'response' function.
Otherwise, fill out the plan.
Only add steps to the plan that still NEED to be done. Do not return previously done steps as part of the plan.`,
);
const parser = new JsonOutputToolsParser();
const replanner = replannerPrompt
.pipe(
new ChatOpenAI({ model: "gpt-4o" }).bindTools([
planTool,
responseTool,
]),
)
.pipe(parser);
创建图¶
我们现在可以创建图了!
import { END, START, StateGraph } from "@langchain/langgraph";
import { RunnableConfig } from "@langchain/core/runnables";
async function executeStep(
state: typeof PlanExecuteState.State,
config?: RunnableConfig,
): Promise<Partial<typeof PlanExecuteState.State>> {
const task = state.plan[0];
const input = {
messages: [new HumanMessage(task)],
};
const { messages } = await agentExecutor.invoke(input, config);
return {
pastSteps: [[task, messages[messages.length - 1].content.toString()]],
plan: state.plan.slice(1),
};
}
async function planStep(
state: typeof PlanExecuteState.State,
): Promise<Partial<typeof PlanExecuteState.State>> {
const plan = await planner.invoke({ objective: state.input });
return { plan: plan.steps };
}
async function replanStep(
state: typeof PlanExecuteState.State,
): Promise<Partial<typeof PlanExecuteState.State>> {
const output = await replanner.invoke({
input: state.input,
plan: state.plan.join("\n"),
pastSteps: state.pastSteps
.map(([step, result]) => `${step}: ${result}`)
.join("\n"),
});
const toolCall = output[0];
if (toolCall.type == "response") {
return { response: toolCall.args?.response };
}
return { plan: toolCall.args?.steps };
}
function shouldEnd(state: typeof PlanExecuteState.State) {
return state.response ? "true" : "false";
}
const workflow = new StateGraph(PlanExecuteState)
.addNode("planner", planStep)
.addNode("agent", executeStep)
.addNode("replan", replanStep)
.addEdge(START, "planner")
.addEdge("planner", "agent")
.addEdge("agent", "replan")
.addConditionalEdges("replan", shouldEnd, {
true: END,
false: "agent",
});
// Finally, we compile it!
// This compiles it into a LangChain Runnable,
// meaning you can use it as you would any other runnable
const app = workflow.compile();
const config = { recursionLimit: 50 };
const inputs = {
input: "what is the hometown of the 2024 Australian open winner?",
};
for await (const event of await app.stream(inputs, config)) {
console.log(event);
}
{
planner: {
plan: [
"Identify the winner of the 2024 Australian Open.",
"Research the hometown of the identified winner."
]
}
}
{
agent: {
plan: [ "Research the hometown of the identified winner." ],
pastSteps: [
[
"Identify the winner of the 2024 Australian Open.",
"The winner of the 2024 Australian Open men's singles title is Jannik Sinner of Italy. He achieved a "... 175 more characters
]
]
}
}
{ replan: { plan: [ "Research the hometown of Jannik Sinner." ] } }
{
agent: {
plan: [],
pastSteps: [
[
"Research the hometown of Jannik Sinner.",
"Jannik Sinner's hometown is Sexten (also known as Sesto) in northern Italy. Located in the Dolomites"... 126 more characters
]
]
}
}
{
replan: {
response: "The objective has been achieved. The hometown of the 2024 Australian Open winner, Jannik Sinner, is "... 47 more characters
}
}
在此处查看 LangSmith 跟踪 here。¶