構築した基盤

LLM を業務パイプラインにつなぐため、Go で OpenAI/Anthropic 互換 API ゲートウェイを構築しました。リクエストの RAG 処理と、NATS JetStream への fire-and-forget publish を担当します。Dagster daemon の sensor がイベントを pull してジョブを実行し、Vector(Rust) が NATS から telemetry を subscribe して Prometheus/Loki に送ります。

gateway は publish のみを担当し、各ミドルウェアが独立して consume します。gateway から Dagster への依存を外し、別々にデプロイできる構成にしました。

初期の Rust(axum) プロトタイプから Go に移行しました。SSE 中継、NATS 購読、PG/Qdrant 書き込みを同時に扱うコードを、goroutine + channel で整理するためです。

使用した構成

  • 言語: Go 1.25, Gin フレームワーク
  • メッセージング: NATS 2.11 (JetStream有効)
  • オーケストレーション: Dagster (Python) — daemon + sensor + asset materialization
  • データストア: PostgreSQL 18 + pgvector (JIT有効)
  • テレメトリ: Vector 0.45 (Rust) → Prometheus + Loki + Grafana
  • 推論バックエンド: vLLM, llama.cpp, LM Studio
  • エンベディング + リランカー: multi-bert-inference — Rust (Axum) + ONNX Runtime (INT8, ~17MB, ~10ms)。ColBERT 256d embed + 64d token MaxSim rerank。DB依存排除、gRPCで256d vec直接送信(~1KB/candidate)
  • コンテナ: Podman rootless + podman-compose
  • ホスト構成: 3台 (storage server / desktop mac / compute server)

設計判断

レイヤードアーキテクチャ

Clean Architecture の依存方向を前提に、Transport、Domain、Infra の3層に分けました。

  Client (opencode CLI, API consumer)
    |
    v
Transport Layer (Gin) [internal/transport/http/]
    |-- middleware: RequestContext -> Logger -> Recovery
    |-- v1/ handlers (OpenAI/Anthropic 互換エンドポイント)
    |-- リクエスト解析・バリデーション
    +-- Presentation DTO への変換
    |
    |  Presentation Layer [pkg/openai/, pkg/anthropic/]
    |    +-- OpenAI/Anthropic 互換 request/response DTO
    |
    v
Domain Layer [internal/domain/]
    |-- knowledge/  RAG オーケストレーション
    |-- llm/        マルチバックエンド LLM ルーティング
    |-- vectorstore/ ベクトル検索 (pgvector ANN)
    +-- pipeline/   パイプライン連携 (NATS publish, fire-and-forget)
    |
    v
Infrastructure Layer [internal/infra/]
    |-- vllm/       vLLM クライアント
    |-- llamacpp/   llama.cpp クライアント
    |-- lmstudio/   LM Studio クライアント
    |-- postgres/   PostgreSQL + pgvector
    |-- nats/       NATS JetStream publish
    |-- vector/     Vector テレメトリ publish
    +-- reranker/   multi-bert-inference gRPC クライアント
  

Domain 層は外部依存を持ちません。LLM バックエンドやデータストアの変更は Infra 層で扱います。業務システム開発でも、業務ロジックと接続先を分けるための構成です。

gatewayのpublish-only設計

gateway は NATS に publish し、subscribe や trigger は行いません。Dagster sensor と Vector が独立して pull / subscribe します。

この分担で、次の点を狙いました。

  • gateway に Dagster を依存させず、デプロイを分ける
  • gateway を変えずに消費側を追加・変更する
  • gateway とパイプラインの障害を分離する

JetStreamを読むDagster sensor

Dagster daemon は durable consumer として JetStream を pull subscribe します。sensor → RunRequest → ジョブ実行に統一し、GraphQL trigger bridge を外しました。

なぜ Dagster (Python) か

Dagster は実行順と状態を管理します。重い計算は Python の外で動かします。

実際の計算実行エンジンPython/Dagster の役割
LLM 推論vLLM / llama.cpp (C++/CUDA)API 呼び出しのみ
ベクトル検索PostgreSQL + pgvector (C)SQL 発行のみ
エンベディング + リランキングmulti-bert-inference (Rust/ONNX Runtime)gRPC 呼び出しのみ
テレメトリ変換Vector (Rust)YAML 設定のみ

この構成では、Dagster の lineage tracking、sensor、asset materialization を採用しました。Rust 製の代替は見つからず、I/O wait と状態管理が中心の処理では、CPU bound 処理やメモリ安全という Rust の強みより、これらの機能を優先しました。

