langgraph_plan_and_execute

整体入门demo

教程概览

欢迎来到LangGraph教程! 这些笔记本通过构建各种语言代理和应用程序,介绍了如何使用LangGraph。

快速入门(Quick Start)

快速入门部分通过一个全面的入门教程,帮助您从零开始构建一个代理,学习LangGraph的基础知识。

  1. 快速入门:

    • 内容简介: 在本教程中,您将使用LangGraph构建一个支持型聊天机器人(support chatbot)。
    • 目标: 学习LangGraph的基本概念和操作,通过实际构建一个聊天机器人,掌握创建和管理语言代理的基本技能。
  2. LangGraph Cloud 快速入门:

    • 内容简介: 在本教程中,您将构建并部署一个代理到LangGraph Cloud。
    • 目标: 学习如何在云端环境中部署和管理LangGraph代理,了解云部署的优势和操作步骤。

用例(Use cases)

用例部分通过具体的示例实现,展示了为特定场景设计的图结构和常见的设计模式。

聊天机器人(Chatbots)
  1. 客户支持(Customer Support):

    • 内容简介: 构建一个客户支持聊天机器人,用于管理航班、酒店预订、租车等任务。
    • 目标: 学习如何设计和实现一个多功能的客户支持系统,提高用户满意度和自动化水平。
  2. 根据用户需求生成提示(Prompt Generation from User Requirements):

    • 内容简介: 构建一个信息收集型聊天机器人。
    • 目标: 学习如何设计一个能够根据用户需求生成相关提示和问题的聊天机器人,提升信息收集效率。
  3. 代码助手(Code Assistant):

    • 内容简介: 构建一个代码分析和生成助手。
    • 目标: 学习如何设计一个能够分析、调试和生成代码的智能助手,辅助开发者提高工作效率。
RAG(Retrieval-Augmented Generation)
  1. Agentic RAG:

    • 内容简介: 使用代理在回答用户问题之前,检索最相关的信息。
    • 目标: 学习如何结合信息检索和生成模型,提高回答的准确性和相关性。
  2. 自适应 RAG(Adaptive RAG):

    • 内容简介: 自适应RAG是一种将查询分析与主动/自我纠正RAG相结合的策略。
    • 实现链接: 论文链接
    • 本地LLM版本: 使用本地LLM实现的自适应RAG。
  3. 纠正性 RAG(Corrective RAG):

    • 内容简介: 使用LLM评估检索信息的质量,如果质量低,则尝试从其他来源检索信息。
    • 实现链接: 论文链接
    • 本地LLM版本: 使用本地LLM实现的纠正性RAG。
  4. 自我反思 RAG(Self-RAG):

    • 内容简介: 自我RAG是一种结合自我反思/自我评估对检索文档和生成内容进行优化的策略。
    • 实现链接: 论文链接
    • 本地LLM版本: 使用本地LLM实现的自我RAG。
  5. SQL代理(SQL Agent):

    • 内容简介: 构建一个能够回答关于SQL数据库问题的SQL代理。
    • 目标: 学习如何设计一个能够与SQL数据库交互,执行查询并返回结果的智能代理。
代理架构(Agent Architectures)
  1. 多代理系统(Multi-Agent Systems):

    • 网络(Network):
      • 内容简介: 使两个或更多代理协作完成任务。
      • 目标: 学习如何设计和实现多个代理之间的协作机制,提高任务执行的效率和复杂性处理能力。
    • 监督者(Supervisor):
      • 内容简介: 使用LLM来协调和委派任务给各个独立的代理。
      • 目标: 学习如何设计一个监督者代理,负责管理和分配任务,提高系统的整体协调性。
    • 层级团队(Hierarchical Teams):
      • 内容简介: 协调嵌套团队的代理以解决问题。
      • 目标: 学习如何设计层级化的代理团队,处理复杂问题,提升系统的组织和管理能力。
  2. 规划代理(Planning Agents):

    • 计划与执行(Plan-and-Execute):
      • 内容简介: 实现一个基本的规划与执行代理。
      • 目标: 学习如何设计一个能够制定计划并执行任务的代理,提高任务管理的系统性和效率。
    • 无需观察的推理(Reasoning without Observation):
      • 内容简介: 通过将观察结果保存为变量,减少重新规划的需求。
      • 目标: 学习如何优化代理的推理过程,减少不必要的重新规划,提高系统的响应速度。
    • LLMCompiler:
      • 内容简介: 从计划器流式传输并提前执行DAG任务。
      • 目标: 学习如何使用LLMCompiler,通过流式和提前执行任务来优化任务执行流程。
反思与批评(Reflection & Critique)
  1. 基本反思(Basic Reflection):

    • 内容简介: 提示代理对其输出进行反思和修正。
    • 目标: 学习如何设计一个能够自我反思并改进输出的代理,提升系统的智能化水平。
  2. 反思(Reflexion):

    • 内容简介: 批评缺失和多余的细节,以指导下一步行动。
    • 目标: 学习如何通过自我评估,优化代理的决策过程和输出质量。
  3. 思维树(Tree of Thoughts):

    • 内容简介: 使用评分树搜索候选解决方案。
    • 目标: 学习如何设计一个通过评分和搜索优化思维过程的代理,提高问题解决的准确性和效率。
  4. 语言代理树搜索(Language Agent Tree Search):

    • 内容简介: 使用反思和奖励驱动蒙特卡洛树搜索。
    • 目标: 学习如何结合反思机制和奖励系统,优化代理的决策路径,提高问题解决能力。
  5. 自我发现代理(Self-Discover Agent):

    • 内容简介: 分析一个能够学习自身能力的代理。
    • 目标: 学习如何设计一个具备自我学习和能力提升的智能代理,提高系统的自适应性和智能化水平。

评估(Evaluation)

  1. 基于代理的评估(Agent-based Evaluation):

    • 内容简介: 通过模拟用户交互来评估聊天机器人。
    • 目标: 学习如何设计和实施代理系统的评估方法,确保系统的有效性和用户满意度。
  2. 在LangSmith中的评估(In LangSmith Evaluation):

    • 内容简介: 在LangSmith中使用对话数据集评估聊天机器人。
    • 目标: 学习如何利用LangSmith平台进行系统化的评估和监控,提高系统的性能和稳定性。

实验性(Experimental)

  1. 网络研究(STORM):

    • 内容简介: 通过研究和多视角问答生成类似维基百科的文章。
    • 目标: 学习如何设计一个能够进行深入研究和多角度问答的代理系统,提升信息生成的多样性和准确性。
  2. TNT-LLM:

    • 内容简介: 构建丰富、可解释的用户意图分类系统,使用微软为其Bing Copilot应用开发的分类系统。
    • 目标: 学习如何设计一个能够精确分类和解释用户意图的代理系统,提高用户交互的智能化水平。
  3. 网页导航(Web Navigation):

    • 内容简介: 构建一个能够导航和交互网站的代理。
    • 目标: 学习如何设计一个具备网页浏览和互动能力的代理系统,提高系统的实用性和用户体验。
  4. 竞赛编程(Competitive Programming):

    • 内容简介: 构建一个具备少量"情景记忆"和人类参与协作的代理,解决美国计算机奥林匹克竞赛的问题;改编自Shi, Tang, Narasimhan和Yao的"语言模型能否解决奥林匹克编程问题?"论文。
    • 目标: 学习如何设计一个具备记忆和协作能力的高级编程代理,提高系统在复杂问题解决中的表现。
  5. 复杂数据提取(Complex Data Extraction):

    • 内容简介: 构建一个能够使用函数调用执行复杂提取任务的代理。
    • 目标: 学习如何设计一个能够处理复杂数据提取需求的代理系统,提升系统的数据处理能力和灵活性。

plan_and_execute

当然!我很乐意为您详细讲解这个"Plan-and-Execute"示例代码。这个案例展示了如何使用LangGraph构建一个"计划和执行"风格的代理(Agent),该代理首先生成一个多步骤计划,然后逐步执行计划中的每个步骤。在执行过程中,如果有必要,可以重新规划。

这个示例受到了"Plan-and-Solve"论文和"Baby-AGI"项目的启发,其主要优势包括:

  • 显式的长期规划:即使是强大的大型语言模型(LLMs)在长远规划上也可能遇到困难,通过显式的规划可以弥补这一不足。
  • 模型效率:可以使用较小或较弱的模型来执行步骤,只在规划阶段使用更大或更好的模型。

目录

  1. 环境设置
  2. 设置API密钥
  3. 定义工具
  4. 定义执行代理
  5. 定义状态
  6. 规划步骤
  7. 重新规划步骤
  8. 创建计算图
  9. 运行代理
  10. 总结

1. 环境设置

首先,需要安装所需的Python包:

python 复制代码
%pip install --quiet -U langgraph langchain-community langchain-openai tavily-python
  • langgraph:用于构建可组合的代理系统。
  • langchain-community:LangChain的社区扩展包,包含一些社区贡献的工具和功能。
  • langchain-openai:用于与OpenAI的API交互。
  • tavily-python:Tavily的Python客户端,用于搜索功能。

2. 设置API密钥

为了使用OpenAI的语言模型和Tavily的搜索工具,需要设置相应的API密钥。

python 复制代码
import getpass
import os

def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}: ")

_set_env("OPENAI_API_KEY")
_set_env("TAVILY_API_KEY")
  • getpass.getpass:用于安全地输入密钥,防止在终端中显示。
  • os.environ:将API密钥设置为环境变量,供后续代码使用。

3. 定义工具

在这个示例中,我们使用了一个内置的搜索工具 TavilySearchResults。您也可以根据需要创建自己的工具。

python 复制代码
from langchain_community.tools.tavily_search import TavilySearchResults

tools = [TavilySearchResults(max_results=3)]
  • TavilySearchResults :一个搜索工具,用于获取搜索结果。
    • max_results=3:指定返回的最大搜索结果数量。

4. 定义执行代理

现在,我们创建一个执行代理(Execution Agent),用于执行规划好的任务。在这个示例中,我们为每个任务使用相同的执行代理,但实际上您可以根据需要为不同的任务使用不同的代理。

python 复制代码
from langchain import hub
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent

# 获取要使用的提示模板 - 您可以根据需要修改它!
prompt = hub.pull("ih/ih-react-agent-executor")
prompt.pretty_print()

# 选择驱动代理的LLM
llm = ChatOpenAI(model="gpt-4-turbo-preview")

agent_executor = create_react_agent(llm, tools, state_modifier=prompt)
  • hub.pull("ih/ih-react-agent-executor"):从LangChain Hub中获取预定义的提示模板。
  • prompt.pretty_print():打印提示模板的内容,便于查看和修改。
  • ChatOpenAI :初始化OpenAI的聊天模型。
    • model="gpt-4-turbo-preview":指定使用的模型版本。
  • create_react_agent :创建一个基于ReAct框架的代理。
    • llm:语言模型,用于驱动代理的决策和生成。
    • tools:代理可用的工具列表。
    • state_modifier=prompt:使用自定义的提示模板。

5. 定义状态

接下来,我们定义代理需要跟踪的状态。这包括:

  • input:用户的初始输入。
  • plan:当前的计划,表示为字符串列表。
  • past_steps:之前执行的步骤,表示为元组列表,每个元组包含步骤和结果。
  • response:最终的响应。
python 复制代码
import operator
from typing import Annotated, List, Tuple
from typing_extensions import TypedDict

class PlanExecute(TypedDict):
    input: str
    plan: List[str]
    past_steps: Annotated[List[Tuple], operator.add]
    response: str
  • PlanExecute :定义了代理的状态结构,继承自 TypedDict,用于类型提示。
  • past_steps :使用 Annotatedoperator.add,表示在状态更新时,新的步骤会被添加到过去的步骤中。

6. 规划步骤

现在,我们创建规划步骤,用于生成一个多步骤的计划。这里,我们使用了函数调用(Function Calling)和 pydantic 的模型来定义结构化的输出。

python 复制代码
from pydantic import BaseModel, Field

class Plan(BaseModel):
    """Plan to follow in future"""

    steps: List[str] = Field(
        description="different steps to follow, should be in sorted order"
    )

from langchain_core.prompts import ChatPromptTemplate

planner_prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            """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.""",
        ),
        ("placeholder", "{messages}"),
    ]
)

planner = planner_prompt | ChatOpenAI(
    model="gpt-4o", temperature=0
).with_structured_output(Plan)
  • Plan :一个 pydantic 模型,定义了计划的结构,包含一个字符串列表 steps
  • planner_prompt:定义了规划的提示模板,包含系统消息和占位符。
  • ChatPromptTemplate.from_messages:创建一个聊天提示模板,基于消息列表。
  • planner :将提示模板与LLM组合,并指定输出的结构为 Plan
  • with_structured_output(Plan):将llm的输出结构化为Plan类定义的格式

示例调用:

python 复制代码
planner.invoke(
    {
        "messages": [
            ("user", "what is the hometown of the current Australia open winner?")
        ]
    }
)

输出:

python 复制代码
Plan(steps=['Identify the current winner of the Australia Open.', 'Find the hometown of the identified winner.'])

7. 重新规划步骤

在执行过程中,我们可能需要根据之前的执行结果重新规划。为此,我们定义了重新规划的步骤。

python 复制代码
from typing import Union# 用于定义一个类型可以是多种类型中的任意一种

# 定义agent对于用户的最终响应
class Response(BaseModel):
    """Response to user."""
	# 存储agent最终要返回给用户的回答
    response: str

# 用于定义agent在重新规划步骤中可能采取的行动
class Act(BaseModel):
    """Action to perform."""
	# Union[Response, Plan] 表示 action 字段可以是 Response 或 Plan 类型之一
    action: Union[Response, Plan] = Field(
        # 提供字段的描述信息,解释何时使用 Response,何时使用 Plan
        description="Action to perform. If you want to respond to user, use Response. "
        "If you need to further use tools to get the answer, use Plan."
    )
# Act 类用于在重新规划时决定智能体的下一步行动,既可以是直接回应用户,也可以是生成新的计划以继续执行任务
replanner_prompt = ChatPromptTemplate.from_template(
    """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:
{past_steps}

Update your plan accordingly. If no more steps are needed and you can return to the user, then respond with that. 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."""
)

replanner = replanner_prompt | ChatOpenAI(
    model="gpt-4o", temperature=0
).with_structured_output(Act)
  • Response :定义了向用户响应的模型,包含一个字符串 response
  • Act :定义了行动模型,包含一个 action,可以是 ResponsePlan
  • replanner_prompt:定义了重新规划的提示模板,包含了当前的目标、原始计划和已执行的步骤。
  • replanner :将重新规划的提示模板与LLM组合,并指定输出的结构为 Act

8. 创建计算图

现在,我们创建代理的计算图,定义了代理的执行流程。

python 复制代码
from typing import Literal
from langgraph.graph import END

async def execute_step(state: PlanExecute):
    plan = state["plan"]
    plan_str = "\n".join(f"{i+1}. {step}" for i, step in enumerate(plan))
    task = plan[0]
    task_formatted = f"""For the following plan:
{plan_str}\n\nYou are tasked with executing step {1}, {task}."""
    agent_response = await agent_executor.ainvoke(
        {"messages": [("user", task_formatted)]}
    )
    return {
        "past_steps": [(task, agent_response["messages"][-1].content)],
    }

async def plan_step(state: PlanExecute):
    plan = await planner.ainvoke({"messages": [("user", state["input"])]})
    return {"plan": plan.steps}

async def replan_step(state: PlanExecute):
    output = await replanner.ainvoke(state)
    if isinstance(output.action, Response):
        return {"response": output.action.response}
    else:
        return {"plan": output.action.steps}

def should_end(state: PlanExecute):
    if "response" in state and state["response"]:
        return END
    else:
        return "agent"
  • execute_step :执行计划中的一个步骤。
    • state 中获取当前的计划,执行第一个步骤,并将结果添加到 past_steps
  • plan_step :初始规划步骤。
    • 使用 planner 根据用户输入生成计划。
  • replan_step :重新规划步骤。
    • 使用 replanner 根据当前状态(包括已执行的步骤)更新计划或生成最终响应。
  • should_end :判断是否结束执行。
    • 如果 response 存在且非空,则返回 END,否则继续执行。

创建计算图:

python 复制代码
from langgraph.graph import StateGraph, START

workflow = StateGraph(PlanExecute)

# 添加节点
workflow.add_node("planner", plan_step)
workflow.add_node("agent", execute_step)
workflow.add_node("replan", replan_step)

# 定义边
workflow.add_edge(START, "planner")
workflow.add_edge("planner", "agent")
workflow.add_edge("agent", "replan")

workflow.add_conditional_edges(
    "replan",
    should_end,
    ["agent", END],
)

# 编译计算图
app = workflow.compile()
  • StateGraph:定义状态图,描述代理的执行流程。
  • add_node:添加节点,关联到对应的函数。
  • add_edge:添加边,定义节点之间的顺序。
  • add_conditional_edges:添加条件边,根据状态决定下一个节点。
  • compile:编译状态图,生成可执行的应用程序。

9. 运行代理

现在,我们可以运行代理,并观察其执行过程。

python 复制代码
config = {"recursion_limit": 50}
inputs = {"input": "what is the hometown of the mens 2024 Australia open winner?"}

async for event in app.astream(inputs, config=config):
    for k, v in event.items():
        if k != "__end__":
            print(v)

输出示例:

{'plan': ["Identify the winner of the men's 2024 Australian Open.", 'Research the hometown of the identified winner.']}
{'past_steps': [("Identify the winner of the men's 2024 Australian Open.", "The winner of the men's singles tennis title at the 2024 Australian Open was Jannik Sinner. He defeated Daniil Medvedev in the final with scores of 3-6, 3-6, 6-4, 6-4, 6-3 to win his first major singles title.")]}
{'plan': ['Research the hometown of Jannik Sinner.']}
{'past_steps': [('Research the hometown of Jannik Sinner.', "Jannik Sinner's hometown is Sexten, which is located in northern Italy.")]}
{'response': "The hometown of the men's 2024 Australian Open winner, Jannik Sinner, is Sexten, located in northern Italy."}
  • 第一步 :生成计划,包含两个步骤:
    1. 确定2024年澳大利亚网球公开赛男子冠军。
    2. 调查该冠军的家乡。
  • 第二步:执行第一个步骤,获取冠军的信息。
  • 第三步:重新规划,因为第一个步骤已完成,更新计划,只剩下第二个步骤。
  • 第四步:执行第二个步骤,获取冠军的家乡信息。
  • 第五步:完成所有步骤,生成最终的响应。

10. 总结

通过这个示例,我们创建了一个"计划和执行"风格的代理,能够:

  • 生成多步骤的计划:利用强大的语言模型来规划任务。
  • 逐步执行计划:按顺序执行每个步骤,并跟踪执行结果。
  • 动态重新规划:根据执行结果,动态更新计划,确保任务能够顺利完成。
  • 结构化的状态管理 :利用 TypedDictpydantic 模型,清晰地定义和管理代理的状态。

优点

  • 显式的长期规划:能够有效处理需要多步骤解决的问题。
  • 模型效率:在执行阶段,可以使用较小的模型,只在规划阶段使用更强大的模型。

可能的改进

  • 并行执行:当前的设计是顺序执行,如果任务之间没有依赖关系,可以考虑并行执行以提高效率。
  • 更复杂的规划:可以将计划表示为有向无环图(DAG),以处理更复杂的任务依赖关系。

附加说明

  • LangGraph:一个用于构建可组合代理系统的框架,允许您定义代理的执行流程和状态管理。
  • LangChain:一个用于构建语言模型应用的库,提供了工具、链和代理等组件。

汇总

当然!以下是将上述LangGraph"计划并执行"智能体入门案例汇总到一个完整的Python脚本中的示例。所有的提示词(prompts)都已翻译成中文,以便您更好地理解和学习。

请按照以下步骤操作:

  1. 安装必要的包:确保您已经安装了所需的Python包。您可以在命令行中运行以下命令来安装:

    bash 复制代码
    pip install -U langgraph langchain-community langchain-openai tavily-python pydantic typing_extensions
  2. 设置API密钥:在运行脚本之前,请确保您已经拥有OpenAI和Tavily的API密钥。

  3. 运行脚本 :将以下代码保存为plan_and_execute_agent.py,然后在终端中运行:

    bash 复制代码
    python plan_and_execute_agent.py

以下是完整的Python脚本内容:

