🤖 第四章 应用实战:今日头条批量发布助手
📄 一句话概括

Agent 这一侧要交出来的东西,不是【一篇好文章】,而是一批【能被机器接住、且彼此不像】的稿子。所以本节的工作流里有一个别处没有的节点——差异化节点:在写之前,先给每一篇分配一个互不相同的角度、人称、开头方式和结构。 这个节点是批量的命门:没有它,你产出的就是 4.1 公告里点名的【模板化拼装】。

📝 本节和前面几节的关系

4.2 的任务表里有【出稿】一格,4.3.1 造好了写稿的那个智能体,本节是把它们接成一条【一次出一批】的工作流。它的产物是一份契约 JSON,下一节(4.4.1)提交给 Agent,4.4.2 再从里面把结果收回来。

一、先定输出:Agent 到底要交什么

批量场景下,输出格式比输出质量更重要——因为格式错了下游会直接卡死,质量差下游还能靠人补。

所以本节先做一件事:把 Agent 的输出写成一份【契约】。契约里每一格对应 4.2 任务表里的一格:

契约字段 对应任务表 谁用它
id batch_id + seq 全流程的身份证
topic topic 人看
angle / persona (不落在表里,但决定质量) 差异化节点的产出
title 标题框 4.4.4 填进后台
lead (摘要用) 4.4.4
body 正文框 4.4.3 排版、4.4.4 上传
tags 标签框 4.4.4
cover_prompt 封面图 4.4.3 生成、4.4.4 上传
ai_flag ai_flag 4.4.4 的 AI 声明

一句话:契约里有几格,后面就有几步要填。 这是【字段驱动流程】——先定字段,再定步骤;字段变了,步骤跟着变。

二、头条的内容特点,决定了几个字段的具体形状

同一个【出稿】动作,放到不同平台上字段的形状完全不同。头条这一侧的关键量:

项 头条的量 落在契约里
标题 建议 18 到 28 字(超过 30 字在信息流里会被截断) title:上限 30,目标 20 左右
正文 平台底线是 100 字(首发要求正文不少于 100 字);多数教程建议 800 到 1200 字 body:硬门槛 100,建议 800 以上
配图 3 到 5 张 cover_prompt(+正文图提示词)
标签 3 到 5 个 tags
封面 1 图或 3 图;电脑端推荐 1080×608 像素,比例 1:1 或 16:9 cover_prompt 里带上比例
分类 必须选对垂直领域 category(由人定,不由模型定——见第七节)
AI 声明 图文在标题下方或正文开头标注【含 AI 生成内容】 ai_flag
📝 注意【底线】和【建议】是两回事

【正文不少于 100 字】是平台硬规则,违反会被拦——这是阻断级; 【正文 800 到 1200 字】是经验建议,写 400 字也不会违规,只是不容易被推荐——这是提示级。 把这两级分开,是本节 Demo 的第一个设计要点。 机器负责守在【底线】上,人负责决定要不要往【建议】靠。

三、工作流的形状:一次一批,而不是一次一篇

把 4.3.1 那份【设定】接进来,工作流长这样:

输入:主题批次 [T01..T06] + 差异配方表
  │
  ├─ 节点 1  主题拆解    每个主题 → 2~3 个可选角度
  ├─ 节点 2  差异分配    ★ 给每篇锁定:角度 / 人称 / 开头方式 / 结构
  ├─ 节点 3  标题候选    每篇 3 条候选,人工或规则选 1
  ├─ 节点 4  导语 + 正文 按锁定的配方写
  ├─ 节点 5  标签 + 封面提示词
  │
  └─ 输出:契约 JSON 数组(6 篇)

节点 2 是本节新增的那个——前两章的 Agent 只做【一篇】,不需要管差异;批量之后,差异必须被【分配】,不能被【期望】。

四、差异化节点:批量的命门

为什么必须【分配】,不能让模型自己发挥

因为模型的默认行为是趋同。 你把六个主题一次性丢给同一个模型、用同一段提示词,得到的结果大概率是:

