Compilación de aplicaciones RAG con Azure Database for PostgreSQL y Python

Completado

Ahora que la base de datos está lista con incrustaciones e índices vectoriales, es el momento de convertir la recuperación en una aplicación en funcionamiento. El objetivo es sencillo: tomar una pregunta del usuario, recuperar los fragmentos más relevantes de PostgreSQL y generar una respuesta con base en esos fragmentos mediante Azure OpenAI a través de LangChain.

En esta unidad, aprenderá los conceptos básicos de la creación de una aplicación RAG en Python. La aplicación se conecta a la base de datos, ejecuta una búsqueda de similitud en la base de datos y pasa los resultados de búsqueda a un modelo con instrucciones claras para evitar alucinaciones. Dado que el modelo recibe un contexto bien definido basado en los datos de la base de datos, puede generar respuestas más precisas y relevantes basadas en ese contexto.

Generación aumentada por recuperación con PostgreSQL y LangChain

Recuerde de nuestras unidades anteriores, el flujo RAG es un conjunto de pasos que combina la recuperación y la generación. Vamos a verlo:

  1. El usuario hace una pregunta.
  2. La base de datos calcula una inserción para la pregunta y usa el índice vectorial para buscar las coincidencias más cercanas en la base de datos (fragmentos superiores).
  3. Los fragmentos principales se pasan al modelo con un mensaje que indica: "Responde únicamente sobre este contexto. Si no sabes, dilo."
  4. El modelo devuelve una respuesta de lenguaje natural basada en el contexto proporcionado.

LangChain proporciona un marco para compilar aplicaciones con modelos de lenguaje. Simplifica el proceso de conexión a varios orígenes de datos, la administración de mensajes y el control de respuestas. Al combinar Azure Database for PostgreSQL con Python y LangChain, puede crear una aplicación RAG eficaz que recupere información relevante y genere respuestas precisas. Aunque la aplicación RAG puede evolucionar, estos principios básicos guían su desarrollo.

En esta aplicación, usará el contenedor LangChain AzureChatOpenAI para interactuar con el servicio Azure OpenAI .

Para ver los siguientes ejemplos de Python , use la siguiente estructura de tabla:

CREATE TABLE company_policies (
    id SERIAL PRIMARY KEY,
    title TEXT,
    policy_text TEXT,
    embedding VECTOR(1536)
);

En esta tabla, se supone que se crea un índice vectorial en la embedding columna para habilitar búsquedas de similitud eficaces.

Vamos a revisar los fragmentos de código de Python para cada paso. La mayoría de las aplicaciones RAG siguen una estructura similar.

Conectarse a la base de datos

Mantenga las conexiones de corta duración y seguras. En este ejemplo, los secretos se encuentran en variables de entorno y se pasan a la conexión.

import os, psycopg2
from contextlib import contextmanager

@contextmanager
def get_conn():
    conn = psycopg2.connect(
        host=os.getenv("PGHOST"),
        user=os.getenv("PGUSER"),
        password=os.getenv("PGPASSWORD"),
        dbname=os.getenv("PGDATABASE"),
        connect_timeout=10
    )
    try:
        yield conn
    finally:
        conn.close()

Este script configura un administrador de contextos para las conexiones de base de datos, lo que garantiza que se cierran correctamente después de su uso. Ahora es momento de pasar a la recuperación efectiva.

Recuperación de los fragmentos pertinentes

Dado que el usuario está haciendo una pregunta, necesita una función para consultar la base de datos con esa pregunta. Esa función debe devolver los fragmentos pertinentes en función de la pregunta. En este ejemplo se supone que el índice vectorial se clasifica mediante la distancia de coseno, por lo que se usa la clase de operador correspondiente (<=>) para realizar consultas.

def retrieve_chunks(question, top_k=5):
    sql = """
    WITH q AS (
        SELECT azure_openai.create_embeddings(%s, %s)::vector AS qvec
    )
    SELECT id, title, policy_text
    FROM company_policies, q
    ORDER BY embedding <=> q.qvec
    LIMIT %s;
    """
    params = (os.getenv("OPENAI_EMBED_DEPLOYMENT"), question, top_k)
    with get_conn() as conn, conn.cursor() as cur:
        cur.execute(sql, params)
        rows = cur.fetchall()
    return [{"id": r[0], "title": r[1], "text": r[2]} for r in rows]