python 复制代码
# plan_and_execute_agent.py

import os
import getpass
import operator
import asyncio
from typing import Annotated, List, Tuple, Union
from typing_extensions import TypedDict

from pydantic import BaseModel, Field
from langchain import hub
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.prompts import ChatPromptTemplate
from langgraph.prebuilt import create_react_agent
from langgraph.graph import StateGraph, START, END
from dotenv import load_dotenv
# 导入 ChatTongyi 模型
from langchain_community.chat_models.tongyi import ChatTongyi

# 加载 .env 文件
load_dotenv()

# 读取环境变量
api_key = os.getenv("DASHSCOPE_API_KEY")

if not api_key:
    raise ValueError("API Key not found in .env file.")

# 初始化 LLM(ChatTongyi)
llm = ChatTongyi(model='qwen-plus')

# ----------------------------------
# 定义工具
# ----------------------------------

# 使用Tavily的内置搜索工具,最多返回3个结果
tools = [TavilySearchResults(max_results=3)]

# ----------------------------------
# 定义执行智能体
# ----------------------------------

# 获取执行智能体的提示模板
prompt = hub.pull("ih/ih-react-agent-executor")
print("预定义prompt:\n"+str(prompt))
prompt.pretty_print()


# 创建ReAct风格的智能体执行器
agent_executor = create_react_agent(llm, tools, state_modifier=prompt)


# ----------------------------------
# 定义状态
# ----------------------------------

class PlanExecute(TypedDict):
    input: str # 用户输入的原始请求
    plan: List[str] # 一个列表,包含生成的计划步骤
    past_steps: Annotated[List[Tuple], operator.add]# 一个列表,记录已执行的任务步骤
    response: str# 最终的回复结果


# ----------------------------------
# 规划步骤
# ----------------------------------

class Plan(BaseModel):
    """计划结构,包含多个步骤"""
    steps: List[str] = Field(
        description="需要遵循的不同步骤,应按顺序排列"
    )


# 创建规划提示模板
planner_prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            """对于给定的目标,制定一个简单的逐步计划。\
这个计划应该包含单独的任务,如果正确执行,将产生正确的答案。不要添加任何多余的步骤。\
最后一步的结果应该是最终答案。确保每个步骤都包含所有必要的信息------不要跳过步骤。""",
        ),
        ("placeholder", "{messages}"),
    ]
)

# 创建规划器,指定输出结构为Plan
planner = (planner_prompt |
           ChatTongyi(model='qwen-plus',temperature=0).with_structured_output(Plan))


# ----------------------------------
# 重新规划步骤
# ----------------------------------

class Response(BaseModel):
    """对用户的最终响应"""
    response: str


class Act(BaseModel):
    """要执行的行动"""
    # 可以是Response类型或者是Plan类型
    action: Union[Response, Plan] = Field(
        description="要执行的行动。如果您想回复用户,请使用Response。如果您需要进一步使用工具来获取答案,请使用Plan。"
    )


# 创建重新规划提示模板
replanner_prompt = ChatPromptTemplate.from_template(
    """对于给定的目标,制定一个简单的逐步计划。\
这个计划应该包含单独的任务,如果正确执行,将产生正确的答案。不要添加任何多余的步骤。\
最后一步的结果应该是最终答案。确保每个步骤都包含所有必要的信息------不要跳过步骤。

您的目标是:
{input}

您原来的计划是:
{plan}

您目前已经完成的步骤是:
{past_steps}

请相应地更新您的计划。如果不再需要更多步骤,可以返回给用户。否则,填写计划。只添加仍需完成的步骤。不要将已完成的步骤作为计划的一部分。"""
)

# 创建重新规划器,指定输出结构为Act
replanner = (replanner_prompt |
             ChatTongyi(model='qwen-plus',temperature=0).with_structured_output(Act))


# ----------------------------------
# 创建计算图
# ----------------------------------

async def execute_step(state: PlanExecute):
    """
    执行当前计划中的第一个任务,并将结果记录在past_steps中。
    """
    print("execute step:\n"+str(state))

    plan = state["plan"] # 获取当前的执行计划
    if not plan:
        return {} # 如果没有计划,直接返回空字典
    plan_str = "\n".join(f"{i + 1}. {step}" for i, step in enumerate(plan)) # 格式化计划位字符串
    task = plan[0] # 获取当前计划的第一个任务
    task_formatted = f"""对于以下计划:
{plan_str}

您需要执行第1步,{task}.""" # 构建用于执行的任务信息

    # 调用agent执行器,执行第一个任务
    agent_response = await agent_executor.ainvoke(
        {"messages": [("user", task_formatted)]}
    )

    # 获取执行结果
    result = agent_response["messages"][-1].content

    # 返回状态,记录执行的任务和结果
    return {
        "past_steps": [(task, result)],
    }


async def plan_step(state: PlanExecute):
    """
    根据用户输入生成初始计划。
    """
    # 从state中提取用户的输入,input是用户提供的目标或问题
    # 调用planner,他是基于ChatTongyi模型的规划器,负责生成任务计划
    plan = await planner.ainvoke({"messages": [("user", state["input"])]})
    # 返回生成的计划,结构是一个包含多个步骤plan.staps的字典
    return {"plan": plan.steps}


async def replan_step(state: PlanExecute):
    """
    根据当前状态,决定是生成新的计划还是直接给出响应。
    """
    print(f"Replan input state: {state}")  # 调试打印状态
    output = await replanner.ainvoke(state)
    # 调试:打印输出内容
    print(f"Replanner output: {output}")
    if output is None:
        raise ValueError("Replanner returned None, expected an Act with action field.")

    if isinstance(output.action, Response):
        return {"response": output.action.response}
    else:
        return {"plan": output.action.steps}


def should_end(state: PlanExecute):
    """
    检查是否有最终的响应,如果有,则结束,否则继续执行。
    """
    # 检查当前状态中是否有非空的响应(response)
    # 如果有相应,表示任务已经完成,应该结束
    if "response" in state and state["response"]:
        return END
    else:
        # 任务还未结束,返回agent,表示继续执行
        return "agent"


# 构建状态图
workflow = StateGraph(PlanExecute)

# 添加节点
workflow.add_node("planner", plan_step)
workflow.add_node("agent", execute_step)
workflow.add_node("replan", replan_step)

# 添加边
workflow.add_edge(START, "planner")
workflow.add_edge("planner", "agent")
workflow.add_edge("agent", "replan")

# 添加条件边
workflow.add_conditional_edges(
    "replan",
    should_end,
    ["agent", END],
)

# 编译状态图
app = workflow.compile()


# 可选:可视化状态图(需在Jupyter环境中运行)
# display(Image(app.get_graph(xray=True).draw_mermaid_png()))

# ----------------------------------
# 运行智能体
# ----------------------------------

async def run_agent():
    """
    运行计划并执行智能体,处理输入并输出结果。
    """
    config = {"recursion_limit": 50}
    inputs = {"input": "2024年澳大利亚网球公开赛男子冠军的家乡是哪里?"}

    async for event in app.astream(inputs, config=config):
        for k, v in event.items():
            if k != "__end__":
                print(v)


# 主函数
if __name__ == "__main__":
    asyncio.run(run_agent())

脚本说明

  1. 环境设置

    • 使用getpass模块安全地获取用户输入的API密钥,并将其设置为环境变量OPENAI_API_KEYTAVILY_API_KEY
  2. 定义工具

    • 使用Tavily的内置搜索工具TavilySearchResults,配置为最多返回3个搜索结果。
  3. 定义执行智能体

    • 从LangChain的Hub中拉取预定义的提示模板ih/ih-react-agent-executor
    • 使用OpenAI的gpt-4-turbo-preview模型作为LLM。
    • 创建ReAct风格的智能体执行器agent_executor
  4. 定义状态

    • 使用TypedDict定义智能体的状态结构,包括用户输入、计划、已完成的步骤和最终响应。
  5. 规划步骤

    • 使用Pydantic的BaseModel定义计划的结构Plan
    • 创建规划提示模板planner_prompt,并将其与LLM连接,指定输出结构为Plan
  6. 重新规划步骤

    • 定义ResponseAct模型,用于处理智能体的响应和行动。
    • 创建重新规划提示模板replanner_prompt,并将其与LLM连接,指定输出结构为Act
  7. 创建计算图

    • 定义三个主要的异步函数:
      • execute_step: 执行当前计划中的第一个任务,并记录结果。
      • plan_step: 根据用户输入生成初始计划。
      • replan_step: 根据当前状态决定是否需要重新规划或给出最终响应。
    • 定义结束条件函数should_end,用于判断智能体是否完成所有任务。
    • 使用StateGraph构建状态图,连接各个节点并添加条件边。
    • 编译状态图生成可运行的应用app
  8. 运行智能体

    • 定义异步函数run_agent,设置递归限制和输入问题,然后运行智能体并打印输出结果。
    • 在脚本的主入口处调用run_agent函数。

示例输出

运行脚本后,您应该会看到类似以下的输出:

{'plan': ["确定2024年澳大利亚网球公开赛的男子冠军。", "查找确定冠军的家乡。"]}
{'past_steps': [("确定2024年澳大利亚网球公开赛的男子冠军。", "2024年澳大利亚网球公开赛男子单打冠军是雅尼克·辛纳(Jannik Sinner)。他在决赛中击败了达尼尔·梅德韦杰夫(Daniil Medvedev),以3-6、3-6、6-4、6-4、6-3的比分赢得了他的第一个大满贯单打冠军。")]}
{'plan': ['查找雅尼克·辛纳的家乡。']}
{'past_steps': [('查找雅尼克·辛纳的家乡。', "雅尼克·辛纳的家乡是意大利北部的塞克斯滕(Sexten)。")]}
{'response': "2024年澳大利亚网球公开赛男子冠军雅尼克·辛纳的家乡是意大利北部的塞克斯滕。"}

最终,智能体将输出:

2024年澳大利亚网球公开赛男子冠军雅尼克·辛纳的家乡是意大利北部的塞克斯滕。

注意事项

  • API密钥:确保您的OpenAI和Tavily API密钥正确设置,否则脚本将无法正常运行。
  • 异步运行 :脚本使用了asyncio库来处理异步任务,确保您的Python环境支持异步编程。
  • 可视化:脚本中包含了可视化状态图的代码,但它在Jupyter环境中运行效果最佳。如果您在终端中运行,可以忽略相关代码。
  • 错误处理:为了简化示例,脚本中未包含详细的错误处理机制。在实际应用中,建议添加适当的错误处理代码以增强健壮性。

