我从零搭建 RAG 学到的 10 件事

一个 Python 开发者的 RAG 入门实战总结

大家好,我是一名有 Python 基础的开发者。最近两周我从零开始学习 RAG(检索增强生成),并亲手搭建了一个能问答的 Demo。过程中踩过不少坑,也收获了很多"原来如此"的时刻。

今天总结成 10 条经验,分享给想入门 RAG 的你。


1. RAG 流程其实就 5 步

刚开始看各种文章,被"索引""检索""生成"这些术语搞得晕头转向。后来自己写代码才发现,RAG 的核心流程其实就是 5 步:

复制代码
📄 文档加载 → ✂️ 文本切分 → 🔢 向量化 → 🔍 检索 → 💬 生成回答

用代码表示就是这样简单:

python 复制代码
# 伪代码,但逻辑是真实的
documents = load_documents("docs/")           # 1. 加载
chunks = split_text(documents, size=500)       # 2. 切分
vectors = embed_model.encode(chunks)           # 3. 向量化
db.add(vectors, chunks)                        # 3. 存储

# 查询时
query_vector = embed_model.encode(["用户问题"])  # 4. 向量化问题
results = db.search(query_vector, top_k=3)     # 4. 检索
answer = llm.generate(context=results, query)  # 5. 生成

我的思考 :不要被术语吓到。RAG 的本质就是"把文档切成小块,用向量表示,检索相关块,让 LLM 基于这些块回答"。理解了这个核心,再看各种优化技巧就清晰多了。


2. Embedding 不是魔法,就是"给词一个坐标"

Embedding 是我一开始最难理解的概念。什么"高维空间""稠密向量",听得一头雾水。

后来我想通了:Embedding 就是给每个词/句子一个"数字坐标",让意思相近的内容,坐标也接近。

python 复制代码
# 例子:用 SentenceTransformer 生成 Embedding
from sentence_transformers import SentenceTransformer

model = SentenceTransformer('all-MiniLM-L6-v2')

texts = [
    "机器学习是人工智能的核心",
    "深度学习是机器学习的子领域",
    "今天天气不错"
]

vectors = model.encode(texts)
print(vectors.shape)  # (3, 384) → 3 个句子,每个 384 维

这 384 个数字,就是这个句子的"坐标"。语义相近的句子,坐标也接近。

我的思考:不要纠结"每个数字代表什么"。就像你不需要知道 GPS 坐标的 x/y 具体怎么算的,只需要知道"坐标相近 = 位置相近"就够了。


3. 余弦相似度:只看方向,不看长度

检索的核心是计算"两个向量有多像"。最常用的是余弦相似度

公式看起来吓人:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> cosine ( A , B ) = A ⋅ B ∥ A ∥ × ∥ B ∥ \text{cosine}(A, B) = \frac{A \cdot B}{\|A\| \times \|B\|} </math>cosine(A,B)=∥A∥×∥B∥A⋅B

其实用人话说就是:忽略长度,只看两个向量的"方向夹角"

python 复制代码
from sklearn.metrics.pairwise import cosine_similarity

A = [1, 2, 3]
B = [2, 4, 6]  # 是 A 的 2 倍,方向相同

sim = cosine_similarity([A], [B])
print(sim[0][0])  # 1.0 → 完全相同方向
相似度 含义
1.0 方向完全相同
0.8-0.9 非常相似
0.5-0.7 中等相似
<0.5 不太相关

我的思考:为什么不用欧氏距离?因为文本的"长度"不代表什么。"你好"和"你好吗"欧氏距离可能很远,但语义很接近。余弦相似度刚好忽略了长度这个干扰因素。


4. 文本切分:不切分真的不行

我一开始偷懒,想把整篇文档直接向量化。结果有两个问题:

  1. LLM 上下文不够用:一篇 10 万字的文档,直接塞进 Prompt 会超限
  2. 检索精度极差:用户问一个小问题,返回整篇文档,噪音太大
