ブログ記事を LLM に書かせる時、記事全体を 1 回で生成する か セクションごとに分けて生成する かで分岐がある。
- 1 回で生成: 文脈は繋がるがトークン数が巨大になり、長文になるほど各セクションが短くなりがち。Gemini の 1M context 以外だと収まらないこともある
- セクション別生成: 各セクションに focus できる。ただし順番や文脈連続性をどうするかが問題
content-factory ではセクション別生成を選んだ。で、次に出てくるのが「セクション生成を並列化していいか」という話。5 セクションを逐次で生成すると 3〜4 分、並列化すれば 1 分以下になる。
結論から言うと 並列化はできるが、文脈連続性とのトレードオフ になる。この記事では逐次 / 並列の実装差、previousSectionTail で文脈を繋ぐ逐次モードの工夫、1〜5 並列で切り替えられる実装を書く。
逐次モードの基本形

LLM に各セクションを順番に書かせる。前のセクションの末尾を次のセクションに渡して連続性を保つ:
async generateBodySequential(h2Sections: Section[]): Promise<string> {
const bodyParts: string[] = [];
let previousSectionTail: string | null = null;
for (const section of h2Sections) {
const content = await this.generateSectionContent(
section,
mainKeyword,
previousSectionTail, // ← 前セクション末尾 200 字
);
bodyParts.push(content);
previousSectionTail = content.slice(-200);
}
return bodyParts.join("\n\n");
}
ポイントは previousSectionTail = content.slice(-200)。セクション末尾の 200 字を次のプロンプトに挿入する:
前セクション末尾:
{previousSectionTail ?? "(最初のセクション)"}
---
続くセクション「{section.heading}」を書いてください。
これで LLM は「さっき何を書いたか」を意識してブリッジ(「前述の通り〜」「これを踏まえて〜」)を自然に入れられる。
文脈連続性は高い。ただし 5 セクション × 1 セクションあたり 30〜60 秒 = 2.5〜5 分 かかる。体感としては遅い。
並列モードの実装
前セクション末尾の参照を 諦めて、Promise.all でバッチ並列:
async generateBodyParallel(h2Sections: Section[], concurrency: number): Promise<string> {
const bodyParts: string[] = new Array(h2Sections.length).fill("");
for (let i = 0; i < h2Sections.length; i += concurrency) {
const batch = h2Sections.slice(i, i + concurrency);
const results = await Promise.all(
batch.map((section) =>
this.generateSectionContent(
section,
mainKeyword,
null, // ← previousSectionTail 無し
),
),
);
results.forEach((content, j) => {
bodyParts[i + j] = content;
});
}
return bodyParts.join("\n\n");
}
ポイント:
- 配列 index で位置を保持:
bodyParts[i + j] = contentで並行実行しても最終的な並び順は維持 - バッチ単位: 5 セクションを 2 並列なら [0,1] → [2,3] → [4] の順でバッチ実行
- previousSectionTail は null: 参照できないので諦める
5 セクションを 3 並列にすると 1 セクションあたり約 45 秒 → 実時間 90 秒 まで短縮できる。ほぼ 3 倍速。
並列の代償

並列モードで書かれた記事を読むと、だいたい 10〜20% くらい「違和感」が出る:
- 各セクションが完結しすぎて、記事全体の流れが断絶する
- 同じ例え・同じ固有名詞が複数セクションに重複して出る(LLM が知らないので)
- 主張の粒度が揃わない(あるセクションは結論的、別のセクションは問題提起的、みたいな不揃い)
逐次モードと比べると、文章としての「ひと続き感」 が落ちる。
これを後工程でどう補正するか、あるいは「そこまでの品質は要らない」と割り切るかの判断が要る。
統合モードの切り替え
content-factory では ユーザーが 1〜5 で並列度を切り替えできる 設計:
type LLMSettings = {
parallelSections: number; // 1 = 逐次, 2〜5 = 並列
// ...
};
UI にはスライダーを置いた。デフォルトは 1(逐次)。ユーザーが「速度優先」と思えば 3 にする、みたいな運用。
実装側はエントリポイントで分岐:
private async generateBody(
h2Sections: Section[],
concurrency: number,
// ...
): Promise<string> {
const effectiveConcurrency = Math.max(1, Math.min(5, Math.floor(concurrency)));
if (effectiveConcurrency <= 1) {
return this.generateBodySequential(h2Sections);
}
return this.generateBodyParallel(h2Sections, effectiveConcurrency);
}
Math.max(1, Math.min(5, ...)) で 1〜5 にクランプ。ユーザーが 100 とかを設定しても LLM rate limit で詰まるだけなので上限を 5 に設定。
Rate limit との兼ね合い

