[控场AI]
· 10 分钟阅读· 5,075 字

让网页在RAG分块中存活:面向AI引用的写作策略

让网页在RAG分块中存活:面向AI引用的写作策略

AI搜索以段落为引用单元,需按分块逻辑重写页面以赢得引用。

文章指出,Perplexity、ChatGPT搜索等AI搜索系统并不整页阅读内容,而是通过RAG检索管道将页面拆分成段落块,经嵌入、关键词打分和重排后,只把少数几个块交给模型生成答案,因此赢得或失去引用的单元是段落而非整个页面。作者提供了一个Python脚本,在H2/H3边界处模拟标题感知分块并按大小截断,再配合一组静态检查规则(反向指代开头、模糊标题、块过短、缺少数字等)找出薄弱段落。修复方向包括:让每块自包含、先答后述、把标题写成查询句、用表格做比较、控制章节大小。此外建议用JSON-LD固定事实(须与可见页面一致、经过验证、服务端渲染),并对llms.txt保持理性预期。最终以引用率为指标,逐页群验证效果。

当Perplexity、ChatGPT搜索或Google的AI功能回答问题时,它们并不像人类那样阅读你的页面。检索管道会将页面拆分成段落,根据查询对这些段落进行排序,然后将其中几个交给模型。模型再基于这些段落来撰写答案。

这意味着赢得或失去引用的单元是段落(passage),而非整个页面。一个页面整体可能非常优秀,但如果其最佳答案被拆分到三个块中,或者以"如上所述,它……"开头,它就仍可能失去引用。

外界没人确切知道这些公司的具体分块方式。但通过开源RAG系统,其一般机制已被充分理解。你可以模拟得足够好,从而找出薄弱的段落。

典型检索器如何看待你的页面

一个常见的管道流程是:

  1. 抓取HTML,剥离导航和样板内容。
  2. 按结构(标题)拆分,再按大小(几百个token)拆分,并带有一些重叠。
  3. 对每个块进行嵌入,并且通常还会用关键词搜索(BM25)对其进行评分。
  4. 根据查询对顶部候选块进行重新排序。
  5. 将前几个块及其来源URL传递给模型。

检索管道:抓取、分块、索引、重排、生成

步骤2和步骤4是页面结构发挥关键作用的地方。一个块如果包含措辞类似问题的标题,紧接着一个直接答案,那么它就既易于匹配,又易于引用。

什么是RAG? RAG(Retrieval-Augmented Generation,检索增强生成)是大语言模型应用的主流架构:模型在生成答案前,先由一个检索器从外部语料库中取回相关文本片段,再把这些片段作为上下文喂给模型。它解决的是模型参数中知识过时、无法覆盖私有内容的问题。典型RAG由两大部分组成——检索侧(抓取、分块、嵌入、索引、重排)和生成侧(拼接上下文、撰写答案)。Perplexity、ChatGPT搜索这类产品本质上就是面向公网的RAG系统,因此页面能否被引用,取决于它在检索侧的每个环节是否"好处理",而分块(chunking)与重排(reranking)正是其中最容易因页面结构而失分的一环。

BM25与向量嵌入的分工。 向量嵌入把文本映射为高维向量,擅长捕捉语义相近但用词不同的匹配(例如"费用"与"定价");BM25则是一种经典的词频—逆文档频率关键词打分算法,擅长精确命中原词、专有名词和数字。两者互补,因此许多检索系统会并行运行这两条路径,再合并结果,即所谓"混合检索"。对内容创作者的含义很直接:标题和首句里同时出现查询者可能使用的语义表达("多少钱""价格")与精确词汇("Starter计划""49美元"),能在两种召回路径上都获得信号,比只依赖其中一种更稳。

什么是重排(Rerank)? 检索的第一阶段通常追求快和召回广,会取回几十上百个候选块;重排是第二阶段,用一个更慢但更精确的模型(常见的是交叉编码器cross-encoder)逐对评估"查询—段落"的相关性,再从中挑出前几个交给生成模型。交叉编码器能同时看到查询和段落的完整文本,因此对段落内部结构极其敏感:一个自身语义完整、首句即回答问题的块,往往比一个需要依赖上下文才能理解的块得分高得多。这也解释了为什么在重排阶段,反向指代("如前所述")和模糊标题特别致命——它们让块在被孤立评估时丢失关键信息。

模拟分块过程

以下代码在H2/H3边界处拆分页面,然后限制块大小,这大致是许多感知标题的拆分器所做的:

import re, requests
from bs4 import BeautifulSoup

MAX_WORDS = 220

def chunks(url: str) -> list[dict]:
    soup = BeautifulSoup(requests.get(url, timeout=20).text, "html.parser")
    for t in soup(["script", "style", "nav", "footer", "header", "aside", "noscript"]):
        t.decompose()
    main = soup.find("main") or soup.find("article") or soup.body

    out, heading, buf = [], "(intro)", []
    def flush():
        words = " ".join(buf).split()
        for i in range(0, len(words), MAX_WORDS):
            out.append({"heading": heading, "text": " ".join(words[i:i + MAX_WORDS])})
        buf.clear()

    for el in main.find_all(["h1", "h2", "h3", "p", "li", "td"]):
        if el.name in ("h1", "h2", "h3"):
            flush()
            heading = el.get_text(" ", strip=True)
        else:
            buf.append(el.get_text(" ", strip=True))
    flush()
    return out

