Blog
ー
ループエンジニアリングとは何か - プロンプトより「ループ」を設計する
2026-07-12 13:18:33

ループエンジニアリング とは、プロンプトを1回書くことではなく、エージェントを回し続ける「ループ」を設計することです。
ループには、起動の合図(trigger)、検証できるゴール、止め方(停止条件)の3つが要ります。価値の単位が「1回の応答」から「ゴールに到達するまでの軌跡」へ移った、という考え方です。
これは、elatt がこだわってきた「人手をかけずに回す(自走)」の本丸でもあります。
この記事では、ループエンジニアリングが何で、どう設計し、なぜ効くのかを、最小実装を動かしながら説明します。対象はエージェントを設計・運用する人です。
ループエンジニアリングとは
2026年6月、AIコーディングの中心にいる二人の発信が、この言葉を一気に広めました。
Anthropic で Claude Code を作った Boris Cherny は「もう Claude にプロンプトしない。ループを回している。私の仕事はループを書くことだ(元動画)」と語り、OpenClaw の Peter Steinberger の投稿は650万回以上見られ、大きな議論を呼びました。
プロンプトエンジニアリングの次のメタ、という文脈です。
エージェントループ(agentic loop)の定義
エージェントループは、たった2つで成り立ちます。
- trigger(起動):ループを始める何か(PRが開く、スケジュール、人が「やって」と言う)
- verifiable goal(検証可能なゴール):到達すべき、確かめられる終了状態
エージェントは、あなたの次のメッセージを待ちません。
自分で動き、ゴールに着いたかを確かめ、着いていなければ、ゴール達成か停止条件まで繰り返します。
これが、プロンプトを書いては待ち、また書く、という従来のやり方との違いです。
"自動化"との違いは「判断」があること
自動化(automation)は、決められた手順を実行するだけです(レシピをなぞる、スクリプトを走らせる)。何も判断しません。
一方ループは、内部に判断(ゴール検証)を持ちます。実行するだけでなく、「ゴールに着いたか」を毎回評価し、結果に応じて回り直す。このゴール検証ステップこそが、ループを自動化と分ける一線です。
ループの内部: 5つの段階
ループは、停止条件に達するまで次の5段階を回ります。

