项目实战-7×24小时在线的AI客服助手

105 阅读12分钟

目录

  1. 项目目标和定位
  2. 核心挑战分析
  3. 技术架构选型
  4. 安装与准备
  5. 全局配置与模型加载
  6. 文档解析与内容提取详解
  7. 模态处理与索引
  8. RAG问答流程
  9. 93知识切片(Chunking)策略

大家好,我是星浩。该项目为某游乐园AI客服助手,旨在全天候自动解答票务等高频问题并支持图片查询。核心技术为RAG,挑战在于处理多样非结构化文档并确保答案准确。方案涉及PyMuPDF等多模态文档解析、FAISS向量检索、文本与CLIP图像Embedding混合检索,并根据场最选用不同知识切片策略以平衡效果与成本。


1.项目目标和定位

打造一款7x24小时在线的AI客服助手,主要实现以下功能:

  • 自动解答高频问题

如票务、入园须知、会员权益等,减轻人工客服压力。

  • 确保回答准确

所有答案均来源于官方知识库,杜绝错误或过时信息。

  • 支持多模态查询

不仅能处理文本问题,还能理解并回应图片相关的查询(如活动海报等)。

img_2.jpg


2.核心挑战分析

1.知识来源多样化

官方规定(PDF)、内部FAQ(Word)、网页公告、活动介绍(含大量图片和表格)等多种文档格式。

2.非结构化数据处理

如何有效提取并理解PDF、Word中的表格与图片信息,是RAG成功的关键。

3.知识有效组织

需将海量、零散知识点切片(Chunking)并建立索引,确保检索准确。

4.答案有效性保障

确保最终答案严格基于检索内容,避免大模型“幻觉”。

img_4.jpg


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

如果你觉得本文有帮助,欢迎点赞、在看、转发,也欢迎留言分享你的经验!

往期文章回顾: