LLMOps Modèles d’ingénierie de prompt de production avec Hamilton

LLMOps - Modèles d'ingénierie de prompt de production avec Hamilton

Un aperçu des façons de qualité de production d’itérer sur les suggestions avec Hamilton

Suggestions. Comment les faire évoluer dans un contexte de production ? Cet article est basé sur un article qui est apparu initialement ici. Image de pixabay.

Ce que vous envoyez à votre modèle de langage de grande taille (LLM) est très important. De petites variations et des changements peuvent avoir de grands impacts sur les résultats, donc à mesure que votre produit évolue, la nécessité de faire évoluer vos suggestions également. Les LLM sont également constamment développés et publiés, donc à mesure que les LLM changent, vos suggestions devront également changer. Il est donc important de mettre en place un modèle d’itération pour opérationnaliser comment vous “déployez” vos suggestions afin que vous et votre équipe puissiez avancer efficacement tout en minimisant, voire en évitant, les problèmes de production. Dans cet article, nous vous guiderons à travers les meilleures pratiques de gestion des suggestions avec Hamilton, un framework de micro-orchestration open source, en faisant des analogies avec les modèles MLOps et en discutant des compromis à faire en cours de route. Les principales conclusions de cet article restent applicables même si vous n’utilisez pas Hamilton.

Quelques points avant de commencer :

  1. Je suis l’un des co-créateurs de Hamilton.
  2. Pas familier avec Hamilton ? Faites défiler jusqu’en bas pour plus de liens.
  3. Si vous recherchez un article qui parle de “gestion du contexte”, ce n’est pas cet article. Mais c’est l’article qui vous aidera avec les détails pratiques sur la façon d’itérer et de créer cette histoire itérative de “gestion du contexte des suggestions” de qualité de production.
  4. Nous utiliserons les termes “suggestion” et “modèle de suggestion” de manière interchangeable.
  5. Nous supposerons que ces suggestions sont utilisées dans le cadre d’un service web “en ligne”.
  6. Nous utiliserons l’exemple du résumeur de PDF d’Hamilton pour projeter nos modèles.
  7. Quelle est notre crédibilité ici ? Nous avons passé notre carrière à construire des outils de données/MLOps en libre-service, notamment pour plus de 100 Data Scientists de Stitch Fix. Nous avons donc vu notre part de pannes et d’approches se dérouler au fil du temps.

Les suggestions sont aux LLMs ce que les hyper-paramètres sont aux modèles ML

Point : Les suggestions + les API LLM sont analogues aux hyper-paramètres + aux modèles d’apprentissage automatique.

En ce qui concerne les pratiques “Ops”, LLMOps en est encore à ses balbutiements. MLOps est un peu plus ancien, mais les deux ne sont toujours pas largement adoptés si vous les comparez à la connaissance répandue des pratiques DevOps.

Les pratiques DevOps concernent principalement la façon dont vous livrez du code en production, et les pratiques MLOps concernent la façon de livrer du code & des artefacts de données (par exemple, des modèles statistiques) en production. Alors, qu’en est-il de LLMOps ? Personnellement, je pense que c’est plus proche de MLOps car vous avez :

  1. votre flux de travail LLM est simplement du code.
  2. et une API LLM est un artefact de données qui peut être “ajusté” à l’aide de suggestions, tout comme un modèle d’apprentissage automatique (ML) et ses hyper-paramètres.

Par conséquent, vous vous souciez très probablement de versionner étroitement l’API LLM + les suggestions pour de bonnes pratiques de production. Par exemple, dans la pratique MLOps, vous voudriez mettre en place un processus pour valider que votre modèle ML se comporte toujours correctement lorsque ses hyper-paramètres sont modifiés.

Comment devriez-vous penser à opérationnaliser une suggestion ?

Pour être clair, les deux parties à contrôler sont le LLM et les suggestions. Tout comme pour MLOps, lorsque le code ou l’artefact du modèle change, vous voulez pouvoir déterminer lequel a changé. Pour LLMOps, nous voulons la même distinction, en séparant le flux de travail LLM de l’API LLM + des suggestions. Il est important de noter que les LLM (auto-hébergés ou des API) sont principalement statiques, car nous mettons moins fréquemment à jour (voire même contrôlons) leurs parties internes. Ainsi, modifier la partie des suggestions de l’API LLM + des suggestions revient effectivement à créer un nouvel artefact de modèle.

