Главная / Статьи / Практические советы: Ваш первый настоящий проект LangGraph — создание системы поддержки клиентов

Практические советы: Ваш первый настоящий проект LangGraph — создание системы поддержки клиентов

Пошаговое руководство по практическим заметкам: ваш первый настоящий проект LangGraph — создание системы поддержки клиентов с контрактами, проверками и слотами для вставки кода для команд, использующих эту схему.

5139 слов

В следующих заметках описан практический подход к реализации проекта «Ваш первый настоящий проект LangGraph: создание агента поддержки клиентов». Основное внимание уделяется контрактам, проверкам и местам для вставки кода, а не мотивирующим формулировкам. На этапе обзора сначала запишите условия работы: необходимые входные данные, сигнал успешного выполнения и действия при частичной неудаче. Такой список поможет сохранять честность при последующих изменениях кода. Храните настройки вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь граф.

Прежде чем написать хотя бы одну строку: разберитесь с планом

Этап «Прежде чем писать» работает наилучшим образом, когда его рассматривают как измеримую поверхность. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работ. Документируйте одновременно успешный и восстановительный пути выполнения. Повторные попытки, проверки человеком и обработка неработающих сообщений являются частью продукта, а не этапом последующей доработки. Сохраняйте структуру графа простой и типизированной. Вложенные структуры скрывают информацию о том, какой узел заполнил какое поле, и мешают возобновлению работы после прерываний.

Настройка

Этап настройки работает наилучшим образом, когда его рассматривают как измеримую поверхность. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работ. Предпочитайте небольшие, тестируемые единицы кода вместо обширных скриптов. Когда какой-то шаг сбивается, причина сбоя должна указывать на конкретную ответственность, а не на запутанную цепочку операций. Сохраняйте структуру графа простой и типизированной. Вложенные структуры скрывают информацию о том, какой узел заполнил какое поле, и мешают возобновлению работы после прерываний.

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 сначала запишите контракт: необходимые входные данные, сигнал о успешном выполнении и то, что происходит при частичной неудаче. Такой чек-лист поможет сохранять честность при последующих изменениях кода. Предпочитайте небольшие, тестируемые единицы кода вместо обширных скриптов. Когда какой-то шаг терпит неудачу, ошибка должна указывать на конкретную ответственность, а не на запутанную цепочку операций. Выполняйте контрольные точки после дорогостоящих шагов. Функция возобновления работы не должна повторно оплачивать один и тот же вызов большой языковой модели, когда оператор пытается выполнить более поздний узел.

# ============================================================
# 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 сначала запишите контракт: необходимые входные данные, сигнал о успехе и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия элементам, определите критерии успеха и не допускайте безответственного частичного выполнения задач. Выполняйте контрольные точки после дорогостоящих операций. Система возобновления работы не должна повторно оплачивать один и тот же вызов большой языковой модели, когда оператор пытается выполнить следующий узел заново. При работе над этапом маршрутизации краев в Модуле 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: Entrypoint работает наилучшим образом, когда его рассматривают как измеримую структуру. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объёма работ. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия всем элементам, определите критерии успеха и не соглашайтесь на молчаливое частичное выполнение задачи. Сохраняйте структуру графа простой и типизированной. Вложенные структуры скрывают информацию о том, какой узел заполнил тот или иной поле, что приводит к нарушению возобновления работы после перерывов. Модуль 7: Entrypoint работает наилучшим образом, когда его рассматривают как измеримую структуру. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объёма работ. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь граф.

# ============================================================
# 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()

Запуск: как выглядит реальный диалог

Для реализации этого этапа необходимо заранее определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Необходимо одновременно задокументировать успешный сценарий выполнения и сценарий восстановления. Повторные попытки, проверки человеком и обработка неработоспособных сообщений являются частью продукта, а не элементами последующей доработки. Внедрять утверждение человеком для операций, связанных с тратой денег или изменением производственных данных. Подключение компонентов во время компиляции не гарантирует полноты функционала продукта.

=======================================================
  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: не храните ключи поставщика в репозитории, установите лимит токенов на сессию и сохраняйте отчеты рядом с фиксами для оценки, чтобы последующие замены моделей оставались сопоставимыми.

Маркер переписи 1 для ac5eb00f923a: перефразируйте соседние утверждения на языке операторов, сохраните слоты [[CODE_n]] без изменений и избегайте повторения предложений исходного текста.

Маркер переписи 2 для ac5eb00f923a: перефразируйте соседние утверждения на языке операторов, сохраните слоты [[CODE_n]] без изменений и избегайте повторения предложений исходного текста.

Перепишите маркер 3 для ac5eb00f923a: перефразируйте сопутствующие утверждения на языке операторов, сохраните поле [[CODE_n]] без изменений и избегайте повторения предложений исходного текста.

Перепишите маркер 4 для ac5eb00f923a: перефразируйте сопутствующие утверждения на языке операторов, сохраните поле [[CODE_n]] без изменений и избегайте повторения предложений исходного текста.

Перепишите маркер 5 для ac5eb00f923a: перефразируйте сопутствующие утверждения на языке операторов, сохраните поле [[CODE_n]] без изменений и избегайте повторения предложений исходного текста.

Перепишите маркер 6 для ac5eb00f923a: перефразируйте сопутствующие утверждения на языке операторов, сохраните поле [[CODE_n]] без изменений и избегайте повторения предложений исходного текста.

Перепишите маркер 7 для ac5eb00f923a: перефразируйте сопутствующие утверждения на языке операторов, сохраните поле [[CODE_n]] без изменений и избегайте повторения предложений исходного текста.

Перепишите маркер 8 для ac5eb00f923a: перефразируйте сопутствующие утверждения на языке операторов, сохраните слоты [[CODE_n]] без изменений и избегайте повторения предложений исходного текста.

Перепишите маркер 9 для ac5eb00f923a: перефразируйте сопутствующие утверждения на языке операторов, сохраните слоты [[CODE_n]] без изменений и избегайте повторения предложений исходного текста.

Перепишите маркер 10 для ac5eb00f923a: перефразируйте сопутствующие утверждения на языке операторов, сохраните слоты [[CODE_n]] без изменений и избегайте повторения предложений исходного текста.