エージェントはメモリより良いドキュメントが必要:人とエージェント両方が読むリポを書く

1. 概要

コーディングエージェントは徐々にチームの「メンバー」になりつつあります。コードを読み、バグを修正し、テストを書き、プルリクエストを開きます。問題は、エージェントはセッション間で何も覚えていないということです。メモリ機能を提供するツールもありますが、その記憶は通常、個人に結びついており、レビューが難しく、コードのバージョンに追従しません。

この記事は、リポ内のドキュメントはエージェントのメモリより信頼できるという理由を説明します。この記事はまた、人事部を含む Spring Boot リポをどのように書くかも説明します。新しいチーム メンバーとエージェント両方が最初から正しく動作します。この記事を読み終えると、AGENTS.md ガイド ファイルの書き方、検証を 1 つのコマンドにまとめる方法、慣例をテストに変える方法、設計上の決定を記録する方法がわかります。

2. メモリが不十分な理由

エージェントが domain レイヤーが Spring に依存してはいけないことを「知る」2 つの方法を比較してみましょう:

  • メモリ:前回あなたはそれに指摘し、それはどこかにメモを保存しました。
  • リポ:ルールは AGENTS.md にあり、誰かが違反するとテストが失敗します。

最初のアプローチには明らかないくつかの弱点があります:

  • メモは一人のマシンまたはアカウントにのみ存在します。別のエージェントを使用している同僚はそれを持っていません。
  • 誰もそのメモをレビューしません。エージェントが「間違って覚えている」場合、発見するのが非常に難しいです。
  • メモはブランチに追従しません。ルールが main で変わると、古いメモリが残っています。

2 番目のアプローチはバージョン管理され、プルリクエストでレビューされ、各コミットで正確です。さらに重要なことに、それは人間にも役立ちます。エージェントが正しく行動するために必要なものは、新しい開発者が最初の週に必要とするものとほぼ一致します。

これがこのトピックが注目されている理由です。エージェントは新しい問題を明らかにしません。貧弱なドキュメントの代価を明らかにします。なぜなら、エージェントは新しい人のように「これはどうやって動くの?」と聞くことができないからです。

3. エージェント向けガイド ファイル:AGENTS.md

多くのエージェント ツールはリポのルートにあるMarkdownファイルを読んで指示を得ます。ファイル名はツールによって異なりますが、AGENTS.md は一般的な慣例です。ツールが別の名前を使用している場合、コンテンツを複製する代わりにシンボリックリンクを作成できます。

3.1. 含むべき内容

# AGENTS.md

## プロジェクト
注文サービス(Spring Boot 3、Java 21)。メインモジュール:`order`、`payment`、`customer`。

## コマンド
- ビルド + 全検証: `./scripts/verify.sh`
- 単一テスト実行: `./mvnw -q test -Dtest=DiscountPolicyTest`
- ローカルでアプリ実行: `./mvnw spring-boot:run -Dspring-boot.run.profiles=local`

## 必須慣例
- `..domain..` は `org.springframework..` をインポートしない(テスト: `ArchitectureTest`)。
- 通貨は `BigDecimal` を使用、`double` は使用しない。
- ビジネスロジックの変更にはすべて `src/test/java/.../domain` にテストが必要。

## してはいけないこと
- マージされた `src/main/resources/db/migration` 内のファイルを編集しない;新しいmigrationを作成する。
- `application-local.yml` をコミットしない。

## さらに読む
- アーキテクチャ上の決定: `docs/adr/`
- 支払いフロー: `docs/payment-flow.md`

各セクションには独自の目的があります:

  • コマンド:エージェントはコマンドをたくさん実行します。1 つの間違ったコマンドが多くのトライアルラウンドを無駄にします。正確で、コピー・ペーストできるコマンドを書いてください。
  • 必須慣例:各行は検証方法(テスト名、lint ルール名)を示すべきです。検証メカニズムがない慣例は非常に無視しやすいです。
  • してはいけないこと:取り返しのつかない結果をもたらす行動をリストアップしてください。
  • さらに読む:ここで繰り返すのではなく、詳細なドキュメントをポイントしてください。

3.2. 短く保ち、単一の情報源を指す

AGENTS.md は短く、1 分で読み終えるのに十分であるべきです。エージェントは各セッションでこのファイルをコンテキストに読み込みます。ファイルが長いほど、より多くのコンテキストを使用し、時代遅れの情報を含みやすくなります。

一般的なルール:コードまたはビルドがすでに明確に述べている内容を繰り返さないでください。すべての依存関係をリストアップしないでください(pom.xml はすでにあります)。すべてのエンドポイントを説明しないでください(OpenAPI spec はすでにあります)。コードから推論できないものだけを記録してください。

注意: 秘密、トークン、または機密内部 URL を AGENTS.md に絶対に配置しないでください。このファイルは外部ツールによって読み取られ、git 履歴に永遠に残ります。

4. 単一の検証コマンド

エージェントは明確なフィードバックループを持つときに最もよく機能します:コードを編集し、コマンドを実行し、合格か失敗かを確認します。完全な検証に 4 つの異なるコマンドを順番に実行する必要がある場合、エージェントはスキップします。人間も同じです。

#!/usr/bin/env bash
# scripts/verify.sh — CI と全く同じを実行。0 以外の終了コードはマージ準備ができていないことを意味します。
set -euo pipefail

echo ">> Compile + unit test + architecture test"
./mvnw -B -q verify

echo ">> TODO-AGENT の遺漏がないか確認"
if grep -rn "TODO-AGENT" src/; then
  echo "TODO-AGENT がまだ処理されています" >&2
  exit 1
fi

echo "OK: プルリクエストを作成する準備ができています"

説明:

  • set -euo pipefail はコマンドがエラーになるとスクリプトを停止します。これにより、エージェントは終了コード 0 以外を受け取り、長い出力と最後の欺瞞的な「OK」ではなく受け取ります。
  • -B(バッチモード)は色付き出力と進捗バーをオフにします。ログがよりクリーンなため、エージェントはエラーを読みやすくなります。
  • grep ステップはチームの固有の慣例の例です:エージェントは未処理の場所を TODO-AGENT でマークできますが、PR を開くときに落とすことはできません。

成功すると、次のように表示されます:

>> Compile + unit test + architecture test
>> TODO-AGENT の遺漏がないか確認
OK: プルリクエストを作成する準備ができています

最も重要なこと:CI はこのスクリプトを正しく呼び出す必要があります。CI が異なるコマンド セットを実行する場合、verify.sh は徐々に現実から乖離し、不正確なドキュメントになります。

5. ArchUnit でルールをテストに変える

ドキュメント内の「domain は Spring に依存しない」というような文は単なるアドバイスです。それをテストに変えると、それは法則になります。ArchUnit を使用すると、アーキテクチャ ルールを JUnit テストとして記述できます。

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit5</artifactId>
    <version>1.3.0</version>
    <scope>test</scope>
</dependency>
package com.nonla.shop;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

@AnalyzeClasses(packages = "com.nonla.shop", importOptions = ImportOption.DoNotIncludeTests.class)
class ArchitectureTest {

    @ArchTest
    static final ArchRule domainKhongPhuThuocSpring = noClasses()
            .that().resideInAPackage("..domain..")
            .should().dependOnClassesThat().resideInAPackage("org.springframework..")
            .because("domain は Spring コンテキストなしでテストできる必要があります。docs/adr/0003-domain-thuan.md を参照してください");

    @ArchTest
    static final ArchRule webKhongGoiThangRepository = noClasses()
            .that().resideInAPackage("..web..")
            .should().dependOnClassesThat().haveSimpleNameEndingWith("Repository")
            .because("controller はトランザクションを 1 か所に保つためにサービスを経由します");
}

最も価値のあるポイントは .because(...) にあります。ルールが違反されると、エラー メッセージに理由と ADR へのパスが含まれます。誰かが domain パッケージの DiscountPolicy に @Component を追加したとします。出力は次のようになります:

Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package
'..domain..' should depend on classes that reside in a package 'org.springframework..',
because domain は Spring コンテキストなしでテストできる必要があります。docs/adr/0003-domain-thuan.md を参照してください'
was violated (1 times):
Class <com.nonla.shop.order.domain.DiscountPolicy> is annotated with <org.springframework.stereotype.Component> ...

このメッセージを読むエージェントは、すぐに何をすべきかを知ります:アノテーションを削除し、次に infra レイヤーの @Configuration クラスで bean を登録します。良いエラー メッセージは、読み手が必要なときに正確に現れるドキュメントです。

6. テストはビジネス仕様です

ビジネス ルールはエージェントが最も間違える傾向があります。それは「最初の注文の割引は最大 50,000 VND」と推測することはできません。コードとテストに話させてください。

package com.nonla.shop.order.domain;

import java.math.BigDecimal;
import java.math.RoundingMode;

public record Order(String customerId, BigDecimal subtotal, boolean firstOrder) {}

class DiscountPolicy {

    static final BigDecimal FIRST_ORDER_RATE = new BigDecimal("0.10");
    static final BigDecimal FIRST_ORDER_CAP = new BigDecimal("50000");

    BigDecimal discountFor(Order order) {
        if (!order.firstOrder()) {
            return BigDecimal.ZERO;
        }
        BigDecimal raw = order.subtotal().multiply(FIRST_ORDER_RATE)
                .setScale(0, RoundingMode.DOWN);
        return raw.min(FIRST_ORDER_CAP);
    }
}

テストは動作に基づいて名前を付ける必要があり、メソッド名に基づいてではありません:

package com.nonla.shop.order.domain;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;

import static org.assertj.core.api.Assertions.assertThat;

@DisplayName("注文割引")
class DiscountPolicyTest {

    private final DiscountPolicy policy = new DiscountPolicy();

    @Test
    @DisplayName("最初の注文は総額の 10% 割引を受けます")
    void firstOrderGetsTenPercent() {
        var order = new Order("c-1", new BigDecimal("200000"), true);

        assertThat(policy.discountFor(order)).isEqualByComparingTo("20000");
    }