Il existe deux principales façons de traiter les invites :

  1. Les invites en tant que variables d’exécution dynamiques. Le modèle utilisé n’est pas statique lors d’un déploiement.
  2. Les invites en tant que code. Le modèle d’invite est statique/prédéterminé lors d’un déploiement.

La principale différence réside dans le nombre de composants à gérer pour garantir une excellente histoire de production. Ci-dessous, nous expliquerons comment utiliser Hamilton dans le contexte de ces deux approches.

Les invites en tant que variables d’exécution dynamiques

Transmettre/Décharger dynamiquement les invites

Les invites ne sont que des chaînes de caractères. Étant donné que les chaînes de caractères sont un type primitif dans la plupart des langages, il est assez facile de les transmettre. L’idée est d’abstraire votre code de sorte qu’à l’exécution, vous transmettiez les invites requises. Plus concrètement, vous “chargez/rechargez” les modèles d’invite chaque fois qu’un nouveau modèle est disponible.

L’analogie MLOps ici serait le rechargement automatique de l’artefact du modèle ML (par exemple, un fichier pkl) chaque fois qu’un nouveau modèle est disponible.

Analogie MLOps : diagramme montrant à quoi ressemblerait le rechargement automatique du modèle ML. Image de l'auteur.
Diagramme montrant à quoi ressemblerait le rechargement/d'interrogation dynamique des invites. Image de l'auteur.

L’avantage ici est que vous pouvez déployer très rapidement de nouvelles invites car vous n’avez pas besoin de redéployer votre application !

L’inconvénient de cette rapidité d’itération est une charge opérationnelle accrue :

  1. Pour quelqu’un qui surveille votre application, il sera difficile de savoir quand le changement s’est produit et s’il s’est propagé dans vos systèmes. Par exemple, vous venez d’ajouter une nouvelle invite et le LLM renvoie maintenant plus de jetons par requête, ce qui provoque une augmentation de la latence ; celui qui surveille sera probablement perplexe, à moins que vous n’ayez une excellente culture de journal des changements.
  2. Les sémantiques de retour en arrière nécessitent de connaître un autre système. Vous ne pouvez pas simplement revenir à un déploiement antérieur pour corriger les choses.
  3. Vous aurez besoin d’une surveillance approfondie pour comprendre ce qui a été exécuté et quand ; par exemple, lorsque le service client vous signale un problème à enquêter, comment savez-vous quelle invite était utilisée ?
  4. Vous devrez gérer et surveiller le système que vous utilisez pour gérer et stocker vos invites. Ce sera un système supplémentaire que vous devrez entretenir en dehors de ce qui sert votre code.
  5. Vous devrez gérer deux processus, l’un pour mettre à jour et pousser le service, et l’autre pour mettre à jour et pousser les invites. La synchronisation de ces changements vous incombera. Par exemple, vous devez apporter une modification de code à votre service pour gérer une nouvelle invite. Vous devrez coordonner le changement de deux systèmes pour que cela fonctionne, ce qui représente une surcharge opérationnelle supplémentaire à gérer.

Comment cela fonctionnerait avec Hamilton

Notre flux de synthèse PDF ressemblerait à quelque chose comme cela si vous supprimez les définitions de fonctions summarize_text_from_summaries_prompt et summarize_chunk_of_text_prompt :

summarization_shortened.py. Notez les deux entrées *_prompt qui indiquent les invites désormais requises en entrée pour que le flux de données fonctionne. Avec Hamilton, vous pourrez déterminer quelles entrées doivent être requises pour votre modèle d'invite en regardant simplement un diagramme comme celui-ci. Diagramme créé via Hamilton. Image de l'auteur.

Pour faire fonctionner les choses, vous voudrez soit injecter les invites au moment de la demande :

from hamilton import base, driver
import summarization_shortend

# créer le driver
dr = (
    driver.Builder()
    .with_modules(summarization_sortened)
    .build())

