企业官网 GEO 实战:用 ProfilePage 与 knowsAbout 让 AI 记住写文章的人

2026-09-22 01:22:57 1 次浏览
GEOAI搜索ProfilePageknowsAboutSchema.org结构化数据

我们厂技术博客上有三位干了十年以上的工程师在写东西,署名清清楚楚挂在标题下面。可在 AI 搜索里问同一类技术问题,回答里引的是「据某工业软件厂商技术博客」——三个人写了两年,一个名字都没留下。这事儿跟内容质量没关系,问题出在页面上缺了一份机器能读走的作者实体声明。

名字挂在标题下面,机器照样读不到

先把现场摆清楚。我们的文章页模板是这么写的:

主题配图

<!-- 文章页头部:作者信息当时只存在于可见文本里 -->
<div class="byline">
  <span class="author">张磊</span>
  <span class="title">首席工程师 · 数控系统通信</span>
  <time datetime="2026-09-18">2026-09-18</time>
</div>

这段 HTML 人眼看得明明白白,搜索引擎也能抓到「张磊」三个字。但抓到字符串和认出一个「人」是两回事。生成式引擎在组织回答时,要判断这句话该归给谁、这个人懂什么、他写的其他东西在哪儿——这些判断依赖的是实体,不是文本片段。

我们当时的 JSON-LD 只声明了 Article,author 给的是一个纯字符串:

// 改造前的写法:author 只是个名字字符串
{
  "@context": "https://schema.org",
  "@type": "TechArticle",
  // 问题就在这行:字符串无法指向任何已存在的实体
  "author": "张磊"
}

字符串形式的 author 在解析时会被当成一个临时的、没有身份的 Person,它不跟任何页面上的其他声明合并。三篇文章里写三次「张磊」,引擎那边就可能躺着三个互不相识的张磊。名字越常见,分裂得越厉害。

我们事后复盘,症状其实是能提前看出来的:

观察到的现象 当时以为的原因 真实原因
AI 回答里只提站点不提人 文章不够权威 页面上没有稳定的作者实体
同一作者的多篇文章在实体面板里各自独立 收录问题 author 是字符串,@id 缺失导致实体分裂
作者姓名被替换成「某厂商」 引擎刻意匿名 引擎找不到可引用的具名实体,退回站点级表述
换个名字写法(Lei Zhang / 张磊)结果就不一样 随机波动 没有 alternateName 做名字归并

实体消解的机制:@id 是那条主线索

这一节讲清楚底层逻辑,不然照着改也容易改歪。生成式引擎优化(Generative Engine Optimization, GEO)里有一大块工作叫实体消解,说白了就是引擎把从不同网页上抓到的一堆碎片,判断成同一个东西的过程。

Schema.org 的这套词汇表给每个实体留了一个 @id 字段,本质上是这个实体的统一标识(一个 URL 形态的字符串)。引擎在消解时,最可靠的一条规则就是:两个声明的 @id 相同,就当成一个实体合并。文本名字相似度只是辅助信号,sameAs 指向的外部档案是交叉验证信号。

flowchart TD
  A[抓取器拿到作者页 HTML] --> B{能否解析出 JSON-LD}
  B -- 否 --> C[退回纯文本抽取, 只得到姓名字符串]
  B -- 是 --> D[读到 ProfilePage 的 mainEntity]
  D --> E[取 Person 的 @id 作为主键]
  E --> F[合并 knowsAbout / jobTitle / alumniOf 等属性]
  F --> G[与 sameAs 指向的外部档案做交叉验证]
  G --> H[写入实体库, 形成可被引用的具名人]
  C --> I[生成一个无主临时节点]
  I --> J[引用时只能说 某网站]
  H --> K[引用时可说 某某公司张磊]

所以 ProfilePage 在这件事上的角色很朴素:它是一个页面类型,作用是给「这个人」找一个正式的落脚点。真正被记住的是 mainEntity 指向的那个 Person。

