「Claude Codeに実装を任せたが、一度では仕上がらないので結局何往復もしている」「レビューを頑張っても、指摘→修正→再レビューを人間が仲介している」「自動で回したいが、暴走してレート枠を食いつくされるのが怖い」——AI駆動開発を実務で回し始めると、必ずここに行き着きます。
答えはシンプルで、人間がやっている「往復の仲介」をコードに移すことです。ただし、先に安全装置を仕込むのが大前提です。本記事では、弊社で実際に運用しているループ機構の設計をもとに、最小実装と 5つの安全装置を解説します。
・自律ループの最小形は worker(実装)⇄ reviewer(差分レビュー) を指摘が尽きるまで回すだけ
・実装ではなく停止条件が難しい。「指摘がなければ __REVIEW_OK__ だけ出力」とセンチネルを強制する
・--session-id で worker と reviewer の文脈を別々に保つ。混ぜると自分の実装を甘く見る
・最大ラウンド数とレート上限検知の2段構えで空回りを止める
・観測役と実行役を分離する。ループが死んでも監視側は生き残る設計にする
・必ずgit管理下で回し、全ターンの進行ログを残す。あとで「なぜこうなったか」を追える
チームの開発フローに合わせた設計をご提案します。
お問合せはこちら
[目次を開く]
1. 自律ループとは:1往復で終わらせない
通常のAI活用は「指示→回答」の1往復です。しかし実装タスクでは、1回で完成することの方が稀です。現実にはこういう往復を人間が手で回しています。
- 実装させる
- 差分を見て指摘する
- 指摘を伝えて直させる
- また見る……
この2→3の仲介をコードに移すのが自律ループです。考え方の背景はLoop Engineeringとは?で解説しています。本記事は実装・運用編です。
2. 最小構成:worker ⇄ reviewer
一番小さく始められるのは、実装役とレビュー役を分けて回す形です。
flowchart LR
A["worker<br>実装する"] --> B["git diff を取る"]
B --> C["reviewer<br>差分をレビュー"]
C -->|"指摘あり"| D["指摘を worker に戻す"]
D --> A
C -->|"指摘なし"| E["終了"] Claude Codeはヘッドレスで呼べるので、このループは普通のスクリプトで組めます。
cmd = ["claude", "-p", prompt, "--output-format", "json"]
if session_id:
cmd += ["--session-id", session_id] # 初回:UUIDでセッションを固定
if resume:
cmd += ["--resume", resume] # 2回目以降:文脈を継続
if accept_edits:
cmd += ["--permission-mode", "acceptEdits"] --session-id(有効なUUIDが必要)を割り当て、それぞれ --resume で回すのが基本形です。 ここからが本題です。ループ自体は10行で書けますが、安全に回すには仕掛けが要ります。
3. 安全装置①:停止条件をセンチネルで持たせる
自律ループで最も難しいのは実装ではなく、「いつ終わるのか」の判定です。
自然文で「問題ありません」と返させるのは危険です。「大きな問題はありませんが、あえて挙げるなら……」のように書かれると、終了なのが継続なのが判別できません。
解決策は、終了時に出力する文字列を固定することです。
SENTINEL = "__REVIEW_OK__"
reviewer_prompt = f"""以下の差分をレビューしてください。
指摘がある場合は番号付きで具体的に指摘してください。
指摘が一つもない場合は、**{SENTINEL} だけ**を出力し、
前置き・講評・感想は一切書かないでください。
{diff}"""
text, meta = run_claude(reviewer_prompt, resume=reviewer_id, cwd=repo)
if SENTINEL in text:
break # ループ終了 __REVIEW_OK__」のように装飾が付いて、判定は通るもののログが読みにくくなります。逆に指摘の本文中にセンチネルを例示されて誤終了するケースもあるので、センチネルは通常の文章に百%現れない文字列にしましょう。 4. 安全装置②:最大ラウンド数で殖める
センチネルがあっても、reviewer が指摘を出し続ければループは止まりません。細かい好みの問題を指摘し続けることは実際に起きます。
だから回数で物理的に止めます。
for rnd in range(1, args.max + 1): # 既定 6 回
# worker → reviewer …
if SENTINEL in review_text:
break
else:
log(f"最大{args.max}ラウンドに達したため中断します") 経験上、既定6回が実用的です。これを超えても収束しないタスクは、そもそもタスクの切り方が大きすぎることがほとんどでした。回数を増やすよりタスクを分ける方が早く終わります。
5. 安全装置③:レート上限を検知して自動停止する
これが無人運用で最も重要です。レート上限に当たったのにループが回り続けると、エラーを入力にして空回りしながらラウンドを消費します。
出力を見て検知し、例外で抜けます。
_RATE_HINTS = ("rate limit", "usage limit", "rate_limit", "usage_limit",
"too many requests", "リミット", "上限に達")
class RateLimited(Exception):
"""5h/7d のレート上限に当たった。ループは中断する。"""
low = (stdout + "\n" + stderr).lower()
if any(h in low for h in _RATE_HINTS):
raise RateLimited("レート上限(5h/7d)に達したようです") 重要なのは、止めるだけでなく再開できるようにすることです。中断時にセッションIDを表示しておけば、枠が回復したあとに同じ文脈で続きから再開できます。
6. 安全装置④:観測役と実行役を分離する
これはコードではなく構成の話ですが、実際に一番効きました。
ループを回すプロセスの中に監視機能を持たせると、ループが死んだ瞬間に何が起きていたかも見えなくなります。役割を分けます。
flowchart LR
A["実行役<br>能動・使い捨て"] -->|"状態をファイルに書く"| B["状態ファイル"]
B -->|"読む"| C["観測役<br>受動・常駐"]
C --> D["ブラウザで進捗を見る"] - 実行役(ループ):能動的、使い捨て。暴走してもここだけが死ぬ
- 観測役(モニタ):受動的、常駐。ループが死んでも生き残り、何が起きたかを見せる
連携は状態ファイル1枚で十分です。ただしアトミックに書くのがコツで、直接書くと読み取り側が壊れたJSONを踏みます。
def write_status(d):
"""状態をアトミックに書く。失敗は無視してループ本体を止めない。"""
try:
d = dict(d)
d["updated_at"] = int(time.time())
tmp = STATUS_FILE + f".tmp.{os.getpid()}"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(d, f, ensure_ascii=False)
os.replace(tmp, STATUS_FILE) # ← アトミックに差し替える
except OSError:
pass もう一つのポイントは、状態書き出しの失敗でループを止めないことです。観測はあくまでおまけなので、ここで例外を上げると本末転倒です。
7. 安全装置⑤:git管理下+全ターンの進行ログ
自動編集を有効にして回す以上、いつでも捧てられる状態でなければ安心して任せられません。最低限この2つです。
① 対象は必ずgit管理下にする
いつでも git diff で差分を見れ、気に入らなければ捧てられます。そもそもreviewerに渡す入力が git diff なので、ループの前提でもあります。
② 全ターンの全文をログに残す
画面の流れる出力だけだと、あとで「なぜこうなったか」を追えません。弊社では1回の実行ごとにMarkdownの進行ログを出し、各ターンの全文・時刻・トークン数を残しています。
## worker (round 2) `14:23:07` (out 1842tok, $0.0421)
(実装の応答全文)
## reviewer (round 2) `14:24:11` (out 512tok, $0.0133)
(レビューの全文)
---
## 結果: __REVIEW_OK__ で正常終了
- 所要: 187秒 ・ 呼び出し 4回
- トークン合計: in 48,201 / out 3,104 このログはチューニングの材料になります。どのラウンドで無駄な往復をしているかが見えるので、プロンプトを直す判断がつきます。
8. 運用してわかったこと
実際に回してみて得た知見を、良いことも悪いことも書いておきます。
- 回数を増やすより、タスクを小さく切る方が効く。6ラウンドで収束しないのは大抵タスクが大きすぎる合図
- reviewerの観点を指定すると質が安定する。「正しさ・セキュリティ・テスト」のように軸を与えないと、毎回違うところを見て収束が遅くなる
- 完全無人(
--dangerously-skip-permissions)は隔離環境だけにした方がいい。通常は--permission-mode acceptEditsで十分 - テストを回せるならレビューより先にテストループ。合否が機械的に決まる分、停止判定が圧倒的に楽
- 生成物は最後に人が見てからコミットする。自動コミットは便利だが、レビューを飛ばす言い訳になりやすい
まとめ
- 自律ループの最小形は worker(実装)⇄ reviewer(差分レビュー)。ループ自体は10行で書ける
- 難しいのは停止条件。センチネル文字列を強制し、装飾を禁止する
-
--session-idで worker と reviewer の文脈を分ける。混ぜると自分の実装を甘く見る - 最大ラウンド数(既定6)とレート上限検知の2段構えで空回りを防ぐ。中断時は再開できるようにIDを残す
- 観測役と実行役を分離し、連携は状態ファイル1枚でアトミックに。観測の失敗でループを止めない
- git管理下+全ターンの進行ログ。あとで追えることがチューニングの前提になる
自律ループは「放っておけば完成する魔法」ではありません。人間がやっていた往復の仲介を、安全装置付きでコードに移すというだけの話です。その分、安全装置を先に入れてしまえば、安心して回せるようになります。
お問合せはこちら
よくある質問(FAQ)
Q. ループはどれくらいレート枠を消費しますか?
タスクの大きさによりますが、1タスクでworkerとreviewerを合わせて数回〜十数回呼び出すので、体感より早く枠を食います。初めは --max を小さくし、実行ログのトークン合計を見ながらペースをつかむのが安全です。
Q. センチネルが出ていないのに終了してしまいます
指摘の本文中にセンチネルを例示されている可能性が高いです(「問題がなければ__REVIEW_OK__と返すべきですが……」のような文)。判定を「出力全体をtrimした結果がセンチネルと完全一致」に変えると堅くなります。
Q. reviewer を複数にするべきですか?
観点が広いタスクでは有効です。「正しさ」「セキュリティ」「テスト」を別セッションのレビュアーに分けると、同じモデルでも見つけるものが変わります。ただしその分レート枠を使うので、まずは1人から始めてください。
Q. claude -p は対話モードと何が違いますか?
-p(--print)は対話UIを出さずに応答を返して終了します。スクリプトから呼ぶならこちらです。--output-format json を付けると応答本文に加えてトークン数やコストが取れるので、ログに残しやすくなります。
関連記事
📄Loop Engineeringとは?プロンプトエンジニアリングの次に来るAIエージェント設計|仕組み・5要素・実践を図解【2026年最新】
📄claude -p(ヘッドレスモード)の課金はサブスク枠?API課金?— 認証方式で決まる仕組みとcron無人運用の設計
📄Claude Codeを並行セッションで安定運用する — tmux起動・git worktree分離・孤児プロセス対策の実践
📄ハーネスエンジニアリングとは?AIエージェント時代に「モデル選び」より重要な設計思想
📄Claude Codeで疑似経営会議を開く — CFO・CHRO・COOを独立エージェントで並列に走らせて経営判断を壁打ちする