LLM バックエンド選定

指定されたモデル名でバックエンドを選びます。

バックエンドデフォルト URLモデル例
vLLMhttp://$COMPUTE_HOST:8000qwen3-next:80b, qwen3-coder:30b
llama.cpphttp://$COMPUTE_HOST:8081nemotron-3-nano:30b
LM Studiohttp://$COMPUTE_HOST:1234lfm2.5-1.2b-instruct-mlx

LLM ルーティングは gateway の goroutine が担当します。回答内容に応じて次のバックエンドを決めるには状態を持つ制御が必要なので、Vector には任せていません。

モデルの性能と品質を継続して評価しています。検証済みモデルは GGUF、NVFP4、ONNX などの形式で、storage server のコールドストレージに保管します。

storage server のモデルアーカイブ一覧
storage server のコールドストレージに保管された検証済みモデル群。GGUF, NVFP4, ONNX 等の各フォーマットで蓄積

Rust から Go への移行

初期には Rust(axum)でプロトタイプを実装しました。型による設計は有用でしたが、非同期処理の記述量が増えました。

  • SSEの中継 + NATSの購読 + PG/pgvectorへの書き込みが同時に走る非同期コンテキストの管理がRustでは冗長
  • goroutine + channelの方がこのパターンに素直にフィットする
  • 実行時オーバーヘッドの差はこのユースケースでは無視できるレベル

Go への移行後も、OpenAI 互換エンドポイント、NATS Pub/Sub、trace_id 付きログ、冪等設計を維持しました。Rust 版の設計文書を仕様として使いました。

実装

エンドポイント

MethodPath説明
POST/v1/chat/completionsチャット補完 (OpenAI互換)
POST/v1/messagesAnthropic Messages API
POST/v1/responsesResponses API
POST/v1/embeddingsエンベディング
GET/v1/modelsモデル一覧
GET/healthzヘルスチェック

ミドルウェアチェーン

Gin ミドルウェア: RequestContext → Logger → Recovery

  • RequestContext — X-Correlation-ID または X-Request-ID ヘッダからリクエスト追跡IDを抽出。未指定時はタイムスタンプベースのIDを自動生成し、コンテキストとレスポンスヘッダに設定
  • Logger — HTTPリクエストごとに method, path, status, duration を slog で記録
  • Recovery — Gin 標準のパニックリカバリ

RAG データフロー

  Client
  |
  v
Chat Completion Request
  |
  v
Knowledge Service (RAG オーケストレーション)
  |-- 1. ユーザクエリ抽出
  |-- 2. エンベディング生成 (multi-bert-inference -> 256d ColBERT)
  |-- 3. ベクトル検索 (pgvector HNSW ANN) + リランキング (ColBERT MaxSim)
  |-- 4. コンテキスト付与
  |-- 5. LLM バックエンドへ送信
  |-- 6. NATS publish (pipeline.* + telemetry.*)  <- fire-and-forget
  +-- 7. レスポンス返却
  

システムトポロジー

                                 NATS JetStream :4222
                              +----------------------+
                              |  pipeline.*           |
                              |  telemetry.*          |
                              +---+----------+--------+
                    publish ------+          | pull subscribe
                    (fire & forget)         | (durable consumer)
                                           |
+-- agent-gateway :8080 ------+    +-- Dagster daemon -----------------------+
|                             |    |                                         |
|  Client Request             |    |  sensor: nats_dagster_chat_persist      |
|    |                        |    |  sensor: nats_dagster_embedding         |
|    v                        |    |  sensor: nats_dagster_retrieve          |
|  Knowledge Service          |    |  sensor: nats_dagster_compose           |
|    |-- embed (multi-bert)   |    |  sensor: nats_dagster_flow_lineage     |
|    |-- vector search (pg)   |    |  sensor: nats_dagster_tool_call        |
|    |-- rerank (ColBERT)     |    |         |                               |
|    |-- LLM inference -------|-->-|  vLLM / llama.cpp / LM Studio          |
|    |                        |    |         |                               |
|    |-- NATS pub: pipeline.* |    |         v                               |
|    |-- NATS pub: telemetry.*|    |  RunRequest -> Dagster job 実行        |
|    +-- Response             |    |    |-- chat persist -> pg               |
|                             |    |    |-- embedding event -> pg            |
|  LLM Backends               |    |    |-- retrieve event -> pg             |
|    |-- vLLM :8000           |    |    |-- compose event -> pg              |
|    |-- llama.cpp :8081      |    |    |-- flow lineage -> pg               |
|    +-- LM Studio :1234     |    |    +-- tool call -> pg                  |
|                             |    |                                         |
+-----------------------------+    +-----------------------------------------+
                                           |
                              +------------+
                              | pull subscribe
                              v
                    +-- Vector (Rust) ----------------------+
                    |  source: nats (telemetry.*)           |
                    |    |-- sink: Prometheus exporter      |
                    |    +-- sink: Loki (CorrelationID)    |
                    +--------------------------------------+
  