Por lo tanto, esta función recupera fragmentos relevantes de la base de datos en función de la pregunta del usuario. A continuación, estos fragmentos se usan para generar una respuesta compatible con el contexto en un paso posterior.

Generación de una respuesta con LangChain

Ahora que tiene una función para recuperar fragmentos pertinentes, debe generar una respuesta mediante esos fragmentos. El modelo solo debe usar el contexto recuperado y citar títulos de directiva. Tiempo para crear la función de generación de respuestas.

from langchain_openai import AzureChatOpenAI

SYSTEM_PROMPT = """
You are a helpful assistant. Answer using ONLY the provided context.
If the answer is not in the context, say you don’t have enough information.
Cite policy titles in square brackets, e.g., [Vacation policy].
"""

def format_context(chunks):
    return "\n\n".join([f"[{c['title']}] {c['text']}" for c in chunks])

def generate_answer(question, chunks):
    llm = AzureChatOpenAI(
        azure_deployment=os.getenv("OPENAI_CHAT_DEPLOYMENT"),
        api_key=os.getenv("AZURE_OPENAI_API_KEY"),
        azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
        api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
        temperature=0
    )
    context = format_context(chunks)
    messages = [
        ("system", SYSTEM_PROMPT),
        ("human", f"Question: {question}\nContext:\n{context}")
    ]
    return llm.invoke(messages).content

Nota:

Para usar AzureChatOpenAI:

  • Pase mensajes como una lista de tuplas (rol, contenido) (u objetos de mensaje).
  • Proporcione la configuración de Azure a través de env vars y constructor: AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINTy azure_deployment, api_version.
  • Mantente temperature=0 para respuestas fácticas. Los valores más grandes aumentan la creatividad, pero pueden reducir la precisión.

Esta función es el núcleo de la aplicación RAG, controlando la interacción con el modelo de lenguaje. Observe cómo se da formato a los fragmentos recuperados y se incluyen en el contexto. Además, la solicitud del sistema está diseñada para asegurarse de que el modelo se adhiere al contexto proporcionado y reduce las respuestas generadas por ia que podrían ser incorrectas. Por último, el modelo de lenguaje procesa los mensajes para generar una respuesta mediante el método de invocaciónLangChain. Se llama al método invoke con los mensajes formateados y se devuelve la respuesta del modelo como texto en lenguaje natural.

Únalo todo

Lo último que necesita es una función sencilla para ejecutar el flujo completo:

def answer_question(question):
    chunks = retrieve_chunks(question)
    if not chunks:
        return "I couldn’t find relevant content in the policy store."
    return generate_answer(question, chunks)

# Quick test
print(answer_question("How many vacation days do employees get?"))

En este ejemplo, recuperará los fragmentos pertinentes de la base de datos y los usará para generar una respuesta compatible con el contexto. Esta llamada a función muestra el flujo completo desde la generación de preguntas hasta la generación de respuestas. En primer lugar, llama a la retrieve_chunks función para obtener el contexto pertinente (básicamente las filas devueltas de la base de datos). A continuación, esta función pasa ese contexto a la generate_answer función que interactúa con el modelo de lenguaje para generar la respuesta final (usando las filas como parte del contexto) como respuesta de lenguaje natural. El flujo completo garantiza que la respuesta se base en los datos recuperados, lo que proporciona una respuesta más precisa y confiable.

Conclusiones clave

Una aplicación RAG práctica toma una pregunta del usuario, crea una inserción en SQL, usa un índice vectorial para capturar los pasajes más cercanos de la base de datos y solo aplica ese contexto al modelo. Para reducir la alucinación, también se indica al modelo que permanezca dentro del contexto proporcionado y admita cuándo falta información. Mantenga las conexiones de corta duración, parametrice las consultas y pase secretos a través de variables de entorno. Use una temperatura baja para las respuestas fácticas e incluya citas ligeras (títulos o identificadores) para que las respuestas sean rastreables.

Ahora debería tener una comprensión sólida de cómo crear una aplicación de generación aumentada de recuperación (RAG) mediante Azure Database for PostgreSQL y Python. Aunque la aplicación RAG puede ser mucho más compleja en un escenario real, los principios básicos siguen siendo los mismos.