4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スモールケースで学ぶループエンジニアリング入門

4
Posted at

この記事の位置づけ

この記事は、@Syoitu 氏の Qiita 記事 入門から実践 -「🔁 ループエンジニアリング」 を土台にした、手を動かすための実践編です。

元記事は「ループエンジニアリングとは何か」を概念から広く解説し、その中の「Claude Code で最初のループを組む」節は、すでにある React / Next.js / TypeScript プロジェクトを前提にしていました。本記事はそこを切り出し、プロジェクトを持っていない人でも試せるよう、FizzBuzz の練習環境をゼロから組む形に作り直したものです。用語や考え方は元記事に準拠しています。

ループエンジニアリング(loop engineering): AI エージェント運用の設計手法を指す言葉。「エージェントにプロンプトを打つ人」を自分から外し、代わりに打ち続ける仕組み(ループ)を設計する、という考え方。2026 年 6 月ごろに定着した比較的新しい用語です。

この記事のゴール

Claude Code に「テストが通るまで直して」と一度頼むだけで、AI が自分でコードを書き、テストで失敗を見つけ、直し、また試す——この往復を人が介入せずに回す。それを小さく確実な例で腹落ちさせるのがゴールです。

難しい実プロジェクトは使いません。狙いは「ループが何によって駆動されているか」を、FizzBuzz という正解が一意に決まるお題で体験することです。

そもそもループエンジニアリングとは

ふつうの AI との対話は「指示 → 返答 → また指示」の直線です。人が毎回プロンプトを打ち、結果を確認し、次を指示します。

ループエンジニアリングは、この「確認して次を指示する」部分を仕組み化して、人を往復の外に出します。元記事が引用している Addy Osmani(Google Chrome チーム)の定義を借りれば、要は「自分自身を “エージェントに指示を打つ人” から外し、代わりに打ってくれる仕組みを設計する」ことです。人は最初に一度だけゴールを与え、あとは AI が「作る → 検査する → ダメなら直す」を自動で繰り返す。人の役割は、最後に結果(差分)を見るだけになります。

元記事は、ループを回す動作を 5 つに分解しています。

動作 やること
発見(discovery) この 1 周で何をやるべきかを自分で見つける
受け渡し(handoff) タスクを隔離して、作業役に渡す
検証(verification) 別の関門が「これでいいか」を判定する
記憶(persistence) 状態を会話の外(ファイル等)に書き出す
スケジューリング(scheduling) 放っておいても回り続けさせる

この 5 つのうち心臓部は 検証(verification) です。AI が「できました」と言っても鵜呑みにせず、別の関門がテストで合否を判定し、緑になるまで終わらせない。この「ダメと言える評価役」があるかどうかが、ただの一発生成とループの分かれ目になります。本記事の FizzBuzz 環境は、この検証を厚く作り、残りの動作は最小限にした構成です。

なお元記事では、評価役を「作業役とは別のモデル」にするのが基本とされています。ただし FizzBuzz のように合否をコードで決定的に(True/False で)判定できるお題なら、判定役のモデルは不要です。テストコマンドそのものが、買収できない評価役になります。今回はこの決定的な検証を使います。

想定環境

  • macOS / Linux / WSL(Windows の場合は WSL 上で行う)
  • Node.js 18 以上(推奨 20 / 22)
  • Claude Code CLI(フックとサブエージェントに対応したバージョン)

フック(hook): Claude Code の用語。エージェントの動作の特定タイミング(編集後・停止時など)で、指定したコマンドを自動実行させる仕組みのこと。

Windows ネイティブではなく WSL を使うのは、後述のフック用スクリプトが Unix 系シェル(bash)とそのコマンドを前提にしているためです。

全体像:何を用意し、何がループを回すのか

この練習環境は、次のファイルで構成されます。

ファイル 役割 対応する動作
CLAUDE.md ループの回し方と完了条件を定義 (回し方の指示)
.claude/settings.json フックと権限モードの設定 (フックの配線)
.claude/hooks/check.sh テストで合否を判定する評価役 検証
.claude/agents/fixer.md 行き詰まり打破の別動エージェント 受け渡し / 検証
src/fizzbuzz.ts 実装対象のお題コード (お題)
src/fizzbuzz.test.ts 合否を決める仕様兼テスト (仕様)
package.json test / typecheck コマンドの定義 (実行設定)
tsconfig.json 型チェックの設定 (実行設定)

ループを実際に駆動している中心は 3 つです。CLAUDE.md(どう回すかの指示)、check.sh(緑になるまで止めない評価役)、fixer.md(詰まったときの打破役)。残りは、その上でループが働くための土台(お題・仕様・実行設定)です。元記事が既存プロジェクトで用意していた 3 ファイル(CLAUDE.md / settings.json / fixer.md)に、お題一式を足したものだと思ってください。

この構成は「テストが仕様も兼ねる」点が最小化のポイントです。FizzBuzz は正解が一意に決まるので、自然言語の仕様書を別に用意せず、テストファイルが「何を作るか」をそのまま表せます。仕様が曖昧だったり、見た目のように○×で測れない要素が入ると、この畳み方は使えず、別途の仕様書が必要になります。

