24-客服工单系统实战(一):CSV数据导入与向量化存储

31 阅读8分钟

客服工单系统实战(一):CSV数据导入与向量化存储

前言

本文是客服工单系统实战系列的第一篇,将详细介绍如何将CSV格式的历史工单数据导入到向量数据库Weaviate中,为后续的智能问答打下基础。

本系列基于真实项目代码,所有示例均可在GitHub仓库中找到对应实现。

适合读者: AI工程师、数据工程师、后端开发者


一、项目背景

1.1 业务场景

客服团队每天处理大量工单,积累了丰富的问题解决经验。传统方式下,客服人员需要手动搜索历史工单,效率低下。通过构建RAG系统,可以:

  • 快速检索:秒级找到相似历史工单
  • 智能推荐:自动推荐解决方案
  • 知识沉淀:历史经验可复用
  • 降低成本:减少重复劳动

1.2 数据结构

我们的工单数据(service_tickets.csv)包含以下字段:

ticket_id,customer_name,issue_type,description,solution,status,priority,create_date,resolve_date,agent,satisfaction
TK001,张先生,产品咨询,咨询笔记本电脑的配置信息...,详细介绍了该型号的配置参数...,已解决,低,2024-11-15,2024-11-15,李明,5

核心字段说明:

  • ticket_id:工单唯一标识
  • issue_type:问题类型(产品咨询、订单查询、技术支持、退换货、账户问题、物流问题、优惠活动)
  • description:客户问题描述
  • solution:解决方案(最重要的字段)
  • priority:优先级(高、中、低)
  • satisfaction:客户满意度(1-5分)

二、技术架构

2.1 整体流程

CSV数据 → Pandas清洗 → 文本组装 → Ollama向量化 → Weaviate存储

2.2 技术栈

组件作用版本
Pandas数据清洗和处理2.0+
Ollama文本向量化(nomic-embed-text)-
Weaviate向量数据库1.27.1
LangChain向量化工具封装0.1.0+

三、数据导入模块实现

3.1 项目结构

agent/
├── agent/
│   ├── __init__.py
│   ├── config.py          # 配置文件
│   ├── import_data.py     # 数据导入模块(本文重点)
│   └── ticket_agent.py    # Agent主逻辑
├── data/
│   └── service_tickets.csv  # 工单数据
└── http_service.py        # HTTP服务

3.2 配置文件

# agent/config.py
import os

# Ollama配置
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434")
EMBED_MODEL = "nomic-embed-text"  # 向量化模型
CHAT_MODEL = "llama3.2:latest"    # 对话模型

# Weaviate配置
WEAVIATE_URL = os.getenv("WEAVIATE_URL", "http://localhost:8080")
WEAVIATE_CLASS = "ServiceTicket"  # 集合名称

# 数据路径
DATA_PATH = "data/service_tickets.csv"

# RAG配置
TOP_K = 5  # 检索Top-K个相似工单

# Prompt模板
QA_PROMPT_TEMPLATE = """你是一个专业的客服助手,擅长根据历史工单提供解决方案。

以下是相关的历史工单记录:

{context}

客户问题:{question}

请基于以上历史工单,为客户提供专业的解决方案。要求:
1. 如果找到相关解决方案,请详细说明处理步骤
2. 如果历史工单中没有完全匹配的案例,可以综合多个相似案例给出建议
3. 保持友好、专业的语气
4. 如果确实无法解决,建议客户联系人工客服

回答:"""

3.3 数据导入类

# agent/import_data.py
import pandas as pd
import ollama
from typing import List, Dict
from tqdm import tqdm
from . import config


