生成式人工智能系统(GenAISys)的控制器需要两个关键组件:对话代理和编排器。对话代理由生成式 AI 模型驱动,与人类用户和系统流程交互。另一方面,编排器则是一组生成式 AI 和非 AI 功能,例如管理用户角色、内容生成、激活机器学习算法以及执行传统查询。构建一个功能完整的 GenAISys,这两者缺一不可。
如果我们仔细审视这个架构,会发现软件编排器和用户界面早在第一代计算机时代就已经存在。任何具备基本功能的操作系统,都有能够触发磁盘空间警报、内存使用情况及数百项其他功能的编排器。如今的用户界面直观且具备事件驱动功能,但从高层次看,GenAISys 的底层架构仍然延续了数十年来的软件设计原则。那么,经典软件控制器与 GenAISys 控制器的区别在哪里?
我们用一个词来总结区别:适应性。在经典软件控制器中,任务序列或多或少是硬编码的;而在 GenAISys 中,用户界面是一个灵活的对话式 AI 代理,背后的生成式 AI 模型经过预训练,能够响应广泛请求,无需额外编码。此外,编排器也不是锁定在静态流程中,它可以根据用户(人类或系统)的提示动态调整触发的任务。
本章我们将采取实践的方式,基于上一章定义的 GenAISys 架构,构建一个定制的 GenAISys。首先在 Python 中定义 AI 控制器的结构,拆分为两部分——对话代理和编排器,探讨二者如何交互。接着用 GPT-4o 构建对话代理,自动化实现第一章中的上下文感知和记忆保留功能。我们的系统将支持短期和长期记忆,以及多用户和跨会话能力,超越标准协助工具的典型功能。
最后,我们将构建一个 AI 控制器的结构,用以解释用户输入并触发响应场景。响应可能是情感分析,或是语义(硬科学)分析,具体取决于 AI 控制器将分析和管理的内容。我们定制的 GenAISys 将为领域专用的 RAG 打下基础,而这正是标准 ChatGPT 级系统在处理大量数据时难以实现的,尤其是在数据每日更新(如产品或服务的日销售数据)等场景中。到本章结束时,你将掌握构建 GenAISys AI 控制器基础的技能,后续章节将继续完善它。
简而言之,本章涵盖以下内容:
- AI 控制器的架构
- AI 对话代理的架构及工作流程
- 在代码中实现短期和长期记忆会话的存储
- AI 编排器的架构及意图功能
- 创建包含指令场景的 GenAI 场景库
- 通过向量搜索处理输入以编排指令
- 通过 GPT-4o 分析处理输入以编排指令
- 使用多功能编排器基于输入选择并执行任务
让我们从定义 AI 控制器的架构开始。
AI 控制器的架构
我们将继续实现第一章图 1.1 中定义的 GenAISys 架构,而图 2.1 则进一步深入展示了 GenAISys 的底层功能。
我们在上一章已经明确,人类角色至关重要,前述图示也承认了这一点。无论构建块(模型或框架)多么先进,我们都是 GenAISys 的核心。我们的首要任务是发挥人类创造力,设计出有效实现 GenAISys 控制器的方法。GenAISys 需要人类的创造力、判断力和技术决策能力。像 ChatGPT、Gemini 和微软 Copilot 这样的无缝协助工具背后,隐藏着复杂的 AI 与非 AI 逻辑层。如果我们想构建自己的类 ChatGPT 系统,就必须由我们人类来承担重任!
我们将构建两个独立程序:
- 一个用 GPT-4o 实现的对话代理,支持短期和长期记忆,有助于我们在多轮交互中实现上下文感知。这对应图 2.1 中的功能 F3。
- 一个 AI 控制器编排器,同样使用 GPT-4o 来分析用户输入,检索指令库,使用合适指令增强输入,并执行指令中的功能。
本章将重点关注两个场景:情感分析和语义(硬科学)分析,分别对应架构中的功能 F1 和 F2。功能 F4 和 F5 将在第 3 章添加。
尽管这些示例是基于 OpenAI 的 API 构建的,其逻辑是模型无关的。一旦理解其工作原理,你可以调整代码,使用任何大型语言模型(LLM)——如 Meta 的 Llama、xAI 的 Grok、Google 的 Gemini 或 Cohere。
在分别构建对话代理和控制器编排器程序后,我们将把它们合并成统一的智能 AI 控制器,如图 2.2 所示。
目前,我们需要专注于单独构建每个组件,以便充分理解它们的行为。一旦奠定了基础,第三章我们将通过 Pinecone 向量存储将它们合并。现在,让我们直接进入代码,开始开发对话式 AI 代理。
对话式 AI 代理
本节的两个主要目标是构建一个具备以下功能的对话式 AI 代理:
- 短期记忆保留,实现完整的类似 ChatGPT 的对话循环。用户和代理可以进行任意次数的交互,交互次数无限制。
- 多用户多会话的长期记忆保留。我们会将会话存储在内存中,并持久化到记忆存储(此处为文本文件)。这将支持像 John、Myriam 和 Bob 等用户的多用户上下文感知。我们的对话代理将超越经典的一对一 ChatGPT 风格对话,走向能够处理多会话、多用户交互的定制 GenAISys。
开始之前,请打开本章 GitHub 目录中的 Conversational_AI_Agent.ipynb(github.com/Denis2054/B…)。该 notebook 将指导你完成环境搭建。
环境搭建
我们将复用上一章的搭建流程。如果需要复习,可以随时回顾相关章节。首先安装 OpenAI 并下载所需文件:
!curl -L https://raw.githubusercontent.com/Denis2054/Building-Business-Ready-Generative-AI-Systems/master/commons/grequests.py --output grequests.py
from grequests import download
download("commons", "requirements01.py")
download("commons", "openai_setup.py")
download("commons", "openai_api.py")
我们还需要下载两个额外的函数文件,用于构建对话式代理:
download("commons","conversational_agent.py"):包含管理完整对话轮次循环和记忆对话的函数。download("commons","processing_conversations.py"):包含加载、展示和清理历史对话的工具,扩展对话代理在多会话多用户间的记忆跨度。这一定制的多会话、多用户特性超出标准 ChatGPT 协助工具的范围。
接下来,让我们实现 conversational_agent.py 中的函数,这些函数将在整个与对话式 AI 代理的会话过程中被调用。
对话式 AI 代理工作流程
对话式 AI 代理包含两个主要部分:
- 启动初始对话,与 AI 代理开启交流
- 运行完整的对话循环,允许用户与 AI 代理进行任意多的内存中交互。每次会话结束时,对话内容会被保存,以便同一用户或其他用户以后继续。
启动初始对话
初始对话标志着新会话的入口,由 AI 控制器处理,详见图 2.3。
我们将逐步讲解生成式 AI 模型的初始对话流程,详细了解一个小规模类 ChatGPT 对话代理的工作原理。这个十步流程从“开始”启动。
1. 启动对话
程序从 openai_api.py 中的 run_conversational_agent 函数开始执行,notebook 中通过 conversational_agent 调用该函数及其参数:
# 启动对话代理
def run_conversational_agent(
uinput, mrole, mcontent, user_role, user_name
):
conversational_agent(uinput, mrole, mcontent, user_role, user_name)
此处对话代理处理的参数如下:
uinput:包含输入(用户或系统),例如 “Hawaii 在哪里?”mrole:定义消息角色,可为user或system。也可以赋予其他角色,API 会解析,如定义 AI 的身份,例如 “你是一名地质专家”。mcontent:系统预期的内容,例如 “你是一名地质专家”。user_role:定义用户角色,例如user。user_name:用户姓名,例如John。
2–3. 初始化 API 变量和消息对象
messages_obj 用上一节“启动对话”中的参数初始化:
messages_obj = [{"role": mrole, "content": mcontent}]
messages_obj 关注系统的记忆。只要会话持续,它会随着与 GPT-4o 模型的交互不断追加,用于记录跨会话对话。第一条消息包含角色和内容,用于设置代理的上下文。
4. 打印欢迎消息
系统准备好与用户交互,代理先显示欢迎消息,并说明如何结束会话:
print("欢迎使用对话代理!输入 'q' 或 'quit' 结束对话。")
5. 处理初始用户输入
将用户的初始输入添加至 messages_obj,为代理提供记忆并指明预期方向。初始输入由对话代理发送:
if initial_user_input:
print(f"{user_name}: {initial_user_input}")
messages_obj.append(
{"role": user_role, "content": initial_user_input}
)
6. 清理初始对话日志
messages_obj 以结构化格式保存对话历史。对于应用中的某些操作(如生成简化展示、创建合并日志或为文本函数准备输入),需将结构化日志转换成单一连续字符串,确保数据格式正确,避免合并多条消息时出现标点或格式问题:
conversation_string = cleanse_conversation_log(messages_obj)
清理函数清理对话并返回字符串:
def cleanse_conversation_log(messages_obj):
conversation_str = " ".join(
[f"{entry['role']}: {entry['content']}" for entry in messages_obj]
)
# 移除问题标点符号
return re.sub(r"[^\w\s,.?!:]", "", conversation_str)
7. 发起初始 API 调用
将清理后的对话字符串发送给 API 处理。API 根据最后输入和对话历史生成响应,系统获得记忆:
agent_response = make_openai_api_call(
input=conversation_string,
mrole=mrole,
mcontent=mcontent,
user_role=user_role
)
8. 追加初始 API 响应
API 返回的助手回复被处理并追加至 messages_obj,系统记忆及上下文感知不断增强:
messages_obj.append({"role": "assistant", "content": agent_response})
9. 显示助手初始回复
显示系统响应,供用户分析判断是否继续或退出会话:
print(f"Agent: {agent_response}")
10. 启动对话循环
系统进入对话循环,用户可以多轮交互,直到决定退出:
while True:
user_input = input(f"{user_name}: ")
if user_input.lower() in ["q", "quit"]:
print("退出对话,再见!")
break
现在,我们已准备好开始完整的对话循环。
完整的对话循环
初始对话已经初始化完成。我们将从第 11 步开始,进入完整的对话循环,如图 2.4 所示。
11. 提示用户输入
对话继续初始交流,并通过 messages_obj 进行记忆。用户提示触发完整对话循环。第一步是输入用户名。这个自定义功能超越了标准 ChatGPT 类对话代理的单用户限制,实现了多用户对话:
user_input = input(f"{user_name}: ")
12. 检查退出条件
如果输入 q 或 quit,结束会话:
if user_input.lower() in ["q", "quit"]:
print("退出对话,再见!")
break
13. 将用户输入追加到消息对象
系统现具备完整对话循环的记忆,使用我们定义的通用 API 格式。将用户输入追加至 messages_obj:
messages_obj.append({"role": user_role, "content": user_input})
14. 清理对话日志(循环)
清理更新后的 messages_obj,确保符合 API 调用格式,方法同第 6 步“清理初始对话日志”:
conversation_string = cleanse_conversation_log(messages_obj)
15. 在对话循环中调用 API
在完整的对话循环中,将整个对话发送至 API。API 会基于整个对话上下文和新输入返回响应:
agent_response = make_openai_api_call(
input=conversation_string,
mrole=mrole,
mcontent=mcontent,
user_role=user_role
)
16. 在对话循环中追加 API 响应
每轮对话中,API 的回复都会追加至 messages_obj:
messages_obj.append({"role": "assistant", "content": agent_response})
17. 显示助手回复
循环中每轮对话都会显示 API 回复:
print(f"Agent: {agent_response}")
18. 退出并保存对话日志
当用户退出循环时,对话内容会被保存。此功能模拟 ChatGPT 类平台可保存同一用户两次会话间对话的特性。然而,正如我们在“运行对话代理”部分的实现中将看到的,我们的程序还能保存团队成员之间的多用户会话:
with open("conversation_log.txt", "w") as log_file:
log_file.write("\n".join([f"{(user_name if entry['role'] == 'user' else entry['role'])}: {entry['content']}" for entry in messages_obj]))
19. 结束
对话代理在记忆对话后结束会话:
print("对话已保存至 'conversation_log.txt'。")
我们已经探索了对话代理的功能。现在,让我们继续介绍代表 AI 控制器的 AI 对话代理程序。
运行对话式 AI 代理
主程序 Conversational_AI_Agent.ipynb 调用 conversational_agent.py 中的必要函数来处理 AI 交互。我们将通过以下场景运行三位用户的会话:
- John 以短期记忆会话开始与对话式 AI 代理的交流。
- John 的对话会在会话结束时保存到日志文件。
- Myriam 使用同一个日志文件继续会话。
- Myriam 的对话也会在会话结束时保存到与 John 相同的日志文件。
- Bob 继续 John 和 Myriam 停下的地方。
- Bob 的对话结束后,也保存到与 John 和 Myriam 相同的日志文件。
这三位用户依次进行会话。第三章中,我们将通过 Pinecone 向量存储进一步实现用户分组,支持多用户实时参与同一会话。目前,让我们一步步了解这个多用户设置,并观察对话式 AI 代理如何处理这些会话。先从第一步开始:John 的短期记忆会话。
短期记忆会话
会话从对话代理第 1 步“启动对话”中描述的参数开始:
uinput = "Hawai is on a geological volcano system. Explain:"
mrole = "system"
mcontent = "You are an expert in geology."
user_role = "user"
我们还添加了用户姓名,类似 ChatGPT 会话:
user_name = "John"
这个简单的添加——user_name,使我们的 GenAISys 超越了标准 ChatGPT 类平台。它允许我们将记忆与特定用户关联,并在单一系统中扩展到多用户对话。
接下来导入第一个函数,即 OpenAI API 功能,用于向 OpenAI API 发起请求,正如第 1 章所述:
from openai_api import make_openai_api_call
程序随后导入第二个函数,对话代理,并如本节前文所述运行它:
from conversational_agent import run_conversational_agent
run_conversational_agent(uinput, mrole, mcontent, user_role, user_name)
让我们逐步浏览由这两个函数实现的对话:
代理首先欢迎我们:
欢迎使用对话代理!输入 'q' 或 'quit' 结束对话。
第一位用户 John 提出关于夏威夷地质系统的解释请求:
John: Hawai is on a geological volcano system. Explain:
代理给出了令人满意的回答:
Agent: Hawaii is part of a geological volcanic system known as a "hotspot"…
John 接着询问那里能否冲浪:
John: Can we surf there?
感谢我们为代理构建的记忆功能,代理现在通过记忆保留实现了上下文感知,正确回答了关于夏威夷冲浪的问题:
Agent: Yes, you can definitely surf in Hawaii! The Hawaiian Islands are renowned …
John 接下来问了最佳住宿地点,但未提及夏威夷:
John: Where are the best places to stay?
代理利用上下文感知正确作答:
Agent: Hawaii offers a wide range of accommodations …
John 然后退出了会话:
John: quit
代理结束对话并将对话保存到日志:
Agent: Exiting the conversation. Goodbye!
Conversation saved to 'conversation_log.txt'.
短期会话结束,但多亏了通过 conversation_log.txt 进行的记忆保留,我们可以轻松从 John 停下的地方继续对话。我们可以立即或稍后利用自动生成的日志文件,继续该对话。
长期记忆会话
短期会话已保存。我们有三种选择:
- 现在停止程序。此时,
conversation_log.txt仅包含 John 的会话,之后可继续或不继续。 - 决定为下一位用户 Myriam 初始化一个单独的
conversation_log.txt。 - 继续多用户会话,将 John 的对话加载到 Myriam 的初始对话上下文中。
本章程序选择继续多会话、多用户场景。
继续与 John 的对话的第一步是使用我们在“环境搭建”章节下载的 processing_conversations.py 中的函数加载并显示对话日志。现在导入并运行该函数:
from processing_conversations import load_and_display_conversation_log
conversation_log = load_and_display_conversation_log()
该函数是标准的 IPython 过程,利用 HTML 功能读取并展示对话内容:
from IPython.core.display import display, HTML
import re
# 第一步:加载并显示对话日志
def load_and_display_conversation_log():
try:
with open("conversation_log.txt", "r") as log_file:
conversation_log = log_file.readlines()
# 准备 HTML 显示内容
html_content = "<h3>已加载的对话日志</h3><table border='1'>"
for line in conversation_log:
html_content += f"<tr><td>{line.strip()}</td></tr>"
html_content += "</table>"
# 显示 HTML
display(HTML(html_content))
return conversation_log
except FileNotFoundError:
print("错误:未找到 conversation_log.txt。请确认该文件存在于当前目录。")
return []
输出显示对话中每位参与者的内容,依次是系统信息、John 的请求,然后是 GPT-4o 助手的回复:
system: You are an expert in geology.
John: Hawai is on a geological volcano system. Explain:
assistant: Hawaii is part of a geological volcanic system…
在将对话加入下一条输入的上下文前,我们会先清理并准备它。为此,我们依次从 processing_conversations.py 导入 cleanse_conversation_log 和 initialize_uinput:
from processing_conversations import cleanse_conversation_log
from processing_conversations import initialize_uinput
接着调用这两个函数清理并准备新输入:
cleansed_log = cleanse_conversation_log(conversation_log)
nuinput = initialize_uinput(cleansed_log)
cleanse 函数会移除标点和潜在的特殊字符:
# 第二步:清理对话日志,移除标点和特殊字符
def cleanse_conversation_log(conversation_log):
cleansed_log = []
for line in conversation_log:
# 移除问题标点和特殊字符
cleansed_line = re.sub(r"[^\w\s,.?!:]", "", line)
cleansed_log.append(cleansed_line.strip())
return " ".join(cleansed_log) # 将所有行合并为单一字符串
最后,初始化新的输入:
# 第三步:用清理后的对话日志初始化 `uinput`,以继续对话
def initialize_uinput(cleansed_log):
if cleansed_log:
print("\n清理后的对话日志,用于继续:")
print(cleansed_log)
return cleansed_log # 使用清理后的日志作为新输入
else:
print("错误:无可用数据初始化 `uinput`。")
return ""
输出确认对话日志已被清理:
清理后的对话日志,用于继续:
system: You are an expert in geology…
随后输出确认 nuinput 包含用于继续的对话日志:
# `nuinput` 现包含清理后的对话日志,可用于后续操作
print("\n初始化的 `nuinput`,用于继续:", nuinput)
继续之前的会话
现在我们可以继续 John 开始的对话,使用 nuinput 作为上下文感知的记忆保留变量。我们将像之前一样,用消息变量将上下文 nuinput 添加到 Myriam 的请求中:
ninput = nuinput + "What about surfing in Long Beach"
mrole = "system"
mcontent = "You are an expert in geology."
user_role = "user"
user_name = "Myriam"
消息调用包含两个关键特性:
ninput = nuinput + [用户输入],表明 AI 控制器现在具备超越单次会话的长期记忆。user_name = "Myriam",展示多用户功能,证明我们的定制小规模类 ChatGPT AI 控制器比标准协助工具更具灵活性。
整体流程与 John 的对话相同。Myriam 提问:
Myriam: What about surfing in Long Beach
代理回复:
Agent: Long Beach, California, offers a different surfing experience compared to Hawai…
Myriam 退出:
Myriam: quit
代理确认对话结束并保存至对话日志:
Agent: Exiting the conversation. Goodbye!
Conversation saved to 'conversation_log.txt'.
AI 控制器现在拥有了 John 的会话记录和 Myriam 的会话续接。控制器还可以继续扩展,添加更多用户参与对话。
继续长期多用户记忆
让我们加入 Bob,一起继续对话。首先,再次显示对话日志:
# 运行过程
conversation_log = load_and_display_conversation_log()
你会看到 John 和 Myriam 的记录:
system: You are an expert in geology.
Myriam: system: You are an expert …
然后,按照之前的步骤清理并准备日志。nuinput 现在包含了 John 和 Myriam 的会话:
uinput = nuinput + "Read the whole dialog then choose the best for geology research"
mrole = "system"
mcontent = "You are an expert in geology."
user_role = "user"
user_name = "Bob"
Bob 专注于地质研究任务,而非娱乐:
Bob: "Read the whole dialog then choose the best for geology research"
AI 代理给出了准确回复:
Agent: For geology research, the most relevant part of the dialogue is the explanation of Hawaii's geological volcanic system. This section provides detailed insights into the Hawaiian hotspot, mantle plumes, volcanic activity,…
随后 Bob 退出会话:
Bob: quit
代理结束对话并保存至对话日志:
Agent: Exiting the conversation. Goodbye!
Conversation saved to 'conversation_log.txt'.
通过这三个场景,我们实现了由 AI 控制器管理的多用户全轮对话循环中的对话代理。接下来让我们来看看该对话代理的后续步骤。
后续步骤
至此,我们已经完成了对话代理的基本结构。接下来需要将其集成到 AI 控制器编排器中。在开始构建 AI 控制器编排器之前,让我们先总结一下为对话代理所做的工作。
如前图所示,AI 对话代理执行以下操作:
- 代理处理输入(系统或人类用户)。
- 代理生成响应。
- 启动记忆保留功能。
- 将对话作为上下文添加到后续输入中。
- 用户可以退出对话。
然而,入口和出口点还不完整。我们可以进入和退出对话,但尚无法调用函数来编排任务,例如激活情感分析和语义分析。要完善 AI 控制器的架构,我们需要开始构建 AI 控制器编排器。
AI 控制器编排器
本节我们将构建 AI 控制器编排器的第一个组件:选择合适任务的能力。我们将此组件开发为独立模块,并从第 3 章开始集成,届时通过 Pinecone 向量存储将对话代理与 AI 控制器编排器连接起来。
图 2.6 展示了我们将要开发的 AI 控制器编排器的工作流程:
- C1. AI 控制器入口输入触发流程。
- C2. 分析输入,输入可能来自系统或人类用户的提示。
- C3. 通过 GPT-4o 原生功能对用户输入进行嵌入。
- C4. 通过 GPT-4o 原生功能对任务场景库进行嵌入。
- C5. 选择一个最匹配输入的场景来执行任务。
- C6. 执行由 AI 控制器编排器选定的场景。
我们将使用 OpenAI 的 GPT-4o API 和 Python 开发 AI 控制器编排器的第一个组件。此外,鉴于我们的目标是充分利用生成式 AI 模型的强大能力来执行 AI 控制器编排器请求的多个任务,因此我们将避免给编排器引入过多额外库,以便专注于 GenAISys 的架构设计。
在本笔记本中,GPT-4o 将执行程序中的三个关键功能,如图 2.7 所示:
- 嵌入:GPT-4o 会系统地对通过提示接收的所有数据进行嵌入。输入在经过模型各层处理前会被嵌入。第 3 章中,我们将进一步扩展,将指令场景等可复用数据嵌入并更新到 Pinecone 向量存储中。
- 相似度搜索:GPT-4o 能执行相似度搜索并提供可靠结果。GPT-4o 并不使用确定性的固定余弦相似度函数,而是通过其复杂的神经网络学习理解关系,以更细腻、非确定性的方式模拟相似性判断。
- 任务执行:一旦选定场景,GPT-4o 可以执行多个标准任务,如情感分析和语义分析。
我们已经定义了编排器的工作流程及生成式 AI 模型的使用方式,但必须进一步探讨模型如何识别其应执行的任务。
理解意图功能
无论像 GPT-4o 这样的生成式 AI 模型多么强大,如果没有明确表达意图的提示,它无法猜测用户的需求。我们不能仅仅说“科罗拉多大峡谷是亚利桑那州一个很棒的旅游地”,却指望模型能猜出我们想对这句话做情感分析。我们必须明确表达意图,例如输入:“请对以下文本进行情感分析:科罗拉多大峡谷是亚利桑那州一个很棒的旅游地。”
为了解决 AI 控制器的意图问题,我们需要为其编排任务找到合适的框架。一个良好的起点是研究“文本到文本转换器”(Text-to-Text Transfer Transformer,简称 T5),这是一种文本到文本的模型(Raffel 等人,2020)。T5 模型通过任务标签或特定任务前缀,向转换器模型提供提示的意图。任务标签包含诸如摘要、翻译、分类等指令。模型会检测标签并据此执行相应操作,如图 2.8 所示。
训练一个 T5 模型时,需要在创建输入时显式添加任务标签,然后提供对应的响应。然而,OpenAI 的 GPT 模型通过分析包含指令和响应的数十亿语言序列来学习执行何种任务,而不是依赖显式的结构。因此,采用 GPT 架构的生成式 AI 模型会通过提示的上下文隐式地学习执行的任务。例如,一个结构清晰的提示语:“请对以下文本进行情感分析:科罗拉多大峡谷是亚利桑那州一个很棒的旅游地。”就包含了足够的上下文信息,使 GPT-4o 能够推断出所需的操作,而无需显式的标签。
接下来,我们将通过运行类似 T5 风格的示例,演示 GPT 模型如何隐式分析并确定需要执行的任务。
从 T5 到 GPT 模型
本节我们将编写一个程序,演示 GPT-4o 如何理解指令——这是我们在编排器中将要利用的能力。目标是展示尽管 GPT 风格的模型能够隐式推断意图,但它们仍然需要明确的指令。
我们将从打开 GitHub 上 Chapter02 目录下的 T52GPT.ipynb 开始。环境搭建与“对话式 AI 代理”部分的“环境搭建”小节完全相同,仅需安装 OpenAI 环境:
download("commons","requirements01.py")
download("commons","openai_setup.py")
download("commons","openai_api.py")
无需额外安装。现在,让我们从一个 CoLA 任务开始。
语言可接受性语料库(CoLA)
语言可接受性语料库(CoLA)是一个公开的数据集,包含许多简短的英文句子,每个句子都被标记为“可接受”(语法正确)或“不接受”(语法错误)。通过在这些示例上测试 GPT-4o,我们可以展示先进的生成式模型能够仅凭理解语言本身来解决新任务,而无需针对特定任务的微调。这意味着我们可以将先进的生成式 AI 模型应用于许多未专门训练过的任务。
首先,我们向 GPT-4o 模型提交以下输入,看看在没有显式任务标签的情况下它是否能判断句子是否可接受:
input = "This aint the right way to talk."
mrole = "system"
user_role = "user"
mcontent = "Follow the instructions in the input"
使用本章一直在用的函数调用 OpenAI API:
# API 调用
task_response = openai_api.make_openai_api_call(
input, mrole, mcontent, user_role
)
print(task_response)
输出显示,即便是最强大的生成式 AI 模型之一,没有任务标签时也不知道该做什么:
I apologize if my previous response didn't meet your expectations. Please let me know how I can assist you better!
接下来,我们添加带有任务标签的指令和同样的句子:
input = "Is the following sentence gramatically correct: This aint the right way to talk."
mrole = "system"
user_role = "user"
mcontent = "Follow the instructions in the input"
# API 调用
task_response = openai_api.make_openai_api_call(
input, mrole, mcontent, user_role
)
print(task_response)
此时输入明确指出了期望生成式 AI 模型执行的任务,输出准确无误:
The sentence "This aint the right way to talk." is not grammatically correct. The response corrects the sentence:
"This isn't the right way to talk."
Alternatively, if you want to maintain the informal tone, you could write:
"This ain't the right way to talk."
Note that "ain't" is considered informal and nonstandard in formal writing.
现在,让我们执行一个翻译任务。
翻译任务
该任务以用自然语言表达的任务标签开始:
input = "Translate this sentence into French: Paris is quite a city to visit."
mrole = "system"
user_role = "user"
mcontent = "Follow the instructions in the input"
# API 调用
task_response = openai_api.make_openai_api_call(
input, mrole, mcontent, user_role
)
print(task_response)
输出结果准确:
Paris est vraiment une ville à visiter.
现在,让我们执行一个语义文本相似度基准(STSB)任务。
语义文本相似度基准(STSB)
STSB 风格的评分是 GenAISys AI 控制器的一项重要功能,该功能依赖相似度搜索来选择合适的指令场景、文档和其他资源。编排器将依赖这一能力。在接下来的测试中,我们向模型提交两句话,请它判断它们的语义相似度:
input = "stsb:Sentence 1: This is a big dog. Sentence 2: This dog is very big."
mrole = "system"
user_role = "user"
mcontent = "Follow the instructions in the input"
# API 调用
task_response = openai_api.make_openai_api_call(
input, mrole, mcontent, user_role)
print(task_response)
输出准确如下:
句子 “This is a big dog.” 和 “This dog is very big.” 在语义上是相似的。两句话都传达了该狗体型较大的意思。虽然措辞不同,但意义并未发生显著变化,因为两句话都描述了该狗的相同特征。
当我们在数据集中搜索与输入匹配的数据时,这个功能将非常有用。现在,让我们运行一个摘要任务。
摘要任务
在下面的输入中,GPT-4o 能够检测到摘要指令标签,并理解所需的最大响应长度:
input = "Summarize this text in 10 words maximum: The group walked in the forest on a nice sunny day. The birds were singing and everyone was happy."
mrole = "system"
user_role = "user"
mcontent = "Follow the instructions in the input"
# API 调用
task_response = openai_api.make_openai_api_call(
input, mrole, mcontent, user_role)
print(task_response)
输出依然准确:
Group enjoyed a sunny forest walk with singing birds.
这次探索的结论是,无论我们实现哪种生成式 AI 模型,都需要任务标签来让模型按照预期进行响应。接下来,我们将利用这一认识,在编排器中实现语义文本相似度功能,以处理任务标签。
实现用于指令选择的编排器
本节中,我们将开始构建基于任务标签的编排器,用于两个指令场景,如图 2.9 所示:情感分析(用于判断句子的情感倾向)和语义分析(用于分析句子中的事实内容)。
我们将通过让生成式 AI 模型根据输入自动找到最佳任务标签场景(情感分析或语义分析)来使系统更复杂。换句话说,任务标签将不再作为输入的一部分,而是由 GPT-4o 的语义文本相似度功能来选择正确的任务标签。
环境搭建与之前相同:
download("commons","requirements01.py")
download("commons","openai_setup.py")
download("commons","openai_api.py")
编排器不需要额外安装。我们将从实现指令场景选择开始。
选择场景
AI 控制器的核心任务是在接收到输入(系统或用户)时决定下一步做什么。任务的选择打开了多种可能的方法,我们将在本书中逐步探索。不过,我们可以将这些方法分为两类:
- 使用显式任务标签来触发指令。该标签可以作为生成式 AI 模型的上下文,在提示中以多种形式自由表达。
- 提示中没有任务指令,而是包含一个场景库,AI 控制器通过语义文本相似度从中决策选择。
这里,我们将探索第二种更主动的方法。我们将测试两个没有指令、没有任务标签、且没有任何暗示生成式 AI 模型期望做什么的提示。虽然之后我们会实现其他更显式的带任务标签的方法,但一个 GenAISys AI 控制器编排器必须能在某些情况下主动决策。
第一个提示是关于电影的观点,暗示用户可能感兴趣的是情感分析:
if prompt == 1:
input = "Gladiator II is a great movie although I didn't like some of the scenes. I liked the actors though. Overall I really enjoyed the experience."
第二个提示是事实描述,暗示用户可能感兴趣的是语义分析:
if prompt == 2:
input = "Generative AI models such as GPT-4o can be built into Generative AI Systems. Provide more information."
为了赋予 AI 控制器决策能力,我们需要一个指令场景库。
定义任务/指令场景
场景是一组存储在 GenAISys 仓库中的指令。虽然 ChatGPT 类模型天生能处理多种指令,但领域特定的应用需要自定义场景(我们将在第 5 章开始深入探讨)。例如,GenAISys 可能接收到一条消息:“客户订单 #9283444 延迟”。这条消息可能涉及生产延迟或交付延迟。通过检查发送者用户名及其所属组(生产或交付部门),AI 控制器可以确定上下文,并选择合适的场景,做出恰当决策。
在本笔记本中,场景存储于内存。第 3 章中,我们将把这些指令集存储和检索功能组织到 Pinecone 向量库中。
无论哪种情况,我们都从创建结构化场景库开始(市场分析、情感分析和语义分析):
scenarios = [
{
"scenario_number": 1,
"description": "市场语义分析。你将获得针对某系列产品的市场调查。输入中必须包含“market”一词。你的任务是提供分析。"
},
{
"scenario_number": 2,
"description": "情感分析。读取内容并将其归类为观点。如果不是观点,则终止。如果是观点,执行情感分析,给出一个介于0和1之间的分数,标记为“Analysis score:”,分数无正负号,并附解释。"
},
{
"scenario_number": 3,
"description": "语义分析。这不是分析,而是语义搜索。提供有关该主题的更多信息。"
}
]
我们还将添加一个同样场景的字典,包含简要定义:
# 原始字典列表
scenario_instructions = [
{
"市场语义分析。你将获得针对某系列产品的市场调查。输入中必须包含“market”一词。你的任务是提供分析。"
},
{
"情感分析。读取内容,对文本进行情感分析,给出一个介于0和1之间的分数,标记为“Sentiment analysis score”,无正负号,并附解释以说明评分依据。"
},
{
"语义分析。这不是分析,而是语义搜索。提供有关该主题的更多信息。"
}
]
接下来,我们从字典中提取字符串,并存入列表:
# 从每个字典中提取字符串
instructions_as_strings = [
list(entry)[0] for entry in scenario_instructions
]
此时,我们的 AI 控制器具备了识别意图的全部必要条件——能够将任何输入提示匹配到最合适的场景。
执行意图识别与场景选择
我们首先定义对话式 AI 代理的参数,方法与“对话式 AI 代理”部分相同:
# 定义函数调用参数
mrole = "system"
mcontent = "You are an assistant that matches user inputs to predefined scenarios. Select the scenario that best matches the input. Respond with the scenario_number only."
user_role = "user"
编排器的任务是为任何给定输入找到最佳任务,使 AI 控制器具备灵活性和适应性。在某些情况下,编排器可能决定不应用任何场景,仅按照用户输入执行。然而在下面的例子中,编排器会选择并应用一个场景。
我们现在调整输入,以考虑编排器的请求:
# 调整 `input`,将用户输入与场景结合
selection_input = f"User input: {input}\nScenarios: {scenarios}"
print(selection_input)
GPT-4o 现在将执行文本语义相似度搜索,如“语义文本相似度基准(STSB)”部分所示。这次不仅仅是普通的文本比较,而是将一段文本(用户输入)与一组文本(我们的场景描述)进行匹配:
# 使用标准 API 调用函数
response = openai_api.make_openai_api_call(
selection_input, mrole, mcontent, user_role
)
假设我们的用户输入如下:
User input: Gladiator II is a great movie
随后,选择场景:
# 输出响应
print("Scenario:", response)
接着选定场景编号,结合对应的指令一起存储并显示:
scenario_number = int(response)
instructions = scenario_instructions[scenario_number - 1]
print(instructions)
针对 Gladiator II 的示例,编排器正确选中了情感分析场景:
{'Sentiment analysis Read the content return a sentiment analysis on this text and provide a score with the label named : Sentiment analysis score followed by a numerical value between 0 and 1 with no + or - sign and add an explanation to justify the score.'}
这种自主任务选择能力——让 GenAISys 在没有显式标签的情况下选择正确的分析任务——将在实际部署中发挥巨大价值(详见第 5 章)。程序现在利用生成式 AI 代理运行这些场景。
使用生成式 AI 代理运行场景
现在 AI 控制器已经确定了正确的 scenario_number,是时候执行所选任务了。在本笔记本中,我们将逐步演示该过程。
首先打印输入内容:
print(input)
然后,利用 scenario_number,从 instructions_as_strings 列表中获取对应的场景描述:
# 按行号访问(基于1的索引)
line_number = scenario_number
instruction = instructions_as_strings[line_number - 1] # 调整为基于0的索引
print(f"第 {line_number} 行的指令内容:\n{instruction}")
设置参数:
mrole = "system"
user_role = "user"
mcontent = instruction
编排器现在准备执行情感分析任务。
情感分析
我们将场景描述附加到用户的原始输入后,合并请求发送给 GPT-4o:
第 2 行的指令内容:
情感分析 读取内容,对文本进行情感分析,并给出一个介于 0 到 1 之间的分数,标记为:“Sentiment analysis score”,分数无正负号,并附带解释说明评分理由。
# API 调用
sc_input = instruction + " " + input
print(sc_input)
task_response = openai_api.make_openai_api_call(
sc_input, mrole, mcontent, user_role
)
print(task_response)
以 Gladiator II 为例,响应可能如下:
Sentiment analysis score 0.75
这段文本整体表达了对电影《角斗士 II》的积极情感。诸如“great movie”(好电影)、“liked the actors”(喜欢演员)和“really enjoyed the experience”(真的很享受这次经历)等词语显示了正面评价。然而,提到“不喜欢某些场景”带来了一些负面因素。尽管如此,整体的愉快感受以及对演员和电影的积极评价压倒了负面,导致情感评分倾向于正面。
该响应显示编排器找到了与输入匹配的场景并生成了合适的输出。现在,我们回头修改提示,看看编排器是否能找到正确的场景。
语义分析
现在的目标是验证,在不更改任何代码的情况下,编排器是否能访问另一个场景。编排器将依赖 GPT-4o 的原生能力执行语义文本相似度搜索。
我们将激活提示 2:
prompt = 2
…
if prompt == 2:
input = "Generative AI models such as GPT-4o can be built into Generative AI Systems. Provide more information."
该输入显然要求进行语义分析,而非情感分析。然后我们重复使用与情感分析相同的代码:
# 按行号访问(基于1的索引)
line_number = scenario_number
instruction = instructions_as_strings[line_number - 1] # 调整为基于0的索引
print(f"第 {line_number} 行的指令内容:\n{instruction}")
mrole = "system"
user_role = "user"
mcontent = instruction
输出显示找到了正确的场景:
第 3 行的指令内容:
语义分析。这不是分析,而是语义搜索。提供有关该主题的更多信息。
任务响应显示如下:
print(task_response)
输出表明编排器生成了连贯的语义分析:
Generative AI models, like GPT-4, are advanced machine learning models designed to generate human-like text based on the input they receive….
这表明在某些情况下,编排器能够在没有任务标签的情况下找到正确的场景。这将在我们处理更复杂的工作流(如高级生产和支持)时非常有用。
总结
本章的第一个要点是,人类在 GenAISys 中扮演着核心角色。正是人类的设计推动了我们对话代理和编排器的创建。我们仅使用 OpenAI API 和 Python 开始开发这两个复杂组件,但正是人类设计了驱动我们定制 GenAISys 的 AI 控制器的初始层级。GenAISys 的基本规则永远适用:没有人类角色,就没有 GenAISys。我们设计 AI 系统,实施它们,维护它们,并根据持续反馈不断进化。
第二个要点是我们的对话式 AI 代理超越了小规模的 ChatGPT 类结构。我们不仅构建了支持完整对话轮次的短期上下文和记忆保留,还添加了跨多个用户和多个主题的长期记忆。我们的对话涵盖了三位用户(John、Myriam 和 Bob)以及两个主题(地质学和冲浪)。随着本书的深入,我们将扩大这些多用户、多主题会话的应用范围,覆盖需要团队协作的场景。
第三个要点涉及我们的 AI 控制器编排器。我们为编排器提供了一个包含自定义指令的小型场景数据集,未来可扩展到特定领域的用例,并利用 GPT-4o 来选择合适的场景并执行相应任务。
到此为止,我们已经拥有了一个对话代理和一个初步的 AI 控制器编排器。当我们组装 AI 控制器时,它们将共同构建一个独特的多用户、多领域定制的 GenAISys。为了构建我们的多用户、多领域 GenAISys AI 控制器,我们将在下一章开始构建 Pinecone 向量存储。