NATS イベント設計

同じ CorrelationID を、独立した2つの経路で使います。

Pipeline (Dagster sensor → ジョブ実行):

トピックDagster ジョブ
pipeline.knowledge.chat.persistknowledge_chat_persist + knowledge_chat_pair_materialize
pipeline.knowledge.embeddingknowledge_embedding
pipeline.knowledge.retrieveknowledge_retrieve
pipeline.knowledge.composeknowledge_compose
pipeline.knowledge.flow.lineageknowledge_flow_lineage
pipeline.knowledge.tool_callknowledge_tool_call

Telemetry (Vector → Prometheus / Loki):

トピック内容
telemetry.knowledge.chatモデル, バックエンド, トークン使用量, ストリーミング有無
telemetry.knowledge.embeddingモデル, 入力数, 次元数
telemetry.knowledge.retrieveTopK, ヒット数, スコア
telemetry.knowledge.tool_call関数名, モデル

JetStreamストリーム設定:

  • PIPELINE — subjects: pipeline.>, retention: limits, max-age: 72h, storage: file
  • TELEMETRY — subjects: telemetry.>, retention: limits, max-age: 24h, storage: file

Dagster Assets と Jobs

Dagster は asset materialization と sensor で構成しています。

Dagster Assets カタログ — knowledge グループのアセット一覧
Dagster Assets カタログ。chat_pair, flow_lineage, tool_call が Materialized、他は Never materialized の状態

sensor が NATS JetStream を pull subscribe し、イベントに応じて RunRequest を発行します。GraphQL API を経由せずジョブを実行します。

Dagster tool_call_record アセット詳細
tool_call_record アセットのメタデータ。event_id, trace_id, function_name, model がMaterialization時に記録される

tool_call_record には、例えば function_name: tokei、model: minimax-m2.5 を記録します。モデルが呼んだツールを Dagster の lineage で追えます。

Dagster knowledge_chat_pair_materialize ジョブ
chat_pair_record → chat_pair_dataset のアセットグラフ。prompt-responseペアの永続化ジョブ

knowledge_chat_pair_materialize は chat_pair_record と chat_pair_dataset の2つのアセットを materialize します。prompt-response ペアを PostgreSQL に保存し、将来の fine-tuning 用データセットに使う設計です。

devstack コンテナ構成

podman-compose でミドルウェアを rootless コンテナとして起動します。

Podman Desktop — devstack コンテナ一覧
devstack の稼働コンテナ: postgres, reranker, vector, nats, dagster (user-code, webserver, daemon), grafana, node-exporter, promtail
サービスイメージポート役割
NATSnats:2.11-alpine4222, 8222JetStream メッセージング
PostgreSQLpostgres:18-jit-vector5432pgvector + JIT、agent_gateway / dagster DB
Dagsterカスタムビルド3000Webserver + Daemon + User-code gRPC
Vectortimberio/vector:0.45.0-alpine8686, 9598NATS テレメトリ → Prometheus/Loki
multi-bert-inferenceRust (Axum) + ONNX Runtime3001 (HTTP), gRPCColBERT embed (256d) + rerank (64d MaxSim)

Lakehouse profile(Phase 2用、明示的に起動):

  • Nessie (19120) — Iceberg メタデータカタログ (git-like ブランチ)
  • Trino (8081) — Iceberg カタログ付き SQL エンジン
  • dbt-fusion — dbt CLI コンテナ

3ホスト ネットワークトポロジー

NATS を gateway と同居させ、左右の経路を1 hop 以内に収めています。

  +-- storage server (observability) --+  +-- desktop mac (gateway) ----------+  +-- compute server (inference) ------+
