大模型知识-使用本地Embedding模型+Chromadb构建知识库(构建RAG完整链路)

 RAG 完整链路:

文档加载 → 文本切分 → 本地 Embedding 编码 → Chroma 向量入库→ 用户提问 → 问题 Embedding → 向量检索召回 TopN 知识片段→ 构造 Prompt(问题 + 召回上下文) → 本地大模型生成答案;

主要分为两大流程:一是知识库构建流程;二是问题检索+生成结果,即RAG检索+生成;

1.知识库构建流程:

  • 文档入库过程
    • 文档读取
    • 文档切块(chunk)
    • 进行本地 Embedding 模型向量化
    • 入库ChromaDB向量库
  • 根据输入信息查询
  • 检索向量数据库返回结果

image

注:文档入库使用的embedding模型与问题向量化使用的embedding模型要保持一致

2.使用本地Embedding模型+Chromadb实践过程

使用在线/本地Embedding模型+Chromadb向量库+Agently框架 -> 知识库

1.文档读取

# 1.读取文件
from PyPDF2  import PdfReader
from agently  import Agently
from agently.integrations.chromadb  import ChromaData, ChromaEmbeddingFunction
from chromadb  import Client

with  open ( "./prd/xx需求文档.pdf" , "rb" ) as f:
    reader  = PdfReader(f)
    context  = "\n\n" .join( [page.extract_text()  for page  in reader.pages])

2.文档内容切片

说明:

  • 因受到向量化模型向量化能力限制,需要控制向量化的内容块长度
  •  作用:信息聚焦便于应用、关键信息清晰便于检索
  • 切分正文:是为了保证高质量内容能够为后续智能工作流程服务
  • 切分常用的工具:
    • LangChain TextSplitters
    • 根据需要自行撰写处理脚本(与业务逻辑有关)
  • 所谓的切分方式可以是:
    • 按长度切分
    • 按段落切分
    • 按内容切分
    • 不仅仅是切分,还可以是信息二次组织(如汇总、重拼装等)
# 2.文件切片,使用langchain_text_split
from langchain_text_splitters  import RecursiveCharacterTextSplitter

# 创建切片对象
splitter  = RecursiveCharacterTextSplitter(
    separators = [ "\n\n" , "\n" ], #使用切分的符号
    chunk_size = 1000 , #期望切块的长度
    chunk_overlap = 100 ,  #允许超过的最大长度
    keep_separator = True #是否保留分隔符
)

chunk_data  = splitter.split_text(context)

print (chunk_data)

3.向量化并入库

  说明:向量化主要由本地Embedding向量化模型完成,选择模型的要点:

  • 考虑对应的语言、场景,选择更合适的模型(如英文场景、中文场景、代码场景)
  • 考虑上下文窗口长度(512窗口就只能处理400字左右内容,8k窗口能做的事情就很多)
  • 考虑运算能力允许
  • Chromadb的数据结构:
    • documents 文本内容
    • metadatas 元数据:就是描述「切片文本 chunk」本身的附属信息,不是 chunk 正文内容,用来标记、分类、过滤文档片段。在 Chroma / Milvus 里,元数据是可以直接存入向量库,检索时用来做过滤条件
    • ids 文档chunk ids:在 Chroma 中,可以用这个 id 做删除、更新、按 ID 查询。
    • embeddings 向量
# 3.向量化并入库
improt chromadb
from chromadb import embedding_functions
# 1. 初始化客户端,持久化存储到 ./chroma_db
client = chromadb.PersistentClient(path="./chroma_db") # 2.创建embedding函数,使用本地embedding模型
embedding_function = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="BAAI/bge-small-zh-v1.5")

#3.创建集合
collection = client.get_or_create_collection(
name = "swagger" ,
metadata = { "hnsw:space" : "cosine" },
embedding_function = embedding_function )

#4.添加数据,制作入库数据 doc_data = ChromaData([ { "document" :chunk } for chunk in chunk_data ]) collection.add(**doc_data.get_kwargs())

 

4.根据问题检索(RAG向量检索阶段),返回查询结果

RAG 流程的检索阶段:拿到用户问题,从向量库召回最相关的知识片段,后续把召回片段 + 用户问题一起交给大模型做答案生成。

1) 问题向量化
  • 不涉及到复杂数据结构,只需要对问题进行向量化,即把自然语言的问题送入本地embedding模型,转换为向量(query embedding)
  • 原理:本地embedding模型把语义信息编码成多维浮点数向量,相同/相近语义的文本,在向量空间中位置靠近。
  • 目的:计算“问题向量和向量库中已存储的文档索引向量”之间的距离(向量距离越近,概念越接近)
  • ✅ 优势:使用本地 Embedding,文本不会上传外网,数据私有化,无 API 调用费用。
2) 设置查询方案,获取结果