这不是模型的缺陷,是它的本性——它学习的就是【最常见、最稳妥的写法】。所以批量场景下,差异不能靠它自觉,必须由流程指定。

四个差异维度(一张配方表)

维度 可选值(示例) 作用
角度 做减法 / 分区 / 预算 / 断舍离 / 硬件 / 时间成本 决定这篇写什么,是最大的差异来源
人称 第一人称 / 第二人称 / 第三人称 决定叙述视角
开头方式 场景白描 / 反问 / 数据 / 结论前置 决定第一句长什么样
结构 清单式 / 时间线 / 对比式 / 问题—解决 决定全文骨架

给一批 6 篇分配时,只要保证【同一批次里,没有任何两篇的四个维度全部相同】——同质化的风险就基本解除了。

⚠️ 这一条直接对应 4.1 的红线一

头条 2026-09-15 的 AI 低质内容公告,第一类被点名的就是【模板化拼装、套路化表达——批量套用固定标题、人物设定、情节结构和语言模板】,并且公告明确:通过矩阵账号、更换题材人物、替换表达模板规避治理的,也从严处置。 差异化节点就是对着这一条设计的。 它不是一个【锦上添花】的优化项,它是这批内容能不能发出去的准入条件。

五、动手 Demo:批量契约校验 + 同质化检测

下面这段代码只用 Python 标准库。它做两件事:第一件按【阻断 / 提示】两档体检每一篇的硬规则,第二件用四种方法检测整批的同质化(标题相似度、开头重复、角度人称同款、角度复用)。

# -*- coding: utf-8 -*-
"""4.3.2 Demo:批量内容契约校验 + 同质化检测(纯标准库)
   分两档:阻断(平台的硬底线,机器可判定)与提示(业务建议,交给人判断)"""
import json
import itertools

