近年来,大型语言模型 (LLMs) 的能力突飞猛进。其中,一种名为 ReAct (Reason + Act,推理 + 行动) 的模式脱颖而出,展现出强大的能力。ReAct 模式不仅仅允许语言模型生成回复,更赋予了它们推理任务、执行外部操作(例如搜索或计算),并利用操作结果进行进一步推理,最终形成完整答案的能力。本文将深入探讨如何利用 LangChain 框架,结合 Google 的 Gemini 模型,从零开始构建一个 ReAct 代理,并允许该模型基于推理调用自定义工具。

ReAct:让语言模型具备思考、行动与观察的能力

ReAct 模式的核心在于模拟人类解决问题的过程。它并非简单地给出答案,而是通过一个反馈循环,让 LLM 能够“思考 (Reasoning)”、“行动 (Act)”、“观察 (Observe)” 结果,并根据观察结果进行持续的思考,最终得出解决方案。这种循环式的过程,使 LLM 具备了更强的适应性和解决复杂问题的能力。

例如,假设我们向一个传统的 LLM 询问:“2023年票房最高的科幻电影的导演是谁?”。它可能会直接搜索“2023年票房最高的科幻电影”并尝试从搜索结果中提取导演信息。但如果搜索结果不直接包含导演信息,或者信息不准确,LLM 就可能给出错误的答案。

而一个 ReAct 代理在处理相同问题时,会首先“思考”解决这个问题需要哪些步骤,例如:

  1. 思考: 需要先确定2023年票房最高的科幻电影是什么。
  2. 行动: 使用搜索工具查找“2023年票房最高的科幻电影”。
  3. 观察: 搜索结果显示《阿凡达:水之道》是票房最高的科幻电影。
  4. 思考: 现在需要确定《阿凡达:水之道》的导演。
  5. 行动: 使用搜索工具查找“《阿凡达:水之道》导演”。
  6. 观察: 搜索结果显示《阿凡达:水之道》的导演是詹姆斯·卡梅隆。
  7. 思考: 现在我已经找到了答案。
  8. 行动: 返回答案:“《阿凡达:水之道》的导演是詹姆斯·卡梅隆”。

通过这种 “思考-行动-观察” 的循环,ReAct 代理能够更加可靠地解决问题,即使信息并非唾手可得。

LangChain:构建 ReAct 代理的强大框架

LangChain 是一个用于构建基于 LLM 的应用程序的框架。它提供了一系列的模块化组件,例如模型、提示模板、链、代理和回调等,可以帮助开发者快速构建各种 LLM 应用。 LangChain 极大地简化了 ReAct 代理的构建过程,使开发者能够专注于定义模型的行为和工具,而无需从底层实现复杂的推理逻辑。

LangChain 提供了一个名为 AgentType.REACT_DOCSTORE 的预定义代理,可以直接利用已有的文档存储进行推理和行动。然而,为了更好地理解 ReAct 模式的原理,并实现更灵活的定制化,本文将从零开始构建一个 ReAct 代理。

Gemini 模型:驱动 ReAct 代理的强大引擎

Google 的 Gemini 模型是一系列多模态大型语言模型,旨在理解和生成文本、图像、音频等多种类型的数据。 Gemini 模型强大的推理能力和生成能力,使其成为驱动 ReAct 代理的理想选择。

与传统的 LLM 相比,Gemini 模型在理解上下文、进行逻辑推理和生成连贯的文本方面表现出色。这使得它能够更好地执行 ReAct 模式中的“思考”环节,并生成更清晰、更准确的行动指令。

自定义工具:扩展 ReAct 代理的能力边界

为了让 ReAct 代理能够执行特定的任务,我们需要定义一些自定义工具 (Tools)。这些工具可以与外部 API 或服务进行交互,获取数据或执行操作。

在本文中,我们将使用以下两个自定义工具:

  1. movie_plot: 给定电影名称,返回电影的剧情简介。
  2. character_count: 计算给定句子中的字符数。

这些工具只是示例,实际应用中可以根据需要定义各种各样的工具,例如:

  • 天气查询工具:查询特定地区的天气预报。
  • 计算器工具:执行数学计算。
  • 数据库查询工具:从数据库中检索数据。
  • 代码执行工具:执行代码片段。

通过自定义工具,我们可以将 ReAct 代理与外部世界连接起来,使其能够解决更加复杂和多样化的任务。

实现 ReAct 循环:代码示例与解析

下面将展示如何使用 LangChainGemini 模型实现一个 ReAct 循环。

首先,我们需要安装必要的库:

pip install langchain google-generativeai

接下来,定义自定义工具:

from langchain.tools import BaseTool
from typing import Optional, Type
from langchain.pydantic_v1 import BaseModel, Field

class MoviePlotInput(BaseModel):
    movie_name: str = Field(description="The name of the movie to get the plot for.")

class CharacterCountInput(BaseModel):
    sentence: str = Field(description="The sentence to count characters in.")


class MoviePlotTool(BaseTool):
    name = "movie_plot"
    description = "Useful for getting the plot of a movie. Input should be the movie name."
    args_schema: Type[BaseModel] = MoviePlotInput

    def _run(self, movie_name: str):
        # Replace with your actual movie plot retrieval logic
        # This is just a placeholder
        if movie_name == "The Matrix":
            return "A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers."
        elif movie_name == "Inception":
            return "A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea into the mind of a CEO."
        else:
            return "Plot not found."

    async def _arun(self, movie_name: str):
        raise NotImplementedError("This tool does not support asynchronous execution")


