AI代理:规划与执行

APPLICATION Jan 10, 2025

AI 代理,也称为自主代理或智能代理,是一种能够与其环境交互并自主完成预定任务的生成式 AI 程序。这些人工智能代理使用大型语言模型 (LLM) 的推理能力来自主制定完成给定任务的计划。AI 代理还可以访问现实世界的工具(例如 Web 浏览器)来执行其计划。

那么这些代理与 AI 副驾驶等相邻技术有何不同?虽然有些代理内置了代理式 AI,但副驾驶并非旨在自主运行。相反,它们通过直接的实时交互来增强员工的能力。想象一下客户服务副驾驶检索信息并提出响应建议以帮助联系中心代理解决复杂查询,或者现场服务副驾驶帮助技术人员修理发动机。

回到构建 AI 代理,假设我们想让任务一订购披萨。我们对代理的指示可能如下所示:

You are a helpful autonomous assistant. Your task is to provide a comprehensive plan to execute the given task. Think step by step in a lot of detail. Assume you have access to a web browser. You may also ask for additional tools or access you need.
翻译:你是一位乐于助人的自主助手。你的任务是提供执行给定任务的全面计划。一步一步地详细思考。假设你可以使用网络浏览器。你也可以要求提供所需的其他工具或访问权限。

AI 代理将响应执行任务的全面计划(为简洁起见,已截断):

I can certainly assist you in purchasing a pizza from Domino's. Below is a step-by-step plan to ensure the order is placed successfully:

### Step 1: Gather Necessary Information

Before proceeding with the order, I'll need some details from you:

1. **Delivery Address:** The full address where the pizza should be delivered.

2. **Contact Information:** A phone number that the delivery driver can reach if needed.

…

### Step 2: Access Domino’s Website

1. Open a web browser.

2. Navigate to the Domino's website [www.dominos.com](http://www.dominos.com).

### Step 3: Set Up the Order
…
翻译:我当然可以帮助您从 Domino 购买披萨。以下是确保成功下订单的分步计划:
步骤 1:收集必要的信息
在继续下订单之前,我需要您提供一些详细信息:
送货地址:披萨应送达的完整地址。
联系信息:送货司机在需要时可以拨打的电话号码。

步骤 2:访问 Domino 网站
打开网络浏览器。
导航到 Domino 网站 www.dominos.com
步骤 3:设置订单
...‍

通过确保 AI 代理拥有正确的工具,我们使其能够自行完成任务。在本教程中,我们将详细介绍如何使用 Python、OpenAI 和 Beautiful Soup 构建自己的自主计划和执行代理。

1、设置 AI 代理的工具和内存

我们将构建一个计划和执行代理,它能够接受指令并使用页面上的链接在 Web 浏览器上下文中导航。为了实现这一点,我们需要为我们的代理配备一些功能:

  • 工具:这些是 LLM 可以选择并可以访问的功能。在这个项目中,我们将赋予它打开 Chrome 浏览器、导航到给定页面的能力,以及解析页面上链接的功能。
  • 内存:这是一种数据结构,允许 LLM 记住在规划未来任务时已完成的事情。这包括自身的面包屑以及调用了哪些工具。内存可以短期保存在代理中,也可以长期保存以跟踪总体目标进度。

有了这些核心组件,我们现在可以设置我们的计划和执行循环。

2、创建计划和执行循环

代理通过分析其目标、创建合理实现该目标的步骤并使用工具执行这些步骤来工作。

用于构建 AI 代理的计划和执行循环图

此过程的核心是计划和执行循环。我们的示例围绕以下计划和执行循环构建:

首先,我们通过终端向代理提供指令。在本例中为“从 Hacker News 获取所有链接”。

接下来,代理根据其指令和可用的工具制定完成任务的计划。鉴于我们概述的工具,它会通过以下步骤做出响应:

  • open_chrome
  • navigate_to_hackernews
  • get_all_links

最后,在完成计划后,代理将进入执行阶段,按顺序调用函数。调用函数时,内存将使用被调用的工具和相关元数据进行更新,包括任务、参数和调用结果。

3、增强代理的内存、提示和工具

为了使我们的工具具有更强大的功能,我们将导入多个库。我们选择这些工具来展示代理与系统交互的不同方式:

  • subprocess 允许我们打开系统应用程序。
  • requestsBeautifulSoup 允许我们从 URL 获取链接。
  • OpenAI 允许我们进行 LLM 调用。

设置:

from dotenv import load_dotenv
load_dotenv()

import os
import json
import subprocess

import requests
from bs4 import BeautifulSoup