class TicketImportData:
    """客服工单数据导入器"""
    
    def __init__(self, csv_path: str):
        self.csv_path = csv_path
        self.embed_model = config.EMBED_MODEL
    
    def load_and_prepare(self) -> List[Dict]:
        """加载CSV数据并准备向量化"""
        print("📂 加载客服工单数据...")
        df = pd.read_csv(self.csv_path, encoding='utf-8')
        print(f"✅ 成功加载 {len(df)} 条工单记录")
        
        # 数据清洗
        df = self._clean_data(df)
        
        tickets = []
        print("🔧 准备工单数据...")
        for _, row in tqdm(df.iterrows(), total=len(df), desc="处理工单"):
            # 构建用于向量化的文本描述
            text = self._build_text_description(row)
            
            tickets.append({
                'id': str(row['ticket_id']),
                'text': text,
                'metadata': {
                    'ticket_id': str(row['ticket_id']),
                    'customer_name': row['customer_name'],
                    'issue_type': row['issue_type'],
                    'description': row['description'],
                    'solution': row['solution'],
                    'status': row['status'],
                    'priority': row['priority'],
                    'agent': row['agent'],
                    'satisfaction': float(row['satisfaction']) if pd.notna(row['satisfaction']) else 0.0
                }
            })
        
        print(f"✅ 准备完成 {len(tickets)} 条工单数据")
        return tickets
    
    def _clean_data(self, df: pd.DataFrame) -> pd.DataFrame:
        """数据清洗"""
        print("🧹 开始数据清洗...")
        
        original_count = len(df)
        
        # 1. 删除重复数据
        df = df.drop_duplicates(subset=['ticket_id'])
        
        # 2. 删除关键字段缺失的行
        df = df.dropna(subset=['ticket_id', 'issue_type', 'description', 'solution'])
        
        # 3. 填充可选字段的缺失值
        df['customer_name'] = df['customer_name'].fillna('未知客户')
        df['status'] = df['status'].fillna('已解决')
        df['priority'] = df['priority'].fillna('中')
        df['agent'] = df['agent'].fillna('未知客服')
        
        cleaned_count = len(df)
        removed_count = original_count - cleaned_count
        
        if removed_count > 0:
            print(f"⚠️  已删除 {removed_count} 条无效数据")
        
        print(f"✅ 数据清洗完成,保留 {cleaned_count} 条有效数据")
        return df
    
    def _build_text_description(self, row) -> str:
        """构建工单的文本描述用于向量化"""
        # 构建丰富的文本描述,突出问题和解决方案
        description = f"""
工单编号: {row['ticket_id']}
问题类型: {row['issue_type']}
优先级: {row['priority']}
状态: {row['status']}

问题描述:
{row['description']}

解决方案:
{row['solution']}

处理客服: {row['agent']}
客户满意度: {row['satisfaction'] if pd.notna(row['satisfaction']) else '未评价'}
        """.strip()
        
        return description
    
    def generate_embeddings(self, texts: List[str]) -> List[List[float]]:
        """批量生成文本向量"""
        print(f"🔢 正在生成 {len(texts)} 个文本的向量...")
        embeddings = []
        
        for i, text in enumerate(tqdm(texts, desc="生成向量")):
            try:
                # 调用Ollama生成向量
                response = ollama.embeddings(
                    model=self.embed_model,
                    prompt=text
                )
                embeddings.append(response['embedding'])
            except Exception as e:
                print(f"❌ 向量生成失败: {e}")
                # 使用零向量作为fallback
                embeddings.append([0.0] * 768)  # nomic-embed-text维度为768
        
        print("✅ 向量生成完成")
        return embeddings

四、Weaviate存储实现

4.1 创建Schema

# agent/ticket_agent.py(部分代码)
import weaviate
import weaviate.classes as wvc
from langchain_community.embeddings import OllamaEmbeddings

class ServiceTicketAgent:
    def __init__(self):
        # 连接Weaviate
        self.client = weaviate.connect_to_local(
            host="localhost",
            port=8080,
            grpc_port=50051
        )
        
        # 初始化Embedding模型
        self.embeddings = OllamaEmbeddings(
            model="nomic-embed-text",
            base_url="http://localhost:11434"
        )
        
        # 构建数据库
        self._build_database()
    
    def _build_database(self):
        """构建向量数据库"""
        print("🏗️  开始构建向量数据库...")
        
        # 1. 加载和准备数据
        loader = TicketImportData("data/service_tickets.csv")
        tickets = loader.load_and_prepare()
        
        # 2. 创建Weaviate集合
        self.collection = self.client.collections.create(
            name="ServiceTicket",
            properties=[
                wvc.config.Property(name="content", data_type=wvc.config.DataType.TEXT),
                wvc.config.Property(name="ticket_id", data_type=wvc.config.DataType.TEXT),
                wvc.config.Property(name="issue_type", data_type=wvc.config.DataType.TEXT),
                wvc.config.Property(name="priority", data_type=wvc.config.DataType.TEXT),
                wvc.config.Property(name="status", data_type=wvc.config.DataType.TEXT),
            ]
        )
        
        # 3. 批量插入数据
        print("📥 开始插入数据到Weaviate...")
        for ticket in tqdm(tickets, desc="插入数据"):
            # 生成向量
            vector = self.embeddings.embed_query(ticket['text'])
            
            # 插入数据
            self.collection.data.insert(
                properties={
                    "content": ticket['text'],
                    "ticket_id": ticket['metadata'].get('ticket_id', ''),
                    "issue_type": ticket['metadata'].get('issue_type', ''),
                    "priority": ticket['metadata'].get('priority', ''),
                    "status": ticket['metadata'].get('status', ''),
                },
                vector=vector
            )
        
        print(f"✅ 成功存储 {len(tickets)} 条工单记录到向量数据库")

4.2 验证数据导入

