从零开始搭建电商智能客服:知识图谱 + 大模型,这篇保姆级教程让你彻底搞懂

0 阅读21分钟

一篇代码能跑通、原理讲清楚、注释写到位的硬核实战文章

写在前面

你有没有遇到过这种情况:在电商平台问客服“帮我找一款拍照好、续航长的手机”,结果客服机器人给你推荐了一堆完全不相关的商品?或者你问“苹果手机有哪些型号支持5G”,它只给你返回一个“苹果手机”的链接?

传统的客服机器人大多基于关键词匹配向量检索。关键词匹配只能识别你话里预设好的词,换个说法就失效;向量检索虽然能理解语义,但它不擅长处理关系——比如“苹果手机”和“iPhone”是什么关系?“拍照好”这个描述跟哪些商品有关?

今天我要带你实现的是一个基于知识图谱的智能电商客服系统。它不是简单地匹配关键词,而是先把电商数据(商品表、分类表、品牌表)和商品描述文本里的特征标签(比如“拍照旗舰”“续航持久”)整理成一个图数据库,然后让大模型理解你的问题,自动去图里查询,最后用自然语言回答你。

这套方案代码完整、注释详细、可以直接运行。我保证你跟着一步步做,不仅能跑起来,还能真正理解里面的每一处设计。


一、整体架构:先看图,再说话

在动手写代码之前,我们先搞明白整个系统长什么样。

这里面有两个关键模块:

  • 知识图谱

    :存储电商的结构化数据(商品、分类、品牌、属性)以及从描述文本里抽出来的标签。

  • 实体抽取模型(NER)

    :从商品描述里自动提取标签,比如“顶级拍照旗舰” → “拍照旗舰”。

下面我们按顺序,从环境配置开始,一步步搭建。


二、环境准备:把地基打好

2.1 为什么用 Conda?

不同项目依赖的 Python 版本、库版本可能冲突。Conda 能创建隔离的环境,每个项目一套独立的环境,互不干扰。

# 创建名为 graph 的环境,指定 Python 3.12
conda create -n graph python=3.12
 
# 激活环境
conda activate graph

激活后,命令行前面会出现 (graph),表示你现在就在这个隔离环境里。

2.2 安装 PyTorch(根据你的显卡)

PyTorch 是深度学习框架,我们用来训练和运行 NER 模型。如果你的电脑有 NVIDIA 显卡(CUDA),安装 GPU 版本会快很多;如果没有,装 CPU 版本也能跑。

# 有 CUDA 12.x 的显卡(比如 RTX 30/40 系列)
pip3 install torch --index-url https://download.pytorch.org/whl/cu128
 
# 如果没有独立显卡,或者不想折腾 CUDA,装 CPU 版本
pip3 install torch --index-url https://download.pytorch.org/whl/cpu

小提示:不确定自己有没有 CUDA,可以在命令行输入 nvidia-smi,如果能显示显卡信息就有。

2.3 安装所有依赖库

下面这条命令会安装本项目用到的所有第三方库。我把每个库的用途都写在注释里了:

pip install \
    pymysql \              # 连接 MySQL 数据库
    neo4j \                # 连接 Neo4j 图数据库
    transformers \         # Hugging Face 的预训练模型库(BERT等)
    accelerate \           # 加速模型训练(自动混合精度等)
    datasets \             # 方便地加载和处理数据集
    tensorboard \          # 可视化训练过程(看损失曲线)
    fastapi \              # 写 API 接口的 Web 框架
    uvicorn \              # 运行 FastAPI 的服务器
    langchain \            # 大模型应用开发框架
    langchain-deepspeak \  # 连接 DeepSeek 大模型(也可以用其他模型)
    python-dotenv \        # 加载 .env 配置文件(存放 API Key)
    evaluate \             # 评估模型指标(精确率、召回率等)
    langchain_huggingface \# LangChain 对 Hugging Face 的集成
    langchain_neo4j \      # LangChain 对 Neo4j 的集成
    sentence-transformers  # 生成文本向量(用于混合检索)

2.4 数据库准备

MySQL(存放原始电商数据)

项目里的模拟数据是一个叫 gmall.sql 的脚本。你需要先创建一个数据库:

CREATE DATABASE gmall CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

然后用数据库管理工具(比如 Navicat、DataGrip,或者命令行)把这个脚本导入进去。导入后,你会看到这些表:

  • base_category1/2/3:商品的三级分类(比如“手机”是一级,“智能手机”是二级,“拍照手机”是三级)

  • base_attr_info:平台属性(比如“运行内存”“屏幕尺寸”)

  • spu_info:标准产品单元(比如“Apple iPhone 16 Pro”)

  • sku_info:库存量单位(比如“iPhone 16 Pro 黑色 256G”)

  • base_trademark:品牌(比如“Apple”)

