IA 101 - Épisode 4. Donner à vos outils d’IA ColdFusion : laisser le robot utiliser vos CFC sans lui remettre un lance-flammes

Permettre à votre assistant IA d’appeler la logique d’application ColdFusion réelle au moyen d’outils CFC soigneusement restreints, tout en gardant la validation, l’autorisation et la sécurité en production exactement là où elles doivent être : dans votre code.

Dans les derniers articles, nous avons lentement appris à ColdFusion à parler à l’IA sans provoquer immédiatement un incident de production.

Dans le préambule, nous avons couvert le vocabulaire : LLM, prompts, jetons, fenêtres de contexte, température, hallucinations, outils, RAG, MCP et garde-fous.

Ensuite, nous avons construit la version ColdFusion de l’IA pour « Hello World » en utilisant ChatModel(). Nous avons configuré un modèle, envoyé un prompt avec .chat(), lu response.message et affiché le résultat de façon sécuritaire.

Après cela, nous sommes passés des appels sans état à Agent(), où nous avons ajouté de la mémoire. L’assistant pouvait enfin se souvenir de ce que l’utilisateur venait de dire, conserver le contexte récent et éviter de traiter chaque demande comme un premier rendez-vous.

C’était utile, mais la mémoire ne nous mène que jusqu’à un certain point. Si un utilisateur dit :

Mon numéro de billet est TKT-12345.

Et qu’il demande ensuite :

Quel est le statut de mon billet?

La mémoire peut aider l’assistant à se souvenir du numéro de billet, mais elle ne peut pas nous dire le statut réel du billet. La réponse n’est pas dans le modèle ni dans l’historique de la conversation. Elle ne flotte pas dans la soupe de jetons en attendant d’être découverte par la pensée positive.

La réponse se trouve dans votre application. Votre base de données. Vos services. Vos CFC. C’est là qu’entrent en jeu les outils.

Où nous en sommes dans la pile

Jusqu’à maintenant, notre progression ressemble à ceci :

  • ChatModel() : bon pour les prompts simples, sans état.
  • Agent() avec mémoire : bon pour les conversations à plusieurs tours et le contexte par utilisateur.
  • Agent() avec outils : bon pour permettre à l’IA de demander de vraies capacités applicatives.

Cet article porte sur cette troisième couche.

Les outils de fonction permettent à un agent IA de demander à votre application ColdFusion de faire quelque chose. Ce « quelque chose » peut être une méthode de CFC qui récupère des données, effectue un calcul, crée un enregistrement, valide une entrée ou déclenche un flux de travail.

C’est une amélioration majeure. Sans outils, l’IA ne peut répondre qu’en utilisant le prompt, la mémoire et les connaissances du modèle. Avec des outils, l’IA peut interagir avec votre application réelle.

C’est aussi là que le niveau de danger augmente, parce qu’il y a une grande différence entre « l’assistant a résumé un paragraphe » et « l’assistant a créé 400 billets de soutien parce que quelqu’un a écrit “aidez-moi svp” avec enthousiasme ».

Nous allons donc ajouter des outils avec prudence.

Qu’est-ce qu’un outil de fonction?

Un outil de fonction est une capacité appelable exposée à l’agent IA. En ColdFusion, cela signifie généralement une méthode de CFC. Par exemple, imaginez que vous avez un outil de soutien avec ces méthodes :

getTicketStatus( ticketId )

createTicket( summary, priority )

L’IA n’a pas besoin d’un accès direct à la base de données ni de vos identifiants de source de données. Elle n’a pas besoin de connaître les noms de vos tables, ni d’exécuter SELECT * FROM anything_please. À la place, l’IA peut demander un appel d’outil :

Appeler getTicketStatus avec ticketId = TKT-12345.

Ensuite, votre application ColdFusion décide quoi faire. Cette dernière partie est essentielle. Le modèle demande l’outil. Votre application exécute l’outil. Le modèle ne se promène pas dans votre application sans supervision, avec un presse-papiers et une quantité de confiance suspecte.

Pourquoi les outils sont importants

Les outils résolvent un problème très précis… Le modèle peut raisonner sur l’intention de l’utilisateur, mais votre application possède les faits et les actions.

Un modèle peut comprendre que l’utilisateur pose une question au sujet d’un billet de soutien, mais votre CFC sait comment rechercher le billet.

Un modèle peut comprendre que l’utilisateur veut s’inscrire à un atelier, mais votre application sait si l’inscription est ouverte, si l’utilisateur est admissible, si un paiement est requis et si l’atelier a déjà été annulé parce que quelqu’un l’a planifié pendant une longue fin de semaine.

Un modèle peut comprendre que l’utilisateur veut réinitialiser quelque chose, mais votre application décide s’il a le droit de le réinitialiser.

Les outils permettent à l’IA de faire le pont entre le langage et la capacité applicative. L’assistant peut dire : « Ça ressemble à une question sur le statut d’un billet. Je devrais demander l’outil de statut du billet », puis votre code ColdFusion s’occupe du vrai travail.

C’est la bonne répartition du travail. L’IA gère l’intention et l’explication. ColdFusion gère la logique d’affaires, la validation, la sécurité, l’accès à la base de données et toutes les choses ennuyeuses qui évitent aux entreprises de se retrouver devant les tribunaux.

Un simple CFC d’outil

Commençons par un CFC volontairement ennuyeux. L’ennui, c’est bien. L’ennui, c’est ce qui nous évite d’expliquer au CTO pourquoi l’assistant de démonstration a envoyé un haïku à chaque client. Créez un CFC appelé SupportTool.cfc.

