0
138
0
0
首页/专栏/ 技术分享/ 查看内容

用 LlamaIndex 和 ClickHouse 为Hacker News 和 Stack Overflow 构建聊天机器人 ...

 admin   发表于  2024-10-15 15:01
专栏 技术分享

介绍

Hacker News 和 StackOverflow 上包含大量关于开发者工具动态的数据,无论是让人么感到兴奋的某些事物,还是他们遇到的问题。虽然这些工具是以帖子为基础使用的,但如果将所有数据都聚合在一起,它们将为您提供关于开发者生态系统的很好概况。作为这两个工具的热心用户,我们想知道诸如以下问题的答案:

“对于在1000多员工的组织中工作的人们而言,他们在最想使用的基础设施工具方面,主要的观点是什么?”

在本博客文章中,我们将构建一个名为“HackBot”的基于LLM的聊天机器人,通过使用 ClickHouse、LlamaIndex、Streamlit 和 OpenAI 来回答这些问题。您将学习如何:

  • 在 ClickHouse 中存储和查询向量

  • 使用 LlamaIndex 将文本转换为 SQL 查询,然后使用新的 ClickHouse-Llama Index 集成在 ClickHouse 中执行这些查询,以查询 Stack Overflow 的调查数据。

  • 在 Hacker News 上进行带有元数据过滤的向量搜索

  • 将两种搜索方法结合起来,为 LLM 提供丰富的上下文

  • 使用 Streamlit 快速构建基于聊天的用户界面


一些背景

去年,我们探索了:当用户需要用高性能线性扫描,来获得准确结果和/或能够将向量搜索与元数据过滤和聚合结合使用时,ClickHouse 如何作为向量数据库使用。用户可以利用这些功能,通过 Retrieval-augmented generation (RAG) 管道为 LLM 应用提供上下文。随着我们在支持向量搜索的底层支持的持续投入,我们意识到,支持用户构建依赖向量搜索的应用程序也需要投资于周围生态系统。

本着这一精神,我们最近在 LlamaIndex 中添加了对 ClickHouse 的支持,并增强了 Langchain 对 ClickHouse 精确匹配和模板的支持,以简化入门体验。

作为改进这些集成的一部分,我们还花了一些时间将它们付诸实践,并构建了一个名为 HackBot 的应用程序。此应用程序将基于 LlamaIndex。特别是,我们将使用 ClickHouse 和 LlamaIndex 的组合,将 SQL 表中的结构化结果与对 Hacker News 的非结构化向量搜索相结合,以为 LLM 提供上下文。

如果您好奇,我们如何在不到两百行代码的情况下构建了以下内容(提示:Streamlit 在这里有所帮助),请继续阅读,或直接在这里阅读代码【https://github.com/ClickHouse/examples/tree/main/blog-examples/llama-index/hacknernews_app】


为什么选择 LlamaIndex?

我们在之前的帖子中讨论了检索增强生成(RAG)的概念,以及这种技术如何旨在将预训练的语言模型的强大功能与信息检索系统的优势结合起来。这里的目标通常很简单:通过向模型提供从其他来源(通常通过向量搜索获取)获得的额外信息(上下文),来提高生成文本的质量和相关性。

虽然理论上用户可以手动构建这些 RAG 流程,但 LlamaIndex 提供了一个灵活的数据框架和工具包,用于将数据源连接到大型语言模型。通过将许多现有工作流程作为函数库提供,并支持向几乎任何数据存储插入数据和查询数据,开发人员可以专注于对结果质量产生影响的系统组件,而不是担心应用程序的“粘合-组合方式”。在本博客文章中,我们将使用 LlamaIndex 的查询接口来保持代码的最小化。

LlamaIndex 的优势之一是:它能够与各种集成进行互操作。除了可插拔的向量存储接口外,用户还可以集成他们的 LLM、嵌入式模型、图形存储和文档存储,以及钩入(hook)并定制几乎 RAG 流水线的任何步骤。所有这些集成都可以通过 LlamaHub 进行浏览。



我们的应用程序

