人工智能实践(语言智能)
第9讲:GEO

实验9:在秘塔搜索上做一次 GEO 小型实证

选择可观测中文入口、对同一主题起 20 个 query、记录引用源、按 GEO 策略改写自己的博文后再测

实验概述

本实验将完成你的第一次 GEO 实证——在可观测的中文生成式搜索入口上测量自己内容的被引率,按 GEO 策略改写后再次测量,并分析策略收益。

项目详情
目标引擎秘塔搜索(metaso.cn)为主;可选 DeepSeek Web 作对照
主题选择自由——建议选你已有博文或学术笔记的主题
查询规模20 个同主题变体 query
核心任务基线测量 → 选定策略改写 → 冷却 7 天 → 再测 → 对比分析
预计时间首轮约 100 分钟(含数据整理),二轮 + 分析 30 分钟

冷却期提示:本实验第二轮测量需要在改写发布后至少 7 天进行。建议第一课时完成基线测量 + 改写发布,第二课时(下周)做后测和分析。请合理安排课程时间线

实验步骤

步骤 1:选定主题与查询集(15 分钟)

主题选取原则

  • 你已有一篇(或可写一篇)中文博文 / 笔记的主题
  • 主题要足够具体(不要是"Python 入门"这种红海),但也要有一定搜索量
  • 推荐方向:技术工具教程、某领域概念解释、一个小众学术主题、一个地方/校园办事流程

示例主题

  • "LoRA 在 Qwen3 微调中的实际显存占用"
  • "DITA 结构化写作 vs Markdown 的可维护性"
  • "北大研究生学位论文 LaTeX 模板配置"

查询集设计(20 条):

queries = [
    # 5 条直接查询(与你标题高度相关)
    "LoRA 微调 Qwen3 显存占用",
    "LoRA r=32 和 r=8 显存差多少",
    ...
    # 5 条同义变体(不同措辞表达相同意图)
    "Qwen3 LoRA 训练需要多少显存",
    ...
    # 5 条长尾查询(更具体的子问题)
    "LoRA alpha 32 显存会怎么变化",
    ...
    # 5 条横向对比查询(与你内容竞争但不完全重合)
    "LoRA vs QLoRA 显存对比",
    ...
]
assert len(queries) == 20

把 queries 保存到 queries.json,后续每次实验复用同一份。

步骤 2:基线测量(25 分钟)

手动或半自动测量每条 query 在秘塔搜索上的表现。

测量脚本(半自动推荐)

import json
from datetime import datetime
from pathlib import Path

# ===== 1. 加载查询集 =====
queries = json.loads(Path("queries.json").read_text(encoding="utf-8"))

# ===== 2. 记录模板 =====
def record_query(query: str, engine: str = "metaso") -> dict:
    """
    对每条 query,在秘塔搜索上手动查询后填写以下字段。
    建议每条 query 重复 3 次取最稳定的结果。
    """
    return {
        "query": query,
        "engine": engine,
        "timestamp": datetime.now().isoformat(),
        "answer_text": "",        # 粘贴秘塔完整答案文本
        "all_cited_urls": [],     # 答案中所有编号引用的 URL 列表
        "my_url": "",             # 你自己博文的 URL
        "my_cited": False,        # 是否被引用
        "my_rank": None,          # 被引用时的编号位次(1-based;未引用填 None)
        "my_word_count": 0,       # 你内容中有多少词元出现在答案里(人工估计或脚本计算)
        "competitor_cited": [],   # 竞争内容被引的 URL
        "notes": "",              # 主观观察
    }

# ===== 3. 为每条 query 手动填写 =====
baseline_records = []
for q in queries:
    for repeat in range(3):       # 每条重复 3 次
        rec = record_query(q)
        # ... 手动填入 ...
        baseline_records.append(rec)

Path("baseline.json").write_text(
    json.dumps(baseline_records, ensure_ascii=False, indent=2),
    encoding="utf-8"
)

位置加权词数的计算

def position_adjusted_wc(answer_text: str, my_content_tokens: list[str]) -> float:
    """
    统计 my_content_tokens 中有多少出现在 answer_text 中,
    并按位置倒数加权。
    """
    answer_tokens = answer_text.split()  # 简化:按空格切;中文可用 jieba
    pawc = 0.0
    for my_tok in my_content_tokens:
        if my_tok in answer_tokens:
            pos = answer_tokens.index(my_tok) + 1  # 1-based
            pawc += 1.0 / pos
    return pawc

聚合统计

import statistics

def summarize(records):
    grouped = {}
    for r in records:
        grouped.setdefault(r["query"], []).append(r)

    summary = {}
    for q, runs in grouped.items():
        cited = [r["my_cited"] for r in runs]
        ranks = [r["my_rank"] for r in runs if r["my_rank"] is not None]
        pawc = [r.get("pawc", 0) for r in runs]
        summary[q] = {
            "cite_rate": sum(cited) / len(cited),
            "mean_rank": statistics.mean(ranks) if ranks else None,
            "mean_pawc": statistics.mean(pawc),
        }
    return summary

baseline_summary = summarize(baseline_records)

记录到此时的关键问题:

  1. 在 20 条 query 中,你的内容被引多少次?(命中率)
  2. 被引时平均位次多少?
  3. 哪些 query 从未命中?为什么?
  4. 竞争内容(被频繁引用的其它 URL)有什么共同特征?

步骤 3:策略选择与改写(30 分钟)

根据基线分析,从 9.2 节策略表中选 1 条进行单变量干预。推荐优先级:

适用:你的博文偏"观点 + 叙述"型,缺乏具体引用和数据。

改写清单

  • 每个主要论断后加至少 1 条权威引用(论文、官方文档、权威媒体)
  • 至少插入 2—3 处直接引语("根据 X 的研究……")
  • 补充具体数字(百分比、年份、样本量)

改写强度指标

  • 原文命题句密度:约 1.2 条 / 段 → 目标 ≥ 2 条 / 段
  • 原文引用密度:< 1 条 / 1000 字 → 目标 ≥ 3 条 / 1000 字

适用:你的博文偏"大段散文",段落过长、缺少小标题。

改写清单

  • 段落长度调整至 150—300 词(偏长的切分)
  • 每个 H2 下至少 2 个 H3
  • 格式多样元素(列表 / 表格 / 代码块 / 引用块)占 25%—35%
  • 核心关键词放在段首 30% 的位置

改写前后可量化对比

def structural_features(markdown_text: str) -> dict:
    lines = markdown_text.split("\n")
    return {
        "n_h2": sum(1 for l in lines if l.startswith("## ")),
        "n_h3": sum(1 for l in lines if l.startswith("### ")),
        "n_list_items": sum(1 for l in lines if l.startswith(("- ", "* ", "1. "))),
        "n_code_blocks": markdown_text.count(chr(96) * 3) // 2,
        "avg_para_len": statistics.mean([len(p) for p in markdown_text.split("\n\n") if p.strip()]),
    }

适用:有预算或人脉资源;不适合纯学生练习。可作为分析题讨论。

  • 在相关技术社区(知乎 / CSDN / 掘金 / SegmentFault)发布导向你博文的深度帖
  • 请同学互为评测博客——你写他的评测,他写你的
  • 把博文内容改写为可投稿的文章,投给相关公众号 / 行业媒体

本方案不做实测,只做策略讨论与计划。

发布改写版本

  • 新建 URL(如 post-v2 或在同 URL 上更新)
  • 记录发布时间,开始冷却期(≥ 7 天)
  • 在改写前后各自提交一次 sitemap 让搜索引擎重新索引

步骤 4:冷却期观察(同步做其它课程,7 天后返回)

冷却期做以下事情:

  • 每 2 天快速查 3—5 条 query,观察引用生态是否已更新
  • 记录观察日志(主观)
  • 如果发现改写后内容在 3 天内就被引,说明引擎已重建索引

冷却期可选的并行阅读:Aggarwal 等人(2024)GEO 原始论文的 Section 4 实验方法部分、Puerto 等人(2025)C-SEO Bench 的负面发现。对比他们如何处理"非确定性"和"混淆变量"。

步骤 5:后测与对比分析(25 分钟)

重复步骤 2 的测量流程,保存到 post.json。然后做对比:

