LayerX エンジニアブログ

LayerX の エンジニアブログです。

LLMで業務ワークフローを自動生成・最適化する! 〜ワークフロー自動生成・最適化の取り組みについて〜

こんにちは。LayerX AI Workforce事業部でR&Dチームのリサーチエンジニアの矢野目です。

こちらはLayerX AI エージェントブログリレー49日目の記事です。前回の記事はKenta WatanabeさんのAIエージェントを開発するPdMがやることをプロンプトを書きながら考えるでした。

今回の記事では、AIワークフローの自動生成技術開発の取り組みについてお話しします。

AIワークフローを構築する際、「どのような処理ステップを組み合わせるか」「各ステップでどんなプロンプトを使うか」といった設計に多くの時間がかかります。特に、お客様のドメイン知識が必要なタスクでは、試行錯誤を繰り返しながら精度を高めていく必要があり、これが大きな課題となっています。

そこで我々Applied R&Dチームでは、プロンプトとワークフロー構造を同時に自動生成する手法に取り組んでいます。

本稿では、300ページ超の報告書から構造化データを抽出するタスクを例に、実際に生成されたワークフローの詳細を紹介します。「アルゴリズムはどのような工夫を自動発見したのか」「どこまで実用的なのか」といった点を、実際に生成されたワークフロー、プロンプト、コードとともに解説します。

なお、より詳細な探索アルゴリズム(Repeated samplingの活用)、コンテキストの設計、フィードバックの与え方などについては、以下のスライドを参照ください。

speakerdeck.com

1. 背景

AIワークフローとは

AIワークフローの例

AIワークフローとは、LLMとコードを複数回呼び出し、結果を組み合わせて複雑なタスクを解決する手法です。

シンプルなタスクであればLLMへの単発指示で完了しますが、実務レベルの複雑なタスクでは精度が出ません。例えば、見積書から数百種類の固定資産コードに分類するタスクでは、PDFから情報を抽出し、商品とサービスを区別し、ドメイン知識に基づいて分類する必要があります。

こうした複雑なタスクでは、「文章を要約する」→「情報を抽出する」→「整形する」といったように複数のステップを組み合わせることで、タスクを分解して精度を高めます。

AIワークフロー構築の課題

しかし、AIワークフローを構築するには、2つの大きな課題があります。

① ワークフロー構築に手間と時間がかかる

第一に、ワークフロー構築に手間と時間がかかることです。お客様のドメインを理解し、精度の出るワークフローを構築する作業には相当の時間が必要です。

どのようなノード構成にするか、各ノードでどのようなプロンプトを使うか、といった設計を試行錯誤しながら進める必要があります。

② LLMの変更やデータの変化に対して堅牢でない

第二に、LLMの変更やデータの変化に対して堅牢でないことです。対応できないデータが出てきた場合に、ワークフローを修正する必要がありますが、工数がかかります。

また、LLMのバージョンアップや、新しいLLMへの切り替えがあると、調整が必要になることもあります。

2. ワークフロー自動生成・最適化手法

これらの課題を解決するために、最近はプロンプトとワークフロー構造を同時に自動生成する手法が提案されています。

