RAG基础与实践

RAG是神么

大模型就像一个 “学生”,面对 “答题”(生成答案)任务,两种技术对应两种完全不同的 “备考 + 答题” 模式:

核心概念 类比场景 核心逻辑
大模型预训练 学生上学时的基础学习 掌握通用知识(语文、数学、常识),但缺乏针对性知识点
微调(Fine-tuning) 闭卷考试的“考前死记硬背” 把私有/最新数据“背进脑子里”,考试时不查资料,全靠记忆答题
RAG(检索增强生成) 开卷考试的“边查资料边答题” 先检索私有/最新文档片段,再结合基础知识点现场组织答案

二、为什么需要 RAG?—— 闭卷考试的 “三大痛点”

大模型只靠预训练(或仅微调),就像学生只靠知识记忆参加闭卷考试,必然遇到三个问题,这正是 RAG 要解决的核心:

  1. 知识盲区 = 闭卷考试考了 “课外题”
    这就像CDA三级有部分超纲题一样,学生(大模型)只背了课本或是大纲(预训练数据)里的内容,但考试突然考 “公司内部规章制度”“班级专属活动安排”—— 这些大纲和课本里没有的 “课外题”,学生完全不会答。
    对应大模型:对企业内部文档、个人私有数据,业务说明资料等 “课外知识” 一无所知。
  2. 知识过期 = 闭卷考试考了 “新教材内容”
    就像CDA三级一个难度也是它变化很快,跟着时代发展,会不断更新大纲,学生(大模型)背的是 3 年前的旧课本(预训练数据有截断日期,比如 2023 年),但考试考的是 2025 年的新政策、新科技(如transformer ,大模型等)—— 旧知识根本覆盖不到。
    对应大模型:预训练数据之后的 “新鲜事”(如最新产品参数、突发新闻),模型完全不知道。
  3. 幻觉编故事 = 闭卷考试 “不会就瞎编”
    学生(大模型)遇到不会的题,又不能查资料,为了 “交卷” 可能编一个听起来很真的答案(比如 “这个知识点我记得是 XXX”),但实际完全错误。
    对应大模型:面对知识盲区或过期内容时,会生成逻辑通顺但与事实不符的 “伪答案”,即 “幻觉”。
    而 RAG 的核心思路,就是把 “闭卷考试” 改成 “开卷考试”——不让学生硬背所有知识点,而是允许他考试时先查 “权威参考资料”(私有 / 最新文档),再结合自己的基础认知答题:既补了 “课外知识” 和 “新知识”,又因为答案有原文支撑,大幅减少 “瞎编”(幻觉)。

三、详细拆解:RAG(开卷)vs 微调(闭卷)

  1. RAG:开卷考试(边查资料边答题)
    答题流程:
    拿到题目(用户提问)→ 2. 翻参考资料(检索引擎从 “知识库” 中找到与问题相关的文档片段,比如公司内部手册、2025 年新规原文)→ 3. 结合自己的基础知识点(预训练数据)→ 4. 基于 “资料片段 + 基础认知” 生成答案。
    类比场景:
    老师让学生答 “2025 年某城市的垃圾分类新规”—— 学生没背过,但开卷考试允许查政府官网原文,于是先找到新规文档,再结合自己对 “垃圾分类” 的基础理解,整理出答案。
    核心优势(解决闭卷痛点):
    知识不过期:资料库(参考资料)随时更新(比如新增 2025 年新规),无需学生重新 “背”(模型无需重新训练);
    无知识盲区:资料库可以放任意私有内容(如公司内部文档、图片说明,数据库),查得到就答得对;
    幻觉率极低:答案基于 “原文片段” 生成,相当于 “引用资料答题”,不会瞎编。
    缺点:
    答题稍慢:多了 “查资料” 步骤(检索过程),比闭卷答题(直接调用模型)响应时间略长;
    依赖资料质量:如果资料库没有相关内容,还是答不出(相当于开卷考试没找到对应参考资料)。
  2. 微调:闭卷考试(考前死记硬背)
    答题流程:
    考前把新知识点(私有 / 最新数据,如公司手册、2025 年新规)“硬背” 下来(模型通过微调,把数据融入预训练参数)→ 2. 考试时不查资料,直接靠 “记忆”(模型参数)生成答案。
    类比场景:
    老师让学生答 “2025 年某城市的垃圾分类新规”—— 学生考前把新规原文死记硬背下来,考试时不查资料,直接默写 + 整理答案。
    核心优势:
    答题快:没有 “查资料” 步骤,模型直接调用参数生成答案,响应速度比 RAG 快;
    答案更连贯:知识点 “内化” 在模型里,生成的答案比 “拼接资料片段” 更流畅自然。
    缺点(对应闭卷痛点):
    知识容量有限:学生(模型)的 “记忆力”(参数量)有限,背太多知识点会记混(比如把 A 公司手册和 B 公司手册搞混,导致幻觉);
    更新成本高:新规变了(数据更新),学生要重新背(模型要重新微调),耗时耗力(需要大量计算资源);
    知识盲区难覆盖:如果要背的知识点太多(比如 1000 本内部手册),学生根本背不完(模型微调会 “过拟合”,反而效果变差)。