数据库脚本下载:pan.baidu.com/s/1SbovpOMR…

Neo4j(图数据库)

去 Neo4j 官网 下载 Desktop 版本(免费),创建一个新数据库,设置用户名和密码(默认都是 neo4j)。后面代码里的配置文件会用到这些信息。


三、实体抽取模型:让机器读懂商品描述

3.1 这个模型要解决什么问题?

电商的商品描述里有很多特征词,比如“顶级拍照旗舰”“高性价比”“续航持久”。这些词不在 SKU 表里,但用户搜索时经常用。我们的目标是:自动从描述文本里把这些标签抽出来,然后作为节点放进知识图谱。

举个例子:

原始描述:“顶级拍照旗舰,影像效果超乎想象。”

模型输出:["拍照旗舰"]

原始描述:“2018秋冬季新款韩版平底高帮鞋女休闲二棉鞋加绒运动厚底高邦鞋潮”

模型输出:["2018秋冬季新款", "韩版", "加绒", "厚底"]

这种任务叫命名实体识别(NER)。我们用的是 BERT 中文预训练模型,在标注好的数据上微调。

3.2 数据标注:用 LabelStudio 打标签

你不需要自己从零标数据。我提供了标注好的 JSON 文件。

数据集下载pan.baidu.com/s/1WW9NapxT…

但为了让你理解格式,我解释一下:

{
    "text": "麦德龙德国进口双心多维叶黄素护眼营养软胶囊30粒x3盒眼干涩",
    "label": [
        {
            "start": 3,      // 实体开始位置(从0数)
            "end": 7,        // 实体结束位置(不包含7)
            "text": "德国进口",
            "labels": ["TAG"]
        },
        {
            "start": 14,
            "end": 16,
            "text": "护眼",
            "labels": ["TAG"]
        }
    ]
}

start 和 end 是字符位置。比如“麦德龙”三个字占位置 0,1,2,那么“德国进口”就从位置 3 开始,到位置 7 结束(不包含7,实际占 3,4,5,6)。

3.3 数据预处理:把标注转成 BIO 格式

BERT 模型不能直接吃 start/end,我们需要把它转成BIO 标签序列

  • B

    :实体的第一个字

  • I

    :实体的后续字

  • O

    :非实体

例如“德国进口”四个字,对应的标签是 B, I, I, I。

代码 process.py 负责这个转换:

# graph/src/models/ner/process.py
from datasets import load_dataset
from transformers import AutoTokenizer
from configuration import config   # 配置文件,里面定义了数据路径、模型名称等
 
def process():
    # ------------------- 1. 加载原始 JSON 数据 -------------------
    # load_dataset 是 Hugging Face 提供的万能数据加载器,支持 json、csv、parquet 等格式
    dataset = load_dataset("json", data_files=str(config.DATA_DIR / 'ner' / 'raw' / 'data.json'))['train']
 
    # 去掉我们用不到的字段,只保留 text 和 label
    dataset = dataset.remove_columns(["id", "annotator", "annotation_id", "created_at", "updated_at", "lead_time"])
 
    # ------------------- 2. 划分训练集、验证集、测试集 -------------------
    # 先分出 80% 作为训练集,剩下 20% 再平分给验证集和测试集(各10%)
    dataset_dict = dataset.train_test_split(train_size=0.8)
    dataset_dict['test'], dataset_dict['valid'] = dataset_dict['test'].train_test_split(test_size=0.5).values()
 
    # ------------------- 3. 加载分词器和标签映射 -------------------
    tokenizer = AutoTokenizer.from_pretrained(config.NER_MODEL)  # config.NER_MODEL = "bert-base-chinese"
    id2label = ['B', 'I', 'O']   # 数字 -> 标签
    label2id = {label: id for id, label in enumerate(id2label)}  # 标签 -> 数字
 
    # ------------------- 4. 定义转换函数(核心) -------------------
    def map_func(example):
        # 把字符串拆成单个字符列表,因为中文适合按字切分
        tokens = list(example['text'])
 
        # 先用 tokenizer 把字符转成模型输入的 id,同时生成 attention_mask 等
        # is_split_into_words=True 表示输入已经是 token 列表,不需要再分词
        inputs = tokenizer(tokens, truncation=True, is_split_into_words=True)
 
        # 初始化所有位置的标签为 O(数字2,因为 O 对应 id=2)
        labels = [label2id['O']] * len(tokens)
 
        # 遍历标注的实体,把对应位置的标签改为 B 和 I
        for entity in example['label']:
            start = entity['start']
            end = entity['end']
            # 第一个字是 B,后面都是 I
            labels[start:end] = [label2id['B']] + [label2id['I']] * (end - start - 1)
 
        # BERT 会在句子开头加 [CLS]  token,结尾加 [SEP] token
        # 对应位置的标签我们设为 -100,训练时会自动忽略
        labels = [-100] + labels + [-100]
        inputs['labels'] = labels
        return inputs
 
    # 对三个数据集都执行 map_func,并删除原来的 text 和 label 列
    dataset_dict = dataset_dict.map(map_func, batched=False, remove_columns=['text', 'label'])
 
    # 保存处理后的数据到磁盘(格式是 Hugging Face 的 Dataset 格式,方便下次直接加载)
    dataset_dict.save_to_disk(config.DATA_DIR / 'ner' / 'processed')
 
if __name__ == '__main__':
    process()

通俗解释:这个过程就像把一份标注了“哪个词到哪个词是标签”的文档,变成每个字旁边写一个字母(B/I/O)的新文档,方便模型学习。

3.4 模型训练:让 BERT 学会识别标签

训练脚本 train.py 负责加载预处理好的数据,用 BERT 进行微调。

# graph/src/models/ner/train.py
import evaluate
from datasets import load_from_disk
from transformers import (
    AutoModelForTokenClassification, Trainer, TrainingArguments,
    DataCollatorForTokenClassification, AutoTokenizer
)
from configuration import config
 
# ------------------- 1. 加载模型 -------------------
# AutoModelForTokenClassification 会自动为 BERT 加上一个分类头,输出维度 = 标签数(3)
model = AutoModelForTokenClassification.from_pretrained(
    config.NER_MODEL,               # "bert-base-chinese"
    num_labels=len(config.LABELS),  # config.LABELS = ['B','I','O']
    id2label=id2label,
    label2id=label2id
)
 
# ------------------- 2. 加载数据集 -------------------
train_dataset = load_from_disk(config.DATA_DIR / 'ner' / 'processed' / 'train')
valid_dataset = load_from_disk(config.DATA_DIR / 'ner' / 'processed' / 'valid')
 
# ------------------- 3. 评估指标 -------------------
# seqeval 是专门用于序列标注(NER、分词)的评估库
seqeval = evaluate.load('seqeval')
 
def compute_metrics(prediction):
    """
    输入:模型预测的 logits 和真实标签
    输出:精确率、召回率、F1 等
    """
    logits = prediction.predictions
    preds = logits.argmax(axis=-1)   # 取概率最大的类别索引
    labels = prediction.label_ids
 
    # 把数字标签转回 'B','I','O' 字符串,同时过滤掉 -100(填充位)
    true_predictions = [
        [id2label[p] for (p, l) in zip(pred, label) if l != -100]
        for pred, label in zip(preds, labels)
    ]
    true_labels = [
        [id2label[l] for (p, l) in zip(pred, label) if l != -100]
        for pred, label in zip(preds, labels)
    ]
 
    return seqeval.compute(predictions=true_predictions, references=true_labels)
 
# ------------------- 4. 训练参数 -------------------
training_args = TrainingArguments(
    output_dir=str(config.CHECKPOINT_DIR / 'ner'),   # 模型保存目录
    per_device_train_batch_size=2,    # 每块 GPU 的训练批次大小(小数据集可以设小点)
    logging_steps=20,                 # 每20步打印一次日志
    num_train_epochs=10,              # 训练10轮
    save_steps=20,                    # 每20步保存一次检查点
    save_total_limit=3,               # 最多保留3个检查点
    eval_strategy='steps',            # 按步数进行评估
    eval_steps=20,                    # 每20步评估一次
    load_best_model_at_end=True,      # 训练结束后加载验证集上最好的模型
    metric_for_best_model='eval_overall_f1',  # 用 F1 分数判断好坏
    greater_is_better=True
)
 
# ------------------- 5. 创建 Trainer 并开始训练 -------------------
trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=DataCollatorForTokenClassification(tokenizer=tokenizer, padding=True),
    train_dataset=train_dataset,
    eval_dataset=valid_dataset,
    compute_metrics=compute_metrics
)
 
trainer.train()
trainer.save_model(config.CHECKPOINT_DIR / 'ner' / 'best_model')

