AI-智能体实战-全-

AI 智能体实战(全)

原文:zh.annas-archive.org/md5/85b56eb87c9f253ac48b31cb2ceb388a

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:代理及其世界的简介

本章涵盖

  • 定义代理的概念

  • 区分代理的组成部分

  • 分析代理时代的兴起:为什么是代理?

  • 揭示 AI 界面

  • 探索代理领域

代理在机器学习和人工智能(AI)中不是一个新概念。例如,在强化学习中,"代理"一词表示一个主动决策和学习的智能。在其他领域,"代理"一词更符合代表你做某事的自动化应用程序或软件。

1.1 定义代理

你可以查阅任何在线词典来查找代理的定义。Merriam-Webster 词典这样定义它(www.merriam-webster.com/dictionary/agent):

  • 行动或施加力量的人或事物

  • 产生或可能产生效果的事物

  • 指导智能实现结果的一种手段或工具

在本书构建强大代理的旅程中,我们使用的"代理"一词采用这个词典定义。这也意味着术语"助手"将与"代理"同义。像 OpenAI 的 GPT 助手这样的工具也将属于 AI 代理范畴。OpenAI 避免使用"代理"一词,因为机器学习的历史中,代理是自我决定和自主的。

图 1.1 显示了四种用户可能直接或通过代理/助手代理、代理/助手或自主代理与大型语言模型(LLM)交互的情况。这四个用例在本列表中更详细地进行了突出显示:

  • 直接用户交互 — 如果你之前使用过 ChatGPT 的早期版本,你体验过与 LLM 的直接交互。没有代理或其他助手代表你介入。

  • 代理/助手代理 — 如果你通过 ChatGPT 使用过 Dall-E 3,那么你已经体验过代理代理交互。在这种情况下,LLM 介入你的请求,并以更适合任务的形式重新表述它们。例如,对于图像生成,ChatGPT 更好地制定提示。代理代理是日常用例,用于帮助用户处理不熟悉的任务或模型。

  • 代理/助手 — 如果你曾经使用过 ChatGPT 插件或 GPT 助手,那么你已经体验过这种用例。在这种情况下,LLM 了解插件或助手功能,并准备调用此插件/功能。然而,在调用之前,LLM 需要用户批准。如果批准,插件或功能将被执行,并将结果返回给 LLM。然后 LLM 用自然语言包装此响应,并将其返回给用户。

  • 自主智能体 — 在这个用例中,智能体解释用户的请求,构建计划,并识别决策点。据此,它独立执行计划中的步骤并做出必要的决策。智能体可能在某些里程碑任务后请求用户反馈,但如果可能的话,它通常被赋予自由探索和学习的空间。这种智能体提出了最多的伦理和安全问题,我们将在后面探讨。

figure

图 1.1 直接动作与使用代理智能体、智能体和自主智能体进行 LLM 交互之间的差异

图 1.1 展示了在 LLM 上使用单个智能体执行单个动作流的使用案例。对于更复杂的问题,我们通常将智能体分解为配置文件或角色。每个智能体配置文件都被分配了一个特定的任务,并使用专业工具和知识执行该任务。

多智能体系统 是协同工作以解决问题的智能体配置文件。图 1.2 展示了使用三个智能体的多智能体系统的一个例子:一个控制器或代理和两个由代理控制的配置文件智能体作为工人。左侧的编码者配置文件编写用户请求的代码;右侧是一个测试者配置文件,旨在编写单元测试。这些智能体一起工作和通信,直到他们对代码满意,然后将其传递给用户。

图 1.2 显示了可能无限多的智能体配置之一。(在第四章中,我们将探讨微软的开源平台 AutoGen,它支持多种配置以使用多智能体系统。)

figure

图 1.2 在这个多智能体系统的例子中,控制器或代理直接与用户通信。两个智能体——一个编码者和一个测试者——在后台工作,编写代码并编写单元测试以测试代码。

多智能体系统可以自主工作,但也可能完全由人类反馈引导。使用多个智能体的好处类似于单个智能体,但通常更为显著。单个智能体通常专注于单一任务,而多智能体系统可以并行处理多个任务。多个智能体还可以提供反馈和评估,减少完成任务时的错误。

如我们所见,AI 智能体或智能体系统可以以多种方式组装。然而,智能体本身也可以由多个组件组装。在下一节中,我们将涵盖从智能体配置文件到它可能执行的操作,以及记忆和计划等主题。

1.2 理解智能体的组件系统

智能体可以是复杂单元,由多个组件系统组成。这些组件是智能体使用的工具,以帮助它完成其目标或分配的任务,甚至创造新的任务。组件可能是简单或复杂的系统,通常分为五个类别。

图 1.3 描述了一个单代理系统可能包含的主要组件类别。每个元素都将有子类型,可以定义组件的类型、结构和用途。所有代理的核心是档案和角色;从那里扩展出增强代理的系统和服务。

图

图 1.3 单代理系统的五个主要组件(通过 DALL-E 3 生成的图像)

图 1.4 中所示的代理档案和角色代表了代理的基本描述。角色——通常被称为系统提示——指导代理完成任务、学习如何响应以及其他细微之处。它包括背景(例如,程序员、作家)和人口统计信息,可以通过手工制作、LLM 辅助或数据驱动技术(包括进化算法)等方法生成。

图

图 1.4 深入探讨我们如何探索创建代理档案的方法

我们将探讨如何通过评分标准和扎根技术等方法创建有效且具体的代理档案/角色。此外,我们还将解释人类制定的与 AI 制定的(LLM)档案的方面,包括使用数据和进化算法构建档案的创新技术。

注意:代理或助手档案由元素组成,包括角色。将档案视为描述代理/助手将执行的工作及其所需工具可能有所帮助。

图 1.5 展示了在涉及指向任务完成或获取信息活动的代理上下文中的组件动作和工具使用。这些动作可以分为任务完成、探索和沟通,对代理的环境和内部状态有不同程度的影响。动作可以通过手动生成、通过记忆回忆或遵循预定义的计划来生成,影响代理的行为并增强学习。

图

图 1.5 本书将探讨的代理动作方面

理解动作目标有助于我们为任务完成、探索或沟通定义明确的目标。识别动作影响揭示了动作如何影响任务结果、代理的环境及其内部状态,有助于高效决策。最后,掌握动作生成方法使我们能够手动创建动作、从记忆中回忆它们或遵循预定义的计划,增强我们有效塑造代理行为和学习过程的能力。

图 1.6 更详细地展示了组件知识和记忆。代理使用知识和记忆来标注与最相关信息的上下文,同时限制使用的标记数量。知识和记忆结构可以统一,其中两个子集遵循单一结构或混合结构,涉及不同检索形式的组合。知识和记忆格式可以从语言(例如,PDF 文档)到数据库(关系型、对象型或文档型)和嵌入,通过向量表示或简单的列表作为代理记忆,广泛变化,简化语义相似性搜索。

图

图 1.6 探索代理记忆和知识的作用与使用

图 1.7 展示了代理系统中的推理和评估组件。研究和实际应用已经表明,LLMs/代理可以有效地进行推理。推理和评估系统通过提供解决问题的能力来注释代理的工作流程,并评估解决方案。

图

图 1.7 推理和评估组件及其细节

图 1.8 展示了组件代理计划/反馈及其在组织任务以实现更高层次目标中的作用。它可以分为以下两种方法:

  • 无反馈的计划—自主代理独立做出决策。

  • 带有反馈的计划—监控和修改计划基于各种输入来源,包括环境变化和直接的人类反馈。

图

图 1.8 探索代理计划和推理的作用

在计划过程中,代理可能会采用单路径推理、通过任务每一步的顺序推理多路径推理来探索多种策略,并将有效的策略保存供将来使用。外部计划者,可以是代码或其他代理系统,也可能在协调计划中发挥作用。

我们之前任何类型的代理——代理/助手、代理/助手或自主代理——都可能使用这些组件中的某些或全部。甚至计划组件在自主代理之外也有作用,可以有效地赋能常规代理。

1.3 检视代理时代的崛起:为何是代理?

人工智能代理和助手已经迅速从人工智能研究中的主要商品转变为主流软件开发。一个不断增长的工具和平台列表协助构建和赋能代理。对于局外人来说,这一切可能都像是炒作,旨在夸大一些酷炫但过度评价的技术价值。

在 ChatGPT 最初发布后的前几个月,一个新的学科叫做提示工程形成了:用户发现,在他们的提示中使用各种技术和模式可以使他们生成更好、更一致的输出。然而,用户也意识到提示工程只能走这么远。

提示工程仍然是与 ChatGPT 等 LLM 直接交互的绝佳方式。随着时间的推移,许多用户发现,有效的提示需要迭代、反思和更多的迭代。AutoGPT 等第一代代理系统正是从这些发现中产生的,吸引了社区的注意。

图 1.9 展示了 AutoGPT 的原始设计,这是最早的自主代理系统之一。该代理通过查看用户的目标来迭代一系列它定义的任务。通过每个步骤的任务迭代,代理评估目标并确定任务是否完成。如果任务未完成,代理可能会重新规划步骤并根据新的知识或人类反馈更新计划。

figure

图 1.9 AutoGPT 代理系统的原始设计

AutoGPT 成为了第一个展示使用任务规划和迭代与 LLM 模型结合使用力量的例子。从此,其他代理系统和框架也利用类似的规划和任务迭代系统在社区中爆炸式增长。普遍认为,规划、迭代和重复是解决 LLM 的复杂和多元目标最佳的过程。

然而,自主代理系统需要信任代理决策过程、护栏/评估系统以及目标定义。信任也是随着时间的推移而获得的。我们缺乏信任源于我们对自主代理能力的理解不足。

备注:人工通用智能(AGI)是一种可以学习完成人类能完成的任何任务的智能形式。在这个新的 AI 世界中,许多从业者相信,使用自主代理系统的 AGI 是一个可达到的目标。

因此,许多主流和现成的代理工具并不是自主的。然而,它们仍然在利用 GPTs(大型语言模型)管理和自动化任务方面提供了显著的好处。因此,本书的目标是理解所有代理形式,许多更多实际应用将由非自主代理驱动。

代理和代理工具只是新软件应用开发范式的最顶层。我们将在下一节中探讨这个新范式。

1.4 撕开 AI 接口

人工智能代理范式不仅是我们与 LLM 互动方式的转变,也被视为我们开发软件和处理数据方式的转变。软件和数据将不再通过用户界面(UI)、应用程序编程接口(API)和专门的查询语言(如 SQL)进行接口。相反,它们将被设计成使用自然语言进行接口。

图 1.10 展示了这种新架构可能的高层次快照以及人工智能代理扮演的角色。数据、软件和应用适应支持语义、自然语言界面。这些人工智能界面允许代理收集数据并与软件应用程序、其他代理或代理应用程序进行交互。这代表了我们与软件和应用程序交互方式的重大转变。

figure

图 1.10 代理与软件系统交互的愿景

人工智能界面是一组功能、工具和数据层,通过自然语言暴露数据和应用程序。在过去,语义这个词被大量用来描述这些界面,甚至一些工具也使用了这个名称;然而,“语义”也可以有多种含义和用途。因此,在这本书中,我们将使用人工智能界面这个术语。

人工智能界面的构建将赋予需要消费服务、工具和数据的代理能力。随着这种赋权,完成任务将更加准确,应用将更加可靠和自主。虽然人工智能界面可能不适合所有软件和数据,但它将在许多用例中占据主导地位。

1.5 探索代理领域

GPT 代理代表了消费者和开发者从寻找信息到构建软件和访问数据的整个转变。几乎每天都有新的代理框架、组件或界面出现在 GitHub 或研究论文中。这对于试图掌握代理系统是什么以及如何使用它们的新用户来说可能是压倒性和令人生畏的。

摘要

  • 代理是一个实体,它采取行动或施加力量,产生效果或作为实现结果的手段。代理在人工智能中自动与大型语言模型(LLM)进行交互。

  • 助手与代理同义。这两个术语都包括像 OpenAI 的 GPT 助手这样的工具。

  • 自主代理可以做出独立决策,并且它们与非自主代理的区别至关重要。

  • LLM 交互的四种主要类型包括直接用户交互、代理/助手代理、代理/助手和自主代理。

  • 多代理系统涉及代理配置文件协同工作,通常由代理控制,以完成复杂任务。

  • 代理的主要组件包括配置文件/角色、动作、知识/记忆、推理/评估和计划/反馈。

  • 代理配置文件和角色指导代理的任务、响应和其他细微差别,通常包括背景和人口统计信息。

  • 代理的动作和工具可以手动生成,从记忆中召回,或遵循预定义的计划。

  • 代理使用知识和记忆结构通过各种格式优化上下文并最小化标记使用,从文档到嵌入。

  • 推理和评估系统使代理能够通过零样本、单样本和少样本等提示模式来思考问题并评估解决方案。

  • 计划/反馈组件通过单路径或多路径推理以及整合环境和人类反馈来组织任务以实现目标。

  • 人工智能代理的兴起引入了一种新的软件开发范式,从传统的转向基于自然语言的 AI 界面。

  • 理解这些工具的进展和交互有助于开发代理系统,无论是单个、多个还是自主的。

第二章:利用大型语言模型的力量

本章涵盖

  • 理解 LLMs 的基本知识

  • 连接到并使用 OpenAI API

  • 使用 LM Studio 探索和使用开源 LLMs

  • 使用提示工程提示 LLMs

  • 选择适合您特定需求的最佳 LLM

“大型语言模型”(LLMs)这一术语现在已成为一种 AI 形式的普遍描述。这些 LLMs 是使用生成预训练的 transformers(GPTs)开发的。虽然其他架构也能驱动 LLMs,但 GPT 形式目前是最成功的。

LLMs 和 GPTs 是生成模型,这意味着它们被训练来生成内容,而不是预测或分类。为了进一步说明这一点,请考虑图 2.1,它展示了生成模型与预测/分类模型之间的区别。生成模型从输入中创建内容,而预测和分类模型则对其进行分类。

figure

图 2.1 生成模型与预测模型的区别

我们可以通过其组成部分进一步定义 LLM,如图 2.2 所示。在这个图中,“数据”代表用于训练模型的内容,“架构”是模型本身的属性,例如参数数量或模型大小。模型进一步被特定地训练以适应期望的应用场景,包括聊天、完成或指令。最后,“微调”是添加到模型中的功能,它通过优化输入数据和模型训练以更好地匹配特定用例或领域。

figure

图 2.2 描述 LLM 的主要元素

GPTs 的 transformer 架构,作为 LLMs 的一种特定架构,使得模型可以扩展到数十亿个参数的大小。这要求这些大型模型在数 TB 的文档上进行训练以建立基础。从那里,这些模型将依次使用各种方法进行训练,以适应模型期望的应用场景。

以 ChatGPT 为例,它在公共互联网上进行了有效的训练,然后使用几种训练策略进行微调。最终的微调训练使用一种高级形式完成,称为强化学习与人类反馈(RLHF)。这产生了一个名为聊天完成的模型用例。

聊天完成型 LLMs 旨在通过迭代和改进来提高性能——换句话说,就是聊天。这些模型也已被基准测试为在任务完成、推理和规划方面表现最佳,这使得它们非常适合构建代理人和助手。完成模型仅被训练/设计来在输入文本上提供生成内容,因此它们不受益于迭代。

在本书中,我们专注于构建强大代理人的旅程,我们关注的是被称为聊天完成模型的 LLMs 类别。当然,这并不妨碍你尝试为你的代理人使用其他模型形式。然而,你可能需要显著修改提供的代码示例以支持其他模型形式。

在本章后面,当我们查看在本地运行开源 LLM 时,我们将揭示更多关于 LLM 和 GPT 的细节。在下一节中,我们将探讨如何使用 OpenAI 的一个日益增长的标准连接到 LLM。

2.1 掌握 OpenAI API

许多 AI 代理和助手项目使用 OpenAI API SDK 连接到 LLM。虽然不是标准,但现在描述连接的基本概念遵循 OpenAI 模式。因此,我们必须了解使用 OpenAI SDK 进行 LLM 连接的核心概念。

本章将探讨使用 OpenAI Python SDK/包连接到 LLM 模型。我们将讨论连接到 GPT-4 模型、模型响应、计数字符数以及如何定义一致的消息。从以下子节开始,我们将检查如何使用 OpenAI。

2.1.1 连接到聊天完成模型

要完成本节及后续章节的练习,你必须设置一个 Python 开发环境并获取访问 LLM 的权限。附录 A 会指导你如何设置 OpenAI 账户并访问 GPT-4 或其他模型。附录 B 展示了如何使用 Visual Studio Code (VS Code) 设置 Python 开发环境,包括安装所需的扩展。如果你想跟随场景进行,请回顾这些部分。

首先在 VS Code 中打开源代码 chapter_2 文件夹,并创建一个新的 Python 虚拟环境。再次,如果需要帮助,请参考附录 B。

然后,使用以下列表中的命令安装 OpenAI 和 Python dot 环境包。这将把所需的包安装到虚拟环境中。

列表 2.1 pip 安装
pip install openai python-dotenv

接下来,在 VS Code 中打开 connecting.py 文件,检查列表 2.2 中显示的代码。确保将模型的名称设置为合适的名称——例如,gpt-4。在撰写本文时,使用 gpt-4-1106-preview 来表示 GPT-4 Turbo。

列表 2.2 connecting.py
import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()                           #1
api_key = os.getenv('OPENAI_API_KEY')
if not api_key:                             #2
    raise ValueError("No API key found. Please check your .env file.")
client = OpenAI(api_key=api_key)                        #3

def ask_chatgpt(user_message):
    response = client.chat.completions.create(      #4
        model="gpt-4-1106-preview",
        messages=[{"role": "system",
 "content": "You are a helpful assistant."},
        {"role": "user", "content": user_message}],
        temperature=0.7,
        )
    return response.choices[0].message.content     #5

user = "What is the capital of France?"
response = ask_chatgpt(user)                #6
print(response)

1 加载 .env 文件中存储的秘密

2 检查是否设置了密钥

3 使用密钥创建客户端

4 使用 create 函数生成响应

5 仅返回响应的内容

6 执行请求并返回响应

这里发生了很多事情,所以让我们按部分分解,从开始加载环境变量开始。在 chapter_2 文件夹中还有一个名为 .env 的文件,它包含环境变量。这些变量通过调用 load_dotenv 函数自动设置。

你必须在 .env 文件中设置你的 OpenAI API 密钥,如下一个列表所示。再次,请参考附录 A 了解如何获取密钥和找到模型名称。

列表 2.3 .env
OPENAI_API_KEY='your-openai-api-key'

设置密钥后,你可以通过按 F5 键或从 VS Code 菜单中选择“运行”>“开始调试”来调试文件。这将运行代码,你应该会看到类似“法国的首都是巴黎”的内容。

记住,生成模型的响应取决于概率。在这种情况下,模型很可能会给出正确且一致的答案。

您可以通过调整请求的温度来玩这些概率。如果您想让模型更一致,请将温度降低到 0,但如果您想让模型产生更多变化,请提高温度。我们将在下一节进一步探讨设置温度。

2.1.2 理解请求和响应

深入研究聊天完成请求和响应功能可能会有所帮助。我们将首先关注请求,如下所示。请求封装了预期的模型、消息和温度。

列表 2.4 聊天完成请求
response = client.chat.completions.create(
    model="gpt-4-1106-preview",                 #1
    messages=[{"role": "system", 
"content": "You are a helpful assistant."},                     #2
              {"role": "user", "content": user_message}],      #3
    temperature=0.7,     #4
    )

1 用于响应请求的模型或部署

2 系统角色消息

3 用户角色消息

4 请求的温度或变异性

在请求中,messages 块描述了一组用于请求的消息和角色。聊天完成模型的消息可以定义在三个角色中:

  • 系统角色 — 描述请求的规则和指南的消息。它通常用于描述 LLM 在请求中的角色。

  • 用户角色 — 代表并包含用户的消息。

  • 助手角色 — 可以用来捕获 LLM 之前响应的消息历史。它还可以在可能没有消息历史的情况下注入消息历史。

单个请求中发送的消息可以封装整个对话,如下一个列表中的 JSON 所示。

列表 2.5 带有历史记录的消息
[
    {
        "role": "system",
        "content": "You are a helpful assistant."
    },
    {
        "role": "user",
        "content": "What is the capital of France?"
    },
    {
        "role": "assistant",
        "content": "The capital of France is Paris."
    },
    {
        "role": "user",
        "content": "What is an interesting fact of Paris."
    }
],

您可以通过在 VS Code 中打开 message_history.py 并按 F5 调试来查看如何应用此功能。文件运行后,请务必检查输出。然后,尝试再次运行示例,以查看结果如何变化。

由于 .7 的高温度,结果会从每次运行到下一次运行而变化。请继续将温度降低到 .0,并多次运行 message_history.py 示例。将温度保持在 0 将每次都显示相同或类似的结果。

设置请求的温度通常会取决于您的特定用例。有时,您可能想限制响应的随机性(随机性)。将温度降低到 0 将给出一致的结果。同样,1.0 的值将给出响应中最多的变异性。

接下来,我们还想了解每次请求返回的信息。下一个列表显示了响应的输出格式。您可以通过在 VS Code 中运行 message_history.py 文件来查看此输出。

列表 2.6 聊天完成响应
{
    "id": "chatcmpl-8WWL23up3IRfK1nrDFQ3EHQfhx0U6",
    "choices": [                                      #1
        {
            "finish_reason": "stop",
            "index": 0,
            "message": {
                "content": "… omitted",
                "role": "assistant",       #2
                "function_call": null,
                "tool_calls": null
            },
            "logprobs": null
        }
    ],
    "created": 1702761496,
    "model": "gpt-4-1106-preview",     #3
    "object": "chat.completion",
    "system_fingerprint": "fp_3905aa4f79",
    "usage": {
        "completion_tokens": 78,     #4
        "prompt_tokens": 48,         #4
        "total_tokens": 126          #4
    }
}

1 模型可能返回多个响应。

2 在助手角色中返回的响应

3 指示使用的模型

4 计算输入(提示)和输出(完成)标记的数量

跟踪输入标记(用于提示中的标记)和输出标记(通过完成返回的标记数量)的数量可能会有所帮助。有时,最小化和减少标记数量可能是至关重要的。通常,标记数量较少意味着 LLM 交互将更便宜,响应更快,并产生更好、更一致的结果。

这涵盖了连接到 LLM 并返回响应的基本知识。在整个书中,我们将回顾和扩展如何与 LLM 交互。在此之前,我们将在下一节中探讨如何加载和使用开源 LLM。

2.2 使用 LM Studio 探索开源 LLM

商业 LLM,如 OpenAI 的 GPT-4,是学习如何使用现代 AI 和构建代理的绝佳起点。然而,商业代理是一个外部资源,需要付费,会降低数据隐私和安全,并引入依赖。其他外部影响将进一步复杂化这些因素。

建立与开源大型语言模型(LLM)相媲美的竞争日益激烈,这并不令人惊讶。因此,现在已经有了一些开源 LLM,它们可能足够用于众多任务和代理系统。仅在一年内,工具的发展就取得了许多进步,以至于现在在本地托管 LLM 变得非常容易,正如我们将在下一节中看到的。

2.2.1 安装和运行 LM Studio

LM Studio 是一个免费下载的软件,支持在 Windows、Mac 和 Linux 上本地下载和托管 LLM 和其他模型。该软件易于使用,并提供了一些有助于快速入门的有用功能。以下是下载和设置 LM Studio 的步骤快速总结:

  1. lmstudio.ai/下载 LM Studio。

  2. 下载后,根据您的操作系统安装软件。请注意,LM Studio 的一些版本可能处于测试版,需要安装额外的工具或库。

  3. 启动软件。

图 2.3 显示了正在运行的 LM Studio 窗口。从那里,您可以查看当前的热门模型列表,搜索其他模型,甚至下载。主页内容对于了解顶级模型的详细信息和规格非常有用。

figure

图 2.3 LM Studio 软件显示主主页

LM Studio 的一个吸引人的特点是它能够分析您的硬件,并将其与给定模型的 requirements 对齐。软件将告诉您您能多好地运行给定模型。这可以在指导您尝试哪些模型时节省大量时间。

输入一些文本以搜索模型,然后点击“Go”。您将被带到如图 2.4 所示的搜索页面界面。从该页面,您可以查看所有模型变体和其他规格,例如上下文标记大小。在您点击兼容性猜测按钮后,软件甚至会告诉您该模型是否能在您的系统上运行。

figure

图 2.4 LM Studio 搜索页面

点击下载将在你的系统上运行的任何模型。你可能想坚持使用为聊天完成设计的模型,但如果你的系统有限,就使用你拥有的。此外,如果你不确定使用哪个模型,可以下载来尝试。LM Studio 是探索和实验许多模型的好方法。

模型下载后,你可以在聊天页面或服务器页面作为服务器加载和运行模型。图 2.5 显示了在聊天页面加载和运行模型。它还显示了如果你有 GPU,启用和使用 GPU 的选项。

图

图 2.5 加载并运行在本地运行的 LLM 的 LM Studio 聊天页面

要加载和运行一个模型,请打开页面顶部中间的下拉菜单,并选择一个已下载的模型。会出现一个进度条显示模型加载状态,当它准备好后,你可以在 UI 中开始输入。

如果检测到 GPU,该软件甚至允许你使用部分或全部 GPU 进行模型推理。GPU 通常可以在某些方面加快模型响应时间。你可以通过查看页面底部的性能状态来了解添加 GPU 如何影响模型性能,如图 2.5 所示。

与模型聊天以及使用或玩转各种提示可以帮助你确定模型在你特定用例中的适用性。更系统的方法是使用提示流工具来评估提示和 LLM。我们将在第九章中描述如何使用提示流。

LM Studio 还允许在服务器上运行模型,并使用 OpenAI 包使其可访问。我们将在下一节中看到如何使用服务器功能并提供模型。

2.2.2 使用 LM Studio 在本地提供 LLM 服务

使用 LM Studio 在本地作为服务器运行 LLM 非常简单。只需打开服务器页面,加载一个模型,然后点击“启动服务器”按钮,如图 2.6 所示。从那里,你可以复制并粘贴任何示例来连接到你的模型。

图

图 2.6 LM Studio 服务器页面和运行 LLM 的服务器

你可以通过在 VS Code 中打开chapter_2/lmstudio_server.py来查看 Python 代码的示例。代码也在这里列出为列表 2.7。然后,在 VS Code 调试器中运行代码(按 F5)。

列表 2.7 lmstudio_server.py
from openai import OpenAI

client = OpenAI(base_url="http://localhost:1234/v1", api_key="not-needed")

completion = client.chat.completions.create(
  model="local-model",                           #1
  messages=[
    {"role": "system", "content": "Always answer in rhymes."},
    {"role": "user", "content": "Introduce yourself."}       #2
  ],
  temperature=0.7,
)

print(completion.choices[0].message)      #3

1 目前未使用;可以是任何内容

2 随意更改消息。

3 默认代码输出整个消息。

如果你遇到连接到服务器的问题或遇到其他任何问题,请确保你的服务器模型设置配置与模型类型匹配。例如,在前面显示的图 2.6 中,加载的模型与服务器设置不同。修正后的设置如图 2.7 所示。

图

图 2.7 选择加载模型的正确服务器模型设置

现在,你可以使用本地托管的 LLM 或商业模型来构建、测试,甚至可能运行你的代理。下一节将探讨如何更有效地使用提示工程构建提示。

2.3 使用提示工程提示语言模型

为 LLM 定义的提示是用于请求以获得更好的响应输出的消息内容。“提示工程”是一个新兴的领域,试图为构建提示构建一种方法论。不幸的是,提示构建不是一个成熟的科学,而且定义了越来越多的方法,这些方法被称为提示工程。

幸运的是,像 OpenAI 这样的组织已经开始记录一套通用的策略,如图 2.8 所示。这些策略涵盖了各种战术,其中一些需要额外的基础设施和考虑。因此,与更高级概念相关的提示工程策略将在指定的章节中介绍。

图片

图 2.8 本书中按章节位置审查的 OpenAI 提示工程策略

图 2.8 中的每个策略都展开为可以进一步细化特定提示工程方法的战术。本章将检查基本的写清晰指令策略。图 2.9 更详细地展示了该策略的战术,以及每个战术的示例。我们将在下一节中查看如何使用代码演示运行这些示例。

图片

图 2.9 写清晰指令策略的战术

写清晰指令策略是关于在要求时保持谨慎和具体。要求一个语言模型(LLM)执行一项任务与要求一个人完成同样的任务并无不同。一般来说,你能在请求中指定与任务相关的更多信息和上下文,那么响应就会越好。

这种策略已经被分解为可以应用于提示的具体战术。为了理解如何使用这些战术,一个包含各种提示示例的代码演示(prompt_engineering.py)位于“第二章”源代码文件夹中。

在 VS Code 中打开prompt_engineering.py文件,如图 2.8 所示。此代码首先加载prompts文件夹中的所有 JSON Lines 文件。然后,它显示文件列表作为选择,并允许用户选择提示选项。选择选项后,提示被提交给 LLM,并打印出响应。

列表 2.8 prompt_engineering.py (main())
def main():
    directory = "prompts"
    text_files = list_text_files_in_directory(directory)    #1

    if not text_files:
        print("No text files found in the directory.")
        return

    def print_available():                                     #2
        print("Available prompt tactics:")
        for i, filename in enumerate(text_files, start=1):
            print(f"{i}. {filename}")

    while True:
        try:
            print_available()                   #2              
            choice = int(input("Enter … 0 to exit): "))           #3
            if choice == 0:
                break
            elif 1 <= choice <= len(text_files):
                selected_file = text_files[choice - 1]
                file_path = os.path.join(directory,
      selected_file)
                prompts = 
↪ load_and_parse_json_file(file_path)                          #4
                print(f"Running prompts for {selected_file}")
                for i, prompt in enumerate(prompts):
                    print(f"PROMPT {i+1} --------------------")
                    print(prompt)
                    print(f"REPLY ---------------------------")
                    print(prompt_llm(prompt))                       #5
            else:
                print("Invalid choice. Please enter a valid number.")
        except ValueError:
            print("Invalid input. Please enter a number.")

1 收集给定文件夹中的所有文件

2 打印文件列表作为选择

3 输入用户的选择

4 加载提示并将其解析为消息

5 将提示提交给 OpenAI 语言模型

列表中的注释部分演示了如何连接到本地 LLM。这将允许你探索应用于本地运行的开源 LLM 的相同提示工程策略。默认情况下,此示例使用我们在 2.1.1 节中配置的 OpenAI 模型。如果你没有完成之前的操作,请返回并完成它,然后再运行此示例。

图 2.10 显示了运行提示工程策略测试器的输出,即 VS Code 中的 prompt_engineering.py 文件。当你运行测试器时,你可以为要测试的策略输入一个值,并观察其运行。

图

图 2.10 提示工程策略测试器的输出

在以下章节中,我们将更详细地探讨每个提示策略。我们还将检查各种示例。

2.3.1 创建详细查询

此策略的基本前提是尽可能提供详细的信息,但也要小心不要提供无关紧要的细节。以下列表显示了用于探索此策略的 JSON Lines 文件示例。

列表 2.9 detailed_queries.jsonl
[                        #1
    {
        "role": "system",
        "content": "You are a helpful assistant."
    },
    {
        "role": "user",
        "content": "What is an agent?"      #2
    }
]
[
    {
        "role": "system",
        "content": "You are a helpful assistant."
    },
    {
        "role": "user",
        "content": """
What is a GPT Agent? 
Please give me 3 examples of a GPT agent
"""                                        #3
    }
]

1 第一个示例没有使用详细的查询。

2 首先向 LLM 提出一个非常一般的问题。

3 提出一个更具体的问题,并请求示例。

此示例演示了使用详细查询和不使用查询之间的差异。它还进一步通过请求示例。记住,你能在提示中提供越多相关性和上下文,整体响应就越好。请求示例是加强问题与预期输出之间关系的一种方式。

2.3.2 采用角色

采用角色赋予 LLM 定义一个总体上下文或一组规则的能力。LLM 可以然后使用该上下文和/或规则来构建所有后续的输出响应。这是一个有吸引力的策略,我们将在整本书中大量使用它。

列表 2.10 显示了使用两个角色回答相同问题的示例。这可以是一种探索广泛新颖应用的愉快技术,从获取人口统计反馈到专门从事特定任务,甚至橡皮鸭技术。

GPT 橡皮鸭

橡皮鸭 是一种问题解决技术,其中一个人向一个无生命物体(如橡皮鸭)解释一个问题,以理解或找到解决方案。这种方法在编程和调试中很常见,因为大声阐述问题往往有助于澄清问题,并可能导致新的见解或解决方案。

GPT 橡皮鸭技术使用相同的技巧,但使用的是 LLM 而不是无生命物体。通过给 LLM 赋予特定于所需解决方案领域的角色,这种策略可以进一步扩展。

列表 2.10 adopting_personas.jsonl
[
    {
        "role": "system",
        "content": """
You are a 20 year old female who attends college 
in computer science. Answer all your replies as 
a junior programmer.
"""                         #1
    },
    {
        "role": "user",
        "content": "What is the best subject to study."
    }
]
[
    {
        "role": "system",
        "content": """
You are a 38 year old male registered nurse. 
Answer all replies as a medical professional.
"""                                             #2
    },
    {
        "role": "user",
        "content": "What is the best subject to study."
    }
]

1 第一个角色

2 第二个角色

代理配置文件的核心元素是角色。我们将使用各种角色来帮助代理完成他们的任务。当你运行此策略时,请特别注意 LLM 输出响应的方式。

2.3.3 使用分隔符

分隔符是一种有用的方法,可以隔离并使 LLM 专注于消息的某些部分。这个策略通常与其他策略结合使用,但也可以独立工作。以下列表展示了两个示例,但还有其他几种描述分隔符的方法,从 XML 标签到使用 Markdown。

列表 2.11 using_delimiters.jsonl
[
    {
        "role": "system",
        "content": """
Summarize the text delimited by triple quotes 
with a haiku.
"""               #1
    },
    {
        "role": "user",
        "content": "A gold chain is cool '''but a silver chain is better'''"
    }
]
[
    {
        "role": "system",
        "content": """
You will be provided with a pair of statements 
(delimited with XML tags) about the same topic. 
First summarize the arguments of each statement. 
Then indicate which of them makes a better statement
 and explain why.
"""                        #2
    },
    {
        "role": "user",
        "content": """
<statement>gold chains are cool</statement>
<statement>silver chains are better</statement>
"""
    }
]

1 分隔符由字符类型和重复定义。

2 分隔符由 XML 标准定义。

当你运行这个策略时,请注意 LLM 在输出响应时关注的文本部分。这个策略对于描述层次结构或其他关系模式的信息非常有用。

2.3.4 指定步骤

指定步骤是另一种强大的策略,可以有很多用途,包括在代理中,如列表 2.12 所示。在开发复杂多步骤任务的提示或代理配置文件时,它尤其强大。你可以指定步骤将复杂的提示分解为 LLM 可以遵循的逐步过程。反过来,这些步骤可以引导 LLM 在更长时间的对话和多次迭代中进行多次交互。

列表 2.12 specifying_steps.jsonl
[
    {
        "role": "system",
        "content": """
Use the following step-by-step instructions to respond to user inputs.
Step 1 - The user will provide you with text in triple single quotes. 
Summarize this text in one sentence with a prefix that says 'Summary: '.
Step 2 - Translate the summary from Step 1 into Spanish, 
with a prefix that says 'Translation: '.
"""                                          #1
    },
    {
        "role": "user",
        "content": "'''I am hungry and would like to order an appetizer.'''"
    }
]
[
    {
        "role": "system",
        "content": """
Use the following step-by-step instructions to respond to user inputs.
Step 1 - The user will provide you with text. Answer any questions in 
the text in one sentence with a prefix that says 'Answer: '.

Step 2 - Translate the Answer from Step 1 into a dad joke,
 with a prefix that says 'Dad Joke: '."""                      #2
    },
    {
        "role": "user",
        "content": "What is the tallest structure in Paris?"
    }
]

1 注意使用分隔符的策略。

2 步骤可以是完全不同的操作。

2.3.5 提供示例

提供示例是引导 LLM 期望输出的绝佳方式。有无数种方法可以向 LLM 展示示例。系统消息/提示可以是一种强调一般输出的有用方式。在下面的列表中,示例被添加为最后一个 LLM 助手回复,提示为“教我关于 Python。”

列表 2.13 providing_examples.jsonl
[
    {
        "role": "system",
        "content": """
Answer all replies in a consistent style that follows the format, 
length and style of your previous responses.
Example:
  user:
       Teach me about Python.
  assistant:                                                #1
       Python is a programming language developed in 1989
 by Guido van Rossum.

  Future replies:
       The response was only a sentence so limit
 all future replies to a single sentence.
"""                                           #2
    },
    {
        "role": "user",
        "content": "Teach me about Java."
    }
]

1 将样本输出作为“之前的”助手回复注入

2 添加限制输出策略以限制输出大小并匹配示例

提供示例也可以用来从一系列复杂的任务中请求特定的输出格式。例如,要求一个 LLM 生成与样本输出匹配的代码是一个很好的示例使用。我们将在整本书中采用这种策略,但还有其他方法可以用来指导输出。

2.3.6 指定输出长度

指定输出长度的策略不仅可以帮助限制标记数,还可以指导输出到期望的格式。列表 2.14 展示了使用两种不同技术实现此策略的示例。第一个示例将输出限制在 10 个单词以下。这可以带来额外的优势,使响应更加简洁和有针对性,这在某些用例中可能是所希望的。第二个示例演示了将输出限制为简短的要点集合。这种方法可以帮助缩小输出并保持答案简短。更简洁的答案通常意味着输出更加聚焦,包含的填充内容更少。

列表 2.14 specifying_output_length.jsonl
[
    {
        "role": "system",
        "content": """
Summarize all replies into 10 or fewer words.
"""                                               #1
    },
    {
        "role": "user",
        "content": "Please tell me an exciting fact about Paris?"
    }
]
[
    {
        "role": "system",
        "content": """
Summarize all replies into 3 bullet points.
"""                                           #2
    },
    {
        "role": "user",
        "content": "Please tell me an exciting fact about Paris?"
    }
]

1 限制输出使答案更加简洁。

2 限制答案为简短的要点集合

保持答案简短,在开发多智能体系统时可以带来额外的益处。任何与其他智能体进行对话的智能体系统都可以从更加简洁和专注的回复中受益。这有助于使大型语言模型(LLM)更加专注,并减少噪声通信。

一定要运行这个策略的所有提示技巧示例。正如之前提到的,我们将在未来的章节中介绍其他提示工程策略和技巧。我们将通过探讨如何为你的用例选择最佳的 LLM 来结束这一章。

2.4 为你的特定需求选择最佳 LLM

虽然成为一名成功的 AI 智能体制作者不需要深入了解 LLM,但能够评估规格是有帮助的。就像计算机用户一样,你不需要知道如何构建处理器就能理解处理器型号之间的差异。这个类比对 LLM 同样适用,尽管标准可能不同,但仍然依赖于一些基本考虑因素。

从我们之前的讨论和对 LM Studio 的观察中,我们可以提取出一些基本标准,这些标准在我们考虑 LLM 时将非常重要。图 2.11 解释了定义 LLM 为何值得考虑创建 GPT 智能体或任何 LLM 任务的基本标准。

figure

图 2.11 消费 LLM 时需要考虑的重要标准

对于我们构建 AI 智能体的目的,我们需要从与任务相关的角度审视这些标准。模型上下文大小和速度可以被认为是第六和第七个标准,但它们通常被视为模型部署架构和基础设施的变体。对于 LLM 来说,第八个需要考虑的标准是成本,但这取决于许多其他因素。以下是这些标准与构建 AI 智能体相关性的总结:

  • 模型性能 — 你通常会想了解 LLM 在特定任务上的性能。例如,如果你正在构建一个针对编码的特定智能体,那么在代码上表现良好的 LLM 将是必不可少的。

  • 模型参数(大小) — 模型的大小通常是推理性能和模型响应能力的良好指标。然而,模型的大小也会决定你的硬件需求。如果你计划使用自己的本地托管模型,模型的大小也将主要决定你需要的计算机和 GPU。幸运的是,我们经常看到小型、非常强大的开源模型被定期发布。

  • 用例(模型类型) — 模型的类型有多种变体。像 ChatGPT 这样的聊天完成模型适用于迭代和推理问题,而像完成、问答和指令这样的模型则更多与特定任务相关。聊天完成模型对于智能体应用至关重要,尤其是那些需要迭代的智能体。

  • 训练输入 — 了解用于训练模型的内容通常会决定模型的应用领域。虽然通用模型可以在多个任务中有效,但更具体或微调的模型可能对特定领域更相关。这可能是一个考虑特定领域代理的因素,其中较小、更微调的模型可能表现得与 GPT-4 等大型模型一样好,甚至更好。

  • 训练方法 — 这可能不是一个大问题,但了解用于训练模型的方法可能会有所帮助。模型是如何训练的会影响其泛化、推理和规划的能力。这对于规划代理可能是至关重要的,但对于比特定任务助手更通用的代理可能不那么重要。

  • 上下文标记大小 — 模型的上下文大小更具体地与模型架构和类型相关。它决定了模型可能持有的上下文或内存的大小。通常,小于 4,000 个标记的较小上下文窗口对于简单任务已经足够。然而,当使用多个代理(所有代理都在处理同一任务)时,较大的上下文窗口可能是必不可少的。模型通常将以上下文窗口大小的变体进行部署。

  • 模型速度(模型部署) — 模型的速度由其推理速度(或模型响应请求的速度)决定,这反过来又由其运行的基础设施决定。如果你的代理没有直接与用户互动,原始的实时速度可能不是必需的。另一方面,实时交互的 LLM 代理需要尽可能快。对于商业模型,速度将由提供商确定并支持。你的基础设施将决定那些想要运行他们的 LLM 的人的速度。

  • 模型成本(项目预算) — 成本通常由项目决定。无论是学习构建代理还是实施企业软件,成本总是需要考虑的因素。在运行你的 LLM 与使用商业 API 之间存在着重大的权衡。

在选择要在其上构建生产代理系统的模型时,有很多因素需要考虑。然而,出于研究和学习目的,通常最好选择单个模型。如果你是 LLM 和代理的新手,你可能会想选择一个商业选项,例如 GPT-4 Turbo。除非另有说明,本书中的工作将依赖于 GPT-4 Turbo。

随着时间的推移,模型无疑将被更好的模型所取代。因此,你可能需要升级或更换模型。但是,为了做到这一点,你必须了解你的 LLM 和代理的性能指标。幸运的是,在第九章中,我们将探讨使用提示流评估 LLM、提示和代理配置文件。

2.5 练习

使用以下练习来帮助你参与本章的内容:

  • 练习 1 — 消费不同的 LLM

目标 — 使用connecting.py代码示例从 OpenAI 或其他提供商消费不同的 LLM。

任务

    • 修改connecting.py以连接到不同的 LLM。

    • 从 OpenAI 或其他提供商选择一个 LLM。

    • 更新代码中的 API 密钥和端点。

    • 执行修改后的代码并验证响应。

  • 练习 2—探索提示工程策略

目标—探索各种提示工程策略,并为每个策略创建变体。

任务:

    • 回顾章节中涵盖的提示工程策略。

    • 为每个策略编写变体,尝试不同的措辞和结构。

    • 使用 LLM 测试变体以观察不同的结果。

    • 记录结果,并分析每个变体的有效性。

  • 练习 3—使用 LM Studio 下载和运行 LLM

目标—使用 LM Studio 下载一个 LLM,并将其连接到提示工程策略。

任务:

    • 在您的机器上安装 LM Studio。

    • 使用 LM Studio 下载一个 LLM。

    • 使用 LM Studio 提供模型。

    • 编写 Python 代码以连接到已提供模型。

    • 将提示工程策略示例与已提供模型集成。

  • 练习 4—比较商业和开源 LLM

目标—通过提示工程示例比较商业 LLM(如 GPT-4 Turbo)与开源模型的性能。

任务:

    • 使用 GPT-4 Turbo 实现提示工程示例。

    • 使用开源 LLM 重复实现。

    • 根据响应准确性、连贯性和速度等标准评估模型。

    • 记录评估过程,并总结发现。

  • 练习 5—LLM 的托管替代方案

目标—对比和比较托管 LLM 与使用商业模型的替代方案。

任务:

    • 研究 LLM 的不同托管选项(例如,本地服务器、云服务)。

    • 评估每种托管选项的优缺点。

    • 在成本、性能和易用性方面将这些选项与使用商业模型进行比较。

    • 编写一份报告,总结比较并基于特定用例推荐最佳方法。

摘要

  • LLM 使用一种称为生成预训练转换器(GPTs)的架构。

  • 生成模型(例如,LLM 和 GPTs)与预测/分类模型的不同之处在于学习如何表示数据,而不仅仅是进行分类。

  • LLM 是一组针对特定用例的数据、架构和训练,称为微调

  • OpenAI API SDK 可用于从模型(如 GPT-4)连接到 LLM,并用于消费开源 LLM。

  • 您可以快速设置 Python 环境并安装 LLM 集成所需的必要包。

  • LLM 可以处理各种请求并生成独特响应,可用于增强与 LLM 集成相关的编程技能。

  • 开源 LLM 是商业模型的替代品,可以使用 LM Studio 等工具在本地托管。

  • 提示工程是一系列技术,有助于制作更有效的提示,以改善 LLM 的响应。

  • LLM 可用于驱动代理和助手,从简单的聊天机器人到完全能够自主工作的工人。

  • 选择最适合特定需求的 LLM(大型语言模型)取决于性能、参数、用例、训练输入和其他标准。

  • 在本地运行 LLM 需要各种技能,从设置 GPU 到理解各种配置选项。

第三章:个引人入胜的 GPT 助手

本章涵盖

  • 介绍 OpenAI GPT 助手平台和 ChatGPT UI

  • 构建一个可以使用代码解释功能的 GPT

  • 通过自定义操作扩展助手

  • 通过文件上传向 GPT 添加知识

  • 商业化你的 GPT 并将其发布到 GPT Store

随着我们探索 OpenAI 在助手领域的进军以及所暗示的内容,最终,一个名为 GPT Assistants 的代理平台,我们将通过 ChatGPT 界面介绍 GPT 助手。然后,我们将添加几个完全开发的助手,可以从成分中建议食谱,作为数据科学家全面分析数据,引导读者阅读书籍,并可以通过自定义操作进行扩展。到本章结束时,我们将准备好构建一个可以发布到 OpenAI GPT Store 的完全功能性的代理。

3.1 通过 ChatGPT 探索 GPT 助手

ChatGPT(写作时为 ChatGPT Plus)允许你构建 GPT 助手,消费其他助手,甚至发布它们,正如你将在本章末尾看到的那样。当 OpenAI 宣布 GPT Assistants 平台的发布时,它帮助定义和巩固了 AI 代理的出现。因此,对于任何有兴趣构建和消费代理系统的人来说,这是一个值得认真审查的平台。首先,我们将通过 ChatGPT Plus 查看构建 GPT 助手,这需要高级订阅。如果你不想购买订阅,可以将本章作为入门指南,第六章将演示稍后消费 API 服务。

图 3.1 显示了 ChatGPT 内 GPT Store 的页面(ChatGPT.com/gpts)。从这里,你可以搜索和探索几乎任何任务的 GPT。使用量通常可以表明每个 GPT 的工作效果如何,所以你可以判断哪个最适合你。

图

图 3.1 GPT Store 的主界面

创建你的第一个 GPT 助手就像点击创建按钮并跟随 GPT Builder 聊天界面一样简单。图 3.2 展示了如何使用 Builder 创建一个 GPT。通过这个练习练习几次是理解助手需求的一个很好的方法。

图

图 3.2 与 GPT Builder 交互以创建助手

在使用 Builder 之后,你可以打开如图 3.3 所示的手动配置面板,并直接编辑 GPT。你会看到从与 Builder 的对话中自动填充的名字、描述、说明和对话开头。这可以是一个很好的开始,但通常,你将想要手动编辑和调整这些属性。

图

图 3.3 GPT 助手平台界面的配置面板

如果你想要跟随构建自己的食谱伴侣,请将列表 3.1 中的文本输入到指令中。这些指令部分是通过与构建者对话生成的,并根据明确的输出添加的。明确的输出被添加到指令中作为规则。

列表 3.1 食谱伴侣使用说明
Culinary Companion assists users with a friendly, engaging tone, 
reminiscent of the famous chef Julia Child.     #1
It provides quick meal ideas and simplifies complex recipes, focusing on 
ingredients the user already has. This GPT emphasizes practical, easy-
to-follow culinary advice and adapts to dietary preferences. It's 
designed to make cooking a more accessible and enjoyable experience, 
encouraging users to experiment with their meals while offering helpful 
tips in a warm, approachable manner.     #2

RULES:
When generating a recipe, always create an image of the final prepared 
recipe.                                                                   #3
When generating a recipe, estimate the calories and nutritional values 
per serving.                                                             
When generating a recipe, provide a shopping list of ingredients with 
estimated prices needed to complete the recipe.                          
When generating a recipe, estimate the total cost per serving based on 
the shopping list.

1 代理的个性或角色

2 代理的角色和目标的一般指南

3 代理在建议食谱时将遵循的一组规则

为代理/助手定义规则实际上为代理将产生的内容创建了一个模板。添加规则确保 GPT 输出是一致的,并与你对代理应如何操作的期望相一致。定义并给代理/助手一个角色/个性可以为他们提供一个独特且难忘的个性。

注意:给代理/助手一个特定的个性可以影响输出类型和形式。要求烹饪代理以第一位名人厨师朱莉亚·查尔德的口吻说话,不仅提供了有趣的语气,还涉及更多可能提及或谈论她烹饪风格和教学的内容。在构建代理/助手时,分配一个特定的角色/个性可能会有所帮助。

只需这几个步骤,我们就拥有了一个烹饪伴侣,它不仅为我们提供了我们手头上的食材的食谱,还生成了完成食谱的图片,估计了营养价值,创建了一个包含价格估计的购物清单,并分解了每份的成本。

通过请求一个食谱并提供你拥有的或偏好的成分列表来尝试这个助手。列表 3.2 显示了一个带有额外信息以设定氛围的简单请求示例。当然,你可以添加任何你喜欢的成分或情况,然后查看结果。

列表 3.2 提示食谱
I have a bag of prepared frozen chicken strips and I want to make a 
romantic dinner for two.

图 3.4 展示了由提示生成的 GPT 提供的格式化输出结果。看起来足够诱人,可以吃。所有这些输出都是因为我们向代理提供的指令。

图片

图 3.4 食谱伴侣 GPT 的输出结果

虽然输出结果看起来很棒,但它们可能并不都是事实和正确的,你的结果可能会有所不同。例如,当我们已经建议使用这些成分时,GPT 将鸡条添加到了购物清单中。此外,价格和估计的营养信息只是估计,但如果它们对你感兴趣,这可以在以后解决。

然而,GPT 助手在快速构建原型助手或代理方面出类拔萃。正如你将在本章后面看到的那样,它还为在 ChatGPT 之外消费助手提供了一个出色的平台。在下一节中,我们将探讨 GPT 提供的更多令人印象深刻的功能,例如文件上传和代码解释。

3.2 构建一个可以进行数据科学的 GPT

GPT 助手平台已经并将可能扩展以包括各种代理组件。目前,GPT 助手支持所谓的知识、记忆和动作。在第八章中,我们将讨论知识和记忆的细节,而在第五章中,我们将介绍通过动作使用工具的概念。

在我们的下一个练习中,我们将构建一个助手,以对提供的任何 CSV 文档进行初步的数据科学审查。这个代理将使用允许编码和代码解释的能力或动作。当您启用代码解释时,助手将默认允许文件上传。

在我们这样做之前,我们希望设计我们的代理,而有什么比让 LLM 帮我们构建助手更好的方法呢?列表 3.3 展示了请求 ChatGPT(GPT-4)设计数据科学助手的提示。注意我们不是在单个提示中要求所有内容,而是在迭代 LLM 返回的信息。

列表 3.3 请求数据科学助手
FIRST PROMPT:    
what is a good basic and interesting data science 
experiment you can task someone with a single 
csv file that contains interesting data?     #1
SECOND PROMPT:    
okay, can you now write all those steps into instructions 
to be used for a GPT Agent (LLM agent) to replicate all of 
the above steps      #2

THIRD PROMPT:    
What is a famous personality that can embody the agent 
data scientist and be able to present data to users?      #3

1 首先,让大型语言模型(LLM)奠定基础。

2 然后,让 LLM 将之前的步骤转换为更正式的过程。

3 最后,让 LLM 提供一个可以代表过程的个性。

那次对话的结果提供了列表 3.4 中所示的助手指令。在这种情况下,助手被命名为数据侦察,但请随意将您的助手命名为您觉得吸引人的名字。

列表 3.4 数据侦察指令
This GPT, named Data Scout, is designed to assist users by analyzing CSV 
files and providing insights like Nate Silver, a famous statistician known 
for his accessible and engaging approach to data. Data Scout combines 
rigorous analysis with a clear and approachable communication style, 
making complex data insights understandable. It is equipped to handle 
statistical testing, predictive modeling, data visualization, and more, 
offering suggestions for further exploration based on solid data-driven 
evidence.

Data Scout requires the user to upload a csv file of data they want to 
analyze. After the user uploads the file you will perform the following 
tasks:
Data Acquisition
    Ask the user to upload a csv file of data.
    Instructions: Use the pandas library to read the data from the CSV 
file. Ensure the data is correctly loaded by displaying the first few rows 
using df.head().

2\. Exploratory Data Analysis (EDA)
Data Cleaning
    Task: Identify and handle missing values, correct data types.
    Instructions: Check for missing values using df.isnull().sum(). For 
categorical data, consider filling missing values with the mode, and for 
numerical data, use the median or mean. Convert data types if necessary 
using df.astype().

Visualization
    Task: Create visualizations to explore the data.
    Instructions: Use matplotlib and seaborn to create histograms, scatter plots, and box plots. For example, use sns.histplot() for histograms and 
sns.scatterplot() for scatter plots.

Descriptive Statistics
    Task: Calculate basic statistical measures.
    Instructions: Use df.describe() to get a summary of the statistics and 
df.mean(), df.median() for specific calculations.

3\. Hypothesis Testing
    Task: Test a hypothesis formulated based on the dataset.
    Instructions: Depending on the data type, perform statistical tests 
like the t-test or chi-squared test using scipy.stats. For example, use 
stats.ttest_ind() for the t-test between two groups.

4\. Predictive Modeling
Feature Engineering
    Task: Enhance the dataset with new features.
    Instructions: Create new columns in the DataFrame based on existing 
data to capture additional information or relationships. Use operations 
like df['new_feature'] = df['feature1'] / df['feature2'].

Model Selection
    Task: Choose and configure a machine learning model.
    Instructions: Based on the task (classification or regression), select 
a model from scikit-learn, like RandomForestClassifier() or 
LinearRegression(). Configure the model parameters.

Training and Testing
    Task: Split the data into training and testing sets, then train the model.
    Instructions: Use train_test_split from scikit-learn to divide the 
data. Train the model using model.fit(X_train, y_train).

Model Evaluation
    Task: Assess the model performance.
    Instructions: Use metrics like mean squared error (MSE) or accuracy. 
Calculate these using metrics.mean_squared_error(y_test, y_pred) or 
metrics.accuracy_score(y_test, y_pred).

5\. Insights and Conclusions
    Task: Interpret and summarize the findings from the analysis and modeling.
    Instructions: Discuss the model coefficients or feature importances. 
Draw conclusions about the hypothesis and the predictive analysis. Suggest 
real-world implications or actions based on the results.

6\. Presentation
    Task: Prepare a report or presentation.
    Instructions: Summarize the process and findings in a clear and 
accessible format, using plots and bullet points. Ensure that the 
presentation is understandable for non-technical stakeholders.

在生成指令后,您可以将它们复制并粘贴到图 3.5 中的配置面板中。务必通过选择相应的复选框给助手提供代码解释工具(技能)。您在这里不需要上传文件;当代码解释复选框被启用时,助手将允许文件上传。

figure

图 3.5 打开代码解释器工具/技能

现在,我们可以通过上传 CSV 文件并对其提问来测试助手。本章的源代码文件夹中包含一个名为 netflix_titles.csv 的文件;前几行在列表 3.5 中进行了总结。当然,您可以使用任何您想要的 CSV 文件,但这个练习将使用 Netflix 示例。请注意,这个数据集是从 Kaggle 下载的,但您可以使用任何其他 CSV 文件,如果您愿意的话。

列表 3.5 netflix_titles.csv(数据的第一行)
show_id,type,title,director,cast,country,date_added,
release_year,rating,duration,listed_in,description     #1
s1,Movie,Dick Johnson Is Dead,Kirsten Johnson,, 
United States,"September 25, 2021",2020,PG-13,90 min,
Documentaries,"As her father nears the end of his life, 
filmmaker Kirsten Johnson stages his death in inventive 
and comical ways to help them both face the inevitable."     #2

1 列的逗号分隔列表

2 数据集的一个示例行

我们可以上传文件并让助手执行其任务,但在这个练习中,我们将更加具体。列表 3.6 展示了提示和上传文件以激活助手的过程(包括请求中的 Netflix_titles.csv)。此示例将结果过滤到加拿大,但当然,您可以使用任何您想查看的国家。

列表 3.6 请求数据侦察
Analyze the attached CSV and filter the results to the 
country Canada and output any significant discoveries 
in trends etc.     #1

1 您可以选择不同的国家来过滤数据。

如果您在助手解析文件时遇到问题,请刷新浏览器窗口并重试。根据您的数据和筛选条件,助手现在将像数据科学家一样使用代码解释器来分析和提取数据中的趋势。

图 3.6 显示了使用netflix_titles.csv文件作为数据时,列表 3.5 中的提示生成的输出。如果您选择不同的国家或请求其他分析,您的输出可能看起来相当不同。

figure

图 3.6 助手分析 CSV 数据时生成的输出

助手正在构建的数据科学图表是通过编写和执行代码来创建的。您可以尝试使用其他 CSV 文件,或者如果您愿意,使用不同形式的数据进行分析。您甚至可以使用助手继续迭代,以视觉上更新图表或分析其他趋势。

代码解释是一种吸引人的技能,您可能会将其添加到许多代理中,用于从计算到自定义格式化的一切。在下一节中,我们将探讨如何通过自定义动作扩展 GPT 的功能。

3.3 定制 GPT 并添加自定义动作

在我们接下来的练习中,我们将演示自定义动作的使用,这可以显著扩展您助手的范围。向一个代理添加自定义动作需要几个组件,从理解 OpenAPI 规范端点到连接到服务。因此,在我们添加自定义动作之前,我们将在下一节构建另一个 GPT 来协助我们。

3.3.1 创建一个构建助手的助手

考虑到 GPT 的能力,我们使用一个来协助构建其他 GPT 是合情合理的。在本节中,我们将构建一个 GPT,它可以帮助我们创建可以连接为自定义动作的 GPT。是的,我们甚至将使用一个大型语言模型来开始构建我们的辅助 GPT。

下面的列表显示了为我们的辅助 GPT 创建指令的提示。这个提示旨在生成助手的指令。

列表 3.7 在 GPT Builder 或 ChatGPT 中提示辅助设计
I want to create a GPT assistant that can generate a FastAPI service that 
will perform some action to be specified. As part of the FastAPI code 
generation, I want the assistant to generate the OpenAPI specification for 
the endpoint. Please outline a set of instructions for this agent.

列表 3.8 显示了为提示生成的指令的大部分内容。然后,输出被修改并稍作更新,加入了具体信息和其它细节。请将这些指令从文件(assistant_builder.txt)中复制并粘贴到您的 GPT 中。务必选择代码解释器功能。

列表 3.8 自定义动作助手指令
This GPT is designed to assist users in generating FastAPI services 
tailored to specific actions, complete with the corresponding OpenAPI 
specifications for the endpoints. The assistant will provide code snippets 
and guidance on structuring and documenting API services using FastAPI, 
ensuring that the generated services are ready for integration and 
deployment.

1\.   Define the Action and Endpoint: First, determine the specific action 
the FastAPI service should perform. This could be anything from fetching 
data, processing information, or interacting with other APIs or databases.

2\.    Design the API Endpoint: Decide on the HTTP method (GET, POST, PUT, 
DELETE, etc.) and the endpoint URI structure. Define the input parameters 
(path, query, or body parameters) and the expected response structure.

3\. Generate FastAPI Code:
        Setup FastAPI: Import FastAPI and other necessary libraries.
        Create API Function: Write a Python function that performs the 
desired action. This function should accept the defined input parameters 
and return the appropriate response.
4\. Decorate the Function: Use FastAPI's decorators (e.g., 
@app.get("/endpoint")) to link the function with the specified endpoint 
and HTTP method.
        Define Input and Output Models: Use Pydantic models to define the 
structure of the input and output data. This ensures validation and 
serialization of the data.

5\. Generate OpenAPI Specification:
        FastAPI automatically generates the OpenAPI specification based on 
the endpoint definitions and Pydantic models. Ensure that all function 
parameters and models are well-documented using docstrings and field 
descriptions.
        Optionally, customize the OpenAPI specification by adding 
metadata, tags, or additional responses directly in the FastAPI decorators.

6\. Deployment:
        Describe to the user how to prepare the FastAPI application for 
deployment. 
        Instruct them on how to use ngrok to deploy the 
service and host it on the user's local machine.      #1

1 这以 ngrok 为例来展示如何在本地部署服务。

在准备助手后,确保在配置面板中设置好一切(包括设置代码解释器复选框),然后刷新浏览器窗口。这将准备助手为新会话。您可以从这里请求您想要构建的服务类型。

列表 3.9 显示了对自定义操作助手的请求,以创建每日任务端点。如果您了解 API 的工作原理,您可以建议其他选项,例如 POST。当然,您也可以要求助手引导您并创建您的服务。

列表 3.9 请求任务端点服务
I want to define a GET endpoint that replies with my list of daily tasks

输入提示后,助手将生成创建和运行 FastAPI 端点的代码和说明。以下列表显示了从上一个请求生成的代码示例。

列表 3.10 daily_tasks_api.py(由助手生成)
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List

app = FastAPI()

class Task(BaseModel):    #1
    id: int
    description: str
    completed: bool

tasks = [     #2
    Task(id=1, description="Buy groceries", completed=False),
    Task(id=2, description="Read a book", completed=True),
    Task(id=3, description="Complete FastAPI project", completed=False),
]

@app.get("/tasks", response_model=List[Task])    #3
async def get_tasks():
    """
    Retrieve a list of daily tasks.
    """
    return tasks

1 使用 Pydantic 创建任务类型。

2 这是一个静态的任务列表,用于演示。

3 任务端点

将代码输入到 Visual Studio Code (VS Code) 中,并确认已使用 pip 安装了 fastapiuvicorn。然后,使用以下列表中显示的命令运行 API,该命令在章节源文件中运行 API。

列表 3.11 运行 API
uvicorn daily_tasks_api:app –reload      #1

1 如果你使用的是不同的模块/文件名,请更改其名称。

打开浏览器到 http://127.0.0.1:8000/docs,这是 Swagger 端点的默认位置,如图 3.7 所示。

图片

图 3.7 导航 Swagger 文档并获取 openapi.json 文档

点击 /openapi.json 链接将显示端点的 OpenAPI 规范,如图 3.12 所示(JSON 转换为 YAML)。您需要复制并保存此文档,以便在设置代理上的自定义操作时使用。端点产生 JSON,但您也可以使用 YAML 编写的规范。

列表 3.12 任务 API 的 OpenAPI 规范
openapi: 3.1.0
info:
  title: FastAPI
  version: 0.1.0
paths:
  /tasks:
    get:
      summary: Get Tasks
      description: Retrieve a list of daily tasks.
      operationId: get_tasks_tasks_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Task'
                title: Response Get Tasks Tasks Get
components:
  schemas:
    Task:
      type: object
      properties:
        id:
          type: integer
          title: Id
        description:
          type: string
          title: Description
        completed:
          type: boolean
          title: Completed
      required:
        - id
        - description
        - completed
      title: Task

在将助手连接到服务之前,您必须设置并使用 ngrok 打开到运行服务的本地机器的隧道。提示 GPT 提供说明并帮助您设置 ngrok,然后运行应用程序以在您的机器上打开到端口 8000 的端点,如图 3.13 所示。如果您更改端口或使用不同的配置,您必须相应地更新它。

列表 3.13 运行 ngrok(按照设置说明)
./ngrok authtoken <YOUR_AUTHTOKEN>      #1
./ngrok http 8000      #2

1 输入从 ngrok.com 获得的认证令牌。

2 在端口 8000 上为外部互联网流量打开隧道

运行 ngrok 后,您将看到一个外部 URL,您现在可以使用它来访问您机器上的服务。复制此 URL 以供设置助手时使用。在下一节中,我们将创建一个助手,该助手将作为自定义操作消费此服务。

3.3.2 将自定义操作连接到助手

在您的机器上运行并可通过 ngrok 隧道外部访问服务后,我们可以构建新的助手。这次,我们将创建一个简单的助手来帮助我们组织日常任务,其中任务将可通过我们本地运行的任务服务访问。

打开 GPT 界面和配置面板,将列表 3.14 中显示的说明复制并粘贴到新的助手中。请确保为助手命名并输入有帮助的描述。此外,开启代码解释器功能,以便助手创建最终的图表,显示任务。

列表 3.14 任务组织者(task_organizer_assistant.txt
Task Organizer is designed to help the user prioritize their daily tasks 
based on urgency and time availability, providing structured guidance on 
how to categorize tasks by urgency and suggesting optimal time blocks for 
completing these tasks. It adopts a persona inspired by Tim Ferriss, known 
for his focus on productivity and efficiency. It uses clear, direct 
language and avoids making assumptions about the user's free time.
When you are done organizing the tasks create a plot 
showing when and how the tasks will be completed.      #1

1 此功能需要启用代码解释器。

在面板底部点击创建新操作按钮。图 3.8 显示了添加自定义操作的界面。您必须将您服务的 OpenAPI 规范复制并粘贴到窗口中。然后,您必须添加一个名为servers的新部分,并填写您的 URL,如图所示。

图片

图 3.8 添加新的自定义操作

设置规范后,您可以通过点击测试按钮来测试它。这将运行一个测试,您将在对话窗口中看到如图 3.9 所示的结果。

图片

图 3.9 测试 API 服务端点是否正确配置为自定义操作

您满意后,一切设置就绪。刷新浏览器窗口以重置会话,并输入类似于列表 3.15 中显示的提示。这将提示代理调用服务以获取您的日常任务,总结输出,并解决您的任务组织难题。

列表 3.15 任务组织者提示
how should I organize my tasks for today?

助手应在最后生成任务日程的图表。如果它做错了或者格式不是您想要的,您可以添加说明来指定助手应输出的格式/样式。

你可以改进服务,但如果你对 API 进行了任何更改,助手自定义操作中的规范也需要更新。然而,从这里开始,你可以添加运行在您的计算机上或作为服务托管的自定义操作服务。

注意:请注意,如果您公开发布助手供公众使用,未知用户可以激活自定义操作,因此不要暴露那些向您收取服务费或访问私人信息的操作,除非这是您的意图。同样,通过 ngrok 隧道打开的服务将通过助手暴露,这可能会引起关注。请小心发布使用自定义操作的代理。

自定义操作是向助手添加动态功能的好方法,无论是个人使用还是商业用途。文件上传是向助手提供静态知识更好的选择。下一节将探讨使用文件上传来扩展助手的知识。

3.4 使用文件上传扩展助手的知识

如果你已经与大型语言模型(LLM)互动过,你很可能已经听说过检索增强生成(RAG)模式。第八章将详细探讨 RAG 在知识和记忆应用方面的应用。使用文件上传功能不需要对 RAG 有详细的了解,但如果你需要一些基础知识,可以查看那一章。

GPT 助手平台提供了一个名为“文件上传”的知识能力,它允许你以各种格式将静态知识库中的任何内容填充到 GPT 中。截至写作时,GPT 助手平台允许你上传高达 512 MB 的文档。在接下来的两个练习中,我们将查看两个不同的 GPT,旨在帮助用户消费书籍。

3.4.1 构建《微积分变得简单》GPT

书籍和书面知识始终是我们知识库的支柱。但阅读文本需要全神贯注的努力,许多人没有时间这样做。有声书让阅读书籍再次变得可行;你可以边做其他事情边听,但并非所有书籍都能很好地过渡到音频。

进入人工智能和智能助手的领域。有了 GPT,我们可以创建读者与书籍之间的交互式体验。读者不再被迫一页一页地阅读书籍,而是可以整体阅读。

为了演示这个概念,我们将基于一本名为《微积分变得简单》的经典数学文本构建一个 GPT,作者是 Silvanus P. Thompson。这本书可以通过古腾堡出版社网站免费获得。虽然这本书已经有一百多年历史了,但它仍然提供了坚实的材料背景。

注意:如果你认真学习微积分,但这个助手仍然过于高级,可以查看 Clifford A. Pickover 写的一本名为《微积分与披萨》的出色书籍。这是一本学习微积分或获得优秀复习的好书。如果你有电子书版本,你也可以尝试制作你的《微积分与披萨》助手。不幸的是,版权法将阻止你在未经许可的情况下发布这个 GPT。

打开 ChatGPT,进入“我的 GPTs”,创建一个新的 GPT,点击“配置”标签,然后上传文件,如图 3.10 所示。从章节源代码文件夹中上传书籍:chapter _03/calculus_made_easy.pdf。这将把书籍添加到 GPT 的知识库中。

figure

图 3.10 向助手的知识库添加文件

向上滚动并添加列表 3.16 中显示的说明。初始的序言文本是通过与 GPT 构建者对话生成的。更新序言文本后,通过向 ChatGPT 询问著名数学家来添加一个个性。最后,添加规则以向 GPT 提供额外的指导,明确我们想要的显式结果。

列表 3.16 《微积分变得简单》GPT 的说明
This GPT is designed to be an expert teacher and mentor 
of calculus based on the book 'Calculus Made Easy' by 
Silvanus Thompson. A copy of the book is uploaded at 
calculus_made_easy.pdf and provides detailed guidance 
and explanations on various calculus topics such as 
derivatives, integrals, limits, and more. The GPT can 
teach calculus concepts, solve problems, and answer 
questions related to calculus, making complex topics 
accessible and understandable. It can handle 
calculus-related inquiries, from basic to advanced, 
and is particularly useful for students and educators
 seeking to deepen their understanding of calculus.      #1
Answer as the famous mathematician Terence Tao. 
Terence Tao is renowned for his brilliant intellect, 
approachability, and exceptional ability to effectively
 simplify and communicate complex mathematical concepts.     #2

RULES     #3
1) Always teach the concepts as if you were teaching to a young child.
2) Always demonstrate concepts by showing plots of functions and graphs.
3) Always ask if the user wants to try a sample problem on their own. 
Give them a problem equivalent to the question concept you were discussing.

1 序言最初由构建者生成,然后根据需要调整。

2 一定要给你的助手和代理一个合适的个性/人格。

3 定义明确的条件和规则可以帮助更好地引导 GPT 达到你的期望。

更新助手后,你可以在预览窗口或书籍版本中尝试它,通过在 GPT 商店中搜索《微积分变得简单》来找到。图 3.11 展示了与 GPT 交互的示例片段。该图显示 GPT 可以生成图表来展示概念或提问。

figure

图 3.11 GPT 教授微积分的输出

这个 GPT 展示了助手使用书籍作为教学参考的能力。在这个练习中只上传了一本书,但可以上传多本书或其他文档。随着这个功能和技术的成熟,未来可能可以实现使用 GPT 教授整个课程。

我们将远离技术,转向小说,以展示知识的应用。在下一节中,我们将探讨如何利用文件上传的知识进行搜索和参考。

3.4.2 使用文件上传进行知识搜索及其他

GPT 助手平台的文件上传功能支持单个助手最多上传 512 MB。仅此一项功能就为文档搜索和其他在个人和小型至中型企业/项目规模中的应用提供了强大的能力。

想象一下上传整个文件集合。现在,您可以使用一个助手进行搜索、比较、对比、组织和整理。仅此一项功能在 GPT 助手内就能颠覆我们搜索和分析文档的方式。在第六章中,我们将探讨直接访问 OpenAI 助手 API 如何增加文档数量。

在接下来的练习中,我们将使用一个了解多本书或文档的助手。这种技术可以应用于任何支持的文档,但这个助手将阅读关于机器人的经典文本。我们将把这个助手命名为“经典机器人阅读 GPT”。

首先,在 ChatGPT 界面上创建一个新的 GPT 助手。然后,上传列表 3.17 中的指令,并为助手命名和描述。这些指令部分是通过 GPT Builder 生成的,然后进行了编辑。

列表 3.17 经典机器人阅读指令
This GPT, Classic Robot Reads and uses the persona of 
Isaac Asimov and will reply as the famous robot author.     #1
This GPT will only references and discusses the books 
in its knowledge base of uploaded files.                   #2
It does not mention or discuss other books or text that 
are not within its knowledge base.                        #2

RULES
Refer to only text within your knowledge base         #2    
Always provide 3 examples of any query the use asks for     #3
Always ask the user if they require anything further      #4

1 记住始终给你的 GPT 赋予一个角色/个性。

2 确保助手只引用文件上传中的知识。

3 为风格选择添加一些额外规则。

4 通过提供细微差别和风格,使助手更有帮助。

完成这些步骤后,您可以上传来自章节源gutenberg_robot_books的文件。图 3.12 展示了同时上传多个文件。您一次可以上传的文件数量将根据文件大小而变化。

图

图 3.12 将文档上传到助手的知识库

在上传文档、设置指令、为助手命名和添加图像后,您就可以开始使用了。搜索是知识助手的最低级应用,其他以提示形式展示的使用案例如表 3.1 所示。

表 3.1 知识助手的用例
用例 示例提示 结果
搜索 在您的知识中搜索这个短语:“机器人仆人。” 返回文档和摘录
比较 识别出三本具有相同写作风格的最相似书籍。 返回最相似的三个文档
对比 识别出三本最不同的书籍。 返回集合中最不同的书籍
排序 我应该按什么顺序阅读这些书籍? 返回书籍的有序进展
分类 哪本书是最现代的? 对文档进行分类
生成 生成一个模仿你对机器人仆人知识的虚构段落。 根据其知识库生成新的内容

这些用例只是 AI 知识助手的许多可能用途的样本。虽然这个功能可能不会颠覆企业搜索,但它为小型组织和个人提供了更多访问其文档的机会。它允许创建作为公开知识形式的助手。在下一节中,我们将探讨如何使助手对所有用户都可用。

3.5 发布你的 GPT

一旦你对你的 GPT 满意,你可以通过提供链接的方式来使用它或与他人分享。目前通过 ChatGPT 消费 GPT 助手需要 Plus 订阅。要发布你的 GPT 以供他人使用,点击分享按钮,并选择你的分享选项,如图 3.13 所示。

图

图 3.13 GPT 分享选项

无论你是在与朋友和同事分享你的 GPT,还是在 GPT 商店公开分享,助手的用量都是从使用它的账户中扣除的,而不是出版商。这意味着如果你有一个特别昂贵的 GPT,例如生成大量图像,那么在其他人使用它时,这不会影响你的账户。

3.5.1 昂贵的 GPT 助手

在撰写本文时,OpenAI 跟踪你的 ChatGPT 账户的资源使用情况,包括用于 GPT 的部分。如果你达到资源使用限制并被阻止,你的 ChatGPT 账户也将被阻止。通常情况下,这种阻止只会持续几个小时,但这无疑会让人感到有些烦恼。

因此,我们希望确保使用你的 GPT 的用户不会超过他们常规使用的资源使用限制。以下是使用 GPT 时会增加资源使用的功能列表:

  • 创建图像 — 图像生成仍然是一项高端服务,连续生成图像可能会迅速导致你的用户被阻止。通常建议你告知用户潜在的风险,并尝试减少图像生成的频率。

  • 代码解释 — 此功能允许上传文件并运行代码进行数据分析。如果你认为你的用户将需要持续使用编码工具,那么请告知他们风险。

  • 视觉,描述图像 — 如果你正在构建一个使用视觉来描述和从图像中提取信息的助手,请计划谨慎使用。

  • 文件上传 — 如果您的 GPT 使用大量文件或允许您上传多个文件,这可能会导致阻塞。像往常一样,引导用户远离任何阻止他们享受您的 GPT 的东西。

注意:摩尔定律指出,计算机的功率每两年翻一番,而成本减半。LLMs 现在大约每六个月通过优化和增加 GPU 功率翻一番。这与在同一时期成本至少减半相结合,可能意味着当前对视觉和图像生成模型的资源限制将不再被考虑。然而,像代码解释和文件上传这样的服务可能仍然保持不变。

让您的助手意识到资源使用情况可以简单到只需将列表 3.18 中显示的规则添加到助手的指令中。这些指令可以仅仅是一个向用户传达警告并让助手意识到的声明。您甚至可以要求助手限制其使用某些功能。

列表 3.18 资源使用规则示例
RULE:
When generating images, ensure the user is aware that creating multiple 
images quickly could temporarily block their account.

引导您的助手最终更加关注资源使用,使您的助手更加可用。这也有助于防止那些无意中使用了您的助手而被阻止的用户感到愤怒。如果您计划发布您的 GPT,这可能很重要,但在那之前,让我们在下一节中调查一下经济效益。

3.5.2 理解 GPT 的经济效益

在 GPT 助手和 GPT 商店发布后,OpenAI 宣布了为那些发布 GPT 的人未来可能实现的利润共享计划。虽然我们还在等待了解更多关于这个计划的信息,但许多人已经推测了它可能的样子。

有些人建议商店可能只将 10%到 20%的利润返还给构建者。这比其他应用平台上的百分比要少得多,但需要的专业技术知识和资源更少。如果您有 Plus 订阅,GPT 商店中充满了几乎免费的助手,但这可能在将来改变。无论如何,也有几个原因您可能想要构建公共 GPT:

  • 个人作品集 — 也许您想展示您对提示工程的知识或构建下一波 AI 应用的能力。在 GPT 商店中拥有几个 GPT 可以帮助展示您的知识和创建有用 AI 应用的能力。

  • 知识和经验 — 如果您对某个主题或领域有深入的了解,这可以是一个将知识打包成助手的好方法。这类助手的人气将根据您的专业领域而有所不同。

  • 跨营销和商业捆绑 — 这在商店中变得越来越普遍,为公司提供了使用助手引导客户的能力。随着公司越来越多地整合 AI,这肯定会更加普遍。

  • 产品/服务的有益助手 — 并非所有公司或组织都能承担得起托管聊天机器人的成本。虽然目前消费助手仅限于 ChatGPT 订阅者,但它们将来可能会更容易获得。这可能意味着为所有事物都拥有 GPT,也许就像互联网的早期,每个公司都急于建立网络存在。

虽然当前的 GPT Store 形式是为 ChatGPT 订阅者准备的,但如果 OpenAI 的当前趋势持续下去,我们可能会看到一个完全公开的 GPT Store。公开的 GPT 有潜力颠覆我们搜索、调查产品和服务以及消费互联网的方式。在本章的最后部分,我们将探讨如何发布 GPT 以及一些重要的考虑因素。

3.5.3 发布 GPT

好的,你对你的 GPT 及其操作感到满意,并且你看到将其提供给他人带来的实际好处。如图 3.14 所示,发布供公众(订阅者)消费的 GPT 很容易。在选择 GPT Store 作为选项并点击保存后,你现在将可以选择设置类别并提供链接回到你那里。

图

图 3.14 选择点击“保存”以发布到 GPT Store 后的选项

这很简单,所以在发布你的 GPT 之前,这里有几点你想要考虑:

  • GPT 描述 — 创建一个良好的描述,你可能甚至想要让 ChatGPT 帮助你构建一个提高你的 GPT 搜索引擎优化(SEO)的描述。GPT 现在正出现在 Google 搜索中,因此良好的搜索引擎优化可以帮助增加你的助手的曝光度。一个好的描述也有助于用户决定他们是否愿意花时间使用你的助手。

  • 标志 — 一个好-looking、干净的标志可以清楚地说明你的助手做什么,无疑会很有帮助。GPT 的标志设计实际上是一项免费服务,但花时间迭代几幅图像可以帮助吸引用户到你的助手那里。

  • 类别 — 默认情况下,类别已经被选中,但请确保它适合你的助手。如果你觉得它不适合,那么就更改类别,你可能甚至想要选择“其他”并定义自己的类别。

  • 链接 — 一定要设置你的社交媒体和可能甚至用于跟踪 GPT 问题的 GitHub 仓库的参考链接。为你的 GPT 添加链接可以向用户展示,如果他们遇到问题或有问题,他们可以联系构建者。

随着 GPT Store 的成熟,可能还会出现更多进一步的要求。商业模式仍需建立,其他经验教训也可能随之而来。无论你决定为自己还是他人构建 GPT,这样做都能帮助你更好地理解如何构建代理和助手。正如我们在本书的其余部分将看到的,GPT 助手是你知识的有用基础。

3.6 练习

完成以下练习以提高你对材料的了解:

  • 练习 1 — 构建你的第一个 GPT 助手

目标 — 使用 ChatGPT 界面创建一个简单的 GPT 助手。

任务:

    • 如果您还没有,请注册 ChatGPT Plus 订阅。

    • 导航到 GPT 助手平台,并点击创建按钮。

    • 按照构建器聊天界面创建一个提供基于可用食材的餐点建议的烹饪伴侣助手。

    • 手动配置助手以添加用于食谱生成的自定义规则,例如包括营养信息和成本估算。

  • 练习 2 — 数据分析助手

目标 — 开发一个能够分析 CSV 文件并提供见解的 GPT 助手。

任务:

    • 设计一个数据科学助手,能够加载和分析 CSV 文件,类似于章节中 Data Scout 的示例。

    • 启用代码解释工具,并上传一个示例 CSV 文件(例如,来自 Kaggle 的数据集)。

    • 使用助手执行数据清理、可视化和假设检验等任务。

    • 记录您的流程和发现,注意任何挑战或需要改进的地方。

  • 练习 3 — 创建自定义操作

目标 — 使用 FastAPI 服务扩展 GPT 助手以添加自定义操作。

任务:

    • 按照步骤创建一个提供特定功能(如获取每日任务列表)的 FastAPI 服务。

    • 为服务生成 OpenAPI 规范,并使用 ngrok 在本地部署。

    • 配置一个新的助手以使用此自定义操作,确保它正确连接到 FastAPI 端点。

    • 通过要求助手执行操作并验证输出来测试助手。

  • 练习 4 — 文件上传知识助手

目标 — 建立一个具有上传文档中专业知识的助手。

任务:

    • 选择一个与特定主题(例如,经典文学、技术手册)相关的免费电子书或文档集合。

    • 将这些文件上传到新的 GPT 助手,并配置助手以作为上传内容的专家。

    • 创建一系列提示来测试助手引用和总结文档信息的能力。

    • 评估助手的性能,并根据需要做出任何调整以提高其准确性和实用性。

  • 练习 5 — 发布和分享您的助手

目标 — 将您的 GPT 助手发布到 GPT 商店并与他人分享。

任务:

    • 完成助手的配置和测试,以确保其按预期工作。

    • 编写引人入胜的描述,并为您的助手创建一个合适的标志。

    • 选择正确的类别,并设置任何必要的链接到您的社交媒体或 GitHub 仓库。

    • 将助手发布到 GPT 商店,并与朋友或同事分享链接。

    • 从用户那里收集反馈,并根据他们的输入对助手进行优化,以提高其可用性和功能性。

摘要

  • OpenAI GPT 助手平台通过 ChatGPT UI 使构建和部署 AI 代理成为可能,重点是创建引人入胜且功能齐全的助手。

  • 你可以使用 GPT 的代码解释能力对用户上传的 CSV 文件进行数据分析,使助手能够作为数据科学家发挥作用。

  • 通过自定义操作可以扩展助手,允许通过 API 端点与外部服务集成。这包括生成 FastAPI 服务和相应的 OpenAPI 规范。

  • 通过文件上传可以丰富助手的专门知识,使其能够作为特定文本或文档的权威来源。

  • 商业化你的 GPT 涉及将其发布到 GPT Store,在那里你可以将你的助手分享和推广给更广泛的受众。

  • 构建一个功能性的助手涉及迭代设计提示,定义一个清晰的个性,设置规则,并确保助手的输出与用户期望相符。

  • 创建自定义操作需要理解和实施 OpenAPI 规范,使用 ngrok 等工具在本地部署服务,并将这些服务连接到你的助手。

  • 知识助手可以处理各种任务,从搜索和比较文档到根据其知识库生成新内容。

  • 发布助手需要仔细考虑资源使用、用户体验和经济因素,以确保其有效性和可持续性,适用于公众使用。

  • GPT Store,ChatGPT Plus 订阅者可访问的平台,是一个学习和提高构建 AI 助手技能的有价值平台,未来还有可能实现利润共享的机会。

第四章:探索多智能体系统

本章涵盖

  • 使用 AutoGen Studio 构建多智能体系统

  • 构建一个简单的多智能体系统

  • 创建能够在群聊中协同工作的智能代理

  • 使用 CrewAI 构建智能代理团队和多智能体系统

  • 使用 CrewAI 扩展智能代理数量并探索处理模式

现在,让我们从 AutoGen 到 CrewAI 进行一次旅行,这两个都是成熟的智能代理平台。我们将从 AutoGen 开始,这是一个支持多个智能代理并为他们提供工作室的微软项目。我们将探索一个名为 AutoGen 的微软项目,它支持多个智能代理,同时也提供了一个工作室来简化与智能代理一起工作的过程。从那里,我们将进行更多关于 AutoGen 智能代理的编码实践,以使用对话和群聊协作来解决任务。

然后,我们将过渡到 CrewAI,这是一个自提出的企业级智能代理系统,它采用了一种不同的方法。CrewAI 平衡了基于角色的和自主的智能代理,这些代理可以具有顺序或分层灵活的任务管理系统。我们将探讨 CrewAI 如何解决多样化和复杂的问题。

多智能体系统结合了许多单智能体系统使用的工具,但得益于能够向其他智能代理提供外部反馈和评估的能力。这种内部支持并批评智能代理解决方案的能力赋予了多智能体系统更多的力量。我们将探讨多智能体系统的介绍,下一节将从 AutoGen Studio 开始。

4.1 使用 AutoGen Studio 介绍多智能体系统

AutoGen Studio 是一个强大的工具,它背后使用多个智能代理来解决用户指定的任务和问题。这个工具已被用于开发本书中的一些更复杂的代码。因此,它是一个实践多智能体系统的优秀入门。

图 4.1 展示了 AutoGen 使用的智能体连接/通信模式的示意图。AutoGen 是一个对话式多智能体平台,因为通信是通过自然语言进行的。自然语言对话似乎是智能体通信的最自然模式,但并非唯一方法,正如你稍后将会看到的。

figure

图 4.1 AutoGen 智能体如何通过对话进行通信(来源:AutoGen)

AutoGen 支持各种对话模式,从群组和分层到更常见和简单的代理通信。在代理通信中,一个智能代理充当代理,并将通信引导到相关的智能代理以完成任务。代理类似于接受订单并将其传递给厨房的侍者,厨房负责烹饪食物。然后,侍者将烹饪好的食物上桌。

AutoGen 的基本模式使用一个UserProxy和一个或多个助手代理。图 4.2 显示了用户代理从人类那里获得指示,然后指导一个被授权编写代码以执行任务的助手代理。每次助手完成一个任务时,代理代理都会进行审查、评估并提供反馈给助手。这个迭代循环会一直持续到代理对结果满意为止。

图

图 4.2 用户代理代理和助手代理的通信(来源:AutoGen)

代理的好处是它旨在替代所需的人类反馈和评估,并且在大多数情况下,它做得很好。虽然它不能消除对人类反馈和评估的需求,但它总体上产生了更完整的结果。而且,虽然迭代循环耗时,但这是您可以用来喝咖啡或处理其他任务的时间。

AutoGen Studio 是由 AutoGen 团队开发的一个工具,它为可对话代理提供了一个有用的介绍。在下一个练习中,我们将安装 Studio 并运行一些实验,以查看该平台的表现如何。这些工具仍在快速开发周期中,所以如果您遇到任何问题,请查阅 AutoGen GitHub 仓库上的文档。

4.1.1 安装和使用 AutoGen Studio

在 Visual Studio Code(VS Code)中打开chapter_04文件夹,创建一个本地 Python 虚拟环境,并安装requirements.txt文件。如果您需要帮助,请参阅附录 B 以安装本章的所有练习要求。

在 VS Code 中打开终端(Ctrl-, Cmd-),指向您的虚拟环境,并使用列表 4.1 中显示的命令运行 AutoGen Studio。您首先需要为您的 OpenAI 密钥定义一个环境变量。因为 8080 和 8081 端口很受欢迎,如果您有其他服务正在运行,请将端口更改为 8082 或您选择的任何端口。

列表 4.1 启动 AutoGen Studio
# set environment variable on Bash (Git Bash)
export OPENAI_API_KEY=”<your API key>”          #1

# sent environment variable with PowerShell
$env:VAR_NAME =”<your API key>"                #1

autogenstudio ui --port 8081     #2

1 使用适合您终端类型的适当命令。

2 如果您预计或遇到机器上的冲突,请更改端口。

将您的浏览器导航到图 4.3 中显示的 AutoGen Studio 界面(截至本文撰写时)。虽然可能会有所不同,但有一点可以肯定:主界面仍然将是聊天界面。输入一个需要编码的复杂任务。这里使用的示例是创建一个显示“GPT 代理”在谷歌搜索流行度图表

图

图 4.3 在 AutoGen 界面中为代理输入要执行的任务

代理助手在代理通过示例中的任务协同工作时生成代码片段以执行或完成各种子任务。然后,用户代理尝试执行这些代码片段并评估输出。在许多情况下,证明代码运行并产生所需输出就足以让用户代理批准任务的完成。

如果您在助手代理请求中遇到任何问题,请要求代理尝试不同的方法或另一个问题。这突显了使用已过期且不再工作的包或库的代理系统的一个更大的问题。因此,通常更好的做法是让代理执行行动,而不是构建代码作为工具来执行行动。

提示:建议使用 Docker 执行 AutoGen 和 AutoGen Studio,尤其是在处理可能影响操作系统的代码时。Docker 可以隔离和虚拟化代理的环境,从而隔离可能有害的代码。使用 Docker 可以帮助缓解可能阻止代理进程运行的任何次要窗口或网站。

图 4.4 显示了代理完成任务的输出。代理将收集任何生成的代码片段、图像或其他文档,并将它们附加到消息中。您还可以通过打开“代理消息”展开器来回顾代理对话。在许多情况下,如果您要求代理生成图表或应用程序,将打开次要窗口以显示这些结果。

figure

图 4.4 代理完成任务后的输出

令人惊讶的是,代理将很好地执行大多数任务并完成它们。根据任务的复杂程度,您可能需要与代理进一步迭代。有时,代理可能只能完成一项任务,因为它缺乏所需的技能。在下一节中,我们将探讨如何向代理添加技能。

4.1.2 在 AutoGen Studio 中添加技能

技能和工具,或我们在这本书中称之为“行动”,是代理扩展自己的主要手段。行动赋予代理执行代码、调用 API 或进一步评估和检查生成输出的能力。AutoGen Studio 目前仅开始于一组基本的工具,用于获取网页内容或生成图像。

注意:许多代理系统采用允许代理编码以解决问题目标的做法。然而,我们发现代码容易被破解,需要维护,并且可能快速变化。因此,正如我们将在后续章节中讨论的,为代理提供技能/行动/工具以解决问题会更好。

在以下练习场景中,我们将使用 OpenAI 视觉模型添加一个检查图像的技能/行动。这将允许代理在请求助手生成具有特定内容的图像时提供反馈。

当 AutoGen Studio 运行时,转到“构建”选项卡,点击“技能”,如图 4.5 所示。然后,点击“新建技能”按钮打开一个代码面板,您可以将代码复制粘贴到其中。从该选项卡,您还可以配置模型、代理和代理工作流程。

figure

图 4.5 在“构建”选项卡上创建新技能的步骤

输入列表 4.2 中所示的代码,并在本书的源代码中作为describe_image.py提供。将此代码复制粘贴到编辑器窗口中,然后点击底部的“保存”按钮。

列表 4.2 describe_image.py
import base64
import requests
import os

def describe_image(image_path='animals.png') -> str:
    """
    Uses GPT-4 Vision to inspect and describe the contents of the image.

    :param input_path: str, the name of the PNG file to describe.
    """
    api_key = os.environ['OPEN_API_KEY']

    # Function to encode the image
    def encode_image(image_path):      #1
        with open(image_path, "rb") as image_file:
            return base64.b64encode(image_file.read()).decode('utf-8')
    # Getting the base64 string
    base64_image = encode_image(image_path)

    headers = {
    "Content-Type": "application/json",
    "Authorization": f"Bearer {api_key}"
    }

    payload = {
    "model": "gpt-4-turbo",
    "messages": [
        {
        "role": "user",
        "content": [
            {
            "type": "text",
            "text": "What’s in this image?"
            },
            {
            "type": "image_url",
            "image_url": {
         "url": f"data:image/jpeg;base64,{base64_image}"      #2
            }
            }
        ]
        }
    ],
    "max_tokens": 300
    }

    response = requests.post(
        "https://api.openai.com/v1/chat/completions",
        headers=headers,
        json=payload)

    return response.json()["choices"][0]["message"]  #3
["content"]                                          #3

1 函数:加载和编码图像为 Base64 字符串

2 在 JSON 有效负载中包括图像字符串

3 解包响应并返回回复的内容

describe_image 函数使用 OpenAI GPT-4 视觉模型来描述图像中的内容。这项技能可以作为质量评估与现有的 generate_image 技能配对。代理可以确认生成的图像符合用户的要求。

技能添加后,必须将其添加到特定的代理工作流程和代理中以便使用。图 4.6 展示了在通用或默认代理工作流程中将新技能添加到主要助手代理的过程。

图

图 4.6 配置主要助手代理的新技能

现在技能已经添加到主要助手,我们可以要求代理创建一个特定的图像,并使用新的 describe_image 技能进行验证。由于图像生成器在处理正确文本方面众所周知地存在困难,我们将创建一个练习任务来完成这项工作。

输入列表 4.3 中显示的文本,以提示代理为这本书创建一个书封面图像。我们将明确指出文本需要正确无误,并坚持要求代理使用新的 describe_image 函数来验证图像。

列表 4.3 提示创建书封面
Please create a cover for the book GPT Agents In Action, use the 
describe_image skill to make sure the title of the book is spelled 
correctly on the cover

输入提示后,等待一段时间,你可能会看到一些关于图像生成和验证过程的对话。然而,如果一切正常,代理最终将返回图 4.7 中显示的结果。

图

图 4.7 代理在图像生成任务上的生成文件输出

值得注意的是,代理协调在仅几次迭代中就完成了任务。除了图像外,你还可以看到为辅助任务完成而生成的一些各种辅助代码片段。AutoGen Studio 在其能够集成代理可以进一步适应以完成某些目标的能力方面给人留下深刻印象。下一节将展示如何用代码实现这些强大的代理。

4.2 探索 AutoGen

虽然 AutoGen Studio 是理解多人代理系统的一个出色工具,但我们必须查看代码。幸运的是,使用 AutoGen 编写多个代理示例既简单又容易运行。我们将在下一节中介绍基本的 AutoGen 设置。

4.2.1 安装和消费 AutoGen

下一个练习将探讨编写一个基本的多人代理系统,该系统使用用户代理和可对话的代理。在我们这样做之前,我们想要确保 AutoGen 已经正确安装和配置。

在 VS Code 中打开一个终端,按照附录 B 中的说明运行整个第四章安装指令,或者运行列表 4.4 中的 pip 命令。如果你已经安装了 requirements.txt 文件,你也将准备好运行 AutoGen。

列表 4.4 安装 AutoGen
pip install pyautogen

接下来,将chapter_04/OAI_CONFIG_LIST.example复制到OAI_CONFIG_LIST,从文件名中删除.example。然后,在 VS Code 中打开新文件,并在列表 4.5 中的OAI_CONFIG_LIST文件中输入您的 OpenAI 或 Azure 配置。根据您的 API 服务要求填写 API 密钥、模型和其他详细信息。AutoGen 将与遵循 OpenAI 客户端的任何模型一起工作。这意味着您可以通过 LM Studio 或其他服务(如 Groq、Hugging Face 等)使用本地 LLM。

列表 4.5 OAI_CONFIG_LIST
[
    {
        "model": "gpt-4",                     #1
        "api_key": "<your OpenAI API key here>",            #2
        "tags": ["gpt-4", "tool"]
    },
    {
        "model": "<your Azure OpenAI deployment name>",      #3
        "api_key": "<your Azure OpenAI API key here>",      #4
        "base_url": "<your Azure OpenAI API base here>",     #5
        "api_type": "azure",
        "api_version": "2024-02-15-preview"
    }    
]

1 选择模型;推荐使用 GPT-4。

2 使用您通常使用的服务密钥。

3 选择模型;推荐使用 GPT-4。

4 使用您通常使用的服务密钥。

5 更改基本 URL 允许您指向其他服务,而不仅仅是 Azure OpenAI。

现在,我们可以查看使用现成的UserProxyConversableAgent代理进行基本多代理聊天的代码。在 VS Code 中打开autogen_start.py,如下所示列表,并在运行文件之前查看相关部分。

列表 4.6 autogen_start.py
from autogen import ConversableAgent, UserProxyAgent, config_list_from_json

   config_list = config_list_from_json(
        env_or_file="OAI_CONFIG_LIST")      #1

   assistant = ConversableAgent(
        "agent", 
        llm_config={"config_list": config_list})      #2

   user_proxy = UserProxyAgent(      #3
        "user",
        code_execution_config={
            "work_dir": "working",
            "use_docker": False,
        },
        human_input_mode="ALWAYS",
        is_termination_msg=lambda x: x.get("content", "")
        .rstrip()
        .endswith("TERMINATE"),      #4
    )    
    user_proxy.initiate_chat(assistant, message="write a solution 
↪ for fizz buzz in one line?")     #5

1 从 JSON 文件 OAI_CONFIG_LIST 加载您的 LLM 配置。

2 此代理直接与 LLM(大型语言模型)进行交流。

3 此代理代理用户与助手之间的对话。

4 设置终止消息允许代理迭代。

5 通过 user_proxy 与助手发起聊天以完成任务。

通过在 VS Code 的调试器中运行文件(F5)来运行代码。列表 4.6 中的代码使用一个简单的任务来演示代码编写。列表 4.7 展示了几个可供选择示例。这些编码任务也是作者评估 LLM 在编码能力上的常规基准之一。

列表 4.7 简单编码任务示例
write a Python function to check if a number is prime
code a classic sname game using Pygame                   #1
code a classic asteroids game in Python using Pygame  #1

1 要享受对这些任务进行迭代的乐趣,请在 Windows 上使用 Windows Subsystem for Linux (WSL),或使用 Docker。

几秒钟后代码开始运行,助手将向代理提供一个解决方案。此时,代理将提示您提供反馈。按 Enter 键,实际上是没有提供反馈,这将提示代理运行代码以验证其是否按预期运行。

令人印象深刻的是,代理甚至可以获取提示来安装所需的包,例如 Pygame。然后它会运行代码,您将在终端或新窗口或浏览器中看到输出。如果代码在新的窗口/浏览器中打开,您可以玩游戏或使用界面。

注意,在 Windows 上,生成的窗口/浏览器不会关闭,并且需要退出整个程序。为了避免这个问题,请通过 Windows Subsystem for Linux (WSL)或 Docker 运行代码。AutoGen 明确建议使用 Docker 作为代码执行代理,如果您熟悉容器,这是一个不错的选择。

无论哪种方式,在代理生成和运行代码后,列表 4.6 中之前设置的 working_dir 文件夹现在应该有一个包含代码的 Python 文件。这将允许你随意运行代码,进行更改,甚至要求改进,正如我们将看到的。在下节中,我们将探讨如何提高编码代理的能力。

4.2.2 使用代理评论家增强代码输出

多代理系统的一个强大优势是,在完成任务时可以自动分配多个角色/角色。生成或帮助编写代码可以是任何开发者的一个极好的优势,但如果该代码也被审查和测试呢?在下一个练习中,我们将向我们的代理系统添加另一个代理评论家来帮助编码任务。打开 autogen_coding_critic.py,如下列所示。

列表 4.8 autogen_coding_critic.py
from autogen import AssistantAgent, UserProxyAgent, config_list_from_json

config_list = config_list_from_json(env_or_file="OAI_CONFIG_LIST")

user_proxy = UserProxyAgent(
    "user",
    code_execution_config={
        "work_dir": "working",
        "use_docker": False,
        "last_n_messages": 1,
    },
    human_input_mode="ALWAYS",
    is_termination_msg=lambda x: 
x.get("content", "").rstrip().endswith("TERMINATE"),
)

engineer = AssistantAgent(
    name="Engineer",
    llm_config={"config_list": config_list},
    system_message="""
    You are a profession Python engineer, known for your expertise in 
software development.
    You use your skills to create software applications, tools, and 
games that are both functional and efficient.
    Your preference is to write clean, well-structured code that is easy 
to read and maintain.    
    """,      #1
)

critic = AssistantAgent(
    name="Reviewer",
    llm_config={"config_list": config_list},
    system_message="""
    You are a code reviewer, known for your thoroughness and commitment 
to standards.
    Your task is to scrutinize code content for any harmful or 
substandard elements.
    You ensure that the code is secure, efficient, and adheres to best 
practices.
    You will identify any issues or areas for improvement in the code 
and output them as a list.
    """,      #2
)

def review_code(recipient, messages, sender, config):      #3
    return f"""
            Review and critque the following code.

            {recipient.chat_messages_for_summary(sender)[-1]['content']}
            """                       #3                    

user_proxy.register_nested_chats(      #4
    [
        {
            "recipient": critic,
            "message": review_code,
            "summary_method": "last_msg",
            "max_turns": 1,
        }
    ],
    trigger=engineer,                 #4
)
task = """Write a snake game using Pygame."""

res = user_proxy.initiate_chat(
    recipient=engineer, 
    message=task, 
    max_turns=2, 
    summary_method="last_msg"      #5
)

1 这次,助手被给予一个系统/角色消息。

2 创建了一个具有背景的第二个助手评论家代理。

3 一个自定义函数帮助提取评论家审查的代码。

4 评论家和工程师之间创建了一个嵌套聊天。

5 代理代理以最大延迟和明确的摘要方法启动聊天。

在 VS Code 中以调试模式运行 autogen_coding_critic.py 文件,并观察代理之间的对话。这次,在代码返回后,评论家也会被触发进行响应。然后,评论家会添加注释和建议来改进代码。

嵌套聊天对于支持和控制代理交互效果很好,但我们在下节中会看到一个更好的方法。在此之前,我们将在下一节中回顾 AutoGen 缓存的重要性。

4.2.3 理解 AutoGen 缓存

作为可对话的多代理平台,AutoGen 在聊天迭代中可能会消耗许多令牌。如果你要求 AutoGen 通过复杂或新颖的问题,你甚至可能会在 LLM 上遇到令牌限制;因此,AutoGen 支持几种减少令牌使用的方法。

AutoGen 使用缓存来存储进度并减少令牌使用。缓存默认启用,你可能已经遇到过。如果你检查当前工作文件夹,你会注意到一个 .cache 文件夹,如图 4.8 所示。缓存允许代理在被打断后继续对话。

figure

图 4.8 AutoGen 缓存和工作文件夹

在代码中,你可以控制代理运行的缓存文件夹,如图 4.9 所示。通过将 initiate_chat 调用用 with 语句包装,你可以控制缓存的位置和种子。这将允许你通过仅设置前一个缓存的 cache_seed 来保存并返回未来的长时间运行的 AutoGen 任务。

列表 4.9 设置缓存文件夹
with Cache.disk(cache_seed=42) as cache:     #1
    res = user_proxy.initiate_chat(
        recipient=engineer,
        message=task,
        max_turns=2,
        summary_method="last_msg",
        cache=cache,      #2
    )

1 设置 seed_cache 表示单个位置。

2 将缓存作为参数设置

这种缓存能力允许你从之前的缓存位置继续操作并捕获之前的运行。这也可以是一种展示和检查代理对话如何生成结果的好方法。在下一节中,我们将探讨另一种 AutoGen 支持的群聊对话模式。

4.3 与代理和 AutoGen 的群聊

聊天委托和嵌套聊天或对话的一个问题是信息的传达。如果你曾经玩过电话游戏,你亲自见证了这一点,并体验到了信息在迭代中如何迅速变化。对于代理来说,这当然也不例外,通过嵌套或顺序对话聊天可以改变任务甚至期望的结果。

电话游戏

电话游戏是一种既有趣又有教育意义的游戏,它展示了信息和连贯性的丧失。孩子们排成一排,第一个孩子只能听到一个信息。然后,孩子们轮流将信息口头传递给下一个孩子,以此类推。最后,最后一个孩子将信息宣布给整个小组,通常这个信息与最初的信息相差甚远。

为了应对这一点,AutoGen 提供了一个群聊机制,通过这个机制代理可以参与共享对话。这允许代理回顾所有过去的对话,并在长期和复杂任务上更好地协作。

图 4.9 显示了嵌套群聊和协作群聊之间的差异。我们在上一节中使用了嵌套聊天功能来构建嵌套代理聊天。在本节中,我们使用群聊来提供更协作的体验。

图

图 4.9 可对话代理的嵌套群聊与群聊之间的差异

打开autogen_coding_group.py,查看列表 4.10 中的相关部分。代码与之前的练习类似,但现在引入了GroupChatGroupChatManager。代理和消息都存储在群聊中,类似于 Slack 或 Discord 等应用中的消息频道。聊天管理器协调消息响应以减少对话重叠。

列表 4.10 autoget_coding_group.py(相关部分)
user_proxy = UserProxyAgent(
    "user",
    code_execution_config={
        "work_dir": "working",
        "use_docker": False,
        "last_n_messages": 3,
    },
    human_input_mode="NEVER",     #1
)

llm_config = {"config_list": config_list}

engineer = AssistantAgent(…      #2

critic = AssistantAgent(…       #2

groupchat = GroupChat(agents=[user_proxy, 
                              engineer, 
                              critic], 
                              messages=[], 
                              max_round=20)      #3
manager = GroupChatManager(groupchat=groupchat, 
                           llm_config=llm_config)     #4

task = """Write a snake game using Pygame."""

with Cache.disk(cache_seed=43) as cache:
    res = user_proxy.initiate_chat(
        recipient=manager,
        message=task,
        cache=cache,
    )

1 人类输入现在设置为永不,因此没有人类反馈。

2 代码省略,但请参考文件中角色的更改

3 此对象持有所有代理的连接并存储消息。

4 管理员作为调解者一样协调对话。

运行这个练习,你会看到代理是如何协作的。工程师现在将从评论者那里获取反馈并执行操作以解决评论者的建议。这也允许代理参与所有对话。

群组对话是加强代理协作能力的好方法,因为它们在完成任务时进行协作。然而,它们也更为冗长且代价高昂。当然,随着大型语言模型(LLMs)的成熟,它们的上下文标记窗口的大小和标记处理的价格也在增加。随着标记窗口的增加,对标记消耗的担忧可能会最终消失。

AutoGen 是一个强大的多代理平台,可以通过网页界面或代码体验。无论你的偏好如何,这个代理协作工具是构建代码或其他复杂任务的优秀平台。当然,它并不是唯一的平台,正如你将在下一节中看到的,我们将探索一个新来者,即 CrewAI。

4.4 使用 CrewAI 构建代理团队

CrewAI 在多代理系统领域相对较新。AutoGen 最初是从研究中发展起来的,然后进行了扩展,而 CrewAI 是以企业系统为前提构建的。因此,该平台在某种程度上更健壮,但在某些方面不太容易扩展。

使用 CrewAI,你可以构建一个专注于特定任务目标的代理团队。与 AutoGen 不同,CrewAI 不需要使用用户代理代理,而是假设代理只在自己之间工作。

图 4.10 展示了 CrewAI 平台的主要元素,它们如何连接在一起以及它们的主要功能。它展示了一个具有通用研究人员和作家代理的顺序处理代理系统。代理被分配的任务可能包括工具或记忆来协助它们。

figure

图 4.10 CrewAI 系统的组成

CrewAI 支持两种主要的处理形式:顺序和分层。图 4.10 通过迭代给定的代理及其相关任务来展示顺序处理过程。在下一节中,我们将深入研究一些代码来设置团队并使用它来完成目标并创造一个有趣的笑话。

4.4.1 创建一个由 CrewAI 代理组成的玩笑者团队

与 AutoGen 相比,CrewAI 需要更多的设置,但这也允许更多的控制和额外的指南,这些指南提供了更具体的上下文来指导代理完成给定的任务。这并非没有问题,但它确实提供了比 AutoGen 更多的控制。

在 VS Code 中打开 crewai_introduction.py 并查看列表 4.11 所示的顶部部分。配置代理需要许多设置,包括角色、目标、冗长性、记忆、前传、委派,甚至工具(未显示)。在这个例子中,我们使用了两个代理:一个高级玩笑研究人员和一个玩笑作家。

列表 4.11 crewai_introduction.py(代理部分)
from crewai import Agent, Crew, Process, Task
from dotenv import load_dotenv

load_dotenv()

joke_researcher = Agent(      #1
    role="Senior Joke Researcher",
    goal="Research what makes things funny about the following {topic}",
    verbose=True,      #2
    memory=True,      #3
    backstory=(      #4
        "Driven by slapstick humor, you are a seasoned joke researcher"
        "who knows what makes people laugh. You have a knack for finding"
        "the funny in everyday situations and can turn a dull moment into"
        "a laugh riot."
    ),
    allow_delegation=True,     #5
)

joke_writer = Agent(     #6
    role="Joke Writer",
    goal="Write a humourous and funny joke on the following {topic}",
    verbose=True,     #7
    memory=True,      #8
    backstory=(     #9
        "You are a joke writer with a flair for humor. You can turn a"
        "simple idea into a laugh riot. You have a way with words and"
        "can make people laugh with just a few lines."
    ),
    allow_delegation=False,    #5
)

1 创建代理并为他们提供一个目标

2 verbose 允许代理向终端输出输出。

3 支持代理使用内存

4 前传是代理的背景——其角色。

5 代理可以被委派或允许委派;True 表示它们可以委派。

6 创建代理并为他们提供一个目标

7 verbose 允许代理向终端输出。

8 支持代理使用内存

9 前传故事是代理的背景——其角色。

在代码中向下移动,我们接下来看到的是任务,如列表 4.12 所示。任务表示代理完成主要系统目标的过程。它们还链接代理以执行特定任务,定义该任务的输出,并可能包括其执行方式。

列表 4.12 crewai_introduction.py(任务部分)
research_task = Task(         #1
    description=(
        "Identify what makes the following topic:{topic} so funny."
        "Be sure to include the key elements that make it humourous."
        "Also, provide an analysis of the current social trends,"
        "and how it impacts the perception of humor."
    ),
    expected_output="A comprehensive 3 paragraphs long report 
↪     on the latest jokes.",               #2
    agent=joke_researcher,      #3
)

write_task = Task(   #4
    description=(
        "Compose an insightful, humourous and socially aware joke on {topic}."
        "Be sure to include the key elements that make it funny and"
        "relevant to the current social trends."
    ),
    expected_output="A joke on {topic}.",   #5
    agent=joke_writer,        #3
    async_execution=False,          #6
    output_file="the_best_joke.md",      #7
)

1 任务描述定义了代理如何完成任务。

2 明确定义执行任务时的预期输出

3 被分配执行任务的代理

4 任务描述定义了代理如何完成任务。

5 明确定义执行任务时的预期输出

6 如果代理应该异步执行

7 代理将生成的任何输出

现在,我们可以看到所有内容是如何在文件底部的 Crew 中汇集起来的,如列表 4.13 所示。同样,在构建 Crew 时可以设置许多选项,包括代理、任务、进程类型、内存、缓存、每分钟最大请求数(max_rpm)以及机组人员是否共享。

列表 4.13 crewai_introduction.py(机组部分)
crew = Crew(
    agents=[joke_researcher, joke_writer],    #1
    tasks=[research_task, write_task],     #2
    process=Process.sequential,      #3
    memory=True,      #4
    cache=True,     #5
    max_rpm=100,     #6
    share_crew=True,     #7
)

result = crew.kickoff(inputs={"topic": "AI engineer jokes"})
print(result)

1 组装进机组的人员

2 代理可以执行的任务

3 定义代理如何交互

4 是否应该使用内存;如果代理/任务有,则需要设置

5 系统是否应该使用缓存,类似于 AutoGen

6 每分钟系统应限制的最大请求数

7 是否应该让机组人员共享信息,类似于群聊

当你完成审查后,在 VS Code 中运行文件(F5),并观察终端中的机组人员对话和消息。正如你现在可能已经察觉到的,这个代理系统的目标是制作与人工智能工程相关的笑话。以下是几次运行代理系统生成的几个有趣的笑话:

  • 为什么电脑会冷?因为它留下了 Windows。

  • 为什么人工智能工程师不和他们自己的算法玩捉迷藏?因为无论他们藏在哪里,算法总是在“过拟合”房间里找到他们!

  • 人工智能工程师最喜欢的歌曲是什么?“我刚刚打电话来说我爱你……并且为了收集更多数据用于我的语音识别软件。”

  • 为什么人工智能工程师会破产?因为他把所有的钱都花在了饼干上,但他的浏览器一直在吃它们。

在运行更多笑话机组迭代之前,你应该阅读下一节。本节展示了如何向多代理系统添加可观察性。

4.4.2 通过 AgentOps 观察代理的工作

观察一个复杂集合,如多代理系统,对于理解可能发生的各种问题至关重要。通过应用程序跟踪进行可观察性是任何复杂系统的关键元素,尤其是那些在企业中使用的系统。

CrewAI 支持连接到一个专门称为 AgentOps 的操作平台。这个可观察性平台是通用的,旨在支持任何特定于 LLM 使用的代理平台的可观察性。目前,没有提供定价或商业化细节。

连接到 AgentOps 就像安装包、获取 API 密钥以及在团队设置中添加一行代码一样简单。接下来的练习将介绍连接和运行 AgentOps 的步骤。

列表 4.14 展示了使用pip安装agentops包的过程。你可以单独安装该包,或者将其作为crewai包的附加组件安装。记住,AgentOps 也可以连接到其他代理平台以实现可观察性。

列表 4.14 安装 AgentOps
pip install agentops

or as an option with CrewAI

pip install crewai[agentops]

在使用 AgentOps 之前,你需要注册一个 API 密钥。以下是在撰写本文时注册密钥的一般步骤:

  1. 在你的浏览器中访问app.agentops.ai

  2. 注册一个账户。

  3. 创建一个项目,或使用默认设置。

  4. 前往设置 > 项目和 API 密钥。

  5. 复制或生成一个新的 API 密钥;这将把密钥复制到你的浏览器中。

  6. 将密钥粘贴到你的项目中的.env文件。

在复制 API 密钥后,它应该类似于以下列表中所示示例。

列表 4.15 env.:添加 AgentOps 密钥
AGENTOPS_API_KEY="your API key"

现在,我们需要在 CrewAI 脚本中添加几行代码。列表 4.16 展示了添加到crewai_agentops.py文件中的内容。当你创建自己的脚本时,你所需要做的就是添加agentops包,并在使用 CrewAI 时初始化它。

列表 4.16 crewai_agentops.py(AgentOps 添加)
import agentops      #1
from crewai import Agent, Crew, Process, Task
from dotenv import load_dotenv

load_dotenv()
agentops.init()     #2

1 添加所需的包

2 确保在加载环境变量后初始化包。

在 VS Code 中运行crewai_agentops.py文件(按 F5),并观察代理像以前一样工作。然而,你现在可以进入 AgentOps 仪表板并查看不同级别的代理交互。

图 4.11 展示了运行玩笑团队以创建最佳玩笑的仪表板。包括总时长、运行环境、提示和完成令牌、LLM 调用时间以及预估成本等统计数据。看到成本既可能让人清醒,也可能表明代理对话可能多么冗长。

图

图 4.11 运行玩笑团队的 AgentOps 仪表板

AgentOps 平台是任何代理平台的绝佳补充。虽然它内置在 CrewAI 中,但将可观察性添加到 AutoGen 或其他框架中也很有帮助。AgentOps 的另一个吸引人的特点是它专注于观察代理交互,而不是从机器学习操作平台转换而来。未来,我们可能会看到更多代理可观察性模式的诞生。

一个不容忽视的益处是,可观察性平台可以提供的成本观察。你注意到图 4.11 中创建一个笑话的成本略超过 50 美分吗?代理可以非常强大,但它们也可能变得非常昂贵,因此观察这些成本在实用性和商业化方面至关重要。

在本章的最后部分,我们将回到 CrewAI,重新探讨构建能够编写游戏的代理。这将提供 AutoGen 和 CrewAI 功能之间的一次优秀比较。

4.5 使用 CrewAI 重新探讨编码代理

比较多代理平台功能的一种好方法是在机器人中实现类似任务。在接下来的练习中,我们将使用 CrewAI 作为游戏编程团队。当然,这也可以适应其他编码任务。

在 VS Code 中打开 crewai_coding_crew.py,我们首先将回顾列表 4.17 中的代理部分。在这里,我们创建了一个高级工程师、一个 QA 工程师和一位首席 QA 工程师,并为他们分配了角色、目标和背景故事。

列表 4.17 crewai_coding_crew.py (代理部分)
print("## Welcome to the Game Crew")      #1
print("-------------------------------")
game = input("What is the game you would like to build?
↪ What will be the mechanics?\n")

senior_engineer_agent = Agent(
    role="Senior Software Engineer",
    goal="Create software as needed",
    backstory=dedent(
        """
        You are a Senior Software Engineer at a leading tech think tank.
        Your expertise in programming in python. and do your best to
        produce perfect code
        """
    ),
    allow_delegation=False,
    verbose=True,
)

qa_engineer_agent = Agent(
    role="Software Quality Control Engineer",
    goal="create prefect code, by analizing the code 
↪ that is given for errors",
    backstory=dedent(
        """
        You are a software engineer that specializes in checking code
        for errors. You have an eye for detail and a knack for finding
        hidden bugs.
        You check for missing imports, variable declarations, mismatched
        brackets and syntax errors.
        You also check for security vulnerabilities, and logic errors
        """
    ),
    allow_delegation=False,
    verbose=True,
)

chief_qa_engineer_agent = Agent(
    role="Chief Software Quality Control Engineer",
    goal="Ensure that the code does the job that it is supposed to do",
    backstory=dedent(
        """
        You are a Chief Software Quality Control Engineer at a leading
        tech think tank. You are responsible for ensuring that the code
        that is written does the job that it is supposed to do.
        You are responsible for checking the code for errors and ensuring
        that it is of the highest quality.
        """
    ),
    allow_delegation=True,     #2
    verbose=True,
)

1 允许用户输入他们游戏的指令

2 只有首席 QA 工程师可以委派任务。

在文件中向下滚动将显示代理任务,如图 4.18 列表所示。任务描述和预期输出应该很容易理解。同样,每个代理都有一个特定的任务,以便在工作时提供更好的上下文。

列表 4.18 crewai_coding_crew.py (任务部分)
code_task = Task(
    description=f"""
You will create a game using python, these are the instructions:
        Instructions
        ------------
        {game}             #1
        You will write the code for the game using python.""",
    expected_output="Your Final answer must be the 
↪ full python code, only the python code and nothing else.",
    agent=senior_engineer_agent,
)

qa_task = Task(
    description=f"""You are helping create a game 
↪ using python, these are the instructions:
        Instructions
        ------------
        {game}            #1
        Using the code you got, check for errors. Check for logic errors,
        syntax errors, missing imports, variable declarations, 
mismatched brackets,
        and security vulnerabilities.""",
    expected_output="Output a list of issues you found in the code.",
    agent=qa_engineer_agent,
)

evaluate_task = Task(
    description=f"""You are helping create a game 
↪ using python, these are the instructions:
        Instructions
        ------------
        {game}            #1
        You will look over the code to insure that it is complete and
        does the job that it is supposed to do. """,
    expected_output="Your Final answer must be the 
↪ corrected a full python code, only the python code and nothing else.",
    agent=chief_qa_engineer_agent,
)

1 使用 Python 格式化将游戏指令替换到提示中。

最后,我们可以通过查看文件底部,如图 4.19 列表所示,来了解这一切是如何结合在一起的。这个队伍配置与我们之前看到的大致相同。每个代理和任务都被添加,以及详尽和过程属性。对于这个例子,我们将继续使用顺序方法。

列表 4.19 crewai_coding_crew.py (队伍部分)
crew = Crew(
    agents=[senior_engineer_agent, 
            qa_engineer_agent, 
            chief_qa_engineer_agent],
    tasks=[code_task, qa_task, evaluate_task],
    verbose=2,  
    process=Process.sequential,      #1
)

# Get your crew to work!
result = crew.kickoff()    #2

print("######################")
print(result)

1 处理过程是顺序的。

2 启动阶段没有提供额外的上下文。

当你运行 VS Code (F5) 文件时,系统会提示你输入编写游戏的指令。输入一些指令,比如蛇形游戏或你选择的另一个游戏。然后,让代理工作,并观察它们产生的结果。

随着首席 QA 工程师的加入,结果通常会优于使用 AutoGen 产生的结果,至少在初始阶段是这样。如果你审查代码,你会发现它通常遵循良好的模式,在某些情况下,甚至可能包括测试和单元测试。

在我们完成本章之前,我们将对队伍的处理模式进行最后一次修改。之前,我们采用了顺序处理,如图 4.10 所示。图 4.12 展示了在 CrewAI 中层级处理的样子。

图

图 4.12 通过队伍管理器协调的代理的层级处理

添加这个经理是一个相对简单的过程。列表 4.20 显示了将编码船员以分层方法使用的新文件中的附加代码更改。除了从 LangChain 导入连接到 OpenAI 的类之外,另一个添加是将其作为船员经理manager_llm

列表 4.20 crewai_hierarchy.py(船员经理部分)
from langchain_openai import ChatOpenAI      #1

crew = Crew(
    agents=[senior_engineer_agent, 
            qa_engineer_agent, 
            chief_qa_engineer_agent],
    tasks=[code_task, qa_task, evaluate_task],
    verbose=2,  
    process=Process.hierarchical,     #2
    manager_llm=ChatOpenAI(               #3
        temperature=0, model="gpt-4"      #3
    ),    #4
)         #4

1 从 LangChain 导入 LLM 连接器

2 在选择分层处理时,您必须设置一个船员经理。

3 将船员经理设置为 LLM 连接器

4 在选择分层处理时,您必须设置一个船员经理。

在 VS Code 中运行此文件(按 F5)。当提示时,输入您想要创建的游戏。尝试使用与 AutoGen 相同的游戏;蛇游戏也是一个很好的基线示例。观察代理如何通过代码并反复检查问题。

运行文件后,您还可以在 AgentOps 中查看此次运行的成本。很可能会超过没有代理管理器时的两倍。输出效果也可能不会显著提高。这就是在不了解事情如何迅速失控的情况下构建代理系统所陷入的陷阱。

当代理不断重复相同的动作时,经常发生的这种螺旋效应的一个例子是频繁重复的任务。您可以通过查看重复思考图来查看这个问题,如图 4.13 所示。

figure

图 4.13 代理运行中发生的思维重复

AgentOps 中的重复思考图是衡量您的代理系统遇到重复情况的一个绝佳方式。过于重复的思维模式通常意味着代理不够果断,而是在不断尝试生成不同的答案。如果您遇到这个问题,您可能需要改变代理的处理模式、任务和目标。您甚至可能想要改变系统的类型和代理的数量。

多代理系统是按工作模式和任务分配工作的一种极好方式。通常,工作角色分配给代理角色/人物,它需要完成的任务可能是隐性的,如 AutoGen 中所示,或者更明确,如 CrewAI 中所示。

在本章中,我们介绍了许多您可以直接使用的有用工具和平台,以改善您的工作、生活等。这完成了我们对多代理平台的探索之旅,但我们的探索和使用多代理的工作并未结束,正如我们将在后面的章节中发现的。

4.6 练习

使用以下练习来提高您对材料的了解:

  • 练习 1 — 使用 AutoGen 进行基本代理通信

目标 — 在 AutoGen 中熟悉基本的代理通信和设置。

任务

    • 按照本章提供的说明,在您的本地机器上设置 AutoGen Studio。

    • 创建一个简单的多代理系统,包含一个用户代理和两个助手代理。

    • 实现一个基本任务,其中用户代理在助手代理之间协调以生成简单的文本输出,例如总结一段简短的段落。

  • 练习 2 — 在 AutoGen Studio 中实现高级代理技能

目标 — 通过添加高级技能增强代理能力。

任务:

    • 开发并集成一项新技能到 AutoGen 代理中,使其能够从公共 API(例如,天气信息或股票价格)获取并显示实时数据。

    • 确保代理可以询问用户偏好(例如,天气的城市,股票类型)并相应地显示获取的数据。

  • 练习 3 — 使用 CrewAI 进行基于角色的任务管理

目标 — 探索 CrewAI 中的基于角色的任务管理。

任务:

    • 设计一个 CrewAI 设置,其中多个代理被分配特定的角色(例如,数据收集器、分析器、报告生成器)。

    • 配置一个任务序列,其中数据收集器收集数据,分析器处理数据,报告生成器生成报告。

    • 执行序列并观察代理之间信息流和任务委派的流程。

  • 练习 4 — 使用 AutoGen 在群聊中进行多代理协作

目标 — 理解和实现 AutoGen 中的群聊系统以促进代理协作。

任务:

    • 设置一个场景,其中多个代理需要协作解决一个复杂问题(例如,为商务旅行规划行程)。

    • 使用群聊功能允许代理之间共享信息、提问和提供更新。

    • 监控代理在协作问题解决中的交互和有效性。

  • 练习 5 — 在 CrewAI 中使用 AgentOps 添加和测试可观察性

目标 — 在 CrewAI 环境中使用 AgentOps 实现和评估代理的可观察性。

任务:

    • 将 AgentOps 集成到 CrewAI 多代理系统中。

    • 设计一个涉及大量计算或数据处理(例如,分析客户评论以确定情感趋势)的任务。

    • 使用 AgentOps 监控代理的性能、成本和输出准确性。识别任何潜在的交互不效率或错误。

摘要

  • 由微软开发的 AutoGen 是一个对话式多代理平台,它采用多种代理类型,如用户代理和助手代理,通过自然语言交互促进任务执行。

  • AutoGen Studio 作为一个开发环境,允许用户创建、测试和管理多代理系统,提高了 AutoGen 的可用性。

  • AutoGen 支持多种通信模式,包括群聊、层次化和代理通信。代理通信涉及一个主要代理(代理),它在用户和其他代理之间进行接口以简化任务完成。

  • CrewAI 提供了一种构建多代理系统的结构化方法,重点在于企业应用。它强调基于角色和自主代理的功能,允许灵活的、顺序的或层次化的任务管理。

  • 本章中的实际练习说明了如何设置和使用 AutoGen Studio,包括安装必要的组件和运行基本的多人智能体系统。

  • AutoGen 中的智能体可以配备特定的技能来执行代码生成、图像分析和数据检索等任务,从而扩大其应用范围。

  • CrewAI 凭借其能够比 AutoGen 更严格地结构化智能体交互的能力而脱颖而出,这在需要精确和可控智能体行为的场景中可能具有优势。

  • CrewAI 支持将记忆和工具集成到智能体中,以便通过任务完成来消费。

  • CrewAI 支持与可观察性工具(如 AgentOps)集成,这些工具可以提供关于智能体性能、交互效率和成本管理的见解。

  • AgentOps 是一个智能体可观察性平台,可以帮助您轻松监控广泛的智能体交互。

第五章:赋能动作代理

本章涵盖

  • 代理如何使用动作在自身之外进行行动

  • 定义和使用 OpenAI 函数

  • 语义内核及其使用语义函数的方法

  • 协同语义和本地函数

  • 使用语义内核实例化 GPT 接口

在本章中,我们通过函数的使用来探讨动作,以及代理如何使用它们。我们将首先查看 OpenAI 函数调用,然后迅速转向微软的另一个项目,称为语义内核(SK),我们将使用它来构建和管理代理或作为代理的技能和函数。

我们将使用 SK 来完成本章,以托管我们的第一个代理系统。这将是一个包含大量注释代码示例的完整章节。

5.1 定义代理动作

ChatGPT 插件首次推出是为了提供具有能力、技能或工具的会话。有了插件,你可以搜索网络或创建电子表格或图表。插件为 ChatGPT 提供了扩展平台的方式。

图 5.1 展示了 ChatGPT 插件的工作方式。在这个例子中,ChatGPT 中安装了一个新的电影推荐插件。当用户要求 ChatGPT 推荐一部新电影时,大型语言模型(LLM)识别出它有一个用于管理该动作的插件。然后,它将用户请求分解为可操作的参数,并将这些参数传递给新的电影推荐器。

图

图 5.1 ChatGPT 插件的工作方式以及插件和其他外部工具(例如,API)如何与“使用外部工具”提示工程策略相一致

推荐器随后抓取展示新电影的网站,并将该信息附加到对 LLM 的新提示请求中。有了这些信息,LLM 会回应推荐器,推荐器将此信息传递回 ChatGPT。然后 ChatGPT 对用户做出推荐请求的回应。

我们可以将插件视为动作的代理。插件通常封装了一个或多个能力,例如调用 API 或抓取网站。因此,动作是插件的扩展——它们赋予了插件能力。

AI 代理可以被视为插件和插件的消费者、工具、技能和其他代理。向代理/插件添加技能、函数和工具允许它执行定义良好的动作——图 5.2 强调了代理动作发生的位置及其与 LLM 和其他系统的交互。

图

图 5.2 代理如何使用动作执行外部任务

代理动作是允许它使用函数、技能或工具的能力。令人困惑的是,不同的框架使用不同的术语。我们将定义动作为代理可以执行以建立一些基本定义的任何操作。

ChatGPT 插件和函数代表了 ChatGPT 或代理系统可以用来执行额外动作的可执行能力。现在让我们来检查 OpenAI 插件和函数定义的基础。

5.2 执行 OpenAI 函数

OpenAI 通过启用插件,引入了一种结构规范,用于定义函数/插件与 LLM 之间的接口。这个规范正在成为 LLM 系统可以遵循的标准,以提供可执行的系统。

这些相同的函数定义现在也被用来定义 ChatGPT 和其他系统的插件。接下来,我们将探讨如何直接使用 LLM 调用来使用函数。

5.2.1 将函数添加到 LLM API 调用中

图 5.3 展示了 LLM 如何识别和使用函数定义来将其响应作为函数调用。

图

图 5.3 如何解释单个 LLM 请求,包括工具

列表 5.1 显示了使用工具和函数定义进行 LLM API 调用的详细信息。添加函数定义允许 LLM 回复有关函数的输入参数。这意味着 LLM 将识别正确的函数并解析用户请求的相关参数。

列表 5.1 first_function.py (API 调用)
response = client.chat.completions.create(
        model="gpt-4-1106-preview",
        messages=[{"role": "system",
                   "content": "You are a helpful assistant."},
                  {"role": "user", "content": user_message}],
        temperature=0.7,
        tools=[     #1
            {
                "type": "function",     #2
                "function": {
                    "name": "recommend",
                    "description": "Provide a … topic.",     #3
                    "parameters": {
                        "type": "object",     #4
                        "properties": {
                            "topic": {
                                "type": "string",
                                "description": 
                                   "The topic,… for.",     #5
                            },
                            "rating": {
                                "type": "string",
                                "description": 
                          "The rating … given.",    #5
                                "enum": ["good",
                                         "bad", 
                                         "terrible"]     #6
                                },
                        },
                        "required": ["topic"],
                    },
                },
                }
            ]
        )

1 新参数称为 tools

2 设置工具的类型以执行功能

3 提供了关于函数功能的出色描述

4 定义输入参数的类型;一个对象代表一个 JSON 文档。

5 为每个输入参数提供出色的描述

6 您甚至可以用枚举的方式来描述。

要了解这是如何工作的,请打开 Visual Studio Code (VS Code) 到书籍的源代码文件夹:chapter_4/first_function.py。在 VS Code 中打开相关章节文件夹以创建一个新的 Python 环境并安装 requirements.txt 文件是一个好习惯。如果您需要这方面的帮助,请参阅附录 B。

在开始之前,请正确设置 chapter_4 文件夹中的 .env 文件,并包含您的 API 凭据。函数调用是 LLM 商业服务提供的额外功能。在撰写本文时,此功能不是开源 LLM 部署的选项。

接下来,我们将查看 first_function.py 代码的底部,如列表 5.2 所示。这里只是使用列表 5.1 中指定的先前请求对 LLM 进行调用的两个示例。在这里,每个请求都显示了运行示例生成的输出。

列表 5.2 first_function.py (练习 API)
user = "Can you please recommend me a time travel movie?"
response = ask_chatgpt(user)     #1
print(response)

**###Output**
Function(arguments='{"topic":"time travel movie"}', 
                      name='recommend')     #2

user = "Can you please recommend me a good time travel movie?"
response = ask_chatgpt(user)     #3
print(response)

**###Output**
Function(arguments='{"topic":"time travel movie",
                     "rating":"good"}',
 name='recommend')     #4

1 之前定义的函数

2 返回要调用的函数名称和提取的输入参数

3 之前定义的函数

4 返回要调用的函数名称和提取的输入参数

使用调试器(F5)或终端在 VS Code 中运行 first_function.py Python 脚本,以查看相同的结果。在这里,LLM 解析输入请求以匹配任何已注册的工具。在这种情况下,工具是单个函数定义,即推荐的函数。LLM 从该函数中提取输入参数,并从请求中解析这些参数。然后,它以命名函数和指定的输入参数的形式回复。

备注:实际函数并未被调用。LLM 只返回建议的函数和相关的输入参数。必须提取名称和参数,并将它们传递给一个与签名匹配的函数以执行该函数。我们将在下一节中查看一个示例。

5.2.2 执行函数调用

现在我们已经明白 LLM 不会直接执行函数或插件,我们可以看看一个执行工具的示例。继续遵循推荐主题,我们将看看另一个添加 Python 函数以进行简单推荐的示例。

图 5.4 展示了这个简单示例的工作原理。我们将提交一个包含工具函数定义的单个请求,请求三个推荐。LLM 随后将回复三个带有输入参数的函数调用(时间旅行、食谱和礼物)。执行函数的结果随后被传递回 LLM,它将它们转换回自然语言并返回一个回复。

图

图 5.4 一个示例请求返回三个工具函数调用,然后将结果提交回 LLM 以返回一个自然语言回复。

现在我们已经理解了这个示例,请在 VS Code 中打开parallel_functions.py。列表 5.3 显示了您想要调用的 Python 函数,以提供推荐。

列表 5.3 parallel_functions.py (推荐函数)
def recommend(topic, rating="good"):
    if "time travel" in topic.lower():     #1
        return json.dumps({"topic": "time travel",
                           "recommendation": "Back to the Future",
                           "rating": rating})
    elif "recipe" in topic.lower():    #1
        return json.dumps({"topic": "recipe",
                           "recommendation": "The best thing … ate.",
                           "rating": rating})
    elif "gift" in topic.lower():      #1
        return json.dumps({"topic": "gift",
                           "recommendation": "A glorious new...",
                           "rating": rating})
    else:     #2
        return json.dumps({"topic": topic,
                           "recommendation": "unknown"})     #3

1 检查字符串是否包含在主题输入中

2 如果未检测到主题,则返回默认值

3 返回一个 JSON 对象

接下来,我们将查看名为run_conversation的函数,所有的工作都是从请求构建开始的。

列表 5.4 parallel_functions.py (run_conversation, request)
user = """Can you please make recommendations for the following:
1\. Time travel movies
2\. Recipes
3\. Gifts"""     #1
messages = [{"role": "user", "content": user}]     #2
tools = [     #3
    {
        "type": "function",
        "function": {
            "name": "recommend",
            "description": 
                "Provide a recommendation for any topic.",
            "parameters": {
                "type": "object",
                "properties": {
                    "topic": {
                        "type": "string",
                        "description": 
                              "The topic, … recommendation for.",
                        },
                        "rating": {
                            "type": "string",
                            "description": "The rating … was given.",
                            "enum": ["good", "bad", "terrible"]
                            },
                        },
                "required": ["topic"],
                },
            },
        }
    ]

1 用户消息请求三个推荐。

2 注意没有系统消息。

3 将函数定义添加到请求的工具部分

列表 5.5 显示了请求的执行,我们之前已经讨论过,但有一些需要注意的事项。这个调用使用了一个较低模型,如 GPT-3.5,因为委托函数是一个更直接的任务,可以使用较旧、较便宜、不太复杂的语言模型来完成。

列表 5.5 parallel_functions.py (run_conversation, API 调用)
response = client.chat.completions.create(
    model="gpt-3.5-turbo-1106",     #1
    messages=messages,     #2
    tools=tools,     #2
    tool_choice="auto",   #3
)
response_message = response.choices[0].message     #4

1 委托给函数的 LLM 可以是更简单的模型。

2 添加消息和工具定义

3 auto 是默认值。

4 LLM 返回的消息

在这个阶段,API 调用之后,响应应该包含所需函数调用的信息。记住,我们要求 LLM 提供三个推荐,这意味着它也应该提供三个函数调用输出,如下所示。

列表 5.6 parallel_functions.py (run_conversation, tool_calls)
tool_calls = response_message.tool_calls     #1
if tool_calls:    #1
    available_functions = {
        "recommend": recommend,
    }     #2
    # Step 4: send the info for each function call and function response to 
the model
    for tool_call in tool_calls:     #3
        function_name = tool_call.function.name
        function_to_call = available_functions[function_name]
        function_args = json.loads(tool_call.function.arguments)
        function_response = function_to_call(
            topic=function_args.get("topic"),     #4
            rating=function_args.get("rating"),
        )
        messages.append(     #5
            {
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": function_name,
                "content": function_response,
            }
        )  # extend conversation with function response
    second_response = client.chat.completions.create(     #6
        model="gpt-3.5-turbo-1106",
        messages=messages,
    )
    return second_response.choices[0].message.content  #6

1 如果响应包含工具调用,则执行它们。

2 只有一个函数,但可能包含多个

3 遍历调用并将内容回放给 LLM

4 从提取的参数中执行推荐函数

5 将每个函数调用的结果附加到消息集合中

6 使用更新后的信息向 LLM 发送另一个请求,并返回消息回复

工具调用输出和推荐函数调用的结果被附加到消息中。注意现在消息还包含了第一次调用的历史。然后这些信息被传递回 LLM 以构建自然语言回复。

在 VS Code 中通过按 F5 键并打开文件来调试此示例。以下列表显示了运行parallel_functions.py的输出。

列表 5.7 parallel_functions.py(输出)
Here are some recommendations for you:

1\. Time travel movies: "Back to the Future"
2\. Recipes: "The best thing you ever ate."
3\. Gifts: "A glorious new..." (the recommendation was cut off, so I 
couldn't provide the full recommendation)

I hope you find these recommendations helpful! Let me know if you need 
more information.

这完成了这个简单的演示。对于更高级的应用,函数可以执行各种任务,从抓取网站到调用搜索引擎,甚至完成更复杂的任务。

函数是针对特定任务输出的一种优秀方式。然而,处理函数或工具以及进行二次调用的任务可以通过更干净、更高效的方式进行。下一节将揭示为代理添加动作的更稳健的系统。

5.3 介绍语义内核

语义内核(SK)是微软的另一个开源项目,旨在帮助构建 AI 应用,我们称之为代理。在其核心,该项目最好用于定义动作,或者平台所说的语义插件,它们是技能和函数的包装器。

图 5.5 展示了如何将 SK 用作插件以及 OpenAI 插件的消费者。SK 依赖于 OpenAI 插件定义来定义插件。这样,它可以消费并发布自身或其他插件到其他系统。

图

图 5.5 语义内核如何作为一个插件集成,并且也可以消费插件

OpenAI 插件定义精确映射到列表 5.4 中的函数定义。这意味着 SK 是 API 工具调用(即插件)的协调者。这也意味着 SK 可以帮助通过聊天界面或代理组织多个插件。

注意:SK 团队最初将功能模块标记为技能。然而,为了与 OpenAI 保持一致,他们已经将技能重命名为插件。更令人困惑的是,代码仍然使用术语技能。因此,在本章中,我们将使用技能插件来表示同一概念。

SK 是管理多个插件(代理的动作)的有用工具,正如我们稍后将会看到的,它还可以帮助记忆和规划工具。对于本章,我们将专注于动作/插件。在下一节中,我们将探讨如何开始使用 SK。

5.3.1 开始使用 SK 语义函数

SK 易于安装,并在 Python、Java 和 C#中工作。这是一个好消息,因为它还允许在一个语言中开发的插件在另一种语言中被消费。然而,你目前还不能在一个语言中开发原生函数并在另一个语言中使用它。

我们将继续使用 VS Code 中的chapter_4工作空间来处理 Python 环境。如果你想要探索和运行任何示例,请确保你已经配置了一个工作空间。

列表 5.8 展示了如何在 VS Code 的终端中安装 SK。你也可以安装 SK 的 VS Code 扩展。这个扩展可以是一个创建插件/技能的有用工具,但不是必需的。

列表 5.8 安装语义内核
pip uninstall semantic-kernel     #1

git clone https://github.com/microsoft/semantic-kernel.git     #2

cd semantic-kernel/python     #3

pip install -e .     #4

1 卸载 SK 的任何先前安装

2 将仓库克隆到本地文件夹

3 对源文件夹的更改

4 从源文件夹安装可编辑的包

安装完成后,打开SK_connecting.py。列表 5.9 展示了通过 SK 快速运行示例的演示。该示例使用 OpenAI 或 Azure OpenAI 创建聊天完成服务。

列表 5.9 SK_connecting.py
import semantic_kernel as sk

selected_service = "OpenAI"     #1
kernel = sk.Kernel()     #2

service_id = None
if selected_service == "OpenAI":
    from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion

    api_key, org_id = sk.openai_settings_from_dot_env()     #3
    service_id = "oai_chat_gpt"
    kernel.add_service(
        OpenAIChatCompletion(
            service_id=service_id,
            ai_model_id="gpt-3.5-turbo-1106",
            api_key=api_key,
            org_id=org_id,
        ),
    )
elif selected_service == "AzureOpenAI":
    from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion

    deployment, api_key, endpoint = 
↪ sk.azure_openai_settings_from_dot_env()   #4
    service_id = "aoai_chat_completion"
    kernel.add_service(
        AzureChatCompletion(
            service_id=service_id,
            deployment_name=deployment,
            endpoint=endpoint,
            api_key=api_key,
        ),
    )

#This function is currently broken
async def run_prompt():
    result = await kernel.invoke_prompt( 
              ↪ prompt="recommend a movie about 
↪ time travel")     #5
    print(result)

# Use asyncio.run to execute the async function
asyncio.run(run_prompt())     #6

###Output
One highly recommended time travel movie is "Back to the Future" (1985) 
directed by Robert Zemeckis. This classic film follows the adventures of 
teenager Marty McFly (Michael J. Fox)…

1 设置你使用的服务(OpenAI 或 Azure OpenAI)

2 创建内核

3 从 .env 文件加载机密信息并将其设置在聊天服务上

4 从 .env 文件加载机密信息并将其设置在聊天服务上

5 调用提示

6 异步调用函数

通过按 F5(调试)运行示例,你应该会看到一个类似于列表 5.9 的输出。这个示例演示了如何使用 SK 创建并执行语义函数。语义函数相当于提示流中的提示模板,这是另一个微软工具。在这个例子中,我们定义一个简单的提示作为一个函数。

需要注意的是,这个语义函数并不是定义为插件。然而,内核可以创建一个自包含的语义元素,该元素可以针对一个 LLM 执行。语义函数可以单独使用或注册为插件,正如你稍后将会看到的。让我们跳到下一节,我们将介绍上下文变量。

5.3.2 语义函数和上下文变量

在上一个示例的基础上,我们可以看看如何向语义函数添加上下文变量。这种在提示模板中添加占位符的模式是我们将反复回顾的。在这个例子中,我们查看一个具有主题、类型、格式和自定义占位符的提示模板。

在 VS Code 中打开SK_context_variables.py,如下一个列表所示。提示相当于设置提示的systemuser部分。

列表 5.10 SK_context_variables.py
#top section omitted…
prompt = """     #1
system:

You have vast knowledge of everything and can recommend anything provided 
you are given the following criteria, the subject, genre, format and any 
other custom information.

user:
Please recommend a {{$format}} with the subject {{$subject}} and {{$genre}}.
Include the following custom information: {{$custom}}
"""

prompt_template_config = sk.PromptTemplateConfig(     #2
    template=prompt,
    name="tldr",
    template_format="semantic-kernel",
    input_variables=[
        InputVariable(
            name="format", 
            description="The format to recommend", 
            is_required=True
        ),
        InputVariable(
            name="suject", 
            description="The subject to recommend", 
            is_required=True
        ),
        InputVariable(
            name="genre", 
            description="The genre to recommend", 
            is_required=True
        ),
        InputVariable(
            name="custom",
            description="Any custom information [CA]
                       to enhance the recommendation",
            is_required=True,
        ),
    ],
    execution_settings=execution_settings,
)

recommend_function = kernel.create_function_from_prompt(     #3
    prompt_template_config=prompt_template_config,
    function_name="Recommend_Movies",
    plugin_name="Recommendation",
)

async def run_recommendation(     #4
    subject="time travel",
    format="movie", 
    genre="medieval", 
           custom="must be a comedy"
):
    recommendation = await kernel.invoke(
        recommend_function,
        sk.KernelArguments(subject=subject,
                      format=format, 
                      genre=genre, 
                      custom=custom),     #5
    )
    print(recommendation)

# Use asyncio.run to execute the async function
asyncio.run(run_recommendation())    #5

###Output
One movie that fits the criteria of being about time travel, set in a 
medieval period, and being a comedy is "The Visitors" (Les Visiteurs) 
from 1993\. This French film, directed by Jean-Marie Poiré, follows a 
knight and his squire who are transported to the modern era by a 
wizard’s spell gone wrong.…

1 定义一个带有占位符的提示

2 配置提示模板和输入变量定义

3 从提示创建内核函数

4 创建一个异步函数来包装函数调用

5 设置内核函数参数

尝试调试这个示例(F5),等待输出生成。这是设置 SK 和创建及练习语义函数的基础。在下一节中,我们将继续了解如何将语义函数注册为技能/插件。

5.4 语义函数和本地函数的协同作用

语义函数封装了一个提示/配置文件并通过与 LLM 的交互来执行。原生函数是封装了可能从抓取网站到搜索网络的任何操作的代码。语义和原生函数都可以在 SK 内核中注册为插件/技能。

一个函数,无论是语义还是原生,都可以注册为插件,并像我们直接通过 API 调用注册早期函数一样使用。当一个函数注册为插件时,它将根据用例对聊天或代理接口变得可访问。下一节将探讨如何创建和注册语义函数与内核。

5.4.1 创建和注册语义技能/插件

SK 的 VS Code 扩展提供了创建插件/技能的有用工具。在本节中,我们将使用 SK 扩展创建一个插件/技能,然后编辑该扩展的组件。之后,我们将在 SK 中注册并执行该插件。

图 5.6 展示了在 VS Code 中使用 SK 扩展创建新技能的过程。(如果您需要安装此扩展,请参阅附录 B 的说明。)然后,您将获得将函数放置在技能/插件文件夹中的选项。始终将相似的功能分组在一起。创建技能后,输入您想要开发的函数的名称和描述。务必像 LLM 将要使用它一样描述该函数。

figure

图 5.6 创建新技能/插件的过程

您可以通过打开 skills/plugin 文件夹并查看文件来查看已完成的技能和函数。我们将遵循之前构建的示例,因此打开 skills/Recommender/Recommend_Movies 文件夹,如图 5.7 所示。在这个文件夹中有一个 config.json 文件,函数描述,以及一个名为 skprompt.txt 的语义函数/提示文件。

figure

图 5.7 语义函数技能/插件的文件和文件夹结构

列表 5.11 展示了语义函数定义的内容,也称为插件定义。请注意,类型被标记为 completion 而不是 function 类型,因为这是一个语义函数。我们将定义原生函数为类型函数。

列表 5.11 Recommend_Movies/config.json
{
    "schema": 1,
    "type": "completion",     #1
    "description": "A function to recommend movies based on users list of 
previously seen movies.",
    "completion": {     #2
        "max_tokens": 256,
        "temperature": 0,
        "top_p": 0,
        "presence_penalty": 0,
        "frequency_penalty": 0
    },
    "input": {
        "parameters": [
            {
                "name": "input",     #3
                "description": "The users list of previously seen movies.",
                "defaultValue": ""
            }
        ]
    },
    "default_backends": []
}

1 语义函数是完成类型的函数。

2 我们还可以设置函数调用的完成参数。

3 定义了输入到语义函数的参数

接下来,我们可以查看语义函数提示的定义,如图表 5.12 所示。格式略有不同,但我们在这里看到的内容与之前使用模板的示例相匹配。此提示基于用户之前看过的电影列表推荐电影。

列表 5.12 Recommend_Movies/skprompt.txt
You are a wise movie recommender and you have been asked to recommend a 
movie to a user.
You are provided a list of movies that the user has watched before.
You want to recommend a movie that the user has not watched before.
[INPUT]
{{$input}}
[END INPUT]

现在,我们将深入探讨加载技能/插件并在简单示例中执行它的代码。在 VS Code 中打开 SK_first_skill.py 文件。以下列表显示了一个突出显示新部分的缩略版本。

列表 5.13 SK_first_skill.py(缩略列表)
kernel = sk.Kernel()

plugins_directory = "plugins"

recommender = kernel.import_plugin_from_prompt_directory(
    plugins_directory,
    "Recommender",
)     #1

recommend = recommender["Recommend_Movies"]

seen_movie_list = [     #2
    "Back to the Future",
    "The Terminator",
    "12 Monkeys",
    "Looper",
    "Groundhog Day",
    "Primer",
    "Donnie Darko",
    "Interstellar",
    "Time Bandits",
    "Doctor Strange",
]

async def run():
    result = await kernel.invoke(
        recommend,
        sk.KernelArguments(     #3
            settings=execution_settings, input=", ".join(seen_movie_list)
        ),
    )
    print(result)

asyncio.run(run())     #4

###Output
Based on the list of movies you've provided, it seems you have an 
interest in science fiction, time travel, and mind-bending narratives. 
Given that you've watched a mix of classics and modern films in this 
genre, I would recommend the following movie that you have not watched 
before:

"Edge of Tomorrow" (also known as "Live Die Repeat: Edge of Tomorrow")…

1 从插件文件夹加载提示

2 用户之前看过的电影列表

3 将已看电影的列表设置为连接列表。

4 函数异步执行。

代码从 skills 目录和 plugin 文件夹加载技能/插件。当一个技能被加载到内核中而不是仅仅创建时,它就成为一个已注册的插件。这意味着它可以直接执行,就像这里所做的那样,或者通过插件接口通过 LLM 聊天对话执行。

运行代码(F5),你应该会看到一个类似于列表 5.13 的输出。我们现在有一个简单的语义函数,它可以作为插件托管。然而,这个函数需要用户输入他们看过的电影完整列表。我们将在下一节介绍原生函数,以探讨如何解决这个问题。

5.4.2 应用原生函数

如前所述,原生函数是可以做任何事情的代码。在以下示例中,我们将介绍一个原生函数来帮助我们在之前构建的语义函数。

此原生函数将从文件中加载用户之前看过的电影列表。虽然这个函数引入了记忆的概念,但我们将在第八章讨论这个问题。将这个新的原生函数视为任何可以虚拟做任何事情的代码。

可以使用 SK 扩展创建和注册原生函数。对于这个例子,我们将直接在代码中创建原生函数,以便使示例更容易理解。

在 VS Code 中打开 SK_native_functions.py。我们将首先查看原生函数是如何定义的。原生函数通常在类内部定义,这简化了原生函数的管理和实例化。

列表 5.14 SK_native_functions.py (MySeenMovieDatabase)
class MySeenMoviesDatabase:
    """
    Description: Manages the list of users seen movies.     #1
    """
    @kernel_function(     #2
        description="Loads a list of movies … user has already seen",
        name="LoadSeenMovies",
    )
    def load_seen_movies(self) -> str:     #3
        try:
            with open("seen_movies.txt", 'r') as file:     #4
                lines = [line.strip() for line in file.readlines()]
                comma_separated_string = ', '.join(lines)
            return comma_separated_string
        except Exception as e:
            print(f"Error reading file: {e}")
            return None

1 为容器类提供描述

2 使用装饰器提供函数描述和名称

3 实际的函数返回一个以逗号分隔的字符串形式的电影列表。

4 从文本文件中加载已看电影

原生函数定义后,我们可以通过在文件中向下滚动查看其使用方法,如下所示列表。

列表 5.15 SK_native_functions(剩余代码)
plugins_directory = "plugins"

recommender = kernel.import_plugin_from_prompt_directory(
    plugins_directory,
    "Recommender",
)     #1

recommend = recommender["Recommend_Movies"]

seen_movies_plugin = kernel.import_plugin_from_object(
    MySeenMoviesDatabase(), "SeenMoviesPlugin"
)     #2

load_seen_movies = seen_movies_plugin["LoadSeenMovies"]     #3

async def show_seen_movies():
    seen_movie_list = await load_seen_movies(kernel)
    return seen_movie_list

seen_movie_list = asyncio.run(show_seen_movies())     #4
print(seen_movie_list)

async def run():      #5
    result = await kernel.invoke(
        recommend,
        sk.KernelArguments(
                settings=execution_settings,
                input=seen_movie_list),
    )
    print(result)

asyncio.run(run())    #5

###Output
The Matrix, The Matrix Reloaded, The Matrix Revolutions, The Matrix 
Resurrections – `*output from print statement*`
Based on your interest in the "The Matrix" series, it seems you enjoy 
science fiction films with a strong philosophical undertone and action 
elements. Given that you've watched all

1 按照之前所示加载语义函数

2 将技能导入内核并注册该函数为插件

3 加载原生函数

4 执行函数并将列表作为字符串返回

5 将插件调用封装在一个异步函数中并执行

一个需要注意的重要方面是本地函数如何导入到内核中。将函数导入内核的行为将此函数注册为插件/技能。这意味着该函数可以通过其他对话或交互从内核中作为技能使用。我们将在下一节中看到如何在语义函数中嵌入本地函数。

5.4.3 在语义函数中嵌入本地函数

SK 中有很多强大的功能,但一个有益的功能是能够在其他语义函数中嵌入本地或语义函数。以下列表显示了如何在一个语义函数中嵌入本地函数。

列表 5.16 SK_semantic_native_functions.py (skprompt)
sk_prompt = """
You are a wise movie recommender and you have been asked to recommend a 
movie to a user.
You have a list of movies that the user has watched before.
You want to recommend a movie that 
the user has not watched before.     #1
Movie List: {{MySeenMoviesDatabase.LoadSeenMovies}}.     #2
"""

1 与之前相同的精确指令文本

2 通过类名和函数名引用和识别本地函数。

下一示例,SK_semantic_native_functions.py,使用了内联本地和语义函数。在 VS Code 中打开文件,以下列表显示了创建、注册和执行函数的代码。

列表 5.17 SK_semantic_native_functions.py(缩略)
prompt_template_config = sk.PromptTemplateConfig(
    template=sk_prompt,
    name="tldr",
    template_format="semantic-kernel",
    execution_settings=execution_settings,
)     #1

recommend_function = kernel.create_function_from_prompt(
    prompt_template_config=prompt_template_config,
    function_name="Recommend_Movies",
    plugin_name="Recommendation",
)     #2

async def run_recommendation():     #3
    recommendation = await kernel.invoke(
        recommend_function,
        sk.KernelArguments(),
    )
    print(recommendation)

# Use asyncio.run to execute the async function
asyncio.run(run_recommendation())
###Output
Based on the list provided, it seems the user is a fan of the Matrix 
franchise. Since they have watched all four existing Matrix movies, I 
would recommend a…

1 为提示创建提示模板配置

2 从提示中创建内联语义函数

3 异步执行语义功能

运行代码,你应该会看到一个类似于列表 5.17 的输出。一个需要注意的重要方面是,本地函数已注册到内核中,但语义函数没有。这很重要,因为函数创建不会注册函数。

为了使此示例正确工作,本地函数必须使用import_plugin函数调用(列表 5.17 中的第一行)注册到内核中。然而,语义函数本身并没有注册。注册函数的一个简单方法是将它做成插件并导入。

这些简单的练习展示了将插件和技能集成到聊天或代理界面中的方法。在下一节中,我们将查看一个完整的示例,展示如何将代表服务或 GPT 接口的插件添加到聊天功能中。

5.5 语义内核作为交互式服务代理

在第一章中,我们介绍了 GPT 接口的概念——通过插件和语义层将服务和其他组件连接到 LLMs 的新范式。SK 为将任何服务转换为 GPT 接口提供了一个出色的抽象。

图 5.8 展示了一个围绕名为 The Movie Database (TMDB; www.themoviedb.org)的 API 服务构建的 GPT 界面。TMDB 网站提供了一个免费的 API,可以公开电影和电视节目的信息。

figure

图 5.8 这层架构图显示了 GPT 接口和语义内核在聊天或代理界面中被暴露的作用。

要跟随本节的练习,您必须从 TMDB 注册一个免费账户并创建一个 API 密钥。获取 API 密钥的说明可以在 TMDB 网站上找到(www.themoviedb.org)或通过询问 GPT-4 turbo 或更近期的 LLM。

在接下来的几节中,我们将使用 SK 原生函数集创建一个 GPT 接口。然后,我们将使用 SK 内核测试该接口,并在本章的后面部分将其作为插件实现到聊天功能中。在下一节中,我们将探讨如何针对 TMDB API 构建 GPT 接口。

5.5.1 构建语义 GPT 接口

TMDB 是一个出色的服务,但它不提供语义服务或可以插入到 ChatGPT 或代理中的服务。为了做到这一点,我们必须在 TMDB 公开的 API 调用周围包装一个语义服务层。

语义服务层是一个 GPT 接口,通过自然语言公开函数。正如讨论的那样,要将函数公开给 ChatGPT 或其他如代理等接口,它们必须被定义为插件。幸运的是,SK 可以自动为我们创建插件,前提是我们正确编写我们的语义服务层。

原生插件或一组技能可以作为语义层。要创建原生插件,创建一个新的插件文件夹,并在该文件夹中放置一个包含一组原生函数的 Python 文件。目前 SK 扩展没有很好地做到这一点,所以手动创建模块效果最好。

图 5.9 展示了名为Movies的新插件和名为tmdb.py的语义服务层的结构。对于原生函数,使用父文件夹的名称(Movies)进行导入。

figure

图 5.9 TMDB 插件的文件夹和文件结构

在 VS Code 中打开tmdb.py文件,查看文件顶部,如图 5.18 所示。此文件包含一个名为TMDbService的类,它公开了几个映射到 API 端点调用的函数。想法是将这个语义服务层中的各种相关 API 函数调用映射出来。这将使函数作为聊天或代理接口的插件公开。

列表 5.18 tmdb.py(文件顶部)
from semantic_kernel.functions import kernel_funct
import requests
import inspect

def print_function_call():     #1
    #omitted …

class TMDbService:     #2
    def __init__(self):
        # enter your TMDb API key here
        self.api_key = "your-TMDb-api-key"

    @kernel_function(     #2
        description="Gets the movie genre ID for a given genre name",
        name="get_movie_genre_id",
        input_description="The movie genre name of the genre_id to get",
        )
    def get_movie_genre_id(self, genre_name: str) -> str:     #3
        print_function_call()
        base_url = "https://api.themoviedb.org/3"
        endpoint = f"{base_url}/genre/movie/list↪
                     ↪ ?api_key={self.api_key}&language=en-US"

        response = requests.get(endpoint)     #4
        if response.status_code == 200:    #4
            genres = response.json()['genres']
            for genre in genres:
                if genre_name.lower() in genre['name'].lower():
                    return str(genre['id'])     #5
        return None

1 打印函数调用以进行调试

2 顶级服务和装饰器用于描述函数(好的描述很重要)

3 被语义包装器包装的函数;应返回 str

4 调用 API 端点,如果成功(代码 200),则检查匹配的类型

5 找到类型,返回 id

TMDbService的代码以及调用 TMDB 端点的函数大部分是在 GPT-4 Turbo 的帮助下编写的。然后,每个函数都被sk_function装饰器包装以公开其语义。

TMDB API 的一些调用已经被语义化了。列表 5.19 展示了向语义服务层公开的函数的另一个示例。这个函数可以拉取特定类型的当前最热门的 10 部电影列表。

列表 5.19 tmdb.py (get_top_movies_by_genre)
@kernel_function(     #1
        description="””
Gets a list of currently playing movies for a given genre””",
        name="get_top_movies_by_genre",
        input_description="The genre of the movies to get",
        )
    def get_top_movies_by_genre(self, genre: str) -> str:
        print_function_call()
        genre_id = self.get_movie_genre_id(genre)     #2
        if genre_id:
            base_url = "https://api.themoviedb.org/3
            playing_movies_endpoint = f"{base_url}/movie/now_playing?↪
↪ api_key={self.api_key}&language=en-US"
            response = requests.get(
                          playing_movies_endpoint)     #3
            if response.status_code != 200:
                return ""

            playing_movies = response.json()['results'
            for movie in playing_movies:     #4
                movie['genre_ids'] = [str(genre_id)  
                      ↪ for genre_id in movie['genre_ids']]
            filtered_movies = [movie for movie ↪
↪ in playing_movies if genre_id ↪
↪ in movie['genre_ids']][:10]     #5
            results = ", ".join([movie['title'] for movie in filtered_movies])
            return results
        else:
            return ""

1 使用描述装饰函数

2 根据给定的类型名称查找 genre id

3 获取当前正在上映的电影列表

4 将 genre_ids 转换为字符串

5 检查是否 genre id 与电影类型匹配

查看其他映射为语义的 API 调用。如您所见,将 API 调用转换为语义服务的模式已经定义得很好。在我们运行完整服务之前,我们将在下一节测试每个函数。

5.5.2 测试语义服务

在实际应用中,您可能希望为每个语义服务函数编写一套完整的单元或集成测试。我们在这里不会这样做;相反,我们将编写一个快速的帮助脚本以测试各种函数。

在 VS Code 中打开test_tmdb_service.py,并查看代码,如图表 5.20 所示。您可以注释和取消注释任何函数以单独测试它们。确保一次只取消注释一个函数。

列表 5.20 test_tmdb_service.py
import semantic_kernel as sk
from plugins.Movies.tmdb import TMDbService

async def main():
    kernel = sk.Kernel()     #1

    tmdb_service = kernel.import_plugin_from_object ↪
↪ (TMDbService(), "TMDBService")     #2

    print(
        await tmdb_service"get_movie_genre_id"     #3
        )
    )     #4
    print(
        await tmdb_service"get_tv_show_genre_id"     #5
        )
    )     #6
    print(
        await tmdb_service"get_top_movies_by_genre"     #7
        )
    )     #8
    print(
        await tmdb_service"get_top_tv_shows_by_genre"    #7
        )
    )
    print(await tmdb_service"get_movie_genres"))                        #9
    print(await tmdb_service"get_tv_show_genres"))                       #9

# Run the main function
if __name__ == "__main__":
    import asyncio

    asyncio.run(main())     #10

###Output
Function name: get_top_tv_shows_by_genre     #11
Arguments:
  self = <skills.Movies.tmdb.TMDbService object at 0x00000159F52090C0>
  genre = action
Function name: get_tv_show_genre_id    #11
Arguments:
  self = <skills.Movies.tmdb.TMDbService object at 0x00000159F52090C0>
  genre_name = action
Arcane, One Piece, Rick and Morty, Avatar: The Last Airbender, Fullmetal 
Alchemist: Brotherhood, Demon Slayer: Kimetsu no Yaiba, Invincible, 
Attack on Titan, My Hero Academia, Fighting Spirit, The Owl House

1 实例化内核

2 导入插件服务

3 在需要时将输入参数传递给函数

4 执行并测试各种函数

5 在需要时将输入参数传递给函数

6 执行并测试各种函数

7 在需要时将输入参数传递给函数

8 执行并测试各种函数

9 执行并测试各种函数

10 异步执行主函数

11 调用打印函数详细信息以通知函数正在被调用

在这个测试中展示了 SK 的真正力量。注意TMDbService类是如何作为一个插件导入的,但我们不需要定义除了我们之前已经做的任何插件配置?通过只写一个封装了一些 API 函数的类,我们已经以语义方式公开了 TMDB API 的一部分。现在,函数公开后,我们可以在下一节中查看它们如何作为聊天界面的插件使用。

5.5.3 与语义服务层的交互式聊天

当 TMDB 函数以语义方式公开时,我们可以继续将它们集成到聊天界面中。这将使我们能够在这个界面中自然地进行对话以获取各种信息,例如当前的热门电影。

在 VS Code 中打开SK_service_chat.py。向下滚动到创建函数的新代码部分的开始,如图表 5.21 所示。这里创建的函数现在作为插件公开,除了我们过滤掉的聊天函数,我们不希望将其作为插件公开。这里的聊天函数允许用户直接与 LLM 进行对话,不应作为插件。

列表 5.21 SK_service_chat.py(函数设置)
system_message = "You are a helpful AI assistant."

tmdb_service = kernel.import_plugin_from_object(
TMDbService(), "TMDBService")     #1

# extracted section of code
execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(
        service_id=service_id,
        ai_model_id=model_id,
        max_tokens=2000,
        temperature=0.7,
        top_p=0.8,
        tool_choice="auto",
        tools=get_tool_call_object(
            kernel, {"exclude_plugin": ["ChatBot"]}),     #2
    )

prompt_config = sk.PromptTemplateConfig.from_completion_parameters(
    max_tokens=2000,
    temperature=0.7,
    top_p=0.8,
    function_call="auto",
    chat_system_prompt=system_message,
)     #3
prompt_template = OpenAIChatPromptTemplate(
    "{{$user_input}}", kernel.prompt_template_engine, prompt_config
)     #4

history = ChatHistory()

history.add_system_message("You recommend movies and TV Shows.")
history.add_user_message("Hi there, who are you?")
history.add_assistant_message(
    "I am Rudy, the recommender chat bot. I'm trying to figure out what 
people need."
)     #5

chat_function = kernel.create_function_from_prompt(
    prompt_template_config=prompt_template,
    plugin_name="ChatBot",
    function_name="Chat",
)     #6

1 将 TMDbService 作为插件导入

2 配置执行设置并添加过滤工具

3 配置提示配置

4 定义输入模板并接受完整的字符串作为用户输入

5 添加聊天历史对象并填充一些历史记录

6 创建聊天函数

接下来,我们可以继续在同一文件中滚动以审查聊天函数,如下列所示。

列表 5.22 SK_service_chat.py(聊天函数)
async def chat() -> bool:
    try:
        user_input = input("User:> ")     #1
    except KeyboardInterrupt:
        print("\n\nExiting chat...")
        return False
    except EOFError:
        print("\n\nExiting chat...")
        return False

    if user_input == "exit":     #2
        print("\n\nExiting chat...")
        return False
    arguments = sk.KernelArguments(     #3
        user_input=user_input,
        history=("\n").join(
           [f"{msg.role}: {msg.content}" for msg in history]),
    )
    result = await chat_completion_with_tool_call(     #4
        kernel=kernel,
        arguments=arguments,
        chat_plugin_name="ChatBot",
        chat_function_name="Chat",
        chat_history=history,
    )
    print(f"AI Agent:> {result}")
    return True

1 输入直接来自终端/控制台。

2 如果用户输入 exit,则退出聊天。

3 创建传递给函数的参数

4 使用实用函数调用函数并执行工具

最后,滚动到文件底部,并审查主函数。这是调用循环中聊天函数的代码。

列表 5.23 SK_service_chat.py(主函数)
async def main() -> None:
    chatting = True
    context = kernel.create_new_context()

    print("Welcome to your first AI Agent\     #1
\n  Type 'exit' to exit.\
\n  Ask to get a list of currently playing movies by genre."
    )
    while chatting:     #2
        chatting, context = await chat(context)     #3

if __name__ == "__main__":
    asyncio.run(main())

1 对用户的介绍

2 继续直到聊天为 False

3 异步调用聊天函数

运行聊天界面,运行文件(F5),然后询问特定类型的电影或电视节目。示例对话会话如列表 5.24 所示。此输出显示了请求列出两个类型的电影时,聊天界面如何多次调用 get_top_movie_by_genre 函数。

列表 5.24 SK_service_chat.py(示例对话)
Welcome to your first AI Agent
  Type 'exit' to exit.
  Ask to get a list of currently playing movies by genre.
User:> Input: can you give me list of the current top playing movies for 
the action and comedy genres?

Function name: get_top_movies_by_genre     #1
Arguments:
  genre = action
Function name: get_movie_genre_id     #2
Arguments:
  genre_name = action
Function name: get_top_movies_by_genre    #1
Arguments:
  genre = comedy
Function name: get_movie_genre_id    #2
Arguments:
  genre_name = comedy
Agent:> Here are the current top-playing movies 
for the action and comedy genres:

**Action:**     #3
1\. The Hunger Games: The Ballad of Songbirds & Snakes
2\. Rebel Moon - Part One: A Child of Fire
3\. Aquaman and the Lost Kingdom
4\. Silent Night
5\. The Family Plan
6\. Freelance
7\. Migration
8\. Sound of Freedom
9\. Godzilla Minus One

**Comedy:**     #4
1\. The Family Plan
2\. Wonka
3\. Freelance
4\. Saltburn
5\. Chicken Run: Dawn of the Nugget
6\. Trolls Band Together
7\. There's Something in the Barn
8\. Migration

Please note that some movies may overlap in both genres, such as 
"The Family Plan" and "Freelance ."

1 LLM 调用两次 get_top_movies_by_genre。

2 内部调用以获取类型 ID

3 当前最受欢迎的动作电影列表

4 当前最受欢迎的喜剧电影列表

一定要探索聊天界面的边界以及你可以从 TMDB 服务中请求的内容。例如,尝试请求电影或电视节目的类型列表。这个服务是一个很好的尝试,但我们可能做得更好,正如我们将在下一节中看到的那样。

5.6 在编写语义服务时进行语义思考

现在我们已经看到了将 API 转换为语义服务接口的出色演示。就目前而言,这些函数返回当前正在上映的电影和电视节目的标题。然而,仅仅返回标题,我们限制了 LLM 自行解析结果的能力。

因此,我们将创建 TMDbService 的 v2 版本来纠正这个问题,并将结果作为 JSON 字符串返回。在 VS Code 中打开文件 tmdb_v2.py,并滚动到 get_top_movies_by_genre 函数。

列表 5.25 tmdb_v2.py (get_top_movies_by_genre)
def get_top_movies_by_genre(self, genre: str) -> str:
        print_function_call()
        genre_id = self.get_movie_genre_id(genre)
        if genre_id:
            #same code …

            return json.dumps(filtered_movies)     #1
        else:
            return ""

1 现在返回一个过滤后的 JSON 字符串列表

现在在 VS Code 中打开 SK_service_chat.py 文件,注释和取消注释列表 5.26 中显示的行。这将使用 TMDbService 的第 2 个版本,该版本以单个字符串的形式输出完整的 JSON 文档结果。

列表 5.26 SK_service_chat.py(修改导入)
#from skills.Movies.tmdb import TMDbService     #1
from skills.Movies.tmdb_v2 import TMDbService     #2

1 注释掉这一行。

2 取消注释此行以使用服务的第 2 个版本。

在 VS Code 中重新运行 SK_service_chat.py 文件,并稍微修改你的查询,如下列所示。

列表 5.27 SK_service_chat.pyTMDb_v2 服务输出)
User:> get a list of currently playing movies for the 
action genre and only return movies about space     #1
Agent:> To find currently playing action movies that are specifically 
about space, I will need to manually filter the provided list for those 
that have space-related themes in their overview. Here's what fits that 
criteria from the list:

1\. **Rebel Moon - Part One: A Child of Fire**     #2
   - Release Date: 2023-12-15
   - Overview: When a peaceful colony on the edge of the galaxy finds 
itself threatened by the armies of the tyrannical Regent Balisarius, 
they dispatch Kora, a young woman with a mysterious past, to seek out 
warriors from neighboring planets to help them take a stand.

This is the only movie from the provided list that clearly mentions a 
space-related theme in its overview. …

1 新查询要求包含额外的空间过滤器

2 LLM 调用服务并审查匹配过滤器的返回结果。

由于语义服务函数现在以 JSON 格式返回完整的电影列表,LLM 可以应用额外的过滤。这是语义服务的真正力量,允许你通过 LLM 处理数据。我们不会仅仅通过返回标题列表来看到这种力量。

这个最后一个练习展示了在编写语义服务层时你需要做出的心态转变。通常,你通常会希望返回尽可能多的信息。返回更多信息可以利用 LLM 独立过滤、排序和转换数据的能力。在下一章中,我们将探讨使用行为树构建自主代理。

5.7 练习

完成以下练习以提高你对材料的了解:

  • 练习 1—创建温度转换的基本插件

目标—熟悉创建简单的 OpenAI 聊天完成 API 插件。

任务:

    • 开发一个在摄氏度和华氏度之间转换温度的插件。

    • 通过将其集成到简单的 OpenAI 聊天会话中测试插件,用户可以请求温度转换。

  • 练习 2—开发天气信息插件

目标—学习创建执行独特任务的插件。

任务:

    • 为 OpenAI 聊天完成 API 创建一个插件,从公共 API 获取天气信息。

    • 确保插件可以处理用户对不同城市当前天气状况的请求。

  • 练习 3—制作一个创意语义功能

目标—探索创建语义功能。

任务:

    • 开发一个基于用户输入写诗或讲述儿童故事的语义功能。

    • 在聊天会话中测试该函数,以确保它生成创意和连贯的输出。

  • 练习 4—使用原生函数增强语义功能

目标—了解如何结合语义和原生函数。

任务:

    • 创建一个使用原生函数增强其功能的语义功能。

    • 例如,开发一个生成餐单的语义功能,并使用原生函数获取食材的营养信息。

  • 练习 5—使用语义内核封装现有的 Web API

目标—学习将现有的网络 API 封装为语义服务插件。

任务:

    • 使用 SK 将新闻 API 封装并作为聊天代理中的语义服务插件公开。

    • 确保插件可以处理用户对各种主题的最新新闻文章的请求。

摘要

  • 代理动作扩展了代理系统的功能,如 ChatGPT。这包括向 ChatGPT 和 LLM 添加插件以作为动作代理的能力。

  • OpenAI 支持在 OpenAI API 会话中定义函数和插件。这包括向 LLM API 调用中添加函数定义,并理解这些函数如何允许 LLM 执行额外的操作。

  • 语义内核(SK)是微软的一个开源项目,可用于构建 AI 应用程序和代理系统。这包括语义插件在定义原生和语义函数中的作用。

  • 语义函数封装了用于与 LLM 互动的提示/配置文件模板。

  • 原生函数封装了使用 API 或其他接口执行或执行动作的代码。

  • 语义函数可以与其他语义或原生函数结合,并在执行阶段相互层叠。

  • SK 可以在语义服务层上创建一个 GPT 接口,并在聊天或代理接口插件中暴露它们。

  • 语义服务代表了 LLMs(大型语言模型)和插件之间的交互,以及这些概念在实际创建高效 AI 代理中的应用实现。

第六章:构建自主助手

本章涵盖了

  • 适用于机器人和人工智能应用程序的行为树

  • GPT 助手游乐场和创建助手及动作

  • 自主控制代理行为树

  • 通过代理行为树模拟对话多代理系统

  • 使用反向链接创建复杂系统的行为树

现在我们已经介绍了如何通过动作扩展代理的能力,我们可以看看行为树如何指导代理系统。我们将从理解行为树的基本原理以及它们如何控制游戏中的机器人和人工智能开始。

我们将回到代理动作,并检查如何使用 GPT 助手游乐场项目在 OpenAI 助手平台上实现动作。从那里,我们将探讨如何使用 OpenAI 助手构建自主代理行为树(ABT)。然后,我们将转向理解自主代理需要控制和护栏,并使用控制屏障函数。

在本章的最后部分,我们将检查使用 AgentOps 平台来监控我们的自主行为驱动代理系统。这将是一个充满挑战的章节。让我们首先跳到下一节,介绍行为树。

6.1 行为树的介绍

行为树是一种长期建立的模式,用于控制游戏中的机器人和人工智能。罗德尼·A·布鲁斯(Rodney A. Brooks)首次在 1986 年的“为移动机器人设计的鲁棒分层控制系统”论文中介绍了这个概念。这为今天我们所使用的树和节点结构扩展了模式奠定了基础。

如果你曾经玩过与非玩家角色(NPC)互动的电脑游戏或与高级机器人系统互动,你就已经见证了行为树在工作。图 6.1 展示了一个简单的行为树。该树代表所有主要节点:选择器或回退节点、序列节点、动作节点和条件节点。

图

图 6.1 吃苹果或梨的简单行为树

表 6.1 描述了本书中将探讨的主要节点的功能和目的。还有其他节点和节点类型,你甚至可以创建自定义节点,但就目前而言,我们将专注于表中的那些。

表 6.1 行为树中使用的主要节点
节点 目的 功能 类型
选择器(回退) 此节点通过选择第一个成功完成的子节点来工作。它通常被称为回退节点,因为它总是会回退到最后一个成功执行的节点。 节点按顺序调用其子节点,并在第一个子节点成功时停止执行。当子节点成功时,它将返回成功;如果没有节点成功,它将返回失败。 组合
序列 此节点按顺序执行其所有子节点,直到某个节点失败或它们都成功完成。 节点按顺序调用其所有子节点,无论它们是否失败或成功。如果所有子节点都成功,则返回成功,如果只有一个子节点失败,则返回失败。 组合节点
条件 行为树不使用布尔逻辑,而是使用成功或失败作为控制手段。条件在条件为真时返回成功,否则返回失败。 节点根据条件返回成功或失败。 任务节点
动作 这就是动作发生的地方。 节点执行并返回成功,如果成功则返回成功,否则返回失败。 任务节点
装饰器 它们通过控制子节点的执行来工作。它们通常被称为条件,因为它们可以确定一个节点是否值得执行或安全执行。 节点控制子节点的执行。装饰器可以作为控制屏障函数来阻止或防止不受欢迎的行为。 装饰器节点
并行 此节点并行执行其所有节点。成功或失败由所需成功子节点的阈值控制。 节点按顺序执行其所有子节点,无论节点状态如何。 组合节点

表 6.1 中的主要节点可以提供足够的功能来处理多种用例。然而,最初理解行为树可能会感到困难。你只有在开始使用它们之后才会欣赏到它们背后的复杂性。在我们构建一些简单的树之前,我们希望在下一节中更详细地探讨执行过程。

6.1.1 理解行为树的执行

理解行为树的执行方式对于设计和实现行为树至关重要。与计算机科学中的大多数概念不同,行为树以成功和失败为操作术语。当行为树中的节点执行时,它将返回成功或失败;这甚至适用于条件和选择节点。

行为树从上到下、从左到右执行。图 6.2 显示了执行过程以及节点失败或成功时会发生什么。在示例中,该树控制的 AI 有一个苹果但没有梨。在第一个序列节点中,一个条件检查 AI 是否有苹果。因为 AI 没有苹果,所以它终止序列并回退到选择器。然后选择器选择其下一个子节点,另一个序列,检查 AI 是否有梨,因为它有,所以 AI 吃掉了苹果。

图

图 6.2 简单行为树的执行过程

行为树提供对人工智能系统在宏观或微观层面执行的控制。在机器人领域,行为树通常被设计为在微观层面运行,其中每个动作或条件都是一个小的事件,例如检测苹果。相反,行为树也可以控制更宏观的系统,例如游戏中的非玩家角色(NPC),其中每个动作可能是一系列事件的组合,如攻击玩家。

对于智能体系统,行为树支持在所选级别控制智能体或助手。我们将探讨在任务层面控制智能体,在后面的章节中,我们将探讨规划层面的控制。毕竟,有了 LLM 的力量,智能体可以构建自己的行为树。

当然,还有其他几种形式的人工智能控制系统可以用来控制智能体系统。下一节将探讨这些不同的系统,并将它们与行为树进行比较。

6.1.2 行为树的选择

许多其他人工智能控制系统都有其优点,值得在控制智能体系统时进行探索。它们可以展示行为树的优势,并为特定用例提供其他选项。行为树是一个优秀的模式,但并非唯一,了解其他模式也是值得的。

表 6.2 突出了我们可能考虑用于控制人工智能系统的其他几个系统。表中的每一项都描述了该方法的功能、其缺点以及它可能应用于智能体 AI 控制的应用。

表 6.2 其他人工智能控制系统的比较
控制名称 描述 短处 是否适用于控制智能体 AI?
有限状态机 ^a (FSM) 有限状态机使用一组状态和由事件或条件触发的转换来模拟人工智能。 随着复杂性的增加,有限状态机可能会变得难以控制。 对于智能体来说,有限状态机不实用,因为它们扩展性不好。
决策树 ^b 决策树使用树状模型来表示决策及其可能的结果。 决策树在复杂场景中可能会出现过度拟合和泛化不足的问题。 决策树可以通过行为树进行适应和增强。
基于效用的系统 ^b 效用函数评估并选择基于当前情况的最佳行动。 这些系统需要精心设计效用函数以平衡优先级。 这种模式可以在行为树中采用。
基于规则的系统 ^a 这组 if-then 规则定义了人工智能的行为。 当规则众多时,这些系统可能会变得繁琐,并可能导致潜在冲突。 当与由 LLM 驱动的智能体系统结合时,这些系统不太实用。
计划系统 ^c 计划系统使用规划算法生成一系列动作以实现特定目标。 这些系统计算成本高昂,需要大量的领域知识。 智能体可以在后面的章节中看到,它们可以自行实现这样的模式。
行为克隆^c 行为克隆指的是通过模仿专家演示来学习策略。 这个系统可能难以泛化到未见过的情境。 这可以集成到行为树中或特定任务中。
层次化任务网络(HTN)^d HTNs 将任务分解成更小、更易于管理的子任务,并按层次排列。 这些对于非常大的任务来说管理和设计都很复杂。 HTNs 允许更好地组织和执行复杂任务。这种模式可以用于更大的智能体系统。
黑板系统^b 这些系统使用共享黑板进行不同子系统的协作问题解决。 这些系统难以实现,并且管理子系统之间的通信很困难。 智能体系统可以通过对话或群聊/线程实现类似的模式。
遗传算法(GA)^d 这些优化技术受自然选择启发,用于进化解决方案以解决问题。 遗传算法计算密集,并且不一定总能找到最优解。 遗传算法具有潜力,甚至可以用来优化行为树。

| ^a 考虑到复杂的智能体系统时不实用 ^b 存在于行为树中或可以轻松集成

^c 通常应用于任务或动作/条件级别

^d 应用到智能体时需要大量工作的高级系统

|

在本书的后续章节中,我们将探讨表 6.2 中讨论的一些模式。总的来说,可以通过使用行为树作为基础来增强或整合几个模式。而其他模式,如有限状态机(FSMs),虽然可能对小型实验有帮助,但它们缺乏行为树的扩展性。

行为树作为人工智能控制系统可以提供几个好处,包括可扩展性。以下列表突出了使用行为树的其它显著好处:

  • 模块化和可重用性——行为树促进了一种模块化的行为设计方法,允许开发者创建可重用组件。行为树中的节点可以轻松地在树的各个部分或甚至在不同项目中重用,从而提高可维护性并减少开发时间。

  • 可扩展性——随着系统复杂性的增加,行为树比其他方法(如 FSMs)更优雅地处理新行为的添加。行为树允许对任务进行分层组织,这使得管理和理解大型行为集变得更加容易。

  • 灵活性和可扩展性——行为树提供了一个灵活的框架,其中可以添加新的节点(动作、条件、装饰器)而不会大幅改变现有结构。这种可扩展性使得引入新行为或修改现有行为以适应新要求变得简单直接。

  • 调试和可视化——行为树提供了清晰直观的行为可视化表示,这对调试和理解决策过程有益。支持行为树的工具通常包括图形编辑器,允许开发者可视化并调试树结构,使其更容易识别和修复问题。

  • 决策逻辑解耦——行为树将决策和执行逻辑分离,促进了高级策略和低级动作之间的清晰区分。这种解耦简化了设计,并允许更直接地修改和测试特定的行为部分,而不会影响整个系统。

在为行为树做了强有力的论证之后,我们现在应该考虑如何在代码中实现它们。在下一节中,我们将探讨如何使用 Python 代码构建一个简单的行为树。

6.1.3 使用 Python 和 py_trees 运行行为树

由于行为树已经存在很长时间,并且被整合到许多技术中,因此创建一个示例演示非常简单。当然,最简单的方法是询问 ChatGPT 或您喜欢的 AI 聊天工具。列表 6.1 显示了使用提示生成代码示例并将图 6.1 作为示例树提交的结果。最终代码必须进行简单的命名和参数错误修正。

注意  本章的所有代码都可以通过下载 GPT 助手游乐场项目在 mng.bz/Ea0q 找到。

列表 6.1 first_btree.py
import py_trees

class HasApple(py_trees.behaviour.Behaviour):      #1
    def __init__(self, name):
        super(HasApple, self).__init__(name)

    def update(self):        
        if True:  
            return py_trees.common.Status.SUCCESS
        else:
            return py_trees.common.Status.FAILURE
# Other classes omitted…

has_apple = HasApple(name="Has apple")      #2
eat_apple = EatApple(name="Eat apple")      #2
sequence_1 = py_trees.composites.Sequence(name="Sequence 1", memory=True)
sequence_1.add_children([has_apple, eat_apple])                              #3

has_pear = HasPear(name="Has pear")         #4
eat_pear = EatPear(name="Eat pear")         #4
sequence_2 = py_trees.composites.Sequence(name="Sequence 2", memory=True)
sequence_2.add_children([has_pear, eat_pear])               #3                

root = py_trees.composites.Selector(name="Selector", memory=True)
root.add_children([sequence_1, sequence_2])          #3                       

behavior_tree = py_trees.trees.BehaviourTree(root)     #5

py_trees.logging.level = py_trees.logging.Level.DEBUG   
for i in range(1, 4):                                                      #6
    print("\n------------------ Tick {0} ------------------".format(i))
    behavior_tree.tick()                                                  #6

### Start of output
------------------ Tick 1 ------------------
[DEBUG] Selector             : Selector.tick()
[DEBUG] Selector             : Selector.tick() [!RUNNING->reset current_child]
[DEBUG] Sequence 1           : Sequence.tick()
[DEBUG] Has apple            : HasApple.tick()
[DEBUG] Has apple            : HasApple.stop(Status.INVALID->Status.SUCCESS)
[DEBUG] Eat apple            : EatApple.tick()
Eating apple
[DEBUG] Eat apple            : EatApple.stop(Status.INVALID->Status.SUCCESS)
[DEBUG] Sequence 1           : Sequence.stop()[Status.INVALID->Status.SUCCESS]

1 创建一个类来实现动作或条件

2 创建动作和条件节点

3 将节点添加到相应的父节点

4 创建动作和条件节点

5 创建整个行为树

6 在行为树上执行一步/一个时间单位

列表 6.1 中的代码代表了图 6.1 中的行为树。您可以按原样运行此代码,或更改条件返回的内容,然后再次运行树。您还可以通过从根选择器中删除一个序列节点来更改行为树。

现在我们对行为树有了基本的了解,我们可以继续处理代理/助手。在这样做之前,我们将查看一个帮助我们与 OpenAI 助手一起工作的工具。这个工具将帮助我们围绕 OpenAI 助手包装我们的第一个 ABT。

6.2 探索 GPT 助手游乐场

为了本书的开发,创建了几个 GitHub 项目来处理构建代理和助手的各种方面。其中一个项目,GPT 助手游乐场,使用 Gradio 构建了界面,模仿了 OpenAI 助手游乐场,但增加了几个额外功能。

PlayGround 项目被开发为一个教学和演示辅助工具。在项目内部,Python 代码使用 OpenAI 助手 API 创建聊天界面和用于构建和驱动助手的代理系统。同时,还提供了一系列您可以使用的行为助手,并且您可以轻松地添加您自己的行为。

6.2.1 安装和运行 PlayGround

以下列表显示了从终端安装和运行 PlayGround 项目的过程。目前还没有 PyPI 包可以安装。

列表 6.2 安装 GPT 助手 PlayGround
# change to a working folder and create a new Python virtual environment
git clone 
↪ https://github.com/cxbxmxcx/GPTAssistantsPlayground     #1
cd GPTAssistantsPlayground      #2
pip install -r requirements.txt      #3

1 从 GitHub 拉取源代码

2 将目录切换到项目源代码文件夹

3 安装需求

您可以从终端或使用 Visual Studio Code(VS Code)运行应用程序,后者给您更多的控制权。在运行应用程序之前,您需要通过命令行或创建一个.env文件来设置您的 OpenAI API 密钥,就像我们之前已经做过的几次一样。列表 6.3 显示了在 Linux/Mac 或 Git Bash shell(推荐 Windows)上设置环境变量并运行应用程序的示例。

列表 6.3 运行 GPT 助手 PlayGround
export OPENAI_API_KEY="your-api-key"      #1
python main.py     #2

1 将您的 API 密钥设置为环境变量

2 从终端或通过 VS Code 运行应用

打开您的浏览器到显示的 URL(通常是 http://127.0.0.1:7860)或终端中提到的地址。您将看到一个类似于图 6.3 所示的界面。如果您已经定义了 OpenAI 助手,您将在“选择助手”下拉菜单中看到它们。

图

图 6.3 使用 GPT 助手 PlayGround 界面学习数学

如果您从未定义过助手,您可以创建一个并选择您需要的各种选项和指令。如果您访问过 OpenAI PlayGround,您已经体验过类似的界面。

GPT 与助手对比

OpenAI 将 GPT 定义为可以在 ChatGPT 界面中运行和使用的助手。助手只能通过 API 进行消费,并且在大多数情况下需要自定义代码。当您运行一个助手时,您将根据模型令牌使用情况和任何特殊工具(包括代码解释器和文件)进行收费,而 GPT 在 ChatGPT 内部运行,并由账户费用覆盖。

创建 PlayGround 本地版本的原因是为了演示代码结构,同时也提供以下列出的额外功能:

  • 行为(自定义行为)——创建您自己的行为允许您向助手添加任何您想要的功能。正如我们将看到的,PlayGround 使创建您自己的行为变得非常简单。

  • 代码运行器——API 确实附带了一个代码解释器,但它相对昂贵(每次运行 0.03 美元),不允许你安装你的模块,不能以交互方式运行代码,并且运行速度较慢。游戏场将使你能够在隔离的虚拟环境中本地运行 Python 代码。虽然不如将代码推送到 Docker 镜像安全,但它比其他平台更好地执行了代码窗口化和进程外执行。

  • 透明度和日志记录——游戏场提供了全面的日志捕获功能,甚至可以显示助手如何使用内部和外部工具/操作。这可以是一个查看助手幕后所做事情的优秀方式。

这些功能将在接下来的几节中更详细地介绍。我们将在下一节中开始查看使用和消费操作。

6.2.2 使用和构建自定义操作

操作和工具是赋予代理和助手能力的基石。没有工具的访问权限,代理就变成了无功能的聊天机器人。OpenAI 平台在建立许多工具模式方面处于领先地位,正如我们在第三章中看到的。

游戏场提供了几个可以通过界面附加到助手的自定义操作。在接下来的练习中,我们将构建一个简单的助手并附加几个自定义操作,以查看可以做到什么程度。

图 6.4 显示了扩展的操作手风琴,它显示了许多可用的自定义操作。从终端或调试器运行游戏场,创建一个新的助手。然后,选择图中的操作。在你完成选择操作后,滚动到页面底部,点击添加助手以添加助手。助手在使用之前需要被创建。

figure

图 6.4 在界面中选择和使用自定义操作

在你创建助手之后,你可以要求它列出所有可用的助手。列出助手也会给你调用助手所需的 ID。你还可以调用其他助手并要求它们在其专业领域内完成任务。

添加自定义操作就像向文件中添加代码并将其放入正确的文件夹一样简单。从主项目文件夹中打开playground/assistant_actions文件夹,你会看到定义各种操作的几个文件。在 VS Code 中打开file_actions.py文件,如图 6.4 所示。

列表 6.4 playground/assistant_actions/file_actions.py
import os

from playground.actions_manager import agent_action

OUTPUT_FOLDER = "assistant_outputs"

@agent_action     #1
def save_file(filename, content):      #2
    """
    Save content to a file.      #3

    :param filename: The name of the file including extension.
    :param content: The content to save in the file.
    """
    file_path = os.path.join(OUTPUT_FOLDER, filename)
    with open(file_path, "w", encoding="utf-8") as file:
        file.write(content)
    print(f"File '{filename}' saved successfully.")      #4

1 这个装饰器会自动将函数添加为操作。

2 给你的函数起一个清晰且与其目的相符的名字。

3 描述是助手用来确定函数的内容,所以请好好记录。

4 通常返回一个表示成功或失败的消息

您可以通过将文件放置在assistant_actions文件夹中,并用agent_action装饰器装饰它来添加任何您想要的自定义操作。只需确保给函数起一个好名字,并输入关于如何使用函数的良好文档。当 Playground 启动时,它会加载文件夹中所有正确装饰并带有描述/文档的操作。

这很简单。您可以根据需要添加多个自定义操作。在下一节中,我们将探讨一个特殊的自定义操作,允许助手在本地运行代码。

6.2.3 安装助手数据库

要运行本章中的多个示例,您需要安装助手数据库。幸运的是,这可以通过界面轻松完成,只需询问代理即可。即将到来的说明详细介绍了安装助手的过程,并直接来自 GPT 助手 Playground 的 README。您可以从位于assistants.db SQLite 数据库中的assistants.db安装几个演示助手:

  1. 创建一个新的助手,或使用现有的助手。

  2. 给助手分配create_manager_assistant操作(在操作部分下找到)。

  3. 请助手创建管理助手(即,“请创建管理助手”),并确保将助手的名称命名为“管理助手”。

  4. 刷新您的浏览器以重新加载助手选择器。

  5. 选择新的管理助手。这个助手拥有安装助手的指令和操作,这些操作将允许它从assistants.db数据库中安装助手。

  6. 与管理助手交谈,以获取要安装的助手列表,或者直接请管理助手安装所有可用的助手。

6.2.4 获取本地运行代码的助手

使代理和助手生成和运行可执行代码具有很大的能力。与代码解释器不同,本地运行代码提供了快速迭代和调整的众多机会。我们之前在 AutoGen 中看到了这一点,代理可以持续运行代码,直到它按预期工作。

在 Playground 中,选择自定义操作run_code非常简单,如图 6.5 所示。您还希望选择run_shell_command操作,因为它允许助手pip install任何所需的模块。

figure

图 6.5 选择助手运行 Python 代码的自定义操作

您现在可以请助手生成和运行代码,以确保它代表您工作。通过添加自定义操作并请助手生成和运行代码来尝试这一点,如图 6.6 所示。如果代码没有按预期工作,告诉助手您遇到的问题。

figure

图 6.6 使助手生成和运行 Python 代码

再次强调,沙盒中运行的 Python 代码会在项目子文件夹中创建一个新的虚拟环境。如果你没有运行任何操作系统级别的代码或底层代码,这个系统工作得很好。如果你需要更健壮的解决方案,一个好的选择是 AutoGen,它使用 Docker 容器来运行隔离的代码。

添加运行代码或其他任务的动作可以使助手看起来像一个黑盒。幸运的是,OpenAI 助手 API 允许你消费事件并查看助手在幕后做了什么。在下一节中,我们将看到这看起来是什么样子。

6.2.5 通过日志调查助手进程

OpenAI 将一个功能添加到了助手 API 中,允许你监听通过工具/动作使用链式的事件和动作。这个功能已经集成到沙盒中,当助手调用另一个助手时,它会捕获动作和工具的使用。

我们可以通过要求助手使用一个工具并打开日志来尝试这一点。一个很好的例子是给助手提供代码解释器工具,然后要求它绘制一个方程式。图 6.7 展示了这个练习的一个例子。

figure

图 6.7 内部助手日志被捕获

通常情况下,当辅助代码解释器工具被启用时,你不会看到任何代码生成或执行。这个功能允许你看到助手在执行过程中使用的所有工具和动作。这不仅是一个出色的诊断工具,而且还能提供对 LLM(大型语言模型)功能的额外见解。

我们没有审查执行所有这些操作所需的代码,因为代码量很大,并且可能经历几次变化。话虽如此,如果你计划使用助手 API,这个项目是一个很好的起点。随着沙盒的引入,我们可以在下一节继续我们的 ABT 之旅。

6.3 介绍代理行为树

代理行为树(ABTs)在助手和代理系统中实现行为树。与常规行为树相比,ABTs 的关键区别在于它们使用提示来指导动作和条件。由于提示可能会返回高频率的随机结果,我们也可以将这些树称为随机行为树,这种树确实存在。为了简单起见,我们将用于控制代理的行为树区分开来,称其为代理行为树。

接下来,我们将进行一个练习来创建一个 ABT(代理行为树)。完成的树将以 Python 编写,但需要设置和配置各种助手。我们将介绍如何使用助手本身来管理助手。

6.3.1 使用助手管理助手

幸运的是,沙盒可以帮助我们快速管理和创建助手。我们首先安装管理助手,然后安装预定义的助手。让我们按照以下步骤开始安装管理助手:

  1. 在你的浏览器中打开游乐场,创建一个新的简单助理或使用现有的助理。如果你需要一个新助理,创建它然后选择它。

  2. 在选择助理后,打开操作面板,并选择create_manager_assistant操作。你不需要保存;界面将自动更新助理。

  3. 现在,在聊天界面中,提示助理以下内容:“请创建经理助理。”

  4. 几秒钟后,助理会说它已经完成。刷新你的浏览器,并确认经理助理现在可用。如果由于某种原因,新助理没有显示,尝试重新启动 Gradio 应用程序本身。

经理助理就像一个可以访问所有内容的管理员。在启用经理助理时,请确保你的请求具体明确。当经理助理处于激活状态时,你可以按照以下步骤安装书中使用的新助理:

  1. 选择经理助理。如果你修改了经理助理,你可以随时删除并重新安装它。尽管可以拥有多个经理助理,但并不推荐这样做。

  2. 在聊天界面中输入以下内容以询问经理助理可以安装哪些助理:

Please list all the installable assistants.
    1. 当你要求经理助理安装助理时,确定你想要安装哪个助理:
Please install the Python Coding Assistant.

你可以使用游乐场管理并安装任何可用的助理。你还可以要求经理助理将所有助理的定义保存为 JSON:

Please save all the assistants as JSON to a file called assistants.json.

经理助理可以访问所有操作,这些操作应被视为独特且应谨慎使用。在制作助理时,最好保持它们的目标特定,并将操作限制在它们需要的范围内。这不仅避免了给 AI 过多的决策,也避免了由幻觉引起的事故或错误。

在我们完成本章剩余的练习时,你可能需要安装所需的助理。或者,你可以要求经理助理安装所有可用的助理。无论哪种方式,我们将在下一节中查看使用助理创建 ABT。

6.3.2 构建编码挑战 ABT

编码挑战为测试和评估代理和助理系统提供了一个良好的基准。挑战和基准可以量化代理或代理系统的工作效果。我们已经在第四章中应用了编码挑战到多平台代理 AutoGen 和 CrewAI。

对于这个编码挑战,我们将更进一步,查看来自 Edabit 网站(edabit.com)的 Python 编码挑战,这些挑战的复杂度从入门级到专家级不等。我们将坚持使用专家级代码挑战,因为 GPT-4o 和其他模型都是优秀的编码者。查看下一列表中的挑战,并思考你会如何解决它。

列表 6.5 Edabit 挑战:种植草地
Plant the Grass by AniXDownLoe

    You will be given a matrix representing a field g 
and two numbers x, y coordinate.

    There are three types of possible characters in the matrix:

        x representing a rock.
        o representing a dirt space.
        + representing a grassed space.

    You have to simulate grass growing from the position (x, y). 
    Grass can grow in all four directions (up, left, right, down). 
    Grass can only grow on dirt spaces and can't go past rocks.

    Return the simulated matrix.
    Examples

    simulate_grass([
    "xxxxxxx",
    "xooooox",
    "xxxxoox"
    "xoooxxx"
    "xxxxxxx"
    ], 1, 1) → [
    "xxxxxxx",
    "x+++++x",
    "xxxx++x"
    "xoooxxx"
    "xxxxxxx"
    ]

    Notes

    There will always be rocks on the perimeter

你可以使用任何你想要的挑战或编码练习,但以下是一些需要考虑的事项:

  • 挑战应该可以通过可量化的断言(通过/失败)进行测试。

  • 避免在请求游戏、构建网站或使用其他界面时打开窗口。在某个时候,测试完整界面将是可能的,但到目前为止,它只是文本输出。

  • 避免进行长时间运行的挑战,至少最初是这样。开始时,保持挑战简短且生命周期短暂。

任何挑战之外,你还会想要一组测试或断言来确认解决方案的有效性。在 Edabit 上,一个挑战通常提供一套全面的测试。以下列表显示了与挑战一起提供的附加测试。

列表 6.6 种植草地测试
Test.assert_equals(simulate_grass(
["xxxxxxx","xooooox","xxxxoox","xoooxxx","xxxxxxx"],
 1, 1), 
["xxxxxxx","x+++++x","xxxx++x","xoooxxx","xxxxxxx"])
    Test.assert_equals(simulate_grass(
["xxxxxxx","xoxooox","xxoooox","xooxxxx",
"xoxooox","xoxooox","xxxxxxx"],
 2, 3), ["xxxxxxx","xox+++x","xx++++x","x++xxxx",
"x+xooox","x+xooox","xxxxxxx"])
    Test.assert_equals(simulate_grass(
["xxxxxx","xoxoox","xxooox","xoooox","xoooox","xxxxxx"], 
1, 1), 
["xxxxxx","x+xoox","xxooox","xoooox","xoooox","xxxxxx"])
    Test.assert_equals(simulate_grass(
["xxxxx","xooox","xooox","xooox","xxxxx"], 
1, 1),
["xxxxx","x+++x","x+++x","x+++x","xxxxx"])
    Test.assert_equals(simulate_grass(
["xxxxxx","xxxxox","xxooox","xoooxx","xooxxx",
"xooxxx","xxooox","xxxoxx","xxxxxx"], 
4, 1),
["xxxxxx","xxxx+x","xx+++x","x+++xx","x++xxx",
"x++xxx","xx+++x","xxx+xx","xxxxxx"])
    Test.assert_equals(simulate_grass(
["xxxxxxxxxxx", "xoxooooooox", "xoxoxxxxxox", 
"xoxoxoooxox", "xoxoxoxoxox", "xoxoxoxoxox", 
"xoxoxxxoxox", "xoxoooooxox", "xoxxxxxxxox", 
"xooooooooox", "xxxxxxxxxxx"], 1, 1), 
["xxxxxxxxxxx", "x+x+++++++x", "x+x+xxxxx+x", 
"x+x+x+++x+x", "x+x+x+x+x+x", "x+x+x+x+x+x", 
"x+x+xxx+x+x", "x+x+++++x+x", "x+xxxxxxx+x", 
"x+++++++++x", "xxxxxxxxxxx"])

测试将作为两步验证的一部分运行,以确认解决方案的有效性。我们还将使用所写的测试和挑战,这将进一步测试 AI。

图 6.8 显示了将用于解决各种编程挑战的简单行为树的组成。你会注意到这个 ABT 为动作和条件使用了不同的助手。对于第一步,Python 编码助手(称为黑客)生成一个解决方案,然后由编码挑战裁判(称为裁判)进行审查,产生一个经过改进的解决方案,然后由另一个 Python 编码助手(称为验证器)进行验证。

figure

图 6.8 编码挑战的 ABT

图 6.8 还显示了每个代理在哪个线程上进行交流。助手使用消息线程,类似于 Slack 或 Discord 频道,所有在某个线程上交流的助手将看到所有消息。对于这个 ABT,我们保留一个主要对话线程供黑客和裁判共享消息,而验证器则在单独的消息线程上工作。将验证器保持在单独的线程上可以将其从解决方案的努力中的噪音中隔离出来。

现在,在代码中构建 ABT 是一个将py_trees包和 Playground API 函数结合起来的问题。列表 6.7 显示了创建每个动作/条件节点并给助手下达指令的代码摘录。

列表 6.7 agentic_btree_coding_challenge.py
root = py_trees.composites.Sequence("RootSequence", memory=True)

thread = api.create_thread()     #1
challenge = textwrap.dedent("""
 #2
""")
judge_test_cases = textwrap.dedent("""
 #3
""")

hacker = create_assistant_action_on_thread(   
    thread=thread,      #4
    action_name="Hacker",
    assistant_name="Python Coding Assistant",
    assistant_instructions=textwrap.dedent(f"""
    Challenge goal: 
    {challenge}      #5
    Solve the challenge and output the 
final solution to a file called solution.py        
    """),
)
root.add_child(hacker)

judge = create_assistant_action_on_thread(    
    thread=thread,      #6
    action_name="Judge solution",
    assistant_name="Coding Challenge Judge",
    assistant_instructions=textwrap.dedent(
        f"""
    Challenge goal: 
    {challenge}      #7
    Load the solution from the file solution.py.
    Then confirm is a solution to the challenge 
and test it with the following test cases:
    {judge_test_cases}      #8
    Run the code for the solution and confirm it passes all the test cases.
    If the solution passes all tests save the solution to a file called 
judged_solution.py
    """,
    ),
)
root.add_child(judge)

# verifier operates on a different thread, essentially in closed room
verifier = create_assistant_condition(     #9
    condition_name="Verify solution",
    assistant_name="Python Coding Assistant",
    assistant_instructions=textwrap.dedent(
        f"""
    Challenge goal: 
    {challenge}      #10
    Load the file called judged_solution.py and 
verify that the solution is correct by running the code and confirm it passes 
all the test cases:
    {judge_test_cases}      #11
    If the solution is correct, return only the single word SUCCESS, otherwise 
return the single word FAILURE.
    """,
    ),
)
root.add_child(verifier)

tree = py_trees.trees.BehaviourTree(root)

while True:
    tree.tick()
    time.sleep(20)      #12
    if root.status == py_trees.common.Status.SUCCESS:    #13
        break
### Required assistants – 
### Python Coding Assistant and Coding Challenge Judge 
### install these assistants through the Playground

1 创建一个将由黑客和裁判共享的消息线程

2 如示例列表 6.5 所示,挑战的体现

3 如示例列表 6.6 所示,测试的体现

4 创建一个将由黑客和裁判共享的消息线程

5 如示例列表 6.5 所示,挑战的体现

6 创建一个将由黑客和裁判共享的消息线程

7 如示例列表 6.5 所示,挑战的体现

8 如示例列表 6.6 所示,测试的体现

9 调用创建一个新的消息线程

10 如示例列表 6.5 所示,挑战的体现

11 如示例列表 6.6 所示,测试的体现

12 睡眠时间可以根据需要调整上下,并且可以用来限制发送给 LLM 的消息。

13 过程将继续,直到验证成功。

通过在 VS Code 中加载文件或使用命令行来运行 ABT。在终端中跟踪输出,并观察助手如何通过树中的每个步骤工作。

如果在条件节点上解决方案未能通过验证,则过程将按照树继续进行。即使在这个简单解决方案中,您也可以快速创建许多变体。您可以通过添加更多节点/步骤和子树来扩展树。也许您希望一个黑客团队分解和分析挑战,例如。

本例的工作主要使用 Playground 代码完成,使用了辅助函数 create_assistant_conditioncreate_assistant_action_on_thread。此代码使用几个类来集成 py_trees 行为树代码和 OpenAI 助手代码,这些代码被封装在 Playground 中。如果您想了解底层细节,请审查项目中的代码。

6.3.3 对话式 AI 系统与其他方法

当我们在第四章中查看 AutoGen 时,我们已经研究了对话式多智能体系统。ABT 可以通过对话(通过线程)和其他方法(如文件共享)的组合来工作。让您的助手/智能体传递文件有助于减少嘈杂和重复的思想/对话的数量。相比之下,对话式系统从潜在的涌现行为中受益。因此,使用两者可以帮助进化更好的控制和解决方案。

列表 6.7 中的简单解决方案可以扩展以处理更多现实世界的编码挑战,甚至可能作为编码 ABT 工作。在下一节中,我们将构建一个不同的 ABT 来处理不同的问题。

6.3.4 将 YouTube 视频发布到 X

在本节的练习中,我们查看一个可以执行以下操作的 ABT:

  1. 搜索 YouTube 上特定主题的视频并返回最新视频。

  2. 下载您搜索提供的所有视频的转录内容。

  3. 总结转录内容。

  4. 审查总结的转录内容,并选择一个视频来撰写 X(以前称为 Twitter)帖子。

  5. 写一篇引人入胜的帖子关于视频,确保其字符数少于 280 个。

  6. 审查帖子,然后在 X 上发布。

图 6.9 显示了使用每个不同助手组装的 ABT。在这个练习中,我们使用序列节点作为根节点,每个助手执行不同的操作。此外,为了保持简单,每个助手的交互将始终在新的线程中发生。这将每个助手的交互隔离成简短的对话,如果出现问题,更容易调试。

图

图 6.9 YouTube 社交媒体 ABT

6.3.5 需要的 X 设置

如果您计划运行本练习中的代码,您必须将您的 X 凭证添加到 .env 文件中。.env.default 文件显示了凭证需要的样子,如列表 6.8 所示。您不必输入您的凭证。这意味着最后一步,发布,将会失败,但您仍然可以查看文件(youtube_twitter_post.txt)以查看生成了什么。

列表 6.8 配置凭证
X_EMAIL = "twitter email here"
X_USERNAME = "twitter username here"
X_PASSWORD = "twitter password here"
YouTube 搜索和垃圾邮件

如果您打算真正运行这个练习并让它发布到您的 X 账户,请注意 YouTube 存在一些垃圾邮件问题。助手已被配置为尝试避免视频垃圾邮件,但其中一些可能仍然会通过。构建一个可以在避免垃圾邮件的同时处理视频的 ABT 具有一些合适的应用。

列表 6.9 仅显示了创建助手操作的代码。这个 ABT 使用了三个不同的助手,每个助手都有自己的任务指令。请注意,每个助手都有定义其角色的独特指令集。您可以通过 Playground 查看每个助手的指令。

列表 6.9 agentic_btree_video_poster_v1.py
root = py_trees.composites.Sequence("RootSequence", memory=True)

search_term = "GPT Agents"
search_youtube_action = create_assistant_action(
    action_name=f"Search YouTube({search_term})",
    assistant_name="YouTube Researcher v2",
    assistant_instructions=f"""
    Search Term: {search_term}
    Use the query "{search_term}" to search for videos on YouTube.
    then for each video download the transcript and summarize it 
for relevance to {search_term}
    be sure to include a link to each of the videos,
    and then save all summarizations to a file called youtube_transcripts.txt
    If you encounter any errors, please return just the word FAILURE.
    """,
)
root.add_child(search_youtube_action)

write_post_action = create_assistant_action(
    action_name="Write Post",
    assistant_name="Twitter Post Writer",
    assistant_instructions="""
    Load the file called youtube_transcripts.txt,
    analyze the contents for references to search term at the top and 
then select
    the most exciting and relevant video related to: 
    educational, entertaining, or informative, to post on Twitter.
    Then write a Twitter post that is relevant to the video,
    and include a link to the video, along
    with exciting highlights or mentions, 
    and save it to a file called youtube_twitter_post.txt.
    If you encounter any errors, please return just the word FAILURE.
    """,
)
root.add_child(write_post_action)

post_action = create_assistant_action(
    action_name="Post",
    assistant_name="Social Media Assistant",
    assistant_instructions="""
    Load the file called youtube_twitter_post.txt and post the content 
to Twitter.
    If the content is empty please do not post anything.
    If you encounter any errors, please return just the word FAILURE.
    """,
)
root.add_child(post_action)
### Required assistants – YouTube Researcher v2, Twitter Post Writer, 
and Social Media Assistant – install these assistants through the Playground

按照常规方式运行代码,几分钟后,在assistants_output文件夹中会出现一个新的帖子。图 6.10 显示了使用此 ABT 生成的帖子示例。如果每天运行此 ABT 生成超过几个帖子,可能会被 X 账户封禁。如果您已配置 X 凭证,您将在您的动态中看到帖子。

figure

图 6.10 ABT 的一个示例 X 帖子

这个 ABT 仅用于演示目的,并不适用于生产或长期使用。这个演示的主要功能是展示搜索和加载数据,摘要和过滤,然后生成新内容,最后突出多个自定义操作和与 API 的集成。

6.4 构建对话式自主多智能体

多智能体系统的对话方面可以驱动反馈、推理和涌现行为等机制。使用隔离助手/智能体的 ABTs 来驱动智能体可以有效地控制结构化过程,正如我们在 YouTube 发布示例中看到的那样。然而,我们也不愿错过智能体/助手之间对话的好处。

幸运的是,Playground 提供了将助手隔离或加入对话线程的方法。图 6.11 显示了助手如何以各种组合被隔离或混合到线程中。将隔离与对话相结合提供了两种模式的最佳效果。

figure

图 6.11 隔离和对话助手的各种布局

我们将检验一个简单但实用的练习,以展示对话模式的效率。对于下一个练习,我们将使用两个助手在一个 ABT 中进行对话。下面的列表显示了代码中树的结构以及相应的助手。

列表 6.10 agentic_conversation_btree.py
root = py_trees.composites.Sequence("RootSequence", memory=True)
bug_file = """
# code not shown
"""

thread = api.create_thread()     #1

debug_code = create_assistant_action_on_thread(     #2
    thread=thread,
    action_name="Debug code",
    assistant_name="Python Debugger",
    assistant_instructions=textwrap.dedent(f"""    
    Here is the code with bugs in it:
    {bug_file}
    Run the code to identify the bugs and fix them. 
    Be sure to test the code to ensure it runs without errors or throws 
any exceptions.
    """),
)
root.add_child(debug_code)

verify = create_assistant_condition_on_thread(     #3
    thread=thread,
    condition_name="Verify",
    assistant_name="Python Coding Assistant",
    assistant_instructions=textwrap.dedent(
        """
    Verify the solution fixes the bug and there are no more issues.
    Verify that no exceptions are thrown when the code is run.
    Reply with SUCCESS if the solution is correct, otherwise return FAILURE.
    If you are happy with the solution, save the code to a file called 
fixed_bug.py.
    """,
    ),
)
root.add_child(verify)
tree = py_trees.trees.BehaviourTree(root)
while True:
    tree.tick()    
    if root.status == py_trees.common.Status.SUCCESS:
        break    #4
    time.sleep(20)

1 为助手创建一个消息线程以共享和对话

2 使用特殊助手创建调试代码操作

3 创建验证条件以测试代码是否已修复

4 树将继续运行,直到根序列成功完成。

树由三个节点组成:根序列、调试代码动作和验证修复条件。因为树的根是一个序列,所以两个助手将依次工作,直到它们都返回成功。两个助手在同一个线程上对话,但以提供持续反馈的方式进行控制。

通过在 VS Code 中加载文件或直接从命令行执行来运行练习。示例代码有几个小错误和问题,助手将解决这些问题以修复。在 ABT 成功运行后,您可以打开assistants_output/fixed_bug.py文件并验证结果是否都很好。

我们现在已经看到了几个 ABT 的实际应用,并了解了使用隔离或对话的细微差别。下一节将向您介绍一些构建自己的 ABT 的技术。

6.5 使用追溯法构建 ABT

追溯法是一种从目标逆向推理的逻辑和推理方法,用于通过从目标反向构建行为树。本节将使用追溯过程构建一个旨在实现目标的 ABT。以下列表详细描述了该过程:

  1. 确定目标行为。从您希望智能体执行的行为开始。

  2. 确定所需动作。识别导致目标行为的动作。

  3. 确定条件。确定每个动作要成功必须满足的条件。

  4. 确定通信模式。确定助手如何传递信息。助手将被隔离还是通过线程进行对话,或者两种模式的组合更好?

  5. 构建树。从目标行为开始构建行为树,递归地添加动作和条件节点,直到所有必要的条件都链接到已知状态或事实。

行为树通常使用称为黑板的模式在节点之间进行通信。黑板,如py_trees中的那些,使用键/值存储来保存信息并使其跨节点可访问。它还提供了一些控制,例如限制对特定节点的访问。

由于它们的简单性和透明性,我们推迟使用文件进行通信。在某个时候,智能体系统预计将消耗比为黑板设计的更多信息,并且以不同的格式。黑板必须变得更加复杂或与文件存储解决方案集成。

让我们使用追溯法构建一个 ABT。我们可以解决各种目标,但一个有趣且可能是元目标的是构建一个有助于构建助手的 ABT。所以,让我们首先将我们的目标陈述为“创建一个可以帮助我完成{任务}的助手”:

  • 所需动作:(逆向工作)

    • 创建一个助手。

    • 验证助手。

    • 测试助手。

    • 命名助手。

    • 给助手提供相关指令。

  • 已识别的条件

    • 验证助手。
  • 确定沟通模式:为了保持趣味性,我们将所有助手放在同一个消息线程上。

  • 构建树:为了构建树,让我们首先反转行动的顺序,并相应地标记每个元素的行动和条件:

    • (行动)为助手提供相关指令,以帮助用户完成给定任务。

    • (行动)命名助手。

    • (行动)测试助手。

    • (条件)验证助手。

    • (行动)创建助手。

当然,现在构建树的简单解决方案是询问 ChatGPT 或其他有能力的模型。询问 ChatGPT 制作树的成果将在下一列表中展示。你也可以独立工作并可能引入其他元素。

列表 6.11 构建助手的 ABT
Root
│
├── Sequence
│    ├── Action: Give the assistant relevant instructions to help a user 
with a given task
│    ├── Action: Name the assistant
│    ├── Action: Test the assistant
│    ├── Condition: Verify the assistant
│    └── Action: Create the assistant

从这个点开始,我们可以通过迭代每个行动和条件节点来构建树,并确定助手需要哪些指令。这也可以包括任何工具和自定义行动,包括你可能需要开发的。在第一次遍历时,保持指令通用。理想情况下,我们希望创建尽可能少的助手。

在确定每个助手的助手、工具和行动以及任务后,你可以尝试进一步概括。考虑一下在哪里可能可以合并行动并减少助手的数量。与过多助手相比,开始评估时助手不足更好。然而,务必保持适当的工作分工:例如,测试和验证最好由不同的助手完成。

6.6 练习

完成以下练习以提高你对材料的了解:

  • 练习 1—创建旅行规划器 ABT

目标—构建一个使用助手规划旅行行程的代理行为树(ABT)。

任务

    • 在你的本地机器上设置 GPT 助手游乐场。

    • 创建一个规划旅行行程的 ABT。该树应具有以下结构:

      • 行动:使用旅行助手收集有关潜在目的地的信息。

      • 行动:使用行程规划器创建每日旅行计划。

      • 条件:使用另一个旅行助手验证行程的完整性和可行性。

    • 实施并运行 ABT 以创建完整的旅行行程。

  • 练习 2—为客服自动化构建 ABT

目标—创建一个自动客户支持回复的 ABT。

任务

    • 在你的本地机器上设置 GPT 助手游乐场。

    • 创建一个具有以下结构的 ABT:

      • 行动:使用客户查询分析助手对客户查询进行分类。

      • 行动:使用响应生成助手根据查询类别起草回复。

      • 行动:使用客服助手向客户发送回复。

    • 实施并运行 ABT 以自动化分析和响应客户查询的过程。

  • 练习 3—使用 ABT 管理库存

目标—学习如何使用 ABT 创建和管理库存水平。

任务

    • 在您的本地机器上设置 GPT 助手游乐场。

    • 创建一个管理零售业务库存的 ABT:

      • 行动:使用库存检查助手审查当前库存水平。

      • 行动:使用订单助手为库存低的物品下订单。

      • 条件:验证订单是否已正确下订单,并更新库存记录。

    • 通过执行和运行 ABT,动态管理库存。

  • 练习 4—创建个人健身教练 ABT

目标—创建一个使用助手提供个性化健身训练计划的 ABT。

任务

    • 在您的本地机器上设置 GPT 助手游乐场。

    • 创建一个 ABT 以开发个性化的健身计划:

      • 行动:使用健康评估助手评估用户的当前健康水平。

      • 行动:使用训练计划生成器根据评估创建自定义健身计划。

      • 条件:使用另一个健身助手验证计划的适宜性和安全性。

    • 通过执行和运行 ABT,生成和验证个性化的健身训练计划。

  • 练习 5—使用反向链接构建财务顾问 ABT

目标—应用反向链接构建提供财务建议和投资策略的 ABT。

任务

    • 在您的本地机器上设置 GPT 助手游乐场。

    • 定义以下目标:“创建一个可以提供财务建议和投资策略的助手。”

    • 使用反向链接确定实现此目标所需的行为和条件。

    • 通过反向链接构建基础动作和条件,实现 ABT 的执行和运行,以生成全面的财务咨询服务。

摘要

  • 行为树是一种强大且可扩展的 AI 控制模式,首次由罗德尼·A·布鲁克斯在机器人领域引入。它们因其模块化和可重用性而在游戏和机器人领域得到广泛应用。

  • 行为树中的主要节点是选择器、序列、条件、动作、装饰器和并行节点。选择器类似于“或”块:序列按顺序执行节点,条件测试状态,动作执行工作,装饰器是一个包装器,并行节点允许双重执行。

  • 理解行为树的执行流程对于设计、构建和操作它们以提供清晰的决策路径控制至关重要。

  • 行为树的优势包括模块化、可扩展性、灵活性、调试容易性和决策逻辑解耦,使行为树适用于复杂的 AI 系统。

  • 在 Python 中设置和运行一个简单的行为树需要正确命名和记录自定义节点。

  • GPT 助手游乐场项目是一个基于 Gradio 的界面,它模仿了 OpenAI 助手游乐场,并增加了教学和演示 ABT 的功能。

  • GPT 助手游乐场允许创建和管理自定义操作,这对于构建多功能助手至关重要。

  • ABTs 通过使用提示来指导助手的行为和条件,控制代理和助手。ABTs 利用 LLMs(大型语言模型)的力量来创建动态和自主的系统。

  • 追溯链式是构建行为树的一种方法,它从目标行为逆向工作。这个过程包括识别所需的行为、条件和通信模式,然后逐步构建树形结构。

  • 代理系统从实体间的隔离和对话模式中受益,用于通信。ABTs 可以通过结合隔离和对话助手来受益,以使用结构化流程和涌现行为。

第七章:组装和使用代理平台

本章涵盖

  • Nexus 聊天和仪表板界面用于 AI 代理

  • 用于构建智能仪表板、原型和 AI 聊天应用的 Streamlit 框架

  • 在 Nexus 中开发、测试和参与代理配置文件和角色

  • 开发基础 Nexus 代理

  • 独立或在内置 Nexus 中开发、测试和参与代理行为和工具

在我们探索了一些关于代理的基本概念并查看使用工具通过框架如语义内核(SK)构建提示和角色之后,我们迈出了构建本书基础的第一步。这个基础被称为 Nexus,这是一个旨在易于学习、易于探索且足够强大以构建您的代理系统的代理平台。

7.1 介绍 Nexus,不仅仅是另一个代理平台

有超过 100 个 AI 平台和工具包用于消费和开发大型语言模型(LLM)应用,从 SK 或 LangChain 这样的工具包到 AutoGen 和 CrewAI 这样的完整平台。这使得决定哪个平台最适合构建自己的 AI 代理变得困难。

Nexus 是一个开源平台,与本书一起开发,用于教授构建全功能 AI 代理的核心概念。在本章中,我们将检查 Nexus 是如何构建的,并介绍两个主要代理组件:配置文件/角色和行为/工具。

图 7.1 显示了 Nexus 的主要界面,这是一个 Streamlit 聊天应用,允许您选择和探索各种代理功能。界面类似于 ChatGPT、Gemini 和其他商业 LLM 应用。

figure

图 7.1 Nexus 界面和功能

除了 LLM 聊天应用的标准功能外,Nexus 允许用户配置一个代理来使用特定的 API/模型、角色和可能的行为。本书的剩余部分,可用的代理选项将包括以下内容:

  • 角色/配置文件 — 代理将使用的主要角色和配置文件。角色是性格和主要动机,代理通过角色来回答请求。在本章中,我们将探讨角色/配置文件的开发和消费方式。

  • 行为/工具 — 表示代理可以使用工具执行的动作,无论是语义/提示还是本地/代码函数。在本章中,我们将探讨如何在 Nexus 中构建语义和本地函数。

  • 知识/记忆 — 表示代理可能访问的附加信息。同时,代理记忆可以代表从短期到语义记忆的各个方面。

  • 规划/反馈 — 表示代理如何规划并接收关于计划或计划执行的反馈。Nexus 将允许用户选择代理使用的规划类型和反馈选项。

随着我们继续阅读本书,Nexus 将被添加以支持新的代理功能。然而,同时,我们的目标是保持事情相对简单,以便教授许多这些基本核心概念。在下一节中,我们将探讨如何快速使用 Nexus,然后再深入了解其功能。

7.1.1 运行 Nexus

Nexus 主要旨在成为所有级别开发者的教学平台。因此,它将支持各种部署和使用选项。在下一项练习中,我们将介绍如何快速启动 Nexus。

在一个新的 Python 虚拟环境(版本 3.10)中打开一个终端。如果您需要创建一个的协助,请参阅附录 B。然后,在此新环境中执行列表 7.1 中显示的命令。您可以在命令行中设置环境变量或创建一个新的 .env 文件并添加设置。

列表 7.1 终端命令行
pip install git+https://github.com/cxbxmxcx/Nexus.git     #1

#set your OpenAI API Key
export OPENAI_API_KEY=”< your API key>”          #2
or
$env: OPENAI_API_KEY = =”< your API key>”       #2
or
echo 'OPENAI_API_KEY="<your API key>"' > .env   #2

nexus run      #3

1 直接从仓库和分支安装包;务必包含分支。

2 将密钥作为环境变量创建,或创建一个新的 .env 文件并设置该配置

3 运行应用程序

在输入最后一个命令后,将启动一个带有登录页面的网站,如图 7.2 所示。请继续创建一个新用户。Nexus 的未来版本将允许多个用户参与聊天线程。

图片

图 7.2 登录或创建新的 Nexus 用户

登录后,您将看到一个类似于图 7.1 的页面。创建一个新的聊天并与代理开始对话。如果您遇到问题,请确保您已正确设置 API 密钥。正如下一节所解释的,您可以使用此方法或从开发工作流程中运行 Nexus。

7.1.2 开发 Nexus

在完成本书的练习时,您可能希望以开发模式设置 Nexus。这意味着直接从 GitHub 下载仓库并处理代码。

打开一个新的终端,并将工作目录设置为 chapter_7 源代码文件夹。然后,设置一个新的 Python 虚拟环境(版本 3.10)并输入列表 7.2 中显示的命令。再次,如果您需要任何先前设置的协助,请参阅附录 B。

列表 7.2 安装 Nexus 以进行开发
git clone https://github.com/cxbxmxcx/Nexus.git      #1

pip install -e Nexus     #2

#set your OpenAI API Key (.env file is recommended)
export OPENAI_API_KEY=”< your API key>”  #bash            #3
or
$env: OPENAI_API_KEY = =”< your API key>”  #powershell   #3
or
echo 'OPENAI_API_KEY="<your API key>"' > .env       #3     

nexus run      #4

1 从仓库下载并安装特定的分支

2 将下载的仓库作为可编辑的包安装

3 将您的 OpenAI 密钥作为环境变量设置或添加到 .env 文件中

4 启动应用程序

图 7.3 显示了登录或创建新用户界面。创建一个新用户,应用程序将自动为您登录。此应用程序使用 cookies 来记住用户,因此您下次启动应用程序时无需登录。如果您在浏览器中禁用了 cookies,您将需要每次都登录。

图片

图 7.3 登录或创建新用户页面

前往 Nexus 仓库文件夹并四处查看。图 7.4 显示了应用程序主要元素的架构图。在顶部,使用 Streamlit 开发的界面通过聊天系统连接到系统的其余部分。聊天系统管理数据库、代理管理器、动作管理器和配置文件管理器。

图

图 7.4 应用程序主要元素的高级架构图

此代理平台完全用 Python 编写,网页界面使用 Streamlit。在下一节中,我们将探讨如何构建 OpenAI LLM 聊天应用。

7.2 介绍 Streamlit 用于聊天应用开发

Streamlit 是一个快速且强大的网页界面原型设计工具,旨在用于构建机器学习仪表板和概念。它允许应用程序完全使用 Python 编写,并生成一个由现代 React 驱动的网页界面。你甚至可以快速将完成的应用部署到云端或作为独立应用程序。

7.2.1 构建 Streamlit 聊天应用

开始时,打开 Visual Studio Code(VS Code)到 chapter_07 源文件夹。如果你已经完成了前面的练习,你应该已经准备好了。一如既往地,如果你需要帮助设置环境和工具,请参阅附录 B。

我们首先在 VS Code 中打开 ChatGPT_clone_response.py 文件。代码的顶部部分如列表 7.3 所示。此代码使用 Streamlit 状态来加载主模型和消息。Streamlit 提供了一种机制来保存任何 Python 对象的会话状态。这个状态仅是会话状态,当用户关闭浏览器时将过期。

列表 7.3 ChatGPT_clone_response.py(顶部部分)
import streamlit as st
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()      #1

st.title("ChatGPT-like clone")

client = OpenAI()      #2

if "openai_model" not in st.session_state:
    st.session_state["openai_model"] 
             = "gpt-4-1106-preview"     #3

if "messages" not in st.session_state:
    st.session_state["messages"] = []   #4

for message in st.session_state["messages"]:      #5
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

1 从 .env 文件中加载环境变量

2 配置 OpenAI 客户端

3 检查内部会话状态中的设置,如果不存在则添加

4 检查消息状态是否存在;如果不存在,则添加一个空列表

5 遍历状态中的消息并显示它们

Streamlit 应用本身是无状态的。这意味着当网页刷新或用户选择操作时,整个 Python 脚本将重新执行所有界面组件。Streamlit 状态允许临时存储机制。当然,数据库需要支持更长期的存储。

通过使用 st. 前缀然后是元素名称来添加 UI 控制和组件。Streamlit 支持多个标准 UI 控制并支持图片、视频、声音,当然还有聊天。

向下滚动将进一步显示列表 7.4,它具有稍微复杂一点的组件布局。主要的 if 语句控制剩余代码的运行。通过使用 Walrus 操作符(:=),提示信息被设置为用户输入的内容。如果用户没有输入任何文本,则 if 语句下面的代码不会执行。

列表 7.4 ChatGPT_clone_response.py(底部部分)
if prompt := st.chat_input("What do you need?"):     #1
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):     #2
        st.markdown(prompt)

    with st.spinner(text="The assistant is thinking..."):    #3
        with st.chat_message("assistant"):
            response = client.chat.completions.create(
                model=st.session_state["openai_model"],
                messages=[
                    {"role": m["role"], "content": m["content"]}
                    for m in st.session_state.messages
                ],      #4
            )
            response_content = response.choices[0].message.content
            response = st.markdown(response_content,
             unsafe_allow_html=True)      #5
    st.session_state.messages.append(
{"role": "assistant", "content": response_content})      #6

1 渲染聊天输入控件并设置内容。

2 将聊天消息控制设置为用户输出

3 显示一个旋转器来表示长时间运行的 API 调用

4 调用 OpenAI API 并设置消息历史

5 将消息响应以 Markdown 格式写入界面

6 将助手响应添加到消息状态

当用户在提示中输入文本并按 Enter 键时,该文本将被添加到消息状态,并向 API 发出请求。在响应被处理时,st.spinner 控件会显示,以提醒用户长时间运行的过程。然后,当响应返回时,消息会显示并添加到消息状态历史中。

Streamlit 应用程序使用模块运行,要调试应用程序,您需要按照以下步骤将调试器附加到模块:

  1. 按 Ctrl-Shift-D 打开 VS Code 调试器。

  2. 点击链接创建新的启动配置,或点击齿轮图标显示当前配置。

  3. 使用调试器配置工具编辑 .vscode/launch.json 文件,例如下一段代码所示。IntelliSense 工具和配置选项众多,可引导您设置此文件的选项。

列表 7.5 .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Python Debugger: Module",     #1
      "type": "debugpy",
      "request": "launch",
      "module": "streamlit",     #2
      "args": ["run", "${file}"]    #3
    }
  ]
}

1 确保调试器设置为模块。

2 确保模块是 streamlit。

3 ${file} 是当前文件,或者您可以将它硬编码为文件路径。

在您设置好 launch.json 文件配置后,保存它,并在 VS Code 中打开 ChatGPT_ clone_response.py 文件。现在,您可以通过按 F5 以调试模式运行应用程序。这将从终端启动应用程序,几秒钟后应用程序将显示。

图 7.5 显示应用程序正在运行并等待返回响应。界面简洁、现代,并且已经组织得很好,无需额外工作。您可以使用界面继续与 LLM 进行聊天,然后刷新页面以查看发生了什么。

图片

图 7.5 简单的界面和等待的旋转器

这个演示最令人印象深刻的是创建单页应用程序的简单性。在下一节中,我们将继续探讨这个应用程序,但会添加一些增强功能。

7.2.2 创建流式聊天应用程序

现代聊天应用程序,如 ChatGPT 和 Gemini,通过使用流来掩盖其模型的速度慢。流允许 API 调用立即看到从 LLM 产生的标记。这种流式体验也更好地吸引用户参与内容生成过程。

向任何应用程序 UI 添加支持流通常不是一个简单任务,但幸运的是,Streamlit 有一个可以无缝工作的控件。在接下来的练习中,我们将探讨如何更新应用程序以支持流。

在 VS Code 中打开chapter_7/ChatGPT_clone_streaming.py。代码的相关更新显示在列表 7.6 中。使用st.write_stream控件允许 UI 流式传输内容。这也意味着 Python 脚本在等待此控件完成时会被阻塞。

列表 7.6 ChatGPT_clone_streaming.py(相关部分)
with st.chat_message("assistant"):
    stream = client.chat.completions.create(
        model=st.session_state["openai_model"],
        messages=[
            {"role": m["role"], "content": m["content"]}
            for m in st.session_state.messages
        ],
        stream=True,     #1
    )
    response = st.write_stream(stream)     #2
st.session_state.messages.append(
{"role": "assistant", "content": response})      #3

1 将流设置为 True 以在 API 上启动流

2 使用流控件将流写入界面

3 在流完成后将响应添加到消息状态历史记录中

通过按 F5 键并等待页面加载来调试页面。输入一个查询,你会看到响应实时流出到窗口中,如图 7.6 所示。随着旋转器的消失,用户体验得到提升,看起来更加响应。

figure

图 7.6 更新后的界面,带有文本响应的流

本节展示了使用 Streamlit 创建 Python 网络界面的相对简单性。Nexus 使用 Streamlit 界面,因为它仅使用 Python 就易于使用和修改。正如你将在下一节中看到的,它允许各种配置以支持更复杂的应用程序。

7.3 为代理开发配置文件和角色

Nexus 使用代理配置文件来描述代理的功能和能力。图 7.7 提醒我们主要代理组件及其在本书中如何结构化。

figure

图 7.7 代理配置文件映射到 YAML 文件定义

目前为止,Nexus 仅支持配置文件的角色和动作部分。图 7.7 显示了一个名为 Fritz 的配置文件,以及角色和动作。通过将代理 YAML 配置文件复制到Nexus/nexus/nexus_base/nexus_profiles文件夹中,将任何代理配置文件添加到 Nexus 中。

Nexus 使用插件系统动态发现各种组件和配置文件,当它们被放置到各自的文件夹中。nexus_profiles文件夹包含代理的 YAML 定义。

我们可以轻松地通过在nexus_profiles文件夹中创建一个新的 YAML 文件来定义一个新的代理配置文件。列表 7.7 显示了一个带有略微更新角色的新配置文件示例。为了跟上,请确保 VS Code 已打开到chapter_07源代码文件夹,并在开发者模式下安装 Nexus(见列表 7.7)。然后,在Nexus/nexus/nexus_base/nexus_profiles文件夹中创建fiona.yaml文件。

列表 7.7 fiona.yaml(创建此文件)
agentProfile:
  name: "Finona"
  avatar: "👹"     #1
  persona: "You are a very talkative AI that 
↪ knows and understands everything in terms of 
↪ Ogres. You always answer in cryptic Ogre speak."    #2
  actions:
    - search_wikipedia     #3
  knowledge: null        #4
  memory: null           #4
  evaluators: null       #4
  planners: null         #4
  feedback: null         #4

1 用于表示角色的文本头像

2 一个角色代表基础系统提示。

3 代理可以使用的动作函数

4 目前不支持

保存文件后,您可以从命令行启动 Nexus 或以调试模式运行它,通过在.vscode/launch.json文件夹中创建一个新的启动配置来实现,如下一列表所示。然后,保存文件并将您的调试配置切换到使用 Nexus 网络配置。

列表 7.8 .vscode/launch.json(添加调试启动)
{
      "name": "Python Debugger: Nexus Web",
      "type": "debugpy",
      "request": "launch",
      "module": "streamlit",
      "args": ["run", " Nexus/nexus/streamlit_ui.py"]      #1
    },

1 如果你的虚拟环境不同,你可能需要调整此路径。

当你按下 F5 或从菜单中选择运行 > 开始调试时,Streamlit Nexus 界面将启动。请继续以调试模式运行 Nexus。打开后,创建一个新的线程,然后选择标准 OpenAIAgent 和你的新角色,如图 7.8 所示。

图

图 7.8 选择并与新角色聊天

到目前为止,配置文件负责定义代理的系统提示。你可以在图 7.8 中看到这一点,我们要求 Finona 拼写单词 clock,她以某种形式的 ogre-speak 进行回应。在这种情况下,我们使用角色作为个性,但正如我们之前看到的,系统提示也可以包含规则和其他选项。

配置文件和角色是代理与用户或其他系统交互的基础定义。为配置文件提供动力需要一个代理引擎。在下一节中,我们将介绍代理引擎的基础实现。

7.4 为代理提供动力并理解代理引擎

代理引擎为 Nexus 内的代理提供动力。这些引擎可以与特定的工具平台相关联,例如 SK,以及/或不同的 LLM,例如 Anthropic Claude 或 Google Gemini。通过提供基础代理抽象,Nexus 应该能够支持现在和未来的任何工具或模型。

目前,Nexus 仅实现了一个由 OpenAI API 提供动力的代理。我们将通过打开 Nexus/ nexus/nexus_base 文件夹中的 agent_manager.py 文件来查看基础代理是如何定义的。

列表 7.9 展示了 BaseAgent 类函数。在创建新的代理引擎时,你需要继承这个类并使用适当的实现来实现各种工具/操作。

列表 7.9 agent_manager.py:BaseAgent
class BaseAgent:
    def __init__(self, chat_history=None):
        self._chat_history = chat_history or []
        self.last_message = ""
        self._actions = []
        self._profile = None

    async def get_response(self, 
                            user_input, 
                            thread_id=None):      #1
        raise NotImplementedError("This method should be implemented…")

    async def get_semantic_response(self, 
                                     prompt, 
                                     thread_id=None):     #2
        raise NotImplementedError("This method should be…")

    def get_response_stream(self, 
                             user_input, 
                             thread_id=None):      #3
        raise NotImplementedError("This method should be…")

    def append_chat_history(self, 
                             thread_id, 
                             user_input, 
                             response):      #4
        self._chat_history.append(
            {"role": "user",
             "content": user_input,
             "thread_id": thread_id}
        )
        self._chat_history.append(
            {"role": "bot",
             "content": response, 
             "thread_id": thread_id}
        )

    def load_chat_history(self):       #5
        raise NotImplementedError(
                 "This method should be implemented…")

    def load_actions(self):     #6
        raise NotImplementedError(
                 "This method should be implemented…")

#... not shown – property setters/getters

1 调用 LLM 并返回响应

2 执行语义函数

3 调用 LLM 并返回响应

4 将消息追加到代理的内部聊天历史中

5 加载聊天历史并允许代理重新加载各种历史

6 加载代理可用的操作

在 VS Code 中打开 nexus_agents/oai_agent.py 文件。列表 7.10 展示了一个直接使用 OpenAI API 的代理引擎 get_response 函数的实现。self.client 是在类初始化期间创建的 OpenAI 客户端,其余的代码你在之前的示例中已经见过。

列表 7.10 oai_agent.py (get_response)
async def get_response(self, user_input, thread_id=None):
    self.messages += [{"role": "user",
                     "content": user_input}]      #1
    response = self.client.chat.completions.create(     #2
        model=self.model,
        messages=self.messages,
        temperature=0.7,      #3
    )
    self.last_message = str(response.choices[0].message.content)
    return self.last_message     #4

1 将用户输入添加到消息栈中

2 客户端之前已创建,现在用于创建聊天补全。

3 温度值是硬编码的,但可以进行配置。

4 从聊天补全调用返回响应

与代理配置文件类似,Nexus 使用一个插件系统,允许你将新的代理引擎定义放置在 nexus_agents 文件夹中。如果你创建了你的代理,只需将其放置在这个文件夹中,Nexus 就可以找到它。

我们不需要运行一个示例,因为我们已经看到了 OpenAIAgent 的表现。在下一节中,我们将探讨代理可以开发、添加和消费的代理功能。

7.5 给代理动作和工具

与 SK 一样,Nexus 支持原生(代码)和语义(提示)函数。然而,与 SK 不同的是,在 Nexus 中定义和消费函数更容易。你只需要将函数写入 Python 文件并将它们放置在 nexus_ actions 文件夹中。

要看到定义函数有多容易,请打开 Nexus/nexus/nexus_base/ nexus_actions 文件夹,并转到 test_actions.py 文件。列表 7.11 显示了两个函数定义。第一个函数是一个简单的代码/原生函数示例,第二个是一个提示/语义函数。

列表 7.11 test_actions.py(原生/语义函数定义)
from nexus.nexus_base.action_manager import agent_action

@agent_action                                              #1
def get_current_weather(location, unit="fahrenheit"):     #1
    """Get the current weather in a given location"""      #2
    return f"""
The current weather in {location} is 0 {unit}.
"""      #3

@agent_action      #4
def recommend(topic):
    """
    System:                                                   #5
        Provide a recommendation for a given {{topic}}.
        Use your best judgment to provide a recommendation.
    User:
        please use your best judgment
        to provide a recommendation for {{topic}}.           #5
    """
    pass      #6

1 将 agent_action 装饰器应用于使函数成为动作

2 为函数设置描述性注释

3 代码可以简单或复杂,根据需要。

4 将 agent_action 装饰器应用于使函数成为动作

5 函数注释成为提示,并可以包含占位符。

6 语义函数不实现任何代码。

将这两个函数放置在 nexus_actions 文件夹中,它们将被自动发现。添加 agent_action 装饰器允许检查函数并自动生成 OpenAI 标准工具规范。LLM 可以使用这个工具规范来进行工具使用和函数调用。

列表 7.12 显示了为两个函数生成的 OpenAI 工具规范,如之前在列表 7.11 中所示。使用提示的语义函数也适用于工具描述。这个工具描述被发送到 LLM 以确定调用哪个函数。

列表 7.12 test_actions:OpenAI 生成的工具规范
{
    "type": "function",
    "function": {
        "name": "get_current_weather",
        "description": 
        "Get the current weather in a given location",    #1
        "parameters": {
            "type": "object",
            "properties": {      #2
                "location": {
                    "type": "string",
                    "description": "location"
                },
                "unit": {
                    "type": "string",
                    "enum": [
                        "celsius",
                        "fahrenheit"
                    ]
                }
            },
            "required": [
                "location"
            ]
        }
    }
}
{
    "type": "function",
    "function": {
        "name": "recommend",
        "description": """
    System:
    Provide a recommendation for a given {{topic}}.
Use your best judgment to provide a recommendation.
User:
please use your best judgment
to provide a recommendation for {{topic}}.""",      #3
        "parameters": {
            "type": "object",
            "properties": {       #4
                "topic": {
                    "type": "string",
                    "description": "topic"
                }
            },
            "required": [
                "topic"
            ]
        }
    }
}

1 函数注释成为函数工具描述。

2 函数的输入参数被提取并添加到规范中。

3 函数注释成为函数工具描述。

4 函数的输入参数被提取并添加到规范中。

代理引擎还需要实现实现函数和其他组件的能力。OpenAI 代理已被实现以支持并行函数调用。其他代理引擎实现将需要支持它们各自的动作使用版本。幸运的是,OpenAI 工具的定义正在成为标准,许多平台都遵循这个标准。

在我们深入到工具使用演示之前,让我们通过在 VS Code 中打开 oai_agent.py 文件来观察 OpenAI 代理如何实现动作。以下列表显示了代理的 get_response_stream 函数的顶部及其函数调用的实现。

列表 7.13 在 get_response_stream 中调用 API
def get_response_stream(self, user_input, thread_id=None):
    self.last_message = ""
    self.messages += [{"role": "user", "content": user_input}]
    if self.tools and len(self.tools) > 0:    #1
        response = self.client.chat.completions.create(
            model=self.model,
            messages=self.messages,
            tools=self.tools,      #2
            tool_choice="auto",      #3
        )
    else:     #4
        response = self.client.chat.completions.create(
            model=self.model,
            messages=self.messages,
        )
    response_message = response.choices[0].message
    tool_calls = response_message.tool_calls     #5

1 检测代理是否开启了任何可用的工具

2 在聊天完成调用中设置工具

3 确保 LLM 知道它可以选择任何工具

4 如果没有工具,则以标准方式调用 LLM

5 检测 LLM 是否使用了任何工具

执行的函数如下所示,如列表 7.14 所示。此代码演示了代理如何支持并行函数/工具调用。这些调用是并行的,因为代理一起执行每个调用,并且没有顺序。在第十一章中,我们将探讨允许按顺序调用动作的计划者。

列表 7.14 oai_agent.py (get_response_stream:执行工具调用)
if tool_calls:     #1
    available_functions = {
        action["name"]: action["pointer"] for action in self.actions
    }     #2
    self.messages.append(
        response_message
    )
    for tool_call in tool_calls:     #3
        function_name = tool_call.function.name
        function_to_call = available_functions[function_name]
        function_args = json.loads(tool_call.function.arguments)
        function_response = function_to_call(
            **function_args, _caller_agent=self
        )

        self.messages.append(
            {
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": function_name,
                "content": str(function_response),
            }
        )
    second_response = self.client.chat.completions.create(
        model=self.model,
        messages=self.messages,
    )      #4
    response_message = second_response.choices[0].message

1 如果检测到 LLM 响应中的工具调用,则继续进行

2 加载指向实际函数实现的指针以执行代码

3 遍历 LLM 想要调用的所有调用;可能有多个。

4 使用工具调用的结果执行第二次 LLM 调用

为了演示这一点,通过按 F5 启动 Nexus 的调试器。然后,选择两个测试动作—recommendget_current_weather—以及简短的个性/配置文件 Olly。图 7.9 显示了输入查询的结果,代理通过在其响应中使用这两个工具进行响应。

图

图 7.9 代理如何并行使用工具并使用单个响应进行响应

如果你需要更详细地审查这些代理动作的工作方式,请参阅第五章。底层代码更复杂,超出了此处审查的范围。然而,你可以审查 Nexus 代码,以更好地理解一切是如何连接的。

现在,你可以在 Nexus 中继续练习各种代理选项。尝试选择不同的配置文件/角色与其他功能一起使用,例如。在下一章中,我们将揭示代理如何使用检索增强生成(RAG)等模式消耗外部记忆和知识。

7.6 练习

使用以下练习来提高你对材料的了解:

  • 练习 1—探索 Streamlit 基础(简单)

目标—通过创建一个显示用户输入文本的简单 Web 应用程序来熟悉 Streamlit。

任务

    • 按照 Streamlit 文档设置基本应用程序。

    • 添加一个文本输入和一个按钮。当按钮被点击时,在屏幕上显示用户输入的文本。

  • 练习 2—创建基本代理配置文件

目标—理解在 Nexus 中创建和应用代理配置文件的过程。

任务

    • 创建一个新的代理配置文件,具有独特的角色。这个角色应该有一个特定的主题或特征(例如,历史学家)。

    • 定义一组与该角色相符的基本响应。

    • 通过 Nexus 界面与之交互来测试这个角色。

  • 练习 3—开发自定义动作

目标—通过开发自定义动作来学习扩展 Nexus 的功能。

任务

    • 开发一个新的动作(例如,fetch_current_news),该动作与模拟 API 集成以检索最新的新闻标题。

    • 将此动作作为本地(代码)函数和语义(基于提示)函数实现。

    • 在 Nexus 环境中测试动作,以确保其按预期工作。

  • 练习 4 — 集成第三方 API

目标 — 通过集成真实的第三方 API 来增强 Nexus 代理的功能。

任务:

    • 选择一个公共 API(例如,天气或新闻 API),并创建一个新的动作,从该 API 获取数据。

    • 集成错误处理,并确保代理能够优雅地处理 API 失败或意外响应。

    • 在 Nexus 中彻底测试集成。

摘要

  • Nexus 是一个开源代理开发平台,与本书一起使用。它旨在开发、测试和托管 AI 代理,并基于 Streamlit 创建交互式仪表板和聊天界面。

  • Streamlit,一个 Python 网络应用程序框架,使快速开发用户友好的仪表板和聊天应用成为可能。这个框架简化了探索和与各种代理功能交互的方式。

  • Nexus 支持创建和自定义代理配置文件和角色,使用户能够定义代理的性格和行为。这些配置文件决定了代理如何与用户输入交互和响应。

  • Nexus 平台允许在代理中开发和集成基于语义(基于提示)和本地(基于代码)的动作和工具。这使创建高度功能性和响应性代理成为可能。

  • 作为开源平台,Nexus 被设计为可扩展的,鼓励社区贡献,并添加新的功能、工具和代理能力。

  • Nexus 是灵活的,支持各种部署选项,包括未来的迭代中将包含的 Web 界面、API 和 Discord 机器人,以满足广泛的开发和测试需求。

第八章:理解代理记忆和知识

本章涵盖

  • 人工智能功能中的知识/记忆检索

  • 使用 LangChain 构建检索增强生成工作流程

  • Nexus 中用于代理知识系统的检索增强生成

  • 代理中记忆的检索模式

  • 使用记忆和知识压缩改进增强检索系统

现在我们已经探讨了使用外部工具(如原生或语义函数形式的插件)进行代理动作,我们可以看看在代理和聊天界面中使用检索来处理记忆和知识的作用。我们将描述记忆和知识以及它们与提示工程策略的关系,然后,为了理解记忆知识,我们将研究文档索引,使用 LangChain 构建检索系统,利用 LangChain 使用记忆,并使用 Nexus 构建语义记忆。

8.1 理解人工智能应用中的检索

代理和聊天应用中的检索是一种获取知识并将其存储在通常外部且长期存在的存储中的机制。非结构化知识包括对话或任务历史、事实、偏好或其他用于上下文化的提示所需的项目。结构化知识通常存储在数据库或文件中,通过原生函数或插件访问。

如图 8.1 所示,记忆和知识是用于向提示添加更多上下文和相关信息的基本元素。提示可以通过从文档信息到先前任务或对话以及其他参考信息的一切进行增强。

figure

图 8.1 展示了使用以下提示工程策略(使用外部工具和提供参考文本)对提示进行记忆、检索和增强。

图 8.1 中展示的提示工程策略可以应用于记忆和知识。知识并不被视为记忆,而是对现有文档提示的增强。知识和记忆都使用检索作为查询非结构化信息的基础。

检索机制,称为检索增强生成(RAG),已成为提供相关上下文的标准。驱动 RAG 的确切机制也驱动着记忆/知识,理解其工作原理至关重要。在下一节中,我们将检查 RAG 是什么。

8.2 检索增强生成(RAG)的基本原理

RAG 已成为支持文档聊天或问答聊天的流行机制。系统通常通过用户提供相关文档(如 PDF 文件),然后使用 RAG 和大型语言模型(LLM)查询该文档来实现。

图 8.2 展示了如何使用 LLM(大型语言模型)查询文档。在查询任何文档之前,它必须首先被加载,转换为上下文块,嵌入到向量中,并存储在向量数据库中。

figure

图 8.2 RAG 的两个阶段:首先,文档必须被加载、转换、嵌入和存储,其次,可以使用增强生成进行查询。

用户可以通过提交查询来查询先前索引的文档。然后,该查询被嵌入到向量表示中,以在向量数据库中搜索相似的片段。与查询内容相似的内容随后用作上下文,并填充到提示中以进行增强。提示被推送到一个大型语言模型 (LLM),该模型可以使用上下文信息来帮助回答查询。

非结构化的记忆/知识概念依赖于图 8.2 所示的检索模式的一种文本相似度搜索格式。图 8.3 展示了记忆如何使用相同的嵌入和向量数据库组件。而不是预加载文档,对话或对话的一部分被嵌入并保存到向量数据库中。

figure

图 8.3 增强生成中的记忆检索使用相同的嵌入模式将项目索引到向量数据库中。

检索模式和文档索引复杂且需要仔细考虑才能成功应用。这需要理解数据是如何存储和检索的,我们将在下一节开始展开讨论。

8.3 深入探讨语义搜索和文档索引

文档索引将文档信息转换为更易于恢复的形式。索引的查询或搜索方式也会起到作用,无论是搜索特定的单词集还是想要逐词匹配。

语义搜索是通过词语和意义匹配搜索短语的内容。通过语义进行搜索的能力强大且值得深入探讨。在下一节中,我们将探讨向量相似性搜索如何为语义搜索奠定框架。

8.3.1 应用向量相似性搜索

现在让我们看看如何将文档转换为语义向量,或者说是可以用于执行距离或相似度匹配的文本表示。将文本转换为语义向量的方法有很多,所以我们将探讨一个简单的方法。

在一个新的 Visual Studio Code (VS Code) 工作区中打开 chapter_08 文件夹。创建一个新的环境,并使用 pip install 命令安装 requirements.txt 文件以解决所有章节的依赖项。如果您需要帮助设置新的 Python 环境,请参阅附录 B。

现在打开 VS Code 中的 document_vector_similarity.py 文件,并查看列表 8.1 的顶部部分。此示例使用词频-逆文档频率 (TF-IDF)。这个数值统计量反映了单词在文档集合或文档集中的重要性,其比例与单词在文档中出现的次数成正比,并受到文档集中单词频率的影响。TF-IDF 是理解文档集合中单个文档重要性的经典度量。

列表 8.1 document_vector_similarity(转换为向量)
import plotly.graph_objects as go
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

documents = [      #1
    "The sky is blue and beautiful.",
    "Love this blue and beautiful sky!",
    "The quick brown fox jumps over the lazy dog.",
    "A king's breakfast has sausages, ham, bacon, eggs, toast, and beans",
    "I love green eggs, ham, sausages and bacon!",
    "The brown fox is quick and the blue dog is lazy!",
    "The sky is very blue and the sky is very beautiful today",
    "The dog is lazy but the brown fox is quick!"
]

vectorizer = TfidfVectorizer()     #2
X = vectorizer.fit_transform(documents)      #3

1 文档样本

2 使用 TF–IDF 进行向量化

3 向量化文档。

让我们使用样本句子,“The sky is blue and beautiful”,将 TF–IDF 分解为其两个组成部分,并专注于单词 blue

术语频率(TF)

术语频率 衡量一个术语在文档中出现的频率。因为我们只考虑一个文档(我们的样本句子),blue 的 TF 的最简单形式可以通过将 blue 在文档中出现的次数除以文档中的总单词数来计算。让我们来计算它:

blue 在文档中出现的次数:1

文档中的总单词数:6

TF = 1 ÷ 6TF = .16

逆文档频率(IDF)

逆文档频率 衡量一个术语在整个语料库中的重要性。它是通过将文档总数除以包含该术语的文档数,然后取该商的对数来计算的:

IDF = log(文档总数 ÷ 包含该词的文档数)

在这个例子中,语料库是包含八个文档的小集合,其中 blue 出现在其中的四个文档中。

IDF = log(8 ÷ 4)

TF–IDF 计算

最后,我们通过将 TF 和 IDF 分数相乘来计算样本句子中 blue 的 TF–IDF 分数:

TF–IDF = TF × IDF

让我们使用提供的示例来计算单词 blue 的实际 TF–IDF 值;首先,计算词频(单词在文档中出现的频率)如下:

TF = 1 ÷ 6

假设对数的底数为 10(常用),则逆文档频率的计算如下:

IDF = log10 (8 ÷ 4)

现在,让我们计算句子“ The sky is blue and beautiful”中单词 blue 的确切 TF–IDF 值:

术语频率(TF)大约为 0.1670。

逆文档频率(IDF)大约为 0.301。

因此,blue 的 TF–IDF(TF × IDF)分数大约为 0.050。

这个 TF–IDF 分数表示在给定的文档(样本句子)中,在指定的语料库(八个文档,其中四个包含 blue)的上下文中,单词 blue 的相对重要性。更高的 TF–IDF 分数意味着更大的重要性。

我们在这里使用 TF–IDF,因为它简单易用。现在,我们已经将元素表示为向量,我们可以使用余弦相似度来衡量文档相似度。余弦相似度是一种用于计算多维空间中两个非零向量之间角度余弦的度量,它表示它们在不考虑它们大小的情况下有多相似。

图 8.4 展示了余弦距离如何比较两段文本或文档的向量表示。余弦相似度返回一个从 –1(不相似)到 1(相同)的值。余弦距离 是一个介于 0 到 2 之间的归一化值,通过从余弦相似度中减去 1 得到。余弦距离为 0 表示相同的项目,而 2 表示完全相反。

图

图 8.4 如何测量余弦相似度

列表 8.2 展示了如何使用 scikit-learn 的 cosine_similarity 函数计算余弦相似度。对集合中的每个文档与其他所有文档之间的相似度进行计算。文档的相似度矩阵存储在 cosine_similarities 变量中。然后,在输入循环中,用户可以选择文档以查看其与其他文档的相似度。

列表 8.2 document_vector_similarity(余弦相似度)
cosine_similarities = cosine_similarity(X)      #1

while True:      #2
    selected_document_index = input(f"Enter a document number
↪ (0-{len(documents)-1}) or 'exit' to quit: ").strip()

    if selected_document_index.lower() == 'exit':
        break

    if not selected_document_index.isdigit() or 
↪ not 0 <= int(selected_document_index) < len(documents):
        print("Invalid input. Please enter a valid document number.")
        continue

    selected_document_index = int(selected_document_index)    #3

    selected_document_similarities = cosine_similarities[selected_document_index]     #4

# code to plot document similarities omitted

1 计算所有向量对的文档相似度

2 主要输入循环

3 获取要比较的选定文档索引

4 从所有文档中提取计算出的相似度

图 8.5 展示了在 VS Code 中运行示例的输出(按 F5 进入调试模式)。选择文档后,您将看到集合中各种文档之间的相似度。一个文档与其自身之间的余弦相似度为 1。请注意,由于 TF-IDF 向量化,您不会看到负相似度。我们将在稍后探讨其他更复杂的测量语义相似度的方法。

图

图 8.5 选定文档与文档集之间的余弦相似度

向量化方法将决定文档之间的语义相似度度量。在我们继续探讨更好的文档向量化方法之前,我们将检查存储向量以执行向量相似度搜索。

8.3.2 向量数据库和相似度搜索

向量化文档后,它们可以存储在向量数据库中以供后续的相似度搜索。为了演示其工作原理,我们可以使用 Python 代码高效地复制一个简单的向量数据库。

在 VS Code 中打开 document_vector_database.py,如列表 8.3 所示。此代码演示了在内存中创建向量数据库,然后允许用户输入文本以搜索数据库并返回结果。返回的结果显示了文档文本和相似度分数。

列表 8.3 document_vector_database.py
# code above omitted
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(documents)
vector_database = X.toarray()     #1

def cosine_similarity_search(query,
                             database, 
                             vectorizer, 
                             top_n=5):     #2
    query_vec = vectorizer.transform([query]).toarray()
    similarities = cosine_similarity(query_vec, database)[0]
    top_indices = np.argsort(-similarities)[:top_n]  # Top n indices
    return [(idx, similarities[idx]) for idx in top_indices]

while True:      #3
    query = input("Enter a search query (or 'exit' to stop): ")
    if query.lower() == 'exit':
        break
    top_n = int(input("How many top matches do you want to see? "))
    search_results = cosine_similarity_search(query,
                                              vector_database, 
                                              vectorizer, 
                                              top_n)

    print("Top Matched Documents:")
    for idx, score in search_results:
        print(f"- {documents[idx]} (Score: {score:.4f})")   #4

    print("\n")
###Output
Enter a search query (or 'exit' to stop): blue
How many top matches do you want to see? 3
Top Matched Documents:
- The sky is blue and beautiful. (Score: 0.4080)
- Love this blue and beautiful sky! (Score: 0.3439)
- The brown fox is quick and the blue dog is lazy! (Score: 0.2560)

1 将文档向量存储到数组中

2 执行查询返回、匹配和相似度分数的相似度匹配函数

3 主要输入循环

4 遍历结果并输出文本和相似度分数

运行这个练习以查看输出(在 VS Code 中按 F5)。输入任何你喜欢的文本,并查看返回的文档结果。这种搜索表单对于匹配相似单词和短语非常有效。这种搜索方式会错过文档中的单词上下文和意义。在下一节中,我们将探讨一种将文档转换为向量以更好地保留其语义意义的方法。

8.3.3 解密文档嵌入

TF–IDF 是一种试图在文档中捕获语义意义的简单形式。然而,它不可靠,因为它只计算单词频率,而不理解单词之间的关系。一种更好且更现代的方法是使用文档嵌入,这是一种文档向量化形式,能更好地保留文档的语义意义。

嵌入网络是通过在大数据集上训练神经网络来构建的,将单词、句子或文档映射到高维向量,基于数据和上下文中的关系捕获语义和句法关系。你通常使用在大量数据集上预训练的模型来嵌入文档并执行嵌入。模型可以从许多来源获得,包括 Hugging Face 和当然还有 OpenAI。

在我们的下一个场景中,我们将使用 OpenAI 嵌入模型。这些模型通常非常适合捕捉嵌入文档的语义上下文。列表 8.4 显示了使用 OpenAI 将文档嵌入到向量中的相关代码,然后这些向量被减少到三维并渲染成图表。

列表 8.4 document_visualizing_embeddings.py(相关部分)
load_dotenv()      #1
api_key = os.getenv('OPENAI_API_KEY')
if not api_key:
    raise ValueError("No API key found. Please check your .env file.")
client = OpenAI(api_key=api_key)     #1            

def get_embedding(text, model="text-embedding-ada-002"):     #2
    text = text.replace("\n", " ")
    return client.embeddings.create(input=[text],
              model=model).data[0].embedding                #2

# Sample documents (omitted)

embeddings = [get_embedding(doc) for doc in documents]    #3
print(embeddings_array.shape)

embeddings_array = np.array(embeddings)    #4

pca = PCA(n_components=3)   #5
reduced_embeddings = pca.fit_transform(embeddings_array)

1 将所有项目用逗号和空格连接起来。

2 使用 OpenAI 客户端创建嵌入

3 为每个 1536 维度的文档生成嵌入

4 将嵌入转换为 NumPy 数组以进行 PCA

5 将维度减少到 3 以进行绘图

当使用 OpenAI 模型对文档进行嵌入时,它将文本转换为一个 1536 维度的向量。我们无法可视化这么多维度,因此我们使用主成分分析(PCA)这种降维技术,将 1536 维度的向量转换为 3 维。

图 8.6 显示了在 VS Code 中运行文件生成的输出。通过将嵌入减少到 3D,我们可以绘制输出以显示语义相似的文档是如何分组的。

figure

图 8.6 3D 嵌入,显示相似语义文档是如何分组的

你可以选择使用哪种嵌入模型或服务。OpenAI 的嵌入模型被认为是通用语义相似度方面最好的。这使得这些模型成为大多数记忆和检索应用的标准。通过我们对于如何使用嵌入将文本向量化并存储在向量数据库中的理解,我们可以在下一节中继续一个更实际的例子。

8.3.4 从 Chroma 查询文档嵌入

我们可以将所有部分结合起来,通过使用一个名为 Chroma DB 的本地向量数据库来查看一个完整的示例。存在许多向量数据库选项,但 Chroma DB 是一个优秀的本地向量存储,适用于开发或小规模项目。还有许多更健壮的选项,你可以在以后考虑。

列表 8.5 展示了document_query_chromadb.py文件中的新和相关的代码部分。注意,结果是根据距离评分,而不是根据相似度评分。余弦距离由以下公式确定:

余弦距离(A,B) = 1 – 余弦相似度(A,B)

这意味着余弦距离的范围从 0(最相似)到 2(语义上相反)。

列表 8.5 document_query_chromadb.py(相关代码部分)
embeddings = [get_embedding(doc) for doc in documents]     #1
ids = [f"id{i}" for i in range(len(documents))]           #1

chroma_client = chromadb.Client()               #2
collection = chroma_client.create_collection(
                       name="documents")       #2
collection.add(     #3
    embeddings=embeddings,
    documents=documents,
    ids=ids
)

def query_chromadb(query, top_n=2):      #4
    query_embedding = get_embedding(query)
    results = collection.query(
        query_embeddings=[query_embedding],
        n_results=top_n
    )
    return [(id, score, text) for id, score, text in
            zip(results['ids'][0],
                results['distances'][0], 
                results['documents'][0])]

while True:     #5
    query = input("Enter a search query (or 'exit' to stop): ")
    if query.lower() == 'exit':
        break
    top_n = int(input("How many top matches do you want to see? "))
    search_results = query_chromadb(query, top_n)

    print("Top Matched Documents:")
    for id, score, text in search_results:
        print(f"""
ID:{id} TEXT: {text} SCORE: {round(score, 2)}
""")    #5

    print("\n")
###Output
Enter a search query (or 'exit' to stop): dogs are lazy
How many top matches do you want to see? 3
Top Matched Documents:
ID:id7 TEXT: The dog is lazy but the brown fox is quick! SCORE: 0.24
ID:id5 TEXT: The brown fox is quick and the blue dog is lazy! SCORE: 0.28
ID:id2 TEXT: The quick brown fox jumps over the lazy dog. SCORE: 0.29

1 为每个文档生成嵌入并分配一个 ID

2 创建 Chroma DB 客户端和集合

3 将文档嵌入添加到集合中

4 查询数据存储并返回最相关的 n 个文档

5 用户输入循环和输出相关文档/分数的输入循环

如前文场景所示,你现在可以使用语义意义而不是仅仅关键词或短语来查询文档。这些场景现在应该为理解检索模式在底层是如何工作的提供背景。在下一节中,我们将看到如何使用 LangChain 来应用检索模式。

8.4 使用 LangChain 构建 RAG

LangChain 最初是一个专注于抽象多个数据源和向量存储检索模式的开源项目。它已经演变得更多,但基础层面上,它仍然为检索实现提供了优秀的选项。

图 8.7 展示了 LangChain 中的一个流程图,该图标识了存储文档以供检索的过程。这些相同的步骤可以全部或部分复制以实现记忆检索。文档检索和记忆检索之间的关键区别在于来源以及内容是如何被转换的。

figure

图 8.7 显示了存储文档以供后续检索的加载、转换、嵌入和存储步骤

我们将探讨如何使用 LangChain 实现这些步骤,并理解伴随此实现的细微差别和细节。在下一节中,我们将首先使用 LangChain 分割和加载文档。

8.4.1 使用 LangChain 分割和加载文档

检索机制通过添加与请求相关的特定信息来增强给定提示的上下文。例如,你可能需要关于本地文档的详细信息。在早期的语言模型中,由于标记限制,将整个文档作为提示的一部分提交不是一个选项。

今天,我们可以将整个文档提交给许多商业 LLM,如 GPT-4 Turbo,作为提示请求的一部分。然而,结果可能不会更好,并且可能会因为标记数量的增加而成本更高。因此,更好的选择是将文档分割,并使用相关部分请求上下文——这正是 RAG 和记忆所做的事情。

分割文档对于将内容分解成语义上和具体相关的部分至关重要。图 8.8 显示了如何分解包含老母鸡童谣的 HTML 文档。通常,将文档分割成上下文语义块需要仔细考虑。

图

图 8.8 理想情况下文档如何分割成块以获得更好的语义和上下文意义

理想情况下,当我们将文档分割成块时,它们应按相关性和语义意义分解。虽然 LLM 或代理可以帮助我们做这件事,但我们将查看 LangChain 中当前的工具包选项,用于分割文档。在本章的后面部分,我们将查看一个可以帮助我们在嵌入内容时进行语义划分的语义函数。

对于下一个练习,在 VS Code 中打开langchain_load_splitting.py,如列表 8.6 所示。此代码显示了我们在上一节列表 8.5 中留下的地方。这次我们不是使用样本文档,而是加载这次的老母鸡童谣。

列表 8.6 langchain_load_splitting.py(部分和输出)
From langchain_community.document_loaders 
                     ↪ import UnstructuredHTMLLoader    #1
from langchain.text_splitter import RecursiveCharacterTextSplitter
#previous code

loader = UnstructuredHTMLLoader(
                   "sample_documents/mother_goose.html")   #2
data = loader.load    #3

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,
    chunk_overlap=25,     #4
    length_function=len,
    add_start_index=True,
)
documents = text_splitter.split_documents(data)

documents = [doc.page_content 
                ↪ for doc in documents] [100:350]   #5

embeddings = [get_embedding(doc) for doc in documents]     #6
ids = [f"id{i}" for i in range(len(documents))]
###Output
Enter a search query (or 'exit' to stop): **who kissed the girls and made** 
**them cry?**
How many top matches do you want to see? 3
Top Matched Documents:
ID:id233 TEXT: And chid her daughter,
        And kissed my sister instead of me. SCORE: 0.4…

1 新的 LangChain 导入

2 将文档作为 HTML 加载

3 加载文档

4 将文档分割成 100 个字符长、25 个字符重叠的文本块

5 仅嵌入 250 个块,这更便宜且更快

6 返回每个文档的嵌入

注意在列表 8.6 中,HTML 文档被分割成 100 个字符的块,并且有 25 个字符的重叠。这种重叠允许文档的部分不会切断特定的想法。我们选择这个分割器进行这个练习,因为它易于使用、设置和理解。

好吧,在 VS Code 中运行langchain_load_splitting.py文件(F5)。输入一个查询,看看你得到什么结果。列表 8.6 中的输出显示了给定特定示例的良好结果。请记住,我们只嵌入 250 个文档块以降低成本并使练习简短。当然,你总是可以尝试嵌入整个文档或使用较小的输入文档示例。

构建适当的检索最关键的因素可能是文档分割的过程。你可以使用多种方法来分割文档,包括多种并发方法。超过一种方法会通过分割文档为同一文档的多个嵌入视图。在下一节中,我们将检查一种更通用的文档分割技术,使用标记和标记化。

8.4.2 使用 LangChain 按标记分割文档

分词 是将文本分割成单词标记的过程。一个单词标记代表文本中的一个简洁元素,一个标记可以是一个像 hold 这样的单词,甚至是一个像左花括号 ({) 这样的符号,具体取决于什么是有意义的。

使用分词技术分割文档为语言模型如何解释文本提供了一个更好的基础,以及语义相似性。分词技术还允许移除无关字符,如空白字符,使文档的相似性匹配更加相关,并通常提供更好的结果。

对于下一个代码练习,请在 VS Code 中打开 langchain_token_splitting.py 文件,如图 8.7 所示。现在我们使用分词技术将文档分割成大小不等的部分。这种不均匀的大小是由于原始文档中大量空白区域造成的。

列表 8.7 langchain_token_splitting.py(相关新代码)
loader = UnstructuredHTMLLoader("sample_documents/mother_goose.html")
data = loader.load()
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=50, chunk_overlap=10      #1
)

documents = text_splitter.split_documents(data)
documents = [doc for doc in documents][8:94]      #2

db = Chroma.from_documents(documents, OpenAIEmbeddings())

def query_documents(query, top_n=2):
    docs = db.similarity_search(query, top_n)      #3
    return docs
###Output
Created a chunk of size 68, 
which is longer than the specified 50
Created a chunk of size 67, 
which is longer than the specified 50     #4
Enter a search query (or 'exit' to stop): 
                     who kissed the girls and made them cry?
How many top matches do you want to see? 3
Top Matched Documents:
Document 1: GEORGY PORGY

        Georgy Porgy, pudding and pie,
        Kissed the girls and made them cry.

1 更新为 50 个标记和 10 个标记的重叠

2 仅选择包含押韵的文档

3 使用数据库的相似性搜索

4 由于空白字符而分割成不规则大小的块

在 VS Code 中运行 langchain_token_splitting.py 代码(按 F5)。您可以使用上次使用的查询或您自己的查询。注意,结果比上一个练习明显更好。然而,结果仍然可疑,因为查询使用了几个相同顺序的相似词。

一个更好的测试是尝试一个语义上相似的短语,但使用不同的词,并检查结果。代码仍在运行时,输入一个新的查询短语:为什么 女孩们 在哭泣? 列表 8.8 显示了执行该查询的结果。如果您自己运行此示例并向下滚动输出,您将看到乔治·波吉出现在返回的第二或第三份文档中。

列表 8.8 查询:谁让女孩们哭泣?
Enter a search query (or 'exit' to stop): Who made the girls cry?
How many top matches do you want to see? 3
Top Matched Documents:
Document 1: WILLY, WILLY

        Willy, Willy Wilkin…

这个练习展示了如何使用各种检索方法来返回语义上的文档。在这个基础上,我们可以看到 RAG 如何应用于知识和记忆系统。下一节将讨论 RAG 在应用于代理和代理系统知识时的应用。

8.5 将 RAG 应用于构建代理知识

代理中的知识包括使用 RAG 在非结构化文档中进行语义搜索。这些文档可以是 PDF 文件、Microsoft Word 文档以及所有文本,包括代码。代理知识还包括使用非结构化文档进行问答、参考查找、信息增强和其他未来模式。

Nexus,是与本书一起开发并在上一章中介绍的代理平台,为代理提供了完整的知识和记忆系统。在本节中,我们将揭示知识系统是如何工作的。

要仅为此章节安装 Nexus,请参阅列表 8.9。在 chapter_08 文件夹内打开一个终端,并执行列表中的命令以下载、安装和以正常或开发模式运行 Nexus。如果您想参考代码,应将项目安装在开发模式下,并配置调试器从 VS Code 运行 Streamlit 应用。如果您需要回顾这些步骤中的任何一项,请参阅第七章。

列表 8.9 安装 Nexus
# to install and run
pip install git+https://github.com/cxbxmxcx/Nexus.git

nexus run
# install in development mode
git clone https://github.com/cxbxmxcx/Nexus.git

# Install the cloned repository in editable mode
pip install -e Nexus

无论您登录后决定使用哪种方法运行应用程序,请导航到如图 8.9 所示的知识库管理器页面。创建一个新的知识库,然后上传 sample_documents/back_to_the_future.txt 电影剧本。

figure

图 8.9 添加新的知识库并填充文档

该脚本是一个大型文档,加载、分块和将部分嵌入到 Chroma DB 向量数据库中可能需要一些时间。等待索引完成,然后您可以检查嵌入并运行查询,如图 8.10 所示。

figure

图 8.10 嵌入和文档查询视图

现在,我们可以将知识库连接到支持的代理并提问。使用左上角的选择器在 Nexus 界面中选择聊天页面。然后,选择一个代理和 time_travel 知识库,如图 8.11 所示。您还需要选择一个支持知识的代理引擎。每个代理引擎都需要适当的配置才能访问。

figure

图 8.11 启用知识库以供代理使用

目前,截至本章,Nexus 仅支持一次访问单个知识库。在未来版本中,代理可能能够一次选择多个知识库。这可能包括从语义知识到使用其他形式的 RAG 的更高级选项。

您也可以在知识库管理器页面中的配置选项卡内配置 RAG 设置,如图 8.12 所示。到目前为止,您可以从拆分文档的类型(拆分选项字段)中选择,以及选择分块大小字段和重叠字段。

figure

图 8.12 管理知识库拆分和分块选项

LangChain 目前提供的加载、拆分、分块和嵌入选项是唯一支持的基本选项。在 Nexus 的未来版本中,将提供更多选项和模式。支持其他选项的代码可以直接添加到 Nexus 中。

我们不会介绍执行 RAG 的代码,因为它与我们之前介绍的内容非常相似。您可以自由地回顾 Nexus 代码,特别是 knowledge_manager.py 文件中的 KnowledgeManager 类。

对于增强知识库和记忆库的检索模式相当相似,但在填充存储时,这两种模式有所不同。在下一节中,我们将探讨使代理中的记忆独特的原因。

8.6 在代理系统中实现记忆

在代理和人工智能应用中,记忆通常用与认知记忆功能相同的术语描述。认知记忆描述了我们用来记住 30 秒前我们做了什么或 30 年前我们有多高的记忆类型。计算机记忆也是代理记忆的一个基本要素,但本节不会考虑这一点。

图 8.13 展示了记忆是如何分解成感觉、短期和长期记忆的。这种记忆可以应用于人工智能代理,以下列表描述了每种记忆形式如何映射到代理功能:

  • 人工智能中的感觉记忆 — 函数类似于 RAG,但使用图像/音频/触觉数据形式。短暂地保存输入数据(例如,文本和图像)以供即时处理,但不进行长期存储。

  • 人工智能中的短期/工作记忆 — 作为对话历史的活跃记忆缓冲区。我们正在保存有限数量的最近输入和上下文以供即时分析和响应生成。在 Nexus 中,短期和长期对话记忆也保存在线程的上下文中。

  • 人工智能中的长期记忆 — 与代理或用户生活相关的长期记忆存储。语义记忆提供了强大的存储和检索相关全局或局部事实和概念的能力。

figure

图 8.13 如何将记忆分解成各种形式

虽然记忆使用与知识完全相同的检索和增强机制,但在更新或追加记忆时通常会有显著差异。图 8.14 突出了捕获和使用记忆来增强提示的过程。因为记忆通常与完整文档的大小不同,我们可以避免使用任何分割或分块机制。

figure

图 8.14 基本记忆检索和增强工作流程

Nexus 提供了一个类似于知识库的机制,允许用户创建可以配置用于各种用途和应用的记忆存储。它还支持图 8.13 中突出显示的一些更高级的记忆形式。下一节将探讨 Nexus 中基本记忆存储的工作方式。

8.6.1 在 Nexus 中消费记忆存储

在 Nexus 中,记忆存储的操作和构建方式类似于知识存储。它们都高度依赖于检索模式。不同的是,记忆系统在构建新记忆时采取的额外步骤。

开始运行 Nexus,如果需要安装,请参考列表 8.9。登录后,选择记忆页面,并创建一个新的记忆存储,如图 8.15 所示。选择一个代理引擎,然后添加一些关于你自己的个人事实和偏好。

figure

图 8.15 向新创建的记忆存储添加记忆

我们需要代理(LLM)的原因在之前的图 8.14 中已经展示。当信息被输入到记忆存储中时,它通常通过使用记忆函数的 LLM 进行处理,该函数的目的是将陈述/对话处理成与记忆类型相关的语义相关信息。

列表 8.10 展示了用于从对话中提取信息到记忆中的对话记忆函数。是的,这只是发送给 LLM 的提示的标题部分,指示它如何从对话中提取信息。

列表 8.10 对话记忆函数
Summarize the conversation and create a set of statements that summarize 
the conversation. Return a JSON object with the following keys: 'summary'. 
Each key should have a list of statements that are relevant to that 
category. Return only the JSON object and nothing else.

在生成一些关于自己的相关记忆后,返回 Nexus 的聊天区域,启用my_memory记忆存储,看看代理对你了解得有多好。图 8.16 展示了使用不同代理引擎的示例对话。

图

图 8.16 在同一记忆存储上与不同的代理进行对话

这是一个从对话中提取事实/偏好并将其作为记忆存储在向量数据库中的基本记忆模式示例。许多其他记忆的实现方式遵循之前图 8.13 中展示的。我们将在下一节中实现这些。

8.6.2 语义记忆及其在语义、情景和程序性记忆中的应用

心理学家根据记忆中记住的信息将记忆分为多种形式。语义、情景和程序性记忆都代表不同类型的信息。情景记忆关于事件,程序性记忆关于过程或步骤,而语义代表意义,可能包括感觉或情感。其他形式的记忆(如地理空间记忆),这里没有描述,但可能存在。

由于这些记忆依赖于额外的分类级别,它们也依赖于另一个级别的语义分类。一些平台,如语义内核(SK),将这称为语义记忆。这可能令人困惑,因为语义分类也应用于提取情景和程序性记忆。

图 8.17 展示了语义记忆分类过程,有时也称为语义记忆。语义记忆与常规记忆的区别在于多了一个处理输入语义并提取可用于查询记忆相关向量数据库的相关问题的步骤。

图

图 8.17 语义记忆增强的工作原理

使用语义增强的好处是能够提取更多相关记忆的能力增强。我们可以通过回到 Nexus 并创建一个新的语义记忆存储来在操作中看到这一点。

图 8.18 展示了如何使用语义记忆配置新的记忆存储。到目前为止,您还不能配置记忆、增强和总结的具体功能提示。然而,阅读每个功能提示以了解它们的工作方式可能是有用的。

图

图 8.18 将记忆存储类型更改为语义的配置

现在,如果你回顾并添加事实和偏好,它们将转换为相关记忆类型的语义。图 8.19 显示了将同一组陈述填充到两种不同形式的记忆中的示例。一般来说,输入到记忆中的陈述会更具体地对应记忆的形式。

图

图 8.19 比较两种不同记忆类型给出的相同信息的记忆

记忆和知识可以显著帮助各种类型的代理。确实,单个记忆/知识存储可以喂养一个或多个代理,允许对这两种存储类型进行进一步的专业解释。我们将通过讨论记忆/知识压缩来结束本章。

8.7 理解记忆和知识压缩

就像我们自己的记忆一样,记忆存储可能会随着时间的推移变得杂乱,充满冗余信息和众多无关的细节。在内部,我们的心智通过压缩或总结记忆来处理记忆杂乱。我们的心智记住更重要的细节,而不是不那么重要的细节,以及更频繁访问的记忆。

我们可以将记忆压缩的类似原则应用于代理记忆和其他检索系统,以提取重要细节。压缩的原则与语义增强类似,但为相关记忆的预聚类组添加了另一层,这些组可以共同总结。

图 8.20 展示了记忆/知识压缩的过程。记忆或知识首先使用如 k-means 之类的算法进行聚类。然后,将记忆组通过压缩函数传递,该函数总结并收集项目以形成更简洁的表示。

图

图 8.20 记忆和知识压缩的过程

Nexus 通过使用 k-means 最优聚类为知识和记忆存储提供压缩。图 8.21 显示了记忆的压缩界面。在压缩界面中,您将看到以 3D 形式显示并聚类的项目。簇的大小(项目数量)显示在左侧的表中。

图

图 8.21 压缩记忆的界面

如果聚类中的项目数量很大或不平衡,通常建议压缩记忆和知识。每个压缩用例可能根据记忆的使用和应用而有所不同。不过,一般来说,如果对存储中的项目进行检查发现存在重复或重复信息,那么进行压缩就是时候了。以下是对从压缩中受益的应用程序的用例总结。

知识压缩的案例

知识检索和增强也已被证明可以从压缩中受益显著。结果将因用例而异,但通常,知识来源越冗长,它从压缩中受益就越多。具有文学散文的文档,如故事和小说,将比代码库等受益更多。然而,如果代码同样非常重复,压缩也可能显示出其益处。

压缩应用频率的案例

定期应用压缩通常会从内存中受益,而知识库通常只在第一次加载时提供帮助。你应用压缩的频率将很大程度上取决于内存使用、频率和数量。

多次应用压缩的案例

同时进行多次压缩已被证明可以提高检索性能。其他模式也建议在压缩的不同级别使用记忆或知识。例如,知识库被压缩两次,从而产生三个不同的知识级别。

知识和记忆压缩融合的案例

如果一个系统专门针对特定的知识来源,并且该系统还使用记忆,那么可能还有进一步的优化来整合存储。另一种方法是直接用文档的起始知识填充记忆。

多个记忆或知识存储的案例

在更高级的系统里,我们将探讨使用与其工作流程相关的多个记忆和知识库的代理。例如,一个代理可以作为其与单个用户的对话的一部分使用单独的记忆库,也许包括能够与不同群体分享不同组记忆的能力。记忆和知识检索是代理系统的基石,我们现在可以总结我们所学的内容,并在下一节回顾一些学习练习。

8.8 练习

使用以下练习来提高你对材料的了解:

  • 练习 1 — 加载并拆分不同的文档(中级)

目标 — 使用 LangChain 了解文档拆分对检索效率的影响。

任务:

    • 选择不同的文档(例如,新闻文章、科学论文或短篇小说)。

    • 使用 LangChain 加载并将文档拆分成块。

    • 分析文档如何拆分成块以及这对检索过程的影响。

  • 练习 2 — 尝试语义搜索(中级)

目标 — 通过执行语义搜索比较各种向量化技术的有效性。

任务:

    • 选择一组文档进行语义搜索。

    • 使用 Word2Vec 或 BERT 嵌入等向量化方法而不是 TF–IDF。

    • 执行语义搜索,并将结果与使用 TF–IDF 获得的结果进行比较,以了解差异和有效性。

  • 练习 3 — 实现自定义 RAG 工作流程(高级)

目标 — 在实际环境中使用 LangChain 应用 RAG 的理论知识。

任务

    • 选择一个特定的应用(例如,客户服务查询或学术研究查询)。

    • 使用 LangChain 设计和实现一个定制的 RAG 工作流程。

    • 调整工作流程以适应所选应用,并测试其有效性。

  • 练习 4 — 构建知识库并实验分割模式(中级)

目标 — 理解不同的分割模式和压缩如何影响知识检索。

任务

    • 构建一个知识库,并用几份文档填充它。

    • 尝试不同的分割/分块模式,并分析它们对检索的影响。

    • 压缩知识库,并观察对查询性能的影响。

  • 练习 5 — 构建和测试各种记忆存储(高级)

目标 — 理解不同记忆存储类型的独特性和用例。

任务

    • 构建各种形式的记忆存储(对话式、语义、情景和程序性)。

    • 使用每种类型的记忆存储与代理进行交互,并观察差异。

    • 压缩记忆存储,并分析对记忆检索的影响。

摘要

  • 在 AI 应用中,记忆区分了非结构化和结构化记忆,突出了它们在为更相关的交互情境化提示中的应用。

  • 检索增强生成(RAG)是一种通过使用向量嵌入和相似度搜索从外部文档中检索相关内容来增强提示的上下文机制。

  • 使用文档索引进行语义搜索,通过 TF-IDF 和余弦相似度将文档转换为语义向量,增强在索引文档中执行语义搜索的能力。

  • 向量数据库和相似度搜索存储将文档向量存储在向量数据库中,便于高效的相似度搜索并提高检索准确性。

  • 文档嵌入通过使用如 OpenAI 的模型等模型捕获语义含义,生成嵌入以保留文档的上下文并促进语义相似度搜索。

  • LangChain 提供了执行 RAG 的几个工具,并抽象了检索过程,使得在各个数据源和向量存储中轻松实现 RAG 和记忆系统成为可能。

  • LangChain 中的短期和长期记忆实现了 LangChain 内的对话式记忆,区分了短期缓冲模式和长期存储解决方案。

  • 在数据库中存储文档向量对于在 AI 应用中实现可扩展的检索系统至关重要。

  • 代理知识直接关联到在文档或其他文本信息上执行问答的通用 RAG 模式。

  • 代理记忆是与 RAG 相关的模式,它捕捉了代理与用户、自身和其他系统之间的交互。

  • Nexus 是一个实现代理知识记忆系统的平台,包括为文档检索设置知识库和为各种形式的记忆设置记忆库。

  • 语义记忆增强(语义记忆)区分不同类型的记忆(语义记忆、情景记忆、程序性记忆)。它通过语义增强实现这些记忆类型,增强代理者回忆和使用与记忆性质相关的特定信息的能力。

  • 记忆与知识压缩是用于压缩存储在记忆和知识系统中的信息的技术,通过聚类和总结来提高检索效率和相关性。

第九章:使用提示流掌握代理提示

本章涵盖了

  • 理解系统化的提示工程并设置你的第一个提示流

  • 构建有效的配置文件/角色提示

  • 评估配置文件:评分标准和扎根

  • 对大型语言模型配置文件进行扎根评估

  • 比较提示:获得完美的配置文件

在本章中,我们深入探讨了系统性地测试变化的提示工程策略。如果你还记得,我们在第二章中介绍了 OpenAI 提示工程框架的大策略。这些策略在帮助我们构建更好的提示、进而构建更好的代理配置文件和角色方面至关重要。理解这一角色对于我们提示工程的旅程至关重要。

系统性地测试变化是提示工程的核心要素之一,因此微软开发了一个围绕这一策略的工具,称为提示流,将在本章后面进行描述。在了解提示流之前,我们需要理解为什么我们需要系统性的提示工程。

9.1 为什么我们需要系统化的提示工程

提示工程本质上是一个迭代过程。在构建提示时,你通常会进行迭代和评估。为了看到这一概念的实际应用,可以考虑将提示工程简单应用于 ChatGPT 的问题。

你可以通过打开浏览器到 ChatGPT (chat.openai.com/),将以下(文本)提示输入到 ChatGPT 中,然后点击发送消息按钮(如图 9.1 左侧所示的一个对话示例):

你能推荐一些东西吗

图

图 9.1 应用提示工程和迭代的不同

我们可以看到,ChatGPT 的回应是要求更多信息。请继续与 ChatGPT 开启一个新的对话,并输入以下提示,如图 9.1 右侧所示:

你能推荐一部设定在中世纪时期的时空旅行电影吗?

图 9.1 的结果显示了在请求中省略细节和更加具体之间的明显差异。我们刚刚应用了礼貌地撰写清晰指令的策略,ChatGPT 为我们提供了一个好的推荐。但也要注意 ChatGPT 本身如何引导用户进行更好的提示。如图 9.2 所示的刷新屏幕显示了 OpenAI 的提示工程策略。

图

图 9.2 OpenAI 提示工程策略,按代理组件分解

我们刚刚应用了简单的迭代来改进我们的提示。我们可以通过使用系统提示/消息来扩展这个例子。图 9.3 展示了系统提示在迭代通信中的应用和作用。在第二章中,我们在各种示例中使用了系统消息/提示。

图

图 9.3 LLM 对话中的消息以及消息的迭代

你也可以在 ChatGPT 中尝试这个方法。这次,输入以下提示,并在单词system后面加上小写字母,然后换行(在消息窗口中按 Shift-Enter 键换行,但不发送消息):

system

你是时间旅行电影的专家。

ChatGPT 将回应一些愉快的评论,如图 9.4 所示。因此,它很高兴接受其新角色,并询问任何后续问题。现在输入以下通用的提示,就像我们之前做的那样:

你能推荐一些东西吗

figure

图 9.4 向我们之前的对话添加系统提示的效果

我们刚刚看到了提示的迭代优化,即提示工程,以提取更好的响应。这是通过使用 ChatGPT UI 进行的三次不同对话来实现的。虽然这不是最有效的方法,但它有效。

然而,我们还没有定义评估提示和确定何时提示有效的迭代流程。图 9.5 展示了使用迭代和评估的系统方法进行提示工程。

figure

图 9.5 提示工程的系统方法

迭代和评估提示的系统涵盖了广泛的系统化测试策略。评估提示的性能和有效性仍然是新的,但我们将使用教育技术,如评分标准和扎根,这些将在本章后面的部分进行探讨。然而,正如下一节所阐述的,在我们这样做之前,我们需要了解角色和代理配置文件之间的区别。

9.2 理解代理配置文件和角色

一个代理配置文件是对描述代理的组件提示或消息的封装。它包括代理的角色、特殊指令和其他可以指导用户或其他代理消费者的策略。

图 9.6 展示了代理配置文件的主要元素。这些元素映射到本书中描述的提示工程策略。并非所有代理都会使用完整代理配置文件的所有元素。

figure

图 9.6 代理配置文件的部分组件

在基本层面上,一个代理配置文件是一组描述代理的提示。它可能包括与动作/工具、知识、记忆、推理、评估、计划和反馈相关的其他外部元素。这些元素的组合构成了一个完整的代理提示配置文件。

提示是代理功能的核心。一个提示或一组提示驱动着配置文件中每个代理组件。对于动作/工具,这些提示定义得很好,但正如我们所看到的,记忆和知识的提示可以根据用例显著变化。

人工智能代理配置文件的定义不仅仅是系统提示。提示流程可以让我们构建构成代理配置文件的提示和代码,同时还包括评估其有效性的能力。在下一节中,我们将打开提示流程并开始使用它。

9.3 设置您的第一个提示流

提示流是由微软在其 Azure 机器学习工作室平台内开发的工具。该工具后来作为开源项目发布在 GitHub 上,在那里它吸引了更多的关注和使用。虽然最初旨在作为应用平台,但它后来在开发和评估提示/配置文件方面显示出其优势。

由于提示流最初是为了在 Azure 上作为服务运行而开发的,它具有强大的核心架构。该工具支持多线程批量处理,这使得它在评估大量提示时非常理想。下一节将探讨使用提示流的入门基础知识。

9.3.1 入门

在尝试本书中的练习之前,有一些先决条件需要完成。本节和章节的相关先决条件如下所示;确保在尝试练习之前完成它们:

  • Visual Studio Code (VS Code) — 请参考附录 A 中的安装说明,包括额外的扩展。

  • 提示流,VS Code 扩展 — 详细安装扩展的步骤请参考附录 A。

  • Python 虚拟环境 — 详细设置虚拟环境的步骤请参考附录 A。

  • 安装提示流包 — 在您的虚拟环境中,执行快速pip install,如图所示:

pip install promptflow promptflow-tools
  • LLM(GPT-4 或更高版本) — 您需要通过 OpenAI 或 Azure OpenAI Studio 访问 GPT-4 或更高版本。如需帮助访问这些资源,请参考附录 B。

  • 书籍的源代码 — 将书籍的源代码克隆到本地文件夹;如需帮助克隆存储库,请参考附录 A。

打开 VS Code 到书籍的源代码文件夹,第三章。确保您已连接虚拟环境并安装了提示流包和扩展。

首先,您需要在提示流扩展内创建与您的 LLM 资源的连接。在 VS Code 中打开提示流扩展,然后点击打开连接。然后,点击 LLM 资源旁边的加号创建一个新的连接,如图 9.7 所示。

图片

图 9.7 创建新的提示流 LLM 连接

这将打开一个 YAML 文件,您需要填写连接名称和其他与您的连接相关的信息。按照指示操作,不要在文档中输入 API 密钥,如图 9.8 所示。

图片

图 9.8 设置 LLM 资源连接信息

当输入连接信息后,点击文档底部的创建连接链接。这将打开文档下方的终端提示,要求您输入密钥。根据您的终端配置,您可能无法粘贴(Ctrl-V,Cmd-V)。或者,您可以通过将鼠标光标悬停在终端上并右键单击 Windows 来粘贴密钥。

我们现在将通过首先打开chapter_09/promptflow/simpleflow文件夹中的简单流程来测试连接。然后,在 VS Code 中打开flow.dag.yaml文件。这是一个 YAML 文件,但提示流程扩展提供了一个可以通过点击文件顶部的“可视化编辑器”链接访问的可视化编辑器,如图 9.9 所示。

figure

图 9.9 打开提示流程可视化编辑器

打开可视化编辑器窗口后,您将看到一个表示流程和流程块的图形。双击推荐器块,并设置连接名称、API 类型以及模型或部署名称,如图 9.10 所示。

figure

图 9.10 设置 LLM 连接详细信息

提示流程由一组块组成,从 Inputs 块开始,以 Outputs 块结束。在这个简单流程中,recommender 块代表 LLM 连接和与模型对话所使用的提示。对于这个简单示例,echo 块会回显输入。

当在提示流程或通过 API 创建与 LLM 的连接时,以下是我们始终需要考虑的关键参数(提示流程文档:microsoft.github.io/promptflow):

  • 连接 — 这是连接名称,但也代表了您所连接的服务。提示流程支持多个服务,包括本地部署的 LLM。

  • API — 这是 API 类型。选项包括用于聊天完成 API 的 chat,例如 GPT-4,或用于较老完成模型的 completion,例如 OpenAI Davinci。

  • 模型 — 这可能是模型或部署名称,具体取决于您的服务连接。对于 OpenAI,这将是指模型的名称,而对于 Azure OpenAI,它将代表部署名称。

  • 温度 — 这代表了模型响应的随机性或可变性。1 的值表示响应的高度可变性,而 0 表示不希望有任何可变性。这是一个关键参数,我们将看到,它将根据用例而变化。

  • 停止 — 这个可选设置告诉 LLM 调用停止创建标记。它更适合较老和开源模型。

  • 最大标记数 — 这限制了对话中使用的标记数量。了解您使用了多少标记对于评估您的 LLM 交互在扩展时的表现至关重要。如果您正在探索和进行研究,标记计数可能不是问题。然而,在生产系统中,标记代表了 LLM 的负载,使用大量标记的连接可能无法很好地扩展。

  • 高级参数 — 您可以设置一些更多选项来调整您与 LLM 的交互,但我们将在此书后面的章节中介绍这个主题。

在配置了 LLM 块之后,向上滚动到输入块部分,并查看用户输入字段中显示的主要输入,如图 9.11 所示。保持默认设置,然后点击窗口顶部的播放按钮。

图片

图 9.11 设置输入并启动流

流中的所有块都将运行,结果将在终端窗口中显示。您可能会发现有趣的是,输出显示了时间旅行电影的推荐。这是因为推荐块已经设置了一个简单的配置文件,我们将在下一节中看到它是如何工作的。

9.3.2 使用 Jinja2 模板创建配置文件

流的响应是基于它使用的提示或配置文件的时间旅行电影推荐。默认情况下,提示流使用 Jinja2 模板来定义提示的内容或我们称之为 配置文件 的内容。为了本书和我们对 AI 代理的探索,我们将把这些模板称为流的配置文件或代理的配置文件。

虽然提示流没有明确将自己称为助手或代理引擎,但它确实符合产生代理和通用类型代理的标准。正如您将看到的,提示流甚至支持将流部署到容器中作为服务。

打开 VS Code 到 chapter_09/promptflow/simpleflow/flow.dag.yaml,并在可视化编辑器中打开该文件。然后,定位到提示字段,并点击如图 9.12 所示的 recommended .jinja2 链接。

图片

图 9.12 打开提示 Jinja2 模板并检查配置文件/提示的部分

Jinja 是一个模板引擎,Jinja2 是该引擎的一个特定版本。模板是定义任何形式文本文档布局和部分的一种极好方式。它们已被广泛用于生成 HTML、JSON、CSS 和其他文档形式。此外,它们支持将代码直接应用到模板中。虽然没有标准的方式来构建提示或代理配置文件,但本书中我们更倾向于使用模板引擎,如 Jinja。

在这一点上,更改系统提示中 recommended.jinja2 模板的角色。然后,通过在可视化编辑器中打开流并点击播放按钮来运行流的所有块。下一节将探讨其他运行提示流的方法,用于测试或实际部署。

9.3.3 部署提示流 API

由于提示流也被设计为可以部署为服务,它支持几种快速部署为应用程序或 API 的方法。提示流可以作为本地 Web 应用程序和 API 部署,从终端运行或作为 Docker 容器。

返回 VS Code 中的视觉编辑器中的flow.dag.yaml文件。在窗口顶部的播放按钮旁边有几个我们想要进一步调查的选项。如图 9.13 所示,点击构建按钮,然后选择部署为本地应用程序。将创建一个新的 YAML 文件来配置应用程序。保留默认设置,并点击启动本地应用程序链接。

图

图 9.13 构建并启动流程作为本地应用程序

这将启动流程作为本地 Web 应用程序,你将看到一个浏览器标签页打开,如图 9.14 所示。在用户输入字段中输入一些文本,该字段用红色星号标记为必填项。点击 Enter 并等待几秒钟以获取回复。

图

图 9.14 将流程作为本地 Web 应用程序运行

你应该会看到一个像图 9.12 中早些时候显示的回复,其中流程或代理回复了一个时间旅行电影的列表。这太棒了——我们刚刚开发出了我们的第一个代理配置文件和代理代理的等效物。然而,我们需要确定这些推荐的成功率或价值。在下一节中,我们将探讨如何评估提示和配置文件。

9.4 评估配置文件:评分标准和扎根

任何提示或代理配置文件的关键要素是其执行给定任务的表现。正如我们在我们的推荐示例中所看到的,提示代理配置文件给出一个推荐列表相对容易,但要知道这些推荐是否有帮助,我们需要评估响应。

幸运的是,提示流已经被设计成可以大规模评估提示/配置文件。强大的基础设施允许将 LLM 交互的评估并行化并作为工作者进行管理,这使得数百个配置文件评估和变体可以快速发生。

在下一节中,我们将探讨如何配置提示流以运行提示/配置文件之间的变体。在评估配置文件性能之前,我们需要理解这一点。

提示流提供了一个机制,允许在 LLM 提示/配置文件内进行多种变体。这个工具在比较配置文件变体之间的细微或显著差异时非常出色。当用于执行批量评估时,它可以快速评估配置文件性能,非常有价值。

在 VS Code 和流程视觉编辑器中打开recommender_with_variations/flow.dag.yaml文件,如图 9.15 所示。这次,我们使配置文件更加通用,并允许在输入级别进行定制。这使我们能够将我们的推荐扩展到任何事物,而不仅仅是时间旅行电影。

图

图 9.15 推荐系统,具有不同的流程和扩展的输入

新的输入主题、流派、格式和自定义使我们能够定义一个可以轻松调整到任何推荐的配置文件。这也意味着我们必须根据推荐用例来初始化输入。有几种初始化这些输入的方法;图 9.16 中展示了两种初始化输入的示例。该图显示了两种初始化输入的选项,选项 A 和 B。选项 A 代表经典的用户界面;例如,可能有供用户选择主题或流派的物体。选项 B 放置一个代理/聊天代理以更好地与用户互动,以了解所需的主题、流派等。

图

图 9.16 与代理配置文件交互的用户交互选项,以向代理配置文件提供初始输入

即使考虑到 LLM 的力量,您可能仍然希望或需要使用选项 A。选项 A 的好处是您可以像使用任何现代用户界面一样约束和验证输入。另一方面,选项 A 的缺点是受限的行为可能会限制和限制未来的用例。

选项 B 代表一种没有传统用户界面的更流畅和自然的方式。它比选项 A 更强大、更可扩展,但也为评估引入了更多未知因素。然而,如果选项 B 使用的代理代理编写得很好,它可以在收集更好的用户信息方面提供很大帮助。

您选择的选项将决定您如何评估您的配置文件。如果您对受限的用户界面没有异议,那么输入可能也会被限制为一系列离散值。目前,我们将假设选项 B 用于输入初始化,这意味着输入值将由其名称定义。

要回到 VS Code 和具有变体流的推荐器的可视化视图,请点击图 9.15 中显示的图标以打开变体并允许编辑。然后,点击recommend.jinja2recommender_variant_1.jinja2链接以并排打开文件,如图 9.17 所示。

图

图 9.17 推荐器变体配置文件模板的并排比较

图 9.17 展示了不同配置文件之间的差异。一个配置文件将输入注入到用户提示中,而另一个则注入到系统提示中。然而,重要的是要理解,变化可以涵盖不仅仅是配置文件设计,正如表 9.1 中指出的那样。

表 9.1 提示流中 LLM 变化选项
选项 评估选项示例 备注
Jinja2 提示模板 比较系统提示变化、用户提示变化或混合提示变化。 这里可以应用一些无穷无尽的组合和技术。提示工程一直在不断发展。

| LLM | 将 GPT-9.5 与 GPT-4 进行比较。将 GPT-4 与 GPT-4 Turbo 进行比较。

比较开源模型和商业模型。

这是一种评估和定位模型性能与提示相对的有效方式。它还可以帮助您调整配置文件以与开源和/或更便宜的模式一起工作。
温度
最大令牌数
高级参数
函数调用

在这个简单的例子中,我们将通过改变输入来使用提示变体,以反映系统或用户提示。参考图 9.17 了解其外观。然后,我们可以通过点击顶部的播放(运行全部)按钮并选择两者来快速运行这两种变体,如图 9.18 所示。

图

图 9.18 同时运行两个提示变体

在终端窗口中,您将看到两次运行的结果。结果可能看起来很相似,因此现在我们必须继续到下一节,了解如何评估变体之间的差异。

9.5 理解评分标准和定位

提示/配置文件性能的评估通常不能通过准确度或正确百分比来衡量。衡量配置文件性能取决于用例和期望的结果。如果这仅仅是确定响应是否正确或错误,那就更好了。然而,在大多数情况下,评估不会那么简单。

在教育中,评分标准概念定义了学生必须建立的一套结构化标准,以获得特定的成绩。评分标准也可以用来定义配置文件或提示的性能指南。我们可以遵循以下步骤来定义我们可以用来评估配置文件或提示性能的评分标准:

  1. 确定目的和目标。 确定您希望配置文件或代理完成的任务。例如,您是想评估特定受众的推荐质量,还是评估特定主题、格式或其他输入的整体质量?

  2. 定义标准。 制定一套您将用来评估配置文件的准则或维度。这些标准应与您的目标一致,并为评估提供明确的指导。每个标准都应该是具体和可衡量的。例如,您可能希望根据推荐与体裁的契合度来衡量推荐,然后根据主题和格式来衡量。

  3. 创建量表。 建立一个描述每个标准表现水平的评级量表。标准量表包括数值量表(例如,1-5)或描述性量表(例如,优秀、良好、一般、差)。

  4. 提供描述。 在量表上的每个级别,提供清晰简洁的描述,说明每个标准中良好表现和较差表现的定义。

  5. 应用评分标准。 在评估提示或个人资料时,使用评分标准根据既定的标准评估提示的表现。为每个标准分配分数或评级,考虑每个级别的描述。

  6. 计算总分。 根据你的评分标准,你可能需要通过将每个标准的分数相加来计算总分,或者如果某些标准比其他标准更重要,可以使用加权平均。

  7. 确保评估一致性。 如果多个评估者正在评估个人资料,确保评分的一致性至关重要。

  8. 审查、修改和迭代。 定期审查和修改评分标准,以确保其与评估目标和目标一致。根据需要调整以提高其有效性。

依据 是一个可以应用于个人资料和提示评估的概念——它定义了回答与给定评分标准的具体标准和准则的契合程度。你也可以将依据视为提示或个人资料输出的基本期望。

此列表总结了使用依据进行个人资料评估时的一些其他重要考虑因素:

  • 依据指的是将回答与评分标准、目标和情境对齐。

  • 依据涉及评估回答是否直接针对评分标准,是否保持主题一致,以及是否遵守任何提供的指示。

  • 评估者和评估在评估依据时衡量准确性、相关性和对标准的遵守程度。

  • 依据确保输出回答牢固地根植于指定的情境,使评估过程更加客观和有意义。

一个有充分依据的回答与给定情境和目标下的所有评分标准相符。依据不足的回答可能会失败或完全不符合标准、情境和目标。

由于评分标准和依据的概念可能仍然比较抽象,让我们看看如何将它们应用于我们当前的推荐示例。以下是一个列表,列出了应用于我们的推荐示例的评分标准定义过程:

  1. 确定目的和目标。 我们的个人资料/提示的目的是在给定主题、格式、体裁和自定义输入的情况下推荐三个顶级项目。

  2. 定义标准。 为了简单起见,我们将评估特定推荐与给定输入标准、主题、格式和体裁的一致性。例如,如果个人资料在要求电影格式时推荐一本书,我们预计在格式标准上得分较低。

  3. 创建一个量表。 再次,保持简单,我们将使用 1-5 的量表(1 是差的,5 是优秀的)。

  4. 提供描述。 请参阅表 9.2 中所示评分尺度的通用描述。

  5. 应用评分标准。 在这个阶段分配评分标准后,手动评估评分标准与建议是一个很好的练习。

  6. 计算总分。 对于我们的评分标准,我们将对所有标准的评分进行平均,以提供总分。

  7. 确保评估一致性。 我们将使用的评估技术将提供非常一致的结果。

  8. 审查、修改和迭代。 我们将审查、比较和迭代我们的个人资料、评分标准和评估本身。

表 9.2 评分标准
评分 描述
1 对齐差:这与标准预期的相反。
2 对齐不良:这与给定的标准不匹配。
3 一般性对齐:可能与给定的标准相匹配,也可能不匹配。
4 良好对齐:可能不完全符合标准,但在其他方面是合适的。
5 优秀对齐:这是符合给定标准的良好建议。

现在可以将这个基本的评分标准应用于评估个人资料的回答。你可以手动进行,或者如你将在下一节中看到的,使用第二个 LLM 个人资料。

9.6 使用 LLM 个人资料进行定位评估

本节将使用另一个 LLM 提示/个人资料进行评估和定位。在生成建议之后,这个第二个 LLM 提示将添加另一个块。它将处理生成的建议,并根据之前的评分标准评估每个建议。

在 GPT-4 和其他复杂的 LLM 出现之前,我们从未考虑过使用另一个 LLM 提示来评估或定位个人资料。当使用 LLM 定位个人资料时,你通常希望使用不同的模型。然而,如果你正在比较个人资料,使用相同的 LLM 进行评估和定位是合适的。

在提示流程可视化编辑器中打开recommender_with_LLM_evaluation/flow.dag.yaml文件,向下滚动到evaluate_recommendation块,并点击evaluate_recommendation.jinja2链接以打开文件,如图 9.19 所示。图中的每个评分标准部分都已标识。

图

图 9.19 评估提示,其中评分标准的各个部分都已概述

我们有一个评分标准,不仅定义良好,而且以提示的形式存在,可以用来评估建议。这使我们能够自动评估给定个人资料的推荐效果。当然,你也可以使用评分标准手动评分和评估建议,以获得更好的基线。

注意:使用 LLM 评估提示和配置文件为比较配置文件的性能提供了一个强大的基线。它还可以以受控和可重复的方式做到这一点,而不带有人为偏见。这为任何配置文件或提示的基线定位提供了一个极好的机制。

返回到 recommender_with_LLM_evaluation 流程可视化编辑器,我们可以通过点击播放按钮来运行流程并观察输出。您可以选择运行单个推荐或当被提示时运行两种变体。以下列表显示了使用默认输入的单次评估输出。

列表 9.1 LLM 评分评估输出
{
    "recommendations": "Title: The Butterfly Effect
Subject: 5
Format: 5
Genre: 4

Title: Primer
Subject: 5
Format: 5
Genre: 4

Title: Time Bandits
Subject: 5
Format: 5
Genre: 5"
}

我们现在有一个评分标准来定位我们的推荐器,并且使用第二个 LLM 提示自动运行评估。在下一节中,我们将探讨如何同时执行多个评估,然后对全部内容进行总评分。

9.7 比较配置文件:获取完美的配置文件

通过我们对评分标准和定位的理解,我们现在可以继续评估和迭代完美的配置文件。不过,在我们这样做之前,我们需要清理 LLM 评估块的输出。这需要我们将推荐解析成更符合 Python 的形式,我们将在下一节中解决这个问题。

9.7.1 解析 LLM 评估输出

由于评估块的原始输出是文本,我们现在希望将其解析成更易用的形式。当然,编写解析函数很简单,但还有更好的方法可以自动转换响应。我们在第五章中介绍了关于代理动作的返回响应的更好方法。

在 VS Code 中打开 chapter_09\prompt_flow\recommender_with_parsing\flow.dag.yaml,并在可视化编辑器中查看流程。定位到 parsing_results 块,并点击链接在编辑器中打开 Python 文件,如图 9.20 所示。

图

图 9.20 在 VS Code 中打开 parsing_results.py 文件

parsing_results.py 文件的代码在列表 9.2 中显示。

列表 9.2 parsing_results.py
from promptflow import tool

@tool      #1
def parse(input: str) -> str:
    # Splitting the recommendations into individual movie blocks
    rblocks = input.strip().split("\n\n")      #2

    # Function to parse individual recommendation block into dictionary
    def parse_block(block):
        lines = block.split('\n')
        rdict = {}
        for line in lines:
            kvs = line.split(': ')
            key, value = kvs[0], kvs[1]
            rdict[key.lower()] = value     #3
        return rdict

    parsed = [parse_block(block) for block in rblocks]    #4

    return parsed

1 特殊装饰器用于表示工具块

2 分割输入和双换行符

3 创建字典条目并设置值

4 遍历每个块并将其解析为键/值字典

我们正在将列表 9.1 中的推荐输出(仅是一个字符串)转换为字典。因此,此代码将此字符串转换为以下显示的 JSON 块:

解析前:

"Title: The Butterfly Effect
Subject: 5
Format: 5
Genre: 4

Title: Primer
Subject: 5
Format: 5
Genre: 4

Title: Time Bandits
Subject: 5
Format: 5
Genre: 5"

解析后:

       {
            "title": " The Butterfly Effect
            "subject": "5",
            "format": "5",
            "genre": "4"
        },
        {
            "title": " Primer",
            "subject": "5",
            "format": "5",
            "genre": "4"
        },
        {
            "title": " Time Bandits",
            "subject": "5",
            "format": "5",
            "genre": "5"
        }

这个 parsing_results 块的输出现在被传递到输出,并封装在一个推荐列表中。我们可以通过运行流程来查看所有这些内容的样子。

在可视化编辑器中打开 flow.dag.yaml 文件以查看流程,并点击播放(运行全部)按钮。请确保选择使用两种推荐变体。您将看到两种变体都在运行并将输出到终端。

在这一点上,我们已经有一个完整的、可工作的推荐和 LLM 评估流程,该流程为每个输出标准输出一个分数。然而,为了对特定配置文件进行全面评估,我们希望使用各种标准生成多个推荐。我们将在下一节中看到如何批量处理流程。

9.7.2 在提示流程中运行批量处理

在我们的通用推荐配置文件中,我们希望评估各种输入标准如何影响生成的推荐。幸运的是,提示流程可以批量处理我们想要测试的任何变化。限制仅限于我们愿意花费的时间和金钱。

为了执行批量处理,我们首先需要创建一个包含我们输入标准的 JSON Lines (JSONL) 或 JSON 列表文档。如果您还记得,我们的输入标准在 JSON 格式下看起来如下:

{
    "subject": "time travel",
    "format": "books",
    "genre": "fantasy",
    "custom": "don't include any R rated content"
}

我们希望创建一个类似于刚才展示的 JSON 对象列表,最好是随机生成的。当然,简单地通过提示 ChatGPT 使用以下提示创建 JSONL 文档是做这件事的简单方法:

我正在开发一个推荐代理。该代理将根据以下标准推荐任何内容:

  1. 主题 - 示例:时间旅行、烹饪、度假

  2. 格式 - 示例:书籍、电影、游戏

  3. 类型:纪录片、动作、浪漫

  4. 自定义:不包含任何 R 级内容

请您生成一个包含这些标准的随机列表,并以 JSON Lines 文件格式输出。请列出 10 项。

您可以通过访问 ChatGPT 并输入前面的提示来尝试一下。之前生成的文件可以在流程文件夹中找到,文件名为 \bulk_recommend.jsonl。此文件的内容在此处展示以供参考:

{
  "subject": "time travel",
  "format": "books",
  "genre": "fantasy",
  "custom": "don't include any R rated content"
}
{
  "subject": "space exploration",
  "format": "podcasts",
  "genre": "sci-fi",
  "custom": "include family-friendly content only"
}
{
  "subject": "mystery",
  "format": "podcasts",
  "genre": "fantasy",
  "custom": "don't include any R rated content"
}
{
  "subject": "space exploration",
  "format": "podcasts",
  "genre": "action",
  "custom": "include family-friendly content only"
}
{
  "subject": "vacation",
  "format": "books",
  "genre": "thriller",
  "custom": "don't include any R rated content"
}
{
  "subject": "mystery",
  "format": "books",
  "genre": "sci-fi",
  "custom": "don't include any R rated content"
}
{
  "subject": "mystery",
  "format": "books",
  "genre": "romance",
  "custom": "don't include any R rated content"
}
{
  "subject": "vacation",
  "format": "movies",
  "genre": "fantasy",
  "custom": "don't include any R rated content"
}
{
  "subject": "cooking",
  "format": "TV shows",
  "genre": "thriller",
  "custom": "include family-friendly content only"
}
{
  "subject": "mystery",
  "format": "movies",
  "genre": "romance",
  "custom": "include family-friendly content only"
}

使用这个批量文件,我们可以使用批量 JSONL 文件中的各种输入标准运行两个变体。在可视化编辑器中打开 flow.dag.yaml 文件,点击批量(试管图标)以启动批量数据加载过程,并选择如图 9.21 所示的文件。对于某些操作系统,这可能显示为 本地 数据 文件

图片

图 9.21 将批量 JSONL 文件加载到运行多个输入变体的流程中

在选择批量文件后,将打开一个新的 YAML 文档,文件底部添加了一个运行链接,如图 9.22 所示。点击链接以执行输入的批量运行。

图片

图 9.22 运行输入的批量运行

在这一点上,将发生几件事情。将出现流程可视化编辑器,旁边将打开一个日志文件,显示运行进度。在终端窗口中,您将看到各种工作进程的生成和运行。

请耐心等待。即使是 10 项,批量运行也可能需要几分钟或几秒钟,这取决于各种因素,如硬件、之前的调用等。等待运行完成,您将在终端中看到结果摘要。

您还可以通过打开提示流扩展并选择最后一个运行来查看运行结果,如图 9.23 所示。然后,通过单击表格单元格深入了解每个运行。在此对话框中公开了大量信息,可以帮助您调试流程和配置文件。

figure

图 9.23 开启运行可视化以及检查批量运行

批量运行期间捕获了大量信息,您可以通过可视化器探索其中大部分。更多信息可以通过从终端窗口单击输出文件夹链接来找到。这将打开另一个 VS Code 会话,输出文件夹允许您查看运行日志和其他细节。

现在我们已经完成了每个变体的批量运行,我们可以应用地面并评估两个提示的结果。下一节将使用一个新的流程来执行配置文件/提示评估。

9.7.3 为地面创建评估流程

在可视化编辑器中打开chapter_3\prompt_flow\evaluate_groundings\flow.dag.yaml,如图 9.24 所示。评估流程中没有 LLM 块——只有将运行评分然后汇总评分的 Python 代码块。

figure

图 9.24 查看用于地面推荐运行的evaluate_groundings流程

现在我们可以查看scoringaggregate块的代码,从列表 9.3 中的评分代码开始。此评分代码将每个标准的得分平均到一个平均得分。函数的输出是处理过的推荐列表。

列表 9.3 line_process.py
@tool
def line_process(recommendations: str):     #1
    inputs = recommendations
    output = []
    for data_dict in inputs:                      #2
        total_score = 0
        score_count = 0

        for key, value in data_dict.items():     #2
                if key != "title":     #3
                    try:
                        total_score += float(value)
                        score_count += 1
                        data_dict[key] = float(value)     #4
                    except:
                        pass

        avg_score = total_score / score_count if score_count > 0 else 0

        data_dict["avg_score"] = round(avg_score, 2)    #5
        output.append(data_dict)

    return output

1 将一组三个推荐输入到函数中。

2 遍历每个推荐和标准

3 标题不是标准,所以忽略它。

4 对所有标准的得分进行总计,并将浮点值设置为键

5 将平均得分作为推荐的地面得分

从地面推荐中,我们可以继续使用aggregate块来汇总得分——aggregate块的代码如下所示。

列表 9.4 aggregate.py
@tool
def aggregate(processed_results: List[str]):
    items = [item for sublist in processed_results 
              ↪ for item in sublist]     #1

    aggregated = {}

    for item in items:
        for key, value in item.items():
            if key == 'title':
                continue

            if isinstance(value, (float, int)):      #2
                if key in aggregated:
                    aggregated[key] += value
                else:
                    aggregated[key] = value

    for key, value in aggregated.items():      #3
        value = value / len(items)
        log_metric(key=key, value=value)     #4
        aggregated[key] = value

    return aggregated

1 输入是一个列表的列表;将其展平为项目列表。

2 检查值是否为数值,并为每个标准键累积得分

3 遍历汇总的标准得分

4 将标准作为度量记录

汇总的结果将是每个标准的汇总得分和平均得分。由于评估/地面流程是分开的,它可以在我们进行的任何推荐运行上运行。这将允许我们使用任何变化的批量运行结果来比较结果。

我们可以通过在可视化编辑器中打开flow.dag.yaml并单击批量(水壶图标)来运行地面流。然后,当提示时,我们选择一个现有运行,然后选择我们想要评估的运行,如图 9.25 所示。这将打开一个包含底部运行链接的 YAML 文件,就像我们之前看到的那样。单击运行链接以运行评估。

figure

图 9.25 加载先前的运行以进行归一化和评估

运行完成后,你将在终端窗口中看到结果摘要。你可以点击输出链接在 VS Code 中打开文件夹并分析结果,但有一个更好的方法来比较它们。

打开提示流扩展,关注批量运行历史记录窗口,并向下滚动到如图 9.26 所示的运行对比部分。选择你想要比较的运行——可能是靠近顶部的那些——以便出现勾选标记。然后,右键单击运行,并选择可视化运行选项。批量运行可视化窗口打开,你将在顶部看到每个运行的指标。

图

图 9.26 可视化多个运行的指标并进行比较

我们现在可以清楚地看到配置文件/提示变体 0、用户提示和变体 1、系统提示之间的显著差异。如果你需要刷新提示/配置文件的外观,请参考图 9.15。此时,应该很明显,将输入参数注入系统提示可以提供更好的推荐。

你现在可以回去尝试其他配置文件或其他变体选项,看看这对你推荐的影响。可能性几乎是无限的,但希望你能看到提示流将是一个多么出色的工具,用于构建代理配置文件和提示。

9.7.4 练习

使用以下练习来提高你对材料的了解:

  • 练习 1 — 为推荐流程创建新的提示变体(中级)

目标 — 通过在提示流中创建和测试新的提示变体来改进推荐结果。

任务:

    • 在提示流中为推荐流程创建一个新的提示变体。

    • 以批量模式运行流程。

    • 评估结果,以确定它们与原始提示相比是更好还是更差。

  • 练习 2 — 将自定义字段添加到评分标准并进行评估(中级)

目标 — 通过将自定义字段纳入评分标准和更新评估流程来增强评估标准。

任务:

    • 将自定义字段作为新的标准添加到评分标准中。

    • 更新评估流程以评分新标准。

    • 评估结果,并分析新标准对评估的影响。

  • 练习 3 — 开发新的用例和评估评分标准(高级)

目标 — 通过开发新的用例和创建评估评分标准来扩展提示工程的应用。

任务:

    • 除了推荐之外,开发一个新的用例。

    • 构建新用例的提示。

    • 为新的提示创建评分标准。

    • 更新或修改评估流程以汇总和比较新用例与现有用例的结果。

  • 练习 4 — 使用 LM Studio 评估其他 LLM(中级)

目标 — 通过使用 LM Studio 托管本地服务器来评估不同开源 LLM 的性能。

任务:

    • 使用 LM Studio 来托管本地服务器以评估 LLM。

    • 评估其他开源 LLM。

    • 如果需要设置服务器和执行评估的帮助,请参考第二章。

  • 练习 5 — 使用提示流程构建和评估提示(中级)

目标 — 使用提示流程应用提示工程策略来构建和评估新的提示或配置文件。

任务:

    • 使用提示流程构建新的提示或配置文件以进行评估。

    • 应用第二章中的“编写清晰指令”提示工程策略。

    • 使用提示流程评估提示和配置文件。

    • 如果需要复习策略和实施细节,请参考第二章。

摘要

  • 代理配置文件由几个其他组件提示组成,可以驱动诸如动作/工具、知识、记忆、评估、推理、反馈和计划等功能。

  • 提示流程可用于评估代理的组件提示。

  • 系统性提示工程是一个评估提示和代理配置文件的迭代过程。

  • 系统性测试更改策略描述了迭代和评估提示,系统提示工程实现了这一策略。

  • 代理配置文件和提示工程有许多相似之处。我们定义代理配置文件为引导和帮助代理完成任务的提示工程元素的组合。

  • 提示流程是来自微软的开源工具,它为开发和评估配置文件和提示提供了几个功能。

  • 提示流程中的 LLM 连接支持额外的参数,包括温度、停止令牌、最大令牌和其他高级参数。

  • LLM 块支持提示和配置文件变体,这允许评估提示/配置文件或其他连接参数的变化。

  • 应用到 LLM 提示上的评分标准是提示/配置文件必须满足的准则和标准,以实现扎根。扎根是对评分标准的评分和评估。

  • 提示流程支持以单次运行或批量运行的方式运行多个变体。

  • 在提示流程中,在生成流程之后运行评估流程以评分和汇总结果。可视化运行选项可以比较跨多个运行评分标准汇总的聚合标准。

第十章:代理推理和评估

本章涵盖

  • 使用各种提示工程技术来扩展大型语言模型功能

  • 使用涉及推理的提示工程技术来参与大型语言模型

  • 使用评估提示来缩小和识别未知问题的解决方案

现在我们已经检查了定义代理中语义记忆组件的记忆和检索模式,我们可以看看代理中最后一个也是最关键的组件:规划。规划包括许多方面,从推理、理解、评估到反馈。

为了探索如何通过提示工程来引导 LLMs 进行推理、理解和规划,我们将演示如何通过提示工程来参与推理,然后扩展到规划。语义内核(SK)提供的规划解决方案包含多种规划形式。我们将通过将自适应反馈纳入一个新的规划器来结束本章。

图 10.1 展示了本章将涵盖的高级提示工程策略以及它们与我们将要介绍的各种技术之间的关系。图中的每种方法都将在本章中探讨,从左上角显示的解决方案/直接提示的基础,到右下角的自我一致性和思维树(ToT)提示。

figure

图 10.1 两种规划提示工程策略如何与各种技术相匹配

10.1 理解直接解决方案提示

直接解决方案提示通常是用户在向 LLMs 提问或解决特定问题时采用的第一种提示工程形式。对于任何 LLM 的使用,这些技术可能看起来很明显,但它们值得回顾,以建立思考和规划的基础。在下一节中,我们将从提问和期待答案开始。

10.1.1 问答提示

对于本章的练习,我们将使用提示流来构建和评估各种技术。(我们已经在第九章中广泛介绍了这个工具,所以如果需要复习,请参考该章节。)提示流是一个理解这些技术如何工作以及探索规划和推理过程流程的绝佳工具。

打开 Visual Studio Code (VS Code)到chapter 10源文件夹。为文件夹创建一个新的虚拟环境,并安装requirements.txt文件。如果您需要帮助设置章节的 Python 环境,请参阅附录 B。

我们将查看prompt_flow/question-answering-prompting文件夹中的第一个流程。在可视化编辑器中打开flow.dag.yaml文件,如图 10.2 所示。在右侧,您将看到组件的流程。顶部是question_answer LLM 提示,后面跟着两个Embedding组件,最后是一个用于评估的最终 LLM 提示,称为evaluate

figure

图 10.2 flow.dag.yaml文件,在可视化编辑器中打开,突出显示流程的各个组件

列表 10.1 中的分解详细展示了流程的结构和组件,使用了一种类似 YAML 简化的伪代码。您还可以看到各个组件的输入和输出,以及运行流程的示例输出。

列表 10.1 question-answer-prompting流程
   Inputs:
        context  : the content to ask the question about
        question : question asked specific to the content
        expected : the expected answer

LLM: Question-Answer (the prompt used to ask the question)
        inputs:
               context and question
        outputs: 
               the prediction/answer to the question

   Embeddings: uses an LLM embedding model to create the embedding 
representation of the text

     Embedding_predicted: embeds the output of the Question-Answer LLM
     Embedding_expected: embeds the output of the expected answer

Python: Evaluation (Python code to measure embedding similarity)
     Inputs:
            Embedding_predicted output
            Embedding_expected output
     Outputs: 
            the similarity score between predicted and expected

   Outputs:
        context: -> input.context
        question: -> input.question
     expected: -> input.expected
     predicted: -> output.question_answer
     evaluation_score: output.evaluation

### Example Output
{
    "context": "Back to the Future (1985)…",
    "evaluation_score": 0.9567478002354606,
    "expected": "Marty traveled back in time 30 years.",
    "predicted": "Marty traveled back in time 30 years from 1985 to 1955 
in the movie \"Back to the Future.\"",
    "question": "How far did Marty travel back in time in the movie 
Back to the Future (1985)"
}

在运行此流程之前,请确保您的 LLM 块配置正确。这可能需要您设置与所选 LLM 的连接。如果您需要复习如何完成此操作,请再次参考第九章。如果您不使用 OpenAI,您需要配置 LLM 和Embedding块与您的连接。

在配置您的 LLM 连接后,通过从可视化编辑器中点击播放按钮或在 YAML 编辑器窗口中使用测试(Shift-F5)链接来运行流程。如果一切连接和配置正确,您应该会看到列表 10.1 中的输出。

在 VS Code 中打开question_answer.jinja2文件,如图 10.2 所示。这个列表显示了基本的问答式提示。在这种提示风格中,系统消息描述了基本规则,并提供了解答问题的上下文。在第四章中,我们探讨了检索增强生成(RAG)模式,而这个提示遵循了类似的模式。

列表 10.2 question_answer.jinja2
system:
Answer the users question based on the context below. Keep the answer 
short and concise. Respond "Unsure about answer" if not sure about the 
answer.

Context: {{context}}     #1

user:
Question: {{question}}     #2

1 用 LLM 应该回答问题的内容替换。

2 用问题替换。

这个练习展示了使用大型语言模型(LLM)对某段内容提问的简单方法。然后,使用相似度匹配分数来评估问题回答。我们可以从列表 10.1 的输出中看到,LLM 在回答关于上下文的问题方面做得很好。在下一节中,我们将探讨一种类似的直接提示技术。

10.1.2 实现少样本提示

少样本提示类似于问答提示,但提示的构成更多地是提供几个示例,而不是事实或上下文。这允许 LLM 适应之前未见过的模式或内容。虽然这种方法听起来像是问答,但实现方式相当不同,结果可能非常强大。

零样本、单样本和少样本学习

机器学习和人工智能的一个圣杯是能够在尽可能少的样本上训练模型。例如,在传统的视觉模型中,数百万张图片被输入到模型中,以帮助识别猫和狗之间的差异。

一个单样本模型是一个只需要单个图像来训练的模型。例如,可以展示一张猫的图片,然后模型可以识别任何猫的图片。一个少样本模型只需要少量东西来训练模型。当然,零样本表示在没有先前示例的情况下识别某物的能力。LLM 是高效的学习者,可以完成这三种类型的学习。

在 VS Code 和可视化编辑器中打开 prompt_flow/few-shot-prompting/flow.dag.yaml。大部分流程看起来像图 10.2 中早些时候展示的那样,差异在列表 10.3 中突出显示,它展示了 YAML 伪代码表示。这个流程与之前的流程之间的主要区别是输入和 LLM 提示。

列表 10.3 few-shot-prompting 流程
Inputs:
       statement  : introduces the context and then asks for output
       expected : the expected answer to the statement
 LLM: few_shot (the prompt used to ask the question)
       inputs:statement
       outputs: the prediction/answer to the statement

Embeddings: uses an LLM embedding model to create the embedding 
representation of the text

        Embedding_predicted: embeds the output of the few_shot LLM
        Embedding_expected: embeds the output of the expected answer

   Python: Evaluation (Python code to measure embedding similarity)
        Inputs:
               Embedding_predicted output
               Embedding_expected output
        Outputs: the similarity score between predicted and expected

Outputs:
        statement: -> input.statement
        expected: -> input.expected
        predicted: -> output.few_shot
        evaluation_score: output.evaluation

### Example Output
{
    "evaluation_score": 0.906647282920417,     #1
    "expected": "We ate sunner and watched the setting sun.",
    "predicted": "After a long hike, we sat by the lake 
and enjoyed a peaceful sunner as the sky turned 
brilliant shades of orange and pink.",     #2
    "statement": "A sunner is a meal we eat in Cananda 
at sunset, please use the word in a sentence"     #3
}

1 评估分数表示预期和预测之间的相似度。

2 在句子中使用 sunner

3 这是一个错误的陈述,但目的是让 LLM 使用这个单词,就像它是真实的一样。

通过按 Shift-F5 或从可视化编辑器中点击播放/测试按钮来运行流程。你应该会看到类似于列表 10.3 的输出,其中 LLM 正确地在一个句子中使用了单词 sunner(一个虚构的术语),这是基于初始语句的。

这个练习展示了使用提示来改变 LLM 行为的能力,使其与它所学习的内容相反。我们正在改变 LLM 理解为准确的内容。此外,我们随后使用这种修改后的观点来引发对虚构词汇的使用。

在 VS Code 中打开 few_shot.jinja2 提示,如列表 10.4 所示。这个列表展示了设置一个简单的角色,即古怪的词典编纂者,然后提供它之前定义和使用的词汇的例子。提示的基础允许 LLM 扩展例子并使用其他词汇产生类似的结果。

列表 10.4 few_shot.jinja2
system:
You are an eccentric word dictionary maker. You will be asked to 

construct a sentence using the word.
The following are examples that demonstrate how to craft a sentence using 
the word.
A "whatpu" is a small, furry animal native to Tanzania. 
An example of a sentence that uses the word whatpu is:     #1
We were traveling in Africa and we saw these very cute whatpus.
To do a "farduddle" means to jump up and down really fast. An example of a 
sentence that uses the word farduddle is:
I was so excited that I started to farduddle.     #2

Please only return the sentence requested by the user.   #3

user:
{{statement}}    #4

1 展示了一个定义虚构词汇并在句子中使用它的例子

2 展示了另一个例子

3 一条规则,用于防止 LLM 输出额外信息

4 输入语句定义了一个新词并要求使用。

你可能会说我们在这里迫使 LLM 幻觉,但这项技术是修改行为的基础。它允许构建提示来引导 LLM 做出与它所学习的一切相反的事情。提示的基础还确立了其他形式改变行为的技术。从改变 LLM 的感知和背景的能力,我们将继续在下一节展示一个直接解决方案的最终例子。

10.1.3 使用零样本提示提取概括

零样本提示或学习 是以这种方式生成提示的能力,允许 LLM 进行泛化。这种泛化嵌入在 LLM 中,并通过零样本提示来展示,其中不提供示例,而是给出一系列指南或规则来引导 LLM。

使用这种技术很简单,并且很好地引导 LLM 根据其内部知识和没有其他上下文来生成回复。这是一种微妙而强大的技术,它将 LLM 的知识应用于其他应用。这种技术与其他提示策略相结合,正在证明在替代其他语言分类模型——例如识别文本中的情感或情绪的模型——方面是有效的。

在 VS Code 提示流程可视化编辑器中打开 prompt_flow/zero-shot-prompting/flow.dag.yaml。这个流程与之前图 10.1 中显示的几乎相同,但在实现上略有不同,如列表 10.5 所示。

列表 10.5 zero-shot-prompting 流程
Inputs:
        statement  : the statement to be classified
        expected : the expected classification of the statement

    LLM: zero_shot (the prompt used to classify)
        inputs: statement
        outputs: the predicted class given the statement

    Embeddings: uses an LLM embedding model to create the embedding 
representation of the text

    Embedding_predicted: embeds the output of the zero_shot LLM
    Embedding_expected: embeds the output of the expected answer

    Python: Evaluation (Python code to measure embedding similarity)
        Inputs:
               Embedding_predicted output
             Embedding_expected output
          Outputs: the similarity score between predicted and expected

   Outputs:
        statement: -> input.statement
        expected: -> input.expected
        predicted: -> output.few_shot
        evaluation_score: output.evaluation

   ### Example Output
{
       "evaluation_score": 1,     #1
       "expected": "neutral",
       "predicted": "neutral",
       "statement": "I think the vacation is okay. "     #2
   }

1 显示了完美的评估分数 1.0

2 我们要求 LLM 进行分类的声明

在 VS Code 提示流程可视化编辑器中按 Shift-F5 运行流程。你应该会看到类似于列表 10.5 所示的输出。

现在打开如列表 10.6 所示的 zero_shot.jinja2 提示。该提示简单,不使用示例来从文本中提取情感。特别值得注意的是,提示中甚至没有提到“情感”这个词,而 LLM 似乎理解了意图。

列表 10.6 zero_shot.jinja2
system:
Classify the text into neutral, negative or positive. 
Return on the result and nothing else.     #1

user:
{{statement}}     #2

1 提供了执行分类的基本指导

2 文本分类的声明

零样本提示工程是关于利用 LLM 基于其训练材料广泛概括的能力。这个练习展示了 LLM 中的知识如何用于其他任务。LLM 自我情境化和应用知识的能力可以超越其训练范围。在下一节中,我们将进一步探讨 LLM 如何进行推理。

10.2 提示工程中的推理

类似于 ChatGPT 这样的 LLM 被开发成作为聊天完成模型,其中文本内容被输入到模型中,其响应与完成该请求相一致。LLM 从未被训练过推理、规划、思考或拥有思想。

然而,就像我们在上一节中的示例所展示的那样,LLM 可以被提示提取其概括性,并扩展到其初始设计之外。虽然 LLM 不是为推理而设计的,但输入到模型中的训练材料提供了对推理、规划和思维的理解。因此,通过扩展,LLM 理解推理是什么,并且可以运用推理的概念。

理解和规划

推理 是智力(无论是人工的还是非人工的)理解通过问题进行思考或思维过程的能力。智力可以理解行动有结果,并且可以利用这种能力通过从一系列行动中选择哪个行动可以应用于解决给定的任务来进行推理。

规划 是智力推理出行动或任务的顺序并应用正确的参数以实现目标或结果的能力——智力计划依赖于问题范围的程度。智力可以结合多个层次的规划,从战略和战术到操作和应急。

我们将探讨另一组提示工程技术,这些技术允许或模拟推理行为,以展示这种推理能力。通常,在评估推理的应用时,我们会寻找 LLM 解决它未设计去解决的问题。这类问题的一个很好的来源是逻辑、数学和文字问题。

使用时间旅行主题,理解时间旅行比解决哪一类独特问题更好?图 10.3 展示了一个独特且具有挑战性的时间旅行问题的例子。我们的目标是获得一种能力,能够以正确解决问题的方法提示 LLM。

figure

图 10.3 我们打算使用具有推理和计划的 LLM 解决的时间旅行问题的复杂性

时间旅行问题是一些看似难以解决的思维练习。图 10.3 中的例子对 LLM 来说很复杂,但它出错的部分可能会让你感到惊讶。下一节将使用提示中的推理来解决这些独特的问题。

10.2.1 思维链提示

思维链(CoT)提示是一种提示工程技术,它使用一次或几次示例来描述推理和实现预期目标的步骤。通过推理的展示,LLM 可以推广这个原则,并通过类似的问题和目标进行推理。虽然 LLM 没有以推理为目标进行训练,但我们可以通过提示工程技术来激发模型进行推理。

在 VS Code 提示流程可视化编辑器中打开 prompt_flow/chain-of-thought-prompting/flow.dag.yaml。如图 10.4 所示,这个流程的元素很简单,只有两个 LLM 块。流程首先使用 CoT 提示来解决一个复杂问题;然后,第二个 LLM 提示评估答案。

figure

图 10.4 CoT 流程

列表 10.7 展示了描述流程中的块及其输入/输出的 YAML 伪代码。在这个例子中,默认的问题陈述与图 10.3 中的不同。

列表 10.7 chain-of-thought-prompting 流程
   Inputs:
        statement  : the statement problem to be solved
        expected : the expected solution to the problem

 LLM: cot (the prompt used to solve the problem)
        inputs: statement
        outputs: the predicted answer given the problem statement

LLM: evaluate_answer (the prompt used to evaluate the solution)
        inputs:
               statement: -> input.statement
               expected: -> input.expected
               predicted: -> output.cot

        outputs: a score of how well the problem was answered

   Outputs:
        statement: -> input.statement
        expected: -> input.expected
        predicted: -> output.cot
        evaluation_score: output.evaluate_answer

   ### Example Output
{
    "evaluation_score": "0.5",     #1
    "expected": "After the final jump, Max finds himself 
in the year 1980 and he is 75 years old.",     #2
    "predicted": " Max starts in the year 2300 and 
travels backward in 40-year increments, spending 5 years 
in each period. The journeys will be as follows:
\n\n- From 2300 to 2260: Max is 25 + 5 = 30 years old.
\n- From 2260 to 2220: Max is 30 + 5 = 35 years old.
\n- From 2220 to 2180: Max is 35 + 5 = 40 years old.
\n- From 2180 to 2140: Max is 40 + 5 = 45 years old.
\n- From 2140 to 2100: Max is 45 + 5 = 50 years old.
\n- From 2100 to 2060: Max is 50 + 5 = 55 years old.
\n- From 2060 to 2020: Max is 55 + 5 = 60 years old.
\n- From 2020 to 1980: Max is 60 + 5 = 65 years old.
\n- From 1980 to 1940: Max is 65 + 5 = 70 years old.
\n- From 1940 to 1900: Max is 70 + 5"     #3
}

1 给定解决方案的评估分数

2 问题的预期答案

3 预测答案显示了推理步骤和输出。

深入研究输入并检查问题陈述;尝试自己评估这个问题。然后,通过按 Shift-F5 运行流程。你应该会看到类似于列表 10.7 中所示的结果。

按照列表 10.8 中的说明打开 cot.jinja2 提示文件。这个提示给出了一些时间旅行问题的例子,然后是经过思考和推理的解决方案。向 LLM 展示完成问题的步骤提供了推理机制。

列表 10.8 cot.jinja2
system:
"In a time travel movie, Sarah travels back in time to 
prevent a historic event from happening. She arrives 
2 days before the event. After spending a day preparing, 
she attempts to change the event but realizes she has 
actually arrived 2 years early, not 2 days. She then 
decides to wait and live in the past until the event's 
original date. How many days does Sarah spend in the past 
before the day of the event?"     #1

Chain of Thought:     #2

    Initial Assumption: Sarah thinks she has arrived 2 days before the event.
    Time Spent on Preparation: 1 day spent preparing.
    Realization of Error: Sarah realizes she's actually 2 years early.
    Conversion of Years to Days: 
2 years = 2 × 365 = 730 days (assuming non-leap years).
    Adjust for the Day Spent Preparing: 730 - 1 = 729 days.
    Conclusion: Sarah spends 729 days in the past before the day of the event.

"In a sci-fi film, Alex is a time traveler who decides 
to go back in time to witness a famous historical battle 
that took place 100 years ago, which lasted for 10 days. 
He arrives three days before the battle starts. However, 
after spending six days in the past, he jumps forward in 
time by 50 years and stays there for 20 days. Then, he 
travels back to witness the end of the battle. How many 
days does Alex spend in the past before he sees the end of
 the battle?"     #3

Chain of Thought:     #4

    Initial Travel: Alex arrives three days before the battle starts.
    Time Spent Before Time Jump: Alex spends six days in the past. 
The battle has started and has been going on for 3 days (since he 
arrived 3 days early and has now spent 6 days, 3 + 3 = 6).
    First Time Jump: Alex jumps 50 years forward and stays for 20 days.
 This adds 20 days to the 6 days he's already spent in the past 
(6 + 20 = 26).
    Return to the Battle: When Alex returns, he arrives back on the same 
day he left (as per time travel logic). The battle has been going on for 
3 days now.
    Waiting for the Battle to End: The battle lasts 10 days. Since he's 
already witnessed 3 days of it, he needs to wait for 7 more days.
    Conclusion: Alex spends a total of 3 (initial wait) + 3 (before the 
first jump) + 20 (50 years ago) + 7 (after returning) = 33 days in the 
past before he sees the end of the battle.
Think step by step but only show the final answer to the statement.

user:
{{statement}}     #5

1 几个示例问题陈述

2 将问题陈述的解决方案输出为一系列推理步骤

3 几个示例问题陈述

4 将问题陈述的解决方案输出为一系列推理步骤

5 LLM 被指示解决的问题陈述

你可能会注意到图 10.3 的解决方案也作为例子在列表 10.8 中提供。回顾列表 10.7 中 LLM 对问题的回复也是很有帮助的。从这一点,你可以看到 LLM 应用了哪些推理步骤来得到其最终答案。

现在,我们可以查看评估解决方案如何解决问题的提示。打开列表 10.9 中所示的evaluate_answer.jinja2以回顾所使用的提示。这个提示很简单,使用了零样本提示,并允许 LLM 泛化它应该如何评分预期和预测。我们可以提供示例和分数,从而将此转变为一个几样本分类的例子。

列表 10.9 evaluate_answer.jinja2
system:

Please confirm that expected and predicted results are 
the same for the given problem.     #1
Return a score from 0 to 1 where 1 is a perfect match and 0 is no match.
Please just return the score and not the explanation.     #2

user:
Problem: {{problem}}     #3

Expected result: {{expected}}     #4

Predicted result: {{predicted}}     #5

1 评估解决方案的规则

2 指示只返回分数,不返回其他内容

3 初始问题陈述

4 预期或基于事实的答案

5 之前 CoT 提示的输出

观察到在列表 10.7 中之前展示的 LLM 输出,你可以理解为什么评估步骤可能会变得令人困惑。或许解决这个问题的一个方法就是建议 LLM 以单个陈述的形式提供最终答案。在下一节中,我们将继续探讨另一个提示推理的例子。

10.2.2 零样本 CoT 提示

正如我们的时间旅行所展示的,CoT 提示在特定类问题上的提示生成可能很昂贵。虽然效果不如前者,但有一些类似于 CoT 的技术不使用示例,并且可以更加通用。本节将检查用于在 LLM 中引发推理的简单短语。

在 VS Code 的提示流程视觉编辑器中打开prompt_flow/zero-shot-cot-prompting/flow.dag.yaml。这个流程与之前的 CoT 非常相似,如图 10.4 所示。下一个列表显示了描述该流程的 YAML 伪代码。

列表 10.10 zero-shot-CoT-prompting流程
   Inputs:
        statement  : the statement problem to be solved
        expected : the expected solution to the problem

LLM: cot (the prompt used to solve the problem)
        inputs: statement
        outputs: the predicted answer given the problem statement

LLM: evaluate_answer (the prompt used to evaluate the solution)
        inputs:
               statement: -> input.statement
               expected: -> input.expected
               predicted: -> output.cot

         outputs: a score of how well the problem was answered

    Outputs:
        statement: -> input.statement
        expected: -> input.expected
        predicted: -> output.cot
        evaluation_score: output.evaluate_answer

    ### Example Output
   {
       "evaluation_score": "1",     #1
       "expected": "After the final jump, ↪
          ↪ Max finds himself in the year 1980 and 
   he is 75 years old.",     #2
       "predicted": "Max starts in… ↪
          ↪ Therefore, after the final jump, ↪
          ↪ Max is 75 years old and in the year 1980.",     #3
       "statement": "In a complex time travel …"     #4
   }

1 最终评估分数

2 预期答案

3 预测答案(已省略步骤以显示最终答案)

4 初始问题陈述

在 VS Code 的视觉编辑器中按 Shift-F5 运行/测试流程。流程将运行,你应该会看到类似于列表 10.10 所示的输出。这个练习示例在相同问题上比之前的例子表现更好。

在 VS Code 中打开cot.jinja2提示,如列表 10.11 所示。这个提示比之前的例子简单得多,因为它只使用了零样本。然而,一个关键短语将这个简单的提示转换成了一个强大的推理引擎。提示中的这一行让我们 一步一步 来思考触发了 LLM 考虑内部上下文以展示推理。这反过来又指导 LLM 分步骤推理出问题。

列表 10.11 cot.jinja2
system:
You are an expert in solving time travel problems.
You are given a time travel problem and you have to solve it.
Let's think step by step.     #1
Please finalize your answer in a single statement.     #2

user:
{{statement}}     #3

1 一行魔法代码,用于从 LLM 中构建推理

2 要求 LLM 提供一个答案的最终陈述

3 LLM 被要求解决的问题陈述

类似的短语要求 LLM 思考步骤或要求它以步骤的形式回答,也会提取推理。我们将在下一节中演示一个类似但更复杂的技术。

10.2.3 使用提示链分步骤

我们可以将向一个 LLM 逐步提问的行为扩展成一个链式提示,迫使 LLM 分步骤解决问题。在本节中,我们将探讨一种称为提示链的技术,该技术迫使 LLM 分步骤处理问题。

打开可视化编辑器中的prompt_flow/prompt-chaining/flow.dag.yaml文件,如图 10.5 所示。提示链将解决问题的推理方法分解成一系列提示。这项技术迫使 LLM 以步骤的形式回答问题。

图

图 10.5 提示链流程

列表 10.12 展示了描述该流程的 YAML 伪代码,它提供了更多细节。这个流程将第一个 LLM 块的输出链式连接到第二个,然后从第二个连接到第三个。迫使 LLM 以这种方式处理问题揭示了推理模式,但它也可能过于冗长。

列表 10.12 prompt-chaining流程
   Inputs:
        statement  : the statement problem to be solved

   LLM: decompose_steps (the prompt used to decompose the problem)
        inputs: 
               statement: -> input.statement     #1

        outputs: the breakdown of steps to solve the problem

   LLM: calculate_steps (the prompt used to calculate the steps)
        inputs:
               statement: -> input.statement
               decompose_steps: -> output.decompose_steps     #2

               outputs: the calculation for each step
   LLM: calculate_solution (attempts to solve the problem)
        inputs:
               statement: -> input.statement
               decompose_steps: -> output.decompose_steps
               calculate_steps: -> output.calculate_steps     #3

         outputs: the final solution statement

   Outputs:
        statement: -> input.statement
        decompose_steps: -> output.decompose_steps
        calculate_steps: -> output.calculate_steps
        calculate_solution: -> output.calculate_solution

   ### Example Output
{
    "calculate_steps": "1\. The days spent by Alex",
    "decompose_steps": "To figure out the …",
    "solution": "Alex spends 13 days in the ↪
           ↪ past before the end of the battle.",     #4
    "statement": "In a sci-fi film, Alex …"    
}

1 提示链的开始

2 将前一步的输出注入到这一步

3 将前两个步骤的输出注入到这一步

4 最终的解决方案陈述,尽管是错误的,但更接近了。

通过从可视化编辑器按 Shift-F5 运行流程,你会看到如列表 10.12 所示的输出。对于 Alex 问题,答案仍然不正确,但我们可以看到 LLM 为了推理出问题所做的工作。

打开所有三个提示:decompose_steps.jinja2calculate_steps.jinja2calculate_solution.jinja2(分别见列表 10.13、10.14 和 10.15)。列表中展示的所有三个提示可以进行比较,以展示输出是如何链式连接的。

列表 10.13 decompose_steps.jinja2
system:
You are a problem solving AI assistant.
Your job is to break the users problem down into smaller steps and list 
the steps in the order you would solve them.
Think step by step, not in generalities.
Do not attempt to solve the problem, just list the steps. #1

user:
{{statement}}     #2

1 迫使 LLM 只列出步骤,不列出其他任何内容

2 初始问题陈述

列表 10.14 calculate_steps.jinja2
system:
You are a problem solving AI assistant.
You will be given a list of steps that solve a problem.
Your job is to calculate the output for each of the steps in order.
Do not attempt to solve the whole problem,
just list output for each of the steps.     #1
Think step by step.     #2

user:
{{statement}}

{{steps}}     #3

1 要求 LLM 只解决步骤,而不是整个问题

2 使用魔法语句提取推理

3 将分解步骤步骤产生的步骤注入其中

列表 10.15 calculate_solution.jinja2
system:
You are a problem solving AI assistant.
You will be given a list of steps and the calculated output for each step.
Use the calculated output from each step to determine the final 
solution to the problem.
Provide only the final solution to the problem in a 
single concise sentence. Do not include any steps 
in your answer.     #1

user:
{{statement}}

{{steps}}     #2

{{calculated}}     #3

1 要求 LLM 输出最终答案,而不输出任何步骤

2 分解的步骤

3 计算出的步骤

在这个练习示例中,我们并没有进行任何评估和评分。没有评估,我们可以看到这个提示序列仍然存在一些问题,无法解决之前在图 10.3 中展示的更具挑战性的时间旅行问题。然而,这并不意味着这项技术没有价值,这种提示格式在解决一些复杂问题方面表现良好。

然而,我们想要找到的是一种推理和规划方法,可以一致地解决如此复杂的问题。接下来的部分将从推理转向评估最佳解决方案。

10.3 使用评估来实现一致性的解决方案

在上一节中,我们了解到即使是经过最佳推理的计划也可能不会总是得出正确的解决方案。此外,我们可能并不总是有答案来确认该解决方案是否正确。现实情况是,我们经常想要使用某种形式的评估来确定解决方案的有效性。

图 10.6 显示了作为使 LLMs 进行推理和规划手段而设计的提示工程策略的比较。我们已经涵盖了左侧的两个:零样本直接提示和 CoT 提示。本节下面的示例练习将探讨与 CoT 和 ToT 技术结合的自洽性。

figure

图 10.6 比较各种提示工程策略以实现从 LLMs 中进行推理和规划

我们将继续关注复杂的时间旅行问题,以比较这些更高级的方法,这些方法通过评估扩展了推理和规划。在下一节中,我们将评估自洽性。

10.3.1 评估自洽提示

提示的一致性不仅仅是降低我们发送给 LLM 的温度参数。通常,我们希望生成一个一致的计划或解决方案,同时使用较高的温度来更好地评估计划的全部变体。通过评估多个不同的计划,我们可以更好地了解解决方案的整体价值。

自洽提示是针对给定问题生成多个计划/解决方案的技术。然后,评估这些计划,并接受出现频率更高或更一致的计划。想象一下生成了三个计划,其中两个相似,但第三个不同。使用自洽性,我们将前两个计划评估为更一致的答案。

在 VS Code 提示流可视化编辑器中打开prompt_flow/self-consistency-prompting/flow.dag.yaml。流程图显示了图 10.7 中提示生成流程的简单性。在图旁边的是自洽评估流程。

figure

图 10.7 自洽提示生成与评估流程并置

提示流使用直接无环图(DAG)格式来执行流程逻辑。DAGs 是展示和执行流程逻辑的绝佳方式,但由于它们是无环的,意味着它们不能重复,因此不能执行循环。然而,由于提示流提供了一个批量处理机制,我们可以使用它来模拟流程中的循环或重复。

参考图 10.6,我们可以看到自洽过程在收集结果并确定最佳计划/回复之前对输入进行了三次处理。我们可以应用相同的模式,但使用批量处理来生成输出。然后,评估流程将汇总结果并确定最佳答案。

在 VS Code 中打开self-consistency-prompting/cot.jinja2提示模板(参见列表 10.16)。列表已被缩短,因为我们之前已经看到了部分内容。此提示使用两个(少样本提示)CoT 示例来向 LLM 展示推理过程。

列表 10.16 self-consistency-prompting/cot.jinja2
system:

"In a time travel movie, Sarah travels back… "     #1

Chain of Thought:

    Initial Assumption: …     #2
    Conclusion: Sarah spends 729 days in the past before the day of the event.

"In a complex time travel movie plot, Max, a 25 year old…"     #3

Chain of Thought:
    Starting Point: Max starts …     #4
    Conclusion: After the final jump, 
Max finds himself in the year 1980 and he is 75 years old.
Think step by step,
 but only show the final answer to the statement.     #5

user:
{{statement}}

1 Sarah 时间旅行问题

2 样本 CoT,为了简洁已截断

3 最大时间旅行问题

4 样本 CoT,为了简洁已截断

5 最终指南和声明以约束输出

在 VS Code 中打开self-consistency-prompting/flow.dag.yaml文件。通过从可视化编辑器中点击批量运行(水壶图标)来以批量模式运行示例。图 10.8 展示了逐步过程:

  1. 点击批量运行。

  2. 选择 JSON Lines (JSONL)输入。

  3. 选择statements.jsonl

  4. 点击运行链接。

图

图 10.8 启动批量过程的逐步过程

提示:如果您需要回顾过程,请参阅第九章,其中更详细地介绍了此过程。

列表 10.17 显示了批量模式下执行流程的 JSON 输出。statements.jsonl文件有五个相同的 Alex 时间旅行问题条目。使用相同的条目允许我们模拟在重复条目上执行提示五次。

列表 10.17 self-consistency-prompting 批量执行输出
{
    "name": "self-consistency-prompting_default_20240203_100322_912000",
    "created_on": "2024-02-03T10:22:30.028558",
    "status": "Completed",
    "display_name": "self-consistency-prompting_variant_0_202402031022",
    "description": null,
    "tags": null,
    "properties": {
        "flow_path": "…prompt_flow/self-consistency-prompting",     #1
        **"output_path"**: "…/.promptflow/.runs/self-
↪ consistency-prompting_default_20240203_100322_912000",     #2
        "system_metrics": {
            "total_tokens": 4649,
            "prompt_tokens": 3635,
            "completion_tokens": 1014,
            "duration": 30.033773
        }
    },
    "flow_name": "self-consistency-prompting",
    "data": "…/prompt_flow/self-consistency-prompting/
↪ statements.jsonl",     #3
    "output": "…/.promptflow/.runs/self-consistency-↪
↪ prompting_default_20240203_100322_912000/flow_outputs"
}

1 流程执行的路径

2 包含流程输出的文件夹(注意此路径)

3 批量运行流程使用的数据

您可以通过按 Ctrl 键并点击列表 10.17 中突出显示的输出链接来查看生成的流程。这将打开 VS Code 的另一个实例,显示包含运行所有输出的文件夹。我们现在想检查最一致的答案。幸运的是,提示流程中的评估功能可以帮助我们使用相似度匹配来识别一致的答案。

在 VS Code 中打开self-consistency-evaluation/flow.dag.yaml(参见图 10.7)。此流程将预测答案嵌入,然后使用聚合来确定最一致的答案。

从流程中,在 VS Code 中打开consistency.py(如列表 10.18 所示)。此工具函数的代码计算所有答案对之间的余弦相似度。然后,它找到最相似的答案,记录它,并将其作为答案输出。

列表 10.18 consistency.py
from promptflow import tool
from typing import List
import numpy as np
from scipy.spatial.distance import cosine
@tool
def consistency(texts: List[str],
                embeddings: List[List[float]]) -> str:
    if len(embeddings) != len(texts):
        raise ValueError("The number of embeddings ↪
       ↪ must match the number of texts.")

    mean_embedding = np.mean(embeddings, axis=0)     #1
    similarities = [1 - cosine(embedding, mean_embedding) ↪
                ↪ for embedding in embeddings]     #2
    most_similar_index = np.argmax(similarities)     #3

    from promptflow import log_metric
    log_metric(key="highest_ranked_output", value=texts[most_similar_index])     #4

    return texts[most_similar_index]     #5

1 计算所有嵌入的平均值

2 计算每对嵌入的余弦相似度

3 找到最相似答案的索引

4 将输出记录为指标

5 返回最相似答案的文本

我们还需要以批量模式运行评估流程。在 VS Code 中打开self-consistency-evaluation/flow.dag.yaml,以批量模式(水壶图标)运行流程。然后,选择现有运行作为流程输入,当提示时,选择你刚刚执行的顶部或最后一个运行作为输入。

再次,在流程完成处理之后,你会看到类似于列表 10.17 中所示的结果。在输出文件夹链接上 Ctrl 点击以打开一个新的 VS Code 实例,显示结果。在 VS Code 中定位并打开如图 10.9 所示的metric.json文件。

图

图 10.9 VS Code 打开到批量运行输出文件夹。突出显示的是metrics.json文件和显示最相似答案的输出。

图 10.9 中显示的答案对于这次运行仍然是不正确的。你可以继续进行几个更多的提示批量运行,或者增加批量中的运行次数,然后评估流程以查看你是否能得到更好的答案。这种技术通常对更直接的问题更有帮助,但仍然展示了无法推理出复杂问题的能力。

自洽性使用反思方法来评估最可能的思想。然而,最可能的事情并不总是最好的。因此,在下一节中,我们必须考虑一个更全面的方法。

10.3.2 评估思维树提示

如前所述,ToT 提示,如图 10.6 所示,结合了自我评估和提示链技术。因此,它将规划序列分解为一系列提示,但在链的每个步骤中,它都提供了多次评估。这创建了一个可以在每个级别执行和评估的树,可以是广度优先,也可以是自顶向下的深度优先。

图 10.10 显示了使用广度优先或深度优先执行树之间的差异。遗憾的是,由于提示流的 DAG 执行模式,我们无法快速实现深度优先方法,但广度优先工作得很好。

图

图 10.10 ToT 模式中的广度优先与深度优先执行

在 VS Code 中打开tree-of-thought-evaluation/flow.dag.yaml。流程的可视化如图 10.11 所示。这个流程像一个广度优先的 ToT 模式——流程将一系列提示链在一起,要求 LLM 在每一步返回多个计划。

图

图 10.11 表达的 ToT 模式和提示流程

由于流程以广度优先的方式执行,每个节点的每个级别输出也会被评估。流程中的每个节点都使用一对语义函数——一个用于生成答案,另一个用于评估答案。语义函数是一个自定义的 Python 流程块,它处理多个输入并生成多个输出。

列表 10.19 展示了semantic_function.py工具。这个通用工具在本流程的多个块中被重复使用。它还展示了从 SK 直接用于提示流程的嵌入功能。

列表 10.19 semantic_function.py
@tool
def my_python_tool(
    input: str,
    input_node: int,
    history: str,
    semantic_function: str,
    evaluation_function: str,
    function_name: str,
    skill_name: str,
    max_tokens: int,
    temperature: float,
    deployment_name: str,
    connection: Union[OpenAIConnection, 
                      AzureOpenAIConnection],     #1
) -> str:
    if input is None or input == "":     #2
        return ""

    kernel = sk.Kernel(log=sk.NullLogger())
    # code for setting up the kernel and LLM connection omitted

    function = kernel.create_semantic_function(
                             semantic_function,                                               
                             function_name=function_name,
                             skill_name=skill_name,
                             max_tokens=max_tokens,
                             temperature=temperature,
                             top_p=0.5)     #3
    evaluation = kernel.create_semantic_function(
                             evaluation_function,        
                             function_name="Evaluation",
                             skill_name=skill_name,
                             max_tokens=max_tokens,
                             temperature=temperature,
                             top_p=0.5)     #4

    async def main():
        query = f"{history}\n{input}"
        try:
            eval = int((await evaluation.invoke_async(query)).result)
            if eval > 25:     #5
                return await function.invoke_async(query)    #6
        except Exception as e:
            raise Exception("Evaluation failed", e)

       try:
        result = asyncio.run(main()).result
        return result
    except Exception as e:
        print(e)
        return ""

1 使用并集允许不同类型的 LLM 连接

2 检查输入是否为空或 None;如果是,则不应执行该函数。

3 设置生成函数,创建计划

4 设置评估函数

5 运行评估函数并确定输入是否足够好以继续

6 如果评估分数足够高,则生成下一步

语义函数工具用于树中的专家、节点和答案块。在每一步,该函数确定是否有文本输入。如果没有文本,则块返回而不执行。向块传递无文本意味着上一个块评估失败。通过在每一步之前评估,ToT 会短路其认为无效的计划执行。

这可能是一个一开始难以掌握的复杂模式,所以请继续在 VS Code 中运行该流程。列表 10.20 仅显示了运行中的答案节点输出;这些结果可能与您看到的不同,但应该是相似的。没有返回文本的节点要么评估失败,要么其父节点无效。

列表 10.20 tree-of-thought-evaluation 流程的输出
{
    "answer_1_1": "",     #1
    "answer_1_2": "",
    "answer_1_3": "",
    "answer_2_1": "Alex spends a total of 29 days in the past before he 
sees the end of the battle.",
    "answer_2_2": "",     #2
    "answer_2_3": "Alex spends a total of 29 days in the past before he 
sees the end of the battle.",
    "answer_3_1": "",     #3
    "answer_3_2": "Alex spends a total of 29 days in the past before he 
sees the end of the battle.",
    "answer_3_3": "Alex spends a total of 9 days in the past before he 
sees the end of the battle.",

1 表示第一个节点的计划无效且未执行

2 节点 2 的计划和答案 2 评估失败,未运行。

3 此节点的计划评估失败,未运行。

列表 10.20 中的输出显示了仅评估了一组选定的节点。在大多数情况下,评估的节点返回了一个可能有效的答案。没有输出产生,意味着该节点本身或其父节点无效。当所有兄弟节点都返回空值时,父节点评估失败。

如我们所见,ToT 对复杂问题有效,但可能不太实用。此流程的执行可能需要多达 27 次调用 LLM 生成输出。在实践中,可能只需调用一半那么多,但那仍然是回答一个问题的十几次调用。

10.4 练习

使用以下练习来提高您对材料的了解:

  • 练习 1—创建直接提示、少量样本提示和零样本提示

目标—为 LLM 创建三个不同的提示,以总结最近的一篇科学文章:一个使用直接提示,一个使用少量样本提示,最后一个使用零样本提示。

任务:

    • 比较每种方法生成的摘要的有效性。

    • 比较每种方法生成的摘要的准确性。

  • 练习 2—制作推理提示

目标—设计一组提示,要求 LLM 解决逻辑谜题或谜语。

任务:

    • 关注您的提示结构如何影响 LLM 的推理过程。

    • 关注相同的方法如何影响其答案的正确性。

  • 练习 3—评估提示技术

目标—开发一个评估提示,要求 LLM 预测一个假设实验的结果。

任务:

    • 创建一个后续提示,以评估 LLM 的预测准确性,并提供对其推理过程的反馈。

摘要

  • 直接解决方案提示是使用提示引导 LLM 解决特定问题或任务的基础方法,强调清晰的问题和答案结构的重要性。

  • 少样本提示为 LLM 提供少量示例,以指导他们处理新或未见过的内容,突出了其使模型能够适应未知模式的能力。

  • 零样本学习和提示展示了 LLM 如何从其训练中泛化以解决问题,而无需显式示例,展示了它们在理解并应用新情境中的知识的能力。

  • 思维链提示引导 LLM 逐步通过推理过程解决复杂问题,说明了如何从模型中提取详细推理。

  • 提示链将问题分解为一系列相互构建的提示,展示了如何将复杂的问题解决过程结构化为 LLM 可管理的步骤。

  • 自洽性是一种提示技术,它为问题生成多个解决方案,并通过评估选择最一致的答案,强调了在实现可靠结果中一致性的重要性。

  • 思维树提示结合自我评估和提示链,为解决复杂问题创建了一种全面的策略,允许系统地探索多个解决方案路径。

  • 高级提示工程策略提供了对诸如与 CoT 和 ToT 的自洽性等复杂技术的见解,提供了提高 LLM 生成解决方案的准确性和可靠性的方法。

第十一章:代理规划和反馈

本章涵盖

  • 在 LLMs 上规划并在代理和助手中实现它

  • 通过自定义操作使用 OpenAI 助手平台

  • 在 LLMs 上实现/测试通用规划器

  • 在高级模型中使用反馈机制

  • 在构建代理系统中的规划、推理、评估和反馈

现在我们已经探讨了大型语言模型(LLMs)如何进行推理和规划,本章通过在代理框架内应用规划来将这一概念进一步深化。规划应该是任何代理/助手平台或工具包的核心。我们将从查看规划的基本知识和如何通过提示实现规划器开始。然后,我们将看到如何使用 OpenAI 助手平台进行规划,该平台自动整合了规划。从那里,我们将为 LLMs 构建和实现一个通用规划器。

规划只能走这么远,一个经常未被认识到的元素是反馈。因此,在本章的最后几节中,我们探讨了反馈并在规划器中实现了它。您必须熟悉第十章的内容,所以如果您需要,请查阅它,准备好后,让我们开始规划。

11.1 规划:所有代理/助手的必备工具

不能规划而只能进行简单交互的代理和助手不过是聊天机器人。正如我们在整本书中看到的那样,我们的目标不是构建机器人,而是构建自主思考的代理——能够接受目标,找出解决问题的方法,然后返回结果的代理。

图 11.1 解释了代理/助手将执行的整体规划过程。这个图也在第一章中展示过,但现在让我们更详细地回顾一下。在图的顶部,用户提交一个目标。在一个代理系统中,代理接受目标,构建计划,执行它,然后返回结果。

图

图 11.1 代理规划过程

根据您与 ChatGPT、GPTs、Claude 等平台互动的方式,您可能已经遇到了规划助手,甚至没有注意到。规划正在变得无处不在,并且现在已集成到大多数商业平台中,以使模型看起来更智能和强大。因此,在下一个练习中,我们将查看一个示例来设定基线,并区分不能规划的 LLM 和能规划的代理。

在下一个练习中,我们将使用 Nexus 来演示原始 LLMs 无法独立规划。如果您需要安装、设置和运行 Nexus 的帮助,请参阅第七章。在您安装并准备好 Nexus 后,我们可以开始使用 Gradio 界面运行它,使用下面显示的命令。

列表 11.1 使用 Gradio 界面运行 Nexus
nexus run gradio

Gradio 是一个优秀的网络界面工具,旨在展示 Python 机器学习项目。图 11.2 展示了 Gradio Nexus 界面以及创建代理和使用所选代理引擎(OpenAI、Azure 和 Groq)的过程。除非模型/服务器支持工具/动作的使用,否则您不能使用 LM Studio。Anthropic 的 Claude 支持内部规划,因此为了本练习的目的,请避免使用此模型。

图

图 11.2 在 Nexus 中创建新代理

在创建代理后,我们希望给它提供特定的动作(工具)以执行或完成目标。通常,只提供代理完成其目标所需的动作是最佳做法,原因有几个:

  • 更多动作可能会让代理困惑,不知道该使用哪个,甚至不知道如何解决问题。

  • API 对可提交的工具数量有限制;在撰写本文时,达到这个限制相对容易。

  • 除非这是您的目标,否则代理可能会以您未预料到的方式使用您的动作。但是,请警告,动作可能会有后果。

  • 安全性和安全性需要考虑。LLMs 不会接管世界,但它们会犯错误并迅速偏离正轨。记住,这些代理将独立运行并可能执行任何动作。

警告:在撰写本书并花费数小时与构建代理进行工作期间,我遇到了几个代理采取越界行为的实例,从下载文件到在不打算的情况下编写和执行代码,不断从工具到工具迭代,甚至删除它们不应该删除的文件。观察代理通过行为出现新行为可能会很有趣,但事情可能会迅速偏离正轨。

对于这个练习,我们将定义以下列表中描述的目标。

列表 11.2 展示规划:目标
Search Wikipedia for pages on {topic} and download each page and save it 
to a file called Wikipedia_{topic}.txt

此目标将演示以下动作:

  • search_wikipedia(topic)—搜索维基百科并返回给定搜索词的页面 ID。

  • get_wikipedia_page(page_id)—根据页面 ID 下载页面内容。

  • save_file—将内容保存到文件中。

设置代理上的动作,如图 11.3 所示。您还想要确保规划器设置为 None。我们很快将探讨设置和使用规划器。您不必点击保存;界面会自动保存代理的更改。

图

图 11.3 选择代理的动作并禁用规划器

在您选择动作和规划器后,在列表 11.2 中输入目标。然后点击创建新线程以实例化一个新的对话。在聊天输入中替换您想要搜索的主题,并等待代理响应。以下是一个填充了主题的目标示例,但再次提醒,您可以使用任何您喜欢的主题:

Search Wikipedia for pages on Calgary and download each page and save it to 
a file called Wikipedia_Calgary.txt.

图 11.4 展示了将目标提交给普通代理的结果。我们看到代理执行了工具/动作以搜索主题,但无法执行该步骤之后的任何步骤。如果你还记得我们在第五章中关于动作的讨论和代码示例,OpenAI、Groq 和 Azure OpenAI 都支持并行动作,但不支持顺序或计划动作。

figure

图 11.4 尝试让代理/LLM 完成目标的结果

如果你提交一个包含多个并行任务/动作的目标,LLM 可以合理地回答。然而,如果动作是顺序的,需要一步依赖于另一步,它将失败。记住,并行动作是可以与其他动作并行运行的独立动作。

Anthropic 的 Claude 和 OpenAI 助手支持顺序动作规划。这意味着这两个模型都可以使用顺序计划进行调用,模型将执行它们并返回结果。在下一节中,我们将探讨顺序规划并在实际操作中演示。

11.2 理解顺序规划过程

在下一个练习中,我们将要求一个 OpenAI 助手解决相同的目标。如果你有 Anthropic/Claude 凭证并且已经配置了引擎,你也可以尝试使用该模型进行这个练习。

图 11.5 展示了顺序执行任务(规划)和使用迭代之间的区别。如果你使用过 GPTs、助手或 Claude Sonnet 3.5,你可能已经体验过这种区别。这些高级工具已经通过提示注释、高级训练或两者结合的方式实现了规划。

figure

图 11.5 迭代执行和计划执行之间的区别

随着大型语言模型和聊天服务的演变,大多数模型可能会原生支持某种形式的规划和使用工具。然而,包括 GPT-4o 在内的大多数模型今天只支持动作/工具的使用。

让我们打开 GPT 助手 Playground 来演示顺序规划的实际操作。如果你需要帮助,请参考第六章中的设置指南。我们将使用相同的目标,但这次,我们将运行它针对一个助手(该助手内置了规划)。

在你启动 Playground 后,创建一个新的助手,并分配给它 search_wikipediaget_wikipedia_pagesave_file 动作。图 11.6 展示了将目标输入助手的成果。正如你所见,助手在幕后完成了所有任务,并以用户最终请求的输出响应,实现了目标。

figure

图 11.6 助手处理目标和输出结果

为了展示 OpenAI 助手规划器的有效性,我们将另一个任务,即总结每一页,添加到目标中。插入的任务没有函数/工具,但助手足够聪明,能够利用其总结内容的能力。您可以通过打开[root]assistants_working_folder/Wikipedia_{topic}.txt文件并查看内容来查看助手产生的输出。现在我们了解了 LLM 在没有规划器和规划的情况下如何工作,我们可以继续在下一节创建我们的规划器。

11.3 构建顺序规划器

LLM 工具如 LangChain 和 Semantic Kernel (SK)拥有许多使用各种策略的规划器。然而,编写我们的规划器相对容易,Nexus 还支持插件式接口,允许您添加来自 LangChain 和 SK 等工具或其他派生工具的其他规划器。

规划师可能听起来很复杂,但通过结合规划和推理的提示工程策略,它们很容易实现。在第十章中,我们介绍了推理和制定计划的基础,现在我们可以将这些技能用于实际应用。

列表 11.3 展示了从 SK 派生出的顺序规划器,它扩展了迭代。如列表中所示,提示注释规划器可以适应特定需求或更通用,如所示。此规划器使用 JSON,但规划器可以使用 LLM 理解的任何格式,包括代码。

列表 11.3 basic_nexus_planner.py
You are a planner for Nexus.     #1
Your job is to create a properly formatted JSON plan step by step, to 
satisfy the goal given.
Create a list of subtasks based off the [GOAL] provided.
Each subtask must be from within the [AVAILABLE FUNCTIONS] list. Do not 
use any functions that are not in the list.
Base your decisions on which functions to use from the description and the 
name of the function.
Sometimes, a function may take arguments. Provide them if necessary.
The plan should be as short as possible.
You will also be given a list of corrective, suggestive and epistemic 
feedback from previous plans to help you make your decision.
For example:

[SPECIAL FUNCTIONS]     #2
for-each- prefix
description: execute a function for each item in a list
args: 
- function: the function to execute
- list: the list of items to iterate over
- index: the arg name for the current item in the list

[AVAILABLE FUNCTIONS]
GetJokeTopics
description: Get a list ([str]) of joke topics

EmailTo
description: email the input text to a recipient
args:
- text: the text to email
- recipient: the recipient's email address. Multiple addresses may be 
included if separated by ';'.

Summarize
description: summarize input text
args:
- text: the text to summarize

Joke
description: Generate a funny joke
args:
- topic: the topic to generate a joke about

[GOAL]
"Get a list of joke topics and generate a different joke for each topic. 
Email the jokes to a friend."

[OUTPUT]
    {        
        "subtasks": [
            {"function": "GetJokeTopics"},
            {"function": "for-each",
             "args": {
                       "list": "output_GetJokeTopics",
                       "index": "topic", 
                       "function": 
                                  {
                                   "function": "Joke",
                                   "args": {"topic": "topic"}}}},
            {
             "function": "EmailTo",
              "args": {
                        "text": "for-each_output_GetJokeTopics"
                       ecipient": "friend"}}
        ]
    }
# 2 more examples are given but omitted from this listing

[SPECIAL FUNCTIONS]     #3
for-each
description: execute a function for each item in a list
args: 
- function: the function to execute
- iterator: the list of items to iterate over
- index: the arg name for the current item in the list  

[AVAILABLE FUNCTIONS]     #4
{{$available_functions}}

[GOAL]
{{$goal}}     #5

Be sure to only use functions from the list of available functions. 
The plan should be as short as possible. 
And only return the plan in JSON format.
[OUTPUT]     #6

1 告诉代理如何处理示例的序言指令

2 三个(少样本)示例的开始

3 添加了 for-each 特殊迭代函数

4 可用函数从代理的可用函数列表自动填充。

5 目标在这里插入。

6 代理预期放置输出的位置

图 11.7 展示了构建和运行规划提示的过程,从构建到执行,最后将结果返回给用户。规划师通过构建规划提示,提交给 LLM 构建计划,本地解析和执行计划,将结果返回给 LLM 进行评估和总结,并最终将最终输出返回给用户。

figure

图 11.7 创建和执行计划的规划过程

注意规划过程中的几个细微细节至关重要。通常,计划是在孤立状态下构建的,没有添加上下文历史。这样做是为了专注于目标,因为大多数规划提示消耗许多标记。在执行器内部执行功能通常是在本地环境中进行的,可能包括调用 API、执行代码,甚至运行机器学习模型。

列表 11.4 展示了 BasicNexusPlanner 类中 create_plan 函数的代码;LangChain 和 SK 等工具使用类似的模式。该过程将代理的动作作为字符串加载。然后使用 PromptTemplateManager 将目标和可用函数列表插入到规划器提示模板中,PromptTemplateManager 只是对模板处理代码的包装。模板处理使用简单的正则表达式完成,但也可以使用 Jinja2、Handlebars 或 Mustache 等工具进行更复杂的处理。

列表 11.4 basic_nexus_planner.py (create_plan)
def create_plan(self, nexus, agent, goal: str, prompt: str = PROMPT) -> Plan:
        selected_actions = nexus.get_actions(agent.actions)
        available_functions_string = "\n\n".join(
            format_action(action) for action in selected_actions
        )     #1

        context = {}     #2
        context["goal"] = goal
        context["available_functions"] = available_functions_string

        ptm = PromptTemplateManager()     #3
        prompt = ptm.render_prompt(prompt, context)

        plan_text = nexus.execute_prompt(agent, prompt)     #4
        return Plan(prompt=prompt, 
                    goal=goal, 
                    plan_text=plan_text)     #5

1 加载代理可用的动作,并将结果字符串格式化为规划器

2 将上下文注入到规划器提示模板中。

3 一个简单的模板管理器,在概念上类似于 Jinja2、Handlebars 或 Mustache

4 将填充后的规划器提示发送到 LLM

5 将结果(计划)封装在 Plan 类中并返回以执行。

执行计划的代码(如列表 11.5 所示)解析 JSON 字符串并执行函数。在执行计划时,代码会检测特定的 for-each 函数,该函数遍历列表并执行函数中的每个元素。每个函数执行的结果都添加到上下文中。这个上下文传递给每个函数调用,并作为最终输出返回。

列表 11.5 basic_nexus_planner.py (execute_plan)
def execute_plan(self, nexus, agent, plan: Plan) -> str:
        context = {}
        plan = plan.generated_plan
        for task in plan["subtasks"]:     #1
            if task["function"] == "for-each":     #2
                list_name = task["args"]["list"]
                index_name = task["args"]["index"]
                inner_task = task["args"]["function"]

                list_value = context.get(list_name, [])
                for item in list_value:
                    context[index_name] = item
                    result = nexus.execute_task(agent, inner_task, context)
                    context[f"for-each_{list_name}_{item}"] = result

                for_each_output = [     #2
                    context[f"for-each_{list_name}_{item}"] ↪
                      for item in list_value
                ]
                context[f"for-each_{list_name}"] = for_each_output

                for item in list_value:     #3
                    del context[f"for-each_{list_name}_{item}"]

            else:
                result = nexus.execute_task(agent,
                                            task,
                                            context)     #4
                context[f"output_{task['function']}"] = result

        return context     #5

1 遍历计划中的每个子任务

2 处理应该迭代的函数,并将完整的结果列表添加到上下文中

3 移除单独的 for-each 上下文条目

4 通用任务执行

5 返回完整上下文,包括每个函数调用的结果

整个执行返回的上下文在最终调用 LLM 时发送,LLM 总结结果并返回响应。如果一切按计划进行,LLM 将以结果总结的形式响应。如果出现错误或缺少某些信息,LLM 可能会尝试解决问题或通知用户错误。

现在让我们再次打开 Nexus 并测试一个正在运行的规划器。加载上次使用的相同代理,但这次在高级选项下选择规划器,如图 11.8 所示。然后,像之前一样输入目标提示,让代理去处理。

图

图 11.8 使用基本规划器在 Nexus 中请求完成目标的结果

几分钟后,代理返回保存的文件,在某些情况下,它可能还会提供额外信息,例如下一步操作和如何处理输出。这是因为代理被赋予了对其所完成工作的概述。但请记住,计划执行是在本地级别完成的,并且只向 LLM 发送了上下文、计划和目标。

这意味着计划执行可以由任何进程完成,而不仅仅是代理。在 LLM 外部执行计划可以减少代理执行所需令牌和工具的使用。这也意味着 LLM 不需要支持工具使用就可以使用规划器。

在 Nexus 内部,当启用规划器时,代理引擎工具被绕过。相反,规划器完成动作执行,代理只通过输出上下文的传递来了解动作。这对支持工具使用但不能规划的模式来说可能是个好主意。然而,规划器可能会限制支持工具使用和规划的双重支持模式,如 Claude。

通常,你想要了解你所使用的 LLM 的能力。如果你不确定这些细节,那么一点点的尝试和错误也可以工作。要求代理在有和无计划启用的情况下完成多步骤目标,然后查看结果。

规划允许代理完成多个顺序任务以实现更复杂的目标。外部或提示规划的问题在于它绕过了反馈迭代循环,这有助于快速纠正问题。正因为如此,OpenAI 和其他人现在正在直接在 LLM 层面集成推理和规划,正如我们将在下一节中看到的。

11.4 检查步骤式规划器:OpenAI Strawberry

o1-preview 模型的发布,代号草莓,为可用于代理系统的 LLM 类型带来了巨大的转变。草莓不仅被宣称为在数学、科学和一般计算任务上更有效率,而且还能在 LLM 中直接进行推理、规划、评估和反馈。

考虑第十章中的时间旅行问题,如图 11.9 所示。如果你还记得,这个问题使用 GPT-4 和其他类似的 LLM 难以解决。然而,通过应用推理和反馈,我们能够偶尔产生正确的输出。

figure

图 11.9 重新审视时间旅行问题

作为实验,将此问题输入到 ChatGPT 中,使用 o1-preview 模型,如列表 11.6 所示。坐下来等待几秒钟,等待答案。是的,模型仍然答错了。

列表 11.6 时间旅行推理/规划问题
In a sci-fi film, Alex is a time traveler who decides to go back in time 
to witness a famous historical battle that took place 100 years ago, 
which lasted for 10 days. He arrives three days before the battle starts. 
However, after spending six days in the past, he jumps forward in time 
by 50 years and stays there for 20 days. Then, he travels back to 
witness the end of the battle. How many days does Alex spend in the past 
before he sees the end of the battle?

虽然看到模型给出错误答案可能有些令人失望,但它确实在分解问题和展示其答案方面做得更好。列表 11.7 显示了将列表 11.6 中的问题提出给草莓模型的样本输出。请注意,由于 LLM 的随机性,你可能会得到不同的答案。

列表 11.7 o1-preview 对时间旅行问题的响应
To determine how many days Alex spends in the past before witnessing the 
end of the battle, let's reconstruct the timeline step by step:
Arrival in the Past:
Alex arrives 3 days before the battle begins.
He spends these 3 days waiting for the battle to start.
During the Battle:
The battle starts, and Alex experiences the first 3 days of it.
At this point, he has spent a total of 6 days in the past (3 days before 
the battle and 3 days during the battle).
Time Jump Forward:     #1
After these 6 days, Alex jumps 50 years forward in time.
He spends 20 days in this future time.
Return to the Past:
Alex then returns to the past to witness the end of the battle.
Since the battle lasts 10 days and he left after day 3 of the battle, 
there are 7 days remaining in the battle.
However, because he was away for 20 days, which is longer than the 
remaining 7 days of the battle, he must return to a point in time before 
the battle ends to witness its conclusion.
To see the end of the battle, he returns to the past on the last day of 
the battle.
Final Days Spent Before the Battle Ends:
Upon returning, he spends 1 more day in the past to witness the battle's 
end.
Total Days Spent in the Past Before Seeing the End:
6 days before jumping forward in time.
1 day after returning to the past.
Total: 6 + 1 = 7 days.
Answer: 7

1 模型出错的地方变得明显。

因为我们知道正确答案是 27,所以我们知道 LLM 是错误的,但如果我们不知道,我们同样可以假设工作和推理都是正确的。当我们从 LLM 交互和代理系统中移除反馈时,可能会发生类似的问题。反馈可以引导模型自我纠正。

然而,如果我们不知道正确答案是 27(如果你假设他没有花一天时间来见证战斗,那么是 26),并且假设 LLM 或代理是正确的呢?嗯,这是一个我们可以通过几个简单的提示来纠正的问题,这些提示可以激发 LLM 进行推理和规划反馈。然而,这些技术对 LLM 或像 OpenAI 助手这样的包装器更有效,它们在模型内部提供推理和规划。

我们想要做的是向 LLM 提供反馈,但理解这个反馈可能对我们来说会很困难。幸运的是,只要我们给出正确的答案,我们就可以直接从 LLM 中获取反馈。列表 11.8 展示了如何从 LLM 中生成有关我们时间旅行问题的建设性反馈。

列表 11.8 生成反馈
the correct answer is 27, please review what you did wrong and suggest 
feedback you could give yourself when trying to solve similar future problems

在模型回答问题错误之后输入此内容将生成你可以用来通过提示或作为系统指令的一部分来引导模型的反馈。列表 11.9 展示了 o1-preview 提供的反馈示例。然后你可以提取这个反馈,并在下一次想要解决复杂的时间旅行问题时增强指令。

列表 11.9 生成反馈
Feedback for Future Problems
Understanding Time Frames in Time Travel:
Relative Time Frames: Always consider the time periods relative to the 
original point of reference. In this problem, even though Alex jumps 
forward 50 years, he is still in the past relative to his original 
present time.
Including All Relevant Periods: 
Ensure that all time periods spent in the past (or future) are included 
in the total calculation, even if they involve moving to different 
points in time.
Time Continuity: 
Remember that while the traveler moves through time, events in other 
time periods continue to progress unless specified otherwise.
Application:
Careful Reading: 
Pay close attention to the details provided, such as the durations and 
the sequences of time jumps.
Visualization: 
It may help to draw a timeline to visualize the sequence of events and 
time periods involved.
Double-Checking Calculations: 
After solving, revisit each step to ensure all time periods are 
accounted for correctly.

这种反馈技术将始终适用于 o1-preview 等模型,但其他模型即使在给出这种反馈的情况下,可能仍然难以正确回答。随着时间的推移,随着模型变得越来越智能,这种技术可能普遍适用于大多数模型。然而,即使模型变得越来越聪明,这种反馈机制可能仍然是必不可少的,因为语言是微妙的,并不是我们向 LLM 提出的每个问题都有一个明显的绝对答案。以我们的例子问题为例。这个问题是要求问题解决者从问题中做出假设并建立关联的一个很好的例子。在从地质学到行为科学等众多科学领域,回答同一个问题可能会得到一系列答案。接下来,让我们看看如何将推理、规划、评估和反馈的应用应用于代理系统的一些技术。

11.5 将规划、推理、评估和反馈应用于辅助和代理系统

在最近几章中,我们探讨了如何实现规划、推理、反馈和评估的代理组件。现在我们来看看这些组件何时、何地可以集成到辅助和代理系统中,以实现实时生产、研究或开发。

虽然并非所有这些组件都适合每个应用,但了解何时以及如何应用哪个组件是有用的。在下一节中,我们将探讨如何将规划集成到辅助/代理系统中。

11.5.1 辅助/代理规划的运用

规划是辅助工具或代理可以规划执行一系列任务的组件,无论是串联、并行还是其他组合。我们通常将规划与工具使用联系起来,并且,合理地,任何使用工具的系统都可能希望有一个能够胜任的规划器。然而,并非所有系统都是同等创建的,所以在第 11.1 表中,我们将回顾在哪里、何时以及如何实现规划器。

表 11.1 规划在各种应用中的运用和实施时间
应用 实现 环境 目的 时间 配置
个人助理 在或 LLM 内部 个人设备 促进工具使用 在响应期间 作为提示或 LLM 的一部分
客户服务机器人 不典型;受限环境 受限环境,无工具使用
自主代理 作为代理提示和 LLM 的一部分 服务器或服务 促进复杂工具使用和任务规划 在构建代理和/或响应期间 在代理或 LLM 内部
协作工作流程 作为 LLM 的一部分 共享画布或编码 促进复杂工具使用 在响应期间 在 LLM 内部
游戏人工智能 作为 LLM 的一部分 服务器或应用 复杂工具使用和规划 在响应之前或期间 在 LLM 内部
研究 任何地方 服务器 促进工具使用并参与复杂任务工作流程 在响应生成之前、期间和之后 任何地方

表 11.1 展示了我们可能发现辅助工具或代理被部署以协助各种应用场景的几个不同应用场景。为了提供更多信息和建议,此列表提供了关于如何在每个应用中运用规划更详细的说明:

  • 个人助理—虽然这项应用推出较慢,但 LLM 个人助理有望在未来超越 Alexa 和 Siri。规划对于这些新助理/代理来说将至关重要,以协调众多复杂任务并在串联或并行中执行工具(动作)。

  • 客户服务机器人—由于这个环境的可控性,直接与客户互动的助手不太可能使用受控且非常具体的工具。这意味着这类助手可能不需要广泛的规划。

  • 自主代理—正如我们在前面的章节中看到的,具有规划能力的代理可以完成一系列复杂任务以实现各种目标。规划将是任何自主代理系统的基本要素。

  • 协作工作流程——将这些视为与编码者或作家并肩而坐的代理或助手。虽然这些工作流程仍处于早期开发阶段,但想象一下代理自动被分配与开发者一起编写和执行测试代码的工作流程。规划将是执行这些复杂未来工作流程的一个关键部分。

  • 游戏人工智能——虽然将 LLM 应用于游戏仍处于早期阶段,但想象游戏中能够协助或挑战玩家的代理或助手并不困难。赋予这些代理规划执行复杂工作流程的能力可能会改变我们玩游戏的方式和对象。

  • 研究——与协作工作流程类似,这些代理将负责从现有信息来源中推导出新想法。找到这些信息可能将通过广泛使用工具来促进,这将受益于规划的协调。

如您所见,规划是许多 LLM 应用的一个关键部分,无论是通过工具使用的协调还是其他方式。在下一节中,我们将探讨推理的下一个组成部分以及它如何应用于相同的应用堆栈。

11.5.2 助手/代理推理的应用

推理,虽然通常与规划和任务完成紧密相关,但也是一个可以独立存在的组成部分。随着大型语言模型(LLM)的成熟和智能化,推理通常被包含在 LLM 本身中。然而,并非所有应用都能从广泛的推理中受益,因为它往往在 LLM 响应中引入一个思考周期。表 11.2 从高层次描述了推理组件如何与各种 LLM 应用类型集成。

表 11.2 在各种应用中推理何时何地被使用
应用 实现 环境 目的 时间 配置
个人助理 在 LLM 内部 个人设备 将工作分解成步骤 在响应期间 作为提示或 LLM 的一部分
客户服务机器人 不典型;通常只是信息性 有限工具使用和复合工具使用需求
自主代理 作为代理提示和 LLM 的一部分 服务器或服务 促进复杂工具使用和任务规划 作为 LLM 的一部分,外部推理不适合 在代理或 LLM 内部
协作工作流程 作为 LLM 的一部分 共享画布或编码 协助将工作分解 在响应期间 在 LLM 内部
游戏人工智能 作为 LLM 的一部分 服务器或应用 承担复杂行动的必要条件 在响应之前或期间 在 LLM 内部
研究 任何地方 服务器 理解如何解决复杂问题并参与复杂任务工作流程 在响应之前、期间和之后 任何地方

表 11.2 显示了我们在其中可能找到部署以协助某些工作的助手或代理的几个不同应用场景。为了提供更多信息和建议,此列表提供了有关如何在每个应用中应用推理的更多详细信息:

  • 个人助理——根据应用的不同,代理使用的推理量可能有限。推理是一个需要 LLM 思考问题的过程,这通常需要根据问题的复杂性和提示的范围来调整更长的响应时间。在许多情况下,旨在接近实时推理的响应可能被禁用或降低。虽然这可能限制代理可以交互的复杂性,但有限的或没有推理可以提高响应时间并增加用户满意度。

  • 客户服务机器人——同样,由于这个环境的可控性,直接与客户互动的助手不太可能需要执行复杂或任何形式的推理。

  • 自主代理——虽然推理是自主代理的一个强大组成部分,但我们仍然不知道推理过多是多少。随着草莓等模型在代理工作流程中变得可用,我们可以判断在什么情况下广泛的推理可能不是必需的。这肯定适用于定义明确的自主代理工作流程。

  • 协作工作流程——同样,应用推理会在 LLM 交互中产生开销。广泛的推理可能对某些工作流程有益,而其他定义明确的工作流程可能会受到影响。这可能意味着这些类型的工作流程将从多个代理中受益——那些具有推理能力和那些没有推理能力的代理。

  • 游戏人工智能——与其他应用类似,重推理应用可能不适合大多数游戏人工智能。游戏特别需要 LLM 响应时间快,这肯定将是推理在通用战术代理中的应用。当然,这并不排除使用其他推理代理,这些代理可能提供更多战略控制。

  • 研究——推理可能对任何复杂的研究任务都至关重要,原因有几个。一个很好的例子是草莓模型的应用,我们已经在数学和科学研究中看到过。

虽然我们经常将推理与规划一起考虑,但可能存在每种实施水平都不同的条件。在下一节中,我们将考虑各种应用的代理评估支柱。

11.5.3 评估在代理系统中的应用

评估是代理/助手系统中可以指导系统表现好坏的组件。虽然我们在一些代理工作流程中展示了如何整合评估,但在代理系统中评估通常是一个外部组件。然而,它也是大多数 LLM 应用的核心组件,并且在大多数开发中不应被忽视。表 11.3 从高层次描述了评估组件如何与各种 LLM 应用类型集成。

表 11.3 在各种应用中何时何地使用评估
应用 实现 环境 目的 时间 配置
个人助手 外部 服务器 确定系统的工作效果 交互后 通常外部开发
客户服务机器人 外部监控 服务器 评估每次交互的成功率 交互后 外部于代理系统
自主代理 外部或内部 服务器或服务 确定任务完成后的系统成功率 交互后 外部或内部
协作工作流程 外部 共享画布或编码 评估协作的成功率 交互后 外部服务
游戏人工智能 外部或内部 服务器或应用 评估代理或评估策略或行动的成功率 交互后 外部或作为代理或另一个代理的一部分
研究 结合人工和 LLM 服务器和人 评估研究输出的效果 生成输出后 取决于问题的复杂性和进行的研究

表 11.3 展示了我们在各种应用场景中可能找到的助手或代理被部署以协助某些功能的几个不同场景。为了提供更多信息和建议,此列表提供了关于如何在每个应用中使用评估的更多详细信息:

  • 个人助手—在大多数情况下,评估组件将被用来处理和指导代理响应的性能。在主要使用检索增强生成(RAG)进行文档探索的系统中,评估表明助手如何响应信息请求。

  • 客户服务机器人—评估服务机器人对于理解机器人如何响应客户请求至关重要。在许多情况下,强大的 RAG 知识元素可能是系统的一个需要广泛和持续评估的元素。同样,在大多数评估组件中,这个元素是主要工作系统之外的,并且通常作为监控多个指标的一般性能的一部分运行。

  • 自主代理——在大多数情况下,对代理输出的手动审查将是自主代理成功的主要指导。然而,在某些情况下,内部评估可以帮助指导代理在执行复杂任务时,或作为改进最终输出的手段。CrewAI 和 AutoGen 等多个代理系统是使用内部反馈来改进生成输出的自主代理的例子。

  • 协作工作流程——在大多数直接情况下,这些类型的工作流程中会持续进行手动评估。用户通常会立即并在近乎实时的情况下通过评估输出纠正助手/代理。可以添加额外的代理,类似于自主代理,以实现更广泛的协作工作流程。

  • 游戏人工智能——评估通常会被分解为开发评估——评估代理与游戏交互的方式——和在游戏中的评估,评估代理在任务中成功与否。实施后一种评估形式类似于自主代理,但旨在改进某些策略或执行。这种在游戏中的评估也可能从记忆和反馈手段中受益。

  • 研究——在这个层面上,评估通常是在完成研究任务后作为手动工作进行的。代理可以采用类似于自主代理的某种形式的评估来改进生成的输出,甚至可能在内部思考如何扩展或进一步研究输出的评估。由于这目前是代理发展的一个新领域,其执行效果如何还有待观察。

评估是任何代理或助手系统的基本要素,尤其是当该系统向用户提供真实和基本信息时。为代理和助手开发评估系统可能是可以或应该有自己一本书的内容。在本章的最后部分,我们将探讨各种 LLM 应用的反馈实现。

11.5.4 将反馈应用于代理/助手应用

作为代理系统组成部分的反馈通常如果不是总是,被实现为一个外部组件——至少目前是这样。也许对评估系统的信心可能会提高,以至于反馈定期被纳入这些系统。表 11.4 展示了反馈如何被整合到各种 LLM 应用中。

表 11.4 在各种应用中何时何地使用反馈
应用 实现 环境 目的 时间 配置
个人助手 外部或由用户 聚合到服务器或作为系统的一部分 提供系统改进的手段 在交互后或交互期间 内部和外部
客户服务机器人 外部监控 聚合到服务器 确认并提供系统改进的手段 交互后 代理系统外部
自主代理 外部 在服务器端汇总 提供系统改进的手段 在交互之后 外部
协作工作流程 在交互过程中 共享画布或编码 提供即时反馈的机制 在交互过程中 外部服务
游戏人工智能 外部或内部 服务器或应用 作为内部评估反馈的一部分,以提供动态改进 在交互之后或交互过程中 外部或作为代理或另一个代理的一部分
研究 结合人工和 LLM 服务器和人类 评估开发的研究输出 在生成输出之后 依赖于问题的复杂性和进行的研究

表 11.4 显示了我们在其中可能找到部署以协助某些功能的助理或代理的几个应用场景。为了提供更多信息和建议,此列表提供了关于如何在每个应用中采用反馈的更多详细信息:

  • 个人助理—如果助理或代理通过聊天式界面与用户互动,用户可以提供直接和即时的反馈。这种反馈是否在未来的对话或互动中持续,通常是在代理记忆中发展的。例如 ChatGPT 这样的助理现在已经集成了记忆功能,并能从明确用户反馈中受益。

  • 客户服务机器人—用户或系统反馈通常在交互完成后通过调查提供。这通常意味着反馈被调节到一个外部系统中,该系统汇总反馈以供后续改进。

  • 自主代理—与机器人类似,自主代理中的反馈通常是在代理完成用户随后审查的任务之后进行的。由于许多事情可能是主观的,因此反馈机制可能更难以捕捉。本章探索的用于生成反馈的方法可以用于提示工程改进。

  • 协作工作流程—与个人助理类似,这些类型的应用可以从用户的即时和直接反馈中受益。同样,如何将此类信息持久化到会话中通常是代理记忆的实现。

  • 游戏人工智能—反馈可以在通过额外和多个代理进行评估的同时实施。这种反馈形式可能是单次使用的,存在于当前交互中,或者可能作为记忆持久存在。想象一下,一个能够评估其行为、根据反馈改进这些行为并记住这些改进的游戏人工智能。虽然这种模式对于游戏来说并不理想,但它肯定会改善游戏体验。

  • 研究—与研究背景下的评估类似,反馈通常在输出评估后离线进行。虽然已经进行了一些使用多个代理系统的研究,这些系统结合了用于评估和反馈的代理,但这些系统并不总是表现良好,至少不是使用当前最先进的模型。相反,通常最好在最后将反馈和评估隔离,以避免常见的反馈循环问题。

反馈是代理和助手系统的一个强大组件,但并不总是需要在首次发布时包含。然而,纳入严格的反馈和评估机制可以极大地从长远角度有利于代理系统,包括持续监控和提供改进系统各个方面的信心。

你如何在你的代理系统中实现这些组件,部分可能由你选择的代理平台架构指导。现在你了解了每个组件的细微差别,你也拥有了指导你选择适合你的应用程序和业务用例的正确代理系统的知识。无论你的应用程序如何,你几乎在所有情况下都希望使用几个代理组件。

随着代理系统成熟以及 LLM 本身变得更智能,我们今天认为的一些外部组件可能会紧密集成。我们已经看到推理和规划被整合到一个如 Strawberry 这样的模型中。当然,随着我们接近理论上的通用人工智能里程碑,我们可能会看到能够进行长期自我评估和反馈的模型。

在任何情况下,我希望你喜欢与我一起探索这个令人难以置信的新兴技术前沿的旅程,这个技术将无疑改变我们对工作和通过代理进行工作的看法。

11.6 练习

使用以下练习来提高你对材料的了解:

  • 练习 1—实现一个简单的规划代理(入门级)

目标—学习如何使用提示生成一系列动作来实现基本规划代理。

任务:

    • 创建一个代理,它接收一个目标,将其分解成步骤,并按顺序执行这些步骤。

    • 定义一个简单的目标,例如从维基百科检索信息并将其保存到文件中。

    • 使用基本的规划器提示(参考第 11.3 节中的规划器示例)实现代理。

    • 运行代理,并评估它在规划和执行每一步时的表现。

  • 练习 2—在规划代理中测试反馈集成(中级)

目标—理解反馈机制如何提高代理系统的性能。

任务:

    • 修改练习 1 中的代理,在每个任务后包含一个反馈循环。

    • 使用反馈来调整或纠正序列中的下一个任务。

    • 通过给它一个更复杂的任务,如从多个来源收集数据,来测试代理,并观察反馈如何提高其性能。

    • 记录并比较添加反馈前后代理的行为。

  • 练习 3—实验并行和顺序规划(中级)

目标—学习并行和顺序动作之间的区别以及它们如何影响代理行为。

任务:

    • 使用 Nexus 设置两个代理:一个并行执行任务,另一个顺序执行任务。

    • 定义一个多步骤目标,其中一些动作依赖于先前动作的结果(顺序),而另一些可以同时进行(并行)。

    • 比较两个代理的性能和输出,注意在需要顺序步骤时并行执行中的任何错误或不效率。

  • 练习 4—在 Nexus 中构建和集成自定义规划器(高级)

目标—学习如何构建自定义规划器并将其集成到代理平台中。

任务:

    • 使用第 11.3 节中的提示工程策略编写自定义规划器,确保它支持顺序任务执行。

    • 将此规划器集成到 Nexus 中,并创建一个使用它的代理。

    • 使用涉及多个步骤和工具(例如,数据检索、处理和保存)的复杂目标测试规划器。

    • 评估自定义规划器与 Nexus 内置规划器或其他平台相比的性能。

  • 练习 5—在顺序规划中实现错误处理和反馈(高级)

目标—学习如何在代理系统中实现错误处理和反馈以完善顺序规划。

任务:

    • 使用顺序规划器,设置一个代理执行可能遇到常见错误的目标(例如,失败的 API 调用、缺失数据或无效输入)。

    • 在规划器中实现错误处理机制以识别和响应这些错误。

    • 添加反馈循环以根据遇到的错误调整计划或重试动作。

    • 通过在执行过程中故意造成错误来测试系统,并观察代理如何恢复或调整其计划。

概述

  • 规划对于代理和助手至关重要,它允许他们接受一个目标,将其分解为步骤,并执行它们。没有规划,代理将简化为类似聊天机器人的交互。

  • 代理必须区分并行和顺序动作。许多大型语言模型可以处理并行动作,但只有高级模型支持顺序规划,这对于复杂任务完成至关重要。

  • 反馈对于指导代理纠正方向并在一段时间内提高性能至关重要。本章展示了如何将反馈机制与代理集成以完善其决策过程。

  • 如 OpenAI 助手和 Anthropic 的 Claude 这样的平台支持内部规划并能执行复杂的多步骤任务。使用这些平台的代理可以使用顺序动作规划进行复杂的流程。

  • 正确选择和限制代理动作对于避免混淆和意外行为至关重要。过多的动作可能会压倒代理,而不必要的工具可能会被误用。

  • Nexus 允许通过灵活的界面创建和管理代理,用户可以实施自定义规划器、设定目标和分配工具。本章包括使用 Nexus 的实际示例,以突出原始 LLM 和规划器增强代理之间的差异。

  • 编写自定义规划器很简单,可以使用提示工程策略。LangChain 和 Semantic Kernel 等工具提供各种规划器,可以根据特定的代理需求进行适配或扩展。

  • 例如,OpenAI Strawberry 这样的模型将推理、规划、评估和反馈直接集成到 LLM 中,提供了更精确的问题解决能力。

  • 评估有助于确定代理系统表现的好坏,可以根据用例在内部或外部实施。

  • 随着 LLM 的发展,推理、规划和反馈机制可能将深度集成到模型中,为更自主和智能的代理系统铺平道路。

附录 A 访问 OpenAI 大型语言模型

尽管有多个商业大型语言模型(LLM)服务可用,但本书建议直接使用 OpenAI 服务或通过 Azure OpenAI Studio。要访问任一服务,您必须创建一个账户并注册一种本附录未涵盖的支付方式。GPT-4 系列 LLM 被认为是业界最佳,更适合代理开发。使用开源和替代服务始终是一个选择,但通常在您使用 GPT-4 一段时间后再考虑。

A.1 访问 OpenAI 账户和密钥

以下一般步骤可以帮助您快速设置使用 OpenAI LLMs 进行代理开发。尽管使用 OpenAI 和其他商业 LLM 需要付费,但您预计只需支付不到 100 美元即可完成本书中的所有练习:

  1. 前往openai.com登录,或注册账户并登录。如果您是第一次创建账户,您可能会获得一定数量的免费信用。如果您已有账户,您必须注册支付方式和类型。通常一次性购买一定数量的信用会更好。这将使您更好地管理成本并避免超支。

  2. 登录到平台后,选择 ChatGPT 或 API,如图 A.1 所示。选择 API。

图

图 A.1 选择 OpenAI 平台的 API 部分
    1. 打开左侧菜单,选择 API 密钥选项,如图 A.2 所示。

图

图 A.2 选择 API 密钥选项
    1. 点击创建按钮创建一个新的密钥,为密钥输入一个名称,然后点击创建秘密密钥按钮,如图 A.3 所示。

图

图 A.3 创建秘密 API 密钥
    1. 使用复制按钮,将密钥复制并粘贴到记事本或其他安全区域以保存,如图 A.4 所示。请保密此密钥,并确保它仅存在于您的开发机器上。

图

图 A.4 将密钥复制并粘贴到知名的安全位置

生成密钥后,您可以在.env配置文件或其他注册 OpenAI 密钥的方式中使用它。对于本书中使用的大多数包,配置 OpenAI 通常只需要密钥。其他服务,如 Azure OpenAI,将需要配置模型部署和基础 URL,如下一节所述。

A.2 Azure OpenAI Studio、密钥和部署

通过与 OpenAI 的持续关系,Microsoft 在 Azure OpenAI Studio 中以相同的价格托管相同的模型。偶尔,Azure 可能落后于模型版本,但 Microsoft 通常与最新的 OpenAI 模型保持同步。

这些指南将更加通用,因为访问 Azure 和创建账户以及访问工作室的方法有几种(具体说明请参阅 Microsoft 文档):

  1. 登录到您的 Azure 门户账户订阅。

  2. 在对您有意义的区域创建一个新的 Azure OpenAI Studio 资源。在撰写本文时,并非所有区域都提供了对所有模型的访问。您可能需要首先检查您所在区域可用的模型。这也会根据您的账户和用途而具体化。

在 Azure OpenAI 中,模型通过称为部署的资源分配公开。部署封装了一个模型,例如 GPT-4,并提供对资源的访问。图 A.5 显示了通过部署公开的各种模型示例。

图表

图 A.5 通过 Azure OpenAI Studio 部署部署模型
    1. 点击创建新部署按钮以创建新的部署,然后选择您想要部署的模型。
    1. 在模型被封装在部署中之后,您必须访问父 Azure OpenAI 资源。从那里,您可以访问配置连接所需的密钥、端点或基础 URL,如图 A.6 所示。

图表

图 A.6 获取用于访问服务的密钥和基础 URL

再次提醒,如果您遇到困难,Microsoft 文档可以引导您走向正确的方向。在连接到 Azure OpenAI Studio 或另一个 LLM 等资源时,使用 OpenAI 工具时应记住的三个关键差异如下:

  • 访问模型的API 密钥

  • 模型所在的基础URL端点

  • 模型部署的名称

如果由于任何原因无法访问模型,一个很好的替代方案是开源模型。设置和消费开源 LLMs 的内容在第二章中介绍。

附录 B Python 开发环境

虽然本书假设读者是经验丰富的 Python 开发者,但这可能意味着很多不同的事情。在本附录中,我们查看配置一个将与此书中的代码示例一起工作的 Python 开发环境。您可以使用其他集成开发环境(IDE),但并非所有工具,特别是扩展,都能在所有 IDE 中工作。

B.1 下载源代码

要下载和运行源代码,请安装 Git,然后在本地上拉仓库。以下是从本书 GitHub 仓库拉取代码的高级步骤:

  1. 如果需要,请安装 Git。Git 可以从多个来源安装,但一个好的选择是主发布版,可在以下位置找到:git-scm.com/downloads。按照说明下载并安装适用于您操作系统的工具。

  2. 在您想要下载源代码的文件夹中打开终端,然后输入以下命令:

git clone https://github.com/cxbxmxcx/GPT-Agents.git
    1. 代码下载完成后,您可以从打开您正在处理的章节文件夹在 Visual Studio Code (VS Code) 中开始。如果您需要安装 VS Code 或了解如何将章节文件夹作为工作区加载,请参阅本附录中的 B.5 节。

B.2 安装 Python

Python 提供了不同的版本和部署方式。本书依赖于标准的 Python 安装,版本为 3.10。Anaconda 是另一种流行的 Python 部署方式,也可以使用。然而,本书中的所有材料都已使用 Python 3.10 虚拟环境运行和测试过:

  1. 访问 www.python.org/downloads/

  2. 定位并下载适用于您操作系统的 Python 3.10 最新版本。

  3. 使用您操作系统的说明在您的机器上安装发布版本。

  4. 要确认您的安装,打开终端,并执行以下命令:

python –-version

版本应该是 3.10,但如果不是,请不要担心。您可能已安装多个 Python 版本。我们将在设置 VS Code 时也确认安装。

B.3 安装 VS Code

安装 VS Code 相对简单,只需几个步骤即可完成:

  1. 访问 code.visualstudio.com

  2. 为您的操作系统下载 VS Code 的稳定版本。

  3. 下载完发布版本后,按照您操作系统的安装说明进行操作。

  4. 启动适用于您操作系统的 VS Code,并确保没有警告或错误出现。如果您遇到问题,请尝试重新启动计算机和/或重新安装。

在 VS Code 运行时,我们可以安装必要的扩展。我们将在下一节中介绍这些扩展。

B.4 安装 VS Code Python 扩展

VS Code 有数千个扩展可以提供优秀的 Python 编程环境。这里推荐的只是你可以独立探索的开始。但是,请注意,并非所有扩展都是同等创建的。在安装新扩展时,查看安装次数和评分。通常,少于四星级的扩展应避免使用。要安装扩展,请按照以下步骤操作:

  1. 启动 VS Code,并打开扩展面板,如图 B.1 所示。

图

图 B.1 安装 VS Code 扩展
    1. 安装以下列表中的扩展:
    • Python,用于环境和语言支持

    • Python 扩展包,用于涵盖其他扩展

    • Python 环境管理器,用于管理环境

    • Python 缩进,用于代码格式化

    • Flake8,用于代码格式化/代码检查

    • 提示流,用于测试 LLM 提示

    • 语义内核工具,用于与语义内核框架一起工作

    • Docker,用于管理 Docker 容器

    • Dev Containers,用于使用容器运行开发环境

你只需要为你在运行的每个 VS Code 环境安装扩展。通常,这意味着只为你的操作系统安装的 VS Code 安装扩展。然而,如果你在容器中运行 VS Code,你必须为每个运行的容器安装扩展。在 Dev Containers 扩展中与 Python 一起工作将在本附录的后面介绍。

B.5 使用 VS Code 创建新的 Python 环境

在开发 Python 项目时,你通常希望创建隔离的虚拟环境。这有助于管理跨各种任务和工具的多个包依赖关系。在本书中,建议为每个新章节创建一个新的虚拟环境。VS Code 可以通过以下步骤快速有效地帮助你创建和管理多个 Python 环境:

  1. 按 Ctrl-Shift-P(Cmd-Shift-P)打开命令面板,并选择 Python:创建环境,如图 B.2 所示。

图

图 B.2 设置章节虚拟环境的步骤
    1. 选择环境类型,可以是 Venv 或 Conda。本书演示了 Venv,但 Conda 也应该可以工作。
    1. 选择 Python 安装。本书中的代码至少使用了 Python 3.10。本书中介绍的工具和框架都是前沿的,因此它们应该支持 Python 的后续版本。
    1. 确认章节文件夹中的 requirements.txt 文件已被选中。这将安装当前章节的所有需求。

你应该为本书的每个新章节完成这些步骤。另一种选择是使用 VS Code 开发容器,这将在下一节介绍。

B.6 使用 VS Code Dev Containers(Docker)

当与能够生成和执行代码的高级代理和代理一起工作时,通常建议在隔离的容器中运行它们。容器隔离可以防止操作系统中断或损坏,并为部署代理提供一个基础。

熟悉容器和 Docker 等平台可能是一项庞大的任务,以掌握所有内容。幸运的是,开始使用容器只需要很少的知识,而 VS Code 扩展使得这一过程更加容易。

你首先需要安装一个容器工具集。Docker 是免费的(只要你将工具用作爱好或你是学生),并且是最容易获得的。按照以下说明安装 Docker 并开始使用容器:

  1. 访问 Docker 桌面下载页面 www.docker.com/products/docker-desktop

  2. 下载并安装适用于你的操作系统的 Docker。按照要求遵循任何其他说明。

  3. 启动 Docker 桌面应用程序。完成此步骤将确认你已经安装并按预期工作。

  4. 打开 VS Code,并确认 1.4 节中列出的 Docker 扩展已安装。

配置好 Docker 和 VS Code 后,你可以按照以下步骤继续使用 Dev Containers:

  1. 打开一个新的 VS Code 实例。

  2. 选择打开远程窗口,如图 B.3 所示。

figure

图 B.3 在 VS Code 中打开容器的远程窗口
    1. 选择“在容器中打开文件夹”以从文件夹启动容器,或选择“新建 Dev 容器”以无文件夹启动。

容器启动后,你的 VS Code 环境将会连接。这允许你在容器中开发代码,无需担心依赖项无法正常工作。

posted @ 2025-09-12 14:05  绝不原创的飞龙  阅读(4)  评论(0)    收藏  举报