topic: ai

RAG 系统搭建全过程

2023 年上半年,公司决定上线一个内部知识库问答系统。

需求很简单:员工可以问系统问题,系统从文档库中找到答案,用自然语言回复。

听起来简单,但做起来全是坑。

架构设计

1
用户 → 问题 → 向量化 → 向量检索 → 文档拼接 → LLM → 回答

核心流程:

  1. 文档入库:PDF/Word/MD → 文本 → 分块 → 向量化 → 存入向量库
  2. 查询:用户问题 → 向量化 → 向量检索 Top-K → 拼接上下文 → LLM 生成

技术选型

向量数据库

数据库 优点 缺点
Qdrant Rust 实现,性能高 社区相对较小
Milvus 功能完善,分布式 重,资源消耗大
Chroma 简单,Python 原生 生产环境不够

最终选了 Qdrant,部署简单,性能也够用。

分块策略

这是 RAG 效果的关键。

试了三种:

  • 固定长度(500字符):效果一般,上下文容易断裂
  • 递归分块(按段落、句子):效果最好
  • 语义分块(用 LLM 判断):成本太高

最终用 递归分块

1
2
3
4
5
6
7
from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", " ", ""]
)

Embedding 模型

  • OpenAI text-embedding-ada-002:效果好,但贵
  • 本地模型(sentence-transformers):免费,效果稍差

前期用 OpenAI,后期考虑切换本地。

核心代码

文档处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

def process_document(file_path):
# 1. 加载文档
if file_path.endswith('.pdf'):
loader = PyPDFLoader(file_path)
elif file_path.endswith('.md'):
loader = TextLoader(file_path)

docs = loader.load()

# 2. 分块
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50
)
chunks = splitter.split_documents(docs)

# 3. 向量化
embeddings = OpenAIEmbeddings()
vectors = embeddings.embed_documents([c.page_content for c in chunks])

# 4. 存入向量库
qdrant_client.upsert(
collection_name="knowledge_base",
points=[
{
"id": i,
"vector": vectors[i],
"payload": {"content": chunks[i].page_content}
}
for i in range(len(chunks))
]
)

查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def query(question, top_k=3):
# 1. 问题向量化
embeddings = OpenAIEmbeddings()
question_vector = embeddings.embed_query(question)

# 2. 向量检索
results = qdrant_client.search(
collection_name="knowledge_base",
query_vector=question_vector,
limit=top_k
)

# 3. 拼接上下文
context = "\n\n".join([r.payload["content"] for r in results])

# 4. LLM 生成
prompt = f"""基于以下上下文回答问题。

上下文:
{context}

问题:{question}

回答:"""

response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}]
)

return response.choices[0].message.content

效果优化

1. Rerank

直接检索的结果不够准确,加了 rerank 模型:

1
2
3
4
5
6
7
8
from langchain_community.cross_encoders import HuggingFaceCrossEncoder

reranker = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base")

def rerank(query, documents):
scores = reranker.predict([(query, doc) for doc in documents])
sorted_docs = sorted(zip(documents, scores), key=lambda x: x[1], reverse=True)
return [d for d, s in sorted_docs[:3]]

效果:从 60% 提升到 85%。

2. 混合检索

向量检索 + 关键词检索 结合:

1
2
3
4
5
6
7
8
# 向量检索
vector_results = vector_db.similarity_search(query)

# 关键词检索
keyword_results = bm25.retrieve(query)

# 合并结果
combined = fusion_rank(vector_results, keyword_results)

3. Prompt 优化

针对公司场景定制 prompt:

1
2
3
4
5
6
7
8
9
10
11
12
13
prompt = """你是一个内部知识库助手,只能基于提供的上下文回答。
如果上下文中没有相关信息,请回答"抱歉,我暂时没有找到相关信息"。

回答要求:
1. 简洁明了
2. 如有必要,可以列出参考文档
3. 禁止编造答案

上下文:{context}

问题:{question}

回答:"""

遇到的问题

问题1:文档格式混乱

PDF 有表格、图片、公式,处理起来很麻烦。

解决:针对不同格式用不同的解析器,表格转成 Markdown,图片忽略或 OCR。

问题2:检索召回低

有些问题明明文档里有,但就是搜不到。

解决:增加同义词扩展,比如”服务器”→”主机”、”网络”→”网卡”。

问题3:LLM 幻觉

LLM 会一本正经地编造答案。

解决:要求 LLM 引用原文,增加置信度检查。

总结

RAG 系统看似简单,实际要做好需要大量调优。

核心经验:

  1. 分块策略是基础,分不好后面都白搭
  2. 检索优化效果明显,rerank + 混合检索
  3. 持续迭代,没有一劳永逸的方案

目前系统运行稳定,准确率基本维持在 85% 以上。接下来计划接入更多数据源,优化响应速度。