並列度を上げると当然 LLM API の rate limit に当たりやすくなる。content-factory で使っている主要モデルの実測 / 公称値は以下:
| モデル | RPM(公称 or 体感) | 体感の限界並列 |
|---|---|---|
| Gemini 2.5 Flash(free tier) | 5 RPM(AI Studio 公称) | 2〜3 |
| Gemini 2.5 Flash(paid tier) | 1000 RPM | 5+ |
| Codex CLI(subscription) | 公称値なし、体感では軽負荷時 3〜5 req/分程度 | 1〜3 |
| Claude Sonnet(CLI subscription) | 公称値なし、体感では連続 2〜3 回でも詰まることあり | 1〜2 |
Claude Sonnet CLI で並列 3 以上にすると体感で詰まることが多い。その場合は withRetry(指数バックオフ)で透過的に吸収するか、並列度を下げるか、別モデルに逃すかの選択になる。
content-factory では withRetry を挟んであるので 1 回の rate limit hit は透過的に吸収される。でも連続して詰まると maxRetries 超過で失敗する。その場合は部分失敗が発生する。
部分失敗時の補償

並列で 5 セクション生成、1 セクションが rate limit で失敗した場合どうするか。
現状の content-factory は 全体が throw して全部失敗 にしている(atomicity 優先)。理由:
- 部分的に生成された記事を保存すると「どこから欠けているか」のフラグ管理が要る
- ユーザーから見て「5 セクションのうち 1 つ欠けた下書き」より「エラーで全部やり直し」のほうが mental model がシンプル
- 失敗時のコストは「LLM 呼び出し 5 回分」。1 記事あたり数円なので許容
より本格的には「失敗したセクションだけリトライして差し込み」も可能だが、複雑性が跳ね上がるので採用しなかった。個人開発だと「全部やり直し」の素朴さが勝つ。
実運用の設定感
実際にユーザー(俺)はどう設定しているか:
- デフォルト運用:
parallelSections: 1(逐次)。文脈連続性を取る - 速度優先のテスト記事:
parallelSections: 3。品質はやや落ちるが 3 倍速く出る - 長文記事(8 セクション以上):
parallelSections: 2。完全並列だと文脈が崩れすぎるので、半分並列で妥協
トレードオフを理解した上で、記事のタイプ別に並列度を変える のが実務的。
まとめ

- LLM でセクション別生成するなら、逐次 / 並列の選択肢がある
- 逐次は文脈連続性が高い が時間がかかる。
previousSectionTailで前セクション末尾を渡すと自然な繋ぎになる - 並列は速い が文脈が独立する。重複表現や主張のブレが増える
- Promise.all + バッチ単位の concurrency で実装、
Math.max(1, Math.min(5, ...))でクランプ - Rate limit を考慮して上限を 5 にする。Claude 系は 1〜2 並列が無難
- ユーザーが切り替えられる設計 にすると、記事タイプ別の最適化が効く
- 部分失敗は全体 throw で素朴に扱う(個人運用なら許容)
同じ悩みを持っている人(LLM で長文を分割生成したいが速度が気になる)は、逐次をデフォルト + 並列オプション という二段構えがおすすめ。毎回全力並列にすると「書き直しが増えて結局時間かかってた」状況になりやすい。
並列度を自分で設定できるようにすれば、記事によって「速度を取るか質を取るか」を都度選べる。5 並列固定より遥かに実用的。


コメント