实用指南:你的第一个真正意义上的LangGraph项目——构建客户支持系统
《实用笔记:你的第一个真正意义上的LangGraph项目——构建客户支持系统》操作指南:为采用该模式的团队提供合同、检查清单以及可直接插入的代码片段。
以下笔记为“你的第一个真正意义上的LangGraph项目:构建客户支持智能体”提供了实用的操作路径。重点在于合同定义、检查项以及可直接插入的代码占位符,而非激励性表述。 在完成概览阶段时,首先列出合同要求:所需输入、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 将配置信息与应用程序代码分开。环境文件、密钥存储以及功能开关应集中存放于一个位置,以便操作人员无需查看整个系统结构即可进行审计。
在编写任何代码之前:先了解计划
在将“Before We Write”阶段视为可度量的对象时,其效果最佳。在扩大范围之前,需记录一份理想的操作日志、一个故障案例以及回滚说明。 同时记录正常流程与恢复流程。重试机制、人工审核环节以及死信处理都是产品本身的一部分,而非后续需要补充的内容。 保持图结构的状态扁平且具有类型约束。嵌套的数据块会掩盖哪个节点编写了哪个字段的信息,还会导致在中断后无法继续执行。
准备工作
将“准备工作”视为可度量的对象时,其效果最佳。在扩大范围之前,需记录一份理想的操作日志、一个故障案例以及回滚说明。 相比庞大的脚本,应优先使用小型且可测试的单元。当某个步骤出现故障时,故障原因应能指向单一的责任主体,而非复杂的流程链。 保持图结构的状态扁平且具有类型约束。嵌套的数据块会掩盖哪个节点编写了哪个字段的信息,还会导致在中断后无法继续执行。
pip install langgraph langchain langchain-openai langgraph-checkpoint-sqlite python-dotenv
OPENAI_API_KEY=your-key-here
模块1:导入与配置
将“模块1:导入与配置”视为一个可度量的对象来处理效果最佳。在扩大范围之前,需记录一份理想状态下的运行日志、一个故障案例以及回滚说明。 可将此阶段视为输入与已验证输出之间的契约。为相关成果命名,明确成功标准,杜绝默许的半完成状态。 保持图表状态的简洁性与类型一致性。嵌套的数据块会掩盖具体是哪个节点修改了哪个字段,且在中断后会导致无法继续处理。 将“模块1:导入与配置”视为一个可度量的对象来处理效果最佳。在扩大范围之前,需记录一份理想状态下的运行日志、一个故障案例以及回滚说明。 应将配置信息置于应用程序代码之外。环境文件、密钥存储及功能开关应集中存放于一个位置,以便操作人员无需查看整个图表即可进行审计。
# ============================================================
# MODULE 1: IMPORTS & CONFIGURATION
# ============================================================
import os
import sqlite3
from typing import Annotated, Literal
from datetime import datetime
from dotenv import load_dotenv
# LangChain - the AI layer
from langchain_openai import ChatOpenAI
from langchain_core.messages import (
HumanMessage,
AIMessage,
SystemMessage,
BaseMessage,
RemoveMessage,
)
from langchain_core.tools import tool
# LangGraph - the graph layer
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command
load_dotenv()
# ── The LLM ─────────────────────────────────────────────────
# temperature=0 means deterministic - the agent behaves
# consistently, which is what you want for a support bot.
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
模块2:状态
在模块2的状态阶段,应在修改代码之前明确输入参数、该步骤的负责人以及退出标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏的状态。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理都是产品本身的组成部分,而非后续需要补充的内容。 对于涉及资金支出或修改生产数据的操作,必须经过人工审批。编译时的连接方式并不等同于业务功能的完整性。
# ============================================================
# MODULE 2: STATE
# ============================================================
class SupportState(MessagesState):
# MessagesState already gives us:
# messages: Annotated[list[BaseMessage], add_messages]
# We add three more fields for our specific needs:
# The running summary of the conversation (Part 2 pattern).
# Starts empty. Gets written by summarize_node when conversation gets long.
summary: str
# The name of the customer, extracted early in the conversation.
# Used to personalise every response. Starts empty.
customer_name: str
# Tracks the current ticket category, set by the agent.
# Helps the human reviewer understand context during escalation.
# Values: "order_inquiry" | "refund_request" | "complaint" | "general"
ticket_category: str
为何选择这三个字段?
在修改代码之前,需明确这三个阶段的理由、输入内容、该步骤的负责人以及退出标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。相比庞大的脚本,更应采用小型且可测试的单元。当某个步骤失败时,故障原因应能指向单一责任主体,而非复杂的流程链。对于涉及资金支出或修改生产数据的操作,必须经过人工审批。编译时的逻辑连接并不等同于业务功能的完整性。
模块3:工具
在模块3工具阶段,修改代码之前需明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 应将此阶段视为输入与已验证输出之间的契约。为相关成果命名,定义成功检测标准,并拒绝默许的半完成状态。 在网关处进行身份验证,在数据层再次授权。仅凭承载令牌并不足以界定租户边界。 在模块3工具阶段,修改代码之前需明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 将配置信息置于应用程序代码之外。环境文件、密钥存储以及功能标志应集中存放于一个操作人员可审计的位置,无需查看整个系统结构。
# ============================================================
# MODULE 3: TOOLS
# ============================================================
# ── Fake Database ────────────────────────────────────────────
# In a real project, these would be database queries or API calls.
# For learning purposes, we use a simple Python dictionary.
ORDERS_DB = {
"ORD-001": {
"customer": "Alex",
"product": "Wireless Headphones",
"status": "Delivered",
"amount": 89.99,
"delivery_date": "2025-06-10",
},
"ORD-002": {
"customer": "Sam",
"product": "Phone Case",
"status": "In Transit",
"amount": 14.99,
"delivery_date": "Expected 2025-06-18",
},
"ORD-003": {
"customer": "Jordan",
"product": "Laptop Stand",
"status": "Processing",
"amount": 45.00,
"delivery_date": "Expected 2025-06-20",
},
}
@tool
def lookup_order(order_id: str) -> str:
"""Look up the details of a customer's order by order ID.
Use this when the customer provides an order number and wants
to know the status, product name, or delivery date of their order.
Args:
order_id: The order ID string, e.g. 'ORD-001'
Returns:
A formatted string with full order details, or an error message
if the order is not found.
"""
order = ORDERS_DB.get(order_id.upper())
if not order:
return f"No order found with ID '{order_id}'. Please double-check the order number."
return (
f"Order {order_id.upper()}: {order['product']} | "
f"Status: {order['status']} | "
f"Amount: ${order['amount']:.2f} | "
f"Delivery: {order['delivery_date']}"
)
@tool
def check_refund_eligibility(order_id: str) -> str:
"""Check whether an order is eligible for a refund.
Use this BEFORE processing any refund request. An order is eligible
for a refund only if its status is 'Delivered'. Orders Fthat are
'In Transit' or 'Processing' cannot be refunded yet.
Args:
order_id: The order ID string, e.g. 'ORD-001'
Returns:
A string stating whether the order is eligible and why.
"""
order = ORDERS_DB.get(order_id.upper())
if not order:
return f"Cannot check refund: order '{order_id}' not found."
if order["status"] == "Delivered":
return (
f"Order {order_id.upper()} IS eligible for a refund. "
f"Product: {order['product']}, Amount: ${order['amount']:.2f}. "
f"Proceed to refund processing."
)
else:
return (
f"Order {order_id.upper()} is NOT eligible for a refund yet. "
f"Current status: {order['status']}. Refunds are only available "
f"for delivered orders."
)
@tool
def process_refund(order_id: str, reason: str) -> str:
"""Process a refund for a delivered order.
IMPORTANT: This tool actually issues the refund. It should only be
called AFTER human approval has been obtained. Never call this tool
without prior confirmation.
Args:
order_id: The order ID to refund
reason: The customer's stated reason for the refund
Returns:
A confirmation string with the refund reference number.
"""
order = ORDERS_DB.get(order_id.upper())
if not order:
return f"Refund failed: order '{order_id}' not found."
# In a real system, this would hit your payments API.
refund_ref = f"REF-{order_id.upper()}-{datetime.now().strftime('%H%M%S')}"
return (
f"Refund APPROVED and PROCESSED. Reference: {refund_ref}. "
f"${order['amount']:.2f} will be returned to the original payment method "
f"within 3–5 business days. Reason logged: '{reason}'."
)
# ── Collect tools and bind to LLM ───────────────────────────
# All three tools in one list.
tools = [lookup_order, check_refund_eligibility, process_refund]
# llm_with_tools = the LLM that KNOWS about the tools and can decide to call them.
# This is what we use inside agent_node.
llm_with_tools = llm.bind_tools(tools)
# tool_node = the pre-built node that EXECUTES whatever tool the LLM chose.
# This is what we register in Module 6.
tool_node = ToolNode(tools)
文档字符串规则——再讲一次
在处理“文档字符串规则”这一阶段时,首先需明确接口规范:所需的输入参数、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误消息处理都是产品功能的一部分,而非后续需要补充的内容。 在耗时较高的步骤之后设置检查点。当操作员重新尝试某个后续节点时,恢复流程不应再次调用相同的大型语言模型。
模块4:节点
在处理第4模块的节点阶段时,首先写下合约内容:所需的输入参数、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 优先选择小型且可测试的单元,而非冗长的脚本。当某个步骤失败时,故障应指向单一的责任模块,而非复杂的流程链。 在成本较高的步骤之后设置检查点。当操作员重新尝试后续节点时,恢复流程不应再次调用相同的LLM接口。
# ============================================================
# MODULE 4: NODES
# ============================================================
# ── The System Prompt ────────────────────────────────────────
# Written once, used in every call to the LLM from agent_node.
# This is the personality and rulebook of your agent.
SYSTEM_PROMPT = """You are ShopBot, a friendly and professional customer support \
agent for an e-commerce store.
Your capabilities:
- Look up order details using the lookup_order tool
- Check if an order qualifies for a refund using check_refund_eligibility
- Process approved refunds using the process_refund tool
Your rules:
- Always greet the customer by name once you know it
- Always check refund eligibility BEFORE attempting to process a refund
- For refund requests, set ticket_category to "refund_request" in your reasoning
- Be empathetic, clear, and concise
- If you cannot help, offer to escalate to a human agent
Important: The process_refund tool requires prior human approval. Do not call it \
unless the conversation shows that a human has already approved the refund."""
# ── Node 1: agent_node ──────────────────────────────────────
def agent_node(state: SupportState) -> dict:
"""The brain of the operation. Reads state, calls the LLM, and decides
whether to use a tool, give a final answer, or do something else.
This node handles two cases:
1. Normal conversation - just call the LLM and respond
2. Long conversation - if a summary exists, prepend it so the LLM
has context without seeing all the raw messages
"""
# Part 2 pattern: check for an existing summary
summary = state.get("summary", "")
if summary:
# Build context: system prompt + compressed history + recent messages
system_with_summary = SystemMessage(
content=f"{SYSTEM_PROMPT}\n\nSummary of conversation so far:\n{summary}"
)
messages_to_send = [system_with_summary] + state["messages"]
else:
# No summary yet - full history is short enough to send as-is
system_msg = SystemMessage(content=SYSTEM_PROMPT)
messages_to_send = [system_msg] + state["messages"]
# Call the LLM. It sees tools and can choose to call one.
response = llm_with_tools.invoke(messages_to_send)
# Detect ticket category from the response for routing purposes.
# A smarter version would have the LLM explicitly set this -
# for now, we scan for keywords.
content_lower = response.content.lower() if response.content else ""
updates: dict = {"messages": [response]}
if "refund" in content_lower or (
hasattr(response, "tool_calls")
and any("refund" in str(tc).lower() for tc in (response.tool_calls or []))
):
updates["ticket_category"] = "refund_request"
return updates
# ── Node 2: review_refund ───────────────────────────────────
def review_refund(state: SupportState) -> dict:
"""The human approval gate. Pauses execution, shows the pending refund
details to a human agent, and waits for their decision.
This implements the Part 3 interrupt() pattern. Execution stops here
until someone calls graph.invoke(Command(resume=...), config).
Three outcomes the human can choose:
- "approve" → let the refund tool call proceed unchanged
- "reject" → cancel the refund, send a message to the customer
- "escalate" → hand the entire ticket to a human support agent
"""
last_message = state["messages"][-1]
# Find the refund-related tool call in the last AI message.
# We look for process_refund specifically - the "real action" tool.
refund_tool_call = None
if hasattr(last_message, "tool_calls"):
for tc in last_message.tool_calls:
if "refund" in tc["name"].lower():
refund_tool_call = tc
break
# Surface the context to the human reviewer via interrupt().
# Everything in this dict is what the human sees before deciding.
human_decision = interrupt({
"message": "⚠️ Refund approval required",
"customer_name": state.get("customer_name", "Unknown"),
"tool_being_called": refund_tool_call["name"] if refund_tool_call else "refund tool",
"arguments": refund_tool_call["args"] if refund_tool_call else {},
"conversation_summary": state.get("summary", "No summary yet"),
"options": ["approve", "reject", "escalate"],
})
# ── Handle the human's decision ─────────────────────────
if human_decision == "approve":
# Do nothing to state - let tool_node execute the tool call as-is
return {}
elif human_decision == "reject":
# Cancel the tool call. The LLM will see a ToolMessage explaining why,
# and generate a polite response to the customer.
from langchain_core.messages import ToolMessage
return {
"messages": [
ToolMessage(
content=(
"Refund request was reviewed and declined by our support team. "
"Please inform the customer politely and offer alternatives."
),
tool_call_id=refund_tool_call["id"] if refund_tool_call else "unknown",
)
]
}
elif human_decision == "escalate":
# Signal escalation - in a real system you'd open a ticket,
# ping Slack, or transfer to a live agent queue.
from langchain_core.messages import ToolMessage
return {
"messages": [
ToolMessage(
content=(
"This ticket has been escalated to a senior support agent. "
"Inform the customer that a human agent will contact them "
"within 2 business hours."
),
tool_call_id=refund_tool_call["id"] if refund_tool_call else "unknown",
)
]
}
# Fallback - treat as approve
return {}
# ── Node 3: summarize_node ──────────────────────────────────
def summarize_node(state: SupportState) -> dict:
"""Triggered when the conversation exceeds 6 messages. Compresses the
full message history into a short summary, then deletes old raw messages.
This is the rolling summary pattern from Part 2. The summary grows
richer turn by turn. Token costs stay nearly flat no matter how long
the conversation runs.
"""
existing_summary = state.get("summary", "")
if existing_summary:
# Extend the existing summary with new messages
summary_instruction = (
f"Current summary:\n{existing_summary}\n\n"
"Extend this summary with the new messages above. "
"Keep it under 5 sentences. Focus on: the customer's name, "
"their issue, any orders mentioned, and what actions were taken."
)
else:
# First time summarising
summary_instruction = (
"Summarise this customer support conversation in under 5 sentences. "
"Include: the customer's name (if mentioned), their issue, "
"any order numbers discussed, and what actions were taken so far."
)
messages = state["messages"] + [HumanMessage(content=summary_instruction)]
response = llm.invoke(messages) # Plain llm, no tools needed here
# Delete all but the 2 most recent messages.
# The summary now holds everything that was in the deleted messages.
messages_to_delete = [
RemoveMessage(id=m.id) for m in state["messages"][:-2]
]
return {
"summary": response.content,
"messages": messages_to_delete,
}
第5模块:边与路由
在处理第5模块“边缘路由”阶段时,首先需明确相关约定:所需输入、成功信号以及部分失败时的处理方式。这份清单能确保后续的代码修改始终符合约定。 将此阶段视为输入与验证后输出之间的契约。为相关组件命名,定义成功判定标准,杜绝无声的半完成状态。 在成本较高的步骤之后设置检查点。当操作员重新尝试后续节点时,恢复流程不应再次调用相同的LLM接口。 在处理第5模块“边缘路由”阶段时,首先需明确相关约定:所需输入、成功信号以及部分失败时的处理方式。这份清单能确保后续的代码修改始终符合约定。 将配置信息置于应用程序代码之外。环境文件、密钥存储及功能开关应集中存放于一个位置,以便操作员无需查看整个流程即可进行审计。
# ============================================================
# MODULE 5: EDGES & ROUTING
# ============================================================
def route_after_agent(state: SupportState) -> Literal[
"review_refund", "tools", "summarize_node", "__end__"
]:
"""Called after agent_node runs. Decides what happens next.
Four possible routes:
1. The LLM wants to call process_refund → must go through human review first
2. The LLM wants to call any other tool → go directly to tool_node
3. The LLM gave a plain text answer AND the conversation is long → summarise
4. The LLM gave a plain text answer and conversation is short → we're done
"""
last_message = state["messages"][-1]
has_tool_calls = hasattr(last_message, "tool_calls") and bool(last_message.tool_calls)
if has_tool_calls:
# Check if ANY of the tool calls is the sensitive process_refund tool
tool_names = [tc["name"] for tc in last_message.tool_calls]
if "process_refund" in tool_names:
return "review_refund" # → Pause for human approval first
return "tools" # → Safe tool, run it directly
# No tool call - the LLM gave a plain response.
# Check if the conversation is long enough to need summarisation.
if len(state["messages"]) > 6:
return "summarize_node"
return "__end__" # → Conversation turn is complete
def route_after_review(state: SupportState) -> Literal["tools", "agent_node"]:
"""Called after review_refund runs (i.e., after the human has decided).
Two routes:
1. Human approved or escalated → run the tool (tool_node handles the call)
2. Human rejected → the review node already added a ToolMessage cancelling
the tool call, so skip tool_node and go back to agent_node to respond
"""
last_message = state["messages"][-1]
# If the last message is a ToolMessage, the review node cancelled the call.
# Go back to agent_node so it can generate a customer-facing response.
from langchain_core.messages import ToolMessage
if isinstance(last_message, ToolMessage):
return "agent_node"
# Otherwise, the review node returned {} (approved) - proceed to tools.
return "tools"
第6模块:图结构构建
模块6:将图结构视为可测量的表面来处理时,其功能表现最佳。在扩大范围之前,需记录一个成功的案例、一个失败案例以及回滚说明。同时记录正常流程与恢复流程的文档。重试机制、人工审核环节以及死信处理都是产品本身的一部分,而非后续需要补充的内容。要保持图结构的层级简单且类型明确,嵌套的数据块会掩盖哪个节点编写了哪个字段的信息,并且在中断后会导致流程无法继续。
# ============================================================
# MODULE 6: GRAPH ASSEMBLY
# ============================================================
# ── Step 1: Initialize ──────────────────────────────────────
graph_builder = StateGraph(SupportState)
# ── Step 2: Register All Nodes ──────────────────────────────
# Format: add_node("string_name", function)
# The string name is what you use in every edge definition below.
graph_builder.add_node("agent_node", agent_node)
graph_builder.add_node("tools", tool_node) # Pre-built from Module 3
graph_builder.add_node("review_refund", review_refund)
graph_builder.add_node("summarize_node", summarize_node)
# ── Step 3: Set Entry Point ─────────────────────────────────
# The first node that runs when a user sends a message.
graph_builder.add_edge(START, "agent_node")
# ── Step 4: Wire the Edges ──────────────────────────────────
# After agent_node: conditional - depends on what the LLM decided
graph_builder.add_conditional_edges(
"agent_node", # Source
route_after_agent, # Router function from Module 5
{
"review_refund": "review_refund", # Refund tool → human review first
"tools": "tools", # Other tools → run directly
"summarize_node": "summarize_node", # Long conversation → summarise
"__end__": END, # Plain answer → done
}
)
# After review_refund: conditional - depends on human's decision
graph_builder.add_conditional_edges(
"review_refund",
route_after_review,
{
"tools": "tools", # Approved → execute the tool
"agent_node": "agent_node", # Rejected → back to agent to respond
}
)
# After tools run: always go back to agent_node
# (the ReAct loop - agent sees tool result, decides what to do next)
graph_builder.add_edge("tools", "agent_node")
# After summarization: conversation turn is done
graph_builder.add_edge("summarize_node", END)
# ── Step 5: Compile ─────────────────────────────────────────
# Using MemorySaver for development.
# For production, swap this one line to SqliteSaver or PostgresSaver.
memory = MemorySaver()
shopbot = graph_builder.compile(checkpointer=memory)
可视化图结构(可选但推荐)
将“可视化图表”这一可选阶段视为可度量的表面来处理效果最佳。在扩大范围之前,先记录一个成功的案例、一个失败案例以及回滚说明。 优先选择小型且可测试的单元,而非庞大的脚本。当某个步骤失败时,故障应指向单一的责任主体,而非复杂的流程链。 保持图表状态的简洁性与类型化。嵌套的数据结构会掩盖哪个节点编写了哪个字段的信息,还会在中断后导致无法继续处理。
from IPython.display import display, Image
from langchain_core.runnables.graph import MermaidDrawMethod
display(Image(
shopbot.get_graph().draw_mermaid_png(
draw_method=MermaidDrawMethod.API
)
))
模块7:入口点
模块7:将入口点视为可度量的界面时,其功能表现最佳。在扩大范围之前,需记录一份理想运行案例、一个故障场景以及回滚说明。 将此阶段视为输入与已验证输出之间的契约。为相关成果命名,明确成功标准,杜绝默许的半完成状态。 保持图结构简洁且类型明确。嵌套的数据块会掩盖哪个节点修改了哪个字段,还会在中断后导致无法继续处理。 模块7:将入口点视为可度量的界面时,其功能表现最佳。在扩大范围之前,需记录一份理想运行案例、一个故障场景以及回滚说明。 将配置置于应用程序代码之外。环境文件、密钥存储和功能开关应集中存放于一处,以便操作人员无需查看整个图结构即可进行审计。
# ============================================================
# MODULE 7: ENTRYPOINT
# ============================================================
def run_shopbot():
"""
Interactive command-line session with ShopBot.
Demonstrates: multi-turn conversation, tool use, and human-in-the-loop.
"""
print("=" * 55)
print(" ShopBot - Customer Support Agent")
print(" Powered by LangGraph")
print("=" * 55)
print("Type your message below. Type 'exit' to quit.")
print("Type 'state' to inspect what ShopBot currently remembers.\n")
# One config per session.
# thread_id is the session key - same ID = same memory thread.
# Change the ID to start a completely fresh conversation.
config = {"configurable": {"thread_id": "customer-session-001"}}
while True:
user_input = input("You: ").strip()
if not user_input:
continue
if user_input.lower() == "exit":
print("ShopBot: Thank you for contacting support. Have a great day!")
break
# ── Debug: inspect current state ────────────────────
if user_input.lower() == "state":
snapshot = shopbot.get_state(config)
print("\n[DEBUG] Current State:")
print(f" Messages in state : {len(snapshot.values.get('messages', []))}")
print(f" Customer name : {snapshot.values.get('customer_name', '(not set)')}")
print(f" Ticket category : {snapshot.values.get('ticket_category', '(not set)')}")
print(f" Summary : {snapshot.values.get('summary', '(none yet)')}")
print(f" Next node(s) : {snapshot.next}\n")
continue
# ── Normal message: invoke the graph ─────────────────
result = shopbot.invoke(
{"messages": [HumanMessage(content=user_input)]},
config=config,
)
# ── Check if graph paused for human approval ─────────
# This is how you detect that interrupt() was called inside review_refund.
while "__interrupt__" in result:
interrupt_data = result["__interrupt__"][0].value
print("\n" + "=" * 55)
print(" HUMAN APPROVAL REQUIRED")
print("=" * 55)
print(f" Customer : {interrupt_data.get('customer_name', 'Unknown')}")
print(f" Action : {interrupt_data.get('tool_being_called', 'refund')}")
print(f" Arguments : {interrupt_data.get('arguments', {})}")
print(f" Context : {interrupt_data.get('conversation_summary', 'N/A')}")
print("=" * 55)
print("Options: [a] Approve [r] Reject [e] Escalate")
human_choice = input("Your decision: ").strip().lower()
if human_choice == "a":
resume_value = "approve"
elif human_choice == "r":
resume_value = "reject"
elif human_choice == "e":
resume_value = "escalate"
else:
print("Invalid choice. Defaulting to reject.")
resume_value = "reject"
# Resume the graph with the human's decision.
# Command(resume=...) answers the pending interrupt() call.
result = shopbot.invoke(
Command(resume=resume_value),
config=config,
)
# ── Print the agent's final response ─────────────────
last_message = result["messages"][-1]
print(f"\nShopBot: {last_message.content}\n")
if __name__ == "__main__":
run_shopbot()
实际运行:真实对话的样貌
在修改代码之前,首先要明确“Running It What a stage”流程中的输入参数、各步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理措施都是产品不可或缺的部分,而非后续需要补充的内容。 对于涉及资金支出或修改生产数据的操作,必须经过人工审批。仅靠编译时的配置并不能保证业务的完整性。
=======================================================
ShopBot — Customer Support Agent
Powered by LangGraph
=======================================================
Type your message below. Type 'exit' to quit.
Type 'state' to inspect what ShopBot currently remembers.
You: Hi, my name is Alex and I need help with order ORD-001
ShopBot: Hi Alex! I'd be happy to help you with order ORD-001.
I can see your order:
- Product: Wireless Headphones
- Status: Delivered on 2025-06-10
- Amount: $89.99
What can I help you with regarding this order?
You: I want to return it and get a refund
ShopBot: I've checked your order ORD-001 and it's eligible for a refund
since it's been delivered. Let me process that for you right away.
=======================================================
HUMAN APPROVAL REQUIRED
=======================================================
Customer : Alex
Action : process_refund
Arguments : {'order_id': 'ORD-001', 'reason': 'Customer requested return'}
Context : No summary yet
=======================================================
Options: [a] Approve [r] Reject [e] Escalate
Your decision: a
ShopBot: Great news, Alex! Your refund has been successfully processed.
Refund Reference: REF-ORD-001-143022
Amount: $89.99 will be returned to your original payment method within
3–5 business days.
Is there anything else I can help you with?
You: state
[DEBUG] Current State:
Messages in state : 6
Customer name : (not set)
Ticket category : refund_request
Summary : (none yet)
Next node(s) : ()
You: exit
ShopBot: Thank you for contacting support. Have a great day!
你刚刚构建了什么以及为何它很重要
在“你刚刚构建了什么”阶段,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 优先选择小型、可测试的单元,而非庞大的脚本。当某个步骤失败时,故障应指向单一的责任主体,而非错综复杂的流程。 对于涉及资金支出或修改生产数据的操作,必须经过人工审批。编译时的连接方式并不等同于业务功能的完整性。
扩展此项目(你的下一步行动)
在“扩展此项目”阶段,修改代码之前需明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 将此阶段视为输入与已验证输出之间的契约。为相关成果命名,定义成功判定标准,杜绝默许的半完成状态。 对于涉及资金支出或修改生产数据的操作,必须经过人工审批。编译时的配置并不等同于业务上的完整性。 在“扩展此项目”阶段,修改代码之前需明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 将配置信息置于应用程序代码之外。环境文件、密钥存储以及功能开关应集中存放于操作人员可审计的位置,无需阅读整个代码文件。
图表。
该项目的关键词总结
在完成“关键词总结”阶段时,首先需记录下相关约定:所需输入、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 同时记录正常流程与异常恢复流程。重试机制、人工审核环节以及错误处理都属于产品本身的功能,而非后续的优化内容。 在成本较高的步骤之后设置检查点。当操作员重新尝试某个节点时,恢复流程不应再次调用相同的大型语言模型。
结论:地图不等于实际地域
在处理“结论:地图已绘就”这一阶段时,首先写下相关约定:所需的输入参数、成功标志,以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 优先选择小型且可测试的单元,而非庞大的脚本。当某个步骤失败时,故障应指向单一的责任模块,而非复杂的流程链。 在成本较高的步骤之后设置检查点。当操作员重新尝试后续节点时,恢复流程不应再次调用相同的大型语言模型。
本项目第二部分:优化用于现实世界电子商务的LangGraph智能体
在处理“第二阶段”时,首先写下契约:所需的输入、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 将这一阶段视为输入与验证后输出之间的契约。为相关成果命名,明确成功判定标准,杜绝无声的半完成状态。 在成本较高的步骤之后设置检查点。当操作员重新尝试后续节点时,恢复流程不应再次调用相同的大型语言模型。
操作检查清单
在“操作检查清单”阶段,应在修改代码之前明确输入内容、该步骤的负责人以及退出标准。操作员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。
在功能结果旁记录处理时间以及令牌或查询成本。提前了解成本情况,可避免在系统从演示环境切换到共享环境时出现意外账单。
对于会产生费用或修改生产数据的操作,必须经过人工审批。仅靠编译时的配置并不足以确保业务功能的完整性。
编写简短的操作手册:说明如何轮换密钥、如何清空队列、以及如何回滚上一次的数据导入操作。
将配置信息与应用程序代码分开存放。环境文件、密钥存储和功能开关应集中于一个位置,以便操作人员无需查看整个系统结构即可进行审计。
对于会产生费用或修改生产数据的操作,必须经过人工审批。仅靠编译时的配置并不足以确保业务功能的完整性。
在推广该技术栈之前,应先冻结版本,为关键流程记录标准输出文本,并明确回滚步骤。共享环境需要设置速率限制、租户验证机制,以及负责密钥轮换的明确责任人。与其展示花哨的一次性演示,不如注重扎实的可靠性。
针对 ac5eb00f923a 的批量说明:不要将提供商密钥放入代码仓库,为每个会话设置令牌使用上限,并将输出文本存储在评估用示例文件旁边,以便后续模型更换时保持对比性。
针对 ac5eb00f923a 的重写标记1:用操作员用语重新表述相关内容,保留 [[CODE_n]] 占位符不变,避免重复原文句子。
针对 ac5eb00f923a 的重写标记2:用操作员用语重新表述相关内容,保留 [[CODE_n]] 占位符不变,避免重复原文句子。
为 ac5eb00f923a 的标记 3 进行重写:用操作员语言重新表述周围的陈述,保持 [[CODE_n]] 占位符不变,且避免重复源文本句子。
为 ac5eb00f923a 的标记 4 进行重写:用操作员语言重新表述周围的陈述,保持 [[CODE_n]] 占位符不变,且避免重复源文本句子。
为 ac5eb00f923a 的标记 5 进行重写:用操作员语言重新表述周围的陈述,保持 [[CODE_n]] 占位符不变,且避免重复源文本句子。
为 ac5eb00f923a 的标记 6 进行重写:用操作员语言重新表述周围的陈述,保持 [[CODE_n]] 占位符不变,且避免重复源文本句子。
为 ac5eb00f923a 的标记 7 进行重写:用操作员语言重新表述周围的陈述,保持 [[CODE_n]] 占位符不变,且避免重复源文本句子。
为 ac5eb00f923a 的标记 8 进行重写:用操作符语言重新表述周围的声明,保持 [[CODE_n]] 占位符不变,避免重复源句内容。
为 ac5eb00f923a 的标记 9 进行重写:用操作符语言重新表述周围的声明,保持 [[CODE_n]] 占位符不变,避免重复源句内容。
为 ac5eb00f923a 的标记 10 进行重写:用操作符语言重新表述周围的声明,保持 [[CODE_n]] 占位符不变,避免重复源句内容。