AI 工程师第六课:RAG 检索增强生成
英文标题:AI工程师第六课 - RAG检索增强生成
RAG(检索增强生成)通过先检索私有文档、再将相关内容连同问题交给 LLM,解决大模型不了解企业内部知识且容易幻觉的问题。本文拆解了加载、分块、向量化、存储、检索与生成五个核心步骤,并以 Chroma、Sentence Transformers 和 OpenAI 兼容接口演示手写实现。文章还分别介绍了 LangChain 与 LlamaIndex 的 RAG 工作流,以及重排序、混合检索和多轮检索等进阶优化。对开发者而言,RAG 是将通用大模型连接企业知识库、构建可追溯问答应用的基础架构。
6.8 KB
中文
第五课中使用 OpenAI 调用 DeepSeek 模型的代码,可作为基准测试代码:
from openai import OpenAI
from dotenv import load_dotenv
import os
load_dotenv(override=True)
api_key = os.getenv("API_KEY")
base_url = os.getenv("BASE_URL")
client = OpenAI(api_key=api_key, base_url=base_url)
def main():
prompt = "Hi"
print(call_model([{"role": "user", "content": prompt}]))
def call_model(messages):
response = client.chat.completions.create(
model="deepseek-v4-pro",
messages=messages,
stream=False,
reasoning_effort="high",
extra_body={"thinking": {"type": "enabled"}}
)
return response.choices[0].message.content
if __name__ == "__main__":
main()
RAG 是什么
LLM 不知道你的私有数据。
你问:“公司最新的差旅报销标准是什么?”
LLM:“抱歉,我的知识截止到某个日期,无法回答这个问题。”
它不知道你公司的内部文档。让它看过相关资料后,它就知道了。
RAG = Retrieval Augmented Generation = 检索增强生成
核心思路:先检索相关文档,再把文档和问题一起交给 LLM。
不使用 RAG:
用户问题 → LLM → 回答(只能依赖训练数据中的知识,可能编造)
使用 RAG:
用户问题 → 检索相关文档 → 文档 + 问题 → LLM → 回答(基于文档,有据可查)
RAG 五步流程
1. 加载(Load) 文档进入系统
2. 分块(Chunk) 切分为小段
3. 向量化(Embed) 文字 → 数字向量
4. 存储(Store) 将向量存入数据库
5. 检索 + 生成(Retrieve + Generate) 搜索相关片段,并交给 LLM
1. 加载文档
with open("README.md", "r") as f:
document = f.read()
print(document[:200])
2. 分块(Chunking)
LLM 的上下文窗口有限,不能一次塞进整本书,因此需要将文档切分为小段,每段通常为几百字。
为什么不能切得太碎?
太碎 → 每段信息不完整,检索可能找不到所需内容
为什么不能切得太大?
太大 → 检索精度下降,LLM 上下文窗口也装不下
经验值:每块 500—1000 字,块与块之间保留 100—200 字重叠
def chunk_text(text, chunk_size=500, overlap=100):
"""简单分块:按字符数切分,块之间保留重叠"""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start = end - overlap
return chunks
chunks = chunk_text(document)
print(f"分了 {len(chunks)} 块,每块约 500 字")
生产环境应使用更智能的分块方式,例如按句子或段落边界切分,避免从一句话中间截断。
3. 向量化(Embedding)
**计算机不理解“语义”,只理解数字。**Embedding 会把文字转换为一串数字,即向量;含义相近的文字,其向量通常也更接近。
| 模型 | 维度 | 中文效果 | 特点 |
|---|---|---|---|
| bge-small-zh-v1.5 | 512 | 好 | 开源本地模型,轻量,推荐入门 |
| bge-large-zh-v1.5 | 1024 | 最好 | 开源本地模型,效果最优 |
| m3e-base | 768 | 好 | 开源本地模型,社区活跃 |
| text-embedding-3-small | 1536 | 中 | OpenAI API,价格较低但需要联网 |
使用本地 Embedding 模型,免费、离线,且数据不出域:
pip install sentence-transformers
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-small-zh-v1.5",cache_folder="./models")
def get_embedding(text):
return model.encode(text, normalize_embeddings=True).tolist()
embeddings = [get_embedding(chunk) for chunk in chunks]
print(embeddings[0][:10])
要判断两段文字有多相似,可以计算两个向量之间的余弦相似度:
import numpy as np
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
similarity = cosine_similarity(embeddings[0], embeddings[1])
print(f"相似度:{similarity:.4f}")
4. 向量数据库
将所有文档块的向量存储起来,查询时找出最相似的内容。
| 数据库 | 适用场景 | 特点 |
|---|---|---|
| Chroma | 学习、原型 | 通过 pip 安装即可使用,轻量 |
| FAISS | 百万级数据 | Meta 出品,纯向量检索,速度快 |
| Milvus | 生产环境、海量数据 | 分布式,支持混合检索 |
| Pinecone | 不想运维 | 云服务,按量付费 |
使用 Chroma(轻量,pip 安装后即可使用):
pip install chromadb
import chromadb
chroma_client = chromadb.PersistentClient(path="./chroma_db")
chroma_client.delete_collection("my_docs")
collection = chroma_client.get_or_create_collection(name="my_docs")
collection.add(
ids=[str(i) for i in range(len(chunks))],
documents=chunks,
metadatas=[{"source": "README.md"}] * len(chunks),
embeddings=embeddings,
)
print(collection.count())
查询时,将用户问题向量化,再找到最相似的文档。top_k 表示返回最相似的若干条结果:
def search(query, top_k=3):
query_embedding = get_embedding(query)
results = collection.query(
query_embeddings=[query_embedding],
n_results=top_k,
)
return results["documents"][0]
results = search("环境配置", top_k=3)
for doc in results:
print(doc)
5. 检索 + 生成
将检索到的文档和用户问题拼接后,一起交给 LLM:
def rag_answer(question):
relevant_docs = search(question)
context = "\n".join(relevant_docs)
prompt = f"""
根据以下参考资料回答问题。如果资料中没有相关信息,如实说不知道。
参考资料:
{context}
问题:{question}
回答:
"""
response = call_model([{"role": "user", "content": prompt}])
return response
print(rag_answer("环境配置"))
print(rag_answer("Python基础知识"))
测试结果基本都会引用所提供文档中的内容,整体较为可靠。
使用 LangChain / LlamaIndex 构建 RAG
上面是手写实现:自行分块、调用 Embedding API、存储向量、拼接 Prompt、调用 LLM。这样有助于理解原理,但代码量不小。LangChain 和 LlamaIndex 已将这些步骤封装完成。
LangChain
安装:
pip install langchain langchain-openai langchain-huggingface langchain_community langchain_chroma
1. 加载文档
通过 DirectoryLoader 批量加载指定目录下的特定格式文件,并指定 TextLoader 解析文件内容。
from langchain_community.document_loaders import DirectoryLoader, TextLoader
loader = DirectoryLoader(".", glob="*.md", loader_cls=TextLoader)
docs = loader.load()
2. 分块
RecursiveCharacterTextSplitter 会按语义边界切分,尽量避免从一句话中间截断。
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=100, separators=["\n## ", "\n", "。", ".", " "]
)
chunks = text_splitter.split_documents(docs)
3. 向量化(Embedding)
使用 LangChain 二次封装的 langchain_huggingface,切换模型时更方便。
from langchain_huggingface import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5", cache_folder="./models"
)
4. 存储至 Chroma
使用 LangChain 二次封装的 langchain_chroma,切换数据库时更方便。
from langchain_chroma import Chroma
vectorstore = Chroma.from_documents(
chunks, embeddings, persist_directory="./chroma_langchain"
)
5. 创建模型调用链
这里使用 langchain_openai 对 OpenAI 模型进行封装,调用更加便捷。先构建模板,再通过 LangChain 特有的 | 管道操作实现链式调用。
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
llm = ChatOpenAI(model="deepseek-v4-pro", api_key=api_key, base_url=base_url, temperature=0, max_tokens=500)
template = """
根据以下参考资料回答问题。如果资料中没有相关信息,如实说不知道。
参考资料:
{context}
问题:{question}
回答:
"""
prompt = ChatPromptTemplate.from_template(template)
chain = (
{
"context": vectorstore.as_retriever(search_kwargs={"k": 3}),
"question": RunnablePassthrough(),
}
| prompt
| llm
| StrOutputParser()
)
6. 问答
print(chain.invoke("环境配置"))
print(chain.invoke("Python基础知识"))
LlamaIndex 版本
LlamaIndex 相较于 LangChain 更专注于 RAG,API 也更简洁。
安装所需依赖包:
pip install llama-index llama-index-embeddings-huggingface llama-index-vector-stores-chroma llama-index-llms-openai-like
1. 加载文档
from llama_index.core import SimpleDirectoryReader
documents = SimpleDirectoryReader(".", required_exts=[".md"]).load_data()
2. 配置向量化模型
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import Settings
Settings.embed_model = HuggingFaceEmbedding(
model_name="BAAI/bge-small-zh-v1.5", cache_folder="./models"
)
Settings.chunk_size = 500
Settings.chunk_overlap = 100
3. 建立索引(自动分块、向量化并存入 Chroma)
这里的 Chroma 不会像前文一样持久化存储到本地,而是直接使用内存存储;程序结束时数据会自动销毁。如有需要,可以增加持久化缓存。
from llama_index.core import VectorStoreIndex
index = VectorStoreIndex.from_documents(documents)
4. 配置模型
同样直接封装 OpenAI 接口。注意参数略有不同:api_base ≠ base_url。由于使用的是 deepseek,需要定义 is_chat_model=True。
from llama_index.llms.openai_like import OpenAILike
Settings.llm = OpenAILike(
api_key=api_key,
api_base=base_url,
model="deepseek-v4-pro",
temperature=0,
max_tokens=500,
is_chat_model=True,
)
5. 问答
不再需要手动构建模板,LlamaIndex 已在内部完成封装。也可以通过参数覆盖默认模板。
query_engine = index.as_query_engine(similarity_top_k=3)
print(query_engine.query("环境配置"))
print(query_engine.query("Python基础知识"))
高级 RAG
基础 RAG 可以使用,但在部分场景中效果可能不够理想。以下是几个常见优化方法:
LangChain 和 LlamaIndex 默认都不会执行这些高级优化;如有需要,需要自行创建模型并进行配置。
重排序(Rerank)
第一次检索先返回粗筛结果,再使用更强的模型进行精排。
基础:用户问题 → Embedding 检索 → Top 3 → 交给 LLM
Rerank:
用户问题 → Embedding 检索 → Top 20(粗筛)
→ Rerank 模型精排 → Top 3(精选)→ 交给 LLM
效果:检索精度显著提升,成本增加不多
在第一章节中已手写实现过自定义 RAG 流程代码。修改 rag_answer 方法,加入 Rerank 过程:
from sentence_transformers import CrossEncoder
reranker = CrossEncoder(
"cross-encoder/ms-marco-MiniLM-L-6-v2",
cache_folder="./models",
)
def rag_answer(question):
relevant_docs = search(question, 10)
pairs = [(question, doc) for doc in relevant_docs]
scores = reranker.predict(pairs)
ranked = sorted(zip(scores, relevant_docs), key=lambda x: x[0], reverse=True)
top_docs = [doc for _, doc in ranked[:3]]
context = "\n".join(top_docs)
为了便于本地测试,此处使用的是小模型。
混合检索
纯向量检索有时会漏掉精确的关键词匹配。
问题:“2024 年 Q3 财报”
向量检索 → 可能返回语义相近但无关的“财务”相关内容
关键词检索(BM25)→ 精确匹配包含“2024 Q3 财报”的文档
混合检索 = 向量检索 + 关键词检索,取并集后再排序
继续加入混合检索的实现代码。首先需要提取关键词,使用 jieba 对语句分词,再通过 rank_bm25 算法计算匹配得分:
from rank_bm25 import BM25Okapi
import jieba
def main():
tokenized_chunks = [jieba.lcut(chunk) for chunk in chunks]
bm25 = BM25Okapi(tokenized_chunks)
results = hybrid_search(bm25, chunks, "环境配置")
for doc in results:
print(doc)
def hybrid_search(bm25, chunks, query, top_k=5):
results = search(query, top_k=top_k)
kw_scores = bm25.get_scores(jieba.lcut(query))
kw_results = sorted(zip(chunks, kw_scores), key=lambda x: x[1], reverse=True)
kw_results = [c for c, _ in kw_results[:10]]
seen = set(results)
result = list(results)
for doc in kw_results:
if doc not in seen:
result.append(doc)
return result[:top_k]
多轮检索
当用户问题过于模糊时,可以先让 LLM 改写问题,再进行检索。
用户:“上次说的那个方案”
→ LLM 改写:“关于 XX 项目的技术方案”
→ 检索“XX 项目 技术方案”
→ 交给 LLM 回答
其中一个关键点是保留历史记录:通过历史记录推断用户意图,再对问题进行改写。
English / 原文
第五课 中使用OpenAI调用模型DeepSeek 代码,作为基准测试代码:
from openai import OpenAI
from dotenv import load_dotenv
import os
load_dotenv(override=True)
api_key = os.getenv("API_KEY")
base_url = os.getenv("BASE_URL")
client = OpenAI(api_key=api_key, base_url=base_url)
def main():
prompt = "Hi"
print(call_model([{"role": "user", "content": prompt}]))
def call_model(messages):
response = client.chat.completions.create(
model="deepseek-v4-pro",
messages=messages,
stream=False,
reasoning_effort="high",
extra_body={"thinking": {"type": "enabled"}}
)
return response.choices[0].message.content
if __name__ == "__main__":
main()
RAG 是什么
LLM 不知道你的私有数据。
你问:"公司最新的差旅报销标准是什么?"
LLM:"抱歉,我的知识截止到某个日期,无法回答这个问题。"
它不知道你公司的内部文档。让它看过了,它就知道了。
RAG = Retrieval Augmented Generation = 检索增强生成
核心思路:先检索相关文档,再把文档和问题一起喂给 LLM。
不用 RAG:
用户问题 → LLM → 回答(只能靠训练数据里的知识,可能编造)
用 RAG:
用户问题 → 检索相关文档 → 文档 + 问题 → LLM → 回答(基于文档,有据可查)
RAG 五步流程
1. 加载(Load) 文档进来
2. 分块(Chunk) 切成小段
3. 向量化(Embed) 文字 → 数字向量
4. 存储(Store) 向量存数据库
5. 检索+生成(Retrieve+Generate) 搜相关片段,喂给 LLM
1. 加载文档
with open("README.md", "r") as f:
document = f.read()
print(document[:200])
2. 分块(Chunking)
LLM 有上下文窗口限制,不能一次塞整本书。把文档切成小段,每段几百字。
为什么不能切太碎?
太碎 → 每段信息不完整,检索找不到
为什么不能切太大?
太大 → 检索精度下降,LLM 窗口装不下
经验值:每块 500-1000 字,块与块之间有 100-200 字重叠
def chunk_text(text, chunk_size=500, overlap=100):
"""简单分块:按字符数切,块之间有重叠"""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start = end - overlap
return chunks
chunks = chunk_text(document)
print(f"分了 {len(chunks)} 块,每块约 500 字")
生产环境用更智能的分块——按句子、段落边界切,不把一句话拦腰截断。
3. 向量化(Embedding)
计算机不理解"语义",只理解数字。 Embedding 把文字变成一串数字(向量),含义相近的文字向量也相近。
| 模型 | 维度 | 中文 | 特点 |
|---|---|---|---|
| bge-small-zh-v1.5 | 512 | 好 | 开源本地,轻量,推荐上手 |
| bge-large-zh-v1.5 | 1024 | 最好 | 开源本地,效果最优 |
| m3e-base | 768 | 好 | 开源本地,社区活跃 |
| text-embedding-3-small | 1536 | 中 | OpenAI API,便宜但要联网 |
用本地 Embedding 模型,免费、离线、数据不出域:
pip install sentence-transformers
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-small-zh-v1.5",cache_folder="./models")
def get_embedding(text):
return model.encode(text, normalize_embeddings=True).tolist()
embeddings = [get_embedding(chunk) for chunk in chunks]
print(embeddings[0][:10])
知道两段文字有多相似——算两个向量的余弦 相似度:
import numpy as np
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
similarity = cosine_similarity(embeddings[0], embeddings[1])
print(f"相似度:{similarity:.4f}")
4. 向量数据库
把所有文档块的向量存起来,查询时找最相似的。
| 数据库 | 适用场景 | 特点 |
|---|---|---|
| Chroma | 学习、原型 | pip 装直接用,轻量 |
| FAISS | 百万级数据 | Meta 出品,纯向量检索,快 |
| Milvus | 生产、海量数据 | 分布式,支持混合检索 |
| Pinecone | 不想运维 | 云服务,按量付费 |
用 Chroma(轻量,pip 装完直接用):
pip install chromadb
import chromadb
chroma_client = chromadb.PersistentClient(path="./chroma_db")
chroma_client.delete_collection("my_docs")
collection = chroma_client.get_or_create_collection(name="my_docs")
collection.add(
ids=[str(i) for i in range(len(chunks))],
documents=chunks,
metadatas=[{"source": "README.md"}] * len(chunks),
embeddings=embeddings,
)
print(collection.count())
查询时,把用户问题向量化,找最相似的文档,top_k 找最相似的几条结果:
def search(query, top_k=3):
query_embedding = get_embedding(query)
results = collection.query(
query_embeddings=[query_embedding],
n_results=top_k,
)
return results["documents"][0]
results = search("环境配置", top_k=3)
for doc in results:
print(doc)
5. 检索 + 生成
把搜到的文档和用户问题拼在一起,喂给 LLM:
def rag_answer(question):
relevant_docs = search(question)
context = "\n".join(relevant_docs)
prompt = f"""
根据以下参考资料回答问题。如果资料中没有相关信息,如实说不知道。
参考资料:
{context}
问题:{question}
回答:
"""
response = call_model([{"role": "user", "content": prompt}])
return response
print(rag_answer("环境配置"))
print(rag_answer("Python基础知识"))
测试结果基本都是从给的文档里引用的,还是比较靠谱的。
用 LangChain / LlamaIndex 做 RAG
上面是裸写——自己分块、调 Embedding API、存向量、拼接 Prompt、调 LLM。理解了原理,但代码量不小。LangChain 和 LlamaIndex 把这些步骤封装好了。
LangChain
安装:
pip install langchain langchain-openai langchain-huggingface langchain_community langchain_chroma
1. 加载文档
通过DirectoryLoader批量加载指定目录下的某些格式文件,指定加载器TextLoader 解析文件内容。
from langchain_community.document_loaders import DirectoryLoader, TextLoader
loader = DirectoryLoader(".", glob="*.md", loader_cls=TextLoader)
docs = loader.load()
2. 分块
RecursiveCharacterTextSplitter 按语义边界切,不会拦腰截断一句话。
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=100, separators=["\n## ", "\n", "。", ".", " "]
)
chunks = text_splitter.split_documents(docs)
3. 向量化(Embedding)
使用langchain二次封装的langchain_huggingface,切换模型时更方便。
from langchain_huggingface import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5", cache_folder="./models"
)
4. 存 Chroma
使用langchain二次封装的langchain_chroma,切换数据库更方便。
from langchain_chroma import Chroma
vectorstore = Chroma.from_documents(
chunks, embeddings, persist_directory="./chroma_langchain"
)
5. 创建模型调用链
使用了langchain_openai对OpenAI模型封装,使用更便捷。构建模板,然后通过langchain特有的写法|管道操作,使得链式调用更简单。
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
llm = ChatOpenAI(model="deepseek-v4-pro", api_key=api_key, base_url=base_url, temperature=0, max_tokens=500)
template = """
根据以下参考资料回答问题。如果资料中没有相关信息,如实说不知道。
参考资料:
{context}
问题:{question}
回答:
"""
prompt = ChatPromptTemplate.from_template(template)
chain = (
{
"context": vectorstore.as_retriever(search_kwargs={"k": 3}),
"question": RunnablePassthrough(),
}
| prompt
| llm
| StrOutputParser()
)
6. 问答
print(chain.invoke("环境配置"))
print(chain.invoke("Python基础知识"))
LlamaIndex 版
LlamaIndex 比 LangChain 更专注 RAG,API 更简洁。
安装所需的包:
pip install llama-index llama-index-embeddings-huggingface llama-index-vector-stores-chroma llama-index-llms-openai-like
1. 加载文档
from llama_index.core import SimpleDirectoryReader
documents = SimpleDirectoryReader(".", required_exts=[".md"]).load_data()
2. 配置向量化模型
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import Settings
Settings.embed_model = HuggingFaceEmbedding(
model_name="BAAI/bge-small-zh-v1.5", cache_folder="./models"
)
Settings.chunk_size = 500
Settings.chunk_overlap = 100
3. 建索引(自动分块、向量化和存 Chroma)
存储Chroma 并没有和之前一样存储到本地,直接调用内存存储,程序结束时会自动销毁。如果有需要可以加持久化缓存。
from llama_index.core import VectorStoreIndex
index = VectorStoreIndex.from_documents(documents)
4. 配置模型
也是直接封装的OpenAI,注意参数有点变化api_base ≠ base_url,因为用的是deepseek要定义is_chat_model=True.
from llama_index.llms.openai_like import OpenAILike
Settings.llm = OpenAILike(
api_key=api_key,
api_base=base_url,
model="deepseek-v4-pro",
temperature=0,
max_tokens=500,
is_chat_model=True,
)
5. 问答
不再需要构建模板了,LlamaIndex 内部直接封装了。也可以通过参数覆盖默认模板。
query_engine = index.as_query_engine(similarity_top_k=3)
print(query_engine.query("环境配置"))
print(query_engine.query("Python基础知识"))
高级 RAG
基础 RAG 能用,但有些场景不够好。几个常见优化:
LangChain 和LlamaIndex 默认都不做这些高级优化,需要的话要自己手动创建模型,进行配置。
重排序(Rerank)
第一次检索返回粗筛结果,再用更强的模型精排。
基础:用户问题 → Embedding 检索 → Top 3 → 喂 LLM
Rerank:
用户问题 → Embedding 检索 → Top 20(粗筛)
→ Rerank 模型精排 → Top 3(精选)→ 喂 LLM
效果:检索精度显著提升,成本增加不多
在第一章节手写过自定义实现Rag过程代码,修改方法rag_answer,增加Rerank 过程。
from sentence_transformers import CrossEncoder
reranker = CrossEncoder(
"cross-encoder/ms-marco-MiniLM-L-6-v2",
cache_folder="./models",
)
def rag_answer(question):
relevant_docs = search(question, 10)
pairs = [(question, doc) for doc in relevant_docs]
scores = reranker.predict(pairs)
ranked = sorted(zip(scores, relevant_docs), key=lambda x: x[0], reverse=True)
top_docs = [doc for _, doc in ranked[:3]]
context = "\n".join(top_docs)
本地测试为了方便,使用的小模型。
混合检索
纯向量检索有时会漏掉精确的关键词匹配。
问题:"2024 年 Q3 财报"
向量检索 → 可能返回语义相近但无关的"财务"相关内容
关键词检索(BM25)→ 精确匹配"2024 Q3 财报"出现的文档
混合检索 = 向量检索 + 关键词检索,取并集再排序
继续增加混合检索的实现代码,首先要找到关键词,使用jieba对语句分词,然后使用rank_bm25算法,进行匹配得分计算:
from rank_bm25 import BM25Okapi
import jieba
def main():
tokenized_chunks = [jieba.lcut(chunk) for chunk in chunks]
bm25 = BM25Okapi(tokenized_chunks)
results = hybrid_search(bm25, chunks, "环境配置")
for doc in results:
print(doc)
def hybrid_search(bm25, chunks, query, top_k=5):
results = search(query, top_k=top_k)
kw_scores = bm25.get_scores(jieba.lcut(query))
kw_results = sorted(zip(chunks, kw_scores), key=lambda x: x[1], reverse=True)
kw_results = [c for c, _ in kw_results[:10]]
seen = set(results)
result = list(results)
for doc in kw_results:
if doc not in seen:
result.append(doc)
return result[:top_k]
多轮检索
用户问题太模糊,先让 LLM 改写问题再检索。
用户:"上次说的那个方案"
→ LLM 改写:"关于 XX 项目的技术方案"
→ 检索"XX 项目 技术方案"
→ 喂 LLM 回答
一个主要点就是要有历史记录,通过历史记录来推断用户的意图,然后进行改写。