通俗解释:训练就像让一个聪明的学生(BERT)做很多道“给字贴标签”的练习题,每做一批题就看看答案,错了就调整一下思路。10轮下来,它就学会了。

3.5 模型预测:从文本里抽实体

训练好后,我们需要一个方便的类来调用模型,从任意文本中提取实体标签。

# graph/src/models/ner/predict.py
import torch
from transformers import AutoModelForTokenClassification, AutoTokenizer
from configuration import config
 
class Predictor:
    def __init__(self, model, tokenizer, device):
        self.model = model.to(device)
        self.model.eval()          # 切换到评估模式(关闭 dropout 等)
        self.tokenizer = tokenizer
        self.device = device
 
    def predict(self, inputs: str | list, batch_size=8):
        """返回 BIO 标签序列"""
        # 为了方便,统一处理成列表
        is_str = isinstance(inputs, str)
        if is_str:
            inputs = [inputs]
 
        predictions = []
        for i in range(0, len(inputs), batch_size):
            batch = inputs[i:i+batch_size]
            # 把每个句子拆成字列表
            batch = [list(text) for text in batch]
            batch_inputs = self.tokenizer(batch, return_tensors="pt", padding=True, 
                                          truncation=True, is_split_into_words=True)
            batch_inputs = {k: v.to(self.device) for k, v in batch_inputs.items()}
 
            with torch.no_grad():   # 不计算梯度,加快推理
                outputs = self.model(**batch_inputs)
                logits = outputs.logits
                batch_preds = torch.argmax(logits, dim=-1).tolist()
 
            # 去掉 [CLS] 和 [SEP] 对应的预测
            for text, pred in zip(batch, batch_preds):
                pred = pred[1:1+len(text)]   # 第一个是 [CLS],最后一个是 [SEP],都去掉
                pred = [self.model.config.id2label[id] for id in pred]
                predictions.append(pred)
 
        if is_str:
            return predictions[0]
        return predictions
 
    def extract(self, inputs: str | list):
        """从文本中提取实体列表(连续 B-I 序列)"""
        is_str = isinstance(inputs, str)
        if is_str:
            inputs = [inputs]
 
        # 先预测 BIO 标签
        results = self.predict(inputs)
 
        all_entities = []
        for text, pred in zip(inputs, results):
            entities = self._bio_to_entities(text, pred)
            all_entities.append(entities)
 
        return all_entities[0] if is_str else all_entities
 
    def _bio_to_entities(self, tokens, labels):
        """
        把 BIO 序列转成实体列表。
        规则:遇到 B 开始新实体,遇到 I 且前面有实体则追加,遇到 O 结束当前实体。
        """
        entities = []
        current_entity = ""
        for token, label in zip(tokens, labels):
            if label == "B":
                if current_entity:
                    entities.append(current_entity)
                current_entity = token
            elif label == "I":
                if current_entity:      # 只有前面有 B 才接续
                    current_entity += token
                # 如果前面没有 B,这个 I 就丢弃
            else:  # 'O'
                if current_entity:
                    entities.append(current_entity)
                    current_entity = ""
        if current_entity:
            entities.append(current_entity)
        return entities
 
# 使用示例
if __name__ == '__main__':
    model = AutoModelForTokenClassification.from_pretrained(str(config.CHECKPOINT_DIR / 'ner' / 'best_model'))
    tokenizer = AutoTokenizer.from_pretrained(str(config.CHECKPOINT_DIR / 'ner' / 'best_model'))
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    predictor = Predictor(model, tokenizer, device)
 
    text = "2018秋冬季新款韩版平底高帮鞋女休闲二棉鞋加绒运动厚底高邦鞋潮"
    entities = predictor.extract(text)
    print(entities)   # 输出: ['2018秋冬季新款', '韩版', '加绒', '厚底']

四、知识图谱构建:把数据变成一张大网

4.1 为什么要用图数据库?

传统数据库(MySQL)存的是表格,要查“苹果手机有哪些型号”需要做多次 JOIN(连接表),很慢且不直观。图数据库里,数据是节点关系,比如:

  • 节点:(品牌:Apple)、(SPU:iPhone 16 Pro)、(分类:手机)

  • 关系:(iPhone 16 Pro)-[:Belong_to]->(Apple)、(iPhone 16 Pro)-[:Has_Tag]->(拍照旗舰)

查询时沿着关系走几步就能拿到结果,非常自然。

4.2 同步结构化数据:把 MySQL 的表搬到 Neo4j