omponent output = false {
    remote string function getTicketStatus(
        required string ticketId
    ) hint = "Returns the current status of a support ticket by its ID." {

        if ( arguments.ticketId == "TKT-12345" ) {
            return "in progress";
        }

        return "closed";
    }

    remote struct function createTicket(
        required string summary,
        required string priority
    ) hint = "Creates a new support ticket with the given summary and priority." {
        return {
            id : createUUID(),
            summary : arguments.summary,
            priority : arguments.priority,
            status : "new"
        };
    }
}
 

Ce n’est clairement pas du code de production. Il n’y a pas de base de données, pas de vérification des permissions, pas de validation au-delà des arguments requis. Ce code ne verrait jamais le jour. Il est volontairement minuscule pour que nous puissions nous concentrer sur le flux d’outil IA.

Dans une application réelle, ces méthodes appelleraient votre couche de services, valideraient l’accès, vérifieraient l’utilisateur actuel, consigneraient l’action et vous agaceraient probablement un peu avec des cas particuliers, parce que les vraies applications restent engagées envers le développement personnel.

Enregistrer des outils avec Agent()

Nous enregistrons maintenant le CFC comme outil lors de la création de l’agent.

<cfscript>
    chatModel = ChatModel( {
        provider : "openAI",
        modelName : "gpt-5-nano",
        apiKey : application.aiApiKey,
        temperature : 0.3,
        maxTokens : 700,
        timeout : 30
    } );

    agent = Agent( {
        CHATMODEL : chatModel,
        CHATMEMORY : {
            TYPE : "messageWindowChatMemory",
            MAXMESSAGES : 20,
            PERUSER : true
        },
        TOOLS : [
            {
                CFC : "tools.SupportTool",
                METHODS : [
                    {
                        METHOD : "getTicketStatus",
                        DESCRIPTION : "Obtenir le statut d’un billet de soutien à l’aide de son ID. Utilisez ceci lorsque l’utilisateur pose une question sur la progression, l’état ou le statut d’un billet existant."
                    },
                    {
                        METHOD : "createTicket",
                        DESCRIPTION : "Créer un nouveau billet de soutien. Utilisez ceci lorsque l’utilisateur veut signaler un nouveau problème ou demander de l’aide."
                    }
                ]
            }
        ]
    } );
</cfscript>
 

La nouvelle partie est le tableau TOOLS. Celui-ci indique à l’agent : « Ce sont les méthodes du CFC que vous êtes autorisé à demander. » Remarquez que nous exposons des méthodes précises. C’est intentionnel.

La documentation d’Adobe montre que vous pouvez omettre le tableau METHODS et exposer toutes les méthodes distantes d’un CFC. Cela peut être utile pour le développement ou pour des CFC conçus spécialement comme collections d’outils, mais la documentation souligne aussi l’exposition de méthodes précises comme bonne pratique en production, parce que cela limite ce que l’IA peut invoquer. (guides.adobe.com)

Autrement dit :

TOOLS : [
{ CFC : "tools.SupportTool" }
]

Peut être pratique, mais :

TOOLS : [
{
CFC : "tools.SupportTool",
METHODS : [
{ METHOD : "getTicketStatus", DESCRIPTION : "..." }
]
}
]

…est généralement plus sûr. Et c’est mieux d’être prudent quand on donne des mains au robot.

Les descriptions ne sont pas de la décoration

La DESCRIPTION n’est pas seulement de la documentation pour les humains. Elle aide le modèle à décider quand utiliser l’outil. Une mauvaise description ressemble à ceci :

DESCRIPTION : "Obtient le billet."

Ce n’est pas utile. Obtient le billet quoi? Le statut? Le propriétaire? Les commentaires? La priorité? L’état émotionnel du billet? Quand vous fournissez une description, utilisez quelque chose de plus précis :

DESCRIPTION : "Obtenir le statut d’un billet de soutien à l’aide de son ID. Utilisez ceci lorsque l’utilisateur pose une question sur la progression, l’état ou le statut d’un billet existant."

Cela indique au modèle quand l’outil s’applique. C’est important, parce que le modèle ne parcourt pas votre code source pour développer une compréhension spirituelle profonde de vos conventions de nommage. Il voit le schéma de l’outil et les descriptions.

De bonnes descriptions d’outils réduisent la confusion. De mauvaises descriptions entraînent des choix d’outils étranges. Aucune description, c’est inviter le modèle à hausser les épaules numériquement et à inventer quelque chose.

Les appels d’outil sont des demandes, pas une exécution automatique

C’est la partie à ralentir et à lire deux fois. Lorsque le modèle décide qu’il a besoin d’un outil, agent.chat() peut retourner une structure de réponse contenant toolExecutionRequests. Cela ne signifie pas que l’outil a déjà été exécuté en toute sécurité. Cela signifie que le modèle demande l’appel d’un outil.

La documentation d’Adobe décrit la réponse comme contenant un tableau toolExecutionRequests, chaque entrée décrivant le nom de l’outil et les arguments. Votre application est responsable de vérifier ces demandes, d’exécuter l’outil et de gérer le résultat. (guides.adobe.com)

C’est bien. Cela permet à votre application de garder le contrôle. Par exemple :

<cfscript>
    response = agent.chat(
       "Quel est l’état du billet TKT-12345?",
       session.sessionId
   );

   if (
       structKeyExists( response, "toolExecutionRequests" )
       && arrayLen( response.toolExecutionRequests )
   ) {
       toolRequest = response.toolExecutionRequests[ 1 ];

       writeOutput( "Outil demandé : " & encodeForHtml( toolRequest.name ) );
       writeOutput( "<br>" );
       writeOutput( "Arguments : " & encodeForHtml( serializeJSON( toolRequest.arguments ) )         );
   } else {
       writeOutput( encodeForHtml( response.message ) );
   }
</cfscript>

 

À ce stade, nous examinons seulement la demande de l’outil.À ce stade, nous examinons seulement la demande de l’outil. C’est la première étape de débogage.