|                                    |  |                                   |  |                                     |
|  Prometheus <-- scrape --------------|-- :9598 (Vector exporter)          |  |  vLLM :8000 (GPU inference)         |
|  Grafana                           |  |                                   |  |  llama.cpp :8081                    |
|  Loki <-- push -----------------------  Vector (nats source)             |  |                                     |
|  Vector (aggregator)               |  |       |                          |  |  Dagster :3000                       |
|    |-- nats source --------------------  NATS JetStream :4222 <---------|--|---- daemon sensor (pull subscribe)  |
|    |-- -> Loki sink                |  |       ^ publish                  |  |    |-- webserver                     |
|    +-- -> Prometheus exporter      |  |       | (fire-and-forget)        |  |    |-- daemon (sensor -> job)        |
|  MinIO :9000                       |  |       |                          |  |    +-- user-code gRPC               |
|                                    |  |  agent-gateway :8080             |  |                                     |
|                                    |  |    |-- proxy API                 |  |  PostgreSQL 18 :5432                |
|                                    |  |    |   (OpenAI/Anthropic/        |  |    +-- pgvector (ANN + full-text)   |
|                                    |  |    |    Responses)               |  |                                     |
|                                    |  |    |-- knowledge orchestrator    |  |  multi-bert-inference :3001         |
|                                    |  |    +-- NATS pub ----------------|--->  pipeline.* + telemetry.*          |
|                                    |  |                                   |  |                                     |
|                                    |  |  LM Studio :1234 (CPU inference) |  |  Trino :8080                        |
|                                    |  |                                   |  |  Nessie :19120                       |
+------------------------------------+  +-----------------------------------+  +-------------------------------------+
  
ホスト役割主要サービス稼働
storage serverObservability + オブジェクトストレージPrometheus, Grafana, Loki, Vector, MinIO24/7
desktop macGateway + メッセージングagent-gateway, NATS JetStream, LM Studio作業時のみ
compute serverGPU 推論 + データ基盤vLLM, llama.cpp, Dagster, PostgreSQL, Trino作業時のみ

NATS は最大の publisher である gateway と同じ desktop mac に配置し、ホスト間の publish 通信をなくしました。desktop と compute は同時に起動・停止します。storage server は常時稼働し、Vector は接続断後に自動再接続します。

データ保管戦略

用途に応じて PostgreSQL (pgvector) と Iceberg (Parquet) を使い分けます。

                      PostgreSQL (pgvector)             Iceberg (Parquet via Trino/Nessie)
                    --------------------             -----------------------------------
特性                構造化 + ベクトル検索              ファイル指向 + 大量蓄積
クエリ              WHERE + ANN (近傍探索)            SQL JOIN + time-travel
更新パターン        UPSERT (行単位)                   append-only (immutable Parquet)
バージョン管理      なし (行履歴は自前)                Nessie ブランチ + Iceberg snapshot
  

Phase 1 は全データを PostgreSQL に直接書き込みます。Phase 2 以降は、Dagster batch で PG → Parquet → Iceberg append を行い、dbt-fusion で分析用データセットを作る計画です。

PostgreSQL スキーマ

  • document_chunks — 256次元ColBERTエンベディング + HNSW vectorインデックス
  • chat_history — correlation_id別の会話履歴
  • rerank_scores — クエリ × ドキュメント × モデル別のリランクスコア
  • api_responses — Responses API 永続化

注意事項

  • desktop mac / compute server 停止時はNATSも落ちるが、JetStreamの永続化データはdesktop macのディスクに保持され、次回起動時に未消費メッセージから再開する
  • Vectorは接続断を検知して自動再接続するが、NATS停止中のtelemetryイベントは失われる(pipeline側はJetStreamのdurable consumerで保護される)
  • PostgreSQL 18のJIT有効化は shm_size: 4gb の設定が前提。不足するとOOM killのリスクがある

検証

  • Dagster UIでasset materialization状態を確認: chat_pair, flow_lineage, tool_callが正常にMaterialized
  • NATSモニタリング (:8222) でstream状態とconsumer lagを監視
  • Vector exporter (:9598) → Prometheusでtelemetryメトリクスの到達を確認
  • インテグレーションテスト: go test -tags=integration ./internal/infra/integration ./internal/domain/pipeline

次のアクション

記事時点では Phase 1 のリアルタイム基盤が稼働しています。以降は次の計画です。

Phase内容状態
1gateway → NATS → Dagster/pg, Vector → Prometheus/Loki稼働中
2pg → Parquet → Iceberg バッチ, dbt-fusion 加工devstack済
3MCP server tool 定義, Trino クエリ tool, 仮説検証自動化未着手
4fine-tuning パイプライン, MOE expert pruning, 合成データ増幅未着手