はじめに
LangGraphは、LLM(大規模言語モデル)を使ったアプリケーションを「グラフ構造」で構築するためのフレームワークです。本記事では、最もシンプルなチャットボットをLangGraphで実装します。
対象読者
- Pythonの基本を理解している方
- LangChain・LangGraphを初めて学ぶ方
- LLMを使ったアプリケーション開発に興味がある方
今回作るもの
START → chatbot → END
この一直線のグラフで、シンプルなチャットボットを構築します。
1. State(状態)
Stateとは
LangGraphの公式ドキュメントによると:
"The
Stateconsists of the schema of the graph as well asreducerfunctions which specify how to apply updates to the state."
Stateはグラフ全体で共有されるデータ構造であり、ノードが読み書きする対象です。
基本的なState定義
from typing import Annotated, TypedDict
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
TypedDictを使う理由
- 型でステートの構造を明示できる
- エディタの補完が効く
- どんなデータがグラフ内を流れるか一目瞭然
リデューサー(Reducer)とは
ノードが返した値をどのようにStateに適用するかを決める関数です。
リデューサーがない場合(デフォルト)
class State(TypedDict):
bar: list[str]
# 現在の状態: {"bar": ["hi"]}
# ノードが返す: {"bar": ["bye"]}
# 結果: {"bar": ["bye"]} ← 完全に上書き!
会話履歴が毎回消えてしまいます。
リデューサーがある場合
from operator import add
class State(TypedDict):
bar: Annotated[list[str], add]
# 現在の状態: {"bar": ["hi"]}
# ノードが返す: {"bar": ["bye"]}
# 結果: {"bar": ["hi", "bye"]} ← 追加される!
リデューサーの種類
| リデューサー | 動作 | 用途 |
|---|---|---|
| なし(デフォルト) | 上書き | 単一値、フラグ |
operator.add |
リスト連結 | ログ、検索結果 |
add_messages |
メッセージ追加・マージ | 会話履歴 |
リデューサーの実行タイミング
ノードが値を返すたびに、そのキーに対応するリデューサーが実行されます。複数のノードを通過すれば、その回数だけ実行されます。
Stateの拡張
公式ドキュメントより:
"Typically, there is more state to track than just messages"
メッセージ以外にも追跡したいデータがあるのが普通です。
拡張例1: ドキュメント保存(RAG用)
class State(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
documents: list[str] # 検索で取得したドキュメント
拡張例2: ユーザー情報
class State(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
user_id: str
user_preferences: dict
拡張例3: 中間結果の保存
class State(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
search_results: Annotated[list[str], add]
current_step: str
2. Messageオブジェクト
主要なMessageタイプ
HumanMessage - ユーザーからの入力
from langchain_core.messages import HumanMessage
messages.append(HumanMessage(content=user_input))
AIMessage - LLMからの応答
response = llm.invoke(state["messages"]) # AIMessageが返される
主な属性:
-
content: テキスト内容 -
tool_calls: ツール呼び出し情報 -
usage_metadata: トークン使用量
SystemMessage - システム指示
from langchain_core.messages import SystemMessage
messages = [
SystemMessage(content="あなたは親切で丁寧な日本語アシスタントです。")
]
公式ドキュメントより:
"Establishes initial instructions that prime the model's behavior. Used to set the tone, define the model's role, and establish guidelines for responses."
使用タイミング:
- AIの役割を定義する時
- 応答のトーンや形式を指定する時
- 制約やルールを設定する時
SystemMessageはmessagesリストの先頭に置くのが一般的です。
実際に送られるデータ形式
LangChainのMessageオブジェクト(コード内)
[
SystemMessage(content="あなたは親切なアシスタントです"),
HumanMessage(content="こんにちは"),
AIMessage(content="こんにちは!どうされましたか?")
]
OpenAI APIに送られる形式(内部で変換)
[
{"role": "system", "content": "あなたは親切なアシスタントです"},
{"role": "user", "content": "こんにちは"},
{"role": "assistant", "content": "こんにちは!どうされましたか?"}
]
プロバイダー間の違い
| LangChain | OpenAI API | Claude API |
|---|---|---|
SystemMessage |
messages配列内 | 別パラメータ |
HumanMessage |
"user" |
"user" |
AIMessage |
"assistant" |
"assistant" |
LangChainを使う利点:各プロバイダーの形式の違いを吸収してくれるため、統一的なMessageオブジェクトだけ意識すれば良い。
3. LLMの初期化
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.7,
)
Temperatureとは
応答のランダム性・創造性を制御するパラメータです。
仕組み
LLMは次の単語を確率に基づいて選択します。
- Temperature = 0: 常に最も確率の高い単語を選ぶ → 決定論的
- Temperature 高い: 確率が少し低い単語も選ぶ → ランダム・創造的
推奨値
| 値 | 特徴 | 用途 |
|---|---|---|
| 0 〜 0.3 | 決定論的、一貫性高い | 翻訳、分類、コード生成 |
| 0.5 〜 0.7 | バランス良い | 一般的な会話、Q&A |
| 0.7 〜 1.0+ | 創造的、多様性あり | マーケティング文、創作 |
注意: Temperatureが高すぎるとハルシネーション(嘘)が増える可能性があります。
4. ノード関数
def chatbot(state: State) -> dict:
response = llm.invoke(state["messages"])
return {"messages": [response]}
ノードの基本ルール
- 引数: 現在のState全体を受け取る
- 戻り値: 更新したいキーだけを含む辞書を返す
重要なポイント
return {"messages": [response]} # リストで返す
-
responseは単一のAIMessage -
リストで包んで返す(
add_messagesが期待する形式) - State全体を返す必要はなく、更新したい部分だけでOK
5. グラフの構築
from langgraph.graph import StateGraph, START, END
def create_graph():
# 1. グラフビルダーの作成
graph_builder = StateGraph(State)
# 2. ノードの追加
graph_builder.add_node("chatbot", chatbot)
# 3. エッジの追加
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
# 4. コンパイル
return graph_builder.compile()
各ステップの説明
- StateGraph(State): Stateの型を渡して型安全なグラフを作成
- add_node: ノード名と関数を登録
- add_edge: ノード間の接続を定義
- compile: 実行可能なグラフオブジェクトに変換
6. invoke() - グラフの実行
result = graph.invoke({"messages": messages})
実行フロー
-
初期Stateを渡す
{"messages": [SystemMessage, HumanMessage]} -
chatbotノード実行
-
llm.invoke(state["messages"])を実行 - AIMessageを返す
-
-
リデューサー適用
# ノードの戻り値 {"messages": [AIMessage]} # ↓ add_messagesにより追加 {"messages": [SystemMessage, HumanMessage, AIMessage]} -
最終Stateを返す
戻り値の使用
# 最新のAIメッセージを取得
ai_message = result["messages"][-1]
print(f"AI: {ai_message.content}")
# 次のループのためにmessagesを更新
messages = result["messages"]
完全なコード例
動作環境
- Python 3.12.12
ディレクトリ構成
langgraph-chatbot/
├── .env # APIキーを記載
├── main.py # メインコード
└── requirements.txt # 依存パッケージ
requirements.txt
langchain-openai==1.0.3
langgraph==1.0.3
python-dotenv==1.2.1
main.py
from dotenv import load_dotenv
from typing import Annotated, TypedDict
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, BaseMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
load_dotenv()
# State定義
class State(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
# LLM初期化
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
# ノード関数
def chatbot(state: State) -> dict:
response = llm.invoke(state["messages"])
return {"messages": [response]}
# グラフ構築
def create_graph():
graph_builder = StateGraph(State)
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
return graph_builder.compile()
# メイン
def main():
graph = create_graph()
messages = [
SystemMessage(content="あなたは親切で丁寧な日本語アシスタントです。")
]
while True:
user_input = input("あなた: ")
if user_input.lower() == "quit":
break
messages.append(HumanMessage(content=user_input))
result = graph.invoke({"messages": messages})
ai_message = result["messages"][-1]
messages = result["messages"]
print(f"AI: {ai_message.content}\n")
if __name__ == "__main__":
main()
まとめ
本記事で学んだLangGraphのチャットボット実装:
| 概念 | 説明 |
|---|---|
| State | グラフ全体で共有されるデータ構造 |
| リデューサー | ノードの戻り値をどうStateに適用するか |
| add_messages | メッセージを追加・マージするリデューサー |
| ノード | 処理単位(Stateを受け取り、更新を返す) |
| エッジ | ノード間の接続 |
| invoke() | グラフを実行し、最終Stateを返 |