Quivr 实战指南:基于 quivr-core 与 Chainlit 构建文档问答聊天机器人
本文以仓库中的示例文档 backend/core/examples/chatbot/README.md 为主体,完整讲解如何用 Quivr 的 Brain 类和 Chainlit 快速搭建一个"上传文本文件即可提问"的聊天机器人:从环境准备、依赖安装、启动命令,到上传、问答的完整操作流程,并结合 quivr-core 的源码剖析 Brain 从文件构建、向量库默认配置、RAG 流式问答的底层实现链路,帮助你既会跑通示例,又理解每一步背后的机制。
一、示例定位与整体架构
该示例演示的核心链路非常清晰(引自 README):
- Quivr 负责核心智能:将用户上传的文本文件构建成一个 "brain"(大脑),并基于该 brain 回答关于文件内容的问题;
- Chainlit 负责交互层:提供 Web 聊天界面,处理文件上传和消息流式渲染。
对应到实现文件 backend/core/examples/chatbot/main.py,整个示例只有两个异步回调函数:
@cl.on_chat_start:等待用户上传.txt文件,调用Brain.from_files(...)构建 brain,并把 brain 存入 Chainlit 会话(cl.user_session);@cl.on_message:每次用户提问时,取出会话中的 brain,调用brain.ask_streaming(message.content)逐 token 流式输出答案。
也就是说,示例把"文档 → 知识"和"提问 → 答案"这两件事分别收敛到了 Quivr 的两个入口:Brain.from_files 与 Brain.ask_streaming,Chainlit 只是把它们串起来。
二、环境准备与依赖安装
2.1 前置条件
- Python 3.8 或更高版本(README 的官方要求)。需要注意的是,backend/core/pyproject.toml 中 quivr-core 源码实际使用了
typing.Self等较新语法(brain.py 第 5 行from typing import ... Self),从源码结构看,实际运行建议采用 Python 3.11+,以兼容示例所依赖的quivr-core发行版。
2.2 安装依赖
示例目录自带依赖清单 backend/core/examples/chatbot/requirements.txt,内容固定为两行:
quivr-core[base]==0.0.8
chainlit==1.1.306
克隆仓库后进入示例目录执行安装即可:
cd backend/core/examples/chatbot
pip install -r requirements.txt
这里有一个值得注意的细节:安装的是 quivr-core[base] 带 base 额外依赖的版本。为什么必须带 base?查看默认实现 backend/core/quivr_core/brain/brain_defaults.py 可以发现,示例并没有显式传入向量库、嵌入器和 LLM,全靠默认实现兜底,而这三者的默认实现分别来自 LangChain 社区组件:
- 向量库:
build_default_vectordb使用langchain_community.vectorstores.FAISS构建内存向量库(brain_defaults.py#L13-L30); - 嵌入器:
default_embedder使用langchain_openai.OpenAIEmbeddings; - LLM:
default_llm通过LLMEndpoint.from_config(LLMEndpointConfig())构建,即 OpenAI 的ChatOpenAI。
如果导入失败,源码会抛出带明确指引的 ImportError,提示 "Please provide a valid vector store or install quivr-core['base'] package"。这就是 base 扩展依赖存在的意义。
另外,由于默认嵌入器和 LLM 都是 OpenAI 实现,实际运行前需要设置 OPENAI_API_KEY 环境变量,否则构建 brain 时会失败。
三、启动 Chainlit 服务
按照 README 的 "Running the Chatbot" 章节,启动方式只有一条命令:
chainlit run main.py
启动后在浏览器中打开终端提示的地址(通常是 http://localhost:8000)即可看到聊天界面。chainlit run 会加载 main.py 并注册其中的 @cl.on_chat_start 和 @cl.on_message 回调,Chainlit 自身负责把回调中的 cl.Message 更新推送到前端 WebSocket,这也是后续"流式输出"能够生效的原因。
四、使用流程:上传文件到开始问答
README 给出的交互流程("Using the Chatbot" 章节)可以完整映射到 main.py 的源码行为:
-
界面加载后提示上传:
on_chat_start中使用cl.AskFileMessage发送一个文件请求消息,参数直接限定了行为边界——files = await cl.AskFileMessage( content="Please upload a text .txt file to begin!", accept=["text/plain"], # 仅接受纯文本 max_size_mb=20, # 大小上限 20MB timeout=180, # 180 秒超时 ).send()这与 README 中 "select a
.txtfile ... The file size should not exceed 20MB" 的说明一一对应。 -
文件处理:代码先以 UTF-8 读取上传文件,再写入一个
tempfile.NamedTemporaryFile(suffix=".txt",delete=False以便 Quivr 稍后读取),然后把临时文件路径传给Brain.from_files(name="user_brain", file_paths=[temp_file_path])。处理期间会先发一条 "Processing文件名..." 的消息,构建完成后更新为 "Processing ... done. You can now ask questions!"。 -
开始提问:文件就绪后,用户直接在输入框提问。
@cl.on_message回调先检查会话中是否存在 brain(没有则提示 "Please upload a file first."),随后通过ask_streaming逐 chunk 调用msg.stream_token(chunk.answer),实现答案边生成边显示。@cl.on_message async def main(message: cl.Message): brain = cl.user_session.get("brain") # type: 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()
五、How It Works 源码级解析:Brain 是怎么"读懂"文件的
README 的 "How It Works" 一句话概括为 "用 Quivr 库从上传的文本文件创建 brain,再用它回答问题"。下面沿着 Brain 的源码把这个过程展开。
5.1 构建 brain:Brain.from_files 的完整链路
from_files 是同步入口,内部通过事件循环直接运行异步版本 afrom_files(brain.py#L169-L192)。afrom_files 做了四件事(brain.py#L118-L167):
- 补齐默认依赖:
llm为 None 时调用default_llm(),embedder为 None 时调用default_embedder(); - 文件入库:对每个
file_paths调用load_qfile加载为 Quivr 文件对象,再上传到storage(默认是内存型的TransparentStorage); - 解析切块:
process_files按文件扩展名从处理器注册表取出对应 processor,异步解析文件并产出 LangChainDocument列表; - 构建向量库:
vector_db为 None 时调用build_default_vectordb(docs, embedder),即把所有文档用默认 FAISS 索引一次性建库。
关于第 3 步的处理器选择,注册表实现在 backend/core/quivr_core/processor/registry.py:.txt 的默认处理器是 SimpleTxtProcessor(见 registry.py#L32-L47 中 base_processors 对 FileExtension.txt 的映射)。这也解释了为什么示例只要求上传 .txt 文件——它是基础处理器注册表中原生支持的扩展名之一,无需额外依赖即可解析;而 PDF 等则依赖 Tika 等可选处理器。
5.2 回答问题的 RAG 流水线:ask_streaming
ask_streaming 是 brain 的异步流式问答入口(brain.py#L278-L309),内部流程为:
- 根据
rag_config决定使用哪个 LLM(示例未传rag_config,直接使用 brain 自带的默认 LLM); - 构建
QuivrQARAG流水线(backend/core/quivr_core/quivr_rag.py); - 调用
rag_pipeline.answer_astream(question, chat_history, [])逐块产出答案,并把用户问题与完整答案追加到默认聊天历史中,保证多轮上下文。
QuivrQARAG.build_chain 展示了典型的对话式 RAG 结构(quivr_rag.py#L94-L143):
- 历史过滤:
filter_history按 "1 token ≈ 4 字符" 的近似规则裁剪历史,受max_tokens与max_history双重限制(quivr_rag.py#L65-L92); - 问题改写:用
CONDENSE_QUESTION_PROMPT把带上下文的问题改写为独立问题,再做向量检索; - 向量检索:通过
vector_store.as_retriever()从 FAISS 索引召回文档; - 生成答案:结合 context 与
ANSWER_PROMPT调用 LLM;若模型支持函数调用,则绑定cited_answer工具强制输出带引用的结构化答案(quivr_rag.py#L130-L136)。
5.3 默认参数一览:RAGConfig 与 LLMEndpointConfig
示例没有显式配置任何参数,实际生效的是 backend/core/quivr_core/config.py 中的默认值:
| 参数 | 默认值 | 说明 |
|---|---|---|
LLMEndpointConfig.model |
gpt-3.5-turbo-0125 |
默认 LLM 型号,可通过 llm_base_url / llm_api_key 指向兼容端点 |
LLMEndpointConfig.max_input |
2000 |
输入 token 上限 |
LLMEndpointConfig.max_tokens |
2000 |
输出/历史预算上限,也用于 filter_history 的历史裁剪 |
LLMEndpointConfig.temperature |
0.7 |
采样温度 |
LLMEndpointConfig.streaming |
True |
启用流式输出,ask_streaming 依赖此特性 |
RAGConfig.max_history |
10 |
保留的历史轮数(一问一答为一对) |
RAGConfig.max_files |
20 |
回答时引用的文件列表上限 |
RAGConfig.prompt |
None |
可追加的自定义指令 |
在示例中可以通过给 Brain.from_files(...) 显式传入 llm、embedder、vector_db 来替换任意默认组件;brain.ask_streaming 也接受可选的 rag_config,允许在提问时临时切换模型(见 brain.py#L278-L290 中的覆盖逻辑)。
六、最小可运行参照:simple_question.py
如果想脱离 Web 界面验证同一套核心链路,仓库提供了更精简的脚本 backend/core/examples/simple_question.py:
import tempfile
from quivr_core import Brain
if __name__ == "__main__":
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt") as temp_file:
temp_file.write("Gold is metal.")
temp_file.flush()
brain = Brain.from_files(name="test_brain", file_paths=[temp_file.name])
answer = brain.ask("Property of gold?")
print("answer :", answer.answer)
print("brain information: ", brain)
它只用了 Brain.from_files 和同步版 brain.ask(非流式),与聊天机器人示例形成对照:一个是"文件 → 向量库 → 检索 → 流式生成"的完整交互形态,另一个是同一 API 的最小调用形态。
七、运行前提与限制说明
- OpenAI API Key 必需:默认 LLM 与嵌入器均基于 OpenAI(brain_defaults.py#L33-L55),运行示例前需配置
OPENAI_API_KEY; - 仅支持
.txt且 ≤ 20MB:由AskFileMessage的accept=["text/plain"]与max_size_mb=20决定(main.py#L13-L18); - 内存向量库:默认 FAISS 索引保存在进程内存中,brain 生命周期与 Chainlit 会话一致,服务重启后需重新上传文件建 brain;
- 上传超时:等待用户上传的超时时间为 180 秒,超时后需要刷新会话重新发起。
综上,这个示例用最少的代码展示了 Quivr 的核心用法:Brain.from_files 负责"把文件变成知识",ask / ask_streaming 负责"从知识中取答案",而 Chainlit 只承担界面职责——这正是 Quivr "Focus on your product rather than the RAG" 理念的体现:交互层可以随意替换,RAG 内核保持稳定且可配置。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00