RAG 系统的基本搭建流程
搭建过程:
- 文档加载,并按一定条件切割成片段
- 将切割的文本片段灌入检索引擎
- 封装检索接口
- 构建调用流程:Query -> 检索 -> Prompt -> LLM -> 回复
1. 文档的加载与切割
python
# !pip install --upgrade openai
# 安装 pdf 解析库
# !pip install pdfminer.six
from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextContainer
def extract_text_from_pdf(filename, page_numbers=None, min_line_length=1):
'''从 PDF 文件中(按指定页码)提取文字'''
paragraphs = []
buffer = ''
full_text = ''
# 提取全部文本
for i, page_layout in enumerate(extract_pages(filename)):
# 如果指定了页码范围,跳过范围外的页
if page_numbers is not None and i not in page_numbers:
continue
for element in page_layout:
if isinstance(element, LTTextContainer):
full_text += element.get_text() + '\n'
# 按空行分隔,将文本重新组织成段落
lines = full_text.split('\n')
for text in lines:
if len(text) >= min_line_length:
buffer += (' '+text) if not text.endswith('-') else text.strip('-')
elif buffer:
paragraphs.append(buffer)
buffer = ''
if buffer:
paragraphs.append(buffer)
return paragraphs
paragraphs = extract_text_from_pdf("llama2.pdf", min_line_length=10)
for para in paragraphs[:4]:
print(para+"\n")
######
##输出:
Llama 2: Open Foundation and Fine-Tuned Chat Models
Hugo Touvron∗ Louis Martin† Kevin Stone† Peter Albert Amjad Almahairi Yasmine Babaei Nikolay Bashlykov Soumya Batra Prajjwal Bhargava Shruti Bhosale Dan Bikel Lukas Blecher Cristian Canton Ferrer Moya Chen Guillem Cucurull David Esiobu Jude Fernandes Jeremy Fu Wenyin Fu Brian Fuller Cynthia Gao Vedanuj Goswami Naman Goyal Anthony Hartshorn Saghar Hosseini Rui Hou Hakan Inan Marcin Kardas Viktor Kerkez Madian Khabsa Isabel Kloumann Artem Korenev Punit Singh Koura Marie-Anne Lachaux Thibaut Lavril Jenya Lee Diana Liskovich Yinghai Lu Yuning Mao Xavier Martinet Todor Mihaylov Pushkar Mishra Igor Molybog Yixin Nie Andrew Poulton Jeremy Reizenstein Rashi Rungta Kalyan Saladi Alan Schelten Ruan Silva Eric Michael Smith Ranjan Subramanian Xiaoqing Ellen Tan Binh Tang Ross Taylor Adina Williams Jian Xiang Kuan Puxin Xu Zheng Yan Iliyan Zarov Yuchen Zhang Angela Fan Melanie Kambadur Sharan Narang Aurelien Rodriguez Robert Stojnic Sergey Edunov Thomas Scialom∗
GenAI, Meta
这段代码实现了从PDF文档中提取文本并按段落进行切割的功能,是构建RAG(检索增强生成)系统中文档处理的关键步骤。下面详细解析其工作原理和实现逻辑:
1. 环境准备
python
# !pip install --upgrade openai
# !pip install pdfminer.six
from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextContainer
- pdfminer.six:PDF解析库,用于提取PDF中的文本和布局信息。
extract_pages
:逐页解析PDF文档。LTTextContainer
:识别PDF中的文本块(段落或文字区域)。
2. 核心函数 extract_text_from_pdf
输入参数
filename
:PDF文件路径。page_numbers
:指定提取的页码范围(可选)。min_line_length
:最小行长度阈值,用于过滤无意义的短行(如页码或页眉)。
步骤解析
步骤1:逐页提取文本
python
for i, page_layout in enumerate(extract_pages(filename)):
if page_numbers is not None and i not in page_numbers:
continue
for element in page_layout:
if isinstance(element, LTTextContainer):
full_text += element.get_text() + '\n'
- 遍历PDF每一页,通过
LTTextContainer
识别文本块。 - 将文本块内容拼接为
full_text
,并用换行符分隔不同文本块。
步骤2:处理换行与断词
python
lines = full_text.split('\n')
for text in lines:
if len(text) >= min_line_length:
buffer += (' '+text) if not text.endswith('-') else text.strip('-')
elif buffer:
paragraphs.append(buffer)
buffer = ''
- 断词处理 :若行以连字符
-
结尾,表示单词跨行,需拼接(如"inter- esting"
合并为"interesting"
)。 - 空格拼接:普通行首添加空格,避免直接拼接导致单词粘连。
步骤3:段落切割
- 当遇到短行(如空行或页眉),将
buffer
中的内容作为一个段落存入paragraphs
。 - 遍历结束后,检查
buffer
是否剩余内容并存入。
3. 代码调用示例
python
paragraphs = extract_text_from_pdf("llama2.pdf", min_line_length=10)
for para in paragraphs[:4]:
print(para+"\n")
- 提取
llama2.pdf
中所有页的文本,过滤长度小于10的短行。 - 打印前4个段落,验证输出效果。
4. 设计优缺点
设计优点
- 跨页处理:自动合并跨页的段落。
- 断词修复:处理因换行导致的单词拆分。
- 灵活过滤 :通过
min_line_length
过滤无意义短行。
局限性
- 依赖PDF结构:若PDF使用非标准布局(如分栏、图片内文字),提取可能不准确。
- 段落切割逻辑:依赖空行或短行分割段落,对无空行的长文本可能不够鲁棒。
5. 拓展建议
- 布局分析 :结合
LTFigure
或LTImage
处理图片中的文本。 - 高级分段:使用NLP工具(如spacy)基于语义分割段落。
- 并行处理:对大文档采用多线程加速解析。
通过这段代码,可以实现基础的PDF文本提取与段落切割,为后续的向量化存储和检索增强生成(RAG)奠定基础。实际应用中需结合具体文档结构调整参数和逻辑。
2. 检索引擎
先看一个最基础的ES实现
python
pip install elasticsearch7
# 安装 NLTK(文本处理方法库)
pip install nltk
python
from elasticsearch7 import Elasticsearch, helpers
from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
import nltk
import re
import warnings
warnings.simplefilter("ignore") # 屏蔽 ES 的一些Warnings
nltk.download('punkt') # 英文切词、词根、切句等方法
nltk.download('stopwords') # 英文停用词库
nltk.download('punkt_tab')
def to_keywords(input_string):
'''(英文)文本只保留关键字'''
# 使用正则表达式替换所有非字母数字的字符为空格
no_symbols = re.sub(r'[^a-zA-Z0-9\s]', ' ', input_string)
word_tokens = word_tokenize(no_symbols)
# 加载停用词表
stop_words = set(stopwords.words('english'))
ps = PorterStemmer()
# 去停用词,取词根
filtered_sentence = [ps.stem(w)
for w in word_tokens if not w.lower() in stop_words]
return ' '.join(filtered_sentence)
# 此处 to_keywords 为针对英文的实现,针对中文的实现请参考 chinese_utils.py
将文本灌入检索引擎
python
import os, time
# 引入配置文件
ELASTICSEARCH_BASE_URL = os.getenv('ELASTICSEARCH_BASE_URL')
ELASTICSEARCH_PASSWORD = os.getenv('ELASTICSEARCH_PASSWORD')
ELASTICSEARCH_NAME= os.getenv('ELASTICSEARCH_NAME')
# 1. 创建Elasticsearch连接
es = Elasticsearch(
hosts=[ELASTICSEARCH_BASE_URL], # 服务地址与端口
http_auth=(ELASTICSEARCH_NAME, ELASTICSEARCH_PASSWORD), # 用户名,密码
)
# 2. 定义索引名称
index_name = "teacher_demo_index111"
# 3. 如果索引已存在,删除它(仅供演示,实际应用时不需要这步)
if es.indices.exists(index=index_name):
es.indices.delete(index=index_name)
# 4. 创建索引
es.indices.create(index=index_name)
# 5. 灌库指令
actions = [
{
"_index": index_name,
"_source": {
"keywords": to_keywords(para),
"text": para
}
}
for para in paragraphs
]
# 6. 文本灌库
helpers.bulk(es, actions)
# 灌库是异步的
time.sleep(2)
实现关键字检索
python
def search(query_string, top_n=3):
# ES 的查询语言
search_query = {
"match": {
"keywords": to_keywords(query_string)
}
}
res = es.search(index=index_name, query=search_query, size=top_n)
return [hit["_source"]["text"] for hit in res["hits"]["hits"]]
results = search("how many parameters does llama 2 have?", 2)
for r in results:
print(r+"\n")
### 输出
1. Llama 2, an updated version of Llama 1, trained on a new mix of publicly available data. We also increased the size of the pretraining corpus by 40%, doubled the context length of the model, and adopted grouped-query attention (Ainslie et al., 2023). We are releasing variants of Llama 2 with 7B, 13B, and 70B parameters. We have also trained 34B variants, which we report on in this paper but are not releasing.§
3. LLM接口封装
python
from openai import OpenAI
import os
# 加载环境变量
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) # 读取本地 .env 文件,里面定义了 OPENAI_API_KEY
client = OpenAI()
#%%
def get_completion(prompt, model="gpt-3.5-turbo"):
'''封装 openai 接口'''
messages = [{"role": "user", "content": prompt}]
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=0, # 模型输出的随机性,0 表示随机性最小
)
return response.choices[0].message.content
4. Prompt模版
python
def build_prompt(prompt_template, **kwargs):
'''将 Prompt 模板赋值'''
inputs = {}
for k, v in kwargs.items():
if isinstance(v, list) and all(isinstance(elem, str) for elem in v):
val = '\n\n'.join(v)
else:
val = v
inputs[k] = val
return prompt_template.format(**inputs)
prompt_template = """
你是一个问答机器人。
你的任务是根据下述给定的已知信息回答用户问题。
已知信息:
{context}
用户问:
{query}
如果已知信息不包含用户问题的答案,或者已知信息不足以回答用户的问题,请直接回复"我无法回答您的问题"。
请不要输出已知信息中不包含的信息或答案。
请用中文回答用户问题。
"""
5. RAG Pipeline
python
user_query = "how many parameters does llama 2 have?"
# 1. 检索
search_results = search(user_query, 2)
# 2. 构建 Prompt
prompt = build_prompt(prompt_template, context=search_results, query=user_query)
print("===Prompt===")
print(prompt)
# 3. 调用 LLM
response = get_completion(prompt)
print("===回复===")
print(response)
python
===Prompt===
你是一个问答机器人。
你的任务是根据下述给定的已知信息回答用户问题。
已知信息:
1. Llama 2, an updated version of Llama 1, trained on a new mix of publicly available data. We also increased the size of the pretraining corpus by 40%, doubled the context length of the model, and adopted grouped-query attention (Ainslie et al., 2023). We are releasing variants of Llama 2 with 7B, 13B, and 70B parameters. We have also trained 34B variants, which we report on in this paper but are not releasing.§
In this work, we develop and release Llama 2, a collection of pretrained and fine-tuned large language models (LLMs) ranging in scale from 7 billion to 70 billion parameters. Our fine-tuned LLMs, called Llama 2-Chat, are optimized for dialogue use cases. Our models outperform open-source chat models on most benchmarks we tested, and based onour human evaluations for helpfulness and safety, may be a suitable substitute for closed source models. We provide a detailed description of our approach to fine-tuning and safety improvements of Llama 2-Chat in order to enable the community to build on our work and contribute to the responsible development of LLMs.
用户问:
how many parameters does llama 2 have?
如果已知信息不包含用户问题的答案,或者已知信息不足以回答用户的问题,请直接回复"我无法回答您的问题"。
请不要输出已知信息中不包含的信息或答案。
请用中文回答用户问题。
===回复===
Llama 2有7B, 13B和70B参数。
扩展:
Elasticsearch(简称ES)是一个广泛应用的开源搜索引擎: https://www.elastic.co/
关于ES的安装、部署等知识,网上可以找到大量资料,例如: https://juejin.cn/post/7104875268166123528
关于经典信息检索技术的更多细节,可以参考: https://nlp.stanford.edu/IR-book/information-retrieval-book.html
6. 关键字检索的局限性
同一个语义,用词不同,可能导致检索不到有效的结果
python
# user_query="Does llama 2 have a chat version?"
user_query = "Does llama 2 have a conversational variant?"
search_results = search(user_query, 2)
for res in search_results:
print(res+"\n")
python
1. Llama 2, an updated version of Llama 1, trained on a new mix of publicly available data. We also increased the size of the pretraining corpus by 40%, doubled the context length of the model, and adopted grouped-query attention (Ainslie et al., 2023). We are releasing variants of Llama 2 with 7B, 13B, and 70B parameters. We have also trained 34B variants, which we report on in this paper but are not releasing.§
variants of this model with 7B, 13B, and 70B parameters as well.