大模型知识-使用本地Embedding模型+Chromadb构建知识库(构建RAG完整链路)
RAG 完整链路:
文档加载 → 文本切分 → 本地 Embedding 编码 → Chroma 向量入库→ 用户提问 → 问题 Embedding → 向量检索召回 TopN 知识片段→ 构造 Prompt(问题 + 召回上下文) → 本地大模型生成答案;
主要分为两大流程:一是知识库构建流程;二是问题检索+生成结果,即RAG检索+生成;
1.知识库构建流程:
- 文档入库过程
- 文档读取
- 文档切块(chunk)
- 进行本地 Embedding 模型向量化
- 入库ChromaDB向量库
- 根据输入信息查询
- 检索向量数据库返回结果

注:文档入库使用的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」错误。
运行结果如下:

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)
运行结果如下:

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)
说明:
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")
运行结果如下:

方式 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)
5.5 重要工程要点(RAG 避坑)
- 上下文窗口限制 :本地大模型有最大上下文长度限制。召回的文档片段太多,会超出模型上下文窗口,造成截断。
解决:控制 TopN 数量,或者做上下文压缩 / 重排。
- 幻觉控制: Prompt 里必须加上约束:找不到信息就如实告知,禁止编造。
temperature调低,减少模型自由发挥。 - 检索后重排(进阶优化,可选) 当前只做向量召回,语义相近但无关的文本也可能被召回。可以增加重排模型(Reranker),对 TopN 结果做二次打分筛选,提升送入大模型的资料质量。
链路升级:向量检索 Top5 → Reranker 重排,筛选 Top3 → 组装 Prompt 送入大模型
3.文档与索引的关系:
知识库是一个存储和管理结构化、半结构化或非结构化信息的系统,
而索引是为了快速检索这些信息而建立的结构化数据映射。
在实际应用中,索引就像图书馆的目录系统,帮助用户在海量数据中迅速定位目标内容。没有索引的知识库,检索效率会大幅下降,尤其在百万级甚至更大规模的数据场景中。
核心区别
-
知识库:存储全部知识条目(文档、图片、视频、结构化数据等),包含内容本身及其元数据。
-
索引:对知识库内容的关键词、向量特征、分类标签等进行组织,以支持高效搜索。
索引在知识库中的作用
-
加速检索:通过倒排索引、向量索引(如FAISS、HNSW)等技术,将查询延迟控制在毫秒级。
-
提升可发现性:支持同义词、近义词匹配,增加召回率。
-
多维度搜索:结合全文索引、元数据索引、语义向量索引,实现混合检索。
-
动态更新:实时反映新增或修改的内容,保持知识库的时效性。


浙公网安备 33010602011771号