首页
/ Quivr Core + Chainlit 实战:用几十行代码搭建本地文档问答 Chatbot

Quivr Core + Chainlit 实战:用几十行代码搭建本地文档问答 Chatbot

2026-09-05 17:53:46作者:傅爽业Veleda

本篇指南基于仓库中 chainlit.mdmain.py 示例,讲解如何用 quivr-core 库和 Chainlit 快速搭一个"上传文档即可问答"的 RAG Chatbot。读完后你将掌握完整的安装与运行步骤,并能读懂示例背后 Brain 的建库流程、默认的向量库/Embedding/LLM 组件,以及 QuivrQARAG 的流式回答链路,从而把这个最小示例迁移到自己的产品集成场景中。

示例定位与整体结构

该示例位于 backend/core/examples/chatbot 目录,是一个面向 quivr-core 库(而非完整 Quivr 后端服务)的独立小项目,仅包含四个文件:

整体思路是:用户通过 Chainlit 提供的 Web 界面上传一个 .txt 文件,程序用 quivr_core.Brain.from_files 把文件解析、切块、向量化并构建内存向量库(即"大脑"),之后所有提问都通过该大脑做 RAG 检索 + 生成,回答以流式 token 形式返回。

环境要求与安装

前置条件:Python 3.8 及以上(文档明确标注)。

进入示例目录并安装依赖:

cd backend/core/examples/chatbot
pip install -r requirements.txt

当前仓库的 requirements.txt 锁定为两个精确版本:

quivr-core[base]==0.0.8
chainlit==1.1.306

这里有两个值得注意的点:

  1. quivr-core[base]base 额外依赖不是可选项。从 brain_defaults.py 的源码可以看到,默认的向量库(FAISS)、默认 Embedder(OpenAIEmbeddings)和默认 LLM(ChatOpenAI)都通过 try: from langchain_community... / from langchain_openai... 动态导入,缺包时会抛出 ImportError,并提示 "Please provide a valid vector store or install quivr-core['base'] package"。也就是说,不装 base 组件就必须自己传 vector_db / embedder / llm 参数,否则建脑直接失败。
  2. 示例锁定的是 PyPI 上的 0.0.8 版本,与仓库 backend/core 目录中的最新源码可能存在 API 差异(例如当前源码中 Brain 的构造签名、ask_streaming 的返回结构)。本文的源码分析以仓库当前源码为准,作为理解机制的依据;若以 0.0.8 版本运行,行为细节以该版本为准。

由于默认 Embedder 与 LLM 都是 OpenAI 组件,运行前需要设置 OPENAI_API_KEY 环境变量(LLMEndpointConfig.llm_api_key 为空时,ChatOpenAI 会回退读取该环境变量)。

运行与使用流程

启动 Chainlit 服务:

chainlit run main.py

然后打开终端显示的地址(通常是 http://localhost:8000)。使用步骤与文档一致:

  1. 界面加载后,机器人会提示你上传一个 .txt 文本文件;
  2. 在上传区选择一个 .txt 文件,大小不得超过 20MB(该限制来自代码中的 max_size_mb=20);
  3. 上传完成后,机器人会先发送 "Processing ..." 消息,处理完成(文件解析 + 向量化 + 建库)后更新为 "You can now ask questions!";
  4. 之后在输入框提问即可,回答基于上传文件内容生成。

源码走读:main.py 的两个生命周期钩子

Chainlit 应用的全部逻辑由两个装饰器函数构成。

on_chat_start:等待上传并建脑

main.py 中的关键片段:

@cl.on_chat_start
async def on_chat_start():
    files = None

    # Wait for the user to upload a file
    while files is None:
        files = await cl.AskFileMessage(
            content="Please upload a text .txt file to begin!",
            accept=["text/plain"],
            max_size_mb=20,
            timeout=180,
        ).send()

    file = files[0]
    ...
    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".txt", delete=False
    ) as temp_file:
        temp_file.write(text)
        ...
    brain = Brain.from_files(name="user_brain", file_paths=[temp_file_path])
    cl.user_session.set("brain", brain)

几个设计细节:

  • cl.AskFileMessage 的参数与文档中的使用规则一一对应:accept=["text/plain"] 决定只接受纯文本,max_size_mb=20 就是文档所说的 20MB 上限,timeout=180 给上传预留 3 分钟;
  • Chainlit 上传的文件对象自带 path,代码按 UTF-8 读取全文后重新写入一个 delete=False 的临时文件再交给 Brain.from_files。这样做的效果是:传给大脑的文件始终是一个带 .txt 后缀的磁盘文件(suffix=".txt"),保证后续的文件处理器能按扩展名路由;
  • cl.user_session.set("brain", brain) 把 Brain 实例挂到会话上,实现"每个浏览器会话独立一份大脑"——两个不同用户分别上传不同文件时互不干扰;
  • 处理前后复用同一个 cl.Message,通过 msg.update() 把 "Processing" 就地更新为 "done",避免聊天流里堆两条系统消息。

on_message:流式问答

@cl.on_message
async def main(message: cl.Message):
    brain = cl.user_session.get("brain")
    if brain is None:
        await cl.Message(content="Please upload a file first.").send()
        return

    msg = cl.Message(content="")
    await msg.send()

    async for chunk in brain.ask_streaming(message.content):
        await msg.stream_token(chunk.answer)

    await msg.send()