我们需要把 MySQL 里的分类、属性、SPU、SKU、品牌都转换成 Neo4j 的节点和关系。

首先写两个工具类:MysqlReader 负责从 MySQL 读数据,Neo4jWriter 负责往 Neo4j 写数据。

# graph/src/datasync/utils.py
import pymysql
from neo4j import GraphDatabase
from pymysql.cursors import DictCursor
from configuration import config
 
class MysqlReader:
    def __init__(self):
        # 建立 MySQL 连接
        self.connection = pymysql.connect(**config.MYSQL_CONFIG)
        # DictCursor 让查询结果返回字典,字段名作为 key
        self.cursor = self.connection.cursor(cursor=DictCursor)
 
    def read_data(self, sql):
        self.cursor.execute(sql)
        return self.cursor.fetchall()   # 返回列表,每个元素是一个 dict
 
    def close(self):
        self.cursor.close()
        self.connection.close()
 
 
class Neo4jWriter:
    def __init__(self):
        self.driver = GraphDatabase.driver(
            uri=config.NEO4J_CONFIG['uri'],
            auth=(config.NEO4J_CONFIG['user'], config.NEO4J_CONFIG['password'])
        )
 
    def write_nodes(self, label, batch_data, batch_size=20):
        """批量创建节点,使用 MERGE 避免重复"""
        for i in range(0, len(batch_data), batch_size):
            batch = batch_data[i:i+batch_size]
            # UNWIND 把列表展开成多行,每行执行 MERGE
            cypher = f"""
                UNWIND $batch AS row
                MERGE (n:{label} {{id: row.id, name: row.name}})
            """
            self.driver.execute_query(cypher, parameters_={"batch": batch})
 
    def write_relationships(self, start_label, end_label, relationships, rel_type, batch_size=20):
        """批量创建关系"""
        for i in range(0, len(relationships), batch_size):
            batch = relationships[i:i+batch_size]
            cypher = f"""
                UNWIND $batch AS row
                MATCH (start:{start_label} {{id: row.start_id}})
                MATCH (end:{end_label} {{id: row.end_id}})
                MERGE (start)-[:{rel_type}]->(end)
            """
            self.driver.execute_query(cypher, parameters_={"batch": batch})

然后,对每一张表写一个同步方法。以分类为例:

# graph/src/datasync/table_sync.py
class TableSynchronizer:
    def __init__(self):
        self.mysql_reader = MysqlReader()
        self.neo4j_writer = Neo4jWriter()
 
    # 同步一级分类
    def sync_base_category1(self):
        sql = "SELECT id, name FROM base_category1"
        data = self.mysql_reader.read_data(sql)
        self.neo4j_writer.write_nodes(label="Category1", batch_data=data)
 
    # 同步二级分类
    def sync_base_category2(self):
        sql = "SELECT id, name FROM base_category2"
        data = self.mysql_reader.read_data(sql)
        self.neo4j_writer.write_nodes(label="Category2", batch_data=data)
 
    # 同步三级分类
    def sync_base_category3(self):
        sql = "SELECT id, name FROM base_category3"
        data = self.mysql_reader.read_data(sql)
        self.neo4j_writer.write_nodes(label="Category3", batch_data=data)
 
    # 同步一级分类和二级分类之间的 Belong 关系
    def sync_category1_category2(self):
        sql = """
            SELECT c2.id AS start_id, c2.category1_id AS end_id
            FROM base_category2 c2
        """
        rels = self.mysql_reader.read_data(sql)
        self.neo4j_writer.write_relationships(
            start_label="Category2",
            end_label="Category1",
            relationships=rels,
            rel_type="Belong"
        )
 
    # ... 其他同步方法类似:属性、SPU、SKU、品牌等

最后在 if __name__ == '__main__' 里按顺序调用所有同步方法。执行后,Neo4j 里就会出现完整的商品结构图谱。

4.3 同步非结构化数据:把商品描述变成标签节点

这是项目的一大亮点。我们利用上面训练好的 NER 模型,从 spu_info 表的 description 字段里提取标签,然后把每个标签作为一个 Tag 节点创建到图谱中,并建立 (SPU)-[:Have]->(Tag) 的关系。

# graph/src/datasync/text_sync.py
import torch
from transformers import AutoModelForTokenClassification, AutoTokenizer
from configuration import config
from datasync.utils import Neo4jWriter, MysqlReader
from models.ner.predict import Predictor
 