class CharacterCountTool(BaseTool):
    name = "character_count"
    description = "Useful for counting the number of characters in a sentence. Input should be a sentence."
    args_schema: Type[BaseModel] = CharacterCountInput

    def _run(self, sentence: str):
        return f"The sentence '{sentence}' has {len(sentence)} characters."

    async def _arun(self, sentence: str):
        raise NotImplementedError("This tool does not support asynchronous execution")


tools = [MoviePlotTool(), CharacterCountTool()]

这段代码定义了两个工具 MoviePlotToolCharacterCountTool,分别用于获取电影剧情和计算字符数。注意 _run 方法是工具执行的核心逻辑。在实际应用中,你需要替换这里的占位符代码,使用真实的 API 或服务来获取数据或执行操作。

接下来,我们需要定义 ReAct 提示模板:

from langchain.prompts import PromptTemplate

template = """Answer the following questions as best you can. You have access to the following tools:

{tools}

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought:{agent_scratchpad}"""

prompt = PromptTemplate.from_template(template)

这个提示模板定义了 ReAct 代理的行为规范。它告诉代理可以使用哪些工具,以及如何使用这些工具进行推理和行动。 {tools} 占位符会被工具的描述信息替换, {tool_names} 占位符会被工具的名称列表替换, {input} 占位符会被用户的问题替换, {agent_scratchpad} 占位符会被代理的推理过程记录替换。

然后,我们需要配置 Gemini 模型:

import os
import google.generativeai as genai
from langchain.llms import GoogleGenerativeAI

# Replace with your actual Google API key
os.environ["GOOGLE_API_KEY"] = "YOUR_API_KEY"
genai.configure(api_key=os.environ["GOOGLE_API_KEY"])

model = GoogleGenerativeAI(model="gemini-pro", temperature=0)

这段代码使用 langchain.llms.GoogleGenerativeAIGemini 模型集成到 LangChain 框架中。你需要将 "YOUR_API_KEY" 替换为你自己的 Google API 密钥。 temperature 参数控制模型的随机性,设置为 0 可以让模型给出更确定的答案。

最后,我们需要创建 ReAct 代理:

from langchain.agents import AgentExecutor, LLMSingleActionAgent, AgentOutputParser
from langchain.schema import AgentAction, AgentFinish, OutputParserException
import re

class CustomOutputParser(AgentOutputParser):
    def parse(self, llm_output: str) -> AgentAction | AgentFinish:
        # Check if agent should finish
        if "Final Answer:" in llm_output:
            return AgentFinish(
                # Return values is generally always a dictionary with a single `output` key
                # It is not mandatory!
                return_values={"output": llm_output.split("Final Answer:")[-1].strip()},
                log=llm_output,
            )

        # Parse out the action and action input
        try:
            action = re.search(r"Action:\s*(.*?)", llm_output, re.DOTALL).group(1).strip()
            action_input = re.search(r"Action Input:\s*(.*?)", llm_output, re.DOTALL).group(1).strip(" ").strip('"')
            return AgentAction(tool=action, tool_input=action_input, log=llm_output)
        except Exception:
            raise OutputParserException(f"Could not parse LLM output: `{llm_output}`")


output_parser = CustomOutputParser()

tool_names = [tool.name for tool in tools]
agent = LLMSingleActionAgent(
    llm=model,
    prompt=prompt,
    stop=["\nObservation:"],
    tool_names=tool_names,
    output_parser=output_parser
)

agent_executor = AgentExecutor.from_agent_and_tools(agent=agent, tools=tools, verbose=True)

这段代码创建了一个 LLMSingleActionAgent 代理,并使用 AgentExecutor 执行代理。 LLMSingleActionAgent 代理每次只执行一个行动,这使得我们可以更好地控制代理的行为。 AgentExecutor 负责协调代理和工具之间的交互。 verbose=True 参数可以打印出代理的推理过程,方便调试。 CustomOutputParser 用于解析模型的输出,提取行动指令和行动输入。

现在,我们可以测试 ReAct 代理了:

question = "What is the plot of The Matrix, and how many characters are in the first sentence of that plot?"
result = agent_executor.run(question)
print(result)

这段代码向 ReAct 代理提出了一个问题:“《黑客帝国》的剧情是什么?剧情的第一句话有多少个字符?”。 ReAct 代理会根据这个问题进行推理,并调用相应的工具来获取信息,最终给出答案。

总结与展望:ReAct 的未来

本文深入探讨了 ReAct 模式的原理和实现方法,并展示了如何使用 LangChain 框架和 Gemini 模型构建一个 ReAct 代理。 ReAct 模式为构建更智能、更灵活的 LLM 应用提供了新的思路。

随着 LLM 技术的不断发展,ReAct 模式将在更多领域得到应用。例如,可以利用 ReAct 模式构建:

  • 自动化的客户服务机器人:能够理解客户的问题,并根据问题调用相应的工具来解决问题。
  • 智能化的数据分析助手:能够根据用户的需求,自动分析数据并生成报告。
  • 更强大的代码生成工具:能够根据用户的描述,生成更复杂、更可靠的代码。

ReAct 模式的潜力是巨大的,相信在未来它将成为 LLM 应用开发的重要组成部分。 通过结合推理、行动和观察,我们可以构建出更加智能、更加强大的 LLM 应用,为人类带来更多的便利和价值。 随着开源工具和更强大的模型的不断涌现,ReAct 的未来将更加光明。