1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIエージェントの出力を信用しない設計 — 受託開発の受け入れテストと検収

1
Posted at

「動いた」では終わらない

受託開発には検収がある。動くだけでは足りず、意図通りに動くことを合意された基準で見せなければならない。ここが自社プロダクトとの決定的な違いだ。

この連載の1回目で仕様の渡し方を、2回目で並列実行と衝突回避を書いた。両方うまくいくと、今度は別の問題が出る。エージェントが生成したコードが増えすぎて、人間が全部読み切れなくなる。

ある案件で、1週間で 40 本の PR が積み上がった時期があった。1本1本レビューしていたら、その週の他の仕事が止まる。それでも検収期日は動かない。「読まずにマージ」を一度でも許すと、後戻りできない負債になる。この記事はその手前で止めるための、うちの現在の運用メモだ。

品質ゲートを通過するプロダクトのイメージ

前提 — 何を「信用しない」と決めるか

まず言葉の整理から。ここでの「信用しない」は「使わない」ではない。「レビューだけを最後の砦にしない」という意味だ。

エージェントの出力に対しては、次の順に確認する。

  • 機械が判定できる部分は、機械に判定させる(型·lint·テスト)

  • 機械が判定しにくいがパターン化できる部分は、CIとレビューガイドで縛る

  • それ以外の設計判断·外向きの変更だけを、人間が集中して読む

ここの線引きが曖昧なまま「一応レビューはしています」という運用が、一番危ない。全部見ているつもりで、実は何も見ていない状態になる。

ステップ1 — 型·lint·テストを「先に」敷く

順序が重要だ。エージェントに書かせてからガードレールを整えるのでは遅い。案件初日にリポジトリを受け取ったら、実装に入る前にまず次を確定させる。

## リポジトリ受領直後にやること
# 1. TypeScript strict の有効化(既存プロジェクトで無効なら段階的に)
# 2. lint ルールの明文化 — ESLint / Biome など
# 3. pre-commit で型 + lint + フォーマット
# 4. CI で同じチェックを再実行(agent 側の checks をスキップされても落ちるように)

ポイントは 3 と 4 を両方やること。エージェントは pre-commit を回避する裏道を見つけることがある(実話で、コミットフックが失敗しても --no-verify で通そうとしたケースがあった)。CI 側で同じ検査を独立に走らせる二重化が要る。

この段階で入れておくと後々効くのが、実行時契約の型化だ。TypeScript なら Zod、Python なら Pydantic で、外部境界のスキーマを型として書き切る。

// 例:外部APIのレスポンスを Zod で受ける
import { z } from "zod";
const InvoiceSchema = z.object({
id: z.string(),
amount: z.number().int().nonnegative(),
issued_at: z.string().datetime(),
status: z.enum(["draft", "sent", "paid", "void"]),
});
export type Invoice = z.infer<typeof InvoiceSchema>;
export async function fetchInvoice(id: string): Promise<Invoice> {
const raw = await http.get(/invoices/${id});
return InvoiceSchema.parse(raw); // ← ここで落とす
}

エージェントは境界でも中でもだいたい似た書き方をしてくる。境界で必ずスキーマ検証を通す構造にしておけば、真ん中で「たぶん来るはず」の値に依存した危ういコードは自然と落ちる。

ステップ2 — 受け入れテストを、実装より先に書く

1回目で「受け入れ条件を実装前に書く」話をしたが、その延長線上にある。受け入れ条件は文章では足りず、実行可能な形にしておく。

うちが最近固まってきた運用は、案件ごとに tests/acceptance/ というディレクトリを掘って、そこに「発注側との合意事項」だけを置くというもの。ユニットテストとは別建てにする。理由は運用が変わるからだ。

tests/
├── unit/          # 実装詳細。エージェントが自由に増やしていい
├── integration/   # 内部結合。エージェントが増やす、人がざっと見る
└── acceptance/    # 発注合意。人が書く。エージェントは触らない

acceptance だけは、CLAUDE.md やエージェント用ドキュメントで「このディレクトリのファイルを勝手に変更·削除·スキップしないこと」と明示的に禁止しておく。テストを合わせに来られると意味がないからだ。

// tests/acceptance/invoice.spec.ts
// 発注書 §3「支払期日を超えた invoice は status=overdue に自動遷移」
import { describe, it, expect } from "vitest";
import { runDailyBatch } from "@/batches/invoice-status";
import { seedInvoice, findInvoice } from "./helpers";
describe("受入 §3: 支払期日超過の自動遷移", () => {
it("期日を1日でも超えた sent invoice は overdue になる", async () => {
const id = await seedInvoice({
status: "sent",
due_date: "2026-08-28",
});
await runDailyBatch({ today: "2026-08-29" });
expect((await findInvoice(id)).status).toBe("overdue");
});
});

コメントに発注書の項番を残しているのは意図的だ。検収時に「§3 のテストが通っています」と示せる。CI のグリーンが、そのまま合意との対応表になる。

ステップ3 — 差分レビューを、優先度で潰す

全部読む前提を捨てる。1本の PR に対して、次のような優先度で人間が目を通す。

優先度が高いのは、外向きの変更だ。ユーザー入力を受ける関数、外部 API を呼ぶ箇所、DB スキーマの変更、認証·認可、料金や個人情報に触れる処理。ここはエージェントが一番間違えるところでもあり、間違えたときに一番戻せない部分でもある。

優先度が低いのは、テストの追加·内部のリファクタリング·ドキュメント更新。ここは差分の総量を見るだけで、中身は基本的に読まない。読む時間があったら、上の高優先度に回す。

この分岐を自動化するのに、簡単なラベル付けを CI に仕込んでいる。

# .github/workflows/pr-triage.yml (抜粋)

name: Auto-label by change surface
run: |
if git diff --name-only origin/main | grep -qE "^(app/api|db/migrations|lib/auth)"; then
gh pr edit ${{ github.event.pull_request.number }} --add-label "review:human-required"
else
gh pr edit ${{ github.event.pull_request.number }} --add-label "review:machine-ok"
fi

雑な仕組みだが、これで PR の一覧を見たときに、どれを開くべきか一瞬で分かる。「全部レビューする」から「読むべき PR を選ぶ」に発想を変えるだけで、1週間の消耗がだいぶ違う。

ステップ4 — 回帰を検知する仕組み

エージェントに任せていて怖いのは、一度直した挙動が別の PR で再度壊されるパターンだ。人間の開発者なら「これ前も直したよね?」と気付くが、エージェントは平気で同じ穴を再オープンする。

対策として、うちでは回帰テスト専用のディレクトリを切っている。バグを直すたびに、その再現ケースをここに一本ずつ追加する。

tests/regression/
├── 2026-08-11-webhook-double-fire.spec.ts
├── 2026-08-17-oauth-refresh-race.spec.ts
└── 2026-08-22-invoice-status-off-by-one.spec.ts

ファイル名に日付とキーワードを入れているのは、後から git blame なしで由来を追えるようにするため。エージェントに新規テストを書かせる時も「まず regression を全部通してから、新しいケースを足せ」と指示の順番を固定している。

厳密にはユニットテストで拾えるものも多いが、「これは回帰である」というマーキング自体に意味がある。次に触るエージェントに、この一行が過去の痛みを伝える。

ステップ5 — レビュー負債を可視化する

正直に書くと、それでもレビューしきれない PR は出る。締め切りが近い週や、大量の依存アップデートが降ってきたときなど。ここで「全部読んだつもり」にしないための小さな工夫が要る。

やっているのは、PR ごとに「レビューの深さ」を明示的にラベルで残すこと。

  • reviewed:deep — 差分を全部追った

  • reviewed:surface — 変更範囲と受け入れテストの通過だけ確認

  • reviewed:trusted-tests — テストが緑という事実だけで通した

後から surface と trusted-tests のラベルが付いた PR を検索すれば、「読まずに通した箇所」の一覧が出る。バグが起きたときにここから当たれば、原因の当たりが早い。何よりも、自分が何を見ていないかを自分に対して隠さないための装置だ。

動作確認 — 1本の PR がマージされるまで

ここまでの仕組みを通した時の、1本の PR の流れを書いておく。

  1. エージェントがブランチで実装、pre-commit で型·lint·フォーマットが走る

  2. PR が上がると CI が同じ検査を再実行、加えて acceptance / regression を全通し

  3. 変更ファイルを見て review:human-required か review:machine-ok のラベルが自動で付く

  4. 人間は前者だけを開く。後者はマージキューに入れる

  5. マージ時に reviewed:* のラベルを1つ付けて履歴を残す

ここまでを毎回やる。しんどい週もあるが、この手順を崩さないことが結局は一番速い。崩したときに支払う後日のコストの方が、ずっと大きい。

応用 — この設計をどう広げるか

  • 非受託の自社プロダクトにも同じ acceptance/regression の分け方は使える。「発注書との対応」は「PdM の合意事項」に読み替える

  • 複数リポジトリ横断で運用するなら、CLAUDE.md の共通部分を組織テンプレ化してリポジトリ間で同期する

  • LLM 自体の出力を評価する必要が出てきたら、テスト戦略を「決定的な入出力」から「分布としての品質」に切り替える必要が出てくる。これは連載の範囲を超えるので別途書きたい

まとめ

3回にわたって、エージェントを受託開発に組み込む時の実装ノートを書いた。仕様の渡し方(1回目)、並列実行と分担(2回目)、そして今回の受け入れと検収。3つとも共通しているのは、エージェントを速くするより、エージェントの出力を安全に受け止める側の作りを整える方が効くという実感だ。

次に自分で書きたいのは、この記事で「別途」と書いた、LLM 自体の出力品質を継続的に評価する仕組みの話。決定的なテストで捕まえられない部分をどう見張るか、CI に評価ゲートをどう組み込むか、そのあたりを掘る予定でいる。

筆者は 5years+ で AI エージェントを使った受託開発のパイプライン整備を担当している。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?