Skip to content
··9 min de lecture

Comment je me suis sorti des sables mouvants du code grâce à un agent IA

Je me suis lancé dans la création d'un chatbot sans compétences en programmation, avec beaucoup d'enthousiasme — pour découvrir que ma v0.1 était un désastre de bases de données CSV et de découpage primitif, jusqu'à ce que des agents IA me sortent de là.

Mise à jour (2026) : Ce chatbot a évolué pour devenir Sydney ! Après de nombreuses itérations, Sydney vit désormais à /ask/ et se concentre sur le contenu du blog et les produits.


Au début du mois de novembre de l'année dernière, j'ai publié mon chatbot DIY version 0.1. À ce moment-là, j'avais écrit : "Bien que la v0.1 ait représenté un grand premier pas en tant que novice en programmation, elle avait des limitations significatives." Eh bien, comme il s'est avéré, c'est un euphémisme. Je n'avais pas apprécié à quel point l'ensemble du processus et ma construction étaient maladroits. La réalité, c'est que ma tentative initiale, bien que sincère, était davantage un prototype bricolé avec enthousiasme mais peu de savoir-faire.

Ce post n'est pas juste un suivi ; c'est une plongée en profondeur dans le voyage qui s'est déroulé à partir de ce point — un voyage rempli d'essais, d'erreurs et de leçons inestimables. Je mets à nu les rouages de cette aventure, non pas par souci de transparence, mais dans l'espoir que mes expériences, aussi détaillées soient-elles, puissent résonner avec ou même aider quelqu'un d'autre dans une démarche similaire. (Pour le contexte, je suis un professionnel de la publicité d'âge moyen sans aucune expérience préalable en programmation.)

Comme mentionné, pour livrer la v0.1 de mon chatbot, j'ai principalement suivi les instructions de ce court cours "Building Systems with the ChatGPT API" et deux livres de recettes d'OpenAI : Question answering using embeddings-based search et How to count tokens with tiktoken.

Voici pourquoi mon chatbot v0.1 était terrible :

  • Découpage (Chunking) : J'ai simplement divisé les longs articles de blog en morceaux plus petits en fonction de la longueur en tokens, soit un simple découpage statique par caractères. La façon la plus primitive de diviser possible :D. Si tu veux comprendre pourquoi c'est une terrible idée, lis les 5 niveaux de découpage de texte de Greg Kamradt ici.
  • Embedding : J'ai eu du mal avec les embeddings. J'utilisais le modèle d'embedding d'OpenAI, mais je continuais à atteindre les limites de requêtes API, ce qui faisait échouer le processus d'embedding à mi-chemin. J'ai ensuite appris à regrouper les requêtes et à ajouter des délais entre les lots pour éviter les limites. Finalement, j'ai sauvegardé les embeddings générés dans un simple fichier .csv comme ma "base de données" de fortune.
  • Base de données : Je savais qu'un CSV n'était pas optimal comme base de données, mais je n'avais pas les compétences pour de meilleures alternatives.
  • Métadonnées : Je n'avais pas initialement réalisé que l'inclusion de métadonnées comme les dates de publication et les URLs des articles était importante pour que les chatbots répondent précisément aux questions des utilisateurs. J'ai dû répéter l'embedding et la sauvegarde pour incorporer les métadonnées pertinentes.
  • Récupérateur (Retriever) : Je n'étais pas conscient des différents types de récupérateurs et algorithmes. J'utilisais simplement la recherche par pertinence d'OpenAI pour récupérer un nombre fixe de résultats.
  • Mémoire : Pour avoir une conversation, le chatbot doit pouvoir se souvenir de ce qui a été dit précédemment par l'utilisateur. Et c'est là qu'avec une fenêtre de contexte limitée de gpt-3.5 (à l'époque), il y avait un compromis clair entre la taille des chunks et le nombre de résultats à récupérer.
    • Par exemple, si ta taille de chunk est de 800 tokens et que le récupérateur retourne les 8 meilleurs résultats, c'est 6 400 tokens soit plus de 50 % de l'ancienne limite du modèle.
    • Ce qui précède n'est qu'une seule question, donc imagine une conversation multi-tours et comment la mémoire peut se remplir très rapidement.
    • Une façon de résoudre ce problème est d'avoir une taille de chunk plus petite et que le récupérateur retourne moins de résultats, mais avec un récupérateur basique (ci-dessus), cela signifie que le modèle n'a pas d'informations complètes pour répondre à la question.
  • Je n'utilisais même pas d'IDE du tout. Tout le code était édité avec TextEdit sur Mac :D (T'ai-je dit que j'étais un noob avant ? :P)
  • Je pourrais continuer indéfiniment, mais je pense que tu vois l'idée.

Ma "vallée de la mort"

Impatient d'aller au-delà des limitations de la v0.1, j'ai tenté plusieurs cours en ligne, espérant qu'ils m'apporteraient les pièces manquantes pour monter en compétences. Mais les progrès ne menaient qu'à des impasses.

