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

Vue+Mpvue+Koa 聊天机器人 vue-mpvue-ChatRobot vue-mpvue-ChatRobot 详细介绍 前端(移动端&小程序)/后端(nodejs) 前端 : Vue + Mpvue(支持移动端与小程序) ; 后端 : koa(使用Koa2开发服务端)  Vue是一套用于构建用户界面的渐进式框架。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,`Vue`... 阅读详情

图片

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

您会注意到我们在(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

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

大数据处理与分析:ClickHouse在大数据场景的优势 1.背景介绍 在大数据处理和分析领域,ClickHouse是一种高性能的列式存储数据库,它在处理大量数据时具有显著的优势。本文将深入探讨ClickHouse在大数据场景的优势,包括背景介绍、核心概念与联系、核心算法原理和具体操作步骤、数学模型公式详细讲解、具体最佳实践、实际应用场景、工具和资源推荐以及总结和未来发展趋势与挑战。 阅读详情

相关推荐

实战指南 | 利用TSMaster的C小程序实时监控DBC报文发送周期

本文提供了一份利用TSMaster的C小程序实时监控DBC报文发送周期的实战指南。通过创建用户变量、编写C小程序核心逻辑并关联图形模块,工程师可以直观地可视化关键报文(如ID 0x64)的发送间隔,从而快速发现周期异常,提升汽车电子开发和测试中总线问题定位的效率与实时性。

weixin_29178069的博客 417

2024年精选推荐的16个向量数据库:提升你的AI应用性能

向量搜索数据库推荐

uncle_ll的博客 4979

机器学习——Anaconda下载安装教程

Anaconda详细下载安装教程

2401_83693485的博客 3065

【RAG实践】基于LlamaIndex和Qwen1.5搭建基于本地知识库的问答机器人

LLM会产生误导性的 “幻觉”,依赖的信息可能过时,处理特定知识时效率不高,缺乏专业领域的深度洞察,同时在推理能力上也有所欠缺。正是在这样的背景下,检索增强生成技术(Retrieval-Augmented Generation,RAG)应时而生,成为 AI 时代的一大趋势。RAG 通过在语言模型生成答案之前,先从广泛的文档数据库中检索相关信息,然后利用这些信息来引导生成过程,极大地提升了内容的准确性和相关性。

机器学习社区 3783

[CTFSHOW]JWT

文章目录前言JWT简介CTFSHOW Web345CTFSHOW Web346前置知识开始做题CTFSHOW Web347CTFSHOW Web348CTFSHOW Web349CTFSHOW Web350前置知识利用WP如何防御 前言 有些师傅向我咨询WP,索性就发了吧,其实早就写好了,只是懒,在github仓库里囤着 JWT简介 可以看看我的Github总结给自己看的,Y4tacker CTFSHOW Web345 首先打开题目,查看源码,接下来访问/admin被跳转回主页啥也没有发生 where is

Y4tacker的博客 6044

基于STM32与FPGA Cyclone IV的FSMC高速数据通信实战解析

本文详细解析了基于STM32的FSMC接口与FPGA Cyclone IV实现高速并行数据通信的实战方案。通过将FPGA模拟为SRAM设备,构建了MCU与FPGA之间的高速数据通道,并深入探讨了硬件连接、STM32软件配置、FPGA逻辑设计及性能优化等关键步骤,为图像处理、高速采集等需要大数据量交换的嵌入式应用提供了稳定高效的通信解决方案。

swift5iosmith的博客 294

用Vue实现小Q聊天机器人(四)

本项目所用的闲聊接口为腾讯开放平台提供,具体可参考: 用Flutter实现小Q聊天机器人(五) 或者官方文档: https://ai.qq.com/doc/nlpchat.shtml 说到前端网络请求,就不得不说跨域问题了,本项目仍然会面临跨域问题而导致无法去直接请求接口,一般的跨域解决办法有这么几种:jsonp,后端允许跨域,代理。由于本项目环境无法使用jsonp,所以就剩下后端允许跨域和代理两种办法了,让腾讯去为我们允许跨域?显然是不可能的,那么就只能用代理了吗?其实还有一种解决办法就是proxy,网上

白玉梁的专栏 3057

项目管理面试常见问题及答案

1. 项目管理软件工具知识,   2. 编制项目计划的技术,   3. 人员管理技能   4. 沟通技能   5. 原理体系知识(标生命周期和项目管理)。 项目管理软件工具知识   问题1:工期和工作量之间的差异是什么?   答案1:工期是商业/日历上的天数,与人数和工作量无关。工作量是与日历天数无关的人的工作。例如:   一天的工作量对于一个一只花50%在时间在上面的人来说,他的工期就是两天。...

okbin1991的博客 2840

SpringBoot+Vue开发的一款聊天机器人,带图详细流程可提供源码

大家好呀!我是爷爷的茶七里香,不知道有没有小伙伴和我一样喜欢听周董的歌呢?今天是五一假期最后一天了,小伙伴们都去哪玩了呢?我趁五一还有两天时间,便动手开发了这个带有聊天页面的机器人,效果如下: 有没有心动呀?想体验下吗?哈哈~ 我已经将项目打包好了,接下来我将教大家如何搭建环境并部署到自己的电脑上体验体验!!! 一、环境搭建 (1)JDK下载 ...

提笔忘字的帝国 4182

LVGL(lv_table)

LVGL中的lv_table是一个轻量级表格控件,用于显示行列式文本数据。它支持设置单元格内容/样式、行列调整、事件响应等基础功能,适用于参数配置、数据展示等场景。通过API可快速创建表格,设置行列数、填充数据,并自定义对齐方式与样式。但需注意默认无滚动条、行数限制等问题。该控件适合简单表格需求,复杂交互需结合自定义事件处理。

m0_49476241的博客 1262

LlamaIndex:轻松构建索引查询本地文档的神器

LlamaIndex(以前称为 GPT Index)是一个开源项目,它在 LLM 和外部数据源(如 API、PDF、SQL 等)之间提供一个简单的接口进行交互。它提了供结构化和非结构化数据的索引,有助于抽象出数据源之间的差异。它可以存储提示工程所需的上下文,处理当上下文窗口过大时的限制,并有助于在查询期间在成本和性能之间进行权衡。向量存储索引:最常用,允许您回答对大型数据集的查询。树索引:对于总结文档集合很有用。列表索引:对于合成一个结合了多个数据源信息的答案很有用。关键字表索引。

FrenzyTechAI的博客 9493

基于YOLOv11的手写数字与符号识别系统设计与实现

手写数字与符号识别系统需要丰富多样的训练数据才能达到理想的泛化性能。MNIST:包含70,000个手写数字样本(0-9),28x28灰度图像优点:干净整齐,适合初学者缺点:多样性不足,只有数字EMNIST:MNIST的扩展,包含数字、字母(大小写)和特殊符号样本量:814,255张图像覆盖62个类别(10数字+26小写字母+26大写字母)HASYv2:专门的手写符号数据集包含369个不同符号类别约168,000个标注实例包括数学符号、箭头、括号等:英文手写文本数据库。

YOLO 291

ClickHouse 用 AgentHouse 向你展示通过 MCP 集成 LLM 的可能性

AgentHouse 是一个非常值得探索的演示环境,快来尝试一下吧!

ClickHouseDB的博客 980

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

AI大模型时代下运维开发探索第二篇:基于大模型(LLM)的数据仓库

一份需求查询SQL,利用LLM生成两份代码,一份用于Pull:直接查询返回结果,预查询调试用;一份用于Push:构建消费链路进实时数仓。避免ETL过程的过度加工:按需加字段,不会加工太多用不到字段浪费计算、浪费存储。降低使用者维护ETL加工过程成本:虽然Flink SQL的可维护性已经很好了,但是面向计算过程的SQL编写方式还是让很多用户不适应。如果直接用查询SQL来进行自动生成,就大大降低了维护的门槛。以查询为驱动的数据模型,可以使得使用者始终面向数据源表进行需求思考。

科技很有意思 3250

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

Langfuse 与 ClickHouse:面向现代大语言模型(LLM)应用的新一代数据栈

ClickHouse 已经成为 Langfuse 架构的中枢,让平台能够伴随社区一起成长。

ClickHouseDB的博客 1228

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

使用ClickHouse构建向量存储

ClickHouse是一个开源的列式数据库管理系统,以其高性能著称。我们可以利用ClickHouse来存储和查询向量化的数据,适用于需要快速检索相似数据的场景。

ppoojjj的博客 1030

总结不同方案实现-LLM数据库查询-即Text2SQL

让我们从定义机器人的工具开始。表列表 - 我们可以将其放在系统提示符中,以便模型对我们拥有的数据有一定的了解,并且不需要每次都为其执行工具表的列列表,以便模型可以理解数据模式表中列的顶部值,以便模型可以查找过滤器的值SQL查询执行的结果才能得到实际的数据。为了在LangChain中定义工具,我们需要使用@tool装饰器来实现函数。我们将使用 Pydantic 指定每个函数的参数模式,以便模型知道要传递给函数的内容。下面的代码定义了三个工具:execute_sql。

wangning0714的博客 4567

为可观测性注入LLM :结合 ClickStack、OpenTelemetry 与 MCP

LLM 可观测性在生产环境尤其关键,尤其是针对智能体应用以及频繁借助 MCP 工具与 LLM 交互的场景。

ClickHouseDB的博客 1236

LangChain - 为何我们选择 ClickHouse 作为 LangSmith 的动力

本文字数:4742;估计阅读时间:12 分钟作者:Ankush Gola审校:庄晓东(魏庄)本文在公众号【ClickHouseInc】首发“我们在使用 ClickHouse 方面有着良好的经历。它使我们能够将 LangSmith 扩展到生产环境,并提供一个用户可以记录所有数据的服务。如果没有 ClickHouse,我们无法取得这样的成就。

ClickHouseDB的博客 1280

ClickHouse走向AI数据栈的技术路线与工程边界

ClickHouse获得3.5亿美元融资,资本认可了列式分析引擎的商业价值,但工程问题更具体:一个为扫描聚合设计的引擎能否同时承载BI报表、可观测性日志与AI特征工程三类负载。文章从列存与向量化执行机制拆起,讨论稀疏索引与分区裁剪的代价模型、MergeTree家族引擎选型、副本与分片拓扑权衡、写入攒批去重与背压、物化视图增量聚合、宽表特征建模、向量检索与标量过滤协同、存算分离成本核算,并复盘六个真实失败案例,给出迁移落地五步法与运维边界建设建议。

搞定过:百亿级数据、万级QPS、零宕机、AI工业化。 367

ClickHouse 数据目录迁移翻车实录:整十分 SIGQUIT 疑云、uuid 撞车双节点互踢与 ZooKeeper 僵尸副本排查(附迁移 SOP)

ClickHouse 两节点集群做数据目录迁移,迁完节点 2 后节点 1 无故 stop,此后两节点随便启动哪个都会被自动终止——退出全部发生在整十分,日志写着 Received termination signal (Quit)。本文完整还原排查:信号溯源五步法(journalctl/strace si_pid/auditd)、整点规律与 ZK 分桶过期的辨析、system.replicas 三板斧取证,最终锁定 rsync 迁移经典坑:/var/lib/clickhouse/uuid 撞车导致双节点互踢

Sayai的专栏 218

ClickHouse 运维排障工具箱:视图创建类型报错、CPU 打满拒绝连接、合并 OOM 与 query_log 查询溯源(附速查表)

ClickHouse 一个集群的多事发日:CREATE VIEW 报 ILLEGAL_TYPE_OF_ARGUMENT(isNaN 只吃浮点,聚合状态要先 sumMerge 再 CAST Float64);CPU 打满开始拒绝连接(SERVER_OVERLOADED 是背压不是宕机,processes/merges/mutations 三表定位元凶);OOM 崩溃堆栈解读(MemoryTracker + MergeTask = 合并内存超限,16G 节点配置基线);毫秒级短查询刷屏用 query_log 三

Sayai的专栏 256

EDB 收购 Altinity,整合 Postgres 与 ClickHouse

EnterpriseDB 于11月5日收购 ClickHouse 服务商 Altinity,交易金额未披露,计划通过共享 Iceberg 目录和统一访问控制整合 Postgres 与 ClickHouse 能力。

老孙正经胡说 244

案例 | 高精度具身时序数据标注:澳鹏如何支撑头部机器人企业模型迭代

具身智能落地面临高质量时序标注数据瓶颈,某头部企业与澳鹏合作,通过多基地协同、双语专家团队、标准化SOP及自研MatrixGo平台,实现5,000+小时高精度具身时序数据标注与10,000+小时审核,攻克双语语义理解、0.1秒级时间戳精度与工具缺失难题,构建可复用的数据闭环体系,显著提升模型泛化能力与数据集质量。

Appen_China的博客 229

智能咖啡机器人项目:机械臂自动化设备如何服务消费场景

咖啡机器人并非简单的机械臂加咖啡机,而是一套涵盖机器人本体、末端执行器、咖啡机通讯、控制系统和安全防护的完整自动化工作站。云汇核心能力在于将这些硬件和软件深度集成,让机械臂能够与各功能单元协同作业,按照订单指令完成标准化操作。整套设备的工作流程高度自动化,覆盖从下单到出杯的完整环节。

2601_95749022的博客 206
上一篇: ClickHouse Grafana插件4.0版 - 升级SQL可观测性
下一篇: MySQL之旅
ClickHouseDB
博客等级 码龄3年 1707粉丝 320原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值