Avant d’exécuter quoi que ce soit, regardez ce que le modèle demande. C’est ainsi que vous apprenez si les descriptions de vos outils sont bonnes, si le modèle comprend l’intention de l’utilisateur et si les arguments attendus sont transmis correctement.

C’est aussi comme ça que vous évitez de construire une fonctionnalité où le modèle demande discrètement d’appeler deleteCustomer() et que tout le monde l’apprend à partir des journaux d’audit.

Exécution de l’outil demandé

Maintenant, exécutons réellement l’outil demandé. Pour une démo simple, nous pouvons faire correspondre les noms de méthodes autorisés à des appels de services d’application.

Cela démontre le modèle d’application important :

<cfparam name="form.message" default="">
<cfscript>
    if ( !structKeyExists( application, "memoryDemoAgent" ) ) {
        lock scope = "application" type = "exclusive" timeout = 10 {
            if ( !structKeyExists( application, "memoryDemoAgent" ) ) {
                chatModel = ChatModel( {
                    provider : "openAI",
                    modelName : "gpt-5-nano",
                    apiKey : application.aiApiKey,
                    temperature : 0.3,
                    maxTokens : 700,
                    timeout : 30
                } );
                application.memoryDemoAgent = Agent( {
                    CHATMODEL : chatModel,
                    CHATMEMORY : {
                        TYPE : "messageWindowChatMemory",
                        MAXMESSAGES : 20,
                        PERUSER : true
                    }
                } );
                application.memoryDemoAgent.systemMessage(
                    "You are a helpful ColdFusion AI assistant. Use CFScript examples when code is helpful. Be concise."
                );
            }
        }
    }
    userId = session.sessionId;
    result = "";
    if ( len( trim( form.message ) ) ) {
        try {
            response = application.memoryDemoAgent.chat(
                trim( form.message ),
                userId
            );
            result = response.message;
        } catch ( any error ) {
            writeLog(
                file = "ai",
                type = "error",
                text = "AI memory demo failed: #error.message#"
            );
            result = "Désolé, je n’ai pas pu générer de réponse pour le moment.";
        }
    }
</cfscript>

<cfoutput>
<form method="post">
    <label for="message">Message</label>
    <br><textarea id="message" name="message" rows="5" cols="80">#encodeForHtml( form.message )#</textarea>
    <br><button type="submit">Envoyer </button>
</form>
<cfif len( result )>
    <h2>Réponse</h2>
    <pre>#encodeForHtml( result )#</pre>
</cfif>
</cfoutput>

Interrogez l’agent. Vérifiez si l’agent a demandé un outil. Validez que l’outil est autorisé. Validez les arguments. Exécutez la méthode CFC. Gérez le résultat.

C’est le modèle mental sûr.

Ne faites pas ça à l’aveuglette :

evaluate( "application.supportTool.#toolRequest.name#()" );

Non. Mauvaise idée. Laissez ça.

L’exécution dynamique basée sur la sortie du modèle, c’est ainsi qu’on transforme un bon tutoriel en vidéo de formation sur la sécurité. Utilisez des listes d’autorisation explicites. Utilisez un switch. Utilisez la validation. Utilisez du code normal.

ColdFusion n’a pas survécu à des décennies d’applications en production pour qu’on confie evaluate() à un robot conversationnel et qu’on espère le meilleur.

Retourner les résultats de l’outil au modèle

Pour un flux de travail conversationnel complet avec des outils, votre application doit généralement renvoyer le résultat de l’outil au modèle afin qu’il puisse l’expliquer à l’utilisateur. Conceptuellement, le flux ressemble à ceci :

 
Utilisateur :
    Quel est le statut du billet TKT-12345 ?

Agent :
    Je dois appeler getTicketStatus avec ticketId TKT-12345.

ColdFusion :
    Vérifie la demande.
    Exécute getTicketStatus.
    Obtient « en cours ».

Agent :
    Explique à l’utilisateur :
    Votre billet TKT-12345 est actuellement en cours.

Les détails exacts de l’implémentation dépendent du modèle de réponse et de continuation pris en charge dans votre version de ColdFusion ainsi que du comportement du fournisseur. L’aspect architectural est la partie importante… Le modèle d’IA ne devrait pas être la source de vérité. Le résultat de l’outil devrait l’être. La réponse finale de l’assistant devrait être basée sur ce que l’outil a retourné, et non sur ce que le modèle a deviné avant l’exécution de l’outil.

Pour des démonstrations simples, vous pouvez afficher directement le résultat de l’outil. Pour un assistant raffiné, vous passerez généralement le résultat de l’outil dans la conversation et demanderez au modèle de produire une réponse conviviale. Par exemple :

<cfscript>
    toolSummaryPrompt = "
        The user asked: What is the status of ticket TKT-12345?
        The application looked up the ticket and returned this result:
        #serializeJSON( toolResult )#
        Answer the user in one short sentence.
    ";
    finalResponse = agent.chat(
        toolSummaryPrompt,
        session.sessionId
    );

    writeOutput( encodeForHtml( finalResponse.message ) );
</cfscript>

C’est volontairement simple. Dans une vraie implémentation, vous voudrez peut-être un flux de continuation plus structuré, mais le principe demeure :

  • D’abord l’outil.
  • Ensuite, le modèle explique.

Pas l’inverse.

Les arguments d’outil sont des entrées non fiables

Le modèle génère les arguments de l’outil. Cela signifie que les arguments de l’outil sont des entrées externes. Traitez-les comme des champs de formulaire. Parce que c’est essentiellement ce qu’ils sont, sauf qu’au lieu d’être saisis dans un formulaire par un utilisateur, ils ont été déduits par un modèle probabiliste qui avait peut-être, ou non, assez de café.

Validez tout. Par exemple :

