☰
Python电商订单数据可视化分析系统:Django与大模型Agent实战
2026/9/30 3:17:23 网站建设 项目流程

每年到毕业季,总有一堆人对着"计算机毕业设计"这几个字头疼。选题太老吧,答辩老师看一眼就没兴趣;选题太新吧,又怕自己hold不住。今天分享一个我实际带过多次、也在线上跑通的选题方向:Python电商订单数据可视化分析系统,基于Django框架,把数据分析、可视化、大模型和Agent串起来做。这套东西不是空中楼阁,是真能跑、真能演示、也真能讲出东西的。

先把这个系统是啥说清楚:你有一个电商平台产生的订单数据(可以是模拟数据,也可以是爬虫抓的公开数据),然后用Django写后端,把订单数据做清洗、聚合、统计,通过API给前端提供数据;前端用ECharts等可视化库做成大屏看板,展示销售额趋势、商品排行、用户画像这些东西;再往上加一层,接一个大模型接口,让用户直接用中文问"上个月卖得最好的三个商品是什么",系统自动从数据库里查出结果并返回答案。这个设计的好处是:既有传统的数据处理和可视化展示,又有当前热门的大模型应用场景,技术深度够,答辩时能讲的东西非常多。

这篇文章面向的读者,我觉得有三类人:一是正在选题的计算机专业学生,想把毕设做得既有工作量又有亮点;二是想快速上手Django数据分析项目的开发者,需要一个完整的参考路径;三是对大模型落地应用感兴趣的同行,想看看除了聊天机器人之外,大模型还能怎么接到业务系统里。不论你是哪类,下面这套从设计到落地的完整拆解都值得收藏。

1. 项目概述与整体设计思路

1.1 这个系统到底解决什么问题

电商平台的订单数据是典型的"高价值密度低"的数据。一个平台每天产生几千几万条订单,但真正对运营决策有用的信息,比如哪些商品卖得好、哪个时段下单最集中、哪些地区的用户购买力强,需要经过清洗、聚合、统计才能真正看出来。这个系统的核心目标,就是把这串原始订单数据变成一眼能看懂的可视化图表,再用自然语言问答的方式降低看数据门槛。

换句话讲,系统解决的是三个层面的问题。第一个层面是数据管理:订单数据散落在Excel或数据库里,查询和统计都要写SQL或者手动操作,系统把这些集中起来并提供统一接口;第二个层面是分析展示:光有数据不够,得画出来,销售额趋势用折线图、商品排行用柱状图、品类占比用饼图,让决策者扫一眼大屏就知道当前经营状况;第三个层面是智能交互:非技术用户不会写SQL,但他们想知道"江浙沪地区上周的客单价是多少",这时候大模型语言接口就派上用场,把用户的问题翻译成数据查询动作,并组织成自然语言回答。

这些需求叠在一起,就决定了系统的整体形态:一个B/S架构的数据应用,后端负责数据存取和业务逻辑,前端负责可视化展示和人机交互,中间用API通信。Django在这个场景下非常合适,它的ORM让数据模型定义变得简洁,自带Admin后台可以直接管理数据,模板和DRF(Django REST Framework)又能快速搭出接口层,对整个开发周期来说非常友好。

1.2 技术选型背后的逻辑

Django、MySQL、ECharts、DeepSeek,这套组合是我反复校验过的,每个选择都有明确的理由。

先说Django框架。网上经常有人争论Flask轻量、FastAPI高性能,为什么选Django?核心原因是毕设或者中小型数据项目要的不是极致性能,而是完善的配套和整体的开发效率。Django自带Admin后台,数据模型一定义,后台管理页面就出来了,演示的时候可以直接进去增删改查,这个效果在答辩现场非常加分;ORM抽象层让你不需要手写大量SQL,定义好模型类就能做复杂查询;模板引擎加上Django的静态文件管理,部署起来也比前后端彻底分离的方案简单。如果你的项目经验里没有大型前端工程的需求,用Django经典的服务端渲染配合部分Ajax动态刷新,完全够用,而且架构清晰,逻辑好讲。

再说可视化技术选型。目前主流有ECharts、AntV G2Plot、Chart.js、D3.js这么几个方向。我的建议是ECharts,理由很直接:文档最全、例子最多、对中文场景的支持最好,而且地图组件可以直接用中国地图GeoJSON,做地域分布分析的时候特别方便。ECharts是纯前端库,通过npm或者CDN引入即可,后端只需要提供规范的JSON数据结构,两端通过接口对好字段名就能画出图来。

