无人值守AI内容矩阵实战(8):把标题 A/B 实验做成可判胜的流水线

标题这件事,我一开始是靠感觉的。写了三十多篇之后翻后台,阅读量加起来不到八百,横向对比才发现:同一个话题换个标题写法,阅读量能差三倍。于是我把标题实验从「灵感」挪到了「流水线」里------不增加发布量,只是给当天的稿子打两个标记,隔日补对照,数据回流后自动判胜。

这篇讲工程实现。踩过的最大一个坑是幂等闸和实验机制的冲突,我会放在第三节细说,那是我两个月里最贵的一次调试。

一、数据模型:实验是标记,不是分支

先把语义定清楚,否则后面全是坑。A/B 实验不产生额外的发布行为,它只在内容台账的一条记录上加两个字段:

python 复制代码
@dataclass
class ArticleRecord:
    title: str
    platform: str
    topic: str
    url: str | None = None
    ab_group: str | None = None      # ab-<主题>-<YYYYMMDD>
    ab_variant: str | None = None    # A / B
    views: int = 0
    likes: int = 0

当天三个平台照常各发一篇。我选流量最好的平台(这里是 CSDN)给它当天的稿子打 ab_variant="A",另外两个平台发同主题、不同标题的稿子,实验字段留空。

为什么坚持「标记而非分支」?因为发布是每天必须完成的硬任务,实验只是附加观测。我第一版把两者耦合------「今天做实验,所以只发实验平台」------直接导致另外两个平台断更,账号权重掉了一周才缓过来。把实验做成可选的元数据,最坏情况是实验失败,而不是当天没内容。

实验组 ID 必须带日期。同一主题做第二次实验时,如果不带日期,两条记录会混进同一个组,判胜直接判错,而且这种错误在报表上完全看不出来------你只会觉得「这组数据怎么这么怪」。

二、调度:pending-b 的自动提醒

B 变体要第二天发,靠人记必然漏。我把提醒做进调度器:每天开工先扫台账,找出状态为 pending-b 的组(A 已发、B 未发),为它生成一条带 ab_variant="B" 的任务;然后遍历三个平台,把没有任务的平台补上普通稿。

python 复制代码
PLATFORMS = ("csdn", "juejin", "toutiao")

def plan_today(records):
    tasks: dict[str, dict] = {}
    for r in records:
        if r.ab_group and r.ab_variant == "A" and not has_b(records, r.ab_group):
            tasks[r.platform] = {
                "platform": r.platform,
                "ab_group": r.ab_group,
                "ab_variant": "B",
                "style": next_style(r.ab_group),   # 与 A 不同的风格
            }
    for p in PLATFORMS:
        tasks.setdefault(p, {"platform": p, "ab_group": None, "ab_variant": None})
    return list(tasks.values())

最后那个 setdefault 循环是三平台不缺篇的保证。我一开始漏了它,结果补 B 那天只产出两篇------实验平台有稿,另外两个平台空着,当天日报看起来还很正常,因为任务清单确实「都执行完了」。

另一个细节:B 变体的标题风格必须和 A 明确不同。我在生成任务时就把 next_style 定下来(A 用数字列表,B 就用疑问式),而不是让模型自由发挥------自由发挥的结果往往是它挑自己最熟的那种,最后两个变体风格趋同,实验白做。

三、幂等闸冲突:deduped 不是成功

这是核心坑。我的发布器里有一道幂等闸,发布前对标题做归一化再算哈希,命中历史记录就跳过:

python 复制代码
def publish(platform: str, title: str, body: str) -> dict:
    h = sha256(normalize(title))
    if h in history(platform):
        return {"ok": True, "deduped": True, "url": lookup_url(h)}
    ...
    return {"ok": True, "deduped": False, "url": new_url}

这道闸救过我:有一次网络抖动触发重试,同一个标题连发五次,账号差点被封。但它和 A/B 实验天然冲突------我让模型「参考历史高表现标题」来写 B 变体,模型非常听话地复刻了一篇旧文的标题,哈希撞库,闸门静默跳过,返回 ok: True。而调度器只看 ok,把它记成了成功。当天该平台零新文章,日报却显示「已发布」。

修复分两层。

第一层,调用方必须把 deduped 当失败:

python 复制代码
res = publish(platform, title, body)
if res.get("deduped"):
    raise NotPublishedError(f"标题被幂等闸拦截,本平台今日零产出: {title}")

第二层,生成标题时加相似度约束,从源头避免撞库:

python 复制代码
def too_similar(title: str, history_titles: list[str], th: float = 0.85) -> bool:
    s1 = set(title)
    return any(
        len(s1 & set(t)) / len(s1 | set(t)) > th
        for t in history_titles
    )

字符级 Jaccard 足够用,不必上向量检索------标题普遍在 20 到 40 字,集合重叠度对复刻非常敏感。命中就重生成,最多三次,三次还撞就换风格模板。

这个教训我认为是通用的:任何带幂等设计的系统,调用方都必须区分「被执行」和「被跳过」 。它们在日志里长得几乎一样,而语义完全相反。我后来在所有写操作里都统一了这个约定:返回体必须显式带 deduped 或 skipped 字段,调用方不检查就抛异常。

四、判胜:样本门槛比公式重要

数据回流后判胜。我最初想搞加权公式,试了两轮就放弃了------样本量太小,加权只是给噪声乘系数。现在的规则极简:

python 复制代码
def judge(a: ArticleRecord, b: ArticleRecord, min_views: int = 50) -> str | None:
    if a.views < min_views or b.views < min_views:
        return None                    # 样本不足,保持 awaiting-data
    if a.views != b.views:
        return "A" if a.views > b.views else "B"
    return "A" if a.likes >= b.likes else "B"

min_views 是必须的。我有一组实验 A 变体 1 阅读、B 变体 0 阅读,按规则 B 输了,但这数据毫无信息量。没到门槛就老实挂着,别急着沉淀洞察------错误的洞察比没有洞察更糟,它会持续误导后续所有选题。

判胜之后,结果写进一张洞察表,维度是「平台 × 标题风格 → 胜出次数」。下次起标题先查表,查不到再凭感觉。这张表是这套机制真正的产出物,单次实验的输赢反倒次要。

五、两组真实数据

第一组,九月底,CSDN,接口框架实战主题:

变体 风格 阅读 点赞
A 数字列表 + 实战干货 109 4
B 疑问式 + 悬念反差 39 1

A 胜,接近三倍差距。

第二组,十月下旬开的,A 变体已发,B 变体数据尚未回流,状态为 awaiting-data。我没有把它当结论用。

把已判胜的几组摊开看,CSDN 上「实战干货 + 数字列表」胜出六次,疑问式只在极少数场景翻盘。所以现在 CSDN 默认走这个风格。

六、几条工程约定

A 和 B 的间隔压在一两天内。隔太久,推荐池状态、账号权重、热点都变了,测的就不只是标题。我有一组隔了五天,数据完全没法归因。

正文可以微调,但主题和核心内容别动,否则变量不唯一。

同一天不要给多个平台同时开实验。第二天要补多个 B 变体,出错概率翻三倍,一个平台挂了整组实验都废。

实验状态机只有三个值:pending-b(A 已发待补 B)、awaiting-data(两变体已发等回流)、concluded(已判胜)。状态少的系统才不容易错。

收尾

这套东西跑下来,最大的变化不是阅读量涨了多少,而是早上打开文档不用再纠结标题了------表里有答案。

代码层面真正重要的就三件事:实验标记与发布解耦、deduped 必须当失败处理、判胜要设样本门槛。前两件保证系统不出错,第三件保证结论不跑偏。剩下的都是细节。

相关推荐
用户2054555788001 小时前
飞书运费台账同步到 MySQL:单一策略落地教程
前端
LEE1 小时前
别把 Agent 架构画成组织架构图——子代理该不该拆,用任务级 Eval 说话
前端·后端·ai编程
梦醒江南1 小时前
RAG 证据链验收:别只测答案像不像,要证明它从哪里来
前端·agent·ai编程
lemon_yyds1 小时前
vue 实现轮播 - 一张半效果
前端
阿明的小蝴蝶1 小时前
自定义桌面应用、内置系统应用与镜像
前端
代码搬运媛2 小时前
原生websocket 双工demo
前端
limit00752 小时前
三维 WebGIS 工具选型:加载、样式、分析、交付,一套界面能做完什么
android·前端·web3·安卓·webgl
全栈项目管理程序猿2 小时前
ArcGIS JS 基础教程(35):体元系列 - VoxelLayer 时间维度动画
前端·javascript
mantou1322 小时前
从纯 CSR 到 SSR:用 WebComponents 做一个 Next.js 式框架 Gemkit
前端·全栈·web components