这里 brain.ask_streaming 返回一个异步生成器,每块是带 answer 字段的 ParsedRAGChunkResponse;Chainlit 侧用 msg.stream_token 逐块追加,实现打字机式的流式输出。如果用户在未上传文件时直接提问,会话里取不到 brain,会收到 "Please upload a file first." 的引导提示。

Brain 建脑管线:from_files 内部做了什么

Brain.from_files(name="user_brain", file_paths=[...]) 只是入口。从 brain.py 的源码看,同步版 from_files(L169-L192)内部通过事件循环调用异步版 afrom_files,后者按四步完成建脑:

  1. 文件入库:对每个路径调用 load_qfile(brain_id, path) 生成文件实体,再 storage.upload_file(file) 存入 storage(默认是 TransparentStorage,即不落盘、仅内存中转);
  2. 解析成 Documentprocess_files 按文件扩展名从处理器注册表 get_processor_class(file_extension) 取出对应的 processor(txt 走文本处理器),输出 LangChain Document 列表(即切块后的知识块);
  3. 构建向量库:未显式传入 vector_db 时调用 build_default_vectordb(docs, embedder),用 FAISS 一次性 afrom_documents 完成 embedding 与索引;
  4. 组装 Brain:绑定 llmembeddervector_dbstorage,并初始化一个默认 ChatHistory 会话——后续所有问答都挂在这个默认会话上,多轮上下文由此维护。

也就是说,文档里"chatbot uses the Quivr library to create a 'brain' from the uploaded text file"这句话的完整含义是:文本解析 → 分块 → 向量化 → FAISS 内存索引 → 绑定 LLM 与会话历史

默认组件与可调参数

brain_defaults.py 定义了三项默认值:

组件 默认实现 源码位置
向量库 FAISS(CPU,FAISS.afrom_documents build_default_vectordb
Embedder OpenAIEmbeddings default_embedder
LLM LLMEndpoint.from_config(LLMEndpointConfig()),即 ChatOpenAI default_llm

LLM 的默认参数在 config.py 中:

class LLMEndpointConfig(BaseModel):
    model: str = "gpt-3.5-turbo-0125"
    llm_base_url: str | None = None
    llm_api_key: str | None = None
    max_input: int = 2000
    max_tokens: int = 2000
    temperature: float = 0.7
    streaming: bool = True

llm_base_url 可指向任意 OpenAI 兼容端点(本地 Ollama、Groq 等),这是替换模型的最小改动点。RAG 侧还有 RAGConfigmax_history=10(最多回看 10 轮对话)、max_files=20prompt(可选的系统提示/角色设定)。示例代码没有显式传 rag_configask_streaming 会用 Brain 自身 LLM 的配置构造默认的 RAGConfig(见 brain.py)。

回答链路:QuivrQARAG 的流式 RAG

每次提问时,Brain.ask_streaming 会构造 QuivrQARAG 管道,build_chain(L94-L143)串起了四个环节:

  1. 历史裁剪 filter_history:按"1 token ≈ 4 字符"的粗估和 max_history 上限,只保留最近的若干轮 Human/AI 消息对,控制上下文长度;
  2. 问题改写 standalone_question:用 CONDENSE_QUESTION_PROMPT 把带指代/省略的追问改写成独立问题(例如"它多少钱" → "XX 多少钱"),再拿去检索;
  3. 检索:改写后的问题经过 ContextualCompressionRetriever 从 FAISS 向量库取文档(retriever 属性即 vector_store.as_retriever());
  4. 生成:把检索上下文拼进 ANSWER_PROMPT 交给 LLM;若模型支持函数调用(supports_func_calling()),会绑定 cited_answer 工具强制"先引用来源再回答"(tool_choice="any")。

流式版本 answer_astream(L165-L234)对该 chain 做 astream:支持函数调用的模型会做增量 diff 只 yield 新增部分,最后再补一个 last_chunk=True 的块携带元数据。Brain.ask_streamingbrain.py)把这些块逐个 yield 给 Chainlit,同时把完整的 Human/AI 消息追加进 default_chat,供下一轮 filter_history 使用——这就是示例能进行多轮对话的原因。

适用前提与限制

  • 模型与密钥:默认链路(OpenAI Embeddings + ChatOpenAI)要求 OPENAI_API_KEY;换本地模型时需在 Brain.from_files 中显式传入自定义 llm/embedder,因为默认的 default_llm()/default_embedder() 只认 OpenAI 兼容客户端;
  • 文件类型:Chainlit 端 accept=["text/plain"] + 临时文件后缀 .txt 决定了该示例面向纯文本;其他格式需要放宽这两处并依赖对应 processor;
  • 规模:FAISS 向量库与 TransparentStorage 都在进程内存中,脑随会话结束而销毁,适合单文件、中小语料的演示与集成原型,不适合持久化多文档服务;
  • 版本:示例依赖锁定 quivr-core==0.0.8,与仓库当前源码的 API 细节可能有差异,以你实际安装的版本行为为准。

小结

这个示例的价值在于用最少的代码展示了 quivr-core 的完整 RAG 闭环:Chainlit 负责交互(AskFileMessage 上传、stream_token 流式输出),Brain 负责解析、向量库与多轮会话,QuivrQARAG 负责"改写问题 → 检索 → 引用式生成"。把 main.py 中的 Brain.from_files 换成你自己的文件列表或自定义 llm/vector_db,就能得到一个可直接嵌入产品的文档问答基线。

登录后查看全文
热门项目推荐
相关项目推荐