class TextSynchronizer:
    def __init__(self):
        self.neo4j_writer = Neo4jWriter()
        self.mysql_reader = MysqlReader()
        self.extractor = self._init_extractor()
 
    def _init_extractor(self):
        model = AutoModelForTokenClassification.from_pretrained(str(config.CHECKPOINT_DIR / 'ner' / 'best_model'))
        tokenizer = AutoTokenizer.from_pretrained(str(config.CHECKPOINT_DIR / 'ner' / 'best_model'))
        device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        return Predictor(model, tokenizer, device)
 
    def sync_spu_desc(self):
        # 1. 从 MySQL 读取所有 SPU 的 id 和 description
        sql = "SELECT id, description FROM spu_info"
        spus = self.mysql_reader.read_data(sql)
        ids = [spu['id'] for spu in spus]
        descs = [spu['description'] for spu in spus]
 
        # 2. 批量调用 NER 模型提取标签(每个描述可能提取出多个标签)
        all_entities = self.extractor.extract(descs)   # 返回 list of list
 
        # 3. 准备节点和关系数据
        nodes = []      # 将要创建的 Tag 节点
        rels = []       # 将要创建的 Have 关系
        for spu_id, entities in zip(ids, all_entities):
            for idx, entity in enumerate(entities):
                node_id = f"{spu_id}-{idx}"   # 组合 ID 确保唯一性
                nodes.append({"id": node_id, "name": entity})
                rels.append({"start_id": spu_id, "end_id": node_id})
 
        # 4. 写入 Neo4j
        self.neo4j_writer.write_nodes("Tag", nodes)
        self.neo4j_writer.write_relationships("SPU", "Tag", rels, "Have")
 
if __name__ == '__main__':
    synchronizer = TextSynchronizer()
    synchronizer.sync_spu_desc()

执行后,Neo4j 里就会出现大量 Tag 节点,比如“拍照旗舰”“续航持久”“高性价比”,并且每个商品都通过 Have 关系连接到了对应的标签。


五、智能客服:让大模型替你写查询

现在图谱已经建好了,里面有商品、分类、品牌、标签,还有各种关系。用户问“推荐一款拍照好的手机”,我们怎么让系统理解并查询?

思路是:让大模型(LLM)根据用户问题,自动写出图查询语言(Cypher)。然后再用混合检索把模糊的实体名称(比如“拍照好”)对齐到图谱里准确的标签(比如“拍照旗舰”),最后执行查询,把结果交给大模型生成回答。

5.1 创建混合检索索引

实体对齐需要同时用到全文检索(精确匹配)和向量检索(语义相似)。我们提前为 SPU、品牌、分类等实体创建两种索引。

# graph/src/web/utils.py
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_neo4j import Neo4jGraph
 
def create_full_text_index(graph, index_name, label, property_name):
    """创建全文索引"""
    cypher = f"""
        CREATE FULLTEXT INDEX {index_name}
        FOR (n:{label}) ON EACH [n.{property_name}]
    """
    graph.query(cypher)
 
def create_embedding_index(graph, index_name, label, property_name, embedding_model, dim=512):
    """创建向量索引:先生成 embedding 存到节点属性,再建索引"""
    # 1. 查询所有需要 embedding 的节点
    query = f"MATCH (n:{label}) WHERE n.{property_name} IS NOT NULL RETURN id(n) AS node_id, n.{property_name} AS text"
    nodes = graph.query(query)
 
    # 2. 分批生成 embedding 并更新节点
    batch_size = 100
    for i in range(0, len(nodes), batch_size):
        batch = nodes[i:i+batch_size]
        texts = [record['text'] for record in batch]
        embeddings = embedding_model.embed_documents(texts)  # 调用模型生成向量
        for record, emb in zip(batch, embeddings):
            update_query = f"MATCH (n) WHERE id(n)=$node_id SET n.embedding=$embedding"
            graph.query(update_query, {'node_id': record['node_id'], 'embedding': emb})
 
    # 3. 创建向量索引(HNSW 算法)
    cypher_index = f"""
        CREATE VECTOR INDEX {index_name}
        FOR (n:{label}) ON n.embedding
        OPTIONS {{indexConfig: {{
            `vector.dimensions`: {dim},
            `vector.similarity_function`: 'cosine'
        }}}}
    """
    graph.query(cypher_index)
 