然后是大模型的选型。标题里提到的DeepSeek是目前国内开源开放做得比较成熟的模型,它的API兼容OpenAI的调用格式,用Python的openai库或者直接requests都能调,非常轻量。更重要的是,它支持function calling(函数调用),这让Agent的实现路径变得非常顺:你给模型定义好"有什么工具可以用",模型收到用户问题后,自己决定调用哪个工具、传什么参数,然后你执行工具返回结果,模型再把结果整理成人话。整个过程不用微调模型,纯靠提示词和工具定义就能完成一个可用的数据分析Agent。关于这部分,后面有一整节专门讲。

最后补充一句关于数据库的思考。毕设级别的数据量通常是几万到几十万条,MySQL完全够用。但如果你想在系统设计里体现"大数据"的要素,可以在数据导入环节引入Pandas做批量处理,在架构说明里预留一个"数据量增大后可迁移到ClickHouse/StarRocks"的扩展点即可。你要清楚,毕设的核心是让老师看到你掌握了方案选型的判断力,而不是真的在单机环境里跑一个分布式集群。

2. 电商数据建模与预处理

2.1 订单核心表结构设计

任何数据分析系统的地基都是数据模型。电商订单领域,最核心的几张表我建议这样设计:订单主表、订单明细表、商品表、用户表、支付流水表,再加一个地区维度表辅助地域分析。

订单主表存的是每一笔订单的概要信息,字段包括:订单号(order_no)、用户ID(user_id)、订单状态(status,取值如已支付/已发货/已完成/已取消)、下单时间(create_time)、支付时间(pay_time)、订单金额(total_amount)、实付金额(pay_amount)、优惠金额(discount_amount)、收货省份(province)、收货城市(city)、支付方式(payment_method)。这里面需要注意,订单金额和实付金额必须分开存,因为电商场景里促销满减太常见,统计的时候两套口径各有用途。

订单明细表呢,一个订单可能包含多个商品,所以明细表要记录:订单ID(order_id)、商品ID(product_id)、商品数量(quantity)、商品单价(price)、小计金额(subtotal)。为什么要单独建明细表?因为商品维度的分析(哪个商品卖得多、哪个SKU贡献了多少营收)必须靠明细表聚合才能算出来,如果你把所有商品信息瘫在主表里,一个多商品订单的数据就会非常冗余,而且很难做商品级拆分。

商品表用来存商品本身的属性:商品名、类目(category)、品牌(brand)、上架时间、销售状态、成本价(可选)、标签。商品表的意义在于给聚合结果关联商品信息,比如算出"销售额TOP10的商品"之后,你得回到商品表把这些商品的类目、品牌拿出来,才能进一步做"类目占比"这种分析。

用户表存用户的基础信息:用户名、注册时间、性别(如果有)、年龄区间、会员等级。用户表的加入让"用户画像"和"复购分析"成为可能。不过实际做的时候要注意,电商数据里的用户信息往往不全,性别年龄大把空值,这种情况要么用随机值填充模拟,要么干脆不做用户属性分析,只做用户交易行为分析(比如下单频次、消费金额分层)。

地区维度的设计有一点技巧:直接在主表里存省份城市字段是最省事的,尤其在数据量几万条这个级别,没必要单独拆维度表。但如果你想在架构上更像数仓范儿,可以建一张region表存省市区编码和名称,订单表里只存代码,查询通过外键关联。这个选择了看你的数据量级,如果只是几万条,直接存文本字段,查询还更快。

以下是一份参考的Django模型骨架,可以直接照搬或者改造成自己的:

from django.db import models class Order(models.Model): STATUS_CHOICES = ( ('paid', '已支付'), ('shipped', '已发货'), ('completed', '已完成'), ('cancelled', '已取消'), ) order_no = models.CharField(max_length=64, unique=True, verbose_name='订单号') user_id = models.CharField(max_length=32, db_index=True, verbose_name='用户ID') status = models.CharField(max_length=20, choices=STATUS_CHOICES, verbose_name='订单状态') create_time = models.DateTimeField(db_index=True, verbose_name='下单时间') pay_time = models.DateTimeField(null=True, blank=True, verbose_name='支付时间') total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='订单金额') pay_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='实付金额') discount_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='优惠金额') province = models.CharField(max_length=32, verbose_name='收货省份') city = models.CharField(max_length=32, verbose_name='收货城市') payment_method = models.CharField(max_length=20, verbose_name='支付方式') class Meta: db_table = 'order_main' indexes = [ models.Index(fields=['create_time', 'status']), ] class OrderItem(models.Model): order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE) product_id = models.CharField(max_length=32, db_index=True) product_name = models.CharField(max_length=128) quantity = models.IntegerField(default=1) price = models.DecimalField(max_digits=10, decimal_places=2) subtotal = models.DecimalField(max_digits=10, decimal_places=2) class Meta: db_table = 'order_item' class Product(models.Model): product_id = models.CharField(max_length=32, unique=True) product_name = models.CharField(max_length=128) category = models.CharField(max_length=64, db_index=True) brand = models.CharField(max_length=64, blank=True) create_time = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'dim_product' class User(models.Model): user_id = models.CharField(max_length=32, unique=True) nickname = models.CharField(max_length=64, blank=True) register_time = models.DateTimeField(null=True, blank=True) level = models.CharField(max_length=16, default='普通会员') class Meta: db_table = 'dim_user'

建表时的几个约定俗成经验:时间字段统一用DateTimeField,且db_index=True加上,因为后面所有按时间聚合的查询都靠这个索引;订单号加唯一约束,防止重复数据;金额字段一律用DecimalField而不是FloatField,浮点数做金额计算会出现0.1+0.2不等于0.3的精度问题,这在答辩的时候是能拿出来讲的细节。数据量大了以后,Django的ORM自动生成的查询如果有慢查询风险,可以用connection.queries查看实际执行的SQL语句,或者用django-debug-toolbar这个工具在调试模式下看每条请求的SQL耗时。

2.2 数据清洗与聚合计算

模型建好了,接下来是把数据灌进去。如果数据是自己造的,那直接写脚本用Django ORM批量插入;如果数据是从公开数据集或爬虫来的,就得先做清洗。

我见过太多做数据分析项目的人,上来就直接往库里灌数据,结果做可视化的时候发现各种诡异问题:销售额趋势图出现个离谱的峰值,一查原来是某天导入了一笔金额1000万的测试订单;用户分布图显示某省的用户占了90%,仔细看是爬虫数据里默认值没处理干净。这些都是清洗不到的坑。

清洗的常规步骤应该是:

  • 去重:订单号是唯一键,重复的订单直接丢弃或者保留第一条。写脚本时判断Django ORM的get_or_create或者先查询再插入。
  • 空值处理:支付时间为空的订单一般是未支付或支付失败,这类数据有两种处理:直接过滤掉(分析维度只关注已支付订单),或者把状态设置成cancelled后保留。我的建议是做销售分析时只保留pay_time不为空的数据,因为这才是真实的交易。
  • 异常值过滤:订单金额为0或负数、数量为0或负数、单价远超正常范围,这些数据要么是测试生成的,要么是异常订单,做个阈值过滤或标记。
  • 时间对齐:把日期格式统一成YYYY-MM-DD或标准时间戳,时区也要统一,不然后面按天统计的时候会发现部分数据归到了错误的日子。
  • 编码处理:CSV文件要用encoding='utf-8-sig'读取,不然Excel导出的文件在Windows下读进来中文会乱码。

清洗完之后就是聚合计算。Django的ORM提供了非常方便的聚合工具,annotate和aggregate这俩函数就是为数据分析场景设计的。

比如你要算每日销售额趋势,用一行查询就行:

from django.db.models.functions import TruncDate from django.db.models import Sum daily_sales = (Order.objects .filter(status='completed') .annotate(day=TruncDate('pay_time')) .values('day') .annotate(total=Sum('pay_amount')) .order_by('day'))

这段代码的含义是:取所有已完成订单,按支付时间截断到天(TruncDate),再按天分组求和。返回的QuerySet是[{'day': date对象, 'total': Decimal('1234.56')}, ...]这样的结构,直接可以转成JSON给前端。类似的,你想按月份、按周统计,把TruncDate换成TruncMonth或TruncWeek就行,这是Django从3.2开始就内置的数据库函数,非常好用。

再比如商品TOP10的排行,核心是一条:

top_products = (OrderItem.objects .values('product_id', 'product_name') .annotate(total_quantity=Sum('quantity'), total_sales=Sum('subtotal')) .order_by('-total_sales')[:10])

这里利用了values()加annotate()的组合,values先指定分组字段,annotate为每组计算聚合值,最后order_by排序取前10。这种写法能替代大部分手写SQL的GROUP BY场景,而且可读性非常好。

在真实的项目中,有些客户会在需求里指定"看累计销售额、订单量、客单价、退款率"这些核心指标。客单价的算法是总销售额除以总订单数,退款率就是退款订单数除以总订单数。这些指标建议在系统里做一个"概览仪表盘"接口,一次性返回一组数字,前端配一个数字翻牌器的效果展示,视觉冲击力很强,答辩映象分直接拉满。

3. Django后端核心实现

3.1 Django项目框架搭建与分层

Django项目的目录结构,我不建议把所有代码塞到默认的单一app里。数据分析系统涉及的模块比较清晰,建议按业务边界拆成多个app,一个app管一个领域,这样后面写接口、写权限、写测试都清爽很多。

一个参考的项目结构是这样的:

project/ ├── manage.py ├── config/ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── orders/ # 订单数据模型与订单服务 │ ├── goods/ # 商品数据模型 │ ├── users_app/ # 用户数据模型 │ ├── analysis/ # 聚合查询、报表接口 │ ├── dashboard/ # 可视化大屏相关视图 │ └── agent/ # 大模型Agent接口 ├── static/ │ ├── css/ │ ├── js/ │ └── vendor/echarts/ ├── templates/ │ ├── dashboard.html │ ├── agent_chat.html │ └── admin.html ├── scripts/ │ ├── generate_mock_data.py │ ├── import_csv.py │ └── clean_data.py └── requirements.txt

这个结构的好处一目了然:orders、goods、users_app是基础数据层,analysis是核心分析逻辑层,dashboard是展示层,agent是大模型集成层。每一层的职责清晰,写代码的时候心理有数,答辩画架构图也能画得非常规范。用Django命令python manage.py startapp orders挨个创建即可,别忘了把app加到INSTALLED_APPS里,在config/settings.py中配置好。

Settings层面的几个关键配置,我直接给出来参考。数据库配置指定MySQL,注意字符集要加utf8mb4,否则存不了emoji和一些生僻字:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'ecommerce_analysis', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", }, } }

这个是PyMySQL驱动的配置方式。记得先pip install pymysql,然后在config/__init__.py里写一行import pymysql; pymysql.install_as_MySQLdb(),Django就能通过MySQLdb接口用上PyMySQL了。

还有一个必须提的是DEBUG=True的时候Django会打印所有SQL日志,开发调试很好用,但部署上线一定要把DEBUG=False,同时配置ALLOWED_HOSTS,这是防信息泄露的基本操作。静态文件的处理也要注意:DEBUG=False之后Django不再自动提供静态文件服务,你需要用whitenoise或者Nginx把static目录配好,否则页面CSS、JS全部加载不出来。这个小坑几乎每届都有学生踩,后面用专门一节讲排错。

3.2 API接口设计与关键实现

前后端交互的数据格式,我建议统一用JSON。接口的URL设计要RESTful一点,让答辩老师听着专业:

接口路径方法功能说明
/api/dashboard/overview/GET核心KPI概览:总销售额、订单量、客单价、退款率
/api/dashboard/sales_trend/GET销售额/订单量时间趋势
/api/dashboard/category_ratio/GET商品类目销售额占比
/api/dashboard/top_products/GET商品销量/销售额TOP10
/api/dashboard/geo_distribution/GET各省份订单量/销售额分布
/api/dashboard/payment_method/GET支付方式占比
/api/agent/chat/POST大模型数据分析问答

以销售趋势接口为例,实现非常直接:

import json from django.http import JsonResponse from django.views.decorators.http import require_GET from django.db.models.functions import TruncMonth from django.db.models import Sum, Count from apps.orders.models import Order @require_GET def sales_trend(request): metric = request.GET.get('metric', 'sales') # sales / orders group_by = request.GET.get('group_by', 'month') # day / week / month trunc_func = { 'day': TruncDay, 'week': TruncWeek, 'month': TruncMonth, }.get(group_by, TruncMonth) qs = (Order.objects .filter(status='completed') .annotate(period=trunc_func('pay_time')) .values('period') .annotate( total_sales=Sum('pay_amount'), order_count=Count('id') ) .order_by('period')) result = [{ 'period': item['period'].strftime('%Y-%m-%d') if item['period'] else None, 'total_sales': float(item['total_sales'] or 0), 'order_count': item['order_count'], } for item in qs] return JsonResponse({'code': 0, 'data': result})

这里有个非常关键的细节:item['total_sales']是Decimal类型,不是json序列化不了的,必须用float()转成浮点数,否则JsonResponse直接报错。同理日期对象也要strftime转字符串。我建议在项目里封一个通用的小函数,把ORM返回的QuerySet结果统一转成可JSON序列化的dict,这个函数你后面写几十个接口都会用到。

还有一个要说的就是QuerySet的惰性求值特性。Django的ORM查询在真正迭代之前不会执行SQL,这导致一个常见bug:你在视图函数里先定义了一个qs,然后修改了某个条件,最后循环使用qs,得到的结果和你预期不一致。理解这一点后,写出来的代码就会更注意及时执行或者注意作用域,不然在循环里二次过滤QuerySet会造成数据库多次查询,性能上也会有问题。

3.3 查询性能优化的实战经验

数据分析系统的查询模式是典型的"少量大查询",一个接口可能要对整张表做聚合。数据量在10万条以内,MySQL索引优化得好,Django ORM的聚合查询通常都在几百毫秒内完成,问题不大。但有几个地方要注意,否则数据量一到几十万条,页面就会卡得没法看。

第一,日期字段必须有索引。所有按时间分组的查询,比如销售额趋势、按月统计、按周统计,底层SQL都是GROUP BY date(pay_time)这类带范围条件的查询,如果pay_time没有索引,全表扫描是必然的。建模型的时候给pay_time加db_index=True,或者建表后手动加索引:ALTER TABLE order_main ADD INDEX idx_pay_time (pay_time);。

第二,避免在循环中查询数据库。新手最常见的写法是查了商品列表之后,在for循环里对每个商品再查一次订单表,这就是N+1问题。比如你想算每个商品卖了多少,正确做法是一次聚合查询拿到全部分组计数,而不是循环里一个个查。Django的select_related和prefetch_related也要学会用,它们能把外键关联的查询合并成少数几条SQL。

第三,用Redis做缓存。数据分析的图表数据通常是分钟级不变的,完全没必要每次请求都去算一遍。引入Redis之后,把聚合结果缓存起来,设置过期时间,例如5分钟或10分钟。Django里有现成的缓存框架,只需要在settings里配置:

CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.redis.RedisCache', 'LOCATION': 'redis://127.0.0.1:6379/1', 'TIMEOUT': 300, } }

然后在视图里用cache.get和cache.set包一层,命中缓存时直接返回JSON,未命中时再算并写入缓存。注意缓存key的设计要包含查询参数,比如sales_trend_2024_06_month,不然不同参数的请求会串数据。

第四,把非核心指标做成异步预计算。有些指标耗时较长又不需要实时,比如用户复购率、RFM分层,这类计算可以每晚或者每小时跑一次定时任务,把结果存到一张独立的统计表里,页面直接查这张表。Django有django-crontab或者django-celery-beat可以用,用最简单的django-crontab就能扛住大部分场景。

4. 可视化大屏与图表实现

4.1 ECharts选型与数据对接格式

可视化部分,我强烈推荐ECharts,理由前面说过,现在讲讲具体怎么用、怎么和数据接口对接。

ECharts是百度开源(现在Apache孵化)的JavaScript图表库,支持折线图、柱状图、饼图、散点图、雷达图、地图等几十种图表,而且图表交互成熟,有缩放、tooltip、图例开关这些内置功能,你不需要自己造轮子。引入方式很简单,直接下个echarts.min.js放到静态目录,或者在HTML里用CDN:

<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>

但要注意,毕设如果用CDN,答辩现场可能会遇到没网的情况。稳妥起见,把echarts.min.js下载到本地static目录,做成离线也能跑。这一点在毕业设计里非常加好感——现场演示不怕断网。

图表初始化的套路都一样,三步走:先准备一个DOM容器,设置宽度高度;然后echarts.init(dom)创建实例;最后chart.setOption(option)配置数据和样式。核心的工作都在option里,我们需要让后端的JSON数据和ECharts的data结构对得上。

举个销售额趋势图的例子。后端返回的数据形如[{"period": "2024-01", "total_sales": 12345.67}, {"period": "2024-02", ...}],前端拿到后要把它转换成ECharts需要的两个数组:x轴类目数据和y轴数值数据。

fetch('/api/dashboard/sales_trend/?group_by=month') .then(res => res.json()) .then(res => { const data = res.data; const xData = data.map(item => item.period); const yData = data.map(item => item.total_sales); const chart = echarts.init(document.getElementById('salesChart')); chart.setOption({ title: { text: '月度销售额趋势' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: xData }, yAxis: { type: 'value', name: '销售额' }, series: [{ name: '销售额', type: 'line', smooth: true, areaStyle: {}, data: yData }] }); });

这里有个经验点:ECharts的data数组顺序必须和后端返回的顺序一致。如果查询结果里有些月份没有数据,后端要么补零,要么前端做处理,不然图表会多出空档或者对不齐。更稳的方式是后端把完整的月份列表生成出来,缺数据月份自动补0。

4.2 大屏布局与数据联动

可视化大屏的布局,简单来说就是"中间突出核心、四周分布辅助"。一个标准的电商大屏可以这样规划:

  • 顶部:系统标题 + 当前时间 + 日期切换
  • 左上:核心KPI数字卡片(总销售额、总订单量、客单价、退款率)
  • 右上:销售额趋势折线图(面积图效果最直观)
  • 中间:商品类目占比饼图/环形图
  • 左下:省份销售分布地图
  • 右下:商品销售TOP10横向柱状图

布局上我用的是CSS Grid,把大屏分成若干区域,每个区域放一个div容器,页面加载后分别初始化各个图表。大屏的背景色一般是深色系,比如#0f1c3a这种深蓝,配上高亮的卡片和数据,视觉冲击力强。ECharts主题可以直接用'dark'内置主题:echarts.init(dom, 'dark')。

数据联动是另一个值得讲的设计点。比如你在商品TOP10柱状图上点击某个商品,其他图表(比如趋势图和品类占比图)同步切换到该商品的数据。实现思路是给柱状图绑定click事件,拿到商品名参数后重新请求后端接口,再更新其他图表的数据。这个交互效果本身不复杂,但演示起来非常唬人,能体现出系统不是静态图表堆砌,而是有交互逻辑在里面的。

另一个建议是定时刷新。用setInterval每60秒重新拉一次数据接口,更新所有图表,配合一个"最后更新于XX:XX"的提示,让大屏看起来像实时监控系统。当然,如果后端数据本来就是静态的,定时刷新纯属形式,但演示效果确实好,答辩现场老师会觉得系统是活的。

还有一点关于前端框架的选择提醒:如果你熟悉前端工程化,用Vue或React拆分组件会更好维护,但毕设如果时间紧,就直接用原生HTML+JS+ECharts,不引入构建工具,部署简单,不容易出环境问题。两种方案没有绝对的对错,关键是你能讲清楚自己的技术选型。

5. 大模型Agent智能分析模块

5.1 DeepSeek接入与Function Calling实现

这个模块是整套系统最亮的部分,也是很多同学觉得"听起来高大上但不知道从哪下手"的部分。其实思路理清楚之后实现非常直接。

核心思路是:用户在前端输入自然语言问题,比如"今年6月和5月相比销售额增长了多少";系统把这个问题连同可用的分析工具定义一起发给大模型;大模型理解问题后,返回一个调用某个工具的动作(比如调用get_sales_trend,参数是start_date=2024-05-01, end_date=2024-06-30);系统执行这个函数,拿到真实数据;再把数据返回给大模型;大模型把数据组织成自然语言回答;最后回答展示给用户。

这个链路里的关键概念就是Function Calling(函数调用),DeepSeek的API是支持这个能力的。你需要做两件事:一是定义一个函数清单(JSON Schema格式),告诉模型"你有哪些工具可以用,每个工具的入参是什么";二是在对话循环里处理模型返回的tool_calls,真正去执行工具。

下面给一个简化的实现参考。假设我们封装了一个call_deepseek(messages, tools)函数:

import requests import json DEEPSEEK_API_KEY = "sk-xxxx" DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions" def call_deepseek(messages, tools=None): headers = { "Content-Type": "application/json", "Authorization": f"Bearer {DEEPSEEK_API_KEY}", } payload = { "model": "deepseek-chat", "messages": messages, "tools": tools or [], "tool_choice": "auto", "temperature": 0.2, } resp = requests.post(DEEPSEEK_API_URL, headers=headers, json=payload, timeout=60) resp.raise_for_status() return resp.json()["choices"][0]["message"]

然后定义可用工具。这里定义两个分析函数的schema:

analysis_tools = [ { "type": "function", "function": { "name": "get_sales_trend", "description": "获取指定时间范围内的销售趋势数据(金额和订单量)", "parameters": { "type": "object", "properties": { "start_date": {"type": "string", "description": "起始日期,格式YYYY-MM-DD"}, "end_date": {"type": "string", "description": "结束日期,格式YYYY-MM-DD"}, "group_by": {"type": "string", "enum": ["day", "week", "month"], "default": "day"} }, "required": ["start_date", "end_date"] } } }, { "type": "function", "function": { "name": "get_top_products", "description": "获取指定时间范围内销量或销售额排名前N的商品", "parameters": { "type": "object", "properties": { "start_date": {"type": "string"}, "end_date": {"type": "string"}, "limit": {"type": "integer", "default": 10}, "order_by": {"type": "string", "enum": ["sales", "quantity"], "default": "sales"} }, "required": ["start_date", "end_date"] } } } ]

接下来在/api/agent/chat/的视图里把上面的串起来:

