Laisser le modèle orchestrer les outils avec du code local
Fait partie de la série Construire le Code Mode pour les API avec MCP et Workers
Cloudflare a popularisé le Code Mode en septembre 2025 avec un article intitulé Code Mode: the better way to use MCP. L'article mérite vraiment d'être lu, mais il peut sembler un peu déroutant au premier abord. Il m'a fallu plusieurs mois pour bien comprendre le concept et savoir comment l'utiliser correctement.
Dans cette partie, nous remplacerons les deux outils Drive directs par un unique outil code générique. Nous exposerons une façade Drive typée et utiliserons un évaluateur local de confiance basé sur le constructeur Function. Ce raccourci nous servira pendant le développement avant son isolation dans la partie 5.
Partir d'un mode en plusieurs tours
Dans l'article précédent, nous avons étudié l'exemple suivant : créer un fichier qui contient tous les titres d'un fichier Markdown présent dans un Drive.
Pour y parvenir, nous avons construit un serveur MCP avec deux outils, readFile et writeFile, puis anticipé le comportement du modèle.
Le modèle doit d'abord lire le fichier avec readFile, ce qui charge l'intégralité de son contenu dans le contexte. Il doit ensuite extraire les titres de ce contenu et appeler writeFile pour les écrire dans un nouveau fichier.
Du point de vue du réseau, les messages échangés entre le modèle et le serveur MCP ressemblent à ceci :
LLM -> MCP: readFile(path="myfile.md")
MCP -> LLM: { content: "..." } // entire file content
LLM -> MCP: writeFile(path="myfile_titles.md", content="...")
MCP -> LLM: { content: "{ success: true }" }Passer à un seul tour
L'approche précédente fonctionne, mais elle présente quelques défauts.
- Pour atteindre notre objectif, l'écriture dépend directement de la lecture. Nous savons dès le départ que ces deux étapes sont nécessaires. En théorie, un seul tour devrait donc suffire.
- Le modèle doit charger l'intégralité du fichier dans son contexte. Il consomme ainsi du temps de traitement, des jetons et de l'espace de contexte pour des informations qu'il n'a pas besoin de connaître.
C'est précisément dans ce cas que le Code Mode montre son intérêt.
Comme s'il s'agissait d'une API
Si le Drive était exposé sous la forme d'une API de package plutôt que d'un MCP, à quoi ressemblerait notre code ?
import { readFile, writeFile } from 'drive'
async function createFileWithTitles(inputPath: string, outputPath: string) {
const content = await readFile(inputPath)
const titles = content.match(/^# (.*)$/gm)?.map(title => title.replace(/^# /, '')) ?? []
await writeFile(outputPath, titles.join('\n'))
}Il pourrait ressembler à cela.
Au lieu d'exposer readFile et writeFile comme des appels d'outils séparés, nous les exposons comme des fonctions. L'implémentation sous-jacente pourrait rester identique. Seule la façade change, tandis que le comportement attendu reste le même.
Plutôt que de demander au modèle d'orchestrer les outils, nous pourrions maintenant lui demander d'orchestrer les fonctions en écrivant le code à notre place.
Les LLM connaissent bien le code
Les LLM sont souvent plus fiables lorsqu'ils écrivent des structures de code familières que lorsqu'ils doivent choisir parmi un ensemble complexe d'outils. Le Code Mode exploite cette force sans donner au code généré un accès illimité à l'hôte.
Au lieu de fournir plusieurs outils au modèle, nous pouvons donc lui fournir un seul outil code. Pour lui indiquer quoi faire et quelles fonctions sont disponibles, nous pouvons ajouter une interface typée à la description de l'outil, exactement comme dans les fichiers TypeScript .d.ts.
Run one small JavaScript program against the local Drive capability.
The generated code is an async arrow function. It receives only the named capabilities below and should return the compact result needed by the next decision.
interface Drive {
readFile: (path: string) => Promise<string>;
writeFile: (path: string, content: string) => Promise<void>;
}
declare const drive: Drive;
Example:
async () => {
const content = await drive.readFile('article.md');
const headings = content
.split('\\n')
.filter((line) => /^#{1,6}\\s/.test(line))
.map((line) => line.replace(/^#+\\s*/, ''));
await drive.writeFile('titles.md', headings.join('\\n'));
return { headingCount: headings.length };
}Cette description fournit plusieurs informations au modèle :
- Une courte explication de ce que le code doit accomplir.
- Le format attendu, ici une fonction fléchée asynchrone qui reçoit uniquement les capacités nommées et renvoie un résultat compact.
- L'interface
Drive, qui décrit les fonctions disponibles et leurs types. - La constante
drive, une instance de l'interfaceDriveque le modèle peut utiliser. Elle lui indique quedrive.readFileetdrive.writeFilesont disponibles comme variable globale pour lire et écrire des fichiers. - Un exemple d'utilisation de la constante
drive. Cet exemple donne au modèle une référence claire, améliore sa fiabilité et réduit le risque qu'il génère un code défaillant.
L'outil pourrait alors ressembler à ceci :
server.registerTool(
'code',
{
title: 'Drive Code Mode',
description: '...', // Description from the previous code block
inputSchema: z.object({
code: z
.string()
.describe('An async arrow function that uses the typed Drive capability')
}),
annotations: {
title: 'Drive Code Mode'
}
},
async ({ code }) => {
const result = await evaluateCode(code)
const text = formatCodeResult(result)
return { content: [{ type: 'text', text }] }
}
)Comme vous pouvez le constater, l'outil code reste très générique. Toute sa puissance se trouve dans la fonction evaluateCode, qui exécute le code, de préférence dans un environnement isolé.
Pour notre exemple, nous utiliserons toutefois un évaluateur local de confiance basé sur le constructeur Function. Il n'est pas sûr pour la production, mais il suffit pour cette étape de développement.
// eslint-disable-next-line no-new-func
const orchestrate = new Function(
...Object.keys(capabilities),
`"use strict"; return (${code.trim()});`
)(...Object.values(capabilities))
const result = await orchestrate()Important
Dans un environnement de production, vous devez utiliser un évaluateur isolé. Consultez Remplacer l'évaluateur par des Workers Cloudflare isolés pour en savoir plus.
Si vous n'avez jamais utilisé ce constructeur, c'est normal. Il n'est pas sûr et ne doit pas exécuter de code non fiable, y compris du code généré par un LLM, en dehors de cette étape de développement de confiance.
Pour résumer rapidement, le constructeur Function crée une nouvelle fonction à partir de la chaîne de code fournie. Les premiers arguments correspondent aux noms des paramètres, et le dernier contient le corps de la fonction. Ici, nous transmettons la capacité drive comme paramètre et renvoyons la fonction fléchée asynchrone générée par le modèle.
Des bibliothèques pour le Code Mode
Le Code Mode peut être pénible à implémenter, car les types de la description de l'outil doivent rester synchronisés avec l'implémentation réelle.
Heureusement, des bibliothèques comme Cloudflare Code Mode et TanStack Code Mode permettent de l'implémenter facilement à partir de la même définition d'outil qu'un MCP.
Ce que nous avons construit
Nous avons remplacé deux appels d'outils dépendants par un programme généré qui utilise une façade Drive typée et renvoie uniquement le résultat nécessaire à la décision suivante. La frontière de l'exécuteur reste volontairement étroite. Les prochains articles conserveront la même approche, mais remplaceront le Drive local par une spécification OpenAPI bien plus volumineuse.
Dans la partie 3, nous utiliserons cette frontière pour rechercher dans un document OpenAPI sans placer toute la spécification dans le contexte du modèle.
Merci de me lire ! Je m'appelle Estéban, et j'adore écrire sur le développement web et le parcours humain qui l'entoure.
Je code depuis plusieurs années maintenant, et j'apprends encore de nouvelles choses chaque jour. J'aime partager mes connaissances avec les autres, car j'aurais aimé avoir accès à des ressources aussi claires et complètes lorsque j'ai commencé à apprendre la programmation.
Si vous avez des questions ou souhaitez discuter, n'hésitez pas à commenter ci-dessous ou à me contacter sur Bluesky, X, et LinkedIn.
J'espère que vous avez apprécié cet article et appris quelque chose de nouveau. N'hésitez pas à le partager avec vos amis ou sur les réseaux sociaux, et laissez un commentaire ou une réaction ci-dessous, cela me ferait très plaisir ! Si vous souhaitez soutenir mon travail, vous pouvez me sponsoriser sur GitHub !
Discussions
Ajouter un commentaire
Vous devez être connecté pour accéder à cette fonctionnalité.