资讯动态

基于Django的智能客服系统源码解析:从关键词匹配到WebSocket升级

发布时间:2026/9/16 14:26:07 来源:尧图企业网站定制
简介这是一份基于Django框架与Python开发的智能客服系统完整源码包主要面向计算机、人工智能、通信工程等专业的在校学生和毕业设计者也可用于课程大作业或项目初期立项演示。压缩包共556个文件约2.75MB以318个Python源码文件为主干配合89个JavaScript文件、21个CSS样式文件以及SVG、PNG等图片素材覆盖后端逻辑、前端交互与界面视觉整体轻量、结构清晰便于本地运行和二次修改。系统源自高分毕业设计代码均经过运行测试答辩评审平均分达96.5分压缩包内附项目说明和README既能帮助读者理解模块划分与启动流程也能提供核心实现思路的参考。目前已有270人学习下载适合希望用Django快速搭建智能客服Demo、学习完整Web开发链路或在此基础上完成课设、毕设改造的开发者。1. 这个基于Django的智能客服系统值不值得扒开源码看你如果做过客服系统相关的毕设选题一定遇到过这种尴尬网上搜到的“智能客服”要么是空壳界面要么是只写了一个关键词匹配的函数根本无法回答“这个项目到底智能在哪”。而这份基于Django框架Python开发的智能客服系统源码是在答辩评分96.5分的前提下传出来的它把会话管理、知识库匹配和后台运营都串了起来不是玩具。我把它下载下来跑通之后最直观的感受是它的结构很规矩适合计算机相关专业做课程设计和毕业设计第二版修改也适合想搞懂Django项目怎么分层的人。本文我会从源码目录、数据模型、匹配引擎、运行排错到进阶改造逐个拆给你看其中有些细节是文档里没写、只有跑起来才会发现的。2. Django项目结构与智能客服的核心设计2.1 源码目录里那些奇怪的文件名是什么解压zip后除了常规的manage.py、myproject/、myapp/之外你会在static或templates目录里看到一堆类似bootstrap5152.css、main5152.css、responsive5152.css、prettyPhotoaeb9.css这类带数字后缀的文件。这些不是乱码而是前端工具在打包时为了防止浏览器缓存给文件名加上的版本指纹。prettyPhotoaeb9.css是图片灯箱插件prettyPhoto的样式select2.css则对应select2搜索下拉框组件。这说明项目的前端不是原生写的而是基于Bootstrap和jQuery插件拼的你在改后台页面时不要去动这些压缩后的css要改就去找对应的less或scss源文件。项目主体是典型的Django单应用结构我用树形命令看一下tree -L 2 -d myproject输出一般是这样的核心目录myproject/ ├── myproject/ # 项目配置settings.py、urls.py、wsgi.py ├── myapp/ # 业务应用models、views、urls、admin ├── templates/ # Django模板含base.html和业务页面 ├── static/ # css/js/images压缩文件都在这里 └── media/ # 用户上传的头像、图片等参数说明-L 2表示只显示两层目录-d表示只看目录不看文件。如果你的压缩包解压后第一层不是myproject而是其他名字先cd进去再看。这个结构的好处是应用目录myapp独立方便你直接把整个应用挪到自己的Django项目里复用。2.2 数据模型会话、消息、知识库怎么建表智能客服最核心的表不是用户表而是会话表和消息表。这个项目的数据模型定义在myapp/models.py中我看过之后发现它用了三个主要的模型。下面是我简化后的关键代码from django.db import models from django.contrib.auth.models import User class Conversation(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name发起用户) session_key models.CharField(max_length64, uniqueTrue, verbose_name会话唯一标识) status models.CharField( max_length16, choices[(open, 开放), (closed, 已关闭), (transferred, 已转人工)], defaultopen ) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: ordering [-updated_at] class Message(models.Model): conversation models.ForeignKey(Conversation, on_deletemodels.CASCADE, related_namemessages) sender models.CharField(max_length8, choices[(user, 用户), (bot, 机器人), (agent, 人工)]) content models.TextField() created_at models.DateTimeField(auto_now_addTrue) class KnowledgeItem(models.Model): question models.CharField(max_length255) answer models.TextField() keywords models.CharField(max_length500, blankTrue, help_text用逗号分隔的关键词) enabled models.BooleanField(defaultTrue)代码逻辑说明Conversation通过session_key识别是不是同一个访客这样用户刷新页面后还能继续上一次对话而不是每次刷新都新建会话。Message用外键关联会话sender字段区分消息来自用户还是机器人这是后来做人工转接时判断消息来源的依据。KnowledgeItem是知识库表keywords字段存储逗号分隔的扩展词方便在匹配阶段做加权。参数说明max_length64的session_key是Django默认会话ID的容量上限如果你改了会话引擎存储的key变长这里要同步扩大。on_deletemodels.CASCADE表示删除会话时级联删除这条会话下所有消息这在维护时很有用但生产环境如果要做操作审计建议改为PROTECT。related_namemessages让反向查询可以直接用conversation.messages.all()不用再写message_set。2.3 用Django ORM管理会话状态的要点会话状态管理在普通web项目里很简单但在客服系统里有一个坑用户可能同时开多个页面会产生多个session_key如果再叠加异步请求就会把会话搞乱。常见做法是在登录后把session_key固定为用户ID或者只允许同一IP只有一个活跃会话。项目里用的是最简单的一种也就是每次都拿request.session.session_key不存在就创建def get_or_create_conversation(request): session_key request.session.session_key if not session_key: request.session.save() session_key request.session.session_key conversation, created Conversation.objects.get_or_create( session_keysession_key, status__in[open, transferred], defaults{user: request.user if request.user.is_authenticated else None} ) return conversation这里get_or_create的status__in条件是个小陷阱如果用户上一轮已经手动关闭了会话这里不会匹配到会新建一条。而defaults里的user字段在匿名访问时会被赋成None前提是你的User外键设置了nullTrue如果没有这一行会直接报错。所以你自己复刻时建议把外键写成ForeignKey(User, on_deletemodels.SET_NULL, nullTrue, blankTrue)这样匿名用户也能正常创建会话。3. 客服匹配引擎从关键词到意图识别的实现3.1 第一步先做带权重的关键词匹配所谓智能本质上就是多个规则叠加。项目里的myapp/services.py里封装了match_answer函数它并不直接用if in这种原始逻辑而是先对用户问题做词频统计再和知识库里的关键词集合做加权打分。我简化出核心代码def match_answer(message, knowledge_items): import re from collections import Counter # 去除标点切成词 words re.findall(r[\u4e00-\u9fa5a-zA-Z0-9], message.lower()) word_count Counter(words) best_item None best_score 0.0 for item in knowledge_items: if not item.enabled: continue # 知识条目的关键词列表如发票,报销,开票 item_keywords [kw.strip() for kw in item.keywords.split(,) if kw.strip()] score 0.0 for kw in item_keywords: # 关键词在问题中出现基础分1 if kw in message: score 1.0 # 关键词长度超过2额外加权0.5 if len(kw) 2: score 0.5 # 问题本身的词频也参与打分避免长问题无脑匹配 for w, cnt in word_count.items(): if w in item_keywords: score cnt * 0.2 if score best_score: best_score score best_item item # 阈值低于2分直接不返回让调用方走兜底逻辑 if best_score 2.0: return None return best_item.answer逻辑说明先把用户输入切成词然后用Counter计数接着遍历所有启用的知识条目。对每个条目先检查其keywords中是否有能直接命中的词命中一次加1分长词额外加0.5这是因为长词信息量更大。之后再遍历用户问题分词结果如果又匹配到同一个关键词额外加cnt*0.2把关键词反复出现的情况也考虑进去。最终得分低于2直接不返回答案避免误答。参数说明2.0这个阈值是经验值你手里的数据如果知识库很全面可以降为1.5如果经常答非所问就调高到2.5。word_count的cnt*0.2是我建议的加权系数原始项目里没有这一步我在实际操作中加上的效果是用户连续问“发票发票发票”时能更准地命中发票相关条目。3.2 第二步用jieba分词和TF-IDF补相似度关键词匹配的硬伤是同义词。比如知识库里写的是“运费”用户问的是“邮费”关键词匹配就挂了。项目文档里没有提但代码里其实预留了相似度计算的接口在services.py下有cosine_similarity函数。我帮它补齐了TF-IDF逻辑import jieba import jieba.analyse def calc_tfidf_similarity(user_text, kb_text): # 用TF-IDF提取每段文字的关键词 user_keywords jieba.analyse.extract_tags(user_text, topK10, withWeightTrue) kb_keywords jieba.analyse.extract_tags(kb_text, topK10, withWeightTrue) # 构建词权重字典 user_dict dict(user_keywords) kb_dict dict(kb_keywords) all_words set(user_dict.keys()) | set(kb_dict.keys()) # 构造向量点积和模长 dot_product sum(user_dict.get(word, 0) * kb_dict.get(word, 0) for word in all_words) user_norm sum(w**2 for w in user_dict.values()) ** 0.5 kb_norm sum(w**2 for w in kb_dict.values()) ** 0.5 if user_norm 0 or kb_norm 0: return 0.0 return dot_product / (user_norm * kb_norm)逻辑说明jieba.analyse.extract_tags返回的是一个(词, 权重)元组列表权重越高说明这个词在本段文字中越能代表主题。通过把两个文本的TF-IDF权重向量化再计算余弦相似度就能把“邮费”和“运费”这类词拉近——因为它们在知识库中常出现在同一句话里词向量类似但这里只用了TF-IDF没有词向量所以对同义词的泛化能力有限。如果你想让这个系统更聪明下一步可以换用word2vec或fastText把关键词换成向量平均值再算相似度。参数说明topK10表示每段文本抽取10个关键词抽取过多会引入噪声词。withWeightTrue要求返回权重如果改成默认False下面dict(user_keywords)就会报错。调用时我建议把关键词匹配和TF-IDF匹配的结果做加权融合final_score keyword_score * 0.7 tfidf_score * 1.3这里tfidf_score是0到1的小数所以权重给到1.3也不太会超过关键词分数你可以根据测试集调整。3.3 兜底回复和人工转接的触发条件没有兜底回复的客服系统就是一个只会复读的机器人。项目里兜底逻辑写在views.py中我摘出关键分支def answer_with_fallback(request, message): answer match_answer(message, KnowledgeItem.objects.filter(enabledTrue)) if answer is None: # 记录一句话当前问题无人能答 fallback_text 亲这个问题我还没学会你可以换个说法或者直接输入“转人工”。 else: fallback_text answer # 如果用户请求转人工触发人工转接 if any(kw in message for kw in [人工, 客服, 转人工]): conversation get_or_create_conversation(request) conversation.status transferred conversation.save() fallback_text 已为您转接人工坐席请稍候。判断message里是否包含“人工”等词用的是any(kw in message)没有用正则因为这几个词长度短且没有歧义。注意“转人工”这个意图必须放在兜底回复之前判断否则match_answer永远返回None用户永远触发不了人工转接。这是我在测试时发现的顺序问题。4. 跑通这个项目环境配置、初始化数据与排错4.1 Python与Django版本怎么搭配这份源码是在Python 3.8 Django 3.2上跑的如果你用Python 3.10以上的版本Django 3.2会报django.core.exceptions.ImproperlyConfigured这是因为新版本Python对cgi模块的移除。建议直接用Python 3.8或3.9新建虚拟环境python3.8 -m venv venv source venv/bin/activate pip install django3.2.25 jieba0.42.1参数说明venv是虚拟环境目录不装虚拟环境直接装到系统里也不影响跑但会影响你换项目时依赖打架。jieba是分词组件只有用到3.2节的相似度功能才需要装如果你只跑原项目可以不装。如果你系统里没有python3.8命令在Ubuntu/Debian上执行sudo apt install python3.8 python3.8-venvCentOS上则是yum install python3.8。4.2 数据库迁移和初始化数据解压后的源码里有没有db.sqlite3我看了内容发现zip里没有带数据库文件这说明你需要在本地自己执行迁移。直接跑python manage.py makemigrations myapp python manage.py migrate python manage.py createsuperuser python manage.py loaddata initial_knowledge.json注意migrate会创建Django自带的所有表包括auth_user、session等。loaddata是装载知识库初始数据如果这个json文件在myapp/fixtures/目录下Django会自动找到。如果没有初始数据文件你就得去Django admin里手动添加知识条目。此时可以先启动服务进入后台添加python manage.py runserver 0.0.0.0:8000然后浏览器打开http://127.0.0.1:8000/admin用刚才创建的超级用户登录。在后台找到“知识条目”或“KnowledgeItem”的添加页面填一个问题、一个答案、几个关键词记得勾选启用状态。4.3 启动后如何验证系统真的在工作光看不报错不算运行成功我建议你按下面这张表逐项检查检查项预期表现失败时看什么首页加载客服聊天窗口出现且无404静态资源报错浏览器F12看Networkcss/js是否返回200发送问题“你们公司怎么报销”机器人回复报销相关答案看后端日志有无异常检查知识库里是否有该问题发送“转人工”会话状态变为transferred回复转接提示在admin中打开Conversation看status字段连续刷新页面对话记录还在确认session_key没有变化检查Cookie是否被禁用如果静态文件全是404这是Django开发环境最常踩的坑。原因是你的settings.py里没有配STATICFILES_DIRS。在settings.py末尾加上import os STATIC_URL /static/ STATICFILES_DIRS [ os.path.join(BASE_DIR, static), ]这段代码的作用是告诉Django除了每个app自带的static目录项目根目录下的static文件夹也是静态文件来源。注意加完必须重启runserver否则不生效。如果你用的是Django 4.0以上os.path.join没问题但更好的是用BASE_DIR / static不过原项目是这样写的改不改不影响。4.4 另一个排错点模板继承路径错误模板报错TemplateDoesNotExist时你会看到完整模板名和自己实际的模板路径对不上。这个项目的模板是放在templates/base.html然后各个页面用{% extends base.html %}继承。如果报错找不到检查你的settings.py里TEMPLATES中的DIRS有没有加入templates目录TEMPLATES [ { BACKEND: django.template.backends.django.DjangoTemplates, DIRS: [os.path.join(BASE_DIR, templates)], APP_DIRS: True, ... } ]DIRS之后Django才会在项目根目录的templates文件夹下找模板。如果这里配置为空它只会去每个app里的templates子目录找而项目根目录的base.html就永远不被加载。5. 把毕设升级成可落地的骚扰级客服的五个实操点5.1 用WebSocket替换HTTP轮询让回复实时推送原项目的对话是基于HTTP表单提交的用户发一条消息后得整页刷新才能看到回复。生产环境没法这么干。你不需要引入重量级的Channels只需要用django-channels的轻量写法pip install channels然后在myproject/settings.py里加INSTALLED_APPS [ ... channels, ] ASGI_APPLICATION myproject.asgi.application接着在myapp/consumers.py中实现一个简单的聊天消费者from channels.generic.websocket import AsyncWebsocketConsumer import json class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): # 每个会话一个房间房间名用session_key self.room_name self.scope[url_route][kwargs][session_key] await self.channel_layer.group_add(self.room_name, self.channel_name) await self.accept() async def receive(self, text_data): text_data_json json.loads(text_data) message text_data_json[message] # 此处调用你的回答函数注意因为views里的逻辑是同步的 # 这里要改用database_sync_to_async包装 answer await self.get_answer(message) await self.send(text_datajson.dumps({answer: answer})) async def disconnect(self, close_code): await self.channel_layer.group_discard(self.room_name, self.channel_name)这段代码里self.scope[url_route][kwargs][session_key]是从WebSocket URL里取会话ID比如/ws/chat/abc123/这样每个用户进入自己的独立通道。database_sync_to_async是Channels自带的同步转异步装饰器因为你的match_answer和Django ORM都是同步代码不包装会阻塞事件循环。改造后前端的JavaScript只需要用原生WebSocket对象连接不用再写轮询。5.2 知识库自动导入从Excel批量补充问答毕设演示时你可能会想快速加几十条问答一条条在admin里点太慢。我写了一个Django management命令脚本放在myapp/management/commands/import_knowledge.pyfrom django.core.management.base import BaseCommand import csv from myapp.models import KnowledgeItem class Command(BaseCommand): help 从CSV文件导入知识库格式question,answer,keywords def add_arguments(self, parser): parser.add_argument(file, typestr) def handle(self, *args, **options): file_path options[file] with open(file_path, r, encodingutf-8-sig) as f: reader csv.DictReader(f) count 0 for row in reader: KnowledgeItem.objects.update_or_create( questionrow[question], defaults{ answer: row[answer], keywords: row[keywords], } ) count 1 self.stdout.write(self.style.SUCCESS(f成功导入 {count} 条知识))运行命令是python manage.py import_knowledge excels/kb.csv注意utf-8-sig是为了兼容Excel导出的UTF-8 BOM格式如果文件是GBK你要改成gb18030。update_or_create用question作为唯一匹配键意味着同样的问法重复出现在Excel里不会导致数据重复而是直接更新答案。这个技巧做完后你会发现把一份几百行的客服FAQ塞进系统只需要几秒钟。如果你想让这个项目在答辩时更有亮点建议再改一个地方把匹配引擎从纯关键词改成先用关键词粗筛候选集比如只选出前5条再对候选集用TF-IDF相似度排序最后返回相似度超过0.35的答案否则走兜底。这个改进既快又不会丢失准确率而且在答辩时能讲清楚“为什么不用全部遍历”的性能优化思路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价