STORM 接入统一 API:五个 LM 槽位一份配置

最后更新: 2026-07-16

STORM 把每个语言模型都构造成 LitellmModel,litellm 接受 api_base。共用的 openai_kwargs 字典里填 https://api.kkaiapi.com/v1,模型 ID 前加 openai/ 前缀,写报告管线的五个 LM 槽位就都能经一个端点和一个 Key,各自挑目录里的模型。

先说结论:openai_kwargs 里的 api_base,ID 前加 openai/

STORM 的 LitellmModel 会保存构造时传入的 kwargs,合并进每一次 litellm.completion() 调用。litellm 的 api_base 参数就是用来把 openai 供应商指向不同 host 的地方,所以把 api_base 加进 STORM 自己的示例代码本就在用的 openai_kwargs 字典,就是全部的覆盖动作。给每个模型 ID 前面加上 openai/ 前缀,让 litellm 对这个 base 说 chat-completions 协议,斜杠后面的字符串原样转发给网关。 因为示例代码构造一份 openai_kwargs 字典、给每个模型复用,加一个键就能重路由整条管线。不用改 STORM 代码,不用 fork;这就是 knowledge_storm 的标准行为,叠加在 litellm 文档记录的路由能力上。

openai_kwargs = {
    "api_key": os.getenv("KKAIAPI_KEY"),
    "api_base": "https://api.kkaiapi.com/v1",
    "temperature": 1.0,
    "top_p": 0.9,
}
fast = LitellmModel(model="openai/deepseek-v4-flash", max_tokens=500, **openai_kwargs)
strong = LitellmModel(model="openai/claude-sonnet-4-6", max_tokens=3000, **openai_kwargs)

STORM 怎么把一篇文章拆成五个 LM 槽位

STORM(GitHub 上的 stanford-oval,约 3 万星标)从零开始写百科式的研究报告:通过模拟的多视角对话研究一个主题,从学到的东西里建大纲,逐节生成全文,再做一轮润色。STORMWikiLMConfigs 把这条管线暴露成五个能独立设置的模型:conv_simulator_lm 和 question_asker_lm 驱动研究对话,outline_gen_lm 搭建文章结构,article_gen_lm 写正文,article_polish_lm 做最后一遍打磨。 上游 README 对经济账说得很直白:对话模拟器调用量最大,建议用一个更快的模型,文章生成建议用更强的模型。这个建议原本是假设在 OpenAI 各档模型之间选,放到多厂商端点背后会变得更有用。每个槽位都是独立的 LitellmModel,带自己的 model 字符串,研究阶段的对话可以跑一个快速的 DeepSeek ID,而大纲和正文生成跑 Claude,润色用你信得过文笔的那个模型,全部用同一个 Key 认证同一个 api_base。 检索这一侧是单独的机制:STORM 的 runner 接受一个 RM 模块(You.com、Bing 等多种搜索后端),带自己的 API Key。改动语言模型指向的地方,不会碰到信息来源怎么被抓取这件事。

完整配置:五个槽位,一份 kwargs 字典

实际能跑的写法照搬仓库自己的运行脚本:构造一份共享 kwargs,按角色各建一个 LitellmModel,再通过 STORMWikiLMConfigs 的 setter 分别赋值。api_key 这个键可以起任何你喜欢的变量名,因为是显式传进去的,示例用了一个专属变量名,清楚地表明这不是 OpenAI 账号的凭证。 litellm 也支持供应商级的环境变量,openai 供应商会读 OPENAI_API_BASE,所以纯环境变量的覆盖方式也可行。显式 kwargs 路径依然是更推荐的:它能在代码里看得见是什么产出了某篇文章,在环境状态不同的机器上依然成立,而且如果哪天想让某个阶段单独走不同端点,也留了余地。

import os
from knowledge_storm import STORMWikiRunnerArguments, STORMWikiRunner, STORMWikiLMConfigs
from knowledge_storm.lm import LitellmModel
from knowledge_storm.rm import YouRM

