last30days-skill 的 X 搜索 Retrieve-Judge-Retry:从 Rome 事故看如何用语料裁判修复离题洪流
本篇技术指南围绕 Retrieve-Judge-Retry 计划文档 展开,讲解 last30days-skill 如何修复 X(Twitter)搜索中"多词查询被短语引号包裹后返回大量离题内容"的问题:包括查询编译(Query Compilation)、扇出查询(Fanout Queries)、语料裁判(Corpus Judging)、一次性重试(Retry)与分裂式 FROM 提升(Split FROM Promotion)五个环节的完整设计。读完后,你将掌握该功能从 raw_topic 查询编译、x_judge 语料评分、重试触发条件,到 from:handle 通道门控的完整调用链,以及各项阈值常量的取值依据。
背景:一次真实事故暴露的级联失效
计划文档记录的 2026-08-14 "Rome 事故"是这项工作的直接动因。其失效链条有五个环节,每一环都在放大前一环的错误:
- Planner 生成了短语引号包裹的
search_query: "Rome Italy"; - 短语引号搜索只能命中极薄的结果,且夹杂着蹭流量的"美景城市"与地缘政治账号(PrettyCitiesX、visegrad24 等);
- entity_extract 按出现频率给这些离题账号排名,把离题账号排到了前面;
- pipeline.py 把这些高频率账号提升进 FROM 通道("该账号发的帖子");
- FROM 通道用离题时间线填满了 X 源的 40 个预算槽位。
关键教训是:频率不等于相关性。一个在"每个话题下都发帖"的蹭流账号会在任何检索结果中高频出现,按频率排名等于让垃圾账号购买免疫权。修复思路因此是"先检索、再裁判、必要时重试"(retrieve-judge-retry),而不是在检索之前猜对查询。
方案总览:七个环节各司其职
计划文档把解法拆成 R1–R7 七项,它们共同构成一条纵深防御链:
| 环节 | 内容 | 对应实现 |
|---|---|---|
| R2 查询编译 | X 与 Reddit/YouTube 一样使用 raw_topic,不再使用 planner 的 search_query |
pipeline.py 中 _retrieve_stream_impl 的 X 分支 |
| R3 扇出查询 | 多词主题用不带引号的 AND 作为第一变体,仅专有名词才用短语引号 | grok_x.py 中 _fanout_queries / _is_proper_name |
| R7 语料裁判 | 新增 x_judge.py 模块,检索后评估语料的离题比例 |
x_judge.py |
| R1 重试 | 检测到离题洪流(比例 < 0.4)时,在 X 流内部用简化关键词查询只重试一次(不走 _retry_thin_sources) |
pipeline X 分支内的 retry 逻辑 |
| R4 分裂 FROM 提升 | 显式 handle 永远走 FROM 且不 AND 主题;抽取 handle 需满足"≥2 条在题命中且 ≥50% 比例"才提升,且必须 AND 主题 | x_judge.promotable_handles + grok_x.search_handles(and_topic=...) |
| R5 第一方豁免 | Floor 免疫保持保守,只给显式 handle | pipeline.py 中 first_party_for_normalize |
| R6 状态报告 | 离题洪流产生 artifact 警告,而不是 record_failure(PARTIAL) |
pipeline X 分支的 artifact["_warnings"] |
查询编译:X 源改用 raw_topic
修复的第一刀在查询来源。_retrieve_stream_impl 的 X 分支现在这样编译查询:
if source == "x":
# Compile X query from raw_topic (like Reddit/YouTube), not planner's
# search_query which may contain operator strings like "Rome Italy".
x_query = raw_topic or topic or subquery.search_query
ranking_query = subquery.ranking_query
见 pipeline.py。这与 Reddit、YouTube、TikTok、Instagram 各分支的处理完全一致(均以 raw_topic or subquery.search_query 编译),意味着 X 源不再继承 planner 可能携带的算符字符串或短语引号变体。_fetch_x_backend 的 docstring 也明确记录了这一契约:query 参数是编译后的查询(通常是 raw_topic or topic),而不是 planner 的 search_query,见 pipeline.py。
另一个关键细节是 ranking_query:它是 planner 给出的、用于相关性打分的查询,裁判时若提供则优先于原始 topic 使用,这给了裁判一个比原始主题更精准的评分基准。
扇出查询:短语引号只留给专有名词
X 的 Grok 后端每次调用最多返回 10 条帖子(_MAX_LIMIT_PER_CALL = 10),深度靠扇出多个查询变体达成。_fanout_queries 生成"最宽信号在前"的变体序列:
def _fanout_queries(topic: str, from_date: str, to_date: str, calls: int) -> List[str]:
window = f"since:{from_date} until:{to_date}"
# First variant: unquoted AND (multi-word topics naturally AND their terms)
variants = [
f"{topic} {window}",
f"{topic} {window} min_faves:5",
]
# Third variant: phrase-quote only for proper names, else filter:links
if " " in topic and _is_proper_name(topic):
variants.append(f'"{topic}" {window}')
else:
variants.append(f"{topic} {window} filter:links")
variants.append(f"{topic} {window} -filter:replies")
return variants[:calls]
见 grok_x.py。规则是:
- 第一个变体永远是不带引号的 AND——多词主题词之间天然 AND,覆盖面最宽;
- 第三个变体只有在主题"看起来是专有名词"时才短语引号,否则改用
filter:links过滤出带链接的帖子; - 最后追加
-filter:replies变体排除回复帖。
_is_proper_name 的判定逻辑是:主题至少两个词、每词首字母大写其余小写,并且所有词不是地名/消歧词(内置了 italy、rome、paris、new york、los angeles 等地名词表)也不是全大写缩写。因此 "Peter Steinberger" → True(人名,短语引号),"Rome Italy" → False(地名消歧串,不引号)。见 grok_x.py。这正是成功标准中两条用例的出处:_fanout_queries("Rome Italy") 不得含 "Rome Italy" 变体,而 search_name("Peter Steinberger") 仍须短语引号。
语料裁判:x_judge 模块的三个阈值
新增的 x_judge.py 是纯计算模块(docstring 明确"No I/O: judges items already retrieved"),复用 relevance.py 的 tokenize,围绕三个阈值常量工作:
CORPUS_ON_TOPIC_FLOOR = 0.4 # 语料整体在题比例下限(Rome 实测约 0.2,8/40)
HANDLE_ON_TOPIC_FLOOR = 0.5 # 单个 handle 帖子在题比例下限,低于则不给 FROM 提升
MIN_ON_TOPIC_HITS = 2 # handle 至少要有 2 条在题命中才可提升
judge_x_corpus:四条函数中的核心
judge_x_corpus(items, topic, *, ranking_query="") 返回五个字段:on_topic_ratio、is_off_topic_flood、on_topic_items、off_topic_items、handle_stats。其判定逻辑值得注意的两点:
- 逐条打分:每条帖子用
_compute_relevance计算查询词覆盖度,达到relevance.RELEVANCE_FLOOR(0.1)记为在题。空语料直接返回比例 1.0、非洪流,避免空窗误判。 - 双条件洪流判定:
is_off_topic_flood为真的条件是——整体在题比例低于 0.4,或者"频率前三的账号在题比例全部低于 0.5 且在题条目不足 2 条"。这个复合条件专门捕捉那种"比例刚过线但头部账号全是离题账号"的隐蔽洪水。
_compute_relevance 还处理了一类经典歧义:_CASE_SENSITIVE_ACRONYMS = frozenset({'us'})。当查询 token 是 us 而文本里只有小写代词 "us"(无大写 "US")时,该 token 不计入匹配,防止 "Tell us what you think" 被误判为关于美国的帖子。这一行为由测试 test_us_pronoun_does_not_match_us_topic / test_us_acronym_matches_us_topic 精确锁定,见 test_x_judge.py。同理,"AI" 不会子串匹配到 "said"、"Rome" 不会匹配到 "promoter"——词元化是整词匹配而非子串匹配,test_x_judge.py 中有对应回归用例。
promotable_handles:分裂 FROM 提升的门控
promotable_handles(items, topic, extracted_handles, *, explicit_handles=None, ranking_query="") 返回二元组 (explicit_promotable, extracted_promotable):
- 显式 handle(
--x-handle/--x-related):无条件进入explicit_promotable,走 FROM 且不 AND 主题——因为人发的帖子通常不会带上自己的名字,AND 主题会把这条通道直接清空(这在search_handles的 docstring 中被标注为"prior defect"); - 抽取 handle(
entity_extract的产出):必须同时满足on_topic ≥ MIN_ON_TOPIC_HITS (2)且on_topic/total ≥ HANDLE_ON_TOPIC_FLOOR (0.5),才进入extracted_promotable,且这些拉取必须 AND 主题(from:handle Rome),以此证明账号确实产出在题内容。
注意它内部是按在题比例而非频率做门控——这正是对 Rome 事故第 3 环(频率排名)的直接反制。测试用例覆盖了所有门:mamboitaliano__(3 条在题)可提升、visegrad24(4 条中仅 1 条擦边)不可提升、只有 1 条在题的 handle 被 MIN_ON_TOPIC_HITS 卡掉,见 test_x_judge.py。
should_retry_x_search 与 prune_off_topic_items
should_retry_x_search(items, topic, *, ranking_query="", depth="default"):depth == "quick"或空语料直接返回 False(quick 深度不重试,与 Phase 2 的跳过策略一致),否则返回裁判的is_off_topic_flood;prune_off_topic_items(items, topic, *, ranking_query=""):只返回在题条目。docstring 解释了设计取舍:"8 条在题 + 32 条被剪 → 带着 8 条正常交付;0 条在题 → 报无结果,而不是带着 40 条垃圾交付"。
重试:X 流内部的一次性自救
_retrieve_stream_impl 的 X 分支在拿到后端结果后执行"裁判→重试→修剪"三步(见 pipeline.py):
- 触发:
items非空、depth != "quick"且非 mock 时,调用x_judge.should_retry_x_search(...); - 重试查询:触发后用 query.py 的
extract_core_subject(x_query)提取核心词(剥掉 "best/top/latest/news" 等NOISE_WORDS噪声词),得到retry_query,然后经_fetch_x_backend(used_backend, retry_query, ...)再查一次。注释强调"strip noise words but preserve all significant terms"——避免丢掉 "react server components" 这类消歧词。整个重试只走一次、复用原后端; - 择优:分别裁判重试语料与原语料,只有重试的
on_topic_ratio严格大于原语料才采用重试结果,并打印retry improved on-topic ratio: N% -> M%供诊断; - 修剪:无论是否重试,进入融合池之前都执行修剪——只保留在题条目(无文本的条目放行,无法裁判),并写入 artifact 警告
X: pruned N off-topic items; M on-topic remain。
这里有两个与 R6 直接对应的工程细节:
- 修剪警告写入
artifact["_warnings"],而不是bundle.record_failure(x_slug, PARTIAL, ...)。后者会把 X 源标记为 PARTIAL 状态,处于严格退出状态的白名单之外,会让使用LAST30DAYS_STRICT_EXIT的包装脚本在一份其实很健康的 X 覆盖上以退出码 3 失败。同文件中 partial coverage 的处理注释(pipeline.py)明确说明了这一权衡:"A warning, not a source outcome"。 - 该重试刻意放在 X 流内部,而不是复用
_retry_thin_sources——后者针对"结果太薄",而这里针对"结果太多但离题",是两种不同的失败模式。
分裂 FROM 提升在 pipeline 中的落点
Phase 2 的补充搜索 _run_supplemental_searches 是 promotable_handles 的调用方(见 pipeline.py):
# Split FROM promotion: determine which handles get FROM lane and how.
primary_explicit = [x_handle] if x_handle else []
explicit_promotable, extracted_promotable = x_judge.promotable_handles(
x_dicts, # Phase 1 X items for judging
topic, handles, # entity_extract handles
explicit_handles=primary_explicit,
ranking_query=ranking_query,
)
其后两条 FROM 通道分别执行,且 AND 策略不同:
# 显式 handle:FROM,不 AND 主题
explicit_items, ... = _from_lane(explicit_promotable, FROM_LANE_COUNT_PER, and_topic=False)
# 抽取 handle:FROM,AND 主题(from:handle Rome)
extracted_items, ... = _from_lane(extracted_promotable, FROM_LANE_COUNT_PER, and_topic=True)
and_topic 参数最终落到 grok_x.py 的 search_handles:
if and_topic and topic:
query = f"from:{clean} {topic} since:{from_date} until:{to_date}"
else:
query = f"from:{clean} since:{from_date} until:{to_date}"
即 search_handles(["steipete"], "topic") 默认不 AND 主题,而 search_handles(["visegrad24"], "Rome", and_topic=True) 会生成 from:visegrad24 Rome ...——两条都是成功标准中的验收用例。此外 --x-related 的 related handle 以 0.3 权重走独立的 supplemental-related 子查询标签,同样 and_topic=False,见 pipeline.py。
第一方豁免保持保守(R5)
promotable_handles 只决定"谁进 FROM 通道",不决定"谁享受第一方豁免"。归一化时的 first-party 集合只包含显式 handle:
# First-party handles: only primary explicit handle, not promoted commentators
# (first-party exempts from relevance floor; granting to commentators
# would let junk become un-prunable)
first_party_for_normalize = list(set(
h.lower().lstrip("@") for h in primary_explicit if h
))
见 pipeline.py。第一方状态会豁免相关性 floor、抬高单作者上限,属于强保护;如果按频率或按抽取授予,等于允许蹭流账号让垃圾内容变得不可修剪。这与 resolved_handles 构建处的注释("Require the handle to look like the topic's subject, or to have been named explicitly by the user")是同一套保守原则。
测试与验收标准
tests/test_x_judge.py 是该模块的专属测试文件,共五组:
TestJudgeXCorpus:空语料、正常语料、Rome 洪水语料(8 在题 + 32 离题,实测比例 < 0.4 且触发洪流判定)、子串碰撞防护(AI/said、Rome/promoter)、US 大小写歧义、handle 统计;TestPromotableHandles:显式 handle 恒提升、抽取 handle 的在题门控、MIN_ON_TOPIC_HITS门槛;TestShouldRetryXSearch:quick 深度跳过重试、洪流触发重试、好语料不重试;TestPruneOffTopicItems:修剪保留在题条目、全剪后返回空列表;TestRomeScenario:以 Rome 事故为原型构造 40 条语料(含 visegrad24 地缘帖、PrettyCitiesX 他城帖、AS Roma 足球帖、Rome Odunze NFL 帖——后两类是"Rome"一词的实体碰撞),断言整语料被识别为离题洪流、mamboitaliano__可提升、visegrad24/PrettyCitiesX不可提升、显式指定的 handle 恒提升。
计划文档列出的九条成功标准(fanout 无引号变体、专有名词仍引号、search_handles 默认不 AND、and_topic=True 时 AND、显式 handle 恒进 FROM、离题 handle 不提升、在题 handle 提升、状态为 artifact 警告而非 PARTIAL、全测试通过)与上述测试一一对应;tests/test_grok_x.py 补充了 fanout 与 and_topic 用例,tests/test_pipeline_v3.py 则更新了 fixture 以提供可提升内容。
明确的范围外事项
计划文档的 Out of Scope 部分同样值得保留,它界定了这项修复的边界:
- 测试中不发起真实 Grok 调用(
x_judge本身无 I/O,测试全部用构造语料); - 不触碰 auth/doctor 路径(Grok 鉴权探测由
stored_auth_status等独立函数负责,见 grok_x.py); - 不建碰撞词典——"Rome" 撞上 AS Roma(足球)和 Rome Odunze(NFL 球员)的情况依然存在,靠的是"裁判 + ranking_query 打分"把它们压下去,而不是维护同义词/异义词表;
bird_x保留引号的build_topic_query列为后续跟进项(当前 pipeline 中 bird/xquik 后端的search_handles尚不支持and_topic,见 pipeline.py 中的占位实现)。
小结
这套 retrieve-judge-retry 机制的核心思想可以概括为:检索侧放宽(不引号、多变体扇出)换取召回,裁判侧收紧(0.4 语体底线、0.5/2 次 handle 门控)换取精度,中间用一次带择优的重试和池前修剪兜底。它没有试图在检索前猜对每一次查询,而是承认 LLM 驱动的后端"可以自信地返回错东西"(grok_x.py 模块 docstring 原话),把质量保障移到检索之后、融合之前——这正是 Rome 事故五个环节中被逐一点掉的防御链。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00