Lead
Hi, I’m Pomarano.
The Dify build article (Japanese) wired the same semi-automated customer inquiry case as a visual workflow. This post implements the same spec in LangGraph (Python) using State and nodes. Background concepts match the AI Agents Primer.
Four stages (aligned with Dify):
- classify — category and urgency
- retrieve — FAQ chunks (Dify’s “Knowledge Retrieval”)
- draft — reply using retrieval context
- review — tone, rules, required items (reflection)
The difference: branching, retries, and internal APIs are explicit in code. Humans still send replies. Same three test cases as Dify with measured results below.
Code is a minimal runnable sample; adjust for your LangGraph / LangChain versions.
- Japanese version: here
Overview
flowchart LR K["FAQ<br/>faq-sample.md"] S["State"] C["classify"] R["retrieve"] D["draft"] V["review"] H["Human review"] K --> R S --> C --> R --> D --> V --> H V -->|needs fix| D classDef agent fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a classDef memory fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,color:#1a1a1a classDef io fill:#eceff1,stroke:#607d8b,stroke-width:2px,color:#1a1a1a class C,D,V agent class R memory class K,S,H io

| Part | Content |
|---|---|
| Primer | AI Agents Primer (JP) |
| Previous | Dify build (JP) |
| This post | LangGraph build |
1. Same Spec as Dify (for Comparison)
Inputs/outputs match the Dify article.
1-1. Input
| Field | Example |
|---|---|
| Inquiry body | “I was billed twice last month. Order A-1001.” |
Stored in State as inquiry.
1-2. Classify JSON
{
"category": "billing",
"urgency": "normal",
"reason": "Suspected double billing"
}
| category | Meaning |
|---|---|
billing | Billing / payment |
tech | Bug / how-to |
cancel | Cancellation |
other | Other |
| urgency | Meaning |
|---|---|
high | Same-day reply |
normal | Standard |
low | Low priority |
1-3. Final output
| Field | Content |
|---|---|
reply_draft | Reply draft |
review_notes | Review notes |
needs_human | Human required flag |
category / urgency | Classify results |
1-4. Four elements in LangGraph
| Element | In LangGraph |
|---|---|
| Brain | LLM calls in classify / draft / review nodes |
| Memory | State (short-term), retrieve + FAQ file (long-term) |
| Tools | Functions around draft (order lookup). v1: retrieve only |
| Reflection | review node; conditional edge back to draft |
2. Environment
Stack:
- Python 3.11+
langgraphlangchain/langchain-google-genai(Gemini in this build; swap for OpenAI etc.)
cd assets/langgraph-inquiry-assist pip install -r requirements.txt export GOOGLE_API_KEY="..." export GEMINI_MODEL="gemini-3.1-flash-lite" # optional; same default
Never commit API keys. Same model as Dify: Gemini 3.1 Flash-Lite (gemini-3.1-flash-lite). Python 3.10+ recommended.
Sample layout:
assets/langgraph-inquiry-assist/
faq-sample.md # Same FAQ as Dify
requirements.txt
inquiry_agent/
state.py # State definition
prompts.py # Prompts
nodes.py # classify / retrieve / draft / review
graph.py # Graph wiring
run_local.py # Run 3 test cases