(出典: Zhang et al. "AFlow: Automating Agentic Workflow Generation" ICLR 2025, https://iclr.cc/virtual/2025/poster/27691

これらの先行研究を参考に、我々はワークフロー自動生成・最適化手法を開発しています。

アーキテクチャー

我々のワークフロー自動生成・最適化アルゴリズムは、以下の4つのコンポーネントで構成されています。

現行のワークフロー自動生成・最適化のアーキテクチャー

Generatorは、LLMを用い、meta promptと過去の試行結果(Context)から新しいワークフロー候補を生成します。

我々は3種類のノードタイプを用意しており、アルゴリズムは、どのノードをどの順番で使うか、各ノードでどのようなプロンプト/コードを使うかを自動生成します。

  1. LLMノード : LLMで処理する
  2. Vision LLMノード : 画像やPDFをVLMで処理する
  3. Codeノード : Pythonコードを実行する

Executorは、生成されたフロー(LLMとPythonの組み合わせ)を実行します。

Evaluatorは、実行結果を定量評価し、そのワークフローの性能を定量的に評価します。

Memoryは、過去の試行結果(ワークフロー、スコア、フィードバック)を保存し、Generatorにフィードバックします。これにより、Generatorは過去の失敗から学習し、より良いワークフローを生成できるようになります。

これらのコンポーネントが連携して試行錯誤を繰り返すことで、徐々にワークフロー構造とプロンプト、コードが洗練されていきます。今回検証した2つのタスクでは、5〜7回の試行で高精度なワークフローが見つかりました。

実行時間・コスト

1回の試行は10〜15分程度で、コストは約5ドルです。このコストには、ワークフロー生成(Generator)とワークフロー評価(Executor + Evaluator)の両方が含まれます。

なお、今回はワークフロー評価に比較的安価なモデル(gpt-4o-miniなど)を使用しています。より高性能なモデル(gpt-4.1、gpt-5など)をワークフロー内で使用する場合は、コストはさらに高くなる点に注意が必要です。

3. 実例:プロジェクト完了報告書からの実績データ抽出・計算タスク

では、この手法で実際にどのようなワークフローが生成されたのか、プロジェクト完了報告書からの実績データ抽出タスクを詳しく見ていきます。

注:本タスクは実際の財務報告書からの抽出で検証しましたが、機密性の観点から、本稿ではプロジェクト報告書という形式に置き換え、数値も変更して記載しています。タスクの構造や難易度は同等です。

どんなタスクか

プロジェクトの完了報告書PDF(300ページ超、約100万文字)から、プロジェクト実績データ(工数、コスト、スケジュール、品質指標など)を構造化抽出・計算するタスクです。

入力ドキュメント

入力は、以下のようなページ数の多いPDFです。

大規模プロジェクト完了報告書

プロジェクト名:次世代ECプラットフォーム構築プロジェクト
期間:2023年1月〜2024年12月(24ヶ月)
総ページ数:約300ページ

【構成】
- Executive Summary(要約)
- Project Timeline(スケジュール)
- Resource Allocation(リソース配分)
- Deliverables(成果物)
- Quality Metrics(品質指標)
- Budget Performance(予算実績)
- Lessons Learned(教訓)
- Appendix(付録)

正解データ

正解データは、報告書から以下の5つのカテゴリの実績データを抽出したものです。プロジェクトスケジュール(総日数、各フェーズの日数、アクティビティ別の日数)、リソース実績(総工数、役割別内訳、インフラコスト)、成果物(コード行数(言語別)、テストカバレッジ、ドキュメント量)、品質指標(バグ統計(総数、重大度別、密度)、パフォーマンス指標)、予算実績(計画予算、実績コスト、差異)です。

これらは最大6層の深い階層構造で整理されており、合計48個の数値を全て正確に抽出する必要があります。

以下、正解データのスキーマ(抜粋)です。

{
  "project_timeline": {
    "total_duration_days": {
      "value": 730,  // 総日数
      "breakdown": {
        "planning_phase": {
          "value": 120,  // 計画フェーズの日数
          "activities": {
            "requirement_gathering": {"value": 60},  // 要件定義
            "design_specification": {"value": 35},   // 設計
            "resource_planning": {"value": 25}       // リソース計画
          }
        },
        "development_phase": {...},  // 開発フェーズ
        "testing_phase": {...},      // テストフェーズ
        "deployment_phase": {...}    // デプロイフェーズ
      }
    }
  },
  
  "resource_utilization": {
    "human_resources": {
      "total_person_months": {
        "value": 285,  // 総工数(人月)
        "by_role": {
          "engineering_team": {
            "value": 210,  // エンジニアリングチーム
            "breakdown": {
              "senior_engineers": {"value": 85},      // シニア
              "mid_level_engineers": {"value": 75},   // ミッド
              "junior_engineers": {"value": 50}       // ジュニア
            }
          }
        }
      }
    }
  }
}

タスクの難しさ

このタスクの難しさは、まず300ページ、850,000文字という超大規模文書を処理する必要がある点です。関連情報が様々なセクションに分散しており、それらを統合して抽出する必要があります。

また、最大6層という深い階層構造でデータが整理されており、例えば「リソース実績 → 人的リソース → 総工数 → 役割別 → エンジニアリング → 内訳 → シニアエンジニア」というパスを辿る必要があります。

最後に、一部のフィールドは文書に明記されておらず、計算処理が必要な点も難しいポイントの1つです。(総工数 = エンジニア + QA + PM + DevOps、予算差異 = 予算 - 実績、バグ密度 = (総バグ数 / 総コード行数) × 1000など)。

自動生成されたワークフロー

アルゴリズムが発見したのは、6つのノードからなるワークフローです。各報告書ごとにこのワークフローを実行します。

報告書PDF(300ページ)
  ↓
【ノード1】1ページずつテキスト化(Codeノード)
  ↓
【ノード2】重要ページを判定(LLMノード・300回ループ)
  ↓
【ノード3】重要ページを選択・結合(Codeノード)
  ↓
【ノード4】データを抽出(LLMノード)
  ↓
【ノード5】合計値を計算(Codeノード)
  ↓
【ノード6】単位を正規化(Codeノード)
  ↓
最終出力(精度:98.1%)

アルゴリズムは、300ページ(850,000文字)という大規模データに対して、自動的にチャンキング(分割・選択・結合)が必要だと判断しました。LLMのコンテキスト制限(通常10万文字程度)を考慮し、ノード2とノード3で重要なページだけを抽出して85,000文字に圧縮するワークフローを自ら生成しています。この「大規模データへの対処方法」を人間が指示することなく、アルゴリズムが自動発見した点が重要です。

ノード1:1ページずつテキスト化(Codeノード)

このノードは、Code(Python)のノードで、300ページのPDFから、各ページをテキストに変換し、ページ番号とテキストのペアのリストを作成します。

生成されたコード(抜粋)

import pypdf

reader = pypdf.PdfReader(pdf_path)
pages = []
for i, page in enumerate(reader.pages, start=1):
    text = page.extract_text() or ""
    pages.append({"page_num": i, "text": text})

出力例:

[
  {"page_num": 1, "text": "Executive Summary\n\nThis report..."},
  {"page_num": 2, "text": "Project Timeline\n\nTotal duration: 730 days..."},
   ......
  {"page_num": 300, "text": "Appendix\n\n..."}
]

ノード2:重要ページを判定(LLMノード・ループ処理)

このノードは、LLMノードで、300ページ全てに対して、各ページを4カテゴリに分類(主要データ、成果物・品質、補足詳細、その他)し、具体的な数値が含まれるかを判定し、keep=true/falseフラグを付与します。各ページをループ処理するため、300ページなら300回LLM呼び出しを行います。

生成されたプロンプト(抜粋)

タスク: このページが48個の数値フィールド抽出に必要かを判定してください。

カテゴリ定義:
- primary_data: Timeline/Budget/Resource等の主要数値データ
- deliverable_quality: Code/Quality等の成果物・品質データ
- detail_supplement: 内訳、グラフ、補足説明
- other: 要約、教訓、謝辞等

判定ルール:
keep=true: primary_dataまたはdeliverable_qualityで、具体的な数値を含む
keep=false: otherまたは数値を含まない

出力例:

[
  {
    "page_num": 1,
    "category": "other",
    "keep": false,
    "reason": "Executive Summary - 要約のみ"
  },
  {
    "page_num": 45,
    "category": "primary_data",
    "keep": true,
    "reason": "Project Timeline with specific numbers"
  },
  {
    "page_num": 120,
    "category": "primary_data",
    "keep": true,
    "reason": "Resource Allocation breakdown"
  }
]

上記のプロンプトは、4カテゴリによる明確な分類基準と、数値の有無による判定、各ページに対する個別判断の点で工夫がなされています。300ページから重要な30ページ程度に絞り込むことで、次のノードの処理負荷を削減します。

ノード3:重要ページを選択・結合(Codeノード)

このノードは、Codeノードで、ノード2の判定結果から、keep=trueのページだけを選択し、カテゴリ別の優先度でソート(主要データ→成果物→補足)し、上位20ページ、85,000文字まで結合します。

生成されたコード(抜粋):

# keep=trueのページを選択
important_pages = [p for p in judgments if p["keep"]]

# カテゴリ優先度でソート
priority = {"primary_data": 1, "deliverable_quality": 2, "detail_supplement": 3}
important_pages.sort(key=lambda x: priority.get(x["category"], 99))

# 上位20ページ、85,000文字まで結合
combined = ""
for page_info in important_pages[:20]:
    page_text = pages[page_info["page_num"] - 1]["text"]
    if len(combined) + len(page_text) <= 85000:
        combined += page_text + "\n\n"

出力例:

{
  "combined_text": "Project Timeline\n\nTotal duration: 730 days...\n\nResource Allocation\n\nTotal person-months: 285...",
  "total_chars": 84523,
  "pages_included": [45, 120, 67, 89, ...]
}

上記の生成されたコードでは、カテゴリ優先度による効率的なソートと、LLMのコンテキスト制限を考慮した85,000文字制限の点で工夫がなされています。300ページ(850,000文字)を20ページ(85,000文字)に圧縮しながら、重要情報は全て保持します。

ノード4:データを抽出(LLMノード)

このノードは、LLMノードで、85,000文字のテキストから、48個の数値を6層の階層構造で抽出します。明示された数値のみ抽出し(推測・計算禁止)、見つからない項目は0とします。

生成されたプロンプト(抜粋):

タスク: テキストから48個の数値フィールドを抽出してください。

厳守事項:
- 明示された数値のみ抽出(推測・計算禁止)
- 単位はそのまま(変換禁止)
- 見つからない項目は0
- 合計値が明記されていない場合も0(後段で計算)

抽出対象:
1. project_timeline: 総日数、フェーズ別日数、アクティビティ別日数
2. resource_utilization: 役割別工数、インフラコスト
3. deliverables: コード行数(言語別)、カバレッジ
4. quality_metrics: バグ統計、パフォーマンス指標
5. budget_performance: 計画予算、実績コスト

出力例(抜粋):

{
  "project_timeline": {
    "total_duration_days": {"value": 730},
    "breakdown": {
      "planning_phase": {"value": 120},
      "development_phase": {"value": 400}
    }
  },
  "resource_utilization": {
    "human_resources": {
      "total_person_months": {"value": 0},  // 後段で計算
      "by_role": {
        "engineering_team": {"value": 210},
        "qa_team": {"value": 42}
      }
    }
  }
}

上記のプロンプトは、「計算禁止」を徹底することでLLMの誤差を防ぐ点、48個のフィールドの詳細な定義、見つからない場合の明確な処理の点で工夫がなされています。LLMには「読み取り」だけをさせ、「計算」は次のノードで確実に行います。

ノード5:合計値を計算(Codeノード)

このノードは、Codeノードで、ノード4の出力から、合計値を計算(総日数、総工数など)、差異を計算(予算差異など)、バグ密度を計算します。

生成されたコード(抜粋):

# Timeline: Total = sum of all phases
total_duration = (data["project_timeline"]["breakdown"]["planning_phase"]["value"] +
                  data["project_timeline"]["breakdown"]["development_phase"]["value"] +
                  data["project_timeline"]["breakdown"]["testing_phase"]["value"] +
                  data["project_timeline"]["breakdown"]["deployment_phase"]["value"])
data["project_timeline"]["total_duration_days"]["value"] = total_duration

# Resource: Total person-months = sum of all roles
total_pm = (data["resource_utilization"]["human_resources"]["by_role"]["engineering_team"]["value"] +
            data["resource_utilization"]["human_resources"]["by_role"]["qa_team"]["value"] +
            data["resource_utilization"]["human_resources"]["by_role"]["project_management"]["value"] +
            data["resource_utilization"]["human_resources"]["by_role"]["devops_team"]["value"])
data["resource_utilization"]["human_resources"]["total_person_months"]["value"] = total_pm

# Quality: Defect density = (Total defects / Total LOC) * 1000
total_defects = data["quality_metrics"]["defect_statistics"]["total_defects_found"]["value"]
total_loc = data["deliverables"]["code_metrics"]["total_lines_of_code"]["value"]
density = int((total_defects / total_loc) * 1000) if total_loc > 0 else 0
data["quality_metrics"]["defect_statistics"]["defect_density_per_kloc"]["value"] = density

出力例(抜粋):

{
  "project_timeline": {
    "total_duration_days": {"value": 730}  // 計算済み
  },
  "resource_utilization": {
    "human_resources": {
      "total_person_months": {"value": 285}  // 計算済み: 210+42+18+15
    }
  },
  "quality_metrics": {
    "defect_statistics": {
      "defect_density_per_kloc": {"value": 38}  // 計算済み: (1842/485000)*1000
    }
  }
}

上記の生成されたコードでは、Pythonによる確実な計算処理と、階層構造を辿った値の取得、ゼロ除算の防止の点で工夫がなされています。LLMには計算をさせず、Pythonで確実に処理することで精度を高めます。

ノード6:単位を正規化(Codeノード)

このノードは、Codeノードで、パーセント値を整数化(99.85% → 9985)し、全てのvalueフィールドを再帰的に処理し、最終的な出力形式に変換します。

生成されたコード(抜粋):

def normalize_values(obj):
    if isinstance(obj, dict):
        for key, value in obj.items():
            if key == "value" and isinstance(value, (int, float)):
                # パーセント値の正規化ロジック
                if "percent" in str(obj.get("field_name", "")):
                    obj[key] = int(value * 100) if value < 100 else int(value)
            else:
                normalize_values(value)
    elif isinstance(obj, list):
        for item in obj:
            normalize_values(item)
    return obj

result = normalize_values(data)

出力例(抜粋):

{
  "quality_metrics": {
    "performance_benchmarks": {
      "uptime_percent": {"value": 9985},  // 99.85% → 9985
      "test_coverage_percent": {"value": 87}  // 87% → 87
    }
  }
}

4. 考察

生成されたワークフローを分析すると、いくつかの興味深い特徴が見られました。ここでは、LLMとPythonの役割分担、大規模データへの対処戦略という2つの観点から考察します。

LLMとPythonの役割分担

生成されたワークフローを分析すると、興味深い役割分担が見られました。LLMは「読み取り」「分類」「判断」といった意味理解が必要なタスクに使われ、Pythonは「計算」「データ変換」「集約」といった確実性が求められるタスクに使われていました。

例えば、このタスクでは、LLMがテキストから数値を読み取り(ノード4)、Pythonが合計値を計算する(ノード5)という明確な分担がありました。プロンプトには「計算禁止」と明示されており、LLMの計算能力の不確実性を避け、Pythonの確実性を活用する設計になっています。

チャンキング戦略の自動発見

このタスクで特に重要だったのは、アルゴリズムが大規模データへの対処方法を自動発見したことです。

300ページの文書を処理するために、「重要ページを判定→選択・結合→データ抽出」という3ステップの戦略を生成しました。この戦略により、850,000文字を85,000文字に圧縮しながら、必要な情報は全て保持することができました。

5. 限界と今後の方針

精度評価について

本稿で示した約90%の精度は、訓練データに対する精度です。これには理由があります。

まず、我々の現在の目標は、アルゴリズムが訓練データに対して適切にフィットできるかを確認することです。訓練データでさえ高精度が出せなければ、実用化は困難です。

次に、我々のアプローチは、未知のデータが来た時にも同じアルゴリズムで再フィッティングすることを前提としています。新しいフォーマットの見積書や報告書が登場した際、数件のサンプルデータを用意し、再度ワークフロー自動生成を実行することで対応します。このため、まずは訓練データでの性能を確実にすることを優先しています。

もちろん、未知のテストデータでの汎化性能の検証は重要です。今後は、訓練データとは別のテストデータを用意し、汎化性能を評価していく予定です。このような段階的なアプローチにより、技術の実用性を着実に高めていきます。

6. まとめ

本稿では、ワークフロー自動生成技術により生成されたワークフローの詳細を、300ページ超の報告書からのデータ抽出タスクを例に紹介しました。

このタスクでは、6ノードのワークフローで約90%の精度を達成しました。特に注目すべきは、アルゴリズムが自動的にチャンキング戦略を発見し、大規模データを効率的に処理できることが示された点です。また、LLMには「読み取り」を、Pythonには「計算」を担当させるという役割分担も、自動的に発見されました。

これらの結果から、ワークフロー自動生成技術は実務レベルのタスクに応用できる可能性を示唆しています。今後はより複雑なタスクへの適用、さらなる精度向上、そして様々なドメインでの検証を進めていく予定です。

最後に

R&Dチームではリサーチエンジニアを募集しています。また、検索エンジニアやデータエンジニア、MLOpsエンジニアも募集しています。一緒にR&Dチームおよびデータ検索基盤チームを0→1で立ち上げたい方はぜひ気軽にお話できると幸いです。