8月14日、SD Timesが「Elixir, Clojure, or Python for LLM Agents? Our Experience with All Three」と題した記事を公開した。この記事では、LLMエージェントの実装言語としてPython・Elixir・Clojureを実際に使い比べた経験をもとに、各言語の特性と適切な使いどころを解説している。「とりあえずPython」という選択が本当に正しいのか、現場の視点から問い直す内容だ。
LLMエージェントの実装といえば、LangChain・AutoGen・CrewAI・LangGraphのようなPython-firstなフレームワークが市場を席巻している。しかし、JVMやErlang/OTPをすでに本番で動かしている組織にとっては「エージェントだけPythonに移すか、それとも使い慣れたランタイムで実装するか」という現実的な選択が迫られる。
この記事を書いたのは、関数型プログラミングのElixir・Clojureを業務で使うチームだ。「週次アクティブユーザー数をDBから取得し、必要に応じてグラフを生成する」シンプルな分析エージェントを三言語で実装しながら比較している。
Pythonの実装:フレームワークあり・なしで設計思想が変わる
Pythonはフレームワーク(LangChain等)を使う場合と、素のコードで書く場合で性格が大きく異なる。
LangChain版は initialize_agent の中にループが隠蔽され、状態やトレースはフレームワークのAPIを通じてしか見えない。テストでフレームワーク内部をモックする必要が生じるのはこの構造による。
フレームワークなし版では、ツールも状態も辞書(dict)で表現する。たとえばツール定義を {"name": "run_sql", "description": "...", "fn": run_sql} のようなdictとして持ち、エージェントループ内でLLMのレスポンスを受け取ってdispatchする構造になる。制御フローが見える分、テストはClojure版と同じ感覚で書ける。ただし、Pythonのミュータブルなデータ構造は「ツール関数が参照経由で state を書き換えても、トレースに残らない」という問題をはらむ。記事はこれを「言語の欠陥ではなく、管理すべき特性」と表現している。
Clojureの強み:状態が不変なので、エージェントの挙動を後から追える
Clojureの実装で最も重要な特性はイミュータブルな状態管理だ。
;; 実際の実装では decision の取得・tool-def の解決なども含む。
;; ここでは構造把握のため主要な変換フローを示している
(defn run-agent-once [state config]
(let [decision (llm/call-llm-with-tools ...)] ; LLMにツール付きで問い合わせ
(case (:type decision)
:tool-call
(let [result ((:run tool-def) params')] ; ツールを実行して結果を取得
{:state (append-tool-result state tool params' result)
:done? false}))))
各イテレーションは「古いstateを受け取り、新しいstateを返す」純粋な変換だ。前のstateは変更されない。これにより以下が可能になる:
- 2つのstateをdiffして、そのイテレーションで何が変わったかを確認できる
- stateをEDNにシリアライズして保存し、実行を後からリプレイできる
- 開発中はREPLで
run-agent-onceにキャプチャしたstateを渡し、手動でステップ実行できる
ツール定義にはMalliを使う。スキーマがクラスやデコレーターではなくデータ構造として表現されるため、LLM APIが期待するJSON形式への変換がプログラマティックに行える。
テストもシンプルだ。
(deftest agent-produces-trace
(let [state (core/run-agent "How many active users?" config)]
(is (= 1 (count (:trace state))))
(is (= "run_sql" (-> state :trace first :tool)))))
関数を呼び出して返ってきたマップをassertするだけ。モックライブラリは不要だ。
Elixirの強み:並列処理とフォルト・トレランスがランタイムに組み込まれている
ElixirはActorモデルを採用し、各エージェントを独立したプロセス(GenServer)として動かす。
defmodule AnalyticsAgent do
use GenServer
def handle_call({:ask, question}, _from, state) do
{result, new_state} = run_loop(state, max_steps: 8)
{:reply, result, new_state}
end
end
プロセスはメモリ数KBと軽量で、1台のマシンで数百万プロセスを動かせる。複数のエージェントを並列実行するために特別なセットアップは不要で、プロセスを起動するだけで済む。
障害対応も組み込み済みだ。Supervisorを定義すれば、エージェントプロセスがクラッシュしても(LLMの不正出力、APIタイムアウト、ツール結果の破損など)自動で再起動し、他のエージェントには影響しない。
defmodule AgentSupervisor do
use Supervisor
def init(_opts) do
children = [
{AnalyticsAgent, name: :analytics},
{CodeGenAgent, name: :codegen},
{ReviewAgent, name: :review}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
このSupervision Treeはエージェント専用に発明されたものではなく、1980年代からErlang/OTPが本番システムで使ってきた仕組みをそのまま適用したものだ。
分散についても、Elixirはプロセス間のメッセージパッシングが同一マシン上でも別マシン上でも同一のコードで動く。PythonがKubernetes・Celery・Rayといった外部インフラを必要とするのとは対照的だ。
機械学習・推論まわりのエコシステムとしては、数値計算ライブラリのNx、モデル推論を担うBumblebee、LLMの構造化出力を扱うInstructorといったライブラリが整備されており、Python依存なしにML周辺の処理をElixir内で完結させる素地が育ちつつある。
三言語の使い分け
| 観点 | Python | Clojure | Elixir |
|---|---|---|---|
| 並列処理 | asyncio(GIL制約あり)、大規模はRay/Celery | JVMスレッド・core.async等で対応 | BEAMの軽量プロセスで標準対応 |
| 状態管理 | ミュータブル、追跡は規律次第 | イミュータブル、diff・リプレイ可 | プロセス分離、外部から直接参照不可 |
| 障害対応 | try/except、ライブラリ依存 | JVM例外処理、スーパービジョンは外部ライブラリ | Supervision Treeがランタイム組み込み |
| エコシステム | 最大。全主要プロバイダーがSDK提供 | 小さい。JVM経由でJavaライブラリは使える | 成長中。Nx・Bumblebee・Instructorあり |
記事の結論として、三言語は排他的ではないとしている。プロンプトやツール設計の試行錯誤はPythonで素早く行い、本番のオーケストレーション層はElixir(並列性・障害耐性重視)またはClojure(追跡性・監査重視)で実装するという組み合わせも現実的な選択肢だ。
詳細はElixir, Clojure, or Python for LLM Agents? Our Experience with All Threeを参照していただきたい。