from apps.analysis.services import get_sales_trend, get_top_products @require_POST def agent_chat(request): data = json.loads(request.body) user_question = data.get("message", "") system_prompt = """你是电商数据分析助手。用户会输入关于订单数据的问题。 你会根据可用工具获取真实数据,然后用中文友好地回复用户。 如果问题无法用工具回答,告知用户你能回答哪些类的问题。""" messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_question}, ] # 第一轮调用:模型可能返回需要调用函数的结果 ai_message = call_deepseek(messages, tools=analysis_tools) if ai_message.get("tool_calls"): # 把模型的function call结果追加到对话中 messages.append(ai_message) for tool_call in ai_message["tool_calls"]: func_name = tool_call["function"]["name"] func_args = json.loads(tool_call["function"]["arguments"]) if func_name == "get_sales_trend": func_result = get_sales_trend(**func_args) elif func_name == "get_top_products": func_result = get_top_products(**func_args) else: func_result = {"error": "unknown tool"} messages.append({ "role": "tool", "tool_call_id": tool_call["id"], "content": json.dumps(func_result, ensure_ascii=False) }) # 第二轮调用:模型根据真实数据生成最终回答 final_message = call_deepseek(messages, tools=analysis_tools) answer = final_message["content"] else: answer = ai_message["content"] return JsonResponse({"code": 0, "message": answer})

这个流程踩过坑的同学都知道,最容易出错的地方是messages的格式。做Function Calling时,tool_calls在assistant消息里,对应每个tool的role: "tool"消息必须带正确的tool_call_id,并且顺序跟上文一致。漏掉tool_call_id或者顺序错乱,API就会报错。另外,调get_sales_trend这类工具函数时,参数可能来自用户自然语言,比如用户说"上个月",模型不一定能正确把日期算出来。解决方法是你可以多定义一个工具get_current_date,让模型先获知当前日期,再去推算"上个月"是哪一天。这是Agent开发里很经典的一招,我称之为"给Agent提供感知时间的能力"。

5.2 Agent的设计边界与降级兜底

大模型Agent模块虽然炫,但要让它稳定可用,必须做好边界控制和降级兜底。

边界控制的第一件事是只允许Agent调用白名单内的函数。上面例子里的analysis_tools就是白名单,模型只能在这几个函数里选择,不能随便调用其他系统功能。这样即使模型被恶意提示词注入,也拿不到系统权限。你还可以在函数内部加上参数校验,比如limit限制在1~100,date必须符合日期格式,防止模型生成异常参数把系统拖垮。

第二件事是超时和异常处理。大模型API调用肯定有网络延迟,如果用户问了个复杂问题,模型要两轮对话、要执行函数,整个流程可能耗时10~20秒。前端要加"正在分析"的loading状态,后端要设置合理的超时时间,比如30秒。如果第一轮调用就超时或者报错,要返回友好的提示信息,而不是让用户看到500错误页。我的做法是捕获异常后返回固定文案:"智能分析服务暂时不可用,请稍后再试,或直接查看可视化图表。"

第三件事是回答降级。如果模型无法理解用户的问题,或者没有调用任何工具,不要硬编一个答案,诚实告诉用户系统能回答哪些范围的问题,并给出几个示例问题(比如"上个月销售额多少""销量最高的商品是什么""各省份订单量分布")。这其实也是一种提示词策略,在system prompt里明确说明边界,模型的答非所问率会大幅下降。

还有一种增强方案是把历史图表数据作为上下文。比如用户在对话中问"上个月的销售额趋势如何",Agent返回文字描述的同时,可以附带一组JSON图表数据,前端拿到后直接渲染一个迷你趋势图。这需要你在工具函数里同时返回聚合数据和描述文本,实现上也不复杂,但用户体验会提升一个档次。

6. 常见问题与排查技巧实录

6.1 数据、编码与静态文件的坑

先说一个每个新手都会踩的坑:读取CSV文件中文乱码。这个问题基本是文件编码引起的,Excel在Windows下默认保存的是GBK/GB2312编码,而Python读取时默认用UTF-8,所以读出来全是乱码。解决办法是pd.read_csv('data.csv', encoding='gbk'),或者读取时自动检测编码:

import chardet with open('data.csv', 'rb') as f: result = chardet.detect(f.read(10000)) encoding = result['encoding'] df = pd.read_csv('data.csv', encoding=encoding)

如果数据是从网上下载的,用chardet检测一下通常都能正确识别。另一个容易踩的是,用Pandas处理完DataFrame后,直接把数据写入MySQL时字段类型不一致,比如DataFrame里的日期字段是object类型,写入MySQL datetime字段时报错。解决方法是入库前显式做pd.to_datetime()转换。

再一个高发问题是Django静态文件404。开发模式下DEBUG=True,Django自带静态文件服务;一旦你为了展示效果把DEBUG=False,所有CSS、JS全部404。这是我每年都能遇到的问题。解决方案是安装whitenoise:

pip install whitenoise

在settings.py的MIDDLEWARE里加上'whitenoise.middleware.WhiteNoiseMiddleware',放在SecurityMiddleware之后,然后运行python manage.py collectstatic把静态文件收集到一个目录,Whitenoise就能帮你把静态文件服务起来。如果是部署到云服务器,更推荐用Nginx单独配静态文件,但Whitenoise的方案对学生来说是性价比最高的。