python 复制代码
# ❌ 错误做法
chunks = ["整篇 10 万字的技术文档"]

# ✅ 正确做法
chunks = [
    "第一章:机器学习基础...",
    "第二章:深度学习入门...",
    "第三章:实战案例..."
]

我的思考:切分的本质是把"文档级检索"变成"段落级检索"。就像图书馆不是给你整本书,而是帮你复印相关的那几页。


5. chunk_size:从 500 开始,然后测试

关于切分多大,我查了很多资料,最后总结出一个实用策略:

文档类型 推荐 chunk_size
FAQ/问答 200-300
通用文档 500(推荐起点)
技术报告 800-1000
代码文件 按函数/类切分
python 复制代码
# 我的测试代码
from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,      # 每块 500 字符
    chunk_overlap=50,    # 重叠 50 字符
    separators=["\n\n", "\n", "。", "!", "?", " ", ""]
)

chunks = splitter.split_text(long_text)

我的思考:没有"最佳"chunk_size,只有"最适合你场景"的。我的建议是先设 500,然后用 10-20 个测试问题验证,看哪个值召回率最高。


6. chunk_overlap:防止信息被"拦腰切断"

overlap 是相邻 chunk 之间的重叠部分。一开始我觉得这是浪费,后来发现真不是。

vbnet 复制代码
原文:"机器学习是人工智能的核心。深度学习是机器学习的子领域。"

❌ 没有 overlap:
chunk_1: "机器学习是人工智能"
chunk_2: "的核心。深度学习是"
chunk_3: "机器学习的子领域"

✅ 有 overlap(重叠 50 字):
chunk_1: "机器学习是人工智能的核心。深度"
chunk_2: "核心。深度学习是机器学习的子"
chunk_3: "子领域。深度学习是机器学习的"

我的思考 :overlap 的核心作用是防止关键信息被切分。我设置的规则是:overlap 至少包含 1-2 个完整句子,一般是 chunk_size 的 10-20%。


7. 检索不到正确答案?先查这 4 个地方

我遇到最多的问题是:"检索结果完全答非所问"。后来总结了一个诊断清单:

症状 可能原因 解决方法
检索结果完全无关 切分丢失上下文 带上标题/父级信息
结果"有点相关但不是答案" chunk 太碎了 调大 chunk_size
专业术语检索不到 Embedding 模型问题 用领域模型或加同义句
口语问法检索不到 表达方式不匹配 Query 改写或加 FAQ

一个真实案例

arduino 复制代码
用户问:"这个手机防水吗?"
文档写的是:"IP68 级防尘防水"

❌ 直接检索:相似度很低
✅ 解决:在文档里加一句"这个手机支持防水,等级是 IP68"

我的思考:RAG 不是"建好就能用",需要不断测试和优化。我建了一个 Excel,记录 20 个测试问题和每次调整后的效果,迭代几次后准确率从 60% 提升到 85%。


8. Embedding 模型怎么选?中文就用 BGE-M3

Embedding 模型有很多选择,我的建议是:

场景 推荐模型
中文为主 BGE-M3(免费开源)
追求效果 + 预算够 Voyage API
已在用 OpenAI 生态 text-embedding-3-small
数据敏感/本地部署 BGE-M3
python 复制代码
# 用 BGE-M3 的例子
from sentence_transformers import SentenceTransformer

model = SentenceTransformer('BAAI/bge-m3')
vectors = model.encode(["你好,世界"])

我的思考:不要一开始就追求"最好"的模型。我一开始纠结选哪个模型,浪费了一周。后来直接用 BGE-M3,效果够用,省下的时间用来优化切分和检索策略,收益更大。


9. 维度不是越高越好

Embedding 向量有 384 维、1536 维、3072 维等选择。我一开始觉得越高越好,后来发现不是。

维度 优点 缺点
384 快、省空间 语义区分能力一般
1536 平衡 适中
3072 语义区分细腻 慢、贵、存储大

