Création d'un système de questions-réponses sur des données SQL avec LangChain 0.2

Permettre aux systèmes de modèles de langage (LLM) de requêter des données structurées et non structruées présente des défis distincts. Pour les données non structurées, on utilise souvent des bases de vecteurs, tandis que pour les données structurées comme dans une base SQL, le LLM génère et exécute des requêtes DSL (par exemple, SQL). Ce guide illustre comment construire un système de questions-réponses sur des données tabulaires avec des chaînes et des agents LangChain. Ces systèmes permettent de poser des questions sur la base de données et d'obtenir des réponses en langage naturel, avec la capacité pour les agents d'itérer pour formuler des requêtes correctes.

Avertissement sécurité : Construire un tel système requiert l'exécution de requêtes SQL générées par le modèle, ce qui comporte des risques. Limitez les permissions de connexion à la base de données pour atténuer ces risques. Pour plus de détails, consultez les bonnes pratiques de sécurité générales.

Architecture : Les étapes principales sont : 1. Convetrir la question en requête SQL. 2. Exécuter la requête. 3. Générer une réponse à partir des résultats.

Configuraton initiale

Installez les paquets nécessaires et configurez les variables d'environnement :

%pip install --upgrade langchain langchain-community langchain-openai

Définissez la clé API OpenAI :

import os
import getpass

os.environ["OPENAI_API_KEY"] = getpass.getpass("Entrez votre clé API OpenAI: ")

Pour cet exemple, utilisons une base de données SQLite. Créez une base nommée 'music_store.db' avec des tables similaires à Chinook (par exemple, Artist, Album, Customer). Connectez-vous avec SQLAlchemy :

from langchain_community.utilities import SQLDatabase

db_connection = SQLDatabase.from_uri("sqlite:///music_store.db")
print(db_connection.dialect)
print(db_connection.get_usable_table_names())
db_connection.run("SELECT * FROM Artist LIMIT 10;")

Chaînes LangChain

Les chaînes (combinaisons de runnables) conviennent aux applications prévisibles. Créons une chaîne qui convertit une question en requête SQL, l'exécute et répond.

Conversion de la question en requête SQL

LangChain propose une chaîne intégrée pour cette tâche :

from langchain.chains import create_sql_query_chain
from langchain_openai import ChatOpenAI

llm_model = ChatOpenAI(model="gpt-3.5-turbo-0125")
sql_query_generator = create_sql_query_chain(llm_model, db_connection)
generated_query = sql_query_generator.invoke({"question": "Combien d'employés y a-t-il"})
print(generated_query)

Vérifions l'exécution :

query_result = db_connection.run(generated_query)
print(query_result)

Exécution de la requête SQL

Intégrons l'exécution de requêtes dans la chaîne avec un outil dédié :

from langchain_community.tools.sql_database.tool import QuerySQLDataBaseTool

query_executor_tool = QuerySQLDataBaseTool(db=db_connection)
chain_with_execution = sql_query_generator | query_executor_tool
execution_result = chain_with_execution.invoke({"question": "Combien d'employés y a-t-il"})
print(execution_result)

Génération de la réponse

Combinons la question, la requête et le résultat pour formuler la réponse finale :

from operator import itemgetter
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough

answer_prompt = PromptTemplate.from_template(
    "Étant donné la question, la requête SQL et le résultat, répondez.\n"
    "Question: {question}\nRequête: {query}\nRésultat: {result}\nRéponse: "
)

final_response_chain = (
    RunnablePassthrough.assign(query=sql_query_generator).assign(
        result=itemgetter("query") | query_executor_tool
    )
    | answer_prompt
    | llm_model
    | StrOutputParser()
)

final_answer = final_response_chain.invoke({"question": "Combien d'employés y a-t-il"})
print(final_answer)

Agents SQL

Les agents SQL LangChain offrent plus de flexibilité, permettant de gérer les erreurs, d'itérer sur les requêtes et d'optimiser les tokens.

Initialisons un agent avec SQLDatabaseToolkit :

from langchain_community.agent_toolkits import SQLDatabaseToolkit

sql_toolkit = SQLDatabaseToolkit(db=db_connection, llm=llm_model)
available_tools = sql_toolkit.get_tools()

Prompt système

Définissons un prompt pour guider l'agent :

from langchain_core.messages import SystemMessage

system_instruction = """Vous êtes un agent interagissant avec une base SQL.
Générez une requête SQLite correcte à partir de la question, exécutez-la et répondez.
Limitez les résultats à 5 sauf indication contraire. Récupérez uniquement les colonnes nécessaires.
Vérifiez les requêtes avant exécution. Évitez les modifications de données (INSERT, UPDATE, etc.).
Commencez par lister les tables, puis interrogez les schémas pertinents."""

sys_message = SystemMessage(content=system_instruction)

Création de l'agent

Utilisons LangGraph pour construire l'agent :

from langchain_core.messages import HumanMessage
from langgraph.prebuilt import chat_agent_executor

agent_instance = chat_agent_executor.create_tool_calling_executor(
    llm_model, available_tools, messages_modifier=sys_message
)

# Exemple de question
for step in agent_instance.stream(
    {"messages": [HumanMessage(content="Quel pays a les clients les plus dépensiers?")]}
):
    print(step)

Gestion des colonnes à haute cardinalité

Pour les noms propres, utilisez un outil de recherche pour valider l'orthographe :

import ast
import re

def fetch_unique_values(db, query):
    res = db.run(query)
    items = [el for sub in ast.literal_eval(res) for el in sub if el]
    cleaned = [re.sub(r"\b\d+\b", "", s).strip() for s in items]
    return list(set(cleaned))

artist_list = fetch_unique_values(db_connection, "SELECT Name FROM Artist")

from langchain.agents.agent_toolkits import create_retriever_tool
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

vector_db = FAISS.from_texts(artist_list, OpenAIEmbeddings())
noun_search_tool = create_retriever_tool(
    vector_db.as_retriever(search_kwargs={"k": 5}),
    name="search_artist_nouns",
    description="Rechercher des noms d'artistes similaires pour filtrage."
)

# Cet outil peut être ajouté à l'agent pour améliorer la précision

Étiquettes: langchain SQL Python sqlite OpenAI

Publié le 1 juin à 05h53