数据截至 (上游 commit fb951af7744d)
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_module | conv_simulator_lm / question_asker_lm(gpt-4o-mini) | conversation_log.json、raw_search_results.json |
| ② 大纲 | run_outline_generation_module | outline_gen_lm(gpt-4) | storm_gen_outline.txt、direct_gen_outline.txt |
| ③ 成文 | run_article_generation_module | article_gen_lm(gpt-4o) | storm_gen_article.txt、url_to_info.json |
| ④ 润色 | run_article_polishing_module | article_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_fs 调 StormInformationTable.from_conversation_log_file,_load_outline_from_local_fs 调 StormArticle.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_retrieval用SentenceTransformer("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 问出又深又广的好问题。那是下一章的主题。