<cfscript>
    if ( !structKeyExists( toolRequest.arguments, "ticketId" ) ) {
        throw(
            type = "AiTool.MissingArgument",
            message = "The ticketId argument is required."
        );
    }

    ticketId = trim( toolRequest.arguments.ticketId );

    if ( !reFindNoCase( "^TKT-[0-9]{5}$", ticketId ) ) {
        throw(
            type = "AiTool.InvalidArgument",
            message = "The ticketId argument is invalid."
        );
    }
</cfscript>

 

Ne supposez pas :

  • que l’argument existe
  • que l’argument est du bon type
  • que l’argument appartient à l’utilisateur actuel
  • que l’argument est sécuritaire
  • que l’argument est complet
  • que l’argument devrait être digne de confiance parce que « l’IA l’a dit »
  • L’IA n’a pas de passe-droit.

L’autorisation doit être dans l’outil

C’est la règle de production la plus importante de l’article : Les outils doivent appliquer l’autorisation. Pas le prompt. Pas le modèle. Pas l’interface. L’outil.

Si getTicketStatus() retourne des renseignements sur un billet de soutien, il devrait vérifier que l’utilisateur actuel est autorisé à voir ce billet. Cela signifie que l’outil a probablement besoin du contexte de l’utilisateur.

Une meilleure méthode, au style de production, pourrait ressembler à ceci :

component output = false {
    public struct function getTicketStatus(
        required numeric userId,
        required string ticketId
    ) {
        var ticket = application.ticketService.getTicketByPublicId(
            ticketId = arguments.ticketId
        );

        if ( !application.ticketService.userCanViewTicket(
            userId = arguments.userId,
            ticketId = ticket.id
        ) ) {
            throw(
                type = "Security.NotAuthorized",
                message = "The current user is not allowed to view this ticket."
            );
        }

        return {
            ticketId : ticket.publicId,
            status : ticket.status,
            lastUpdated : ticket.updatedAt
        };
    }
}

Ensuite, lors de l’exécution de la demande de l’outil, votre application injecte l’identifiant de l’utilisateur authentifié.

toolResult = application.supportTool.getTicketStatus(
	userId = session.userId,
	ticketId = toolRequest.arguments.ticketId
);

Ne laissez pas le modèle fournir userId. L’utilisateur n’a pas le droit de dire : « en fait, je suis l’identifiant utilisateur 1 ». Le modèle ne peut pas transmettre cela comme si cela descendait de la montagne sur des tables de pierre. L’application connaît l’utilisateur authentifié. L’application injecte ce contexte. L’outil vérifie l’autorisation. C’est ça, le modèle.

Outils de lecture versus outils d’écriture

Tous les outils ne se valent pas. Certains outils lisent des données. Certains outils modifient des données. Cette différence compte.

Outils de lecture :

  • obtenir l’état du billet
  • lister les événements à venir
  • calculer une estimation d’expédition
  • vérifier la disponibilité des inscriptions
  • récupérer le sommaire du compte
  • valider le code promo

Outils d’écriture :

  • créer un billet
  • annuler l’inscription
  • envoyer un courriel
  • mettre à jour le profil
  • émettre un remboursement
  • supprimer un dossier
  • publier une annonce

Les outils de lecture peuvent quand même divulguer de l’information sensible, donc ils nécessitent une autorisation. Mais les outils d’écriture peuvent modifier concrètement le monde. Cela signifie qu’ils demandent encore plus d’attention. Pour les outils d’écriture, tenez compte de :

  • étapes de confirmation
  • approbation explicite de l’utilisateur
  • journalisation d’audit
  • idempotence
  • limites de taux
  • vérifications des autorisations
  • validation
  • stratégie de retour en arrière
  • révision humaine pour les actions à haut risque

Un bon premier outil est généralement un outil de lecture. Ne faites pas de votre premier outil d’IA refundAllOrders(). Cela peut susciter de l’enthousiasme, mais pas dans le bon sens.

Confirmation avant les écritures

Pour les actions d’écriture, utilisez un flux de confirmation. Prenons l’exemple de conversation suivant :

Utilisateur :
	Crée un billet de priorité élevée disant que je ne peux pas me connecter.

[removed]
	Je peux créer ce billet. Veuillez confirmer :
	Résumé : Je ne peux pas me connecter
	Priorité : Élevée

Utilisateur :
	Oui, crée-le.

Application :
	Exécute createTicket().

Cela donne à l’utilisateur la chance de repérer les malentendus, car le modèle pourrait déduire la mauvaise priorité. Ou mal résumer. Ou créer quelque chose de trop large. Ou mal interpréter « je n’arrive pas à me connecter à la version de préproduction » comme « la production est en feu », ce qui, pour être juste, est parfois le cas.

Le modèle devrait fonctionner ainsi :

  • Le modèle extrait l’action prévue.
  • L’application présente la confirmation.
  • L’utilisateur confirme.
  • L’application exécute l’outil d’écriture.
  • L’assistant résume le résultat.

Ce n’est pas seulement convivial, c’est aussi un mécanisme de sécurité.

Conseils de conception d’outils

Les bons outils sont ennuyeux, précis et restreints. C’est un compliment. Une bonne méthode d’outil d’IA devrait :

  1. faire une seule chose
  2. avoir un nom clair
  3. utiliser des arguments typés
  4. valider les entrées
  5. faire respecter l’autorisation
  6. retourner des données structurées
  7. éviter d’exposer les détails d’implémentation internes
  8. éviter d’obliger le modèle à connaître les identifiants de base de données
  9. éviter un comportement trop large de type « faire n’importe quoi »

Mauvais outil :

public any function runSql( required string sql )

Absolument pas. Non. Ce n’est pas un outil. C’est une lettre de démission écrite en CFML.

Meilleur outil :

public struct function getOrderStatus(
required numeric userId,
required string orderNumber
)

Précis. Restreint. Compréhensible. Testable. Vérifiable. Moins susceptible de devenir une conférence intitulée « Leçons apprises ».

Retourner des données structurées

Les méthodes d’outil devraient généralement retourner des données structurées. Par exemple :

return {
	ticketId : ticket.publicId,
	status : ticket.status,
	priority : ticket.priority,
	lastUpdated : dateTimeFormat( ticket.updatedAt, "yyyy-mm-dd HH:nn:ss" )
};

Cela donne au modèle des faits clairs à expliquer. Évitez de renvoyer de gros blocs de HTML, des lignes brutes de base de données, des noms de champs internes ou de gigantesques structures imbriquées, à moins que le modèle en ait réellement besoin. Le modèle n’a pas besoin de tout votre objet billet. Il a probablement besoin de :

  • ID du billet
  • état
  • priorité
  • dernière mise à jour
  • peut-être l’étape suivante

Retournez le minimum d’information utile. Cela garde les instructions plus petites, plus sûres et plus faciles à utiliser pour le modèle. Cela évite aussi le classique schéma d’entreprise où une méthode nommée getSummary() renvoie 4 Mo de « au cas où ».

Gardez les détails internes à l’interne

Ne divulguez pas les identifiants internes, sauf si l’utilisateur en a besoin. Mauvais résultat :

return {
	id : 4815162342,
	internalQueueId : 7,
	databaseShard : "legacy-east-2",
	statusId : 3,
	assignedUserId : 99
};

Meilleur résultat :

return {
	ticketId : "TKT-12345",
	status : "in progress",
	assignedTeam : "Support",
	lastUpdated : "2026-06-26 09:15:00"
};

L’IA ne devrait pas dire accidentellement à l’utilisateur : « votre billet est dans le status_id 3 sur le segment de base de données legacy-east-2 ». Personne ne veut ça, sauf possiblement la personne qui a créé legacy-east-2, et même elle préférerait ne pas en parler.

Un exemple plus complet

Assemblons une démo plus réaliste. D’abord, le CFC de l’outil :

component output = false {

	public struct function getTicketStatus(
		required numeric userId,
		required string ticketId
	)
		hint = "Retourne l’état actuel d’un billet de soutien si l’utilisateur authentifié est autorisé à le consulter."
	{
		var normalizedTicketId = uCase( trim( arguments.ticketId ) );

		if ( !reFindNoCase( "^TKT-[0-9]{5}$", normalizedTicketId ) ) {
			throw(
				type = "AiTool.InvalidTicketId",
				message = "Invalid ticket ID."
			);
		}

		// Demo data. Replace with a real service/database lookup.
		if ( normalizedTicketId != "TKT-12345" ) {
			return {
				ticketId : normalizedTicketId,
				found : false,
				message : "No matching ticket was found."
			};
		}

		// Demo authorization. Replace with real permission logic.
		if ( arguments.userId <= 0 ) {
			throw(
				type = "Security.NotAuthorized",
				message = "The current user is not authorized."
			);
		}

		return {
			ticketId : normalizedTicketId,
			found : true,
			status : "in progress",
			priority : "normal",
			lastUpdated : "2026-06-26 09:15:00"
		};
	}

}

Voici maintenant la page :

<cfparam name="form.message" default="">

<cfscript>
result = "";

if ( !structKeyExists( application, "supportTool" ) ) {
	application.supportTool = new tools.SupportTool();
}

if ( !structKeyExists( application, "toolDemoAgent" ) ) {
	lock scope = "application" type = "exclusive" timeout = 10 {
		if ( !structKeyExists( application, "toolDemoAgent" ) ) {
			chatModel = ChatModel( {
				provider : "openAI",
				modelName : "gpt-5-nano",
				apiKey : application.aiApiKey,
				temperature : 0.3,
				maxTokens : 700,
				timeout : 30
			} );

			application.toolDemoAgent = Agent( {
				CHATMODEL : chatModel,
				CHATMEMORY : {
					TYPE : "messageWindowChatMemory",
					MAXMESSAGES : 20,
					PERUSER : true
				},
				TOOLS : [
					{
						CFC : "tools.SupportTool",
						METHODS : [
							{
								METHOD : "getTicketStatus",
								DESCRIPTION : "Obtenir l’état actuel d’un billet de soutien à partir de son ID. Utilisez ceci lorsque l’utilisateur demande l’avancement, l’état ou le statut d’un billet de soutien existant."
							}
						]
					}
				]
			} );

			application.toolDemoAgent.systemMessage(
				"You are a helpful support assistant. Use tools when current application data is needed. Do not guess ticket status."
			);
		}
	}
}

if ( len( trim( form.message ) ) ) {
	try {
		response = application.toolDemoAgent.chat(
			trim( form.message ),
			session.sessionId
		);

		if (
			structKeyExists( response, "toolExecutionRequests" )
			&& arrayLen( response.toolExecutionRequests )
		) {
			toolRequest = response.toolExecutionRequests[ 1 ];

			switch ( toolRequest.name ) {
				case "getTicketStatus":
					if ( !structKeyExists( toolRequest.arguments, "ticketId" ) ) {
						throw(
							type = "AiTool.MissingArgument",
							message = "Ticket ID is required."
						);
					}

					toolResult = application.supportTool.getTicketStatus(
						userId = session.userId,
						ticketId = toolRequest.arguments.ticketId
					);

					finalPrompt = "
						The user asked:
						#trim( form.message )#

						The application returned this ticket status result:
						#serializeJSON( toolResult )#

						Answer the user in one short, helpful paragraph.
						Do not add facts that are not in the tool result.
					";

					finalResponse = application.toolDemoAgent.chat(
						finalPrompt,
						session.sessionId
					);

					result = finalResponse.message;
					break;

				default:
					throw(
						type = "AiTool.UnsupportedTool",
						message = "Unsupported tool requested: #toolRequest.name#"
					);
			}
		} else {
			result = response.message;
		}
	} catch ( any error ) {
		writeLog(
			file = "ai",
			type = "error",
			text = "AI tool demo failed: #error.message#"
		);

		result = "Sorry, I could not complete that request right now.";
	}
}
</cfscript>

<cfoutput>
<form method="post">
	<label for="message">Message</label>
	<br>

	<textarea
		id="message"
		name="message"
		rows="5"
		cols="80"
	>#encodeForHtml( form.message )#</textarea>

	<br>

	<button type="submit">
		Envoyer
	</button>
</form>

<cfif len( result )>
	<h2>Response</h2>
	<pre>#encodeForHtml( result )#</pre>
</cfif>
</cfoutput>

Essayez de demander :

What is the status of ticket TKT-12345?

L’agent devrait reconnaître que cela nécessite les données actuelles de l’application et demander l’outil getTicketStatus. Votre application exécute ensuite l’outil, obtient le résultat et demande à l’agent de produire une réponse conviviale pour l’utilisateur.

Encore une fois, c’est une démo. L’important n’est pas la fausse donnée de billet. L’important est la frontière :

  • Le modèle demande.
  • ColdFusion valide.
  • ColdFusion exécute.
  • ColdFusion décide de ce qui est retourné.

Le modèle explique.

Mais pourquoi ne pas laisser le modèle tout appeler?

Parce que les modèles ne sont pas des frontières de sécurité. Ils sont très utiles, mais ils ne sont pas des systèmes d’autorisation. Ils ne savent pas quelles méthodes sont sécuritaires. Ils ne savent pas quels arguments sont sensibles. Ils ne savent pas si un utilisateur est autorisé à effectuer une action. Ils ne savent pas si deleteOldRecords() veut dire « supprimer d’anciens aperçus de brouillon » ou « retirer la moitié de l’historique client de l’entreprise ».

Votre application le sait… ou du moins, elle devrait. Si ce n’est pas le cas, veuillez mettre cet article en pause et aller avoir une conversation utile avec votre couche de service.

Exposer toutes les méthodes distantes peut être pratique pendant l’expérimentation, mais en production, exposez seulement les méthodes que l’IA est autorisée à demander. Le fait qu’une méthode soit accessible à distance en CFML ne signifie pas qu’elle soit appropriée comme outil d’IA. Accessible à distance pour votre application n’est pas la même chose qu’accessible au robot.

Les descriptions des outils devraient indiquer quand utiliser l’outil

Une erreur courante consiste à décrire ce que fait la méthode, mais pas quand le modèle devrait l’utiliser. Moins utile :

DESCRIPTION : "Returns ticket status."

Plus utile :

DESCRIPTION : "Get the current status of a support ticket by ticket ID. Use this when the user asks about the progress, state, or status of an existing support ticket."

Encore mieux, incluez des exemples si c’est utile :

DESCRIPTION : "Get the current status of a support ticket by ticket ID. Use this when the user asks questions like 'What is happening with ticket TKT-12345?' or 'Is my support ticket resolved yet?'"

La description de l’outil fait partie du contexte de décision du modèle. Traitez-la comme un mini guide d’instructions. Pas comme un commentaire que vous avez écrit en étant émotionnellement fini avec le sprint.

Gardez les outils petits

Les petits outils sont plus faciles à utiliser pour le modèle et plus faciles à sécuriser pour vous.

Bien : getTicketStatus( userId, ticketId )

Bien : listUpcomingEvents( userId, startDate, endDate )

Bien : calculateCartTotal( userId, cartId )

Suspect : handleUserRequest( input )

Très suspect : doEverything( payload )

Absolument pas : runArbitraryCode( code )

Le modèle ne devrait pas avoir une immense méthode magique où n’importe quoi peut arriver. Ce n’est pas un outil. C’est un portail. Et les portails, c’est comme ça que commencent les films.

Utilisez des services d’application en dessous

Votre CFC d’outil n’a pas besoin de contenir toute la logique d’affaires. En fait, il ne devrait généralement pas le faire. Un CFC d’outil peut être une fine enveloppe autour des services d’application existants. Par exemple :

component output = false {

	public order_tool function init(
		required any orderService,
		required any securityService
	) {
		variables.orderService = arguments.orderService;
		variables.securityService = arguments.securityService;

		return this;
	}

	public struct function getOrderStatus(
		required numeric userId,
		required string orderNumber
	) {
		var order = variables.orderService.getByOrderNumber(
			orderNumber = arguments.orderNumber
		);

		if ( !variables.securityService.userCanViewOrder(
			userId = arguments.userId,
			orderId = order.id
		) ) {
			throw(
				type = "Security.NotAuthorized",
				message = "The current user cannot view this order."
			);
		}

		return {
			orderNumber : order.orderNumber,
			status : order.status,
			placedAt : order.placedAt,
			total : order.totalFormatted
		};
	}

}

Cela empêche votre intégration IA de devenir une version d’univers parallèle de votre logique d’affaires. Vous avez déjà des services, utilisez-les. Ne faites pas de copier-coller des règles d’affaires dans le CFC d’outil parce que « c’était juste une petite démo rapide ». Cette phrase a créé plus de dette technique que n’importe quelle migration de base de données écrite à 1 h du matin.

Les erreurs d’outil devraient être sûres pour l’utilisateur

Les outils échouent.

  • Billet introuvable.
  • Utilisateur non autorisé.
  • Argument manquant.
  • Date invalide.
  • Service indisponible.
  • Délai d’attente de la base de données dépassé.
  • Quelqu’un a encore renommé l’API de préproduction et n’a laissé aucun témoin.

N’exposez pas les erreurs brutes aux utilisateurs. Interceptez les erreurs et traduisez-les en réponses sûres. Par exemple :

try {
	toolResult = application.supportTool.getTicketStatus(
		userId = session.userId,
		ticketId = toolRequest.arguments.ticketId
	);
} catch ( Security.NotAuthorized error ) {
	toolResult = {
		success : false,
		errorCode : "not_authorized",
		message : "The current user is not allowed to view this ticket."
	};
} catch ( AiTool.InvalidTicketId error ) {
	toolResult = {
		success : false,
		errorCode : "invalid_ticket_id",
		message : "The ticket ID format is invalid."
	};
} catch ( any error ) {
	writeLog(
		file = "ai-tools",
		type = "error",
		text = "Ticket status tool failed: #error.message#"
	);

	toolResult = {
		success : false,
		errorCode : "tool_failed",
		message : "The ticket status could not be retrieved right now."
	};
}

Ensuite, l’assistant peut expliquer le résultat sûr. L’utilisateur n’a pas besoin d’une trace de pile. Le modèle n’a pas besoin d’une trace de pile. Personne n’a besoin d’une trace de pile dans la fenêtre de clavardage. C’est à ça que servent les journaux.

Consignez l’utilisation des outils

Les appels d’outil sont des événements importants. Consignez-les. Les métadonnées utiles comprennent :

  • ID utilisateur
  • ID du locataire/compte/groupe
  • ID de conversation
  • nom de l’outil demandé
  • arguments assainis
  • si la demande était autorisée
  • si l’exécution a réussi
  • latence
  • code d’erreur
  • ID de la demande

Faites attention lorsque vous consignez des arguments bruts. Si un outil accepte du contenu utilisateur, des descriptions de billets, du texte d’courriel ou d’autres renseignements sensibles, assainissez ou masquez au besoin. Par exemple :

writeLog(
	file = "ai-tools",
	type = "information",
	text = "AI tool requested. userId=#session.userId# tool=#toolRequest.name#"
);

En production, les journaux structurés sont meilleurs. Mais même des journaux de base sont meilleurs que de découvrir que votre assistant IA demande des outils depuis trois semaines et que personne ne sait lesquels. L’observabilité n’est pas facultative. C’est ainsi que votre futur vous apprend ce que votre ancien vous a déchaîné.

Les outils et la mémoire ensemble

Les outils deviennent plus utiles lorsqu’ils sont combinés avec le sujet de notre article précédent, la mémoire. Par exemple :

User:
	My ticket is TKT-12345.

[removed]
	Got it.

User:
	What is the status?

[removed]
	Uses memory to know the ticket ID.
	Requests getTicketStatus.
	Answers with current status.

La mémoire fournit le contexte de la conversation. Les outils fournissent les faits de l’application. C’est exactement le genre de comportement en couches que nous voulons. Mais n’oubliez pas : la mémoire peut se souvenir d’un numéro de billet. L’outil doit quand même vérifier que l’utilisateur actuel peut accéder à ce billet. La mémoire rend l’assistant  cohérent. Les outils rendent l’assistant utile. L’autorisation rend l’assistant sécuritaire. Sautez ce dernier point et votre assistant devient un stagiaire zélé avec une imprimante à badges.

Outils et hallucinations

Les outils réduisent aussi les hallucinations. Sans outil, l’assistant pourrait répondre : “Your ticket is probably being reviewed.” Ce n’est pas bon. « Probablement » n’est pas un statut de billet.

Avec un outil, l’assistant peut dire : “Your ticket TKT-12345 is currently in progress,” parce que ce statut provient de votre application. C’est l’un des plus grands avantages des outils. Ils ancrent l’assistant dans l’état réel de l’application. Pas dans des impressions de modèle. Pas dans des données d’entraînement. Pas dans une supposition confiante en cravate. Des données réelles.

Les outils ne sont pas du RAG

Les outils et le RAG résolvent des problèmes différents. Utilisez les outils lorsque la réponse exige le comportement de l’application ou des données structurées. Par exemple :

  • Quel est l’état de ma commande ?
  • Suis-je inscrit ?
  • Quel est le solde de mon compte ?
  • Créer un billet de soutien.
  • Calculer les frais de livraison.
  • Afficher mes événements à venir.

Utilisez le RAG lorsque la réponse exige de récupérer du contenu pertinent de documents. par exemple :

  • Que dit la politique de remboursement ?
  • Comment configurer le SSO ?
  • Quelles sont les règles d’inscription ?
  • Que dit le guide de l’employé à propos du travail à distance ?
  • Comment fonctionne ce point de terminaison d’API ?

Vous pouvez les combiner. Un utilisateur pourrait demander : “Puis-je annuler mon inscription et obtenir un remboursement ?

Cela peut nécessiter :

  • un outil pour vérifier l’inscription de l’utilisateur
  • un outil pour vérifier l’état du paiement/remboursement
  • du RAG pour récupérer la politique de remboursement
  • une réponse finale de l’IA expliquant le résultat

C’est du ressort des séries ultérieures. Pour l’instant, les outils sont la façon dont l’assistant parle à votre application. Le RAG est la façon dont l’assistant parle à vos documents. Veuillez ne pas confondre les deux, à moins d’aimer construire des systèmes à la fois coûteux et faux.

Les outils ne sont pas du MCP

Les outils CFC sont locaux à votre application ColdFusion. Le MCP est un protocole standardisé pour exposer et consommer des outils, des instructions et des ressources à travers les systèmes. Utilisez les outils CFC lorsque :

  • la logique se trouve dans votre application ColdFusion
  • vous voulez une intégration locale simple
  • vous exposez directement des services d’application
  • vous n’avez pas besoin d’un serveur d’outils ou d’une frontière de protocole distincte

Utilisez MCP lorsque :

  • les outils se trouvent à l’extérieur de votre application
  • plusieurs clients ont besoin des mêmes outils
  • vous voulez une découverte d’outils standardisée
  • les outils doivent être partagés entre les applications
  • la gouvernance et l’auditabilité d’entreprise exigent une couche de protocole

Encore une fois, ils peuvent fonctionner ensemble. ColdFusion peut exposer la logique CFC par l’intermédiaire de MCP, et les agents ColdFusion peuvent utiliser des clients MCP. Mais pour cet article, les outils CFC sont le point de départ le plus simple et le plus direct.

Nous couvrirons MCP ensuite.

C’est à ce moment-là que le robot commence à demander un passeport.

Erreurs courantes

Passons en revue les façons évidentes que cela peut déraper.

  1. Exposer trop de méthodes. N’exposez pas un CFC entier à moins que chaque méthode publique soit intentionnellement sécuritaire pour une utilisation par l’IA. Utilisez METHODS. Soyez précis. Le robot n’a pas besoin d’accéder à toute votre boîte à outils. Surtout pas à la scie à chaîne.
  2. Descriptions médiocres. Les descriptions devraient indiquer au modèle quand utiliser l’outil, pas seulement ce que fait la méthode. De mauvaises descriptions font deviner le modèle. Le modèle est déjà assez bon pour deviner. C’est ça, le problème.
  3. Faire confiance aux arguments. Les arguments d’outil proviennent du modèle. Validez-les comme des entrées utilisateur. Parce que ce sont des entrées utilisateur après être passées dans un mélangeur à langage.
  4. Laisser le modèle fournir le contexte de sécurité. Ne laissez pas le modèle décider de userId, accountId, du rôle, du locataire, des autorisations ou de la propriété. Votre application fournit ce contexte. Toujours.
  5. Utiliser des outils d’écriture trop larges. Évitez les outils comme : updateUser( userData ) Ou : processRequest( action, payload ). Préférez des outils étroits et explicites.
  6. Aucune confirmation pour les écritures. Pour toute action qui modifie des données, envoie des messages, facture de l’argent, annule des choses, supprime des choses ou agace des humains, exigez une confirmation. Le modèle ne devrait pas pouvoir modifier la production parce qu’il semblait certain.
  7. Aucun journal. Les appels d’outil devraient être consignés. Si l’assistant peut demander des actions d’application, vous avez besoin d’une piste d’audit.
  8. Retourner trop de données. Ne retournez pas de gros objets internes. Retournez le minimum de données structurées nécessaire pour répondre à l’utilisateur. Votre modèle n’a pas besoin de tout votre schéma. L’utilisateur non plus. Honnêtement, certains développeurs n’en ont pas besoin non plus, mais c’est un autre article.
  9. Une meilleure première fonctionnalité d’outil. Une bonne première fonctionnalité d’outil CFC est en lecture seule et peu risquée. Par exemple :
  • vérifier l’état d’un billet
  • afficher les événements à venir
  • calculer une estimation
  • récupérer un résumé public de commande
  • valider si l’inscription est ouverte
  • vérifier si un nom d’utilisateur est disponible
  • résumer les paramètres du compte déjà visibles par l’utilisateur

Évitez de faire de votre première fonctionnalité d’outil :

  • envoyer des courriels
  • supprimer des dossiers
  • émettre des remboursements
  • mettre à jour la facturation
  • modifier les permissions
  • publier du contenu public
  • modifier la configuration de production
  • déclencher des tâches par lots avec des noms comme final_cleanup_REAL.cfm

Commencez en lecture seule. Puis ajoutez prudemment des outils d’écriture, avec confirmation et journalisation d’audit. Ce n’est pas de la lâcheté. C’est de l’ingénierie.

Où nous allons ensuite

À ce stade, notre assistant peut faire plus que répondre de mémoire. Il peut demander un accès contrôlé aux capacités de l’application. C’est une grande étape. Nous avons maintenant :

ChatModel() pour une génération simple sans état

Agent() pour la mémoire et les conversations à plusieurs tours

Des outils CFC pour les actions de l’application et les données réelles. Mais les outils CFC demeurent encore locaux à notre application. Que se passe-t-il lorsque les outils sont ailleurs? Que se passe-t-il lorsque plusieurs applications doivent partager les mêmes outils? Que se passe-t-il lorsque vous voulez un protocole standard pour exposer et consommer des outils, des invites et des ressources?

C’est là qu’entre en jeu MCP.

Dans le prochain article, nous présenterons le Model Context Protocol et verrons comment il s’intègre aux applications ColdFusion IA. Les outils CFC permettent à l’assistant d’utiliser votre application. MCP aide l’assistant à utiliser un écosystème. Ça sonne dramatique, mais surtout, ça veut dire qu’on peut ajouter un autre acronyme à la pile.

Dernière réflexion

Les outils sont l’endroit où l’IA commence à vraiment sembler utile dans une application. Un modèle peut expliquer. La mémoire peut se souvenir. Mais les outils permettent à l’assistant de faire quelque chose avec les véritables capacités de votre application.

C’est puissant.

C’est aussi risqué si vous traitez le modèle comme un opérateur de confiance. Alors ne le faites pas. Exposez des méthodes précises. Rédigez des descriptions claires. Validez les arguments. Injectez le contexte utilisateur authentifié à partir de votre application. Appliquez l’autorisation à l’intérieur de l’outil. Confirmez les actions d’écriture. Journalisez l’utilisation des outils. Ne retournez que les données dont le modèle a besoin.

Et souvenez-vous de la règle qui empêche toute cette série de se transformer en rapport d’incident :

L’IA peut demander… ColdFusion décide.