openai_kwargs = {
    "api_key": os.getenv("KKAIAPI_KEY"),
    "api_base": "https://api.kkaiapi.com/v1",
    "temperature": 1.0,
    "top_p": 0.9,
}
fast = LitellmModel(model="openai/deepseek-v4-flash", max_tokens=500, **openai_kwargs)
strong = LitellmModel(model="openai/claude-sonnet-4-6", max_tokens=3000, **openai_kwargs)

lm_configs = STORMWikiLMConfigs()
lm_configs.set_conv_simulator_lm(fast)
lm_configs.set_question_asker_lm(fast)
lm_configs.set_outline_gen_lm(strong)
lm_configs.set_article_gen_lm(strong)
lm_configs.set_article_polish_lm(strong)

engine_args = STORMWikiRunnerArguments(output_dir="./results")
rm = YouRM(ydc_api_key=os.getenv("YDC_API_KEY"), k=engine_args.search_top_k)
runner = STORMWikiRunner(engine_args, lm_configs, rm)
runner.run(topic="小型模块化反应堆")

按管线阶段选模型

把这五个 setter 当成预算刻度盘,不是样板代码。上游的建议已经说了要在阶段之间拆快慢模型,多厂商端点只是把每个阶段的菜单变宽了。同一个主题下每次只换一个槽位,对比输出,按 Key 的用量日志给每种配置定价。

  • conv_simulator_lm 和 question_asker_lm 是走量阶段:每个主题要跑好几个视角的多轮模拟访谈。deepseek-v4-flash 这类快速 ID 能防止研究阶段主导花费,不完美的对话可以接受,因为它只是喂给笔记,不是直接进正文
  • article_gen_lm 是旗舰槽位。它从积累的研究里写出长篇、结构化、带引用的段落,这是持续生成的活,claude-sonnet-4-6 或 gpt-5.5 明显强于小模型
  • outline_gen_lm 调用次数少但杠杆大,和规划类槽位是同一个形状:一个弱大纲会限死整篇文章的上限,不管写手有多好。这里是测 claude-opus-4-7 的自然位置
  • article_polish_lm 给已经拼好的文章重写、去重,受益于长上下文 ID;gemini-3.1-pro-preview 值得在这里跑跑分

主力模型价格

价格按 kkaiapi 中文区实时价格计算,单位 ¥/1M tokens(输入/输出)。

实时价格以 /pricing 为准
模型模型 IDkkaiapi 价格(输入/输出)适合槽位
DeepSeek V4 Flashdeepseek-v4-flash¥0.9 / ¥1.8对话模拟、提问(走量)
Claude Sonnet 4.6claude-sonnet-4-6¥3 / ¥15大纲、正文、润色
Claude Opus 4.7claude-opus-4-7¥5 / ¥25大纲(高杠杆)
GPT-5.5gpt-5.5¥4 / ¥25正文生成对比
Gemini 3.1 Pro Previewgemini-3.1-pro-preview¥7 / ¥42长上下文润色

STORM 专属的翻车点

裸模型 ID 按推断路由,不看你的 api_base。litellm 靠前缀选供应商,一个不带前缀的 Claude ID 会被推断成 Anthropic 原生调用,转而要求 ANTHROPIC_API_KEY,完全绕开你的网关。每一个打算走网关的 ID 都要带 openai/ 前缀;这个前缀说的是协议,不是厂商。 漏掉一个槽位。每个 LitellmModel 在构造时就固定了自己的 kwargs。如果四个槽位共用 openai_kwargs,第五个是临时手写的、没带 api_base,那个槽位会悄悄发到厂商默认地址,认证失败,而报错点名的是一个管线阶段,不是一行配置。所有槽位都从同一份字典构造,这类 bug 就不会出现。 检索失败被怪到端点头上。研究阶段需要一个能用的搜索后端;一个无效或额度耗尽的检索 Key(YDC_API_KEY、BING_SEARCH_API_KEY 或你选的其它 RM)会在信息收集阶段让运行失败。这个阶段和 LM 调用是交织的,先读报错是哪个客户端抛出来的,再去动 LM 配置。 demo 的 secrets.toml 不是脚本的配置。Streamlit demo 读 secrets.toml,程序化运行读脚本自己传入的东西。改了一边、跑的却是另一边,是个经典的对不上。 max_tokens 也是按槽位设的。STORM 的示例给走量槽位设小值(500),给生成阶段设大值(3000)。给一个长文本模型的槽位不调大它的 max_tokens,会悄悄截断章节,看起来像模型能力不够,其实是一个数字配置的问题。

