Skip to content

필사 모드: 用 AI 搭建博客写作流水线 —— 瓶颈不是初稿,是核实

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

引言 —— 初稿三分钟,核实九十分钟

一旦开始用 AI 写文章,几乎所有人都会经历同一种错觉:初稿三分钟就能出来,那写一篇文章花的时间应该能降到十分之一。真正动手做了才发现,初稿确实三分钟搞定,之后才是九十分钟。核对数字对不对、点开每个链接看还能不能打开、比对引文是不是原文里真实存在的句子、删掉把同一句话用三种说法重复三遍的段落,就是这九十分钟在干的事。

所以设计流水线的时候,如果把初稿生成放在中心位置,这条流水线基本上就搭错了 —— 因为你自动化的根本不是瓶颈所在。真正吃时间的环节是核实,而这也是改进空间最大的地方。把生成初稿的速度提高一倍,三分钟变成一分半;把核实时间砍掉一半,九十分钟变成四十五分钟。

本文不是一份提示词合集,而是把从选题收集到复盘效果这七个阶段实际跑了一遍之后的记录,区分出模型在哪些地方确实好用、在哪些地方一旦放手就会出事。AI 写的文章会在哪些具体失败类型上崩掉、每种类型怎么检测,写在了下一篇文章里;这里看的是把这些失败过滤掉的整体流程本身。

流水线的七个阶段,以及每个阶段失败的代价

先摆出整体地图。下表把每个阶段里 AI 真正创造价值的地方,和一旦交出去就难以挽回的地方分开列出来。

阶段AI 真正擅长的事人必须亲自做的事交出去之后失败的代价
1. 选题收集广泛地铺开候选题目,标出与已发表文章的重叠判断对读者是否有价值,淘汰候选题目低。差的选题会在下一阶段被过滤掉
2. 调研与找依据扩展搜索词、总结原文、梳理相互冲突的说法确认是不是一手来源,亲自打开查看高。这里混进来的错误会一直活到最后
3. 写初稿提出结构、消除面对空白屏幕的空档、写出说明性段落的第一版决定论点与顺序,决定删掉什么中。因为看得出来,会在编辑阶段被抓住
4. 事实核查提取论断清单,辅助核对引文出处位置最终确认数字、日期、引文、链接最高。发布之后才暴露,信任就毁了
5. 编辑与统一语气检测重复段落、压缩篇幅、给出措辞候选定义语气标准本身,做最终判断中。读者会立刻察觉
6. 发布生成元数据,自动化链接与构建检查决定是否发布,做好回滚准备中。如果可回滚,代价会降低
7. 复盘效果汇总日志、计算指标、标出异常值把指标转化为对下一篇文章的决策低。但不做的话,改进就会停滞

这张表要读的不是单个格子,而是成本的分布。成本集中在第 2 步和第 4 步。这两步应该自动化,但不能在没有人签字的情况下放行;其余的步骤可以大胆交出去。流水线设计的大部分工作,其实就是守住这个分配比例。

选题与调研 —— AI 负责扩大候选,人负责淘汰

在选题收集这一步,模型擅长发散。从一个种子想法里拉出二十个角度,比人快,也不会累。反过来,它不擅长收敛。你问"这些里哪个最好",它会给出一个听起来有道理的理由,选出一个答案,但那个判断里没有包含你的读者上个月读到什么内容会失望这类信息。把它用在扩大候选上,淘汰的工作自己来做。

这里有一项实务上确实有价值的自动化:对已发表文章做重复检查。把新的选题候选和现有文章的标题、摘要做比对,标出重叠的地方,就能防止换个角度把同一个故事又写一遍这种事故。这是比对,不是判断,机器擅长干这个。

调研阶段的性质不一样。这里混进来的错误会一路活到流水线的终点,后面的每个阶段只是把它打磨得更像那么回事。所以如果只立一条规则,那就是:不要把模型总结的内容当成调研结果来用。 模型能做的,最多是告诉你该去读哪里,真正去读是人的工作。

落实成实际好操作的样子,是这样的。

  • 模型给出的每个 URL,都自己亲手打开。打不开,那条论断就整个丢弃。
  • 从二手来源(博客汇总、新闻摘要)开始没问题,但引用要换成一手来源(官方文档、论文、公告、发行说明)。
  • 没有日期的页面不用作来源。一个不知道它在什么时候是对的事实,没法核实。
  • 遇到相互矛盾的说法,不要问模型哪个是对的,而是把"存在矛盾"这个事实本身写进文章里。

