yoi/docs/research/llm-worker-vs-rust-llm-libs.md
2026-06-01 18:49:23 +09:00

245 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# `llm-worker` vs Rust LLM ライブラリ群 比較レポート
調査日: 2026-04-26
対象: `crates/llm-worker` (本プロジェクト) / `rig` / `genai` / `swiftide`
調査方法: 各リポジトリを ローカルに取得し、ソースREADME, Cargo.toml, src/, examples/)を直接読解。
---
## 0. TL;DR
```
低レイヤ ←──────────────────────────────────────→ 高レイヤ
HTTP wrapper Worker/Loop Agent Framework Pipeline/RAG
───────────── ──────────── ──────────────── ──────────────
genai ●●●●● (out-of-scope)
llm-worker ●●●●● (一部)
rig ●●● ●●●●● ●●●
swiftide ●● ●●●● ●●●●● (主軸)
```
- **`genai`** … マルチプロバイダの「統一 chat client」。エージェントループ・状態管理は明示的にスコープ外。
- **`llm-worker`** … Worker = 「LLM 対話の中央実行器」。低レベルHTTPと高レベルAgent / RAG / Pipelineの中間に位置するレイヤを精緻に作っている。
- **`rig`** … Agent / Tool / Pipeline / VectorStore を一通り揃えた汎用フレームワーク。Provider 数とエコシステムが最大の武器。
- **`swiftide`** … 一次目的はストリーミング Indexing / Query パイプライン。Agent はあとから足された二の矢で、`async-openai` / `async-anthropic` をラップ。
要点: **`llm-worker` の独自性は「型状態でのキャッシュ保護」「Interceptor による上位層への決定委譲」「Prune を context projection として非破壊で扱う」の 3 点**。これらは他 3 者の誰も持っていない。一方、Provider カバレッジ・VectorStore・Pipeline DAG では rig に大差で負ける。
---
## 1. スコープ宣言(各プロジェクトが自分を何と呼んでいるか)
| プロジェクト | 自称 | 提供する | 提供しない(明示・暗黙) |
|---|---|---|---|
| **llm-worker** | "LLM との対話を管理する低レベル基盤クレート" | Worker による turn 実行、Tool 実行、stream イベント、Interceptor、cache 保護 | RAG / Embedding / VectorStore / Pipeline DAG / 永続化 |
| **rig** | "Ergonomic & modular library for building LLM applications" | Agent / Tool / Pipeline / Vector / Embedding / OTel | (ほぼ全部入り。明示的な non-goal は薄い) |
| **genai** | "Standardizing chat completion APIs across major AI services" | Chat (sync/stream)、Tool 定義、Embedding | **Agent loop / 会話状態管理を明示的に out-of-scope** |
| **swiftide** | "Fast, streaming indexing, query, and agentic LLM applications" / "data pipeline" | Indexing pipeline / Query state machine / Agent / Hook | (LLM HTTP は外部 SDK = `async-openai` 等に委譲) |
`llm-worker` のスコープ宣言は他より控えめで、「何かの上位層から呼ばれる前提」が読み取れる(`Interceptor` の存在がそれを裏付ける)。
---
## 2. レイヤ階層と被り
`Worker` が担っているレイヤを基準にすると、各プロジェクトの「同じ層」と「上下層」は次のように整理できる。
### 同じ層(== 競合)
| 機能 | llm-worker | rig | genai | swiftide |
|---|---|---|---|---|
| Provider 抽象 (HTTP) | ◎ 自前 (4) | ◎ 自前 (20+) | ◎ 自前 (18+) | ○ 外部 SDK ラップ |
| Streaming イベント正規化 | ◎ | ◎ | ◎ | ○ |
| Tool 定義 + 実行ループ | ◎ | ◎ | △ (定義のみ、loop は呼び出し側) | ◎ |
| Multi-turn loop | ◎ | ◎ | ✕ | ◎ |
| Hook / Interceptor | ◎ Interceptor + closure | ◎ PromptHook | △ Resolver のみ | ◎ 6+ Hook |
| 履歴管理 | ◎ Item / ContentPart | ◎ Message | △ ChatRequest を呼び出し側で append | ◎ MessageHistory trait |
### 上下層(== 補完関係)
| 機能 | llm-worker | rig | genai | swiftide |
|---|---|---|---|---|
| Embedding / VectorStore | ✕ | ◎ 10+ | △ (Embed のみ) | ◎ 9+ |
| RAG | ✕ | ◎ | ✕ | ◎ |
| Pipeline DAG | ✕ | ◎ `parallel!` / `try_op` | ✕ | ◎ Indexing pipeline |
| 永続化 | ✕ | △ | ✕ | ◎ MessageHistory swap |
| OTel / Observability | △ tracing のみ | ◎ GenAI Semantic Conv | △ | ◎ Langfuse 等 |
| 型状態でのキャッシュ保護 | **◎ 唯一** | ✕ | ✕ | ✕ |
---
## 3. コアの抽象を並べる
### `llm-worker::Worker<C, S>`
- `C: LlmClient`(プロバイダ)、`S: WorkerState``Mutable` | `Locked` の sealed trait
- ライフサイクル: `Mutable` で履歴/プロンプト編集 → `lock()``Locked``run()` / `resume()``unlock()` で戻る
- `state.rs:1-61`: `Locked` 中は履歴 append-only、`set_system_prompt` 等は型レベルで呼べない
- `prune.rs:66-118`: `prunable_indices()` + `project()` で context のみ縮約。永続履歴は不変 = "context projection"
### `rig::Agent` + `PromptRequest`
- 状態機械型: `max_turns`、`extended_details()` で usage / message tracking
- `PromptHook` trait: `on_completion_call`, `on_completion_response`, `on_tool_call` (skip 可), `on_tool_result`, `on_text_delta`, …
- `ToolSet` + `ToolEmbedding` で RAG 可能なツール
- Pipeline DAG: `Op` trait + `parallel!` macro
### `genai::Client`
- `exec_chat(model, ChatRequest, options) -> ChatResponse`
- `ChatStreamEvent { Start, Chunk, ReasoningChunk, ToolCallChunk, End }`
- Adapter pattern: `to_web_request_data` / `to_chat_response` / `to_chat_stream`
- モデル名の prefix で provider 推論 (`gpt-` → OpenAI, `claude-` → Anthropic)
- 状態管理は呼び出し側(`ChatRequest` を毎ターン clone & append
### `swiftide-agents::Agent`
- Builder: `llm`, `context`, `tools`, `toolboxes`, `hooks`
- `AgentContext` trait + `DefaultContext` (AtomicUsize で completions ポインタ管理)
- State enum: `Pending` / `Running` / `Stopped(StopReason)`
- Tool execution: `LocalExecutor` と MCP (Model Context Protocol)
- Provider 層は `async-openai` 0.33+ / `async-anthropic` 0.6 を流用
---
## 4. 三大独自点(`llm-worker` だけが持っているもの)
### 4.1 型状態によるキャッシュ保護 — `state::WorkerState`
```
Worker<C, Mutable> Worker<C, Locked>
├─ history_mut() ✓ ├─ history_mut() ✗ (compile error)
├─ set_system_prompt() ✓ ├─ set_system_prompt() ✗
├─ register_tool() ✓ ├─ run() / resume() ✓
└─ lock() ─────────────────→ │
└─ unlock() ──────────→ Mutable
```
- **何を防ぐか**: KV cache の prefix が変わるような編集を `Locked` 中に行うこと(=次の `run()` で cache miss を引く事故)
- **誰がやっているか**: rig / genai / swiftide のいずれもキャッシュ整合性は「呼び出し側のお気持ち」レベル。genai は `CacheControl::Ephemeral / Stable` enum を持つが、それはマーク用であって状態機械ではない
- **意義**: production の Anthropic / OpenAI prompt cache 利用で、誤った `set_system_prompt` が静かに課金を倍増させる事故を**コンパイル時に潰せる**
### 4.2 Interceptor による上位層への決定委譲 — `interceptor.rs`
`Interceptor` trait が持つフック点:
- `on_prompt_submit``PromptAction`
- `pre_llm_request``PreRequestAction`
- `pre_tool_call``PreToolAction`
- `post_tool_call``PostToolAction`
- `on_turn_end``TurnEndAction`
**rig の `PromptHook` との違い**: rig のフックは「観察 + skip/abort のフラグ」。`llm-worker` は **戻り値が ActionEnum** で、上位層が次の挙動を選択できる(例: `PreToolAction::Override(custom_result)`)。これは `Worker` を「自律ループ」と「上位 orchestrator から駆動される実行器」の両方として使える設計上の選択。
### 4.3 Context Projection による prune — `prune.rs`
- 削るのは `Item::ToolResult { content }` の中身だけ。`Item` 自体は履歴に残すsummary は維持)
- `project()`**イミュータブル変換** で永続履歴を変えない
- savings 推定は意図的に上位層に委譲(`prune.rs` 末尾コメント)
- swiftide も MessageHistory に `Summary` メッセージで似たことをやるが、**永続履歴を直接置換**する方式。`llm-worker` の方が rollback / 再生 / debugging に強い
---
## 5. 各競合の「これは llm-worker に勝っている」点
### rig
- **Provider 数**: 20+OpenAI / Anthropic / Gemini / Bedrock / Vertex / Azure / Groq / DeepSeek / Cohere / Mistral / OpenRouter / Together / xAI / Perplexity / HuggingFace …)
- **VectorStore**: MongoDB / LanceDB / Neo4j / Qdrant / SQLite / SurrealDB / Milvus / ScyllaDB / S3Vectors / HelixDB
- **Pipeline DAG**: `parallel!` macro / `try_op` / agent_opsAgent を Op として組み立て可能)
- **OTel**: GenAI Semantic Convention に完全準拠
- **WASM**: `rig-core` のみ WASM 互換
- **更新頻度**: 直近 20 commit が 1〜2 週間以内
### genai
- **Provider 数**: 18+
- **薄さ**: 6,539 LOC / 35 deps で multi-provider client が完成している(`llm-worker` の `llm_client/` は 1.5k LOC で 4 provider
- **Native protocol サポート**: Anthropic reasoning, Gemini thinking, OpenAI strict mode, OpenAI Responses API の `previous_response_id`/`store` を素通し
- **Tool call streaming**: `ToolCallChunk` が分離イベント
- **Resolver** で auth / model / endpoint をフレキシブルに差し替え
### swiftide
- **Indexing pipeline**: streaming loader → transformer → chunker → storage の fluent パイプライン
- **Query state machine**: Pending → Retrieved → Answered の型状態(くしくも型状態だが対象が違う)
- **Hook 粒度**: `before_all` / `before_completion` / `after_completion` / `before_tool` / `after_tool` / `on_new_message` / `on_stop` 等 6+
- **MCP サポート**: ToolExecutor として LocalExecutor と MCP の両方
- **MessageHistory trait** で永続化バックエンドRedis 等)に差し替え可能
- **既存 SDK の活用**: `async-openai` / `async-anthropic` をベースに薄く乗せる戦略
---
## 6. 各競合の「これは llm-worker の方が良い」点
### vs rig
- **キャッシュ整合性**: rig は `extended_details()` で usage を観測できるが、prefix 変更を**防止**するメカニズムはない
- **依存の薄さ**: rig-core 25.5K LOC + 大量 sub-crate vs llm-worker 5.8K LOC
- **Worker の単一責務**: rig の `Agent` は LLM 呼び出し + Tool + (optional) RAG までを抱え込む。`llm-worker` は orchestrator を分離
### vs genai
- **Tool 実行ループ**: genai は呼び出し側で書く必要があるREADME/example で明示)
- **会話履歴の構造化**: genai は `ChatRequest` を毎ターン clone する素朴な API
- **Interceptor**: genai は `Resolver` で初期化時のカスタマイズのみ。実行中フックなし
### vs swiftide
- **Provider 直接実装**: swiftide は `async-openai` 0.33 / `async-anthropic` 0.6 に依存しており、その上流に出来る/出来ないが縛られる。`llm-worker` は HTTP まで自前で握る
- **Agent loop の中核設計**: swiftide-agents は `DefaultContext` の AtomicUsize ベースで素朴。型状態保護なし
- **依存の少なさ**: swiftide-agents 単体でも `async-openai` を呼ぶ前提
---
## 7. 「ライブラリ化したとき競合するか」の解像度
| 相手 | 競合度 | 理由 |
|---|---|---|
| **rig** | 🟥 高 | 同じ "agent + tool + multi-provider" を狙う層。rig は既にエコシステムを持っており、正面衝突は不利 |
| **genai** | 🟨 中 | 層が違うclient vs worker。むしろ `llm-worker``llm_client/` を捨てて genai に乗る選択肢がある |
| **swiftide** | 🟩 低 | 一次目的が違うpipeline。agent の中核実行を `llm-worker` に置き換える "embed" 戦略すら成立し得る |
---
## 8. ポジショニング案(仮にライブラリ化するなら)
`llm-worker` がぶつからない・かつ需要がある場所を考えると、次のような立ち位置が現実的:
> **"Production-grade LLM worker primitive — 型状態でキャッシュ整合性を守り、Interceptor で上位層 (rig / swiftide / 自社 orchestrator) から駆動できる低レベル実行器"**
具体的な打ち出し方:
1. **キャッシュ整合性をコンパイル時に保証する唯一のクレート** という明確な売り文句。Anthropic の prompt cache 課金事故で困った人を狙う
2. **Tool 実行ループ + Interceptor** をプリミティブとして提供し、上位フレームワークから plug できることを主張
3. **HTTP まで自前** はやめて、`llm_client/` を外す or feature 化し、`genai` バックエンドを optional 提供する選択肢も検討に値する(実装コストを 1.5k LOC 削れる)
4. **Pipeline / RAG / VectorStore は提供しない** ことを明示。rig / swiftide の代替を狙わないことで、共存ストーリーが書ける
---
## 9. ライブラリ化の判断材料(再)
- **需要がある層か**: yes。rig / swiftide / genai は揃って "もう一段下のキャッシュ整合性プリミティブ" を持っていない
- **既存と被るか**: 上記 3 案でかわせる
- **維持コスト**: API 安定化 + provider 追従が恒常的に乗る。`llm_client/` を外すか genai に寄せるかでだいぶ軽くなる
- **タイミング**: yoi 本体がリリースされ、`Worker` の API が stress test を受ける前に公開すると、後から破壊的変更を強いられる。**先に yoi をリリースして、production 使用例として参照させてからライブラリ化する**方が安全
---
## 付録: 主要ファイルへの参照
### llm-worker
- `crates/llm-worker/src/lib.rs:1-59` — モジュール構成
- `crates/llm-worker/src/worker.rs:101-191` — Worker<C, S>
- `crates/llm-worker/src/state.rs:1-61` — Mutable / Locked 型状態
- `crates/llm-worker/src/interceptor.rs:1-147` — Interceptor trait
- `crates/llm-worker/src/prune.rs:66-118` — context projection
- `crates/llm-worker/src/llm_client/{auth,client,event,transport,types}.rs`
- `crates/llm-worker/src/tool_server.rs:27-52` — Deferred 登録
### rig
- `github.com/0xPlaygrounds/rig/rig/rig-core/src/{agent,completion,tool,pipeline,vector_store,streaming}/mod.rs`
### genai
- `github.com/jeremychone/rust-genai/src/lib.rs:1-25`
- `.../src/client/client_impl.rs:62-67`
- `.../src/chat/chat_request.rs:10-34`
- `.../src/chat/chat_stream.rs:11-86`
- `.../src/adapter/adapter_types.rs:11-59`
### swiftide
- `github.com/bosun-ai/swiftide/swiftide-agents/src/agent.rs:45-100`
- `.../swiftide-agents/src/hooks.rs`
- `.../swiftide-core/src/chat_completion/traits.rs`
- `.../swiftide-indexing/src/pipeline.rs`
- `.../swiftide-query/src/`