AIGC书虫福音:智能推荐让你3天读完一年份的好书
AIGC书虫福音:智能推荐让你3天读完一年份的好书
友情提示:本文 1.2w+ 字,代码量巨大,建议先收藏再蹲坑慢慢看。
如果你一边看一边骂“这写的啥玩意儿”,右下角有个“踩”按钮,使劲戳,我脸皮厚。
先吐槽两句,再进入正题
前阵子我在地铁上,隔壁小姐姐抱着手机嘎嘎乐,我斜眼一瞅——好家伙,微信读书给她推了一本《如何优雅地拒绝前男友的饭局》,她一边看一边点头,就差把“老娘学会了”写在脸上。那一刻我深刻体会到:推荐系统要是懂人心,比相亲对象还靠谱。
可转念一想,我去年双11买的《C++ Primer》还在墙角吃灰,Kindle 盖泡面都盖出包浆了,凭啥人家能 3 天刷 20 本,我连目录都没啃完?
直到我撸完这套 AIGC 推荐链路,才悟了——不是我不爱看书,是系统没把我当“人”看。今天就把我踩过的坑、写过的 bug、被产品小姐姐追着打的 727 个需求,一股脑倒出来。
别被“智能”俩字唬住,它也有翻车现场
先给你们看一段凌晨两点的日志,我当时差点把键盘掀了:
[02:14:53] user_id: 9527, scene: "深夜emo", action: "scroll",
sentiment: -0.82, recall_books: ["《如何戒掉拖延症》",
《自控力》,"《早睡早起身体好》"]
[02:14:54] user_close_app
翻译成人话:用户都emo到快哭了,系统还给他喂鸡汤,让他早点睡。
这感觉就像你跟闺蜜哭诉“我失恋了”,她回你“多喝热水”。
结论: 情绪识别不准,推荐就是社死现场。
推荐系统怎么突然变得这么聪明?
2.1 从“关键词”到“读心术”
传统协同过滤,说白了就是“你宿舍老二爱看《鬼吹灯》,你大概率也爱看”。
现在呢?大模型把你所有行为嚼碎了咽下去:
- 翻页速度 0.8 秒 → 你可能在扫情节,爽点要密集
- 凌晨 1 点还在划 → 大概率失眠,来点短篇诗歌
- 看到“分手”关键词停顿 3 秒 → 情感空窗期,推《霍乱时期的爱情》
代码说话最实在,走,上 Python:
# 片段 1:情绪 + 场景特征拼接
import torch
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
bert = AutoModel.from_pretrained("bert-base-chinese")
def build_user_feat(user_id, scene, scroll_speed, stay_time, sentiment):
"""
把用户行为变成 768 维向量,供下游大模型吃
"""
text = f"{scene} 划动{scroll_speed}秒 停留{stay_time}秒 情绪{sentiment}"
inputs = tokenizer(text, return_tensors="pt")
with torch.no_grad():
cls_vec = bert(**inputs).last_hidden_state[:, 0] # [1,768]
return cls_vec.squeeze(0) # [768]
# 片段 2:双塔召回,向量检索 100 ms 内返回 200 本书
import faiss
import numpy as np
book_index = faiss.read_index("book_embedding.index") # 200 万本书向量
user_vec = build_user_feat("9527", "深夜emo", 0.8, 3.2, -0.82)
user_vec = user_vec.numpy().astype("float32")
D, I = book_index.search(user_vec.reshape(1, -1), k=200) # I 是书籍 id
recall_ids = I[0] # 200 本候选
# 片段 3:大模型精排——把 200 本压到 10 本
from transformers import AutoModelForSequenceClassification
ranker = AutoModelForSequenceClassification.from_pretrained("our-ranker-bert")
top10 = []
for book_id in recall_ids:
book_meta = get_book_meta(book_id) # 标题、简介、封面 OCR 文本
text = f"用户情绪-0.82,场景深夜emo。书籍:{book_meta}"
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=256)
score = ranker(**inputs).logits[0, 1].item() # 0~1 匹配度
top10.append((book_id, score))
top10 = sorted(top10, key=lambda x: -x[0])[:10]
精排完再把 10 本书甩给前端,让客户端决定“要不要装文艺、要不要偷偷推广告位”。
前端怎么让用户觉得“这书我非看不可”
3.1 别把“猜你喜欢”挂嘴边,太油腻
产品小姐姐原话:“用户一看见‘猜你喜欢’就想翻白眼,好像被大数据扒光了。”
于是我连夜把文案改了:
| 原来 | 改后 |
|---|---|
| 猜你喜欢 | 编辑部今晚也在熬夜看 |
| 相似推荐 | 这本书的闺蜜 |
| 热门畅销 | 上周被借爆 3 次 |
React 代码走一个:
// components/RecCard.tsx
import { memo } from "react";
import { ReactComponent as ArrowSvg } from "./arrow.svg";
interface Props {
book: any;
reason: string; // 推荐理由,后端把大模型吐的文案塞进来
}
const RecCard = memo(({ book, reason }: Props) => {
return (
<div className="rec-card">
<img src={book.cover} alt={book.title} loading="lazy" />
<div className="info">
<h3>{book.title}</h3>
<p className="author">{book.author}</p>
{/* 关键:把推荐理由写成人话 */}
<p className="why">
<ArrowSvg className="icon" />
{reason}
</p>
</div>
{/* 一键吐槽 */}
<button
className="dislike-btn"
onClick={() => {
fetch("/api/feedback", {
method: "POST",
body: JSON.stringify({ bid: book.id, action: "dislike" }),
});
}}
>
这啥玩意
</button>
</div>
);
});
export default RecCard;
/* rec-card.scss */
.rec-card {
position: relative;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transition: transform 0.25s;
&:hover {
transform: translateY(-4px);
}
.why {
font-size: 13px;
color: #666;
margin-top: 6px;
display: flex;
align-items: center;
.icon {
width: 12px;
height: 12px;
margin-right: 4px;
fill: #ff5722;
}
}
.dislike-btn {
position: absolute;
right: 8px;
top: 8px;
border: none;
background: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
}
}
3.2 允许用户“一键吐槽”——负反馈是宝藏
很多团队只顾着点“喜欢”,把“讨厌”按钮藏得比前任还深。
其实“踩”才是让模型快速收敛的加速器。
// hooks/useFeedback.ts
import { useCallback } from "react";
export function useFeedback() {
const send = useCallback(
async (params: { bid: number; action: "like" | "dislike"; scene?: string }) => {
await fetch("/api/feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...params, ts: Date.now() }),
});
// 本地立刻把卡片干掉,别等后端 200
// 这里用 optimistic update,用户体验爽到飞起
},
[]
);
return send;
}
冷启动用户:别把新人劝退在 5 秒内
新用户打开 App,后台一片空白,连性别都没填,怎么推?
三板斧:
- 先问场景,不让用户填年龄收入——“通勤/睡前/蹲坑”三选一,点一下就行。
- 用“伪互动”套偏好——甩 6 张封面,让用户选“想翻/不想翻”,其实是快速标注。
- 前端立刻把标注打包发回,服务端走“小样本学习”——用 6 个样本微调用户向量。
# 片段 4:6 次点击 → 微调用户向量
import torch.nn as nn
class UserVectorAdapter(nn.Module):
def __init__(self, dim=768):
super().__init__()
self.net = nn.Sequential(
nn.Linear(dim, 512),
nn.ReLU(),
nn.Linear(512, dim),
)
def forward(self, base_vec, likes, dislikes):
"""
likes: List[Tensor] 用户点“想翻”的书籍向量
dislikes:List[Tensor] 用户点“不想翻”的书籍向量
"""
if likes:
like_vec = torch.stack(likes).mean(0)
else:
like_vec = torch.zeros(768)
if dislikes:
dislike_vec = torch.stack(dislikes).mean(0)
else:
dislike_vec = torch.zeros(768)
delta = like_vec - dislike_vec # 喜好方向
return base_vec + 0.3 * self.net(delta) # 0.3 是超参,别问我怎么调,问就是 grid search
自动生成章节摘要——让“太长不看”党也能装文化人
产品提需求:“用户分享书籍到朋友圈,要配一段 50 字摘要,逼格要高。”
我当场脑壳痛:50 字还要逼格?
解决方案:
- 用 BART-Chinese 微调,训练集把“章节全文”→“豆瓣短评高赞”做平行语料。
- 推理阶段把全文按 512 token 滑窗,抽 3 个最“高潮”窗口,再分别生成 50 字,最后选 ROUGE 分最高那段。
# 片段 5:摘要服务
from transformers import BartForConditionalGeneration, BartTokenizer
model = BartForConditionalGeneration.from_pretrained("bart-book-summary")
tok = BartTokenizer.from_pretrained("bart-book-summary")
def gen_summary(chapter_text: str) -> str:
inputs = tok(chapter_text, return_tensors="pt", max_length=512, truncation=True)
ids = model.generate(
inputs.input_ids,
max_length=60,
min_length=40,
num_beams=4,
temperature=0.8,
repetition_penalty=2.0,
)
return tok.decode(ids[0], skip_special_tokens=True)
前端直接调:
// 分享面板弹窗
const shareHandler = async (chapterId: number) => {
const { summary } = await fetch(`/api/summary?cid=${chapterId}`).then((r) => r.json());
wx.shareTimeline({
title: `我在读《${bookTitle}》\n${summary}`,
imageUrl: cover,
});
};
根据心情生成专属书单封面——中二病也要满足
需求:用户选“丧/燃/甜/佛系”四个标签,前端实时生成封面,不让设计师加班。
技术选型:
- 前端用 Canvas + 粒子字体,把书名拆成粒子,根据情绪配色。
- 丧 = 冷灰 + 雨滴下落;燃 = 红橙渐变 + 火焰向上;甜 = 粉系爱心飘;佛系 = 墨绿 + 静止禅意。
// components/MoodCover.tsx
import { useRef, useEffect } from "react";
const palette = {
丧: { bg: "#3a3a3c", particle: "#a0a0a0", gravity: 0.2 },
燃: { bg: "#ff3300", particle: "#ffcc00", gravity: -0.2 },
甜: { bg: "#ffe0f0", particle: "#ff66b2", gravity: 0.05 },
佛系: { bg: "#4a5d23", particle: "#9bb471", gravity: 0 },
};
export default function MoodCover({ mood, title }: { mood: keyof typeof palette; title: string }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current!;
const ctx = canvas.getContext("2d")!;
const { bg, particle, gravity } = palette[mood];
canvas.width = 300;
canvas.height = 400;
ctx.fillStyle = bg;
ctx.fillRect(0, 0, 300, 400);
// 简单画个粒子
ctx.fillStyle = particle;
for (let i = 0; i < 80; i++) {
const x = Math.random() * 300;
const y = Math.random() * 400;
const r = Math.random() * 2;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
}
// 写字
ctx.font = "bold 24px PingFang SC";
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
const lines = title.length > 8 ? [title.slice(0, 8), title.slice(8)] : [title];
lines.forEach((line, i) => ctx.fillText(line, 150, 200 + i * 30));
}, [mood, title]);
return <canvas ref={canvasRef} style={{ borderRadius: 8 }} />;
}
隐私红线别踩,推荐多样性也别放飞
7.1 隐私合规三板斧
- 前端只上传“脱敏行为”——把 user_id 用 md5+salt 打散,后台映射到真实 id。
- 情绪识别模型跑在端侧——用 ONNX Runtime + Int8 量化,40 ms 出结果,原始文本不上传。
- 提供“一键影子模式”——用户点一下,所有行为不记录,推荐降级到热门榜单。
7.2 多样性怎么保
- 后处理加“MMR 最大边缘相关”——在相关度基础上惩罚相似书籍,强制插一本冷门诗集。
- 每周一次“人工彩蛋”——运营小姐姐手动塞 5 本绝版旧书,防止信息茧房。
# 片段 6:MMR 简单实现
def mmr_select(scores, embeddings, lamb=0.5, k=10):
"""
scores: List[float] 相关度分数
embeddings: List[Tensor] 书籍向量
lamb: 平衡相关度与多样性
"""
selected = []
idx_set = set(range(len(scores)))
for _ in range(k):
best = -1
best_val = -float("inf")
for i in idx_set:
sim_to_selected = 0
for j in selected:
sim = embeddings[i] @ embeddings[j] # 余弦
sim_to_selected = max(sim_to_selected, sim)
val = lamb * scores[i] - (1 - lamb) * sim_to_selected
if val > best_val:
best_val, best = val, i
selected.append(best)
idx_set.remove(best)
return selected
尾声:下次你深夜点开一本冷门诗集,别惊讶
它可能不是算法想赚钱,只是 AI 偷看了你朋友圈那句“好累”,于是悄悄把温柔塞到你手心。
别骂它偷窥,要怪就怪人类自己——
我们写代码的时候,早就把“懂人心”当成 KPI 了。
(完)

转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/master_chenchen/article/details/156913636