RAW = r"""
[
 {"id":"T01","topic":"玄关收纳","angle":"做减法","persona":"第一人称",
  "title":"玄关收纳三件套,我最后只留了这三样",
  "lead":"玄关永远堆着鞋和快递盒,出门前翻三分钟找不到钥匙。",
  "body":"玄关永远堆着鞋和快递盒,出门前翻三分钟找不到钥匙。我把玄关重新收拾了一遍,只留三件东西:一个窄边换鞋凳,底下能塞四双常穿的鞋;一个壁挂钥匙盘,进门顺手一挂;一个折叠收纳箱,收快递盒和购物袋。三件加起来占不到半平米,门口却清爽了一倍。",
  "tags":["收纳","玄关","小户型"],
  "cover_prompt":"浅木色玄关换鞋凳与壁挂钥匙盘,自然光,生活感",
  "ai_flag":true},

 {"id":"T02","topic":"玄关收纳三件套","angle":"做减法","persona":"第一人称",
  "title":"玄关收纳三件套,我只留了这三样",
  "lead":"玄关永远堆着鞋和快递盒,出门前要翻三分钟才找得到钥匙。",
  "body":"玄关永远堆着鞋和快递盒,出门前要翻三分钟才找得到钥匙。我的做法是先扔再买:清掉不穿的鞋和攒了没用的袋子,然后只添三样。一个窄边换鞋凳,一个壁挂钥匙盘,一个折叠收纳箱。加起来不到半平米,门口清爽了一倍。买之前一定要量门后到墙的距离。",
  "tags":["收纳","玄关","家居","整理"],
  "cover_prompt":"同款玄关,俯拍视角,暖光",
  "ai_flag":true},

 {"id":"T03","topic":"厨房台面","angle":"分区","persona":"第二人称",
  "title":"厨房台面总是乱?先划三个区",
  "lead":"台面乱,多半不是东西太多,而是没有分区。",
  "body":"台面乱,多半不是东西太多,而是没有分区。把台面按使用频率划成三个区:备菜区放砧板和刀,灶台区只放锅和铲,出菜区放盘子和隔热垫。每个区只放这个区要用的东西,用完立刻归位。一个月后你会发现,需要挪动的物件少了一半。",
  "tags":["厨房","收纳"],
  "cover_prompt":"厨房台面分区示意,明厨,白瓷砖",
  "ai_flag":true},

 {"id":"T04","topic":"阳台改造","angle":"预算","persona":"第一人称",
  "title":"三千块改完阳台,钱花在哪了",
  "lead":"改造阳台最难的不是设计,是控制预算。",
  "body":"改造阳台最难的不是设计,是控制预算。我这次一共花了三千出头,其中地面铺装占了大头。",
  "tags":["阳台","改造","预算"],
  "cover_prompt":"小阳台改造前后对比,绿植,藤椅",
  "ai_flag":false},

 {"id":"T05","topic":"衣柜换季","angle":"断舍离","persona":"第一人称",
  "title":"换季衣柜整理,我扔了三十件",
  "lead":"换季整理衣柜,扔东西比买收纳盒有用得多。",
  "body":"换季整理衣柜,扔东西比买收纳盒有用得多。这次我把衣服全摊在床上,按【一年没穿】【穿着不合身】【洗坏变形】三条标准过了一遍,一共清出三十件。清完之后衣柜空出三分之一,当季衣服全部挂起来,一眼就能看到。剩下的空位先别急着填,空着反而是好事。",
  "tags":["衣柜","断舍离","收纳","换季"],
  "cover_prompt":"摊开的衣服与空出来的衣柜格,自然光",
  "ai_flag":true},

 {"id":"T06","topic":"书桌线缆","angle":"硬件","persona":"第三人称",
  "title":"书桌线缆整理,十分钟搞定",
  "lead":"桌下线缆乱,问题通常出在插座的数量和位置上。",
  "body":"桌下线缆乱,问题通常出在插座的数量和位置上。先在桌板背面固定一条理线槽,把插线板挂上去,再把每根线的长度收到刚好够用。多出来的线用魔术贴捆成一束,标上标签,以后换设备不用重新捋。整个过程十分钟左右,之后基本不用再动。",
  "tags":["书桌","理线","桌面改造"],
  "cover_prompt":"桌下理线槽与魔术贴捆好的线束,冷光",
  "ai_flag":true}
]
"""

rows = json.loads(RAW)

NEED = ["title", "lead", "body", "tags", "cover_prompt", "ai_flag"]
TITLE_MIN, TITLE_MAX = 8, 30
BODY_HARD = 100          # 平台底线:头条【首发】要求正文不少于 100 字 —— 阻断
BODY_GOOD = 800          # 业务建议:图文建议 800 字以上 —— 只提示,不阻断
TAG_MIN, TAG_MAX = 3, 5
SIM_TITLE = 0.50         # 标题 bigram 相似度告警线

block, warn = [], []

# ---- 1. 字段齐备 + 单篇规则(分阻断与提示两档)----
for r in rows:
    tid = r["id"]
    for k in NEED:
        if k not in r or r[k] in ("", [], None):
            block.append((tid, "缺字段 %s" % k))
    if "title" in r:
        n = len(r["title"])
        if n < TITLE_MIN:
            block.append((tid, "标题 %d 字,短于最短可用长度 %d 字" % (n, TITLE_MIN)))
        elif n > TITLE_MAX:
            block.append((tid, "标题 %d 字,超过头条 %d 字上限(信息流会被截断)" % (n, TITLE_MAX)))
    if "body" in r:
        n = len(r["body"])
        if n < BODY_HARD:
            block.append((tid, "正文仅 %d 字,低于平台底线 %d 字" % (n, BODY_HARD)))
        elif n < BODY_GOOD:
            warn.append((tid, "正文 %d 字,低于建议值 %d 字" % (n, BODY_GOOD)))
    if "tags" in r:
        n = len(r["tags"])
        if not (TAG_MIN <= n <= TAG_MAX):
            block.append((tid, "标签 %d 个,超出 %d~%d 个区间" % (n, TAG_MIN, TAG_MAX)))
    if not r.get("ai_flag"):
        block.append((tid, "ai_flag 为假 —— 必须标注【含 AI 生成内容】"))


# ---- 2. 同质化检测(批量真正的风险)----
def bigrams(s):
    return set(s[i:i + 2] for i in range(len(s) - 1))


def jac(a, b):
    A, B = bigrams(a), bigrams(b)
    return len(A & B) / max(1, len(A | B))


print("=" * 64)
print("一、单篇契约体检(共 %d 篇)" % len(rows))
print("=" * 64)
print("  --- 阻断级(机器可判定,必须改)---")
if block:
    for tid, msg in block:
        print("  [阻断] %-4s %s" % (tid, msg))
else:
    print("  [阻断] 无")
print("  --- 提示级(业务建议,交给人判断)---")
if warn:
    for tid, msg in warn:
        print("  [提示] %-4s %s" % (tid, msg))
else:
    print("  [提示] 无")
print("  阻断 %d 条 / 提示 %d 条" % (len(block), len(warn)))

print()
print("=" * 64)
print("二、同质化检测(批量真正的风险在这里)")
print("=" * 64)
hits = 0
for a, b in itertools.combinations(rows, 2):
    ta, tb = a["id"], b["id"]
    s = jac(a["title"], b["title"])
    if s >= SIM_TITLE:
        hits += 1
        print("  [!] 标题撞车  %s vs %s  相似度 %.2f" % (ta, tb, s))
        print("      %s:%s" % (ta, a["title"]))
        print("      %s:%s" % (tb, b["title"]))
    if a["body"][:12] == b["body"][:12]:
        hits += 1
        print("  [!] 开头重复  %s vs %s  前 12 字完全相同:【%s】" % (ta, tb, a["body"][:12]))
    if a["angle"] == b["angle"] and a["persona"] == b["persona"]:
        hits += 1
        print("  [!] 角度人称同款  %s vs %s  均为【%s + %s】" % (ta, tb, a["angle"], a["persona"]))

ang = {}
for r in rows:
    ang.setdefault(r["angle"], []).append(r["id"])
for k, v in ang.items():
    if len(v) > 1:
        print("  [!] 角度复用  【%s】出现在 %s" % (k, "、".join(v)))
        hits += 1
if hits == 0:
    print("  [OK] 未发现同质化")
print("  同质化告警 = %d 条" % hits)

print()
print("=" * 64)
print("三、逐篇摘要")
print("=" * 64)
for r in rows:
    print("  %-4s %-6s %-5s 标题%2d字 正文%3d字 标签%d个 AI声明%s"
          % (r["id"], r["topic"][:6], r["angle"], len(r["title"]),
             len(r["body"]), len(r["tags"]), "是" if r["ai_flag"] else "否"))

实际运行输出(真跑,不是示意):

================================================================
一、单篇契约体检(共 6 篇)
================================================================
  --- 阻断级(机器可判定,必须改)---
  [阻断] T03  标签 2 个,超出 3~5 个区间
  [阻断] T04  正文仅 41 字,低于平台底线 100 字
  [阻断] T04  ai_flag 为假 —— 必须标注【含 AI 生成内容】
  --- 提示级(业务建议,交给人判断)---
  [提示] T01  正文 116 字,低于建议值 800 字
  [提示] T02  正文 115 字,低于建议值 800 字
  [提示] T03  正文 105 字,低于建议值 800 字
  [提示] T05  正文 118 字,低于建议值 800 字
  [提示] T06  正文 108 字,低于建议值 800 字
  阻断 3 条 / 提示 5 条

================================================================
二、同质化检测(批量真正的风险在这里)
================================================================
  [!] 标题撞车  T01 vs T02  相似度 0.76
      T01:玄关收纳三件套,我最后只留了这三样
      T02:玄关收纳三件套,我只留了这三样
  [!] 开头重复  T01 vs T02  前 12 字完全相同:【玄关永远堆着鞋和快递盒,】
  [!] 角度人称同款  T01 vs T02  均为【做减法 + 第一人称】
  [!] 角度复用  【做减法】出现在 T01、T02
  同质化告警 = 4 条

