示例代码进不了 AI 引用的素材库:SoftwareSourceCode 结构化让开发者课程被 GEO 引擎转述
适用读者:运营开发者课程、技术教程站的工程师与内容负责人;负责课程详情页、代码展示页模板的前端与后端开发;已经在做生成式引擎优化(Generative Engine Optimization, GEO)但发现 AI 转述总是跳过代码部分的人。
上个月复盘一个 .NET 课程站的 AI 引用日志时,看到一个扎心的事实:62 次被 AI 搜索引擎转述的课程页里,明确带出代码内容的只有 7 次,转述文本里出现完整代码块的次数是 0。课程最值钱的示例代码,在 AI 引用的素材库里根本不存在——引擎只复述讲解文字,代码被整段略过。这篇记录我们怎么用 SoftwareSourceCode 结构化声明把代码「挂号」进 AI 的素材库,以及 45 天观察期里数据发生了什么变化。
一、先看问题:AI 转述课程时,代码被丢在哪一步
站点结构不复杂:每个课程一个详情页,页面里有讲解文字、内嵌的示例代码块、指向 GitHub 仓库的链接、一个跳转到在线运行环境的按钮;每段示例代码还有一个独立的代码展示页,方便外链和收藏。这是开发者课程站最典型的资产结构。

观测方法说清楚,数据才有讨论价值。我们手工维护了一张引用日志表:用固定的 20 组「课程相关」问题(比如「ASP.NET Core 中间件怎么写异常处理」)分别去问三家 AI 搜索引擎,每周两轮,记录返回结果里引用了我们哪些页面、转述文本里有没有带出代码片段、有没有提到仓库链接。改造前的 45 天基线是这样的:
| 引擎 | 转述次数 | 带代码片段转述 | 仓库链接被提及 | 在线运行环境被提及 |
|---|---|---|---|---|
| 引擎 A | 24 | 3 | 2 | 0 |
| 引擎 B | 21 | 2 | 1 | 0 |
| 引擎 C | 17 | 2 | 0 | 0 |
| 合计 | 62 | 7(11%) | 3 | 0 |
现象归纳成三条:
- 转述只复述讲解文字,代码块整段消失,取而代之的是「该课程提供了示例代码」这类模糊表述,信息量约等于没说;
- GitHub 仓库链接几乎从不出现,用户照着转述走一遍,找不到可以跑起来的东西;
- 在线运行环境入口一次都没被提过——这本是课程转化率最高的入口。
初步归因:页面 HTML 里,代码块就是一段 <pre><code>,仓库链接就是一个普通 <a>,对 AI 引擎的抓取端来说和普通文本没有任何差别。引擎没有额外信号判断「这一段是可验证、可运行的代码资产」,自然按普通文章处理,转述时按压缩比把代码扔掉了。
二、转述机制剖析:AI 引擎怎么决定要不要转述你的代码
把 Schema.org 的 SoftwareSourceCode 类型补进页面之后,转述行为发生了明显偏移。这里先把机制讲透,改造才有依据。
机制一:内容类型识别。AI 引擎在检索阶段会给页面打内容类型标签,决定它在什么问题下被召回。一个课程详情页如果只有 Article 语义,它参与的是「文章类」召回队列,代码块只是文章里的一段格式化文本;声明 SoftwareSourceCode 之后,页面进入「代码实体」队列,programmingLanguage 字段直接给出语言标签,引擎可以做语言级路由——用户问 Python 的问题,优先召回声明了 programmingLanguage: Python 的页面。这一步决定了代码内容「有没有资格」参与转述。
机制二:可信度加权。代码这类素材有一个其他内容类型不具备的性质:可验证性。AI 转述一段代码,用户可以复制下来跑,跑不通立刻能发现——这意味着引擎在转述代码时犯错成本极高,所以它在决定「要不要转述代码」时会先评估这段代码的可靠程度。codeRepository 字段指向一个可克隆、有提交历史、有 README 的真实仓库,就是把这种可验证性显式交给了引擎:仓库可达、内容与页面声明一致,置信度上调;死链或空仓库,置信度反而下调。我们的理解是,引擎把「带可验证出口的代码」当作低错误率引用源处理,宁愿多花转述篇幅也要带出代码。
机制三:引用粒度变化。targetProduct 建立了「这段代码属于哪个课程产品」的实体关系,runtimePlatform 告诉引擎代码跑在什么运行时上。有了这两个字段,引擎转述时能把「代码 + 课程上下文 + 运行环境」打包输出,而不是干巴巴丢一段代码。观察期里被引次数最高的转述,就是引擎把代码展示页的片段和课程详情页的产品信息拼在一起输出的。
把这三条机制串起来,就是一条从抓取到转述的决策链:
flowchart LR
A[AI 引擎抓取课程页] --> B{页面是否声明代码实体}
B -- 无 SoftwareSourceCode --> C[按普通文章处理]
C --> D[转述时按压缩比丢掉代码]
B -- 有 SoftwareSourceCode --> E[进入代码实体召回队列]
E --> F[校验 codeRepository 可达性与一致性]
F --> G[高置信转述 代码与课程上下文打包输出]
字段到引擎用途的对应关系,整理成一张表:
| 字段 | 引擎侧的用途 | 对转述的影响 |
|---|---|---|
| programmingLanguage | 内容类型识别、语言级召回路由 | 决定代码在语言相关问题下被召回 |
| codeRepository | 可信度评估、可验证性校验 | 提升代码转述置信度,错误率预期低 |
| targetProduct | 实体关系、产品打包引用 | 转述时带出课程名与入口链接 |
| runtimePlatform | 运行环境匹配 | 支撑「怎么跑」类问题的转述 |
| license | 再分发合规判断 | 影响代码能否被整段引用 |
三、动手改造:给课程详情页与代码展示页补 SoftwareSourceCode JSON-LD
改造方案定了两条规则:课程详情页输出整课聚合的 SoftwareSourceCode,代码展示页输出单段示例的声明;JSON-LD 由服务端模板直接渲染进 <head>,不做前端异步注入——我们的站有一部分页面是 SPA,之前 structured data 放在客户端渲染,抓取端拿到的初始 HTML 里根本没有这段脚本,等于白写。这是改造里最容易被忽略的坑。
第一处改造用 Python 举例(课程站的教学后台是 FastAPI)。
依赖与环境:Python 3.11、FastAPI 0.110、Jinja2 3.1,安装命令 pip install fastapi jinja2。
# 依赖与环境:Python 3.11 / FastAPI 0.110 / Jinja2 3.1
# 安装命令:pip install fastapi jinja2
import json
# 教学后台入口,课程详情页路由复用同一套模板
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory="templates")
# 课程元数据:实际项目里从数据库或 CMS 读取,这里写死方便对照
COURSE = {
# 课程标题,会同时出现在代码实体与 targetProduct 里
"title": "ASP.NET Core 中间件实战",
# 课程路径,用于拼 targetProduct 的 url
"slug": "aspnetcore-middleware",
# 语言标签,供引擎做语言级召回路由
"language": "C#",
# 必须是真实可达的仓库完整 URL,死链会反噬置信度
"repo": "https://github.com/your-org/middleware-lab",
# 运行时版本,AI 编程助手类引用很看重
"runtime": ".NET 8",
# 开源协议,引擎据此判断能否整段引用
"license": "https://opensource.org/licenses/MIT",
}
def build_software_source_code(course: dict) -> dict:
# 组装 Schema.org SoftwareSourceCode 声明,字段含义见机制剖析一节
return {
# 固定为 schema.org 上下文,位置写错 validator 会直接报错
"@context": "https://schema.org",
# 声明本页主体是源代码实体
"@type": "SoftwareSourceCode",
"name": course["title"],
# 语言标签:引擎按语言召回的关键依据
"programmingLanguage": course["language"],
# 可验证性来源:仓库可达且与页面一致才加分
"codeRepository": course["repo"],
# 运行时:支撑「怎么跑」类问题的转述
"runtimePlatform": course["runtime"],
# 建立代码与课程产品的实体关系,转述时打包输出
"targetProduct": {
"@type": "Course",
"name": course["title"],
# 课程详情页地址,引擎转述时会带出这个入口
"url": f"https://example.com/courses/{course['slug']}",
},
"license": course["license"],
}
@app.get("/courses/{slug}", response_class=HTMLResponse)
def course_page(slug: str, request: Request):
# 服务端渲染:JSON-LD 必须出现在初始 HTML 里,不能等前端再注入
ld = json.dumps(build_software_source_code(COURSE), ensure_ascii=False)
# ensure_ascii=False 避免中文被转成 \uXXXX,抓取端可读性更好
# 模板 head 中输出为 <script type="application/ld+json">{{ json_ld }}</script>
return templates.TemplateResponse(request, "course.html", {"json_ld": ld})
第二处是 ASP.NET Core 站点(代码展示页是 .NET 项目,两套栈并存)。
依赖与环境:.NET 8、ASP.NET Core MVC,无额外 NuGet 包。
// 依赖与环境:.NET 8 / ASP.NET Core MVC,无额外 NuGet 包
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
// 与 Python 版字段一一对应,键名靠 JsonPropertyName 固定
// 避免 C# 命名风格破坏 Schema.org 的键名约定
public static class CourseLdBuilder
{
// 私有嵌套类只用于序列化,不参与业务逻辑
private sealed class SoftwareSourceCodeLd
{
// 上下文与类型是固定值,用只读属性直接给出
[JsonPropertyName("@context")]
public string Context => "https://schema.org";
[JsonPropertyName("@type")]
public string Type => "SoftwareSourceCode";
[JsonPropertyName("name")]
public string? Name { get; set; }
// 语言标签:引擎按语言召回的关键依据
[JsonPropertyName("programmingLanguage")]
public string? ProgrammingLanguage { get; set; }
// 可验证性来源:必须是完整仓库 URL,死链会反噬置信度
[JsonPropertyName("codeRepository")]
public string? CodeRepository { get; set; }
// 运行时版本:AI 编程助手类引用很看重
[JsonPropertyName("runtimePlatform")]
public string? RuntimePlatform { get; set; }
// 指回课程详情页,建立代码与课程的实体关系
[JsonPropertyName("targetProduct")]
public object? TargetProduct { get; set; }
}
public static string Build(string title, string lang,
string repo, string runtime, string courseUrl)
{
// targetProduct 用字典手写 @type 键,匿名对象输出不了 @ 前缀
var target = new Dictionary<string, object>
{
["@type"] = "Course",
["name"] = title,
// 课程详情页地址,引擎转述时会带出这个入口
["url"] = courseUrl,
};
var ld = new SoftwareSourceCodeLd
{
Name = title,
ProgrammingLanguage = lang,
CodeRepository = repo,
RuntimePlatform = runtime,
TargetProduct = target,
};
// 关闭中文转义,避免输出 \uXXXX 影响抓取端可读性
var options = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
// 返回的字符串在布局页 head 中原样输出
return JsonSerializer.Serialize(ld, options);
}
}
还有一个坑要交代:targetProduct 的嵌套对象如果图省事用 C# 匿名对象写(new { type = "Course" }),序列化后输出的是 "type" 而不是 "@type",validator 会直接报类型缺失。上面片段里我们改用了 Dictionary<string, object> 并手写 "@type" 键,Python 的 dict 写法没有这个问题。
渲染出来的 JSON-LD 必须过两道校验才能上线,流程如下:
flowchart TD
A[模板渲染出 JSON-LD] --> B[Schema.org Validator 校验]
B -- 字段报错 --> C[修正字段或键名拼写]
C --> B
B -- 通过 --> D[Google Rich Results Test 复测]
D -- 警告/报错 --> C
D -- 通过 --> E[更新 sitemap 等待抓取]
E --> F[每周固定问题集观测 AI 转述]
校验时遇到的报错记录两条,给后来人排雷:一次是 codeRepository 填成了仓库名称而不是完整 URL,validator 直接给 error;一次是复制模板时 @context 落在了嵌套对象里,Rich Results Test 报「无法解析结构」。两道工具都过了再推 sitemap,别省这一步。
四、45 天观察:改造前后的 AI 引用数据对比
改造在观察窗口第 0 天全量上线,观测方法与基线完全一致:同一组 20 个问题、三家引擎、每周两轮。45 天后的对比数据:
| 指标 | 改造前 45 天 | 改造后 45 天 | 变化 |
|---|---|---|---|
| AI 引用中含代码片段转述的比例 | 11%(7/62) | 34%(23/68) | +23 个百分点 |
| 单次转述平均带出的代码行数 | 0 行 | 6.2 行 | — |
| 仓库链接在转述中被提及 | 3 次 | 21 次 | +18 次 |
| 在线运行环境入口被提及 | 0 次 | 9 次 | 从 0 到 1 |
含代码块转述的比例从 11% 涨到 34%,不是某一家引擎的单独行为——三家分别从 12%、10%、12% 涨到 38%、31%、32%,方向一致。这说明起作用的不是某个引擎的特殊策略,而是页面信号补齐之后,转述决策链路里「这段内容是不是代码资产」的判断从猜变成了读。
另一个值得单独拎出来的变化是引用页类型分布。改造前 AI 引用几乎全部落在课程详情页的讲解文字上,代码展示页很少有人引用;改造后代码展示页的引用占比翻了三倍:
| 页面类型 | 改造前引用占比 | 改造后引用占比 |
|---|---|---|
| 课程详情页 | 71% | 48% |
| 代码展示页 | 12% | 39% |
| 课程列表 / 聚合页 | 17% | 13% |
代码展示页成为新的引用主力,符合预期:这类页面主体就是一段 SoftwareSourceCode,实体信号比详情页(文章 + 代码混合)更纯。带转述日志里质量最高的一条,引擎先引代码展示页的异常处理中间件片段(带出了 11 行代码和 runtimePlatform),再接了一句「来自课程《ASP.NET Core 中间件实战》」——这就是 targetProduct 打包引用的样子。
需要泼一盆冷水:34% 不等于终点,剩下 66% 的转述仍然不带代码。个别情况下引擎引了详情页却只复述课程大纲,说明聚合声明里代码实体和文章实体的权重分配还有优化空间,这块我们还在试。
五、两个误区与一个趋势
误区一:把代码截图发上去,以为 AI 看得见。截图是位图,抓取端拿不到文本、拿不到语言信息、更拿不到结构,对 AI 引用而言它等于不存在——甚至不如纯文本代码块,后者至少还能被按普通文本召回。结构化声明的价值不在于「多写了几行标签」,而在于把代码以机器可读、可验证的方式登记进引擎的素材库,转述时才有资格被点名。
误区二:补了 JSON-LD 就不管仓库。codeRepository 指向的仓库如果长期不更新、README 空白、甚至仓库改名后链接失效,等于在声明里附了一张可被证伪的名片。我们观察期里专门检查了引用高峰那几天的仓库可达性,保持着提交节奏。声明和实体必须一致,这是可信度加权的另一面。
趋势上留一个判断:AI 编程助手(IDE 内的补全与问答)正在成为课程内容的另一条引用出口,它们对 programmingLanguage 和 runtimePlatform 的匹配要求比搜索型引擎苛刻得多——版本对不上宁可不引。结构化声明越完整的课程,越容易进入这类助手的知识检索范围,现在补字段,抢的是这个入口的先手位。关于聚合页的实体权重分配,以及不同引擎对 codeRepository 的校验深度差异,欢迎在评论区交流各自站点的观测数据。
参考与延伸
- Schema.org SoftwareSourceCode 类型定义与全部字段说明:https://schema.org/SoftwareSourceCode
- Google 结构化数据入门文档(含 JSON-LD 语法与 Rich Results Test 用法):https://developers.google.com/search/docs/appearance/structured-data
SoftwareSourceCode、JSON-LD、GEO、AI 搜索、开发者课程、结构化数据、代码可验证性