はじめに
LangGraphを学んでいると、よく次のような説明が出てきます。
- State: グラフ全体で共有される状態
- Node: Stateを受け取って処理し、Stateの差分を返す関数
- Edge: 次にどのNodeへ進むかを決める接続
- Conditional Edge: Stateを見て分岐先を決める条件付きEdge
説明としては理解できるのですが、最初は「普通のループと何が違うのか」が少し分かりにくいです。
そこで今回は、LangGraphで「コード生成 -> 安全確認 -> テスト -> 修正/停止」という流れを作り、それをブラウザ上の小さなダッシュボードで見える化しました。
なお、このサンプルではLLMによるコード生成部分はモックにしています。目的は、LangGraphのState、Node、Conditional Edgeによって処理フローをどう制御できるかを理解することです。
作ったもの
ブラウザ上で、LangGraphの実行状態をリアルタイム風に確認できるデモです。
画面では、次のようなノードが表示されます。
コード生成
↓
安全確認
├─ 問題なし -> テスト
├─ 要件が曖昧 -> 人間確認
└─ 危険コード -> ブロック
テスト
├─ 成功 -> 終了
└─ 失敗 -> エラー分類 -> コード生成へ戻る
実行中は、現在動いているノードが光り、右側にState、生成コード、試行回数、エラー種別などが表示されます。
なぜ普通のwhileではなくLangGraphなのか
単純な「失敗したら再実行」だけなら、普通のwhileで十分です。
例えば、次のような処理です。
while True:
code = generate_code()
success, error = run_test(code)
if success:
break
if retry_count >= 3:
break
ただ、実際のコード生成フローでは、分岐が増えていきます。
- テストに成功したら終了する
- テストに失敗したらエラーを分類する
- 文法エラーなら再生成する
- 要件が曖昧なら人間確認に回す
- 危険なコードなら即ブロックする
- 何度も失敗したら人間に渡す
このような処理をすべてwhileとifで書くと、「今どの状態なのか」「次にどこへ進むのか」が読みにくくなります。
LangGraphでは、処理を次の3つに分けて考えられます。
State = 今の状態
Node = 何をするか
Edge = 次にどこへ進むか
この分離が、LangGraphの分かりやすいところです。
Stateの定義
今回のStateは次のように定義しました。
from typing import TypedDict
class GraphState(TypedDict):
task: str
scenario: str
code: str
error_msg: str
error_type: str
iteration: int
status: str
next_action: str
主な意味は次の通りです。
| キー | 意味 |
|---|---|
task |
ユーザー要件 |
scenario |
デモ用の実行パターン |
code |
生成されたコード |
error_msg |
テストや安全確認のエラー |
error_type |
エラー分類 |
iteration |
コード生成の試行回数 |
status |
現在の状態 |
next_action |
次に進む方向 |
ポイントは、Node同士が直接やり取りするのではなく、共有されたStateを更新しながら進むことです。
Nodeの例
コード生成Node
このNodeは、コードを生成してStateに返します。
今回のデモではLLMを呼ばず、シナリオに応じて固定のコードを返しています。
def generate_code_node(state: GraphState) -> dict[str, str | int]:
iteration = state["iteration"] + 1
scenario = state["scenario"]
if scenario == "dangerous_code":
code = """\
import os
def add(a, b):
os.remove("important_file.txt")
return a + b
"""
elif iteration == 1:
code = """\
def add(a, b):
return a - b
"""
else:
code = """\
def add(a, b):
return a + b
"""
return {
"code": code,
"iteration": iteration,
"status": "generated",
"next_action": "test",
}
本物のLLMを使う場合は、このNodeの中をLLM呼び出しに差し替えます。
安全確認Node
安全確認Nodeでは、生成コードに危険な処理が含まれていないかを見ます。
def safety_check_node(state: GraphState) -> dict[str, str]:
code = state["code"]
dangerous_words = ["os.remove", "shutil.rmtree", "subprocess.run"]
if not code:
return {
"error_type": "unclear_requirement",
"error_msg": "コード生成前に要件確認が必要です。",
"status": "needs_human",
"next_action": "human_review",
}
for word in dangerous_words:
if word in code:
return {
"error_type": "dangerous_code",
"error_msg": f"Blocked dangerous code: {word}",
"status": "blocked",
"next_action": "block",
}
return {
"error_type": "",
"status": "safe",
"next_action": "test",
}
ここでは簡単な文字列検出だけですが、実運用ではAST解析、サンドボックス、許可リスト、権限分離などが必要になります。
テスト実行Node
テストNodeでは、生成されたコードを一時ファイルに保存し、実際にPythonでテストを実行します。
def run_test_node(state: GraphState) -> dict[str, str]:
code = state["code"]
test_code = """\
from solution import add
assert add(1, 2) == 3
assert add(-1, 1) == 0
assert add(10, 5) == 15
"""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
solution_path = temp_path / "solution.py"
test_path = temp_path / "test_solution.py"
solution_path.write_text(code)
test_path.write_text(test_code)
result = subprocess.run(
["python3", str(test_path)],
cwd=temp_path,
text=True,
capture_output=True,
check=False,
)
if result.returncode == 0:
return {
"error_msg": "",
"error_type": "",
"status": "passed",
"next_action": "end",
}
error_msg = result.stderr.strip() or result.stdout.strip()
return {
"error_msg": error_msg,
"status": "failed",
"next_action": "classify_error",
}
ここはモックではなく、本当にPythonコードを実行しています。
Conditional Edgeで分岐する
LangGraphで面白いのは、Nodeの処理結果を見て、次のNodeを決定できるところです。
例えば、安全確認後の分岐は次のように書けます。
def route_after_safety(state: GraphState):
if state["next_action"] == "human_review":
return "human_review"
if state["next_action"] == "block":
return "block"
return "test"
これをadd_conditional_edgesで登録します。
workflow.add_conditional_edges(
"SafetyCheck",
route_after_safety,
{
"test": "Tester",
"human_review": "HumanReview",
"block": "Blocked",
},
)
この設計にすると、AIの出力が危険だった場合でも、AIに次の行動を決めさせる必要がありません。
Pythonの決定論的なロジックで、必ずBlockedへ送れます。
グラフ全体の構築
全体は次のように構築します。
from langgraph.graph import END, StateGraph
workflow = StateGraph(GraphState)
workflow.add_node("Coder", generate_code_node)
workflow.add_node("SafetyCheck", safety_check_node)
workflow.add_node("Tester", run_test_node)
workflow.add_node("ErrorClassifier", classify_error_node)
workflow.add_node("HumanReview", human_review_node)
workflow.add_node("Blocked", blocked_node)
workflow.set_entry_point("Coder")
workflow.add_edge("Coder", "SafetyCheck")
workflow.add_conditional_edges(
"SafetyCheck",
route_after_safety,
{
"test": "Tester",
"human_review": "HumanReview",
"block": "Blocked",
},
)
workflow.add_conditional_edges(
"ErrorClassifier",
route_after_error,
{
"retry": "Coder",
"human_review": "HumanReview",
},
)
workflow.add_edge("HumanReview", END)
workflow.add_edge("Blocked", END)
app = workflow.compile()
ブラウザ表示
今回は、Pythonの小さなHTTPサーバーを用意し、Server-Sent Eventsで実行イベントをブラウザへ流しています。
ブラウザ側では、イベントを受け取るたびに次の表示を更新します。
- 現在動いているNode
- 通過したEdge
- Stateの中身
- 生成コード
- イベントログ
これにより、LangGraphが「ただのループ」ではなく、「状態に応じて経路が変わるステートマシン」であることが見やすくなります。
動くシナリオ
1. テスト失敗から自動修正
最初はわざと間違ったコードを生成します。
def add(a, b):
return a - b
テストでAssertionErrorになり、エラー分類後にコード生成へ戻ります。
2回目で正しいコードになります。
def add(a, b):
return a + b
2. 文法エラーから自動修正
最初は文法エラーのあるコードを生成します。
def add(a, b):
return a +
テスト実行時にSyntaxErrorとなり、エラー分類後にコード生成へ戻ります。
3. 曖昧な要件で人間確認
要件が曖昧な場合は、コード生成を無理に進めず、人間確認へ送ります。
コード生成 -> 安全確認 -> 人間確認 -> 終了
4. 危険コードをブロック
生成コードに危険な処理が含まれている場合は、テストに進めずブロックします。
os.remove("important_file.txt")
この場合は、次の経路になります。
コード生成 -> 安全確認 -> ブロック -> 終了
実行方法
依存関係をインストールします。
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
コンソール版を実行する場合:
.venv/bin/python langgraph_code_test_sample.py
ブラウザ版を実行する場合:
.venv/bin/python dashboard_app.py
その後、ブラウザで次を開きます。
http://127.0.0.1:8001
このサンプルで本当にやっていること
このデモは、すべてが見せかけというわけではありません。
実際にやっていること:
- LangGraphの
StateGraphでグラフを構築している - NodeとConditional Edgeで経路を制御している
- Stateを更新しながら処理している
- 生成コードを一時ファイルに書き出している
- Pythonでテストコードを実行している
- テスト結果に応じて再生成、終了、人間確認、ブロックへ分岐している
- 実行イベントをブラウザへ配信している
一方で、LLMによるコード生成はまだモックです。
つまり、この記事の主題は「本物のAIコード生成エージェントを作った」ではなく、「LangGraphのStateとConditional Edgeによる制御構造を理解する」です。
まとめ
LangGraphの良さは、単にループを書けることではありません。
重要なのは、AIのように出力が揺れやすい処理をNodeに閉じ込めつつ、システム全体の進行ルールをEdgeで決定論的に制御できることです。
今回のようなコード生成フローでは、次のような制御を明示できます。
- 成功したら終了
- テスト失敗なら修正へ戻す
- 文法エラーなら修正へ戻す
- 要件が曖昧なら人間確認へ送る
- 危険コードなら即ブロックする
このように、LangGraphは「AIに全部任せる」ための道具というより、AIを含む処理全体を安全に制御するためのステートマシンとして使うと分かりやすいです。
次に発展させるなら、モックのコード生成Nodeを実際のLLM呼び出しに置き換えたり、チェックポイントやHuman-in-the-loopの再開処理を追加すると、より実運用に近い構成になります。
