Accueil / Articles

Les agents Catalogue et Facture de LangGraph avec la visibilité offerte par LangSmith.

Les agents de catalogue et de facturation dans LangGraph : routage supervisé, interruptions par un intervenant humain, mémoire à long terme et boucles d’évaluation LangSmith qui assurent l’exactitude.

Éditeur : REACTAPP.TOP21 min de lecture4721 mots

Sources: Building a Multi-Agent AI System with LangGraph and LangSmith

LangSmith existe afin que les systèmes multi-agents soient observables : des traces, des évaluations et des exécutions de bases de données qui montrent ce que chaque nœud a vu et renvoyé. Cette version relie un spécialiste du catalogue et un spécialiste des factures sous un LangGraph de type superviseur, puis ajoute une intervention humaine et une mémoire à long terme.

But de LangSmith

L’utilisez comme outil d’observabilité et d’évaluation pour les applications LLM — et non comme orchestrateur en soi. La capacité de débogage est ce qui permet de gérer honnêtement la complexité des systèmes multi-agents.

Architecture multi-agents

Un graphique parent dirige les flux vers des sous-agents chargés du catalogue ou des factures, chacun disposant d’outils et de prompts adaptés à ce domaine. Un état partagé transmet des messages, l’identifiant du client ainsi que des indicateurs pour les interruptions.

import urllib.request
from langchain_community.utilities.sql_database import SQLDatabase
from sqlalchemy import create_engine

def get_northwind_db():
    db_path = "/tmp/northwind.db"
    url = "https://raw.githubusercontent.com/jpwhite3/northwind-SQLite3/main/dist/northwind.db"
    urllib.request.urlretrieve(url, db_path)
    engine = create_engine(f"sqlite:///{db_path}")
    return SQLDatabase(engine)

db = get_northwind_db()

Intégrez ce fragment dans le graphique avant de passer à l’étape suivante ; les graphiques incomplets masquent les erreurs de routage jusqu’au jour de la démonstration.

import os

os.environ["OPENAI_API_KEY"] = "your-openai-api-key"
os.environ["LANGSMITH_API_KEY"] = "your-langsmith-api-key"
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_PROJECT"] = "your-langsmith-project"

from langsmith import utils
print(f"LangSmith tracing is enabled: {utils.tracing_is_enabled()}")

Testez cette unité de manière isolée si possible, puis assemblez-la. L’isolation permet de détecter tôt les erreurs dans le schéma des outils.

from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import MemorySaver
from langgraph.store.memory import InMemoryStore

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# Long-term memory: persists user preferences across conversations
in_memory_store = InMemoryStore()

# Short-term memory (checkpointer): maintains context within a single session
checkpointer = MemorySaver()

La liste ci-dessus représente une étape concrète du processus de construction — maintenez les noms stables afin que les traces générées par LangSmith restent comparables d’une version à l’autre.

from typing import Annotated, List
from typing_extensions import TypedDict
from langgraph.graph.message import AnyMessage, add_messages
from langgraph.managed.is_last_step import RemainingSteps

class State(TypedDict):
    """State schema for the multi-agent customer support workflow."""

    # Verified 5-letter Northwind customer code (e.g. "ALFKI")
    customer_id: str

    # Conversation history with automatic message aggregation
    messages: Annotated[List[AnyMessage], add_messages]

    # User preferences and context loaded from long-term memory store
    loaded_memory: str

    # Counter to prevent infinite recursion in agent workflow
    remaining_steps: RemainingSteps

Intégrez ce fragment au graphe avant de passer à l’étape suivante ; les graphes partiels masquent les erreurs de routage jusqu’au jour de la démonstration.

import ast
from langchain_core.tools import tool

@tool
def search_products_by_name(name: str) -> str:
    """
    Search for products by name in the Northwind catalog.

    Args:
        name (str): Full or partial product name to search for.

    Returns:
        str: Products with category, price, and stock level.
    """
    return db.run(
        f"""
        SELECT p.ProductName, c.CategoryName, p.UnitPrice, p.UnitsInStock
        FROM Products p
        JOIN Categories c ON c.CategoryID = p.CategoryID
        WHERE p.ProductName LIKE '%{name}%'
        ORDER BY p.ProductName
        LIMIT 10;
        """,
        include_columns=True
    )

