こんばんは、久代です。
この記事はAnthrotech Advent Calendar 2025の7日目の記事です。
昨日のここあさんの記事です。8GBがGPURAMのことかと思ったら普通のRAMでびっくりしました。今のローカルLLMってこんなに簡単に動くんですね。
ここあさんに引き続き、私もLLM関連の話になります。
AIエージェントと往く26,000行 + 11,000行のプロダクト
最近、ちょっとしたソーシャルメディアのようなWebサービスを作っています。
このプロダクトの年齢はだいたい1ヶ月程度ですが、現在バックエンド26,000行、フロントエンド11,000行とかなり大きなコードベースに育っています。本業と並行しながら、かつうち1週間はインフルエンザで動けていなかったことを考えるとなかなかの規模だと思います。

AIエージェントを利用して作っていますが、現在も破綻せずにうまく機能追加や改善を繰り返すことができています。このような中規模程度のプロジェクトをAIで作るというのはなかなかない機会でもありますので、備忘録も含めて今のやり方をこの記事にてまとめたいと思います。
個人開発で、一人で回す中規模プロダクト向けの話になりますのでご注意ください。
全てを司る神マークダウン domain.md
本記事で重要になってくるのが domain.md という単一のマークダウンファイルです。実際のファイル名はどのようなものでも構いませんが、この記事では通して domain.md としています。
このファイルはプロダクトの全てを表現するようにします。ファイルの内容は利用するエージェントに合わせて粒度を変えて、できるだけ人間がサボれるようにするのが良いと思います。2025年末のエージェントのレベルでは、下記3つが良いでしょう。それぞれ後述します。
コーディング規約
エンティティ定義とドメイン知識
API定義
何か変更を行いたい時は、AIエージェントにその変更を直接指示する代わりに、このファイルを変更してから、Gitのログを確認して実装に移ってもらいます。このやり方も後述します。
このようにコードの実装と仕様書マークダウンである domain.md を分けて管理するのは、人間側の理解を容易にするための工夫です。なんらかの仕様を確認したい時、コードを読むより自然言語で定義されている方が理解が簡単ですし、それが動作と異なっている場合は、指摘するだけで直してくれます。
KiroやGitHub Spec-Kitじゃダメなの?
KiroやGitHub Spec-Kitなど、いわゆる仕様駆動開発を支援するソフトウェアがいくつか存在します。それを利用してはダメなのでしょうか?
ダメとは言いませんが、私は domain.md は手動で書くべきだと考えています。私が考えるに、domain.mdの役割は2つあります。
欲しい世界の言語化
AIエージェントでは難しい部分の補足
1については想像がしやすいと思います。そのソフトウェアがなぜ欲しいのか、それがあればどのような課題が解決されるのかを記入します。これはAIに依頼するものではなく、自分が世界に対してどういう思想を持つかどうかの話になってきますので、自分の中から絞り出すほかありません。
そして2番目、残念ながら、思想を完璧に言語化したとしても2025年末のAIエージェントでは一発で誤りなく仕様を実装にしてもらうことはできません。テストや型システムなどで補強しつつ、手書きのdomain.mdを用いてAIエージェントの動作を補強してあげる必要があります。ここはある程度AIが関与する余地がありますが、最終的にはモデルの性能に依存した定義しか出てきません。
このようなことを考慮すると、私はKiroやGitHub Spec-Kitを利用するより直接書いた方がより良い答えに早く辿り着くと考えています。
では実際にマークダウンファイル `domain.md` を作っていきましょう。
お好みでコーディング規約
コーディング規約はお好みです。私の場合は簡単にパッケージの役割にだけ触れています。
**domain package**
Entity / Value Object / Store Interface / ビジネスロジック ドメインの知識を組み合わせて純粋関数として表現可能なものは全てdomain packageに置く。 テストを書く。
**usecase package**
ユーザーストーリーをドメインの知識やビジネスロジックを複数組み合わせて実現する。 ビジネスロジックを極力持たない。ドメインの知識を組み合わせ、ドメインで定義されたストアインターフェースを通して永続化を行う。 権限周りはここ。テストを書く。
**interfaces package**
外部とのインターフェース。HTTPハンドラ。
**store package**
DB等の永続化ストアの実装。ただしDeletedAt/UpdatedAtの更新は行って良い。
一番トップに書きましたが、実は私としてはコーディング規約をあまり詰める必要性を感じていません。改行や関数名などのコードの細かい実装は人間が見ることもほとんどなくなるでしょうし、AIがどこに何を書くかくらいの判断ができれば十分です。
ドメイン知識とエンティティ
作ろうとしているプロダクトがどのようなデータの集まりで実現できるのか、そのデータはどのように作成され、どのような振る舞いを持っているのかを定義します。
下記はアカウントの部分の例です。シンプルですね。
Account { // <- これがエンティティ
AccountID
Type // Normal, Admin
AccountStatus: ACTIVE, SUSPENDED, GHOST
}
// ここからドメイン知識
アカウントは、サービスを使う人としての最小単位である。認証にはJWTを利用する。 /admin/ から始まるエンドポイントはAccountType=Adminの場合のみ動作させる。アカウントは作成時にデフォルトのProfileを1つ持つ。
基本的に、エンティティを定義する場合はデータベースにどのように格納されるかを考えながら記入していきます。Adminエンドポイントやプロフィールなど、関連するドメイン知識も関連しそうな部分につらつらと書いていきます。
アカウントの例だとシンプルすぎるのでもう少し。下記は画像の定義です。
MediaEntity {
AccountID
MediaEntityID
Exif
FileName
Hash
}
MediaEntityは、サービス内で利用される画像の単位である。画像のアップロードのみ対応。アップロードはAPIが仲介する。
入力フォーマットはJPEG/PNG/WebP/HEIFを受け付ける。HEIF/WebPはサーバー側でデコードしてリサイズパイプライン(Original / Large / Thumbnail)に流し、保存は既存サイズのJPEG/PNGで行う。
アップロード時にハッシュ計算し、AccountIDにすでに全く同じ画像が存在しないことを確認。同じ画像が存在する場合はその画像をアップロードした画像として扱う。 Original / Large(長辺1600px)/ Thumbnail(長辺400px)の3サイズを生成し、`ImagePath` として保存。 レスポンスの `imagePaths` に `sizeKind` ごとのURLを含める。
少し文量が増えましたが、基本的にはアカウントと同じです。アップロード時にして欲しいリサイズやハッシュ計算に触れています。
写真周りの仕様はプロダクトの将来を先読みしリサイズやハッシュ計算での重複排除を考えたものになっています。当たり前ですが、この辺りの仕様定義をせず「写真をアップロードできるようにする」など一文で済ますと、リサイズや重複排除は絶対に行ってくれません。AIエージェントは気が利かないものなのです。
domain.mdに書かれていない暗黙の仕様をできる限り潰し、考慮漏れや矛盾などを避けながら、解決したい課題もここでカバーする必要があります。この辺りは割とエンジニアリングっぽいですね。
最初のワンショットを大事にしよう
最強のdomain.mdができた!これ以上の仕様は思いつかない!という段階まで来たら、実際にAIエージェントに手を動かしてもらう時です。最初のワンショットです。
最初のワンショットは非常に重要です。今後の変更やコードの追加は全てこのワンショットで生成されたコードに合うように作業されます。そのため、ここの成果物が曖昧なままだと、今後の作業についても曖昧な出力になってしまいます。
下記はここで使うプロンプトの例です。最初はDomain層の実装に集中してもらうのが良いでしょう。現在のAIエージェントはコンテキストが圧迫されると性能が落ちます。
domain.mdに書かれている要素を全て `domain` パッケージに落とし込んでください。 すべてのエンティティは構造体とし、そのまま公開して構いません。 構造体を取得するためのストアもインターフェース化してください。こちらは単に取得・保存するだけではなく、ワークフローで想定される取得が簡単にできるようなメソッドを生やしてください。
コメントは "// {method_name} - 日本語で一行程度" 書いてください。複雑なロジックを持つメソッドは複数行解説を書くことができます。コード内にコメントは書かないでください。
ビジネスロジックとして純粋な関数に閉じ込められるものはドメイン内に定義してください。
引数には構造体の配列を受け取ることができ、Usecase層は単にストアの実体を叩き、その結果をドメインに渡し、その返り値をUsecaseがストアの実体に登録することでビジネスロジックが完了できるような仕組みであることが望ましいです。
ファイルは大まかにEntityごとに分離してください。
Domain層が完成したら、Store層で利用できるインターフェースも完成しているはずです。簡単にレビューして、問題がなければStore層の実装まで依頼してしまいましょう。Store層の実装はインターフェースという正解がありますので、大きくつまずくことはないはずです。
次に実装すべきはUsecase層ですが、これを定義する前にやらなければならないことがあります。
人が定義すべきもう一つのインターフェース API定義
現在のAIエージェントを利用する時は、ここの実装を明確にしておく必要があります。Usecase層とInterfaces層の実装が強力にDomain層のインターフェースに引きずられ、Domain層のインターフェースとAPIが1:1で対応するようなコードを書き始めてしまうためです。
例えば、私のプロダクトでは「アカウント作成時に必ずプロフィールを作る」という制約があります。普通に考えると、このロジックはUsecase層で達成して欲しい事柄です。
package usecase;
func CreateAccount() {
owner = domain.CreateAccount(); // アカウントを作ったら
domain.CreateProfile(owner); // プロファイルも作る
}
ですが、AIエージェントに明確に意図を教えてあげないと、下記のようなコードを吐き出します。Domain層のインターフェースをとにかくUsecase層を経由して叩くことだけが目標となってしまっています。
package usecase;
func CreateAccount() {
domain.CreateAccount();
}
func CreateProfile(owner) {
domain.CreateProfile(owner);
}
これを解消するために、domain.mdにAPIとレスポンスの定義を載せてあげます。
POST /auth/create-account { email }
アカウント作成して自分のプロフィールを取得
{
account: Account
profiles: [Profile]
}
このようにハンドラの定義をしてあげると、AIエージェントに対して「このAPIはアカウント情報とプロフィールを返す必要がある、そのためにはUsecase内でこの二つを作成する必要がある」という思考を強制させることができます。
ここまでできたらUsecase層/Interfaces層の実装に移ることができます。下記はプロンプト例です。
domain.mdをよく理解してください。domain.mdにAPI定義が書かれており、すでにdomain層にエンティティとストアのインターフェースが存在します。
これらの情報を元にUsecase層/Interfaces層/main.goでパスとのマッピングの実装を行なってもらえますか?APIとusecase層は1:1で対応します。
domain層のビジネスロジックは、できる限りUsecase層がストアの実装を叩き、その結果をドメインに渡して返り値をストアに登録することで完了できるような仕組みになっています。
テストを書いて自走を助ける
私自身は怠惰なプログラマなのでテストなんてほとんど書いたことはありませんが、AIエージェントにプログラミングをさせるなら別です。テストを書いておけば、雑な変更指示でコードベースが破壊された時に検知させることができます。
通常のユニットテストに加え、いくつかのUsecaseを絡めたユーザーストーリーベースのテストにも対応しておくと良いでしょう。機能が膨大になってくると、特定の状態のユーザーがなんらかの操作をするとバグる、みたいな事例が出てきがちです。この辺りの検証も自動化しておくと、人間側が楽できます。
これらをカバーするテストを書いておけば、プロダクト全体を堅牢な状態に保つことができます。
変更を意識させる
いくらdomain.mdが完璧だったとしても、生きているソフトウェアは変更する時がきます。domain.mdの変更が単純なものであれば、該当の箇所をAIに教えてあげて実装を依頼するだけ完了できますが、ドメイン全体に関わる大きな変更に関しては少し工夫が必要です。
私がよく使うのはタスクリスト方式です。domain.mdの変更を読んで、それを実現するためのタスクリストをまず作成してもらい、その変更を別のエージェントが実装するというものです。
まず domain.md の変更を読んでもらいましょう。ここで、必ずGitコマンドを利用するように指定します。Gitコマンドを使うことで、どこがどのように変更されたかということを説明せずに理解させることができます。
Gitで直近3日間のdomain.mdの変更を確認し、これからコードに落とし込まなければいけないことをタスクリスト ./tasks.md に書き出してください。
完成したタスクリストが十分変更を説明していることを確認したら、単にそれを実行してもらうだけです。
./tasks.md に書かれているタスクを実行してください。
この方法を利用することで、指定した期間にわたるdomain.mdの変更を概ねコードベースに落とし込むことができます。もちろん、最終的にはフロントエンドなどと合わせて人間が確認する必要があります。
おわりに エンジニアリング = 世界と思想の言語化
いかがでしたでしょうか。
結構気合いの入った文章量になってしまいましたが、AIの進化とは凄まじく早いものでこの記事もわりとすぐに陳腐化していくのだろうなという気持ちがあります。この記事の中でも、テーブル定義やAPIの分け方は人間が行うことを勧めていますが、この辺りもあっという間にAIがいい感じに全体を設計してくれるようになるでしょう。
一方で、システムを通じて誰を幸せにしたいのかという思想の部分と、システムがどのような制限の下動作するのかという世界の部分を言語化する仕事は少なくとも数年程度は残り続けそうな気がします。AIの具体的な能力値が変わっても、この辺りを記述する能力はもうしばらくは必要そうです。
エンジニアとしてこの先うまく生き残っていけるような立ち回りをしていきたいですね。