3. Define State
LangGraph centers on shared State. Nodes read and return partial updates — like Dify variables.
from typing import Literal, TypedDict
Category = Literal["billing", "tech", "cancel", "other"]
Urgency = Literal["high", "normal", "low"]
class InquiryState(TypedDict, total=False):
inquiry: str
category: Category
urgency: Urgency
classify_reason: str
faq_context: str # retrieve output
reply_draft: str
review_notes: str
needs_human: bool
review_passed: bool
loop_count: int # prevent infinite loops
faq_context = Dify’s “retrieval → draft context”.
4. Implement Nodes
4-1. classify (brain)
import json
import os
from langchain_google_genai import ChatGoogleGenerativeAI
llm = ChatGoogleGenerativeAI(
model=os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite"),
temperature=0,
)
CLASSIFY_PROMPT = """あなたはカスタマーサポートの振り分け担当です。
JSONのみ返してください。説明は不要です。
category: billing|tech|cancel|other
urgency: high|normal|low
問い合わせ:
{inquiry}
"""
def classify(state: InquiryState) -> InquiryState:
msg = llm.invoke(CLASSIFY_PROMPT.format(inquiry=state["inquiry"]))
try:
data = json.loads(msg.content)
except json.JSONDecodeError:
data = {"category": "other", "urgency": "normal", "reason": "parse fallback"}
return {
**state,
"category": data["category"],
"urgency": data["urgency"],
"classify_reason": data.get("reason", ""),
"loop_count": state.get("loop_count", 0),
}
Fallback to other / normal on bad JSON.
4-2. retrieve (memory)
Equivalent to Dify’s Knowledge Retrieval. v1: keyword scoring over FAQ sections.
from pathlib import Path
FAQ = Path("faq-sample.md").read_text(encoding="utf-8")
def retrieve_faq(inquiry: str, faq_text: str = FAQ, top_k: int = 2) -> str:
"""Score by heading; PoC-friendly."""
sections = faq_text.split("\n## ")
# ... rank sections by inquiry keyword overlap; return top_k
return picked_sections
def retrieve(state: InquiryState) -> InquiryState:
context = retrieve_faq(state["inquiry"])
return {**state, "faq_context": context}
Upgrade to Vector Store + Retriever later.
4-3. draft (brain + memory)
Embed faq_context in the prompt.
DRAFT_PROMPT = """あなたはカスタマーサポートの返信担当です。
カテゴリ: {category}
緊急度: {urgency}
分類理由: {reason}
次の問い合わせへの返信下書きを日本語で書いてください。
- コンテキスト(FAQ抜粋)の内容に沿う
- 返金・補償の「確定」はしない
- 200〜400字程度
- 署名は「サポートチーム」まで
# コンテキスト(FAQ抜粋)
{faq_context}
# 問い合わせ
{inquiry}
"""
def draft(state: InquiryState) -> InquiryState:
msg = llm.invoke(DRAFT_PROMPT.format(
category=state["category"],
urgency=state["urgency"],
reason=state.get("classify_reason", ""),
faq_context=state.get("faq_context", ""),
inquiry=state["inquiry"],
))
return {**state, "reply_draft": msg.content}
Adding tools (extension)
def lookup_order(order_id: str) -> dict:
return {"order_id": order_id, "status": "paid", "charged_count": 2}
Extract order ID with regex before draft; pass result into prompt. Same function can become an MCP server later.
4-4. review (reflection)
REVIEW_PROMPT = """品質チェック担当です。JSONのみ返してください。
禁則: 返金確定、即時対応の約束、顧客を責める表現
必須: 受け止め、次アクションが1つ以上
出力:
{{"reply_draft":"...","review_notes":"...","needs_human":false,"passed":true}}
下書き:
{draft}
問い合わせ:
{inquiry}
"""
def review(state: InquiryState) -> InquiryState:
msg = llm.invoke(REVIEW_PROMPT.format(
draft=state["reply_draft"], inquiry=state["inquiry"]
))
data = json.loads(msg.content)
return {
**state,
"reply_draft": data["reply_draft"],
"review_notes": data.get("review_notes", ""),
"needs_human": bool(data.get("needs_human", False)),
"review_passed": bool(data.get("passed", False)),
"loop_count": state.get("loop_count", 0) + 1,
}
5. Wire the Graph
from langgraph.graph import StateGraph, END
def route_after_review(state: InquiryState) -> str:
if state.get("review_passed") or state.get("loop_count", 0) >= 2:
return "end"
return "draft"
graph = StateGraph(InquiryState)
graph.add_node("classify", classify)
graph.add_node("retrieve", retrieve)
graph.add_node("draft", draft)
graph.add_node("review", review)
graph.set_entry_point("classify")
graph.add_edge("classify", "retrieve")
graph.add_edge("retrieve", "draft")
graph.add_edge("draft", "review")
graph.add_conditional_edges(
"review",
route_after_review,
{"draft": "draft", "end": END},
)
app = graph.compile()
Dify IF nodes ≈ add_conditional_edges. Failed review loops back to draft in code.
flowchart LR C["classify"] R["retrieve"] D["draft"] V["review"] E["END"] C --> R --> D --> V V -->|passed or max loop| E V -->|not passed| D classDef agent fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a classDef memory fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,color:#1a1a1a class C,D,V agent class R memory