# 验证脚本
def verify_import():
    """验证数据导入是否成功"""
    client = weaviate.connect_to_local()
    collection = client.collections.get("ServiceTicket")
    
    # 统计总数
    response = collection.aggregate.over_all(total_count=True)
    print(f"📊 数据库中共有 {response.total_count} 条工单")
    
    # 查询示例
    results = collection.query.fetch_objects(limit=3)
    print("\n📝 示例工单:")
    for obj in results.objects:
        print(f"  - {obj.properties['ticket_id']}: {obj.properties['issue_type']}")
    
    client.close()

if __name__ == "__main__":
    verify_import()

五、完整运行流程

5.1 环境准备

# 1. 启动Weaviate
docker run -d \
  --name weaviate \
  -p 8080:8080 \
  -p 50051:50051 \
  semitechnologies/weaviate:1.27.1

# 2. 启动Ollama
ollama serve

# 3. 下载模型
ollama pull nomic-embed-text
ollama pull llama3.2:latest

# 4. 安装依赖
pip install -r requirements.txt

5.2 执行导入

# test_import.py
from agent import ServiceTicketAgent

if __name__ == "__main__":
    print("开始构建客服工单知识库...")
    
    # 初始化Agent(会自动导入数据)
    agent = ServiceTicketAgent()
    
    print("\n知识库构建完成!")
    print("可以开始进行智能问答了。")

运行输出:

📂 加载客服工单数据...
✅ 成功加载 27 条工单记录
🧹 开始数据清洗...
✅ 数据清洗完成,保留 27 条有效数据
🔧 准备工单数据...
处理工单: 100%|████████████| 27/27 [00:00<00:00, 1234.56it/s]
✅ 准备完成 27 条工单数据
🏗️  开始构建向量数据库...
📥 开始插入数据到Weaviate...
插入数据: 100%|████████████| 27/27 [00:15<00:00,  1.75it/s]
✅ 成功存储 27 条工单记录到向量数据库

知识库构建完成!

六、性能优化

6.1 批量向量化

def batch_embed(texts: List[str], batch_size: int = 10) -> List[List[float]]:
    """批量生成向量,提升性能"""
    embeddings = []
    
    for i in range(0, len(texts), batch_size):
        batch = texts[i:i+batch_size]
        # 批量调用Ollama
        batch_embeddings = []
        for text in batch:
            response = ollama.embeddings(model="nomic-embed-text", prompt=text)
            batch_embeddings.append(response['embedding'])
        embeddings.extend(batch_embeddings)
    
    return embeddings

6.2 增量更新

def incremental_update(new_tickets: List[Dict]):
    """增量更新工单数据"""
    collection = client.collections.get("ServiceTicket")
    
    for ticket in new_tickets:
        # 检查是否已存在
        existing = collection.query.fetch_objects(
            filters=wvc.query.Filter.by_property("ticket_id").equal(ticket['ticket_id'])
        )
        
        if existing.objects:
            # 更新
            collection.data.update(
                uuid=existing.objects[0].uuid,
                properties={"content": ticket['text'], ...}
            )
        else:
            # 插入
            vector = embeddings.embed_query(ticket['text'])
            collection.data.insert(properties={...}, vector=vector)

七、常见问题

7.1 向量维度不匹配

问题: Vector dimension mismatch

解决: 确保使用相同的Embedding模型,nomic-embed-text的维度是768

7.2 Weaviate连接失败

问题: Connection refused

解决:

# 检查Weaviate是否运行
docker ps | grep weaviate

# 检查端口是否被占用
lsof -i :8080

7.3 中文乱码

问题: CSV读取中文乱码

解决:

df = pd.read_csv("service_tickets.csv", encoding='utf-8')
# 或者
df = pd.read_csv("service_tickets.csv", encoding='gbk')

八、总结

本文介绍了客服工单系统的数据导入流程:

CSV数据加载 - Pandas读取和清洗
文本组装 - 构建结构化描述
向量化 - Ollama nomic-embed-text
存储 - Weaviate向量数据库
验证 - 数据完整性检查

下一篇预告: 《客服工单系统实战(二):RAG检索与智能问答》

我们将介绍如何基于已导入的向量数据,实现智能检索和问答功能。


作者简介: 资深开发者,创业者。专注于视频通讯技术领域。国内首本Flutter著作《Flutter技术入门与实战》作者,另著有《Dart语言实战》及《WebRTC音视频开发》等书籍。多年从事视频会议、远程教育等技术研发,对于Android、iOS以及跨平台开发技术有比较深入的研究和应用,作为主要程序员开发了多个应用项目,涉及医疗、交通、银行等领域。

学习资料:

欢迎交流: 如有问题欢迎在评论区讨论 🚀