LLMによる大規模な技術文書生成

LLMによる大規模な技術文書生成

「資料をLLMに放り込めば、文書一冊くらいすぐに作れるだろう」と考えた瞬間から、実際の問題解決が始まりました。

最近参加したアーキテクチャコンサルティングプロジェクトで、数十個のビジネス境界(Bounded Context)と数百個のサービスを整理した大規模なサービス定義書を作成する必要がありました。入力資料は、複数のシートで構成されたサービスカタログのExcelファイルと、既存の分類体系を整理した24ページのWord文書でした。目標は、これらの資料を基に分類軸を変更した新しい定義書を、統一された書式のWord文書として作成することでした。

最初は単純に考えていました。資料をLLMに渡し、「この構造で文書を作成して」と依頼すればよいと思っていたのです。しかし、実際の成果物は予想とはまったく異なりました。最初の成果物はなんと187ページにも及び、分量だけでなく書式や構造も、そのままでは使用できない状態でした。

この記事では、LLMで大規模な技術文書を生成する際に直面した、分量の急増と書式崩壊の問題を、入力分割・構造再設計・生成ツールの切り替え・整合性検証という4段階で解決していった過程を整理します。最終的に、187ページの失敗作を使用可能な43ページの最終版に仕上げるまでの試行錯誤と、その過程でLLMの出力をどのようにコードと連携させ、検証したかについても説明します。

1. 技術選定の背景 — なぜ「LLMに丸ごと任せる」方法は失敗したのか

最初の試みは、最も単純な方法でした。入力資料全体をLLMに渡し、希望する文書構造を自然言語で説明したうえで、成果物を受け取るという方法です。しかし、この方法には3つの側面で限界がありました。

1つ目は、分量の急増です。最初の成果物(v1)は187ページでした。原因を分析すると、LLMが数百個の個別サービスを、それぞれ独立したセクション(見出し)として展開して記述していたためでした。サービスごとにタイトルと説明文が付くため、分量が幾何級数的に増加しました。人間が期待していたのは「サービスを表の行に圧縮した形式」でしたが、LLMは明示的に制御しない限り、すべての項目をできるだけ詳しく展開して書く傾向がありました。

2つ目は、書式の不統一です。表のスタイルがセクションごとに異なり、見出しレベルもばらばらで、表紙や目次はまったく生成されないか、形式が合っていませんでした。文書全体を貫く一貫した書式ルールが存在しなかったのです。

3つ目は、再現性の欠如です。同じ資料で再度依頼しても、セクション構成と分量が毎回異なりました。大規模文書では、これは致命的でした。一度レビューを終えても、再生成すると構造が崩れるため、レビュー自体が無意味になってしまいました。

結局のところ、LLMは「内容を生成する技術」であって、「大規模文書を構造的に組み立てる技術」ではないということでした。特に、分量と書式の責任をLLMの自然言語出力に丸ごと任せる限り、安定した成果物は得られませんでした。

そこで、以降のアプローチを完全に変更しました。次の2つを分離して制御することにしたのです。

  • 構造・分量:文書の骨格(どの項目を表に圧縮し、どれをセクションにするか)を人間が明示的に定義

  • 書式・レンダリング:表のスタイル、色、見出し、表紙・目次などの視覚的要素をコードが一貫して担当

つまり、LLMは「何を書くか(コンテンツ)」だけを担当し、「どのように構成し、描画するか(構造と書式)」は人間とコードが制御する構成へと切り替えました。

2. 適用プロセスと実装

作業全体は、大きく4つの段階に分けて進めました。入力分割、構造再設計、生成ツールの切り替え、整合性検証です。各段階を独立して検証できるように分離し、問題が発生した場合に、どの段階に起因するのかを迅速に追跡できるようにしました。

2.0 入力分割 — コンテキストウィンドウの制限を考慮したBC単位の呼び出し

実装に先立って最初に決めなければならなかったのは、入力の渡し方でした。入力資料には数十個のビジネス境界と数百個のサービスが含まれていたため、これらを1回のプロンプトにすべて詰め込むと、2つの問題が懸念されました。1つはトークン制限(Context Limit)、もう1つは長い入力の中央部分にある情報が失われる現象(Lost in the Middle)です。

そこで、入力資料を一度に渡すのではなく、ビジネス境界(BC)単位に分割し、ループで個別にLLMを呼び出す方法を採用しました。各呼び出しでは、対象BCのサービス一覧と共通分類体系(レイヤー定義など)だけをコンテキストとして渡し、そのBCに該当する文書断片のみを生成させました。生成した断片は、最後に1つの文書へ統合しました。

この方法の利点は明確でした。各呼び出しの入力が短いため情報の欠落がほとんどなく、特定のBCで問題が発生しても、そのBCだけを再生成すればよいため、全体を作り直す必要がありませんでした。大規模な入力ほど、「一度に」処理するのではなく、「意味単位に分割して巡回」するほうが安定することを確認しました。

2.1 構造再設計 — 「サービス=セクション」から「サービス=表の行」へ

分量急増の直接的な原因は、構造にありました。そこでまず、文書の骨格を明示的に再定義しました。核心となる原則は、「個々のサービスを独立したセクションにしない」ことでした。代わりに、同じビジネス境界に属するサービスを1つの表にまとめ、それぞれのサービスを表の行に圧縮しました。

この方針をLLMのプロンプトに明示的な制約として盛り込みました。単に「簡潔に書いて」と頼むのではなく、「各サービスをH4セクションにしないこと。ドメイン表の行に圧縮すること。ビジネス境界あたり平均1.5ページを超えないこと」のように、構造を数値で制御しました。

[구조 제약 — 프롬프트에 명시]
1. 개별 서비스를 H4 섹션으로 만들지 말 것 (그러면 180쪽 초과).
   → 서비스는 도메인 표의 '행'으로 압축.
2. 비즈니스 경계(BC)당 평균 1.5쪽, 총 40~45쪽 목표.
3. 문서 골격(헤딩 구조)은 아래 고정 템플릿을 그대로 따를 것.
   - H1 = Part, H2 = 비즈니스 경계, H3 = 하위 섹션

この1つの変更だけで、分量は劇的に減少しました。187ページだった文書が、31ページ(v2)まで縮小したのです。「LLMに分量を減らすよう頼むこと」と、「構造自体を分量が増えにくい形に固定すること」では、まったく異なる結果になることを確認しました。

2.2 生成ツールの切り替え — pandocの標準変換の限界

分量の問題を解決したv2(31ページ)は構造こそ適切でしたが、別の問題がありました。LLMが生成したMarkdownをpandocでWordに変換したのですが、pandocの標準スタイルでは、表紙・目次・色・表のデザインといった視覚的な完成度を保証できませんでした。内容は正しくても、顧客に提出できるレベルの文書には見えませんでした。

そこで、レンダリング方式を切り替えました。Markdownをそのまま変換するのではなく、文書構造をコードで直接組み立てるdocx生成ライブラリ(docx-js)を使用することにしました。これにより、表紙、目次、見出しスタイル、表のデザイン、色などの書式要素をコードレベルで精密に制御できました。

この際、必ず押さえておくべきだったのが、LLMの出力とコードをつなぐ「データブリッジ」でした。LLMは本質的に自然言語を出力するモデルであるため、自由形式のテキストをそのまま受け取ると、コードで安定してパースできません。そこで、LLMの最終出力形式を厳格なJSONスキーマに強制しました。自然言語の文書を直接書かせるのではなく、あらかじめ定義したスキーマのフィールドを埋めさせたのです。

// LLM 출력은 자연어가 아니라 이 스키마를 따르는 JSON 으로 강제
{
  "bc": "환자·방문",
  "overview": "환자/encounter/동의 등 ...",
  "coreServices": [
    { "id": "S-001", "name": "환자", "layer": "Data", "owner": "원무" }
  ],
  "events": ["PatientCreated", "EncounterStarted"],
  "kpis": ["환자 등록 정합성", "encounter 완료율"]
}

コードはこのJSONを受け取り、docx-jsのオブジェクトとして機械的にレンダリングするだけにしました。つまり、LLMは「何を書くか」をJSONフィールドに入力し、コードはそのフィールドを読み取って表の行や段落を描画する役割だけを担いました。出力形式をスキーマで固定すると、LLMが形式を勝手に変更することがなくなり、不正な形式の応答はパース段階ですぐに排除できました。

// 서식의 단일 책임: 스타일 규칙을 코드 한 곳에서만 정의
const doc = new Document({
  styles: {
    default: { document: { run: { font: "Malgun Gothic", size: 20 } } },
    paragraphStyles: [
      { id: "Heading1", run: { size: 32, bold: true }, ... },
      { id: "Heading2", run: { size: 28, bold: true }, ... },
    ],
  },
  sections: [{ children: [/* 표지 → 목차 → 본문 */] }],
});

ツール切り替えの核心は、「コンテンツと書式の分離」でした。LLMが作成した内容(表の行データ)はそのままにし、それを描画する方法(フォント・色・罫線)はコードだけが責任を持つようにしたのです。そのおかげで、LLMの出力が多少変わっても、最終文書の見た目は常に同じに保たれました。

2.3 書式スタイリング — 色分けと表のデザイン

最後に、可読性を高めるための視覚的なルールをコードに反映しました。文書には性質の異なる複数の領域が混在していたため、領域ごとに色を変えて、一目で区別できるようにしました。また、表のヘッダーに背景色を付け、行を交互に網掛けするゼブラストライピングを適用して、表の可読性を高めました。

[영역별 색상 코딩]
Core 영역    : 딥블루 (#1F4E79)
AI 지원 영역  : 퍼플   (#7030A0)
Analytics    : 그린   (#00875A)
공통(Shared) : 앰버   (#BF8F00)

これらすべての書式ルールをコードの1か所に集約したことで、文書全体に一貫して適用でき、その後の修正も1か所を変更するだけで全体に反映できました。こうして完成したv3最終版は43ページでした。分量は目標範囲に収まり、表紙・目次・色・表のデザインまで備えた、実際に提出可能なレベルの文書になりました。

2.4 整合性検証 — 欠落とハルシネーションを根本的に防ぐ

大規模生成で最も注意を払ったのは、データの整合性でした。技術文書では、数百個あるサービスのうち、たった1つの欠落や歪曲(ハルシネーション)であっても致命的です。LLMは入力にない項目をもっともらしく作り出したり、逆に一部の項目を黙って抜かしたりする可能性があるため、人間が43ページを1ページずつ照合する方法は、信頼性や継続性の面で現実的ではありませんでした。

そこで、パイプラインの最終段階に整合性検証スクリプトを配置しました。核心は単純です。元のExcelにあるサービスIDの一覧と、LLMが生成したJSONデータのサービスID一覧を機械的に相互照合するのです。2つの集合の差分を計算し、欠落したIDや、元の資料に存在しないID(ハルシネーション)が1つでもあれば検証を失敗として扱い、後続の段階に進めないようにしました。

source_ids = set(load_ids_from_excel())   # 원본 카탈로그
output_ids = set(s["id"] for bc in result for s in bc["coreServices"])

missing = source_ids - output_ids   # 누락된 서비스
halluc  = output_ids - source_ids   # 원본에 없는 서비스(환각)

assert not missing, f"누락: {missing}"
assert not halluc,  f"환각: {halluc}"

この検証により、特定のBCの呼び出しで一部のサービスが抜けたり、入力にないサービスが生成されたりするケースを、人間が確認する前に自動的に検出できました。問題が検出された場合も、そのBCだけを再生成すればよいため、コストを抑えられました。「生成した」ではなく、「入力と正確に一致する形で生成した」ことを保証する段階が、大規模文書の自動化には不可欠であると確認しました。

3. 試行錯誤から得た教訓

今回の作業は一度で完成したわけではなく、3つのバージョンを経て改善されました。各バージョンから得た教訓をまとめると、次のとおりです。

버전   분량     상태       핵심 문제 / 개선
----   ----     ------     -------------------------------
v1     187쪽    실패       서비스마다 별도 섹션 → 분량 폭증
v2      31쪽    분량 OK    구조는 잡힘, 그러나 서식 미흡(pandoc 기본)
v3      43쪽    최종       docx-js로 표지·목차·색상·표 완성

1つ目は、分量の制御は依頼ではなく構造によって行うべきだということです。LLMに「短く書いて」と頼むだけでは、187ページは減りませんでした。「サービスは表の行に圧縮する」という構造上の制約を明示したとき、初めて分量を抑えられました。

2つ目は、「内容が正しい」ことと「提出可能である」ことは異なるという点です。v2は内容と分量がともに適切でしたが、書式が不十分で、そのまま提出するのは困難でした。ツールをpandocからdocx-jsに変更して書式をコードで制御して初めて、完成度が高まりました。

3つ目は、書式は1か所で制御すべきだということです。色・表・見出しなどのルールをコンテンツの各所に分散させず、レンダリングコードの1か所に集約したことで、一貫性を維持しやすくなり、修正も容易になりました。

4つ目は、LLMの出力は信じるのではなく、検証するものだということです。出力をJSONスキーマに強制し、入力に対するIDの整合性を自動的に照合しなければ、欠落やハルシネーションは人間が発見するまで文書に残っていたでしょう。検証をパイプラインにコードとして組み込んだことが、信頼性を左右しました。

4. 適用結果

結果として、187ページに及ぶ使用不可能な草稿を、表紙・目次・色・表のデザインを備えた43ページの提出可能な文書に仕上げることができました。単純に分量だけを見ても約77%の削減であり、何より、毎回揺れ動いていた構造と書式が安定したことが最大の成果でした。

作業方法の面でも変化がありました。初期段階では、LLMの出力を人間が一つひとつ整える必要がありました。しかし、構造を固定し、書式をコードで制御するようになってからは、同種の文書を繰り返し生成する際の人間の介入が大幅に減りました。入力資料が更新された場合も、同じパイプラインで再生成すればよいため、保守コストも低減しました。

5. 限界と今後の計画

今回の作業を通じて、LLMを基盤とした大規模文書生成の基本的な骨格は十分に検証できました。ただし、改善が必要な点も明確になりました。

1つ目は、現在、構造上の制約と書式ルールを人間が直接定義していることです。今後は、入力資料の規模を見て、適切な分量と圧縮レベルを自動的に提案する段階があれば、さらに効率化できると考えています。

2つ目は、入力資料が表(Excel)形式の場合はうまく機能しましたが、非構造化文書を入力として受け取る場合には、まず構造を抽出する前処理が必要になることです。この部分を、以前に整理した文書レイアウト分析の経験と組み合わせれば、「非構造化文書を読み取って構造を理解し、新しい定義書として再構成する」という一連の流れまで拡張できると期待しています。

今回の経験を通じて、単に「AIが文章を上手に書ける」というだけでは、実務で利用できる大規模な成果物は生み出せないということを、身をもって実感できました。結局のところ重要なのは、「内容を生成すること」ではなく、「構造を人間が管理し、書式をコードが担い、その結果を検証すること」なのだと、改めて確認できた経験でした。

参考文献

Junny

Site footer