小结对比

对比维度 RAG(开卷) 微调(闭卷)
核心场景 私有/最新数据多、更新频繁(公司文档、新闻、知识库) 数据量小、更新少、需高流畅度(专属话术、特定领域小数据集)
知识更新 简单(直接换资料库) 复杂(重新微调)
幻觉风险 低(基于原文) 中高(基于记忆,易记混)
响应速度 稍慢(多检索步骤) 快(直接生成)
成本 低(无需大量算力) 高(需 GPU,耗算力)
# 1. 安装依赖(已安装可跳过)
!pip install langchain openai faiss-cpu tiktoken   
!pip uninstall -y langchain langchain-core langchain-community
!pip install langchain langchain-community -U -i https://pypi.tuna.tsinghua.edu.cn/simple
!pip show langchain langchain-community langchain-core  langchain*
!pip list show langchain, langchain-classic, langchain-community, langchain-ollama, langchain-qdrant, langchain-text-splitters, langgraph, langgraph-checkpoint, langgraph-prebuilt

langchain基础操作

1.1 提示模板 Prompt Templates

prompt templates 提供了一种预定义,动态注入,模型无关和参数化的提示词生成方式,以便在不同的语言模型之间重用模板

#基础template
from langchain_core.prompts import PromptTemplate

prompt_template  =  PromptTemplate.from_template("介绍一些{country}的历史悠久的{object}")
prompt1 = prompt_template.format(country="西班牙",object="美食")
print(prompt1)
#chat
from langchain_core.prompts  import ChatPromptTemplate

template = ChatPromptTemplate([
    ("system","you are a helpful AI bot .你的名字是{name}."),
    ("human","你好"),
    ("ai","你好,有什么可以帮助你的?"),
    ("human","{user_input}"),
])

prompt_value = template.invoke(
    {
        "name": "haleyprince",
        "user_input": "你叫什么名字?"
    }
)

print(prompt_value)


template

1.2 模型接口

模型接口有两种形式,一种是专用的,例如ollama,openai等,i啊有一种是二级接口,例如通义千问

!pip install -U  langchain-ollama

from langchain_community.llms import Ollama
from langchain_ollama import ChatOllama

OLLAMA_BASE_URL = "http://192.168.52.88:11434"
MODEL_NAME  = "qwen:0.5b"

llm = ChatOllama(
    #model="qwen2.5:7b-instruct-q4_K_M",
    model=MODEL_NAME,
    base_url=OLLAMA_BASE_URL,   # ← 指定远程地址
    temperature=0.6
)
# 适用chatollaa
from langchain_ollama import ChatOllama
from langchain_core.messages import AIMessage  

# llm = ChatOllama(
#     model="qwen2.5:0.5b",
#     temperature=0.8,
# )

