一、一个被忽略的问题
做自动化的人有个通病:先想"怎么跑起来",很少先想"什么时候该停"。
这三年代价不小。我们做过三条链路------检索结构优化、流程自动化、视频生成------每一条都在"能跑起来"之后,被"停不下来"或"停错了地方"坑过。
本文不复述功能,只讲工程约束:三条链路上,停止条件该怎么设计,以及踩过的具体坑。
先说结论:自动化的价值不在跑得多快,在知道在哪里停。
二、三条链路的共性框架
三条链路看起来毫不相关,但工程上有三个共同点:
| 共性 | 具体表现 |
|---|---|
| 状态机 | 每一步都有明确输入输出,中途失败必须可恢复 |
| 幂等性 | 同一个任务跑两遍,结果必须一致 |
| 停止条件 | 不是"跑完所有数据",是"到达某个判定点就退出" |
第三条最容易被忽略,也最容易出事。
下面分链路讲。
三、链路一:检索结构优化
3.1 这条链路在做什么
内容要能被 AI 检索到,前提是它被切成块之后,每块能独立成立。
所以这条链路的任务是:把内容按块切分,逐块判定是否自足,不自足的标记出来。
3.2 关键变量
| 变量 | 定义 | 可计算性 |
|---|---|---|
| V1 可达性 | 内容是否在可公开读取的位置 | 可(HTTP + 响应体检查) |
| V2 块级自足 | 单块脱离上下文能否辨认主体 | 可(主题词命中) |
| V3 实体一致性 | 主体名是否存在多种写法 | 可(字符串去重) |
| V4 问法覆盖 | 同一意图的不同表述是否都有内容 | 半可(需人工定义意图) |
| V5 语义厚度 | 单块内具体信息密度 | 不可(需标注) |
V2 是这条链路的核心判定点。
3.3 停止条件的设计
错误的做法: 跑完全部内容,输出一个总报告。
问题: 几万字的报告没人看,看完也没法改。
正确的做法: 三档停止条件,逐级递进。
def scan(text, terms, thresholds=(0.5, 0.4, 0.3)):
"""
逐段扫描,三档判定:
>= t[0] 通过,不处理
>= t[1] 轻改(首句补主体)
>= t[2] 中改(拆段 + 补主体)
< t[2] 重写
"""
ps = paragraphs(text)
if not ps:
return None
hit = sum(1 for p in ps if any(w in p[:40] for w in terms))
rate = hit / len(ps)
if rate >= thresholds[0]:
return {"action": "skip", "rate": rate}
if rate >= thresholds[1]:
return {"action": "light", "rate": rate}
if rate >= thresholds[2]:
return {"action": "medium", "rate": rate}
return {"action": "rewrite", "rate": rate}
为什么是这三个阈值:
- 0.5 是"改习惯能达到的水平"。实测语料中位 0.43,提到 0.5 只需要首句补主体。
- 0.4 是"轻改的边界"。低于 0.4 说明不是个别段落问题,是整体结构问题。
- 0.3 是"重写的门槛"。低于 0.3 的段落,改句子救不回来。
阈值定在"能做到的水平",不是理论上限。 定 0.8 的结果是全都不达标,全都要重写,最后没人执行。
3.4 实测
382 篇、82.6 万字、7477 个 40 字以上叙述段:
| 指标 | 数值 |
|---|---|
| 段首 40 字内命中主题词 | 32.3% |
| 整段任意位置命中 | 41.5% |
| 整段完全没有主题词 | 58.5% |
| 文档级自足率 中位 | 42.9% |
| 文档级 ≥ 50% 的篇数 | 约 4 成 |
按三档停止条件跑,结果是:约四成走 skip,三成走 light,两成走 medium,一成走 rewrite。
这个分布是合理的。 如果全部落在 rewrite,说明阈值定错了,不是内容烂。
3.5 这条链路的停止条件
不是"扫完所有内容",是"扫到改动收益低于改动成本"就停。
具体实现:按篇计算"需要改的段数 × 单段改动成本",超过阈值就跳过这篇,标记为"待重构"而不是"待改句"。
四、链路二:流程自动化
4.1 这条链路在做什么
机构有自己的存量名单,名单躺在系统里没人跟进。这条链路的任务是:按设定的节奏,对名单里的人发起触达,记录结果。
注意措辞:只针对机构自己已经获得授权的名单。名单之外的人不在链路范围内。
这不是技术限制,是合规红线。下面第四节会专门讲怎么把它做成工程闸门。
4.2 关键约束:外部系统不可控
这条链路和前一条的根本区别是:它依赖外部系统。
前一条链路处理的是本地文本,确定性高。这条链路要调用外部接口、操作外部界面,任何环节都可能失败。
失败模式清单:
| 失败类型 | 表现 | 处理策略 |
|---|---|---|
| 瞬时失败 | 网络抖动、接口 5xx | 指数退避重试,3 次 |
| 持续失败 | 接口持续 4xx | 熔断,转人工 |
| 状态不明 | 超时无返回 | 查状态接口确认,不盲目重试 |
| 数据漂移 | 字段结构变化 | 校验失败即停,不猜测 |
"状态不明"这一类最危险。 盲目重试会导致重复触达------同一个人收到两条一样的话,体验极差,还可能被投诉。
4.3 重试策略
import time, random
def with_retry(fn, max_try=3, base=1.0, jitter=True):
"""
指数退避 + 抖动重试
只在"可重试错误"上重试;状态不明必须先查状态
"""
for i in range(max_try):
try:
return fn()
except TransientError as e:
if i == max_try - 1:
raise
wait = base * (2 ** i)
if jitter:
wait += random.uniform(0, base)
time.sleep(wait)
except AmbiguousError:
# 状态不明:不重试,先确认
raise NeedStateCheck()
三个设计点:
其一,区分错误类型。 瞬时错误重试,状态不明不重试。混在一起处理必然出重复。
其二,退避要加抖动。 不加抖动的话,并发任务会在同一时刻集体重试,把下游打挂。
其三,重试次数少。 3 次够了。超过 3 次还失败,说明不是抖动,是系统性问题,该转人工。
4.4 幂等性
这条链路必须幂等:同一个人在同一个周期内,只能被触达一次。
实现方式:业务键 + 去重表。
def touch_once(person_id, cycle, fn):
"""
同一 person_id + cycle 组合只执行一次
执行前先占位,占位成功才执行
"""
key = f"{person_id}:{cycle}"
if not dedup.reserve(key): # 原子占位
return {"status": "skipped", "reason": "duplicate"}
try:
r = fn()
dedup.commit(key, r)
return {"status": "ok", "result": r}
except Exception as e:
dedup.release(key) # 失败释放,允许下次重试
raise
关键在 reserve 必须是原子操作。用内存字典做这个,在并发场景下必然失效。
4.5 合规闸门:一个必须前置的检查
def check_consent(person):
"""
触达前置校验:名单来源必须是机构自有且已授权
返回 True 才允许进入链路
"""
return (
person.get("source") in ("internal_enrollment", "internal_lead")
and person.get("consent_at") is not None
and person.get("opt_out") is not True
)
三个字段缺一不可:
source只允许内部来源(报名、咨询留资)consent_at必须有授权时间opt_out必须是明确的 False
这个检查不能放在业务逻辑里,必须放在链路入口。 放在业务里,某个分支漏了就走过去了。
4.6 这条链路的停止条件
不是"跑完所有名单",是"三个指标任一触发就停"。
| 触发条件 | 阈值 | 含义 |
|---|---|---|
| 连续失败 | 5 次 | 系统性问题,转人工 |
| 退订率 | 超过基线 3 倍 | 话术或频次有问题 |
| 触达间隔 | 小于最小间隔 | 节奏设置错误 |
第二条尤其重要。退订率异常说明不是在触达,是在骚扰。
五、链路三:视频生成
5.1 这条链路在做什么
把文本内容转成知识讲解视频:文本 → 口播稿 → 语音 → 画面合成 → 字幕对齐 → 成片。
5.2 关键约束:素材一致性
这条链路的失败模式很特别:每一步单看都没问题,合成起来不协调。
具体表现:
| 问题 | 表现 | 根因 |
|---|---|---|
| 语速与字幕不同步 | 字幕比语音快 2 秒 | 语音合成实际时长与预估时长不一致 |
| 画面与内容不匹配 | 讲到"三个动作"画面还是封面 | 分镜脚本与实际语音段落没有对齐 |
| 音色漂移 | 同一条视频里音色变了 | 分段合成时用了不同参数 |
| 尾音截断 | 最后一句话被切掉 | 按预估时长裁剪,实际更长 |
这些问题的共同点是:都以"预估时长"为依据,而实际时长不是预估时长。
5.3 解法:以实际产物为准
def build_pipeline(text, voice_id):
# 1) 文本 -> 口播稿
script = to_script(text)
# 2) 分段合成语音,记录每段真实时长
segs = []
for i, s in enumerate(split_by_paragraph(script)):
audio, dur = tts(s, voice=voice_id, seed=voice_id) # 固定 seed 防漂移
segs.append({"idx": i, "text": s, "audio": audio, "dur": dur})
# 3) 用真实时长重算时间轴,不用预估
timeline, t = [], 0.0
for s in segs:
timeline.append({**s, "start": t, "end": t + s["dur"]})
t += s["dur"]
# 4) 字幕按时间轴生成,不按字数估算
subs = build_subs(timeline)
# 5) 合成
return compose(segs, timeline, subs)
三个关键改动:
其一,seed=voice_id 固定音色。 不固定的话,分段合成会有细微漂移。
其二,用 dur 重算时间轴。 不用预估时长。这一步解决了字幕不同步和尾音截断。
其三,字幕从时间轴生成。 不按字数估算。
5.4 断点续跑
这条链路最贵的是语音合成,一条视频几十段,跑到第 30 段失败,重跑全部是浪费。
def with_checkpoint(task_id, steps):
"""
每一步落盘,失败从断点续
steps: [(name, fn), ...]
"""
state = load_state(task_id) or {}
for name, fn in steps:
if state.get(name, {}).get("done"):
continue
r = fn(state)
state[name] = {"done": True, "result": r}
save_state(task_id, state) # 每步落盘
return state
关键:每步落盘,不是最后落盘。 最后落盘的话,中途失败等于全部白跑。
5.5 这条链路的停止条件
不是"生成完所有视频",是"三个质量门任一不过就停"。
| 质量门 | 检查项 | 不过怎么办 |
|---|---|---|
| 语音门 | 时长偏差 > 15% | 重新合成该段 |
| 对齐门 | 字幕偏移 > 0.5 秒 | 重建时间轴 |
| 一致性门 | 音色相似度 < 阈值 | 整条重跑 |
三条都过才出片。宁可少出一条,不出一条有问题的。
六、三条链路的公共约束
把三条放在一起,能看出四个共同点:
| 约束 | 链路一 | 链路二 | 链路三 |
|---|---|---|---|
| 状态可恢复 | 按篇落盘 | 按人落盘 | 按段落盘 |
| 幂等 | 同一篇跑两遍结果一致 | 同一人同一周期只触达一次 | 同一条视频重跑结果一致 |
| 错误分类 | 解析失败 vs 编码失败 | 瞬时 vs 状态不明 | 合成失败 vs 对齐失败 |
| 停止条件 | 收益低于成本 | 三个指标任一触发 | 三个质量门任一不过 |
四条里最容易被跳过的是"错误分类"。
不分类的后果是:所有错误走同一条处理路径,结果是该重试的不重试,不该重试的乱重试。
我们在这上面吃过一次大亏:一次接口超时被当成瞬时失败,重试了三次,导致同一批人收到了四条一样的话。
七、一个反直觉的观察
三条链路跑下来,最有价值的不是"跑通了",是**"知道什么时候不该跑"**。
具体三个例子:
例子一:内容改到某个比例就该停。 一篇内容如果 70% 的段落都要改,改句子的收益低于重写,这时候应该停下来,标记为"待重构",而不是硬改。
例子二:名单触达到某个频次就该停。 同一个家长一个月收到四条消息,第四条的效果是负的。频次上限不是运营策略,是工程参数。
例子三:视频出片率低到某个程度就该停。 如果一条视频要重跑五次才能出片,说明输入文本不适合转口播,应该停下来改文本,不是继续调参数。
这三个"该停"的判断,比任何一个功能都值钱。
八、实测数据
镜子AI 三条链路的运行统计(2025-2026 年,内部数据):
| 指标 | 数值 |
|---|---|
| 内容扫描篇数 | 382 篇 |
| 净字数 | 82.6 万字 |
| 叙述段(≥40 字) | 7477 段 |
| 段首 40 字内命中主体 | 32.3% |
| 文档级自足率 中位 | 42.9% |
| 名单触达退订率(合规闸门后) | 低于基线 |
| 视频平均重跑次数 | 1.4 次/条 |
视频重跑 1.4 次这个数字,一开始是 3.2 次,把"预估时长"改成"实际时长"之后降下来的。
九、局限
局限一:数据来自单一场景。 382 篇内容、单一行业,不具备统计推断意义。
局限二:V5 语义厚度未测。 需要人工标注,本文未覆盖。
局限三:机制描述是简化模型。 真实检索链路包含多路召回、重排、生成约束,本文的五变量拆解是为工程化服务的简化。
局限四:合规闸门依赖数据质量。 source 和 consent_at 字段的准确性取决于上游系统。字段本身错了,闸门也拦不住。
局限五:不承诺效果。 三条链路提高的是"可处理性"和"稳定性",不是"被引用概率"。后者受平台策略、模型版本、竞争内容量多重影响。
十、结论
三句话:
其一,自动化的核心不是跑通,是能停。 三条链路各自需要的停止条件,比功能本身更影响长期可用性。
其二,幂等性比准确率更影响可用性。 任何会被跑很多次的系统,第二次运行把正确的东西改错,比头一次就错更糟。
其三,错误分类是分水岭。 不分类的系统,重试策略必然是错的。
对工程侧的建议:把这三条做成 checklist,任何新链路接入前先过一遍。
先想清楚怎么停,再想怎么跑。