コンテンツにスキップ

第16回: LangGraph Platform — 書いたエージェントを REST API として配るしくみ

Software Design 2025年2月号 連載第16回。これまでの回 (09〜15) では「LangGraph で組んだエージェントを CLI / Streamlit で自分のマシンで動かす」段階だったが、本回は エージェントを Docker でパッケージし、REST API + Web UI として動かす ためのツールキット LangGraph Platform が主役。

エージェント本体のコードは最小 (call_model + tool_node だけの 2 ノード ReAct)。新規ポイントは「コードではなく、配布の仕組み」 にある。

全体像

flowchart TD
    Dev["開発者: my_agent/agent.py に graph を書く"] --> Config["langgraph.json<br>(エントリポイントを記述)"]
    Config --> CLI["langgraph CLI: langgraph up"]
    CLI --> Docker["Docker Compose を生成 + 起動"]

    subgraph DockerEnv["Docker (ローカル or Cloud)"]
        Server["LangGraph Server<br>(FastAPI ベース)"]
        Postgres["Postgres<br>(checkpointer/threads 永続化)"]
        Redis["Redis<br>(pub-sub / task queue)"]
        Server --- Postgres
        Server --- Redis
    end

    Docker --> DockerEnv

    DockerEnv -->|REST API :8123| Client1["Python SDK"]
    DockerEnv -->|REST API :8123| Client2["TypeScript SDK"]
    DockerEnv -->|baseUrl=:8123| Studio["LangSmith Studio (Web)<br>https://smith.langchain.com/studio<br>(ブラウザ UI でグラフ可視化)"]

書いたエージェント (graph オブジェクト) を langgraph.json で指し、langgraph up で Docker 一式が立ち上がる。あとは REST API でどの言語からでも呼べる + Studio UI でステップ実行できる。Streamlit が UI 一体型のローカル実行だったのに対し、こちらはサービス化

エージェント内部 (本回サンプルの graph) はシンプル:

flowchart LR
    Start([START]) --> CallModel["call_model<br>LLM 呼び出し"]
    CallModel -->|tool_calls あり| ToolNode["tool_node<br>Tavily 検索実行"]
    CallModel -->|tool_calls なし| End([END])
    ToolNode --> CallModel

第11回 ReAct と同じ構造。違いは: - 11回は create_react_agent ショートカット使用 → 16回は StateGraph でフル明示 - 11回はモデル固定 → 16回は config_schema=GraphConfig で OpenAI / Anthropic を呼び出し時に切替

使用ライブラリ・原理

1. LangGraph Platform とは

LangChain 社が提供する 「LangGraph で書いたエージェントを本番運用するための実行環境 + デプロイツール」 一式の総称:

構成要素 役割
langgraph-cli CLI ツール。langgraph build / langgraph up / langgraph deploy
LangGraph Server FastAPI ベースの実行サーバ。グラフを REST API 化 (Threads, Runs, Streaming)
Postgres thread (会話セッション) と checkpoint (グラフ状態) の永続化
Redis pub-sub / バックグラウンドジョブキュー
LangGraph Studio ブラウザ UI。グラフ構造の可視化 + ステップ実行デバッガ

これまで自前で書いていた「Streamlit + MemorySaver」(14回) の本番版が LangGraph Server + Postgres に置き換わるイメージ。

2. langgraph.json がデプロイの起点

{
  "dependencies": ["."],
  "graphs": {
    "agent": "./my_agent/agent.py:graph"
  },
  "env": ".env"
}

3 行で何を伝えているか: - dependencies: Docker イメージにインストールする Python パッケージ。"." は pyproject.toml の依存全部 - graphs: 公開する graph オブジェクト。agent はエンドポイント名で、./my_agent/agent.py:graph のパス + 変数名を指す - env: コンテナに注入する環境変数ファイル

langgraph build はこれを読んで Dockerfile を自動生成する。手で Dockerfile を書く必要なし。

3. langgraph up の中身

langgraph up 一発で: 1. Dockerfile 自動生成 → Docker イメージビルド 2. docker-compose.yml も自動生成 (langgraph-api / postgres / redis の 3 サービス) 3. compose up 4. http://localhost:8123 に REST API + ドキュメント (/docs) 5. ブラウザで https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:8123 を開くと LangSmith Studio (Web) がローカル API に接続してグラフを可視化

重要: Studio UI は独立した別ポートのサーバではない。LangSmith Cloud 側にホストされた Studio Web アプリに、baseUrl クエリパラメータでローカル LangGraph API を指す形。langgraph up 起動時のログにもこの URL が表示される。

ローカル開発 (up) と本番デプロイ (deploy for LangSmith) で同じイメージが使えるのがポイント。Docker を介すので Python のバージョン差・ライブラリ衝突がない。

4. REST API: Threads と Runs の概念

LangGraph Server が公開するエンドポイントは主に 2 種類:

POST /threads           # 新しい会話セッションを作成
POST /threads/{id}/runs # そのセッションでグラフを実行
GET  /threads/{id}/state # 現在の State を取得
POST /threads/{id}/runs/stream # ストリーミング実行

thread_id は 14回で見た LangGraph の checkpointer の thread_id と同じ。サーバ側で thread ごとに Postgres に状態保存。クライアントは thread_id を引き回すだけで会話継続できる。

5. config_schema=GraphConfig でランタイム切替

class GraphConfig(BaseModel):
    ANTHROPIC: ClassVar[str] = "anthropic"
    OPENAI: ClassVar[str] = "openai"

    model_name: Literal["anthropic", "openai"]

workflow = StateGraph(AgentState, config_schema=GraphConfig)

ANTHROPIC / OPENAIClassVar[str] のクラス定数で Pydantic フィールド扱いされない。_get_model 内で if model_name == GraphConfig.OPENAI: のようにマジック文字列ではなく定数参照で書ける → タイポ防止 + IDE 補完が効く。model_name フィールドの Literal["anthropic", "openai"] と値が一致している必要がある(ここはハードコード重複なので、定数から Literal を生成できれば本来は理想)。

config_schemaruns API 経由でクライアントが渡せるパラメータの型。クライアント側で:

client.runs.create(
    thread_id=...,
    assistant_id="agent",
    input={"messages": [...]},
    config={"configurable": {"model_name": "openai"}},  # ← ここ
)

このパラメータが call_model 内の config.get("configurable", {}).get("model_name", ...) で取れる。「同じグラフで複数のモデルを切り替える」運用がコード変更なしでできる。

6. @lru_cache(maxsize=2) でモデルクライアントを再利用

@lru_cache(maxsize=2)
def _get_model(model_name: str):
    if model_name == GraphConfig.OPENAI:
        model = ChatOpenAI(temperature=0, model_name="gpt-4o")
    elif model_name == GraphConfig.ANTHROPIC:
        model = ChatAnthropic(temperature=0, model_name="claude-3-5-sonnet-20241022")
    model = model.bind_tools(tools)
    return model

サーバが多数のリクエストを処理する想定では、毎回 ChatOpenAI() を new するとコネクションプール初期化コストが嵩む。LRU キャッシュで 2 種類のクライアントをプロセス内で使い回し。

7. add_messages で messages を append 専用に

class AgentState(BaseModel):
    messages: Annotated[Sequence[BaseMessage], add_messages]

add_messages は LangGraph 公式の Reducer 関数operator.add (list 連結) と似ているが、追加機能: - BaseMessageID で重複排除 - メッセージ更新 (同じ ID で別内容を渡すと上書き)

会話履歴を保持する State には事実上必須の Reducer。第14回の operator.add は単純連結だったが、こちらの方が堅牢。

8. Sequence[BaseMessage] を使う理由

list[BaseMessage] ではなく Sequence を使うのは、LangChain が返す list / tuple 両方を許容したいから。tuple は immutable で安全、list は append しやすい。Sequence にしておくと「読み取り専用 list-like」として両方受け入れられる。

ファイル別の役割

ファイル 役割
langgraph.json LangGraph Platform 設定。graph エントリポイント + env ファイル + 依存
pyproject.toml uv プロジェクト定義。langgraph-cli を dev-dependencies に置く設計
uv.lock 依存固定
my_agent/__init__.py パッケージマーカー (空)
my_agent/agent.py グラフ本体。ReAct 2 ノードと graph = workflow.compile() でエントリ変数定義
README.md .env 設定 + langgraph up 起動方法

学んだこと(要点)

1. これまでとパラダイムが違う: 「アプリケーション」から「サービス」へ

01〜15回は「1 つのスクリプトpython xxx.py / streamlit run xxx.py で起動」だった。16 は: - グラフを API として永続稼働させる (POST すると実行される) - 複数クライアント (Python / TypeScript / curl) から同時にアクセス可能 - 状態は Postgres に永続化、サーバ再起動で消えない - Studio UI で外部からデバッグ・観測

つまり 「LLM エージェントをマイクロサービス化するための定石」 が LangGraph Platform。

2. langgraph.json の 3 行が「BFF パターンの省略」

普通 API サーバを書くなら: - FastAPI / Express でエンドポイント定義 - pydantic / zod でリクエスト型定義 - middleware / auth / logging を書く - DB マイグレーション書く

LangGraph Platform はこれを langgraph.json の 3 行 + グラフ実装 に圧縮した。LLM エージェント特化なので、汎用 BFF を書くより 10 倍速い。トレードオフは「カスタマイズ性が低い」。

3. config_schema は「マルチテナント」「A/B テスト」の鍵

model_name だけでなく、temperaturemax_tokens、ユーザー設定、フィーチャーフラグなど、ランタイムで挙動を変えたい値はすべて config_schema に置くのが定石。

LangGraph Studio は config_schema を読んで自動で UI フォームを生成する → 開発中のパラメータチューニングが即座にできる。

4. ローカル up と本番 deploy の互換性が運用負荷を下げる

普通の Web アプリは「ローカル動作と本番環境のギャップ」が一大バグ要因 (パッケージバージョン、env、Postgres バージョン...)。LangGraph Platform は 同じイメージ + 同じスキーマでローカル⇄本番が動くので、ローカルで動けば本番でも動く確率が高い。

5. このサンプルは「ベース動作確認用」

エージェントロジック自体は超シンプル (検索ツール 1 個の ReAct)。ここで学ぶのは「いかに薄くても、サービスとしてデプロイできる」という枠組み。本回のエージェントを差し替えて、14回の HITL ロジックや 12回の ARAG ロジックを乗せれば、即サービス化できる。

6. なぜ Sequence + add_messages か

messages: Annotated[Sequence[BaseMessage], add_messages]

LangGraph Platform でグラフをサービス化すると、API クライアントが送ってくる messages の型が controllable でなくなる (list か tuple か)。Sequence は両方受ける。add_messages は会話継続のため必須。サービス前提の堅牢な書き方

7. langgraph-cli は dev-dependencies に置く

[tool.uv]
dev-dependencies = [
    "langgraph-cli==0.1.56",
]

langgraph-cli は開発・デプロイ時にしか使わない。ランタイムイメージに含めないようにするためわざわざ dev に分ける。サンプルではあるが、本番運用を意識した分離。

ただし README は pip install -U langgraph-cli を別途案内している。これは「uv sync だと dev-dependencies はインストールされるが、本回のように uv sync 後すぐ langgraph up を叩く場合、uv run langgraph up でないと PATH に入らない」混乱を避けるための保険。本来は uv run langgraph upuv tool install langgraph-cli が綺麗。

