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で早期終了を防ぐ

長いコード改修・コンテキスト溢れ対策

このうち OODAInner/Outer 二重ループ は、名前だけでは処理が想像しにくいので、具体的に補足します。

OODA: 生データを「解釈」してから動く

ReAct の「推論 → 行動」に、Orient(状況づけ) という独立の段を挟んだ型です。

取ってきた生データをそのまま判断材料にせず、いったん「いま何が起きているのか」の解釈に翻訳してから決める。ここが違いです。1周はこう流れます。

  1. Observe(観測): データを取る(例:問い合わせが届いた/在庫数/価格の動き)。
  2. Orient(状況づけ): それが どんな状況か に翻訳する(例:「クレームが急増中」「通常運転」「繁忙期で入荷が遅れ気味」)。ここが OODA の心臓部。
  3. Decide(決定): 状況に合った次の一手を選ぶ(例:担当へエスカレーション/定型返信で足りる/発注量を増やす)。
  4. 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 では、この「よく設計された境界の中で勝手に回る」状態を実運用で作り続けています。設計や事業の相談などお気軽にお問い合わせからご連絡ください。

ループエンジニアリングとは何か - プロンプトより「ループ」を設計する - 合同会社ふんどし