はじめに
AIエージェントにコードを書かせるのが当たり前になった。
AIはリポジトリのコードを読めば、「何がどう実装されているか」を正確に把握してくれる。
ただ、コードをいくら読んでもわからないことがある。
なぜその実装になっているのか、つまり「Why」の部分だ。
この記事では、そのWhyをAIに伝える手段として ADR(Architecture Decision Record) を紹介する。
あわせて、自分が実際にどう書いて、どうAIに読ませているかもまとめておきたい。
正直、自分もAIエージェントを使い始めるまでは、ADRのようなドキュメントをちゃんと書いたことはなかった。
ただ、AIエージェントが当たり前になった今は、開発するならADRはめちゃくちゃ大事だと思っている。
ADRとは
ADRは、開発中に何かを決めたときに、なぜそう決めたのかを決まったフォーマットで残しておくドキュメントだ。
2011年にMichael Nygardが「Documenting Architecture Decisions」という記事で提案したものが元になっている。
特別なツールは特に必要ない。
自然言語で書いたMarkdownファイルを、リポジトリに置いておけば十分だ。
1件のADRには、だいたい次のようなことを書く。
- 背景(Context): どんな問題や制約があって、決める必要があったのか
- 決定(Decision): 何を選んだのか、なぜそれを選んだのか
- 結果(Consequences): その決定で得られるものと、引き換えに失うもの
- 検討した代替案(Alternatives): 他にどんな選択肢があって、なぜ選ばなかったのか
ADRの「D」はDecision。つまり、「意思決定」を記録するドキュメントである。
コードからWhatはわかるが、Whyはわからない
ドキュメントがなくても、AIはコードから多くのことを読み取れる。
どういう実装なのか、どういうアーキテクチャなのか。
コード自体が答えになっているので、コードの内容(What)をわざわざドキュメントに残しておく意味は、あまりない。
一方で、なぜその意思決定をしたのか(Why)は、コードだけでは理解できない。
例えば、あるAPIがJavaで実装されているとする。
コードを見ればJavaで書かれていることはわかるが、「なんでPythonじゃなくてJavaなんだっけ?」はわからない。
PythonのWeb APIがFlaskで書かれていれば、「なんでFastAPIじゃダメなんだっけ?」と思うかもしれない。
開発していた当時のメンバーは、その背景を知っているので困らない。
困るのは、時間が経って新しいメンバーが入ってきたときだ。
当時のメンバーがもういなかったり、本人も覚えていなかったりすると、理由をたどるのはなかなか大変になる。
実際には、こうした決定の裏にはいろいろな暗黙知があるはずだ。
- 当時、社内で推奨されていた言語がJavaだった
- 当時はFlaskが盛り上がっていて技術資料も多く、FastAPIはまだそこまで注目されていなかった
- 実際に比べてみたら、レイテンシの面でこっちの方が良かった
こういう情報が残っていないと、後から「本当はこっちの方が良かったんじゃないか」と推測したり、一度却下した案をもう一度検討したりすることになる。
なので、他にどんな選択肢があり、それぞれにどんなメリット・デメリットがあって、その上でなぜそれに決めたのかを残しておく必要がある。
これがADRを書く理由だ。
実はADRの概念はコメントに近い
コードのコメントについて、よく「WhatではなくWhyを書け」と言われる。
ADRに書くべきことも、これと同じ考え方だ。
// ユーザーIDを取得する関数
function getUserId() { ... }
例えばこういうコメントは、仕事で書くプロダクションのコードではほとんど意味がない。
getUserId という名前を見れば、何をする関数かはわかるからだ。
// 認証直後に呼ぶと失敗することがあるため、1秒待ってから実行する
await sleep(1000);
一方でこういうコメントは役に立つ。
コメントがなければ、「なんでここでわざわざ1秒待ってるんだろう?」がわからない。
当時の試行錯誤や、「本当は良くないけど、こういう事情でこうしている」という理由は、コメントに残しておくと良い。
ただ、コメントは関数や変数のすぐそばに書いて、コードと一緒に読むものだ。
なので、コメントで扱えるのはその場所だけの局所的なWhyになる。
ADRで扱うのは、もっと大きな決定である。
| コメント | ADR | |
|---|---|---|
| 扱う範囲 | 関数・変数など、その場所だけ | 言語、フレームワーク、ツール、構成など |
| 例 | なぜここで1秒待つのか | なぜこのフレームワークを選んだのか |
| 置き場所 | コードの中 | リポジトリ内のドキュメント |
なぜAI時代に特に重要なのか
ここまで読んで、「それって別にAIに関係なく大事じゃない?」と思うかもしれない。
もちろんその通りだ。
ただ、これまではこうした背景が、コードとは別の場所にバラバラに置かれていることが多かった。
- ミーティングの議事録
- 社内のWikiやドキュメント
- 「〇〇さんに聞けばわかるよ」という人の記憶
人間なら、こういうものをたどって「ああ、だからこうなってるのね」と理解できた。
でも今は、AIが自分で情報を集めてコードを書く時代だ。
そして、社内の議事録や誰かの記憶には、AIはアクセスできないか、できても取りに行くのが難しい。
なので、決定の理由はAIがコードと一緒に読める場所に置いておく必要がある。
ADRをコードと同じリポジトリに置いておけば、AIはコードと同じようにADRを読みに行き、当時どういう判断でそれが選ばれたのかを理解できる。
結局のところ大事なのは、コード以外に必要な背景知識(コンテキスト)を、いかにAIに提供するかだ。
ADRはそのための手段の1つだと思っている。
ADRの書き方
フォーマットはスキルに任せる
ADRの書き方に絶対の正解はない(そもそも自然言語なので)。
ただ、フォーマットが決まっていると書きやすいのは確かだ。
自分は、GitHubが公開している Awesome Copilot にある create-architectural-decision-record スキルを参考にして使っている。
このスキルのテンプレートには、次のような項目がある。
- 状態(Proposed / Accepted / Rejected / Superseded / Deprecated)
- 背景と決定
- 良い結果と悪い結果
- 検討した代替案と、採用しなかった理由
- 実装メモ、参考資料
代替案やメリット・デメリットまで漏れなく書けるので気に入っている。
自分の場合は、このスキルをほぼそのままコピーして、自分のリポジトリにスキルとして置いている。
スキルとして置いておくと、AIと壁打ちして「こういう理由で、こう作ろう」と決まったときに、会話だけで終わらせずにその場でADRとして残せる。
これが地味に便利だ。
書いてみるとこんな感じ
例えば、さっきのFlaskの例をADRにすると、こんなイメージになる(架空の例)。
---
title: "ADR-0002: Web APIのフレームワークにFlaskを採用する"
status: "Accepted"
supersedes: ""
superseded_by: ""
---
# ADR-0002: Web APIのフレームワークにFlaskを採用する
## Context
社内向けの Web API を Python で新しく作る。チームには Flask の運用経験があり、
社内の共通ライブラリも Flask 前提で作られている。
## Decision
Flask を採用する。チームの経験と社内ライブラリをそのまま活かせるため。
## Consequences
### Positive
- **POS-001**: 社内の共通ライブラリ(認証、ログ)をそのまま使える。
### Negative
- **NEG-001**: 型ヒントからのリクエスト検証や OpenAPI の自動生成は、自前で用意する必要がある。
## Alternatives Considered
### FastAPI
- **ALT-001**: **Description**: 型ヒントからリクエスト検証と OpenAPI を自動生成できるフレームワーク。
- **ALT-002**: **Rejection Reason**: 社内の共通ライブラリが未対応で、移植コストが大きい。
コードを見れば「Flaskで動いている」ことはわかる。
でも「なんでFastAPIじゃないのか」は、このADRがないとわからない。
これはあくまで簡略化したADR例で、実際のものと異なることに留意してほしい。
ADRは連番にし、書き換えではなく「追加」する
ADRには adr-0001-...md、adr-0002-...md のように連番を振っている。
自分が現在使うことが多いのは、決定が変わっても、元のADRは書き換えないという運用だ。
決定が変わったら新しいADRを追加し、古いADRの状態を「Superseded(置き換え済み)」に変える。
どのADRに置き換えられたかは、上の例の supersedes / superseded_by に書いておく。
これはNygardの元記事でも推奨されているやり方である。
こうすると、「最初はライブラリAを使っていたが、こういう事情でライブラリBに変えた」という流れが残る。
人間もAIも、今の技術を使っている理由だけでなく、今の形に至るまでの経緯まで理解できる。
置き場所
これも絶対のルールはないが、自分は docs/adr/ ディレクトリにMarkdownファイルを並べておくことが多い。
docs/
└── adr/
├── adr-0001-use-java-for-api.md
├── adr-0002-web-framework.md
└── adr-0003-migrate-to-fastapi.md
スキルはdescriptionが大事
スキルの本文は、フォーマットさえ整っていればいい。
一番気をつけるべきなのは description だ。
以前の記事でも書いたが、エージェントは最初からスキルの中身を全部読むわけではない。
まず name と description を見て、タスクに関係すると判断したときだけ本文を読む。
つまり、descriptionの書き方で、スキルが読み込まれるタイミングが決まる。(ちなみに、MCPとかもそう)
もちろん、スキルを明示的に指定すれば確実に使われる。
自動で呼んでほしい場合、自分は「ADRを書いてと言われたときに使う」と、使う場面がはっきりわかるように書くことが多い。
---
name: adr-creator
description: 'ADR(Architectural Decision Record)を作成する。ユーザーに「ADRを書いて」と依頼されたときに使用する。'
---
ここを適当に書くと、こういうことが起きる。
- ADRを書いてほしいのに、スキルが自動で呼ばれない
- 関係ないドキュメントを書きたいだけなのに、スキルが呼ばれてしまう
- descriptionは毎回読まれるので、長く書きすぎるとその分コンテキストを食う
なので、本当に必要なときだけ呼ばれる、必要最低限の文章にするのが大事だ。
これはADRに限らず、スキルやMCPのツール全般に言える。
ADRは「必要なときだけ」読ませる
最後に、ADRをAIにどう読ませるかについて。
ADRが何十件にもなり、それぞれにそれなりの分量があるとする。
これを毎回全部AIのコンテキストウィンドウに載せると、コンテキストを圧迫し、大事な情報がノイズに埋もれてしまう。
例えば、AIが最初に読む AGENTS.md や CLAUDE.md に「まずADRを全部読め」と書くと、大量の文章が毎回コンテキストに載る。
これはあまりよろしくない。
それよりも、AGENTS.md は索引として使うのが良いと思っている。
「ADRはここにあるので、必要なときはそこを見てね」くらいにとどめておく。
## ADR
- 技術選定や構成の判断は `docs/adr/` に ADR として残している。
- 「なぜこの技術・構成なのか」を知りたいときだけ、該当する ADR を読む。
こうしておけば、AIは「あれ、なんでこの技術を使ってるんだっけ?」と調べたいときだけ、ADRを探しに行ける。
これが理想かなと思っている。
AGENTS.md にあれこれ書きすぎないほうがいいという話は、『プロフェッショナルAI駆動開発』の書評でも触れたので、気になる人はそちらもどうぞ。
まとめ
- AIはコードを読めば What はわかるが、Why はわからない
- ADRは、意思決定の理由・代替案・メリット・デメリットを残すドキュメント。コメントより大きな、言語やフレームワークなどのWhyを扱う
- AIがアクセスできるよう、ADRはコードと同じリポジトリに置く。決定が変わったら、書き換えずに新しいADRを追加する
- ADRは毎回読ませず、AGENTS.mdを索引にして必要なときだけ読ませる。スキルのdescriptionも同じ考え方で書く
AI時代の開発では、「なぜそう決めたのか」を、今まで以上にAIに伝える必要がある。
ただし全部ではなく、大事な部分に絞って、必要なときに渡すのがポイントだ。
ということで、一番いいたかったことは「ADR、めちゃくちゃ大事」です!