谁会把 STORM 接到一个网关上

  • 批量生成知识报告(简报、类维基内部文档、主题入门)的团队,五槽位拆分让按阶段调成本值得真金白银去做
  • 研究管线组成的研究者:哪个阶段受益于更强的模型是个实证问题,一个端点能让槽位与模型的组合网格枚举起来轻而易举
  • 想在 OpenAI 形状的技术栈里用 Claude 或 Gemini 承担写作槽位、不想给每个模型家族加一个厂商 SDK 的开发者
  • 批量跑主题列表的人,研究阶段的调用量会随主题数放大,用量日志就是每主题的成本台账
  • 拿不到某家厂商官方支付渠道的开发者,充值即用不需要海外卡

验证接入与排查首篇文章

先列出网关的模型:每个槽位里 openai/ 后面的字符串必须和某个提供的 ID 精确匹配。 首次运行的失败按管线顺序排查。报错点名 Anthropic 或 Google,说明一个不带前缀的 ID 路由到了原生供应商;加上 openai/。网关返回 401,说明 kwargs 里的 api_key 不是网关 Key。模型不存在的报错点名了哪个槽位的 ID 拼错了。研究阶段的失败提到你的搜索后端,是检索凭据的问题,不是 LM 路由。文章段落被截断或异常短,通常是生成槽位的 max_tokens 设得太小,不是上游哪里出了问题。 一次完整的 STORM 运行是一次大爆发:跨多个视角的模拟对话,然后是大纲、生成、润色。跑完一次之后,kkaiapi 控制台会显示每次请求的模型、token 数和花费,这份数据能干净地对应到五个槽位,告诉你下一批主题该先调哪个阶段。

curl -s https://api.kkaiapi.com/v1/models \
  -H "Authorization: Bearer $KKAIAPI_KEY" | head -50

常见问题

STORM 怎么支持自定义 OpenAI 兼容端点?

通过 litellm。STORM 把每个语言模型都构造成 LitellmModel,它会把构造参数合并进每次 litellm.completion() 调用,litellm 的 openai 供应商接受 api_base。把 api_base 加进 openai_kwargs 字典,用这份字典构造的每个槽位都会路由到网关。

模型 ID 为什么要加 openai/ 前缀?

litellm 靠前缀选供应商。openai/claude-sonnet-4-6 的意思是"对着我的 api_base 说 OpenAI chat-completions 协议,模型是 claude-sonnet-4-6"。不带前缀,litellm 会从名字推断厂商、原生路由,绕开你的端点。

STORM 不同阶段能用不同厂商的模型吗?

能。五个槽位各自是独立的 LitellmModel,对话模拟器可以跑一个 DeepSeek ID,正文生成跑 Claude,润色跑 GPT,全部经同一个 api_base 和 Key。上游本来就建议在阶段之间拆快慢模型。

换 api_base 会影响搜索检索器吗?

不会。检索通过传给 STORMWikiRunner 的 RM 模块(You.com、Bing 等支持的后端)进行,带自己的 Key。LM 路由和信息来源检索是两个独立系统,在运行的不同阶段各自失败。

有环境变量路径替代 kwargs 吗?

litellm 支持供应商级变量,openai 供应商会读 OPENAI_API_BASE。能用,但显式的 api_base kwarg 更可复现:它跟着脚本走,在环境状态不同的机器上依然成立,还允许某个槽位单独走例外配置。

一篇 STORM 文章大概消耗多少 token?

研究阶段是大头:跨多视角的模拟对话在文章一个字都还没写出来之前就已经把调用次数放大了,之后生成和润色再叠加长篇输出。完整运行常见到几十万 token 的量级,按 Key 的用量视图能看到精确的按阶段拆分。