# récupérer les instructions depuis un endroit
summarize_chunk_of_text_prompt = """QUELQUES INSTRUCTIONS POUR {chunked_text}"""
summarize_text_from_summaries_prompt = """QUELQUES INSTRUCTIONS {summarized_chunks} ... {user_query}"""

# exécuter et passer les instructions en entrée
result = dr.execute(
    ["summarized_text"],
    inputs={
        "summarize_chunk_of_text_prompt": summarize_chunk_of_text_prompt,
        ...
    })

Ou vous pouvez modifier votre code pour charger dynamiquement les instructions, c’est-à-dire ajouter des fonctions pour récupérer les instructions depuis un système externe dans le flux de données de Hamilton. À chaque invocation, elles interrogeront les instructions à utiliser (vous pouvez bien sûr mettre en cache cela pour les performances) :

# prompt_template_loaders.py
def summarize_chunk_of_text_prompt(db_client: Client, other_args: str) -> str:
    # pseudo code ici, mais vous avez compris l'idée :
    _prompt = db_client.query("obtenir la dernière instruction X depuis la base de données", other_args)
    return _prompt

def summarize_text_from_summaries_prompt(db_client: Client, another_arg: str) -> str:
    # pseudo code ici, mais vous avez compris l'idée :
    _prompt = db_client.query("obtenir la dernière instruction Y depuis la base de données", another_arg)
    return _prompt

Code du driver :

from hamilton import base, driver
import prompt_template_loaders # <-- charger ceci pour fournir l'entrée de l'instruction
import summarization_shortend

# créer le driver
dr = (
    driver.Builder()
    .with_modules(
        prompt_template_loaders, # <-- Hamilton appellera les fonctions ci-dessus
        summarization_sortened,
    )
    .build())

# exécuter et passer le résultat des instructions en entrée
result = dr.execute(
    ["summarized_text"],
    inputs={
        # pas besoin de passer les instructions dans cette version
    })

Comment enregistrer les instructions utilisées et surveiller les flux ?

Voici quelques façons de surveiller ce qui s’est passé :

  • Enregistrer les résultats de l’exécution. C’est-à-dire exécuter Hamilton, puis émettre les informations où vous le souhaitez.
result = dr.execute(
    ["summarized_text",
    "summarize_chunk_of_text_prompt",
    ... # et tout ce que vous voulez extraire
    "summarize_text_from_summaries_prompt"],
    inputs={
        # pas besoin de passer les instructions dans cette version
    })

my_log_system(result) # envoyer ce que vous souhaitez conserver en sécurité vers un système que vous possédez.

Note. Dans l’exemple ci-dessus, Hamilton vous permet de demander n’importe quelle sortie intermédiaire simplement en demandant les “fonctions” (c’est-à-dire les nœuds du diagramme) par leur nom. Si vous voulez vraiment obtenir toutes les sorties intermédiaires de tout le flux de données, vous pouvez le faire et le journaliser où vous le souhaitez !

  • Utiliser des enregistreurs à l’intérieur des fonctions de Hamilton (pour voir la puissance de cette approche, consultez ma vieille présentation sur les journaux structurés) :
import logging

logger = logging.getLogger(__name__)

def summarize_text_from_summaries_prompt(db_client: Client, another_arg: str) -> str:
    # pseudo code ici, mais vous avez compris l'idée :
    _prompt = db_client.query("obtenir la dernière instruction Y depuis la base de données", another_arg)
    logger.info(f"Instruction utilisée est [{_prompt}]")
    return _prompt
  • Etendre Hamilton pour émettre ces informations. Vous pouvez utiliser Hamilton pour capturer des informations à partir des fonctions exécutées, c’est-à-dire des nœuds, sans avoir besoin d’insérer des instructions de journalisation à l’intérieur du corps de la fonction. Cela favorise la réutilisabilité, car vous pouvez activer ou désactiver la journalisation entre les paramètres de développement et de production au niveau du Driver. Voir GraphAdapters, ou écrire votre propre décorateur Python pour envelopper les fonctions de surveillance.

<Dans n'importe lequel de ces codes, vous pouvez facilement intégrer un outil tiers pour aider à suivre et surveiller le code, ainsi que l'appel à l'API externe.

Instructions comme code

Instructions comme chaînes statiques

