如何使用 CrewAI 和 Qdrant 构建智能 Agentic RAG
Kacper Łukawski
·2025 年 1 月 24 日

在最近的一次直播中,我们与 CrewAI,一个用于构建智能多 Agent 应用的框架合作。如果您错过了,Kacper Łukawski 来自 Qdrant,以及 Tony Kipkemboi 来自 CrewAI 深入介绍了 CrewAI 的功能,并演示了如何利用 Qdrant 创建 Agentic RAG(检索增强生成)系统。重点在于使用 Obsidian 作为知识库,实现邮件通信的半自动化。
在本文中,我们将指导您如何设置一个 AI 驱动的系统,该系统直接连接您的电子邮件收件箱和知识库,使其能够分析收件和现有内容,以生成上下文相关的回复建议。
后台 Agent
尽管我们习惯了通常具有类似聊天界面的基于 LLM 的应用程序(即使不是真实的 UI 而是 CLI 工具),但许多日常任务都可以在后台自动化,而无需显式的人为触发。这个概念也称为环境 Agent,即 Agent 始终存在,等待触发器来执行操作。
CrewAI 的基本概念
感谢 Tony 的参与,我们对 CrewAI 有了更多了解,并理解了该框架的基本概念。他介绍了 Agent 和 Crew 的概念,以及如何利用它们构建智能多 Agent 应用。此外,Tony 还描述了 CrewAI 应用可以使用的不同类型的内存。
在 CrewAI 应用中,Qdrant 可以用作短期或实体内存,因为这两个组件都基于 RAG 和向量嵌入。如果您想了解更多关于 CrewAI 中的内存,请访问 CrewAI 概念。
Tony 做了一个有趣的类比。他将 Crew 比作公司里的不同部门,每个部门都有自己的职责,但它们都协同工作以实现公司的目标。
使用 CrewAI、Qdrant 和 Obsidian 笔记实现邮件自动化
我们的网络研讨会重点是构建一个 Agentic RAG 系统,用于实现邮件通信的半自动化。RAG 是此类系统的重要组成部分,因为您不希望对无法基于事实的回复负责。该系统将监控您的 Gmail 收件箱,分析收件,如果检测到邮件不是垃圾邮件、新闻简报或通知,它将准备回复草稿。
另一方面,该系统还将通过监视本地文件系统中的任何更改来监控 Obsidian 笔记。当文件被创建、修改或删除时,系统会自动将这些更改同步到 Qdrant 集合中,从而确保知识库始终是最新的。Obsidian 使用 Markdown 文件存储笔记,因此无需复杂的解析。
以下是展示系统目标架构的简化图表
Qdrant 作为知识库,存储 Obsidian 笔记的嵌入向量。
系统实现
由于我们的系统集成了两个外部 API - Gmail 和文件系统。我们不会详细介绍如何使用这些 API,因为这超出了本次网络研讨会的范围。我们将重点关注 CrewAI 和 Qdrant 的集成以及 CrewAI Agent 的实现。
CrewAI <> Qdrant 集成
由于 CrewAI 和 Qdrant 目前还没有官方集成,我们创建了一个自定义实现的 RAGStorage
类,它具有非常直接的接口。
from typing import Optional
from crewai.memory.storage.rag_storage import RAGStorage
class QdrantStorage(RAGStorage):
"""
Extends Storage to handle embeddings for memory entries
using Qdrant.
"""
...
def search(self,
query: str,
limit: int = 3,
filter: Optional[dict] = None,
score_threshold: float = 0,
) -> list[dict]:
...
def reset(self) -> None:
...
完整的实现可以在 GitHub 仓库中找到。您可以将其用于您自己的项目,或作为自定义实现的参考。如果您想设置一个使用 Qdrant 作为实体和短期内存层的 Crew,可以这样做:
from crewai import Crew, Process
from crewai.memory import EntityMemory, ShortTermMemory
from email_assistant.storage import QdrantStorage
qdrant_location= "http://localhost:6333"
qdrant_api_key = "your-secret-api-key"
embedder_config = {...}
crew = Crew(
agents=[...],
tasks=[...], # Automatically created by the @task decorator
process=Process.sequential,
memory=True,
entity_memory=EntityMemory(
storage=QdrantStorage(
type="entity-memory",
embedder_config=embedder_config,
qdrant_location=qdrant_location,
qdrant_api_key=qdrant_api_key,
),
),
short_term_memory=ShortTermMemory(
storage=QdrantStorage(
type="short-term-memory",
embedder_config=embedder_config,
qdrant_location=qdrant_location,
qdrant_api_key=qdrant_api_key,
),
),
embedder=embedder_config,
verbose=True,
)
这两种类型的内存将在 Qdrant 中使用不同的集合名称,以便您可以轻松区分它们,并且数据不会混淆。
我们计划在不久的将来发布一个用于 CrewAI 和 Qdrant 集成的工具,敬请期待!
将 Obsidian 笔记加载到 Qdrant
为了演示的目的,我们决定简单地抓取 CrewAI 和 Qdrant 的文档,并将其存储在 Obsidian 笔记中。使用 Obsidian Web Clipper 可以轻松实现这一点,因为它允许您将网页保存为 Markdown 文件。
假设我们检测到 Obsidian 笔记发生了变化,例如创建新笔记或修改现有笔记,我们希望将这些更改加载到 Qdrant。我们可能会使用一些分块方法,从基本的固定大小分块开始,或者直接进行语义分块。然而,LLMs 也以其将文本分割成有意义的部分的能力而闻名,因此我们决定尝试一下。此外,在许多情况下,标准分块就足够了,但我们也想测试由 Anthropic 引入的上下文检索概念。简而言之,其思想是使用 LLMs 为每个块生成一个简短的上下文,从而将该块定位在整个文档的上下文中。
事实证明,在 CrewAI 中实现这样的 Crew 非常简单。Crew 中有两个角色 - 一个负责文本分块,另一个负责生成上下文。它们都可以像这样在 YAML 文件中定义
chunks_extractor:
role: >
Semantic chunks extractor
goal: >
Parse Markdown to extract digestible pieces of information which are
semantically meaningful and can be easily understood by a human.
backstory: >
You are a search expert building a search engine for Markdown files.
Once you receive a Markdown file, you divide it into meaningful semantic
chunks, so each chunk is about a certain topic or concept. You're known
for your ability to extract relevant information from large documents and
present it in a structured and easy-to-understand format, that increases
the searchability of the content and results quality.
contextualizer:
role: >
Bringing context to the extracted chunks
goal: >
Add context to the extracted chunks to make them more meaningful and
understandable. This context should help the reader understand the
significance of the information and how it relates to the broader topic.
backstory: >
You are a knowledge curator who specializes in making information more
accessible and understandable. You take the extracted chunks and provide
additional context to make them more meaningful by bringing in relevant
information about the whole document or the topic at hand.
CrewAI 使得定义此类 Agent 变得非常容易,即使是非技术人员也能理解和修改 YAML 文件。
另一个 YAML 文件定义了 Agent 应该执行的任务
extract_chunks:
description: >
Review the document you got and extract the chunks from it. Each
chunk should be a separate piece of information that can be easily understood
by a human and is semantically meaningful. If there are two or more chunks that
are closely related, but not put next to each other, you can merge them into
a single chunk. It is important to cover all the important information in the
document and make sure that the chunks are logically structured and coherent.
<document>{document}</document>
expected_output: >
A list of semantic chunks with succinct context of information extracted from
the document.
agent: chunks_extractor
contextualize_chunks:
description: >
You have the chunks we want to situate within the whole document.
Please give a short succinct context to situate this chunk within the overall
document for the purposes of improving search retrieval of the chunk. Answer
only with the succinct context and nothing else.
expected_output: >
A short succinct context to situate the chunk within the overall document, along
with the chunk itself.
agent: contextualizer
仅有 YAML 文件不足以让 Agent 工作,所以我们需要用 Python 实现它们。Agent 的角色、目标和背景故事,以及任务描述和预期输出,都被用来构建发送给 LLM 的提示。此外,代码定义了要使用哪个 LLM,以及交互的其他一些参数,例如结构化输出。我们严重依赖 Pydantic 模型来定义任务的输出,以便应用程序可以轻松处理响应,例如将它们存储在 Qdrant 中。
from crewai import Agent, Crew, Process, Task
from crewai.project import CrewBase, agent, crew, task
from email_assistant import models
...
@CrewBase
class KnowledgeOrganizingCrew(BaseCrew):
"""
A crew responsible for processing raw text data and converting it into structured knowledge.
"""
agents_config = "config/knowledge/agents.yaml"
tasks_config = "config/knowledge/tasks.yaml"
@agent
def chunks_extractor(self) -> Agent:
return Agent(
config=self.agents_config["chunks_extractor"],
verbose=True,
llm="anthropic/claude-3-5-sonnet-20241022",
)
...
@task
def contextualize_chunks(self) -> Task:
# The task description is borrowed from the Anthropic Contextual Retrieval
# See: https://www.anthropic.com/news/contextual-retrieval/
return Task(
config=self.tasks_config["contextualize_chunks"],
output_pydantic=models.ContextualizedChunks,
)
...
@crew
def crew(self) -> Crew:
"""Creates the KnowledgeOrganizingCrew crew"""
return Crew(
agents=self.agents, # Automatically created by the @agent decorator
tasks=self.tasks, # Automatically created by the @task decorator
process=Process.sequential,
memory=True,
entity_memory=self.entity_memory(),
short_term_memory=self.short_term_memory(),
embedder=self.embedder_config,
verbose=True,
)
完整的实现再次可以在 GitHub 仓库中找到。
在 Gmail 收件箱中起草邮件
此时,我们的笔记已经存储在 Qdrant 中,我们可以使用这些笔记作为事实依据在 Gmail 收件箱中回复邮件。系统将监控 Gmail 收件箱,如果检测到不是垃圾邮件、新闻简报或通知的邮件,它将根据存储在 Qdrant 中的知识库起草回复。同样,这意味着我们需要使用两个 Agent - 一个用于检测收件类型,另一个用于起草回复。
这些 Agent 的 YAML 文件可能如下所示
categorizer:
role: >
Email threads categorizer
goal: >
Automatically categorize email threads based on their content.
backstory: >
You're a virtual assistant with a knack for organizing information.
You're known for your ability to quickly and accurately categorize email
threads, so that your clients know which ones are important to answer
and which ones are spam, newsletters, or other types of messages that
do not require attention.
Available categories: QUESTION, NOTIFICATION, NEWSLETTER, SPAM. Do not make
up new categories.
response_writer:
role: >
Email response writer
goal: >
Write clear and concise responses to an email thread. Try to help the
sender. Use the external knowledge base to provide relevant information.
backstory: >
You are a professional writer with a talent for crafting concise and
informative responses. You're known for your ability to quickly understand
the context of an email thread and provide a helpful and relevant response
that addresses the sender's needs. You always rely on your knowledge base
to provide accurate and up-to-date information.
类别集是预定义的,因此分类器不应发明新的类别。任务定义如下:
categorization_task:
description: >
Review the content of the following email thread and categorize it
into the appropriate category. There might be multiple categories that
apply to the email thread.
<messages>{messages}</messages>
expected_output: >
A list of all the categories that the email threads can be classified into.
agent: categorizer
response_writing_task:
description: >
Write a response to the following email thread. The response should be
clear, concise, and helpful to the sender. Always rely on the Qdrant search
tool, so you can get the most relevant information to craft your response.
Please try to include the source URLs of the information you provide.
Only focus on the real question asked by the sender and do not try to
address any other issues that are not directly related to the sender's needs.
Do not try to provide a response if the context is not clear enough.
<messages>{messages}</messages>
expected_output: >
A well-crafted response to the email thread that addresses the sender's needs.
Please use simple HTML formatting to make the response more readable.
Do not include greetings or signatures in your response, but provide the footnotes
with the source URLs of the information you used, if possible.
If the provided context does not give you enough information to write a response,
you must admit that you cannot provide a response and write "I cannot provide a response.".
agent: response_writer
我们特别要求 Agent 包含他们提供信息的来源 URL,以便发件人和收件人都可以验证信息。
工作系统
我们已经定义了两个 Crew,应用程序已准备就绪。剩下的唯一事情就是监控 Gmail 收件箱和 Obsidian 笔记的变化。我们使用 watchdog
库来监控文件系统,使用 google-api-python-client
来监控 Gmail 收件箱,但我们不会详细介绍如何使用这些库,因为集成代码会使这篇博客文章过长。
如果您打开应用程序的 主文件,您会看到它非常简单。它运行两个独立的线程,一个用于监控 Gmail 收件箱,另一个用于监控 Obsidian 笔记。如果检测到任何事件,应用程序将运行相应的 Crew 来处理数据,并将结果回复发送回邮件线程或 Qdrant 集合。无需用户界面,因为您的环境 Agent 正在后台工作。
结果
系统现已准备就绪并可以运行,它可以半自动化邮件通信,并保持知识库最新。如果设置得当,您可以期待系统为非垃圾邮件、新闻简报或通知的邮件起草回复,这样您的收件箱看起来可能像这样,即使您在睡觉:
资料
照例,我们准备了网络研讨会的视频录像,您可以方便时观看
演示的源代码可在 GitHub 上找到,如果您想自己尝试,请随意克隆或 Fork 仓库,并按照 README 文件中的说明操作。
您正在使用 CrewAI 和 Qdrant 构建 Agentic RAG 应用程序吗?请加入 我们的 Discord 社区并分享您的经验!