from openai import OpenAI

api_key = os.getenv("OPENAI_API_KEY")
client = OpenAI()

3.1 内存

为了使我们的 AI 代理的计划有效,我们需要知道代理已经做了什么以及还需要做什么。此步骤通常在框架中抽象,因此调用它很重要。如果不更新代理的“状态”,代理将不知道下一步要调用什么。可以将其视为类似于 ChatGPT 中的对话历史记录。

让我们创建一个 Agent类,并使用可用于完成任务的变量对其进行初始化。我们需要知道已给出的任务以及 LLM 对这些任务的响应。我们可以将它们保存为 memory_tasksmemory_responses。我们还希望存储我们计划的操作以及我们可能遇到的任何 URL 和链接。

class Agent:
    def __init__(self):

        ## Memory
        self.memory_tasks = []
        self.memory_responses = []

        self.planned_actions = []
        self.url = ""
        self.links = []

3.2 计划和执行提示

为了制定和执行计划,我们需要知道我们拥有哪些工具以及已采取步骤的记忆。

如果我们想要导航到网站并获取链接,我们应该调用一些可能的工具来打开 Chrome、导航到网站并获取链接。我们可以清楚地构建角色、任务、说明、可用工具和预期输出格式。通过这种方式,我们要求 LLM 规划使用给定工具完成任务所需的步骤。

 self.planning_system_prompt = """
        % Role: 
        You are an AI assistant helping a user plan a task. You have access to % Tools to help you with the order of tasks.

        % Task: 
        Check the user's query and provide a plan for the order of tools to use for the task if appropriate. Do not execute only out line the tasks in order to accomplish the user's query.

        % Instructions: 
        Create a plan for the order of tools to use for the task based on the user's query.
        Choose the approapriate tool or tools to use for the task based on the user's query.
        Tools that require content will have a variable_name and variable_value.
        A tool variable value can be another tool or a variable from a previous tool or the variable that is set in memory.
        If a variable is set in memory, use the variable value from memory.
        If the tool doesn't need variables, put the tool name in the variable_name and leave the variable_value empty.
        Memories will be stored for future use. If the output of a tool is needed, use the variable from memory.

        % Tools:
        open_chrome - Open Google Chrome
        navigate_to_hackernews - Navigate to Hacker News
        get_https_links - Get all HTTPS links from a page requires URL
        
        % Output:
        Plan of how to accomplish the user's query and what tools to use in order
        Each step should be one a single line with the tool to use and the variables if needed
        """

添加了 update_system_prompt 以专门显示在执行此工作流期间添加的“内存”。这里,计划和执行提示之间的唯一主要区别在于我们内存的添加和提示输出的响应格式。

完成任务时,LLM 将查看之前完成的任务的内存并选择用于给定任务的工具。这将返回工具名称以及完成任务所需的任何变量(JSON 格式)。

 def update_system_prompt(self):
        self.system_prompt = f"""
        % Role: 
        You are an AI assistant helping a user with a task. You have access to % Tools to help you with the task.

        % Task: 
        Check the user's query and provide a tool or tools to use for the task if appropriate. Always check the memory for previous questions and answers to choose the appropriate tool.

        % Memory:
        You have a memory of the user's questions and your answers.
        Previous Questions: {self.memory_tasks}
        Previous Answers: {self.memory_responses}

        % Instructions: 
        Choose the appropriate tool to use for the task based on the user's query.
        Tools that require content will have a variable_name and variable_value.
        A tool variable value can be another tool or a variable from a previous tool.
        Check the memory for previous questions and answers to choose the appropriate variable if it has been set.
        If the tool doesn't need variables, put the tool name in the variable_name and leave the variable_value empty.

        % Tools:
        open_chrome - Open Google Chrome
        navigate_to_hackernews - Navigate to Hacker News
        get_https_links - Get all HTTPS links from a page requires URL

        % Output:
        json only

        {{
            "tool": "tool" or ""
            "variables": [
                {{
                    "variable_name": "variable_name" or ""
                    "variable_value": "variable_value" or ""
                }}
            ]
        }}
        """

openai_call 函数将允许我们调用 OpenAI 并请求不同的 format_responses 来展示规划和执行之间的差异。JSON 格式在这里很重要,因为我们使用实际运行该函数的工具的响应。

def openai_call(self, query: str, system_prompt: str, json_format: bool = False):

        if json_format == True:
            format_response = { "type": "json_object" }
        else:
            format_response = { "type": "text" }

        completion = client.chat.completions.create(
            model="gpt-4o",
            temperature=0,
            messages=[
                {"role": "system", "content": system_prompt},
                {
                    "role": "user",
                    "content": f'User query: {query}'
                }
            ],
            response_format=format_response
        )
        llm_response = completion.choices[0].message.content
        print(llm_response)
        return llm_response