================================================================
三、逐篇摘要
================================================================
  T01  玄关收纳   做减法   标题17字 正文116字 标签3个 AI声明是
  T02  玄关收纳三件 做减法   标题15字 正文115字 标签4个 AI声明是
  T03  厨房台面   分区    标题13字 正文105字 标签2个 AI声明是
  T04  阳台改造   预算    标题13字 正文 41字 标签3个 AI声明否
  T05  衣柜换季   断舍离   标题13字 正文118字 标签4个 AI声明是
  T06  书桌线缆   硬件    标题12字 正文108字 标签3个 AI声明是

读结果,四个值得注意的地方:

  1. T01 与 T02 被四条不同的检查同时抓到——标题相似度 0.76、开头 12 字一字不差、角度人称作同、角度复用。 这说明同质化不是一个点上的问题,它会同时暴露在好几个维度上。换个说法:这四条检查里任何一条命中,都该去人工看一眼;四条全中,就是一个【同一个主题写了两遍】——这正是 4.1 公告第一类【模板化拼装】的教科书样本;
  2. 注意这四条检查是【互补】的:标题相似度管文字层面,开头重复管段落层面,角度人称同款和角度复用管【生成前的配方】层面。最有用的是后面两条——因为它们在正文写出来之前就能报警(只要配方撞了,正文必然像),而前两条只能在写完之后补救;
  3. 【阻断 / 提示】两档的分界,正好落在【平台底线】与【业务建议】之间:T04 正文只有 41 字,低于平台 100 字底线 → 阻断;另外 5 篇 105 到 118 字,高于底线但远低于 800 字建议值 → 提示。这一档分得对不对,直接决定你的脚本会不会被自己烦死——如果全按阻断,一次批量会拦下 8 条,人就不看了;如果全按提示,扣 70 分那条就守不住了;
  4. 【提示级】那 5 条其实指出了一个真实问题:AI 一次给出的初稿普遍偏短。 演示数据里 6 篇都在 110 字上下——这不是我故意写短,这是模型的默认倾向。 所以 4.4.2 收到稿子之后,字数检查必须真的跑,不能说【AI 写的应该够长】。
📝 为什么这个检测器要放在【出稿之后、排版之前】

因为同质化的修复成本,随流程往后走会急剧上升: - 在【出稿后】发现 → 改配方、重出一篇,成本最低; - 在【排版后】发现 → 排版白做一遍; - 在【上传后】发现 → 草稿删掉重来; - 在【发布后】发现 → 要考虑删文——而批量删文本身又是风控信号(4.1 里提过,大规模集中删除会被判行为异常)。 所以这道检查卡在越靠前越好。 这与 4.2 里 B01-04 那条【AI 声明在出稿态就被抓出来】是同一个道理。

六、契约写死的三个好处

把输出写成契约,收益不只是【好解析】:

好处 说明
下游能接住 RPA 按字段名取值,不怕模型换一种表达
能自动体检 本节 Demo 的所有检查,都建立在字段名固定这个前提上
能换模型不换流程 从文心换到扣子、换到任何模型,只要输出还守这份契约,下游一行都不用改
📝 一句话

契约是【模型】与【流程】之间的接口。 模型是可以换的(这一章就换过一次,第三章用的是 GPT),流程不该跟着换。把接口定死,两边就能各自演化。