J'ai peiné avec des exercices sur les bases de données vectorielles (Vector Databases: from Embeddings to Applications with Weaviate), les méthodes RAG d'évaluation (Building and Evaluating Advanced RAG Applications avec Llama-Index et Truera), et les techniques de récupération avancée (Advanced Retrieval for AI with Chroma.) Malgré mes efforts, je ne pouvais pas relier la théorie à l'application pratique en utilisant mes propres données de blog.

Les cours étaient-ils mal conçus ? Non — la faiblesse était mon propre manque de connaissances fondamentales. Malgré tout, échec après échec était massivement frustrant, sans parler du découragement. Je me trouvais dans une vallée figurative, incertain de la marche à suivre.

Des victoires incrémentielles avec le temps

Des pépites de valeur ont quand même émergé des échecs répétés :

  • Adoption de VS Code au lieu de TextEdit

  • Utilisation des extensions GitHub Copilot

  • Appréciation de Jupyter Notebook pour les environnements de développement

Le dernier cours mentionné LangChain, un nouveau framework populaire pour la création de chatbots. J'avais en fait tenté des tutoriels LangChain des mois auparavant ("LangChain: Chat with Your Data" et "Functions, Tools and Agents with LangChain") sans grand succès. Mais maintenant, avec des connaissances durement acquises, revisiter sa documentation s'est avéré éclairant. Les concepts se sont mis en place et son architecture modulaire avait un sens intuitif.

Je pouvais envisager d'adapter les capacités robustes de LangChain à mon projet passion. Enfin, une voie à suivre s'est révélée ! Pas à pas, je me suis orienté avec ses pipelines pour l'ingestion de données, l'embedding, le stockage et la récupération.

Ma confiance grandissait avec chaque élément que je parvenais à implémenter. La V2 commençait à prendre forme...

Reconstruire les fondations

Avec Langchain comme guide, je me suis lancé dans la reconstruction de mon chatbot à partir de zéro :

Ingestion des exports WordPress en JSON

Après quelques essais et erreurs pour ajuster les paramètres d'ingestion, le JSONLoader de LangChain a correctement parsé mes articles exportés. Maintenant, des entrées validées pouvaient alimenter les pipelines en aval. (Le code de cette étape se trouve dans le fichier "DataIngestionAndIndexing.ipynb" dans ce dépôt public Github.)

Découpage automatique du texte

Mon découpage naïf basé sur la longueur en tokens a été remplacé par les SentenceTransformers de LangChain, utilisant le NLP avancé pour diviser les unités sémantiques. Plus de phrases découpées en plein milieu ! Les configurations maintenaient les chunks à une taille appropriée pour les modèles contraints en mémoire.

from langchain.text_splitter import SentenceTransformersTokenTextSplitter
# Define the token splitter with specific configurations
token_splitter = SentenceTransformersTokenTextSplitter(
    chunk_overlap=0,  # Overlap between chunks
    tokens_per_chunk=256  # Number of tokens per chunk
)
# Split the documents into chunks based on tokens
all_splits = token_splitter.split_documents(documents)
print(f"Total document splits: \{len(all_splits)\}")

Génération des embeddings et des index

Les difficultés passées avec les limites de l'API OpenAI se sont évaporées grâce au wrapper de LangChain pour les embeddings OpenAI. Réduites à deux lignes de code, les embeddings extrayaient proprement les caractéristiques saillantes du texte divisé.

Pour le stockage vectoriel, j'ai opté pour FAISS (Facebook AI Similarity Search) plutôt que Weaviate ou Chroma. FAISS, éprouvé par l'industrie, a trouvé le bon équilibre entre capacité et complexité pour mes besoins. Sa version CPU a rapidement indexé les chunks du blog, produisant une base de données recherchable compacte. Je n'ai plus à me soucier du traitement par lots ni d'atteindre les limites de requêtes API d'OpenAI.

Langchain supporte plusieurs magasins vectoriels que tu peux consulter ici.

# Initialize embeddings and FAISS vector store
embeddings = OpenAIEmbeddings()
db = FAISS.from_documents(all_splits, embeddings)

# Save the vector store locally
db.save_local("path/to/save/faiss_index")  # Placeholder for save path/ index name

Deux lignes de code ! C'est tout.

Ce que j'ai aussi remarqué, c'est que si ton contenu n'est pas petit (j'ai 400+ articles de blog), tu ne dois pas essayer d'utiliser le récupérateur immédiatement après l'embedding car les index ont besoin d'un peu de temps pour être complétés/stabilisés.

Évaluation des récupérateurs

La configuration de ce récupérateur ne prend qu'une seule ligne de code :D, en utilisant FAISS comme magasin vectoriel.

retriever = db.as_retriever(search_type="mmr")