一个直观的对比

bash 复制代码
1536 维 → 6 KB / 条
3072 维 → 12 KB / 条

100 万条数据:6 GB vs 12 GB
OpenAI 价格:$0.02 vs $0.13 / 100万 token(贵 6.5 倍)

我的思考 :维度是"精度"和"成本"的权衡。我的建议:不确定就用 1536 维,够用且成本可控。


10. 相似度阈值:不要硬用一刀切

我一开始设了个阈值 0.7,高于这个值才返回结果。后来发现有问题:

arduino 复制代码
严格匹配场景(客服问答):0.85-0.90 才回答,否则说"没找到"
推荐系统场景:0.65 就可以召回,让用户自己筛选

后来我改用"Top K + 重排序"策略:

python 复制代码
# 伪代码
results = db.search(query, top_k=10)      # 先召回 10 条
reranked = reranker.rank(query, results)  # 用精排模型重排
final = reranked[:5]                      # 取前 5 条给 LLM

我的思考:阈值不是不能用,但更好的做法是"召回多一些,用重排序模型精排"。重排序模型的判断比向量检索准确得多,因为它能把 query 和 document 一起编码。


总结:给初学者的 3 条建议

学完这两周,我最大的收获是:

  1. 不要等"完全学会"再动手。我第 1 天就借助AI写出了能跑的 Demo,后面边做边学,让AI帮我理解代码和底层原理。
  2. 先跑通流程,再优化细节。RAG 的 5 步流程很简单,先让它能工作,再调 chunk_size、换 Embedding 模型。
  3. 用数据说话,不要凭感觉。建个测试集,记录每次调整的效果,迭代几次后效果会明显提升。

RAG 不难,难的是开始动手。希望这篇总结能帮你少走弯路。


python 复制代码
# 附上RAG源代码 naive_rag
"""
Naive RAG Pipeline 实现
基于 learn.txt 文本文件的简单检索增强生成示例

RAG (Retrieval-Augmented Generation) 检索增强生成:
1. 将文档分块并向量化存储到向量数据库
2. 用户提问时,先检索相关文档块
3. 将检索到的上下文和问题一起交给大模型生成回答

核心流程:文档处理 → 向量化 → 存储 → 检索 → 生成
"""

import os
from typing import List, Tuple
from dotenv import load_dotenv

# 向量化和向量存储
from sentence_transformers import SentenceTransformer  # HuggingFace 的句子嵌入模型,用于将文本转换为向量
import chromadb  # 轻量级向量数据库,用于存储和检索向量

# 大模型接口
from openrouter import OpenRouter  # OpenRouter API 客户端,用于访问各种大语言模型

# 加载环境变量(从 .env 文件读取 API 密钥等配置)
load_dotenv()

# ============================================================
# 1. 默认配置参数
# ============================================================

# 文本分块默认配置
DEFAULT_CHUNK_SIZE = 1000      # 每个文本块的字符数(控制分块大小,影响检索精度)
DEFAULT_CHUNK_OVERLAP = 100    # 相邻块之间的重叠字符数(避免信息被切断,保持上下文连贯性)

# 检索默认配置
DEFAULT_TOP_K = 3             # 检索时返回最相关的 K 个文档块(数量影响上下文长度和信息量)

# Embedding 模型默认配置
DEFAULT_EMBEDDING_MODEL = "all-MiniLM-L6-v2"  # 句子变换器模型,将文本转为 384 维向量
                                              # 英文场景推荐,中文可换用 "paraphrase-multilingual-MiniLM-L12-v2"

# 向量数据库默认配置
DEFAULT_DB_PATH = "./chroma_dbnaive1"           # ChromaDB 数据持久化存储路径
DEFAULT_COLLECTION_NAME = "learn_documents1"  # 文档集合名称,用于分类管理不同来源的文档

# ============================================================
# 2. 文本分块函数
# ============================================================

