Abstracciones de alto nivel ofrecidas por bibliotecas como ai/en/stable/” rel=”noopener ugc nofollow” target=”_blank”>índice de llama y Cadena Lang han simplificado el desarrollo de sistemas de recuperación de generación aumentada (RAG). Sin embargo, una comprensión profunda de los mecanismos subyacentes que habilitan estas bibliotecas sigue siendo crucial para cualquier ingeniero de aprendizaje automático que desee aprovechar al máximo su potencial. En este artículo, lo guiaré a través del proceso de desarrollo de un sistema RAG desde cero. También iré un paso más allá y crearemos una API de matraz en contenedor. Lo he diseñado para que sea muy práctico: este tutorial está inspirado en casos de uso de la vida real, lo que garantiza que los conocimientos que obtenga no sean sólo teóricos sino también aplicables de inmediato.
Descripción general del caso de uso — Esta implementación está diseñada para manejar una amplia gama de tipos de documentos. Si bien el ejemplo actual utiliza muchos documentos pequeños, cada uno de los cuales representa productos individuales con detalles como SKU, nombre, descripción, precio y dimensiones, el enfoque es altamente adaptable. Ya sea que la tarea implique indexar una biblioteca diversa de libros, extraer datos de contratos extensos o cualquier otro conjunto de documentos, el sistema se puede adaptar para satisfacer las necesidades específicas de estos contextos variados. Esta flexibilidad permite la perfecta integración y procesamiento de diferentes tipos de información.
Nota rápida — esta implementación funcionará únicamente con datos de texto. Se pueden seguir pasos similares para convertir imágenes en incrustaciones utilizando un modelo multimodal como CLIP, que luego puede indexar y consultar.
- Delinear el marco modular
- preparar los datos
- Fragmentación, indexación y recuperación (funcionalidad principal)
- Componente de maestría en Derecho
- Construya e implemente la API
- Conclusión
La implementación tiene cuatro componentes principales que se pueden intercambiar.
- datos de texto
- Modelo de incrustación
- LLM
- Tienda de vectores
La integración de estos servicios en su proyecto es muy flexible, lo que le permite personalizarlos según sus requisitos específicos. En esta implementación de ejemplo, comienzo con un escenario en el que los datos iniciales están en formato JSON, lo que proporciona convenientemente los datos como una cadena. Sin embargo, es posible que encuentre datos en otros formatos, como archivos PDF, correos electrónicos u hojas de cálculo de Excel. En tales casos, es esencial “normalizar” estos datos convirtiéndolos a un formato de cadena. Dependiendo de las necesidades de su proyecto, puede convertir los datos en una cadena en la memoria o guardarlos en un archivo de texto para su posterior refinamiento o procesamiento posterior.
De manera similar, las opciones de modelo de incrustación, almacén de vectores y LLM se pueden personalizar para satisfacer las necesidades de su proyecto. Ya sea que necesite un modelo más pequeño o más grande, o quizás un modelo externo, la flexibilidad de este enfoque le permite simplemente intercambiar las opciones apropiadas. Esta capacidad plug-and-play garantiza que su proyecto pueda adaptarse a diversos requisitos sin modificaciones significativas en la arquitectura central.
Resalté los componentes principales en gris. En esta implementación, nuestro almacén de vectores será simplemente un archivo JSON. Una vez más, dependiendo de su caso de uso, es posible que desee utilizar un almacén de vectores en memoria (Python dict) si solo está procesando un archivo a la vez. Si necesita conservar estos datos, como lo hacemos en este caso de uso, puede guardarlos en un archivo JSON localmente. Si necesita almacenar cientos de miles o millones de vectores, necesitará un almacén de vectores externo (Pinecone, Azure Cognitive Search, etc.).
Como se mencionó anteriormente, esta implementación comienza con datos JSON. Usé GPT-4 y Claude para generarlo sintéticamente. Los datos contienen descripciones de productos para diferentes muebles, cada uno con su propio SKU. Aquí hay un ejemplo:
{
"MBR-2001": "Traditional sleigh bed crafted in rich walnut wood, featuring a curved headboard and footboard with intricate grain details. Queen size, includes a plush, supportive mattress. Produced by Heritage Bed Co. Dimensions: 65\"W x 85\"L x 50\"H.",
"MBR-2002": "Art Deco-inspired vanity table in a polished ebony finish, featuring a tri-fold mirror and five drawers with crystal knobs. Includes a matching stool upholstered in silver velvet. Made by Luxe Interiors. Vanity dimensions: 48\"W x 20\"D x 30\"H, Stool dimensions: 22\"W x 16\"D x 18\"H.",
"MBR-2003": "Set of sheer linen drapes in soft ivory, offering a delicate and airy touch to bedroom windows. Each panel measures 54\"W x 84\"L. Features hidden tabs for easy hanging. Manufactured by Tranquil Home Textiles.","LVR-3001": "Convertible sofa bed upholstered in navy blue linen fabric, easily transitions from sofa to full-size sleeper. Perfect for guests or small living spaces. Features a sturdy wooden frame. Produced by SofaBed Solutions. Dimensions: 70\"W x 38\"D x 35\"H.",
"LVR-3002": "Ornate Persian area rug in deep red and gold, hand-knotted from silk and wool. Adds a luxurious touch to any living room. Measures 8' x 10'. Manufactured by Ancient Weaves.",
"LVR-3003": "Contemporary TV stand in matte black with tempered glass doors and chrome legs. Features integrated cable management and adjustable shelves. Accommodates up to 65-inch TVs. Made by Streamline tech. Dimensions: 60\"W x 20\"D x 24\"H.",
"OPT-4001": "Modular outdoor sofa set in espresso brown polyethylene wicker, includes three corner pieces and two armless chairs with water-resistant cushions in cream. Configurable to fit any patio space. Produced by Outdoor Living. Corner dimensions: 32\"W x 32\"D x 28\"H, Armless dimensions: 28\"W x 32\"D x 28\"H.",
"OPT-4002": "Cantilever umbrella in sunflower yellow, featuring a 10-foot canopy and adjustable tilt for optimal shade. Constructed with a sturdy aluminum pole and fade-resistant fabric. Manufactured by Shade Masters. Dimensions: 120\"W x 120\"D x 96\"H.",
"OPT-4003": "Rustic fire pit table made from faux stone, includes a natural gas hookup and a matching cover. Ideal for evening gatherings on the patio. Manufactured by Warmth Outdoor. Dimensions: 42\"W x 42\"D x 24\"H.",
"ENT-5001": "Digital jukebox with touchscreen interface and built-in speakers, capable of streaming music and playing CDs. Retro design with modern technology, includes customizable LED lighting. Produced by RetroSound. Dimensions: 24\"W x 15\"D x 48\"H.",
"ENT-5002": "Gaming console storage unit in sleek black, featuring designated compartments for systems, controllers, and games. Ventilated to prevent overheating. Manufactured by GameHub. Dimensions: 42\"W x 16\"D x 24\"H.",
"ENT-5003": "Virtual reality gaming set by VR Innovations, includes headset, two motion controllers, and a charging station. Offers a comprehensive library of immersive games and experiences.",
"KIT-6001": "Chef's rolling kitchen cart in stainless steel, features two shelves, a drawer, and towel bars. Portable and versatile, ideal for extra storage and workspace in the kitchen. Produced by KitchenAid. Dimensions: 30\"W x 18\"D x 36\"H.",
"KIT-6002": "Contemporary pendant light cluster with three frosted glass shades, suspended from a polished nickel ceiling plate. Provides elegant, diffuse lighting over kitchen islands. Manufactured by Luminary Designs. Adjustable drop length up to 60\".",
"KIT-6003": "Eight-piece ceramic dinnerware set in ocean blue, includes dinner plates, salad plates, bowls, and mugs. Dishwasher and microwave safe, adds a pop of color to any meal. Produced by Tabletop Trends.",
"GBR-7001": "Twin-size daybed with trundle in brushed silver metal, ideal for guest rooms or small spaces. Includes two comfortable twin mattresses. Manufactured by Guestroom Gadgets. Bed dimensions: 79\"L x 42\"W x 34\"H.",
"GBR-7002": "Wall art set featuring three abstract prints in blue and grey tones, framed in light wood. Each frame measures 24\"W x 36\"H. Adds a modern touch to guest bedrooms. Produced by Artistic Expressions.",
"GBR-7003": "Set of two bedside lamps in brushed nickel with white fabric shades. Offers a soft, ambient light suitable for reading or relaxing in bed. Dimensions per lamp: 12\"W x 24\"H. Manufactured by Bright Nights.",
"BMT-8001": "Industrial-style pool table with a slate top and black felt, includes cues, balls, and a rack. Perfect for entertaining and game nights in finished basements. Produced by Billiard Masters. Dimensions: 96\"L x 52\"W x 32\"H.",
"BMT-8002": "Leather home theater recliner set in black, includes four connected seats with individual cup holders and storage compartments. Offers a luxurious movie-watching experience. Made by CinemaComfort. Dimensions per seat: 22\"W x 40\"D x 40\"H.",
"BMT-8003": "Adjustable height pub table set with four stools, featuring a rustic wood finish and black metal frame. Ideal for casual dining or socializing in basements. Produced by Casual Home. Table dimensions: 36\"W x 36\"D x 42\"H, Stool dimensions: 15\"W x 15\"D x 30\"H."
}
En un escenario del mundo real, podemos extrapolar esto a millones de SKU y descripciones, probablemente todas residiendo en diferentes lugares. El esfuerzo de agregar y organizar estos datos parece trivial en este escenario, pero en general los datos disponibles deberían organizarse en una estructura como esta.
El siguiente paso es simplemente convertir cada SKU en su propio archivo de texto. En total hay 105 archivos de texto (SKU). Nota: puede encontrar todos los datos/códigos vinculados en mi GitHub al final del artículo.
Utilicé este mensaje para generar los datos y los envié numerosas veces:
Given different "categories" for furniture, I want you to generate a synthetic 'SKU' and product description.Generate 3 for each category. Be extremely granular with your details and descriptions (colors, sizes, synthetic manufacturers, etc..).
Every response should follow this format and should be only JSON:
{<SKU>:<description>}.
- master bedroom
- living room
- outdoor patio
- entertainment
- kitchen
- guest bedroom
- finished basement
Para seguir adelante, debe tener un directorio con archivos de texto que contengan las descripciones de sus productos con los SKU como nombres de archivo.
fragmentación
Dado un fragmento de texto, debemos fragmentarlo de manera eficiente para que esté optimizado para su recuperación. Intenté modelar esto después del índice de llamas. ai/en/stable/api_reference/node_parsers/sentence_splitter/” rel=”noopener ugc nofollow” target=”_blank”>Divisor de oraciones clase.
import re
import os
import uuid
from transformers import AutoTokenizer, AutoModeldef document_chunker(directory_path,
model_name,
paragraph_separator='\n\n',
chunk_size=1024,
separator=' ',
secondary_chunking_regex=r'\S+?(\.,;!?)',
chunk_overlap=0):
tokenizer = AutoTokenizer.from_pretrained(model_name) # Load tokenizer for the specified model
documents = {} # Initialize dictionary to store results
# Read each file in the specified directory
for filename in os.listdir(directory_path):
file_path = os.path.join(directory_path, filename)
base = os.path.basename(file_path)
sku = os.path.splitext(base)(0)
if os.path.isfile(file_path):
with open(file_path, 'r', encoding='utf-8') as file:
text = file.read()
# Generate a unique identifier for the document
doc_id = str(uuid.uuid4())
# Process each file using the existing chunking logic
paragraphs = re.split(paragraph_separator, text)
all_chunks = {}
for paragraph in paragraphs:
words = paragraph.split(separator)
current_chunk = ""
chunks = ()
for word in words:
new_chunk = current_chunk + (separator if current_chunk else '') + word
if len(tokenizer.tokenize(new_chunk)) <= chunk_size:
current_chunk = new_chunk
else:
if current_chunk:
chunks.append(current_chunk)
current_chunk = word
if current_chunk:
chunks.append(current_chunk)
refined_chunks = ()
for chunk in chunks:
if len(tokenizer.tokenize(chunk)) > chunk_size:
sub_chunks = re.split(secondary_chunking_regex, chunk)
sub_chunk_accum = ""
for sub_chunk in sub_chunks:
if sub_chunk_accum and len(tokenizer.tokenize(sub_chunk_accum + sub_chunk + ' ')) > chunk_size:
refined_chunks.append(sub_chunk_accum.strip())
sub_chunk_accum = sub_chunk
else:
sub_chunk_accum += (sub_chunk + ' ')
if sub_chunk_accum:
refined_chunks.append(sub_chunk_accum.strip())
else:
refined_chunks.append(chunk)
final_chunks = ()
if chunk_overlap > 0 and len(refined_chunks) > 1:
for i in range(len(refined_chunks) - 1):
final_chunks.append(refined_chunks(i))
overlap_start = max(0, len(refined_chunks(i)) - chunk_overlap)
overlap_end = min(chunk_overlap, len(refined_chunks(i+1)))
overlap_chunk = refined_chunks(i)(overlap_start:) + ' ' + refined_chunks(i+1)(:overlap_end)
final_chunks.append(overlap_chunk)
final_chunks.append(refined_chunks(-1))
else:
final_chunks = refined_chunks
# Assign a UUID for each chunk and structure it with text and metadata
for chunk in final_chunks:
chunk_id = str(uuid.uuid4())
all_chunks(chunk_id) = {"text": chunk, "metadata": {"file_name":sku}} # Initialize metadata as dict
# Map the document UUID to its chunk dictionary
documents(doc_id) = all_chunks
return documents
El parámetro más importante aquí es “chunk_size”. Como puedes ver, estamos usando el transformadores biblioteca para contar el número de tokens en una cadena determinada. Por lo tanto, el tamaño del fragmento representa la cantidad de tokens en un fragmento.
A continuación se muestra un desglose de lo que sucede dentro de la función:
Para cada archivo en el directorio especificado →
- Dividir texto en párrafos:
– Divida el texto de entrada en párrafos usando un separador específico. - Divida párrafos en palabras:
– Para cada párrafo, divídelo en palabras.
– Cree fragmentos de estas palabras sin exceder un recuento de tokens específico (chunk_size). - Refinar fragmentos:
– Si algún fragmento excede el tamaño del fragmento, divídalo aún más usando una expresión regular basada en la puntuación.
– Fusionar subfragmentos si es necesario para optimizar el tamaño de los fragmentos. - Aplicar superposición:
– Para secuencias con múltiples fragmentos, cree superposiciones entre ellos para garantizar la continuidad contextual. - Compilar y devolver fragmentos:
– Recorra cada fragmento final, asígnele una ID única que se asigne al texto y los metadatos de ese fragmento y, finalmente, asigne este diccionario de fragmentos a la ID del documento.
En este ejemplo, donde indexamos numerosos documentos más pequeños, el proceso de fragmentación es relativamente sencillo. Cada documento, al ser breve, requiere una segmentación mínima. Esto contrasta marcadamente con escenarios que involucran textos más extensos, como extraer secciones específicas de contratos extensos o indexar novelas enteras. Para dar cabida a una variedad de tamaños y complejidades de documentos, desarrollé el document_chunker
función. Esto le permite ingresar sus datos, independientemente de su longitud o formato, y aplicar el mismo proceso de fragmentación eficiente. Ya sea que se trate de descripciones de productos concisas o de obras literarias extensas, el document_chunker
garantiza que sus datos estén segmentados adecuadamente para una indexación y recuperación óptimas.
Uso:
docs = document_chunker(directory_path='/Users/joesasson/Desktop/articles/rag-from-scratch/text_data',
model_name='BAAI/bge-small-en-v1.5',
chunk_size=256)keys = list(docs.keys())
print(len(docs))
print(docs(keys(0)))
Out -->
105
{'61d6318e-644b-48cd-a635-9490a1d84711': {'text': 'Gaming console storage unit in sleek black, featuring designated compartments for systems, controllers, and games. Ventilated to prevent overheating. Manufactured by GameHub. Dimensions: 42"W x 16"D x 24"H.', 'metadata': {'file_name': 'ENT-5002'}}}
Ahora tenemos un mapeo con una ID de documento única, que apunta a todos los fragmentos de ese documento, y cada fragmento tiene su propia ID única que apunta al texto y los metadatos de ese fragmento.
Los metadatos pueden contener pares clave/valor arbitrarios. Aquí estoy configurando el nombre del archivo (SKU) como metadatos para que podamos rastrear los resultados de nuestros modelos hasta el producto original.
Indexación
Ahora que hemos creado el almacén de documentos, necesitamos crear el almacén de vectores.
Quizás ya lo hayas notado, pero estamos usando BAY/bge-small-es-v1.5 como nuestro modelo de incrustaciones. En la función anterior solo la usamos para tokenización, ahora la usaremos para vectorizar nuestro texto.
Para prepararnos para la implementación, guardemos el tokenizador y el modelo localmente.
from transformers import AutoModel, AutoTokenizermodel_name = "BAAI/bge-small-en-v1.5"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
tokenizer.save_pretrained("model/tokenizer")
model.save_pretrained("model/embedding")
def compute_embeddings(text):
tokenizer = AutoTokenizer.from_pretrained("/model/tokenizer")
model = AutoModel.from_pretrained("/model/embedding")inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
# Generate the embeddings
with torch.no_grad():
embeddings = model(**inputs).last_hidden_state.mean(dim=1).squeeze()
return embeddings.tolist()
def create_vector_store(doc_store):
vector_store = {}
for doc_id, chunks in doc_store.items():
doc_vectors = {}
for chunk_id, chunk_dict in chunks.items():
# Generate an embedding for each chunk of text
doc_vectors(chunk_id) = compute_embeddings(chunk_dict.get("text"))
# Store the document's chunk embeddings mapped by their chunk UUIDs
vector_store(doc_id) = doc_vectors
return vector_store
Todo lo que hemos hecho es simplemente convertir los fragmentos del almacén de documentos en incrustaciones. Puede conectar cualquier modelo de incrustación y cualquier almacén de vectores. Dado que nuestro almacén de vectores es solo un diccionario, todo lo que tenemos que hacer es volcarlo en un archivo JSON para que persista.
Recuperación
¡Ahora probémoslo con una consulta!
def compute_matches(vector_store, query_str, top_k):
"""
This function takes in a vector store dictionary, a query string, and an int 'top_k'.
It computes embeddings for the query string and then calculates the cosine similarity against every chunk embedding in the dictionary.
The top_k matches are returned based on the highest similarity scores.
"""
# Get the embedding for the query string
query_str_embedding = np.array(compute_embeddings(query_str))
scores = {}# Calculate the cosine similarity between the query embedding and each chunk's embedding
for doc_id, chunks in vector_store.items():
for chunk_id, chunk_embedding in chunks.items():
chunk_embedding_array = np.array(chunk_embedding)
# Normalize embeddings to unit vectors for cosine similarity calculation
norm_query = np.linalg.norm(query_str_embedding)
norm_chunk = np.linalg.norm(chunk_embedding_array)
if norm_query == 0 or norm_chunk == 0:
# Avoid division by zero
score = 0
else:
score = np.dot(chunk_embedding_array, query_str_embedding) / (norm_query * norm_chunk)
# Store the score along with a reference to both the document and the chunk
scores((doc_id, chunk_id)) = score
# Sort scores and return the top_k results
sorted_scores = sorted(scores.items(), key=lambda item: item(1), reverse=True)(:top_k)
top_results = ((doc_id, chunk_id, score) for ((doc_id, chunk_id), score) in sorted_scores)
return top_results
El compute_matches
La función está diseñada para identificar los fragmentos de texto más similares top_k a una cadena de consulta determinada de una colección almacenada de incrustaciones de texto. Aquí hay un desglose:
- Incrustar la cadena de consulta
- Calcular la similitud del coseno. Para cada fragmento, se calcula la similitud del coseno entre el vector de consulta y el vector de fragmento. Aquí,
np.linalg.norm
calcula la norma euclidiana (norma L2) de los vectores, que se requiere para el cálculo de similitud del coseno. - Manejar la normalización y calcular el producto escalar. La similitud del coseno se define como:
4. Ordenar y seleccionar las puntuaciones. Las puntuaciones se ordenan en orden descendente y se seleccionan los resultados top_k
Uso:
matches = compute_matches(vector_store=vec_store,
query_str="Wall-mounted electric fireplace with realistic LED flames",
top_k=3)# matches
(('d56bc8ca-9bbc-4edb-9f57-d1ea2b62362f',
'3086bed2-65e7-46cc-8266-f9099085e981',
0.8600385118142513),
('240c67ce-b469-4e0f-86f7-d41c630cead2',
'49335ccf-f4fb-404c-a67a-19af027a9fc2',
0.7067269230771228),
('53faba6d-cec8-46d2-8d7f-be68c3080091',
'b88e4295-5eb1-497c-8536-59afd84d2210',
0.6959163226146977))
# plug the top match document ID keys into doc_store to access the retrieved content
docs('d56bc8ca-9bbc-4edb-9f57-d1ea2b62362f')('3086bed2-65e7-46cc-8266-f9099085e981')
# result
{'text': 'Wall-mounted electric fireplace with realistic LED flames and heat settings. Features a black glass frame and remote control for easy operation. Ideal for adding warmth and ambiance. Manufactured by Hearth & Home. Dimensions: 50"W x 6"D x 21"H.',
'metadata': {'file_name': 'ENT-4001'}}
Donde cada tupla tiene el ID del documento, seguido del ID del fragmento y seguido de la puntuación.
¡Genial, está funcionando! Todo lo que queda por hacer es conectar el componente LLM y ejecutar una prueba completa de un extremo a otro, ¡y luego estaremos listos para implementar!
Para mejorar la experiencia del usuario al hacer que nuestro sistema RAG sea interactivo, utilizaremos el llama-cpp-python
biblioteca. Nuestra configuración utilizará un modelo de parámetros mistral-7B con cuantificación GGUF de 3 bits, una configuración que proporciona un buen equilibrio entre eficiencia computacional y rendimiento. Según pruebas exhaustivas, este tamaño de modelo ha demostrado ser muy eficaz, especialmente cuando se ejecuta en máquinas con recursos limitados como mi Mac M2 de 8 GB. Al adoptar este enfoque, nos aseguramos de que nuestro sistema RAG no solo brinde respuestas precisas y relevantes, sino que también mantenga un tono conversacional, haciéndolo más atractivo y accesible para los usuarios finales.
Nota rápida sobre la configuración del LLM localmente en una Mac: mi preferencia es usar anaconda o miniconda. Asegúrese de haber instalado una versión arm64 y siga las instrucciones de configuración para 'metal' de la biblioteca. aquí.
Ahora es bastante fácil. Todo lo que necesitamos hacer es definir una función para construir un mensaje que incluya los documentos recuperados y la consulta de los usuarios. La respuesta del LLM se enviará al usuario.
He definido las siguientes funciones para transmitir la respuesta de texto del LLM y construir nuestro mensaje final.
from llama_cpp import Llama
import sysdef stream_and_buffer(base_prompt, llm, max_tokens=800, stop=("Q:", "\n"), echo=True, stream=True):
# Formatting the base prompt
formatted_prompt = f"Q: {base_prompt} A: "
# Streaming the response from llm
response = llm(formatted_prompt, max_tokens=max_tokens, stop=stop, echo=echo, stream=stream)
buffer = ""
for message in response:
chunk = message('choices')(0)('text')
buffer += chunk
# Split at the last space to get words
words = buffer.split(' ')
for word in words(:-1): # Process all words except the last one (which might be incomplete)
sys.stdout.write(word + ' ') # Write the word followed by a space
sys.stdout.flush() # Ensure it gets displayed immediately
# Keep the rest in the buffer
buffer = words(-1)
# Print any remaining content in the buffer
if buffer:
sys.stdout.write(buffer)
sys.stdout.flush()
def construct_prompt(system_prompt, retrieved_docs, user_query):
prompt = f"""{system_prompt}
Here is the retrieved context:
{retrieved_docs}
Here is the users query:
{user_query}
"""
return prompt
# Usage
system_prompt = """
You are an intelligent search engine. You will be provided with some retrieved context, as well as the users query.
Your job is to understand the request, and answer based on the retrieved context.
"""
retrieved_docs = """
Wall-mounted electric fireplace with realistic LED flames and heat settings. Features a black glass frame and remote control for easy operation. Ideal for adding warmth and ambiance. Manufactured by Hearth & Home. Dimensions: 50"W x 6"D x 21"H.
"""
prompt = construct_prompt(system_prompt=system_prompt,
retrieved_docs=retrieved_docs,
user_query="I am looking for a wall-mounted electric fireplace with realistic LED flames")
llm = Llama(model_path="/Users/joesasson/Downloads/mistral-7b-instruct-v0.2.Q3_K_L.gguf", n_gpu_layers=1)
stream_and_buffer(prompt, llm)
Salida final que se devuelve al usuario:
“Según el contexto recuperado y la consulta del usuario, la chimenea eléctrica Hearth & Home con llamas LED realistas se ajusta a la descripción. Este modelo mide 50 pulgadas de ancho, 6 pulgadas de profundidad y 21 pulgadas de alto y viene con un control remoto para una fácil operación”.
Ahora estamos listos para implementar nuestro sistema RAG. Continúe en la siguiente sección y convertiremos este código cuasi espagueti en una API consumible para los usuarios.
Para ampliar el alcance y la usabilidad de nuestro sistema, lo empaquetaremos en una aplicación Flask en contenedor. Este enfoque garantiza que nuestro modelo esté encapsulado dentro de un contenedor Docker, proporcionando estabilidad y coherencia independientemente del entorno informático.
Deberías haber descargado el modelo de incrustaciones y el tokenizador anteriores. Colóquelos al mismo nivel que el código de su aplicación, los requisitos y el Dockerfile. Puedes descargar el LLM aquí.
Debería tener la siguiente estructura de directorios:
aplicación.py
from flask import Flask, request, jsonify
import numpy as np
import json
from typing import Dict, List, Any
from llama_cpp import Llama
import torch
import logging
from transformers import AutoModel, AutoTokenizerapp = Flask(__name__)
# Set the logger level for Flask's logger
app.logger.setLevel(logging.INFO)
def compute_embeddings(text):
tokenizer = AutoTokenizer.from_pretrained("/app/model/tokenizer")
model = AutoModel.from_pretrained("/app/model/embedding")
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
# Generate the embeddings
with torch.no_grad():
embeddings = model(**inputs).last_hidden_state.mean(dim=1).squeeze()
return embeddings.tolist()
def compute_matches(vector_store, query_str, top_k):
"""
This function takes in a vector store dictionary, a query string, and an int 'top_k'.
It computes embeddings for the query string and then calculates the cosine similarity against every chunk embedding in the dictionary.
The top_k matches are returned based on the highest similarity scores.
"""
# Get the embedding for the query string
query_str_embedding = np.array(compute_embeddings(query_str))
scores = {}
# Calculate the cosine similarity between the query embedding and each chunk's embedding
for doc_id, chunks in vector_store.items():
for chunk_id, chunk_embedding in chunks.items():
chunk_embedding_array = np.array(chunk_embedding)
# Normalize embeddings to unit vectors for cosine similarity calculation
norm_query = np.linalg.norm(query_str_embedding)
norm_chunk = np.linalg.norm(chunk_embedding_array)
if norm_query == 0 or norm_chunk == 0:
# Avoid division by zero
score = 0
else:
score = np.dot(chunk_embedding_array, query_str_embedding) / (norm_query * norm_chunk)
# Store the score along with a reference to both the document and the chunk
scores((doc_id, chunk_id)) = score
# Sort scores and return the top_k results
sorted_scores = sorted(scores.items(), key=lambda item: item(1), reverse=True)(:top_k)
top_results = ((doc_id, chunk_id, score) for ((doc_id, chunk_id), score) in sorted_scores)
return top_results
def open_json(path):
with open(path, 'r') as f:
data = json.load(f)
return data
def retrieve_docs(doc_store, matches):
top_match = matches(0)
doc_id = top_match(0)
chunk_id = top_match(1)
docs = doc_store(doc_id)(chunk_id)
return docs
def construct_prompt(system_prompt, retrieved_docs, user_query):
prompt = f"""{system_prompt}
Here is the retrieved context:
{retrieved_docs}
Here is the users query:
{user_query}
"""
return prompt
@app.route('/rag_endpoint', methods=('GET', 'POST'))
def main():
app.logger.info('Processing HTTP request')
# Process the request
query_str = request.args.get('query') or (request.get_json() or {}).get('query')
if not query_str:
return jsonify({"error":"missing required parameter 'query'"})
vec_store = open_json('/app/vector_store.json')
doc_store = open_json('/app/doc_store.json')
matches = compute_matches(vector_store=vec_store, query_str=query_str, top_k=3)
retrieved_docs = retrieve_docs(doc_store, matches)
system_prompt = """
You are an intelligent search engine. You will be provided with some retrieved context, as well as the users query.
Your job is to understand the request, and answer based on the retrieved context.
"""
base_prompt = construct_prompt(system_prompt=system_prompt, retrieved_docs=retrieved_docs, user_query=query_str)
app.logger.info(f'constructed prompt: {base_prompt}')
# Formatting the base prompt
formatted_prompt = f"Q: {base_prompt} A: "
llm = Llama(model_path="/app/mistral-7b-instruct-v0.2.Q3_K_L.gguf")
response = llm(formatted_prompt, max_tokens=800, stop=("Q:", "\n"), echo=False, stream=False)
return jsonify({"response": response})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001)
archivo acoplable
# Use an official Python runtime as a parent image
FROM --platform=linux/arm64 python:3.11# Set the working directory in the container to /app
WORKDIR /app
# Copy the requirements file
COPY requirements.txt .
# Update system packages, install gcc and Python dependencies
RUN apt-get update && \
apt-get install -y gcc g++ make libtool && \
apt-get upgrade -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
pip install --no-cache-dir -r requirements.txt
# Copy the current directory contents into the container at /app
COPY . /app
# Expose port 5001 to the outside world
EXPOSE 5001
# Run script when the container launches
CMD ("python", "app.py")
Algo importante a tener en cuenta: estamos configurando el directorio de trabajo en '/app' en la segunda línea del Dockerfile. Por lo tanto, cualquier ruta local (modelos, vectores o almacén de documentos) debe tener el prefijo '/app' en el código de su aplicación.
Además, cuando ejecute la aplicación en el contenedor (en una Mac), no podrá acceder a la GPU, consulte este hilo. He notado que normalmente se necesitan unos 20 minutos para obtener una respuesta utilizando la CPU.
Construir y ejecutar:
docker build -t <image-name>:<tag> .
docker run -p 5001:5001 <image-name>:<tag>
Al ejecutar el contenedor se inicia automáticamente la aplicación (consulte la última línea del Dockerfile). Ahora puede acceder a su punto final en la siguiente URL:
http://127.0.0.1:5001/rag_endpoint
Llame a la API:
import requests, jsondef call_api(query):
URL = "http://127.0.0.1:5001/rag_endpoint"
# Headers for the request
headers = {
"Content-Type": "application/json"
}
# Body for the request.
body = {"query": query}
# Making the POST request
response = requests.post(URL, headers=headers, data=json.dumps(body))
# Check if the request was successful
if response.status_code == 200:
return response.json()
else:
return f"Error: {response.status_code}, Message: {response.text}"
# Test
query = "Wall-mounted electric fireplace with realistic LED flames"
result = call_api(query)
print(result)
# result
{'response': {'choices': ({'finish_reason': 'stop', 'index': 0, 'logprobs': None, 'text': ' Based on the retrieved context, the wall-mounted electric fireplace mentioned includes features such as realistic LED flames. Therefore, the answer to the user\'s query "Wall-mounted electric fireplace with realistic LED flames" is a match to the retrieved context. The specific model mentioned in the context is manufactured by Hearth & Home and comes with additional heat settings.'}), 'created': 1715307125, 'id': 'cmpl-dd6c41ee-7c89-440f-9b04-0c9da9662f26', 'model': '/app/mistral-7b-instruct-v0.2.Q3_K_L.gguf', 'object': 'text_completion', 'usage': {'completion_tokens': 78, 'prompt_tokens': 177, 'total_tokens': 255}}}
Quiero recapitular todos los pasos necesarios para llegar a este punto y el flujo de trabajo para adaptarlo a cualquier dato/integración/LLM.
- Pase su directorio de archivos de texto al
document_chunker
función para crear el almacén de documentos. - Elige tu modelo de empotramiento. Guárdalo localmente.
- Convierta el almacén de documentos en un almacén de vectores. Guarde ambos localmente.
- Descargue LLM desde el centro HF.
- Mueva los archivos al directorio de la aplicación (modelo de incrustaciones, LLM, almacén de documentos y archivos JSON del almacén vec).
- Construya y ejecute el contenedor Docker.
Básicamente, se puede reducir a esto: utilice el build
notebook para generar doc_store y vector_store, y colocarlos en su aplicación.
GitHub aquí. ¡Gracias por leer!