graph LR
  subgraph 作者页
    PP[ProfilePage] -->|mainEntity| P[Person 张磊]
    P -->|knowsAbout| T1[主题 数控系统通信]
    P -->|knowsAbout| T2[主题 EtherCAT]
    P -->|alumniOf| ORG[Organization 某高校]
    P -->|sameAs| GH[GitHub 主页]
  end
  subgraph 文章页
    AR[TechArticle] -->|author 引用同 @id| P
    AR -->|about| T2
    AR -->|isPartOf| BK[Blog]
  end
  P -->|knowsLanguage| ZH[zh-CN]

双向锚定的意思就在这张图上:作者页向下声明「这个人懂什么」,文章页向上声明「这篇是谁写的」,两边用同一个 @id 扣住。少任何一边,这张网就散。

动手前的两件杂事:盘作者,定 @id

改造之前我们花了一周做清点,这活儿看着琐碎,省下来的返工量很大。

第一件是把每位作者的资产盘出来。工业软件这个行当里,工程师的资历往往散落在论文、专利、开源仓库、行业协会名单里,能拿来做 sameAs 的东西不少。

第二件是定 @id 的规则,这个必须一次定死。我们的规则是 https://域名/authors/{拼音连字符}/#person,全站不许出现第二种写法。

字段 Schema.org 属性 张磊填了什么 踩坑记录
统一标识 @id /authors/zhang-lei/#person 早期带过 www 前缀,导致与不带 www 的分裂成两个实体
姓名 name / alternateName 张磊 / Lei Zhang 只写中文名时,英文提问场景召回明显变弱
主题 knowsAbout 数组,5 项 写成一整句长文本,引擎只取到第一个词
职位 jobTitle 首席工程师,数控系统通信方向 早期写成「技术部」,太泛,回答里没法用
教育经历 alumniOf Organization,带 @id 只给学校名字字符串,没有归并效果
语言 knowsLanguage zh-CN / en 漏了这项时,跨语言提问容易匹配不上
外部档案 sameAs GitHub、协会主页 链到需要登录才能看的页面,等于白填

ProfilePage 骨架怎么搭

下面这份是作者页最终落地的结构。环境是普通的 HTML 模板加服务端变量渲染,没有任何第三方依赖,// 开头的行只是讲解用,输出前要剔掉,JSON 本身不支持注释。

// 环境:无依赖,纯 JSON-LD,由模板渲染后注入作者页 head
// 提醒:讲解用的 // 注释行在上线前必须删除
{
  // 声明词汇表来源
  "@context": "https://schema.org",
  // ProfilePage 是"页面"类型,人是它声明的主体
  "@type": "ProfilePage",
  // 页面自身的地址,与 canonical 保持一致
  "url": "https://example.com/authors/zhang-lei/",
  // 最后修订时间,供引擎判断新鲜度
  "dateModified": "2026-09-18",
  // 面包屑:给引擎一条从首页到作者页的路径
  "breadcrumb": {
    "@type": "BreadcrumbList",
    "itemListElement": [
      // 第一级:首页
      { "@type": "ListItem", "position": 1, "name": "首页", "item": "https://example.com/" },
      // 第二级:团队列表页
      { "@type": "ListItem", "position": 2, "name": "技术团队", "item": "https://example.com/authors/" },
      // 第三级:当前作者
      { "@type": "ListItem", "position": 3, "name": "张磊", "item": "https://example.com/authors/zhang-lei/" }
    ]
  },
  // 核心行:把页面主体声明成一个 Person
  "mainEntity": {
    "@type": "Person",
    // 全站统一标识,文章页的 author 要原样引用这一串
    "@id": "https://example.com/authors/zhang-lei/#person",
    // 中文名
    "name": "张磊",
    // 英文写法,用于跨语言归并
    "alternateName": "Lei Zhang",
    // 职位,回答里"某厂资深工程师"的表述常来自这里
    "jobTitle": "首席工程师,数控系统通信方向",
    // 一句话简介,别写市场口吻
    "description": "从事数控系统通信协议研发 14 年,主导过三代产品的总线栈重构。",
    // 语言能力
    "knowsLanguage": ["zh-CN", "en"],
    // 主题数组:每条一个主题,不要拼成长句
    "knowsAbout": [
      "数控系统通信",
      "EtherCAT",
      "实时以太网",
      "现场总线时延分析",
      "运动控制"
    ],
    // 教育经历用 Organization 并带 @id,才能参与归并
    "alumniOf": {
      "@type": "CollegeOrUniversity",
      "@id": "https://example.com/entities/orgs/#hust",
      "name": "华中科技大学"
    },
    // 任职机构,同样带 @id
    "worksFor": {
      "@type": "Organization",
      "@id": "https://example.com/#organization"
    },
    // 外部档案,用来交叉验证身份
    "sameAs": [
      "https://github.com/example-leizhang",
      "https://www.example-assoc.org.cn/member/lei-zhang"
    ]
  }
}