import nltk
from nltk.tokenize import sent_tokenize

# 下载必要的nltk资源(只需要运行一次)
try:
    nltk.data.find('tokenizers/punkt')
except LookupError:
    nltk.download('punkt')

def chunk_text(text: str, chunk_size: int, chunk_overlap: int) -> List[str]:
    """
    将文本分割成指定大小的块,支持块间重叠,保持句子完整性
    
    参数:
        text: 要分块的文本
        chunk_size: 每个文本块的最大字符数
        chunk_overlap: 相邻块之间的重叠字符数
        
    返回:
        List[str]: 分块后的文本列表
    """
    if not text:
        return []
    
    # 首先将文本分割成句子
    sentences = sent_tokenize(text)
    if not sentences:
        return [text]
    
    chunks = []
    current_chunk = []
    current_chunk_length = 0
    
    for sentence in sentences:
        sentence_length = len(sentence)
        
        # 如果句子本身就超过了块大小,特殊处理
        if sentence_length > chunk_size:
            # 保存当前块
            if current_chunk:
                chunks.append(' '.join(current_chunk))
                current_chunk = []
                current_chunk_length = 0
            
            # 将超长句子分割成更小的块
            start = 0
            while start < sentence_length:
                end = min(start + chunk_size, sentence_length)
                # 尽量在标点处分割
                if end < sentence_length:
                    # 查找最后一个标点符号
                    punctuation = ['.', '!', '?', ';', '。', '!', '?', ';']
                    last_punc = -1
                    for p in punctuation:
                        pos = sentence.rfind(p, start, end)
                        if pos > last_punc:
                            last_punc = pos
                    if last_punc != -1:
                        end = last_punc + 1
                
                chunks.append(sentence[start:end])
                # 处理重叠
                if end < sentence_length:
                    start = max(end - chunk_overlap, start + 1)
                else:
                    start = end
            continue
        
        # 计算添加当前句子后的总长度(包括空格)
        new_length = current_chunk_length + sentence_length + (1 if current_chunk else 0)
        
        # 如果添加后超过限制,完成当前块并开始新块
        if new_length > chunk_size:
            # 保存当前块
            chunks.append(' '.join(current_chunk))
            
            # 处理重叠:从当前块末尾开始取句子,直到达到重叠大小
            overlap_chars = 0
            overlap_sentences = []
            
            # 从后往前遍历当前块的句子
            for sent in reversed(current_chunk):
                sent_len_with_space = len(sent) + 1  # +1 for space
                if overlap_chars + sent_len_with_space > chunk_overlap:
                    break
                overlap_chars += sent_len_with_space
                overlap_sentences.insert(0, sent)
            
            # 开始新块,包含重叠句子
            current_chunk = overlap_sentences
            current_chunk_length = sum(len(s) + 1 for s in current_chunk) - 1 if current_chunk else 0
        
        # 添加当前句子到块
        current_chunk.append(sentence)
        current_chunk_length = current_chunk_length + sentence_length + (1 if current_chunk_length > 0 else 0)
    
    # 添加最后一个块
    if current_chunk:
        chunks.append(' '.join(current_chunk))
    
    return chunks


# ============================================================
# 3. 向量数据库操作
# ============================================================