@tool
def search_products_by_category(category: str) -> str:
    """
    Look up all products in a specific category.
    Available categories: Beverages, Condiments, Confections, Dairy Products,
    Grains/Cereals, Meat/Poultry, Produce, Seafood.

    Args:
        category (str): Category name, partial match allowed.

    Returns:
        str: Products with price and stock level.
    """
    return db.run(
        f"""
        SELECT p.ProductName, c.CategoryName, p.UnitPrice, p.UnitsInStock
        FROM Products p
        JOIN Categories c ON c.CategoryID = p.CategoryID
        WHERE c.CategoryName LIKE '%{category}%'
        ORDER BY p.ProductName;
        """,
        include_columns=True
    )

@tool
def check_product_stock(product_name: str) -> str:
    """
    Check whether a specific product is currently in stock.

    Args:
        product_name (str): The product name to check.

    Returns:
        str: Stock level and whether the product is discontinued.
    """
    return db.run(
        f"""
        SELECT ProductName, UnitsInStock, Discontinued
        FROM Products
        WHERE ProductName LIKE '%{product_name}%';
        """,
        include_columns=True
    )

catalog_tools = [search_products_by_name, search_products_by_category, check_product_stock]
llm_with_catalog_tools = llm.bind_tools(catalog_tools)

Testez cette unité de manière isolée si possible, puis assemblez-la. L’isolation permet de détecter tôt les erreurs dans le schéma des outils.

from langgraph.prebuilt import ToolNode

# ToolNode executes whichever tool the LLM called
catalog_tool_node = ToolNode(catalog_tools)

La liste ci-dessus représente une étape concrète du processus de construction — maintenez les noms stables afin que les traces générées par LangSmith restent comparables d’une version à l’autre.

from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.runnables import RunnableConfig

def generate_catalog_assistant_prompt(memory: str = "None") -> str:
    """Generate the system prompt, injecting any saved customer preferences."""
    return f"""You are a product catalog specialist for Northwind Traders,
a specialty foods distributor.

You help customers find products by name or category using your tools.

Available categories: Beverages, Condiments, Confections, Dairy Products,
Grains/Cereals, Meat/Poultry, Produce, Seafood.

CORE RESPONSIBILITIES:
- Always search before answering — never invent a product, price, or stock level
- If nothing matches, say so and suggest a broader search term
- Include price and stock status when listing products

SEARCH GUIDELINES:
1. Always perform a search before concluding something is unavailable
2. Try partial name matches if exact matches fail
3. If exact matches aren't found, try alternative spellings

Prior saved customer preferences: {memory}

Message history is also attached.
"""

def catalog_assistant(state: State, config: RunnableConfig):
    """
    Catalog assistant node — the LLM reasoning step.

    Reads the conversation and loaded memory, then either calls a catalog
    tool or generates a final response.
    """
    memory = "None"
    if "loaded_memory" in state:
        memory = state["loaded_memory"]

    prompt = generate_catalog_assistant_prompt(memory)

    response = llm_with_catalog_tools.invoke(
        [SystemMessage(content=prompt)] + state["messages"]
    )

    return {"messages": [response]}

Intégrez ce fragment au graphe avant de passer à l’étape suivante ; les graphes partiels masquent les erreurs de routage jusqu’au jour de la démonstration.

from langgraph.graph import StateGraph, START, END

def should_continue(state: State, config: RunnableConfig):
    """
    Conditional edge: checks the last message for tool_calls.
    If tool calls exist, route to the tool node. Otherwise end.
    """
    last_message = state["messages"][-1]
    if not last_message.tool_calls:
        return "end"
    else:
        return "continue"

Testez cette unité de manière isolée si possible, puis assemblez-la. L’isolation permet de détecter tôt les erreurs dans le schéma des outils.

catalog_workflow = StateGraph(State)

catalog_workflow.add_node("catalog_assistant", catalog_assistant)
catalog_workflow.add_node("catalog_tool_node", catalog_tool_node)

catalog_workflow.add_edge(START, "catalog_assistant")

catalog_workflow.add_conditional_edges(
    "catalog_assistant",
    should_continue,
    {
        "continue": "catalog_tool_node",
        "end": END,
    }
)

# After tool execution, loop back to the assistant to process the result
catalog_workflow.add_edge("catalog_tool_node", "catalog_assistant")