拡張アイデア

  1. Streamlit (14回) の UI を本回のサーバに繋ぎ替える
  2. 14回の HumanInTheLoopAgent クラスを my_agent/agent.py:graph に移植
  3. Streamlit 側を REST API クライアントに書き換え (LangGraph Python SDK)
  4. 結果: グラフ実装と UI が完全分離、複数 UI (Streamlit / Slack bot / Web) を同じバックエンドで運用可能

  5. config_schema を拡張して A/B テスト

  6. model_name に加え temperature, system_prompt_version を追加
  7. 50% は新プロンプト、50% は旧プロンプトで A/B → LangSmith Evaluator で評価

  8. langgraph deploy でクラウドデプロイ

  9. LangSmith アカウント + GitHub repo 接続で、push トリガーで自動デプロイ
  10. サンプルを fork → 自分の LangSmith にデプロイして実機で動かしてみる

  11. Postgres を Aurora / Cloud SQL に差し替え

  12. docker-compose.yml の Postgres を外部接続に変更
  13. 本番運用想定の永続化検証

  14. Studio UI で HITL を動かしてみる

  15. 14回の interrupt_before=["human_approval"] を本回エージェントに追加
  16. Studio UI 上で承認ボタンが出るので、Streamlit 不要で HITL デモが可能

  17. 複数 graph を langgraph.json に登録

  18. "graphs": {"agent_v1": "...", "agent_v2": "..."} で 2 種類同居
  19. Canary release / Blue-Green デプロイのベース

現代版に移植するなら

連載原典は LangGraph 0.2.50 / langgraph-cli 0.1.56 (2024年末) で、既に本リポジトリの想定範囲。本リポジトリ方針 (1Password 経由のキー取得) に揃えるなら以下:

1. API キー取得を op 経由に

LangGraph Platform は .env をコンテナに inject するアーキテクチャ。ローカル up 時は .envop inject で生成するのが一案:

op inject -i .env.template -o .env
langgraph up

.env.template:

