跳到正文
LlamaIndex:产品、工程与评测·· 2024-01-25精选AI 评分63

LlamaIndex 教程:用 LlamaIndex、Qdrant 和 Render 构建可学习的 Slack 机器人

Building a Slack bot that learns with LlamaIndex, Qdrant and Render

AI 导读

LlamaIndex 发布分步教程,教读者用 LlamaIndex、Qdrant 和 Render 构建并部署一个监听 Slack 对话、存储事实并回答问题的 Slackbot。

推荐理由

LlamaIndex 官方教程从零搭建一个能监听 Slack 消息、用向量索引存储记忆并用 Qdrant 持久化的机器人,步骤完整可复现。

正文 · AI 翻译

在这篇文章中,我们将引导你完成构建和部署一个 Slackbot 的过程,该机器人会监听你的对话,从中学习,并利用这些知识来回答有关你的 Slack 工作区中正在发生的事情的问题。我们还会将其部署到 Render 上的生产环境!

立即探索我们的免费和付费计划。

开始前你需要准备的事项

  • 对 LlamaIndex 有基本的了解。如果你还没有,我们文档中的 入门教程 将为你提供理解本教程所需的所有知识,只需几分钟即可完成。
  • 具备 Python 的工作知识,并安装了 Python 3.11 或更高版本
  • 一个你可以安装应用的 Slack 工作区(因此你需要是管理员)
  • 在本地机器上克隆 我们的 Slackbot 仓库。在本文中我们将引用此仓库中的文件。

第 1 步:创建一个 Slack 应用,并将其安装到你的工作区

这是最复杂的步骤,因为 Slack 对权限非常挑剔。

你的 Slackbot 的第一个版本将只有大约 20 行代码。它所做的只是提供一个“challenge”端点,Slack 需要它来验证你的应用是否可用。你可以在仓库中看到此代码为文件 1_flask.py。让我们逐步分析它。

首先,我们引入你的依赖项。如果你还没有这些依赖项,你需要用 pip 或 poetry 安装它们。

from flask import Flask, request, jsonify

现在我们将创建你的 Flask 应用并进行设置,以便它可以在开发环境中运行。

flask_app = Flask(__name__)

if __name__ == "__main__":
    flask_app.run(port=3000)

在这些行之间,我们将添加我们的基本路由:如果收到一个包含带有 challenge 键的 JSON 对象的 POST 请求,我们将返回该键的值。否则我们什么都不做。

@flask_app.route("/", methods=["POST"])
def slack_challenge():
    if request.json and "challenge" in request.json:
        print("Received challenge")
        return jsonify({"challenge": request.json["challenge"]})
    else:
        print("Got unknown request incoming")
        print(request.json)
    return

让你的应用对 Slack 可用

要配置一个 Slack 应用,它需要在 Slack 可以看到的地方运行。所以让我们运行我们的 Slack 应用:

python 1_flask.py

我们将使用 ngrok 进行设置,以便全世界都能看到它。你需要为此步骤下载并安装 ngrok。安装完成后,运行以下命令,以便它可以找到我们在端口 3000 上运行的应用:

ngrok http 3000

ngrok 会给你一个类似 https://1bf6-64-38-189-168.ngrok-free.app 的 HTTPS URL。记下它,因为我们需要将其提供给 Slack。还要记住,如果你停止 ngrok 并再次启动它,此 URL 将会改变,你需要告知 Slack。你只需要在开发期间使用此 URL。

在 Slack 上注册你的应用

转到 Slack API 网站 并点击“Create New App”。你会看到类似这样的屏幕,你需要选择“from scratch”:

选择一个友好的名称和你想要安装到的工作区。你会看到类似这样的屏幕:

接下来,你需要设置你的应用需要哪些权限。点击右下角的“Permissions”链接:

这将带你到“scopes”屏幕,你需要添加此图片中看到的所有 scopes,即:

  • channels:read — 让你的应用查看哪些频道可用
  • channels:join — 让你的应用加入频道
  • channels:history — 让你的应用查看频道中的历史消息
  • chat:write — 让你的应用发送消息
  • users:read — 让你的应用查看人们的名字

保存这些 scopes 后,向上滚动到“Install to workspace”以安装你的应用。

现在你需要告诉 Slack 你的应用在哪里,以便你可以从它接收消息。点击左侧导航中的“Event Subscriptions”链接,并填写它,使其看起来像这样,具体来说:

  • 将你的 Request URL 设置为之前 ngrok 给你的那个 URL
  • 订阅 message.channels 事件

如果你的应用正在运行且 ngrok 正确隧道,你的 Request URL 应该被验证。

呼!内容真不少。你的 Slack 应用现在已经注册好了,Slack 会向它发送消息。但要接收这些消息,你得让它加入一个频道。

第 2 步:加入频道并回复消息

为此,我们需要扩展我们的应用。你可以在 2_join_and_reply.py 中看到这一步的最终结果。让我们逐一看看我们添加了什么:

import dotenv, os
dotenv.load_dotenv()

我们需要一些环境变量,所以你需要添加这些行并安装 python-dotenv。你还需要在项目根目录创建一个 .env 文件,包含三个值:

  • OPENAI_API_KEY:你的 OpenAI API 密钥。你现在还不需要它,但也不妨现在就获取。
  • SLACK_BOT_TOKEN:你可以在 Slack 应用的“OAuth and Permissions”部分找到它。
  • SLACK_SIGNING_SECRET:你可以在 Slack 应用的“Basic Information”部分找到它。

我们将使用 Slack 便捷的 Python SDK 来构建应用,所以先 pip install slack-bolt,然后更新所有导入:

from slack_bolt import App
from flask import Flask, request, jsonify
from slack_bolt.adapter.flask import SlackRequestHandler

现在用我们刚才设置的密钥初始化一个 Slack Bolt 应用:

app = App(
    token=os.environ.get("SLACK_BOT_TOKEN"),
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET")
)
handler = SlackRequestHandler(app)

要监听消息,机器人必须在频道中。你可以让它加入所有公开频道,但为了测试,我创建了一个名为 #bot-testing 的频道,它加入的就是这个频道:

channel_list = app.client.conversations_list().data
channel = next((channel for channel in channel_list.get('channels') if channel.get("name") == "bot-testing"), None)
channel_id = channel.get('id')
app.client.conversations_join(channel=channel_id)

app.client 是 Bolt 框架的 Slack WebClient,因此你可以在框架内直接执行 WebClient 能做的任何事情。这里最后添加的是一个非常简单的消息监听器:

@app.message()
def reply(message, say):
    print(message)
    say("Yes?")

在 Bolt 框架中,@app.message 装饰器告诉框架在收到消息事件时触发此方法。say 参数是一个函数,用于将消息发送回消息来源的频道。因此,这段代码会在每次收到消息时向频道回复“Yes?”。

让我们试试看!停止运行 1_flask.py,改为运行 python 2_join_and_reply.py。你不需要重启 ngrok,它会像之前一样继续向端口 3000 发送消息。以下是我的尝试:

成功了!我们有了一个非常烦人的机器人,它会回复任何人说的每一句话。我们可以做得更好!

第 3 步:仅回复提及机器人的消息

表面上看这是一个非常简单的改动,但 Slack 的传入消息格式有点复杂,所以我们必须添加相当多的代码。你可以在 3_reply_to_mentions.py 中看到最终结果。

首先,为了判断我们的机器人何时被提及,我们需要机器人的用户 ID。在底层,Slack 不使用用户名甚至 @ 句柄,而是使用在所有 Slack 安装中全局唯一的 ID。我们必须获取它:

auth_response = app.client.auth_test()
bot_user_id = auth_response["user_id"]

