突然ですが皆さん、Step Functionsの疎通テストってどうやってます?
私は「直す → デプロイ → 落ちる → ログ睨む → また直す」のトライアンドエラーです。
あの作業わりと苦行なんですよね。なので最近やたら耳にする ループエンジニアリング に丸投げしてみました。
この記事でやったこと
- Ralph Loop(自己修正ループ)1 に、わざとバグを仕込んだStep Functionsを渡す
- AIが自律的に・段階的にバグを直して「ローカルGREEN → AWSデプロイ → 実行SUCCEEDED」まで通す
そして、ただ闇雲にループを回しても収束しません。AIって放っておくと「さっきと同じ直し方」を平気でもう一回やったりするんですよ...(マジで)
自律的にちゃんと収束させるためのカギは、この3つ。
- ローカルGREENゲート … ローカルが通るまでAWSに進ませない
-
ERROR_DETAIL.mdによる記憶 … 失敗・試行履歴をイテレーション間で引き継ぐ - 根本原因を直す規律 … 症状の場当たり修正を禁止する
この「ゲート・記憶・規律」の3点セットが効くんですが、それは後ほど。
1. そもそもRalph Loopって何?
簡単に言うと 「エージェントが終わろうとすると、同じプロンプトをもう一回ぶち込む」 ことで自己修正を延々と回し続ける手法です(Claude Codeのプラグインです)。
仕組みはこんな感じ。
-
/ralph-loop "<プロンプト>" --max-iterations N --completion-promise DONEで起動 - エージェントがターンを終えようとすると Stopフック2が発火して、同じプロンプトを再注入
- エージェントが
<promise>DONE</promise>を出力するか、max-iterationsに達したら終了
ポイントは 「完了条件が本当に真になるまで、嘘ついて抜けちゃダメ」 というルール。ここ、地味に大事なんですよね。
AIって「もう直りました!(直ってない)」みたいな雰囲気で帰ろうとすることがあるので、ちゃんと首根っこを掴んでおく必要があります。今回の完了条件は「AWS実行がSUCCEEDEDになったら <promise>DONE</promise>」。これ以外では絶対に帰してもらえません。スパルタです。
こちらの記事がとてもわかりやすくまとめられていました!!
2. 自律ループの全体像
各イテレーションで、ループはこのフローをぐるぐる回します。ローカルが通るまでAWSには進ませないのがミソ。
AWSにデプロイするたびに時間もお金もちょっとずつ溶けます。
安いローカルで失敗を吸収してもらおう、という作戦です。
④の「ローカルGREENゲート」が今回の主役です。ローカルがSUCCEEDEDにならない限り、AWSには一歩も進ませません。RED/FAILEDはぜんぶ ERROR_DETAIL.md に記録されて、Stopフックの再注入で次のイテレーションに引き継がれます。
直してもらう対象のステートマシンは、Lambdaを2個直列に呼ぶだけの超ミニマム構成です。
構成はAWS SAM(Lambda×2 + StateMachineを
template.yamlで定義)。ローカル検証はStep Functions Local +sam local、本番はsam deployです。
3. わざと仕込んだ2つのバグ
ループが「段階的に」直していく様子が見たいので、性質の違うバグを各Lambdaに1個ずつ埋めました。# BUG みたいな親切なネタバレコメントはなしです。
| ファイル | バグ(修正前) | 正解 | どう失敗するか | |
|---|---|---|---|---|
| バグA | functions/first/app.py |
result = value + "1" |
value + 1 |
int + str で TypeError
|
| バグB | functions/second/app.py |
return {"status": "ng", ...} |
"status": "ok" |
Choiceが "ok" を要求 → FailState
|
バグAは「型を間違えて落ちる」古典中の古典、バグBは「処理は通るけど中身が間違っててワークフローが失敗判定になる」やつ。AIが1個ずつ順番に潰していけるかを見たかったわけです。
4. 自律収束の心臓部:ERROR_DETAIL.md とドライバープロンプト
さてここが今回いちばん伝えたいところです。
AIはただループを回すだけだと収束しません。
同じ修正を延々繰り返したり、症状だけを場当たり的にいじって根本原因を放置したり…。なので記憶と規律を仕込んでやる必要があります。
ERROR_DETAIL.md(イテレーション間の引き継ぎ役)
AIには記憶がないので、外付けのメモ帳を持たせます。3セクション構成で、毎イテレーションの冒頭に必ず読ませます。
- 現在の状態(毎回上書き): iteration / phase / status / failed_state / error_type / root_cause / attempted_fix / next_hypothesis
- 禁止リスト: 「失敗ステート+エラー型+試した修正」が再登場したら追記して、二度と同じ手を打たせない(=同じ過ちを繰り返させないための反省ノート)
- 試行履歴(追記のみ・絶対に消さない): 各イテレーションの記録
「あ、それさっきやってダメだったやつだ」をちゃんとファイルに刻んでおくイメージです。
ドライバープロンプトの主なルール
- 同じ修正を2回繰り返さない / 修正前に必ず仮説を1つ書く / next_hypothesisは前回と変える
- ローカルがREDのままAWSへ進まない
- 1イテレーションでは、観測された失敗が示す箇所だけを最小修正
- 症状じゃなくて根本原因を直す/エラーをマスクしない
要するに「焦るな・嘘つくな・前回と同じことするな」を、ひたすらプロンプトで言い聞かせている感じです。
5. 実際のループ実行ログ(3イテレーションで完走)
実際に走らせたログがこちらです。
ERROR_DETAIL.md(全文)
ERROR_DETAIL
運用ルール: 毎イテレーション冒頭でこのファイルを読む。試行履歴に同一の「失敗ステート名+エラー型+試した修正」が既出なら、その修正は禁止リスト扱いとし別の仮説を立てる。next_hypothesis は必ず前回と変える。
現在の状態(毎イテレーション上書き)
- iteration: 3
- phase: AWS
- status: GREEN ✅
- failed_state: -(なし)
- error_type: -(なし)
- root_cause: -(bug A / bug B とも解消済み)
- attempted_fix: -(追加修正なし)
- next_hypothesis: -(完了。AWS 実行が SUCCEEDED に到達)
禁止リスト(同じ修正の再試行を防ぐ)
(なし。各バグは1回の修正で解消したため再試行なし)
試行履歴(追記のみ・絶対に消さない)
iteration-1 (phase: LOCAL, status: RED)
- failed_state: FirstTask
- error_type: TypeError ("unsupported operand type(s) for +: 'int' and 'str'")
- root_cause: functions/first/app.py:4
result = value + "1"(int に str を加算) - attempted_fix:
value + "1"→value + 1 - result: 修正適用済み。iteration-2 の run-local で FirstTask が
{"value": 2, "step1": "done"}を正常出力することを確認 → 解消
iteration-2 (phase: LOCAL, status: RED)
- failed_state: CheckResult → FailState
- error_type: UnexpectedStatus ("$.status was not 'ok'")
- root_cause: functions/second/app.py:4
return {"status": "ng", ...}(Choice が "ok" を要求) - attempted_fix:
"status": "ng"→"status": "ok" - result: 修正適用済み。iteration-3 の run-local で解消を確認 → 解消
iteration-3 (phase: LOCAL→AWS, status: GREEN)
- ローカル実行: SUCCEEDED(LOCAL GREEN)
- AWS デプロイ:
sam deployでスタックaituber-char-sfn作成成功(FirstFunction / SecondFunction / StateMachine + IAM Roles) - AWS 実行: executionArn
...StateMachine-qPLIvdGm96Wl:032a8f6d-...→ status SUCCEEDED(AWS GREEN) - result: 完了。
<promise>DONE</promise>を出力してループ終了
【抜粋版】
| Iter | フェーズ | 観測した失敗 | 根本原因 | 修正 | 結果 |
|---|---|---|---|---|---|
| 1 | LOCAL |
FirstTask で TypeError |
first/app.py value + "1"
|
value + 1 |
バグA解消 |
| 2 | LOCAL |
CheckResult→FailState:$.status was not 'ok'
|
second/app.py "status": "ng"
|
"ok" |
バグB解消 |
| 3 | LOCAL→AWS | なし | — | — | ローカルGREEN → AWSデプロイ → 実行SUCCEEDED |
今回の「動き」を図にすると、失敗箇所が1段ずつ前進しながら収束していくのが分かって、ちょっと気持ちいいです。
イテレーション1(バグA発見・修正)
▶ iteration-1:ローカル実行履歴(全文 / タイムスタンプ・inputDetails 等は省略)
{
"events": [
{ "id": 1, "type": "ExecutionStarted",
"executionStartedEventDetails": { "input": "{\"value\":1}", "roleArn": "arn:aws:iam::012345678901:role/DummyRole" } },
{ "id": 2, "type": "PassStateEntered", "stateEnteredEventDetails": { "name": "PrepareInput", "input": "{\"value\":1}" } },
{ "id": 3, "type": "PassStateExited", "stateExitedEventDetails": { "name": "PrepareInput", "output": "{\"value\":1}" } },
{ "id": 4, "type": "TaskStateEntered", "stateEnteredEventDetails": { "name": "FirstTask", "input": "{\"value\":1}" } },
{ "id": 5, "type": "LambdaFunctionScheduled",
"lambdaFunctionScheduledEventDetails": { "resource": "arn:aws:lambda:us-east-1:123456789012:function:FirstFunction", "input": "{\"value\":1}" } },
{ "id": 6, "type": "LambdaFunctionStarted" },
{ "id": 7, "type": "LambdaFunctionSucceeded",
"lambdaFunctionSucceededEventDetails": {
"output": "{\"errorMessage\": \"unsupported operand type(s) for +: 'int' and 'str'\", \"errorType\": \"TypeError\", \"requestId\": \"fc081a01-...\", \"stackTrace\": [\" File \\\"/var/task/app.py\\\", line 4, in handler\\n result = value + \\\"1\\\"\\n\"]}"
} },
{ "id": 8, "type": "TaskStateExited",
"stateExitedEventDetails": { "name": "FirstTask", "output": "<id:7 と同じ TypeError JSON>" } },
{ "id": 9, "type": "TaskStateEntered",
"stateEnteredEventDetails": { "name": "SecondTask", "input": "<id:7 と同じ TypeError JSON>" } },
{ "id": 10, "type": "LambdaFunctionScheduled",
"lambdaFunctionScheduledEventDetails": { "resource": "arn:aws:lambda:us-east-1:123456789012:function:SecondFunction", "input": "<id:7 と同じ TypeError JSON>" } },
{ "id": 11, "type": "LambdaFunctionStarted" }
// ← SecondFunction が bug A の異常出力("value" キー無し)を受けて停滞し、ポーリングがタイムアウト
]
}
イテレーション2(バグB発見・修正)
▶ iteration-2:ローカル実行履歴(全文 / タイムスタンプ・inputDetails 等は省略)
{
"events": [
{ "id": 1, "type": "ExecutionStarted",
"executionStartedEventDetails": { "input": "{\"value\":1}", "roleArn": "arn:aws:iam::012345678901:role/DummyRole" } },
{ "id": 2, "type": "PassStateEntered", "stateEnteredEventDetails": { "name": "PrepareInput", "input": "{\"value\":1}" } },
{ "id": 3, "type": "PassStateExited", "stateExitedEventDetails": { "name": "PrepareInput", "output": "{\"value\":1}" } },
{ "id": 4, "type": "TaskStateEntered", "stateEnteredEventDetails": { "name": "FirstTask", "input": "{\"value\":1}" } },
{ "id": 5, "type": "LambdaFunctionScheduled",
"lambdaFunctionScheduledEventDetails": { "resource": "arn:aws:lambda:us-east-1:123456789012:function:FirstFunction", "input": "{\"value\":1}" } },
{ "id": 6, "type": "LambdaFunctionStarted" },
{ "id": 7, "type": "LambdaFunctionSucceeded",
"lambdaFunctionSucceededEventDetails": { "output": "{\"value\": 2, \"step1\": \"done\"}" } },
{ "id": 8, "type": "TaskStateExited",
"stateExitedEventDetails": { "name": "FirstTask", "output": "{\"value\": 2, \"step1\": \"done\"}" } },
{ "id": 9, "type": "TaskStateEntered",
"stateEnteredEventDetails": { "name": "SecondTask", "input": "{\"value\": 2, \"step1\": \"done\"}" } },
{ "id": 10, "type": "LambdaFunctionScheduled",
"lambdaFunctionScheduledEventDetails": { "resource": "arn:aws:lambda:us-east-1:123456789012:function:SecondFunction", "input": "{\"value\": 2, \"step1\": \"done\"}" } },
{ "id": 11, "type": "LambdaFunctionStarted" },
{ "id": 12, "type": "LambdaFunctionSucceeded",
"lambdaFunctionSucceededEventDetails": { "output": "{\"status\": \"ng\", \"value\": 2, \"step2\": \"done\"}" } },
{ "id": 13, "type": "TaskStateExited",
"stateExitedEventDetails": { "name": "SecondTask", "output": "{\"status\": \"ng\", \"value\": 2, \"step2\": \"done\"}" } },
{ "id": 14, "type": "ChoiceStateEntered",
"stateEnteredEventDetails": { "name": "CheckResult", "input": "{\"status\": \"ng\", \"value\": 2, \"step2\": \"done\"}" } },
{ "id": 15, "type": "ChoiceStateExited",
"stateExitedEventDetails": { "name": "CheckResult", "output": "{\"status\": \"ng\", \"value\": 2, \"step2\": \"done\"}" } },
{ "id": 16, "type": "FailStateEntered",
"stateEnteredEventDetails": { "name": "FailState", "input": "{\"status\": \"ng\", \"value\": 2, \"step2\": \"done\"}" } },
{ "id": 17, "type": "ExecutionFailed",
"executionFailedEventDetails": { "error": "UnexpectedStatus", "cause": "$.status was not 'ok'" } }
]
}
イテレーション3(ローカルGREEN → AWS)
▶ iteration-3:ローカル GREEN 確認
=== [8/8] 実行完了を待機中 ===
[1] status: RUNNING
[2] status: SUCCEEDED
==========================================
LOCAL GREEN
==========================================
▶ iteration-3:sam deploy ログ(CloudFormation、AWSアカウントIDはマスク)
=== [2/4] sam deploy ===
Deploying with following values
===============================
Stack name : aituber-char-sfn
Region : ap-northeast-1
Capabilities : ["CAPABILITY_IAM"]
CloudFormation stack changeset
-------------------------------------------------------------------------------------------------
Operation LogicalResourceId ResourceType Replacement
-------------------------------------------------------------------------------------------------
+ Add FirstFunctionRole AWS::IAM::Role N/A
+ Add FirstFunction AWS::Lambda::Function N/A
+ Add SecondFunctionRole AWS::IAM::Role N/A
+ Add SecondFunction AWS::Lambda::Function N/A
+ Add StateMachineRole AWS::IAM::Role N/A
+ Add StateMachine AWS::StepFunctions::StateMachine N/A
-------------------------------------------------------------------------------------------------
CloudFormation outputs from deployed stack
-------------------------------------------------------------------------------------------------
Key StateMachineArn Value arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:stateMachine:StateMachine-qPLIvdGm96Wl
-------------------------------------------------------------------------------------------------
Successfully created/updated stack - aituber-char-sfn in ap-northeast-1
OK: sam deploy PASSED
▶ iteration-3:AWS ワークフロー実行(SUCCEEDED)
=== [4/4] AWS ワークフロー実行 ===
executionArn: arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:execution:StateMachine-qPLIvdGm96Wl:032a8f6d-...
実行完了を待機中...
[1] status: RUNNING
[2] status: SUCCEEDED
==========================================
AWS GREEN
==========================================
6. まとめ
わざとバグを2個仕込んだStep FunctionsをRalph Loopに渡したところ、3イテレーションで段階的にバグを潰して、AWS実行SUCCEEDEDまで自律的にたどり着いてくれました。自分のミスの尻拭いをAIに丸投げする時代、来てますね。
ここで一番伝えたいのは「AIすごい」(小並感)ではなく、自己修正ループを"ただ回す"だけではなく、設計に組み込んだ3つのルールの重要性です。
「ゲート・記憶・規律」の3点セット
- ローカルGREENゲート … 失敗は安いローカルで吸収。AWSにはGREENになってからしか進ませない(コスト&時間の節約)
- ERROR_DETAIL.md … 状態と試行履歴を引き継いで、同じ失敗の繰り返しを防ぐ(AIの外付け反省ノート)
- 根本原因を直す規律 … 症状の場当たり修正を禁止し、上流のコードを直させる
AIに自走させたいなら、能力に期待するより 環境(ガードレール)を整えるほうが効く ということですね。
少しでも参考になれば嬉しいです。
いいねを押してもらえると泣いて喜びます!最後まで読んでいただきありがとうございました🙌

