はじめに

私が担当するサービスには、画面ごとの仕様書がありませんでした。あるのは、QAのために書かれたテスト仕様書と、仕様を決めた会議の議事録、設計当時の資料だけです。設計資料はコードの変更に追いついていません。

この状態を、画面ごとに1枚、今どう動いていてなぜそうなっているかを書いた仕様書を全画面分そろえることで解消したいと考えました。画面は3つのサイトを合わせて180を超えます。人が手で書く時間はありません。そこでAIエージェントを書き手にしました。

AIは、もっともらしいが事実と違うことを平気で書きます。関数やテーブルの名前から挙動を推測し、確かめずに断定します。180を超える仕様書が速く出てきても、どれを信じてよいのか分からなければ、業務側に渡せません。

この記事は、その「信じてよいのか分からない」を減らすためにやったことをまとめたものです。結論から言うと、次の3つです。

  • コード・議事録・テスト仕様書・実画面という手元の材料に「何の事実か」という役割を与えて突き合わせ、食い違った事実と決まらないことは「確認が必要な事項」として人に渡す
  • AIが間違える典型を名指しで禁止し、要になる主張は証明できるコード行があるときだけ確定させる
  • 別のAIに反証させて根拠のない断定を撤回させ、この手順一式をスキルとして固定して全画面に回す

説明は最初から最後まで1つの例で通します。カスタマーサポート向けサイトの会員一覧に、退会した会員と削除された会員は表示されるのか。単純に見えますが、AIが最初に出した答えは間違っていました。

題材は、これまでの記事と同じ架空のオンラインレッスンプラットフォームです。会員向け・講師向け・カスタマーサポート向けの3つのサイトが、会員や講師といった共通のドメインを共有しています(構成はモジュラーモノリス構成の記事を参照してください)。バックエンドはGoで、ORMにGORMを使っています。AIエージェントはClaude Codeです。実際に起きた出来事を題材に置き換えて書いていますが、仕組みそのものは言語やツールに依存しません。

対象読者

  • Claude CodeなどのAIエージェントに定型作業を任せていて、成果物の質を担保したい方
  • 仕様書が無い、または古いサービスを抱えていて、AIで整備できないかと考えている方
  • AIの出力に混ざる「もっともらしい誤り」を、実務でどう抑え込むかの具体例を知りたい方

この記事で得られること

  • 全画面分の仕様書をAIに書かせるとき、作る文書と手元の材料をどう定義したか
  • 材料に役割と優先順位を与え、食い違いを決める進め方
  • AIに推測で断定させないための具体ルールと、別のAIによる反証検証の仕組み
  • 出力を業務の言葉に翻訳させることが、なぜ精度に効くのか
  • 手順一式をスキルとして固定し、全画面を同じ品質で回す運用

動作確認環境

種別バージョン
Go1.26.3
GORMv1.30 系
Claude Code2.1 系

1. やりたかったこと ── 全画面分の仕様書を、人が確認できる形で

1.1 作る文書

まず、AIに何を作らせるのかを決めました。ここで言う仕様書は、画面1つにつき1枚の文書です。「この画面が今どう動いていて、なぜそうなっているか」を、業務側の人が読める言葉で書きます。画面の項目(表示する情報や操作のまとまり)ごとに「定義」「業務ルール」「なぜそうなっているか」を持ちます。

そして、仕様書の各項目に必ず「確認が必要な事項」の欄を置き、末尾に一覧として集約しました。AIが材料から確定できなかったこと、材料同士が食い違っていて人の判断が要ることを、ここに集めます。

この欄を最初から用意したのには理由があります。全画面分の仕様書をAIに書かせるとき、人がやるべき仕事は「全文を読んで正しいか確かめる」ことではありません。それでは人の負担がほとんど減りません。人がやる仕事は、AIが「決められなかった」と申告した箇所だけを判断することです。だから仕様書は、「確定した事実」と「確認が必要な事項」の2つに分かれていなければなりません。AIには「決められないことは推測で埋めず、この欄に回せ」と指示します。以降の章で「決める」「残す」と言うときは、この2つの欄のどちらに書くか、という話です。

1.2 全画面をどう回したか

画面は3サイトで180を超えます。進め方は次のとおりです。

  • 各サイトの画面一覧を作り、画面名と画面のURLを列挙する
  • 1画面ずつ、AIエージェントに仕様書を書かせる。手順はスキル(AIエージェント向けの手順書。8章)として固定する
  • 出てきた仕様書の「確認が必要な事項」を人が見て、業務側に確認する
  • 確認結果を仕様書に反映する

1画面あたりの人の作業を、最初の承認と「確認が必要な事項」の判断に絞れたので、全画面でも回りました。ただし、それは「確定した事実」の欄が本当に事実であってこそです。ここが崩れると、人は結局全文を疑って読むことになります。この記事の残りは、その欄を信じられるようにするための取り組みです。


2. 手元にある材料

仕様書は1つの材料からは作れません。手元にあった材料(記事タイトルで言う「ソース」)は次の4つで、どれも多くの会社にある種類のものです。

材料何か読者の会社での対応物
コード実際に動いているプログラムリポジトリ
議事録仕様を決めた会議のメモ会議メモ、決定が残ったチャットのスレッド、チケットのコメント
テスト仕様書「この画面でこう操作するとこうなるはず」を書いた手順書受け入れテスト手順書、QAのテストケース一覧
実画面AIがブラウザ操作で検証環境を開いて読み取った画面ステージング環境

名前が似ているので1つ注意があります。テスト仕様書は材料の1つで、仕様書はこれから作る文書です。この記事では両者を区別して使います。

さっそく会員一覧の例を見ます。ここで「退会」は会員自身が利用をやめた状態で、データは残ります。「削除」は運営がその会員のデータを消した状態です。カスタマーサポート向けサイトの会員一覧について、4つの材料はそれぞれこう言っていました。

材料言っていること
テスト仕様書「退会した会員は一覧に表示されない」。ただし欄外に「担当者に確認」とメモが残っている
議事録「退会した会員は問い合わせ対応で履歴を見るため一覧に残す。削除した会員は出さない」と半年前に決定
コード一見、一覧をそのまま開いたときの取得処理に、退会や削除で絞り込む条件が見当たらない
実画面一覧の上部に「退会」で絞り込むボタンがある

並べただけでは決まりません。テスト仕様書と議事録は正反対で、コードは何も絞っていないように見え、実画面には退会者がいる前提の絞り込みボタンがあります。この状態でAIに「仕様書を書いて」と言うと、どれかをつまみ食いした、もっともらしい文章が出てきます。


3. 材料に役割を与える

材料をそろえたら、次に材料ごとに「これは何の事実か」を決めます。役割を決めずに混ぜると、AIは都合のよい材料を選んで断定します。

材料何の事実か扱い
コード今どう動いているか今の挙動はこれで確定する。ただしバグも含むので「コード=正しい仕様」ではなく、食い違い自体は残す
議事録なぜそうなっているか意図と決定経緯の根拠。挙動の事実ではない
テスト仕様書どう動くと期待されているか人が書いた期待。未確認のメモが混ざるので、鵜呑みにしない
実画面何が表示されているか文言・並び順・遷移先の事実。ただし今のデータ状態しか見えない

ポイントは、テスト仕様書を事実の根拠にしないことです。テスト仕様書は「こう動くはず」という期待を書いた文書であって、事実でも意図でもありません。今回の例でも、「担当者に確認」のメモが残ったまま「退会した会員は表示されない」と書かれていました。これを事実として仕様書に書いてしまうと、間違いがそのまま文書になります。

私たちの場合、もともと仕様書が無く、挙動を書いた資料がテスト仕様書しか無かったので、放っておくとテスト仕様書が事実上の仕様になります。それを避けるために、テスト仕様書は「仕様書の各項目が満たされるか確かめる手段」という位置に置き直しました。具体的には、仕様書の各項目に、その項目を確かめるテスト仕様書の番号を対応づけて末尾に書きます。仕様書が中身を持ち、テスト仕様書はそれを検証する側です。

コードを「今の動きの事実」に置くのは、手元にあって確認しきれるからです。逆に、議事録に書かれた「なぜ」はコードには残っていないので、議事録を読まない限り仕様書の「なぜそうなっているか」の欄は埋まりません。材料ごとに得意なことが違う、というだけの話です。


4. 食い違いをどう決めるか

複数の材料を持ち込めば、必ず食い違います。ここで一番やってはいけないのが、「テスト仕様書ではこう、議事録ではこう」と両論併記して終わることです。それは仕様書ではなく差分メモで、1章で決めた「確定した事実」の欄には入りません。

そこで、食い違ったら次の順で決めるというルールにしました。

  1. 「今どう動いているか」はコードで確定し、確定した事実の欄に書く。挙動そのものを「要確認」のまま放置しない
  2. ただし、材料同士が食い違ったという事実は、挙動が確定しても必ず「確認が必要な事項」に残す。食い違いはコードのバグかもしれず、どちらが正しいかはコードだけでは決まらない
  3. 挙動がコードでも決まらないものも「確認が必要な事項」に残す。多くは業務的な意図や、確認手段が無いもの

会員一覧の例に当てはめます。コードを読んで最終的に確定した結果は次のとおりでした。AIが最初に出した答えは違っていて、その経緯とコードの読み方は次の章で説明します。

  • 削除された会員は、一覧に表示されない
  • 退会した会員は、一覧に表示される

この2行が「確定した事実」の欄に入ります。同時に、「退会」と「削除」は別の軸で、テスト仕様書の「退会した会員は表示されない」はコードと食い違います。議事録の決定(退会者は残す)とも食い違います。テスト仕様書の記述がコードとも議事録とも食い違う、という事実は「確認が必要な事項」に記録します。

食い違った箇所は、そのまま仕様やテストの誤りの発見器になります。確認の結果、今回はテスト仕様書の誤りでしたが、議事録の決定と実装がずれていればバグの発見になります。仕様書を作る作業が、いつのまにか監査になっていました。ここが複数の材料を突き合わせる一番の収穫でした。

挙動がコードでも決まらないことも残ります。議事録の決定は半年前のもので、「退会者を一覧に残す判断が今も有効か」はコードからは分かりません。これも「確認が必要な事項」に残し、担当者に聞きます。

残し方にも1つルールがあります。確定した事実に、AIが想像した意図や「こうすべき」を足させないことです。「退会者を表示するのは意図的か要確認」「業務上は非表示が望ましいのではないか」といった文は、それらしく見えて根拠がありません。先ほどの「退会者を一覧に残す判断が今も有効か」は議事録の決定という根拠があるので残します。根拠なく意図を疑う文は書かせません。書けるのは「何がどうなっているか」と「何と何が食い違っているか」までで、正しいかどうかの判断は業務に委ねます。


5. AIが間違える典型を名指しで禁止する

ここまでのルールを渡しても、AIは確認したつもりで見落とします。会員一覧の例でも、AIが最初に出した答えはこうでした。

一覧をそのまま開いたときの取得処理に退会や削除を絞り込む条件は無い。したがって、退会した会員も削除された会員も一覧に表示される。

半分正しく、半分間違いです。なぜ間違えたかを見ると、対策が見えます。

5.1 禁止したこと

抽象的に「正確に書け」と言っても効きません。過去に実際に間違えたパターンを、具体的に禁止するほうが効きました。

  • 名前から挙動を推測して書かない。たとえば「会員テーブルに withdrawn_at という列があるから退会日時で判定しているはず」のような推測は、実際には別の値で判定していることがある。関数の本体を読んでから書く
  • 対象に含まれるか含まれないかを、明示的な絞り込み条件の有無だけで判断しない。「条件が無いから全部含まれる」も「どこかで自動的に除外されているはずだ」も、確かめずに決めつけている点で同じ誤り。ORMの自動条件や結合条件が暗黙に絞ることがあるので、読んで確かめる
  • 要になる主張は、証明するコード行を確認できたときだけ「確定」にする。要になる主張とは、算出式・対象の絞り込み・保存先・件数・権限のように、間違えると仕様全体が狂うもの。確認できなければ推測で埋めず、「確認が必要な事項」に回す

5.2 会員一覧の例で起きたこと ── ORMの論理削除

AIが間違えたのは2つ目の禁止項目でした。GORMには論理削除の仕組みがあり、公式ドキュメントにはこう書かれています。

If your model includes a gorm.DeletedAt field (which is included in gorm.Model), it will get soft delete ability automatically!

モデルに gorm.DeletedAt 型のフィールドがあると、削除は deleted_at に日時を入れる更新になり、通常の検索には deleted_at IS NULL が自動で付きます。会員のモデルは次のような形でした。

1
2
3
4
5
6
7
// 会員。DeletedAt があるので GORM は論理削除として扱う
type Member struct {
	ID        uint
	Name      string
	Status    string         // 利用状態。"active" / "withdrawn" など
	DeletedAt gorm.DeletedAt // 論理削除の日時
}