def compare(baseline_summary, post_summary):
    comparison = []
    for q in baseline_summary:
        b = baseline_summary[q]
        p = post_summary.get(q, {})
        comparison.append({
            "query": q,
            "cite_rate_delta": p.get("cite_rate", 0) - b["cite_rate"],
            "rank_delta": (p.get("mean_rank") or 99) - (b["mean_rank"] or 99),
            "pawc_delta": p.get("mean_pawc", 0) - b["mean_pawc"],
        })
    return comparison

comparison = compare(baseline_summary, post_summary)

# 统计检验
from scipy import stats
cite_rate_pre = [baseline_summary[q]["cite_rate"] for q in baseline_summary]
cite_rate_post = [post_summary[q]["cite_rate"] for q in post_summary]
t_stat, p_value = stats.ttest_rel(cite_rate_pre, cite_rate_post)
print(f"paired t-test: t={t_stat:.3f}, p={p_value:.4f}")

可视化

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# 被引率分布
axes[0].hist([c["cite_rate_delta"] for c in comparison], bins=10)
axes[0].set_title("Cite Rate Delta per Query")
axes[0].axvline(0, color='red', linestyle='--')

# 位次变化
axes[1].hist([c["rank_delta"] for c in comparison], bins=10)
axes[1].set_title("Rank Delta per Query")

# PAWC 变化
axes[2].hist([c["pawc_delta"] for c in comparison], bins=10)
axes[2].set_title("PAWC Delta per Query")

plt.tight_layout()
plt.savefig("geo_experiment_result.png", dpi=150)
plt.show()

步骤 6:写 1 页分析报告(10 分钟)

必答

  1. 你选择的策略是什么?为什么?(基于基线分析)
  2. 20 条 query 上的平均被引率变化如何?统计显著吗?
  3. 哪些 query 上策略最有效?哪些上无效或反向? 尝试给出机制解释。
  4. 位置加权词数 PAWC 的变化比被引率更大还是更小? 说明什么?
  5. 有没有观察到"事后合理化"现象?(答案似乎和你的改写一致,但没 cite 你)

选答(加分项):

  1. 对比秘塔与 DeepSeek 上的同一主题结果,讨论中文入口差异
  2. 用反事实扰动(把你的页面内容替换成无关文本)验证"真实引用 vs 合理化引用"
  3. 讨论这条策略如果长期应用是否会产生 Hu (2025) 所说的"军备竞赛"动态

常见陷阱与调试

现象原因对策
改写后被引率反而下降可能触发了 Keyword Stuffing 陷阱检查关键词密度;看改写是否破坏了语义自然度
结果高度不稳定重复次数不够,或 query 过于热点每 query 重复 ≥ 3 次;避开热点话题
第二轮测不到 cite冷却期不够或 URL 未被重新抓取在搜索引擎站长后台提交 URL;延长冷却期
秘塔搜索不支持 API需要手动查询用 Playwright 做半自动化;或减少到 10 条 query
部分 query 引用全是百科类内容你的内容不在 "权威生态"中考虑方案 C(赢得媒体)作为长期计划

交付物清单

完成实验后,请提交以下内容:

  • queries.json(20 条查询)
  • baseline.json(基线 60 条记录 = 20 query × 3 次)
  • post.json(后测 60 条记录)
  • 改写前后的博文 URL(或 markdown 快照)
  • geo_experiment_result.png(三张 delta 分布图)
  • 1 页分析报告(必答 5 题 + 选答至少 1 题)

时间预估

  • 查询集与基线测量:~40 分钟
  • 策略选择与改写:~30 分钟
  • 冷却期:7 天(异步)
  • 后测与对比:~25 分钟
  • 分析报告:~10 分钟
  • 总计(净实操):约 100—110 分钟

加分项:跨引擎对照

有余力的同学可以把同一份查询集在 秘塔 + DeepSeek Web + Perplexity(中文 query) 三个入口上各做一轮,对比:

  1. 同一策略在不同引擎上的收益差异
  2. 引用来源生态的差异(印证 Chen et al. 2025 的"语言生态切换")
  3. 针对 Integrated 架构(DeepSeek)与 Iterative 架构(秘塔)的最优策略是否不同

这类对照实验有希望产出一份可公开发表的小型研究报告,推荐作为期末项目深化方向。