LangGraphは、複数の LLM呼び出しをグラフ構造で管理するためのライブラリです。ノード(関数)とエッジ(遷移条件)を組み合わせて、エージェントの思考フローを明示的に書けるのが特徴です。

Ollamaは、Gemma3などのLLMをローカル環境で簡単に実行できるツールです。従来はOpenAI APIなどのクラウドサービスに依存していたLLMを、完全にオフライン環境で動作させることが可能です。

本記事では、Ollamaで動かすローカルLLMとLangGraphを連携させ、「質問に応じて情報源を選び、最終回答を生成するミニAgent」を作ります。

開発環境の準備

①Ollama本体とモデルをインストール

Ollama本体は「LLMサーバ」、モデルは「重みファイル」に相当します。
以下記事の「2.1 Ollamaのインストール」と「2.2.2 モデルをダウンロードする 」を参考に、ollama本体のインストールと、LLMモデルをダウンロードしてください。


インストール方法を一言でいうと、Ollama公式にアクセスし、インストーラーをダウンロードして、ollama pull gemma3でモデルをダウンロードするだけです。

②ライブラリをインストール

#  LangChain と LangGraph 本体
pip install langchain langgraph

#  Ollama Python SDK(ローカル LLM 呼び出し用)
pip install ollama-python
コマンドラインから、pipでライブラリをインストールします。LangGraphは、LangChainのエコシステムの一部として設計されており、プロンプトテンプレートやOutput Parserなど、LangChainの既存コンポーネントをそのまま活用できます。

2. LLM とプロンプトチェーンの準備

2-1. LangChainのプロンプトテンプレート作成

from langchain_core.prompts import (
    ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate)
from langchain_ollama import ChatOllama

# Ollama で稼働しているモデル
ollama_model = "gemma3"

template = [
    SystemMessagePromptTemplate.from_template("{setting}"),
    HumanMessagePromptTemplate.from_template("{message}")
]

prompt = ChatPromptTemplate(template)

# Ollamaにlangchain経由で呼び出す(Ollamaの動作確認用) llm = ChatOllama(model=ollama_model) chain = prompt | llm chain.invoke({ "setting": "ユーザーの質問に回答してください。", "message": "おすすめの音声合成ソフトを教えて" })
まずは、LangChainでOllamaモデルの呼び出し部分を作成します。

ChatPromptTemplateで、SystemメッセージとHumanメッセージを定義します。SystemメッセージはLLMの役割や振る舞いを定義し、Humanメッセージは、ユーザーの入力です。この分離で、同じプロンプトテンプレートを異なる用途で再利用できます。

ChatOllamaは、OllamaサーバとLangChainを橋渡しするラッパークラスです。内部的にはOllama Python SDKを使用してHTTP通信を行い、LangChainの統一インターフェースに準拠したレスポンスを返します。

prompt | llmというパイプ演算子の記法は、LangChainの特徴的な機能です。左側の出力を右側の入力として自動的に接続し、データの流れを直感的に表現できます。

3. State(ノード間で受け渡すデータ)の定義

3-1. TypedDict で State 型を表現

from typing_extensions import TypedDict

class GraphState(TypedDict):
    question: str   # ユーザーの質問
    answer:   str   # 生成された回答
    document: str   # 取得した参考情報
LangGraphでは、すべてのノード間でやり取りされるデータを、一つの辞書オブジェクトとして管理します。これを「State」と呼びます。Stateは各ノードの実行結果を蓄積し、次のノードに必要な情報を提供する中央データストアの役割を果たします。

また、Python標準の辞書ではなくTypedDictを使用することで、型チェックや、使用可能なキーとその型が明確になるメリットがあります。

4. “次に取るべきアクション” を判断するチェーン

4-1. 判断用プロンプトとチェーン

from langchain_core.output_parsers import JsonOutputParser, StrOutputParser

deciding_setting = """あなたはユーザーからの質問に対し、次に取るべきアクションを決定する役割を持っています。
質問に対し、"VOICEVOX" に関する情報が必要な場合は "voice"、
"ollama" に関する情報が必要な場合は "ollama"、
それ以外は "other" と答えてください。
action というキーのみを含む JSON 形式で出力してください。"""

deciding_prompt = prompt.partial(setting=deciding_setting)
deciding_llm    = ChatOllama(model=ollama_model, format="json")
decide          = deciding_prompt | deciding_llm | JsonOutputParser()

decide.invoke({"message": "voicevox について語ってほしい"})
if-else文による条件分岐ではなく、LLMを使用した分岐をしています。自然言語の曖昧さや表記ゆれに対応可能で、分岐ロジックを自然言語で表現できます。

prompt.partialメソッドは、プロンプトテンプレートの一部の変数を事前に固定する機能です。これにより、2-1で作成したテンプレートを使いながら、用途に応じてメッセージを変更できます。

ChatOllamaformat="json"パラメータを指定すると、JSON形式での出力を指示できます。

JsonOutputParserは、ChatOllamaが出力したJSON形式の文字列を、自動的にPythonオブジェクト(辞書やリスト)に変換する処理を行うLangChainのコンポーネントです。ここでの戻り値は{'action': 'voice'}になります。

5. 回答生成チェーン(共通)

