last30days-skill 持久化发现主题队列设计:模糊匹配、状态继承与收尾写入的五个设计规约 📅 发布时间:2026/9/7 16:12:25 👁 浏览次数: last30days-skill 持久化发现主题队列设计模糊匹配、状态继承与收尾写入的五个设计规约【免费下载链接】last30days-skillAI agent skill that researches any topic across Reddit, X, YouTube, HN, Polymarket, and the web - then synthesizes a grounded summary项目地址: https://gitcode.com/GitHub_Trending/la/last30days-skilllast30days-skill 的/last30days discover流水线会在每次真实运行结束时把本次浮现的主题记录进research.db的discovery_topics表让后续运行记得哪些故事已经见过surfaced 3rd time、哪些用户已经产出过内容marked covered。本篇基于仓库中的设计记录docs/solutions/architecture-patterns/discovery-topic-queue-design-conventions.mdPR #852 的设计文档逐条拆解支撑这套队列的五个相互咬合的设计规约并结合 store.py、last30days.py、env.py 的源码与 test_store.py 的回归测试给出可验证的实现细节。读完后你将掌握如何在昂贵流水线的末端安全地挂载本地持久化、如何在 LLM 命名漂移之上构建可靠的模糊身份层以及如何在本仓库的.env配置约定下正确注册功能开关。背景discover 需要跨运行记忆一次 discovery 运行会扫过多个来源产出一份包含多个主题topics的排序报告。同一个故事例如某个模型的模板修复可能在接下来几周反复出现用户可能已经基于其中某个故事制作了播客或文章不希望流水线再把它当作新故事推荐。PR #852 为此引入了持久化主题队列每次真实非 mock运行都记录浮现的主题渲染时给已见过/已覆盖的主题打上管线注释。队列的数据结构是discovery_topics表随 schema v3 建表store.py L228-243CREATE TABLE IF NOT EXISTS discovery_topics ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, normalized_name TEXT NOT NULL UNIQUE, entity_key TEXT, domain TEXT, first_surfaced TEXT NOT NULL, last_surfaced TEXT NOT NULL, surface_count INTEGER NOT NULL DEFAULT 1, status TEXT NOT NULL DEFAULT surfaced CHECK(status IN (surfaced,covered)), covered_at TEXT, last_run_ref TEXT );几个值得注意的设计点normalized_name是唯一的身份锚点小写化、去标点、折叠空白后作为UNIQUE键重复浮现走ON CONFLICTupsert 路径entity_key在写入时一次性计算排序后拼接的实体 tokenstore.py L819-821供后续模糊匹配复用避免每次匹配重复抽取status只有两个值surfaced和coveredCHECK约束covered是用户显式设置的例如queue cover 主题名语义是我已就这个故事产出过内容last_run_ref记录运行身份为同一运行内重试不重复计数的幂等保护提供依据见下文规约 4 的补充。设计文档指出这五个规约在代码评审中各自都是承重墙其中两条直接对应真实缺陷一个 P0。下面逐条展开。规约 1默认开启只通过配置允许列表关闭——永不裸读 os.environ队列对每次真实运行默认生效只有字面值off才会禁用队列写入与注释。开关LAST30DAYS_DISCOVERY_QUEUE注册在env.get_config的键允许列表中env.py L501-503# Discovery topic queue (podcast/X-article pipeline memory). Default # ON; the literal value off disables queue writes and annotations. (LAST30DAYS_DISCOVERY_QUEUE, None),读取端始终从get_config解析后的配置字典取值而不是os.environlast30days.py L1658-1660queue_setting str(config.get(LAST30DAYS_DISCOVERY_QUEUE) or ).strip().lower() if queue_setting off or not report.topics: return report为什么如此严格在本仓库中.env文件里的值只有经过get_config的允许列表合并后才会到达引擎代码——直接os.environ.get()会静默忽略.env用户的配置。设计文档把这类开关在维护者 shell 里生效、对文件配置的所有人静默失效的写法称为本仓库已记录的隐形失效类别invisible-failure class。由此得到的硬性规则是本仓库新增任何引擎开关都必须先在env.get_config的允许列表里注册再从配置字典读取永不裸读os.environ。配套的两个边界约束同样值得保留作用域隔离带--save-dir的运行通过store.scoped_db(_scoped_store_db(args))写入作用域内的research.db绝不写全局库store.py L41-57 实现了这个上下文管理器块内把全局_db_override指向目标路径退出时恢复mock 零副作用--mock运行完全跳过队列钩子保证 100% 无副作用last30days.py L1712-1731 的守卫路径仅在非 mock 时调用。规约 2只做注释的模糊匹配——匹配只盖章绝不合并行store.match_discovery_topicstore.py L912-954的匹配策略是两级的先试精确匹配对归一化名称做normalized_name等值查询再取最佳实体重叠候选取完整entity_keytoken 重叠与锚点 token 重叠两者中的较优值且必须达到保守地板DISCOVERY_QUEUE_OVERLAP_THRESHOLD 0.6 # store.py L810 ... if best is not None and best_overlap DISCOVERY_QUEUE_OVERLAP_THRESHOLD: return dict(best)其中锚点 token 由_discovery_anchor_entities计算store.py L824-839只保留首字母大写、全大写或带数字的词产品/人物/版本锚点剔除 chat、templates 这类通用小写词。这样 Gemma 4 chat templates 与 Gemma 4 tool calling fixes 能靠gemma/4两个锚点互相命中而共享填充词的不同主题不会被误连。源码注释还解释了为什么不能只用全 token 重叠完整 token 重叠会被通用词稀释同主题的近似重复反而永远到不了保守地板。关键约束是语义上的annotate-only模糊匹配只给渲染卡片盖章上下文——即 render.py L181-198 输出的**Pipeline:** surfaced 2nd time, marked covered行——绝不合并或改写队列行。这个取舍的逻辑是不对称的代价模型误报匹配的成本是某张卡片上多一行噪音可恢复而误合并会把两条不同的故事静默折叠成一行其中一条从此被永久隐藏不可恢复的数据丢失。阈值0.6之所以敢做成可调常量恰恰因为标签错了还能改回来数据丢了回不来。规约 3两阶段钩子——先匹配所有主题再记录任何主题钩子函数_annotate_and_record_discovery_queuelast30days.py L1636-1709在一个store.scoped_db块内分两阶段执行with store.scoped_db(_scoped_store_db(args)): store.init_db() # Phase 1: match EVERY topic before recording ANY. Interleaving # matchrecord in one loop lets topic N fuzzy-match a same-anchor # sibling row this very run recorded seconds earlier, falsely # annotating a first-ever topic as surfaced 2nd time. priors [ _pre_run_prior_state(prior, run_ref) for prior in ( store.match_discovery_topic(topic.name) for topic in report.topics ) ] # Phase 2: record this runs surfacings. ... for topic, prior in zip(report.topics, priors): ... store.record_discovery_surfacing(...)为什么不能在一个循环里边匹配边写入一份报告经常包含同锚点的兄弟主题比如 Gemma 4 chat templates 与 Gemma 4 tool calling fixes。如果交替执行 matchrecord主题 N 会模糊命中主题 N-1 几秒前刚写入的行把一个首次出现的主题错误标注成surfaced 2nd time。设计文档明确这条缺陷是在评审中抓出的并已做回归测试。注意这个缺陷更隐蔽的地方损坏发生在单次运行内部跨运行测试天然抓不到它必须专门构造同一份报告内两个同锚点主题的用例才能覆盖。从源码结构看当前实现比设计文档还多了一层防御Phase 1 的每个 prior 都会经过_pre_run_prior_state(prior, run_ref)折叠——如果命中的行本来就是本次运行身份同一run_ref例如--finalize重试刚盖的章就把它还原为运行前状态计数减掉本次、保留 covered 状态保证重试渲染出的报告与首次完全一致而不是宣称了一次并不存在的再次浮现。规约 4covered 继承——新行出生即 covered已有行永不被动record_discovery_surfacing(inherit_covered_at...)store.py L842-909的 upsert 逻辑分两条路径行为截然不同status covered if inherit_covered_at else surfaced ... conn.execute( INSERT INTO discovery_topics (name, normalized_name, entity_key, domain, first_surfaced, last_surfaced, surface_count, last_run_ref, status, covered_at) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?) ON CONFLICT(normalized_name) DO UPDATE SET surface_count surface_count 1, last_surfaced excluded.last_surfaced, last_run_ref excluded.last_run_ref, domain CASE WHEN excluded.domain THEN excluded.domain ELSE domain END, ... )新行INSERT 路径若调用方传了inherit_covered_at该行出生即 coveredcovered_at设为给定的日期已有行ON CONFLICT 路径只更新surface_count、last_surfaced、last_run_ref以及非空时的domain——刻意不碰status和covered_atinherit_covered_at参数对已有行完全无效。调用方在主题的 prior 处于 covered 状态时传入继承参数last30days.py L1689-1699for topic, prior in zip(report.topics, priors): inherit_covered_at None if prior and prior[status] covered: inherit_covered_at prior[covered_at] or prior[last_surfaced] store.record_discovery_surfacing( topic.name, domainreport.domain, run_refrun_ref, as_ofas_of, inherit_covered_atinherit_covered_at, )为什么必须这样根因是LLM 评审judge会跨运行给同一个故事改名。没有继承规则时一次改名就会 fork 出一条全新的未覆盖行用户辛苦盖的 covered 标记静默蒸发流水线开始反复推荐用户已经做过的故事——这恰好是队列存在要防止的失败。反过来没有永不改写规则时一次陈旧的继承参数又能把用户刚刚手动改过的行状态翻回去。两条规则合起来才是完整的不变量用户的 covered 标记只增不减、只随新行迁移不被任何启发式改写。这条规约被三条回归测试锁死tests/test_store.py L1045-1101test_record_discovery_surfacing_inherit_covered_creates_row_born_covered新行带inherit_covered_at时出生即 coveredcovered_at取继承值test_record_discovery_surfacing_inherit_never_alters_existing_row_statusON CONFLICT 路径无视inherit_covered_at——surfaced 行保持 surfacedcovered 行保持 covered 且covered_at不变test_covered_status_survives_judge_rename_across_runs完整的 flip-flop 跨运行回归见后文章节。补充一个源码中的幂等细节当已有行的last_run_ref等于本次调用的run_ref时函数直接返回原行不做任何计数store.py L883-889由test_record_discovery_surfacing_same_run_ref_is_idempotent覆盖——这防止--finalize这类重试把一次浮现计成两次。规约 5守卫式、同步的收尾写入——绝不弄崩已完成的流水线钩子在主流程中的调用点被_record_discovery_queue_safely包装last30days.py L1712-1731def _record_discovery_queue_safely(report, args, config, run_refNone): Annotate record the discovery queue, degrading a broken research.db (locked, read-only dir, corrupt) to a stderr warning: a queue failure must never destroy a finished pipeline run or the protocols final brief. try: return _annotate_and_record_discovery_queue(report, args, config, run_refrun_ref) except (sqlite3.Error, OSError) as exc: sys.stderr.write( f[last30days] Warning: discovery queue unavailable ({exc}); continuing without queue annotations.\n ) return report没有这道守卫时一个被锁定、只读目录或损坏的research.db会在多分钟研究流水线完成之后抛异常把全部产出丢弃。设计文档称这正是 PR #852 评审中的 P0且被实际复现过。失败时的用户可见行为是确定的报告照常完整渲染输出stderr 出现一行[last30days] Warning: discovery queue unavailable (database is locked); continuing without queue annotations.卡片只是缺少 Pipeline 注释行字段保持默认值。另一个约束是同步执行写入发生在流水线返回之后、阻塞完成函数 docstring 明确写了这一点。原因在于它要触盘——本仓库禁止对这类写盘操作使用超时即抛弃的守护线程模式理由记录在 non-daemon-executor-threads-defeat-wall-clock-budget.md后台线程在墙钟预算到期后仍可能继续执行而跑了一半的写入比没写更危险。违反规约的影响面按爆炸半径排序设计文档把五条规约按违反时的爆炸半径排序这个排序本身就是很好的设计评审参考无守卫的收尾写入规约 5整次运行的输出被一次簿记失败毁掉且只发生在降级环境db 被锁、目录只读——CI 全绿恰好在你看不见的机器上引爆。这是评审的 P0。交替 matchrecord规约 3队列的核心承诺你第一次见到这个从第一天起就是错的——首次出现的主题被同运行的兄弟标注成surfaced 2nd time而且跨运行测试抓不到因为损坏发生在单次运行内部。裸读 os.environ规约 1.env用户关不掉队列开关在维护者 shell 里生效对所有文件配置者静默失效。模糊匹配即合并规约 20.6 重叠的一次误报从一行噪音升级为一个被隐藏的故事——启发式造成的不可恢复数据丢失。改写行或跳过继承规约 4用户的 covered 标记随 judge 命名漂移来回翻转队列开始反复推荐用户已产出的故事——队列存在的意义被自己推翻。典型场景推演Covered flip-flop原型性的三次运行场景与 tests/test_store.py L1082-1101 的测试镜像一致Run 1浮现 Gemma 4 chat templates用户录制了节目并执行queue cover Gemma 4 chat templates行状态变为 coveredcovered_at run 1 日期Run 2的 judge 把同一个故事命名为 Gemma 4 template fixes。精确匹配落空模糊匹配锚点gemma/4重叠 ≥ 0.6找到 covered 的 prior新行出生即 covered卡片渲染Pipeline: surfaced 2nd time, marked covered而不是把它当新故事推荐Run 3再次浮现 Gemma 4 template fixes精确命中自己那条 covered 行covered_at仍是 run 1 的日期。若没有规约 4run 2 会 fork 出未覆盖行run 3 就会重新推荐用户已经覆盖过的故事。队列失败行为当research.db被另一进程锁定时discovery 运行仍打印完整渲染报告stderr 显示[last30days] Warning: discovery queue unavailable (database is locked); continuing without queue annotations.卡片只是没有 Pipeline 行。何时套用这套规约设计文档给出的适用判据也是可直接移植的检查清单任何挂在昂贵流水线末端的默认开启本地持久化写入必须带守卫降级为警告而非异常且触盘时同步执行任何构建在 LLM 命名实体之上的模糊身份层匹配保持 annotate-only单次运行内所有匹配先于所有写入批量完成用户设置的状态通过继承到新行而非改写已有行来存活本仓库新增任何引擎开关先注册进env.get_config的键允许列表再从解析后的配置字典读取永不裸读os.environ。延伸阅读同一 PR 中种子来源佐证seed-source corroboration规则的独立设计记录ranked-output-confidence-floor-honest-empty-state.md 第 2b 节为什么放弃超时守护线程的模式不适用于触盘写入器non-daemon-executor-threads-defeat-wall-clock-budget.md队列 API 与回归测试全集tests/test_store.py、发现模式端到端测试 tests/test_discover_mode.py。【免费下载链接】last30days-skillAI agent skill that researches any topic across Reddit, X, YouTube, HN, Polymarket, and the web - then synthesizes a grounded summary项目地址: https://gitcode.com/GitHub_Trending/la/last30days-skill创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考