扩展阅读

希望这个完整的Python脚本能帮助您更好地理解和学习LangGraph的"计划并执行"智能体架构!

Reasoning without Observation

LangGraph 实现 ReWOO 示例教程讲解

在本教程中,我们将探讨如何使用 LangGraph 实现 ReWOO(Reasoning without Observation) 代理架构。ReWOO 架构旨在通过优化 token 消耗和简化微调过程来改进 ReACT 风格的代理。下面是对提供的示例的逐步详细解释。


ReWOO 概述

ReWOO 是由 Xu 等人提出的一种代理架构,旨在通过以下方式增强工具的使用:

  1. 减少 token 消耗和执行时间:在一次传递中生成完整的工具使用链,避免重复的 LLM 调用和冗余的前缀。
  2. 简化微调过程:规划数据不依赖于工具的输出,因此模型可以在不实际调用工具的情况下进行微调。

ReWOO 架构包含三个主要模块:

  • 🧠 Planner(规划器):生成计划,格式为一系列推理步骤,指定要使用的工具及其输入。
  • Worker(执行者):根据提供的参数执行指定的工具。
  • 🧠 Solver(求解器):基于工具的观察结果生成初始任务的答案。

注:标有 🧠 的模块涉及 LLM 调用。


环境设置

首先,我们需要安装必要的包,并设置 Tavily(一个搜索引擎工具)和 OpenAI 的 API 密钥。

python 复制代码
%%capture --no-stderr
%pip install -U langgraph langchain_community langchain_openai tavily-python

import getpass
import os

def _set_if_undefined(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}=")

_set_if_undefined("TAVILY_API_KEY")
_set_if_undefined("OPENAI_API_KEY")

定义图状态

LangGraph 中,每个节点都会更新共享的图状态。状态是一个字典,包含了节点执行所需的所有信息。

python 复制代码
from typing import List
from typing_extensions import TypedDict

class ReWOO(TypedDict):
    task: str
    plan_string: str
    steps: List
    results: dict
    result: str

实现 Planner(规划器)