knowsAbout 这块值得单独说一句。它是 Person 上的一个属性,值应该是主题的数组,每一项是一个独立主题。我们第一版偷懒写成 "knowsAbout": "数控系统通信、EtherCAT 与实时以太网",结果引擎只认出第一个词。改成数组之后,作者在「实时以太网时延」这类提问里的出场率才上来。

文章页回指作者:author 只写名字会掉链子

作者页建好了,文章页不改等于白干。改造点在 author 上:把字符串换成带 @id 的引用。

// 环境:无依赖,注入文章页 head
// 提醒:上线前删除 // 注释行
{
  "@context": "https://schema.org",
  // 技术文章用 TechArticle 比 Article 更贴切
  "@type": "TechArticle",
  // 文章自身的标识
  "@id": "https://example.com/blog/fieldbus-latency/#article",
  "headline": "现场总线时延的三类来源与实测方法",
  // 关键行:author 必须是对象,且 @id 与作者页完全一致
  "author": {
    "@type": "Person",
    // 这一串要和作者页 mainEntity 的 @id 一个字符都不差
    "@id": "https://example.com/authors/zhang-lei/#person",
    // 冗余写一遍名字,便于不做实体合并的解析器兜底
    "name": "张磊",
    // 指向作者页的可访问地址
    "url": "https://example.com/authors/zhang-lei/"
  },
  // 文章主题,与作者的 knowsAbout 形成呼应
  "about": ["现场总线时延分析", "EtherCAT"],
  // 所属博客,帮助引擎理解站点结构
  "isPartOf": {
    "@type": "Blog",
    "@id": "https://example.com/blog/#blog"
  },
  // 发布时间
  "datePublished": "2026-09-18"
}

多作者的情况把 author 换成数组就行,每个元素都带自己的 @id。我们有一篇是张磊和吴桐合写的,两个人各自的 knowsAbout 交集正好覆盖了文章的两个主题,那篇在 AI 回答里带双作者名的概率明显高一些。

上线之后怎么验,不靠玄学

结构化数据这种东西,改完就想看到效果是常态,但它不像改标题那样当天就有反馈。我们做的是两件事:一是自己写脚本扫全站,二是每周固定抽样问 AI 引擎。

# 环境:Python 3.10+,无第三方依赖
# 用途:扫描构建产物,校验 Article.author 的 @id 与作者页 Person @id 是否一一对应
import json
import pathlib

# 站点构建输出目录
ROOT = pathlib.Path("./dist")
# 作者页里 Person 的统一标识,key 是作者 slug
AUTHOR_IDS = {}


def extract_blocks(html):
    # 从 HTML 里抠出所有 ld+json 脚本的内容
    out = []
    for tag in html.split('<script type="application/ld+json">')[1:]:
        # 截断到脚本结束标签
        out.append(tag.split("</script>")[0])
    return out