一覧をそのまま開いたときの取得は、絞り込みを何も書いていないように見えます。

1
2
var members []Member
db.Find(&members)

しかし実際に発行されるSQLには、書いていない条件が付きます。GORMの DryRun(SQLを組み立てるだけで実行しない設定)で確認した結果です。

1
SELECT * FROM `members` WHERE `members`.`deleted_at` IS NULL

つまり、削除された会員はコードに1行も書かれていない条件で除外されます。一方、そのまま開いたときに Status を絞る条件は無いので、退会した会員は表示されます(「退会」ボタンを押したときだけ、Status で絞る条件が付きます)。「絞り込み条件が無い」という観察は正しく、そこから「全部表示される」と結論したのが誤りでした。

これはGORM固有の話に見えますが、同じ罠はどのORMにもあります。デフォルトのスコープ、内部結合の条件、ビューの定義などです。AIは「見えているコード」から推論するので、「見えていない条件」を暗黙に足す仕組みがあると、そこで必ず間違えます。だから、含まれるか含まれないかを書くときはモデルの定義と結合条件まで読んでから、と禁止項目に明記しました。


6. 別のAIに反証させる

禁止だけでは足りません。禁止を渡しても、AIは「確認しました」と言って確認していないことがあります。だから、AIが書いただけの主張を、そのまま信用しない仕組みを後ろに置きました。

6.1 自己確認は効かない

生成を担当したAIに「本当に合っている?」と聞いても、たいてい「合っています」と返ってきます。自分が書いた文章を自分で読み直しても、同じ前提で同じ推論をなぞるだけだからです。

そこで、生成とは別のAIエージェントを「検証役」として起動し、要になる主張を反証させるようにしました。Claude Codeにはサブエージェントという仕組みがあり、別の文脈で動くエージェントに作業を委ねられます。公式ドキュメントには次のようにあります。

Each subagent runs in its own context window with a custom system prompt, specific tool access, and independent permissions.

生成側の会話履歴を引き継がないので、生成側の思い込みを共有しません。これが検証役に向いている理由です。渡すのは主張だけで、生成側がその主張に至った根拠や推論は渡しません。渡してしまうと、思い込みまで検証役が引き継いでしまいます。

6.2 渡す指示と判定

検証役に渡す指示は、おおむね次の形です。

1
2
3
4
次の主張が、実コードで本当にそうか反証せよ。
証明するコードの該当行を引用し、違えば正しい挙動を述べよ。

主張: 「会員一覧には、退会した会員も削除された会員も表示される」

判定は単純です。

  • 証明するコード行を引用できたなら、その主張を確定する。「含まれる」側の主張では、取得処理とモデル定義の両方が証明の対象になる
  • 反証された、または証明する行を出せなかったなら、その主張を撤回し、正しい挙動に直すか「確認が必要な事項」に落とす

会員一覧の例では、検証役はモデル定義の DeletedAt gorm.DeletedAt の行を引用しました。そのうえで次のように返しました。「削除された会員はGORMの論理削除により自動で除外される。退会した会員を暗黙に絞る仕組みはモデルに無く、発行されるSQLの条件も deleted_at IS NULL だけなので表示される」。生成側の主張は撤回され、仕様書は次のように直りました。

1
2
3
4
5
6
7
8
### 会員一覧: 表示対象

- 退会した会員は一覧に表示される。退会で絞り込むこともできる
- 削除された会員は一覧に表示されない

#### 確認が必要な事項
- 退会した会員を一覧に残す判断は半年前の議事録によるもの。現在も有効か担当者に確認
- テスト仕様書には「退会した会員は表示されない」とあり、コードおよび議事録と食い違う

「証明する行を引用できなければ撤回」という基準が肝です。AIは主張を作るのは得意ですが、根拠の提示を強制されると弱くなります。生成と検証を分け、後段で「行を出せ」と迫ると、根拠のない断定があぶり出されます。

6.3 全部にはやらない

反証検証は、すべての文にかけるとコストが見合いません。全画面を回すので、かけるのは次に絞っています。

  • 対象に何が含まれるか(今回の例)
  • 算出式、分岐条件、保存先、権限のように、間違えると仕様全体が狂う主張
  • 前回の生成や別の材料と食い違う主張
  • 「この項目は業務的にこういう意味だ」と踏み込んだ主張

とくに最後の項目は要注意です。コードから分かるのは「どう動くか」までで、「業務的に何を意味するか」はコードに書かれていません。AIはここを名前から補いがちなので、意味を断定している文は必ず反証にかけます。