6.2 数据库连接与查询超时

MySQL默认的max_connections是151,连接数打满就会报Too many connections。Django的数据库连接池本身是每线程一个连接,并发上来之后很容易把连接数打满。毕设场景下最简单的方案就是确保用完后在settings.py里设置CONN_MAX_AGE:

DATABASES = { 'default': { # ... 'CONN_MAX_AGE': 60, # 连接复用60秒 'OPTIONS': { # ... 'pool_size': 10, } } }

注意pool_size这个参数需要配合django-db-connection-pool或者SQLAlchemy的池化方案才生效,如果只是原生Django+PyMySQL,看连接数变化即可。真出现连接数爆了,可以先在MySQL命令行执行SHOW PROCESSLIST;看是谁占着连接,再用SET GLOBAL max_connections = 500;临时调高。

查询超时的问题,前面的性能优化部分讲过。补充一个常见场景:你写了一个跨多个条件的聚合查询,包含时间范围、商品类目、省份等多个过滤条件,如果没有组合索引,查询可能要走几次全表扫描。建议在MySQL里对最常用的组合加联合索引,比如(create_time, status, province)。索引不是越多越好,但分析系统的查询模式比较固定,针对性的联合索引收益非常明显。

6.3 大模型接口对接的典型报错

大模型Agent模块的报错,我整理一个速查表,都是实际运行中最常遇到的:

错误现象原因解决办法
AuthenticationErrorAPI Key错误或过期检查环境变量中的key,确认账户余额
RateLimitError请求频率超限在调用端加time.sleep(1)或做指数退避重试
InvalidRequestErrormessages格式问题检查tool_call_id是否缺失、role字段是否合法
模型返回空content模型选择只调用了工具,但工具结果未正确回传检查tool结果是否append到messages
中文字符被转义没有指定ensure_ascii=FalseJSON输出时设置json.dumps(..., ensure_ascii=False)
用户问题答非所问提示词没有明确边界加强system prompt,给出示例问题和回答格式

其中我特别想强调RateLimitError。现在很多大模型API免费版的调用频率限制是每分钟几十次,如果你的大屏页面每60秒刷一次,再加上Agent接口的调用,很容易触发限流。代码里务必加一个统一的调用封装,里面带重试逻辑和退避策略,别让一个限流报错直接把页面搞崩。

还有一点关于模型选择:DeepSeek有deepseek-chat和deepseek-reasoner两个模型可选。Function Calling场景下我建议用deepseek-chat,响应更快、成本更低;deepseek-reasoner的推理能力强,但延迟更高,不太适合对话式交互。如果你要展示"模型能分析复杂问题",可以预置几个复杂分析case,比如"对比上个月和上上个月各品类的销售变化",这类问题用deepseek-chat也能跑得很好。

6.4 答辩演示时的环境预案

最后讲一点可能和代码无关但至关重要的东西:答辩演示环境预案。每年答辩现场总有人的系统现场跑不起来,原因不外乎几类:数据库连不上、静态文件加载不出来、大模型接口没网、Python环境版本不一致。

我的建议是,答辩前一星期做一次"从零搭建"演练:在一台干净电脑上,从安装Python、创建虚拟环境、pip安装依赖、初始化数据库、导入数据,到最后打开页面完成整个流程。然后把所有依赖固定版本,导出requirements.txt。现场演示时优先用本机环境,不要依赖外部网络。大模型接口如果现场网络不好,要提前准备降级方案——要么在Agent模块里做一个"模拟模式",不真正调用外部API,用预设规则返回答案;要么准备好录制的演示视频作为备胎。这不是投机取巧,而是工程人员应有的风险预案意识。

从数据模型设计到API接口实现,从可视化大屏到大模型Agent,这套跨境电商订单分析系统的每一步都是可以在答辩时展开讲半天的。尤其是大模型接入的部分,你只要讲清楚Function Calling的链路、边界控制策略和降级方案,老师基本就能确认这不是一个"为了蹭热点挂个API"的假亮点,而是真正理解了大模型在业务系统里怎么落地。

我个人在实际带项目的过程中最深的体会是:这个系统最难的不是某个单一技术点,而是把Django、ECharts、大模型这几个不同层次的东西串成一个整体。每层之间通过标准的JSON接口通信,每层内部做好自己的职责,整个系统就非常稳固。最后再分享一个小技巧:如果你想让整个项目看起来更完整,加一个数据导入口——支持上传CSV文件并自动解析入库,这样演示的时候就能现场导入一份新数据给老师看,交互感和完成度瞬间拉满。这个模块在技术上其实就是复用前面讲的数据清洗逻辑,工作量不大,但给人留下的印象非常深刻。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询