Carnegie Mellon の学生が CMPND で開発した Flex、モデルにコード記述を委ねる新モジュールを発表
本文の状態
日本語全文を表示中
詳細モードで約18分の本文を読めます。
同じ出来事の情報源
この情報源を基点に整理
TLDR AI
DSPy の新モジュール「Flex」は、モデルのコーディング能力を活用してプロンプトだけでなくプログラム自体のコードを書き換えることで、AI エコシステムの進化に即応した最適化を実現する。
Continue in AI NEW LAB
このニュースを、実務の判断につなげる
AI NEW LABで、試したことや先に確認したい条件を共有できます。まずはログインなしで読めます。
AI NEW LABで論点を見るAI深層分析を開く2026年8月7日 22:34
AI深層分析
キーポイント
コードレベルでの最適化の実現
Flex は既存の Predict や ReAct モジュールを置き換えるだけで、モデルがプロンプトだけでなく実際のプログラムコード(関数やルーティングロジック)を書き換えられるようにする。
GEPA オプティマイザとの連携
Flex モジュールを dspy.GEPA に渡すことで、反射モデルがプログラムを分解し、ヘルパー関数の実装やルーティングロジックの実装を行いながら最適化を行う。
モデル進化の歴史的背景
2022 年の few-shot 例の自動選択から、プロンプト作成能力への進化を経て、現在はモデルが優れたプログラマーとなった段階に到達したことを示す。
生成コードの安全性
モデルが生成したコードは信頼できないため、デフォルトではホストプロセス内で実行されずサンドボックス内のインタプリタで動作する。予測呼び出しと明示的に提供されたツールのみがホストプロセスへの橋渡しを行い、その回数は cap で制限される。
最適化によるパフォーマンス向上
最適化によりモデルを呼ばずにコードで解決できるケースも現れ、呼び出す場合でも前処理が完了しているため問いが狭められる。結果として呼び出し数が減り、精度が高まるプログラムが得られる。
重要な引用
Models have become excellent programmers.
Flex hands them the program itself, so the optimizer rewrites your code and not just your prompt.
The output is an optimized program that performs best against the metric you gave it.
Code written by a model is still untrusted code, so by default it never runs in your process.
編集コメントを表示
編集コメント
モデルが単なる指示の受け手から、プログラム自体を生成・修正できる「プログラマー」として振る舞えるようになったことは、開発ワークフローの根本的な変革を示唆している。DSPy の Flex モジュールは、この進化を即座に実装環境に取り込むための重要なステップと言える。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
モデルは優れたプログラマーへと進化しました。Flex はプログラムそのものをモデルに手渡し、オプティマイザーがプロンプトだけでなくコード自体を書き換えるようにします。
これは、カーネギーメロン大学のソフトウェアエンジニアリング博士課程在籍中のマイケル・アイザック氏(LinkedIn)による寄稿記事です。氏は cmpnd でのインターンシップ期間中に執筆し、本記事で紹介する Flex モジュールを DSPy に実装しました。
DSPy の中核となる考え方は、「タスクは一度定義すれば、AI エコシステムの進展に合わせて再実装が可能である」という点にあります。これらの再実装の歴史は、モデルの進化の歴史そのものです。私たちが回避してきた弱点や活用した強さがそこに表れています。
- 2022 年当時、モデルにはタスクの実例を見せる必要がありました。そこで BootstrapFewShot のようなオプティマイザーが、few-shot(少サンプル)の事例を自動的に選定する役割を果たしました。
- その後、モデルは優れたプロンプト作成者へと成長し、MIPROv2 や GEPA といったオプティマイザーが、プログラムの指示を書き換えることでプログラム自体を改善できるようになりました。
- 最近では、モデルは優れたプログラマーへと進化しました。
今週、DSPy に `Flex` を導入しました。これはモデルのコーディング能力を活用し、プログラムの指示だけでなくコードそのものを書き換えることを可能にします。
Flex が GEPA でコードを最適化する仕組み
Flex は、モデル自身がコードを書き込むことを可能にする技術です。これにより、GEPA(Generalized Environment Planning Agent)は従来の静的なルールやハードコーディングに頼らず、状況に応じて動的にコードを生成・調整できるようになります。
dspy.Flex(YourSignature) は、既存の Predict、ReAct、または RLM のいずれにも組み込める DSPy モジュールです。
プログラム。例えば:
my_signature = "question -> answer"my_program = dspy.Predict(my_signature)# Make it Flex!my_program = dspy.Flex(my_signature)これらのプログラムをいずれ実行しても、同じ結果が得られます。最適化の前には、Flex は単なる予測モジュール(またはツールを提供する場合は RLM)に過ぎません。
Flex の最大の特徴は、最適化器に対して何を公開するかです。Flex は指示だけでなく、コードそのものも公開します。
Flex モジュールを dspy.GEPA に渡すと、反射モデルがプログラムを分解し、ヘルパー関数を作成し、ルーティングロジックを実装し、プロンプトを書き換えることさえあります。結果として得られるのは、あなたが指定した指標に対して最も高いパフォーマンスを発揮する最適化されたプログラムです。
Flex モジュールを GEPA で最適化する手順は以下の通りです。ここで示されている SamePlace は、次節で詳しく解説する場所の統合タスクのシグネチャです:
program = dspy.Flex(SamePlace) # was: dspy.Predict(SamePlace)# cheap LM to use during inferencedspy.configure(lm=dspy.LM("anthropic/claude-haiku-4-5"))# big LM to write the code and instructionsbig_lm = dspy.LM("anthropic/claude-opus-5")optimized = dspy.GEPA( metric=make_metric(penalty=0.2), reflection_lm=big_lm, max_metric_calls=400,).compile(program, trainset=train, valset=val)最適化後、optimized.save("program.json") によりソースコードが保存され、dspy.Flex(SamePlace).load(...) で復元されます。このアーティファクトは、開いて読み込み、差分を確認し、推論を行うことのできるファイルです。
返ってくるのは、指定した評価指標に対して可能な限り高いスコアを獲得するために、反射モデルが生成したプログラムです。これには通常、2 つの傾向が見られます。
まず、場合によってはモデルを呼び出さないこともあります。これは、コード内で解決できるケースを見つけたためです。次に、モデルを呼び出す場合でも、その呼び出しはより的確になります。モジュール側で既に解析や比較が完了しており、モデルにはより狭い範囲の質問だけが渡されるからです。
呼び出し回数が減り、呼び出しの質も向上し、結果として受け取ったプログラムよりも優れたプログラムが完成します。
モデルによって生成されたコードは依然として信頼できないため、デフォルトではプロセス内で実行されません。Flex は、生成されたソースコードをサンドボックス化されたインタプリタ内で実行します。ホストプロセスと通信できるのは、予測器の呼び出しと、ユーザーが明示的に提供したツールのみです。また、1 回の順方向処理(forward)でこのブリッジが通過する回数は、max_predictor_calls という上限値によって制限されます。
ロケーション統合タスク
昨年の Data + AI Summit では、Drew が地理空間データの統合タスクを通じてプロンプト最適化のデモを行いました。これは、2 つの場所リストを比較し、それらが同じ物理的な場所かどうかを判断するタスクです。このタスクは、特にエッジケースにおいて意外に難しいことが知られています。
例えば、「KIN CAFE」と「KIN」が同じ住所にある場合は、それは同一の場所とみなされます。一方、「CONCESSION #2 KEN MERCER SPORTS PARK」と「KEN MERCER SPORTS PARK」が同じ住所にある場合でも、それらは異なる場所として扱われます。
「Predict」を「Flex」に置き換え、GEPA をこのタスクで実行しました。対象は 1,029 のラベル付きペアで、評価には 240 のホールドアウトデータセット(クラスバランス型のため、偶然の正解率は 50%)を使用しています。キャッシュは無効化しており、以下のコストとレイテンシの数値は、冷間状態での本番トラフィックが実際に支払うものになります。
ベースラインは元の dspy.Predict で、レコードごとに 1 回のモデル呼び出しを行います。精度は 90.4%、コストは 1,000 レコードあたり $0.98 です。
GEPA を用いてプロンプトのみを最適化し、Flex は使用しない場合、精度は 92.5% に向上します。ただし、プロンプト最適化が調整できるのは指示文のみです。その結果、指示文が大幅に長くなり、推論時に各レコードで追加トークン分のコストが発生しました。1,000 レコードあたりのコストは $2.88 でベースラインの 2.9 倍、速度も 48% 低下しています。
Flex は最適化者に「モジュールコード」という第 2 のレバーを提供します。 GEPA を変更せずに Flex プログラムで実行すると、精度は 90.4% から 95.0% に向上し、コストは 1,000 レコードあたり $0.70 に抑えられました。プロンプトとコードの両方を最適化することで、Flex はベースラインよりも 28% コストが安く、40% 高速なプログラムを生成しました。
これはどのように実現されたのでしょうか?その一つとして、比較対象の多くはコードのみで評価できる箇所が含まれています。反射モデル(reflection model)がコードを記述し、これらの簡単なマッチングを特定して通常の Python 関数へルーティングした結果、LLM の呼び出し回数が75% 削減されました。
この挙動を活用するために、メトリクスを更新することも可能です。GEPA のメトリクスはスコアと自然言語によるフィードバックを返しますが、Flex を用いれば、生成されたプログラムが各レコードで LM(Language Model)呼び出しを何回行ったかも把握できます。この値を用いてフィードバックにペナルティを加えることができます。
score = max(0.0, correct - PENALTY * n_llm_calls)ペナルティをゼロに設定すると、呼び出しは無料となり、最適化アルゴリズムは精度のみを追求します。一方、ペナルティ(以降 λ と表記)が高まるにつれて、LLM の呼び出しごとにコストを上回る精度の向上が必要となり、最適化アルゴリズムは Python で処理できるケースを優先し、モデルは真に曖昧な事象のために温存されるようになります。λ が 1.0 を超えると、1 回の呼び出しでそのコストを回収することは不可能になります。これはダイヤルの反対側であり、モデルを一切呼び出さない状態を意味します。
本研究では λ に 0, 0.05, 0.1, 0.2, 0.4 の値を順次与えて検証を行いました。プログラム実行には最も軽量で安価な Claude モデルである Haiku 4.5 を、リフレクション(書き換え)モデルには Opus 5 を使用しています。Haiku が最弱・最安のモデルであるからこそ、ペナルティと精度のトレードオフが興味深いものとなっています。
LLM の呼び出しに対するペナルティの上昇に伴うコスト対精度の関係
baseline(ベースライン)
plain GEPA(プロンプトのみ)
Flex + GEPA
*実行:claude-haiku-4-5 · 書き換え:claude-opus-5 · n = 240 の保持データセット · エラーバーは 95% Wilson 信頼区間*
| プログラム | λ | 精度 | LM 呼び出し / レコード | $ / 1k レコード | 平均レイテンシ* |
|---|---|---|---|---|---|
| dspy.Predict ベースライン | n/a | 90.4% | 1.00 | $0.98 | 1,924 ms |
| GEPA、プロンプトのみ | n/a | 92.5% | 1.00 | $2.88 | 2,841 ms |
| Flex + GEPA | 0 | 95.0% | 0.25 | $0.70 | 1,155 ms |
| Flex + GEPA | 0.05 | 94.6% | 0.17 | $0.45 | 726 ms |
| Flex + GEPA | 0.1 | 90.8% | 0.07 | $0.18 | 347 ms |
| Flex + GEPA | 0.2 | 91.7% | 0.08 | $0.09 | 135 ms |
| Flex + GEPA | 0.4 | 92.1% | 0.004 | $0.01 | 65 ms |
8 並列時のリクエストあたりの平均レイテンシ
呼び出しが無料の場合(λ=0)でも、オプティマイザーはコードを記述しました。評価関数は精度のみをスコアリングの対象としており、唯一の指針は「可能な限りケースをコードで処理するよう求める」メトリクス内のテキストフィードバックだけでした。オプティマイザーが見つけた最良のプログラムでは、レコードの 75% を決定論的な Python ルートに振り分け、モデルを毎回呼び出す場合よりも精度が向上し、95.0%(対照群 90.4%)を達成しました(McNemar p=0.019)。さらに、処理速度もコストも削減できました。単純なケースは小規模なモデルよりもルールの方が適切に処理でき、モデルは本当に判断が必要なケースのみを対象とします。
λ=0.4 の場合、プログラムは 240 レコードに対してモデルを 1 回だけ呼び出しました。精度は 92.1% で、常に呼び出すベースラインと比較して統計的に有意差はなく、コストは約 100 分の 1、レイテンシは約 30 分の 1 でした。他の高いペナルティも同様の結果をもたらします。つまり、ベースラインと同等の精度を、大幅に低いコストで達成できるのです。これは多くの生産環境システムが採用するトレードオフです。
オプティマイザーが記述したコードを読む
λ=0.4 の場合、プログラムは反射モデルによって書かれた約 200 行の Python コードを保持しています。これを骨格部分に凝縮すると以下のようになります。
class SamePlaceModule(dspy.Module):def __init__(self):super().__init__()# The LLM is a LAST-RESORT fallback: it is only consulted for# the narrow band of pairs where the deterministic signals# genuinely conflict (e.g. clearly the same brand/name but at# a mismatching house number and a middling distance).self.judge = dspy.Predict(dspy.Signature("input_name: str, input_address: str, ""match_name: str, match_address: str, ""distance: float, name_similarity: float, ""address_analysis: str -> is_same: bool","You perform entity resolution on business/POI listings. [...] ""3. The house number is the strongest address signal. [...] ""4. The same brand name far apart means two different ""branches => NOT the same place." ))def forward(self, **inputs):import re, difflib# ~150 lines of helpers: normalize names (strip '#30696',# 'LLC', generic words like CAFE/GRILL), parse addresses into# house number + street core, compute fuzzy similarity over# the distinctive tokens... name_same = (nsim >= 0.87) or (containment and shared_len >= 5) name_diff = (not name_same) and (nsim < 0.62) decision = Noneif name_same:if addr_same and (d is None or d <= 400.0): decision = True # same name, same address, close byelif hn_diff and d is not None and d > 120.0: decision = False # different house numbers => brancheselif d is not None and d > 500.0: decision = False # far apart => different branches# ...elif name_diff: decision = False # distinctive name parts disagreeelse:# gray zone (nsim 0.62–0.87): let the address decideif addr_same and (d is None or d <= 150.0): decision = Trueelif hn_diff or (d is not None and d > 200.0): decision = False# ...if decision is None: # rules can't decide -> ask the model out = self.judge(**inputs, name_similarity=round(nsim, 3), address_analysis=analysis) decision = to_bool(out.is_same)return dspy.Prediction(is_same=bool(decision))冒頭のコメント「LLM は最終手段のフォールバックである」は、オプティマイザーが自身のアーキテクチャについて記述したものです。その下に展開されるアルゴリズムには 3 つのステージがあります。
名前を正規化します。名称はすべて大文字に変換し、フランチャイズ番号("#30696")や法的な接尾辞(LLC、INC)、句読点、および「CAFE」「RESTAURANT」「MARKET」「GRILL」などの一般的なビジネス用語約 40 語を除去します。残るのは名称の固有部分のみです。例えば「KIN CAFE」であれば「KIN」だけが残ります。住所も同様に解析され、家番と通りの核心部分が抽出されます。ユニット指定(STE 4、APT B)や通り種別を示す単語は除外されるため、「AVE」と「AVENUE」が混在しても不一致を起こすことはありません。
次に比較を行います。固有の名称トークンは、曖昧一致スコア(0 から 1 の範囲)で評価され、「確実な一致」「確実な不一致」「不確定」の 3 つの分類に振り分けられます。住所は部品ごとに比較されます。家番と通り名は別々に扱われます。これは、反射モデルがデータにおいて家番単体が強力なシグナルであると判断したためです。
最後に決定を下します。各分類には、名称の判定結果、住所データ、および 2 つのジオコード地点間の距離を組み合わせた独自のルールを適用します。例えば、名称と住所が一致し、かつ距離が 400 メートル以内であれば「同一店舗」と判断されます。一方、名称は同じだが家番が異なり、かつ距離が 120 メートル以上離れている場合は、「同一ブランドの別店舗(支店)」として扱います。
「Decide」ルールが一つも発火しない場合、例えば同じブランド名でも住所番号が一致せず、距離も中間的なケースなどでは、レコードはモデルに回されます。ただし、単独で回るわけではありません。このモジュールは自身の分析結果(解析された住所番号、ストリートの核となる部分、類似度スコア)を追加入力フィールドとして転送し、さらに判定者の指示によって学習した内容を少数の番号付きドメインルールに凝縮します。λ=0.4 の条件下では、このフォールバックが 240 件中 1 回発火しました。
決定論的か確率的か?メトリクスが決める
AI を活用してシステムを構築する人々は、常に同じ問いに直面しています。「何をコードに任せるか、何をモデルに任せるか」。これはコーディングエージェントに見られる直感と同じです。彼らは increasingly(次第に)、タスクを一つずつトークンで処理するのではなく、Python スクリプトを作成して作業を完了させるようになっています。
Flex では、学生モデルからのフィードバックとメトリクスに基づき、モデルがこの領域を探求できるようにしています。その挙動は非常に洗練され得ます。例えば、Flex と GEPA を SWE-bench Pro(GitHub の Issue から構成されるコーディングベンチマーク)に適用した際、高レベルのタスクである「Issue を読み込んで修正を生成する」および数個のツールが与えられた条件下で、Haiku 4.5 はサンプリングされた 12 件の Issue に対して 0 件しか解決できませんでした。
GEPA を Flex プログラムに適用し、max_metric_calls を控えめな 60 に設定して実験を行いました。この最適化されたプログラムは、Python と LLM の呼び出しを組み合わせるソフトウェアエンジニアリングのワークフロー(調査、ドラフト作成、評価、修正、最終回答の提出)を設計した結果、12 の問題のうち 4 つを解決しました。自己生成された指示には、特定の失敗モードを防ぐガードも含まれています。
この実験はパイロット版であり、そのように捉えてください。しかし、成熟した手作業で構築されたハネス内で Haiku が報告する 39% という成果に対し、わずか数回のターンでハネスが進化し、12 の問題のうち 4 つを解決する様子は印象的です。
現在、私たちのハネスの多くは一度作成され、問題が発生した際に修正されるものです。Flex を用いれば、新しいデータ、モデル、戦術が登場するたびに、コードとして記述すべき部分と LM に任せるべき部分のバランスを取りながら、ハネスを継続的にコンパイルできます。プロダクトビルダーとして、コスト、レイテンシ、精度の最適なバランスを見つけるために、モデルやメトリクスをスキャンし微調整することが可能になります。
GEPA がさまざまなタスクでプログラムを書き換える様子を見守る中で、4 つの動きが繰り返し現れています:
- 分解。タスクにステップ(解析、正規化、比較、決定など)があることに気づき、各ステップに独自の実装を与えること。
- メソッド選択。各ステップに対して、決定的なコードとモデル呼び出しのどちらを選ぶかを選択し、呼び出しが必要な場合は適切なモジュール(Predict、ChainOfThought、RLM など)を選定すること。
- ルーティング。異なる *入力* が異なるタスクであることを認識し、明確なケースは安価なパスへ、曖昧なケースは判定者へ振り分けること。
進化。構造が安定した後は、その中身を洗練させるフェーズです。分解された各部分のシグネチャや指示、そしてコード自体を改善していきます。
手書きで構築したハルネスも同様の作業を行いますが、新しいモデルやデータセット、戦法が登場しても、自ら進化するわけではありません。特定のタイミングで再作成しない限り、変化に対応できません。しかし現在、モデルはプログラミングにおいて十分な能力を持っています。より多くのデータを収集し、指標を改善し、モデルの性能が向上するにつれて、ハルネスも継続的にコンパイル(更新)することが可能になりました。
背景と由来
Flex は、いくつかの研究分野の成果の上に構築されています。
- GEPA。カリフォルニア大学バークレー校 Sky Computing Lab の Lakshya Agrawal 氏らのチームによる、実行トレースやメトリクスフィードバックを参照しながらプロンプトを進化させる手法です。この研究では、モデルがこれらの情報を活用することで、ロールアウト効率において強化学習を上回る最適化が可能であることを示しました。
- Meta-Harness。スタンフォード大学の Yoonho Lee 氏らのチームによる研究で、モデルを取り巻くハルネス自体を学習可能なオブジェクトとして扱うアプローチです。
- RLM(Recursive Language Models)。MIT の Alex Zhang 氏らのチームが取り組む再帰型言語モデルの分野では、モデルが入力全体を一括処理するのではなく、プログラム的に大規模な入力を探索します。Flex の最適化プロセスにおいて、特定のステップで必要となった際に利用可能なプリミティブの一つです。
原文を表示
Models have become excellent programmers. Flex hands them the program itself, so the optimizer rewrites your code and not just your prompt.
*This is a guest post by Michael Isaac, a PhD student in software engineering at Carnegie Mellon University, written during his internship at cmpnd. Michael implemented Flex, the module this post introduces, for DSPy.*
The core position of DSPy is that you can define a task once, in a way that lets it be re-implemented as the AI ecosystem advances. The history of these re-implementations can be understood as a history of the models; of weaknesses we worked around and strengths we leveraged:
- In 2022, models needed to be shown what a task looked like, so optimizers like BootstrapFewShot automated the picking of few-shot examples.
- Models then grew to be capable prompt authors, so optimizers like MIPROv2 and GEPA could improve programs by rewriting their instructions.
- Lately, models have become excellent programmers.
This week, we're introducing Flex to DSPy, which leverages the coding skills of models to rewrite not just the instructions of your program, but the code itself.
Flex Lets GEPA Optimize the Code
dspy.Flex(YourSignature) is a DSPy module, which can be dropped into any of your existing Predict, ReAct, or RLM programs. For example:
my_signature = "question -> answer"my_program = dspy.Predict(my_signature)# Make it Flex!my_program = dspy.Flex(my_signature)If we run either of these programs, we'd get the same result. Prior to optimization, Flex is just a Predict module (or RLM if you provide tools).
What makes Flex different is what it exposes to an optimizer: Flex exposes its code, in addition to its instructions. Hand a Flex module to dspy.GEPA and the reflection model might decompose your program, write helper functions, implement routing logic, *and* rewrite your prompts. The output is an optimized program that performs best against the metric you gave it.
This is how you'd optimize a Flex module with GEPA. SamePlace here is the signature for the location conflation task we'll walk through in the next section:
program = dspy.Flex(SamePlace) # was: dspy.Predict(SamePlace)# cheap LM to use during inferencedspy.configure(lm=dspy.LM("anthropic/claude-haiku-4-5"))# big LM to write the code and instructionsbig_lm = dspy.LM("anthropic/claude-opus-5")optimized = dspy.GEPA( metric=make_metric(penalty=0.2), reflection_lm=big_lm, max_metric_calls=400,).compile(program, trainset=train, valset=val)After optimization, optimized.save("program.json") persists the source and dspy.Flex(SamePlace).load(...) restores it. The artifact is a file you can open, read, diff, and reason about.
What you get back is a program the reflection model wrote to score as high as it can against your metric. Two things tend to follow. Sometimes it doesn't call the model at all, because it found a case it could settle in code. And when it does call, the call is better aimed, because the module has already done the parsing and comparison and hands the model a narrower question. Fewer calls, better calls, and a program that outperforms the one you handed it.
Code written by a model is still untrusted code, so by default it never runs in your process. Flex executes the generated source inside a sandboxed interpreter. Only predictor calls and the tools you explicitly provided bridge back to the host process, and a max_predictor_calls cap bounds how many times per forward that bridge can be crossed.
Location Conflation Task
Last year, Drew demonstrated prompt optimization at the Data + AI Summit with a geospatial conflation task: given two place listings, decide whether they're the same physical place. It's deceptively hard in the tail. KIN CAFE and KIN at the same address are the same place. CONCESSION #2 KEN MERCER SPORTS PARK and KEN MERCER SPORTS PARK at the same address are not.
We replaced Predict with Flex and ran GEPA on this task: 1,029 labeled pairs, evaluated on 240 held-out records (class-balanced, so 50% is chance). Caches were disabled throughout, so the cost and latency figures below are what cold production traffic would pay.
The baseline is the original dspy.Predict, one model call per record: 90.4% accuracy at $0.98 per thousand records.
Optimizing only the prompt with GEPA, no Flex, lifts accuracy to 92.5%. But the only lever a prompt optimizer has is the instruction, so it wrote a much longer one, and every record pays for those extra tokens at inference: $2.88 per thousand records, 2.9x the baseline cost, and 48% slower.
Flex gives the optimizer a second lever: the module code. Running GEPA, unchanged, on the Flex program lifted accuracy from 90.4% to 95.0% at a cost of $0.70 per thousand records. By optimizing the prompt *and* the code, Flex produced a program that is 28% cheaper and 40% faster than the baseline.
How is this possible? For one, many of the places being compared can be evaluated using only code. Our reflection model wrote code to identify and route these easy matches to plain Python functions, resulting in *75% fewer LLM calls*.
We can lean into this behavior by updating our metric. A GEPA metric returns a score plus natural-language feedback. With Flex, it can also see how many LM calls the generated program made on each record. We can use this value to penalize our feedback:
score = max(0.0, correct - PENALTY * n_llm_calls)At PENALTY = 0, calls are free, and the optimizer chases accuracy alone. As the penalty (λ from here on) rises, every LM call has to buy back more accuracy than it costs, and the optimizer is pushed to settle cases in Python and reserve the model for genuine ambiguity. (Past λ = 1.0, a call can never pay for itself; that end of the dial means never calling the model.) We swept λ across 0, 0.05, 0.1, 0.2, and 0.4, with Haiku 4.5 running the programs and Opus 5 rewriting them as the reflection model. Haiku is the weakest, cheapest Claude, which is what makes the penalty an interesting tradeoff.
Cost vs accuracy, as the LLM-call penalty rises
baseline
plain GEPA (prompt-only)
Flex + GEPA
*execution claude-haiku-4-5 · reflection claude-opus-5 · n = 240 held-out records · error bars are 95% Wilson CIs*
| program | λ | accuracy | LM calls / record | $ / 1k records | mean latency* |
|---|---|---|---|---|---|
| dspy.Predict baseline | n/a | 90.4% | 1.00 | $0.98 | 1,924 ms |
| GEPA, prompt-only | n/a | 92.5% | 1.00 | $2.88 | 2,841 ms |
| Flex + GEPA | 0 | 95.0% | 0.25 | $0.70 | 1,155 ms |
| Flex + GEPA | 0.05 | 94.6% | 0.17 | $0.45 | 726 ms |
| Flex + GEPA | 0.1 | 90.8% | 0.07 | $0.18 | 347 ms |
| Flex + GEPA | 0.2 | 91.7% | 0.08 | $0.09 | 135 ms |
| Flex + GEPA | 0.4 | 92.1% | 0.004 | $0.01 | 65 ms |
**Mean per-request latency under 8-way concurrency.*
Even with calls free (λ=0), the optimizer wrote code. The metric function scored on accuracy only. The only nudge was in the metric's textual feedback asking for cases to be settled in code where possible. The best program it found routed 75% of records through deterministic Python and came out *more accurate* than calling the model every time at 95.0% vs 90.4% (McNemar p=0.019), while being faster and cheaper. The rules handle the easy cases better than a small model does, and the model only sees the cases that actually need judgment.
At λ=0.4, the program called the model once across 240 records. Accuracy held at 92.1%, statistically indistinguishable from the always-call baseline, at roughly a hundredth of the cost and a thirtieth of the latency. The other high penalties land the same way: accuracy at parity with the baseline for a fraction of the price, a trade most production systems would take.
Reading the Code It Wrote
At λ=0.4, the program holds about two hundred lines of Python code, written by the reflection model. Condensed to its skeleton:
class SamePlaceModule(dspy.Module):def __init__(self):super().__init__()# The LLM is a LAST-RESORT fallback: it is only consulted for# the narrow band of pairs where the deterministic signals# genuinely conflict (e.g. clearly the same brand/name but at# a mismatching house number and a middling distance).self.judge = dspy.Predict(dspy.Signature("input_name: str, input_address: str, ""match_name: str, match_address: str, ""distance: float, name_similarity: float, ""address_analysis: str -> is_same: bool","You perform entity resolution on business/POI listings. [...] ""3. The house number is the strongest address signal. [...] ""4. The same brand name far apart means two different ""branches => NOT the same place." ))def forward(self, **inputs):import re, difflib# ~150 lines of helpers: normalize names (strip '#30696',# 'LLC', generic words like CAFE/GRILL), parse addresses into# house number + street core, compute fuzzy similarity over# the distinctive tokens... name_same = (nsim >= 0.87) or (containment and shared_len >= 5) name_diff = (not name_same) and (nsim < 0.62) decision = Noneif name_same:if addr_same and (d is None or d <= 400.0): decision = True # same name, same address, close byelif hn_diff and d is not None and d > 120.0: decision = False # different house numbers => brancheselif d is not None and d > 500.0: decision = False # far apart => different branches# ...elif name_diff: decision = False # distinctive name parts disagreeelse:# gray zone (nsim 0.62–0.87): let the address decideif addr_same and (d is None or d <= 150.0): decision = Trueelif hn_diff or (d is not None and d > 200.0): decision = False# ...if decision is None: # rules can't decide -> ask the model out = self.judge(**inputs, name_similarity=round(nsim, 3), address_analysis=analysis) decision = to_bool(out.is_same)return dspy.Prediction(is_same=bool(decision))The comment at the top, "the LLM is a LAST-RESORT fallback," was written by the optimizer about its own architecture. The algorithm underneath it has three stages:
- Normalize: Names are uppercased, stripped of franchise numbers ("#30696"), legal suffixes (LLC, INC), punctuation, and some forty generic business words (CAFE, RESTAURANT, MARKET, GRILL...). What remains is the distinctive part of the name: for KIN CAFE, just KIN. Addresses also get parsed into a house number and a street core, discarding unit designators (STE 4, APT B) and street-type words, so that AVE versus AVENUE can never cause a mismatch.
- Compare: The distinctive name tokens are scored zero to one with a fuzzy similarity, and binned into three buckets: confident matches, confident misses, and unsure. Addresses are compared piece by piece, with house numbers treated separately from street names, because the reflection model determined that the house number alone is a strong signal in the data.
- Decide: Each bucket gets its own rules combining the name verdict, the address data, and the distance between the two geocoded points. For example, if the names and address match and they're within 400 meters: same place. If they're the same name but different house numbers more than 120 meters apart: two branches of one brand.
Only when *none* of the "Decide" rules fire, for example the same brand name at a mismatched house number and a middling distance, does the record go to the model. And it doesn't go alone: the module forwards its own analysis as extra input fields (the parsed house numbers, street cores, and similarity scores) and the judge's instructions distill what it learned into a handful of numbered domain rules. At λ=0.4, this fallback fired once in 240 records.
Deterministic or Stochastic? Let the Metric Decide
Everyone building with AI keeps re-answering the same question: what do you give to code and what do you give to the model? It's the same instinct you see in coding agents, which increasingly write a Python script to do a job rather than doing it token by token.
With Flex, we can let a model explore this space as it gets feedback from the student model and the metric. It can get quite sophisticated. For example, we pointed Flex and GEPA at SWE-bench Pro, a coding benchmark comprised of Github Issues. Given the high-level task of reading an issue and generating a fix, and a few tools, Haiku 4.5 resolved 0 out of 12 sampled issues.
We then ran GEPA on our Flex program, with max_metric_calls set to a modest 60. This optimized program resolved 4 out of 12 problems, after designing a software engineering workflow that mixed Python and LLM calls to research, draft, evaluate, repair, and submit a final answer. Its self-written instructions included guards against specific failure modes.
This experiment is a pilot and should be read as one. But it is striking to watch a harness evolve in a handful of turns to 4 of 12, against the 39% Haiku is reported to reach inside a mature, hand-built harness.
Today, most of our harnesses are written once and edited as issues arise. With Flex, we can continually compile our harness, balancing what's written as code and what's handed off to an LM, as new data, models, and tactics arrive. As product builders, we can sweep models and tweak our metrics to find the optimal balance between cost, latency, and accuracy for our applications.
As we've watched GEPA rewrite programs across many types of tasks, four moves keep showing up:
- Decomposition. Noticing that a task has steps (parse, normalize, compare, decide) and giving each step its own implementation.
- Method selection. Choosing, for each step, between deterministic code and a model call, and picking the right module (Predict, ChainOfThought, RLM) when it's a call.
- Routing. Recognizing that different inputs are different tasks: clear cases down the cheap path, ambiguous ones to the judge.
- Evolution. Once the structure settles, refining what's inside it: the signatures and instructions of the decomposed parts, and the code itself.
Hand-written harnesses make these moves too. But they don't evolve with new models, datasets, or tactics unless you specifically go back and rewrite them. The models are good enough at programming that we can now continually compile our harnesses as we get more data, better metrics, and better models.
Where This Comes From
Flex builds on a few research threads:
- GEPA. Lakshya Agrawal and team's reflective prompt evolution, from the Sky Computing Lab at UC Berkeley. It showed that a model reading execution traces and metric feedback can out-optimize reinforcement learning on rollout efficiency.
- Meta-Harness. Yoonho Lee and team's work out of Stanford on treating the harness around a model as a learnable object.
- RLM. Alex Zhang and team's work on Recursive Language Models at MIT, where a model explores a large input programmatically instead of swallowing it whole. RLM is one of the primitives Flex's optimizer can reach for when a step needs it.
関連記事
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み