Chercher dans OpenAPI sans saturer la fenêtre de contexte

Fait partie de la série Construire le Code Mode pour les API avec MCP et Workers

Dans l'article précédent, nous avons découvert le Code Mode et l'intérêt de laisser le modèle écrire le code d'orchestration au lieu d'appeler directement les outils.

Dans cette troisième partie, nous allons voir comment utiliser le Code Mode lorsque la fenêtre de contexte est limitée ou lorsqu'un document, ici une spécification OpenAPI, dépasse la taille de cette fenêtre.

Le problème de la fenêtre de contexte

Lorsque nous développons un service ou une application, nous construisons souvent une API pour permettre à d'autres services d'interagir avec notre système. Les développeurs apprennent traditionnellement à utiliser cette API grâce à des sites de documentation, comme la référence de l'API Cloudflare.

Pour faciliter la création et la maintenance de ces sites, tout en les gardant synchronisés avec l'API réelle, le secteur a créé un format de spécification : OpenAPI.

yaml
openapi: 3.0.0
info:
  title: Sample API
  version: 1.0.0
paths:
  /users:
    get:
      summary: Get all users
      responses:
        '200':
          description: A list of users
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                    name:
                      type: string

Cet exemple présente une spécification OpenAPI simple pour une API qui possède un seul endpoint, /users, et renvoie une liste d'utilisateurs. La spécification décrit l'endpoint, la méthode HTTP, la réponse attendue et le format des données.

Ce format est très utile pour les traitements automatisés et peut générer une documentation pratique pour les humains. Une spécification brute volumineuse reste toutefois difficile à parcourir manuellement et peut dépasser la fenêtre de contexte d'un modèle. Dans ce cas, nous ne pouvons pas simplement transmettre le document complet au modèle et espérer qu'il l'utilise correctement.

Ces fichiers évoluent, leurs tailles exactes changeront donc avec le temps. Le point important est que les descriptions d'API réelles peuvent être bien plus volumineuses que les informations nécessaires à une tâche donnée.

L'instantané Cloudflare utilisé dans cette série contient 23 792 523 caractères. L'approximation courante de quatre caractères par jeton donne environ 6 millions de jetons, même si la tokenisation varie selon le modèle et le contenu. Dans tous les cas, ce volume dépasse largement de nombreuses fenêtres de contexte disponibles.

Nous ne pouvons pas transmettre toute la spécification au modèle.

Les spécifications OpenAPI s'adressent aux humains et aux machines

OpenAPI permet aux humains comme aux logiciels de comprendre une API. Un générateur de documentation peut transformer la spécification en site web, tandis qu'un générateur de client peut la convertir en code. Pour un modèle, renvoyer toute la spécification dans le contexte reste généralement une mauvaise interface. Le modèle a besoin des endpoints et des schémas utiles à sa tâche, pas de toutes les opérations de l'API.

Rien ne nous empêche cependant de construire un outil capable de rechercher dans la spécification OpenAPI et de ne renvoyer que les informations pertinentes. Imaginez que vous intégrez une plateforme de vente en ligne et que vous cherchez comment créer un retour.

Au lieu d'utiliser Ctrl+F ou Cmd+F dans la spécification OpenAPI, pourquoi ne pas appliquer l'algorithme suivant ?

text
searchTerm = "return"
results = []

for each URL path and path item in spec.paths:
    for each HTTP method and operation in the path item:
        if the entry is not an HTTP operation:
            continue

        searchableText = combine:
            - the URL path
            - the operation ID
            - the summary
            - the description
            - the operation tags

        if searchableText does not contain searchTerm:
            continue

        endpoint = {
            method: the HTTP method in uppercase,
            path: the URL path,
            operationId: the operation ID,
            summary: the operation summary,
            parameters: [
                for each parameter:
                    keep its name, location, and required flag
            ],
            requestBody: keep only its content types and required flag,
            responses: [
                for each response:
                    keep its status code and description
            ]
        }

        add endpoint to results

return results

Nous n'extrayons ainsi que les informations pertinentes. Mais nous pouvons aller plus loin.

Même avec un tel algorithme, nous devons encore exécuter le code manuellement et rien ne garantit qu'il couvrira tous les cas. Pour créer un retour, nous pourrions par exemple ne conserver que les endpoints qui utilisent la méthode POST. Un filtrage par tags serait peut-être plus efficace que de deviner les bons mots à rechercher dans le résumé et la description. Nous pourrions aussi ne conserver que les endpoints qui possèdent un code de réponse 200. Les possibilités de filtrage varient selon le client, les données nécessaires et la tâche à accomplir. Écrire un algorithme générique devient donc bien trop complexe.

Il faudrait écrire un algorithme personnalisé pour chaque cas d'usage.

Les LLM connaissent le code et les algorithmes

Ou nous pouvons déléguer cette tâche à un LLM.

Les LLM savent notamment générer du code. Ils peuvent transformer une demande précise en petit programme de recherche que les développeurs peuvent relire et exécuter.

Au lieu d'écrire un algorithme générique, ou des milliers pour tenter de couvrir tous les cas, nous pourrions simplement demander au LLM d'écrire celui dont nous avons besoin. Il ne resterait ensuite qu'à l'exécuter. Le LLM recevrait exactement les informations utiles, sans que la fenêtre de contexte devienne un problème.