product_catalog_subagent = catalog_workflow.compile(
    name="product_catalog_subagent",
    checkpointer=checkpointer,
    store=in_memory_store
)

La liste ci-dessus représente une étape concrète du processus de construction — maintenez les noms stables afin que les traces générées par LangSmith restent comparables d’une version à l’autre.

import uuid

thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}

question = "Do you have any Seafood products? What are the prices?"

result = product_catalog_subagent.invoke(
    {"messages": [HumanMessage(content=question)]},
    config=config
)

for message in result["messages"]:
    message.pretty_print()

Intégrez ce fragment au graphe avant de passer à l’étape suivante ; les graphes partiels masquent les erreurs de routage jusqu’au jour de la démonstration.

@tool
def get_invoices_by_customer_sorted_by_date(customer_id: str) -> str:
    """
    Retrieve all orders for a customer sorted by date, most recent first.

    Args:
        customer_id (str): The 5-letter Northwind customer code (e.g. "ALFKI").

    Returns:
        str: Orders with order ID, date, and shipping info.
    """
    return db.run(
        f"""
        SELECT OrderID, OrderDate, ShippedDate, ShipCity, ShipCountry
        FROM Orders
        WHERE CustomerID = '{customer_id}'
        ORDER BY OrderDate DESC
        LIMIT 10;
        """,
        include_columns=True
    )

@tool
def get_invoices_sorted_by_unit_price(order_id: str) -> str:
    """
    Retrieve the line items of a specific order, highest unit price first.

    Args:
        order_id (str): The order ID number.

    Returns:
        str: Line items with product name, unit price, quantity, and discount.
    """
    return db.run(
        f"""
        SELECT p.ProductName, od.UnitPrice, od.Quantity, od.Discount
        FROM "Order Details" od
        JOIN Products p ON p.ProductID = od.ProductID
        WHERE od.OrderID = {order_id}
        ORDER BY od.UnitPrice DESC;
        """,
        include_columns=True
    )

@tool
def get_employee_by_invoice_and_customer(order_id: str, customer_id: str) -> str:
    """
    Find the Northwind employee (sales rep) who handled a specific order.

    Args:
        order_id (str): The order ID.
        customer_id (str): The 5-letter customer code.

    Returns:
        str: Employee name and title.
    """
    return db.run(
        f"""
        SELECT e.FirstName, e.LastName, e.Title
        FROM Employees e
        JOIN Orders o ON o.EmployeeID = e.EmployeeID
        WHERE o.OrderID = {order_id}
          AND o.CustomerID = '{customer_id}';
        """,
        include_columns=True
    )

invoice_tools = [
    get_invoices_by_customer_sorted_by_date,
    get_invoices_sorted_by_unit_price,
    get_employee_by_invoice_and_customer,
]

Testez cette unité de manière isolée si possible, puis assemblez-la. L’isolation permet de détecter tôt les erreurs dans le schéma des outils.

from langgraph.prebuilt import create_react_agent

invoice_subagent_prompt = """You are a subagent among a team of assistants.
You are specialized for retrieving order and invoice information for Northwind Traders customers.

You have access to three tools:
- get_invoices_by_customer_sorted_by_date: retrieves all orders for a customer, most recent first
- get_invoices_sorted_by_unit_price: retrieves the items in a specific order
- get_employee_by_invoice_and_customer: finds the sales rep who handled a specific order

CORE RESPONSIBILITIES:
- Retrieve and present order information accurately from the database
- Provide details about orders including items, prices, and shipping
- Always maintain a professional, friendly, and patient tone
"""

invoice_information_subagent = create_react_agent(
    llm,
    tools=invoice_tools,
    name="invoice_information_subagent",
    prompt=invoice_subagent_prompt,
    state_schema=State,
    checkpointer=checkpointer,
    store=in_memory_store
)

La liste ci-dessus représente une étape concrète du processus de construction — maintenez les noms stables afin que les traces générées par LangSmith restent comparables d’une version à l’autre.

thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}

question = "My customer id is ALFKI. What was my most recent order, and who was the employee that helped me with it?"

result = invoice_information_subagent.invoke(
    {"messages": [HumanMessage(content=question)]},
    config=config
)

for message in result["messages"]:
    message.pretty_print()