セットアップ手順

以下を上から順に実行します。

1. プロジェクトの箱を作る

mkdir loop-practice && cd loop-practice
mkdir -p src .claude/agents .claude/hooks

2. 依存を入れる

npm init -y
npm i -D typescript vitest

3. package.json に検査コマンドを配線

package.jsonscripts を次に書き換え、あわせて "type": "module" を足します。

"scripts": {
  "test": "vitest run",
  "typecheck": "tsc --noEmit"
}

4. tsconfig.json を作る

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true,
    "types": ["vitest/globals"]
  },
  "include": ["src"]
}

5. お題とテストを作る

お題(src/fizzbuzz.ts)は、わざと未実装にしておきます。これがループの出発点(=テストが赤い状態)になります。

// 練習用のお題: この関数を tests が通るように実装する。
export function fizzbuzz(n: number): string {
  throw new Error("not implemented");
}

テスト(src/fizzbuzz.test.ts)が「何を作るか」を定義します。

import { describe, it, expect } from "vitest";
import { fizzbuzz } from "./fizzbuzz";

describe("fizzbuzz", () => {
  it("3で割れる数は Fizz", () => { expect(fizzbuzz(3)).toBe("Fizz"); });
  it("5で割れる数は Buzz", () => { expect(fizzbuzz(5)).toBe("Buzz"); });
  it("15で割れる数は FizzBuzz", () => { expect(fizzbuzz(15)).toBe("FizzBuzz"); });
  it("それ以外はその数の文字列", () => { expect(fizzbuzz(7)).toBe("7"); });
});

6. .gitignore を作る

node_modules などを Git に含めないようにします。

node_modules/
.claude/.loop-count

7. CLAUDE.md(ループ協議)

「完了とは何か」を再定義します。ここが「直線ではなくループで進めろ」という指示の本体です。元記事の協議から linter を外し(今回は lint 設定を持たないため)、テストと型チェックの 2 つに絞っています。

## ループ協議

各タスクは「直線」ではなく「ループ」として走らせる:

1. 変更を書く
2. チェックを走らせる: テスト + 型チェック
3. 失敗した? エラーを読み、原因を特定し、直して、2 に戻る
4. ループは最大 5 回まで

停止条件:
- 全チェック通過 → 「完了」と報告。通過した出力を証拠として添える
- 5 回使い切った → 止まって、何が残っているか報告する
- 同じエラーが 2 回連続 → ループを止め、@fixer を呼ぶよう促す

禁止: チェック出力なしで「完了」と報告すること
禁止: アサーション削除やテスト弱体化で通すこと。直すのはコードで、スコアボードではない

最後の 2 つの「禁止」が、実はこの設定でいちばん効きます。停止条件(評価基準)を曖昧にすると、ループは平気でズルをします。元記事の著者も、これを入れずに回したら Claude がアサーションを 1 行こっそり消してテストを通した、と報告しています。テストは緑、でもバグは残ったまま——という事故を防ぐための一文です。

8. .claude/settings.json(フックと権限)

{
  "permissions": {
    "defaultMode": "acceptEdits"
  },
  "hooks": {
    "Stop": [
      { "hooks": [ { "type": "command", "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/check.sh\"" } ] }
    ],
    "PostToolUse": [
      { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "npx tsc --noEmit --pretty false 2>&1 | head -20" } ] }
    ]
  }
}
  • Stop フックは、Claude が「完了」と言おうとするたびに check.sh を実行します。これが元記事でいう 「ダメと言える評価役」 の本体で、ここが通らない限りループは終われません。
  • PostToolUse フックは、ファイルを編集するたびに型チェックを自動で走らせます。
  • defaultMode: "acceptEdits" は、ファイル編集の確認プロンプトを自動承認するモードです。

9. .claude/hooks/check.sh(評価役の本体)

#!/usr/bin/env bash
set -o pipefail
MAX_LOOPS=5
DIR="${CLAUDE_PROJECT_DIR:-.}"
STATE_FILE="$DIR/.claude/.loop-count"
cd "$DIR" || exit 0

OUT="$(npm test --silent 2>&1)"
CODE=$?

if [ "$CODE" -eq 0 ]; then rm -f "$STATE_FILE"; exit 0; fi

COUNT=0; [ -f "$STATE_FILE" ] && COUNT="$(cat "$STATE_FILE" 2>/dev/null || echo 0)"
COUNT=$((COUNT + 1)); echo "$COUNT" > "$STATE_FILE"

if [ "$COUNT" -gt "$MAX_LOOPS" ]; then
  rm -f "$STATE_FILE"
  echo "テストが赤のまま上限(${MAX_LOOPS}回)に到達。停止します。" 1>&2
  exit 0
fi

{ echo "テスト失敗(${COUNT}/${MAX_LOOPS}回目)。緑にするまで完了にしないこと。"; printf '%s\n' "$OUT" | tail -20; } 1>&2
exit 2

作成後、実行権限を付けます。

chmod +x .claude/hooks/check.sh

