读到”ChatGPT 一直把我退出登录”这篇底部,相关推荐里出现另外三篇:“ChatGPT 会话过期太快”、“ChatGPT 自动登出问题”、“ChatGPT 5 分钟后被登出”。三篇其实是同一个问题,只是过去一年追长尾时换着标题写了三遍。推荐器把它们顶上来,是因为每一个信号(标签重合、标题词 Jaccard、向量余弦)都说它们最相关。读者看到三条重复,跳出,Google 看到一个站点在给自己推荐重复内容——这是一个很强的质量负信号。这是成熟内容站里比较难的一类问题,因为推荐器在做的事情正是你最初让它做的。
这篇讲清楚怎么检测、怎么打分”相关但不重复”、以及在流水线哪一处修。
常见原因
按命中率从高到低排列。
1. 标签重合推荐器把近重复标签当成完美匹配
两篇文章 6 个标签里重 5 个,按标签重合排序它们永远互相置顶,哪怕回答的是同一个问题。
怎么判断:对每篇文章看 top-3 相关。如果标题跟宿主文章 70% 以上词重合,推荐器在奖励近重复。
2. embedding 相似度把重复排在互补之前
升级到 sentence-embedding 余弦后,近重复跑到 0.95+ 占第一,互补内容(规范修复、预防指南、相关工具)只有 0.7-0.8,被挤出面板。
怎么判断:把每个候选的余弦分打出来,如果 top-3 都 > 0.92,你推荐的是重复,不是相关。
3. 标题词重合是唯一特征
旧推荐器只对标题分词做 shingle。共享 3-4 个高频词(“ChatGPT”、“登录”、“失败”)的文章形成一个紧簇,只推自己人。
怎么判断:生成一张共推图。3-5 篇形成紧密小团、几乎没有出边的就是泄漏。
4. 真的存在该合并的重复,不是推荐器的锅
推荐器把重复浮上来,是因为重复真实存在。要修的是内容层的 canonical,不是更聪明的小部件。
怎么判断:把三篇”相关”读一遍,如果你说不出一句话区别,就是重复。小部件是症状不是 bug。
5. 多个 URL 指向同一个 canonical,泄漏到相关里
tag 页、分类页、旧 slug 都指向同一篇 canonical。推荐器把每个 URL 当独立候选,三条全推。
怎么判断:相关条目经过重定向后,落到同一个 canonical URL。
6. 手动编辑挑选覆盖了算法,而编辑挑了重复
CMS 里支持”手动相关”字段,编辑选了流量最高的几篇兄弟稿,正好都是近重复。
怎么判断:把算法输出跟最终渲染的小部件对比。如果系统性偏向手动挑选、且挑选都是近重复,这是人不是模型。
7. 推荐器对宿主文章自己的主题簇没有权重衰减
所有推荐都从同一个 subcategory 来,没有”广度惩罚”,面板永远是来自 10 篇同簇的 3 篇。
怎么判断:统计所有渲染相关面板的 subcategory 分布。如果 80%+ 推荐都待在宿主 subcategory 内,模型没有簇多样性项。
最短修复路径
第 1 步:构建时算一份”重复检测”信号
为每个文章对算一个相似分并落盘。推荐器用它做硬截断。
// scripts/article-similarity.mjs
import fs from 'node:fs';
import { encode } from 'gpt-tokenizer';
function shingles(text, n = 5) {
const tokens = encode(text.toLowerCase());
const set = new Set();
for (let i = 0; i <= tokens.length - n; i++) {
set.add(tokens.slice(i, i + n).join('-'));
}
return set;
}
function jaccard(a, b) {
const inter = [...a].filter(x => b.has(x)).length;
return inter / (a.size + b.size - inter);
}
const articles = loadAllArticles();
const shing = new Map(articles.map(a => [a.slug, shingles(a.title + ' ' + a.description + ' ' + a.body.slice(0, 2000))]));
const pairs = [];
for (let i = 0; i < articles.length; i++) {
for (let j = i + 1; j < articles.length; j++) {
const s = jaccard(shing.get(articles[i].slug), shing.get(articles[j].slug));
if (s > 0.4) pairs.push({ a: articles[i].slug, b: articles[j].slug, score: s });
}
}
fs.writeFileSync('data/similar.json', JSON.stringify(pairs, null, 2));
Jaccard > 0.5 的对就是推荐器必须排除的近重复。
第 2 步:推荐器加硬过滤
无论你的相似函数是标签重合、向量、还是两者,都先过近重复过滤再排序:
const NEAR_DUP_THRESHOLD = 0.4;
function isNearDup(host, candidate) {
const key = [host, candidate].sort().join('|');
return similarityMap.get(key) > NEAR_DUP_THRESHOLD;
}
const related = candidates
.filter(c => !isNearDup(host.slug, c.slug))
.slice(0, 3);
有时小部件会少于 3 条。这是好事——空着也比误导好。
第 3 步:排序里加一个簇多样性项
过完去重之后,再对跟宿主同 subcategory 的候选罚一点分:
function score(host, candidate) {
const base = embeddingCosine(host, candidate);
const sameSub = host.subcategory === candidate.subcategory ? 0.15 : 0;
return base - sameSub;
}
调到 top-3 最多只有 1 条同 subcategory 邻居。
第 4 步:决策:合并还是差异化
审计跑出近重复对之后,你只有两个选项:
- 合并:留一篇 canonical,其他 301。一篇流量明显领先、其他是变体时最合适。
- 差异化:把每一篇改写到回答不同子问题。每篇都有独立外链或独立流量时合适。
不要让它们”以重复存在但被小部件藏起来”。藏起来的重复还会出现在 tag 页、sitemap、搜索结果里。
第 5 步:候选先过一次重定向再推荐
function canonicalSlug(slug) {
const target = redirectMap.get(`/zh/articles/${slug}/`);
if (target) return target.replace(/^\/zh\/articles\//, '').replace(/\/$/, '');
return slug;
}
const candidates = rawCandidates.map(c => ({ ...c, slug: canonicalSlug(c.slug) }));
按解析后的 slug 去重。否则同一篇 canonical 会出现三次。
第 6 步:审计编辑手动挑选
如果 CMS 支持”编辑精选相关”字段,跑一次性审计:
for (const article of articles) {
for (const manual of article.manualRelated || []) {
const sim = similarityMap.get([article.slug, manual].sort().join('|')) || 0;
if (sim > 0.4) console.log(`Manual pick near-dup: ${article.slug} -> ${manual} (${sim})`);
}
}
把这份单子退给编辑,要求按”合并/差异化”逐一定夺。
第 7 步:CI 护栏
全站近重复对超过 N 对就让构建失败。趋势比绝对数重要——如果连续三周上涨,在影响簇里冻结新发文,先消化再加内容。
哪些不该算在你头上
真正覆盖长尾的内容站永远会有近邻。门槛不是”没有两篇相似”,而是”没有相关面板把同一个答案推三遍”。兄弟稿之间 Jaccard 在 0.3-0.4 是健康的,也是有用的。
容易误判成
- “推荐模型有问题。“——模型可能在”找相似”上很好。问题是你让它找”相关”,它找了”相似”,这是两件事。
- “我们要再写一些,把这个簇撑开。“——加更多通常更糟。簇致密是话题本身就窄。
- “标签体系太粗。“——有时确实,但跟小部件推不推重复无关。先修小部件,再审计标签。
预防
- 构建时跑成对 Jaccard / embedding 余弦,矩阵落盘。
- 推荐器在排序之前先过近重复过滤,而不是事后藏。
- 加簇多样性惩罚,使 top-3 在目录允许时至少跨 2 个 subcategory。
- 候选 slug 过一次重定向表再去重。
- 季度审计:Jaccard > 0.5 的对全部进入”合并/差异化”决策日志。
- CI 给全站近重复对总数加上限,涨了就在受影响簇里暂停发文。
FAQ
- 为什么不能只是在小部件里隐藏重复、不合并? 因为它们还会出现在 tag 页、sitemap、搜索里。在一个入口隐藏,不能修底层的重复本身。
- 少推几条会不会伤互动? 一般不会。三条不重复的链接比三条重复的更有点击,因为读者真的会点不重复的。
相关
- 簇重叠和关键词自相残杀
- 大量页面标题重复
- 瘦页面太多
- 内容页变成孤岛
- 话题簇深度不够
- 全站内链分布不均
- 首页无法把权重分发出去
- AI 生成内容缺乏独特价值
- Search Console 报告大量低价值 URL
- 页面多曝光少
- 站点扩张太快内容显得重复
- 文章长期不更新
标签: #内容运营 #排查 #SEO #internal-linking #duplicate-content #recommendations