七、⚠️ 2026 现状:本节设计的五处依据

  1. 标题 30 字上限:多个来源交叉后取【建议 18 到 28 字、超过 30 字信息流会截断】。不同教程给的数字不完全一致(有的说 20 字以内)——取保守值,写 18 到 25 字最稳;
  2. 正文 100 字是平台底线:这条来自头条【首发】的公开整理规则(原创、72 小时内头条独发、正文不少于 100 字)。把它当阻断线,把 800 字当提示线;
  3. AI 声明是必填:图文要在标题下方或正文开头标注【含 AI 生成内容】;未标注已明确落进【失实表述】这一档(扣 10 分)。所以 ai_flag 是契约里的硬字段;
  4. 同质化是专项治理对象:2026-09-15 公告点名【模板化拼装、套路化表达】,且【更换题材人物、替换表达模板】的规避手法也从严处置。这是差异化节点的全部依据;
  5. 不要用 AI 定分类:头条的【分类】是垂直领域标签,选错会削弱推荐(选错等于告诉算法你不是这个领域的人)。这一格由人定,不由模型定——这是本节唯一一个【人指定、机器不碰】的字段,原因是它属于账号策略,不属于内容生成。

30 秒实验

把 Demo 里的 SIM_TITLE 从 0.50 改成 0.30,再跑一次。

会发生什么:

结论:阈值调高调低,改变的只是【误报多少】,不改变【真阳性抓不抓得到】。 所以业务上应该把阈值调保守(宁可多报几条、让人去看),而不是调激进(漏掉真撞车)。

这与上一节的设定卡检查器是同一条纪律:自动检查的任务是【不漏】,不是【不烦】。 漏掉的代价是不可逆的(发出去就是一次违规),烦一点的成本只是人多看两眼。

常见误区

  1. 【让模型一次写 6 篇,它自然会写得不一样】——错。 模型的默认行为是趋同(标题句式、开头、结构、人称都会撞)。差异必须由流程分配,不能靠期望。
  2. 【主题不同,内容自然就不同,不需要查同质化】——错。 本节 Demo 里的 T01 与 T02 主题不同(一个是【玄关收纳】,一个是【玄关收纳三件套】),但配方一模一样,正文就像双胞胎。 同质化的根源在配方,不在主题名。
  3. 【正文越长越好,先按 2000 字写】——错。 平台只给了一条底线(不少于 100 字),800 到 1200 字是经验建议而不是规则。写太长还有副作用:完读率下降。底线用阻断、建议用提示,是最稳的分法。
  4. 【同类检查放在发布前做一次就够了】——错。 同质化的修复成本随流程往后走急剧上升(重出 → 重排版 → 删草稿 → 删已发布)。检查要卡在【出稿之后、排版之前】。
  5. 【分类也让 AI 一起给了,省事】——错。 分类属于账号的垂直策略(选错会削弱推荐),它决定【这个账号是什么号】,不是【这篇文章是什么内容】。这一格必须人定。
自测三问(点开对答案) 1. **为什么批量场景下,Agent 的输出格式比输出质量更重要?** 因为两者的失败后果不对称:**格式错了,下游会直接卡死**【流程性失败】,整批停摆;**质量差,下游还能靠人工抽检补上**【判断性失败】,批量仍能跑完。**所以先保证【能被机器接住】,再追求【写得好】**——这也是把输出定成契约的原因。 2. **为什么【差异化】必须由流程分配,不能交给模型自己发挥?** 因为**模型的默认行为是趋同**——它学习的是【最常见、最稳妥的写法】,所以同一段提示词 + 一批主题,产出的标题句式、开头、结构、人称大概率一模一样。**差异不能靠期望,只能靠指定**:**在写之前,由流程给每篇锁定【角度 / 人称 / 开头方式 / 结构】四个维度,并保证批内没有任何两篇四维全同。** 3. **为什么把检查都放在【出稿之后、排版之前】这个位置?** 因为**同质化的修复成本随流程往后急剧上升**:出稿后发现,只需改配方重出一篇;排版后发现,排版白做;上传后发现,草稿要删重来;**发布后发现,要考虑删文——而大规模集中删除(修改)本身就是平台判定【行为异常】的信号**。**所以检查越靠前越好。** 这也是 4.2 里那条【AI 声明在出稿态就被抓出来】的同一条纪律。

相关笔记