最后一条在实战中特别有用。写这篇文章的姊妹篇、调研 Office 键盘快捷键的时候,就真的遇到过微软自己的官方文档对同一个操作给出不同按键的情况。这时候正确的做法不是选一个,而是写清楚"文档之间说法不一致"。

写初稿 —— 消灭空白屏幕,但不要交出结构

在写初稿这一步,模型的价值很明确。它省掉了你面对空白屏幕纠结第一句话怎么写的时间。光这一点就足够有用,而且这个阶段就算出错,代价也很低 —— 哪里不对劲会显出来,显出来就会被改掉。

但有一样东西绝不能交出去,那就是结构。把大纲交给模型,出来的几乎总是同一个模样:定义 → 优点 → 缺点 → 案例 → 结论。这个结构不算错,但它什么都没主张。读者读完一篇文章通常记住的是结构塑造出的那个顺序,而如果那个顺序是用平均值填出来的,读完之后就什么都留不下。

实务中这样分工:H2 标题和顺序自己定,每个 H2 下面要写什么用两三行写清楚,交给模型。这样模型的工作就是填段落,论点保持不变。反过来,如果只说"就这个主题写篇文章",出来的就是没有论点的文章 —— 这不是模型做不到,而是你从来没给过它论点。

写进初稿提示词里之后体感会明显不同的几条约束:

  • 让它把不知道的地方留白并标记出来。不明确说"不要瞎填",它就会填上。
  • 数字和专有名词只准使用你提供的材料里出现过的。
  • 给一个段落长度上限。没有上限,它就会把同一个意思拉长了写。
  • 每个 H2 先让它写一句"读者在这一节能学到的新东西",如果这句话和上一节重复,就把两节合并。

事实核查 —— 这条流水线的重心

这是本文最重要的一节。前六个阶段做得再粗糙,只要这一步是扎实的,文章依然值得发布;这一步要是缺了,不管其他环节做得多好,迟早会出事。

写得自信的句子和已核实的句子不是一回事

模型的输出不带任何"我有多确信"的标记。在训练数据里见过几千次的事实,和听起来挺像那么回事、其实是刚刚编出来的句子,会用完全相同的语气说出来。人写东西的时候,没把握就会不自觉地在措辞上露出犹豫,我们也会下意识地读出这个信号;模型的文字里没有这个信号。所以"读起来挺自然"根本不构成核实 —— 反而越自然的地方越危险。

需要特别留意的项目是固定的:数字、日期、版本号、价格、引文、人名与所属机构,还有 URL。这些恰恰是模型最容易编得像模像样的项目,同时也是读者最容易验证的项目。哪怕其余的句子全对,这里面只要错一处,整篇文章的可信度都会跟着掉下去。

知识截止日期也在同一个地方惹麻烦。模型不会说"这方面我不知道最新情况",而是把自己知道的最后状态用现在时说出来。价格、套餐、产品名称、API 参数、政策措辞这类经常变动的内容,模型给出的答案只能当作起点,必须回到当前文档重新核实一遍。

让模型只引用它真正读过的内容

你要求"加上出处",它就会加上出处。问题在于,那个出处可能只是"听起来像"它读过的地方,而不是它真正读过的地方。URL 的格式很容易学会,所以一个根本不存在的文档编号、一个失效的锚点,都能被生成得看起来天衣无缝。

防住这个问题的办法不是改变要求本身,而是强制让输出变成可核实的形式。关键在于逐字引用。让模型照抄原文的原始文字,而不是做总结,这样机器才能核对那段字符串是不是真的出现在原文里。

对每一条论断,都要求它同时给出这四样东西:

  1. 论断句本身
  2. 出处 URL
  3. 从那个 URL 的正文里逐字摘出的一句以上引文
  4. 一句话说明为什么这段引文能支撑这条论断

第 3 项是自动核实的关键。如果引文不在那个页面上,这条论断就是无凭无据编出来的,脚本能在人去读它之前先把它抓出来。下面是完成这项核对的最小实现。

