使用OpenAI 实时 API,你可以构建语音到文本的应用程序,让你可以直接通过说话与生成式AI模型互动。直接与模型对话感觉非常自然,这使用户感到很亲切,而实时API使得你可以将这种体验集成到你自己的应用和业务中。
这是一个由Twilio构建的例子:它使你能够用Node.js(或者如果你更喜欢的话,Python)将电话呼叫连接至GPT-4。这个例子确实很棒,但它只展示了如何连接电话到一个普通的GPT-4,并附带一个鼓励分享猫头鹰相关事实和笑话的系统提示。虽然我很喜欢猫头鹰相关的信息,我还想看看利用这样的语音代理可以实现哪些其他功能。
本文将向您展示如何将原有的助手扩展为一个能够自主选择使用工具来增强其响应的助手。我们将利用[检索增强生成(RAG)]技术通过Astra DB为其提供最新知识。
想先试试吗?打这个电话 (855) 687-9438 (也就是 855-6-TSWIFT)然后聊一聊!
前提条件首先,你需要按照Twilio 博客文章中的说明来设置应用程序,所以你需要一个 Twilio 账户以及一个 OpenAI API 密钥。确保你能够成功地与机器人进行电话通话和聊天。
您还需要一个这样的免费DataStax账户,以便您可以在Astra DB上设置RAG。
我们要建的...我们已经有了一个可以打电话交谈的语音机器人。我们将收集最新数据并存储在Astra DB中,以帮助它回答问题。
OpenAI的实时API功能允许你定义模型可以使用的工具,以执行功能并增强其能力。我们将为此模型提供一个工具,让它能够从数据库中搜索更多信息(这是一个代理式检索和生成(RAG)的例子)。
数据导入为了测试这个代理程序,我们将编写一个简短的脚本,加载并解析网页,将内容分块,将这些块转换为向量,并将它们存入Astra DB。
创建自己的数据库
为了开始这个流程,你需要创建一个数据库。登录到你的DataStax账户,在Astra DB控制台中,点击创建数据库。选择一个_无服务器(Vector)_数据库,给它起个名字,然后选择一个提供商和地域。这可能需要几分钟来设置。在这段时间里,想想你可能想要导入到这个数据库中的某些好的网页资源。
当数据库准备好后,点击 数据探索器 选项卡,然后点击 创建集合+ 按钮。给您的集合命名,并确保它是支持向量的集合,选择 NVIDIA 作为嵌入生成方式。这将自动为插入集合的内容生成向量。
连接到数据库
在您喜欢的文本编辑器中打开应用代码,为了让应用运行,您需要创建一个.env
文件,并用您的OpenAI API密钥填写它。如果没有这样做,请现在来。打开该_.env_
文件,再添加一些环境变量。
ASTRA_DB_APPLICATION_TOKEN= # 数据库应用令牌
ASTRA_DB_API_ENDPOINT= # 数据库API端点
ASTRA_DB_COLLECTION_NAME= # 数据库集合名称
# 此类变量用于数据库连接设置
全屏切换,退出全屏
用您数据库中的信息填充变量。您可以在Astra DB仪表板的数据库概览中找到API端点并且生成应用程序令牌。同时输入您刚刚创建的集合名称。
现在,我们可以在应用程序中连接到数据库,并且可以从npm安装Astra DB客户端。
npm install @datastax/astra-db-ts // 安装 DataStax Astra-DB TypeScript 客户端
全屏 退出全屏
在应用程序中创建一个名为 db.js 的新文件,并打开这个文件,在里面输入以下代码:
import { DataAPIClient } from "@datastax/astra-db-ts";
import dotenv from "dotenv";
dotenv.config();
const {
ASTRA_DB_APPLICATION_TOKEN,
ASTRA_DB_API_ENDPOINT,
ASTRA_DB_COLLECTION_NAME,
} = process.env;
const client = new DataAPIClient(ASTRA_DB_APPLICATION_TOKEN);
const db = client.db(ASTRA_DB_API_ENDPOINT);
export const collection = db.collection(ASTRA_DB_COLLECTION_NAME);
// 以下是代码的中文注释部分
// 导入环境变量配置
dotenv.config();
// 从环境变量中获取数据库的相关配置
const {
ASTRA_DB_APPLICATION_TOKEN,
ASTRA_DB_API_ENDPOINT,
ASTRA_DB_COLLECTION_NAME,
} = process.env;
// 使用应用程序令牌创建一个新的DataAPIClient实例
const client = new DataAPIClient(ASTRA_DB_APPLICATION_TOKEN);
// 使用API端点连接到数据库
const db = client.db(ASTRA_DB_API_ENDPOINT);
// 选择数据库中的特定集合
export const collection = db.collection(ASTRA_DB_COLLECTION_NAME);
全屏模式, 退出全屏
此代码从Astra DB模块加载客户端,并将_.env_文件中的变量加载到环境中。然后使用这些环境变量作为凭证来连接到集合,并将集合对象导出以供应用程序中其他部分使用。
弄点数据
现在让我们创建一个脚本,该脚本加载并解析网页,然后将其分割成块并存储在Astra DB中。这个脚本会结合一些关于网页抓取、文本拆分和向量嵌入的博客中的技术。如需深入了解,请查看相关博客。
安装所需的依赖项:
npm install @langchain/textsplitters @mozilla/readability jsdom
全屏模式下 退出全屏
创建一个名为_ingest.js_的文件,并将以下代码复制进去。
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { Readability } from "@mozilla/readability";
import { JSDOM } from "jsdom";
import { collection } from "./db.js";
import { parseArgs } from "node:util";
const { values } = parseArgs({
args: process.argv.slice(2),
options: { url: { type: "string", short: "u" } },
});
const { url } = values;
const html = await fetch(url).then((res) => res.text());
const doc = new JSDOM(html, { url });
const reader = new Readability(doc.window.document);
const article = reader.parse();
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 500,
chunkOverlap: 100,
});
const docs = (await splitter.splitText(article.textContent)).map((chunk) => ({
$vectorize: chunk,
}));
await collection.insertMany(docs);
点击全屏 点击退出全屏
这个脚本。
-
通过 Node.js 的 参数解析器 从命令行参数中提取 URL
-
加载该 URL 对应的网页
-
使用
RecursiveCharacterTextSplitter
将文本分割成 500 个字符的块,每块之间有 100 个字符的重叠 -
将这些块转换成对象,使文本块成为
$vectorize
属性 - 将所有文档插入集合
启用 $vectorize
功能从而使 Astra DB 自动为该内容创建向量表示。如需了解更多信息,请参阅 [原文链接]。
我们现在可以通过命令行来运行这个文件。这里是如何从命令行加载关于Taylor Swift的维基百科页面的例子。
运行以下脚本来抓取维基百科上的泰勒·斯威夫特页面:
node ingest.js --url https://en.wikipedia.org/wiki/Taylor_Swift
这是一个Node.js命令,用于爬取维基百科页面。
切换到全屏模式 退出全屏
运行完此命令后,检查DataStax仪表板上的集合,查看内容和向量数据。
为了把我们现有的语音助手变成一个可以自主决定是否搜索数据库获取更多相关信息的代理,我们需要给它提供一个工具或功能,让它可以使用。
创建一个新的名为 tools.js 的文件,并在你的编辑器里打开这个文件。首先从 db.js 文件中引入 collection
:
import { collection } from "./db.js"
全屏模式,退出全屏
接下来我们得创建一个用于搜索数据库的智能代理函数。
当 OpenAI 代理提供参数调用一个函数时,它会以对象的形式传递这些参数。因此,函数应该接收一个对象,从中我们可以解构来提取查询。然后我们将使用该查询对我们数据集做向量搜索。
我们可以使用Astra DB Vectorize自动生成查询的向量表示。同时,我们将结果限制为前10个,并在投影中选择$vectorize
,以确保返回每个分块的文本。
使用这些参数调用 find
方法会返回一个游标,转换成数组之后,我们可以调用 toArray
。接着,遍历这些文档,提取文本内容,用换行符将这些结果拼接成一个字符串,这个结果可以作为代理的上下文。
async function taylorSwiftFacts({ query }) {
const docs = await collection.find(
{},
{ $vectorize: query, limit: 10, projection: { $vectorize: 1 } }
);
return (await docs.toArray()).map((doc) => doc.$vectorize).join("\\n");
}
以下是函数 `taylorSwiftFacts` 的功能描述:
```taylorSwiftFacts``` 函数是一个异步函数,接受一个包含查询参数的对象作为输入。该函数从集合中查找文档,使用 `$vectorize` 方法将查询向量化,并限制返回的文档数量为10个。返回的结果是将每个文档的向量化字段连接成一个字符串,每条事实之间用换行符 `\n` 分隔。
点击全屏模式,点击退出
我将这个函数命名为`taylorSwiftFacts`,因为这是我的ingestion脚本加载的内容;你可以随意使用其他名称。
这是我们第一个工具,现在我们只能将它导出为一个工具对象;不过,我们后续可以添加更多内容。
export const 工具常量 = {
taylorSwiftFacts
};
全屏模式 退出全屏
为了帮助模型知道何时使用此工具,它需要一个描述,描述该工具的功能及其期望的参数类型。对于每个工具,你需要提供类型、名称、描述以及参数。
对于我们这个函数,类型将是 "function",名称是 `taylorSwiftFacts`。我们将告诉代理,我们有关于泰勒·斯威夫特的最新信息,可以供其查询。这个工具比较简单,只需要一个参数,叫做 query,它是一个字符串。完整描述如下:
参数是一个 JSON 格式的参数描述。
export const DESCRIPTIONS = [
{
type: "function",
name: "taylorSwiftFacts",
description:
"从她的维基百科页面获取泰勒·斯威夫特的最新信息",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "查询",
},
},
},
},
];
全屏模式 退出全屏
现在我们已经完成了工具定义,让我们把它们添加到我们的智能代理中吧。
### 语音助手中的函数呼叫处理
迄今为止,我们为现有应用构建支持功能,但要将我们的工具连接到代理,我们需要深入到主代码中。在编辑器中打开\_index.js\_文件,并从导入刚刚定义的工具开始:
import Fastify from 'fastify';
import WebSocket from 'ws';
import dotenv from 'dotenv';
import fastifyFormBody from '@fastify/formbody';
import fastifyWs from '@fastify/websocket';
import { DESCRIPTIONS, TOOLS } from "./tools.js";
进入全屏 退出全屏
我们需要更新一下系统提示,以便更准确地描述代理能用这些工具做什么。自从我们之前导入了泰勒·斯威夫特的维基页面,我们可以将其更新为一个泰勒·斯威夫特超级粉丝的样子。
找到 `SYSTEM_MESSAGE` 并更新为以下内容:
const SYSTEM_MESSAGE = "你是一个乐于助人且活泼的AI助手,超级喜欢泰勒·斯威夫特。你可以用你对泰勒·斯威夫特的了解来回答问题,但如果不知道答案的话,你可以用手头的工具搜索相关事实。";
全屏显示 退出全屏
接下来我们需要将我们构建的工具提供给代理程序。找到`initializeSession`函数,它定义了一个包含初始化代理程序所需所有细节的`sessionUpdate`对象。使用我们之前导入的`DESCRIPTIONS`对象,在会话对象中添加一个工具属性,如`tools`。
const sessionUpdate = {
type: 'session.update',
session: {
turn_detection: { type: 'server_vad' },
input_audio_format: 'g711_ulaw',
output_audio_format: 'g711_ulaw',
voice: VOICE,
instructions: SYSTEM_MESSAGE,
modalities: ["文字", "音频"],
temperature: 0.8,
tools: DESCRIPTIONS
}
};
切换到全屏 退出全屏
我们也可以按需提供工具,但这个智能代理每次互动中都能从中受益。
最后我们需要处理模型请求使用工具时的事件处理。找到当连接到OpenAI接收消息时的处理程序,它长这样:`openAiWs.on('message', … )`。
将事件处理器改为异步函数(` async`)。
openAiWs.on('message', async (data) => {
进入全屏,退出全屏
当实时API需要使用一个工具时,它会发送一个类型为"response.done"的事件消息。在事件对象中包含有输出数据,如果其中一个输出数据的类型为"function_call",我们就知道模型想要调用它的其中一个工具。
输出提供要调用的函数名称及其参数。在我们导入的 `TOOLS` 对象中查找相应的函数,然后用参数调用。
当我们得到函数调用的结果之后,我们会把结果传回给模型,这样模型就知道下一步该做什么了。我们通过创建一个类型为"conversation.item.create"的消息来实现这一点,在该消息里,我们会包括一个类型为"function_call_output"的项,加上函数调用的输出和原始事件的ID,这样模型就能把回复和最初的问题对应起来了。
我们将这个消息发送给模型,同时发送另一条类型为 "response.create" 的消息,请求是利用这条新信息给出一个新回复。
总的来说,这能让模型请求使用我们定义的数据库搜索功能,并提供它需要传递给该函数的参数。我们则需要调用该函数并将结果反馈给模型。整个代码如下:
openAiWs.on('message', async (data) => {
try {
const response = JSON.parse(data);
if (LOG_EVENT_TYPES.includes(response.type)) {
console.log(`接收到事件: ${response.type}`, response);
}
if (response.type === "response.done") {
const outputs = response.response.output;
const functionCall = outputs.find(
(output) => output.type === "function_call"
);
if (functionCall && TOOLS[functionCall.name]) {
const result = await TOOLS[functionCall.name](JSON.parse(functionCall.arguments));
const conversationItemCreate = {
type: "conversation.item.create",
item: {
type: "function_call_output",
call_id: functionCall.call_id,
output: result,
},
};
openAiWs.send(JSON.stringify(conversationItemCreate));
openAiWs.send(JSON.stringify({ type: "response.create" }));
}
}
// 处理其他事件
}
进入全屏模式,退出全屏
打开应用程序,并确保它已连接到你的Twilio号码,如[这篇文章](https://www.twilio.com/en-us/blog/voice-ai-assistant-openai-realtime-api-node)所述。现在我们可以打电话聊天,讨论一切关于泰勒·斯威夫特的话题。
如果你想试试我的助手,可以打个电话给她:(855) 687-9438。
这现在是我们连接[我们之前建立的 Taylor Swift 机器人](https://www.datastax.com/blog/using-astradb-vector-to-build-taylor-swift-chatbot?utm_medium=byline&utm_source=devto&utm_campaign=voice-agent)的新方式。现在你可以在线与 SwiftieGPT 聊天或通过电话联系。
## 让语音助手更有主动性
实时语音代理非常酷,但它们和普通的LLM一样存在所有相同的缺点。这次分享里,我们给语音代理增加了代理式RAG功能,它能用最新的信息回答我们关于Taylor Swift的问题。
当你给语音助手一些工具,比如来自向量数据库的上下文信息时,结果非常令人印象深刻。结合了 Twilio、OpenAI 和 Astra DB,就造就了一个非常强大的语音助手。
你可以在我的[fork的Twilio项目](https://github.com/philnash/speech-assistant-openai-realtime-api-node)中找到与此相关的代码。不过你也可以继续扩展,比如定义和添加更多的功能给代理。请务必查看[OpenAI关于为模型定义功能的最佳实践](https://platform.openai.com/docs/guides/function-calling#best-practices-for-defining-functions)。
有兴趣构建其他AI代理的话,可以查看[如何与Langflow和Composio一起工作](https://www.datastax.com/blog/build-simple-ai-agent-with-langflow-composio?utm_medium=byline&utm_source=devto&utm_campaign=voice-agent),或最近“黑客代理活动”中的[工作坊和相关视频](https://www.youtube.com/watch?v=mn1ZnlqnQlg)。
你对语音代理或代理型RAG感到激动吗?来我们这里聊聊你正在开发的东西。
[DataStax Devs Discord](https://discord.gg/datastax)
想和OpenAI、Twilio、Cloudflare、Unstructured和DataStax一起撸起袖子大干一场吗?[2月28日来旧金山参加Hacking Agents黑客马拉松](https://lu.ma/hacking-agents-hackathon)(详情请点击链接),这是一场精彩的24小时黑客马拉松,在这里我们将深入探讨开发者能用最新的AI工具创造出什么。