Intégrez ce fragment au graphe avant de passer à l’étape suivante ; les graphes partiels masquent les erreurs de routage jusqu’au jour de la démonstration.

from langgraph_supervisor import create_supervisor

supervisor_prompt = """You are an expert customer support assistant for Northwind Traders,
a specialty foods distributor.

You are dedicated to providing exceptional service and ensuring customer queries are resolved.

You have a team of two subagents that you can use to help answer customer queries:
1. product_catalog_subagent: handles questions about products, categories, prices, and stock availability.
   This agent has access to the Northwind product catalog.
2. invoice_information_subagent: handles questions about a customer's past orders,
   order line items, shipping, and their assigned sales representative.

Based on the customer's message, route it to the correct specialist.
This could be one step in an inquiry that needs multiple sub-agent calls.
"""

supervisor_prebuilt_workflow = create_supervisor(
    agents=[invoice_information_subagent, product_catalog_subagent],
    output_mode="last_message",
    model=llm,
    prompt=(supervisor_prompt),
    state_schema=State
)

supervisor_prebuilt = supervisor_prebuilt_workflow.compile(
    name="product_catalog_subagent",
    checkpointer=checkpointer,
    store=in_memory_store
)

Tentez de tester cette unité de manière isolée si c’est possible, puis assemblez-la. L’isolation permet de détecter tôt les erreurs dans le schéma des outils.

thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}

result = supervisor_prebuilt.invoke(
    {"messages": [HumanMessage(content="What Beverages do you have in stock?")],
     "customer_id": "ALFKI"},
    config=config
)

for message in result["messages"]:
    message.pretty_print()

La liste ci-dessus représente une étape concrète du processus de construction — maintenez les noms stables afin que les traces générées par LangSmith restent comparables d’une version à l’autre.

from pydantic import BaseModel, Field
from typing import Optional
from langgraph.types import interrupt

class UserInput(BaseModel):
    """Schema for parsing the customer's account identifier."""
    identifier: str = Field(
        description="The customer identifier extracted from the message. "
                    "Can be a 5-letter customer ID (e.g. ALFKI), phone number, "
                    "or company name. Return empty string if not found."
    )

# Structured LLM — always returns a UserInput object, never free text
structured_llm = llm.with_structured_output(schema=UserInput)

structured_system_prompt = """You are a customer service representative responsible for
verifying customer identity at Northwind Traders.
Only extract the customer's account identifier from the message.
This can be a 5-letter customer ID (like ALFKI), a phone number, or a company name.
If the customer has not provided any identifier, return an empty string.
"""

Intégrez ce fragment au graphe avant de passer à l’étape suivante ; les graphes partiels masquent les erreurs de routage jusqu’au jour de la démonstration.

def get_customer_id_from_identifier(identifier: str) -> Optional[str]:
    """
    Retrieve a CustomerID from the Northwind database using one of three identifiers:
    1. Direct 5-letter customer ID (e.g. "ALFKI")
    2. Phone number
    3. Company name (partial match)
    """
    if not identifier:
        return None

    identifier = identifier.strip()

    # Direct customer ID: 5 alpha characters
    if identifier.replace(" ", "").isalpha() and len(identifier.replace(" ", "")) == 5:
        result = db.run(
            f"SELECT CustomerID FROM Customers WHERE CustomerID = '{identifier.upper()}';"
        )
        if result and result != "[]":
            try:
                formatted = ast.literal_eval(result)
                if formatted:
                    return formatted[0][0]
            except Exception:
                pass

    # Phone number: contains digits
    if any(c.isdigit() for c in identifier):
        result = db.run(
            f"SELECT CustomerID FROM Customers WHERE Phone = '{identifier}';"
        )
        if result and result != "[]":
            try:
                formatted = ast.literal_eval(result)
                if formatted:
                    return formatted[0][0]
            except Exception:
                pass

    # Company name: partial match
    result = db.run(
        f"SELECT CustomerID FROM Customers WHERE CompanyName LIKE '%{identifier}%' LIMIT 1;"
    )
    if result and result != "[]":
        try:
            formatted = ast.literal_eval(result)
            if formatted:
                return formatted[0][0]
        except Exception:
            pass

    return None

Tentez de tester cette unité de manière isolée si c’est possible, puis assemblez-la. L’isolation permet de détecter tôt les erreurs dans le schéma des outils.

def verify_info(state: State, config: RunnableConfig):
    """
    Verifies the customer's identity.

    - If customer_id is already in state: skip (already verified)
    - If identifier found and valid in DB: set customer_id and confirm
    - If not found: ask the customer politely for their details
    """
    if state.get("customer_id") is not None:
        return

    system_instructions = """You are a Northwind Traders customer support agent
trying to verify the customer's identity as the first step of the support process.
Only after their account is verified will you be able to help them with orders.
To verify their identity, ask for their 5-letter Customer ID (e.g. ALFKI),
phone number, or company name."""

    user_input = state["messages"][-1]

    parsed_info = structured_llm.invoke(
        [SystemMessage(content=structured_system_prompt), user_input]
    )
    identifier = parsed_info.identifier

    customer_id = ""
    if identifier:
        customer_id = get_customer_id_from_identifier(identifier)

    if customer_id:
        intent_message = SystemMessage(
            content=f"Thank you for providing your information! "
                    f"I was able to verify your account (Customer ID: {customer_id}). "
                    f"How can I help you today?"
        )
        return {
            "customer_id": customer_id,
            "messages": [intent_message]
        }
    else:
        response = llm.invoke(
            [SystemMessage(content=system_instructions)] + state["messages"]
        )
        return {"messages": [response]}

def human_input(state: State, config: RunnableConfig):
    """
    Human-in-the-loop node. Pauses the graph using interrupt() and waits
    for the user to provide their customer identifier.
    """
    user_input = interrupt("Please provide your Customer ID, phone number, or company name.")
    return {"messages": [user_input]}

def should_interrupt(state: State, config: RunnableConfig):
    """Routes to human_input if customer_id is not verified, else continues."""
    if state.get("customer_id") is not None:
        return "continue"
    else:
        return "interrupt"

La liste ci-dessus représente une étape concrète du processus de construction — maintenez les noms stables afin que les traces générées par LangSmith restent comparables d’une version à l’autre.

multi_agent_verify = StateGraph(State)

multi_agent_verify.add_node("verify_info", verify_info)
multi_agent_verify.add_node("human_input", human_input)
multi_agent_verify.add_node("supervisor", supervisor_prebuilt)

multi_agent_verify.add_edge(START, "verify_info")

multi_agent_verify.add_conditional_edges(
    "verify_info",
    should_interrupt,
    {
        "continue": "supervisor",
        "interrupt": "human_input",
    }
)

multi_agent_verify.add_edge("human_input", "verify_info")
multi_agent_verify.add_edge("supervisor", END)

multi_agent_verify_graph = multi_agent_verify.compile(
    name="multi_agent_verify",
    checkpointer=checkpointer,
    store=in_memory_store
)

Intégrez ce fragment au graphe avant de passer à l’étape suivante ; les graphes partiels masquent les erreurs de routage jusqu’au jour de la démonstration.

from langgraph.types import Command

thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}

result = multi_agent_verify_graph.invoke(
    {"messages": [HumanMessage(content="What was in my last order?")]},
    config=config
)

Testez cette unité de manière isolée si possible, puis assemblez-la. L’isolation permet de détecter tôt les erreurs dans le schéma des outils.

result = multi_agent_verify_graph.invoke(
    Command(resume="My customer ID is ALFKI"),
    config=config
)

La liste ci-dessus représente une étape concrète du processus de construction — maintenez les noms stables afin que les traces générées par LangSmith restent comparables d’une version à l’autre.

result = multi_agent_verify_graph.invoke(
    {"messages": [HumanMessage(content="What Beverages do you carry?")]},
    config=config
)

Intégrez ce fragment au graphe avant de passer à l’étape suivante ; les graphes partiels masquent les erreurs de routage jusqu’au jour de la démonstration.

from langgraph.store.base import BaseStore

def format_user_memory(user_data: dict) -> str:
    """Formats the stored UserProfile into a string for injection into prompts."""
    profile = user_data.get("memory")
    result = ""
    if hasattr(profile, "product_preferences") and profile.product_preferences:
        result += f"Product Category Preferences: {', '.join(profile.product_preferences)}"
    return result.strip()