    @Test
    @DisplayName("最初の注文の割引は 50,000 VND を超えません")
    void firstOrderDiscountIsCapped() {
        var order = new Order("c-1", new BigDecimal("2000000"), true);

        assertThat(policy.discountFor(order)).isEqualByComparingTo("50000");
    }

    @Test
    @DisplayName("2 番目の注文以降は割引がありません")
    void repeatOrderGetsNoDiscount() {
        var order = new Order("c-1", new BigDecimal("2000000"), false);

        assertThat(policy.discountFor(order)).isZero();
    }
}

説明:

  • @DisplayName はテスト レポートを読み取り可能な仕様に変えます。製品マネージャーも理解できます。
  • isEqualByComparingTo は BigDecimal を値で比較するため、20000 と 20000.00 は等しいと見なされます。ここで isEqualTo を使用するのは一般的な誤りです。スケールも比較するためです。
  • エージェントが「割引を 15% に引き上げる」ように要求された場合、変更するテストはすぐにわかります。誤ってキャップを削除した場合、2 番目のテストは失敗します。

実行時、レポートは次のようになります:

注文割引
  ✔ 最初の注文は総額の 10% 割引を受けます
  ✔ 最初の注文の割引は 50,000 VND を超えません
  ✔ 2 番目の注文以降は割引がありません

7. 「なぜ」を記録する:ADR と package-info

コードは、システムが何をするかを示しています。テストは、それが何をすべきかを示しています。しかし、なぜその方法を選んだのかを説明するものはありません。「なぜ」の部分がないと、エージェントは不要に見えるが実は理由があるものを「整理」しがちです。

Architecture Decision Record (ADR) は各決定に対する短いファイルです:

# 0003. domain レイヤーは Spring に依存しない

ステータス:受け入れられました

## 背景
domain テストは以前 Spring コンテキストを起動する必要があり、遅く、脆弱でした。

## 決定
パッケージ `..domain..` は JDK のみを使用します。Bean は `..infra.config` で宣言されています。

## 結果
- domain テストは純粋な JUnit で実行されます。
- 追加の設定クラスを記述する必要があります。`ArchitectureTest` によってチェックされます。

パッケージに接続された注記の場合、package-info.java は合理的な場所です。エージェントがフォルダを開いたらすぐに表示されるからです:

/**
 * 注文のビジネスロジック。JDK のみを使用、Spring は使用しません(ADR 0003)。
 * すべての金額は BigDecimal、単位は VND、1 ドンまで切り捨てます。
 */
package com.nonla.shop.order.domain;

8. 一般的なミスといつしてはいけないか

ドキュメントの重複。 同じルールが README.md、AGENTS.md、wiki に記録されています。数か月後、3 つの場所が 3 つの異なることを言っています。単一の情報源を選択し、他の場所からそれをポイントしてください。

指示が CI と矛盾しています。 AGENTS.md は「mvn test を実行する」と言っていますが、CI はさらに統合テストを実行しています。エージェントは PR がまだ赤い間に「完了」と報告します。

曖昧な命令口調でエージェント向けに書く。 「きれいなコードを書いてください」というフレーズは役に立ちません。「..service.. のパブリック メソッドにはテストが必要」というフレーズは役立ち、さらに良いことに検証可能です。

すべてを 1 つのファイルに詰め込む。 何千行もの長い AGENTS.md はコンテキストを薄くします。重要な情報は埋もれます。

注意: すべてのリポがこの完全なセットを必要とするわけではありません。1 回限りのスクリプト、廃棄予定のプロトタイプ、または数百行のコードのみを持つリポの場合、コマンド実行の説明がある README.md で十分です。ArchUnit も、境界を実際に保つ必要がある複数のレイヤーまたはモジュールがある場合に限る価値があります。まだ存在しないものについてドキュメントを作成するだけでは、メンテナンスが必要なだけです。

一方、エージェントのメモリにはまだ用途があります。説明の文体や使用しているエディタなどの個人的な好みに適しています。これらはリポには属しません。プロジェクトのルールはリポに常にあるべきです。

9. 結論

エージェントはより良いメモリを必要としません。より理解しやすいリポが必要です。やり方を見てきました:

  • 短い AGENTS.md を書いて、正確なコマンドと検証可能な慣例を含めてください。
  • すべての検証ステップを scripts/verify.sh にまとめ、CI に同じスクリプトを使用させてください。
  • アーキテクチャ ルールを ArchUnit テストに変え、エラー メッセージが理由をポイントするようにしてください。
  • @DisplayName を含むテストをビジネス仕様として使用してください。
  • ADR と package-info.java で「なぜ」を記録してください。

上記のすべては、最初に人間に役立ちます。エージェントは、これ以上延期できない理由にすぎません。小さく始めましょう:今週中に verify.sh と AGENTS.md の最初の 5 行を書いてください。

ai-agentdocumentationarchunittestingspring-boot