跳到主要内容

01 · STORM-Wiki 四阶段流水线

本章讲清 STORM-Wiki 这条主线整体怎么转:四个阶段各干什么、用哪个 LLM、产物是什么、怎么断点续跑。知识采集阶段的内部细节留到 02 章

1. 阶段总览:四个开关、四个产物

STORM-Wiki 的入口是 STORMWikiRunner.run(knowledge_storm/storm_wiki/engine.py:341-441)。它用四个布尔开关控制四个阶段,每个阶段都把结果写到 article_output_dir,且下一阶段若发现上一阶段对象为空,就从磁盘加载——这让你可以只重跑某一步。

阶段run_* 方法用的 LLM(默认)落盘产物
① 研究run_knowledge_curation_moduleconv_simulator_lm / question_asker_lm(gpt-4o-mini)conversation_log.jsonraw_search_results.json
② 大纲run_outline_generation_moduleoutline_gen_lm(gpt-4)storm_gen_outline.txtdirect_gen_outline.txt
③ 成文run_article_generation_modulearticle_gen_lm(gpt-4o)storm_gen_article.txturl_to_info.json
④ 润色run_article_polishing_modulearticle_polish_lm(gpt-4o)storm_gen_article_polished.txt

为什么不同阶段用不同 LLM? 这是刻意的“成本/质量平衡”:问问题/模拟对话量大但简单,用便宜的 mini 模型;大纲和成文需要更强推理,用大模型。配置集中在 STORMWikiLMConfigs(engine.py:21-124),每个属性以 _lm 结尾——这个命名约定被基类 LMConfigs 用来自动统计 token 用量(interface.py:443-473,靠 if "_lm" in attr_name 反射收集)。

2. 断点续跑是怎么做到的

run 里这段典型结构(engine.py:394-403):

# 示意,源码精简自 engine.py
outline = None
if do_generate_outline:
if information_table is None: # 上一步没在内存里
information_table = self._load_information_table_from_local_fs(...) # 从磁盘读
outline = self.run_outline_generation_module(information_table=information_table, ...)

每个阶段都这样:优先用内存里的对象,没有就从上一阶段落盘的文件加载_load_information_table_from_local_fsStormInformationTable.from_conversation_log_file,_load_outline_from_local_fsStormArticle.from_outline_file(engine.py:312-339)。重点看符号 from_conversation_log_file / from_outline_file / from_string——它们是“产物 ↔ 数据类”的反序列化入口。

3. 阶段②:大纲生成(先盲写,再改进)

它要解决的小问题: 怎么从一堆零散对话里得到一个合理的章节结构?

思路: STORM 用了“两步走”——先让 LLM 只凭自身知识盲写一个草稿大纲(draft_page_outline),再把模拟对话喂进去改进这个草稿(write_page_outline)。两个产物都落盘(storm_gen_outline.txt 是改进后的,direct_gen_outline.txt 是草稿),方便对比。

真实实现 WriteOutline.forward(knowledge_storm/storm_wiki/modules/outline_generation.py:84-125):

# 示意,源码精简自 outline_generation.py
if old_outline is None:
old_outline = clean_up_outline(self.draft_page_outline(topic=topic).outline) # 盲写草稿
outline = clean_up_outline(
self.write_page_outline(topic=topic, old_outline=old_outline, conv=conv).outline # 据对话改进
)

关键细节: 喂给改进步骤前,对话历史被 limit_word_count_preserve_newline(conv, 5000) 截到 5000 词,并先剔除掉“包含 ‘topic you’ 的废话轮次”(outline_generation.py:91-98)。大纲文本统一用 clean_up_outline 清洗:把 - 列表项转成对应层级的 # 标题、删掉 References/See also/Bibliography 等维基尾部章节、去掉残留引用(utils.py:457-503)。

4. 阶段③:分节成文(并行 + 语义取材)

它要解决的小问题: 大纲有了,怎么给每个章节配上正确的资料并写出带引用的段落?

思路:一级章节并行写。每个章节用它的标题+子标题做查询,从信息表里按嵌入相似度召回 top-k 资料,再让 LLM 据此写这一节。

主控逻辑 StormArticleGenerationModule.generate_article(knowledge_storm/storm_wiki/modules/article_generation.py:53-133):

# 示意,源码精简自 article_generation.py
information_table.prepare_table_for_retrieval() # 把所有 snippet 编码成向量
for section_title in sections_to_write: # 一级章节
if section_title.lower() in ("introduction", ...): continue # 不单独写引言/结论
section_query = article_with_outline.get_outline_as_list(root_section_name=section_title)
executor.submit(self.generate_section, topic, section_title, information_table, ...)

几个值得注意的设计:

  • 不单独写引言和结论。 代码显式跳过标题为 introduction、或以 conclusion/summary 开头的章节(article_generation.py:96-103)——引言交给后面的润色阶段(导语 lead)统一处理。
  • 取材是“向量检索”而非全量喂入。 prepare_table_for_retrievalSentenceTransformer("paraphrase-MiniLM-L6-v2") 把所有 snippet 编码;retrieve_information 对每个查询算 cosine 相似度取 top-k(storm_dataclass.py:109-145)。
  • 写完后做引用重映射。 update_section 会把本节局部的 [1][2] 重映射到全文统一编号,并丢弃越界/未使用的引用(storm_dataclass.py:249-299)——细节见 04 章

章节生成的 prompt 在 WriteSection(article_generation.py:162-176),明确要求行内 [1][2] 引用、且“不要在末尾再列 References”。

5. 阶段④:润色(加导语 + 可选去重)

它要解决的小问题: 初稿没有“开篇综述”,且分节并行写容易内容重复。

思路: 两件事——(1)用 WriteLeadSection 写一段不超过四段的导语,作为 # summary 插到文章最前面;(2)可选地用 PolishPage 整篇去重。

StormArticlePolishingModule.polish_article(knowledge_storm/storm_wiki/modules/article_polish.py:29-53):

# 示意,源码精简自 article_polish.py
polish_result = self.polish_page(topic=topic, draft_page=article_text,
polish_whole_page=remove_duplicate)
lead_section = f"# summary\n{polish_result.lead_section}"
polished_article = "\n\n".join([lead_section, polish_result.page])

关键细节: 去重是可选的(remove_duplicate,默认 False),因为它要多一次整篇 LLM 调用,成本高。PolishPage 的 prompt 把模型定位成“忠实编辑”,只删重复、保留所有引用和 # 结构(article_polish.py:68-69)。导语插入时,insert_or_create_section 检测到 section 名为 summary 会用 insert_to_front=True 插到最前(storm_dataclass.py:233-239)。

6. 小结:这条线的“魂”在哪

大纲/成文/润色都是相对标准的 RAG 写作流程——STORM 真正的创新与工程难点,几乎全在阶段①知识采集:怎么让 LLM 问出又深又广的好问题。那是下一章的主题。