AI時代の最適言語はRustでもGoでもなく、Elixirでは?

ElixirVibeCodingRustGoプログラミング

AIがコードを書く時代になった。

GitHub Copilot、Cursor、Claude Code——もはやLLMにコードを生成させることは日常だ。しかし、AIが書いたコードをそのまま本番に出せるだろうか? 出せるわけがない。

2026年のソフトウェア開発で避けて通れないのがハーネスエンジニアリングガードレールの設計だ。AIが生成したコードを安全に動かすための制約、監視、自動復旧の仕組み。これらをアプリケーション層で後付けするのか、それとも言語とランタイムが最初から提供してくれるのか——この違いが、LLM時代の言語選択を根本から変えると私は考えている。

そして、言語自体がハーネスであり、ランタイム自体がガードレールである言語が一つある。Elixirだ。

なぜ「ハーネスとしての言語」が重要なのか

LLMがコードを書く時代に求められるのは、大きく分けて3つのガードレールだ。

1. AIが生成するコードに対するガードレール

  • AIが書いたコードが暴走しない仕組み
  • 想定外の動作をしたときの自動復旧
  • プロセス単位の隔離による障害の封じ込め

2. AIの出力(推論結果)に対するガードレール

  • LLM APIの不安定さ(タイムアウト、レート制限、ハルシネーション)への耐性
  • 構造化されていない出力の安全なパース
  • 大量の並行リクエストの制御

3. AIが生成したコードをマージする前のガードレール

  • CIパイプラインでのテスト・静的解析の自動実行
  • pre-commitフックによるフォーマット・型チェックの強制
  • コンパイルが通るかどうかという最も基本的な検証
  • AIが書いたコードも人間が書いたコードも、同じ品質ゲートを通過させる仕組み

1と2はランタイムの話だが、3はデプロイ前の防波堤だ。AIが自信満々に生成したコードがテストを通らない、型が合わない——これをCIが機械的に弾くことで、ハルシネーションが本番に到達するリスクを断つ。

特に「コンパイルが通るか」は、AI時代においてその重要性が増している。人間がコードを書いていた時代、コンパイルエラーは単なるタイポや凡ミスの検出だった。しかしLLMは存在しないモジュールをimportし、廃止されたAPIを呼び、引数の数を間違える。コンパイラは、こうしたもっともらしいが根本的に壊れたコードを実行前に弾く最初の防衛線だ。Pythonのようなインタプリタ言語では、このエラーが本番のリクエスト処理中に初めて発覚する。明示的なコンパイルステップを持つElixirでは、mix compile --warnings-as-errors の一発でこれを防げる。

従来のアプローチでは、これらのガードレールをライブラリやフレームワークで後付けする。リトライライブラリ、サーキットブレーカー、プロセスマネージャ、ヘルスチェック……。

Elixirでは、これらが言語とBEAM VMに組み込まれている

Elixirのガードレールが刺さる5つの理由

1. プロセス隔離 ── AIが書いたコードを安全に走らせる箱

BEAMの軽量プロセスは、1プロセスあたり数百バイトのメモリで完全に隔離された実行環境を提供する。

AIが生成したコードをプロセスに閉じ込めれば、そのコードがクラッシュしてもシステム全体には影響しない。これは言語レベルのサンドボックスだ。

# AIが生成したコードを隔離して実行
Task.Supervisor.async_nolink(MyApp.TaskSupervisor, fn ->
  # この中で何が起きても、親プロセスは無事
  ai_generated_module.run(input)
end)

Goのgoroutineにはこの隔離がない。1つのgoroutineのpanicは、recoverしなければプロセス全体を道連れにする。Rustはメモリ安全性は保証するが、ランタイムレベルの障害隔離は言語の責務外だ。

2. Supervisor Tree ── クラッシュを前提にした自動復旧

「Let it crash」はElixirの哲学だが、LLM時代にはこれが設計原則としての正しさを持つ。

LLM APIは落ちる。AIが生成したコードはバグを含む。これらは例外ではなく前提だ。Elixirでは、クラッシュしたプロセスをSupervisorが自動で再起動する。開発者はリトライロジックを書く代わりに、クラッシュしても大丈夫な構造を宣言する。

# LLMワーカーの監視ツリー
children = [
  {LLM.ConnectionPool, pool_size: 10},
  {LLM.RateLimiter, provider: :anthropic, rpm: 1000},
  {LLM.StreamManager, max_concurrent: 100}
]

Supervisor.start_link(children, strategy: :one_for_one)
# → どれかが死んでも、そいつだけ再起動される

Rustとの比較: Result<T, E>? 演算子は美しい。だが、LLMの「何が返ってくるかわからない」性質に対して、すべてのエラーパスを型で事前定義するのはハーネスではなく拘束衣だ。

Goとの比較: if err != nil を10回書いたところで、それはガードレールではなくボイラープレートだ。本来のロジックがエラーハンドリングに埋もれていく。Goで同等の障害隔離を実現しようとすると、こうなる:

// Goで「クラッシュしても大丈夫な構造」を自前で書くと…
func supervise(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            func() {
                defer func() {
                    if r := recover(); r != nil {
                        log.Printf("worker crashed: %v", r)
                        // リトライ間隔の計算も自前
                        time.Sleep(backoff())
                    }
                }()
                if err := runLLMWorker(ctx); err != nil {
                    log.Printf("worker error: %v", err)
                    // このエラーハンドリングのエラーハンドリングは?
                }
            }()
        }
    }
}