class NaiveRAG:
    def __init__(self, 
                 db_path: str = DEFAULT_DB_PATH,
                 collection_name: str = DEFAULT_COLLECTION_NAME,
                 embedding_model: str = DEFAULT_EMBEDDING_MODEL,
                 chunk_size: int = DEFAULT_CHUNK_SIZE,
                 chunk_overlap: int = DEFAULT_CHUNK_OVERLAP,
                 top_k: int = DEFAULT_TOP_K):
        """
        初始化 RAG 系统
        
        参数:
            db_path: 向量数据库存储路径
            collection_name: 文档集合名称
            embedding_model: 用于生成向量的模型名称
            chunk_size: 文本分块大小
            chunk_overlap: 文本分块重叠大小
            top_k: 默认检索返回的文档块数量
        """
        # 存储配置参数
        self.db_path = db_path
        self.collection_name = collection_name
        self.embedding_model = embedding_model
        self.chunk_size = chunk_size
        self.chunk_overlap = chunk_overlap
        self.top_k = top_k
        
        # 初始化模型
        print(f"🔄 加载 Embedding 模型: {embedding_model}")
        self.embed_model = SentenceTransformer(embedding_model)

        # 初始化向量数据库
        print(f"🔄 初始化向量数据库: {db_path}")
        self.client = chromadb.PersistentClient(path=db_path)
        self.collection = self.client.get_or_create_collection(collection_name)
        print(f"✅ 初始化完成,当前文档块数: {self.collection.count()}")

    def index_document(self, file_path: str, replace_existing: bool = False) -> int:
        """
        索引文档:读取、分块、向量化、存储
        
        参数:
            file_path: 要索引的文档路径
            replace_existing: 如果文档已存在,是否替换现有索引
            
        返回:
            int: 索引的文档块数
            
        异常:
            FileNotFoundError: 当文件不存在时
            IOError: 当文件读取失败时
            Exception: 当索引过程中发生其他错误时
        """
        # 输入验证
        if not file_path:
            raise ValueError("文件路径不能为空")
        
        # 读取文档
        print(f"📄 读取文档: {file_path}")
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                text = f.read()
        except FileNotFoundError:
            raise FileNotFoundError(f"❌ 文件不存在: {file_path}")
        except IOError as e:
            raise IOError(f"❌ 文件读取失败: {str(e)}")

        # 检查是否已存在该文档的索引
        doc_name = os.path.basename(file_path)
        existing_count = 0
        try:
            # 查询是否存在该文档的索引
            existing_results = self.collection.get(where={"source": doc_name})
            existing_count = len(existing_results.get("ids", []))
        except Exception:
            # 如果查询失败,假设文档不存在
            existing_count = 0
        
        if existing_count > 0:
            if replace_existing:
                print(f"🔄 文档 '{doc_name}' 已存在 {existing_count} 个块,正在替换...")
                # 删除现有索引
                try:
                    self.collection.delete(where={"source": doc_name})
                    print("🗑️  已删除现有索引")
                except Exception as e:
                    raise Exception(f"❌ 删除现有索引失败: {str(e)}")
            else:
                print(f"⚠️  文档 '{doc_name}' 已存在 {existing_count} 个块,跳过索引")
                return 0

        # 分块
        chunks = chunk_text(text, self.chunk_size, self.chunk_overlap)
        print(f"📦 分块完成: {len(chunks)} 个块")

        # 生成 embedding
        print("🔄 生成向量...")
        try:
            embeddings = self.embed_model.encode(chunks).tolist()
        except Exception as e:
            raise Exception(f"❌ 向量生成失败: {str(e)}")

        # 生成 ID
        ids = [f"{doc_name}_chunk_{i}" for i in range(len(chunks))]

        # 存入向量库
        try:
            self.collection.add(
                embeddings=embeddings,
                documents=chunks,
                ids=ids,
                metadatas=[{"source": doc_name} for _ in chunks]
            )
        except Exception as e:
            raise Exception(f"❌ 向量存储失败: {str(e)}")

        print(f"✅ 索引完成,总块数: {self.collection.count()}")
        return len(chunks)

    def retrieve(self, query: str, top_k: int = None) -> List[Tuple[str, float]]:
        """
        检索相关文档块
        返回: [(文档块, 相似度分数), ...]
        
        参数:
            query: 查询文本
            top_k: 返回的相关文档块数量(默认为初始化时设置的值)
            
        返回:
            List[Tuple[str, float]]: 文档块和相似度分数的列表
            
        异常:
            ValueError: 当查询为空或 top_k 无效时
            Exception: 当检索过程中发生错误时
        """
        # 输入验证
        if not query:
            raise ValueError("查询文本不能为空")
            
        # 使用默认值如果top_k为None
        if top_k is None:
            top_k = self.top_k
            
        if top_k <= 0:
            raise ValueError("top_k 必须大于 0")
        
        try:
            # 查询向量化
            query_embedding = self.embed_model.encode([query]).tolist()

            # 检索
            results = self.collection.query(
                query_embeddings=query_embedding,
                n_results=top_k,
                include=["documents", "distances"]
            )

            # 整理结果
            retrieved = []
            for doc, dist in zip(results["documents"][0], results["distances"][0]):
                retrieved.append((doc, dist))

            return retrieved
        except Exception as e:
            raise Exception(f"❌ 检索失败: {str(e)}")

    def build_context(self, query: str, top_k: int = None) -> str:
        """
        构建上下文字符串
        
        参数:
            query: 查询文本
            top_k: 使用的相关文档块数量(默认为初始化时设置的值)
            
        返回:
            str: 构建好的上下文字符串
            
        异常:
            ValueError: 当查询为空时
            Exception: 当构建上下文过程中发生错误时
        """
        # 输入验证
        if not query:
            raise ValueError("查询文本不能为空")
        
        # 使用默认值如果top_k为None
        if top_k is None:
            top_k = self.top_k
        
        try:
            results = self.retrieve(query, top_k)
            context = "\n\n---\n\n".join([doc for doc, _ in results])
            return context
        except Exception as e:
            raise Exception(f"❌ 上下文构建失败: {str(e)}")

    def query(self, query: str, top_k: int = None) -> dict:
        """
        执行完整查询:检索 + 返回结果
        
        参数:
            query: 查询文本
            top_k: 返回的相关文档块数量(默认为初始化时设置的值)
            
        返回:
            dict: 查询结果,包含查询文本、上下文、距离和上下文文本
            
        异常:
            ValueError: 当查询为空时
            Exception: 当查询过程中发生错误时
        """
        # 输入验证
        if not query:
            raise ValueError("查询文本不能为空")
        
        # 使用默认值如果top_k为None
        if top_k is None:
            top_k = self.top_k
        
        try:
            results = self.retrieve(query, top_k)

            return {
                "query": query,
                "contexts": [doc for doc, _ in results],
                "distances": [dist for _, dist in results],
                "context_text": "\n\n---\n\n".join([doc for doc, _ in results])
            }
        except Exception as e:
            raise Exception(f"❌ 查询失败: {str(e)}")

    def clear(self):
        """
        清空向量库
        
        异常:
            Exception: 当清空向量库过程中发生错误时
        """
        try:
            self.client.delete_collection(self.collection_name)
            self.collection = self.client.get_or_create_collection(self.collection_name)
            print("🗑️ 向量库已清空")
        except Exception as e:
            raise Exception(f"❌ 清空向量库失败: {str(e)}")