def load_memory(state: State, config: RunnableConfig, store: BaseStore):
    """
    Loads the customer's saved product preferences from the long-term memory store.
    Runs right after verification and before the supervisor handles the query.
    """
    user_id = config["configurable"].get("user_id", state["customer_id"])
    namespace = ("memory_profile", user_id)
    key = "user_memory"

    existing_memory = store.get(namespace, key)
    formatted_memory = ""

    if existing_memory and existing_memory.value:
        formatted_memory = format_user_memory(existing_memory.value)

    return {"loaded_memory": formatted_memory}

Testez cette unité de manière isolée si possible, puis assemblez-la. L’isolation permet de détecter tôt les erreurs dans le schéma des outils.

class UserProfile(BaseModel):
    """Structured profile saved to long-term memory after each conversation."""
    customer_id: str = Field(description="The customer's ID code")
    product_preferences: List[str] = Field(
        description="List of product categories the customer expressed interest in, "
                    "e.g. ['Seafood', 'Beverages']"
    )

La liste ci-dessus représente une étape concrète du processus de construction — maintenez les noms stables afin que les traces générées par LangSmith restent comparables d’une version à l’autre.

create_memory_prompt = """You are an expert analyst observing a customer support conversation
at Northwind Traders.

You are tasked with analyzing the conversation and updating the customer's memory profile
with any newly identified product category preferences.

*IMPORTANT INFORMATION BELOW*

The conversation between the customer and the support agent:
{conversation}

The existing memory profile associated with the customer:
{memory_profile}

For each field, if there is no new information, do not update the value.
Take a deep breath and think carefully before responding.
"""

def create_memory(state: State, config: RunnableConfig, store: BaseStore):
    """
    Analyzes the conversation and updates the customer's long-term memory profile
    with any new product category preferences they expressed.
    """
    user_id = str(config["configurable"].get("user_id", state["customer_id"]))
    namespace = ("memory_profile", user_id)
    key = "user_memory"

    existing_memory = store.get(namespace, key)
    formatted_memory = ""
    if existing_memory and existing_memory.value:
        formatted_memory = format_user_memory(existing_memory.value)

    formatted_system_message = SystemMessage(
        content=create_memory_prompt.format(
            conversation=state["messages"],
            memory_profile=formatted_memory
        )
    )

    updated_memory = llm.with_structured_output(UserProfile).invoke(
        [formatted_system_message]
    )

    store.put(namespace, key, {"memory": updated_memory})

Intégrez ce fragment au graphe avant de passer à l’étape suivante ; les graphes partiels masquent les erreurs de routage jusqu’au jour de la démonstration.

multi_agent_final = StateGraph(State)

multi_agent_final.add_node("verify_info", verify_info)
multi_agent_final.add_node("human_input", human_input)
multi_agent_final.add_node("load_memory", load_memory)
multi_agent_final.add_node("supervisor", supervisor_prebuilt)
multi_agent_final.add_node("create_memory", create_memory)

multi_agent_final.add_edge(START, "verify_info")

multi_agent_final.add_conditional_edges(
    "verify_info",
    should_interrupt,
    {
        "continue": "load_memory",
        "interrupt": "human_input",
    }
)

multi_agent_final.add_edge("human_input", "verify_info")
multi_agent_final.add_edge("load_memory", "supervisor")
multi_agent_final.add_edge("supervisor", "create_memory")
multi_agent_final.add_edge("create_memory", END)

multi_agent_final_graph = multi_agent_final.compile(
    name="multi_agent_verify",
    checkpointer=checkpointer,
    store=in_memory_store
)

Vérifiez cette unité de manière isolée si possible, puis assemblez-la. L’isolation permet de détecter tôt les erreurs dans le schéma des outils.

thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}

result = multi_agent_final_graph.invoke(
    {"messages": [HumanMessage(
        content="My customer ID is ALFKI. I am really interested in your Seafood and Beverages range!"
    )]},
    config=config
)

# Check what memory was saved
memory_key = in_memory_store.get(("memory_profile", "ALFKI"), "user_memory")
if memory_key:
    print(f"Memory saved: {memory_key.value['memory'].product_preferences}")

L’élément ci-dessus représente une étape concrète du processus de construction — gardez les noms stables afin que les traces générées par LangSmith restent comparables d’une version à l’autre.