3.3 工具

工具是代理可以访问的功能。工具因代理要完成的任务而异。

在下面的代码中,有三个特定的工具展示了如何打开浏览器、导航到网站以及获取链接。当我们将它们放在一起时,这些工具的调用方式如下所示。现在,将其视为 LLM 可以根据计划的当前任务选择是否调用的函数。

open_chrome() 工具可以使用 Python 的子进程打开允许导航的 Chrome 实例。

    def open_chrome(self):
        try:
            subprocess.run(["open", "-a", "Google Chrome"], check=True)
            print("Google Chrome opened successfully.")
        except subprocess.CalledProcessError as e:
            print(f"Failed to open Google Chrome: {e}")

navigation_to_hackernews() 工具是此演示的硬编码,用于展示该想法,应扩展为允许任何 URL。

    def navigate_to_hackernews(self, query):
        self.url = "https://news.ycombinator.com/"
        try:
            subprocess.run(["open", "-a", "Google Chrome", self.url], check=True)
            print(f"Opened '{query}' opened in Google Chrome.")
        except subprocess.CalledProcessError as e:
            print(f"Failed to open search in Google Chrome: {e}")

get_https_links() 工具使用 Beautiful Soup 库。它将抓取我们所在页面的 URL 并获取所有链接。

    def get_https_links(self, url):
        try:
            response = requests.get(url)
            response.raise_for_status()
            
            soup = BeautifulSoup(response.text, 'html.parser')
            links = soup.find_all('a')
            
            for link in links:
                link_text = link.get_text(strip=True)
                link_url = link.get('href')
                if link_url and link_url.startswith('https'):
                    print(f"Link Name: {link_text if link_text else 'No text'}")
                    print(f"Link URL: {link_url}")
                    print('---')
        except requests.exceptions.RequestException as e:
            print(f"Error fetching the URL: {e}")

4、将所有工作整合在一起

在这里,我们创建一个函数来规划,然后执行相同的计划。

为此,我们需要将规划系统提示发送给 LLM 以创建计划并将这些任务保存为列表,然后对于该列表中的每个任务:

  • 跟踪分配给代理的任务。
  • 将任务提示与历史记录一起拉入。
  • 调用 LLM 根据给定的任务选择一个工具并以 JSON 形式返回。
  • 使用 LLM 响应更新我们的内存。
  • 将 LLM 调用的工具与可用工具之一匹配。
  • 运行调用的函数。
  • 开始下一个任务。

在我们的代码中,它看起来是这样的:

## Plan and Execute
    def run(self, prompt: str):
        ## Planning
        planning = self.openai_call(prompt, self.planning_system_prompt)
        self.planned_actions = planning.split("\n")

        ## Task Execution
        for task in self.planned_actions:
            print(f"\n\nExecuting task: {task}")
            self.memory_tasks.append(task)
            self.update_system_prompt()
            task_call = self.openai_call(task, self.system_prompt, True)
            self.memory_responses.append(task_call)

            try:
                task_call_json = json.loads(task_call)
            except json.JSONDecodeError as e:
                print(f"Error decoding task call: {e}")
                continue

            tool = task_call_json.get("tool")
            variables = task_call_json.get("variables", [])

            if tool == "open_chrome":
                self.open_chrome()
                self.memory_responses.append("open_chrome=completed.")

            elif tool == "get_https_links":
                self.links = self.get_https_links(self.url)
                self.memory_responses.append(f"get_https_links=completed. variable urls = {self.links}")

            elif tool == "navigate_to_hackernews" :
                query = variables[0].get("variable_value", "")
                self.navigate_to_hackernews(query)
                self.memory_responses.append(f"navigate_to_hackernews=completed. variable query = {query}")



if __name__ == "__main__":
    agent = Agent()

    while True:
        prompt = input("Please enter a prompt (or type 'exit' to quit): ")
        if prompt.lower() == 'exit':
            break
        agent.run(prompt)

有了代理,我们现在可以运行请求并查看代理如何响应。

“从 Hacker News 获取所有链接。”

代理生成的计划:

1. open_chrome
2. navigate_to_hackernews
3. get_https_links with variable_name: URL and variable_value: (URL of Hacker News)

每个正在运行的任务的输出:

Executing task: 1. open_chrome
{
    "tool": "open_chrome",
    "variables": [
        {
            "variable_name": "",
            "variable_value": ""
        }
    ]
}
Google Chrome opened successfully.
Executing task: 2. navigate_to_hackernews
{
    "tool": "navigate_to_hackernews",
    "variables": [
        {
            "variable_name": "",
            "variable_value": ""
        }
    ]
}
Opened '' opened in Google Chrome.
Executing task: 3. get_https_links with variable_name: URL and variable_value: (URL of Hacker News)
{
    "tool": "get_https_links",
    "variables": [
        {
            "variable_name": "URL",
            "variable_value": "(URL of Hacker News)"
        }
    ]
}
Link Name: No text
Link URL: https://news.ycombinator.com
---
Link Name: Computing with Time: Microarchitectural Weird Machines
Link URL: https://cacm.acm.org/research-highlights/computing-with-time-microarchitectural-weird-machines/
---
Link Name: SQLiteStudio: Create, edit, browse SQLite databases
Link URL: https://sqlitestudio.pl/
---
Link Name: The Nearest Neighbor Attack

...

5、构建计划和执行代理时的陷阱

如果我们通过制定计划来开始每个代理调用,那么我们并不总是能够灵活地处理出现的问题。我们如何处理故障以及代理有哪些工具可以缓解这些故障非常重要。在我们的计划和执行方法中,除非一切都在 try-catch 中,并且我们在失败时更新计划,否则更新计划并不容易。

可能出现的更多问题包括:

  • 一次计划调用和多次执行调用。虽然我们以这种方式呈现事物来说明步骤,但使用单个提示来计划下一步并执行可以让代理更具动态性。
  • 这是一个在开始时创建的线性计划,因此如果我们遇到任何问题,我们将无法有效地继续。在这里,我们可以让代理反思结果并选择下一个最佳方法。
  • 如果 AI 代理必须与任何图形用户界面 (GUI) 交互,它将崩溃。在这里,可以加入多个代理或自定义工具以允许导航特定应用程序。
  • 随着我们添加更多工具,提示可能会变得很多。最好在提示变大时开始将工具存储在提示之外,并为特定任务确定代理的范围。
  • 我们负责创建所有工具。新兴标准旨在为与服务提供商的互动建立最佳实践。
  • 现在,所有内存都保存在类本身中。我们应该将其移动到中心位置以保存这些数据。

幸运的是,大多数这些问题都可以通过更先进的技术和概念来解决。

6、构建 AI 代理的高级技术和概念

凭借我们对计划和执行代理的理解,我们可以基于我们所知道的知识来创建具有更高级功能的代理。这些包括利用代理框架和创建特定架构。

虽然我们在本博客中介绍了构建计划和执行代理的基础知识,但你可以通过结合以下高级技术进一步提升其能力:

  • ReAct(推理和行动):这将计划和执行提示合并为一个,允许提示一步一步思考。
  • ADaPT(复杂任务的按需分解和规划):ADaPT 或许最好被视为 ReAct 的扩展,它一步一步地规划,但也允许代理在问题出现或步骤太大时以递归方式分解问题。
  • Reflexion(反思):这允许代理了解它做得如何,或者在完成任务后是否需要重试。

至于可以融入现有代理或与其他技术一起使用的高级概念,你可以探索以下选项:

  • 递归:代理调用自己的函数来保持任务的进行,直到问题得到解决。将其视为 while True,并小心无限循环。这在自主性方面具有很大的力量。
  • 多个代理:如果你需要代理相互交谈或协调更复杂的任务,这些是完美的选择。并非每个代理都需要访问所有工具或系统。
  • 数据标准化:诸如 Anthropic 的模型上下文协议 (MCP) 之类的标准正在兴起,以利用工具和数据服务与 AI 的集成。为人工智能代理如何与数据服务交互建立最佳实践的努力变得越来越重要。
  • 框架:代理框架允许创建代理,但通常有自己的范例,这些范例是基于上述想法构建的。Autogen、LangGraph 和 LlamaIndex 就是代理框架的几个例子。

借助先进的技术和概念,你可以构建更强大的代理,这些代理能够执行诸如在做出决策时比较不同场景或帮助客户服务聊天机器人处理更复杂的查询等操作。

7、结束语

在这篇文章中,我们通过一个实际的计划和执行示例研究了 AI 代理的结构,展示了代理如何使用计划、执行、工具和内存来完成任务。我们的基本实施强调了工具选择和陷阱等关键考虑因素,同时还介绍了更高级的概念,例如 ReAct 和多代理系统。


原文链接:Building Your First AI Agent with Plan-and-Execute Loops

汇智网翻译整理,转载请标明出处

Tags