def main():
    # 第一轮:先收集作者页声明的 Person @id
    for f in ROOT.rglob("authors/**/index.html"):
        html = f.read_text(encoding="utf-8")
        for raw in extract_blocks(html):
            data = json.loads(raw)
            # ProfilePage 的人挂在 mainEntity 上
            if data.get("@type") == "ProfilePage":
                person = data.get("mainEntity", {})
                # 记录 slug 到 @id 的映射
                AUTHOR_IDS[f.parent.name] = person.get("@id")

    # 第二轮:校验文章页的 author 引用
    broken = []
    for f in ROOT.rglob("blog/**/index.html"):
        html = f.read_text(encoding="utf-8")
        for raw in extract_blocks(html):
            data = json.loads(raw)
            # 只处理文章类型
            if data.get("@type") not in ("Article", "TechArticle"):
                continue
            authors = data.get("author")
            # 兼容单作者与多作者两种形态
            authors = authors if isinstance(authors, list) else [authors]
            for a in authors:
                # 字符串形态直接判失败:它无法锚定到任何实体
                if isinstance(a, str):
                    broken.append((str(f), a, "author 是字符串"))
                    continue
                # @id 不在作者页集合里,说明锚点断了
                if a.get("@id") not in AUTHOR_IDS.values():
                    broken.append((str(f), a.get("name"), a.get("@id")))

    # 输出问题清单
    for item in broken:
        print("锚点异常:", item)
    print("检查文件数:", len(list(ROOT.rglob("blog/**/index.html"))))


if __name__ == "__main__":
    main()

脚本第一次跑出来 61 条异常,其中 43 条是老模板遗留下来的字符串 author,剩下 18 条是 @id 结尾少了 #person。修完再跑是 0 条。这类校验挂到 CI 里很省事,后来又有同事新加作者页忘了同步 @id,是流水线拦下来的。

抽样问引擎的部分,我们固定了 12 个问题,每周一上午同一时段问一遍,人工记录回答里有没有出现作者姓名。下面是改造前后的对比,数据是我们自己的抽样记录,不是行业统计:

观测项 改造前(第 0 周) 第 4 周 第 8 周
12 个问题中回答带作者姓名的次数 0 3 7
回答里出现「某网站」「据某厂商」的次数 9 5 2
三篇文章的实体面板被合并成同一作者的情况 0 / 3 2 / 3 3 / 3
作者页被单独引用并给出链接的次数 0 1 4
全站 @id 锚点校验异常数 61 0 0

第 4 周才出现第一次具名引用,中间那几周我们差点以为白费劲。结构化数据的生效节奏比页面改版慢,这点要有心理准备。

踩过的几个坑,按痛苦程度排序

作者页被 robots 拦了。 我们第一版作者页在 /internal/ 目录下,robots.txt 里被规则挡住,实体声明写得再漂亮引擎也读不到。作者页必须是可公开抓取的普通页面。

@id 用了相对路径。 早期写成 /authors/zhang-lei/#person,看着简洁,但相对路径在不同页面解析出来的地址不一致,反而制造了分裂。一律写全。

alumniOf 只给名字。 学校名写字符串,引擎没法判断你说的是哪个学校,归并效果等于零。带上 @id,哪怕指向的是自家实体库里的一个占位页。

knowsAbout 贪多。 我们一度给一位工程师塞了 17 个主题,结果每个主题的权重都被摊薄,反而不如精准的 5 个。这个字段是给引擎做主题匹配的,不是简历。

作者页内容太空。 结构化数据写满了,页面可见正文只有一句话,这种页面容易被判成低价值。我们后来给每位作者补了负责方向、代表项目、发表过的东西,页面正文和结构化声明对得上,可信度才立得住。

这事儿往后会更值钱

内容署名这件事,以前是给读者看的礼仪,现在多了一层机器语义。生成式引擎在回答专业问题时,越来越倾向于给出「谁说的」,因为带归属的表述更好核验,也更容易被用户信任。谁先把作者建成本地实体,谁就先把这层信任占住。

从技术上看,ProfilePage 加 knowsAbout 这套组合不复杂,难的是全站 @id 的一致性和长期维护——新人入职要建页,老人换方向要更新主题,这些都是持续的活儿,靠 CI 卡着才不会慢慢烂掉。

你们在给作者建实体时遇到过哪些怪现象,评论区聊聊,尤其是多作者署名和跨语言名字归并这两块。

参考与延伸

关键词:GEO、AI搜索、ProfilePage、knowsAbout、Schema.org、AI优化AIO、作者实体、品牌词 AI 回答

🤖
本内容由 AI 辅助生成,经人工校对审核;部分素材、资料来源于公开网络,仅作个人观点分享与交流使用,无任何商业侵权意图。若内容、图片、文字涉及您的合法著作权、版权权益,请联系本人,核实后将第一时间删除、修改相关内容。