课程站的 AI 搜索突围:coursePrerequisites 与 courseCode 把先修链讲给引擎听
适用读者:在线教育/知识付费平台的前端与内容工程同学,负责课程详情页、课程目录与结构化数据的人。如果你想让自己平台的进阶课在 AI 搜索里被正确推荐给有基础的学习者,这篇是实操手册。
「数据可视化实战」这门进阶课,页面流量不差,但在 Perplexity 和豆包里问「零基础学数据可视化该报什么课」,推荐列表里永远只有它隔壁那门入门课。运营同学小蒋十月初拉了一次统计:这门年营收贡献排前五的进阶课,过去半年在各类 AI 引擎回答里被引用次数是 0。
问题不在课程质量,在课程页面没把「先修关系」说清楚。生成式引擎优化(Generative Engine Optimization, GEO)这件事里,最容易被忽略的一环就是课程链路:引擎不知道「数据可视化实战」建立在哪门课之上,也就不敢把它推给搜入门内容的人。这篇文章记录我们怎么用 Schema.org 的 Course 实体,把先修链一条条喂给引擎。
先看引擎到底缺什么信息
打开课程页的源码,我们当时的结构化数据长这样(简化后):

{
"@context": "https://schema.org",
// 旧版标记只有三个字段,等于一张课程名片
"@type": "Course",
"name": "数据可视化实战",
// 描述无法承载结构化的先修关系
"description": "从数据清洗到交互大屏,完整实战项目教学。"
}
// 问题一眼可见:没有 courseCode,也没有任何指向其他课程的关系字段
改造前的问题一眼可见:没有 courseCode,没有先修关系,也没有开课时间。下面是改后的标记(代码里的 // 仅为讲解注释,JSON 本身不支持注释,上线时请删除)。
这段标记对引擎来说等于一张名片:知道课程叫什么、大概讲什么,别的都没了。学习者点进来能看目录、看评价、看「建议先学 XXX」,这些信息全在页面正文里,而正文恰恰是 AI 引擎最不擅长做结构推理的部分。
引擎面对的实际决策是这样的:
flowchart LR
A[用户问: 有基础<br>想学数据可视化] --> B{引擎检索候选课程}
B --> C[入门课: D3 基础]
B --> D[实战课: 数据可视化实战]
D --> E{有无先修信息?}
E -->|没有| F[风险高: 推了用户听不懂<br>不敢选]
E -->|有 coursePrerequisites| G[匹配学习者水平<br>放心推荐]
C --> H[入门课被反复引用]
引擎推荐进阶内容的顾虑很实际:推荐一门用户跟不上课,用户会追问「你为什么推荐这个」,回答质量评分就掉。所以缺先修信息时,引擎的默认策略永远是保守——宁推入门,不推进阶。这就是进阶课被引用为零的直接原因。
用 coursePrerequisites 把链路接上
改法不复杂。Schema.org 的 Course 类型里有 coursePrerequisites 属性,官方定义就是「修这门课之前需要掌握什么」。关键是两点:先修课要指向同平台的 Course 实体(用 @id 或 courseCode),不能写一句纯文本;被指向的那门课自己也得有完整的 Course 标记,两边要能对上。
改后的实战课标记:
{
"@context": "https://schema.org",
// @graph 用来把本课和被引用的先修课实体放进同一张图谱
"@graph": [
{
// 当前课程:进阶课「数据可视化实战」
"@type": "Course",
// @id 是实体锚点,先修课会反向引用这个地址
"@id": "https://example-edu.cn/courses/dataviz-pro#course",
"name": "数据可视化实战",
// 编码规则:课程线-难度档+序号,2 开头即进阶
"courseCode": "DVP-201",
// 描述里显式写出适合人群,和先修声明互相印证
"description": "面向已掌握 D3 基础的学员,完成 3 个真实项目。",
// 先修关系:指向同平台入门课的 Course 实体
"coursePrerequisites": {
"@type": "Course",
// @id 必须对应入门课详情页上声明的同一个锚点
"@id": "https://example-edu.cn/courses/d3-basic#course",
// courseCode 双保险,引擎只解析到编码时也能对上
// 两个值同时给,兼容只认其一的解析器
"courseCode": "DVP-101"
},
// 开课时序:两个班次,供引擎回答「什么时候开课」
"hasCourseInstance": [
{
"@type": "CourseInstance",
// online 表示纯线上班,线下班写 Onsite
"courseMode": "online",
// ISO 8601 时长:PT6H 即 6 小时
"courseWorkload": "PT6H",
"startDate": "2026-11-15"
},
{
"@type": "CourseInstance",
"courseMode": "online",
"courseWorkload": "PT6H",
// 第二班次比第一个晚五周
"startDate": "2026-12-20"
}
]
}
]
}
环境说明:这是纯 JSON-LD,通过 <script type="application/ld+json"> 嵌在课程详情页模板里,不依赖任何运行库,Node 18 或直接静态输出都行。几行关键改动拆开讲:
courseCode: "DVP-201":平台自己定的编码规则,前两位是课程线缩写,后三位里首位代表难度档位,1 开头是入门、2 开头是进阶。coursePrerequisites里的@id直接指向入门课的 Course 实体锚点,courseCode双保险——就算引擎只解析到 code 也能对上。hasCourseInstance里的startDate给引擎一个时间锚点,回答「什么时候开课」这类问题时直接可用。
courseWorkload 的 ISO 8601 格式容易写错,PT6H 表示 6 小时,分钟是 PT90M,别写成 6h。
courseCode 编码规则要写成制度
编码规则看着是小事,实际决定引擎能不能从 code 本身读出层级。我们踩过一个坑:早期课程 code 是编辑随手起的,DATA101、VIZADV、class7 混在一起,引擎无从判断谁先谁后。九月中旬我们重定了规则:
| 字段 | 位置 | 含义 | 示例 |
|---|---|---|---|
| 课程线 | 第 1-3 位 | 业务域缩写 | DVP(数据可视化) |
| 难度档 | 第 4 位 | 1 入门 / 2 进阶 / 3 高阶 | DVP-201 |
| 序号 | 第 5-6 位 | 线内顺序 | DVP-201 |
| 分隔符 | 中间 | 连字符,不用下划线 | - |
规则定了还要落到课程目录的数据表里,不然下个批次又乱。我们的课程主表加了三个字段:
-- MySQL 8.0,课程主表 course 补充字段,请在测试库先验证再上线
ALTER TABLE course
ADD COLUMN course_code VARCHAR(12) NOT NULL UNIQUE COMMENT '平台课程编码,规则见内部文档 CODE-2026-03';
ALTER TABLE course
-- 入门课此列存 NULL,进阶课指向先修课主键
ADD COLUMN prereq_course_id BIGINT NULL COMMENT '先修课主键';
ALTER TABLE course
-- 冗余难度档位,渲染模板时避免每次解析 course_code 字符串
ADD COLUMN difficulty_level TINYINT NOT NULL DEFAULT 1 COMMENT '1入门 2进阶 3高阶';
-- 外键约束保证先修课一定存在于本平台,避免脏引用
ALTER TABLE course
ADD CONSTRAINT fk_course_prereq
-- 删除策略选 RESTRICT,防止先修课被顺手清掉
FOREIGN KEY (prereq_course_id) REFERENCES course(id)
ON DELETE RESTRICT;
页面渲染时,模板从这个表读 prereq_course_id,反查先修课的 course_code 和 @id,拼进 JSON-LD。数据和标记同源,就不会出现页面写着「建议先学 D3 基础」而标记里指向另一门课的错位。
改造前后的差异可以放在一起看:
| 对比项 | 改造前 | 改造后 |
|---|---|---|
| 先修关系 | 仅页面正文一句话 | coursePrerequisites 指向实体 |
| 课程编码 | 编辑随意填写 | DVP-201 式规则化编码 |
| 开课时间 | 正文招生文案里 | hasCourseInstance.startDate |
| 引擎可校验性 | 无法核对 | @id 对同平台实体可交叉验证 |
| 学习时长 | 无 | courseWorkload 标准时长 |
机制剖析:引擎侧为什么认这一套
这里得说清楚平台侧的运作逻辑。主流 AI 引擎对结构化数据的处理分两层:抓取层把 JSON-LD 解析成知识图谱里的实体和关系边;生成层在组织回答时,会优先沿着这些已验证的关系边走,而不是靠语言模型自己从正文里猜。
coursePrerequisites 被解析成一条 prerequisite 边之后,引擎回答「学完 X 课接下来学什么」就有了确定依据。更关键的是保守推荐策略会反转:当引擎能确认「搜数据可视化的用户如果标记过已学 DVP-101,DVP-201 就是安全的下一步」,进阶课就从「不敢推」变成「该推」。
Google 的结构化数据文档里 Course 属于富结果类型,官方明确建议课程间的依赖关系用 coursePrerequisites 表达。这属于各家引擎共同认可的通用语义,一次改造多处受益。
落地过程与效果
改造是十月第二周做的,前后三个工作日。第一步整理 42 门课的先修关系,运营提供人工清单,我们核对到课程表;第二步改详情页模板输出 JSON-LD;第三步用 Rich Results Test 逐页验证。中途翻过一次车:有两门课的 @id 锚点写成了课程列表页地址而不是详情页,导致引擎解析到的实体对不上,验证工具直接报错,改回锚点就过了。
效果数据(平台内部观测,非第三方统计):
| 指标 | 改造前(9 月) | 改造后(10 月下旬) |
|---|---|---|
| 「数据可视化实战」在 AI 回答中被引用次数 | 0 | 27 |
| 先修关系被引擎正确表述的回答样本 | 0 | 9 |
| 课程站整体 AI 渠道推荐量 | 41 | 88 |
27 次引用里,有 9 次回答原文出现了「在完成 D3 基础之后」这类先修表述,说明引擎确实读到了关系边,不是碰巧。数据量还小,别当成普遍规律,但方向是对的。
改完的校验流程建议固化下来:
flowchart TD
A[编辑发布/修改课程] --> B[CI 触发结构化数据校验]
B --> C{JSON-LD 语法合法?}
C -->|否| D[阻断发布, 提示行号]
C -->|是| E{prereq 指向的 @id 存在?}
E -->|否| F[阻断: 先修课实体缺失]
E -->|是| G{difficulty 与 courseCode 第4位一致?}
G -->|否| H[警告并进人工队列]
G -->|是| I[放行, 提交搜索引擎重抓]
最后这道关卡值得多花心思。课程下架是最常见的翻车点:先修课下架了,所有指向它的进阶课标记全部悬空。我们的做法是下架课程先转「隐藏」状态保留实体 90 天,给引擎留出更新窗口。
几个容易想岔的地方
有人说「正文里写了先修建议就够了,引擎那么聪明总能看懂」。真看不懂,至少目前做不到稳定看懂。语言模型从营销文案里推断课程难度,错误率远高于读一条显式的结构化边。正文和标记不冲突,但只写正文等于把判断权交还给引擎的猜测。
另一个误区是给每门课都塞一长串先修课。先修链要克制,链上每一环都得是真实存在、内容上确实依赖的课程。把同级课程也标成先修,引擎交叉验证发现内容重叠,反而会降低对整个站点实体数据的信任。
趋势上看,AI 引擎对垂直领域的关系语义(课程先修、商品适配、内容难度)会越来越依赖站点自己声明的结构化数据。知识付费平台手里有天然的图谱素材——课程、章节、讲师、学员路径,现在把 Course 这层做扎实,后面接 LearningResource、hasCourseInstance 的排期信息都是顺手的活儿。你们平台上有没有课程在 AI 搜索里「隐身」的情况?欢迎评论区聊聊具体场景。
参考与延伸
- Schema.org Course 类型定义:https://schema.org/Course
- Google 搜索中心结构化数据(Course 富结果):https://developers.google.com/search/docs/appearance/structured-data/course
- JSON-LD 1.1 规范:https://www.w3.org/TR/json-ld11/
GEO、AI 搜索引用、Course、coursePrerequisites、courseCode、JSON-LD、在线教育