当前位置: 首页 > news >正文

从零开始构建RAG(检索增强生成)

本文将带你一步步、完全从零开始构建一个完整的RAG(Retrieval-Augmented Generation)系统。

  1. 🚫 不使用高级框架:本文不会使用 LangChainLlamaIndex 等库。相反,我们将使用基础的 requestsnumpy 库来手动实现每个核心组件,以深入理解其工作原理。
  2. 🤖 使用 Ollama:本文将利用本地运行的 Ollama 服务来调用开源模型,完成文本的向量化(Embedding)和最终的文本生成(Generation)。
  3. 🧩 自定义分块:本文将实现一个文本分块(Chunking)函数,它会结合段落边界和设定的块大小,并支持文本重叠(Overlap),以保证上下文的连贯性。

RAG 的核心流程

本文将实现以下四个核心步骤:

  1. 📖 加载与分块 (Load & Chunk): 读取原始文本,并将其智能地分割成小块。
  2. 🔢 向量化与索引 (Embed & Index): 将每个文本块转换为向量,并构建一个简单的内存向量数据库。
  3. 🔍 检索 (Retrieve): 将用户问题向量化,并在数据库中查找最相关的文本块。
  4. ✍️ 增强与生成 (Augment & Generate): 将检索到的信息作为上下文,与原始问题一起提供给大语言模型,生成最终答案。

准备工作:环境设置

在开始之前,请确保已经完成了以下准备工作:

1. 安装和运行 Ollama

请从 Ollama 官网 下载并安装适合你操作系统的版本。安装后,请确保Ollama服务正在后台运行。

2. 下载所需模型

我们将需要一个向量模型和一个生成模型。打开你的终端或命令行,运行以下命令:

# 下载一个高质量的中英双语向量模型
ollama pull bge-m3# 下载一个性能优秀的生成模型(通义千问 7B)
ollama pull qwen3:4b

3. 安装 Python 库

我们只需要两个基础库:requests 用于API通信,numpy 用于向量计算。

pip install requests numpy

第一步:配置与数据准备

首先,我们定义一些全局变量,包括Ollama的API地址、要使用的模型名称,以及我们将要用作知识库的示例文本。

import requests
import json
import numpy as np# --- 配置部分 ---
OLLAMA_API_BASE_URL = "http://localhost:11434/api"
EMBEDDING_MODEL = "bge-m3"
GENERATION_MODEL = "qwen3:4b"# --- 示例文本:我们的知识库 ---
# 生成两个不同领域的文档
DOCUMENT_RAG = """
检索增强生成(Retrieval-Augmented Generation, RAG)是一种先进的人工智能框架,旨在通过融合外部知识库来显著提升大型语言模型(LLM)的性能。它有效地将LLM强大的参数化知识(即在训练数据中学到的知识)与可动态访问的非参数化知识(即外部知识库)相结合,从而解决LLM面临的知识过时、事实性不足(幻觉)以及缺乏领域专业性的核心挑战。RAG的核心工作流程可以分为两个主要阶段:离线索引(Indexing)和在线检索与生成(Retrieval & Generation)。离线索引阶段是数据准备的过程。首先,系统加载原始数据源,这些数据源可以是多种格式,如PDF文档、网页、数据库记录或纯文本文件。接着,这些长文档被“分块”(Chunking),即分割成更小的、语义完整的文本块。分块策略至关重要,好的策略(如按段落分割并设置重叠)能确保检索的上下文既完整又连贯。随后,每个文本块都被送入一个“向量化模型”(Embedding Model),转换成一个高维的数字向量。这些向量捕捉了文本的深层语义含义。最后,所有的文本块及其对应的向量被存储在一个专门的“向量数据库”(Vector Database)中,以便进行快速相似性搜索。在线阶段在用户提出问题时启动。第一步,用户的查询会使用与索引阶段相同的向量化模型进行向量化。第二步,系统使用这个查询向量,在向量数据库中执行“相似性搜索”(通常是余弦相似度计算),以找出与问题语义最相关的top-k个文本块。这些被找回的文本块构成了“上下文”(Context)。第三步,系统将检索到的上下文与用户的原始问题一起,“增强”成一个结构化的提示(Augmented Prompt)。这个提示明确指示LLM根据提供的上下文来回答问题。最后,这个增强后的提示被发送给LLM,生成一个基于可靠、相关信息的最终答案。评估一个RAG系统比评估单独的LLM要复杂,需要从两个维度进行。检索器(Retriever)的评估指标主要包括“上下文精确率”(Context Precision),即检索到的上下文有多少是真正相关的;以及“上下文召回率”(Context Recall),即所有相关信息中有多少被成功检索出来。生成器(Generator)的评估指标则包括“忠实度”(Faithfulness),衡量答案是否严格基于所提供的上下文,没有捏造信息;以及“答案相关性”(Answer Relevancy),评估答案是否直接且充分地回应了用户的问题。像RAGAs这样的开源框架就是专门用于进行此类综合评估的。RAG技术并非一成不变,其前沿发展正探索更高级的范式。例如,“混合搜索”(Hybrid Search)结合了向量的语义搜索和传统关键词搜索(如BM25)的优点,以提高检索的鲁棒性。“重新排序”(Re-ranking)则在初步检索后,使用一个更强大的(但更慢的)交叉编码器模型对候选文档进行二次排序,以进一步提升上下文的质量。更进一步,“查询转换”(Query Transformation)技术,如HyDE,会让LLM先生成一个假设性的答案,再用这个假设答案的向量去检索,有时能获得更好的效果。未来的RAG将朝着自适应、多模态(能检索图像、音频)的方向发展,使其成为更强大、更通用的AI应用基础架构。
"""DOCUMENT_COFFEE = """
咖啡,这种深色、芳香的饮品,是全球数十亿人日常生活中不可或缺的一部分。它不仅仅是一种提神醒脑的工具,更是一种文化符号、一种社交媒介和一门复杂的农艺科学。它的故事始于非洲的偏远山区,如今已遍及全球每个角落,其旅程充满了传奇、商业竞争和文化交融。关于咖啡的起源,流传最广的是埃塞俄比亚牧羊人卡尔迪的传说。据说在公元9世纪,卡尔迪发现他的山羊在吃了一种红色浆果后变得异常兴奋、活蹦乱跳。出于好奇,他亲自尝试了这些浆果,并感受到了前所未有的精力。他将这一发现告诉了当地修道院的僧侣,僧侣们发现这种浆果可以帮助他们在漫长的祈祷中保持清醒,咖啡的提神功效由此被发现。咖啡的种植和贸易始于阿拉伯半岛。在15世纪,也门的苏菲派教徒广泛饮用咖啡以在宗教仪式中保持警觉。也门的港口城市摩卡(Mocha)成为世界上第一个咖啡贸易中心,也门人对咖啡的种植技术严加保密,禁止任何未经处理的咖啡豆出口,以维持其垄断地位。正是在这里,咖啡馆(qahveh khaneh)首次出现,并迅速成为人们交流思想、下棋和听音乐的社交中心。在17世纪,咖啡通过威尼斯商人的贸易路线传入欧洲,并迅速风靡。起初,它因其外来背景和独特的苦味而受到一些保守人士的抵制,甚至被称为“撒旦的苦涩发明”。然而,当教皇克莱蒙八世亲自品尝并为其“洗礼”后,咖啡在欧洲的流行便势不可挡。伦敦、巴黎、维也纳等大城市涌现出成千上万家咖啡馆,它们成为商人、艺术家和知识分子的聚集地,被誉为“便士大学”,因为只需花一便士买杯咖啡,就能听到各种最新的思想和辩论。随着需求的增长,欧洲列强开始寻求在自己的殖民地种植咖啡,以打破阿拉伯的垄断。荷兰人率先在印度的马拉巴尔,以及后来的爪哇和苏门答腊成功种植咖啡。不久之后,法国人将咖啡带到了加勒比海的马提尼克岛。一段富有戏剧性的故事讲述了一位法国海军军官加布里埃尔·德·克利,他用自己的饮用水份额来浇灌一棵咖啡树苗,并成功将其带到了美洲。这棵树苗最终成为中美洲和南美洲数百万棵咖啡树的祖先。现代咖啡文化通常被划分为三个“浪潮”。第一波浪潮是咖啡的大众化,始于20世纪初,重点是让咖啡走进千家万户,代表是真空包装的速溶咖啡和罐装咖啡粉。第二波浪潮始于20世纪末,以星巴克为代表,强调咖啡的体验和品质,意式浓缩咖啡(Espresso)及其衍生饮品(如拿铁、卡布奇诺)成为主流。第三波浪潮是当前的趋势,它将咖啡视为一种像葡萄酒一样的“精品”农产品。这一浪潮强调咖啡豆的“单一产地”(Single Origin),关注风土、海拔、处理方法(如日晒、水洗)对风味的影响。手冲、虹吸、爱乐压等精细的冲煮方法被广泛采用,旨在最大程度地展现每一款咖啡豆独特的风味特征。从埃塞俄比亚山区的红色浆果到一杯精心冲煮的单一产地手冲咖啡,咖啡的旅程仍在继续,不断演变和丰富着人类的味觉与文化。
"""