Étant donné que les instructions sont simplement des chaînes, elles sont également très adaptables pour être stockées avec votre code source. L’idée est de stocker autant de versions d’instructions que vous le souhaitez dans votre code afin qu’au moment de l’exécution, l’ensemble des instructions disponibles soit fixe et déterministe.

L’analogie MLOps ici est la suivante : au lieu de recharger dynamiquement les modèles, vous incorporez plutôt le modèle ML dans le conteneur ou codez en dur la référence. Une fois déployée, votre application dispose de tout ce dont elle a besoin. Le déploiement est immuable ; rien ne change une fois qu’il est en place. Cela facilite le débogage et la détermination de ce qui se passe.

Analogie MLOps : réaliser un déploiement immuable en fixant le modèle pour le déploiement de votre application. Image de l'auteur.
Diagramme montrant comment traiter les invites comme du code vous permet de tirer parti de votre CI/CD et de construire un déploiement immuable pour communiquer avec votre API LLM. Image de l'auteur.

Cette approche présente de nombreux avantages opérationnels :

  1. Chaque fois qu’une nouvelle invite est poussée, cela force un nouveau déploiement. La sémantique de retour en arrière est claire s’il y a un problème avec une nouvelle invite.
  2. Vous pouvez soumettre une demande de tirage (PR) pour le code source et les invites en même temps. Il devient plus simple de passer en revue ce qui a changé et les dépendances en aval de ces invites.
  3. Vous pouvez ajouter des vérifications à votre système CI/CD pour vous assurer que les mauvaises invites n’atteignent pas la production.
  4. Il est plus simple de déboguer un problème. Vous n’avez qu’à extraire le conteneur (Docker) qui a été créé et vous pourrez reproduire rapidement et facilement n’importe quel problème client.
  5. Il n’y a pas d’autre “système d’invite” à maintenir ou à gérer. Simplification des opérations.
  6. Cela n’exclut pas l’ajout de surveillance et de visibilité supplémentaires.

Comment cela fonctionnerait avec Hamilton

Les invites seraient encodées sous forme de fonctions dans le flux de données/graphique acyclique dirigé (DAG) :

À quoi ressemble le fichier summarization.py dans l'exemple de résumé de PDF. Les modèles d'invite font partie du code. Diagramme créé via Hamilton. Image de l'auteur.

En associant ce code à git, vous disposez d’un système de versionnement léger pour l’ensemble de votre flux de données (c’est-à-dire la “chaîne”), vous permettant ainsi de connaître toujours l’état du monde à un instant donné, étant donné un commit SHA de git. Si vous souhaitez gérer et avoir accès à plusieurs invites à un moment donné, Hamilton dispose de deux puissantes abstractions pour vous permettre de le faire : @config.when et les modules Python. Cela vous permet de stocker et de rendre disponibles toutes les anciennes versions d’invite ensemble et de spécifier celle à utiliser via du code.

@config.when (docs)

Hamilton dispose d’un concept de décorateurs, qui ne sont que des annotations sur les fonctions. Le décorateur @config.when permet de spécifier des implémentations alternatives pour une fonction, c’est-à-dire un “nœud”, dans votre flux de données. Dans ce cas, nous spécifions des invites alternatives.

from hamilton.function_modifiers import [email protected](version="v1")def summarize_chunk_of_text_prompt__v1() -> str:    """Invite V1 pour résumer des morceaux de texte."""    return f"Résumez ce texte. Extraites les points clés avec explication.\n\nContenu:"@config.when(version="v2")def summarize_chunk_of_text_prompt__v2(content_type: str = "un document académique") -> str:    """Invite V2 pour résumer des morceaux de texte."""    return f"Résumez ce texte de {content_type}. Extraites les points clés avec explication. \n\nContenu:"

Vous pouvez ajouter des fonctions annotées avec @config.when, ce qui vous permet de les interchanger en utilisant la configuration transmise au Driver de Hamilton. Lors de l’instanciation du Driver, il construira le flux de données en utilisant l’implémentation d’invite associée à la valeur de configuration.

from hamilton import base, driver
import summarization

# créer le driver
dr = (driver.Builder()
      .with_modules(summarization)
      .with_config({"version": "v1"}) # V1 est choisi. Utilisez "v2" pour V2.
      .build())

Changement de module

Alternativement à l’utilisation de @config.when, vous pouvez plutôt placer vos différentes implémentations de prompt dans différents modules Python. Ensuite, au moment de la construction du Driver, passez le module correct pour le contexte que vous souhaitez utiliser.

Donc ici, nous avons un module contenant la version V1 de notre prompt:

# prompts_v1.pydef summarize_chunk_of_text_prompt() -> str:    """Prompt V1 pour résumer des morceaux de texte."""    return f"Résumez ce texte. Extrayez les points clés avec leur justification.\n\nContenu:"

Ici, nous avons un module contenant la version V2 (voyez comment elles diffèrent légèrement):

# prompts_v2.pydef summarize_chunk_of_text_prompt(content_type: str = "un document académique") -> str:    """Prompt V2 pour résumer des morceaux de texte."""    return f"Résumez ce texte provenant de {content_type}. Extrayez les points clés avec leur justification. \n\nContenu:"

Dans le code du driver ci-dessous, nous choisissons le bon module à utiliser en fonction du contexte.

# run.pyfrom hamilton import driverimport summarizationimport prompts_v1import prompts_v2# créer le driver -- en passant le bon module que nous voulonsdr = (    driver.Builder()    .with_modules(        prompts_v1,  # ou prompts_v2        summarization,    )    .build())

L’approche du module nous permet d’encapsuler et de versionner des ensembles complets de prompts ensemble. Si vous souhaitez revenir en arrière (via git), ou voir quelle version de prompt était bénie, vous n’avez qu’à naviguer vers le commit correct, puis chercher dans le bon module.

Comment puis-je enregistrer les prompts utilisés et surveiller les flux?

En supposant que vous utilisez git pour suivre votre code, vous n’auriez pas besoin d’enregistrer quels prompts sont utilisés. Au lieu de cela, vous auriez juste besoin de savoir quel SHA de commit git est déployé et vous pourrez suivre la version de votre code et des prompts simultanément.

Pour surveiller les flux, comme pour l’approche ci-dessus, vous disposez des mêmes hooks de surveillance à votre disposition, et je ne les répéterai pas ici, mais les voici:

  • Demander n’importe quelle sortie intermédiaire et les enregistrer vous-même en dehors de Hamilton.
  • Les enregistrer depuis la fonction elle-même, ou construire un décorateur Python / GraphAdapter pour le faire au niveau du framework.
  • Intégrer des outils tiers pour surveiller votre code et les appels à l’API LLM.
  • Ou tout ce qui précède!

Et que dire des tests A/B de mes prompts?

Avec toute initiative d’apprentissage automatique, il est important de mesurer les impacts commerciaux des modifications. De même, avec les LLM + prompts, il sera important de tester et de mesurer les changements par rapport à des indicateurs commerciaux importants. Dans le monde de MLOps, vous effectueriez des tests A/B sur des modèles d’apprentissage automatique pour évaluer leur valeur commerciale en divisant le trafic entre eux. Pour garantir l’aléatoire nécessaire aux tests A/B, vous ne sauriez pas à l’exécution quel modèle utiliser tant qu’une pièce n’est pas lancée. Cependant, pour les déployer, ils devraient tous suivre un processus de qualification. Donc pour les prompts, nous devrions penser de manière similaire.

Les deux modèles de conception de prompt ci-dessus ne vous empêchent pas de pouvoir effectuer des tests A/B sur les prompts, mais cela signifie que vous devez gérer un processus pour permettre autant de modèles de prompts que vous testez en parallèle. Si vous ajustez également les chemins de code, les avoir dans le code sera plus simple pour discerner et déboguer ce qui se passe, et vous pouvez utiliser le décorateur @config.when / le remplacement de module Python à cette fin. Au lieu de devoir vous fier de manière critique à votre pile de logging/surveillance/observabilité pour savoir quel prompt a été utilisé si vous les chargez/déployez dynamiquement, puis devoir mapper mentalement quels prompts vont avec quels chemins de code.