7. 出力を業務の言葉に翻訳させる

もう1つ、地味ですが効いたルールがあります。プログラム内部の名前を仕様書に書かせないことです。

テーブル名や関数名はもちろん、状態を表す内部の値や権限のキーも本文に出しません。会員一覧の例で言えば、withdrawn ではなく「退会」、deleted_at ではなく「削除された会員」と書かせます。

これは体裁の問題に見えて、実は精度に効きます。内部の名前をそのまま貼るのは、「コードを写しただけで、意味を理解していない」状態と表裏一体だからです。業務の言葉に翻訳することを強制すると、AIは意味を確定せざるを得なくなります。確定できないものは翻訳できないので、「確認が必要な事項」として表に出てきます。6章の仕様書の抜粋に内部の名前が1つも無いのは、このルールの結果です。

読み手にとっての効果も大きいです。1章で書いたとおり、仕様書を読むのは業務側の人です。withdrawn と書かれても分かりませんが、「退会」と書いてあれば、議事録の決定と突き合わせて「これは今も有効か」を判断できます。


8. 手順をスキルとして固定し、全画面に回す

ここまでのルールは、毎回プロンプトに書くこともできます。しかし画面は180を超え、生成は何日にも分かれ、実行する人も複数人です。散文のルールは「読まれない・解釈がぶれる・忘れる」で形骸化します。1枚目と150枚目で品質が違えば、業務側は結局全文を疑って読むことになります。

そこで、手順一式をスキルとして固定しました。スキルはAIエージェント(Claude Code)が特定タスクを実行するための手順書で、呼び出すとその手順とルールに沿って成果物を生成します。前作の記事でテストの書き方を固定したのと同じ考え方です。

実行する人は、スラッシュコマンドで画面名を渡します。自然言語の依頼では起動しません。

1
/<スキル名> 会員一覧

スキルは、次の手順を踏みます。

  • 画面一覧から対象画面を特定し、関係するテスト仕様書と読むコードの範囲を示して、人の承認を得る
  • 集めたテスト仕様書を画面の項目ごとに並べ替える
  • 議事録から「なぜそうなっているか」を抽出する
  • コードを読み、食い違いのある箇所と要になる主張を確認する
  • 検証環境の実画面を開き、文言・並び順・遷移先を確認する
  • 別のエージェントを検証役として起動し、要になる主張を反証させる
  • 決まったことは業務の言葉で書き、決まらなかったことは「確認が必要な事項」に集約する
  • 既に仕様書がある画面なら、項目の見出しを引き継いで更新する(再生成のたびに見出しが変わると、確認済みの事項と対応が取れなくなる)

これで、誰がいつ実行しても同じ構造の仕様書が出ます。

スキルに固定してもう1つ効いたのは、禁止項目が育つことです。5章の禁止リストは、最初から揃っていたわけではありません。ORMの論理削除で間違えた、名前から推測して間違えた、という失敗が起きるたびに、その失敗を禁止項目に変換してスキルに足しました。次の画面からは同じ間違いをしません。プロンプトに毎回書いていたら、この蓄積は残りません。


9. まとめ

180を超える画面の仕様書をAIで書き、業務側が読める形で揃えるためにやったことは、次のとおりです。

  • 仕様書を「確定した事実」と「確認が必要な事項」の2つに分け、人は後者だけを判断する
  • 手元の材料に「何の事実か」という役割を与え、テスト仕様書を事実の根拠にしない
  • 食い違いは両論併記せず、今の挙動はコードで確定する。ただし食い違った事実と、コードでも決まらないことは「確認が必要な事項」に残す
  • 過去に間違えたパターンを名指しで禁止し、要になる主張は証明するコード行があるときだけ確定させる
  • 生成とは別のエージェントに反証させ、証明する行を引用できなければ撤回させる
  • 出力を業務の言葉に翻訳させ、意味を確定できないものを表に出す
  • 手順一式をスキルに固定して全画面に回し、失敗を禁止項目として蓄積する

AI生成ドキュメントの信頼性は、生成の速さではなく、材料の役割づけと反証の仕組みで決まります。速く作ること自体はもう難しくありません。難しいのは「速く作ったものを信じてよい状態にする」ことで、それはプロンプトの巧拙ではなく、手順の設計で解く問題だと感じています。

関連リンク