messages = [
    ("system","你是一名专业的翻译家,可以将用户的英文翻译为中文。"),
    ("human","This is a framework for developing applications powered by large language models (LLMs)."),
]

aimsg = llm.invoke(messages) #invoke() 是同步阻塞的:代码执行到这一行时,会暂停等待 Ollama 返回结果,直到拿到回答后才继续执行下一行

print(aimsg)
#流式
stream = llm.stream(messages)
answer = next(stream)
for chunk in stream:
    answer = answer + chunk
    print(answer.content)
    
# 通译千问
import dashscope

API_KEY = "你的apikey"

from langchain_community.chat_models import ChatTongyi

tongyi_chat = ChatTongyi(
    #model="qwen2.5:7b-instruct-q4_K_M",
    model="qwen-turbo", #模型名是线上的名称与本地不一样
    api_key=API_KEY,
    temperature=0.6
)

messages = [
    ("system","你是一名专业的翻译家,可以将用户的英文翻译为中文。"),
    ("human","This is a framework for developing applications powered by large language models (LLMs)."),
]

aimsg = tongyi_chat.invoke(messages) #invoke() 是同步阻塞的:代码执行到这一行时,会暂停等待 Ollama 返回结果,直到拿到回答后才继续执行下一行

print(aimsg.content)

1.3 聊天记录

#定义聊天记录函数
from langchain_core.chat_history import BaseChatMessageHistory ,InMemoryChatMessageHistory  #获取和存储聊天记录
from langchain_core.runnables.history import RunnableWithMessageHistory

store = {}

def get_session_history(session_id: str)->BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]

with_message_history = RunnableWithMessageHistory(tongyi_chat,get_session_history)


#必须设置sessionid
config = {"configurable": {"session_id": "abc"}}

#另一种传入tuple信息的方式

from langchain_core.messages import SystemMessage
from langchain_core.messages import HumanMessage
#每次用户的提问用invoke注入,并告诉是哪个id
response = with_message_history.invoke(
    [SystemMessage(content="you are a helpful AI bot ."),
     HumanMessage(content="您好,我叫一辉")],
    config =config,
)

response.content
response = with_message_history.invoke(
    [HumanMessage(content="我叫什么")],
    config=config,
)
response.content
store

1.4 chains

# 使用LCEL
from langchain_core.prompts import PromptTemplate
from langchain_ollama import ChatOllama

prompt_template = PromptTemplate.from_template("介绍一些{country}的历史悠久的{object}")


llm = ChatOllama(
    #model="qwen2.5:7b-instruct-q4_K_M",
    model=MODEL_NAME,
    base_url=OLLAMA_BASE_URL,   # ← 指定远程地址
    temperature=0.6
)

chain = prompt_template | llm #LCEL 连接langchain的功能

result  = chain.invoke({"country": "中国", "object":'名胜古迹'})

print(result.content)


2 LangChain在RAG中的基础使用

2.1 文档加载

先要将各种形式的内容资转为文本

文档格式 → 文本提取工具

场景 推荐工具 / 方案 一句话说明 开源地址
PDF → Markdown marker 版面还原好,公式、表格一并转 MD https://github.com/VikParuchuri/marker
PDF → 纯文本 pdfplumber 轻量、按页精准提取,支持表格坐标 https://github.com/jsvine/pdfplumber
Word (.doc/.docx) python-docx 无需装 Office,直接读段落/表格 https://github.com/python-openxml/python-docx
Excel (.xlsx) pandas + Awesome-LLM-Tabular 先读成 DataFrame,再让 LLM 做汇总/问答 pandas 官方 + https://github.com/johnnyhwu/Awesome-LLM-Tabular
图片 (JPG/PNG/PPT) PaddleOCR 离线多语言 OCR,一行命令出文本;也可调用 GPT-4 Vision https://github.com/PaddlePaddle/PaddleOCR
YouTube 视频转文字 youtube-dl + 自动字幕 下载视频/字幕,一步到位 https://github.com/yt-dl-org/youtube-dl
B 站视频转文字 bili2text 输入 BV 号,直接输出全文 https://github.com/lanbinshijie/bili2text
##Load文档
from langchain_community.document_loaders import TextLoader

file_path = "./doc.txt"
loader=TextLoader(file_path,encoding="utf-8")
docs = loader.load()

docs

2.2 文本分割

将文本切分为chunks

方法 一句话原理 优点 缺点 代码示例
固定长度 按字符数硬切 最快、零依赖 易断句、断词 [text[i:i+512] for i in range(0,len(text),512)]
句子级 按标点切→句列表 语义完整 标点不规范就崩 nltk.sent_tokenize(text)
递归分块 按优先级分隔符逐级尝试,直到满足长度 长度可控,又能保留段落/句子 比固定长度慢一丢丢 RecursiveCharacterTextSplitter(...).split_text(text)
语义分块 先算句向量,再按相似度聚合并切 块内语义最连贯 需 GPU/模型,耗时 SemanticChunker(encoder, max_chunk_tokens=256).chunks(text)
#1. 固定长度切块(最轻量)
def fixed_chunks(text, size=512):
    return [text[i:i+size] for i in range(0, len(text), size)]

print(fixed_chunks("一二三四五六七八九十"*100, 20))
#2按分隔符切块(csv、日志最常见)
def delim_chunks(text, sep="\n"):
    return text.split(sep)

print(delim_chunks("a|b|c", sep="|"))
import nltk
import os

# 添加路径到NLTK路径列表的开头(优先级最高)
nltk.data.path.insert(0, '/kaggle/kaggle/data/nltk_data')

# 或者清空现有路径,只使用指定路径
nltk.data.path.clear()
nltk.data.path.append('/kaggle/kaggle/data/nltk_data')

# 验证配置
try:
    from nltk.tokenize import sent_tokenize
    print("✓ NLTK配置成功")
except LookupError as e:
    print(f"✗ NLTK配置失败: {e}")
import nltk, ssl, os


def sent_chunks(text):
    return nltk.sent_tokenize(text, language="english")

print(sent_chunks("This is the first sentence. This is the second sentence! Is this the third sentence? Yes, it is indeed."))

#4. 递归字符切块(LangChain 默认,长度+分隔符双保险)
from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
        chunk_size=100,
        chunk_overlap=20,
        separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""]
)
print(splitter.split_text("一二三四五六七八九十。"*10))

from langchain_text_splitters import RecursiveCharacterTextSplitter

#5 使用LangChain的语义分割器
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=20,
    chunk_overlap=10,
    separators=["\n\n", "\n", " ", ""]
)

text = "骑车时需要眼、手、脚协调配合,同时保持平衡,这对前庭系统和本体感觉都是很好的训练。掌握平衡车或自行车能极大提升孩子的自信心。"
chunks = text_splitter.split_text(text)
print(chunks)

快速 demo → 1 或 2
通用 RAG → 4
要求“一句不散” → 3
高端语义边界 → 5
5 种方法已覆盖 99% 场景,按需取用即可。

"""
Args:
    file_path: 文件路径
    size: 块大小,默认512
    overlap: 重叠大小,默认50

Returns:
    分块后的文本列表
"""
file_path = "./doc.txt"
# 检查文件是否存在
if not os.path.exists(file_path):
    raise FileNotFoundError(f"文件不存在: {file_path}")

# 读取文件内容
try:
    with open(file_path, 'r', encoding='utf-8') as file:
        text = file.read()
except UnicodeDecodeError:
    # 如果UTF-8解码失败,尝试其他编码
    with open(file_path, 'r', encoding='gbk') as file:
        text = file.read()

# 创建分割器
splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,
    chunk_overlap=20,
    separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""]
)

# 分割文本
chunks = splitter.split_text(text)
print(chunks)
##语义分块(Sentence-BERT 版)
import os
import re

def semantic_chunking_simple(file_path, max_chunk_size=512):
    """
    简化版语义分块,避免numpy兼容性问题
    """
    # 读取文件
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"文件不存在: {file_path}")
    
    try:
        with open(file_path, 'r', encoding='utf-8') as file:
            text = file.read()
    except UnicodeDecodeError:
        with open(file_path, 'r', encoding='gbk') as file:
            text = file.read()
    
    # 按语义边界分割(句子级别)
    sentences = re.split(r'([。!?!?])', text)
    sentences = [sentences[i] + (sentences[i+1] if i+1 < len(sentences) else '') 
                 for i in range(0, len(sentences)-1, 2)]
    sentences = [s.strip() for s in sentences if s.strip()]
    
    # 智能合并句子到块
    chunks = []
    current_chunk = ""
    
    for sentence in sentences:
        # 如果加上当前句子会超过最大大小,则开始新块
        if len(current_chunk + sentence) > max_chunk_size and current_chunk:
            chunks.append(current_chunk.strip())
            current_chunk = sentence
        else:
            current_chunk += sentence
            # 如果当前块已经很大,也开启新块
            if len(current_chunk) > max_chunk_size:
                # 找到合适的位置分割
                chunks.append(current_chunk.strip())
                current_chunk = ""
    
    # 添加最后一个块
    if current_chunk.strip():
        chunks.append(current_chunk.strip())
    
    return chunks

# 执行分块
try:
    file_path = "./doc.txt"
    chunks = semantic_chunking_simple(file_path, max_chunk_size=512)
    print(f"分块完成,共生成 {len(chunks)} 个块")
    print("\n分块结果:")
    for i, chunk in enumerate(chunks):
        print(f"块 {i+1}: {chunk[:100]}{'...' if len(chunk) > 100 else ''}")
        print("---")
except Exception as e:
    print(f"错误: {e}")
from langchain_community.embeddings.dashscope import DashScopeEmbeddings 

embedding = DashScopeEmbeddings(
    model="text-embedding-v2",
    dashscope_api_key=API_KEY
    
)

text = "阿里dashscope平台上可用的模型https://help.aliyun.com/zh/model-studio/getting-started/models"
query_result = embedding.embed_query(text)
query_result
#隐藏层节点数
len(query_result)

向量数据库

量数据库是专为存储、管理和高效检索高维向量数据而设计的新型数据库系统,广泛应用于生成式AI、语义搜索、推荐系统、图像识别和RAG(检索增强生成)等场景。与传统关系型数据库依赖精确匹配不同,向量数据库通过计算向量之间的相似度(如余弦相似度、欧氏距离)来实现“近似最近邻”(ANN, Approximate Nearest Neighbor)搜索,在海量非结构化数据中快速定位最相关的结果。

随着大模型技术的爆发,向量数据库已成为支撑AI应用的核心基础设施之一。其核心优势在于能够将文本、图像、音频等非结构化数据通过Embedding模型转化为向量,并在高维空间中进行语义级检索,从而实现“语义理解”而非关键词匹配

市场格局与定位

2025 年是向量数据库从“概念验证”向“企业级部署”迈进的关键一年。市场主要分为 托管 SaaS 和 自托管开源 两大阵营。
pinecone
定位:AI 原生云数据库的标杆。提供全托管服务,无需维护集群,适合企业快速落地。优势在于高可用、
自动扩容和极低的运维门槛,但劣势是成本较高,且扩展性受限于厂商套餐

Milvus
定位:当前最成熟、功能最全的开源向量数据库。由 Zilliz 维护,支持多种索引算法(IVF、HNSW、DISKANN 等),
在企业级生产环境中表现最稳健。优势是生态丰富、社区活跃;劣势是部署和调优相对复杂,学习曲线较陡

weaviate
定位:一款将向量搜索与图数据库特性相结合的产品。它不仅存储向量,还支持结构化数据(Schema)和 GraphQL 接口,内置模块化的Text2Vec模块,适合需要复杂关系查询的应用

底层算法

向量数据库的核心竞争力之一在于其底层的近似最近邻(ANN)索引算法,这些算法牺牲少量精度以换取数量级的性能提升。常见算法包括:

HNSW (Hierarchical Navigable Small World)
原理:构建多层图结构,高层用于快速跳转,底层精确搜索。
特点:召回率高、延迟低,是当前最主流的ANN算法之一,被 Milvus、Qdrant、Weaviate 广泛采用

IVF (Inverted File Index)
原理:先聚类(如K-Means),查询时只搜索最近簇内的向量。
特点:适合大数据集,但对分布不均的数据敏感,常与PQ(乘积量化)结合使用

FLAT (暴力搜索)
原理:计算查询向量与所有向量的距离。
特点:精度最高,但时间复杂度O(n),仅适用于百万级以下数据

PQ / SQ / BQ (向量量化技术)
原理:将高维向量压缩编码,减少存储和计算开销。
应用:Qdrant 和 Weaviate 支持标量/二进制量化,显著提升吞吐量

主流产品对比

数据库 类型 核心优势 适用场景 局限性
Pinecone 商业 SaaS 全托管、自动扩缩容、低延迟、混合搜索 实时推荐、企业级 RAG 闭源、定制性差、长期成本高
Milvus 开源 + 云服务 十亿级向量、存算分离、分布式、GPU 加速 大规模语义搜索、AI 中台 需 K8s,部署运维门槛高
Chroma 开源嵌入式 轻量、Python/JS 原生 API、快速原型 本地实验、MVP 验证 单机有限,无法横向扩展
PgVector PG 扩展 SQL 无缝集成、事务一致、混合查询 中小规模、已有 PG 业务 亿级后性能衰减,无原生分布式
Elasticsearch 搜索扩展 全文+向量混合、成熟监控 日志语义检索、多模态搜索 ANN 性能弱于专用库,资源开销大
Redis Stack 内存扩展 亚毫秒延迟、实时更新、标签过滤 实时推荐、会话缓存 内存贵、持久化/备份需额外设计

选型建议:
若追求极致易用性和稳定性,且预算充足,可选择 Pinecone 或 Weaviate Cloud。
若需要大规模部署和企业级控制力,Milvus 是首选,尤其适合已有K8s环境的企业。
若用于快速验证或小型项目,Chroma 和 FAISS 是理想起点。
若已使用 PostgreSQL / Elasticsearch / Redis,可通过插件形式平滑引入向量能力,降低迁移成本。

chroma数据库简介

Chroma 是专为 AI 开发者设计的轻量级向量数据库,像操作列表一样简单,特别适合 Python 环境(如 LangChain、LlamaIndex)和小规模数据实验

Chroma 天然支持向量(Embedding)。你可以直接存放 文本、图像 或 音频 的向量表示,省去手动计算相似度的麻烦
默认是 本地模式(文件持久化)。不像 Milvus 需要部署服务,直接在你的机器上运行,像普通 Python 库一样使用
不仅能查相似,还能按 类别、标签 等属性过滤,功能媲美 SQL 数据库的 WHERE 条件 ,就像like一样的

使用示例 :
Chroma 的 API 设计得非常优雅,通常只需要三步:创建 → 写入 → 查询。

安装数据库

docker-compose 安装

version: "3.8"

services:
  chromadb:
    image: chromadb/chroma:latest
    container_name: chromadb
    ports:
      - "8000:8000"
    volumes:
      - /mnt/e/data/chroma_data:/chroma/chroma   # 宿主机路径按需修改
    environment:
      - IS_PERSISTENT=TRUE
      - ANONYMIZED_TELEMETRY=TRUE

运行启动 docker-compose up

连接测试