thread_id_2 = str(uuid.uuid4())
config_2 = {"configurable": {"thread_id": thread_id_2}}

result2 = multi_agent_final_graph.invoke(
    {"messages": [HumanMessage(
        content="Hi, I'm back. Customer ID ALFKI. What would you recommend for me today?"
    )]},
    config=config_2
)

Intégrez ce fragment au graphe avant de passer à l’étape suivante ; les graphes partiels masquent les erreurs de routage jusqu’au jour de la démonstration.

from langsmith import Client
from langsmith.evaluation import evaluate

client = Client()

examples = [
    {
        "input": {"question": "What Beverages do you carry?", "customer_id": "ALFKI"},
        "output": "Chai"
    },
    {
        "input": {"question": "What was in my most recent order?", "customer_id": "ALFKI"},
        "output": "order"
    },
    {
        "input": {"question": "I'd like a refund.", "customer_id": "ANATR"},
        "output": "additional information"
    },
    {
        "input": {
            "question": "My name is Maria Anders. What is the order ID of my most recent purchase?",
            "customer_id": None
        },
        "output": "verify"
    }
]

dataset = client.create_dataset(
    "northwind-agent-eval",
    description="Test cases for the Northwind multi-agent support system",
)
for ex in examples:
    client.create_example(
        inputs=ex["input"],
        outputs={"answer": ex["output"]},
        dataset_id=dataset.id,
    )

def run_agent(inputs: dict) -> dict:
    thread_id = str(uuid.uuid4())
    config = {"configurable": {"thread_id": thread_id}}
    initial_state = {"messages": [HumanMessage(content=inputs["question"])]}
    if inputs.get("customer_id"):
        initial_state["customer_id"] = inputs["customer_id"]
    result = multi_agent_final_graph.invoke(initial_state, config=config)
    final_answer = next(
        (m.content for m in reversed(result["messages"])
         if hasattr(m, "content") and m.content), ""
    )
    return {"answer": final_answer}

results = evaluate(
    run_agent,
    data="northwind-agent-eval",
    evaluators=["correctness"],
    experiment_prefix="northwind-run-1",
)

Vérifiez cette unité de manière isolée si possible, puis assemblez-la. L’isolation permet de détecter tôt les erreurs dans le schéma des outils.

Mémoire à court terme vs mémoire à long terme

Les points de contrôle couvrent un thread. La mémoire à long terme stocke les préférences entre différents threads. Ne traitez pas les préférences comme des entrées CDN avec une durée de vie limitée.

Intervention humaine

Interrompez le processus en l’absence d’identité ; reprenez-le avec l’ID du client ; continuez sur le même thread pour les suivi. Sans point de contrôle, la reprise crée de l’histoire fictive.

Évaluation

Créez un petit ensemble « doré » : catalogue, facture et pièges inter-domaines. Évaluez la précision du routage ainsi que la fiabilité des réponses finales dans les ensembles de données LangSmith.

Conclusion

Les systèmes multi-agents sont utiles lorsque les domaines diffèrent réellement en termes d’outils et de politiques. Sinon, un seul agent bien équipé avec des mécanismes d’interruption clairs est moins coûteux et plus facile à évaluer.

Indiquez les responsables des décisions à côté des nœuds et des cibles webhook.

Préférez les requêtes de vérification utilisant uniquement SELECT dans les boucles non fiables.

Tenez une capture des incidents critiques avant de modifier les prompts de triage.

Avertissez en cas de boucles d’échec de vérification de la même manière que les services avertissent en cas d’erreurs 5xx.

Séparez les clients locataires dans les étiquettes du graphe dès le premier jour.

Dokumentez les sorties de Terraform que l’exécution de l’agent utilise.

Indiquez les responsables des décisions à côté des nœuds et des cibles webhook.

Préférez les requêtes de vérification utilisant uniquement SELECT dans les boucles non fiables.

Prendre une capture d’écran des incidents critiques avant de modifier les prompts de triage.

Émettre une alerte en cas de boucles d’échec de vérification de la même manière que les services le font pour les codes 5xx.

Séparer les clients et locataires dans les étiquettes du graphe dès le premier jour.

Dokumenter les sorties de Terraform que consomme le runtime de l’agent.

Indiquer les responsables des décisions à côté des nœuds et des cibles webhook.