各goroutineに defer recover() を仕込み、エラーチャネルで親に通知し、再起動ロジックを自前で組み、さらにそのリトライ自体のエラーハンドリングも必要になる。Elixirでは5行のSupervisor宣言で済むことが、Goでは防御コードが本来のロジックを覆い尽くす。失敗パターンを事前に網羅できないLLM統合において、Goの「エラーは値」という明示性はそのままコストになる。

3. パターンマッチング ── LLM出力の宣言的バリデーション

LLMの出力は予測不能だ。JSONで返ってくることもあれば、壊れたJSONかもしれない。function callingの結果かもしれない。エラーかもしれない。

Elixirのパターンマッチングは、この不確実な出力に対する宣言的なガードレールとして機能する。

# LLMの出力を形で分岐する。if文のネストは不要
def handle({:ok, %{"tool_calls" => [%{"name" => name, "args" => args} | _]}}) do
  execute_tool(name, args)
end

def handle({:ok, %{"content" => text}}) when is_binary(text) do
  {:text, text}
end

def handle({:error, %{status: 429}}) do
  {:retry, :rate_limited}
end

def handle({:error, _} = error) do
  {:fail, error}
end

データのが合わなければ、その関数節には入らない。これはコンパイル時に近い安全性をランタイムで提供するガードレールだ。

Goとの比較: response["choices"].([]interface{})[0].(map[string]interface{})["message"] ——型アサーションの連鎖は、ガードレールどころかロープなしのロッククライミングだ。

4. パイプ演算子 ── 処理フローの可視化というガードレール

AIが生成したコードをレビューするとき、処理の流れが一目でわかることはそれ自体がガードレールだ。

user_input
|> sanitize()
|> build_prompt(context)
|> call_llm(model: "claude-4")
|> parse_response()
|> validate_output(schema)
|> format_for_ui()

上から下に読むだけで、データがどう変換されていくかわかる。AIが書いたコードでも、この構造なら何が起きているか人間が検証できる

Rustでは所有権の受け渡しが、Goでは err チェックの挿入が、このクリーンな流れを壊してしまう。

5. ホットコードリロード ── プロンプト調整の即時反映

BEAM VMは本番稼働中にコードを差し替えられる。プロンプトテンプレートの微調整を再起動なしで即座に反映できる。接続中のユーザーに影響はない。

LLM時代のプロンプトエンジニアリングでは、微調整→確認→微調整のサイクルが高速であるほど良い。ElixirのLivebookを使えば、インタラクティブにプロンプトを実験し、うまくいったコードをそのままプロダクションに持っていける。

これもまた、「安全に変更を加えられる」というガードレールだ。

「でもElixirって…」への回答

「動的型付けでは?」

これは正当な懸念だ。Elixirは動的型付け言語であり、Rustのようなコンパイル時の型安全性はない。

ただし、Elixirには@spec(型仕様)とDialyzer(静的解析ツール)がある。プロジェクトで@specの記述を必須ルールにすれば、動的型付けのリスクをかなり抑えられる。

@spec call_llm(prompt :: String.t(), opts :: keyword()) ::
        {:ok, LLM.Response.t()} | {:error, LLM.Error.t()}
def call_llm(prompt, opts \\ []) do
  # Dialyzerが型の不整合を検出してくれる
end

さらに、パターンマッチング自体がランタイム型チェックとして機能する。データの形が合わなければマッチしない。これはRustの型システムほど厳密ではないが、LLMの「何が返ってくるかわからない」出力に対しては、コンパイル時に型を確定させようとするよりも現実的なアプローチだとも言える。

AIが生成するコードに対しても、@specを必須にするCIルールを敷いておけば、型仕様のないコードはマージされない。動的型付けの弱点を、プロセスで補完するわけだ。

「エコシステムが小さいのでは?」

José Valim自身がNx(数値計算)やBumblebee(Hugging Faceモデル on BEAM)の開発をリードしている。LLM APIを叩くだけならHTTPクライアントで十分で、エコシステムの大きさは本質的な制約にならない。

「採用が難しいのでは?」

LLMアプリケーション開発自体が新しい領域だ。どの言語でも経験者は希少。学習コストがRustより確実に低く、関数型の考え方が身につくElixirは、むしろ投資対効果が高い。

「パフォーマンスは?」

LLMアプリのボトルネックはAPI呼び出しのI/O待ちだ。CPUバウンドの数値計算速度勝負ではない。I/O並行性こそが性能を決める領域で、BEAM VMの独壇場だ。そこが問題になるのであれば初めてGoなりRustなりが選択肢になってくるといえよう。

RustとGoが適するケース

公平に言おう。

  • Rust — LLM推論エンジンの実装、ベクトルDBのコア、WASM向けコンパイル。基盤層ではRustが最適
  • Go — CLIツール、APIプロキシ、Kubernetesオペレータ。インフラ層ではGoが適任

ただし、ユーザーとの対話、プロンプト管理、ストリーミングUI、障害復旧——LLMアプリケーション層ではElixirが最もフィットすると考えている。

まとめ:言語をガードレールにせよ

AIがコードを書く時代、私たちの仕事は「コードを書く」から**「AIが書いたコードを安全に動かす仕組みを設計する」**にシフトしている。

ガードレールをライブラリで後付けするか、言語とランタイムに組み込まれたものを使うか。その選択が、LLM時代のアプリケーション開発の生産性と信頼性を分けると思う。

Elixirは30年以上の歴史を持つBEAM VM上に構築された、**「未来のために設計された過去の技術」**だ。プロセス隔離、Supervisor、パターンマッチング——これらはすべて、LLMのために作られたわけではない。でも、LLM時代が求めるガードレールそのものだ。