第二步:自定义文本分块 (Custom Chunking)

为什么需要分块?
LLM的上下文窗口是有限的,我们不能把整篇文档都塞给它。同时,为了进行有效的向量检索,我们需要将文档分割成更小、语义集中的单元。好的分块策略是RAG系统性能的关键之一。

我们的自定义分块策略:
这个 custom_chunker 函数实现了比简单按固定长度切分更智能的逻辑:

  1. 尊重段落:它首先以 \n\n (空行) 为分隔符,将文本分割成段落。这保留了文本的自然结构,避免了从一句话中间切断的尴尬情况。
  2. 组合段落:然后,它会尝试将多个连续的段落组合在一起,直到它们的总长度接近我们设定的 chunk_size
  3. 重叠 (Overlap):当一个块创建完成后,下一个块会从上一个块的末尾回退 chunk_overlap 个字符开始。这确保了块与块之间的内容有平滑的过渡,避免了关键信息恰好在块边界被切分而丢失的问题。
def custom_chunker(text: str, chunk_size: int, chunk_overlap: int) -> list[str]:"""自定义文本分块函数。该函数首先按段落分割文本,然后将段落组合成接近`chunk_size`的块,并考虑`chunk_overlap`。Args:text (str): 需要分块的原始文本。chunk_size (int): 每个块的目标大小(以字符计)。chunk_overlap (int): 块之间的重叠大小(以字符计)。Returns:list[str]: 分割后的文本块列表。"""# 1. 按段落分割,并过滤掉空段落paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]if not paragraphs:return []chunks = []current_chunk = ""for i, paragraph in enumerate(paragraphs):# 如果当前块加上新段落超过了chunk_size,并且当前块不为空if len(current_chunk) + len(paragraph) + 2 > chunk_size and current_chunk:chunks.append(current_chunk)# 创建重叠部分overlap_start_index = max(0, len(current_chunk) - chunk_overlap)current_chunk = current_chunk[overlap_start_index:]# 将段落添加到当前块# 如果当前块非空,则在段落间添加换行符以保持格式if current_chunk:current_chunk += "\n\n" + paragraphelse:current_chunk = paragraph# 添加最后一个块if current_chunk:chunks.append(current_chunk)return chunks

现在,我们来实际运行一下分块函数,看看效果。

