Esta es la tercera parte (3 de 4) de los materiales que preparé para mi clase de Programación con IA. En esta tercera parte, hablaré de cómo conseguir que la aplicación LLMs realice la generación aumentada por recuperación (RAG).
La generación aumentada por recuperación (RAG) ha hecho furor en los últimos meses. A menudo se ha utilizado como solución para combatir la alucinación LLM y como una de las capacidades clave que impulsan muchos casos de uso de procesamiento y generación de texto. En particular, los casos de uso del tipo pregunta y respuesta (QA) recurren mucho a la RAG.
Pero, ¿qué es la RAG? El término generación aumentada por recuperación o RAG se acuñó por primera vez en el documento Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks, de Facebook AI Research, University College London y New York University. Básicamente, la RAG combina memoria paramétrica y no paramétrica para producir un resultado.
La parte no paramétrica es esencialmente una base de datos (una colección de documentos) y se utiliza un mecanismo de recuperación para seleccionar la información específica que se necesita. La parte paramétrica es un LLM que tiene conocimientos ya almacenados en sus pesos.
En un nivel superior, un RAG es una técnica para mejorar las capacidades del LLM utilizando datos de fuera del LLM. Proporciona dos capacidades principales.
En primer lugar, la RAG proporcionará datos que no están disponibles en la memoria paramétrica alias el conocimiento que se almacena en la LLM. En segundo lugar, reducirá las alucinaciones. Los LLM alucinan cuando no tienen los datos para dar una respuesta, por lo que al proporcionarles suficientes datos, se reducirá (aunque desafortunadamente no se eliminará) la alucinación.
Hay muchas formas de hacer RAG, desde tan simple como pasar todos los datos directamente al LLM hasta métodos mucho más elaborados utilizando bases de datos vectoriales o motores de búsqueda. En este artículo, hablaré de 3 tipos:
- RAG simple
- RAG de búsqueda mejorada
- Incrustaciones RAG
Tabla de contenidos
RAG simple
La forma más sencilla de hacer RAG es simplemente agrupar todos los datos y pasarlos al LLM. En otras palabras, el recuperador simplemente toma todo y lo pasa al LLM.
Esto puede parecer una broma, pero es una técnica seria porque es muy sencilla y funciona bastante bien.
En RAG queremos pasar más datos al LLM. En los primeros días de LLMs (increíblemente esto es hace poco más de un año) la ventana de contexto para LLMs estaba mayormente en el rango de 4k tokens o cerca de 3k palabras. Una página a espacio simple tiene unas 500 palabras, así que estamos hablando de 6 páginas. Esto significa que el LLM con una ventana de contexto de 4k sólo puede procesar 6 páginas de datos a la vez. Si tenemos muchos datos, esto puede ser una limitación bastante seria. Esta es la razón por la que en el documento RAG se necesita un recuperador para seleccionar sólo los datos relevantes que se envían al LLM.
Pero en el último año, más o menos, las cosas han cambiado. Todo empezó con el salto de OpenAI de 4k a 16k para GPT-3.5-Turbo, y después a 32k para GPT-4. MosaicML salió con MPT-7B-StoryWriter con 65k de tamaño de ventana de contexto. Un mes más tarde, Anthropic presentó Claude 2 con una ventana de contexto de 100k.
Pronto se convirtió en una carrera para que los LLM aumentaran el tamaño de su ventana de contexto. En el momento de escribir esto, aquí están los tamaños de ventana de contexto para los principales proveedores de LLM en el mercado:
- OpenAI – 128k para GPT-4o y GPT-4-Turbo
- Cohere – 128k para Command-R
- x.AI – 128k para Grok 1.5
- Anthropic – 200 000 para Claude 3, hasta 1 millón para algunos casos de uso
- Google – 1 millón para Gemini 1.5
128k tokens son unas 98k palabras. La longitud media de una novela es de unas 90.000 palabras, así que estamos hablando de poder meter en el LLM los datos de toda una novela. 1 millón de tokens equivale a unas 750.000 palabras, es decir, algo más de 8 novelas. Durante la conferencia de desarrolladores Google I/O de mayo de 2024, Google anunció que Gemini-1.5 Pro tendrá 2 millones de tokens.
Esto cambia las cosas. Antes de estos enormes tamaños de ventana de contexto, necesitamos cortar los datos en trozos más pequeños para que quepan en la ventana de contexto. Si ahora todos los datos caben en la ventana contextual sin necesidad de trocearlos, ¿sigue siendo necesaria la RAG?
En un artículo anterior, comenté que los costes podrían ser un problema. Hace sólo unos meses, GPT-4 costaba 30 $ por millón de tokens y GPT-4-32k 60 $, mientras que Claude 3 costaba 15 $ por millón de tokens. Pero muy rápidamente, la competencia o lo que sea, hizo caer los precios de GPT-4o a 5 dólares por millón de tokens, y Gemini-1.5-Flash a unos ridículos ¡35 céntimos por millón de tokens!
Aquí tienes una aplicación para iOS que escribí y que te permite leer PDFs y enviar todo el documento que estás leyendo a Gemini-1.5-Flash para responder preguntas.
Sin embargo, aunque este método funciona para la mayoría de los documentos, difícilmente serviría para bases de datos con Gigabytes, Terabytes o Petabytes de datos. Tampoco funciona en los casos en que no tenemos acceso inmediato a los datos o si sólo tenemos acceso a ellos sobre la marcha. Un muy buen ejemplo de ello es cuando el recuperador RAG es en realidad un motor de búsqueda.
RAG mejorado para búsquedas
Si lo piensas bien, un motor de búsqueda es probablemente el recuperador ideal si estamos tratando de cargar el GAR con los datos más actualizados y actuales. Los datos devueltos por un buen motor de búsqueda probablemente contengan todos los datos que quieres como entrada para el LLM.
De hecho, cuando se lanzó por primera vez Google Bard (ahora Google Gemini) se pregonaba que tenían los datos más actualizados en comparación con el entonces ChatGPT. Eso se debe a que utilizaron el motor de búsqueda de Google para bombear los datos de la consulta en Palm, el modelo que Google utilizó para Bard entonces. Lo mismo ocurre con Microsoft Bing Chat, que Microsoft te dice literalmente que tomaron los resultados del buscador Bing y los enviaron a través del modelo de OpenAI.
Veamos cómo podemos hacer esto.
Para este ejemplo, voy a utilizar DuckDuckGo como motor de búsqueda, principalmente porque no quería pagar para utilizar una API de búsqueda y es bastante fácil de recrear un SERP (página de resultados del motor de búsqueda) API utilizando DuckDuckGo. Aquí está la función ddg que realiza una búsqueda basada en una consulta y devuelve una cadena con los resultados de la SERP.
func ddg(query string) (string, []SearchResult, error) {
queryURL := fmt.Sprintf("https://html.duckduckgo.com/html/?q=%s", url.QueryEscape(query))
userAgent := "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15"
client := &http.Client{}
request, err := http.NewRequest("GET", queryURL, nil)
if err != nil {
return "", []SearchResult{}, err
}
request.Header.Set("User-Agent", userAgent)
response, err := client.Do(request)
if err != nil {
return "", []SearchResult{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", []SearchResult{}, fmt.Errorf("status is %d", response.StatusCode)
}
doc, err := goquery.NewDocumentFromReader(response.Body)
if err != nil {
return "", []SearchResult{}, err
}
results := []SearchResult{}
sel := doc.Find(".web-result")
// text := ""
for i := range sel.Nodes {
if len(results) > 5 {
break
}
node := sel.Eq(i)
titleNode := node.Find(".result__a")
info := node.Find(".result__snippet").Text()
title := titleNode.Text()
url, _ := node.Find(".result__snippet").Attr("href")
results = append(results, SearchResult{title, info, url})
}
formattedResults := ""
for _, result := range results {
formattedResults += fmt.Sprintf("Title: %s\n"+
"Description: %s\n\n", result.Title, result.Info)
}
return formattedResults, results, nil
}
En el código anterior, he utilizado el cliente HTTP de la biblioteca estándar Go para enviar una petición GET a una página específica de DuckDuckGo, con una consulta. Esto devuelve un SERP y yo uso el paquete goquery para recorrer y extraer los resultados en una rebanada de SearchResult structs, así como una rebanada de URLs.
type SearchResult struct {
Title string
Info string
Url string
}
Por último, he convertido la porción de structs SearchResult en una cadena y la he devuelto junto con la porción de URL.
La cadena de resultados de búsqueda y la porción de URL se pasan a la función de búsqueda.
func search(model string, query string) (string, error) {
data, result, err := ddg(query)
if err != nil {
log.Println("Cannot process query:", err)
return "", err
}
system := `The following search results has come back from a search engine given the query
that came from a user. Respond to the original query using the search results. Do not add any
additional information. Assume the person you are explaining to doesn't know anything about
the answer and provide a detailed response based on the query results only. End the response
with a list of URLs returned.`
prompt := `{
"query" : "` + query + `",
"search_result" : "` + data + `",
"urls" : "` + fmt.Sprint(result) + `"
}`
return chat(model, system, prompt)
}
Los resultados de la búsqueda se utilizan para formar una cadena JSON que representa la consulta del usuario y los resultados de la búsqueda en DuckDuckGo. Tanto la consulta del sistema como la del usuario se pasan a la función de chat.
func chat(model string, system string, query string) (string, error) {
llm, err := ollama.New(ollama.WithModel(model))
if err != nil {
return "", err
}
c := context.Background()
resp, err := llm.GenerateContent(
c, []llms.MessageContent{
llms.TextParts(llms.ChatMessageTypeSystem, system),
llms.TextParts(llms.ChatMessageTypeHuman, query),
}, llms.WithMinLength(2048),
)
if err != nil {
return "", err
}
return resp.Choices[0].Content, nil
}
A su vez, la función de chat utiliza el paquete langchaingo para conectarse a un modelo de Ollama, tanto con el prompt del sistema como con la consulta del usuario.
Ya está. Vamos a darle una vuelta.
En caso de que la captura de pantalla sea demasiado pequeña para leerla, a continuación se muestra el resultado de la ejecución de la RAG mejorada por búsqueda.
$ ./rag search "Who is the current Prime Minister of Singapore and when did he assume this position?"
## RAG USING SEARCH - Who is the current Prime Minister of Singapore and when did he assume this position?
## FROM SEARCH USING DUCKDUCKGO
Title: Who is Lawrence Wong, Singapore's new prime minister?
Description: REUTERS/Isabel Kua/File Photo Purchase Licensing Rights. SINGAPORE, May 15 (Reuters) - Lawrence Wong, 51, will take over as prime minister of Singapore on Wednesday, becoming the fourth leader of ...
Title: Who is Lawrence Wong, the man poised to become Singapore's fourth prime ...
Description: The 51-year-old will be sworn in on May 15, current prime minister Lee Hsien Loong announced on Monday. "Lawrence and the 4G team have worked hard to gain the people's trust, notably during the ...
Title: Singapore's new prime minister vows to 'lead in our own way' as Lee ...
Description: FILE - Singapore's Deputy Prime Minister and Finance Minister Lawrence Wong, left, shares a light moment with Indonesian Defense Minister and president-elect Prabowo Subianto during their meeting at Bogor Presidential Palace in Bogor, Indonesia, Monday, April 29, 2024. Singapore's deputy leader Wong is set to be sworn in Wednesday, May 15 ...
Title: Lawrence Wong sworn in as Singapore's first new prime minister in 20 ...
Description: Wong will retain his current position as finance minister and takes charge of a country led for 20 years by Lee Hsien Loong, the 72-year-old son of Lee Kuan Yew, the founder of modern Singapore ...
Title: Singapore's new PM takes office pledging to lead his own way
Description: Wong will retain his position as finance minister and takes charge of a country led for two decades by Lee Hsien Loong, the 72-year-old son of Lee Kuan Yew, the founder of modern Singapore who ...
Title: DPM Lawrence Wong to take over from PM Lee on May 15
Description: May 13, 2024, 12:18 PM. SINGAPORE - Singapore will have a new prime minister on May 15, with Deputy Prime Minister Lawrence Wong set to take over from Prime Minister Lee Hsien Loong on that day ...
## FINAL OUTPUT
The current Prime Minister of Singapore is Lawrence Wong, who assumed this position on May 15. He is the fourth leader of Singapore and has taken over from Prime Minister Lee Hsien Loong.
According to the search results, Lawrence Wong, 51, will take over as prime minister of Singapore on Wednesday, becoming the fourth leader of the country. He will retain his current position as finance minister and takes charge of a country led for 20 years by Lee Hsien Loong, the 72-year-old son of Lee Kuan Yew, the founder of modern Singapore.
Wong has vowed to "lead in our own way" as he takes office, pledging to continue the legacy of his predecessor while also bringing new ideas and perspectives to the role. He is set to be sworn in on May 15, marking a significant change for the country after two decades under Lee Hsien Loong's leadership.
URLs:
* https://www.reuters.com/world/asia-pacific/singapores-new-leader-lawrence-wong-2024-05-15/
* https://www.abc.net.au/news/2024-04-15/singapore-lawrence-wong-prime-minister-new-lee-hsien-loong/103516362
* https://apnews.com/article/singapore-new-prime-minister-lawrence-wong-5e84846bafbc41cd44f5a75c9e351657
* https://www.aljazeera.com/news/2024/5/15/lawrence-wong-sworn-in-as-singapores-first-new-prime-minister-in-20-years
* https://www.reuters.com/world/asia-pacific/singapore-inaugurate-new-pm-lee-makes-way-after-20-years-charge-2024-05-15/
* https://www.straitstimes.com/singapore/politics/dpm-lawrence-wong-to-take-over-from-pm-lee-on-may-15
El nuevo Primer Ministro de Singapur, Lawrence Wong, tomó posesión de su cargo el 15 de mayo de 2024. Aquí estamos utilizando el modelo 8B de Llama 3, y el corte de conocimiento es de marzo de 2023. Ciertamente, Llama 3 no sabría quién es el nuevo Primer Ministro, pero DuckDuckGo sí lo sabe. Introdujo los resultados de la búsqueda en Llama 3 y Llama 3, a su vez, utiliza estos datos para generar una respuesta.
Utilizar un RAG mejorado por la búsqueda es probablemente lo más útil si queremos los datos más actualizados y públicamente disponibles. ¿Y si queremos hacer lo mismo con datos privados? Se trata de un caso de uso muy común, especialmente en sistemas empresariales en los que queremos aplicar la RAG a datos internos y confidenciales.
Probablemente haya varias formas de hacerlo, incluida la creación de un motor de búsqueda interno. También hay otra forma, bastante popular, que utiliza una base de datos vectorial e incrustaciones de texto.
RAG con incrustaciones
La idea general para hacer RAG con incrustaciones de texto no es muy diferente de utilizar un motor de búsqueda como recuperador. De hecho, estamos recreando un motor de búsqueda utilizando incrustaciones de texto.
Una incrustación de texto, también llamada vector, es un fragmento de texto proyectado en un espacio de alta dimensión. Quizá conozcas los vectores por haber estudiado álgebra en el instituto: básicamente, es una cantidad que tiene una magnitud y una dirección. Mientras que en el instituto solemos tratar con vectores de 2 o 3 dimensiones, las incrustaciones de texto tienen un número mucho mayor de dimensiones. Muchos modelos de incrustación de texto tienen incrustaciones de 768 dimensiones.
La idea general es dividir los datos en fragmentos y crear incrustaciones de texto para cada fragmento. Cuando se hace una pregunta a los datos, tomamos la pregunta, la convertimos en una incrustación de texto y encontramos las incrustaciones de texto que más se le parecen. Con eso podemos encontrar los trozos de datos correspondientes, y ése es nuestro recuperador.
Entonces, ¿cómo podemos encontrar incrustaciones de texto similares?
Semejanzas
Hay muchas formas de encontrar similitudes. Una forma popular de hacerlo con datos de texto es mediante la similitud coseno. No se preocupe: parece complejo, pero en realidad no lo es. Se lo explicaré.
La similitud coseno es una medida de la similitud entre dos vectores. Como su nombre indica, es el coseno del ángulo entre dos vectores. Se trata de un número comprendido entre -1,0 y 1,0, en el que un número cercano a 1,0 es similar y un número cercano a 0 no está relacionado, mientras que un número cercano a -1,0 es opuesto.
En otras palabras, sólo tenemos que hallar la similitud coseno entre 2 incrustaciones de texto y se eligen las que tengan las similitudes más altas, con sus trozos correspondientes. El cálculo de la similitud coseno es bastante sencillo, por eso es rápido. Normalmente no necesitamos hacer este cálculo nosotros mismos, la base de datos vectorial ya debería tenerlo implementado.
Antes de empezar a utilizar la similitud coseno, tenemos que preparar los datos.
Preparación de los datos
Estos son los pasos para preparar los datos:
- Convertir los datos en texto (si no lo eran ya) y limpiarlos.
- Trocear los datos de forma lógica. Hay muchas técnicas para hacerlo, pero la más sencilla es dividirlos en párrafos o segmentos.
- Generar incrustaciones de texto para cada trozo. Esto puede hacerse llamando a la API de un proveedor de LLM o utilizando un LLM abierto.
- Almacenar los trozos y las incrustaciones de texto en una base de datos vectorial (las bases de datos vectoriales están especializadas en almacenar incrustaciones).
Veamos un poco de código. Para el propósito de este artículo, voy a utilizar un documento PDF como los datos y la base de datos Postgres (junto con pgvector) como la base de datos vectorial.
El documento PDF que utilizo aquí es la presentación del Manchester United Football Club ante la Comisión de Bolsa y Valores de Estados Unidos. Este documento tiene 177 páginas y 143k tokens.
Este documento reventará totalmente los tamaños de ventana de contexto de la mayoría de los LLM comerciales, excepto Claude 3 de Anthropic y Gemini 1.5 de Google. Con toda seguridad, podría rebasar el límite de la ventana de contexto de 8k de Llama 3. En el siguiente ejemplo, mostraré cómo podemos utilizar RAG para superar esta limitación.
Convertir datos PDF en texto y trocearlos por párrafos
En primer lugar, queremos convertir el archivo PDF en una cadena de texto, utilizando la herramienta pdftotext.
func convert(inputpdf string) (string, error) {
tempdir, err := os.MkdirTemp("", "rag")
if err != nil {
log.Println("unable to create a temporary directory:", err)
return "", err
}
defer os.RemoveAll(tempdir)
cmd := exec.Command(filepath.Join("bin", "pdftotext"), inputpdf, filepath.Join(tempdir, "output.txt"))
_, err = cmd.CombinedOutput()
if err != nil {
log.Printf("Command error: %s\n", err)
return "", err
}
text, err := os.ReadFile(filepath.Join(tempdir, "output.txt"))
if err != nil {
log.Printf("cannot read text: %s\n", err)
return "", err
}
content := string(text)
content = strings.ToValidUTF8(content, "")
return content, nil
}
A continuación, dividimos la cadena por párrafos y la limpiamos eliminando los duplicados y también las cadenas más cortas.
func clean(content string) []string {
split := strings.Split(content, "\n")
cleaned := []string{}
for _, s := range split {
cleaned = append(cleaned, strings.TrimSpace(s))
}
unique := removeDuplicates(cleaned)
shortRemoved := removeShortStrings(unique)
return shortRemoved
}
func removeDuplicates(s []string) []string {
m := make(map[string]bool)
result := []string{}
for _, item := range s {
if _, ok := m[item]; !ok {
m[item] = true
result = append(result, item)
}
}
return result
}
func removeShortStrings(slice []string) []string {
var result []string
for _, str := range slice {
sl := strings.Split(str, " ")
if len(sl) > 3 {
result = append(result, str)
}
}
return result
}
Convertir trozos en incrustaciones
Ahora que tenemos los trozos, utilizamos el modelo nomic-embed-text de Ollama para obtener las incrustaciones.
func getEmbeddings(content []string) ([][]float32, error) {
llm, err := ollama.New(ollama.WithModel("nomic-embed-text"))
if err != nil {
return [][]float32{}, err
}
c := context.Background()
return llm.CreateEmbedding(c, content)
}
El modelo de incrustación crea incrustaciones con 768 dimensiones.
Añadir chunk e incrustaciones a la base de datos de vectores
Antes de empezar, echemos un vistazo a la base de datos vectorial. Empezaremos con una base de datos relacional Postgres normal llamada rag. Para convertirla en una base de datos vectorial, sólo tenemos que utilizar la extensión pgvector. Ejecute esto en psql en la base de datos rag.
CREATE EXTENSION vector;
Sí, es así de sencillo. Ahora a crear una tabla que admita tanto un trozo de texto como las incrustaciones.
CREATE TABLE docs (
id SERIAL PRIMARY KEY,
text varchar(1024),
embeddings vector(768)
);
Ahora, con esta tabla de base de datos, podemos empezar a añadir los trozos y las incrustaciones.
func add(filepath string) error {
data, err := convert(filepath)
if err != nil {
fmt.Println("err in converting:", err)
return err
}
doc := clean(data)
embeddings, err := getEmbeddings(doc)
if err != nil {
fmt.Println("err in getting embeddings:", err)
return err
}
ctx := context.Background()
conn, err := pgx.Connect(ctx, "postgres://localhost/rag")
if err != nil {
return err
}
defer conn.Close(ctx)
for i := 0; i < len(doc); i++ {
_, err = conn.Exec(ctx, "INSERT INTO docs (text, embeddings) VALUES ($1, $2)",
doc[i], pgvector.NewVector(embeddings[i]))
if err != nil {
return err
}
}
return nil
}
Con esto, terminamos con una tabla con trozos y sus incrustaciones asociadas de 768 dimensiones.
Una vez creada la base de datos de vectores, veamos cómo utilizarla para responder preguntas.
Responder preguntas
Cuando el usuario hace una pregunta sobre los datos, estos son los pasos:
- Generar las incrustaciones de texto de la pregunta y utilizarlas para encontrar los fragmentos cuyas incrustaciones sean más parecidas.
- Combinar la pregunta y los fragmentos y enviarlos al LLM.
Esta es la función que envuelve las distintas llamadas a funciones para responder a las preguntas de los documentos.
func qa(model string, query string) (string, error) {
docs, err := get(query)
if err != nil {
log.Println("Cannot process query:", err)
return "", err
}
data := strings.Join(docs, "\n")
system := `The following data are the relevant parts of the document related to the query.
Respond to the original query using these parts. Do not add any additional information.
Assume the person you are explaining to doesn't know anything about the answer and provide
a detailed response based on the query results only.`
prompt := `{
"query" : "` + query + `",
"parts" : "` + data + `"
}`
return chat(model, system, prompt)
}
La primera función es la función get, que obtiene las incrustaciones de la pregunta y luego las utiliza en una consulta SQL para obtener los trozos más similares a la pregunta.
func get(prompt string) ([]string, error) {
ctx := context.Background()
conn, err := pgx.Connect(ctx, "postgres://localhost/rag")
if err != nil {
return []string{}, err
}
defer conn.Close(ctx)
embeddings, err := getEmbeddings([]string{prompt})
if err != nil {
return []string{}, err
}
rows, err := conn.Query(ctx,
"SELECT id, text, (1 - (embeddings <=> $1)) as cosine_distance "+
"FROM docs ORDER BY cosine_distance DESC LIMIT 5",
pgvector.NewVector(embeddings[0]))
if err != nil {
return []string{}, err
}
defer rows.Close()
type doc struct {
Id int
Text string
Distance float32
}
var list []doc
fmt.Println("\n## FROM VECTOR DATABASE")
for rows.Next() {
var d doc
err = rows.Scan(&d.Id, &d.Text, &d.Distance)
if err != nil {
return []string{}, err
}
fmt.Println(d.Id, "\t", d.Text, "\t", d.Distance)
list = append(list, d)
}
if rows.Err() != nil {
return []string{}, err
}
var results []string
for _, d := range list {
results = append(results, d.Text)
}
return results, nil
}
En el SQL utilizamos el operador <=> que obtiene la distancia coseno entre la pregunta y los trozos. Fíjate que restamos el resultado de 1 para obtener la similitud coseno.
Eso es todo para el código. Vamos a ejecutar esto y hacer una pregunta en este documento de 177 páginas. Queremos averiguar cuánto es la garantía mínima pagadera por Adidas.
Aquí está la respuesta correcta en la página 43.
Ahora hagamos la misma pregunta al LLM utilizando una técnica RAG de incrustación.
Aquí están los resultados de nuevo en texto por si la captura de pantalla es demasiado pequeña.
$ ./rag qa "How much is the minimum guarantee payable by Adidas?"
## RAG USING VECTOR DATABASE - How much is the minimum guarantee payable by Adidas?
## FROM VECTOR DATABASE
1010 Pursuant to our contract with adidas, which began on 1 August 2015, the minimum guarantee payable by adidas over the 10-year term of the agreement is equal to 750 million, subject to certain adjustments. See "Item 4. Information on the Company--Revenue Sectors--Commercial--Retail, Merchandising, Apparel & Product Licensing" for additional information regarding our agreement with adidas. 0.8429114
1686 The Group has a 10-year agreement with adidas which began on 1 August 2015. The minimum guarantee payable by adidas over the term of the agreement is 750 million, subject to certain adjustments. Payments due in a particular year may increase if the club's men's first team wins the Premier League, FA Cup or Champions League, or decrease if the club's men's first team fails to participate in the Champions League for two or more consecutive seasons with the maximum possible increase being 4 million per year and the maximum possible reduction being 30% of the applicable payment for the year in which the second or other consecutive season of non-participation falls. Participation in the UEFA Champions League is typically secured via a top 4 finish in the Premier League or winning the UEFA Europa League. Revenue is currently being recognized based on management's estimate at 30 June 2022 that the full minimum guarantee amount is the most likely amount that will be received, as management does not expect two consecutive seasons of non-participation in the Champions League. 0.8226619
782 We have a 10-year agreement with adidas with respect to our global technical sponsorship and dualbranded licensing rights, which began on 1 August 2015. The minimum guarantee payable by adidas over the term of the agreement is equal to 750 million, subject to certain adjustments. Payments due in a particular year may increase if our men's first team wins the Premier League, FA Cup or Champions League, or 0.80104846
1117 The minimum guarantee payable by adidas over the term of our agreement with them is equal to 750 million, subject to certain adjustments. Payments due in a particular year may increase if our men's first team wins certain competitions or decrease if our men's first team fails to participate in the Champions League for two or more consecutive seasons, with the reduction being 30% of the applicable payment for the year in which the second or other consecutive season of non-participation falls. In the event of a reduction in any year due to the failure to participate in the Champions League for two or more consecutive seasons, the remaining payments revert back to the original terms upon the men's first team participating again in the Champions League. Any increase or decrease in a particular year would have the effect of increasing or decreasing the minimum guarantee amount of 750 million payable over the term of the agreement. A critical estimate in the current and future financial years therefore will be management's assessment as to whether or not our men's first team is likely to fail to participate in the Champions League for two or more consecutive seasons during the term of the agreement. Such assessments of future participation may differ from actual participation, which could result in a difference in the revenue recognized in a given year. 0.76649946
1452 As described in Note 4 to the consolidated financial statements, the Company's consolidated revenue recognised for the year ending 30 June 2022 was 583,201 thousand, of which 257,820 thousand relates to commercial revenue. A number of commercial contracts contain significant estimates in relation to the allocation and recognition of revenue in line with performance obligations. In instances where the sponsorship rights remain the same over the duration of the contract, revenue is recognized as performance obligations are satisfied evenly over time. Minimum guaranteed revenue is recognised over the term of the sponsorship agreement in line with the performance obligations included within the contract and based on the sponsorship benefits enjoyed by the individual sponsor. As disclosed by management, the Company has a 10-year agreement with adidas with respect to global technical sponsorship and dual-branded licensing rights, which began on 1 August 2015. The minimum guarantee payable by adidas over the term of the agreement is 750 million, subject to certain adjustments. Payments due in a particular year may increase if the club's men's first team wins the Premier League, FA Cup or Champions League, or decrease if the club's men's first team fails to participate in the Champions League for two or more consecutive seasons with the maximum possible increase being 4 million per year and the maximum possible reduction being 30% of the applicable payment for the year in which the second or other consecutive season of non-participation falls. Revenue is currently being recognized based on management's estimate at 30 June 2022 that the full minimum guarantee amount is the most likely amount that will be received, as management does not expect two consecutive seasons of non-participation in the Champions League. 0.71642417
## FINAL OUTPUT
The minimum guarantee payable by Adidas is equal to 750 million, subject to certain adjustments.
He aquí de nuevo la misma pregunta, formulada a Gemini-1.5 utilizando mi aplicación qaPDF (que funciona en iPhone, iPad y MacOS).
Resumen
En este artículo se han tratado 3 formas de hacer RAG con LLMs. La primera es no hacer RAG en absoluto, sino simplemente pasar todos los datos al LLM. Desafortunadamente esto requiere una ventana de contexto realmente grande, y puede ser potencialmente bastante caro.
El segundo es más adecuado para los resultados más actualizados, que es muy similar a Bing Chat o Gemini Chat. Esto utiliza un motor de búsqueda como recuperador y pasando los resultados de la SERP al LLM.
Por último, pasamos a utilizar incrustaciones de texto almacenadas en una base de datos vectorial para RAG. Esta es una de las formas más comunes de hacer RAG y puede ser bastante eficaz.