Remarque, tout cela devient plus difficile si vous commencez à avoir besoin de modifier plusieurs prompts pour un test A/B car vous en avez plusieurs dans un flux. Par exemple, vous avez deux prompts dans votre flux de travail et vous modifiez les LLM, vous voudrez tester le changement de manière holistique, plutôt qu’individuellement par prompt. Notre conseil, en mettant les prompts dans le code, votre vie opérationnelle sera plus simple, car vous saurez quels deux prompts appartiennent à quels chemins de code sans avoir à faire de mise en correspondance mentale.

Résumé

Dans cet article, nous avons expliqué deux modèles pour gérer les invites dans un environnement de production avec Hamilton. La première approche considère les invites comme des variables d’exécution dynamiques, tandis que la deuxième considère les invites comme du code pour les paramètres de production. Si vous valorisez la réduction de la charge opérationnelle, notre conseil est d’encoder les invites en tant que code, car c’est plus simple sur le plan opérationnel, sauf si la vitesse de modification vous importe vraiment.

Pour récapituler :

  1. Invites en tant que variables d’exécution dynamiques. Utilisez un système externe pour transmettre les invites à vos flux de données Hamilton, ou utilisez Hamilton pour les extraire d’une base de données. Pour le débogage et la surveillance, il est important de pouvoir déterminer quelle invite a été utilisée pour une invocation donnée. Vous pouvez intégrer des outils open source, ou utiliser quelque chose comme la plateforme DAGWorks pour vous assurer que vous savez ce qui a été utilisé pour chaque invocation de votre code.
  2. Invites en tant que code. L’encodage des invites en tant que code permet une gestion des versions facile avec git. La gestion des modifications peut être effectuée via des demandes de tirage et des vérifications CI/CD. Cela fonctionne bien avec les fonctionnalités de Hamilton telles que @config.when et le changement de module au niveau du pilote, car cela détermine clairement quelle version de l’invite est utilisée. Cette approche renforce l’utilisation de tout outil que vous pourriez utiliser pour surveiller ou suivre, comme la plateforme DAGWorks, car les invites pour un déploiement sont immuables.

Nous voulons avoir de vos nouvelles !

Si vous êtes enthousiaste à propos de tout cela, ou si vous avez des opinions fortes, laissez un commentaire ou venez sur notre canal Slack ! Voici quelques liens pour faire des éloges, se plaindre ou discuter :

  • 📣 rejoignez notre communauté sur Slack – nous sommes plus qu’heureux de vous aider à répondre à vos questions ou de vous aider à démarrer.
  • ⭐️ donnez-nous une étoile sur GitHub.
  • 📝 laissez-nous un problème si vous trouvez quelque chose.
  • 📚 lisez notre documentation.
  • ⌨️ apprenez de manière interactive sur Hamilton dans votre navigateur.

Autres liens/articles Hamilton qui pourraient vous intéresser :

  • tryhamilton.dev – un tutoriel interactif dans votre navigateur !
  • Hamilton + Lineage en 10 minutes
  • Comment utiliser Hamilton avec Pandas en 5 minutes
  • Comment utiliser Hamilton avec Ray en 5 minutes
  • Comment utiliser Hamilton dans un environnement Notebook
  • Histoire générale et introduction à Hamilton
  • Les avantages de la création de flux de données avec Hamilton (Article d’utilisateur organique sur Hamilton !)

We will continue to update IPGirl; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

AI

Le gouvernement de Delhi prévoit de construire un pôle d'intelligence artificielle dans la future ville électronique proposée.

Dans une avancée significative vers le progrès technologique, le gouvernement de Delhi envisage la création d’u...

AI

Transformer la recherche sur les catalyseurs Découvrez CatBERTa, un modèle d'IA basé sur les Transformers conçu pour la prédiction de l'énergie à l'aide de données textuelles.

La recherche sur les catalyseurs chimiques est un domaine dynamique où de nouvelles solutions durables sont toujours ...

AI

Distillons ce que nous savons

Les chercheurs cherchent à réduire la taille des grands modèles GPT.

AI

Une introduction à BentoML un framework d'application d'IA unifié

Dans cet article, explorez comment rationaliser le déploiement de modèles d'apprentissage automatique en utilisant Be...

AI

Rencontrez RPDiff un modèle de diffusion pour le réarrangement d'objets à 6 degrés de liberté dans des scènes 3D.

La conception et la construction de robots pour exécuter des tâches quotidiennes est un domaine passionnant et l̵...