Nakatatsu Tech Notes

生成AI × AWS × IaC でインフラ構築を自動化するクラウドエンジニアのブログ

開発用ドキュメントを構造化しよう。AIファーストの開発環境づくり

はじめに

ソフトウェア開発の現場では、どのような成果物をアウトプットしてほしいのかを明確にするドキュメントが大きな役割を果たします。近年はChatGPTやCopilotのような生成AIの発展著しく、このようなドキュメントをAIに参照させることでコード生成の精度を上げることができるようになっています。拙著『生成AIとプロセス標準化によるIaC効率開発』においては、コーディングガイドラインなどのドキュメントをテンプレートに埋め込んでプロンプトとし、これを生成AIに用いさせてTerraformのコード生成の精度を向上させる例を紹介しました。

しかし課題が残っていました。拙著の中では可読性重視でドキュメントをMarkdownで記載していたのですが、パースが難しいためドキュメント全体を渡していたのです。この結果、次のような問題が潜在的に存在しました。

  • トークン数が肥大化し、不要なAPIコストがかかる
  • 必要ない記述まで渡されるためAIの回答の精度が悪化する

そこで本記事では、改善活動のご紹介も兼ねてドキュメントの構造化とその活用についてご紹介します。構造化はYAMLを利用します。

ドキュメントの構造化の効用

なぜドキュメントをYAMLで構造化する必要があるのでしょうか。それは第一にパースしやすいためです。Markdown形式のドキュメントから必要な箇所だけを抜き出そうとするとスクレイピングのようになってしまい、大変苦労します。その点、予めドキュメントを構造化していれば比較的簡単なスクリプトで必要な箇所だけ抜き出すことができます。タグや名前に対してフィルタリングをかけることも容易です。これは上述のように不要なトークンをなくし、コスト最適化と精度向上に寄与します。

例えばMarkdownで開発ガイドラインを書くと、下記のような形式になります。

# 開発ガイドライン

## 開発環境

### 共通

- 機密情報を含んだリポジトリを決してLLM連動エディタで開いてはいけません。

(中略)

## リポジトリ構造

### 標準

- エントリーポイントとなるルートモジュールは環境名を用いて表現します。これはルートディレクトリの直下に設置します(e.g., `./development` `./production`)

(中略)

## コーディング

### スタイル

- 略語の使用を避け、正式名称を使用します。(e.g., `[NG] cg [OK] coding guidelines `)。ただし`RDS`や`id`のように固有名詞として用いられている略語は利用できます。

この中からコーディングに関係する箇所だけ抜き出してAIに渡したいと思っても、そう簡単なことではないことがお分かりいただけると思います。 その点、構造化していれば下記のようになるわけですが、

- category: コーディング標準
  sections:
    - name: スタイル
      rules:
        - id: coding-writing-acronym
          body: "略語の使用を避け、正式名称を使用します。(e.g., `[NG] cg [OK] coding guidelines `)。ただし`RDS`や`id`のように固有名詞として用いられている略語は利用できます。"
          tags: [code]
          refs: []

これならtagsに"code"と書かれているルールだけを抜き出せば済みます。ドキュメントをYAMLで構造化することは、より良好なプロンプトを用意するための土台となるわけです。

なぜYAMLか、JSONではいけないのか

構造化はJSON形式でも行えます。JSONは開発言語から取り扱うのが簡単で手軽なのに、なぜあえてJSONではなくYAMLを使うのか……そこにはちゃんと理由があります。

人間が読みやすく書きやすい

YAMLはインデント(字下げ)によってデータの階層関係を表現します。おかげで情報の親子関係やグループ化が人にも読み取りやすく、また書きやすいのです。

下記はYAMLで書いている今記述中の開発ガイドラインです。構造化されているものの、パーサーを通さなくてもエディタですぐ読めますし、書けます。

- category: 開発環境
  sections:
    - name: 共通
      rules:
        - id: development-common-tfvarseditor
          body: "機密情報を含んだリポジトリを決してLLM連動エディタで開いてはいけません。"
          tags: [code]
          refs: [https://blog.tricrow.com/entry/ai/diary/2025050816]

- category: リポジトリ構造
  sections:
    - name: 標準
      rules:
        - id: repository-standard-entrypoint
          body: "エントリーポイントとなるルートモジュールは環境名を用いて表現します。これはルートディレクトリの直下に設置します(e.g., `./development` `./production`)"
          tags: []
          refs: [https://developer.hashicorp.com/terraform/language/style#multiple-environments]

一方で下記が同じデータをJSONにしてみたもの。

[
  {
    "category": "開発環境",
    "sections": [
      {
        "name": "共通",
        "rules": [
          {
            "id": "development-common-tfvarseditor",
            "body": "機密情報を含んだリポジトリを決してLLM連動エディタで開いてはいけません。",
            "tags": [
              "code"
            ],
            "refs": [
              "https://blog.tricrow.com/entry/ai/diary/2025050816"
            ]
          }
        ]
      }
    ]
  },
  {
    "category": "リポジトリ構造",
    "sections": [
      {
        "name": "標準",
        "rules": [
          {
            "id": "repository-standard-entrypoint",
            "body": "エントリーポイントとなるルートモジュールは環境名を用いて表現します。これはルートディレクトリの直下に設置します(e.g., `./development` `./production`)",
            "tags": [],
            "refs": [
              "https://developer.hashicorp.com/terraform/language/style#multiple-environments"
            ]
          }
        ]
      }
    ]
  }
]

読むのに大分頭を使います。激しいネストのためエディタで書くのも大変で、見るだにカッコの閉じ忘れ等が多発しそうです。これがJSONではなくYAMLを採用する最大の理由です。

より少ない構文記号で書ける、コメントも書ける。

理由はまだあります。 YAMLJSONより構文記号を少なくできるのもポイントです。先ほどの例にあるように、YAMLJSONと比べて引用符やカンマ、波かっこなどの構文記号が少なくて済む傾向があります。これは読みやすさにもつながるのですが、ドキュメントをそのままAIに渡した場合にトークン数が少なくて済むようにもなります。不要なトークンが減ることはコスト最適化の観点からよいことです。

コメントが書けることも見逃せません。ドキュメントにもコメントを書き残したいもので、ちょっとしたメモが気軽に書けるのは助かります。

# これはコメントです
contents: こちらが本文です

JSONもキーを増やせばコメントを書けるのですが、トリッキーな手法であることは否めません。

{"comment": "これはコメントです", "contents": "こちらが本文です"}

人が読みづらくなる問題

構造化には様々な利点がありますが、人間が読みづらくなるというデメリットもあります。

いくらYAMLが読みやすい方と言っても、やはりMarkdownのほうが読みやすいです。YAMLはデータ項目と階層を表現するのには優れていますが、長い文章を扱うのに向いているとはいいがたいです。

経験上、項目を一つ確認する程度ならYAMLのままで困りません。そのままエディタ上で読めます。ですが全文を通して読むとなると別で、全てYAML形式で読むのは大変です。 面倒なようでも、人が読みやすいようにパースするスクリプトがあるほうがよいです。昔と違って今は生成AIにYAMLの一部を渡してパースするスクリプトを要求すればすぐに出てきますから、大した手間でもありません。例えばこのようなスクリプトになります。

import argparse
import sys
from pathlib import Path
from typing import Iterable, List, Dict, Any

import yaml


def load_entries(path: Path) -> List[Dict[str, Any]]:
    with path.open(encoding="utf-8") as f:
        data = yaml.safe_load(f)

    entries: List[Dict[str, Any]] = []
    for item in data:
        category = item.get("category", "")
        for section in item.get("sections", []):
            section_name = section.get("name", "")
            for rule in section.get("rules", []):
                entries.append(
                    {
                        "category": category,
                        "section": section_name,
                        "body": rule.get("body", ""),
                        "note": rule.get("note", ""),
                        "tags": rule.get("tags", []),
                        "id": rule.get("id", ""),
                    }
                )
    return entries


def filter_entries(entries: Iterable[Dict[str, Any]], tag: str | None, id_: str | None) -> Iterable[Dict[str, Any]]:
    for e in entries:
        if tag and tag not in e["tags"]:
            continue
        if id_ and id_ not in e["id"]:
            continue
        yield e


def print_entries_rule_list(entries: Iterable[Dict[str, Any]]) -> None:
    if not entries:
        return

    for e in entries:
        print(f"- {e['body']}")


def print_entries_document(entries: List[Dict[str, Any]]) -> None:
    if not entries:
        return

    print("# ガイドライン\n")

    last_category = last_section = None
    for e in entries:
        category = e["category"]
        section = e["section"]

        # Category 見出し
        if category != last_category:
            print(f"## {category}\n")
            last_category = category
            last_section = None

        # Section 見出し
        if section != last_section:
            print(f"### {section}\n")
            last_section = section

        # ルール本体
        print(f"- {e['body']}")
        if e["note"]:
            print("\n~~~\n" + e["note"] + "\n~~~\n")
        else:
            print()  # note が無い場合も行間を整える


# ---------- CLI ---------- #
def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="YAML ルール抽出ツール")
    p.add_argument("yaml_file", type=Path, help="入力 YAML ファイル")
    p.add_argument("--tag", help="tags に完全一致する値で絞り込み")
    p.add_argument("--name", help="rule id に部分一致する文字列で絞り込み")
    p.add_argument(
        "--format",
        choices=("rule_list", "document"),
        default="rule_list",
        help="出力形式 (既定: rule_list)",
    )
    return p.parse_args()


def main() -> None:
    args = parse_args()

    if not args.yaml_file.exists():
        print("ファイルが見つかりません。パスを確認してください。", file=sys.stderr)
        sys.exit(1)

    entries = load_entries(args.yaml_file)
    entries = list(filter_entries(entries, args.tag, args.name))

    if args.format == "rule_list":
        print_entries_rule_list(entries)
    else:
        print_entries_document(entries)


if __name__ == "__main__":
    main()

実施するとこのようになります。あとはどこかに出力してプレビューで読んでもよいですし、マークダウンのまま読んでもよいでしょう。

> python .\viewer.py .\guidline.yaml --format document
# ガイドライン

## 開発環境

### 共通

- 機密情報を含んだリポジトリを決してLLM連動エディタで開いてはいけません。

## リポジトリ構造

### 標準

- エントリーポイントとなるルートモジュールは環境名を用いて表現します。これはルートディレクトリの直下に設置します(e.g., `./development` `./production`)

なお同じスクリプトでルールだけ抽出することもできます。

> python viewer.py guidline.yaml --tag code
- リソース名にはスネークケースを使用します(例:web_server)。
- モジュール内で一点しかないリソースはmainと名付けます。(例: resource "aws_vpc" "main" )。
- モジュール内に複数あるリソースは用途に応じた名前をつけます(例: primary、read_replica)。
- リソース名に複数形を使ってはいけません。
(以下省略)

YAMLの構造が崩れないようにするためには

構文が崩れた状態でgitにpushしてしまわないよう、yamllint とjsonschemaを用いた構文チェックも取り入れます。スクリプトは省略しますが、もしyaml構文そのものに問題があれば下記のように出力されます。

[yamllint] line 332, col 81: line too long (133 > 80 characters)  (line-length, error)
PS C:\MyFile\GithubRD\terraform-experimental\temp> python .\check.py
[yamllint] line 317, col 11: duplication of key "refs" in mapping  (key-duplicates, error)

また構文はあっているものの必須プロパティがないなどの問題がある場合はjsonschemaが指摘してくれます。

> python .\check.py
[4.sections.0.rules.2] 'id' is a required property

おわりに

本記事ではYAML形式で開発ドキュメントを構造化する利点および課題とその対応について解説しました。ドキュメントを構造化することで、AIによるドキュメント活用の一層の効率化を図ることができるようになります。AIに読ませることを前提とするドキュメントはぜひ構造化することを検討してみてください。