1 | 业务背景与技术选型
随着移动电商流量红利见顶,「千人千面」已成为提升 GMV 与用户黏性的核心抓手。本项目旨在用最小改动让传统规则系统接入 LLM 精排能力:
graph LR
A[用户进入首页] --> B(候选集生成<br/>MySQL + Redis)
B --> C{候选集<br/>≈100~300 item}
C --> D[调用推荐微服务<br/>/recommendProducts]
D --LLM prompt + profile--> E((GPT-4o))
E --返回 JSON(topK)--> F(ProdService.pageByProdsId)
F --payload--> G[小程序渲染]
引入大模型原因:
-
多模态特征(图 / 文 / 结构化属性)难以一次性编码到简单 LR/BPR 中;
-
我们已有 商品文生向量、图像特征、用户交互序列,直接塞给 LLM 可利用其多模态推理能力做轻量排序;
-
Prompt 易迭代、不改线上二进制。
技术栈概览
|
层级 |
核心框架 |
说明 |
|---|---|---|
|
前端小程序 |
uni-app 3.x + Vue 3 + Pinia |
一次编码,多端运行;Composition API |
|
后端网关 |
Spring Cloud Gateway |
统一鉴权 / 灰度 / 限流 |
|
业务服务 |
Spring Boot 3 + MyBatis-Plus |
轻量 ORM,分页插件 |
|
缓存 |
Redis 7 |
高频列表、本地缓存 Caffeine 二级 |
|
推荐微服务 |
Python 3.11 + FastAPI |
Faiss 向量检索 + OpenAI SDK |
|
消息队列 |
RocketMQ |
异步写曝光 / 点击 / 购买日志 |
2 | 数据模型与表结构
关键表只列需要字段,为方便理解已去除审计字段:
CREATE TABLE `prod` (
`prod_id` BIGINT PRIMARY KEY,
`prod_name` VARCHAR(255),
`put_away_time` DATETIME,
`price` DECIMAL(10,2),
`ori_price` DECIMAL(10,2),
`sold_num` BIGINT,
`daily_sold` BIGINT,
`brief` VARCHAR(255),
`imgs` TEXT,
`delivery_mode` JSON,
`tag_list` JSON
);
CREATE TABLE `sku` (
`sku_id` BIGINT PRIMARY KEY,
`prod_id` BIGINT,
`status` TINYINT,
`attrs` JSON,
`stock` INT
);
CREATE TABLE `prod_tag_reference` (
`id` BIGINT PRIMARY KEY,
`prod_id` BIGINT,
`tag_id` BIGINT
);
CREATE TABLE `user_profile` (
`user_id` VARCHAR(64) PRIMARY KEY,
`gender` CHAR(1),
`age` INT,
`favorite_cats` JSON,
`embedding` BLOB
);
3 | 候选集生成策略
3.1 新品推荐 (New Arrivals)
-
业务规则: put_away_time >= NOW() - INTERVAL 7 DAY,按 put_away_time DESC
-
接口: /prod/lastedProdPage
-
实现: ProductMapper.pageByPutAwayTime 使用 <![CDATA[ put_away_time >= DATE_SUB(...) ]]>
3.2 限时特惠 (Time-Limited Deal)
-
业务规则: ori_price - price ≥ 5% 且 deal_start < NOW() < deal_end
-
优势: 天然带营销氛围,可做限购逻辑。
-
实现: 单独 promotion 表,物化视图写回 Redis ZSET。
3.3 每日疯抢 (Daily Hot)
-
业务规则: daily_sold DESC top N
-
接口: /prod/moreBuyProdList
-
实现: 00:05 定时任务 refreshDailySold() 重置并写入前一天数据。
3.4 商城热卖 (Overall Hot Sale)
-
业务规则: sold_num DESC 排序
-
接口: /prod/tagProdList 中 style === "1"
3.5 猜你喜欢 (You May Like)
-
使用 Faiss 对商品向量做 ANN 召回 M=200 条;
-
无画像 fallback 到 Hot + New 策略。
注: 五类候选生成平均延迟 < 30 ms。
4 | LLM 精排 (Re-Ranking with GPT-4o)
4.1 整体流程
-
请求 /prod/* 接口时注入 X-UserId
-
调用 RecommendService.recommendProducts
-
Python 拼 Prompt(见下)
-
GPT-4o 排序返回 topK
-
pageByProdsId 保序拼装
4.2 Prompt 设计
{
"system": "You are an e-commerce ranking model. Return a JSON array of productIds in best ordering.",
"user": "User profile: {\"age\":25,\"gender\":\"F\",\"favorite_cats\":[3,5,7]}\nCandidate products:\n1. {\"prodId\":101,\"categoryId\":3,\"price\":129.9,\"image\":\"...\",\"title\":\"...\"}\n2. {...} ...\nOutput format: [101, 205, 999]"
}
-
候选数 ≤ 50
-
素材多模态,image 仅 URL,caption 推理
-
GPT temperature=0.2
4.3 Embedding & Feature Schema
|
字段 |
类型 |
说明 |
|---|---|---|
|
title_tok |
文本 |
分词或 SentencePiece 编码 |
|
img_clip |
512-d |
CLIP 图像向量 |
|
price_norm |
float |
归一化后价位 |
|
cat_onehot |
n |
一级分类 one-hot |
5 | 后端源码逐行解析
5.1
ProdController
亮点:
-
@Tag + @Operation → 自动文档
-
SecurityUtils.getUser().getUserId() 安全透传
-
FIELD() 保序排序
@GetMapping("/prodListByTagId")
public ServerResponseEntity<IPage<ProductDto>> prodListByTagId(
@RequestParam("tagId") Long tagId, PageParam<ProductDto> page) {
String userId = SecurityUtils.getUser().getUserId();
String reviewerId = userService.getReviewerIdByUserId(userId);
List<Long> recommendedProdsId =
recommendService.recommendProducts(reviewerId, tagId, 6);
IPage<ProductDto> productPage =
prodService.pageByProdsId(page, recommendedProdsId);
return ServerResponseEntity.success(productPage);
}
5.2
ProductServiceImpl.pageByProdsId
@Override
public IPage<ProductDto> pageByProdsId(Page<ProductDto> page, List<Long> prodsId) {
if (CollectionUtil.isEmpty(prodsId)) {
return Page.of(page.getCurrent(), page.getSize(), 0);
}
return baseMapper.pageByProdsId(page, prodsId);
}
对应 XML:
<select id="pageByProdsId" resultType="com.yami.shop.bean.app.dto.ProductDto">
SELECT <include refid="BaseColumnList"/>
FROM prod
WHERE prod_id IN
<foreach collection="prodsId" item="id" open="(" separator="," close=")">
#{id}
</foreach>
ORDER BY FIELD(prod_id,
<foreach collection="prodsId" item="id" separator=",">
#{id}
</foreach>)
</select>
5.3 推荐微服务片段
def recommend_products(user_id: str, tag_id: int, size: int) -> list[int]:
candidates = recall_candidates(tag_id, size * 3)
profile = db["user_profile"].find_one({"user_id": user_id}) or {}
prompt = build_prompt(profile, candidates)
resp = openai.chat.completions.create(model="gpt-4o", **prompt).json()
product_ids = json.loads(resp["choices"][0]["message"]["content"])
return product_ids[:size]
6 | 前端小程序实现细节
6.1 页面结构
-
<swiper> Banner
-
cat-item 宫格分类
-
up-to-date / hot-sale / more-prod 三类卡片
6.2 异步加载
onLoad(() => { getAllData() })
const getAllData = () => {
Promise.all([
getIndexImgs(),
getNoticeList(),
getTag()
]).finally(() => uni.stopPullDownRefresh())
}
6.3 骨架屏占位
<skeleton v-if="loading" rows="6" theme="article" />
<view v-else>...</view>
6.4 性能优化
-
virtual-list 长列表
-
图片统一 mode="aspectFill"
-
uni.addInterceptor('request') JWT 注入
7 | 缓存与容灾
7.1 Redis 键设计
|
Key 模板 |
类型 |
TTL |
说明 |
|---|---|---|---|
|
prod:new |
ZSET |
10 min |
上架时间 |
|
prod:deal:{date} |
ZSET |
5 min |
折扣百分比 |
|
prod:daily:{date} |
ZSET |
5 min |
当日销量 |
|
prod:hot |
ZSET |
1 h |
累计销量 |
|
rec:llm:{userId}:{tagId} |
LIST |
30 s |
LLM 短缓存 |
7.2 降级策略
-
超时 fallback
-
返回 200 + X-Rec-Mode header
-
Prometheus & 钉钉告警
8 | 监控 & 性能数据
|
指标 |
P50 |
P90 |
P99 |
备注 |
|---|---|---|---|---|
|
候选生成 |
12 ms |
23 ms |
45 ms |
MySQL + Redis |
|
LLM 精排 |
610 ms |
890 ms |
1.4 s |
GPT-4o, 50 candidates |
|
总接口 /prod/* |
680 ms |
960 ms |
1.6 s |
含网络 |
|
小程序首屏白屏 |
820 ms |
1.3 s |
2.0 s |
Android 10, 4G |
9 | 常见坑 & Debug Tips
-
FIELD 排序失效:缺失第二个 foreach
-
uni-app key 冲突:重复触发警告
-
LLM JSON 不合法:正则过滤字符
-
Redis ZSET Tie 分数覆盖:加微秒区分
10 | 总结与展望
本文自下而上拆解了「规则召回 + LLM 精排」全链路,并基于 mall4uni 实现了小程序首页推荐。
未来方向包括:
-
向量检索替换为 Milvus + Delta Lake;
-
RAG 将评价摘要纳入 Prompt;
-
多臂赌博机 + LLM 协同探索;
-
Serverless GPT 方案 降本提效。
🎉 至此,首页推荐的所有实现细节已倾囊相授。如果你觉得本文对你有帮助,欢迎一键三连 👍 💬 ⭐!
959

被折叠的 条评论
为什么被折叠?



