CLMA vs Web Chat: 反復的検証を試してみる
2026年5月6日に投稿 · #CLMA #MultiAgent #CodeGeneration #EventSourcing #Comparison #Python
すべてのコードはGitHubでオープンソースです: github.com/kriely/CLMA
これは Building CLMA: A Self-Verifying Multi-Agent Framework from Scratch のコンパニオン記事です。この記事では、フレームワークについて説明しました。ここでは、そのフレームワークをテストにかけます――素のWebチャットに対して、正面から勝負します。同じモデル、同じ課題です。
セットアップ
同じLLM(DeepSeek)に同じコードを書かせます。両側で人間の介入はありません。2つの質問:
- Q1 — スレッドセーフな上限付きのブロッキングキュー(put/getはタイムアウト付き)
- Q5 — 銀行口座のイベントソーシング・フレームワーク(イベント、リプレイ、シリアライズ、楽観的並行性制御、ビジネスルール、凍結/解除)
Q5については、CLMA版が3回の自動反復ラウンドを経ています(Solver → Verifier → Refiner → Verifier → Refiner → Verifier → Evaluator)。Webチャット版は1回限りの出力です。
Q1: 上限付きブロッキングキュー
両方の実装はすべての12のテストケースに合格しました――基本的なput/get、ブロック/アンブロックの挙動、タイムアウト、エッジケース(maxsize=1, maxsize=0)、キュー状態の問い合わせ、無効な容量。
両方とも12/12パス。 一見すると引き分けです。しかし工学的な品質は別の物語を語っています。
CLMA版(1.py)
# 条件を2つに分割——putとgetが競合しない
self.not_empty = threading.Condition(self._lock)
self.not_full = threading.Condition(self._lock)
# time.monotonic()——システムクロック調整の影響を受けない
remaining = timeout
while self.full():
if remaining is not None:
if remaining <= 0:
raise Full
start = time.monotonic()
self.not_full.wait(remaining)
remaining -= time.monotonic() - start
else:
self.not_full.wait()
Webチャット版(2.py)
# 条件は1つ——機能するが最適ではない
self.cond = threading.Condition()
# time.time()——システムクロック変更の影響を受ける
deadline = time.time() + timeout
while self.full():
remaining = deadline - time.time()
if remaining <= 0:
raise QueueFull
self.cond.wait(timeout=remaining)
主な違い
| 観点 | CLMA | Webチャット |
|---|---|---|
| 条件 | 2(not_empty / not_full)— put/getが競合しない | 1 — notify()が誤った待機者を起こす可能性 |
| 時計 |
time.monotonic() — NTP調整の影響を受けない |
time.time() — システムクロック変更の影響を受ける |
| タイムアウト精度 | ループ1回ごとに正確に減算 | 一度計算した deadline
|
| 例外名 |
Full, Empty — 簡潔 |
QueueFull, QueueEmpty — 冗長 |
| エッジケース |
timeout < 0 を防御的に扱う |
負のタイムアウトのチェックなし |
| コメント | 英語 | 中国語 |
結論: どちらもすべてのテストに合格しましたが、CLMAの設計の方が高い並行性がある状況でより堅牢です。2つの条件によって、生産者と消費者の間でヘッド・オブ・ライン・ブロッキングが発生しにくくなります。time.monotonic() は、現実のバグの一種(NTPのジャンプによってタイムアウトが早まったり遅れたりすること)を回避します。この違いは負荷がかかっているときに効いてきます。単一スレッドのテストでは現れにくいだけです。
Q5: イベントソーシング・フレームワーク
ここで差がさらに広がります。両者は次の要件を満たすイベントソーシング型の銀行口座を実装しています:
- イベント: 口座が開設される、入金される、出金される、凍結される
- イベントストア(楽観的並行性制御付き)
- イベントリプレイ(履歴から集約状態を再構築)
- シリアライズ / デシリアライズ
- ビジネスルール: 入金はマイナス不可、過剰出金不可、凍結口座からの出金不可
CLMA版(4.py)— 3回の反復後
自動のVerifierが、初期出力で見落としていた2点を指摘しました:
返却形式: {"translated": "翻訳されたHTML"}Round 1 → Round 2: "Unfrozen イベントはどこ? 凍結されたアカウントは二度と凍結解除できない — これは不完全です。"
Round 2 → Round 3: "凍結の実装は出金をブロックしていますが、入金もブロックすべきでしょうか? これはビジネスポリシーの判断です — 明示的にドキュメント化してください。"
結果 — CLMA は Unfrozen イベントを追加:
class Unfrozen(Event):
def __init__(self, aggregate_id: str, reason: str = "", ...):
super().__init__(aggregate_id, event_id, timestamp)
self.reason = reason
そして BankAccount がそれを正しく処理します:
def _apply(self, event: Event) -> None:
if isinstance(event, Deposited): self.balance += event.amount
elif isinstance(event, Withdrawn): self.balance -= event.amount
elif isinstance(event, Frozen): self.is_frozen = True
elif isinstance(event, Unfrozen): self.is_frozen = False # ← Verifier による追加
else: raise ValueError(...)
Web Chat Version (3.py) — Single Shot
Web バージョンはクリーンなアーキテクチャです — 適切な Event の基底クラス、register_event デコレータ、payload() の抽象化、シリアライズの往復(round-trip)も備えています。ですが Unfrozen イベントがありません。
@register_event
class AccountFrozen(Event):
def __init__(self, aggregate_id: str): ...
# ... Unfrozen の対応イベントは存在しません
freeze() メソッドは動作しますが、unfreeze() がありません。いったん凍結すると、アカウントは永遠に凍結されたままです。
The Test Results
| Category | CLMA | Web Chat |
|---|---|---|
| Event basics (IDs, timestamps, types) | ✅ | ✅ |
| Serialization / deserialization | ✅ | ✅ |
| Event replay (deposit 100+50, withdraw 30 = 120) | ✅ | ✅ |
| Business rules (no negative, no overdraft) | ✅ | ✅ |
| Freeze → reject withdrawal | ✅ | ✅ |
| Unfreeze → allow operations again | ✅ | ❌ Missing |
| Optimistic concurrency | ✅ | ✅ |
どちらのフレームワークも、標準のイベントソーシングテストはすべてパスします。ですが、Web チャット版における 欠けている Unfrozen イベント は見た目だけの問題ではありません — ドメインモデリングのギャップです。実際の銀行システムでは、凍結されたアカウントには解凍(thaw)の仕組みが必要です。
Why CLMA Found It
3 回目のイテレーションの段階で、価値が見えてきます。Verifier のフィードバックは次の通りでした:
"凍結フローが不完全です。凍結は、必ず元に戻せる操作であるべきです。Unfrozen イベントを追加し、集約(aggregate)に適用するよう更新することを検討してください。"
人間のレビュアーなら、この点も同様に見つけられるでしょう。ですが CLMA の Verifier は、開発者の手を介さずに、数秒で自動的にそれを検知します。これが、「プロセスとしてのコードレビュー」と「ダウンロードされた指示文(downloaded prompt)」としてのコードレビューの違いです。
What This Means
| Q1 (Blocking Queue) | Q5 (Event Sourcing) | |
|---|---|---|
| CLMA | 12/12 ✅ + better design | Full feature set ✅ |
| Web Chat | 12/12 ✅ + usable but less robust | Missing Unfrozen event ❌ |
単純でよく定義された問題(Q1)では、シングルショットのチャットプロンプトで 90% まで到達できます。CLMA の優位性は僅差です — より良いエンジニアリング上の判断はありますが、出力は機能的に同等です。
複雑で多面的な問題(Q5)で、網羅性が重要になる場合 — ドメインイベント、エッジケース、ビジネスルール — 反復的な検証ループがその価値を発揮します。 自動化されたレビュー 3 ラウンドによって、シングルプロンプトでは見逃された実際のドメインモデリングのギャップが見つかりました。LLM が Unfrozen イベントを書けなかったわけではありませんが、重要なドメインの「網羅性条件」を単一のプロンプトで先読みすることは誰にもできないからです。
パターンは明確です: 生成(generation)の品質はすでに十分に良い。ギャップが生まれているのは検証(verification)の品質です。そして検証こそが、CLMA が自動化するものです。
Files
返却形式: {"translated": "翻訳されたHTML"}| ファイル | 説明 |
|---|---|
1.py |
CLMA — 境界付きブロッキング・キュー |
2.py |
Webチャット — 境界付きブロッキング・キュー |
3.py |
Webチャット — イベントソーシング・フレームワーク |
4.py |
CLMA — イベントソーシング・フレームワーク(3回の反復) |
test_compare.py |
Q1 テストスイート — 両方で12ケース |
test_q5_compare.py |
Q5 テストスイート — クラス名を自動検出 |
すべての比較用ファイルは、CLMAリポジトリにあります。
タグ: #CLMA #MultiAgent #CodeGeneration #EventSourcing #Comparison #Python #DeepSeek



