你给流水线加了定时任务,每天自动跑一遍,满心期待每天产出新鲜内容。结果打开一看——跟昨天一模一样,跟前天也一模一样。问题不在定时任务,而在那个被你当成"随机"的挑选函数。
现象
流水线每天都生成同一批候选,内容高度重复。加了定时任务之后依然如此:不是"偶尔重复",而是"每天完全一样",连顺序都一致。
Day 1: 候选 [A, B, C, D, E]
Day 2: 候选 [A, B, C, D, E]
Day 3: 候选 [A, B, C, D, E]
最能说明问题的一个动作,是把连续两天的输出做一次 diff:结果是一行都不差。如果只是"选题撞车",多少会有点出入;而完全一致,说明整个挑选环节根本没有引入任何变化。
根因
候选的挑选函数是确定性的——它固定取候选池的"前 N 条":
candidates = pool[:5] # 每次都是同样的前 5 条
同一份候选池、同一个"取前 N 条"的逻辑,输入不变、函数不变,输出必然不变。这跟"定时任务"毫无关系:定时任务只是"什么时候跑",它改变不了"跑出来是什么"。
确定性的选择逻辑,输出必然是确定性的。 想不重复,就不能只靠"取前 N 条"——你得引入真正的变化来源。
仔细想想,"前 5 条"其实是一个稳定排序后的切片。即使池子的顺序每天略有变化,只要排序键(比如录入时间、文件名)不参与变化,排在前面的仍然是同一批。也就是说,不仅"取前 N 条"是确定性的,连"池子的顺序"本身也常常是确定的。
排查
定位这个坑的动作很简单,核心就是追问"变化从哪来":
# 连跑两次,比较结果是否一致
python pipeline.py > d1.txt
sleep 2
python pipeline.py > d2.txt
diff d1.txt d2.txt # 空输出 = 确定性
如果 diff 为空,再往上游找一步:看看挑选函数里有没有任何一处调用了 random、now()、或者当天的日期。多数情况下你会发现,函数从头到尾只做了切片和排序,压根没有随机源。
解决
三件事要一起做,缺一不可:
第一,让内容"现拟"而非"固定选取"。 让模型依据分类体系现场生成候选,而不是从固定池子里挑前几条:
prompt = f"""
请依据以下分类体系,为今天拟 {n} 个不同的选题:
{category_taxonomy}
要求:避免与前 {k} 天的选题重复。
"""
candidates = llm_generate(prompt)
这一步的关键,是把"变化"交给一个每天确实不同的输入——历史选题和分类体系每天都在变,生成的候选自然跟着变。注意要把"避免与前 k 天重复"写进提示词,否则模型自己也倾向于给出高频套路。
第二,入库查重。生成之后,先去历史库里核对,把已经出现过的标题剔除掉:
existing = load_history_titles()
fresh = [c for c in candidates if c["title"] not in existing]
第三,给标题加唯一索引。这是数据库层面的最后一道防线:
ALTER TABLE topics ADD CONSTRAINT uniq_title UNIQUE (title);
但要注意一个关键限制:唯一索引只挡"完全相同"的标题。 如果同主题换个措辞——"如何提升写作效率"和"写作效率怎么提高"——唯一索引完全挡不住,它们会被当成两条不同的记录插入。这类"近重复"需要语义层面的去重:
# 语义去重:比较标题的向量相似度
from numpy import dot
from numpy.linalg import norm
def cosine(a, b):
return dot(a, b) / (norm(a) * norm(b))
def is_near_dup(new_vec, history_vecs, threshold=0.9):
return any(cosine(new_vec, h) > threshold for h in history_vecs)
延伸与预防
这个坑的通用形态是:把"不变的东西"误当成了"会变的东西"。 常见的几个变体:
- 用固定的
random.seed()却指望每次结果不同(种子固定 = 结果固定); - 用时间排序取"最新",但数据在测试环境里从不更新,于是每次都取到同一条;
- 用"取前 N 条"做抽样,而池子的顺序从不变化。
预防的核心是明确区分"数据带来的变化"和"逻辑带来的变化"。如果输出只依赖于确定的输入和确定的逻辑,那它永远是确定的。要让结果每天不同,就得引入至少一个"每天不同"的来源——模型的现场生成、随机种子、当天的日期、新到的数据,都可以。
最后,去重要做多层次:完全相同的靠唯一索引,措辞相近的靠模糊匹配,语义相近的靠向量相似度。只做第一层,你会很快发现"重复"换了个马甲又回来了。