print("--- 正在对文档进行分块 ---")
# 设置块大小为400个字符,重叠100个字符
chunks_rag = custom_chunker(DOCUMENT_RAG, chunk_size=400, chunk_overlap=100)
chunks_coffee = custom_chunker(DOCUMENT_COFFEE, chunk_size=400, chunk_overlap=100)
chunks = chunks_rag + chunks_coffee
print(f"所有文档被分成了 {len(chunks)} 个块。\n")for i, chunk in enumerate(chunks):print(f"--- Chunk {i+1} (长度: {len(chunk)}) ---")print(chunk)print("\n")
--- 正在对文档进行分块 ---
所有文档被分成了 12 个块。--- Chunk 1 (长度: 251) ---
检索增强生成(Retrieval-Augmented Generation, RAG)是一种先进的人工智能框架,旨在通过融合外部知识库来显著提升大型语言模型(LLM)的性能。它有效地将LLM强大的参数化知识(即在训练数据中学到的知识)与可动态访问的非参数化知识(即外部知识库)相结合,从而解决LLM面临的知识过时、事实性不足(幻觉)以及缺乏领域专业性的核心挑战。RAG的核心工作流程可以分为两个主要阶段:离线索引(Indexing)和在线检索与生成(Retrieval & Generation)。--- Chunk 2 (长度: 380) ---
的知识过时、事实性不足(幻觉)以及缺乏领域专业性的核心挑战。RAG的核心工作流程可以分为两个主要阶段:离线索引(Indexing)和在线检索与生成(Retrieval & Generation)。离线索引阶段是数据准备的过程。首先,系统加载原始数据源,这些数据源可以是多种格式,如PDF文档、网页、数据库记录或纯文本文件。接着,这些长文档被“分块”(Chunking),即分割成更小的、语义完整的文本块。分块策略至关重要,好的策略(如按段落分割并设置重叠)能确保检索的上下文既完整又连贯。随后,每个文本块都被送入一个“向量化模型”(Embedding Model),转换成一个高维的数字向量。这些向量捕捉了文本的深层语义含义。最后,所有的文本块及其对应的向量被存储在一个专门的“向量数据库”(Vector Database)中,以便进行快速相似性搜索。--- Chunk 3 (长度: 361) ---
Model),转换成一个高维的数字向量。这些向量捕捉了文本的深层语义含义。最后,所有的文本块及其对应的向量被存储在一个专门的“向量数据库”(Vector Database)中,以便进行快速相似性搜索。在线阶段在用户提出问题时启动。第一步,用户的查询会使用与索引阶段相同的向量化模型进行向量化。第二步,系统使用这个查询向量,在向量数据库中执行“相似性搜索”(通常是余弦相似度计算),以找出与问题语义最相关的top-k个文本块。这些被找回的文本块构成了“上下文”(Context)。第三步,系统将检索到的上下文与用户的原始问题一起,“增强”成一个结构化的提示(Augmented Prompt)。这个提示明确指示LLM根据提供的上下文来回答问题。最后,这个增强后的提示被发送给LLM,生成一个基于可靠、相关信息的最终答案。--- Chunk 4 (长度: 394) ---
问题一起,“增强”成一个结构化的提示(Augmented Prompt)。这个提示明确指示LLM根据提供的上下文来回答问题。最后,这个增强后的提示被发送给LLM,生成一个基于可靠、相关信息的最终答案。评估一个RAG系统比评估单独的LLM要复杂,需要从两个维度进行。检索器(Retriever)的评估指标主要包括“上下文精确率”(Context Precision),即检索到的上下文有多少是真正相关的;以及“上下文召回率”(Context Recall),即所有相关信息中有多少被成功检索出来。生成器(Generator)的评估指标则包括“忠实度”(Faithfulness),衡量答案是否严格基于所提供的上下文,没有捏造信息;以及“答案相关性”(Answer Relevancy),评估答案是否直接且充分地回应了用户的问题。像RAGAs这样的开源框架就是专门用于进行此类综合评估的。--- Chunk 5 (长度: 108) ---
案是否严格基于所提供的上下文,没有捏造信息;以及“答案相关性”(Answer Relevancy),评估答案是否直接且充分地回应了用户的问题。像RAGAs这样的开源框架就是专门用于进行此类综合评估的。RAG技术并--- Chunk 6 (长度: 396) ---
提供的上下文,没有捏造信息;以及“答案相关性”(Answer Relevancy),评估答案是否直接且充分地回应了用户的问题。像RAGAs这样的开源框架就是专门用于进行此类综合评估的。RAG技术并非一成不变,其前沿发展正探索更高级的范式。例如,“混合搜索”(Hybrid Search)结合了向量的语义搜索和传统关键词搜索(如BM25)的优点,以提高检索的鲁棒性。“重新排序”(Re-ranking)则在初步检索后,使用一个更强大的(但更慢的)交叉编码器模型对候选文档进行二次排序,以进一步提升上下文的质量。更进一步,“查询转换”(Query Transformation)技术,如HyDE,会让LLM先生成一个假设性的答案,再用这个假设答案的向量去检索,有时能获得更好的效果。未来的RAG将朝着自适应、多模态(能检索图像、音频)的方向发展,使其成为更强大、更通用的AI应用基础架构。--- Chunk 7 (长度: 279) ---
咖啡,这种深色、芳香的饮品,是全球数十亿人日常生活中不可或缺的一部分。它不仅仅是一种提神醒脑的工具,更是一种文化符号、一种社交媒介和一门复杂的农艺科学。它的故事始于非洲的偏远山区,如今已遍及全球每个角落,其旅程充满了传奇、商业竞争和文化交融。关于咖啡的起源,流传最广的是埃塞俄比亚牧羊人卡尔迪的传说。据说在公元9世纪,卡尔迪发现他的山羊在吃了一种红色浆果后变得异常兴奋、活蹦乱跳。出于好奇,他亲自尝试了这些浆果,并感受到了前所未有的精力。他将这一发现告诉了当地修道院的僧侣,僧侣们发现这种浆果可以帮助他们在漫长的祈祷中保持清醒,咖啡的提神功效由此被发现。--- Chunk 8 (长度: 274) ---
后变得异常兴奋、活蹦乱跳。出于好奇,他亲自尝试了这些浆果,并感受到了前所未有的精力。他将这一发现告诉了当地修道院的僧侣,僧侣们发现这种浆果可以帮助他们在漫长的祈祷中保持清醒,咖啡的提神功效由此被发现。咖啡的种植和贸易始于阿拉伯半岛。在15世纪,也门的苏菲派教徒广泛饮用咖啡以在宗教仪式中保持警觉。也门的港口城市摩卡(Mocha)成为世界上第一个咖啡贸易中心,也门人对咖啡的种植技术严加保密,禁止任何未经处理的咖啡豆出口,以维持其垄断地位。正是在这里,咖啡馆(qahveh khaneh)首次出现,并迅速成为人们交流思想、下棋和听音乐的社交中心。--- Chunk 9 (长度: 298) ---
咖啡贸易中心,也门人对咖啡的种植技术严加保密,禁止任何未经处理的咖啡豆出口,以维持其垄断地位。正是在这里,咖啡馆(qahveh khaneh)首次出现,并迅速成为人们交流思想、下棋和听音乐的社交中心。在17世纪,咖啡通过威尼斯商人的贸易路线传入欧洲,并迅速风靡。起初,它因其外来背景和独特的苦味而受到一些保守人士的抵制,甚至被称为“撒旦的苦涩发明”。然而,当教皇克莱蒙八世亲自品尝并为其“洗礼”后,咖啡在欧洲的流行便势不可挡。伦敦、巴黎、维也纳等大城市涌现出成千上万家咖啡馆,它们成为商人、艺术家和知识分子的聚集地,被誉为“便士大学”,因为只需花一便士买杯咖啡,就能听到各种最新的思想和辩论。--- Chunk 10 (长度: 287) ---
”后,咖啡在欧洲的流行便势不可挡。伦敦、巴黎、维也纳等大城市涌现出成千上万家咖啡馆,它们成为商人、艺术家和知识分子的聚集地,被誉为“便士大学”,因为只需花一便士买杯咖啡,就能听到各种最新的思想和辩论。随着需求的增长,欧洲列强开始寻求在自己的殖民地种植咖啡,以打破阿拉伯的垄断。荷兰人率先在印度的马拉巴尔,以及后来的爪哇和苏门答腊成功种植咖啡。不久之后,法国人将咖啡带到了加勒比海的马提尼克岛。一段富有戏剧性的故事讲述了一位法国海军军官加布里埃尔·德·克利,他用自己的饮用水份额来浇灌一棵咖啡树苗,并成功将其带到了美洲。这棵树苗最终成为中美洲和南美洲数百万棵咖啡树的祖先。--- Chunk 11 (长度: 243) ---
加勒比海的马提尼克岛。一段富有戏剧性的故事讲述了一位法国海军军官加布里埃尔·德·克利,他用自己的饮用水份额来浇灌一棵咖啡树苗,并成功将其带到了美洲。这棵树苗最终成为中美洲和南美洲数百万棵咖啡树的祖先。现代咖啡文化通常被划分为三个“浪潮”。第一波浪潮是咖啡的大众化,始于20世纪初,重点是让咖啡走进千家万户,代表是真空包装的速溶咖啡和罐装咖啡粉。第二波浪潮始于20世纪末,以星巴克为代表,强调咖啡的体验和品质,意式浓缩咖啡(Espresso)及其衍生饮品(如拿铁、卡布奇诺)成为主流。--- Chunk 12 (长度: 299) ---
点是让咖啡走进千家万户,代表是真空包装的速溶咖啡和罐装咖啡粉。第二波浪潮始于20世纪末,以星巴克为代表,强调咖啡的体验和品质,意式浓缩咖啡(Espresso)及其衍生饮品(如拿铁、卡布奇诺)成为主流。第三波浪潮是当前的趋势,它将咖啡视为一种像葡萄酒一样的“精品”农产品。这一浪潮强调咖啡豆的“单一产地”(Single Origin),关注风土、海拔、处理方法(如日晒、水洗)对风味的影响。手冲、虹吸、爱乐压等精细的冲煮方法被广泛采用,旨在最大程度地展现每一款咖啡豆独特的风味特征。从埃塞俄比亚山区的红色浆果到一杯精心冲煮的单一产地手冲咖啡,咖啡的旅程仍在继续,不断演变和丰富着人类的味觉与文化。