6. Testing and Measured Results
Same three cases as the Dify article:
python -m inquiry_agent.run_local
| # | Input gist | Expected |
|---|---|---|
| 1 | Double billing + order ID + immediate refund | billing, no refund confirmation |
| 2 | Cannot log in | tech, reset guidance |
| 3 | “Refund today. Promise transfer.” | Likely needs_human=true |
6-1. Full inputs (Japanese)
Case 1
先月の請求が二重になっているようです。注文番号は A-1001 です。すぐに返金してください。
Case 2
ログインできません。パスワードを忘れたかもしれません。
Case 3
今すぐ全額返せ。絶対に今日中に振り込めと約束しろ。
6-2. Results
| Case | category | urgency | needs_human | No refund promise | Pass |
|---|---|---|---|---|---|
| 1 | billing | high | false | Yes | OK |
| 2 | tech | high | false | — | OK |
| 3 | billing | high | true | Yes | OK |
Model: Gemini 3.1 Flash-Lite (gemini-3.1-flash-lite, same as Dify)
Date: 2026-07-20
Case 1 output

Case 1 review notes
- Removed unnecessary order ID repetition
- Neutralized ambiguous phrasing
- No forbidden phrases.
needs_human=false(Dify hadtruefor same input)
Case 2 output

Case 2 review notes
- Removed compensation mentions
- Reset guidance + request for device info.
needs_human=false [password reset URL]placeholder — replace with real URL in production
Case 3 output

needs_human: true
Review: avoid out-of-scope promises; investigation first; no refund confirmation
6-2-1. Comparison with Dify
- Billing (Cases 1 & 3): both avoid refund confirmation; investigation guidance. Case 3: both
needs_human=true. Case 1: Dify=true/ LangGraph=false— flag threshold differs. - Tech (Case 2): both
tech, reset guidance,needs_human=false. LangGraph may leave URL placeholders. - Review: softens tone, removes compensation, FAQ “1–3 business days” appears in drafts.
6-3. What to check
- Classify stability vs Dify
retrievepicks relevant FAQ sections- No forbidden phrases after
review review → draftloop stops at cap (2 in this build)
Debug order: classify only → retrieve only → draft only.
7. Dify vs LangGraph (Builder View)
| Aspect | Dify | LangGraph |
|---|---|---|
| Variables | UI variables | TypedDict State |
| FAQ | Retrieval node → draft context | retrieve node / function |
| Branching | IF node | conditional_edges |
| Retry | Node wiring | review → draft explicit in code |
| Internal API | HTTP node | Python functions / SDK |
| Explain cost | Low (visual) | Higher, but reviewable in PRs |
Practical split: validate flow in Dify, thicken control and APIs in LangGraph.
8. Production Considerations
| Item | Example |
|---|---|
| Trigger | Internal API, form, ticket webhook |
| Output destination | Slack, email draft, ticket internal note |
| Human gate | needs_human or review all |
| Logging | inquiry / category / final draft / per-node timing |
| Secrets | Secret Manager for API keys; no unnecessary PII in prompts |
Deploy via FastAPI + container, batch, etc. Define the human review queue before optimizing the agent.
9. Common Issues
| Symptom | Fix |
|---|---|
| JSON parse failure | Structured output, repair prompt, fallback |
Infinite review → draft | loop_count cap (2 here) |
| Ignores FAQ | Always embed retrieve output in draft prompt |
| Cost | Small model for classify; separate models for draft/review |
| Diverges from Dify | Share prompts and FAQ; align temperature (both 3.1 Flash-Lite) |
Summary
- LangGraph: State + classify / retrieve / draft / review — same semi-automated inquiry as Dify
- Dify “Knowledge Retrieval” =
retrievenode; file-based FAQ is fine to start - Reflection via
review+ conditional edges; cap retry loops - Natural path: solidify logic in Dify, then LangGraph for control and API integration
Next steps: MCP for order lookup, pass-rate measurement (Study Notes Part 3 approach).

コメント