打印你某个关键页面的分块,并孤立地阅读它们,就像每个块是模型唯一获得的内容。仅这个练习往往就足以让人大开眼界。

对分块进行静态检查

现在将阅读过程自动化。以下每项检查都会标记出一个可能输给更干净段落的段落:

DANGLING = re.compile(r"^(this|that|these|those|it|they|as (mentioned|noted|discussed)|see above|the above)\b", re.I)
VAGUE_HEADINGS = {"overview", "introduction", "details", "more", "getting started", "our approach", "why us"}

def lint(c: dict) -> list[str]:
    issues = []
    words = c["text"].split()
    first = " ".join(words[:40])

    if DANGLING.match(c["text"]):
        issues.append("以反向引用开头;该块无法独立存在")
    if c["heading"].lower().strip(" :") in VAGUE_HEADINGS:
        issues.append(f"模糊标题 '{c['heading']}' 不携带查询信号")
    if len(words) < 40:
        issues.append("非常短;可能无法包含完整答案")
    if not re.search(r"\d", c["text"]):
        issues.append("没有具体数字")
    if c["heading"].endswith("?") and len(first.split(".")[0].split()) > 35:
        issues.append("疑问句标题,但第一句话太长;答案可能被埋没")
    return issues

这些只是启发式规则。"没有数字"并非每个块的缺陷。将此输出用作阅读清单,而非评分。

修复模式

自包含的答案。 重复主语,而不是使用代词。第一句话即使被单独引用,也应能独立成立。

- ## 定价
- 起价为49美元,包含上述所有内容。
+ ## Acme Analytics 的费用是多少?
+ Acme Analytics 的 Starter 计划为每月49美元,最多5个席位,
+ Growth 计划为每月199美元,最多25个席位。两者均包含无限事件。

先答后述。 让每个部分以一两句直接回答其标题的句子开头,然后再展开。检索器根据块的完整内容对其进行排序,但模型会引用最容易提取的内容。

将标题作为查询。 "Acme 与 Mixpanel 相比如何?"能匹配比较类查询。"Acme 的不同之处"则不能。

用表格进行比较。 比较表格能清晰地分块,模型也能准确地引用它。而埋在四段散文中的比较则两者都做不到。

保持合理的章节大小。 一个H2下1500字的章节会被从中间拆开。在自然边界处用H3将其断开。

用JSON-LD固定事实

散文是模糊的。结构化数据则明确陈述事实:谁制作了它、费用是多少、何时更新。搜索引擎大量使用它,而拥有干净、一致事实的页面更不容易被错误引用。

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "SoftwareApplication",
  "name": "Acme Analytics",
  "offers": [
    { "@type": "Offer", "name": "Starter", "price": "49", "priceCurrency": "USD" },
    { "@type": "Offer", "name": "Growth",  "price": "199", "priceCurrency": "USD" }
  ]
}
</script>

比选择哪种类型更重要的规则:

  • 与可见页面匹配。 如果JSON-LD写的是49美元而页面写的是59美元,你就为模型制造了一个需要解决的矛盾,而它可能解决错误。
  • 验证它。 通过Schema.org验证器或Google的富媒体结果测试运行它。无效的JSON会被静默忽略。
  • 服务端渲染。 由客户端JavaScript注入的JSON-LD与任何其他客户端渲染内容具有相同的可见性问题。

为什么结构化数据会影响引用。 JSON-LD是Schema.org词汇表推荐的序列化格式,它以机器可读的方式声明页面上的实体及其属性:产品名称、报价、货币、更新时间、作者等。搜索引擎和部分AI系统会据此构建知识图谱,并用于消歧与事实核验。当页面的散文表述与结构化数据不一致时,系统就需要在多个候选值之间做判断,而它未必总选对——这正是文中强调"与可见页面匹配"的原因。另一个常见陷阱是类型选择错误:把软件产品标成Product而非SoftwareApplication,或把FAQ内容写成Article,都会让属性无法被正确解析。验证环节不可跳过,因为语法错误的JSON-LD会被解析器静默丢弃,页面上看不出任何异常,但所有结构化收益都已归零。

llms.txt 呢?

llms.txt 是一个提议的约定:在网站根目录放置一个Markdown文件,概述站点并链接到关键页面的LLM友好版本。添加它成本低廉,可以帮助读取它的工具,例如一些编码助手和处理文档的代理。Google已表示不将其用于搜索,也没有任何主要AI搜索引擎确认依赖它进行排名或引用。如果容易添加就添加,但不要将其视为上述段落级工作的替代品。

衡量是否有效

结构性重写是假设。在重写之前,记录页面的基线,发布更改,并在接下来的几周内观察该页面所针对的提示词的引用率。一次只重写一个页面集群,以便归因变化。

简短版本:把每个部分都写成模型唯一能看到的段落,因为通常它确实是。

#RAG分块#AI引用优化#生成引擎优化#结构化数据#JSON-LD#段落检索#AI搜索可见性#llms.txt
分享:

相关推荐