// verify-claims.mjs —— 核对论断清单(claims.jsonl)里的逐字引文是否真的出现在原文中。
// 每行格式: {"claim": "...", "url": "https://...", "quote": "...原文逐字...", "why": "..."}
import { readFileSync } from 'node:fs'

// 空格、引号、大小写的差异不算引用失败,只要抓住内容真正不同的情况。
const normalize = (s) =>
  s
    .replace(/<[^>]+>/g, ' ')
    .replace(/&[a-z]+;|&#\d+;/gi, ' ')
    .replace(/[‘’“”]/g, "'")
    .replace(/\s+/g, ' ')
    .trim()
    .toLowerCase()

const rows = readFileSync('claims.jsonl', 'utf8')
  .split('\n')
  .filter(Boolean)
  .map((l) => JSON.parse(l))

const cache = new Map()
let failed = 0

for (const [i, row] of rows.entries()) {
  const label = `[${i + 1}] ${row.claim.slice(0, 50)}`

  if (!row.url || !row.quote) {
    console.error(`MISSING ${label} —— url 或 quote 为空`)
    failed++
    continue
  }

  if (!cache.has(row.url)) {
    try {
      const res = await fetch(row.url, { redirect: 'follow' })
      // 死链接在核对引文之前就已经算失败了。
      cache.set(row.url, res.ok ? normalize(await res.text()) : null)
    } catch {
      cache.set(row.url, null)
    }
  }

  const page = cache.get(row.url)
  if (page === null) {
    console.error(`DEAD    ${label} —— 无法打开 ${row.url}`)
    failed++
  } else if (!page.includes(normalize(row.quote))) {
    // 这就是这个脚本存在的意义: 抓出那些听起来像真的、但原文里根本没有的引文。
    console.error(`NOQUOTE ${label} —— 原文中找不到该引文: "${row.quote.slice(0, 60)}"`)
    failed++
  }
}

console.log(`\n共检查 ${rows.length} 条,失败 ${failed}`)
process.exit(failed ? 1 : 0)

也要说清楚这个脚本抓不住什么。哪怕引文一字不差地存在于原文里,如果是被掐头去尾、脱离上下文的引用,它照样会放行;引文本身没错,但根本支撑不了那条论断的情况,它也会放行。所以第 4 项(一句话理由)依然需要人来读。不过一旦机器先在前端过滤一遍,人要读的量会大幅减少。实际接入这套核对之后,人工核实的时间明显变短了 —— 因为不用再为了找出一条编造的引文而把原文通读一遍。

发布前检查清单

下面是按下发布按钮之前实际会跑一遍的清单,分成机器能做的和人必须做的。

项目确认方法是否可自动化
所有外部链接是否能打开检查响应码可以
引文是否逐字存在于原文中逐字核对脚本可以
内部链接是否指向真实路径检查文件是否存在可以
数字与日期是否与出处一致与原文肉眼核对不可以
是否存在依赖时效的表述搜索"最新""目前""今年"等表达后判断部分可以
引文是否扭曲了上下文阅读原文引文前后的段落不可以
是否存在无依据的断言搜索断言性的措辞后核实依据部分可以
是否有重复表达同一内容的段落测量段落相似度后判断部分可以
我能不能讲清楚这篇文章不看原文,试着说出要点不可以

最后一项看起来像走过场,实际上过滤出的问题最多。如果不看文章,试着说出核心论点时卡住了,那说明那部分不是被核实过,而只是被放行了而已。

编辑、发布、复盘效果 —— 统一语气,保持可回滚

在编辑这一步,能交给模型的是检测,不能交给它的是标准。"这篇文章是不是我的声音",模型判断不了。因为它倾向于收敛到训练数据里那种平均的文字质感,一旦交出去,句子会变得顺滑,但文章就不再是你的了。

反过来,如果把语气写成规则,机器就能检查。比如不用的词汇清单、段落长度上限、禁止感叹式表达、禁止过度使用某个连接词。这些不是品味,是可检查的条件,所以能自动化。相反,"帮我改得更自然一点"不是可检查的条件,而是把品味外包了出去。

在发布这一步,重要的不是速度,而是能不能回滚。文章和代码不一样,发布之后暴露出的错误没法悄无声息地回滚 —— 已经有人读过了,有时候还已经被别处引用了。所以哪怕把发布本身自动化,也最好先把回滚路径搭好。提前定好一条规则,把更正记录留在文章内部,可以避免出事之后想悄悄改掉的诱惑。

在复盘效果这一步,诀窍是把要看的指标缩减。浏览量不会改变下一篇文章。真正能改变决策的是停留时长的分布(有多大比例的人在第一屏就跳出)、搜索流量的查询词和文章标题之间的落差,以及滚动在哪一节停下来。尤其是搜索查询词和内容之间的落差,会直接告诉你下一篇文章该写什么,如果只看一项指标,推荐看这个。

这里有必要顺带说一下 Google 的政策。Google 明确表示不会因为内容是用 AI 制作的就给予不利对待,判断标准不是生成手段,而是是否给读者带来价值,这一点写在了它的搜索中心文档里。但是,以冲排名为目的、批量生产没有价值的内容,在它的垃圾内容政策里被明确列为规模化内容滥用而禁止。总结一下:一旦把流水线的目标定成"多产出",就已经站到了政策的对立面。

自动化流程图 —— 保留了可以停下来的节点的工作流

把整体拼在一起,大致是下面这个样子。重要的不是自动化的范围,而是在哪里停下来。这条工作流不会让初稿在没通过核实门禁的情况下进入发布阶段,也不会在没有人批准的情况下让初稿碰到发布分支。

# .github/workflows/content-gate.yml
# 初稿的 PR 一打开,就把机器能做的核实全部跑一遍,但即使全部通过,也不会自动合并。
name: content-gate

on:
  pull_request:
    paths: ['data/blog/**/*.mdx']

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'

      - name: 构建安全性 —— MDX 是否能真正编译
        run: node scripts/check-posts.mjs

      - name: 内部链接 —— 指向的路径是否存在
        run: node scripts/check-links.mjs

      - name: 逐字引用核对 —— 引文是否真实存在于原文
        run: node scripts/verify-claims.mjs

      - name: 时效性表述 —— 只标记出需要人来判断的地方
        run: |
          # 不会让构建失败。这是引导评审的信号,不是核实本身。
          grep -nE '最新|目前|今年|去年|最近|最快|业内首个' data/blog/**/*.mdx \
            | tee /tmp/time-sensitive.txt || true
          echo "以上表达可能是知识截止日期之后才成立的事实,评审者需要亲自确认。"

      - name: 段落重复 —— 只抽取相似度最高的几对并报告
        run: node scripts/find-duplicate-paragraphs.mjs --top 5 || true

  # 人工介入节点: 上面的检查即便全部为绿色,也在这里停下来。
  # 批准来自 GitHub 的评审批准,工作流本身不会代替人批准。

这里有两件事是刻意不做的。

第一,没有把核实通过当作发布的触发条件。 机器核实最多只能告诉你"没有明显的事故",说不出"可以发布了"。一旦接上自动合并,检查清单最后三项(上下文是否被扭曲、断言是否有依据、能不能讲清楚这篇文章)就会从流水线里消失。

第二,没有把时效性表述检查设成会导致失败的检查。 这项检查的精确度低,一旦设成硬性失败,很快就会被无视,而一个被无视的门禁比没有门禁还糟。只让它起到告诉人该看哪里的作用,反而能用得更久。

结语 —— 值得自动化的是判断,不是生成

搭建 AI 内容流水线的时候,人的手总是先伸向生成,因为结果看得见,一动手就有种立刻变快了的感觉。但真正把时间还给你、把事故挡在外面的,是判断这一侧。让机器先过滤一遍引文是不是真的、链接是不是活的、有没有说了车轱辘话,人就只需要把时间花在机器做不到的事情上。

而机器做不到的事情,比想象中要少、要清楚:引文有没有扭曲上下文,这个选题对读者有没有价值,以及这篇文章的论点到底是不是你自己的。这三样从头到尾都是人的活儿,如果流水线搭得好,剩下的时间就应该刚好够花在这三样上。

浓缩成一句话就是 —— 不是不能相信初稿,而是没核实过的初稿,压根没有值得相信的理由。

参考资料

현재 단락 (1/144)

一旦开始用 AI 写文章,几乎所有人都会经历同一种错觉:初稿三分钟就能出来,那写一篇文章花的时间应该能降到十分之一。真正动手做了才发现,初稿确实三分钟搞定,之后才是九十分钟。核对数字对不对、点开每个链...

작성 글자: 0원문 글자: 7,701작성 단락: 0/144