この骨格の原型は、2022年の ReAct(Reasoning + Acting、Princeton University と Google Research, Brain team の論文)です。
各ステップで「推論(なぜこうするか)」と「行動(実際のツール呼び出し)」を交互に出し、観測を次の推論に戻す。いまのコーディングエージェントの大半がこの上に立っています。
価値の単位は「応答」ではなく「軌跡」
ここがいちばん大事な発想の転換です。
1ターン目でバグを出しても構わない。システムがそのバグを検知し、テストを走らせ、4ターン目までに直せばよいからです。
問われるのは1回の応答の良さではなく、ゴールに到達するまでの軌跡(trajectory)です。
だからエンジニアの仕事は、良い1球を投げることから、その軌跡を生む「ループの境界」と「ゴールの定義」を設計することへ移ります。
要点: ループエンジニアリングとは、プロンプトの代わりに「ゴール+検証+停止条件」を書くこと。見るべきは1回の応答ではなく、ゴールに着くまでの軌跡。
ループをどう設計するか(停止条件とガードレール)
ループは、放っておくと壊れます。LLMには「終わった」という感覚がありません。
LLMは本質的に「次に続けるのが最もそれらしい一手」を出し続ける確率モデルで、"タスク完了"という状態を自分の内側に持ちません。だから「終わり」は必ず外から与える必要があります(テストが緑になった、ゴール判定が通った、といった形で)。
停止条件を書かなければ、ループはお金が尽きるまで回ります。だからループエンジニアリングの実体は、「賢いプロンプト」ではなく「止め方と境界の設計」です。
まず止め方を決める(停止条件)
本番のループには、最低限これが要ります。
- ハードな反復上限(最大何サイクルで止めて現状を報告するか)
- トークン/コストの予算(1回の実行あたりの上限。最初から入れる)
- 無進捗の検知(出力が前の反復から変わらなければ抜ける)
- 検証可能なゴール判定(エージェントの自己申告ではなく、テスト緑・型エラーなしなど自動で確かめられる基準で)
- タイムアウト(タスク全体と、個々のツール呼び出しの両方に)
「終わりはエージェントに判断させる」は、想像より早くトークン上限を溶かす戦略です。
境界を引く(ガードレール)
ループを「ただ回す」ことと「ループエンジニアリング」の差は、ガードレールがあるかどうかです。
反復上限・コスト予算・無進捗検知に加えて、サーキットブレーカ(ツール呼び出しのリトライ上限と、失敗の明示)、そして不可逆な操作(DB書き込み・デプロイ・外部API)の前に人のチェックを挟む。
目的は自律をなくすことではありません。自律に境界を引くことです。
代表的なループ型と選び方
ループには型があります。タスクに合わせて選びます。
ループ型 | 特徴 | 向くタスク |
|---|---|---|
ReAct | 推論+行動を交互に。多くの本番の土台 | 単発のツール利用+リトライ |
Reflexion | 失敗の自己批評をメモリに入れ次回へ | 試行錯誤・デバッグ・不慣れな領域 |
Plan-and-Execute | 計画→実行→再計画を分離 | 依存関係が読める複数ステップ |
OODA | Observe-Orient-Decide-Act。状況づけを足す | 変化の速い環境 |
Inner/Outer 二重ループ | 内側で実行、外側が戦略をリセット | 戦略ごと見直す可能性がある |
Multi-agent | 監督役が専門サブに割り振る | 並列探索・1コンテキストウィンドウを超える複雑さ |
Ralph Loop | 状態をファイルに持ち、毎回文脈をリセット+Stop Hookで早期終了を防ぐ | 長いコード改修・コンテキスト溢れ対策 |
このうち OODA と Inner/Outer 二重ループ は、名前だけでは処理が想像しにくいので、具体的に補足します。
OODA: 生データを「解釈」してから動く
ReAct の「推論 → 行動」に、Orient(状況づけ) という独立の段を挟んだ型です。
取ってきた生データをそのまま判断材料にせず、いったん「いま何が起きているのか」の解釈に翻訳してから決める。ここが違いです。1周はこう流れます。
- Observe(観測): データを取る(例:問い合わせが届いた/在庫数/価格の動き)。
- Orient(状況づけ): それが どんな状況か に翻訳する(例:「クレームが急増中」「通常運転」「繁忙期で入荷が遅れ気味」)。ここが OODA の心臓部。
- Decide(決定): 状況に合った次の一手を選ぶ(例:担当へエスカレーション/定型返信で足りる/発注量を増やす)。
- Act(実行): 実行し、また Observe へ戻る。
observe() → raw = 生データ
orient(raw) → state = 「いまはこういう局面」 ← ReActには無い翻訳ステップ
decide(state) → action ← 生データではなく“局面”から決める
act(action) → observe() へ戻る生データを鵜呑みにせず 文脈に変換してから動く ので、状況がころころ変わる環境(相場・障害対応・炎上対応)で誤判断が減ります。
逆に、状況がほぼ一定の単純作業では Orient はやりすぎで、素の ReAct で十分です。
Inner/Outer 二重ループ: 作戦の「実行」と「見直し」を分ける
ループを入れ子にして、 内側は決めた作戦を回すこと、外側は作戦そのものを見直すこと に役割を分けます。
内側は短い周期で速く、外側はたまに立ち止まって「そもそもこの作戦でいいのか」を評価し、ダメなら 作戦ごと差し替えて 内側を作り直します。
while (!done) { // 外側ループ:作戦の妥当性を見直す(長い周期)
Strategy s = plan(context); // 作戦を立てる/選ぶ
for (int i = 0; i < K; i++) { // 内側ループ:その作戦で回す(短い周期)
Result r = act(s); // 実行
if (goalMet(r)) return; // ゴール到達なら終了
s = tweak(s, r); // ここでの直しは“同じ作戦の中の微調整”だけ
}
// K回やってもダメ → 外側へ戻り、作戦を丸ごと組み直す(リセット)
}キモは、内側の tweak が 同じ作戦の中の微調整 なのに対し、外側の plan は 作戦そのものの入れ替え だという役割の差です。
内側だけで回し続けると、間違った作戦のまま延々と微調整する“沼”にはまります。外側があると、行き詰まりを検知して戦略をリセットできる。
後述のトレード例でいえば、内側=日々のシグナルで売買を回す、外側=週次で成績を見て負けていれば戦略ごと差し替える、という二層になります。
選び方は単純で、まず一番単純な ReAct から始め、測れる改善があるときだけ複雑にする。
実世界のタスクの多くは、ツール数個の ReAct で足ります。Multi-agent は1セッションあたり約15倍のトークンを食うので、その分の価値が要ります。
実運用での選び方: 「どう起動し・どう止め・何を手放すか」
いま挙げた ReAct や Reflexion は「ループの中でどう考えるか」の型でした。
実運用では、もう一つ別の軸「どう起動し、どう止め、人は何を手放すか」で選ぶと分かりやすい、という整理を Claude Code チームが公開しています(@ClaudeDevs「Getting started with loops」)。彼らはループを「停止条件を満たすまで作業サイクルを繰り返すエージェント」と定義し、次の4つに分けます。
実運用の型 | 起動(trigger) | 止め方(stop) | 人が手放すもの | 向く仕事 |
|---|---|---|---|---|
ターン型(対話ループ) | 人のプロンプト | LLMが「完了 or 要追加情報」と判断 | 検証(自分の代わりに確かめる) | 短い単発の作業 |
ゴール型 | 人のプロンプト | ゴール達成 or 最大ターン到達 | 停止条件 | 完了条件が検証できる作業 |
時間型 | 一定の時間間隔 | 人が止める or 仕事が片付く(PRマージ・キューが空) | 起動のきっかけ | 定期作業・外部システムとの連携 |
能動(プロアクティブ)型 | イベント/スケジュール(無人) | 各タスクはゴールで終了・仕組み自体は止めるまで稼働 | プロンプトそのもの | 定型の流れ作業(不具合トリアージ、移行、依存更新など) |
「人が手放すもの」が段々増えていくのが読みどころです。
ターン型では人が結果を確かめますが、検証をエージェントに任せるほど手離れが進み、最後の能動型では起動もプロンプトも仕組みに委ねます。ここから、実務で効くコツがいくつも出てきます。
- 止め方は“決定的な条件”で書く
ゴール型は、止まろうとするたびに 別の評価モデルが停止条件を判定 し、満たすまで戻す。そのため、「テストが何件通ったか」「スコアがしきい値以上か」のような機械で測れる基準が効く。後述のバックテスト・ゲート(ドローダウンとシャープのしきい値)は、まさにこの“決定的な停止条件”。 - 検証は「スキル」に符号化する
「何をもって完了とするか」を手順として書き出し(Claude Code ならSKILL.md)、エージェントが人と同じ手順で自分の仕事を確かめられるようにする。 定量的なチェックほど自己検証しやすい(画面を開いてクリックして状態変化とスクショを確認、コンソールにエラーゼロ、など)。 - 単発の不合格を直して終わりにしない
基準を満たさなかったとき、その1件を直すだけでなく、 学びを仕組み(スキル・ルール)に符号化して次回以降すべてに効かせる。前回の記事「オーケストレーションの構成パターン整理 - いつ何を組むか」 で「出力ではなく“次の生成プロンプト”を育てた」のと同じ発想。 - コストは境界で管理する
決定的な処理は推論させずスクリプトにする(そのほうが安い)/大量実行の前に小さく試す(自動ワークフローは数百エージェントを生みうる)/監視の間隔は「対象がどれだけ変わるか」に合わせる。
失敗モードを知っておく
本番で必ず出るものです。
無限ループ(ゴール検証がない)、Goal Drift(回すうちに当初の目標から少しずつズレ、似て非なる別の目標を追い始める)、コンテキスト溢れ、サイレント失敗(ツールは動くが何も進んでいない=最も気づきにくい)、コスト爆発(壊れたツールを5分で400回呼ぶ、月$1.3Mといった実例)、誤りの伝播(序盤の1つの誤りが後段に波及)。
わかりにくいのは Goal Drift なので、例を挙げます。
「失敗しているテストを直して」と頼んだのに、直すのが難しくなって、テスト自体を消してしまうのが典型です。テストは通っても、目的(バグを直す)は果たしていません。
LLMは各ステップで「最もそれらしい次の一手」を選ぶだけで、最上位の意図を毎回参照するわけではないため、こうした小さなズレが積み重なって、気づけば別のことをやっています。
だから各ステップで検証し、しかも機械で確かめられるゴール(テストを消したら不合格、など)で毎ターン照らす。終わりだけで確かめないことが要です。
elatt が掲げる「人手をかけずに回す」は、まさにこのループ設計の話です。
停止条件とガードレールがあって初めて、安心して自走させられる。自走とは「放置」ではなく、「よく設計された境界の中で勝手に回る」状態です。
要点: ループ設計の核は「いつ止めるか」と「どこを人が見るか」を先に決めること。自律を消さず、境界を引いて安全にする。まずは ReAct。
実検証: AIにトレード戦略を出させ、「バックテスト」という検証ゲートで通す
いま話題の 自走するトレード(クオンツ)デスク を題材に確かめます。
参考記事は「Loop Engineering for trading agents(自走するクオンツ・デスクを作る)」という投稿です。
要点はひとつです。リサーチ→戦略→執行のどのループにも“検証ゲート”を置くこと。「ゲートのないループは、自分の答えに同意し続けるだけのエージェント。ゲートのあるループは、自分のミスを自分で捕まえるシステム」です。
この例におけるゲートは バックテスト(過去データで戦略を検算する)です。そこで「AIが戦略を提案→バックテストがリスク基準で合否を出す→通らなければ理由を返して直させる」というループを作ります。
- 生成役 = LLM: 移動平均クロス戦略の「短期・長期の日数」を提案する
- 検証役 = 決定的なバックテスト・ゲート: 過去データで戦略を回し、 最大ドローダウン ≤ 20% かつ シャープレシオ ≥ 1.0(リスクに見合うリターンか)を満たすかで合否を出す
- Reflexion(失敗の記憶): 落ちたら、その理由を 次の提案に書き添える(同じ失敗を避ける)
肝は、勝ち戦略を「AIが自分で良い」と言うのを鵜呑みにしないこと。
総リターンだけでなく“リスクあたりのリターン” という機械的な基準で、別の工程が冷静に落とす。
この検証ゲートがあるから、ループは暴走せず良い戦略へ収束します。
用語だけ補足します。
最大ドローダウン は「いちばんの高値から、どこまで下落したか」の最大の落ち込み幅(例: 20%なら資産が一時2割減)。小さいほど、含み損に耐えやすい安全な戦略です。
シャープレシオ は「取ったリスク(値動きの荒さ)1に対して、どれだけ増やせたか」の比率で、高いほど“割に合う”運用を意味します。
この2つを同時に課すと、 たまたま大儲けだが値動きが激しすぎる戦略(=一発の運に頼る戦略)を弾けます。「儲かったか」ではなく「安定して割に合うか」で通す、というのが検証ゲートの設計思想です。
⚠️ これは技術デモです。 投資助言ではなく、実際の売買も行いません。価格は再現用の合成データで、実市場の将来を保証するものでもありません。
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.StructuredMessageCreateParams;
import com.google.common.base.Joiner;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class QuantGateLoop {
private static final String MODEL = System.getenv().getOrDefault("LLM_MODEL", "claude-sonnet-5");
private static final int MAX_ITER = 5; // 停止条件:ハードな反復上限
private static final double MAX_DD = 0.20; // リスクゲート:最大ドローダウン 20% 以下
private static final double MIN_SHARPE = 1.0; // リスクゲート:シャープレシオ 1.0 以上
private static final double COST = 0.002; // 建玉が変わるたびの取引コスト(片道0.2%)
private static final Path OUTPUT_MD = Path.of("output.md");
private final StrategyGenerator llm;
private final OutputLog output;
QuantGateLoop(StrategyGenerator llm, OutputLog output) {
this.llm = llm;
this.output = output;
}
static final class OutputLog {
private final StringBuilder markdown = new StringBuilder();
void println(String line) {
System.out.println(line);
markdown.append(line).append(System.lineSeparator());
}
void printf(String format, Object... args) {
String line = String.format(format, args);
System.out.print(line);
markdown.append(line);
}
void save() throws IOException {
Files.writeString(OUTPUT_MD, markdown.toString(), StandardCharsets.UTF_8);
}
}
public static final class Proposal {
public int shortWindow;
public int longWindow;
public Proposal() {}
public Proposal(int shortWindow, int longWindow) {
this.shortWindow = shortWindow;
this.longWindow = longWindow;
}
int shortWindow() {
return shortWindow;
}
int longWindow() {
return longWindow;
}
}
record BacktestResult(double totalReturn, double maxDrawdown, double sharpe) {
String metrics() {
return String.format(
"リターン%+.1f%% / 最大DD%.1f%% / シャープ%.2f", totalReturn * 100, maxDrawdown * 100, sharpe);
}
}
record VerificationResult(boolean passed, String feedback, BacktestResult backtest) {
static VerificationResult pass(BacktestResult backtest) {
return new VerificationResult(true, "PASS(通過)", backtest);
}
static VerificationResult fail(String feedback, BacktestResult backtest) {
return new VerificationResult(false, feedback, backtest);
}
static VerificationResult fail(String feedback) {
return new VerificationResult(false, feedback, null);
}
}
record SelftestCase(String label, Proposal proposal) {}
interface StrategyGenerator {
Proposal generate(String model, String system, String user);
}
static final class ClaudeStructuredOutputsGenerator implements StrategyGenerator {
private final AnthropicClient client = AnthropicOkHttpClient.fromEnv();
@Override
public Proposal generate(String model, String system, String user) {
StructuredMessageCreateParams<Proposal> params =
MessageCreateParams.builder()
.model(model)
.maxTokens(100L)
.system(system)
.addUserMessage(user)
.outputConfig(Proposal.class)
.build();
return client.messages().create(params).content().stream()
.flatMap(contentBlock -> contentBlock.text().stream())
.map(textBlock -> textBlock.text())
.findFirst()
.orElseThrow(() -> new IllegalStateException("ClaudeからProposal形式の応答を取得できませんでした"));
}
}
// 決定的な過去データ(seed固定の合成価格。誰が回しても同じ)
static double[] prices() {
int days = 252;
int shockStart = 102;
int shockDays = 23;
int reboundDays = 20;
double drift = 0.000118295;
double noiseSd = 0.002600365;
double shockDrag = -0.008605990;
double reboundBoost = 0.008319029;
double[] prices = new double[days];
prices[0] = 100.0;
Random random = new Random(42);
for (int day = 1; day < prices.length; day++) {
double dailyReturn = drift + noiseSd * random.nextGaussian();
if (day >= shockStart && day < shockStart + shockDays) {
dailyReturn += shockDrag;
}
if (day >= shockStart + shockDays && day < shockStart + shockDays + reboundDays) {
dailyReturn += reboundBoost;
}
prices[day] = prices[day - 1] * (1.0 + dailyReturn);
}
return prices;
}
static double sma(double[] values, int end, int window) {
double sum = 0.0;
for (int i = end - window + 1; i <= end; i++) {
sum += values[i];
}
return sum / window;
}
// バックテスト:移動平均クロス(短期>長期で買い・以外は待機)。リターン・最大DD・シャープを返す
static BacktestResult backtest(Proposal proposal) {
double[] prices = prices();
int position = 0;
List<Double> strategyReturns = new ArrayList<>();
for (int t = proposal.longWindow(); t < prices.length; t++) {
int previousPosition = position;
position =
sma(prices, t - 1, proposal.shortWindow()) > sma(prices, t - 1, proposal.longWindow())
? 1
: 0;
double tradeCost = Math.abs(position - previousPosition) * COST;
double marketReturn = prices[t] / prices[t - 1] - 1.0;
strategyReturns.add(previousPosition * marketReturn - tradeCost);
}
double equity = 1.0;
double peak = 1.0;
double maxDrawdown = 0.0;
double sum = 0.0;
for (double strategyReturn : strategyReturns) {
equity *= 1.0 + strategyReturn;
peak = Math.max(peak, equity);
maxDrawdown = Math.max(maxDrawdown, (peak - equity) / peak);
sum += strategyReturn;
}
double mean = sum / strategyReturns.size();
double variance = 0.0;
for (double strategyReturn : strategyReturns) {
variance += Math.pow(strategyReturn - mean, 2);
}
variance /= strategyReturns.size();
double standardDeviation = Math.sqrt(variance);
double sharpe = standardDeviation == 0.0 ? 0.0 : mean / standardDeviation * Math.sqrt(252);
return new BacktestResult(equity - 1.0, maxDrawdown, sharpe);
}
// 検証役=リスクゲート:バックテストが基準を満たすか(決定的な合否)
static VerificationResult verify(Proposal proposal) {
int shortWindow = proposal.shortWindow();
int longWindow = proposal.longWindow();
if (!(shortWindow >= 2 && shortWindow < longWindow && longWindow <= 60)) {
return VerificationResult.fail("日数が不正(2≤short<long≤60 で提案して)");
}
BacktestResult result = backtest(proposal);
if (result.maxDrawdown() > MAX_DD) {
return VerificationResult.fail(
String.format(
"最大ドローダウン %.1f%% が上限%.0f%%を超過(リスク過大)", result.maxDrawdown() * 100, MAX_DD * 100),
result);
}
if (result.sharpe() < MIN_SHARPE) {
return VerificationResult.fail(
String.format("シャープレシオ %.2f が基準%.1f未満(リスクに対しリターンが薄い)", result.sharpe(), MIN_SHARPE),
result);
}
return VerificationResult.pass(result);
}
// 生成役:Structured Outputs が基準+過去の失敗(Reflexion)から戦略の日数を提案
Proposal proposeWithLlm(List<String> failures) {
String system =
"""
あなたは移動平均クロス戦略の設計者です。
短期と長期の移動平均日数を提案してください。
短期と長期の日数は、Structured OutputsのProposalスキーマに従って返してください。
条件: 2≤shortWindow<longWindow≤60
""";
String user =
"最大ドローダウン"
+ (int) (MAX_DD * 100)
+ "%以下、シャープレシオ"
+ MIN_SHARPE
+ "以上を満たす移動平均の日数を提案して。"
+ (failures.isEmpty() ? "" : "\n【これまでのボツ(繰り返すな)】\n" + Joiner.on('\n').join(failures));
return llm.generate(MODEL, system, user);
}
// ── ループ本体:提案→バックテストのゲート→(落ちたら記憶して)再提案。停止は「通過 or 反復上限」──
void run() {
List<String> failures = new ArrayList<>();
for (int turn = 1; turn <= MAX_ITER; turn++) {
Proposal proposal = proposeWithLlm(failures);
VerificationResult verification = verify(proposal);
if (verification.passed()) {
output.printf(
"turn %d: PASS — short=%d, long=%d / %s%n",
turn, proposal.shortWindow(), proposal.longWindow(), verification.backtest().metrics());
output.println("\n停止理由:ゲート通過(リスク基準クリア)/ 成功ターン=" + turn);
return;
}
output.printf(
"turn %d: NG — short=%d, long=%d → %s%n",
turn, proposal.shortWindow(), proposal.longWindow(), verification.feedback());
failures.add(
"short="
+ proposal.shortWindow()
+ ",long="
+ proposal.longWindow()
+ " は "
+ verification.feedback());
}
output.println("\n停止理由:反復上限(基準を満たす戦略が見つからず安全に停止)");
}
public static void main(String[] args) throws IOException {
OutputLog output = new OutputLog();
output.println(
"お題:戦略の移動平均日数を提案 / 生成役=LLM(" + MODEL + ") / 検証役=バックテストのゲート / 上限" + MAX_ITER + "回\n");
new QuantGateLoop(new ClaudeStructuredOutputsGenerator(), output).run();
output.save();
}
}生成役の LLM 呼び出しを動かすと、例えばこうなります。
お題:戦略の移動平均日数を提案 / 生成役=LLM(claude-sonnet-5) / 検証役=バックテストのゲート / 上限5回
turn 1: NG — short=10, long=50 → シャープレシオ -0.04 が基準1.0未満(リスクに対しリターンが薄い)
turn 2: PASS — short=15, long=45 / リターン+4.3% / 最大DD2.5% / シャープ1.54
停止理由:ゲート通過(リスク基準クリア)/ 成功ターン=2読み取れることはシンプルです。
1回目の提案は、“リスクを取ったにもかかわらず、平均すると少しずつ損をしている戦略” でした。
バックテストが「シャープレシオ -0.04=リスクに対しリターンが薄い」と落とし、その失敗情報はそのまま次の提案に与え、LLM が組み直して turn 2 でリスク基準を満たす戦略に収束します。
注意点:
- サンプルコードの価格は再現用の合成データで、手数料以外(スリッページ・レジーム変化)は簡略化しています。 バックテストで良く見えても本番で崩れるのが常です。元記事もゲートを1つで終わらせず、バックテスト → ペーパートレード → アラートのみ → 自動執行 と何段も重ねています。1つのゲートは万能ではありません。
- ループは安くありません(AIエージェントはチャットの約4倍、多数を並べると約15倍のコスト)。そのため、反復上限・予算という停止条件を先に置きます。
要点: 機械で確かめられるゴール(リスク基準のバックテスト)+停止条件 を先に用意し、 落ちた理由を次の入力に回すこと
まとめ
AIを使う単位は、プロンプト(1回の応答)からループ(ゴールに着くまでの軌跡)へ移りつつあります。
ループエンジニアリングとは、その軌跡を生むゴール・検証・停止条件・ガードレールを先に設計すること。まずは ReAct から始め、複雑さは測れるときだけ足す。そして大事なのは、自律をなくすのではなく境界を引くことです。
実運用で型に迷ったら、「検証・停止条件・起動・プロンプトのうち、いま何を安心して手放せるか」で考えると、自分の仕事のどこからループ化できるかが見えてきます。
そして忘れてはいけないのが、ループを回り続けさせるより、良い検証ゲートを1つ用意するほうが難しく、そして価値があることです。
ゴールを機械が判定できる形にできたなら、あとはその周りをループで囲むだけ。逆に「何をもって完了とするか」を言葉にできないタスクは、まだループにする段階ではありません。
elatt では、この「よく設計された境界の中で勝手に回る」状態を実運用で作り続けています。設計や事業の相談などお気軽にお問い合わせからご連絡ください。