このスクリプトが判定する分岐は 3 通りです。

  • テストが緑 → exit 0(停止を許可)
  • テストが赤 かつ 上限内 → exit 2(もう 1 周させる)
  • テストが赤 だが 上限超過 → exit 0(暴走させず打ち切り)

exit 2 が「まだ終わるな、もう一度やれ」の合図で、これがループを次の周へ押し戻す中核です。周回数は .loop-count に記録され、最大 5 回で打ち切ることで、直らない課題を延々と回してコストを浪費する「暴走」を防ぎます。これは 5 動作でいう 記憶(周回数の永続化) と、暴走への上限設定にあたります。

10. .claude/agents/fixer.md(行き詰まり打破役)

サブエージェント(sub-agent): Claude Code の用語。メインの会話とは別コンテキストで動く補助エージェント。失敗ログで埋まった思考をリセットし、白紙から診断させる用途に使う。

同じテストが直しても失敗し続けるとき、まっさらなコンテキストで診断させる別動エージェントです。CLAUDE.md の「同じエラーが 2 回連続 → @fixer を呼ぶ」から呼ばれます。

---
name: fixer
description: 同じテストが2回の修正後も失敗したときの、行き詰まり打破用エージェント
tools: Read, Edit, Grep, Glob, Bash
model: opus
---

あなたは失敗したチェックを直す。推測は禁止。
1. テストと型チェックを自分で実行しエラー全文を読む
2. 失敗パス上のファイルを頭から終わりまで読む
3. 一文で書く: 本当の原因は何か
4. その原因だけ直す。ついでのリファクタはしない
5. チェックを再実行し、修正前後の出力を報告する

禁止: テスト削除、アサーション緩和、エラー握りつぶし、skip化

直前の失敗の記憶を持たないままゼロから診断するので、一発で通ることがよくあります。これは元記事の 6 パーツでいう Sub-agents(生成役と評価役の分離) にあたります。FizzBuzz では 1 周で緑になることが多く、fixer はめったに発火しませんが、詰まったときの保険として置いておきます。

ループを回す

準備ができたら、出発点になっているかを確認します。

npm test        # 4 failed になれば OK(=ループの出発点)
npx tsc --noEmit # エラーなしになれば OK

テストが赤・型チェックが緑なら準備完了です。Claude Code を起動し、こう頼みます。

src/fizzbuzz.ts を実装して。テストが全部通るまでループして。

すると、次のことが自動で起こります。

  1. Claude が fizzbuzz.ts を編集する
  2. 編集のたびに PostToolUse フックが型チェックを走らせる
  3. Claude が「完了」と言おうとすると、Stop フック(check.sh)がテストを実行する
  4. 赤なら exit 2 で編集に差し戻され、緑なら停止する
  5. 緑になったら、あなたは差分(/diff)を見るだけ

FizzBuzz のような単純なお題なら、多くの場合 1 周で緑に到達します。ここで消えるのは「ターミナルのエラーをコピペして貼り直す作業」です。Claude が自分で走らせ、自分で見て、自分で直す。あなたは往復の外に立ちます。

つまずきやすいポイント

許可確認が出て止まる

初期状態では、ファイル編集やコマンド実行のたびに確認プロンプトが出ます。この練習では defaultMode: "acceptEdits" で編集は自動承認していますが、Claude が自分で npm test などを実行しようとすると、別途確認が出ることがあります。

学習として体感するだけなら、都度承認して進めても問題ありません。確認をなくして完全に無人で回す設計は、セキュリティ(隔離環境や権限の絞り込み)を含む別テーマなので、慣れてから取り組むのがおすすめです。

check.sh の落とし穴(重要)

このスクリプトで最も大事なのは、テストの終了コードを、出力を切り詰める前に確保している点です。

OUT="$(npm test --silent 2>&1)" で出力を変数に取り、直後の CODE=$? で npm test 自身の終了コードを拾っています。これは単一コマンドの結果なので、合否は正しく取れます。

もしこれを避けて npm test | tail -20 のようにパイプの末尾で切り詰めると、set -o pipefail を付けない限り、パイプライン全体の成否が末尾の tail の成否(ほぼ常に成功)になり、テストが失敗しても「成功」と誤判定されます。すると評価役が機能せず、緑でもないのにループが止まります。合否の取得と出力の整形は分けて書く、と覚えておくと安全です。

まとめ

このスモールケースは、ループの構成要素のうち検証(check.sh)を厚く作り、発見と記憶を最小限に持たせ、受け渡しとスケジューリングは簡略化した構成です。

要点は次の 3 つです。

  • 人は最初に一度だけ指示し、往復の外に立つ
  • 評価役(Stop フック)が緑になるまで完了させない
  • 周回に上限を設けて暴走を防ぐ

まずはこの最小構成で「AI が自分で赤を見つけて直す」感覚を掴み、そこから実プロジェクトや無人運用へ広げていくのが、無理のない入り口になります。概念の全体像や、Mastra を使った自作フレームワーク側の話、回しっぱなしの代価については、元記事 入門から実践 -「🔁 ループエンジニアリング」(@Syoitu) を参照してください。

4
5
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
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?