ストーリー
ミッション概要
| 項目 | 内容 |
|---|---|
| 目標 | LangGraphを使ったカスタマーサポートエージェントのワークフローを設計・実装する |
| 所要時間 | 90分 |
| ミッション数 | 3つ |
| 使用知識 | StateGraph / 条件分岐 / Human-in-the-Loop / 永続化 |
| 評価観点 | State設計、グラフ構造、条件分岐ロジック、HITL実装 |
Mission 1: カスタマーサポートワークフローのState設計
要件
以下の処理フローに必要なStateを設計してください。
1. 問い合わせ受付 → 意図分類
2. 意図に応じた分岐:
a. 注文照会 → 注文検索 → 回答生成
b. 返金申請 → 注文確認 → 返金計算 → 【人間承認】 → 返金実行 → 回答生成
c. 一般質問 → FAQ検索 → 回答生成
3. 回答送信
設計要件:
- TypedDictでState型を定義
- メッセージ履歴は
add_messagesアノテーション - 各ステップで必要なデータフィールドを過不足なく設計
- 人間の承認結果を保持するフィールドを含む
解答例
from typing import Annotated, TypedDict, Literal
from langgraph.graph.message import add_messages
class CustomerSupportState(TypedDict):
# メッセージ履歴
messages: Annotated[list, add_messages]
# 意図分類結果
intent: Literal["order_inquiry", "refund_request", "general_question"] | None
# 顧客情報
customer_id: str | None
# 注文関連
order_id: str | None
order_data: dict | None # 検索結果の注文データ
# 返金関連
refund_amount: int | None # 返金金額(円)
refund_reason: str | None # 返金理由
refund_approved: bool | None # 人間の承認結果
refund_result: dict | None # 返金処理結果
# FAQ関連
faq_results: list[dict] | None # FAQ検索結果
# ワークフロー制御
current_step: str # 現在のステップ名
error: str | None # エラー情報
final_response: str | None # 最終回答
ポイント:
- 各業務領域(注文、返金、FAQ)のデータを分離
refund_approvedで人間の承認結果を保持errorフィールドでエラー状態を管理current_stepでデバッグ・ログ用の進捗追跡
Mission 2: 条件分岐付きグラフの構築
要件
Mission 1のStateを使い、意図分類に基づく条件分岐付きのLangGraphワークフローを構築してください。
実装要件:
classify_intentノード: LLMで問い合わせの意図を分類- 3つの分岐先ノード:
handle_order_inquiry,handle_refund,handle_general - ルーティング関数: Stateの
intentフィールドに基づいて分岐 - 全分岐が
generate_responseノードに合流 - Mermaid図で可視化
解答例
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
llm = ChatOpenAI(model="gpt-4o", temperature=0)
def classify_intent(state: CustomerSupportState) -> CustomerSupportState:
"""問い合わせの意図を分類"""
classification_prompt = SystemMessage(content="""
以下の問い合わせを分類してください。
カテゴリ: order_inquiry(注文照会), refund_request(返金申請), general_question(一般質問)
カテゴリ名のみを回答してください。
""")
messages = [classification_prompt] + state["messages"]
response = llm.invoke(messages)
intent = response.content.strip().lower()
valid_intents = ["order_inquiry", "refund_request", "general_question"]
if intent not in valid_intents:
intent = "general_question"
return {"intent": intent, "current_step": "intent_classified"}
def handle_order_inquiry(state: CustomerSupportState) -> CustomerSupportState:
"""注文照会の処理"""
# 注文データの検索(モック)
order_data = {
"order_id": state.get("order_id", "ORD-12345"),
"status": "shipped",
"items": [{"name": "ワイヤレスイヤホン", "price": 15800}],
"estimated_delivery": "2026-03-08"
}
return {"order_data": order_data, "current_step": "order_searched"}
def handle_refund(state: CustomerSupportState) -> CustomerSupportState:
"""返金申請の処理(返金計算まで)"""
order_data = {
"order_id": state.get("order_id", "ORD-12345"),
"status": "delivered",
"total": 15800
}
return {
"order_data": order_data,
"refund_amount": order_data["total"],
"refund_reason": "customer_request",
"current_step": "refund_calculated"
}
def handle_general(state: CustomerSupportState) -> CustomerSupportState:
"""一般質問の処理"""
faq_results = [
{"question": "返品ポリシーは?", "answer": "商品到着後14日以内に..."},
{"question": "配送日数は?", "answer": "通常2-5営業日でお届け..."}
]
return {"faq_results": faq_results, "current_step": "faq_searched"}
def generate_response(state: CustomerSupportState) -> CustomerSupportState:
"""最終回答を生成"""
context = f"""
意図: {state.get('intent')}
注文データ: {state.get('order_data')}
FAQ結果: {state.get('faq_results')}
返金結果: {state.get('refund_result')}
"""
prompt = SystemMessage(content=f"以下の情報を基に、丁寧な顧客対応の回答を生成してください。\n{context}")
messages = [prompt] + state["messages"]
response = llm.invoke(messages)
return {
"messages": [response],
"final_response": response.content,
"current_step": "response_generated"
}
def route_by_intent(state: CustomerSupportState) -> str:
intent = state.get("intent", "general_question")
return {
"order_inquiry": "handle_order_inquiry",
"refund_request": "handle_refund",
"general_question": "handle_general"
}.get(intent, "handle_general")
# グラフ構築
workflow = StateGraph(CustomerSupportState)
workflow.add_node("classify_intent", classify_intent)
workflow.add_node("handle_order_inquiry", handle_order_inquiry)
workflow.add_node("handle_refund", handle_refund)
workflow.add_node("handle_general", handle_general)
workflow.add_node("generate_response", generate_response)
workflow.set_entry_point("classify_intent")
workflow.add_conditional_edges("classify_intent", route_by_intent)
workflow.add_edge("handle_order_inquiry", "generate_response")
workflow.add_edge("handle_refund", "generate_response")
workflow.add_edge("handle_general", "generate_response")
workflow.add_edge("generate_response", END)
app = workflow.compile()
Mermaid図:
graph TD
start["__start__"] --> classify_intent
classify_intent -->|order_inquiry| handle_order_inquiry
classify_intent -->|refund_request| handle_refund
classify_intent -->|general_question| handle_general
handle_order_inquiry --> generate_response
handle_refund --> generate_response
handle_general --> generate_response
generate_response --> end_node["__end__"]
Mission 3: Human-in-the-Loop付き返金フローの実装
要件
Mission 2の handle_refund フローにHuman-in-the-Loopの承認ステップを追加してください。
実装要件:
- 返金額の計算後、人間の承認が必要
interrupt_beforeで承認ノードの前に中断- 承認された場合は返金実行、却下された場合は理由を添えて終了
- PostgresSaverまたはMemorySaverでチェックポイントを使用
- 承認/却下後のワークフロー再開処理
解答例
from langgraph.checkpoint.memory import MemorySaver
def calculate_refund(state: CustomerSupportState) -> CustomerSupportState:
"""返金額を計算"""
order_data = {
"order_id": state.get("order_id", "ORD-12345"),
"status": "delivered",
"total": 15800,
"items": [{"name": "ワイヤレスイヤホン", "price": 15800}]
}
return {
"order_data": order_data,
"refund_amount": order_data["total"],
"refund_reason": "customer_request",
"current_step": "refund_calculated",
"messages": [AIMessage(content=f"返金プラン: {order_data['total']}円の返金を提案します。承認待ちです。")]
}
def human_approval(state: CustomerSupportState) -> CustomerSupportState:
"""人間の承認ステップ(中断ポイント)"""
# interrupt_before で中断されるため、
# 再開時には refund_approved が設定されている
return {"current_step": "human_reviewed"}
def execute_refund(state: CustomerSupportState) -> CustomerSupportState:
"""返金を実行"""
if state.get("refund_approved"):
refund_result = {
"status": "completed",
"amount": state["refund_amount"],
"refund_id": "REF-001",
"estimated_arrival": "3-5営業日"
}
return {
"refund_result": refund_result,
"current_step": "refund_executed",
"messages": [AIMessage(content=f"返金 {state['refund_amount']}円 が完了しました。")]
}
else:
return {
"refund_result": {"status": "rejected"},
"current_step": "refund_rejected",
"messages": [AIMessage(content="返金は承認されませんでした。")]
}
# 返金専用サブグラフの構築
refund_workflow = StateGraph(CustomerSupportState)
refund_workflow.add_node("calculate_refund", calculate_refund)
refund_workflow.add_node("human_approval", human_approval)
refund_workflow.add_node("execute_refund", execute_refund)
refund_workflow.add_node("generate_response", generate_response)
refund_workflow.set_entry_point("calculate_refund")
refund_workflow.add_edge("calculate_refund", "human_approval")
refund_workflow.add_edge("human_approval", "execute_refund")
refund_workflow.add_edge("execute_refund", "generate_response")
refund_workflow.add_edge("generate_response", END)
# チェックポイント付きでコンパイル
memory = MemorySaver()
refund_app = refund_workflow.compile(
checkpointer=memory,
interrupt_before=["human_approval"]
)
# --- 実行フロー ---
# Phase 1: 返金計算まで実行して中断
config = {"configurable": {"thread_id": "refund-session-001"}}
result = refund_app.invoke(
{
"messages": [HumanMessage(content="商品を返品して返金してほしい")],
"intent": "refund_request",
"order_id": "ORD-12345",
"customer_id": "CUST-001",
"refund_approved": None,
"current_step": "start",
"order_data": None,
"refund_amount": None,
"refund_reason": None,
"refund_result": None,
"faq_results": None,
"error": None,
"final_response": None
},
config
)
# → calculate_refund まで実行、human_approval の前で中断
# Phase 2: 人間が承認
print(f"返金プラン: {result.get('refund_amount')}円")
# UIやSlackで承認を受けた後...
refund_app.update_state(config, {"refund_approved": True}, as_node="human_approval")
# Phase 3: ワークフロー再開
final_result = refund_app.invoke(None, config)
print(final_result["final_response"])
ポイント:
interrupt_before=["human_approval"]で承認前に自動中断update_stateで人間の判断を注入invoke(None, config)で中断ポイントから再開- 承認/却下の両方のパスを処理
達成度チェック
- Mission 1: カスタマーサポートワークフローに必要なStateを過不足なく設計できた
- Mission 2: 意図分類に基づく条件分岐付きグラフを構築できた
- Mission 2: ルーティング関数を適切に実装できた
- Mission 3: Human-in-the-Loop付きの返金フローを実装できた
- Mission 3: 中断・承認・再開のフローを理解して実装できた
推定所要時間: 90分