调用collection.query()执行向量检索,两个核心参数:

  • Top N(n_results):召回数;

      向量库返回语义最匹配的前N个文本块。N越大,召回更多候选知识,提高找到正确信息的概率(提升召回率);但 N 太大,会把无关文本也带入上下文,增加大模型负担、引入噪声。

  • Distance:向量距离;

      Chroma 返回的是距离值(不是余弦相似度),用来衡量准确性。距离值越小,代表两个向量越相似,文本语义越匹配。

 

    注意区分:向量余弦相似度是一种衡量两个向量方向相似程度的度量方法。它通过计算两个向量夹角的余弦值来确定;

                     余弦相似度范围[-1,1],越接近 1 越相似;余弦距离 = 1 - 余弦相似度,范围[0,2],距离越接近 0 越相似。

  ⚠️ ChromaDB 默认返回余弦距离(1 - 余弦相似度),不是余弦相似度!

  • 余弦相似度 = 0.9 → 余弦距离 = 0.1(非常匹配)
  • 余弦相似度 = 0.5 → 余弦距离 = 0.5(一般匹配)
  • 余弦相似度 = 0 → 余弦距离 = 1(完全不相关)

完整代码示例:

# 4.信息查询:用户问题 -> 向量化 -> Chroma向量检索
# query_texts:用户原始问题文本,Chroma会自动使用绑定的本地Embedding模型做向量化
# n_results=3:Top3,召回相似度最高的3条文档片段
user_query  = "动态召回流程" results  = collection.query(query_texts  = [ user_query ],n_results   = 3 ) # 「入库」用的是本地指定模型时,非chromaDB的默认 Embedding 函数 # query_embedding = embedding_model.encode([user_query]).tolist() # results = collection.query(query_embeddings=query_embedding, n_results  = 3 ) # 遍历本次查询返回的3条召回结果 for index, document  in enumerate (results[  'documents' ][  0 ]): print (f    "\n\n编号:{index} \n" ) print (document) print (    "\n=====================\n" ) print (f    "匹配的距离:{results['distances'][0][index]}" )
  •  返回结果结构说明:
    • results['documents'][0]:第 1 个查询语句对应的召回文本列表;支持批量 query_texts,所以是二维数组。
    • results['distances'][0]:对应每一条文档的向量距离,顺序和 documents 一一对应。
    • 扩展:还可以取出metadatas元数据,例如文档来源、页码、切分块 ID 等,方便溯源。

注意:

如果入库时,指定了本地特定模型,如bge-small-zh-v1.5(512 维),查询时也要走同一个本地模型的 query_embeddings,而不是 query_texts ;因ChromaDB 会调用它自带的默认 Embedding函数 all-MiniLM-L6-v2(384 维),两边维度对不上,会报「expecting 512, got 384」错误。

运行结果如下:

image

5. 组装 Prompt + 调用本地大模型生成最终答案(RAG 生成阶段)

承接上一步向量检索:我们已经拿到了 TopN 相关知识库片段,这一步就是把用户问题 + 检索出来的上下文组装成提示词,交给本地大模型,让模型基于知识库内容回答,避免模型幻觉。

5.1 核心思路

RAG 的核心思想:不让大模型凭自己的记忆回答,而是把检索到的参考资料塞给模型,要求模型只基于提供的资料作答。

流程: 用户提问 → Embedding 向量检索(上一节)→ 获取相关文档片段 → 拼接上下文 Prompt → 本地大模型推理输出答案

5.2 Prompt 模板设计

推荐 RAG 专用提示词模板,约束模型行为,减少幻觉

你是知识库问答助手,请严格根据下面【参考资料】中的内容回答用户问题。
如果参考资料中没有相关信息,请直接回答:知识库中没有找到相关内容,不要编造信息。

【参考资料】
{context_content}

【用户问题】
{user_query}

请给出清晰简洁的回答:

5.3 代码实现:拼接检索结果,组装 Prompt

 1 # ========== 1. 从检索结果中,把多条召回文档拼接成上下文字符串 ==========
 2 # results 是上一步 chroma 查询返回的结果
 3 context_list = results['documents'][0]
 4 # 使用分隔符把多条知识库片段合并
 5 context_content = "\n\n".join(context_list)
 6 
 7 # 用户原始问题
 8 user_query = "动态召回流程"
 9 
10 # ========== 2. 组装RAG提示词 ==========
11 prompt_template = """
12 你是知识库问答助手,请严格根据下面【参考资料】中的内容回答用户问题。
13 如果参考资料中没有相关信息,请直接回答:知识库中没有找到相关内容,不要编造信息。
14 
15 【参考资料】
16 {context_content}
17 
18 【用户问题】
19 {user_query}
20 
21 请给出清晰简洁的回答:
22 """
23 # 填充模板
24 final_prompt = prompt_template.format(context_content=context_content, user_query=user_query)
25 print("===== 送入大模型的完整Prompt =====")
26 print(final_prompt)
View Code

运行结果如下:

image

5.4 调用本地大模型推理(示例,以 llama.cpp/transformers 本地模型为例)

两种常见本地部署方式,任选其一:

方式 1:transformers 加载本地大模型(HuggingFace)

from transformers import AutoTokenizer, AutoModelForCausalLM