Préférer des requêtes de vérification ne contenant que SELECT dans les boucles non fiables.

Prendre une capture d’écran des incidents critiques avant de modifier les prompts de triage.

Émettre une alerte en cas de boucles d’échec de vérification de la même manière que les services le font pour les codes 5xx.

Séparer les clients et locataires dans les étiquettes du graphe dès le premier jour.

Dokumenter les sorties de Terraform que consomme le runtime de l’agent.

Indiquer les responsables des décisions à côté des nœuds et des cibles webhook.

Préférer des requêtes de vérification ne contenant que SELECT dans les boucles non fiables.

Prendre une capture d’écran des incidents critiques avant de modifier les prompts de triage.

Alertez en cas de boucles d’échec de vérification de la même manière que les services le font pour les codes 5xx.

Séparez les clients-locataires dans les étiquettes du graphe dès le premier jour.

Dokumentez les sorties de Terraform que consomme le runtime de l’agent.

Indiquez les responsables des décisions à côté des nœuds et des cibles webhook.

Préférez les requêtes de vérification utilisant uniquement SELECT dans les boucles non fiables.

Tenez une capture d’écran des incidents critiques avant de modifier les prompts de triage.

Alertez en cas de boucles d’échec de vérification de la même manière que les services le font pour les codes 5xx.

Séparez les clients-locataires dans les étiquettes du graphe dès le premier jour.

Dokumentez les sorties de Terraform que consomme le runtime de l’agent.

Indiquez les responsables des décisions à côté des nœuds et des cibles webhook.

Préférez les requêtes de vérification utilisant uniquement SELECT dans les boucles non fiables.

Tenez une capture d’écran des incidents critiques avant de modifier les prompts de triage.

Alertez en cas de boucles d’échec de vérification de la même manière que les services le font pour les codes 5xx.

Séparez les clients et leurs tenants dans les étiquettes du graphe dès le premier jour.

Dokumentez les sorties de Terraform que consomme le runtime de l’agent.

Indiquez les responsables des décisions à côté des nœuds et des cibles webhook.

Préférez les requêtes de vérification utilisant uniquement SELECT dans les boucles non fiables.

Tenez une capture d’écran des incidents critiques avant de modifier les prompts de triage.

Avertissez en cas de boucles d’échec de vérification de la même manière que les services avertissent en cas de codes 5xx.

Séparez les clients et leurs tenants dans les étiquettes du graphe dès le premier jour.

Dokumentez les sorties de Terraform que consomme le runtime de l’agent.

Indiquez les responsables des décisions à côté des nœuds et des cibles webhook.

Préférez les requêtes de vérification utilisant uniquement SELECT dans les boucles non fiables.

Tenez une capture d’écran des incidents critiques avant de modifier les prompts de triage.

Avertissez en cas de boucles d’échec de vérification de la même manière que les services avertissent en cas de codes 5xx.

Séparez les clients et leurs tenants dans les étiquettes du graphe dès le premier jour.

Décrire les sorties de Terraform que le moteur d’exécution de l’agent consomme.

Enregistrer les responsables des décisions à côté des nœuds et des cibles webhook.

Préférer des requêtes de vérification ne contenant que SELECT dans les boucles non fiables.

Télécharger une capture d’écran des incidents critiques avant de modifier les prompts de triage.

Émettre une alerte en cas de boucles d’échec de vérification de la même manière que les services le font pour les codes 5xx.

Séparer les clients locataires dans les étiquettes du graphe dès le premier jour.

Décrire les sorties de Terraform que le moteur d’exécution de l’agent consomme.

Enregistrer les responsables des décisions à côté des nœuds et des cibles webhook.

Préférer des requêtes de vérification ne contenant que SELECT dans les boucles non fiables.

Télécharger une capture d’écran des incidents critiques avant de modifier les prompts de triage.

Émettre une alerte en cas de boucles d’échec de vérification de la même manière que les services le font pour les codes 5xx.

Séparer les clients locataires dans les étiquettes du graphe dès le premier jour.

Décrire les sorties de Terraform que le moteur d’exécution de l’agent consomme.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.

Conservez les responsables, les paramètres et les notes de réversion à côté de chaque graphique ou modification d’agent afin que les démonstrations ne devancent pas la faisabilité opérationnelle.