Avec mon index et mon récupérateur verrouillés, j'avais des pipelines de données prêts à alimenter un chatbot intelligent !

Architecture des agents conversationnels

J'ai décidé d'utiliser le framework agent de Langchain pour construire ce chatbot. Est-ce excessif à ce stade ? Oui. Mais mon espoir est qu'avec le temps, je pourrai faire évoluer ce chatbot et lui donner plus d'"outils", c'est-à-dire de fonctionnalités. Langchain facilite enormément la configuration de l'agent et lui donne des outils à utiliser.

embeddings = OpenAIEmbeddings()
db = FAISS.load_local("path/to/your/faiss_index_file", embeddings)  # Replace the path with your actual FAISS index file path
retriever = db.as_retriever(search_type="mmr")
tool = create_retriever_tool(
    retriever,
    "search_your_blog",  # Replace "search_your_blog" with a descriptive name for your tool
    "Your tool description here"  # Provide a brief description of what your tool does
)

tools = [tool]
prompt_template = ChatPromptTemplate.from_messages([
    # Customize the prompt template according to your chatbot's persona and requirements
])

llm = ChatOpenAI(model_name="gpt-3.5-turbo-1106", temperature=0)
llm_with_tools = llm.bind_tools(tools)
agent = (
    \{
        "input": lambda x: x["input"],
        "agent_scratchpad": lambda x: format_to_openai_tool_messages(x["intermediate_steps"]),
        "chat_history": lambda x: x["chat_history"],
    \}
    | prompt_template
    | llm_with_tools
    | OpenAIToolsAgentOutputParser()
)

agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

Le code Python complet et final pour l'agent

Enfin et surtout, si tu veux essayer le chatbot v2, le voici ici.

Est-ce bizarre que le chatbot ne sache rien sur toi, Chandler ?

P.S : Merci à ceux d'entre vous qui ont pris contact pour me signaler que le chatbot ne sait rien de moi. Et vous avez raison ! C'est parce que j'ai oublié d'exporter la page "About" et n'ai exporté que les "articles publiés". C'est la deuxième fois que j'oublie de le faire, donc je vais inclure des questions de base sur moi dans la liste des questions d'évaluation. Leçon apprise !

Merci à tous pour vos retours constructifs. Continuez à en envoyer. Et oui, je sais que le chatbot est très lent au démarrage, je travaille donc là-dessus aussi. :| (T'ai-je dit que j'étais un noob avant ? :P)

Une mise à jour rapide

Le problème avec le chatbot qui ne sait rien sur moi est maintenant résolu. Voici ce que j'ai fait et appris :

  • Exporter la page "About me" depuis WordPress en .XML comme ci-dessus.
  • Effectué le découpage de texte et généré les embeddings en utilisant FAISS comme ci-dessus. Sauvegardé le magasin vectoriel sous un nom différent localement pour tester le récupérateur.
# save the vector store to local machine
db.save_local("faiss_index_about")
# set up the retriever to test the new vector store about Chandler
from langchain.retrievers.multi_query import MultiQueryRetriever
llm = ChatOpenAI(temperature=0)
retriever_from_llm = MultiQueryRetriever.from_llm(
    retriever=db.as_retriever(), llm=llm
)
# test retriever
question = "Who is Chandler Nguyen?"
results = retriever_from_llm.get_relevant_documents(query=question, top_k=8)
for doc in results:
    print(f"Content: {doc.page_content}, Metadata: {doc.metadata}")

  • Il s'est avéré que le processus pour fusionner deux magasins vectoriels FAISS est étonnamment simple, comme indiqué dans la documentation ici.
# Try to merge two FAISS vector stores into 1
# load the vector store from local machine
from langchain_community.vectorstores import FAISS
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
db_1 = FAISS.load_local("faiss_index_about", embeddings)
db_2 = FAISS.load_local("faiss_index", embeddings)
db_2.merge_from(db_1)
# save the vector store to local machine
db_2.save_local("faiss_index_v2")
# test the new vector store to confirm correct retrieved documents
retriever = db_2.as_retriever(search_type="mmr")
results = retriever.get_relevant_documents("Who is Chandler Nguyen?")
for doc in results:
    print(f"Content: {doc.page_content}, Metadata: {doc.metadata}")
  • Après ça, c'est à peu près le même processus qu'au-dessus, en utilisant le nouveau magasin vectoriel.

Mise à jour du 14 février : Chatbot v2.10 dévoilé

Deux semaines après ce déploiement du chatbot, j'ai introduit la version 2.10 qui élève l'expérience utilisateur avec une vitesse, une évolutivité et une simplicité améliorées. Tu peux en lire plus à ce sujet ici.

Mise à jour du 25 mars : Des améliorations frontend aux difficultés et percées avec Docker

Tu peux en lire plus à ce sujet ici.

Continuer la lecture

Mon parcours
Me suivre
Langue
Preferences