现在我们添加一段烦人地复杂的代码,解析 Slack 的消息对象,以查看传入消息中提及了哪个用户。如果是机器人,机器人就回复,否则就忽略该消息。随着我们进一步深入,我们会将发给机器人的消息视为“查询”,将任何其他消息视为要存储的“事实”,但我们现在还不会存储它。

@app.message()
def reply(message, say):
    if message.get('blocks'):
        for block in message.get('blocks'):
            if block.get('type') == 'rich_text':
                for rich_text_section in block.get('elements'):
                    for element in rich_text_section.get('elements'):
                        if element.get('type') == 'user' and element.get('user_id') == bot_user_id:
                            for element in rich_text_section.get('elements'):
                                if element.get('type') == 'text':
                                    query = element.get('text')
                                    print(f"Somebody asked the bot: {query}")
                                    say("Yes?")
                                    return
    
    print("Saw a fact: ", message.get('text'))

哎。花了好一会儿才弄对!但现在我们的机器人只在被提及时才回复:

第 4 步:使用 LlamaIndex 存储事实并回答问题

我们已经到了第 4 步,却还没有对 LlamaIndex 做任何事情!但现在是时候了。在 4_incremental_rag.py 中,你会看到一个简单的命令行 Python 脚本演示,它使用 LlamaIndex 存储事实并回答问题。我不会逐行讲解(脚本中有帮助性的注释),但让我们看看重要的几行。记得要 pip install llama-index!

首先,我们创建一个新的 VectorStoreIndex,一个内存中的向量存储,用于存储我们的事实。它一开始是空的。

index = VectorStoreIndex([])

接下来我们创建 3 个 Document 对象,并将它们分别插入到我们的索引中。真实的文档可能是巨大的文本块、完整的 PDF,甚至是图像,但这些只是一些简单的、Slack 消息大小的信息。

doc1 = Document(text="Molly is a cat")
doc2 = Document(text="Doug is a dog")
doc3 = Document(text="Carl is a rat")

index.insert(doc1)
index.insert(doc2)
index.insert(doc3)

最后,我们从索引中创建一个 查询引擎,并向它提问:


query_engine = index.as_query_engine()
response = query_engine.query("Who is Molly?")
print(response)

结果是“Molly is a cat”,外加一大堆调试信息,因为我们在 4_incremental_rag.py 中开启了嘈杂的调试模式。你可以看到我们发送给 LLM 的提示、它从索引中检索到的上下文,以及它生成并返回给我们的响应。

第 5 步:使用 LlamaIndex 在 Slack 中存储事实并回答问题

在 5_rag_in_slack.py 中,我们将之前的两件事结合起来:脚本 3(回复查询)和脚本 4(存储事实并回答问题)。我们不会再次逐行讲解,但以下是重要的更改:

首先,如果尚未安装,请安装 pip install llama-index,并引入你的依赖项。顺便初始化你的索引:

from llama_index import VectorStoreIndex, Document

index = VectorStoreIndex([])

之前我们只是回复“Yes?”(第 73 行),现在改为向查询引擎发送查询,并用响应进行回复:

query = element.get('text')
query_engine = index.as_query_engine()
response = query_engine.query(query)
say(str(response))

之前我们只是记录看到了一个事实(第 82 行),现在将其存储到索引中:

index.insert(Document(text=message.get('text')))

结果是一个可以回答关于它被告知内容的问题的 Slackbot:

太棒了!你可以很容易地想象一个机器人,它监听所有人的对话,并能够回答人们几周或几个月前说过的事情,从而节省大家搜索旧消息的时间和精力。

第 6 步:持久化我们的记忆

然而,我们的机器人有一个关键缺陷:索引仅存储在内存中。如果我们重启机器人,它会忘记一切:

在 6_qdrant.py 中,我们引入了 Qdrant,一个开源的本地向量数据库,将这些事实存储在磁盘上。这样,如果我们重启机器人,它就会记住之前说过的话。pip install qdrant-client 并引入一些新的依赖项:

import qdrant_client
from llama_index.vector_stores.qdrant import QdrantVectorStore

现在,我们将初始化 Qdrant 客户端,将其附加到存储上下文,并在初始化索引时将存储上下文提供给索引:

client = qdrant_client.QdrantClient(
    path="./qdrant_data"
)
vector_store = QdrantVectorStore(client=client, collection_name="slack_messages")
storage_context = StorageContext.from_defaults(vector_store=vector_store)

index = VectorStoreIndex([],storage_context=storage_context)

这一步就到这里!你的机器人现在可以在重启后存活,并记得我把“Doug”打错成了“Dough”,而且懒得为截图修复它:

第 7 步:让最近的消息更重要

我们现在有一个相当强大的机器人!但它有一个微妙的问题:人们可能会说相互矛盾的话,而它没有办法决定谁“正确”,比如当我改变主意关于狗的名字应该是什么时:

在真实的 Slack 对话中,随着情况的发展,人们可能会从说一个项目“在规划中”变为“进行中”再到“已启动”。因此,我们需要一种方法来告诉机器人,最近的消息比旧消息更重要。

为了实现这一点,我们需要进行相当多的重构,最终结果可以在 7_recency.py 中看到。首先,我们需要一堆新的依赖项:

import datetime, uuid
from llama_index.schema import TextNode
from llama_index.prompts import PromptTemplate
from llama_index.postprocessor import FixedRecencyPostprocessor
from llama_index import set_global_handler

为了让最近的消息更重要,我们需要知道消息是何时发送的。为此,我们将停止将 Documents 插入索引,而是插入 Nodes,并将时间戳作为元数据附加到它上面(在底层,我们的 Documents 总是被转换为 Nodes,所以这并没有太大变化):

dt_object = datetime.datetime.fromtimestamp(float(message.get('ts')))
formatted_time = dt_object.strftime('%Y-%m-%d %H:%M:%S')

# get the message text
text = message.get('text')
# create a node with metadata
node = TextNode(
    text=text,
    id_=str(uuid.uuid4()),
    metadata={
        "when": formatted_time
    }
)
index.insert_nodes([node])

我还将回复逻辑从消息处理中提取出来,放入自己的函数 answer_question,只是为了使其更易于阅读。我们要改变的第一件事是给 LLM 的提示:我们必须告诉它最近的消息很重要。为此,我们创建一个提示模板:

template = (
    "Your context is a series of chat messages. Each one is tagged with 'who:' \n"
    "indicating who was speaking and 'when:' indicating when they said it, \n"
    "followed by a line break and then what they said. There can be up to 20 chat messages.\n"
    "The messages are sorted by recency, so the most recent one is first in the list.\n"
    "The most recent messages should take precedence over older ones.\n"
    "---------------------\n"
    "{context_str}"
    "\n---------------------\n"
    "You are a helpful AI assistant who has been listening to everything everyone has been saying. \n"
    "Given the most relevant chat messages above, please answer this question: {query_str}\n"
)
qa_template = PromptTemplate(template)

与 LLM 打交道的乐趣在于,你常常会用英语描述你正在做的事情,然后直接把它发送给 LLM。一个提示模板会自动从查询引擎中获取 context_str 和 query_str。但我们必须像这样在我们的查询引擎上设置这个模板:

query_engine.update_prompts(
    {"response_synthesizer:text_qa_template": qa_template}
)

现在我们还要做两处改动。我们将从向量存储中获取的结果按时间近远排序,LlamaIndex 有一个内置类可以实现这一点。它叫做 FixedRecencyPostprocessor。我们告诉它保存时间戳的键(我们之前在上面节点上定义过)以及它应该返回多少个结果:

postprocessor = FixedRecencyPostprocessor(
    top_k=20, 
    date_key="when", # the key in the metadata to find the date
    service_context=ServiceContext.from_defaults()
)

然后我们需要创建带有后处理器的查询引擎:

query_engine = index.as_query_engine(similarity_top_k=20, node_postprocessors=[postprocessor])

顺便我们还做了最后一件事,就是传入 similarity_top_k=20,这意味着向量存储将给我们 20 条 Slack 消息作为上下文(默认只有 2 条,因为通常 Node 中的文本块要大得多)。

哒哒!现在机器人知道要把更近期的陈述当作事实。

第 8 步:画出猫头鹰的其余部分

这个机器人现在运行得相当不错,但我在构建它时玩得太开心,以至于忘乎所以,又添加了两个功能:

  • 我附加了关于谁在说话的元数据,而不仅仅是何时说的,这样机器人就能回答诸如“Logan 对这个项目说了什么?”之类的问题。
  • 我的同事们在与机器人互动时,试图在一个线程中提出后续问题,就像我们彼此之间那样。所以我添加了一种方式,让机器人理解自己处于一个线程中,并将线程中的回复视为后续问题,即使用户没有直接提到机器人:

实现这两点的代码在 8_rest_of_the_owl.py 中,但我不会逐行讲解。我们得部署这个东西了!

第 9 步:部署到 Render

到目前为止,我们一直在使用通过 ngrok 隧道运行的本地脚本,但即使是最专注的程序员有时也会关掉笔记本电脑。让我们把这个东西放到真正的服务器上。

登录 Render

我们将部署到 Render,这是一个对 Python 友好的托管服务,对小型项目免费。注册一个账户(我建议用 GitHub 登录)。

创建一个新的 GitHub 仓库

Render 从 GitHub 仓库部署东西,所以你需要创建一个新仓库,并将我们现有仓库中的 2 个文件复制进去:

  • pyproject.toml
  • 8_rest_of_the_owl.py 为了简单起见,我们将其重命名为“app.py”。

提交这些文件并将它们推送到 GitHub。

创建一个新的 Render Web 服务

在 Render 中,创建一个新的 Web 服务。将其连接到你在 GitHub 上刚创建的仓库:

Render 可能会自动检测到这是一个 Python 应用,但你应该确保以下设置正确:

  • 名称:任意你选择的名字
  • 区域:任何区域都可以
  • 分支:main
  • 根目录:(留空,表示根目录)
  • 运行时:Python 3
  • 构建命令:poetry install
  • 启动命令:gunicorn app:flask_app(这肯定需要设置)

你还需要向下滚动并设置一些环境变量:

  • PYTHON_VERSION:3.11.6(或你使用的任何版本)
  • OPENAI_API_KEY:你的 OpenAI API 密钥
  • SLACK_BOT_TOKEN:你的 Slack 机器人令牌
  • SLACK_SIGNING_SECRET:你之前的 Slack 签名密钥

然后点击部署,就可以开始了!

你现在有了一个生产环境的 Slack 机器人,它监听消息、记忆、学习并回复。恭喜!

接下来呢?

你可以为这个机器人添加一大堆功能,大致按难度递增的顺序排列:

  • 加入每个频道,而不仅仅是一个,显然!
  • 添加一种方式让机器人忘记事情(删除节点)
  • 让机器人能够使用多个索引,例如你的文档索引,或连接到你的电子邮件或日历
  • 给机器人添加“标签”,使其能够为节点附加元数据,并仅使用(或忽略)被标记为某种方式的节点来回答问题
  • 添加多模态能力,使机器人能够读取图像,甚至用生成的图像进行回复
  • 还有更多功能!

这个机器人玩起来很有趣,构建过程也很有趣,希望你在了解 Slackbots 和 LlamaIndex 时,能像我写这篇教程时一样享受!

来源:LlamaIndex:产品、工程与评测 · llamaindex.ai