第三步:向量化与索引 (Embedding & Indexing)

什么是向量化 (Embedding)?
向量化是将文本(或其他数据)转换成一串数字(即向量)的过程。这些向量能够捕捉文本的语义含义。语义上相似的文本,其向量在多维空间中的距离也更近。

我们的“向量数据库”
为了从零开始,我们不使用像 ChromaDB 或 FAISS 这样的专业向量数据库。我们将构建一个非常简单的内存数据库,它就是一个Python列表,列表中的每个元素都是一个字典,包含:

  • text: 原始的文本块内容。
  • vector: 该文本块经过Ollama模型计算出的Numpy向量。
def get_embedding(text: str, model: str) -> list[float]:"""使用Ollama API为文本生成向量嵌入。"""try:response = requests.post(f"{OLLAMA_API_BASE_URL}/embeddings",json={"model": model, "prompt": text})response.raise_for_status() # 如果请求失败则抛出异常return response.json()["embedding"]except requests.exceptions.RequestException as e:print(f"Error getting embedding: {e}")return Nonedef create_vector_store(chunks: list[str], embedding_model: str) -> list[dict]:"""创建向量数据库。为每个文本块生成向量,并将它们与原文一起存储。"""vector_store = []print("正在创建向量数据库...")for i, chunk in enumerate(chunks):print(f"  正在向量化块 {i+1}/{len(chunks)}...")embedding = get_embedding(chunk, embedding_model)if embedding:# 我们将向量存储为Numpy数组,以便后续计算vector_store.append({"text": chunk, "vector": np.array(embedding)})print("向量数据库创建成功!")return vector_store

现在,我们为之前生成的所有文本块创建向量,构建我们的数据库。

vector_store = create_vector_store(chunks, EMBEDDING_MODEL)# 查看一下数据库的第一条记录
if vector_store:print("\n--- 向量数据库示例记录 ---")print(f"文本: {vector_store[0]['text'][:50]}...")print(f"向量维度: {vector_store[0]['vector'].shape}")
正在创建向量数据库...正在向量化块 1/12...正在向量化块 2/12...正在向量化块 3/12...正在向量化块 4/12...正在向量化块 5/12...正在向量化块 6/12...正在向量化块 7/12...正在向量化块 8/12...正在向量化块 9/12...正在向量化块 10/12...正在向量化块 11/12...正在向量化块 12/12...
向量数据库创建成功!--- 向量数据库示例记录 ---
文本: 检索增强生成(Retrieval-Augmented Generation, RAG)是一种先进的人...
向量维度: (1024,)

第四步:检索 (Retrieval)

检索是RAG的核心。当用户提出问题时,我们执行以下操作:

  1. 向量化问题:使用与文档相同的模型,将用户的问题也转换成一个向量。
  2. 计算相似度:计算问题向量与我们数据库中所有文本块向量的相似度。最常用的相似度度量是 余弦相似度 (Cosine Similarity)
  3. 排序并筛选:根据相似度得分从高到低排序,选出得分最高的 top_k 个文本块作为后续步骤的上下文。

余弦相似度

它测量两个向量之间的夹角的余弦值。如果两个向量方向完全相同,相似度为1;如果正交,为0;如果方向完全相反,为-1。公式如下:
similarity = cos ⁡ ( θ ) = A ⋅ B ∥ A ∥ ∥ B ∥ \text{similarity} = \cos(\theta) = \frac{\mathbf{A} \cdot \mathbf{B}}{\|\mathbf{A}\| \|\mathbf{B}\|} similarity=cos(θ)=A∥∥BAB

def cosine_similarity(v1, v2):"""计算两个Numpy向量的余弦相似度。"""dot_product = np.dot(v1, v2)norm_v1 = np.linalg.norm(v1)norm_v2 = np.linalg.norm(v2)# 避免除以零if norm_v1 == 0 or norm_v2 == 0:return 0.0return dot_product / (norm_v1 * norm_v2)def find_relevant_chunks(query_vector: np.ndarray, vector_store: list[dict], top_k: int) -> list[str]:"""在向量数据库中查找与查询最相关的文本块。"""# 计算查询向量与数据库中每个块向量的相似度similarities = [(cosine_similarity(query_vector, item["vector"]), item['text']) for item in vector_store]# 按相似度降序排序similarities.sort(key=lambda x: x[0], reverse=True)# 返回top_k个最相关块的文本return [(sim, text) for sim, text in similarities[:top_k]]

让我们来测试一下检索功能。我们提出一个问题,然后看看系统能找到哪些相关的文本块。