# ============================================================
# 4. 简单的 LLM 接口(可选)
# ============================================================

def generate_answer_with_context(query: str, context: str, model: str = "openai/gpt-5.4") -> str:
    """
    使用上下文生成回答
    通过 OpenRouter 调用大模型
    
    参数:
        query: 用户的问题
        context: 用于回答问题的上下文信息
        model: 使用的大语言模型名称(默认为 openai/gpt-5.4)
        
    返回:
        str: 大模型生成的回答
        
    异常:
        ValueError: 当 OPENROUTER_API_KEY 环境变量未设置时
        Exception: 当调用大模型时发生错误时
    """
    # 检查 API 密钥是否存在
    api_key = os.getenv("OPENROUTER_API_KEY")
    if not api_key:
        raise ValueError("请设置 OPENROUTER_API_KEY 环境变量")
    
    # 构建提示词模板
    prompt = f"""基于以下上下文回答问题。如果上下文中没有相关信息,请说明。

上下文:
{context}

问题:{query}

回答:"""

    with OpenRouter(api_key=api_key) as client:
        response = client.chat.send(
            model=model,  # 使用函数参数传入的模型
            messages=[
                {"role": "user", "content": prompt}
            ]
        )
        return response.choices[0].message.content


# ============================================================
# 5. 主程序
# ============================================================