为了说明 LlamaIndex 的好处,让我们考虑一下我们的应用程序“HackBot”。这将接受旨在获取有关 Hacker News 和 Stack Overflow 所进行过的调查中,人们意见摘要的问题。在我们的概念验证中,这些问题将采用三种一般形式:

  • 结构化问题,可以从 Stack Overflow 的调查数据中回答,例如“最流行的数据库是什么?”。为了回答这个问题,在将响应传递回用户之前,必须生成一个 SQL 查询。我们在之前的博客中探讨了:自然语言到 SQL 生成的挑战。

  • 总结人们对技术的意见的非结构化问题,例如“人们对 ClickHouse 有什么看法?”。这需要对 Hacker News 帖子进行向量搜索,以确定相关评论。然后,可以将这些评论作为上下文提供给 LLM 以生成自然语言响应。

  • 结构化 + 非结构化问题。

    在这种情况下,用户可能提出一个需要从调查结果和帖子中获取上下文的问题。例如,假设用户问:“人们对最流行的数据库有什么看法?”在这种情况下,我们首先需要从调查结果中确定最流行的数据库,然后使用它来搜索 Hacker News 帖子中的意见。然后才能将此上下文提供给 LLM 进行响应生成。

支持这些需求会导致一个相当复杂的 RAG 流程,其中每个流程包含多个决策点:

通过使用 ClickHouse,我们的问题得到了简化,它可以作为结构化信息(调查)的源,并通过向量搜索作为非结构化信息的源。然而,通常情况下,这将需要大量的应用程序粘合和测试,从确保提示有效到在决策点解析响应。

幸运的是,LlamaIndex 允许将所有这些复杂性封装起来,并通过一组现成的库调用来处理。


数据集

任何良好的应用程序首先需要数据。如前所述,我们的 Hacker News(HN)和 Stack Overflow 帖子代表了我们的结构化和非结构化数据,将为我们的应用程序提供信息支持。我们的 Hacker News 数据包含了超过 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 = MergeTreeORDER BY (toDate(time), length, post_score)
你可能会注意到我们有一个评论列和一个文本列。后者包含了评论的文本以及帖子的父级和子级的文本,例如,如果有人回复了评论,那么它就变成了子级。这里的目标只是在返回一行时为 LLM 提供更多上下文。有关我们如何生成这些数据的信息,请参见这里(https://gist.github.com/gingerwizard/fc18cf5a3b9945b626cecb88a2deaa49)

元数据列包含了可以被 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 的有经验的用户会注意到排序键。这将在我们应用程序的后续部分中为按日期、帖子长度(文本中的标记数)和 Hacker News 分配的分数进行过滤的快速查询提供便利。

如果您想测试本文的流程,我们已将所有数据放入了一个 S3 存储桶中的 Parquet 文件中。您可以通过运行以下命令将数据插入:

INSERT INTO hackernews SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/hackernews/embeddings/hackernews-llama.parquet')
我们的 Hacker News 数据涵盖了从 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 = MergeTreeORDER BY tuple()
这里的列名非常描述性,例如,infrastructure_tools_have_worked_with描述了用户希望使用的工具列表。选择这些列名的原因与我们选择在此处大量使用Enum类型而不是LowCardinality类型相同。这些选择使数据具有自描述性。稍后,我们的LLM在生成SQL查询时需要考虑此模式。通过使用Enums和自描述的列名,它避免了在每个列中提供含义和可能值的额外上下文的需要。
从原始格式解析此数据需要一些SQL函数。尽管可以在此处(https://gist.github.com/gingerwizard/d3b32ed801973498e87145ed0c6e4bdb)找到原始命令,但出于简洁起见,我们再次提供了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查询。我们不会构建一个包含我们数据结构的提示,发出HTTP请求到ChatGPT,然后解析响应,而是可以依靠LlamaIndex进行这个操作,只需进行几次调用即可。以下 Python笔记本在此处可见(https://github.com/ClickHouse/examples/blob/main/blog-examples/llama-index/hacknernews_app/structured_nl_to_sql.ipynb)

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 SQLAlchemyengine = 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 enginenl_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 1Answer: The most popular database is MySQL.
您会注意到,我们使用了自定义提示,而不是Llama提供的默认提示。这是必要的,因为我们的survey表包含Array(String)列。为了聚合这些列并返回所需的标量,我们需要使用arrayJoin函数。因此,在我们的模板中包含了这个示例。关于这与默认提示的区别,请参见此处(https://www.diffchecker.com/k3z1umdv/)。


路过

雷人

握手

鲜花

鸡蛋

版权声明:本文为 clickhouse 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
返回顶部