# 主函数:为所有实体类型创建索引
if __name__ == '__main__':
    graph = Neo4jGraph(url=config.NEO4J_CONFIG['uri'], 
                       username=config.NEO4J_CONFIG['user'],
                       password=config.NEO4J_CONFIG['password'])
 
    # 创建全文索引
    for label in ["SPU", "BaseTrademark", "Category3", "Category2", "Category1"]:
        create_full_text_index(graph, f"{label.lower()}_fulltext", label, "name")
 
    # 创建向量索引
    embedding_model = HuggingFaceEmbeddings(
        model_name="BAAI/bge-small-zh-v1.5",
        encode_kwargs={"normalize_embeddings": True}
    )
    for label in ["SPU", "BaseTrademark", "Category3", "Category2", "Category1"]:
        create_embedding_index(graph, f"{label.lower()}_vector", label, "name", embedding_model, dim=512)

5.2 智能客服服务类

核心类 ChatService 整合了 LLM、Neo4j 连接、向量存储和四个步骤。

# graph/src/web/service.py
from langchain_core.output_parsers import JsonOutputParser, StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_deepseek import ChatDeepSeek
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_neo4j import Neo4jGraph, Neo4jVector
from neo4j_graphgraph.types import SearchType
from configuration import config
 
class ChatService:
    def __init__(self):
        # 大模型(使用 DeepSeek API,也可以换成其他)
        self.llm = ChatDeepSeek(model="deepseek-chat", temperature=0, api_key=config.DEEPSEEK_API_KEY)
 
        # Neo4j 图连接
        self.graph = Neo4jGraph(
            url=config.NEO4J_CONFIG['uri'],
            username=config.NEO4J_CONFIG['user'],
            password=config.NEO4J_CONFIG['password']
        )
 
        # Embedding 模型
        self.embeddings = HuggingFaceEmbeddings(
            model_name="BAAI/bge-small-zh-v1.5",
            encode_kwargs={"normalize_embeddings": True}
        )
 
        # 为每个实体类型创建向量存储(支持混合检索)
        self.vector_stores = {
            "SPU": Neo4jVector.from_existing_index(
                self.embeddings, index_name="spu_vector", search_type=SearchType.HYBRID
            ),
            "BaseTrademark": Neo4jVector.from_existing_index(
                self.embeddings, index_name="trademark_vector", search_type=SearchType.HYBRID
            ),
            "Category3": Neo4jVector.from_existing_index(
                self.embeddings, index_name="category3_vector", search_type=SearchType.HYBRID
            ),
            # ... 其他类型
        }
 
        self.json_parser = JsonOutputParser()
        self.str_parser = StrOutputParser()
 
    def _generate_cypher(self, question: str, schema_info: str):
        """步骤1:LLM 生成 Cypher 查询和待对齐实体"""
        prompt = PromptTemplate(
            input_variables=["question", "schema_info"],
            template="""
            你是一个专业的 Neo4j Cypher 查询生成器。根据用户问题生成 Cypher 查询语句。
            用户问题: {question}
            知识图谱结构(节点类型、关系类型、属性):
            {schema_info}
            要求:
            1. 使用参数化查询,参数名用 param_0, param_1, ...
            2. 识别出需要对齐的实体(比如用户说的“拍照好”可能对应 Tag 节点中的“拍照旗舰”)
            输出格式必须是严格的 JSON,例如:
            {{
                "cypher_query": "MATCH (spu:SPU)-[:Have]->(tag:Tag) WHERE tag.name = $param_0 RETURN spu.name",
                "entities_to_align": [
                    {{"param_name": "param_0", "entity": "拍照好", "label": "Tag"}}
                ]
            }}
            """
        )
        chain = prompt | self.llm | self.json_parser
        return chain.invoke({"question": question, "schema_info": schema_info})
 
    def _entity_align(self, entities_to_align):
        """步骤2:用混合检索把模糊实体对齐到图谱里的标准名称"""
        aligned = []
        for item in entities_to_align:
            label = item['label']
            entity_text = item['entity']
            if label in self.vector_stores:
                # 混合检索:先全文匹配,再向量相似度,取最佳结果
                results = self.vector_stores[label].similarity_search(entity_text, k=1)
                if results:
                    # 把实体替换成检索到的标准名称
                    item['entity'] = results[0].page_content
            aligned.append(item)
        return aligned
 
    def _execute_cypher(self, cypher: str, params: dict):
        """步骤3:执行 Cypher 查询"""
        return self.graph.query(cypher, params=params)
 
    def _generate_answer(self, question: str, query_result: list):
        """步骤4:把查询结果转成自然语言回答"""
        prompt = PromptTemplate(
            input_variables=["question", "query_result"],
            template="""
            你是一个电商智能客服。根据用户问题和数据库查询结果,生成简洁、准确的回答。
            用户问题: {question}
            查询结果: {query_result}
            要求:回答要自然、友好,如果查询结果为空,请如实告知。
            """
        )
        chain = prompt | self.llm | self.str_parser
        return chain.invoke({"question": question, "query_result": query_result})
 
    def chat(self, question: str) -> str:
        """主流程"""
        # 获取图谱结构信息(自动从 Neo4j 提取)
        schema = self.graph.get_schema  # LangChain 提供的方法
 
        # 1. 生成 Cypher
        cypher_info = self._generate_cypher(question, schema)
        cypher_query = cypher_info['cypher_query']
        entities = cypher_info['entities_to_align']
 
        # 2. 实体对齐
        aligned_entities = self._entity_align(entities)
        params = {item['param_name']: item['entity'] for item in aligned_entities}
 
        # 3. 执行查询
        result = self._execute_cypher(cypher_query, params)
 
        # 4. 生成回答
        answer = self._generate_answer(question, result)
        return answer

