AI開発の記録を五種類に分ける
判断・順序・完成条件・作業・残件の記録を、AIの推測まかせにしないために
前回の記事では、AIと並走して開発を続けていく中での課題を述べた。この記事ではそれに対しての自分なりの対抗策と、まだ発展途上な箇所を考察していく。
5つの記録を先に並べる
AI開発の記録は、現在では5種類のJSONをプロジェクトごとに用意する形となった。しかし、最初から5種類を考えていたわけではない。最初はプロジェクトの判断を残すためのDecision Mapから始まった。現在は方針をDecision Map、順序をRoadmap、完成の判定をCompletion Criteria、作業結果をDevLog、残件をTodoに保存している。
5つの記録と、それぞれが答えること
一つの文書にまとめなかったのは、情報の変わるタイミングと、確定する人が違うから。
| 項目 | 何が分かるか |
|---|---|
| Decision Map | なぜこの方針なのか |
| Roadmap | どの順序で進めるのか |
| Completion Criteria | 何を満たせば完成なのか |
| DevLog | 何を行い、何を確認したのか |
| Todo | 何が残っているのか |
最初に作ったのはDecision Mapだった
Decision Mapを作ろうと思ったきっかけはAIとの会話のなかだった。それまでの決断を箇条書きではなくツリー構造にまとめてもらったらわかりやすいのではないか、そう思いJSONにさせたのが始まりだ。すると、リストだと見えにくい関係が綺麗に視覚化された気がした。どの判断が同じ方向を向き、どこで別の方向へ分かれたのかが読める。
具体例として、ツリーではなく判断一つ分(ツリーの中にある一つのノード)の形を示す。ノードには、何を決めたかという判断内容それ自体以外にも、「なぜ記録する価値があるのか、どの判断領域に属するのか」といったフィールドを持たせるようにした。
判断ひとつ分の形
Decision Mapのノードが持つ項目。一つの判断と、それを記録する理由を、決まった形で要求する。
DecisionNode の構造
- schemaVersion 数値
- title 文字列
- createdAt 日時
- updatedAt 日時
項目の意味
id 文字列
この判断を指す名前。完成条件やRoadmapは、文章を写さずにこのIDだけを指す
用意 AI 確定 人間
type 文字列
判断の種類。覆すとやり直しになる原則か、領域をまとめる索引か、実装の境界か
用意 AI 確定 人間
priority 選択肢
記録する優先度。重要さではなく、曖昧になったり消えたりしたときの困り方で決める
選択肢:high / medium / low
用意 AI 確定 人間
status 選択肢
判断が効いているかどうか。保留のままAIに読ませないよう、採用前はdraftに留める
選択肢:draft / active / resolved
用意 AI 確定 人間
summary 文字列
何を決めたか
用意 AI 確定 人間
reasonToRecord 文字列
記録しないと何が曖昧になるかを書く欄。ここが埋まらない判断は、そもそも残す必要が無い
用意 AI 確定 人間
parentId 文字列
どの判断領域に属するか。かつては「どの判断の後に生まれたか」を指しており、増えるほど読めなくなった
用意 AI 確定 人間
relatedIds 文字列の繰り返し
親子では表せない結びつき。前提にしている判断や、対になっている判断を指す
用意 AI 確定 人間
content 入れ子
決めた中身そのもの。ここだけは判断ごとに形が違ってよい
用意 AI 確定 人間
AIは下書きを作れるが、この記録に入れてよいかを決めるのは人間に残してある。
MarkdownではなくJSONにした理由は、例えばスキーマでreasonToRecordを必須にすれば、記録の理由が存在しない(書けない)判断をAIが書けなくなるためだ。他にもIDがあればRoadmapや完成条件から同じ判断を参照できたり、UIを作り替えても、判断の正本はJSONのまま残せたりと、Markdownにはないメリットがたくさんある。AIが読めることやGitで差分を追えることはMarkdownでも変わらないが、JSONとスキーマが必要だったのは、判断の形を崩せないようにするためだった。
ただし形を決めてもAIが想定外の運用をすることがあった。例えば最初はparentIdというフィールドが、「どの判断のあとに生まれたか」という時系列を示すフィールドとしてAIエージェントに運用されてしまった。例えば保存方法を決めたあとに画面を決め、そのあとにデータ形式を決めた、という順序で開発が進んだ時、その三つが親子になる形のJSONが作られてしまった。しかし実際には保存方法の親は「永続化」であるべきだし、画面の親は「UI」、データ形式は「データモデル」などであるべきだ。実際に運用してみて発生した破れを防ぐルールは、JSONスキーマを補う形でAGENTS.mdに追記していった。
別のプロジェクトではDecision Mapへフェーズやタスクや進捗の情報が入り込んでしまった。本来、判断だけを入れるはずのJSONの役割が、作業分解表まで広がっていった。そこで順序はRoadmap、残件はTodoへ分けることにした。
作業記録は人間が書くほど続かなかった
Decision Mapは方針を残せても、一回の作業で何をしたかは残せない。そこでDevLogを作った。最初の形では変更内容、確認したこと、未確認のこと、次の作業をすべて人間が書いていた。
最初のDevLogの形
一回の作業に、変更と確認と不安を書く。項目はどれも人間が手で埋めるものだった。
DevLog(最初の形) の構造
項目の意味
title 文字列
後から一覧で探すための見出し
用意 人間 確定 人間
changeSummary 文字列
何をしたかを一本の文章で書く。なぜ変えたかも、どう直したかも、書くならここへ流し込むしかない
用意 人間 確定 人間
checkedItems 文字列の繰り返し
自分の目や操作で確かめたこと
用意 人間 確定 人間
concerns 文字列の繰り返し
確かめられていないこと。未確認を空欄にせず、未確認のまま残す欄
用意 人間 確定 人間
riskTags 選択肢の繰り返し
影響範囲。語彙を固定して、後から横断して数えられるようにする
選択肢:auth / permission / database / deletion / external_api / personal_data / dependency / env / unknown
用意 人間 確定 人間
aiRevisionCount 数値
AIとの往復回数。セッションログを見れば分かる数字を、当時は自分で数えて書き写していた
用意 人間 確定 人間
nextAction 文字列
次に始める場所
用意 人間 確定 人間
status 選択肢
この記録が閉じたかどうか
選択肢:open / closed
用意 人間 確定 人間
変更したファイルを持つ欄はまだ無い。Gitが知っていることを、記録は受け取っていない。
この形には変更したファイルを書く欄がなかった。Gitを見れば調べられるが、DevLogだけを開いたときには作業とファイルを結びつけられない。aiRevisionCountも手で数えていた。記録を増やすほど転記が増え、書くこと自体が面倒になった。
そこでGitやセッションログが持っている情報は機械に集めさせ、DevLogの下書きはAIに任せた。自由記述だったchangeSummaryもwhat、why、howへ分けた。次の図にある担い手はDevLogのフィールドではなく、各項目を誰が用意して誰が確定するかを示す注釈である。
DevLogの形と、項目ごとの担い手
一回の作業記録が持つ項目と、それぞれが何のためにあり、誰が用意して誰が確定するか。
DevLog の構造
項目の意味
changeSummary 入れ子
自由記述だった要約を三つに割った。分けないと、何を変えたかの記述に理由が飲み込まれる
changeSummary.what 文字列
後から探すための索引
用意 AI 確定 人間
changeSummary.why 文字列
なぜ変えたか。この記録の主役であり、コードの中に残らない唯一の情報
用意 AI 確定 人間
changeSummary.how 文字列
どう実現したか。実装の補足
用意 AI 確定 人間
changedFiles 文字列の繰り返し
Gitがすでに知っていること。人間がフォームへ転記する必要はない
用意 機械 確定 機械
checkedItems 文字列の繰り返し
人間が自分の目や操作で確かめたこと。テストが成功したことはここに入らない
用意 AI 確定 人間
concerns 文字列の繰り返し
確かめられていないこと。未確認を空欄にせず、未確認のまま残す欄
用意 AI 確定 人間
riskTags 選択肢の繰り返し
影響範囲。語彙を固定して、後から横断して数えられるようにする
選択肢:auth / permission / database / deletion / external_api / personal_data / dependency / env / unknown
用意 AI 確定 人間
aiRevisionCount 数値
人間の発話数から機械的に決まる。手で意味を作らない
用意 機械 確定 機械
nextAction 文字列
次に始める場所
用意 AI 確定 人間
status 選択肢
この記録が閉じたかどうか
選択肢:open / closed
確定 人間
機械が集められるものは機械に集めさせ、人間にしか書けない欄を残す。
三つの重みは同じではない。whatは後から探すための索引で、howは実装の補足になる。主役はコードに残らないwhyである。作業を終えたと判断する基準も必要になり、実装前にCompletion Criteriaを書くようにした。こうして最初に示した5つの記録が揃った。
承認を飛ばしたのではなく、止まる場所がなかった
Completion Criteriaを先に書く目的は、AIが勝手にゴールを広げたり、自分に都合のよい基準で完了と判断したりするのを防ぐことだった。AIが下書きし、人間が内容を確定してから実装へ進むつもりだった。
ところが実際にはAIが完成条件を書いたまま実装へ進むことがあった。人間が読んだときには作業が始まっているので、追加したかった要望を途中で差し込む。条件を確認する場が、実装中の方向転換に変わっていた。自分が承認を怠っているのだと思っていたが、記録の形と手順を調べると別の理由が見つかった。
完成条件のスキーマにあるのはversion、goal、completionCriteria、nonGoalsなどで、承認状態はなかった。誰が下書きし、誰が確定したのかも保存されない。実データ34件を確認しても、人間による承認を判別できるものは一つもなかった。
完成条件の形
何を満たせば完成なのかを、実装の前に置く記録。実際のスキーマにある項目はこれで全部である。
CompletionCriteria の構造
- schemaName 文字列
- schemaVersion 数値
-
- id 文字列
-
- id 文字列
項目の意味
version 文字列
完成条件・Roadmapのフェーズ・DevLogを繋ぐキー。着手の順序ではなく、ただの識別子
用意 AI 確定 AI
goal 文字列
このバージョンで到達する状態。条件はこれを判定できる形に割ったもの
用意 AI 確定 AI
completionCriteria 入れ子の繰り返し
満たせば完成と判定できる条件
用意 AI 確定 AI
completionCriteria.description 文字列
判定できる一つの条件。読んで判定できなければ条件になっていない
completionCriteria.required 真偽
完成に必須か、満たせば望ましい任意目標か
nonGoals 入れ子の繰り返し
今回やらないこと。スキーマが「スコープ逸脱を防ぐ主装置」と呼んでいる欄で、方向がぶれるのを止めるならここが効くはずだった
用意 AI 確定 AI
nonGoals.description 文字列
やらないと決めたことと、その理由
人間が確定する欄が一つも無い。承認を求めているつもりだったが、形の側がそれを要求していなかった。
記録の書き方を定めたルールも調べた。Decision MapやDevLogには編集と確認のルールがあったが、Completion Criteriaにだけ存在しなかった。最後にリポジトリの手順書を見ると、書き終えたら確認を取らずに次へ進むと自分で書いていた。
手順書が求めている確認
リポジトリに置いたAIエージェント向けの手順書から、記録ごとの確認の要否を抜き出した。
| 項目 | ユーザ確認 |
|---|---|
| Completion Criteria | 不要(書き終えたら確認を取らずに次へ進む) |
| DevLog | 必須 |
| Decision Map | 必須(無断で編集しない) |
承認がいちばん要るはずの記録だけ、確認するなと自分で書いていた。
承認を素通りしていたのではなく、そもそも止まる場所がJSONに設計されていなかったのだ。
AIの心配事と自分のやりたいことは違った
DevLogのconcernsは、確かめられていないことを残す欄である。現在はほとんどAIが書いている。内容は質が高く、IDを差し替えられる保存処理や同時編集で一方が消える問題など、人間が見落としやすい穴を見つけてくれる。
ただし自分が実際に使っていて気になることとは方向が違った。AIは整合性の穴や未確認のリスクを挙げる。自分の側から出てくるのは、新しい機能を足したい、触り心地を直したいという要望である。心配事とやりたいことは同じ欄には入らない。
このブログにはDevLogが4件あり、concernsは合計41件ある。それでもTodoは0件だった。開発が止まっているわけではない。次に何を作るかという判断だけが記録の外で行われている。
そこでTodoを使っているほかのプロジェクトも調べた。DevLogから引き継いだものを除く25件を見ると、AIが書いた分析は60字を超え、自分で書いた要望は60字以下に分かれていた。短い10件のうち7件は完了していたが、長い15件で完了していたのは2件だった。
手で書いたTodoと、AIが書いたTodo
DevLogから引き継いだものを除く25件を、本文の長さで分けた。長いほうがAIの分析で、短いほうが自分の思いつきになる。
| 項目 | 件数 | 片付いたもの |
|---|---|---|
| 短いもの(60字以下) | 10 | 7 |
| 長いもの(60字超) | 15 | 2 |
片付いているのは、自分が思いつきで書いたほうだった。
短いTodoには、タブの選択状態をブラウザ更新後も保ちたい、一覧のIDをタイトルへ変えたいといった要望が並んでいた。片付いているのはこちらだった。AIはconcernsをいくらでも出せるが、それを作業として採用するかは人間が決める。プロジェクトが前へ進んでいると感じるのも、自分の要望をTodoへ書いたときだった。
Roadmapにも決める前の理由は残らなかった
Roadmapはフェーズごとにバージョンを持っている。しかしversionはCompletion CriteriaとDevLogを結ぶ識別子であり、着手順序ではない。実際にはv0.7.1のあとでv0.6.2へ戻ることもある。それでも番号が大きいほど前に進んでいるように見えるため、Roadmapを進捗表として読んでしまった。
本当に欲しかったのは、いま何を持っていて何が欠けているかを示す現在地の地図だった。ところがRoadmapに残っているのは、決めたあとの順序だけである。なぜその方向へ進みたいと思ったのか、ほかに何を考えていたのかは残っていなかった。
足りないのは決める前の記録だった
5つの記録には共通点がある。Decision Mapは決めた方針を残す。Roadmapは決めた順序を示す。Completion Criteriaは決めた完成条件を持つ。DevLogは終わった作業を記録し、Todoは作業として採用した残件を集める。すべて何かを決めたあとの記録だった。
決める前にやりたいことや迷っていることを書き出す場所はない。思いつきはAIとの会話に現れ、そのセッションの中で流れていく。完成条件の確認時に要望を差し込みたくなるのも、ほかに置く場所がないからだった。
ここで二つの欠落を分けて考える必要がある。一つは承認フローの欠陥である。Completion Criteriaに承認状態を持たせ、AIが書き換えたものを承認待ちとして見せなければならない。これは既存の記録を正しく機能させるための修正になる。
もう一つは記録そのものの欠落だ。作業として採用する前の要望を置き、掘り下げる場所が必要になる。これは5つのどれかに欄を足す話ではない。6つ目の記録として考える方がよさそうだ。ただし、どのような形にするかはまだ決まっていない。
これまで人間に残すべきなのは、判断や確定の権限だと考えていた。実際に足りなかったのは、その権限を使うより前の段階だった。まだ何も決めていない時間も、AI開発では保存する必要がある。