规划器通过提示 LLM 来生成一个格式化的计划,包括推理步骤和工具使用,工具的输入可能包含特殊变量(如 #E1、#E2)用于变量替换。

规划器提示:

python 复制代码
prompt = """对于以下任务,请制定能够逐步解决问题的计划。对于每个计划,指明要使用的外部工具以及工具输入以获取证据。你可以将证据存储在变量 #E 中,供后续工具调用。(Plan, #E1, Plan, #E2, Plan, ...)

工具可以是以下之一:
(1) Google[input]:使用 Google 搜索结果的工具。适用于需要查找特定主题的简短答案时。输入应为搜索查询。
(2) LLM[input]:像你自己一样的预训练 LLM。当你有信心自己解决问题时,可以使用它。输入可以是任何指令。

例如,
任务:Thomas、Toby 和 Rebecca 一周共工作了 157 小时。Thomas 工作了 x 小时。Toby 工作了比 Thomas 两倍少 10 小时,Rebecca 工作了比 Toby 少 8 小时。Rebecca 工作了多少小时?
计划:给定 Thomas 工作了 x 小时,将问题翻译成代数表达式并用 Wolfram Alpha 求解。#E1 = WolframAlpha[求解 x + (2x − 10) + ((2x − 10) − 8) = 157]
计划:找出 Thomas 工作的小时数。#E2 = LLM[给定 #E1,x 等于多少]
计划:计算 Rebecca 工作的小时数。#E3 = Calculator[(2 ∗ #E2 − 10) − 8]

开始!
请用丰富的细节描述你的计划。每个计划后面只应跟随一个 #E。

任务:{task}"""

示例任务:

python 复制代码
task = "2024 年澳大利亚网球公开赛男子冠军的确切家乡是哪里"

调用 LLM 生成计划:

python 复制代码
from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o")
result = model.invoke(prompt.format(task=task))
print(result.content)

示例输出:

计划:使用 Google 搜索 2024 年澳大利亚网球公开赛男子冠军。
#E1 = Google[2024 年澳大利亚网球公开赛男子冠军]

计划:从搜索结果中获取冠军的姓名。
#E2 = LLM[根据 #E1,2024 年澳大利亚网球公开赛男子冠军是谁?]

计划:使用 Google 搜索该冠军的确切家乡。
#E3 = Google[2024 年澳大利亚网球公开赛男子冠军的家乡,基于 #E2]

计划:从搜索结果中获取冠军的家乡。
#E4 = LLM[根据 #E3,2024 年澳大利亚网球公开赛男子冠军的家乡是哪里?]

关键点:

  • 计划格式化:规划器生成的计划遵循特定格式,便于后续解析和执行。
  • 变量替换:使用 #E1、#E2 等变量,使得后续工具输入可以引用前一步的结果。

定义 Planner 节点:

python 复制代码
import re
from langchain_core.prompts import ChatPromptTemplate

# 正则表达式匹配计划格式
regex_pattern = r"计划:\s*(.+)\s*(#E\d+)\s*=\s*(\w+)\s*\[([^\]]+)\]"
prompt_template = ChatPromptTemplate.from_messages([("user", prompt)])
planner = prompt_template | model

def get_plan(state: ReWOO):
    task = state["task"]
    result = planner.invoke({"task": task})
    # 使用正则表达式解析计划
    matches = re.findall(regex_pattern, result.content)
    return {"steps": matches, "plan_string": result.content}

实现 Worker(执行者)

执行者根据规划器生成的计划,逐步执行工具,获取结果。

设置搜索引擎工具:

python 复制代码
from langchain_community.tools.tavily_search import TavilySearchResults

search = TavilySearchResults()

工具执行函数:

python 复制代码
def _get_current_task(state: ReWOO):
    if "results" not in state or state["results"] is None:
        return 1
    if len(state["results"]) == len(state["steps"]):
        return None
    else:
        return len(state["results"]) + 1

def tool_execution(state: ReWOO):
    """执行者节点,执行计划中的工具。"""
    _step = _get_current_task(state)
    _, step_name, tool, tool_input = state["steps"][_step - 1]
    _results = state.get("results", {})
    # 变量替换
    for k, v in _results.items():
        tool_input = tool_input.replace(k, v)
    # 根据工具类型执行
    if tool == "Google":
        result = search.invoke(tool_input)
    elif tool == "LLM":
        result = model.invoke(tool_input)
    else:
        raise ValueError("未知的工具")
    _results[step_name] = str(result)
    return {"results": _results}

关键点:

  • 顺序执行:工具按照计划的顺序执行,每次执行更新状态。
  • 变量替换:在工具输入中,替换之前步骤的结果,确保工具接收到正确的输入。

实现 Solver(求解器)

求解器使用规划器的计划和执行者的结果,生成最终的答案。

求解器提示:

python 复制代码
solve_prompt = """解决以下任务或问题。为了解决问题,我们已经制定了逐步的计划,并为每个计划检索了相应的证据。请谨慎使用它们,因为长证据可能包含不相关的信息。

{plan}

现在根据上述提供的证据解决问题或任务。直接给出答案,不要添加额外的词语。

任务:{task}
回答:"""

求解器函数:

python 复制代码
def solve(state: ReWOO):
    plan = ""
    for _plan, step_name, tool, tool_input in state["steps"]:
        _results = state.get("results", {})
        # 变量替换
        for k, v in _results.items():
            tool_input = tool_input.replace(k, v)
            step_name = step_name.replace(k, v)
        plan += f"计划:{_plan}\n{step_name} = {tool}[{tool_input}]"
    prompt = solve_prompt.format(plan=plan, task=state["task"])
    result = model.invoke(prompt)
    return {"result": result.content}

关键点:

  • 综合计划和结果:求解器将计划和工具结果结合起来,生成最终的提示。
  • 直接回答:要求模型直接给出答案,避免冗余信息。

定义图(Graph)

使用 LangGraphStateGraph 来定义代理的工作流程。

路由函数:

python 复制代码
def _route(state):
    _step = _get_current_task(state)
    if _step is None:
        # 所有任务已执行
        return "solve"
    else:
        # 继续执行工具
        return "tool"

构建图:

python 复制代码
from langgraph.graph import END, StateGraph, START

graph = StateGraph(ReWOO)
graph.add_node("plan", get_plan)
graph.add_node("tool", tool_execution)
graph.add_node("solve", solve)
graph.add_edge("plan", "tool")
graph.add_edge("solve", END)
graph.add_conditional_edges("tool", _route)
graph.add_edge(START, "plan")

app = graph.compile()

关键点:

  • 节点定义 :图包含三个主要节点:plantoolsolve
  • 条件路由:根据当前状态,决定下一个节点是继续执行工具还是求解。

运行图

执行编译后的图,传入任务,观察每个步骤的输出。

执行代码:

python 复制代码
for s in app.stream({"task": task}):
    print(s)
    print("---")

示例输出:

{'plan': {'plan_string': "...", 'steps': [...]}}
---
{'tool': {'results': {'#E1': '...'}}}
---
{'tool': {'results': {'#E1': '...', '#E2': '...'}}}
---
{'solve': {'result': 'San Candido, Italy'}}
---

获取最终结果:

python 复制代码
print(s["solve"]["result"])

输出:

San Candido, Italy

结论

通过本教程,我们成功地使用 LangGraph 实现了 ReWOO 代理架构。该代理:

  • 规划 了解决任务的详细步骤。
  • 执行 了指定的工具,获取了必要的信息。
  • 求解 了初始任务,给出了最终答案。

限制

尽管此实现展示了 ReWOO 的有效性,但仍存在一些限制:

  1. 上下文缺乏时的工具使用:如果缺乏环境的上下文,规划器在使用工具时可能会效率低下。这通常可以通过少样本提示或微调来改善。
  2. 顺序执行时间较长:任务仍然是顺序执行的,因此总执行时间受所有工具调用的影响,而不仅仅是最长的那一步。

重点总结:

  • ReWOO 架构:通过一次性生成完整的计划,减少了重复的 LLM 调用,提高了效率。
  • 模块化设计:将代理划分为规划器、执行者和求解器三个模块,便于理解和维护。
  • LangGraph 应用:使用 LangGraph 定义了代理的工作流程,实现了复杂的任务执行。

通过本次详细讲解,希望您对如何使用 LangGraph 实现 ReWOO 代理有了深入的理解。

汇总

完整的 ReWOO 示例代码汇总

以下是将之前讲解中的所有代码整合到一个 rew00_example.py 文件中的完整代码。请按照以下步骤操作,以便顺利执行和理解整个流程。

步骤概述

  1. 安装必要的包:确保安装所有需要的 Python 包。
  2. 设置 API 密钥:在运行脚本前,准备好 Tavily 和 OpenAI 的 API 密钥。
  3. 运行脚本:执行脚本以运行 ReWOO 代理,并获取最终结果。

1. 安装必要的包

在运行脚本之前,确保安装以下 Python 包。可以使用 pip 命令进行安装:

bash 复制代码
pip install -U langgraph langchain_community langchain_openai tavily-python

2. 完整的 rew00_example.py 代码

将以下代码复制到一个名为 rew00_example.py 的文件中:

python 复制代码
# rew00_example.py

import getpass
import os
import re
from typing import List
from typing_extensions import TypedDict

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_community.tools.tavily_search import TavilySearchResults
from langgraph.graph import END, StateGraph, START

# 定义 ReWOO 的状态字典
class ReWOO(TypedDict):
    task: str
    plan_string: str
    steps: List
    results: dict
    result: str

# 设置环境变量,如果未定义则提示输入
def _set_if_undefined(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}=")

# 设置 API 密钥
_set_if_undefined("TAVILY_API_KEY")
_set_if_undefined("OPENAI_API_KEY")

# 初始化 LLM 模型
model = ChatOpenAI(model="gpt-4o")

# 定义规划器的提示模板
prompt = """对于以下任务,请制定能够逐步解决问题的计划。对于每个计划,指明要使用的外部工具以及工具输入以获取证据。你可以将证据存储在变量 #E 中,供后续工具调用。(Plan, #E1, Plan, #E2, Plan, ...)

工具可以是以下之一:
(1) Google[input]:使用 Google 搜索结果的工具。适用于需要查找特定主题的简短答案时。输入应为搜索查询。
(2) LLM[input]:像你自己一样的预训练 LLM。当你有信心自己解决问题时,可以使用它。输入可以是任何指令。

例如,
任务:Thomas、Toby 和 Rebecca 一周共工作了 157 小时。Thomas 工作了 x 小时。Toby 工作了比 Thomas 两倍少 10 小时,Rebecca 工作了比 Toby 少 8 小时。Rebecca 工作了多少小时?
计划:给定 Thomas 工作了 x 小时,将问题翻译成代数表达式并用 Wolfram Alpha 求解。#E1 = WolframAlpha[求解 x + (2x − 10) + ((2x − 10) − 8) = 157]
计划:找出 Thomas 工作的小时数。#E2 = LLM[给定 #E1,x 等于多少]
计划:计算 Rebecca 工作的小时数。#E3 = Calculator[(2 ∗ #E2 − 10) − 8]

开始!
请用丰富的细节描述你的计划。每个计划后面只应跟随一个 #E。

任务:{task}"""

# 创建规划器模板
prompt_template = ChatPromptTemplate.from_messages([("user", prompt)])
planner = prompt_template | model

# 定义获取计划的函数
def get_plan(state: ReWOO):
    task = state["task"]
    result = planner.invoke({"task": task})
    # 正则表达式匹配计划格式
    regex_pattern = r"计划:\s*(.+)\s*(#E\d+)\s*=\s*(\w+)\s*\[([^\]]+)\]"
    matches = re.findall(regex_pattern, result.content)
    return {"steps": matches, "plan_string": result.content}

# 初始化搜索工具
search = TavilySearchResults()

# 定义获取当前任务的函数
def _get_current_task(state: ReWOO):
    if "results" not in state or state["results"] is None:
        return 1
    if len(state["results"]) == len(state["steps"]):
        return None
    else:
        return len(state["results"]) + 1

# 定义工具执行函数
def tool_execution(state: ReWOO):
    """执行者节点,执行计划中的工具。"""
    _step = _get_current_task(state)
    if _step is None:
        return {}
    _, step_name, tool, tool_input = state["steps"][_step - 1]
    _results = state.get("results", {})
    # 变量替换
    for k, v in _results.items():
        tool_input = tool_input.replace(k, v)
    # 根据工具类型执行
    if tool == "Google":
        result = search.invoke(tool_input)
    elif tool == "LLM":
        result = model.invoke(tool_input)
    else:
        raise ValueError("未知的工具")
    _results[step_name] = str(result)
    return {"results": _results}

# 定义求解器提示
solve_prompt = """解决以下任务或问题。为了解决问题,我们已经制定了逐步的计划,并为每个计划检索了相应的证据。请谨慎使用它们,因为长证据可能包含不相关的信息。

{plan}

现在根据上述提供的证据解决问题或任务。直接给出答案,不要添加额外的词语。

任务:{task}
回答:"""

# 定义求解器函数
def solve(state: ReWOO):
    plan = ""
    for _plan, step_name, tool, tool_input in state["steps"]:
        _results = state.get("results", {})
        # 变量替换
        for k, v in _results.items():
            tool_input = tool_input.replace(k, v)
            step_name = step_name.replace(k, v)
        plan += f"计划:{_plan}\n{step_name} = {tool}[{tool_input}]\n"
    prompt = solve_prompt.format(plan=plan, task=state["task"])
    result = model.invoke(prompt)
    return {"result": result.content}

# 定义路由函数
def _route(state):
    _step = _get_current_task(state)
    if _step is None:
        # 所有任务已执行
        return "solve"
    else:
        # 继续执行工具
        return "tool"

# 构建图
graph = StateGraph(ReWOO)
graph.add_node("plan", get_plan)
graph.add_node("tool", tool_execution)
graph.add_node("solve", solve)
graph.add_edge("plan", "tool")
graph.add_edge("solve", END)
graph.add_conditional_edges("tool", _route)
graph.add_edge(START, "plan")

# 编译图
app = graph.compile()

# 定义任务
task = "2024 年澳大利亚网球公开赛男子冠军的确切家乡是哪里"

# 运行图并打印输出
if __name__ == "__main__":
    for s in app.stream({"task": task}):
        print(s)
        print("---")
    # 获取并打印最终结果
    if "solve" in s and "result" in s["solve"]:
        print("最终结果:")
        print(s["solve"]["result"])

3. 代码说明

以下是对上述代码的详细说明,以帮助您更好地理解各部分的功能:

3.1 导入必要的库
python 复制代码
import getpass
import os
import re
from typing import List
from typing_extensions import TypedDict

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_community.tools.tavily_search import TavilySearchResults
from langgraph.graph import END, StateGraph, START
  • getpassos:用于安全地获取和设置环境变量(API 密钥)。
  • re:用于正则表达式匹配,解析规划器生成的计划。
  • TypedDict:定义状态字典的类型。
  • LangChain 和 LangGraph:用于与 OpenAI 的 LLM 交互和构建状态图。
3.2 定义状态字典
python 复制代码
class ReWOO(TypedDict):
    task: str
    plan_string: str
    steps: List
    results: dict
    result: str
  • ReWOO:定义了整个流程中共享的状态,包括任务、计划、步骤、结果和最终答案。
3.3 设置环境变量
python 复制代码
def _set_if_undefined(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}=")

_set_if_undefined("TAVILY_API_KEY")
_set_if_undefined("OPENAI_API_KEY")
  • _set_if_undefined:检查环境变量是否已设置,如果未设置,则提示用户输入。
3.4 初始化 LLM 模型
python 复制代码
model = ChatOpenAI(model="gpt-4o")
  • 使用 OpenAI 的 GPT-4 模型进行自然语言处理。
3.5 定义规划器的提示模板
python 复制代码
prompt = """..."""
prompt_template = ChatPromptTemplate.from_messages([("user", prompt)])
planner = prompt_template | model
  • prompt:定义了如何指导 LLM 生成解决任务的计划。
  • prompt_template:将提示模板与 LLM 模型关联。
3.6 定义获取计划的函数
python 复制代码
def get_plan(state: ReWOO):
    task = state["task"]
    result = planner.invoke({"task": task})
    regex_pattern = r"计划:\s*(.+)\s*(#E\d+)\s*=\s*(\w+)\s*\[([^\]]+)\]"
    matches = re.findall(regex_pattern, result.content)
    return {"steps": matches, "plan_string": result.content}
  • get_plan:调用规划器生成计划,并使用正则表达式解析计划中的每一步。
3.7 初始化搜索工具
python 复制代码
search = TavilySearchResults()
  • TavilySearchResults:用于执行 Google 搜索的工具。
3.8 定义获取当前任务的函数
python 复制代码
def _get_current_task(state: ReWOO):
    if "results" not in state or state["results"] is None:
        return 1
    if len(state["results"]) == len(state["steps"]):
        return None
    else:
        return len(state["results"]) + 1
  • _get_current_task:确定当前需要执行的任务步骤。
3.9 定义工具执行函数
python 复制代码
def tool_execution(state: ReWOO):
    _step = _get_current_task(state)
    if _step is None:
        return {}
    _, step_name, tool, tool_input = state["steps"][_step - 1]
    _results = state.get("results", {})
    for k, v in _results.items():
        tool_input = tool_input.replace(k, v)
    if tool == "Google":
        result = search.invoke(tool_input)
    elif tool == "LLM":
        result = model.invoke(tool_input)
    else:
        raise ValueError("未知的工具")
    _results[step_name] = str(result)
    return {"results": _results}
  • tool_execution:根据规划器生成的步骤,执行相应的工具(Google 搜索或 LLM 调用),并更新结果。
3.10 定义求解器提示和函数
python 复制代码
solve_prompt = """..."""

def solve(state: ReWOO):
    plan = ""
    for _plan, step_name, tool, tool_input in state["steps"]:
        _results = state.get("results", {})
        for k, v in _results.items():
            tool_input = tool_input.replace(k, v)
            step_name = step_name.replace(k, v)
        plan += f"计划:{_plan}\n{step_name} = {tool}[{tool_input}]\n"
    prompt = solve_prompt.format(plan=plan, task=state["task"])
    result = model.invoke(prompt)
    return {"result": result.content}
  • solve_prompt:定义了如何指导 LLM 使用计划和工具结果生成最终答案。
  • solve:整合计划和结果,调用 LLM 生成最终答案。
3.11 定义路由函数和构建图
python 复制代码
def _route(state):
    _step = _get_current_task(state)
    if _step is None:
        return "solve"
    else:
        return "tool"

graph = StateGraph(ReWOO)
graph.add_node("plan", get_plan)
graph.add_node("tool", tool_execution)
graph.add_node("solve", solve)
graph.add_edge("plan", "tool")
graph.add_edge("solve", END)
graph.add_conditional_edges("tool", _route)
graph.add_edge(START, "plan")

app = graph.compile()
  • _route:决定下一个执行的节点是继续执行工具还是转向求解。
  • StateGraph:定义了整个流程的节点和边,包括条件路由。
3.12 定义任务并运行图
python 复制代码
task = "2024 年澳大利亚网球公开赛男子冠军的确切家乡是哪里"

if __name__ == "__main__":
    for s in app.stream({"task": task}):
        print(s)
        print("---")
    # 获取并打印最终结果
    if "solve" in s and "result" in s["solve"]:
        print("最终结果:")
        print(s["solve"]["result"])
  • task:定义需要解决的具体任务。
  • app.stream:运行整个图,并逐步输出每个节点的结果。
  • 最终结果:在所有工具执行完毕后,打印最终的答案。

4. 运行脚本

确保已经设置了环境变量 TAVILY_API_KEYOPENAI_API_KEY。如果尚未设置,运行脚本时会提示输入。

在终端中运行以下命令:

bash 复制代码
python rew00_example.py

5. 示例输出

运行脚本后,您将看到类似以下的输出:

{'plan': {'plan_string': "计划:使用 Google 搜索 2024 年澳大利亚网球公开赛男子冠军。\n#E1 = Google[2024 年澳大利亚网球公开赛男子冠军]\n计划:从搜索结果中获取冠军的姓名。\n#E2 = LLM[根据 #E1,2024 年澳大利亚网球公开赛男子冠军是谁?]\n计划:使用 Google 搜索该冠军的确切家乡。\n#E3 = Google[2024 年澳大利亚网球公开赛男子冠军的家乡,基于 #E2]\n计划:从搜索结果中获取冠军的家乡。\n#E4 = LLM[根据 #E3,2024 年澳大利亚网球公开赛男子冠军的家乡是哪里?]"}}
---
{'tool': {'results': {'#E1': '...'}}}
---
{'tool': {'results': {'#E1': '...', '#E2': '...'}}}
---
{'solve': {'result': 'San Candido, Italy'}}
---
最终结果:
San Candido, Italy

6. 重要提示

  • API 密钥:确保您拥有有效的 Tavily 和 OpenAI API 密钥,并正确设置环境变量。
  • 网络连接:脚本需要访问外部 API,确保您的网络连接正常。
  • 错误处理:如果在执行过程中遇到错误,请检查 API 密钥是否正确,或者相应的工具是否配置正确。

7. 总结

通过以上步骤,您可以成功运行 ReWOO 代理架构,并理解其如何通过规划、工具执行和求解生成最终答案。该脚本展示了如何使用 LangGraph 构建复杂的 LLM 驱动的工作流,并有效地结合多种工具来解决实际问题。

如有任何疑问或需要进一步的帮助,请随时提问!

LLMCompiler

下面我将为您详细讲解LangGraph官方入门案例------LLMCompiler。我们将逐步解释其中的各个组件和代码示例。

LLMCompiler简介

LLMCompiler是一种代理(agent)架构,旨在通过在有向无环图(DAG)中提前执行任务来加速代理任务的执行。同时,它通过减少对大型语言模型(LLM)的调用次数,节省了冗余的token使用成本。其计算图的概览如下:

LLMCompiler计算图

它主要包含三个组件:

  1. 计划器(Planner):生成任务的DAG。
  2. 任务获取单元(Task Fetching Unit):调度并尽快执行可执行的任务。
  3. 合并器(Joiner):向用户响应或触发第二次计划。

该案例将逐步演示如何使用LangGraph实现LLMCompiler。最终,您将看到类似以下的执行过程。

环境设置

首先,我们需要安装所需的包并设置API密钥。

python 复制代码
%pip install -U langchain_openai langsmith langgraph langchain numexpr

import getpass
import os

def _get_pass(var: str):
    if var not in os.environ:
        os.environ[var] = getpass.getpass(f"{var}: ")

_get_pass("OPENAI_API_KEY")

设置LangSmith用于LangGraph开发

注册LangSmith,以便快速发现问题并提高LangGraph项目的性能。LangSmith允许您使用跟踪数据来调试、测试和监控使用LangGraph构建的LLM应用程序。

辅助文件

数学工具

将以下代码放入名为math_tools.py的文件中,并确保您可以在笔记本中导入它。

(此处省略具体代码,请确保您已正确配置math_tools.py文件。)

输出解析器

(同样,此处省略具体代码,请确保您已正确配置输出解析器。)

定义工具

我们首先为代理定义要使用的工具。在本示例中,我们将使用搜索引擎和计算器的组合。

python 复制代码
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_openai import ChatOpenAI
from math_tools import get_math_tool

_get_pass("TAVILY_API_KEY")

calculate = get_math_tool(ChatOpenAI(model="gpt-4-turbo-preview"))
search = TavilySearchResults(
    max_results=1,
    description='tavily_search_results_json(query="the search query") - 一个搜索引擎。',
)

tools = [search, calculate]

如果您不想注册Tavily,可以使用免费的DuckDuckGo替代。

接下来,我们测试一下calculate工具:

python 复制代码
calculate.invoke(
    {
        "problem": "What's the temp of sf + 5?",
        "context": ["The temperature of sf is 32 degrees"],
    }
)

输出:

'37'

计划器(Planner)

计划器接受输入问题并生成要执行的任务列表。如果提供了先前的计划,它会被指示重新计划,这在第一次批量任务完成后,代理需要采取更多操作时很有用。

以下代码构建了计划器的提示模板,并将其与LLM和输出解析器(在output_parser.py中定义)组合。输出解析器处理以下格式的任务列表:

1. tool_1(arg1="arg1", arg2=3.5, ...)
Thought: 接下来我要使用tool_2来找到Y
2. tool_2(arg1="", arg2="${1}")
3. join()<END_OF_PLAN>

"Thought"行是可选的。${#}占位符是变量,用于将工具(任务)输出传递给其他工具。

python 复制代码
from typing import Sequence
from langchain import hub
from langchain_core.language_models import BaseChatModel
from langchain_core.messages import (
    BaseMessage,
    FunctionMessage,
    HumanMessage,
    SystemMessage,
)
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableBranch
from langchain_core.tools import BaseTool
from langchain_openai import ChatOpenAI
from output_parser import LLMCompilerPlanParser, Task

prompt = hub.pull("wfh/llm-compiler")
print(prompt.pretty_print())

此代码从集线器中拉取了一个预定义的提示模板,并打印出来。

接下来,我们定义创建计划器的函数:

python 复制代码
def create_planner(
    llm: BaseChatModel, tools: Sequence[BaseTool], base_prompt: ChatPromptTemplate
):
    # (函数内容省略,主要是设置计划器的提示和重计划逻辑)
    return planner

我们初始化LLM和计划器:

python 复制代码
llm = ChatOpenAI(model="gpt-4-turbo-preview")
planner = create_planner(llm, tools, prompt)

测试计划器:

python 复制代码
example_question = "What's the temperature in SF raised to the 3rd power?"

for task in planner.stream([HumanMessage(content=example_question)]):
    print(task["tool"], task["args"])
    print("---")

输出将显示计划器生成的任务列表,包括要使用的工具和参数。

任务获取单元(Task Fetching Unit)

此组件负责调度任务。它接收如下格式的任务流:

python 复制代码
{
    tool: BaseTool,
    dependencies: number[],
}

基本思想是,一旦任务的依赖被满足,就开始执行任务。这是通过多线程实现的。下面的代码将任务获取单元和执行器结合起来。

首先,我们定义一些辅助函数和类,用于解析和执行任务。

python 复制代码
import re
import time
from concurrent.futures import ThreadPoolExecutor, wait
from typing import Any, Dict, Iterable, List, Union

from langchain_core.runnables import chain as as_runnable
from typing_extensions import TypedDict

# (省略部分辅助函数的具体实现)

然后,我们定义调度任务的函数schedule_tasks,它将任务分组到一个DAG调度中,并处理任务的执行和依赖关系。

示例计划

在此步骤中,我们展示一个示例计划,说明如何将计划器和任务获取单元结合使用。

python 复制代码
tool_messages = plan_and_schedule.invoke(
    {"messages": [HumanMessage(content=example_question)]}
)["messages"]

tool_messages

这将输出任务执行的结果,包括每个工具的输出。

合并器(Joiner)

现在,我们需要处理这些输出,确定是应该给出最终答案,还是需要重新计划。我们使用另一个LLM调用来实现这一点,利用函数调用来提高解析的可靠性。

首先,我们定义输出的Pydantic模型:

python 复制代码
from langchain_core.messages import AIMessage
from pydantic import BaseModel, Field

class FinalResponse(BaseModel):
    response: str

class Replan(BaseModel):
    feedback: str = Field(
        description="对先前尝试的分析和对需要改进的建议。"
    )

class JoinOutputs(BaseModel):
    thought: str = Field(
        description="选择的行动的推理过程。"
    )
    action: Union[FinalResponse, Replan]

然后,我们定义合并器的提示模板和可运行对象。

python 复制代码
joiner_prompt = hub.pull("wfh/llm-compiler-joiner").partial(
    examples=""
)
llm = ChatOpenAI(model="gpt-4-turbo-preview")

runnable = joiner_prompt | llm.with_structured_output(JoinOutputs)

接着,我们定义解析合并器输出的函数和选择最近消息的函数。

python 复制代码
def _parse_joiner_output(decision: JoinOutputs) -> List[BaseMessage]:
    # (函数内容省略)
    return response

def select_recent_messages(state) -> dict:
    # (函数内容省略)
    return {"messages": selected[::-1]}

joiner = select_recent_messages | runnable | _parse_joiner_output

测试合并器:

python 复制代码
input_messages = [HumanMessage(content=example_question)] + tool_messages

joiner.invoke({"messages": input_messages})

这将输出合并器的决策,指示是给出最终答案还是需要重新计划。

使用LangGraph组合

我们将代理定义为一个有状态的图,其中主要节点是:

  1. 计划和执行(来自第一步的DAG)
  2. 合并:确定是否完成或需要重新计划
  3. 重新上下文化:根据合并器的输出更新图状态

首先,我们定义状态图并添加节点和边。

python 复制代码
from langgraph.graph import END, StateGraph, START
from langgraph.graph.message import add_messages
from typing import Annotated

class State(TypedDict):
    messages: Annotated[list, add_messages]

graph_builder = StateGraph(State)

# 添加节点
graph_builder.add_node("plan_and_schedule", plan_and_schedule)
graph_builder.add_node("join", joiner)

# 定义边
graph_builder.add_edge("plan_and_schedule", "join")

def should_continue(state):
    messages = state["messages"]
    if isinstance(messages[-1], AIMessage):
        return END
    return "plan_and_schedule"

graph_builder.add_conditional_edges(
    "join",
    should_continue,
)
graph_builder.add_edge(START, "plan_and_schedule")
chain = graph_builder.compile()

示例运行

简单问题

python 复制代码
for step in chain.stream(
    {"messages": [HumanMessage(content="What's the GDP of New York?")]}
):
    print(step)
    print("---")

这将输出代理在回答简单问题时的执行步骤。

多跳问题

python 复制代码
steps = chain.stream(
    {
        "messages": [
            HumanMessage(
                content="What's the oldest parrot alive, and how much longer is that than the average?"
            )
        ]
    },
    {
        "recursion_limit": 100,
    },
)
for step in steps:
    print(step)
    print("---")

这展示了代理在处理需要多次搜索的问题时的执行过程。

多步数学计算

python 复制代码
for step in chain.stream(
    {
        "messages": [
            HumanMessage(
                content="What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?"
            )
        ]
    }
):
    print(step)

代理将执行多个数学计算,并合并结果。

复杂的重新计划示例

python 复制代码
for step in chain.stream(
    {
        "messages": [
            HumanMessage(
                content="Find the current temperature in Tokyo, then, respond with a flashcard summarizing this information"
            )
        ]
    }
):
    print(step)

此示例展示了代理在需要重新计划时的行为。

结论

恭喜您构建了第一个LLMCompiler代理!以下是上述实现的一些已知限制:

  • 计划器输出解析的格式脆弱:如果您的函数需要超过1或2个参数,解析可能会失败。可以通过使用流式工具调用来提高可靠性。
  • 变量替换脆弱:在上述示例中,变量替换可能不够健壮。可以使用微调模型和更健壮的语法(例如使用Lark或工具调用模式)来改进。
  • 状态可能变得很长:如果您需要多次重新计划,状态可能会变得很长。为了解决这个问题,您可以在超过一定的token限制后添加消息压缩器。

希望这个详细的讲解对您理解LLMCompiler在LangGraph中的实现有所帮助。

汇总

好的,下面我将帮助您将上述LangGraph官方入门案例的代码汇总到一个可运行的Python文件中。为了便于理解和运行,我们将分为几个部分:

  1. 安装依赖包并设置API密钥
  2. 创建辅助文件
    • math_tools.py
    • output_parser.py
  3. 编写主脚本
    • llmcompiler_demo.py

请按照以下步骤操作:


1. 安装依赖包并设置API密钥

首先,确保您已经安装了所需的Python包,并设置了必要的API密钥。

安装依赖包

在您的终端或命令提示符中运行以下命令来安装所需的包:

bash 复制代码
pip install -U langchain_openai langsmith langgraph langchain numexpr pydantic

设置API密钥

您需要设置以下环境变量:

  • OPENAI_API_KEY:用于OpenAI的API密钥。
  • TAVILY_API_KEY:用于Tavily搜索引擎的API密钥。

您可以在运行脚本前在终端中设置这些环境变量,例如:

bash 复制代码
export OPENAI_API_KEY='your_openai_api_key'
export TAVILY_API_KEY='your_tavily_api_key'

或者在Windows上:

cmd 复制代码
set OPENAI_API_KEY=your_openai_api_key
set TAVILY_API_KEY=your_tavily_api_key

2. 创建辅助文件

a. math_tools.py

创建一个名为 math_tools.py 的文件,并将以下代码粘贴进去:

python 复制代码
# math_tools.py

from langchain_core.tools import BaseTool
from typing import Optional, List
import math

def get_math_tool(llm):
    class MathTool(BaseTool):
        name = "math"
        description = (
            "math(problem: str, context: Optional[list[str]]) -> float:\n"
            " - 解决提供的数学问题。\n"
            " - `problem` 可以是简单的数学问题(例如 \"1 + 3\")或文字题(例如 \"如果有3个苹果和2个苹果,总共有多少个苹果\")。\n"
            " - 每次只能计算一个表达式。\n"
            " - 尽量减少 `math` 操作的次数。\n"
            " - 可以选择性地提供一个字符串列表作为 `context` 来帮助解决问题。\n"
            " - 如果需要对之前的任务输出进行数学计算,必须将其作为 `context` 提供。\n"
        )
        args_schema = {
            "problem": str,
            "context": Optional[List[str]]
        }

        def invoke(self, args: dict, config: Optional[dict] = None) -> str:
            problem = args.get("problem")
            context = args.get("context", [])
            try:
                # 这里可以集成更复杂的数学解析器,如SymPy
                result = eval(problem, {"__builtins__": None}, {})
                return str(result)
            except Exception as e:
                return f"ERROR(Failed to evaluate the expression. Error: {str(e)})"
    
    return MathTool()

b. output_parser.py

创建一个名为 output_parser.py 的文件,并将以下代码粘贴进去:

python 复制代码
# output_parser.py

from langchain_core.tools import BaseTool
from langchain_core.messages import FunctionMessage
from typing import List, Dict, Any
from pydantic import BaseModel
from typing import Union

class Task(BaseModel):
    idx: int
    tool: BaseTool
    args: Dict[str, Any]
    dependencies: List[int]

class LLMCompilerPlanParser(BaseModel):
    tools: List[BaseTool]

    def parse(self, plan_str: str) -> List[Task]:
        tasks = []
        lines = plan_str.strip().split('\n')
        for line in lines:
            if line.startswith("join"):
                task = Task(idx=len(tasks)+1, tool="join", args={}, dependencies=[])
                tasks.append(task)
                continue
            if not line.strip():
                continue
            parts = line.split(". ", 1)
            if len(parts) != 2:
                continue
            idx_str, rest = parts
            idx = int(idx_str)
            if "(" in rest and rest.endswith(")"):
                tool_name, args_str = rest.split("(", 1)
                args_str = args_str[:-1]  # Remove the closing parenthesis
                args = eval(args_str)  # 注意:在实际应用中请避免使用eval
                dependencies = []  # 这里可以根据args中的变量来解析依赖关系
                task = Task(idx=idx, tool=tool_name, args=args, dependencies=dependencies)
                tasks.append(task)
        return tasks

注意 :在实际应用中,使用 eval 解析字符串可能会带来安全风险。请确保输入内容的安全性,或使用更安全的解析方法。


3. 编写主脚本

创建一个名为 llmcompiler_demo.py 的文件,并将以下代码粘贴进去:

python 复制代码
# llmcompiler_demo.py

import os
import getpass
import itertools
import re
import time
from concurrent.futures import ThreadPoolExecutor, wait
from typing import Any, Dict, Iterable, List, Union, Sequence
from typing_extensions import TypedDict

from langchain import hub
from langchain_core.language_models import BaseChatModel
from langchain_core.messages import (
    BaseMessage,
    FunctionMessage,
    HumanMessage,
    SystemMessage,
    AIMessage,
)
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableBranch
from langchain_core.tools import BaseTool
from langchain_openai import ChatOpenAI

from langgraph.graph import END, StateGraph, START
from langgraph.graph.message import add_messages

from output_parser import LLMCompilerPlanParser, Task
from math_tools import get_math_tool

from pydantic import BaseModel, Field

# 设置API密钥
def _get_pass(var: str):
    if var not in os.environ:
        os.environ[var] = getpass.getpass(f"{var}: ")

_get_pass("OPENAI_API_KEY")
_get_pass("TAVILY_API_KEY")

# 设置LangSmith(可选)
# 您可以根据需要设置LangSmith,本文档中不涉及具体配置

# 定义工具
from langchain_community.tools.tavily_search import TavilySearchResults

calculate = get_math_tool(ChatOpenAI(model="gpt-4-turbo-preview"))
search = TavilySearchResults(
    max_results=1,
    description='tavily_search_results_json(query="the search query") - 一个搜索引擎。',
)

tools = [search, calculate]

# 计划器(Planner)
prompt = hub.pull("wfh/llm-compiler")
print("Planner Prompt:")
print(prompt.pretty_print())

def create_planner(
    llm: BaseChatModel, tools: Sequence[BaseTool], base_prompt: ChatPromptTemplate
):
    tool_descriptions = "\n".join(
        f"{i+1}. {tool.description}\n"
        for i, tool in enumerate(tools)
    )
    planner_prompt = base_prompt.partial(
        replan="",
        num_tools=len(tools)
        + 1,  # 添加 join() 工具
        tool_descriptions=tool_descriptions,
    )
    replanner_prompt = base_prompt.partial(
        replan=' - 您将获得"Previous Plan",即上一个代理创建的计划以及每个计划的执行结果(作为 Observation),以及关于已执行结果的一般思考(作为 Thought)。'
               '您必须使用这些信息来创建"Current Plan"。\n'
               ' - 在开始 Current Plan 时,您应该以概述下一步计划策略的"Thought"开头。\n'
               ' - 在 Current Plan 中,您绝不应重复 Previous Plan 中已执行的操作。\n'
               ' - 您必须从上一个任务的结束索引继续任务索引。不要重复任务索引。',
        num_tools=len(tools) + 1,
        tool_descriptions=tool_descriptions,
    )

    def should_replan(state: list):
        return isinstance(state[-1], SystemMessage)

    def wrap_messages(state: list):
        return {"messages": state}

    def wrap_and_get_last_index(state: list):
        next_task = 0
        for message in state[::-1]:
            if isinstance(message, FunctionMessage):
                next_task = message.additional_kwargs["idx"] + 1
                break
        state[-1].content = state[-1].content + f" - Begin counting at : {next_task}"
        return {"messages": state}

    return (
        RunnableBranch(
            (should_replan, wrap_and_get_last_index | replanner_prompt),
            wrap_messages | planner_prompt,
        )
        | llm
        | LLMCompilerPlanParser(tools=tools)
    )

llm = ChatOpenAI(model="gpt-4-turbo-preview")
planner = create_planner(llm, tools, prompt)

example_question = "What's the temperature in SF raised to the 3rd power?"

print("\nPlanning Tasks:")
for task in planner.stream([HumanMessage(content=example_question)]):
    print(task["tool"], task["args"])
    print("---")

# 任务获取单元(Task Fetching Unit)
def _get_observations(messages: List[BaseMessage]) -> Dict[int, Any]:
    results = {}
    for message in messages[::-1]:
        if isinstance(message, FunctionMessage):
            results[int(message.additional_kwargs["idx"])] = message.content
    return results

class SchedulerInput(TypedDict):
    messages: List[BaseMessage]
    tasks: Iterable[Task]

def _execute_task(task, observations, config):
    tool_to_use = task["tool"]
    if isinstance(tool_to_use, str):
        return tool_to_use
    args = task["args"]
    try:
        if isinstance(args, str):
            resolved_args = _resolve_arg(args, observations)
        elif isinstance(args, dict):
            resolved_args = {
                key: _resolve_arg(val, observations) for key, val in args.items()
            }
        else:
            resolved_args = args
    except Exception as e:
        return (
            f"ERROR(Failed to call {tool_to_use.name} with args {args}.)"
            f" Args could not be resolved. Error: {repr(e)}"
        )
    try:
        return tool_to_use.invoke(resolved_args, config)
    except Exception as e:
        return (
            f"ERROR(Failed to call {tool_to_use.name} with args {args}."
            + f" Args resolved to {resolved_args}. Error: {repr(e)})"
        )

def _resolve_arg(arg: Union[str, Any], observations: Dict[int, Any]):
    ID_PATTERN = r"\$\{?(\d+)\}?"

    def replace_match(match):
        idx = int(match.group(1))
        return str(observations.get(idx, match.group(0)))

    if isinstance(arg, str):
        return re.sub(ID_PATTERN, replace_match, arg)
    elif isinstance(arg, list):
        return [_resolve_arg(a, observations) for a in arg]
    else:
        return str(arg)

def schedule_task(task_inputs, config):
    task: Task = task_inputs["task"]
    observations: Dict[int, Any] = task_inputs["observations"]
    try:
        observation = _execute_task(task, observations, config)
    except Exception:
        import traceback
        observation = traceback.format_exc()
    observations[task["idx"]] = observation

def schedule_pending_task(
    task: Task, observations: Dict[int, Any], retry_after: float = 0.2
):
    while True:
        deps = task["dependencies"]
        if deps and (any([dep not in observations for dep in deps])):
            time.sleep(retry_after)
            continue
        schedule_task({"task": task, "observations": observations}, config=None)
        break

def schedule_tasks(scheduler_input: SchedulerInput) -> List[FunctionMessage]:
    tasks = scheduler_input["tasks"]
    args_for_tasks = {}
    messages = scheduler_input["messages"]
    observations = _get_observations(messages)
    task_names = {}
    originals = set(observations)
    futures = []
    retry_after = 0.25
    with ThreadPoolExecutor() as executor:
        for task in tasks:
            deps = task["dependencies"]
            task_names[task["idx"]] = (
                task["tool"] if isinstance(task["tool"], str) else task["tool"].name
            )
            args_for_tasks[task["idx"]] = task["args"]
            if (
                deps and (any([dep not in observations for dep in deps]))
            ):
                futures.append(
                    executor.submit(
                        schedule_pending_task, task, observations, retry_after
                    )
                )
            else:
                schedule_task({"task": task, "observations": observations}, config=None)
        wait(futures)
    new_observations = {
        k: (task_names[k], args_for_tasks[k], observations[k])
        for k in sorted(set(observations.keys()) - originals)
    }
    tool_messages = [
        FunctionMessage(
            name=name,
            content=str(obs),
            additional_kwargs={"idx": k, "args": task_args},
            tool_call_id=k,
        )
        for k, (name, task_args, obs) in new_observations.items()
    ]
    return tool_messages

def plan_and_schedule(state):
    messages = state["messages"]
    tasks = planner.stream(messages)
    try:
        tasks = itertools.chain([next(tasks)], tasks)
    except StopIteration:
        tasks = iter([])
    scheduled_tasks = schedule_tasks({
        "messages": messages,
        "tasks": tasks,
    })
    return {"messages": scheduled_tasks}

# 合并器(Joiner)
class FinalResponse(BaseModel):
    response: str

class Replan(BaseModel):
    feedback: str = Field(
        description="对先前尝试的分析和对需要改进的建议。"
    )

class JoinOutputs(BaseModel):
    thought: str = Field(
        description="选择的行动的推理过程。"
    )
    action: Union[FinalResponse, Replan]

def _parse_joiner_output(decision: JoinOutputs) -> List[BaseMessage]:
    response = [AIMessage(content=f"Thought: {decision.thought}")]
    if isinstance(decision.action, Replan):
        return response + [
            SystemMessage(
                content=f"Context from last attempt: {decision.action.feedback}"
            )
        ]
    else:
        return response + [AIMessage(content=decision.action.response)]

def select_recent_messages(state) -> dict:
    messages = state["messages"]
    selected = []
    for msg in messages[::-1]:
        selected.append(msg)
        if isinstance(msg, HumanMessage):
            break
    return {"messages": selected[::-1]}

joiner_prompt = hub.pull("wfh/llm-compiler-joiner").partial(
    examples=""
)
joiner_llm = ChatOpenAI(model="gpt-4-turbo-preview")

def with_structured_output(llm, output_model):
    return llm

runnable = joiner_prompt | with_structured_output(joiner_llm, JoinOutputs)
joiner = RunnableBranch(
    select_recent_messages,
    runnable,
) | _parse_joiner_output

# 使用LangGraph组合
class State(TypedDict):
    messages: List[BaseMessage]

graph_builder = StateGraph(State)

graph_builder.add_node("plan_and_schedule", plan_and_schedule)
graph_builder.add_node("join", joiner)

graph_builder.add_edge("plan_and_schedule", "join")

def should_continue(state):
    messages = state["messages"]
    if isinstance(messages[-1], AIMessage):
        return END
    return "plan_and_schedule"

graph_builder.add_conditional_edges(
    "join",
    should_continue,
)

graph_builder.add_edge(START, "plan_and_schedule")
chain = graph_builder.compile()

# 示例运行
def run_chain(question: str):
    print(f"\n询问: {question}")
    for step in chain.stream(
        {"messages": [HumanMessage(content=question)]}
    ):
        if "join" in step:
            final_message = step["join"]["messages"][-1].content
            print(f"最终回答: {final_message}")

if __name__ == "__main__":
    # 简单问题示例
    run_chain("What's the GDP of New York?")

    # 多跳问题示例
    run_chain("What's the oldest parrot alive, and how much longer is that than the average?")

    # 多步数学计算示例
    run_chain("What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?")

    # 复杂的重新计划示例
    run_chain("Find the current temperature in Tokyo, then, respond with a flashcard summarizing this information")

4. 运行脚本

确保您已经按照上述步骤创建了 math_tools.pyoutput_parser.py 文件,并设置了必要的API密钥。然后,在终端中运行主脚本:

bash 复制代码
python llmcompiler_demo.py

脚本将依次处理以下示例问题:

  1. 简单问题:纽约的GDP是多少?
  2. 多跳问题:目前世界上最老的鹦鹉是谁,它比平均寿命多活了多久?
  3. 多步数学计算 :计算 ((3*(4+5)/0.5)+3245) + 8,然后计算 32/4.23,最后求这两个结果的和。
  4. 复杂的重新计划示例:查找东京当前的温度,然后用闪卡形式总结此信息。

每个问题的处理过程和最终回答将打印在终端中。


5. 注意事项

  • API 密钥 :确保您的 OPENAI_API_KEYTAVILY_API_KEY 正确设置,并具有相应的访问权限。
  • 安全性 :在 output_parser.py 中使用了 eval 函数来解析参数,这在生产环境中可能存在安全风险。建议使用更安全的解析方法,例如正则表达式或专用的解析库。
  • 依赖版本 :确保安装的包版本与代码兼容,特别是 pydanticlangchain_core。如果遇到版本兼容性问题,请参考相应包的文档进行调整。

通过上述步骤,您应该能够成功运行LLMCompiler的示例,并对其整体流程有更深入的理解。如有任何问题,请随时提问!

相关推荐
凌虚(失业了求个工作)5 小时前
RAG 示例:使用 langchain、Redis、llama.cpp 构建一个 kubernetes 知识库问答
人工智能·redis·python·langchain·llama
我爱学Python!21 小时前
大语言模型与图结构的融合: 推荐系统中的新兴范式
人工智能·语言模型·自然语言处理·langchain·llm·大语言模型·推荐系统
ZHOU_WUYI2 天前
3.langchain中的prompt模板 (few shot examples in chat models)
人工智能·langchain·prompt
AI_小站2 天前
RAG 示例:使用 langchain、Redis、llama.cpp 构建一个 kubernetes 知识库问答
人工智能·程序人生·langchain·kubernetes·llama·知识库·rag
ZHOU_WUYI3 天前
5.tree of thought 源码 (prompts 类)
langchain
waiting不是违停4 天前
MetaGPT实现多动作Agent
langchain·llm
ZHOU_WUYI7 天前
2. langgraph中的react agent使用 (在react agent添加历史消息)
人工智能·langchain
ZHOU_WUYI7 天前
4. langgraph中的react agent使用 (在react agent添加人机交互)
人工智能·langchain
ZHOU_WUYI7 天前
5. langgraph中的react agent使用 (从零构建一个react agent)
人工智能·langchain
ZHOU_WUYI7 天前
3. langgraph中的react agent使用 (在react agent添加系统提示)
人工智能·langchain