# 加载本地模型与tokenizer(替换成你的本地模型路径)
model_path = "./models/your-local-llm"
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModelForCausalLM.from_pretrained(
    model_path,
    device_map="auto", # 自动分配GPU/CPU
    trust_remote_code=True
)

# tokenize
inputs = tokenizer(final_prompt, return_tensors="pt").to("cuda")

# 本地推理生成
outputs = model.generate(
    **inputs,
    max_new_tokens=512, # 最多生成多少输出token
    temperature=0.1,    # 越低,回答越确定、随机性越小,知识库问答推荐0.1~0.3
)

# 解码输出
answer = tokenizer.decode(outputs[0], skip_special_tokens=True)
# 只取出模型新生成的部分(去掉prompt原文)
answer = answer[len(final_prompt):]
print("\n===== 大模型返回答案 =====")
print(answer)
View Code

说明:

1)model_path 这个路径,指向的是一个本地文件夹,里面装着一整套模型文件(config.json、tokenizer 文件、.safetensors 权重等)。核心是:需先用 HuggingFace 把模型下载到这个文件夹,路径才有意义。下面三种取法,任选其一:

方法一:huggingface-cli download(推荐,下载到指定目录)

pip install -U huggingface_hub

# 把仓库 Qwen/Qwen2.5-1.5B-Instruct 整体下载到本地文件夹
huggingface-cli download Qwen/Qwen2.5-1.5B-Instruct \
    --local-dir ./models/Qwen2.5-1.5B-Instruct

下载完,model_path 填那个文件夹:model_path = "./models/Qwen2.5-1.5B-Instruct"

方法二:Python 里用 snapshot_download

from huggingface_hub import snapshot_download

snapshot_download(
    "Qwen/Qwen2.5-1.5B-Instruct",
    local_dir="./models/Qwen2.5-1.5B-Instruct",
)

local_dir 参数 = 你想放的路径,也就是后面 model_path 要填的路径。

方法三:直接填「仓库名」,不需要手动下载

其实 from_pretrained 支持直接传 HuggingFace 上的仓库 ID,会自动下载到默认缓存:

model_path = "Qwen/Qwen2.5-1.5B-Instruct"   # 组织/模型名,不是本地路径

下载一次后模型缓存在 ~/.cache/huggingface/hub/,后续运行不重复下载。

缺点是缓存目录带 snapshots/<hash> 哈希子目录、路径不直观,所以想「固定一个本地路径」还是用方法一/二。

 

2)inputs = tokenizer(final_prompt, return_tensors="pt").to("cuda")需要 NVIDIA GPU 才能跑;没有显卡的话需改成 .to("cpu")

 

运行结果如下:

image

方式 2:llama.cpp 轻量本地模型(适合 CPU 低资源机器)

from llama_cpp import Llama

# 加载本地GGUF量化模型
llm = Llama(
    model_path="./models/your-model.gguf",
    n_ctx=2048,  # 上下文窗口大小,要容纳prompt+回答
    n_threads=4
)

output = llm.create_completion(
    prompt=final_prompt,
    max_tokens=512,
    temperature=0.1,
    stop=["</s>"],
    echo=False
)

answer = output["choices"][0]["text"]
print("\n===== 大模型返回答案 =====")
print(answer)
View Code

 

5.5 重要工程要点(RAG 避坑)

  1. 上下文窗口限制 :本地大模型有最大上下文长度限制。召回的文档片段太多,会超出模型上下文窗口,造成截断。

解决:控制 TopN 数量,或者做上下文压缩 / 重排。

  1. 幻觉控制: Prompt 里必须加上约束:找不到信息就如实告知,禁止编造。temperature调低,减少模型自由发挥。
  2. 检索后重排(进阶优化,可选) 当前只做向量召回,语义相近但无关的文本也可能被召回。可以增加重排模型(Reranker),对 TopN 结果做二次打分筛选,提升送入大模型的资料质量。

链路升级:向量检索 Top5 → Reranker 重排,筛选 Top3 → 组装 Prompt 送入大模型

 

3.文档与索引的关系:

知识库是一个存储和管理结构化、半结构化或非结构化信息的系统,

而索引是为了快速检索这些信息而建立的结构化数据映射。

在实际应用中,索引就像图书馆的目录系统,帮助用户在海量数据中迅速定位目标内容。没有索引的知识库,检索效率会大幅下降,尤其在百万级甚至更大规模的数据场景中。

核心区别

  • 知识库:存储全部知识条目(文档、图片、视频、结构化数据等),包含内容本身及其元数据。

  • 索引:对知识库内容的关键词、向量特征、分类标签等进行组织,以支持高效搜索。

索引在知识库中的作用

  1. 加速检索:通过倒排索引、向量索引(如FAISS、HNSW)等技术,将查询延迟控制在毫秒级。

  2. 提升可发现性:支持同义词、近义词匹配,增加召回率。

  3. 多维度搜索:结合全文索引、元数据索引、语义向量索引,实现混合检索。

  4. 动态更新:实时反映新增或修改的内容,保持知识库的时效性。

image

 

posted @ 2026-04-18 17:00  sunshine_coast  阅读(135)  评论(0)    收藏  举报