Accueil / Articles / Agents de remboursement : LangGraph a survécu là où CrewAI et AutoGen ont échoué

Agents de remboursement : LangGraph a survécu là où CrewAI et AutoGen ont échoué

Mêmes outils et même politique dans les trois frameworks : seuls l’état explicite, l’idempotence et les points de contrôle ont survécu aux perturbations.

2754 mots

La charge de travail qui épuise les agents de démonstration

Les frameworks multi-agents ont l’air parfaits dans les démonstrations de recherche ou sur les blogs. Rien n’est en jeu si un outil s’exécute deux fois. Un agent chargé du traitement des remboursements est moins tolérant : face à un message de client, il doit classifier l’intention, charger la commande, évaluer les règles (délai, catégorie, remboursements antérieurs), appeler la passerelle de paiement au plus une fois si les conditions sont remplies, demander une intervention écrite en cas contraire, et laisser une trace d’audit.

Cette structure est courante en production : prendre une décision, interagir avec des systèmes à état, rester responsable. Elle exige un état persistant au fil des étapes, une logique de décision déterministe entre remboursement et demande d’intervention, des effets secondaires idempotents, ainsi que des pauses humaines qui survivent aux redémarrages du processus — et non simplement un arrêt temporaire.

Trois implémentations utilisaient les mêmes outils, le même modèle et la même logique de règles. Seule une a survécu aux tests de chaos qui interrompaient le processus en cours d’exécution.

Outils partagés

Assurez-vous que la concurrence se déroule de manière équitable : fonctions d’outil identiques, y compris une passerelle instable et une clé d’idempotence liée au ID de la commande.

# tools.py — identical across all three implementations
import time
import uuid
from dataclasses import dataclass
from typing import Literal

class PaymentGatewayError(Exception):
    pass

@dataclass
class Order:
    order_id: str
    customer_id: str
    item_category: str
    amount_cents: int
    purchased_at: float
    refund_count: int

# Fake DB — in prod this is Postgres behind a repository class
_ORDERS = {
    "ORD-4471": Order("ORD-4471", "CUST-991", "electronics", 8999, time.time() - 86400 * 5, 0),
    "ORD-2210": Order("ORD-2210", "CUST-102", "electronics", 4200, time.time() - 86400 * 45, 1),
}

_PROCESSED_REFUNDS: set[str] = set()  # idempotency ledger

def get_order(order_id: str) -> Order | None:
    return _ORDERS.get(order_id)

def check_refund_policy(order: Order) -> tuple[bool, str]:
    days_since_purchase = (time.time() - order.purchased_at) / 86400
    if days_since_purchase > 30:
        return False, f"Purchase was {days_since_purchase:.0f} days ago, outside the 30-day window."
    if order.refund_count >= 1:
        return False, "Customer has already received a refund on this order."
    return True, "Eligible: within window, no prior refund."

def issue_refund(order_id: str, idempotency_key: str) -> dict:
    """Calls the payment gateway. MUST be idempotent — retries are expected."""
    if idempotency_key in _PROCESSED_REFUNDS:
        return {"status": "already_processed", "idempotency_key": idempotency_key}
    order = _ORDERS[order_id]
    # simulate a flaky gateway — this matters later
    if uuid.uuid4().int % 5 == 0:
        raise PaymentGatewayError("gateway timeout, retry with same idempotency_key")
    _PROCESSED_REFUNDS.add(idempotency_key)
    return {"status": "refunded", "amount_cents": order.amount_cents, "idempotency_key": idempotency_key}

L’idempotence n’est pas une simple décoration ; c’est ce qui distingue un cadre d’agents fonctionnel d’un simple jouet informatique.

CrewAI : démonstrations solides, contrôle faible

CrewAI définit les rôles et les tâches au sein d’un Crew. Les présentations de produits aiment beaucoup la métaphore du organigramme.

Modèle naïf

from crewai import Agent, Task, Crew, Process
from crewai.tools import tool

@tool("Get Order")
def get_order_tool(order_id: str) -> str:
    """Fetch order details by ID."""
    order = get_order(order_id)
    return str(order) if order else "NOT_FOUND"

@tool("Check Policy")
def check_policy_tool(order_id: str) -> str:
    """Check refund eligibility for an order."""
    order = get_order(order_id)
    if not order:
        return "NOT_FOUND"
    eligible, reason = check_refund_policy(order)
    return f"eligible={eligible}, reason={reason}"

@tool("Issue Refund")
def issue_refund_tool(order_id: str) -> str:
    """Issue a refund for an order."""
    result = issue_refund(order_id, idempotency_key=f"refund-{order_id}")
    return str(result)

triage_agent = Agent(
    role="Refund Triage Specialist",
    goal="Decide whether a customer refund request should be approved or escalated",
    backstory="You are an experienced support agent who follows policy strictly.",
    tools=[get_order_tool, check_policy_tool, issue_refund_tool],
    verbose=True,
)

triage_task = Task(
    description="A customer says: '{customer_message}'. Order ID: {order_id}. "
                "Decide if this qualifies for a refund and act accordingly.",
    expected_output="A short summary of the action taken.",
    agent=triage_agent,
)

crew = Crew(agents=[triage_agent], tasks=[triage_task], process=Process.sequential)
result = crew.kickoff(inputs={"customer_message": "I want a refund, item broke", "order_id": "ORD-4471"})

Les chemins optimisés semblent fonctionner correctement. En tant que service, il a échoué de trois manières. L’ordre des outils était non déterministe : parfois issue_refund s’exécutait avant check_policy, car le modèle établit des associations libres entre les outils. Les tentatives de réessai après PaymentGatewayError pouvaient générer de nouvelles appels d’outils et de nouveaux identifiants, à moins que les outils ne prévoient explicitement l’idempotence. La fonction kickoff() s’exécute jusqu’à son terme sans pause humaine significative ; simuler la reprise signifiait reconstruire l’état en dehors du framework.

Tentative hiérarchique renforcée

manager_agent = Agent(
    role="Refund Process Manager",
    goal="Enforce strict order: lookup, then policy check, then refund or escalate. Never skip steps.",
    backstory="You strictly enforce process compliance and never let steps be skipped.",
    allow_delegation=True,
)

crew = Crew(
    agents=[triage_agent],
    tasks=[triage_task],
    process=Process.hierarchical,
    manager_agent=manager_agent,
)

Un agent gestionnaire et des prompts plus explicites ont réduit le nombre d’étapes sautées, mais n’ont pas créé d’invariants solides. Le langage naturel ne permet pas d’imposer une séquence de niveau conformité. CrewAI convient à des jeux de rôle flexibles — recherche puis critique — et non à des séquences liées aux paiements.

AutoGen : chat flexible, contrôle flou

La discussion de groupe avec sélection automatique du locuteur ajoute un appel basé sur un LLM à chaque tour pour choisir qui parle.

import autogen

config_list = [{"model": "gpt-4o", "api_key": "..."}]

llm_config = {"config_list": config_list, "temperature": 0}

triage_agent = autogen.AssistantAgent(
    name="TriageAgent",
    system_message=(
        "You triage refund requests. Look up the order, check policy, "
        "then either call issue_refund or hand off to EscalationAgent."
    ),
    llm_config=llm_config,
)

escalation_agent = autogen.AssistantAgent(
    name="EscalationAgent",
    system_message="You write a human-readable escalation note explaining why a refund needs manual review.",
    llm_config=llm_config,
)

user_proxy = autogen.UserProxyAgent(
    name="ToolExecutor",
    human_input_mode="NEVER",
    code_execution_config=False,
    function_map={
        "get_order": lambda order_id: str(get_order(order_id)),
        "check_refund_policy_tool": lambda order_id: str(check_refund_policy(get_order(order_id))),
        "issue_refund": lambda order_id: str(issue_refund(order_id, f"refund-{order_id}")),
    },
)

groupchat = autogen.GroupChat(
    agents=[user_proxy, triage_agent, escalation_agent],
    messages=[],
    max_round=10,
    speaker_selection_method="auto",  # an LLM call decides who speaks next
)
manager = autogen.GroupChatManager(groupchat=groupchat, llm_config=llm_config)

user_proxy.initiate_chat(manager, message="Customer wants a refund on ORD-2210, item broke on arrival.")

Faiblesses : boucles de conversation entre le tri et l’escalade, sans état structuré indiquant qu’une décision a déjà été prise — seules les transcriptions existent — ce qui oblige à utiliser des limites strictes définies par max_round. La question « Avons-nous remboursé ? » était traitée par des recherches dans du texte brut. Le nondéterminisme influençait l’ensemble du graphe d’exécution, si bien que les incidents étaient reconstitués à partir des journaux de discussion plutôt que des traces tapées.

LangGraph : le survivant

Un état tapé, des arêtes explicites, des outils de sauvegarde et des tentatives de réessai correspondaient au problème.

from typing import TypedDict, Literal, Optional
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.types import interrupt, Command
import uuid

class RefundState(TypedDict):
    order_id: str
    customer_message: str
    order: Optional[dict]
    eligible: Optional[bool]
    policy_reason: Optional[str]
    decision: Optional[Literal["refund", "escalate", "denied"]]
    refund_result: Optional[dict]
    audit_log: list[str]

def lookup_order_node(state: RefundState) -> RefundState:
    order = get_order(state["order_id"])
    log = state["audit_log"] + [f"Looked up {state['order_id']}: {'found' if order else 'not found'}"]
    if not order:
        return {**state, "decision": "escalate", "audit_log": log}
    return {**state, "order": order.__dict__, "audit_log": log}

def policy_check_node(state: RefundState) -> RefundState:
    order = Order(**state["order"])
    eligible, reason = check_refund_policy(order)
    log = state["audit_log"] + [f"Policy check: eligible={eligible}, reason={reason}"]
    return {**state, "eligible": eligible, "policy_reason": reason, "audit_log": log}

def route_after_policy(state: RefundState) -> str:
    # Plain Python. No LLM call decides this branch. This is the whole point.
    if state.get("decision") == "escalate":
        return "escalate"
    return "refund" if state["eligible"] else "escalate"

def human_approval_node(state: RefundState) -> RefundState:
    # Durable pause: this literally suspends the graph run and persists state
    # via the checkpointer. It can resume hours or days later, across restarts.
    decision = interrupt({
        "reason": "Ambiguous or ineligible refund needs human sign-off",
        "order": state["order"],
        "policy_reason": state["policy_reason"],
    })
    return {**state, "decision": decision, "audit_log": state["audit_log"] + [f"Human decision: {decision}"]}

def issue_refund_node(state: RefundState) -> RefundState:
    idempotency_key = f"refund-{state['order_id']}"  # stable across retries — this is the whole trick
    try:
        result = issue_refund(state["order_id"], idempotency_key)
    except PaymentGatewayError as e:
        # LangGraph re-raises into the node; retry policy (below) handles this,
        # and because the key is stable, a retried call is safe.
        raise
    log = state["audit_log"] + [f"Refund issued: {result}"]
    return {**state, "decision": "refund", "refund_result": result, "audit_log": log}

def escalate_node(state: RefundState) -> RefundState:
    log = state["audit_log"] + ["Escalated to human queue"]
    return {**state, "audit_log": log}
from langgraph.pregel.retry import RetryPolicy

graph = StateGraph(RefundState)

graph.add_node("lookup_order", lookup_order_node)
graph.add_node("policy_check", policy_check_node)
graph.add_node(
    "issue_refund",
    issue_refund_node,
    retry=RetryPolicy(max_attempts=3, retry_on=PaymentGatewayError),
)
graph.add_node("human_approval", human_approval_node)
graph.add_node("escalate", escalate_node)

graph.set_entry_point("lookup_order")
graph.add_edge("lookup_order", "policy_check")
graph.add_conditional_edges("policy_check", route_after_policy, {
    "refund": "issue_refund",
    "escalate": "human_approval",
})
graph.add_edge("human_approval", "issue_refund")  # human can still approve
graph.add_edge("issue_refund", END)
graph.add_edge("escalate", END)

checkpointer = SqliteSaver.from_conn_string("refunds.db")
app = graph.compile(checkpointer=checkpointer)

Les configurations des threads se reprendent après les pannes :

config = {"configurable": {"thread_id": "order-2210-refund-req"}}

# Kick off the run — it will pause at human_approval_node
result = app.invoke(
    {"order_id": "ORD-2210", "customer_message": "second refund please", "audit_log": []},
    config=config,
)
# result contains an interrupt payload; the process can now exit entirely.

# ... hours later, possibly a different process, different machine ...
final_result = app.invoke(Command(resume="escalate"), config=config)

Les tests déterministes vérifient que les commandes non éligibles sont escaladées :

def test_ineligible_order_escalates():
    state = {"eligible": False, "decision": None}
    assert route_after_policy(state) == "escalate"

def test_eligible_order_refunds():
    state = {"eligible": True, "decision": None}
    assert route_after_policy(state) == "refund"

L’interruption du processus au milieu d’un remboursement permettait de le redémarrer à partir du point de sauvegarde, en utilisant la même clé d’idempotence. Cet essai a déterminé le vainqueur.

Lorsque CrewAI ou AutoGen gagnent encore

CrewAI pour la rédaction collaborative avec un ordre flexible. AutoGen pour la recherche exploratoire multi-agents où la conversation constitue le produit final. Aucun de ces outils ne remplace une machine à états lorsque des fonds sont en jeu.

Lesson apprise

Adaptez l’abstraction au mode de défaillance. Si un ordre incorrect des outils ou des effets secondaires multiples sont inacceptables, préférez des graphes explicites avec un état durable plutôt que des équipes basées sur des prompts. Les frameworks ne sont pas de simples enveloppes interchangeables pour des « agents » ; ils encodent des hypothèses différentes concernant le contrôle, la mémoire et la récupération.

Liste de vérification pour la production après le concours

Au moment de promouvoir tout agent similaire à un remboursement, exigez : un état saisi avec un drapeau already_refunded (ou équivalent) ; des clés d’idempotence générées à partir des identifiants métier ; des vérifications de politique sous forme de nœuds de code, et non de suggestions issues d’interrogations ; des interruptions HITL placées après un checkpointer ; des tests de chaos capables d’arrêter les processus en cours d’exécution ; des journaux d’audit ne dépendant pas de recherches textuelles. Si un framework ne peut pas exprimer ces propriétés sans un moteur de workflow caché, c’est ce dernier qui constitue le véritable orchestrateur — et le framework n’est alors qu’un adhésif coûteux.

Mesurez les taux d’échec distincts en environnement de préparation : politiques ignorées, tentatives de remboursement multiples, interruptions perdues après redémarrage, et traces d’audit illisibles. Les prototypes Crew et AutoGen qui ne parviennent pas à surpasser LangGraph sur ces indicateurs devraient rester en laboratoire. Célébrez leur mise au repos ; la prolifération a un coût.

Dokumentez la décision à l’intention des équipes futures afin que de nouvelles compétitions ne soient plus nécessaires. Ajoutez un lien vers le chaos harness dans le README. Préférez des graphiques ennuyeux mais résistants aux perturbations à des discussions astucieuses qui ne survivent qu’aux démonstrations. Cette norme s’applique, au-delà des remboursements, à tout agent qui modifie des systèmes externes sous la pression d’une audit, ce qui correspond en réalité à ce que les entreprises souhaitent vraiment des « agents IA » une fois que le logiciel de présentation disparaît et que les comptes deviennent à nouveau importants chaque trimestre.

Association des échecs aux hypothèses du framework

CrewAI part du principe d’une décomposition flexible des tâches. AutoGen suppose que la conversation constitue un plan de contrôle suffisant. LangGraph prévoit que vous dessinerez vous-même le plan de contrôle. L’automatisation des remboursements va à l’encontre des deux premières hypothèses : les critères d’éligibilité ne sont pas négociables, et le choix du locuteur n’est pas un mécanisme d’autorisation de paiement. Lorsque les hypothèses entrent en conflit avec les règles du domaine, le cadre ayant le moins d’hypothèses l’emporte — même s’il semble moins magique dès la première semaine.

Les ingénieurs tentent parfois de « corriger » CrewAI ou AutoGen en utilisant des instructions système de plus en plus longues. C’est comme utiliser une clôture anti-cyclones comme porte de coffre-fort. Il faut placer la conformité dans des nœuds textuels et conserver les modèles de langage à l’intérieur de nœuds chargés de classer ou de rédiger, et non dans des nœuds qui décident si l’argent doit être transféré.

Exigences communes en matière d’observabilité

Quelle que soit votre choix, générez des éléments pour chaque appel d’outil en incluant l’ID de commande, la clé d’idempotence et le résultat de la politique. Sans cela, les débats au sein du framework deviennent théologiques tandis que la production reste dans l’ignorance. LangGraph a rendu ces mécanismes évidents car les nœuds sont des fonctions ; vous pouvez appliquer la même discipline ailleurs, mais l’évaluation a montré que c’était la solution la plus simple pour ce type de charge de travail.

Conclusion

Construisez l’agent qui correspond au mode de défaillance de votre système. Pour les remboursements, cet agent était un graphe doté de mémoire, et non une équipe basée sur des intuitions. Conservez les autres outils pour les problèmes pour lesquels ils conviennent réellement, et arrêtez de prétendre qu’une seule abstraction peut gérer tous les tickets nécessitant un agent.

Décortiquage de la structure LangGraph utilisée

Le graphe réussi conservait la classification, la récupération, la politique, le remboursement, l’escalade et l’audit en tant que nœuds distincts. Les arêtes codificaient seules les transitions légales autorisées. Le modèle ne choisissait jamais de passer outre la politique ; il se contentait de remplir les champs structurés interprétés par le code de politique. Les tentatives répétées sur le nœud de passerelle utilisaient la même clé d’idempotence stockée dans l’état. L’escalade faisait appel à interrupt() avec un point de contrôle afin qu’un gestionnaire puisse approuver des actions quelques heures plus tard sur une autre réplique.

Cette conception semble verbose par rapport à une définition de Crew avec trois agents. La verbiosité est justement l’objectif : chaque étape irréversible peut être nommée lors d’une revue de code. De nouveaux ingénieurs peuvent lire le graphe et prédire le comportement sans devoir rejouer dix conversations stochastiques.

Comparaison des scénarios de réponse aux incidents

Lorsque CrewAI a exécuté deux fois de suite une étape dans l’environnement de préparation, l’analyse post-mortem a imputé le problème à un « dérive des prompts ». Lorsque AutoGen est entré en boucle, l’analyse post-mortem a pointé du doigt le choix du locuteur. Lorsque LangGraph a échoué, l’analyse post-mortem a identifié un nœud spécifique ainsi qu’un réducteur manquant — des problèmes corrigeables sans débat inutile. La seule taxonomie des incidents justifiait déjà le choix d’un flux de travail lié aux paiements.

Notes sur les coûts et la latence issues de la compétition

Le modèle LLM utilisé par AutoGen pour chaque tour ajoutait de la latence et des tokens. Les tentatives de réessai de CrewAI multipliaient parfois les appels aux outils. LangGraph engendrait des coûts modérés en permanence pour l’enregistrement des points de contrôle, mais se distinguait par des dépenses prévisibles. Pour un volume d’appels de plusieurs milliers par jour, la prévisibilité l’emporte sur des économies occasionnelles mais discutables.

Compétences de l’équipe et recrutement

Le recrutement pour des compétences liées à “CrewAI” est moins fréquent que celui pour des connaissances en “machines d’état et LLM”. Les compétences en LangGraph peuvent être transférées à n’importe quel outil d’orchestration. Si l’entreprise standardise, privilégiez des concepts transférables : état, idempotence, HITL, évaluations. Les tendances des frameworks évoluent plus rapidement que ces concepts.

Élargir l’ensemble des outils de gestion du chaos

Au-delà du simple arrêt et redémarrage : introduisez des erreurs 503 au niveau du gateway, des livraisons dupliquées de webhooks, des décalages horaires au sein des fenêtres de politique, ainsi que des utilisateurs qui rejettent les escalades. Les graphes qui ne fonctionnent qu’en cas de parcours sans problème restent des jouets. Automatisez cet ensemble d’outils dans les processus CI en utilisant des données fictives déterministes, afin que les modifications ne suppriment pas silencieusement les mécanismes de sécurité.

Atterrissage en douceur pour les prototypes

Conservez les environnements de test CrewAI/AutoGen pour les workflows de contenu impliquant des éditeurs humains. Ne bloquez pas l’expérimentation — bloquez plutôt les identifiants de production. Une politique de plateforme stipulant « aucun outil de paiement dans les équipes gérées par des prompts » permet d’éviter de nouveaux incidents sans entraver la curiosité.

Insistance finale

L’affirmation de l’article est précise et forte : pour l’automatisation des remboursements avec de réels effets secondaires, LangGraph a tenu bon là où CrewAI et AutoGen ont échoué, malgré l’utilisation d’outils identiques. Extrapolez avec prudence. Mais extrapolez librement la leçon principale — évaluez les frameworks en fonction de vos modes d’échec, et non en fonction de leur apparence lors des démonstrations — car cette compétition en valait bien la semaine qu’elle a exigée.

Chronologie détaillée des échecs depuis la phase de test

Première semaine : la démonstration de CrewAI a impressionné les parties prenantes. Deuxième semaine : deux tentatives de remboursement double en environnement de test suite à des problèmes du gateway. Troisième semaine : le pilote AutoGen a consommé des tokens lors de boucles d’écoute pour des commandes rejetées. Quatrième semaine : le mécanisme de gestion du chaos de LangGraph a déclenché un remboursement partiel. Le calendrier est important car l’élan organisationnel se fige souvent dès la première démonstration ; inscrivez le chronologie des échecs dans l’ADR afin que cet élan ne puisse pas effacer les preuves.

L’idempotence en tant que exigence transversale

Tout framework testé ici peut appeler des outils. Seuls les designs qui utilisent une clé stable lors des tentatives de réessai sont sûrs pour les transactions financières. Stockez la clé dans l’état du graphe avant la première tentative. Refusez de créer une nouvelle clé sur les nœuds de réessai. Enregistrez ensemble la clé, l’ID de la commande et les codes de réponse du gateway. Si un framework rend cela difficile, cette difficulté est un signal, et non un problème administratif.

L’ergonomie de l’escalade humaine

Le texte d’escalade doit inclure les IDs des clauses de politique, les timestamps des commandes et le nombre de remboursements antérieurs — des structures définies par l’état, et non simplement du texte libre. Les managers doivent voir les mêmes champs que ceux affichés par le nœud de politique. LangGraph rend cela possible car l’état est un TypedDict ; Crew/AutoGen nécessitait de reconstruire les faits à partir des transcriptions, ce qui explique l’apparition de lacunes dans les audits.

Ce que nous avons conservé des solutions moins performantes

Les métaphores liées aux rôles de CrewAI ont facilité les conversations sur le produit — il suffit de les traduire en noms de nœuds LangGraph. La liste explicite des agents d’AutoGen a permis une meilleure attribution des outils à chaque nœud. Il est autorisé de s’inspirer des idées d’UX tout en rejetant des plans de contrôle non sécurisés.

Liste de vérification élargie pour la production

  • Chaos : défaillance pendant l’utilisation d’un outil, pendant l’attente d’une interruption, ou pendant les tentatives de réessai.
  • Tests de propriété : les cas non éligibles n’atteignent jamais le nœud de remboursement.
  • Chargement : génération soudaine de 100 identifiants de commande identiques ; vérification du succès via un seul gateway.
  • Sécurité : les identifiants des outils de paiement ne doivent exister que sur les travailleurs du nœud de remboursement.
  • Observabilité : tableaux de bord montrant le taux d’application de la politique de saut (qui doit être nul).
  • Gouvernance : le contrôle des modifications du code du nœud de politique doit être identique à celui appliqué au moteur de règles.
  • Pourquoi l’approche « il suffit d’ajouter un autre agent » a échoué

    L’ajout d’un agent « PolicyEnforcer » dans CrewAI a conservé une application probabiliste des règles. L’ajout d’un « RefundGuardian » dans AutoGen a maintenu les fonctionnalités dans le chat. Les gardiens incapables de bloquer définitivement certains chemins ne sont que des éléments décoratifs ; le blocage définitif doit faire partie de la topologie du graphe.

    Conclusion

    Mêmes outils, même modèle, philosophie de contrôle différente. Seul le graphe explicite a survécu aux pannes pertinentes pour les remboursements. Utilisez des équipes et des chats lorsque l’ordre incorrect coûte peu cher ; utilisez des graphes lorsque l’ordre incorrect constitue un événement comptable. Publiez cette règle en interne afin d’épargner à l’équipe suivante un concours de quatre semaines.

    Notes supplémentaires pour renforcer la sécurité des graphes de remboursement

    Les fenêtres de politique dépendant des horloges doivent utiliser l’heure du serveur enregistrée lors de la récupération, et non les dates indiquées par le modèle. Les vérifications de catégorie doivent être définies dans le code. Le nombre de remboursements antérieurs provient du registre comptable, et non de la mémoire des chats. Chacun de ces choix élimine un type d’injection de commandes visant à réécrire les critères d’éligibilité en langage naturel. Les graphes rendent ces choix évidents ; les équipes les cachent dans les profils des agents, là où les examinateurs cessent de regarder.