制造业B2B官网GEO实战:用 Python 向量相似度给老产品手册自动补语义内链,让 AI 爬虫抓得更深
适用读者:负责工业品、装备制造类企业官网的前后端工程师与 SEO 技术负责人;正在为存量产品资料做 GEO(生成式引擎优化)改造的团队。文中示例基于 Python 3.11 + jieba + MySQL,思路对其他技术栈同样适用。
我们公司服务的几家制造业客户,官网都运营了八年以上,沉淀了上千篇产品手册、选型指南和技术白皮书。这些老内容有个共同病灶:彼此之间几乎是孤岛。新发布的内容编辑会顺手挂两三个链接,老内容发完就躺在那儿,十年没人动过。
传统 SEO 时代,孤页顶多影响收录排名。但在 GEO 时代,这个问题被急剧放大:AI 爬虫对单个站点的抓取预算远小于搜索引擎,它顺着链接走,走到没有链接的地方就折返。抓取深度浅,意味着大量页面根本进不了 AI 引擎的索引,DeepSeek、豆包在回答选型问题时自然不会引用一个它"没见过"的页面。
这篇文章完整记录我们 45 天的内链改造过程:怎么用向量相似度找出"本该互相链接"的页面对,怎么生成人类读者看着不别扭的锚文本,以及改造前后 AI 爬虫抓取行为和 AI 引用率的真实变化。全程只用了 jieba、pymysql 和标准库,没有调用任何付费 API。
一、先量化问题:AI 爬虫在你站里"逛两页就走了"
改造前我们做了一次基线测量。从 Nginx 日志里筛出四类 AI 爬虫的 UA(GPTBot、Bytespider、DeepSeekBot、PerplexityBot),统计 8 月 1 日至 8 月 7 日一周的抓取行为,以"单次会话内连续抓取的页面数"定义抓取深度:
| 指标 | 数值 | 说明 |
|---|---|---|
| 被抓取页面总数 | 612 | 全站可抓取页面 1487 个,覆盖率仅 41% |
| 平均抓取深度 | 1.8 页 | 大部分会话抓完首页和栏目页就离开 |
| 深度 ≥ 3 页的会话占比 | 11% | 只有少数会话进入详情页 |
| 产品手册页被抓占比 | 6% | 存量老内容是重灾区 |
| AI 引擎回答中引用本站 URL 的提问数(周均) | 4 | 全部集中在首页和两篇新发布的白皮书 |
数据暴露的问题非常直白:AI 爬虫从栏目页进来,顺着首屏那几个链接抓两三页就走了。它没有搜索引擎那种全站遍历的耐心,也不会主动翻页、翻目录。你的站内结构必须让它"顺路"走到内容深处。
为什么内链是控制抓取路径的唯一手段
很多团队第一反应是改 sitemap。但 sitemap 只是"报名清单",告诉爬虫有哪些页面存在;真正的抓取路径由链接决定。一个零入链的老手册页,就算出现在 sitemap 里,AI 爬虫也大概率不会为了它单独发起一次抓取------特别是在抓取预算紧张的时候。sitemap 解决"知不知道",内链解决"顺不顺路",后者才是抓取深度的决定因素。
第二个被低估的因素是锚文本的语义质量。AI 引擎在构建索引时会把锚文本作为目标页主题的补充信号,"点击这里"和"氟橡胶密封件的耐温范围"传递的信号完全不同。这也是后面锚文本生成环节要重点解决的问题。
二、方案思路:把内链建设建模成相似度推荐
人工给 1400 篇老内容补内链不现实,一名熟悉产品线的工程师一天最多审 100 对页面。所以我们的思路是把问题建模成推荐系统:对全站任意两篇内容计算语义相似度,相似度高、且当前没有链接关系的,就是候选内链对。
技术选型上有一个反直觉的决定:第一版没有上 embedding API,而是用 TF-IDF + 余弦相似度起步。原因有二:其一,工业品内容的专业词汇区分度极大------型号、材质、工艺名词高度专有,TF-IDF 在这类语料上的效果出奇地好;其二,1400 篇文档两两比对约 98 万对,TF-IDF 单机 20 分钟能跑完,完全本地运行、零成本。等粗筛跑通后再考虑用向量模型补召回,比一上来就堆模型务实得多。
python
# build_links.py 核心逻辑(节选)
import jieba, math, re
from collections import defaultdict
import pymysql
STOPWORDS = set("的 了 和 是 在 与 及 或 我们 你们 可以 进行 使用 通过 基于 "
"对于 以及 本文 如下 其中 更多 查看 了解 点击 相关 详细".split())
def tokenize(text):
words = jieba.lcut(re.sub(r"[^\u4e00-\u9fa5A-Za-z0-9]", " ", text))
return [w.lower() for w in words if w not in STOPWORDS and len(w) > 1]
# 1. 全量内容入桶:标题 + 摘要 + 正文前 800 字
docs = fetch_docs_from_mysql() # [(id, title, body)]
tokenized = {d[0]: tokenize(d[1] + " " + d[2][:800]) for d in docs}
# 2. TF-IDF 向量化
df = defaultdict(int)
for words in tokenized.values():
for w in set(words):
df[w] += 1
N = len(tokenized)
def tfidfvec(words):
tf = defaultdict(int)
for w in words:
tf[w] += 1
return {w: (1 + math.log(c)) * math.log(N / df[w])
for w, c in tf.items() if df[w] > 1}
vecs = {i: tfidfvec(ws) for i, ws in tokenized.items()}
def cosine(a, b):
dot = sum(v * b.get(w, 0) for w, v in a.items())
na = math.sqrt(sum(v * v for v in a.values()))
nb = math.sqrt(sum(v * v for v in b.values()))
return dot / (na * nb) if na and nb else 0.0
# 3. 暴力两两比对,1400 篇约 98 万对,单机 20 分钟
pairs = []
ids = list(vecs)
for x in range(len(ids)):
for y in range(x + 1, len(ids)):
s = cosine(vecs[ids[x]], vecs[ids[y]])
if s >= 0.18:
pairs.append((ids[x], ids[y], round(s, 3)))
pairs.sort(key=lambda p: -p[2])
跑完这 98 万对之后,还有一个工程上必须处理的环节:剔除已有链接。我们用 SQL 把现有正文中所有内部链接解析成 (source_id, target_id) 集合,再和候选对做差集。这一步漏掉的话,同一条内链会被重复推荐,审核工作量翻倍。
三、从候选对到真实内链:阈值标定与锚文本生成
相似度阈值是整个方案的成败关键,拍脑袋定阈值基本等于白干。我们在 300 个页面对上做了人工标注,按相似度分桶统计"值得互链"的比例:
| 相似度区间 | 人工判定"值得互链"的比例 | 处理策略 |
|---|---|---|
| ≥ 0.35 | 93% | 自动生成,直接写入 |
| 0.25 ~ 0.35 | 61% | 生成候选,人工批量过一遍 |
| 0.18 ~ 0.25 | 22% | 仅当两篇都缺入链时保留 |
| < 0.18 | 4% | 丢弃 |
标注结果证实了阈值的选择,也暴露了一个策略点:0.25~0.35 这个区间的页面数量最大,全部人工审会累死,我们后来按"目标页当前入链数为 0 优先"排序,先补孤页,存量审核压力两周内消化完。
锚文本:决定链接是信号还是噪音
锚文本生成规则比想象中重要得多。最初我们直接拿目标页标题做锚文本,写出来全是"参见《XX 型号选型手册》",机器味十足,业务方看了直摇头。第二版改成"从源文摘句":取两篇文档共享的最高 TF-IDF 权重词组,套上固定句式模板生成:
| 模板 | 生成示例 | 适用场景 |
|---|---|---|
| 关于{词组},可参考{标题}中的{章节} | 关于密封件耐温范围,可参考氟橡胶选型指南中的第三部分 | 技术参数类 |
| {词组}的完整计算过程在{标题}中有推导 | 抽速的完整计算过程在真空泵选型手册中有推导 | 计算推导类 |
| 与{标题}对比来看,{词组}的差异主要在{词组2} | 与螺杆机对比来看,能耗表现的差异主要在加载方式 | 对比选型类 |
| {词组}的现场处理经验记录在{标题}里 | 轴承异响的现场处理经验记录在维修案例集里 | 案例经验类 |
同时定了三条硬规则:单页新增出链不超过 5 条;优先给零入链页面补入链;链接位置放在正文前 60% 的区域内(尾部链接权重和被点击概率都低)。审核通过的候选对导出 CSV,批量写入内容系统的内链表:
sql
-- 审核通过的候选对批量写入内链表
-- 内容系统渲染时在正文指定位置输出"相关阅读"区块
LOAD DATA LOCAL INFILE 'approved_links.csv'
INTO TABLE content_inline_links
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
(source_id, target_id, anchor_text, sim_score, created_at);
-- 上线前校验:不允许出现指向已下线页面的链接
DELETE FROM content_inline_links
WHERE target_id NOT IN (SELECT id FROM contents WHERE status = 1);
渲染侧还有个细节:内链区块输出为普通 <a> 标签即可,不要加 rel="nofollow",也不要放在需要 JS 才能展开的折叠面板里------后者对 AI 爬虫等于不存在。
关于执行节奏还有一条经验:不要一次性把两千条内链全部上线。我们按"每周 300 条左右"分七批灰度发布,每批上线后观察一周日志再放下一批。这样做有两个好处:一是如果某批锚文本模板生成了病句,影响面可控、回滚只涉及一张表;二是 AI 爬虫对"一夜之间全站长出大量新链接"的站点行为模式敏感,灰度节奏更像自然编辑行为。事实上第二批就暴露了问题------对比类模板在两篇文档没有可比对象时生成了强行对比的句子,我们随即给该模板加了"两文必须同属一个产品线"的前置条件,第三批起就没再出现。
四、45 天后的效果数据
改造共写入 2100 余条内链,覆盖约 900 个页面。9 月第二周复测:
| 指标 | 改造前(8 月第一周) | 改造后(9 月第二周) | 变化 |
|---|---|---|---|
| 被抓取页面覆盖率 | 41% | 67% | +26 个百分点 |
| 平均抓取深度 | 1.8 页 | 3.4 页 | +89% |
| 产品手册页被抓占比 | 6% | 19% | +13 个百分点 |
| AI 引擎回答中引用本站 URL 的提问数(周均) | 4 | 17 | 3 倍以上 |
| 被 AI 引用页面的平均入链数 | 0.7 | 4.2 | --- |
两个值得展开的观察。第一,AI 引用的页面几乎全部落在"入链数 ≥ 3"的页面上,孤页即使被抓到也极少被引用------链接不仅帮爬虫找到页面,也是 AI 引擎判断内容主题权重和可信度的信号。第二,引用的提问类型从单一的"参数查询"扩展到了"选型对比"和"故障处理",说明爬虫顺着内链走到了更深、更靠后的内容类型,而这些恰恰是 B2B 决策链后半段的采购人员会问 AI 的问题。
五、误区澄清与下一步
误区一:内链不是越多越好。我们试过把单页出链上限放宽到 15 条做 A/B,AI 引用率不升反降------低相关链接稀释了页面的主题信号,对 AI 引擎来说噪音就是噪音。5 条以内、强相关,是实测出来的平衡点。
误区二:TF-IDF 不是终点。当两篇内容用词完全不同但语义相近------比如一篇说"密封"、另一篇通篇说"防漏"------TF-IDF 会漏掉这类配对。下一步我们计划对相似度 0.10~0.18 的灰色地带页面对,调用本地部署的 embedding 模型做二次筛选,把召回补上,同时在锚文本生成环节接入更细的句式模板。
趋势判断上,AI 引擎的抓取预算分配越来越像人类编辑:顺着链接、带着主题意图走。给存量内容补语义内链,本质上是在 AI 的知识地图里给你的页面修路------这条路修得越语义化,AI 推荐你的时候越有依据。
整套方案的全部依赖只有 jieba、pymysql 和标准库,单机可跑,一个下午就能出第一版候选清单。如果你也在给老站做 GEO 改造,卡在抓取深度或 AI 引用率上,欢迎在评论区聊聊你的日志数据。
关键词:GEO、AI优化AIO、语义内链、TF-IDF、向量相似度、AI爬虫抓取深度、DeepSeek引用、锚文本优化