5.3 提供 Web 接口

最后,我们用 FastAPI 把服务包装成 HTTP 接口,并提供一个简单的聊天页面。

# graph/src/web/schemas.py
from pydantic import BaseModel
class Question(BaseModel):
    message: str
class Answer(BaseModel):
    message: str
# graph/src/web/app.py
import uvicorn
from fastapi import FastAPI
from starlette.responses import RedirectResponse
from starlette.staticfiles import StaticFiles
from web.schemas import Question, Answer
from web.service import ChatService
from configuration import config
 
app = FastAPI()
app.mount("/static", StaticFiles(directory=str(config.WEB_STATIC_DIR)), name="static")
service = ChatService()
 
@app.get("/")
def root():
    return RedirectResponse("/static/index.html")
 
@app.post("/api/chat")
def chat(question: Question) -> Answer:
    answer = service.chat(question.message)
    return Answer(message=answer)
 
if __name__ == '__main__':
    uvicorn.run(app, host="0.0.0.0", port=8000)

启动服务后,访问 http://localhost:8000,你会看到一个聊天界面。输入问题,后台就会按四个步骤处理并返回答案。


六、总结与思考

6.1 我们做了什么?

  1. 环境搭建

    :Conda + PyTorch + 各种库,一条命令全搞定。

  2. 实体抽取模型

    :从商品描述里自动提取标签(NER),把非结构化数据变成可查询的图节点。

  3. 知识图谱构建

    :把 MySQL 里的结构化数据和 NER 提取的标签,全部同步到 Neo4j 图数据库。

  4. 混合检索索引

    :为实体创建全文索引和向量索引,让模糊匹配成为可能。

  5. 智能客服服务

    :用大模型自动生成 Cypher 查询,混合检索对齐实体,执行查询,再生成自然答案。

  6. Web 界面

    :FastAPI + 静态页面,开箱即用。

6.2 这套方案好在哪里?

  • 结构化与非结构化数据融合

    :既有 SKU 表的精确属性,又有描述文本里的特征标签,查询更全面。

  • 关系推理能力

    :图数据库天然支持多跳查询,比如“苹果手机有哪些支持5G的型号”可以沿着品牌→SPU→属性走两步。

  • 大模型辅助查询

    :不用写死规则,LLM 自动理解意图并生成 Cypher,适应各种问法。

  • 混合检索对齐

    :用户说“拍照好”,系统能匹配到“拍照旗舰”,因为向量相似度高;用户说“苹果”,系统能精确匹配“Apple”品牌,因为全文检索也生效。

6.3 可以怎么改进?

  • 多轮对话记忆

    :目前每次对话是独立的。可以引入对话历史,让模型记住用户之前问过什么。

  • 主动推荐

    :根据用户常问的标签(比如“高性价比”),在用户没问的时候主动推荐相关商品。

  • 性能优化

    :embedding 生成和 Cypher 查询都可以加缓存,减少重复计算。

  • 更多模态

    :商品图片也能用多模态模型提取特征标签,进一步丰富图谱。

6.4 写在最后

这篇文章从零开始,完整实现了电商智能客服系统。你可能注意到,整个项目没有用到特别复杂或者闭源的技术——BERT 微调 NER 模型、Neo4j 图数据库、LangChain 调用大模型,都是当前非常成熟且开源的方案。

希望这篇教程能帮你建立起知识图谱 + 大模型的技术直觉。如果你在实践过程中遇到任何问题,或者有新的想法,欢迎交流讨论。

代码即文档,动手出真知。 现在就去敲键盘吧!


完整代码下载(包含数据集):pan.baidu.com/s/1Cn5CxCUZ…