5-1. 回答生成用プロンプト

generating_setting = """あなたはユーザーからの質問に対し、回答する役割を持っています。
もし質問と一緒に関連情報が与えられている場合は必ず関連情報を見て回答を生成してください。"""

generating_prompt = prompt.partial(setting=generating_setting)
generating_llm    = ChatOllama(model=ollama_model)
generate          = generating_prompt | generating_llm | StrOutputParser()
複数のノードで同じ回答生成ロジックを使用するため、共通のチェーンとして定義しています。これにより、回答生成の品質を一元的に管理し、将来的な改善も容易になります。

6. Tool ノードの実装

6-1. VOICEVOX 情報取得ノード

def get_info_about_voice(state: GraphState):
    question  = state.get("question", "")
    document  = "VOICEVOXは、非常に素晴らしい音声合成ソフトです。"
    return {"question": question, "document": document}
documentに、VOICEVOXが選択された時の関連情報を付与します。

6-2. Ollama 情報取得ノード

def get_info_about_ollama(state: GraphState):
    question  = state.get("question", "")
    document  = "ollamaはローカルLLMのソフトウェア環境です。とても優秀な方が開発しました。"
    return {"question": question, "document": document}
documentに、Ollamaが選択された時の関連情報を付与します。

6-3. 最終回答生成ノード

def generating_node(state: GraphState):
    relevant_doc = state.get("document", None)
    question     = state.get("question", None)

    if relevant_doc:
        message = "関連情報:" + relevant_doc + "\n\n質問:" + question
    else:
        message = "質問:" + question

    answer = generate.invoke({"message": message})
    return {"answer": answer}
LangGraphのノード関数を定義しています。各ノードは3-1で定義したStateを受け取りStateを返します。これはLangGraphの基本ルールです。

7. ワークフロー Graph の構築

7-1. グラフの雛形とノード追加

from langgraph.graph import StateGraph
from langgraph.graph import END, START

# Graphの作成(全体の処理構造)
workflow = StateGraph(GraphState)

# nodeの追加
workflow.add_node("get_info_about_voice",get_info_about_voice)
workflow.add_node("get_info_about_ollama",get_info_about_ollama)
workflow.add_node("generating_node",generating_node)
StateGraphのワークフローを定義します。次に、add_node()でノード名と関数を関連付けます。ノード名は文字列で指定し、グラフ内でのルーティングに使用されます。

7-2. ノード間接続(エッジ)

# node間の接続をedgeに追加 
workflow.add_edge("get_info_about_voice", "generating_node") workflow.add_edge("get_info_about_ollama", "generating_node") workflow.add_edge("generating_node", END)
add_edge()でノード間の処理を定義します。常に指定された次のノードに進みます。

ENDは、グラフの終了を示す特別なノードです。このノードに到達すると、グラフの実行が完了し、最終的なStateが返されます。

7-3.  条件付きエッジ(conditional_edges)で分岐設定

# toolの使用を判定する関数
def deciding_function(state: GraphState):
    """GraphState から question を受け取り、遷移ラベルを返す"""

    q = (state.get("question", "") or "").lower()

    if "voicevox" in q or "voice" in q:
        return "voice"
    if "ollama" in q:
        return "ollama"
    return "other"

# LangGraphの分岐は conditional_edges というedgeを使うことで表現
# 第一引数:一つ前のNodeを指定
# 第二引数:分岐を決定する関数を指定
workflow.add_conditional_edges(
    START,
    deciding_function,
    {
        "voice": "get_info_about_voice",
        "ollama": "get_info_about_ollama",
        "other":"generating_node"
    },
)
add_conditional_edges()で、条件分岐関数の戻り値と次のノードを対応付けます。

STARTは、グラフの開始点を示す特別なノードです。通常、初期の分岐判定や前処理を行うノードの前に接続されます。

7-5. グラフのコンパイル

graph = workflow.compile()
compile()メソッドは、定義されたワークフローを実行可能なグラフオブジェクトに変換します。処理実行時に、グラフの整合性チェックが行われます。

8. グラフの可視化

8-1. ASCII 表示

graph.get_graph().print_ascii()
ターミナルにascii文字でグラフが出力され、ノードと分岐先を確認できます。

8-2. Mermaid 画像表示

from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))
output
Mermaid API を叩いて PNG を取得する方法です。オフライン環境では draw_mermaid_png(..., draw_method=MermaidDrawMethod.PYPPETEER) と指定してブラウザ自動レンダリングに切り替えることもできます。

9. グラフの実行

9-1. invoke で同期実行

output = graph.invoke({"question": "VOICEVOXの詳細を教えて?"})
print(output["answer"])

処理の流れは以下の通りです。
1) deciding_function が "voice" を返す。
2) get_info_about_voice が説明文を document に格納。
3) generating_node が関連情報付き回答を生成。
結果として output["answer"] に最終的な応答テキストが入ります。

まとめ

LangGraph では State(TypedDict)Node(関数) を定義し、Graph に登録していくシンプルな流れが基本です。 add_conditional_edges を使うと、「質問内容に応じたツール選択」をIF 文を書かずにグラフ構造で表現できます。
処理フローをドキュメントとしても共有しやすく、チーム開発やデバッグがスムーズになります。

参考書籍: