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

图片

本文字数:21581;估计阅读时间:54 分钟

作者:Dale McDiarmid

审校:庄晓东(魏庄)

本文在公众号【ClickHouseInc】首发

介绍

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 = MergeTree
ORDER 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 = MergeTree
ORDER 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 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函数。因此,在我们的模板中包含了这个示例。关于这与默认提示的区别,请参见此处(https://www.diffchecker.com/k3z1umdv/)。

使用LlamaIndex进行上下文的向量搜索

为了处理我们的非结构化问题,我们需要将问题转换为向量嵌入,以便在查询ClickHouse之前将这些结果传递给我们的LLM进行响应生成。理想情况下,这还应该利用我们的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)}")

在这里,我们首先构建了一个ClickHouseVectorStore(位于1号位置),然后使用它来构建一个VectorIndexAutoRetriever(位于3号位置)。请注意,我们还向此检索器提供了可用元数据信息的信息,它会使用此信息自动向我们的ClickHouse向量查询添加过滤器。我们的sentence-transformers/all-MiniLM-L6-v2模型被全局设置,并将用于在稍后的query方法中传递任何文本之前为其创建嵌入:

我们还改编了默认提示,因为我们发现即使元数据中没有,过滤器也会被注入。

如果我们运行此 Python笔记本(https://github.com/ClickHouse/examples/blob/main/blog-examples/llama-index/hacknernews_app/unstructured_nl_with_vector_store.ipynb),可以在日志中看到这一点。

Using filters: [('by', '==', 'zX41ZdbW')]

我们最终的响应建议zX41ZdbW对ClickHouse非常了解,这是个好消息,因为他是CTO!

“用户zX41ZdbW分享了有关ClickHouse的几个见解。他们提到,仅在聚合函数中添加__restrict后,ClickHouse的性能提高了1.6倍。他们还提到,他们在ClickHouse中修补了Ryu,以提供更好的表示和对看起来像整数的浮点数具有更好的性能。他们还分享了ClickHouse可以进行批量DELETE操作来进行数据清理,这可以满足数据清理、保留和GDPR要求的需求。他们还提到,他们期待着有关比较优化ClickHouse性能的更多参考的博客文章。”

在幕后,此代码调用ClickHouseVectorStore,该存储发出一个cosineDistance(类似于我们早期博客中讨论的那些)查询,以识别概念上相似的帖子。

结合结构化和非结构化

有了以上查询引擎,我们可以将它们与SQLAutoVectorQueryEngine引擎组合在一起。此引擎的文档很好地总结了其功能:

SQL + Vector Index Auto Retriever查询引擎。

此查询引擎可以查询SQL数据库和向量数据库。它首先会决定是否需要查询SQL数据库或向量存储。如果它决定查询SQL数据库,则还会决定是否使用从向量存储检索的结果增加信息。我们使用VectorIndexAutoRetriever来检索结果。

使用nl_sql_engineretriever_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人的公司工作的人对什么样的网络技术有什么看法?”这种需要两个数据源的问题。

您会注意到我们在(1)处重新创建了我们的VectorIndexAutoRetriever,并设置了参数vector_store_kwargs={"where": f"length >= 20"}。这会向任何传递给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查询。请注意组织规模的筛选器以及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是大多数用户想要在较小公司工作的网络技术:

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应用程序。

要了解如何在ClickHouse中使用StreamLit,请参阅Data Mark的以下视频:(https://www.youtube.com/watch?v=2Tra0DU1tfM)

对于我们的应用程序,我们实际上只需要一个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.com/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://github.com/ClickHouse/examples/blob/main/blog-examples/llama-index/hacknernews_app/hackbot.py)找到。

图片

几点观察

以上应用程序代表了一个相当简单的RAG流水线,需要非常少的代码。尽管如此,该流水线可能相当脆弱,有多个步骤可能会失败,导致选择错误的分支或未返回答案。我们只对可能问题的微小百分比进行了采样,也没有测试应用程序对多样化问题集的响应能力。

为概念验证或演示构建基于LLM的应用程序与部署健壮可靠的生产环境应用程序是非常不同的。在我们有限的经验中,这非常具有挑战性,特别是流水线的内省。要谨慎对待任何声称相反的博客或内容!从具有固有随机性的事物中获得可预测的行为,需要的不仅仅是几百行Python代码和几个小时的工作。

为了克服这一点,我们认为任何提供对RAG流水线的可观察性的东西都具有巨大的价值,这样可以诊断问题并轻松评估测试集。因此,我们非常鼓励看到LlamaIndex和LangChain最近的产品发展,并期待尝试这些产品。

就我们上面的应用程序而言,欢迎进行实验和改进。有一些明显的改进空间。例如,该应用程序不记忆先前回答过的问题,也不将其视为未来问题的上下文。通过LlamaIndex中的记忆概念,这很容易添加。此外,添加其他年份的调查结果似乎是一个合理的补充。能够询问诸如“2019年与2023年人们对他们最想在其中工作的数据库的看法发生了什么变化?”这样的问题。

结论

我们在支持向量搜索方面的持续投资包括几个并行方案:

  • 进一步提高线性扫描的性能,例如改进距离函数的矢量化

  • 将对近似技术的支持,例如HNSW,从实验性提升至生产

  • 投资于使用向量搜索的更广泛生态系统

在本博客中,我们探讨了属于最后一项的改进 - ClickHouse与LlamaIndex的集成,构建了一个示例应用程序,用于根据Hacker News帖子和Stack Overflow调查结果,回答有关技术的人们意见的问题。

最后,如果您想知道人们对ClickHouse的看法(请注意,我们的帖子仅限于2021年)...

图片

征稿启示

面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:Tracy.Wang@clickhouse.com

图片

​​联系我们

手机号:13910395701

邮箱:Tracy.Wang@clickhouse.com

满足您所有的在线分析列式数据库管理需求

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值