def main():
    """
    主程序入口,演示 NaiveRAG 的基本功能
    包括:初始化、索引文档、测试查询和交互式查询
    """
    try:
        # 初始化 RAG
        rag = NaiveRAG()

        # 索引文档
        doc_path = os.path.join(os.path.dirname(__file__), "chatWithNavar-2.md")
        if os.path.exists(doc_path):
            rag.index_document(doc_path)
        else:
            print(f"❌ 文件不存在: {doc_path}")
            return
    except Exception as e:
        print(f"❌ 初始化或索引失败: {str(e)}")
        return

    # 测试查询
    print("\n" + "="*60)
    print("🔍 测试查询")
    print("="*60)

    # queries = [
    #     "Gabriel Petersson 的学习方法是什么?",
    #     "什么是递归式知识填充?",
    #     "Unknown Unknowns 是什么意思?",
    #     "四象限学习框架有哪些?"
    # ]

    # for q in queries:
    #     try:
    #         print(f"\n📌 问题: {q}")
    #         print("-" * 40)
    #         result = rag.query(q, top_k=2)

    #         for i, (ctx, dist) in enumerate(zip(result["contexts"], result["distances"])):
    #             print(f"\n[相关块 {i+1}] (距离: {dist:.4f})")
    #             print(ctx[:200] + "..." if len(ctx) > 200 else ctx)

    #         print("\n" + "="*60)
    #     except Exception as e:
    #         print(f"❌ 查询失败: {str(e)}")
    #         print("\n" + "="*60)

    # 交互式查询
    print("\n🤖 进入交互模式 (输入 'quit' 退出)")
    print("-" * 40)

    while True:
        try:
            user_query = input("\n请输入问题: ").strip()
            if user_query.lower() == 'quit':
                break
            if not user_query:
                continue

            result = rag.query(user_query, top_k=3)

            print(f"\n📚 检索到 {len(result['contexts'])} 个相关块:")
            print("-" * 40)

            for i, (ctx, dist) in enumerate(zip(result["contexts"], result["distances"])):
                print(f"\n[块 {i+1}] (相似度距离: {dist:.4f})")
                print(ctx[:300] + "..." if len(ctx) > 300 else ctx)

            # 调用大模型生成回答
            print("\n🤖 AI 回答:")
            print("-" * 40)
            answer = generate_answer_with_context(user_query, result["context_text"])
            print(answer)

        except KeyboardInterrupt:
            print("\n\n👋 再见!")
            break
        except Exception as e:
            print(f"❌ 交互查询失败: {str(e)}")
            print("请检查您的输入或系统配置后重试")


if __name__ == "__main__":
    main()

参考资料

(完)

相关推荐
老歌老听老掉牙4 小时前
PyQt5+Qt Designer实战:可视化设计智能参数配置界面,告别手动布局时代!
python·qt
格鸰爱童话4 小时前
向AI学习项目技能(六)
java·人工智能·spring boot·python·学习
悟空爬虫-彪哥4 小时前
VRChat开发环境配置,零基础教程
python
数据知道4 小时前
《 Claude Code源码分析与实践》专栏目录
python·ai·github·claude code·claw code
曲幽5 小时前
FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了!
python·vue·upload·fastapi·web·blob·chunk·spark-md5
石工记5 小时前
Agent 应用与图状态编排框架LangGraph
python·ai编程
XiYang-DING5 小时前
【Java】二叉搜索树(BST)
java·开发语言·python
赵优秀一一5 小时前
FastAPI 核心
linux·python·fastapi
清水白石0085 小时前
向后兼容的工程伦理:Python 开发中“优雅重构”与“责任担当”的平衡之道
开发语言·python·重构