OPENAI_API_KEY={{op://Personal/openAI_API/credential}}
TAVILY_API_KEY={{op://Personal/Tavily_API_key/credential}}
ANTHROPIC_API_KEY={{op://Personal/Anthropic/credential}}
LANGSMITH_TRACING=true
LANGSMITH_API_KEY={{op://Personal/LangSmith/credential}}
LANGSMITH_PROJECT=sd-16

ただし op inject書き込み系なのでリポジトリ標準では未許可。実行時は人間がターミナルで直接叩く想定。

ただし生成された .env を gitignore に確実に入れること。

2. モデル名を最新に

gpt-4o gpt-4o-2024-08-06 以降を明示、または gpt-4o-mini
claude-3-5-sonnet-20241022 claude-sonnet-4-6 (Sonnet 4.6) または claude-haiku-4-5

3. langgraph-cli のアップデート

langgraph-cli==0.1.56 (2024年末) → 最新の langgraph-cli>=0.2 を確認。Docker compose 構成や CLI コマンドが変わる可能性あり。

4. LangSmith 統合の env 名変更

連載原典は LANGCHAIN_TRACING_V2 / LANGCHAIN_API_KEY だが、現行は LANGSMITH_TRACING / LANGSMITH_API_KEY (README に LANGSMITH_TRACING_V2 と書かれているが正確には LANGSMITH_TRACING=true のみ)。

既知の不具合・注意点

  • 【実機で踏んだ】call_model(state, config: GraphConfig) は 0.8+ ランタイムで TypeError: missing 1 required positional argument: 'config' になる — サーバ側の langgraph-api 0.8.7 は、ノード関数の第 2 引数を RunnableConfig 型で注釈した場合のみ config を自動注入する。サンプル原典は config: GraphConfig (Pydantic 型) のままなので注入されず落ちる。修正は1行:
    from langchain_core.runnables import RunnableConfig
    def call_model(state: AgentState, config: RunnableConfig) -> dict:
    
    本リポジトリでは適用済み。GraphConfigconfig_schema 側でのみ使われ、ノード関数の引数型としては使わないのが正しい
  • 【実機で踏んだ】Studio UI から「質問」しても pydantic ValidationError: model_name Field required で落ちるGraphConfig.model_nameLiteral[...] の必須フィールドにしていると、Studio が Configurable フォーム未編集のまま Submit を許してしまい、空 dict {}GraphConfig(**{}) を作ろうとして validation 失敗。本リポジトリでは Field(default="openai") を付けて回避済み:
    model_name: Literal["anthropic", "openai"] = Field(default="openai")
    
    必須にしておくと Studio の初回起動でほぼ確実に踏む罠なので、config_schema のフィールドは原則 default を持たせるのが安全
  • README が 古い形式の env 名 (LANGSMITH_TRACING_V2) で書かれている。最新の LangSmith は LANGSMITH_TRACING=true
  • langgraph upDocker Desktop 必須。Apple Silicon Mac で Docker Desktop / OrbStack を入れていないと動かない
  • 初回 langgraph up は Docker イメージビルド + 各種ダウンロードで 数分かかる
  • langgraph up を foreground で動かしている状態で docker restart 16-langgraph-api-1 をかけると、CLI 側が compose の終了を検知して全コンテナごと落ちる。コード変更後は langgraph up を一度止めてから再実行するのが安全
  • langgraph.jsondependencies: ["."] は pyproject.toml を見るが、uv ロックファイル (uv.lock) は無視 → 再現性のため langgraph-cli のバージョンを上げるとロック相当の機能 (langgraph build --pip-installer pip) を使うのが安全
  • _get_model の戻り型 ChatOpenAI | ChatAnthropicbind_tools 後の Runnable だが、型注釈は元の BaseChatModel のままになる → mypy strict なら警告
  • TavilySearchResultslangchain-community 0.3.25 で deprecated。langchain-tavily パッケージへの移行が推奨
  • model_name=... 引数は ChatOpenAI / ChatAnthropic の旧 API。新しめでは model=... が推奨 (両方動くが今後 deprecate 予定)

実機で動かしたメモ (2026-05-23)

uv run langgraph up --port 8123 で起動し、REST API 経由で ReAct を一周させた手順:

# 1. .env を 1Password から生成 (op read で値を直接ファイル redirect)
{
  printf 'OPENAI_API_KEY=%s\n' "$(op read 'op://Personal/openAI_API/credential')"
  printf 'ANTHROPIC_API_KEY=%s\n' "$(op read 'op://Personal/anthropic_api_key/credential')"
  printf 'TAVILY_API_KEY=%s\n' "$(op read 'op://Personal/Tavily_API_key/credential')"
  printf 'LANGSMITH_API_KEY=%s\n' "$(op read 'op://Personal/lang_smith_api_key/credential')"
  printf 'LANGSMITH_TRACING=true\n'
  printf 'LANGSMITH_PROJECT=sd-16\n'
} > .env && chmod 600 .env

# 2. サーバ起動 (初回は Docker イメージ pull で数分)
uv run langgraph up --port 8123

# 3. 別シェルで API を叩く
THREAD=$(curl -s http://localhost:8123/threads -X POST \
  -H "Content-Type: application/json" -d '{}' \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['thread_id'])")

curl -s "http://localhost:8123/threads/$THREAD/runs/wait" \
  -X POST -H "Content-Type: application/json" \
  -d '{
    "assistant_id": "agent",
    "input": {"messages": [{"role": "user", "content": "東京の今日の天気をTavilyで調べて、1文で簡潔に答えてください。"}]},
    "config": {"configurable": {"model_name": "openai"}}
  }'

# 4. 結果確認
curl -s "http://localhost:8123/threads/$THREAD/state" | jq '.values.messages'

得られた message 列 (4 件): human → ai(tool_call=tavily_search_results_json) → tool(検索結果) → ai(要約回答) の ReAct パターン通り。config.model_name=openai で OpenAI、anthropic で Anthropic に runtime 切替できることも確認。

ハマったこと: - Anthropic クレジット残高切れBadRequestError: Your credit balance is too low → OpenAI 側で動かして検証 - 1Password の LangSmith 項目名は LangSmith ではなく lang_smith_api_key (snake_case) - 上記 RunnableConfig 問題

記事参照

  • Software Design 2025年2月号 連載第16回「LangGraph Platform」
  • 関連:
  • 14回 Streamlit HITL — UI 一体型の前段。Platform 化すると UI とロジックが分離する
  • 11回 ReAct — 本回エージェントのロジック原型
  • LangGraph Platform 公式: https://langchain-ai.github.io/langgraph/cloud/
  • LangGraph CLI 公式: https://langchain-ai.github.io/langgraph/cloud/reference/cli/

作成: 2026-05-22 / 最終更新: 2026-06-10