简介
Hacker News 和 StackOverflow 包含大量关于开发者工具状态的数据,包括人们对哪些工具感到兴奋,以及他们在使用哪些工具时遇到困难。尽管这些工具是逐个帖子使用的,但如果您将所有数据聚合在一起,它们会为您提供生态系统的概览。作为两者的忠实用户,我们想知道以下问题的答案:
“在超过 1000 人的组织中工作的人们最想使用的基础设施工具的主要观点是什么?”
在这篇博客文章中,我们将构建一个由 LLM 支持的聊天机器人“HackBot”,使我们能够使用 ClickHouse、LlamaIndex、Streamlit 和 OpenAI 回答这些问题。您将学习如何:
- 在 ClickHouse 中存储和查询向量
- 使用 LlamaIndex 将文本转换为 SQL 查询,然后使用新的 ClickHouse-Llama Index 集成对 ClickHouse 中的 Stack Overflow 调查执行这些查询。
- 使用 LlamaIndex 在 Hacker News 上进行向量搜索,并进行元数据过滤
- 结合两种搜索方法,为 LLM 提供丰富的上下文
- 使用 Streamlit 快速构建基于聊天的用户界面
一些背景信息
去年,我们探讨了当用户需要高性能线性扫描以获得准确结果,和/或需要通过 SQL 将向量搜索与元数据过滤和聚合结合起来时,如何将 ClickHouse 用作向量数据库。用户可以使用这些功能,通过检索增强生成 (RAG) 管道为基于 LLM 的应用程序提供上下文。随着我们对向量搜索底层支持的持续投入[1][2][3],我们认识到,支持用户构建依赖向量搜索的应用程序,也需要我们投资于周围的生态系统。
本着这种精神,我们最近添加了 对 LlamaIndex 的 ClickHouse 支持,并增强了 Langchain 对 ClickHouse 精确匹配的支持和模板,以简化入门体验。
作为改进这些集成的一部分,我们还花了一些时间将它们付诸实践,并构建了一个名为 HackBot 的应用程序。此应用程序将基于 LlamaIndex。特别是,我们将使用 ClickHouse 和 LlamaIndex 的组合,将来自 SQL 表的结构化结果与非结构化向量搜索相结合,为 LLM 提供上下文。
如果您好奇我们如何在不到两百行代码中构建以下内容(提示:Streamlit 在这里有所帮助),请继续阅读,或者直接阅读 这里的 代码…
为什么选择 LlamaIndex?
我们曾在 之前的文章 中讨论过检索增强生成 (RAG) 的概念,以及这项技术如何旨在将预训练语言模型的强大功能与信息检索系统的优势相结合。这里的目标通常很简单:通过提供从其他来源(通常通过向量搜索)获得的额外信息(上下文),来提高生成文本的质量和相关性。
虽然理论上用户可以手动构建这些 RAG 流程,但 LlamaIndex 提供了一个灵活的数据框架和工具包,用于将数据源连接到大型语言模型。通过将许多现有的工作流程作为函数库提供,并支持将数据插入和查询几乎任何数据存储,开发人员可以将重点放在影响结果质量的系统组件上,而不是关注应用程序的“粘合剂”。我们将在本博文中使用 LlamaIndex 的查询界面,以保持我们的代码最少。
LlamaIndex 的优势之一是它能够与范围广泛的集成协同工作。除了可插拔的向量存储接口外,用户还可以集成他们的 LLM、嵌入模型、图存储和文档存储,以及挂钩和自定义 RAG 管道几乎任何步骤的能力。所有这些集成都可以通过 LlamaHub 浏览。
我们的应用程序
为了说明 LlamaIndex 的优势,让我们考虑一下我们的应用程序“HackBot”。这将接受旨在从 Hacker News 和 Stack Overflow 进行的调查中获取人们意见摘要的问题。在我们的概念验证中,这些问题将采用三种一般形式
- 结构化问题,可以从 Stack Overflow 调查数据中回答,例如“最流行的数据库是什么?”。为了回答这个问题,必须先生成 SQL 查询,然后才能将响应传递回用户。我们在 之前的博客 中探讨了自然语言到 SQL 生成的挑战。
- 非结构化问题,总结人们对技术的看法,例如“人们对 ClickHouse 有什么看法?”。这需要对 Hacker News 帖子进行向量搜索,以识别相关评论。然后可以将这些评论作为上下文提供给 LLM,以生成自然语言响应。
- 结构化 + 非结构化问题。在这种情况下,用户可能会提出一个问题,该问题需要从调查结果和帖子中获取上下文。例如,假设用户问,“人们对最流行的数据库有什么看法?” 在这种情况下,我们首先需要从调查结果中确定最流行的数据库,然后再使用它在 Hacker News 帖子中搜索意见。只有这样,才能将此上下文提供给 LLM 以生成响应。
支持这些问题会导致相当复杂的 RAG 流程,上述每种问题类型都有一个包含多个决策点的管道
使用 ClickHouse 可以简化我们的问题,ClickHouse 可以充当结构化信息源(调查),通过 SQL 和非结构化向量搜索获得。但是,通常这需要大量的应用程序粘合和测试,从确保提示有效到解析决策点的响应。
幸运的是,LlamaIndex 允许将所有这些复杂性封装并通过一组现有的库调用来处理。
数据集
任何好的应用程序首先都需要数据。如前所述,我们的 Hacker News (HN) 和 Stack Overflow 帖子代表了将驱动我们应用程序的结构化和非结构化数据。我们的 HN 数据在这里超过 2800 万行和 NGiB,而 Stack Overflow 数据量较小,只有 83439 个回复。
我们的 Hacker News 行由用户的评论和关联的元数据组成,例如发布时间、用户名和帖子的评分。文本已使用 sentence-transformers/all-MiniLM-L6-v2
嵌入,以生成 384 维向量。这导致以下模式
CREATE TABLE hackernews
(
`id` String,
`doc_id` String,
`comment` String,
`text` String,
`vector` Array(Float32),
`node_info` Tuple(start Nullable(UInt64), end Nullable(UInt64)),
`metadata` String,
`type` Enum8('story' = 1, 'comment' = 2, 'poll' = 3, 'pollopt' = 4, 'job' = 5),
`by` LowCardinality(String),
`time` DateTime,
`title` String,
`post_score` Int32,
`dead` UInt8,
`deleted` UInt8,
`length` UInt32,
`parent` UInt32,
`kids` Array(UInt32)
)
ENGINE = MergeTree
ORDER BY (toDate(time), length, post_score)
您可能会注意到我们有一个 comment
列以及一个 text
列。后者包含 comment
以及帖子的父级和子级的文本,例如,如果有人回复评论,则它成为子级。这里的目标是在返回行时简单地为 LLM 提供更多上下文。有关我们如何生成此数据的信息,请参阅 此处。
metadata
列包含 LlamaIndex 工作流程可以自动查询的字段,例如,如果他们确定需要额外的过滤器来回答问题。对于我们当前的实现,我们为此列使用包含 JSON 的字符串。将来,一旦准备好生产,我们计划将其移动到 JSON 类型以获得更好的查询性能。目前,我们将所有列复制到此字符串,从而使它们可用于 LlamaIndex,例如
{"deleted":0,"type":"story","by":"perler","time":"2006-10-13 14:46:50","dead":0,"parent":0,"poll":0,"kids":[454479],"url":"http:\/\/www.techcrunch.com\/2006\/10\/13\/realtravel-trip-planner-cut-paste-share-travel-tips\/","post_score":2,"title":"RealTravel Trip Planner: Cut, Paste & Share Travel Tips","parts":[],"descendants":0}
ClickHouse 的经验丰富的用户会注意到排序键。这将有助于稍后在我们的应用程序中快速查询按日期、帖子长度(文本中的标记数)和 HN 分配的分数过滤的查询。
如果您想继续操作,我们将所有数据都放在 S3 存储桶中的 Parquet 文件中。您可以通过运行以下命令插入数据
INSERT INTO hackernews SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/hackernews/embeddings/hackernews-llama.parquet')
我们的 HN 数据涵盖了 2006 年 10 月至 2021 年 10 月期间。为了确保我们的 Stack Overflow 数据与此对齐,我们将仅加载 2021 年的调查结果。
扩展这些数据集将是一项相当简单的练习,尽管调查列在不同年份之间确实有所不同。跨年份协调这些数据将允许诸如“2022 年人们对最流行的 Web 技术有何看法?”之类的问题。我们将此留给热情的读者作为练习。
此数据包含大量列,如下面的模式所示
CREATE TABLE surveys
(
`response_id` Int64,
`development_activity` Enum8('I am a developer by profession' = 1, 'I am a student who is learning to code' = 2, 'I am not primarily a developer, but I write code sometimes as part of my work' = 3, 'I code primarily as a hobby' = 4, 'I used to be a developer by profession, but no longer am' = 5, 'None of these' = 6, 'NA' = 7),
`employment` Enum8('Independent contractor, freelancer, or self-employed' = 1, 'Student, full-time' = 2, 'Employed full-time' = 3, 'Student, part-time' = 4, 'I prefer not to say' = 5, 'Employed part-time' = 6, 'Not employed, but looking for work' = 7, 'Retired' = 8, 'Not employed, and not looking for work' = 9, 'NA' = 10),
`country` LowCardinality(String),
`us_state` LowCardinality(String),
`uk_county` LowCardinality(String),
`education_level` Enum8('Secondary school (e.g. American high school, German Realschule or Gymnasium, etc.)' = 1, 'Bachelor's degree (B.A., B.S., B.Eng., etc.)' = 2, 'Master's degree (M.A., M.S., M.Eng., MBA, etc.)' = 3, 'Other doctoral degree (Ph.D., Ed.D., etc.)' = 4, 'Some college/university study without earning a degree' = 5, 'Something else' = 6, 'Professional degree (JD, MD, etc.)' = 7, 'Primary/elementary school' = 8, 'Associate degree (A.A., A.S., etc.)' = 9, 'NA' = 10),
`age_started_to_code` Enum8('Younger than 5 years' = 1, '5 - 10 years' = 2, '11 - 17 years' = 3, '18 - 24 years' = 4, '25 - 34 years' = 5, '35 - 44 years' = 6, '45 - 54 years' = 7, '55 - 64 years' = 8, 'Older than 64 years' = 9, 'NA' = 10),
`how_learned_to_code` Array(String),
`years_coding` Nullable(UInt8),
`years_as_a_professional_developer` Nullable(UInt8),
`developer_type` Array(String),
`organization_size` Enum8('Just me - I am a freelancer, sole proprietor, etc.' = 1, '2 to 9 employees' = 2, '10 to 19 employees' = 3, '20 to 99 employees' = 4, '100 to 499 employees' = 5, '500 to 999 employees' = 6, '1,000 to 4,999 employees' = 7, '5,000 to 9,999 employees' = 8, '10,000 or more employees' = 9, 'I don't know' = 10, 'NA' = 11),
`compensation_total` Nullable(UInt64),
`compensation_frequency` Enum8('Weekly' = 1, 'Monthly' = 2, 'Yearly' = 3, 'NA' = 4),
`language_have_worked_with` Array(String),
`language_want_to_work_with` Array(String),
`database_have_worked_with` Array(String),
`database_want_to_work_with` Array(String),
`platform_have_worked_with` Array(String),
`platform_want_to_work_with` Array(String),
`web_framework_have_worked_with` Array(String),
`web_framework_want_to_work` Array(String),
`other_tech_have_worked_with` Array(String),
`other_tech_want_to_work` Array(String),
`infrastructure_tools_have_worked_with` Array(String),
`infrastructure_tools_want_to_work_with` Array(String),
`developer_tools_have_worked_with` Array(String),
`developer_tools_want_to_work_with` Array(String),
`operating_system` Enum8('MacOS' = 1, 'Windows' = 2, 'Linux-based' = 3, 'BSD' = 4, 'Other (please specify):' = 5, 'Windows Subsystem for Linux (WSL)' = 6, 'NA' = 7),
`frequency_visit_stackoverflow` Enum8('Multiple times per day' = 1, 'Daily or almost daily' = 2, 'A few times per week' = 3, 'A few times per month or weekly' = 4, 'Less than once per month or monthly' = 5, 'NA' = 6),
`has_stackoverflow_account` Enum8('Yes' = 1, 'No' = 2, 'Not sure/can\'t remember' = 3, 'NA' = 4),
`frequency_use_in_stackoverflow` Enum8('Multiple times per day' = 1, 'Daily or almost daily' = 2, 'A few times per week' = 3, 'A few times per month or weekly' = 4, 'Less than once per month or monthly' = 5, 'I have never participated in Q&A on Stack Overflow' = 6, 'NA' = 7),
`consider_self_active_community_member` Enum8('Yes, definitely' = 1, 'Neutral' = 2, 'Yes, somewhat' = 3, 'No, not at all' = 4, 'No, not really' = 5, 'NA' = 6, 'Not sure' = 7),
`member_other_communities` Enum8('Yes' = 1, 'No' = 2, 'NA' = 4),
`age` Enum8('Under 18 years old' = 1, '18-24 years old' = 2, '25-34 years old' = 3, '35-44 years old' = 4, '45-54 years old' = 5, '55-64 years old' = 6, '65 years or older' = 7, 'NA' = 8, 'Prefer not to say' = 9),
`annual_salary` Nullable(UInt64)
)
ENGINE = MergeTree
ORDER BY tuple()
此处的列名非常具有描述性,例如,
infrastructure_tools_have_worked_with
描述了用户想要使用的工具列表。选择这些列名的原因与我们在此处选择自由使用 Enum 类型而不是 LowCardinality 的原因相同。这些选择使数据具有自描述性。稍后,我们的 LLM 在生成 SQL 查询时需要考虑此模式。通过使用枚举和自描述列名,避免了提供额外的上下文以及每个列中含义和可能值的解释的需要。
从其原始格式解析此数据需要一些 SQL 函数。虽然可以在 此处 找到原始命令,但为了简洁起见,我们再次以 Parquet 格式提供最终数据
INSERT INTO surveys SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/surveys/2021/surveys-llama.parquet')
值得注意的是,数据可以通过 LlamaIndex 插入到 ClickHouse 中。出于性能和简洁性的原因,我们选择直接通过 ClickHouse 客户端执行此操作。
在 LlamaIndex 中构建 RAG 管道
LlamaIndex 以 Python 和 Typescript 两种语言提供。对于我们的示例,我们将使用 Python,原因仅仅是我更喜欢它 :)。
与其一次性构建我们的 RAG 流程,不如先组装构建块:测试用于结构化和非结构化查询的单独查询引擎。
要安装 LlamaIndex 的 ClickHouse 集成,您可以简单地使用
pip install llama-index-vector-stores-clickhouse
使用 LlamaIndex 生成 SQL
如上所述,我们将需要将一些问题转换为针对我们的 Stack Overflow 数据的 SQL 查询。与其构建包含我们模式的提示、向 ChatGPT 发出 HTTP 请求并解析响应,我们可以依靠 LlamaIndex 通过几个调用来完成此操作。以下笔记本可在 此处 获得。
CLICKHOUSE_TEXT_TO_SQL_TMPL = (
"Given an input question, first create a syntactically correct {dialect} "
"query to run, then look at the results of the query and return the answer. "
"You can order the results by a relevant column to return the most "
"interesting examples in the database.\n\n"
"Never query for all the columns from a specific table, only ask for a "
"few relevant columns given the question.\n\n"
"Pay attention to use only the column names that you can see in the schema "
"description. "
"Be careful to not query for columns that do not exist. "
"Pay attention to which column is in which table. "
"Also, qualify column names with the table name when needed. \n"
"If needing to group on Array Columns use the ClickHouse function arrayJoin e.g. arrayJoin(columnName) \n"
"For example, the following query identifies the most popular database:\n"
"SELECT d, count(*) AS count FROM so_surveys GROUP BY "
"arrayJoin(database_want_to_work_with) AS d ORDER BY count DESC LIMIT 1\n"
"You are required to use the following format, each taking one line:\n\n"
"Question: Question here\n"
"SQLQuery: SQL Query to run\n"
"SQLResult: Result of the SQLQuery\n"
"Answer: Final answer here\n\n"
"Only use tables listed below.\n"
"{schema}\n\n"
"Question: {query_str}\n"
"SQLQuery: "
)
CLICKHOUSE_TEXT_TO_SQL_PROMPT = PromptTemplate(
CLICKHOUSE_TEXT_TO_SQL_TMPL,
prompt_type=PromptType.TEXT_TO_SQL,
)
# (1) Query engine for ClickHouse exposed through SQLAlchemy
engine = create_engine(
f'clickhouse+native://{username}:{password}@{host}:' +
f'{native_port}/{database}?compression=lz4&secure={secure}'
)
sql_database = SQLDatabase(engine, include_tables=["surveys"], view_support=True)
# (2) Natural language to SQL query engine
nl_sql_engine = NLSQLTableQueryEngine(
sql_database=sql_database,
tables=["surveys"],
text_to_sql_prompt=CLICKHOUSE_TEXT_TO_SQL_PROMPT,
llm=OpenAI(model="gpt-4"),
verbose=True
)
response = nl_sql_engine.query("What is the most popular database?")
print(f"SQL query: {response.metadata['sql_query']}")
print(f"Answer: {str(response)}")
这里的关键组件是在 (2) 处使用 NLSQLTableQueryEngine
引擎,它处理对我们的 LLM 模型 (OpenAI gpt-4) 的查询,传递模式和提示。从响应中提取 SQL 查询后,它会在我们的 ClickHouse 实例上执行此查询,然后再构建响应。
以上输出 MySQL 作为最流行的数据库(根据 2021 年 Stack Overflow!)
SQL query: SELECT d, count(*) AS count FROM surveys GROUP BY arrayJoin(database_have_worked_with) AS d ORDER BY count DESC LIMIT 1
Answer: The most popular database is MySQL.
您会注意到我们使用了自定义提示,而不是 Llama 提供的默认提示。这是必要的,因为我们的 survey
表包含 Array(String)
列。为了聚合这些列并返回所需的标量,我们需要使用 arrayJoin 函数。因此,我们在模板中包含了一个示例。有关这与默认提示的比较,请参阅 此处。
使用 LlamaIndex 进行向量搜索以获取上下文
为了处理我们的非结构化问题,我们需要能够将问题转换为向量嵌入,以便在将这些结果传递给我们的 LLM 以生成响应之前查询 ClickHouse。理想情况下,这也将利用我们 Hacker News 帖子上的元数据,例如,以便用户可以提出诸如“用户 zX41ZdbW
主要发布关于什么内容?”之类的问题。这需要我们告诉 Llama 哪些字段可用于在我们的 metadata
列中进行查询。
CLICKHOUSE_CUSTOM_SUFFIX = """
The following is the datasource schema to work with.
IMPORTANT: Make sure that filters are only used as needed and only suggest filters for fields in the data source.
Data Source:
{info_str}
User Query:
{query_str}
Structured Request:
"""
CLICKHOUSE_VECTOR_STORE_QUERY_PROMPT_TMPL = PREFIX + EXAMPLES + CLICKHOUSE_CUSTOM_SUFFIX
Settings.embed_model = FastEmbedEmbedding(
model_name="sentence-transformers/all-MiniLM-L6-v2",
max_length=384,
cache_dir="./embeddings/"
)
client = clickhouse_connect.get_client(
host=host, port=port, username=username, password=password,
secure=secure
)
# (1) Build a ClickHouseVectorStore
vector_store = ClickHouseVectorStore(clickhouse_client=client, table="hackernews")
vector_index = VectorStoreIndex.from_vector_store(vector_store)
# (2) Inform the retriever of the available metadata fields
vector_store_info = VectorStoreInfo(
content_info="Social news posts and comments from users",
metadata_info=[
MetadataInfo(
name="post_score", type="int", description="Score of the comment or post",
),
MetadataInfo(
name="by", type="str", description="the author or person who posted the comment",
),
MetadataInfo(
name="time", type="date", description="the time at which the post or comment was made",
),
]
)
# (3) A retriever for vector store index that uses an LLM to automatically set vector store query parameters.
vector_auto_retriever = VectorIndexAutoRetriever(
vector_index, vector_store_info=vector_store_info, similarity_top_k=10,
prompt_template_str=CLICKHOUSE_VECTOR_STORE_QUERY_PROMPT_TMPL, llm=OpenAI(model="gpt-4"),
)
# Query engine to query engine based on context
retriever_query_engine = RetrieverQueryEngine.from_args(vector_auto_retriever, llm=OpenAI(model="gpt-4"))
response = retriever_query_engine.query("What is the user zX41ZdbW saying about ClickHouse?")
print(f"Answer: {str(response)}")
在这里,我们首先在 (1) 处构建 ClickHouseVectorStore
,使用它在 (3) 处构建 VectorIndexAutoRetriever
。请注意,我们还告知此检索器可用的元数据信息,它使用此信息自动将过滤器添加到我们的 ClickHouse 向量查询中。我们的 sentence-transformers/all-MiniLM-L6-v2
模型是全局设置的,将用于为稍后通过 query
方法传递的任何文本创建嵌入,然后再查询 ClickHouse
我们还调整了默认提示,因为我们发现即使元数据中不存在过滤器也会被注入。
如果我们运行 笔记本,则可以在日志中看到这一点。
使用过滤器:[('by', '==', 'zX41ZdbW')]
我们的最终回复表明 zX41ZdbW
非常了解 ClickHouse,这是一个好消息,因为他是 CTO!
“用户 zX41ZdbW 分享了有关 ClickHouse 的一些见解。他们提到,仅仅在聚合函数中添加 __restrict 后,ClickHouse 的性能就提高了 1.6 倍。他们还提到,他们在 ClickHouse 中修补了 Ryu,以提供更友好的表示形式,并为看起来是整数的浮点数提供更好的性能。他们还分享说,ClickHouse 可以为数据清理执行批量 DELETE 操作,这可以满足数据清理、保留和 GDPR 需求。他们还提到,他们期待一篇博客文章,其中包含更多用于比较以优化 ClickHouse 性能的参考资料。”
在幕后,此代码调用 ClickHouseVectorStore
,它发出 cosineDistance(类似于 我们之前的博客 中讨论的那些)查询,以识别概念上相似的帖子。
结合结构化和非结构化
有了上述查询引擎,我们可以将它们与 SQLAutoVectorQueryEngine
引擎结合使用。此引擎的文档很好地总结了其功能
SQL + 向量索引自动检索器查询引擎。
此查询引擎可以查询 SQL 数据库以及向量数据库。它将首先决定是否需要查询 SQL 数据库或向量存储。如果它决定查询 SQL 数据库,它还将决定是否使用从向量存储检索到的结果来扩充信息。我们使用 VectorIndexAutoRetriever 来检索结果。
将 nl_sql_engine
和 retriever_query_engine
引擎与 SQLAutoVectorQueryEngine
结合使用仅需几行代码。为了使此查询引擎确定是向 ClickHouse 发出 SQL 查询还是向量搜索,我们需要向其提供上下文 (2),说明这些引擎提供哪些信息。这是通过从每个引擎创建一个 QueryEngineTool
来提供的,其中包含详细说明其用途的描述。
# (1) create engines as above
vector_auto_retriever = VectorIndexAutoRetriever(
vector_index, vector_store_info=vector_store_info, similarity_top_k=10,
prompt_template_str=CLICKHOUSE_VECTOR_STORE_QUERY_PROMPT_TMPL, llm=OpenAI(model="gpt-4"),
# require context to be of a specific length
vector_store_kwargs={"where": f"length >= 20"}
)
retriever_query_engine = RetrieverQueryEngine.from_args(vector_auto_retriever, llm=OpenAI(model="gpt-4"))
# (2) provide descriptions of each engine to assist SQLAutoVectorQueryEngine
sql_tool = QueryEngineTool.from_defaults(
query_engine=nl_sql_engine,
description=(
"Useful for translating a natural language query into a SQL query over"
f" a table: {stackoverflow_table}, containing the survey responses on"
f" different types of technology users currently use and want to use"
),
)
vector_tool = QueryEngineTool.from_defaults(
query_engine=retriever_query_engine,
description=(
f"Useful for answering semantic questions abouts users comments and posts"
),
)
# (3) engine to query both a SQL database as well as a vector database
sql_auto_vector_engine = SQLAutoVectorQueryEngine(
sql_tool, vector_tool, llm=OpenAI(model="gpt-4")
)
response = sql_auto_vector_engine.query("What are people's opinions on the web technology that people at companies with less than 100 employees want to work with?")
print(str(response))
有了这个,我们现在可以回答更丰富的问题,例如“在员工人数少于 100 人的公司工作的人们最想使用的 Web 技术有哪些?”,这需要同时使用两个数据源。
您会注意到我们在 (1) 处使用参数
vector_store_kwargs={"where": f"length >= 20"}
重新创建了我们的VectorIndexAutoRetriever
。这为任何 ClickHouse 向量查询添加了一个额外的 where 过滤器,将结果限制为至少包含 20 个术语的评论。测试表明,这显着提高了结果的质量。
当我们运行此笔记本时,日志会显示出来。最初,我们可以看到 LLM 用于评估所提问题的类型。这确定我们需要针对调查进行查询。这是通过使用 LlamaIndex 的 路由器功能 实现的,该功能能够通过使用上面提供的描述调用 ChatGPT 在我们的两个不同检索器引擎之间进行选择
INFO:llama_index.core.query_engine.sql_join_query_engine:> Querying SQL database: The first choice is about translating natural language queries into SQL queries over a survey table. This could be used to analyze the responses of people at companies with less than 100 employees about the web technology they want to work with.
ChatGPT 反过来用于获取 SQL 查询。请注意 organization_size
上的过滤器,以及 LLM 如何将其限制为正确的值(这要归功于我们使用了枚举)
SELECT
arrayJoin(web_framework_want_to_work) AS web_tech,
COUNT(*) AS count
FROM surveys
WHERE organization_size IN (1, 2, 3, 4)
GROUP BY web_tech
ORDER BY count DESC
LIMIT 5
此查询在 ClickHouse 上执行,它将 React.js 识别为小型公司中最用户想要使用的 Web 技术
Based on the survey results, the top five web technologies that people at companies with less than 100 employees want to work with are React.js (9765 votes), Vue.js (6821 votes), Express (4657 votes), Angular (4415 votes), and jQuery (3397 votes).
此查询的结果然后用于扩展原始问题,再次通过 SQLAugmentQueryTransform 在幕后使用我们的 LLM
What are the reasons people at companies with less than 100 employees want to work with React.js, Vue.js, Express, Angular, and jQuery?
上述问题将使用我们的 sentence-transformers/all-MiniLM-L6-v2
模型进行嵌入。此嵌入将再次用于通过 ClickHouseVectorStore
查询 ClickHouse,以识别相关的 Hacker News 评论,从而提供上下文并回答我们的问题。
People at smaller companies may prefer to work with React.js, Vue.js, Express, Angular, and jQuery for a variety of reasons. These technologies are widely recognized and supported by large communities, which can be advantageous for troubleshooting and learning new methods. They can also streamline and structure the development process. Furthermore, familiarity with these technologies can be a career asset for developers, as they are frequently sought after in job listings. However, the choice of technology may also be influenced by the project's specific requirements and limitations.
使用 Streamlit 使其更漂亮
虽然以上内容提供了我们应用程序的机制,但用户期望看到比笔记本电脑更具视觉吸引力的东西!对于快速构建应用程序,我们是 Streamlit 的粉丝。这允许用户仅使用 Python 构建应用程序,在几分钟内将数据脚本转换为可共享的 Web 应用程序。
有关将 StreamLit 与 ClickHouse 结合使用的简介,请观看 Data Mark 的以下视频
对于我们的应用程序,我们实际上只需要一个 ChatBot 界面,用户可以在其中输入问题。由于上述代码需要 [OpenAI 的 API 密钥](https://platform.openai.com/account/api-keys),因此用户还应该能够通过界面提供此密钥。此外,由于我们发现将 Hacker News 帖子过滤到一定长度以上的帖子可以提高上下文的质量,因此我们理想情况下还希望将其作为用户可以修改的可选过滤器提供。最后,帖子分数和日期的附加过滤器是可取的,因为我们稍后可能会将数据集扩展到 2021 年以后的调查。幸运的是,Streamlit 已经有一个 优秀的示例应用程序库,甚至有一个用于 LlamaIndex 的应用程序,仅需 43 行代码! 通过结合其他一些相关示例 [1][2],熟悉 Streamlit 缓存 背后的概念,并整合我们的 SQLAutoVectorQueryEngine
,我们可以在 50 行 Python 代码中实现一些非常实用的功能!
st.set_page_config(
page_title="Get summaries of Hacker News posts enriched with Stackoverflow survey results, powered by LlamaIndex and ClickHouse",
page_icon="????????", layout="centered", initial_sidebar_state="auto", menu_items=None)
st.title("????HackBot powered by LlamaIndex ???? and ClickHouse ????")
st.info(
"Check out the full [blog post](https://clickhouse.ac.cn/blog/building-a-hackernews-chat-bot-with-llama-index-with-clickhouse/) for this app",
icon="????")
st.caption("A Streamlit chatbot ???? for Hacker News powered by LlamaIndex ???? and ClickHouse ????")
# Llama Index code here
# identify the value ranges for our score, length and date widgets
if "max_score" not in st.session_state.keys():
client = clickhouse()
st.session_state.max_score = int(
client.query("SELECT max(post_score) FROM default.hackernews_llama").first_row[0])
st.session_state.max_length = int(
client.query("SELECT max(length) FROM default.hackernews_llama").first_row[0])
st.session_state.min_date, st.session_state.max_date = client.query(
"SELECT min(toDate(time)), max(toDate(time)) FROM default.hackernews_llama WHERE time != '1970-01-01 00:00:00'").first_row
# set the initial message on load. Store in the session.
if "messages" not in st.session_state:
st.session_state.messages = [
{"role": "assistant", "content": "Ask me a question about opinions on Hacker News and Stackoverflow!"}]
# build the sidebar with our filters
with st.sidebar:
score = st.slider('Min Score', 0, st.session_state.max_score, value=0)
min_length = st.slider('Min comment Length (tokens)', 0, st.session_state.max_length, value=20)
min_date = st.date_input('Min comment date', value=st.session_state.min_date, min_value=st.session_state.min_date,
max_value=st.session_state.max_date)
openai_api_key = st.text_input("Open API Key", key="chatbot_api_key", type="password")
openai.api_key = openai_api_key
"[Get an OpenAI API key](https://platform.openai.com/account/api-keys)"
"[View the source code](https://github.com/ClickHouse/examples/blob/main/blog-examples/llama-index/hacknernews_app/hacker_insights.py)"
# grab the users OPENAI api key. Don’t allow questions if not entered.
if not openai_api_key:
st.info("Please add your OpenAI API key to continue.")
st.stop()
if prompt := st.chat_input(placeholder="Your question about Hacker News"):
st.session_state.messages.append({"role": "user", "content": prompt})
# Display the prior chat messages
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.write(message["content"])
# If last message is not from assistant, generate a new response
if st.session_state.messages[-1]["role"] != "assistant":
with st.chat_message("assistant"):
with st.spinner("Thinking..."):
# Query our engine for the answer and write to the page
response = str(get_engine(min_length, score, min_date).query(prompt))
st.write(response)
st.session_state.messages.append({"role": "assistant", "content": response})
我们建议您阅读 Streamlit 教程 和 示例,以帮助您理解此处的概念,尤其是关于 缓存 的概念。我们的最终应用程序可以在 此处 找到。
一些观察结果
上述应用程序代表了一个相当简单的 RAG 管道,只需要很少的代码。尽管如此,该管道可能非常脆弱,多个步骤可能会失败,导致使用不正确的决策分支或未返回答案。我们只抽样了可能问题的一小部分,并且没有测试应用程序在各种不同问题中做出响应的能力。
为概念验证或演示构建基于 LLM 的应用程序与部署稳健可靠的生产应用程序截然不同。根据我们有限的经验,这非常具有挑战性,管道的内省尤其棘手。请警惕任何暗示并非如此的博客或内容!从具有内在随机性的事物中获得可预测的行为,需要的不仅仅是几百行 Python 代码和几个小时的工作。
为了克服这个问题,我们看到了任何为 RAG 管道提供可观察性的东西的巨大价值,从而可以诊断问题并轻松评估测试集。因此,我们非常高兴看到 LlamaIndex 和 LangChain 最近的产品开发,我们期待尝试这些产品。
关于我们上面的应用程序,请随时试验和增强它。有一些明显的改进领域。例如,该应用程序没有记忆先前回答过的问题,也没有将这些问题作为未来问题的上下文来考虑。这可以通过 LlamaIndex 中的 内存 概念轻松添加。此外,添加来自其他年份的调查结果似乎是一个合理的补充。能够提出诸如“2019 年与 2023 年相比,人们对他们最想使用的数据库的看法发生了怎样的变化?”之类的问题
结论
我们对支持向量搜索的持续投入包括几个并行轨道
- 进一步提高线性扫描的性能,例如改进的 距离函数向量化
- 将我们对近似技术(例如 HNSW)的支持从实验性转向生产
- 对使用向量搜索的更广泛生态系统的投资
在这篇博客中,我们探讨了属于最后一点的改进 - ClickHouse 与 LlamaIndex 的集成,构建了一个示例应用程序来回答有关人们对技术的看法的问题,这些看法基于 Hacker News 帖子和 Stack Overflow 调查结果。
最后,如果您好奇人们对 ClickHouse 有什么看法(请注意,我们的帖子仅更新到 2021 年)…