J'ai découvert cette idée dans un article de Cloudflare intitulé Code Mode, et ma première réaction a été « quoi ? ». J'ai dû le lire plus de deux fois pour vraiment comprendre le concept. Prenons un exemple.

Imaginez que vous souhaitiez créer un retour sur une plateforme de vente en ligne. Au lieu de fournir toute la spécification OpenAPI au modèle, vous pourriez écrire la méthode suivante :

js
async () => {
  const results = []

  for (const [path, methods] of Object.entries(spec.paths)) {
    const operation = methods.post

    if (operation?.tags?.some(tag => tag.toLowerCase() === 'returns')) {
      results.push({
        method: 'POST',
        path,
        summary: operation.summary,
        requestBody: operation.requestBody
      })
    }
  }

  return results
}

Ce code permet de rechercher rapidement dans la spécification OpenAPI et de ne renvoyer que les informations pertinentes.

Mais qui veut écrire ce code ? Personne.

Heureusement, les LLM peuvent le générer à notre place. Donnez-leur accès à la spécification OpenAPI et quelques exemples, et ils pourront parcourir toute la spécification pour trouver uniquement ce qui compte.

Outil, description et langage

Dans notre MCP, l'outil de recherche ressemblerait à ceci :

typescript
server.registerTool(
  'search',
  {
    title: 'Search OpenAPI Specification',
    description: '...',
    inputSchema: z.object({
      code: z
        .string()
        .max(MAX_CODE_LENGTH)
        .describe('An async arrow function that searches the OpenAPI spec')
    }),
  },
  async ({ code }) => {
    try {
      const result = await evaluateSearchCode(code)
      return { content: [{ type: 'text', text: formatSearchResult(result) }] }
    }
    catch (error) {
      return {
        content: [{ type: 'text', text: `Search failed: ${errorMessage(error)}` }],
        isError: true
      }
    }
  }
)

À première vue, rien ne change par rapport à l'article précédent. Les détails se trouvent dans la description et la fonction d'évaluation.

La description commence comme celle de n'importe quel outil. Elle explique son rôle et ce que le modèle peut en faire.

text
Search the MarketHub OpenAPI specification with JavaScript.

The specification stays inside the server. Your code must be an async arrow function and should return only the small result needed for the next decision. All local $refs are resolved before the code runs.

C'est ensuite que les choses deviennent intéressantes.

Nous pouvons fournir des interfaces TypeScript pour offrir au modèle une façade typée sur laquelle écrire son code. Ce contrat compact augmente ses chances de produire un programme fonctionnel dès la première tentative.

typescript
interface OperationInfo {
  operationId?: string
  summary?: string
  description?: string
  tags?: string[]
  parameters?: Array<{
    name?: string
    in?: string
    required?: boolean
    description?: string
    schema?: unknown
  }>
  requestBody?: unknown
  responses?: Record<string, unknown>
}

declare const spec: {
  paths: Record<string, {
    get?: OperationInfo
    post?: OperationInfo
    put?: OperationInfo
    patch?: OperationInfo
    delete?: OperationInfo
  }>
}

La spécification OpenAPI complète reste sur le serveur. Le programme généré peut l'inspecter par l'intermédiaire de cette façade, tandis que le modèle ne reçoit que l'interface compacte et le résultat final de la recherche.

Note

La spécification OpenAPI est simplifiée dans le MCP pour la rendre plus facile à utiliser par l'IA.

Enfin, voici quelques exemples qui montrent au modèle comment utiliser l'outil et quel résultat renvoyer.

text
// Inspect one operation's request body
async () => spec.paths['/products']?.post?.requestBody

// Inspect endpoint parameters
async () => spec.paths['/products']?.get?.parameters

Le modèle peut désormais rechercher dans la spécification OpenAPI sans recevoir le document complet dans son contexte.

Recherche dans la spécification OpenAPI sans renvoyer le document complet.

Mais pourquoi demander au LLM d'écrire du JavaScript plutôt qu'un autre langage ?

Nous choisissons JavaScript parce qu'il est très répandu, familier aux modèles et simple à exécuter dans l'environnement ciblé. L'interface TypeScript ci-dessus sert de documentation au modèle. Elle fournit les types statiques de la façade, mais elle n'est pas exécutée.

La découverte n'est pas l'exécution

Savoir quels endpoints appeler constitue un bon début, mais cela ne suffit pas. À partir des données recueillies, le LLM pourrait générer le code qui appelle l'endpoint. La boucle serait ainsi complète. Sans connaissance préalable, le LLM pourrait à la fois découvrir l'API et interagir avec elle.

Dans la partie 4, nous ajouterons une capacité d'exécution distincte. Elle permettra au code généré d'appeler une fonction api.request() ciblée et contrôlée par l'hôte, puis de renvoyer uniquement la partie de la réponse dont le modèle aura besoin.

Pd

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 !

Continuer la lectureAppeler des API avec le Code Mode et garder l'essentiel

Réactions

Discussions

Ajouter un commentaire

Vous devez être connecté pour accéder à cette fonctionnalité.