简介
Hacker News 和 Stack Overflow 包含大量关于开发工具现状的数据,包括人们兴奋的工具和他们正在努力解决的问题。虽然这些工具是在逐篇文章的基础上使用的,但如果您将所有数据汇总在一起,它们就会为您提供对生态系统的总体概览。作为这两个平台的狂热用户,我们想知道以下问题的答案
“在拥有超过 1000 人的组织中工作的人最想使用的基础设施工具的主要观点是什么?”
在这篇博客文章中,我们将构建一个基于 LLM 的聊天机器人“HackBot”,它让我们可以使用 ClickHouse、LlamaIndex、Streamlit 和 OpenAI 来回答这些问题。**您将学习如何:**
- 在 ClickHouse 中存储和查询向量
- 使用 LlamaIndex 将文本转换为 SQL 查询,然后使用新的 ClickHouse-Llama Index 集成在 ClickHouse 中针对 Stack Overflow 调查执行这些查询。
- 使用 LlamaIndex 对 Hacker News 进行向量搜索,并使用元数据过滤
- 结合两种搜索方法,为 LLM 提供丰富的上下文
- 使用 Streamlit 快速构建基于聊天的 UI
一些上下文
去年,我们探讨了如何将 ClickHouse 用作向量数据库,当用户需要对准确结果进行高性能线性扫描和/或能够通过 SQL 将向量搜索与元数据过滤和聚合相结合时。用户可以使用这些功能通过检索增强生成 (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 查询时考虑此模式。通过使用 Enum 和自描述性列名称,它避免了需要提供额外的上下文来解释每个列的含义和可能的值。
从其原始格式解析此数据需要几个 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)}")
这里的关键组件是使用 NLSQLTableQueryEngine
引擎(2),它负责处理对我们的 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 如何将其限制在正确的值(这要归功于我们使用了 Enum)
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).
然后使用此查询的结果来扩展原始问题,再次通过我们的 LLM 使用 SQLAugmentQueryTransform 在幕后完成
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 的视频将为您入门
对于我们的应用程序,我们实际上只需要一个聊天机器人界面,用户可以在其中输入问题。由于以上代码需要 [OpenAI 的 API 密钥](https://platform.openai.com/account/api-keys),因此用户也应该能够通过界面提供此密钥。此外,由于我们发现将 Hacker News 帖子过滤为那些超过一定长度的帖子可以提高上下文的质量,因此我们也理想地希望提供此选项作为用户可以修改的可选过滤器。最后,由于我们可能稍后将数据集扩展到 2021 年以后的调查,因此需要对帖子评分和日期进行额外过滤。幸运的是,Streamlit 已经有一个 [很棒的示例应用程序库](https://streamlit.io/gallery?category=llms),甚至有一个 [用于 LlamaIndex 的示例](https://blog.streamlit.io/build-a-chatbot-with-custom-data-sources-powered-by-llamaindex/),只需要 43 行代码!通过组合几个其他相关的示例 [1][2],熟悉 [Streamlit 缓存](https://docs.streamlit.io/library/advanced-features/caching) 背后的概念,并将我们的 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 [教程](https://docs.streamlit.io/get-started/tutorials) 和 [示例](https://streamlit.io/gallery?category=llms&ref=blog.streamlit.io) 来帮助您理解这些概念,特别是关于 [缓存](https://docs.streamlit.io/library/advanced-features/caching) 的内容。我们的最终应用程序可以在 [此处](https://github.com/ClickHouse/examples/blob/main/blog-examples/llama-index/hacknernews_app/hackbot.py) 找到。
一些观察结果
以上应用程序代表一个相当简单的 RAG 管道,它只需要很少的代码。尽管如此,管道可能非常脆弱,包含多个可能失败的步骤,导致使用错误的决策分支或答案无法返回。我们只对可能的问题进行了很小比例的采样,并没有测试应用程序在不同数据集上进行响应的能力。
构建一个用于概念验证或演示的基于 LLM 的应用程序与将健壮可靠的东西部署到生产环境中是非常不同的。根据我们有限的经验,这极其具有挑战性,对管道进行内省尤其棘手。请注意任何暗示相反的博客或内容!从具有固有随机性的东西中获得可预测的行为,需要的不仅仅是几百行 Python 代码和几个小时的工作。
为了克服这个问题,我们认为任何能够提供对 RAG 管道的可观察性的东西都具有巨大的价值,这样就可以诊断问题并轻松地评估测试集。因此,我们非常高兴地看到 [LlamaIndex](https://www.llamaindex.ai/enterprise) 和 [LangChain](https://www.langchain.ac.cn/langsmith) 最近的产品发展,我们期待尝试这些产品。
关于我们上面的应用程序,请随意进行实验和增强。有一些明显的改进领域。例如,应用程序没有记忆先前回答的问题,没有将这些问题作为未来问题的上下文。这可以通过 LlamaIndex 中的 [记忆](https://docs.llamaindex.ai/en/stable/api_reference/memory.html) 概念轻松添加。此外,添加其他年份的调查结果似乎是一个合乎逻辑的补充。能够提出诸如“2019 年与 2023 年,人们对他们最想使用的数据库的看法发生了怎样的变化?”这样的问题。
结论
我们对支持向量搜索的持续投资包括几个并行轨道
- 进一步提高线性扫描的性能,例如改进 [距离函数的向量化](https://github.com/ClickHouse/ClickHouse/pull/60202)
- 将我们对近似技术(如 HNSW)的支持从实验阶段转移到生产阶段
- 投资于使用向量搜索的更广泛的生态系统
在本博客中,我们探讨了属于上述最后一点的改进 - ClickHouse 与 LlamaIndex 的集成,构建了一个示例应用程序,根据 Hacker News 帖子和 Stack Overflow 调查结果回答人们对技术的看法。
最后,如果您想知道人们对 ClickHouse 的看法(但请注意,我们的帖子只到 2021 年)……