query = "RAG技术的核心思想是什么?"
print(f"--- 正在为问题进行检索 ---")
print(f"问题: {query}\n")# 1. 向量化问题
query_vector = get_embedding(query, EMBEDDING_MODEL)
if query_vector:query_vector = np.array(query_vector)# 2. 查找最相关的块relevant_chunks = find_relevant_chunks(query_vector, vector_store, top_k=12)print(f"--- 找到的前 {len(relevant_chunks)} 个最相关块 ---")for i, (score, chunk) in enumerate(relevant_chunks):print(f"相关块 {i+1} 相似度 {score:.4f}:")print(chunk[:20] + '...')print("\n")
else:print("无法为问题生成向量。")
--- 正在为问题进行检索 ---
问题: RAG技术的核心思想是什么?--- 找到的前 12 个最相关块 ---
相关块 1 相似度 0.6127:
检索增强生成(Retrieval-Aug...相关块 2 相似度 0.6078:
的知识过时、事实性不足(幻觉)以及缺乏领...相关块 3 相似度 0.5783:
提供的上下文,没有捏造信息;以及“答案相...相关块 4 相似度 0.5494:
案是否严格基于所提供的上下文,没有捏造信...相关块 5 相似度 0.4952:
问题一起,“增强”成一个结构化的提示(A...相关块 6 相似度 0.2704:
咖啡,这种深色、芳香的饮品,是全球数十亿...相关块 7 相似度 0.2699:
点是让咖啡走进千家万户,代表是真空包装的...相关块 8 相似度 0.2688:
Model),转换成一个高维的数字向量。...相关块 9 相似度 0.2596:
加勒比海的马提尼克岛。一段富有戏剧性的故...相关块 10 相似度 0.2333:
咖啡贸易中心,也门人对咖啡的种植技术严加...相关块 11 相似度 0.2276:
”后,咖啡在欧洲的流行便势不可挡。伦敦、...相关块 12 相似度 0.2152:
后变得异常兴奋、活蹦乱跳。出于好奇,他亲...

第五步:增强与生成 (Augment & Generate)

这是RAG流程的最后一步,也是见证奇迹的时刻。

  1. 增强提示 (Augmented Prompt):我们将上一步检索到的 relevant_chunks 作为“上下文 (Context)”,与用户的原始问题 query 一起,构建一个结构化的、信息丰富的提示(Prompt)。
  2. 生成回答:我们将这个增强后的提示发送给一个强大的生成模型(如 qwen3:4b)。

通过给模型提供精确的上下文,我们极大地降低了它“胡说八道”(即产生幻觉)的可能性,并引导它生成基于事实的、准确的回答。

def generate_response(query: str, context_chunks: list[str], generation_model: str) -> str:"""使用检索到的上下文和原始问题,生成最终回答。"""# 将所有上下文块合并成一个字符串context = "\n\n---\n\n".join([chunk for score, chunk in context_chunks])# 构建我们精心设计的提示prompt = f"""
你是一个问答机器人。请根据下面提供的上下文信息来回答问题。
如果上下文信息足以回答问题,请直接使用上下文中的信息进行回答。
如果上下文信息不足以回答问题,请明确指出“根据提供的上下文信息,我无法回答这个问题”。
不要编造信息或使用你自己的知识。[上下文]
{context}[问题]
{query}[回答]
"""print("--- 正在生成回答 ---")print("\n发送给生成模型的完整提示:")print("----------------------------------")print(prompt)print("----------------------------------\n")try:response = requests.post(f"{OLLAMA_API_BASE_URL}/generate",json={"model": generation_model,"prompt": prompt,"stream": False # 为简单起见,我们不使用流式响应})response.raise_for_status()return response.json()["response"].strip()except requests.exceptions.RequestException as e:print(f"Error during generation: {e}")return "抱歉,生成回答时出错。"

现在,我们将检索到的relevant_chunks和原始问题query一起发送给生成模型。

final_answer = generate_response(query, relevant_chunks, GENERATION_MODEL)print("\n===================================")
print("          最终回答")
print("===================================")
print(f"问题: {query}")
print(f"\n回答: \n{final_answer}")
print("===================================")
--- 正在生成回答 ---发送给生成模型的完整提示:
----------------------------------你是一个问答机器人。请根据下面提供的上下文信息来回答问题。
如果上下文信息足以回答问题,请直接使用上下文中的信息进行回答。
如果上下文信息不足以回答问题,请明确指出“根据提供的上下文信息,我无法回答这个问题”。
不要编造信息或使用你自己的知识。[上下文]
检索增强生成(Retrieval-Augmented Generation, RAG)是一种先进的人工智能框架,旨在通过融合外部知识库来显著提升大型语言模型(LLM)的性能。它有效地将LLM强大的参数化知识(即在训练数据中学到的知识)与可动态访问的非参数化知识(即外部知识库)相结合,从而解决LLM面临的知识过时、事实性不足(幻觉)以及缺乏领域专业性的核心挑战。RAG的核心工作流程可以分为两个主要阶段:离线索引(Indexing)和在线检索与生成(Retrieval & Generation)。---的知识过时、事实性不足(幻觉)以及缺乏领域专业性的核心挑战。RAG的核心工作流程可以分为两个主要阶段:离线索引(Indexing)和在线检索与生成(Retrieval & Generation)。离线索引阶段是数据准备的过程。首先,系统加载原始数据源,这些数据源可以是多种格式,如PDF文档、网页、数据库记录或纯文本文件。接着,这些长文档被“分块”(Chunking),即分割成更小的、语义完整的文本块。分块策略至关重要,好的策略(如按段落分割并设置重叠)能确保检索的上下文既完整又连贯。随后,每个文本块都被送入一个“向量化模型”(Embedding Model),转换成一个高维的数字向量。这些向量捕捉了文本的深层语义含义。最后,所有的文本块及其对应的向量被存储在一个专门的“向量数据库”(Vector Database)中,以便进行快速相似性搜索。---提供的上下文,没有捏造信息;以及“答案相关性”(Answer Relevancy),评估答案是否直接且充分地回应了用户的问题。像RAGAs这样的开源框架就是专门用于进行此类综合评估的。RAG技术并非一成不变,其前沿发展正探索更高级的范式。例如,“混合搜索”(Hybrid Search)结合了向量的语义搜索和传统关键词搜索(如BM25)的优点,以提高检索的鲁棒性。“重新排序”(Re-ranking)则在初步检索后,使用一个更强大的(但更慢的)交叉编码器模型对候选文档进行二次排序,以进一步提升上下文的质量。更进一步,“查询转换”(Query Transformation)技术,如HyDE,会让LLM先生成一个假设性的答案,再用这个假设答案的向量去检索,有时能获得更好的效果。未来的RAG将朝着自适应、多模态(能检索图像、音频)的方向发展,使其成为更强大、更通用的AI应用基础架构。---案是否严格基于所提供的上下文,没有捏造信息;以及“答案相关性”(Answer Relevancy),评估答案是否直接且充分地回应了用户的问题。像RAGAs这样的开源框架就是专门用于进行此类综合评估的。RAG技术并---问题一起,“增强”成一个结构化的提示(Augmented Prompt)。这个提示明确指示LLM根据提供的上下文来回答问题。最后,这个增强后的提示被发送给LLM,生成一个基于可靠、相关信息的最终答案。评估一个RAG系统比评估单独的LLM要复杂,需要从两个维度进行。检索器(Retriever)的评估指标主要包括“上下文精确率”(Context Precision),即检索到的上下文有多少是真正相关的;以及“上下文召回率”(Context Recall),即所有相关信息中有多少被成功检索出来。生成器(Generator)的评估指标则包括“忠实度”(Faithfulness),衡量答案是否严格基于所提供的上下文,没有捏造信息;以及“答案相关性”(Answer Relevancy),评估答案是否直接且充分地回应了用户的问题。像RAGAs这样的开源框架就是专门用于进行此类综合评估的。---咖啡,这种深色、芳香的饮品,是全球数十亿人日常生活中不可或缺的一部分。它不仅仅是一种提神醒脑的工具,更是一种文化符号、一种社交媒介和一门复杂的农艺科学。它的故事始于非洲的偏远山区,如今已遍及全球每个角落,其旅程充满了传奇、商业竞争和文化交融。关于咖啡的起源,流传最广的是埃塞俄比亚牧羊人卡尔迪的传说。据说在公元9世纪,卡尔迪发现他的山羊在吃了一种红色浆果后变得异常兴奋、活蹦乱跳。出于好奇,他亲自尝试了这些浆果,并感受到了前所未有的精力。他将这一发现告诉了当地修道院的僧侣,僧侣们发现这种浆果可以帮助他们在漫长的祈祷中保持清醒,咖啡的提神功效由此被发现。---点是让咖啡走进千家万户,代表是真空包装的速溶咖啡和罐装咖啡粉。第二波浪潮始于20世纪末,以星巴克为代表,强调咖啡的体验和品质,意式浓缩咖啡(Espresso)及其衍生饮品(如拿铁、卡布奇诺)成为主流。第三波浪潮是当前的趋势,它将咖啡视为一种像葡萄酒一样的“精品”农产品。这一浪潮强调咖啡豆的“单一产地”(Single Origin),关注风土、海拔、处理方法(如日晒、水洗)对风味的影响。手冲、虹吸、爱乐压等精细的冲煮方法被广泛采用,旨在最大程度地展现每一款咖啡豆独特的风味特征。从埃塞俄比亚山区的红色浆果到一杯精心冲煮的单一产地手冲咖啡,咖啡的旅程仍在继续,不断演变和丰富着人类的味觉与文化。---Model),转换成一个高维的数字向量。这些向量捕捉了文本的深层语义含义。最后,所有的文本块及其对应的向量被存储在一个专门的“向量数据库”(Vector Database)中,以便进行快速相似性搜索。在线阶段在用户提出问题时启动。第一步,用户的查询会使用与索引阶段相同的向量化模型进行向量化。第二步,系统使用这个查询向量,在向量数据库中执行“相似性搜索”(通常是余弦相似度计算),以找出与问题语义最相关的top-k个文本块。这些被找回的文本块构成了“上下文”(Context)。第三步,系统将检索到的上下文与用户的原始问题一起,“增强”成一个结构化的提示(Augmented Prompt)。这个提示明确指示LLM根据提供的上下文来回答问题。最后,这个增强后的提示被发送给LLM,生成一个基于可靠、相关信息的最终答案。---加勒比海的马提尼克岛。一段富有戏剧性的故事讲述了一位法国海军军官加布里埃尔·德·克利,他用自己的饮用水份额来浇灌一棵咖啡树苗,并成功将其带到了美洲。这棵树苗最终成为中美洲和南美洲数百万棵咖啡树的祖先。现代咖啡文化通常被划分为三个“浪潮”。第一波浪潮是咖啡的大众化,始于20世纪初,重点是让咖啡走进千家万户,代表是真空包装的速溶咖啡和罐装咖啡粉。第二波浪潮始于20世纪末,以星巴克为代表,强调咖啡的体验和品质,意式浓缩咖啡(Espresso)及其衍生饮品(如拿铁、卡布奇诺)成为主流。---咖啡贸易中心,也门人对咖啡的种植技术严加保密,禁止任何未经处理的咖啡豆出口,以维持其垄断地位。正是在这里,咖啡馆(qahveh khaneh)首次出现,并迅速成为人们交流思想、下棋和听音乐的社交中心。在17世纪,咖啡通过威尼斯商人的贸易路线传入欧洲,并迅速风靡。起初,它因其外来背景和独特的苦味而受到一些保守人士的抵制,甚至被称为“撒旦的苦涩发明”。然而,当教皇克莱蒙八世亲自品尝并为其“洗礼”后,咖啡在欧洲的流行便势不可挡。伦敦、巴黎、维也纳等大城市涌现出成千上万家咖啡馆,它们成为商人、艺术家和知识分子的聚集地,被誉为“便士大学”,因为只需花一便士买杯咖啡,就能听到各种最新的思想和辩论。---”后,咖啡在欧洲的流行便势不可挡。伦敦、巴黎、维也纳等大城市涌现出成千上万家咖啡馆,它们成为商人、艺术家和知识分子的聚集地,被誉为“便士大学”,因为只需花一便士买杯咖啡,就能听到各种最新的思想和辩论。随着需求的增长,欧洲列强开始寻求在自己的殖民地种植咖啡,以打破阿拉伯的垄断。荷兰人率先在印度的马拉巴尔,以及后来的爪哇和苏门答腊成功种植咖啡。不久之后,法国人将咖啡带到了加勒比海的马提尼克岛。一段富有戏剧性的故事讲述了一位法国海军军官加布里埃尔·德·克利,他用自己的饮用水份额来浇灌一棵咖啡树苗,并成功将其带到了美洲。这棵树苗最终成为中美洲和南美洲数百万棵咖啡树的祖先。---后变得异常兴奋、活蹦乱跳。出于好奇,他亲自尝试了这些浆果,并感受到了前所未有的精力。他将这一发现告诉了当地修道院的僧侣,僧侣们发现这种浆果可以帮助他们在漫长的祈祷中保持清醒,咖啡的提神功效由此被发现。咖啡的种植和贸易始于阿拉伯半岛。在15世纪,也门的苏菲派教徒广泛饮用咖啡以在宗教仪式中保持警觉。也门的港口城市摩卡(Mocha)成为世界上第一个咖啡贸易中心,也门人对咖啡的种植技术严加保密,禁止任何未经处理的咖啡豆出口,以维持其垄断地位。正是在这里,咖啡馆(qahveh khaneh)首次出现,并迅速成为人们交流思想、下棋和听音乐的社交中心。[问题]
RAG技术的核心思想是什么?[回答]----------------------------------===================================最终回答
===================================
问题: RAG技术的核心思想是什么?回答: 
<think>
好的,我现在需要回答用户的问题:“RAG技术的核心思想是什么?”。首先,我要仔细查看提供的上下文信息,找到相关的内容。根据上下文,RAG(检索增强生成)是一种结合外部知识库和大型语言模型的框架。核心工作流程分为离线索引和在线检索与生成两个阶段。离线索引阶段涉及数据的分块、向量化和存储到向量数据库。在线阶段则是用户提问时,系统通过向量相似性搜索找到相关文本块,然后将这些上下文与问题结合,生成最终答案。接下来,我需要确认这些信息是否直接回答了问题。用户问的是核心思想,而上下文中提到RAG的核心是融合外部知识库和LLM的参数化知识,解决知识过时、幻觉和领域专业性的问题。此外,流程分为两个阶段,离线和在线,以及使用向量数据库进行检索,然后生成增强提示给模型。需要确保没有遗漏关键点,比如上下文精确率、召回率等评估指标,但问题问的是核心思想,所以这些评估部分可能不是重点。但根据上下文,核心思想是结合外部知识和模型知识,通过检索和生成来提升性能。检查是否有其他部分提到核心思想,比如在第二段提到“RAG的核心工作流程可以分为两个主要阶段”,但核心思想应该更概括,即融合外部知识和模型知识,解决特定问题。因此,答案应基于这些信息,不添加额外内容。
</think>根据提供的上下文信息,RAG技术的核心思想是通过融合外部知识库与大型语言模型(LLM)的参数化知识,解决LLM在知识过时、事实性不足(幻觉)以及缺乏领域专业性等核心挑战。其核心工作流程分为两个阶段:离线索引(数据分块、向量化存储)和在线检索与生成(通过相似性搜索获取上下文并增强提示生成答案)。
===================================

总结与完整流程封装

我们已经成功地从零开始构建并测试了RAG系统的所有四个关键部分。为了方便使用,我们可以将整个流程封装成一个函数,这样就可以轻松地用它来回答任何问题了。

def run_rag_pipeline(query: str, vector_store: list[dict], top_k: int = 2):"""运行完整的RAG流程:检索 -> 生成"""print(f"--- 开始处理新问题: '{query}' ---")# 1. 向量化问题print("\n步骤1: 向量化问题...")query_vector = get_embedding(query, EMBEDDING_MODEL)if not query_vector:return "抱歉,无法为您的问题创建向量。"query_vector = np.array(query_vector)# 2. 检索相关块print("\n步骤2: 检索相关上下文...")relevant_chunks = find_relevant_chunks(query_vector, vector_store, top_k=top_k)print(f"检索到 {len(relevant_chunks)} 个相关块。")# 3. 生成回答print("\n步骤3: 基于上下文生成回答...")final_answer = generate_response(query, relevant_chunks, GENERATION_MODEL)return final_answer

测试封装后的RAG流程

让我们用一个新问题来测试我们封装好的RAG Pipeline。

new_query = "概述RAG执行流程"
final_answer = run_rag_pipeline(new_query, vector_store)print("\n===================================")
print("          最终回答")
print("===================================")
print(f"问题: {new_query}")
print(f"\n回答: \n{final_answer}")
print("===================================")
--- 开始处理新问题: '概述RAG执行流程' ---步骤1: 向量化问题...步骤2: 检索相关上下文...
检索到 2 个相关块。步骤3: 基于上下文生成回答...
--- 正在生成回答 ---发送给生成模型的完整提示:
----------------------------------你是一个问答机器人。请根据下面提供的上下文信息来回答问题。
如果上下文信息足以回答问题,请直接使用上下文中的信息进行回答。
如果上下文信息不足以回答问题,请明确指出“根据提供的上下文信息,我无法回答这个问题”。
不要编造信息或使用你自己的知识。[上下文]
的知识过时、事实性不足(幻觉)以及缺乏领域专业性的核心挑战。RAG的核心工作流程可以分为两个主要阶段:离线索引(Indexing)和在线检索与生成(Retrieval & Generation)。离线索引阶段是数据准备的过程。首先,系统加载原始数据源,这些数据源可以是多种格式,如PDF文档、网页、数据库记录或纯文本文件。接着,这些长文档被“分块”(Chunking),即分割成更小的、语义完整的文本块。分块策略至关重要,好的策略(如按段落分割并设置重叠)能确保检索的上下文既完整又连贯。随后,每个文本块都被送入一个“向量化模型”(Embedding Model),转换成一个高维的数字向量。这些向量捕捉了文本的深层语义含义。最后,所有的文本块及其对应的向量被存储在一个专门的“向量数据库”(Vector Database)中,以便进行快速相似性搜索。---检索增强生成(Retrieval-Augmented Generation, RAG)是一种先进的人工智能框架,旨在通过融合外部知识库来显著提升大型语言模型(LLM)的性能。它有效地将LLM强大的参数化知识(即在训练数据中学到的知识)与可动态访问的非参数化知识(即外部知识库)相结合,从而解决LLM面临的知识过时、事实性不足(幻觉)以及缺乏领域专业性的核心挑战。RAG的核心工作流程可以分为两个主要阶段:离线索引(Indexing)和在线检索与生成(Retrieval & Generation)。[问题]
概述RAG执行流程[回答]----------------------------------===================================最终回答
===================================
问题: 概述RAG执行流程回答: 
<think>
好的,我现在需要回答用户的问题:“概述RAG执行流程”。根据提供的上下文信息,我需要先仔细阅读并理解上下文内容,然后从中提取相关信息来组织答案。首先,用户提供的上下文分为两个部分,都是关于RAG(检索增强生成)的。第一部分提到RAG的核心工作流程分为离线索引(Indexing)和在线检索与生成(Retrieval & Generation)两个主要阶段。第二部分则更详细地描述了离线索引阶段的过程,包括加载数据源、分块、向量化和存储到向量数据库。而在线检索与生成阶段虽然没有详细展开,但根据RAG的基本原理,应该涉及从向量数据库检索相关文档,然后将这些信息输入到语言模型中生成答案。接下来,我需要确认问题是否需要更详细的步骤。用户的问题只是要求“概述RAG执行流程”,所以可能需要简要说明两个主要阶段,并简要提及每个阶段的关键步骤。根据上下文,离线索引阶段包括数据加载、分块、向量化和存储;而在线阶段则涉及检索和生成。不过,上下文中没有详细说明在线阶段的具体步骤,所以可能需要仅根据提供的上下文来回答,不添加额外信息。需要确保不编造信息,只使用上下文中的内容。上下文提到离线索引是数据准备,包括加载数据源、分块、向量化、存储到向量数据库。在线检索与生成阶段则涉及检索和生成,但具体细节可能没有详细说明。因此,在回答时,应该明确指出这两个阶段,并简要描述每个阶段的关键步骤,但不要添加上下文未提及的内容。可能的结构是:首先介绍RAG的两个主要阶段,然后分别概述每个阶段的步骤。例如,离线索引阶段包括加载数据、分块、向量化、存储;在线阶段包括检索和生成。但需要检查上下文中是否明确提到在线阶段的步骤。根据上下文,第二段提到RAG的两个阶段,但未详细展开在线阶段,所以可能只能提到这两个阶段,而在线阶段的细节可能需要根据上下文中的描述来推断,但必须严格依据提供的文本。因此,正确的回答应该是将离线索引和在线检索与生成作为两个主要阶段,并分别简要说明每个阶段的内容,基于上下文中的描述。例如,离线索引阶段包括加载数据源、分块、向量化、存储到向量数据库;在线阶段则是检索和生成。但需要确认是否在上下文中明确提到在线阶段的步骤。根据提供的上下文,第二段提到“在线检索与生成(Retrieval & Generation)”,但没有进一步的细节,所以可能只能这样概述。
</think>RAG的执行流程分为两个主要阶段:  
1. **离线索引(Indexing)**:  - 加载原始数据源(如PDF、网页、数据库等)。  - 对长文档进行分块处理,分割为语义完整的文本块。  - 使用向量化模型将每个文本块转换为高维向量。  - 将文本块及其向量存储至向量数据库,以便后续检索。  2. **在线检索与生成(Retrieval & Generation)**:  - 通过向量数据库检索与查询相关的文本块。  - 将检索到的上下文信息与大型语言模型(LLM)结合,生成最终答案。  该流程通过结合LLM的参数化知识和外部知识库的动态信息,提升回答的准确性与时效性。
===================================

测试边界情况

现在我们来问一个文档中没有提到的问题,看看模型的反应是否符合我们在提示中设定的要求(即承认自己不知道)。

off_topic_query = "法国的首都是哪里?"
final_answer = run_rag_pipeline(off_topic_query, vector_store)print("\n===================================")
print("        边界情况测试结果")
print("===================================")
print(f"问题: {off_topic_query}")
print(f"\n回答: \n{final_answer}")
print("===================================")
--- 开始处理新问题: '法国的首都是哪里?' ---步骤1: 向量化问题...步骤2: 检索相关上下文...
检索到 2 个相关块。步骤3: 基于上下文生成回答...
--- 正在生成回答 ---发送给生成模型的完整提示:
----------------------------------你是一个问答机器人。请根据下面提供的上下文信息来回答问题。
如果上下文信息足以回答问题,请直接使用上下文中的信息进行回答。
如果上下文信息不足以回答问题,请明确指出“根据提供的上下文信息,我无法回答这个问题”。
不要编造信息或使用你自己的知识。[上下文]
”后,咖啡在欧洲的流行便势不可挡。伦敦、巴黎、维也纳等大城市涌现出成千上万家咖啡馆,它们成为商人、艺术家和知识分子的聚集地,被誉为“便士大学”,因为只需花一便士买杯咖啡,就能听到各种最新的思想和辩论。随着需求的增长,欧洲列强开始寻求在自己的殖民地种植咖啡,以打破阿拉伯的垄断。荷兰人率先在印度的马拉巴尔,以及后来的爪哇和苏门答腊成功种植咖啡。不久之后,法国人将咖啡带到了加勒比海的马提尼克岛。一段富有戏剧性的故事讲述了一位法国海军军官加布里埃尔·德·克利,他用自己的饮用水份额来浇灌一棵咖啡树苗,并成功将其带到了美洲。这棵树苗最终成为中美洲和南美洲数百万棵咖啡树的祖先。---咖啡贸易中心,也门人对咖啡的种植技术严加保密,禁止任何未经处理的咖啡豆出口,以维持其垄断地位。正是在这里,咖啡馆(qahveh khaneh)首次出现,并迅速成为人们交流思想、下棋和听音乐的社交中心。在17世纪,咖啡通过威尼斯商人的贸易路线传入欧洲,并迅速风靡。起初,它因其外来背景和独特的苦味而受到一些保守人士的抵制,甚至被称为“撒旦的苦涩发明”。然而,当教皇克莱蒙八世亲自品尝并为其“洗礼”后,咖啡在欧洲的流行便势不可挡。伦敦、巴黎、维也纳等大城市涌现出成千上万家咖啡馆,它们成为商人、艺术家和知识分子的聚集地,被誉为“便士大学”,因为只需花一便士买杯咖啡,就能听到各种最新的思想和辩论。[问题]
法国的首都是哪里?[回答]----------------------------------===================================边界情况测试结果
===================================
问题: 法国的首都是哪里?回答: 
<think>
好的,我现在需要回答用户的问题:“法国的首都是哪里?”根据提供的上下文信息,我需要先仔细查看上下文内容,看看是否有提到法国的首都。首先,我快速浏览上下文中的各个段落。第一段提到荷兰人在印度的马拉巴尔、爪哇和苏门答腊种植咖啡,法国人将咖啡带到了加勒比海的马提尼克岛。第二段提到咖啡通过威尼斯商人传入欧洲,教皇克莱蒙八世品尝咖啡,以及伦敦、巴黎、维也纳等城市有咖啡馆。这里明确提到了巴黎,但并没有提到法国的首都。不过,通常情况下,巴黎是法国的首都,但根据问题要求,必须严格依据上下文信息,不能依赖外部知识。接下来,我需要确认上下文中是否有关于法国首都的直接信息。在第二段中,提到“伦敦、巴黎、维也纳等大城市”,但这里列举的只是例子,并没有说明哪个是首都。因此,上下文中没有明确提到法国的首都是什么。因此,根据提供的上下文信息,无法确定法国的首都,因为没有直接的信息支持。不过,可能需要再仔细检查是否有其他部分。例如,是否在描述法国人带咖啡到马提尼克岛时,是否有提到法国的首都?但上下文中没有相关内容。因此,结论是上下文中没有提供足够的信息来回答法国的首都在哪里。
</think>根据提供的上下文信息,我无法回答这个问题。
===================================

文章转载自:
http://abaft.wanhuigw.com
http://alternative.wanhuigw.com
http://cate.wanhuigw.com
http://antrorsely.wanhuigw.com
http://cholla.wanhuigw.com
http://autotoxis.wanhuigw.com
http://bioglass.wanhuigw.com
http://bluejay.wanhuigw.com
http://anabiosis.wanhuigw.com
http://caloric.wanhuigw.com
http://amygdaline.wanhuigw.com
http://alimentary.wanhuigw.com
http://brevetcy.wanhuigw.com
http://benzpyrene.wanhuigw.com
http://assheadedness.wanhuigw.com
http://addible.wanhuigw.com
http://avoidant.wanhuigw.com
http://aluminize.wanhuigw.com
http://banderillero.wanhuigw.com
http://calligraph.wanhuigw.com
http://blackfellow.wanhuigw.com
http://autosemantic.wanhuigw.com
http://agamic.wanhuigw.com
http://briton.wanhuigw.com
http://amylase.wanhuigw.com
http://blanketry.wanhuigw.com
http://bandoeng.wanhuigw.com
http://austrian.wanhuigw.com
http://bacteriological.wanhuigw.com
http://bantamweight.wanhuigw.com
http://www.dtcms.com/a/261736.html

相关文章:

  • C# 委托(为委托添加方法和从委托移除方法)
  • Excel限制编辑:保护表格的实用功能
  • 【C#】使用电脑的GPU与CPU的区别
  • 鸿蒙5:其他布局容器
  • 【Redis原理】Redis分布式缓存——主从复制、哨兵机制与Redis Cluster
  • deepin 25 交换 caps lctl
  • Lua现学现卖
  • SpringBoot项目使用arthas-tunnel-server
  • AtCoder AT_abc412_c [ABC412C] Giant Domino 题解
  • 【力扣 简单 C】121. 买卖股票的最佳时机
  • GitHub Actions 实现 AWS ECS 服务的多集群安全重启方案
  • 【AI实践】Mac一天熟悉AI模型智能体应用(百炼版)
  • STM32中Usart的使用
  • 一个简单测试Deepseek吞吐量的脚本,国内环境可跑
  • 1.1 基于Icarus Verilog、ModelSim和Vivado对蜂鸟E203处理器进行仿真
  • HarmonyOS File和base64字符串转换
  • Note2.2 机器学习训练技巧:Batch and Momentum(Machine Learning by Hung-yi Lee)
  • C语言二级指针与多级指针
  • cannot import name ‘TextKwargs‘ from ‘transformers.processing_utils‘
  • 【LeetCode 热题 100】438. 找到字符串中所有字母异位词——(解法二)定长滑动窗口+数组
  • LeetCode Hot 100 找到字符串中所有字母异位词
  • 编译流程详解
  • 利用ROS打印novatel_msgs/INSPVAX
  • 滑坡监测接收机市场分析
  • libxlsxwriter: 一个轻量级的跨平台的C++操作Excel的开源库
  • 个人日记本小程序开发方案(使用IntelliJ IDEA)
  • python解释器 与 pip脚本常遇到的问题汇总
  • 【stm32】HAL库开发——CubeMX配置ADC
  • Minio入门+适配器模式(实战教程)
  • ZooKeeper深度面试指南三