目录
大家好,我是星浩。该项目为某游乐园AI客服助手,旨在全天候自动解答票务等高频问题并支持图片查询。核心技术为RAG,挑战在于处理多样非结构化文档并确保答案准确。方案涉及PyMuPDF等多模态文档解析、FAISS向量检索、文本与CLIP图像Embedding混合检索,并根据场最选用不同知识切片策略以平衡效果与成本。
1.项目目标和定位
打造一款7x24小时在线的AI客服助手,主要实现以下功能:
- 自动解答高频问题
如票务、入园须知、会员权益等,减轻人工客服压力。
- 确保回答准确
所有答案均来源于官方知识库,杜绝错误或过时信息。
- 支持多模态查询
不仅能处理文本问题,还能理解并回应图片相关的查询(如活动海报等)。
2.核心挑战分析
1.知识来源多样化
官方规定(PDF)、内部FAQ(Word)、网页公告、活动介绍(含大量图片和表格)等多种文档格式。
2.非结构化数据处理
如何有效提取并理解PDF、Word中的表格与图片信息,是RAG成功的关键。
3.知识有效组织
需将海量、零散知识点切片(Chunking)并建立索引,确保检索准确。
4.答案有效性保障
确保最终答案严格基于检索内容,避免大模型“幻觉”。
3.技术架构选型
1.文档处理库
- PyMuPDF(处理PDF)
- python-docx(处理Word)
- pytesseract(OCR识别图片文字)
2.文本Embedding模型
- text-embedding-v4(性能优秀,支持可变维度)
3.图像Embedding模型
- CLIP(OpenAI开发,多模态RAG核心)
4.向量数据库/库
- FAISS(高性能向量检索引擎)
- 生产环境可选:Milvus、ChromaDB、Elasticsearch(提供完整数据管理服务)
5.大语言模型(LLM)
- Qwen-turbo(用于答案生成)
6.流程编排
- 不依赖LangChain,直接使用底层API
4.安装与准备
1.安装所有必需的 Python 库
pip install openai "faiss-cpu" python-docx PyMuPDF Pillow pytesseract transformers torch request
2.安装 Google Tesseract OCR 引擎
- Windows
从 github.com/UB-Mannheim… 下载并安装。
- macOS
brew install tesseract
- Linux (Ubuntu)
sudo apt-get install tesseract-ocr
请确保 tesseract 的可执行文件路径已添加到系统的 PATH 环境变量中。
3.设置环境变量:
- DASHSCOPE_API_KEY
从阿里云百炼平台获取的 API Key。
- HF_TOKEN
(可选) 您的 Hugging Face Token,用于下载 CLIP 模型,避免手动确认。
5.全局配置与模型加载
检查环境变量
DASHSCOPE_API_KEY = os.getenv("DASHSCOPE_API_KEY")
if not DASHSCOPE_API_KEY:
raise ValueError("错误:请设置 'DASHSCOPE_API_KEY' 环境变量。")
初始化百炼兼容的 OpenAI 客户端
client = OpenAI(
api_key=DASHSCOPE_API_KEY,
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
加载 CLIP 模型用于图像处理
try:
clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
clip_processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
print("CLIP 模型加载成功。")
except Exception as e:
print(f"加载 CLIP 模型失败,请检查网络连接或 Hugging Face Token。错误: {e}")
exit()
定义全局变量
DOCS_DIR = "hz_knowledge_base"
IMG_DIR = os.path.join(DOCS_DIR, "images")
TEXT_EMBEDDING_MODEL = "text-embedding-v4"
TEXT_EMBEDDING_DIM = 1024
IMAGE_EMBEDDING_DIM = 512 # CLIP 'vit-base-patch32' 模型的输出维度
6.文档解析与内容提取详解
1. Word文档处理(.docx)
- 整体功能
遍历docx文件所有元素(段落、表格),提取为独立内容区块(chunks)。
def parse_docx(file_path):
"""解析 DOCX 文件,提取文本和表格(转为Markdown)。"""
doc = DocxDocument(file_path)
content_chunks = []
for element in doc.element.body:
if element.tag.endswith('p'):
# 处理段落
elif element.tag.endswith('tbl'):
# 处理表格
return content_chunks
- 段落处理
提取每个段落内的纯文本内容,去除多余空白,并标注"type":"text"。
# 处理段落
paragraph_text = ""
for run in element.findall('.//w:t', {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}):
paragraph_text += run.text if run.text else ""
if paragraph_text.strip():
content_chunks.append({"type": "text", "content": paragraph_text.strip()})
- 表格处理
将Word表格转换为Markdown格式,读取表头与数据,组合为Markdown表格字符串,并标注"type":"table"。
# 处理表格
md_table = []
table = [t for t in doc.tables if t._element is element][0]
if table.rows:
# 添加表头
header = [cell.text.strip() for cell in table.rows[0].cells]
md_table.append("| " + " | ".join(header) + " |")
md_table.append("|" + "---|"*len(header))
# 添加数据行
for row in table.rows[1:]:
row_data = [cell.text.strip() for cell in row.cells]
md_table.append("| " + " | ".join(row_data) + " |")
table_content = "\n".join(md_table)
if table_content.strip():
content_chunks.append({"type": "table", "content": table_content})
2. PDF文档处理(.pdf)
- 整体功能
使用fitz(PyMuPDF)逐页读取PDF,将复合文档拆解为纯文本与独立图片,便于RAG后续处理。
def parse_pdf(file_path,image_dir):
"""解析 PDF 文件,提取文本和图片。"""
doc = fitz.open(file_path)
content_chunks = []
for page_num,page in enumerate(doc):
# 提取文本
# 提取图片
return content_chunks
- 文本提取
遍历每页,提取所有纯文本内容,每页文本保存为独立chunk,附带页码。
# 提取文本
text = page.get_text("text")
content_chunks.append({"type": "text", "content": text, "page": page_num + 1})
- 图片提取
检测并提取页面中所有图片,保存为唯一文件名(含原始文件名、页码、图片索引),记录图片路径。
# 提取图片
for img_index,img in enumerate(page.get_images(full=True)):
xref = img[0]
base_image = doc.extract_image(xref)
image_bytes = base_image["image"]
image_ext = base_image["ext"]
image_path = os.path.join(image_dir,f"{os.path.basename(file_path)}_p{page_num+1}_{img_index}.{image_ext}")
with open(image_path, "wb") as f:
f.write(image_bytes)
content_chunks.append({"type": "image", "content": image_path, "page": page_num + 1})
3. 图片文件处理(.jpg, .png, .jpeg)
- 整体功能
通过OCR“看图识字”,提取图片中的文字信息,适用于扫描版PDF或截图。
- OCR技术(pytesseract)
调用Tesseract OCR引擎的Python封装,image_to_string为核心方法,lang='chi_sim+eng'支持中英文识别。
def image_to_text(image_path):
"""对图片进行OCR和CLIP描述。"""
try:
image = Image.open(image_path)
# OCR
ocr_text = pytesseract.image_to_string(image, lang='chi_sim+eng').strip()
return {"ocr": ocr_text}
except Exception as e:
print(f"处理图片失败 {image_path}: {e}")
return {"ocr": ""}
4.小结
-
word文档分段提取文本与表格,PDF通过PyMuPDF解析文本和图片元素。
-
OCR技术处理扫描文档与图片文字,确保多格式内容完整转换为可检索数据。
-
所有内容按类型标记并分块存储,为后续向量化处理奠定基础。
7.模态处理与索引
1. 文本Embedding
- 模型:text-embedding-v4
- 维度:1024
- 用途:语义相似度计算
def get_text_embedding(text):
"""获取文本的 Embedding。"""
response = client.embeddings.create(
model=TEXT_EMBEDDING_MODEL,
input=text,
dimensions=TEXT_EMBEDDING_DIM
)
return response.data[0].embedding
2. CLIP图像Embedding
- 模型:CLIP ViT-Base-Patch32
- 维度:512
- 用途:跨模态检索
def get_image_embedding(image_path):
"""获取图片的 Embedding。"""
image = Image.open(image_path)
inputs = clip_processor(images=image, return_tensors="pt")
with torch.no_grad():
image_features = clip_model.get_image_features(**inputs)
return image_features[0].numpy()
3. CLIP文本Embedding(用于图像检索)
-
模型:CLIP ViT-Base-Patch32
-
维度:512
-
用途:文本与图像向量在同一空间,支持跨模态相似度计算,可实现文本到图像检索。
def get_clip_text_embedding(text):
"""使用CLIP的文本编码器获取文本的Embedding。"""
inputs = clip_processor(text=text, return_tensors="pt")
with torch.no_grad():
text_features = clip_model.get_text_features(**inputs)
return text_features[0].numpy()
4.创建 Faiss 索引
- 创建文本索引
text_index = faiss.IndexFlatL2(TEXT_EMBEDDING_DIM)
text_index_map = faiss.IndexIDMap(text_index)
text_ids = [m["id"] for m in metadata_store if m["type"] == "text"]
if text_vectors: # 只有当有文本向量时才添加到索引
text_index_map.add_with_ids(np.array(text_vectors).astype('float32'), np.array(text_ids))
- 创建图像索引
image_index = faiss.IndexFlatL2(IMAGE_EMBEDDING_DIM)
image_index_map = faiss.IndexIDMap(image_index)
image_ids = [m["id"] for m in metadata_store if m["type"] == "image"]
if image_vectors: # 只有当有图像向量时才添加到索引
image_index_map.add_with_ids(np.array(image_vectors).astype('float32'), np.array(image_ids))
8.RAG问答流程
1.文本检索和图像检索
结合文本和图像的搜索结果,为LLM提供更丰富的上下文:
- 并行检索:文本与图像分路进行
文本检索:无条件执行,向量化查询后在文本索引库中检索相似文本片段;
query_text_vec = np.array([get_text_embedding(query)]).astype('float32')
distances, text_ids = text_index.search(query_text_vec, k)
图像检索:当用户查询包含“海报”、“图片”等关键词时触发,使用CLIP生成向量,在图像索引库检索相关图片。
# 图像检索 (使用CLIP文本编码器)
# 简单判断是否需要检索图片
if any(keyword in query.lower() for keyword in image_keywords):
print(" - 检测到图像查询关键词,执行图像检索...")
query_clip_vec = np.array([get_clip_text_embedding(query)]).astype('float32')
distances, image_ids = image_index.search(query_clip_vec, 2) # 只找最相关的2张图
- 排序策略:“文本优先,强制包含最佳图片”
# 混合排序
text_results = [{match,distance,"text"} for match,distance in zip(matches,distances)]
image_result = [{match,distance,"image"} for match,distance in zip(matches,distances)]
# 优先文本,强制包含图像
for result,distance,result_type in sorted(text_results,key=lambda x: x[1]):
retrieved_context.append(result)
if image_results:
best_image = main(image_results,key=lambda x: x[1])
retrieved_context.append(best_image[0])
2. 构建 Prompt
context_str = ""
for i, item in enumerate(retrieved_context):
content = item.get('content', '')
source = item.get('metadata', {}).get('source', item.get('source', '未知来源'))
context_str += f"背景知识 {i+1} (来源: {source}):\n{content}\n\n"
prompt = f"""你是一个XX乐园客服助手。请根据以下背景知识,用友好和专业的语气回答用户的问题。请只使用背景知识中的信息,不要自行发挥。
[背景知识]
{context_str}
[用户问题]
{query}
"""
3.调用LLM生成最终答案
调用LLM生成答案
completion = client.chat.completions.create(
model="qwen-plus", # 使用一个强大的模型进行生成
messages=[
{"role": "system", "content": "你是一个XX乐园客服助手。"},
{"role": "user", "content": prompt}
]
)
final_answer = completion.choices[0].message.content
答案后处理:如果上下文包含图片,提示用户
image_path_found = None
for item in retrieved_context:
if item.get("type") == "image_context":
image_path_found = item.get("metadata", {}).get("path")
break
if image_path_found:
final_answer += f"\n\n(同时,我为您找到了相关图片,路径为: {image_path_found})"
4.模拟问题
- CASE1:文本问答
rag_ask(
query="我想了解一下门票的退款流程",
metadata_store=metadata_store,
text_index=text_index,
image_index=image_index
)
- CASE2:多模态问答
rag_ask(
query="最近元旦的活动海报是什么",
metadata_store=metadata_store,
text_index=text_index,
image_index=image_index
)
9.知识切片(Chunking)策略
知识切片是RAG系统的核心,直接影响检索质量和回答准确性。常见切片策略如下:
1. 改进的固定长度切片
-
方法:优先在句子边界切分,采用重叠机制确保上下文连续,长度可控。
-
优点:实现简单、速度快、长度统一,适合批量处理技术文档和规范文件。
-
场景:需统一长度、批量处理大量文档。
def improved_fixed_length_chunking(text, chunk_size=512, overlap=50):
"""改进的固定长度切片 - 在句子边界切分"""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
# 尝试在句子边界切分
if end < len(text):
# 寻找最近的句子结束符
for i in range(end, max(start, end - 100), -1):
if text[i] in '.!?。!?':
end = i + 1
break
chunk = text[start:end]
if len(chunk.strip()) > 0:
chunks.append(chunk.strip())
start = end - overlap
return chunks
2. 语义切片
- 方法:按句子、段落等语义单位切分,保持语义完整,无重叠。
- 优点:语义完整,检索准确性高,长度可能不均匀。
- 场景:适用于需保证语义完整的自然语言文本。
def semantic_chunking(text, max_chunk_size=512):
"""基于语义的切片 - 按句子分割"""
# 使用正则表达式分割句子
sentences = re.split(r'[.!?。!?\n]+', text)
chunks = []
current_chunk = ""
for sentence in sentences:
sentence = sentence.strip()
# 如果当前句子加入后超过最大长度,保存当前块
# 此处略
# 添加最后一个块
if current_chunk.strip():
chunks.append(current_chunk.strip())
return chunks
3. LLM语义切片
- 方法:利用LLM语义理解能力,精确控制长度与语义完整性。
- 优点:分割点智能,语义理解强,但依赖GPU,成本较高。
- 场景:高质量要求、复杂语义结构、有预算支持项目。
prompt = f"""
请将以下文本按照语义完整性进行切片,每个切片不超过{max_chunk_size}字符。
要求:
1. 保持语义完整性
2. 在自然的分割点切分
3. 返回JSON格式的切片列表,格式如下:
{{
"chunks": [
"第一个切片内容",
"第二个切片内容",
...
]
}}
文本内容:
{text}
请返回JSON格式的切片列表:
"""
response = client.chat.completions.create(
model="qwen-turbo-latest",
messages=[
{"role": "system", "content": "你是一个专业的文本切片助手。请严格按照JSON格式返回结果,不要添加任何额外的标记。"},
{"role": "user", "content": prompt}
]
)
4. 层次切片
- 方法:基于文档层次结构(标题、章节、段落)切分。
- 优点:保持文档结构,支持层次化查询,依赖文档格式。
- 场景:结构化文档(如手册、规范、多级标题文档)。
"""层次切片 - 基于文档结构层次进行切片"""
chunks = []
# 定义层次标记
hierarchy_markers = {
'title1': ['# ', '标题1:', '一、', '1. '],
'title2': ['## ', '标题2:', '二、', '2. '],
'title3': ['### ', '标题3:', '三、', '3. '],
'paragraph': ['\n\n', '\n']
}
# 代码太长,此处略
5. 滑动窗口切片
- 方法:固定窗口滑动,产生重叠切片,确保上下文连续性,减少信息丢失。
- 优点:上下文连续,召回率高,但有大量重叠内容。
- 场景:长文档处理、需上下文连续的场景。
def sliding_window_chunking(text, window_size=512, step_size=256):
"""滑动窗口切片"""
chunks = []
for i in range(0, len(text), step_size):
chunk = text[i:i + window_size]
if len(chunk.strip()) > 0:
chunks.append(chunk.strip())
return chunks
6. 切片策略对比与优缺点
改进的固定长度切片:统一长度,适合批量处理,灵活性一般。 语义切片:语义完整,适合自然语言文本,长度不均。 LLM切片:智能分割,质量高,成本高。 层次切片:结构清晰,适合结构化文档,依赖格式。 滑动窗口切片:上下文好,召回率高,重复内容多。 结论:针对不同知识库和应用场景,应根据实际需求选择合适的切片策略,综合考虑处理效率、检索准确性与系统成本。
END
如果你觉得本文有帮助,欢迎点赞、在看、转发,也欢迎留言分享你的经验!
往期文章回顾: