用 Dify 做微博热搜 AI 解读频道:去重与定时调度怎么落地
拆解一条「抓微博热搜 → 搜索引擎补背景 → 大模型写解读 → 定时推送到 Telegram」的流水线:先讨论热搜内容是否值得做,再给出用 Redis 判断新旧词条的具体做法、青龙面板与 cron 的调度取舍、检索与摘要的质量控制,以及模型选托管 API 还是本地 ollama 的判断依据,最后附上线检查清单。
微博热搜更新快、单条信息量小,直接搬进 Telegram 频道价值有限,于是常见做法是加一层加工:抓到词条后先用搜索引擎补充背景,再让大模型写成几句解读。有人用 Dify 串起了这样一条链路——抓热搜前几名、查 DuckDuckGo、用 QwQ 归纳、由青龙面板定时触发并推送到频道——过程中卡在两个地方:怎么判断一条热搜是新出现的还是昨天就有的,以及把定时逻辑放在哪一层更稳。
这两个问题看似琐碎,却决定了频道能不能长期无人值守地跑下去。下面按「内容价值 → 整体架构 → 去重 → 调度 → 质量与成本 → 检查清单」的顺序梳理一遍。
先确认:这类内容值不值得做
讨论里最集中的质疑是热搜本身有没有信息价值。实际观察两天的结论往往不太乐观:大量是娱乐八卦和情绪化话题,模型再加一段总结,很容易变成「把标题换个说法」。
如果目的是练手工作流编排、熟悉大模型调用,那怎么跑都行;如果希望频道有人长期订阅,建议在流水线里加几道筛选:
- 限定品类:只保留科技、财经、政策、突发事故、公共安全等关键词命中的词条,纯娱乐词条直接跳过或降低推送频率。
- 合并同事件:同一事件的不同表述要归一,否则一次热点会刷出七八条。
- 附来源:摘要后面带上检索到的 1–3 条链接,让读者能自己判断,而不是只能相信模型。
- 写「为什么值得看」而不是复述:一句解释比三段同义改写有用得多。
整条流水线的分工
| 环节 | 承担工具 | 职责 | 容易出问题的地方 |
|---|---|---|---|
| 抓取 | Dify 工作流 | 拉热搜榜、取前 N 条、清洗标题 | 页面结构变动、编码问题 |
| 去重 | Redis / 数据库 | 判断词条是否已推送过 | 排名变化被误判为新词条 |
| 检索 | DuckDuckGo 等搜索接口 | 补充背景与多方说法 | 限流、超时、结果为空 |
| 摘要 | 大模型 | 归纳要点、生成解读 | 幻觉、格式不稳定、成本 |
| 调度 | 青龙面板 / cron | 定时触发、失败重试 | 任务重叠、日志分散 |
| 分发 | Telegram Bot | 格式化推送 | 转义报错、频率限制 |
把「触发」和「执行」拆开是关键:工作流本身只负责「跑一次」,谁来触发、多久触发一次,交给外部调度器。这样两边都能单独调试。
问题一:怎么判断热搜是新的还是旧的
用 Redis 做判重的方向是对的,但要注意几个细节。
以词条文本为键,用集合判重
热搜的标题会带标点、空格、话题符号的差异,入库前先做归一化(去空白、统一全半角、去掉 #),再做判断:
import redis
r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
def normalize(title: str) -> str:
return title.replace("#", "").replace(" ", "").strip()
def is_new(title: str, ttl_days: int = 7) -> bool:
key = f"weibo:hot:{normalize(title)}"
# set nx + ex,返回 True 表示这个 key 之前不存在,即新词条
created = r.set(key, "1", nx=True, ex=ttl_days * 86400)
return bool(created)
SET NX EX 是原子操作,比「先查再写」安全,多实例并发也不会重复推送。
排名变化不等于新热搜
这是最容易踩的坑:一条词条上午排第 5、下午排第 1,如果只比对榜单列表,就会被当成新内容再推一次。正确做法是只以「首次出现」作为推送条件,排名变化单独处理——比如只对「新进入前三」或「排名上升超过若干位」的旧词条发一条简短提示。需要记录排名的话,用 Hash 存首次出现时间与上次排名:
HSET weibo:meta:<词条> first_seen <ts> last_rank <n> last_push <ts>
别忽略过期与持久化
- 给判重键设置合理的过期时间(例如 7 天),否则集合会无限膨胀。热点话题过几天再上榜也确实算「新」,重复推送一次可以接受。
- 单机部署时打开 AOF 或定期 RDB,避免重启后把已推送内容全部重发一遍。
更轻的替代方案
如果一天只有几十条要处理,Redis 属于杀鸡用牛刀。SQLite 建一张 pushed(title, ts) 表、甚至一个 JSON 快照文件做差分,都能满足需求。Redis 的优势在于原子性和多实例共享,只有在你确实要并行跑多个抓取任务时才明显。
问题二:定时调度放在哪一层
| 方案 | 适合场景 | 取舍 |
|---|---|---|
| 平台内置定时 | 工作流本身就带触发能力、任务单一 | 配置能力弱,失败重试和告警通常要自己补 |
| 青龙面板 | 已经用它管一堆脚本,想要统一面板 | 多一层依赖,日志和排错分散在两处 |
| 系统 cron | 单机、任务少、追求稳定 | 没有可视化管理,改任务要登机器 |
| 外部工作流工具 | 需要可视化重试、分支、告警 | 学习成本更高 |
务实的组合是:Dify 工作流暴露一个 HTTP 接口,青龙面板只负责定时 curl 触发。职责单一,工作流改动不用动调度配置,也能用面板统一看执行记录。
curl -sS -X POST "https://<你的工作流地址>" \
-H "Authorization: Bearer $DIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"inputs":{},"response_mode":"blocking","user":"cron"}' \
--max-time 180
配套几点:
- 加超时与重试:搜索接口或模型偶发变慢时,不要让任务一直挂着;失败后用较短间隔重试一两次即可。
- 防重叠:任务执行时间可能超过间隔,用锁(文件锁或 Redis 锁)避免两轮同时跑。
- 告警:连续失败时往同一个 Bot 或另一个私聊频道发一条错误摘要,否则断更几天都不会发现。
- 时区:确认调度面板和容器内的时区一致,否则「每小时一次」会跑偏。
如果触发器和模型调用都跑在自己电脑上,机器休眠、断网就直接断更。这类需要长期在线的小任务,放到一台持续开机的机器上更省心;不想为此专门维护家用设备的话,也可以考虑整台 Mac mini 放在机房的独享远程 Mac,按月租用、远程连接使用,适合长时间挂着定时任务和模型调用。这是可选项,按自己的运维意愿取舍。
检索与大模型:质量与成本的平衡
搜索接口要能降级
这类免费搜索接口通常没有服务承诺,限流和超时是常态。建议:
- 只取前几条结果的标题与摘要片段,控制进入模型的 token。
- 对查询结果做一层缓存,同名词条短时间内不重复请求。
- 检索失败时不要让模型凭空编:降级为「仅标题 + 一句通用提示」,或直接跳过该条。
- 在提示词里要求输出固定结构(例如 JSON:
summary、points、sources),并要求「资料不足时明确写未知」。
托管 API 还是本地 ollama
讨论里有人问这类模型调用是否要付费,也有人表示自己在本地用 ollama 部署。两种路线的取舍:
- 托管 API:按量计费、免运维、延迟稳定,适合更新频繁、想随时换模型的情况。是否有免费额度、怎么计价,请以服务商官方页面为准,不要按记忆中的价格估算。
- 本地 ollama:没有按次成本,数据不出机器,但需要足够的显存/内存,并发能力弱,且机器必须常开。中小规模模型在消费级显卡上能跑,更大的模型会明显变慢——具体速度以自己实测为准。
- 折中:本地小模型做初筛和分类(要不要推、属于哪个品类),托管模型只负责最终的少量摘要,成本和质量都能兼顾。
推送环节的小细节
- Telegram 的 Markdown 对特殊字符敏感,标题里的符号很容易导致整条消息发送失败,建议统一转义或改用更宽松的解析模式。
- 避免短时间内连续发送大量消息,超出频率限制会返回错误,需要做退避重试。
- 长文拆段发送时给每条带上编号或标题,避免读者看到一半失去上下文。
上线前的检查清单
- 词条标题做了归一化,判重键不会因为空格和符号差异失效
- 判重逻辑只对「首次出现」推送,排名变化不重复推送
- 判重键有过期时间,Redis 开启了持久化
- 工作流有独立的 HTTP 触发入口,调度器只做触发
- 任务有超时、重试与防重叠锁
- 连续失败会发出告警,而不是静默断更
- 搜索失败有降级策略,模型不会在无资料时编造内容
- 提示词约束了输出结构,包含来源链接
- 推送内容做了转义测试,含特殊字符的标题能正常发出
- 有品类过滤或人工抽查,避免频道变成八卦搬运
小结
这条流水线的技术难度不高,真正决定成败的是两件事:判重的边界(什么算新内容)和调度的可靠性(断了能不能发现)。把「抓取、判重、检索、摘要、推送」拆成互相独立的环节,每一步都能单独重跑,再配上一个只会定时触发的调度器和一条失败告警,才算真正能长期无人值守。至于热搜内容本身值不值得做成频道,答案更多取决于你有没有加那道筛选和解释的工序,而不是模型换成了哪个。