```python
import chromadb
client = chromadb.HttpClient(host='192.168.52.88', port=8000)
try:
    client.get_version()
    print("连接成功")
except Exception as e:
    print(f"连接失败: {e}")

2. 创建数据库实例

创建 Client 和 Collection
Chroma 将 Collection 理解为一张表。这里我们创建一个名为 my_collection 的集合。

import chromadb
# 创建本地客户端(默认保存在 .chromadb 目录)
client = chromadb.Client()
# 创建一个集合(类似于 SQL 的表)
collection = client.create_collection(name="my_collection")

#使用http调用客户端
import chromadb

# 改为 HTTP 客户端,默认端口 8000
client = chromadb.HttpClient(host="192.168.52.88", port=8000)

# 复用或新建集合
collection = client.get_or_create_collection(name="my_collection")

3. 写入数据

添加文档(Document)
每一条记录包含 ids(唯一标识)、documents(文本内容)和 metadatas(元数据,可选)。

# 添加数据到集合
collection.add(
    documents=["这是关于菠萝的文档", "这是关于橙子的文档"],
    ids=["id1", "id2"],  # 唯一标识符
    metadatas=[{"category": "水果"}, {"category": "水果"}]  # 可选的过滤字段
)

4. 进行向量查询

语义搜索(Semantic Search)
输入一段查询文本,Chroma 会自动将其转化为向量(Embedding),并返回语义上最相似的文档。

# 查询 "菠萝" 相关的文档
results = collection.query(
    query_texts=["菠萝是什么"],  # Chroma 会自动调用嵌入模型
    n_results=2,                # 返回最近邻的数量
    where={"category": "水果"}    # 可选的元数据过滤
)
print(results)
# 结果结构: {'ids': [...], 'documents': [...], 'distances': [...]}

向量数据库Qdrant

1. 安装

① 装 Qdrant(Docker 最快)
docker run -d --name qdrant -p 6333:6333 -v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant

docker-compose方式
version: "3.8"
services:
qdrant:
image: qdrant/qdrant:v1.14.1 # 2025-12 官方稳定版
container_name: qdrant
restart: unless-stopped
ports:
- "6333:6333" # REST & Web UI
- "6334:6334" # gRPC
environment:
# 如需认证,取消下行注释并自行更换密钥
# - QDRANT__SERVICE__API_KEY=your_very_strong_key
- QDRANT__CLUSTER__ENABLED=false # 单机模式
- QDRANT__LOG_LEVEL=INFO
- QDRANT__STORAGE__MAX_OPEN_FILES=32768
- QDRANT__PERFORMANCE__MAX_SEARCH_THREADS=4
ulimits:
nofile:
soft: 32768
hard: 32768
deploy:
resources:
limits:
cpus: '4'
memory: 8G

② 装 Python 客户端

docker启动后
webui http://localhost:6333/dashboard

image.png

from langchain_community.embeddings.dashscope import DashScopeEmbeddings 

embeddings = DashScopeEmbeddings(
    model="text-embedding-v2",
    dashscope_api_key=API_KEY
    
)

text = "阿里dashscope平台上可用的模型https://help.aliyun.com/zh/model-studio/getting-started/models"
query_result = embedding.embed_query(text)
#创建集合collection
from langchain_qdrant import QdrantVectorStore
from qdrant_client import QdrantClient
from qdrant_client.http.models import Distance,VectorParams

#client=QdrantClietn(":memory:")
client = QdrantClient(host="192.168.52.88",port=6333)

client.create_collection(
    collection_name="demo_collection1",
    vectors_config=VectorParams(size=1536,distance=Distance.COSINE),
)

vector_store=QdrantVectorStore(
    client=client,
    collection_name="demo_collection1",
    embedding=embeddings,
)


image.png
#add documents
from langchain_core.documents import Document

doc1  = Document(page_content="我是文档一")
doc2 = Document(page_content="我是文档二")
doc3  = Document(page_content="我等下要被删除")
documents = [doc1,doc2,doc3]

vector_store.add_documents(documents=documents)

image.png
#Delete documents
vector_store.delete(ids=['06dec5b0b0fc41bdb8383a4d78d41dbe'])

#search
results = vector_store.similarity_search(query="文档一",k=1)  #返回几个,默认4
results
#uss as retriever
retriever = vector_store.as_retriever(search_kwargs={"k": 1})
retriever.invoke("文档一")
## MMR (maximal marginal reievance) 搜索

# 配置 MMR 检索器
retriever = vector_store.as_retriever(
    search_type="mmr",
    search_kwargs={
        "k": 1,           # 返回前 k 个结果
        "fetch_k": 1,    # 先从向量库中获取 20 个最相似的文档,再从中选出 k 个
        "lambda_mult": 0.7 # MMR 的平衡参数,值越大越注重相关性,越小越注重多样性
    }
)

# 执行查询
query = "一"
docs = retriever.invoke(query)
for doc in docs:
    print(doc.page_content)

RAG pipeline示例案例

import os
from dotenv import load_dotenv
load_dotenv()

from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader  # 可替换为 PyPDFLoader、DirectoryLoader 等
from langchain_community.vectorstores import Qdrant
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.llms import Ollama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from qdrant_client import QdrantClient
# === 4. 构建 Qdrant 向量库(新版兼容写法)===

from qdrant_client import QdrantClient
from qdrant_client.http import models as rest

# 手动管理集合(避免 force_recreate 问题)
client = QdrantClient(host="192.168.52.88", port=6333)
collection_name = "doc1"

if client.collection_exists(collection_name):
    client.delete_collection(collection_name)

client.create_collection(
    collection_name=collection_name,
    vectors_config=rest.VectorParams(size=1536, distance=rest.Distance.COSINE)
)

# ✅ 关键:from_documents 只传 url(字符串),不传 client!
vectorstore = Qdrant.from_documents(
    documents=chunks,
    embedding=embeddings,
    url="http://192.168.52.88:6333",   # 带协议!
    collection_name=collection_name
)

retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

 
# === 5. 初始化 Ollama LLM ===
ollama_base_url = "http://192.168.52.88:11434"
llm = Ollama(
    base_url=ollama_base_url,
    model="qwen:0.5b",      # 可替换为 qwen:14b、qwen:32b 等
    temperature=0.3,
    timeout=300,            # 防止长文本生成超时
)

# === 6. Prompt 模板 ===
prompt_template = """
你是一个专业、准确的 AI 助手。请根据以下提供的上下文信息回答用户的问题。
- 如果上下文中有明确答案,请直接引用或总结。
- 如果上下文中没有相关信息,请回答:“根据现有资料无法回答该问题。”
- 不要编造信息。

上下文:
{context}

问题:
{question}

回答:
"""
prompt = ChatPromptTemplate.from_template(prompt_template)


# === 7. 辅助函数:格式化检索结果 ===
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

# === 8. RAG Chain(标准方式)===
rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)


# === 9. 【新增】显式 Generate 函数(便于调试或自定义)===
# def generate_with_context(question: str, k: int = 3) -> str:
#     """
#     手动检索 + 调用 Ollama 生成答案
#     """
#     # 1. 直接使用 vectorstore 检索(支持 k)
#     docs = vectorstore.similarity_search(question, k=k)
#     context = "\n\n".join(doc.page_content for doc in docs)
    
#     # 2. 构造 prompt
#     formatted_prompt = prompt_template.format(context=context, question=question)
    
#     # 3. 调用大模型
#     response = llm.invoke(formatted_prompt)
    
#     return response.strip()
def generate_with_context(question: str, k: int = 3) -> str:
    """
    手动检索 + 调用 Ollama 生成答案
    """
    # 1. 动态创建一个 k 值可变的 retriever
    dynamic_retriever = vectorstore.as_retriever(search_kwargs={"k": k})
    docs = dynamic_retriever.invoke(question)
    
    context = format_docs(docs)
    
    # 2. 构造 prompt
    formatted_prompt = prompt_template.format(context=context, question=question)
    
    # 3. 调用 Ollama generate
    response = llm.invoke(formatted_prompt)
    
    return response.strip()
    
# === 10. 测试 ===
if __name__ == "__main__":
    question = "请简要说明文档中的核心内容?"
    
    print("【方式1:RAG Chain】")
    answer1 = rag_chain.invoke(question)
    print("回答:\n", answer1)
    
    print("\n" + "="*50 + "\n")
    
    print("【方式2:显式 Generate】")
    answer2 = generate_with_context(question)
    print("回答:\n", answer2)
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容