Como avaliar seu aplicativo LLM
Avalie esse Tutorial
Se você já implantou modelos de machine learning em produção, sabe que a avaliação é uma parte importante do processo. A avaliação é como você escolhe o modelo certo para seu caso de uso, garante que o desempenho do seu modelo seja traduzido do protótipo para a produção e detecta regressões de desempenho. Embora a avaliação de aplicativos de IA generativa (também conhecidos como aplicativos LLM) possa parecer um pouco diferente, os mesmos princípios de por que devemos avaliar esses modelos se aplicam.
Neste tutorial, detalharemos como avaliar aplicativos LLM, com o exemplo de um aplicativo de Retrieval Augmented Generation (RAG). Especificamente, abordaremos o seguinte:
- Desafios na avaliação de aplicativos para LLM
- Definindo métricas para avaliar aplicativos LLM
- Como avaliar um aplicativo RAG
Antes de começar, é importante distinguir a avaliação do modelo LLM da avaliação do aplicativo LLM. A avaliação de modelos LLM envolve a medição do desempenho de um determinado modelo em diferentes tarefas, enquanto a avaliação de aplicativos LLM trata da avaliação de diferentes componentes de um aplicativo LLM, como prompts, recuperadores etc., e do sistema como um todo. Neste tutorial, vamos nos concentrar em avaliar aplicativos LLM.
A razão de não ouvirmos muito sobre a avaliação de aplicativos de LLM é que atualmente ela é um desafio e consome muito tempo. Os modelos convencionais de aprendizado de máquina, como regressão e classificação, têm um conjunto de métricas matematicamente bem definido, como erro quadrático médio (MSE), precisão e recall para avaliação. Em muitos casos, a verdade do campo também está disponível para avaliação. No entanto, esse não é o caso dos aplicativos LLM.
Atualmente, os aplicativos LLM estão sendo usados para tarefas complexas, como resumo, resposta a perguntas longas e geração de códigos. As métricas convencionais, como precisão e exatidão em sua forma original, não se aplicam a esses cenários, pois a saída dessas tarefas não é uma previsão binária simples ou um valor de ponto flutuante para calcular positivos verdadeiros/falsos ou resíduos. Métricas como fidelidade e relevância, que são mais aplicáveis a essas tarefas, estão surgindo, mas são difíceis de quantificar de forma definitiva. A natureza probabilística dos LLMs também torna a avaliação um desafio – alterações simples de formatação no nível do prompt, como adicionar novas linhas ou pontos de marcador, podem ter um impacto significativo nos resultados do modelo. E, finalmente, a verdade do campo é difícil de encontrar e consome tempo para criar manualmente.
Embora não haja uma maneira prescrita de avaliar aplicativos LLM atualmente, alguns princípios orientadores estão emergindo.
Quer se trate de escolher modelos incorporados ou avaliar aplicativos LLM, concentre-se em sua tarefa específica. Isso é especialmente aplicável na escolha de parâmetros para avaliação. Aqui estão alguns exemplos:
Tarefa | Parâmetros de avaliação |
---|---|
Moderação de conteúdo | Recall e precisão sobre toxicidade e viés |
Geração de consultas | Sintaxe e atributos de saída corretos, extrai as informações corretas na execução |
Dialogue (chatbots, summarization, Q&A) | Fidelidade, relevância |
Tarefas como moderação de conteúdo e geração de queries são mais diretas, pois têm respostas esperadas definidas. No entanto, para tarefas abertas envolvendo diálogo, o melhor que podemos fazer é verificar a consistência fatural (lealdade) e a relevância da resposta à pergunta do usuário. Atualmente, uma abordagem comum para realizar essas avaliações é usar LLMs fortes. Embora essa técnica possa estar sujeita a alguns dos desafios que enfrentarmos com LLMs hoje, como atordoamentos e vieses, ela é melhor dimensionada do que a avaliação em humanos. Ao escolher um LLM avaliador, o Tabela de classificação dochatbot cursor é um bom recurso, pois é uma lista de colaboração popular dos LLMs de melhor desempenho classificados por preferência humana.
Depois de descobrir os parâmetros para avaliação, você precisa de um conjunto de dados de avaliação. Vale a pena gastar tempo e esforço criando manualmente um pequeno conjunto de dados (até mesmo amostras de 50 são um bom começo!) consistindo nas perguntas mais comuns que os usuários podem fazer ao seu aplicativo, em alguns casos extremos (leia-se: complexos) e em perguntas que ajudam a avaliar a resposta do seu sistema a entradas maliciosas e/ou inapropriadas. Você pode avaliar o sistema separadamente em cada um desses conjuntos de perguntas para obter uma compreensão mais detalhada dos pontos fortes e fracos do seu sistema. Além de fazer a curadoria de um conjunto de dados de perguntas, talvez você também queira escrever respostas verdadeiras para as perguntas. Embora sejam especialmente importantes para tarefas como a geração de consultas que têm uma resposta certa ou errada definitiva, eles também podem ser úteis para fundamentar LLMs ao usá-los como juízes para avaliação.
Como acontece com qualquer software, você deve avaliar cada componente separadamente e o sistema como um todo. Em sistemas RAG, por exemplo, você desejará avaliar a recuperação e a geração para garantir que está recuperando o contexto correto e gerando respostas adequadas, enquanto em agentes de chamada de ferramentas, você desejará validar as respostas intermediárias de cada uma das ferramentas. Você também vai querer avaliar o sistema geral quanto à correção, normalmente comparando a resposta final com a resposta da verdade básica.
Por fim, considere como você coletará comentários dos usuários, incorpore-os ao seu pipeline de avaliação e acompanhe o desempenho do seu aplicativo ao longo do tempo.
No restante do tutorial, usaremos o RAG como um exemplo para demonstrar como avaliar um aplicativo LLM. Mas antes disso, aqui está uma atualização rápida no RAG.
Um aplicativo RAG pode ser assim:
Em um aplicativo RAG, o objetivo é melhorar a qualidade das respostas geradas por um LLM completando seu conhecimento paramétrico com contexto recuperado de uma base de conhecimento externa. Para criar a base de conhecimento, documentos de referência grandes são divididos em partes menores, e cada parte é armazenada em um banco de dados junto com sua incorporação vetorial gerada usando um modelo de incorporação.
Dada uma query do usuário, ela é primeiro incorporada usando o mesmo modelo de incorporação, e os chunks mais relevantes são recuperados com base na similaridade entre a query e os vetores de chunk. Em seguida, um LLM usa a pergunta do usuário, o prompt e os documentos recuperados para gerar uma resposta para a pergunta.
Os principais elementos a serem avaliados em um aplicativo RAG são os seguintes:
- Recuperação: envolve experimentar diferentes estratégias de processamento de dados, incorporar modelos, etc., e avaliar como elas afetam a recuperação.
- Geração: Depois de decidir sobre as melhores configurações para o recuperador, esta etapa envolve experimentar diferentes LLMs para encontrar o melhor modelo de conclusão para a tarefa.
Neste tutorial, avaliaremos diferentes modelos de incorporação para recuperação, diferentes modelos de conclusão para geração e o sistema como um todo com os modelos de melhor desempenho.
Usaremos as seguintes métricas para avaliação:
Retrieval
- Precisão do contexto: avalia a capacidade do retriever de classificar os itens recuperados em ordem de relevância para a resposta de verdade fundamental
- Recuperação de contexto: mede até que ponto o contexto recuperado se alinha com a resposta básica da verdade
Geração
- Fidelidade: mede a consistência factual da resposta gerada em relação ao contexto recuperado
- Relevância da resposta: mede a relevância da resposta gerada para o prompt fornecido (pergunta + contexto recuperado)
Geral
- Similaridade semântica da resposta: mede a semelhança semântica entre a resposta gerada e a verdade fundamental
- Exatidão da resposta: mede a precisão da resposta gerada em comparação com a verdade fundamental
Usaremos o LangChain para criar um aplicativo RAG de amostra e a estruturaRAGAS para avaliação. O RGAS é open source, tem suporte pronto para uso para todas as métricas acima, oferece suporte a prompts de avaliação personalizados e tem integrações com frameworks como LangChain, LlamaIndex e ferramentas de observabilidade como LangSmith e Arize Linux.
Usaremos o conjuntode dados ragas-wikiqa disponível no Hugging Face. O conjunto de dados consiste em ~230 perguntas de conhecimentos gerais, incluindo as respostas verdadeiras para essas perguntas. Seu conjunto de dados de avaliação, no entanto, deve ser uma boa representação de como os usuários interagirão com seu aplicativo.
Vamos precisar das seguintes bibliotecas para este tutorial:
- datasets: biblioteca Python para obter acesso a conjuntos de dados disponíveis no Hugging Face Hub
- ragas: Biblioteca Python para a estrutura RAGAS
- langchain: biblioteca Python para desenvolver aplicativos LLM usando LangChain
- langchain-mongodb: pacote Python para usar o MongoDB Atlas como armazenamento de vetores com LangChain
- langchain-openai: pacote Python para usar modelos OpenAI no LangChain
- pymongo: driver Python para interagir com o MongoDB
- pandas: biblioteca Python para análise, exploração e manipulação de dados
- tdqm: módulo Python para mostrar um medidor de progresso para loops
- matplotlib, seaborn: bibliotecas Python para visualização de dados
1 ! pip install -qU datasets ragas langchain langchain-mongodb langchain-openai \ 2 pymongo pandas tqdm matplotlib seaborn
Neste tutorial, usaremos o MongoDB Atlas como um armazenamento e recuperação de vetores. Mas, primeiro, você precisará de uma conta do MongoDB Atlas com um cluster de banco de dados e obter a cadeia de conexão para se conectar ao seu cluster. Siga estas etapas para configurar:
Depois de obter a connection string, defina-a em seu código:
1 import getpass 2 MONGODB_URI = getpass.getpass("Enter your MongoDB connection string:")
Usaremos os modelos de incorporação e conclusão de chat do OpenAI, então você também precisará obter uma chave de API do OpenAI e defini-la como uma variável de ambiente para o cliente do OpenAI usar:
1 import os 2 from openai import OpenAI 3 os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API Key:") 4 openai_client = OpenAI()
Como mencionado anteriormente, usaremos o conjuntode dados ragas-wikiqa disponível no Hugging Face. Vamos baixá-lo usando a bibliotecade conjuntos de dados e convertê-lo em um dataframe pandas:
1 from datasets import load_dataset 2 import pandas as pd 3 4 data = load_dataset("explodinggradients/ragas-wikiqa", split="train") 5 df = pd.DataFrame(data)
O conjunto de dados tem as seguintes colunas que são importantes para nós:
- pergunta: Perguntas do utilizador
- resposta_correta: Respostas corretas para as perguntas do usuário
- context: Lista de textos de referência para responder às perguntas do usuário
Notamos que os textos de referência na coluna
context
são bem longos. Normalmente para o RAG, textos grandes são divididos em blocos menores no momento da ingestão. Dada uma query do usuário, somente os chunks mais relevantes são recuperados, para passar como contexto para o LLM. Então, como uma próxima etapa, vamos dividir nossos textos de referência antes de incorporá-los e ingeri-los no MongoDB:1 from langchain.text_splitter import RecursiveCharacterTextSplitter 2 3 # Split text by tokens using the tiktoken tokenizer 4 text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder( 5 encoding_name="cl100k_base", keep_separator=False, chunk_size=200, chunk_overlap=30 6 ) 7 8 def split_texts(texts): 9 chunked_texts = [] 10 for text in texts: 11 chunks = text_splitter.create_documents([text]) 12 chunked_texts.extend([chunk.page_content for chunk in chunks]) 13 return chunked_texts 14 15 # Split the context field into chunks 16 df["chunks"] = df["context"].apply(lambda x: split_texts(x)) 17 # Aggregate list of all chunks 18 all_chunks = df["chunks"].tolist() 19 docs = [item for chunk in all_chunks for item in chunk]
O código acima faz o seguinte:
- Define como dividir o texto em chunks: Usamos o método
from_tiktoken_encoder
daRecursiveCharacterTextSplitter
no LangChain. Dessa forma, os textos são divididos por caractere e recursivamente mesclados em tokens pelo tokenizador, desde que o tamanho do bloco (em termos de número de tokens) seja menor que o tamanho especificado do bloco (chunk_size
). Foi demonstrado que alguma sobreposição entre blocos melhora a recuperação, portanto, definimos uma sobreposição de 30 caracteres no parâmetrochunk_overlap
. O parâmetrokeep_separator
indica se deve ou não manter os separadores padrão, como\n\n
,\n
etc. no texto fragmentado, e oencoding_name
indica o modelo a ser usado para gerar tokens. - Define uma função
split_texts
: essa função recebe uma lista de textos de referência (texts
) como entrada, a divide usando o divisor de texto e retorna a lista de textos em chunks. - Aplica a função
split_texts
à colunacontext
de nosso conjunto de dados - Cria uma lista de textos em chunks para todo o conjunto de dados
Na prática, você também pode experimentar diferentes estratégias de fragmentação ao avaliar a recuperação, mas, neste tutorial, estamos nos concentrando apenas em avaliar diferentes modelos de incorporação.
Agora que dividimos nossos documentos de referência, vamos incorporá-los e inseri-los no MongoDB Atlas para criar uma base de conhecimento (armazenamento de vetores) para nosso aplicativo RAG. Como queremos avaliar dois modelos de incorporação para o retriever, criaremos armazenamentos vetoriais separados (coleções) usando cada modelo.
Avaliaremos os modelos de incorporaçãotext-embedding-ada-002 e text-embedding-3-small (vamos chamá-los de ada-002 e 3-small no resto do tutorial) de OpenAI, então primeiro vamos definir uma função para gerar incorporações usando a API de incorporações do OpenAI:
1 def get_embeddings(docs: List[str], model: str) -> List[List[float]]: 2 """ 3 Generate embeddings using the OpenAI API. 4 5 Args: 6 docs (List[str]): List of texts to embed 7 model (str, optional): Model name. Defaults to "text-embedding-3-large". 8 9 Returns: 10 List[float]: Array of embeddings 11 """ 12 # replace newlines, which can negatively affect performance. 13 docs = [doc.replace("\n", " ") for doc in docs] 14 response = openai_client.embeddings.create(input=docs, model=model) 15 response = [r.embedding for r in response.data] 16 return response
A função de incorporação acima recebe uma lista de textos (
docs
) e um nome de modelo (model
) como argumentos e retorna uma lista de incorporações geradas usando o modelo especificado. A API OpenAI retorna uma lista de objetos de incorporação, que precisam ser analisados para obter a lista final de incorporações. Um exemplo de resposta da API se parece com o seguinte:1 { 2 "data": [ 3 { 4 "embedding": [ 5 0.018429679796099663, 6 -0.009457024745643139 7 . 8 . 9 . 10 ], 11 "index": 0, 12 "object": "embedding" 13 } 14 ], 15 "model": "text-embedding-3-small", 16 "object": "list", 17 "usage": { 18 "prompt_tokens": 183, 19 "total_tokens": 183 20 } 21 }
Agora, vamos usar cada modelo para incorporar os textos em blocos e ingeri-los junto com suas incorporações em uma coleção do MongoDB:
1 from pymongo import MongoClient 2 from tqdm.auto import tqdm 3 4 client = MongoClient(MONGODB_URI) 5 DB_NAME = "ragas_evals" 6 db = client[DB_NAME] 7 batch_size = 128 8 9 EVAL_EMBEDDING_MODELS = ["text-embedding-ada-002", "text-embedding-3-small"] 10 11 for model in EVAL_EMBEDDING_MODELS: 12 embedded_docs = [] 13 print(f"Getting embeddings for the {model} model") 14 for i in tqdm(range(0, len(docs), batch_size)): 15 end = min(len(docs), i + batch_size) 16 batch = docs[i:end] 17 # Generate embeddings for current batch 18 batch_embeddings = get_embeddings(batch, model) 19 # Creating the documents to ingest into MongoDB for current batch 20 batch_embedded_docs = [ 21 {"text": batch[i], "embedding": batch_embeddings[i]} 22 for i in range(len(batch)) 23 ] 24 embedded_docs.extend(batch_embedded_docs) 25 print(f"Finished getting embeddings for the {model} model") 26 27 # Bulk insert documents into a MongoDB collection 28 print(f"Inserting embeddings for the {model} model") 29 collection = db[model] 30 collection.delete_many({}) 31 collection.insert_many(embedded_docs) 32 print(f"Finished inserting embeddings for the {model} model")
O código acima faz o seguinte:
- Cria um PyMongo (
client
) para se conectar a um MongoDB Atlas cluster - Especifica o banco de dados (
DB_NAME
) ao qual se conectar — estamos chamando o banco de dados rasas_evals; se o banco de dados não existir, ele será criado no momento da ingestão - Especifica o tamanho do lote (
batch_size
) para gerar embeddings em massa - Especifica os modelos de incorporação (
EVAL_EMBEDDING_MODELS
) a serem usados para gerar incorporações - Para cada modelo de incorporação, gera incorporações para todo o conjunto de avaliação e cria os documentos a serem ingeridos no MongoDB - um exemplo de documento é o seguinte:
1 { 2 "text": "For the purposes of authentication, most countries require commercial or personal documents which originate from or are signed in another country to be notarized before they can be used or officially recorded or before they can have any legal effect.", 3 "embedding": [ 4 0.018429679796099663, 5 -0.009457024745643139, 6 . 7 . 8 . 9 ] 10 }
- Exclui quaisquer documentos existentes na coleção com o nome do modelo e insere em massa os documentos usando o método
insert_many()
Para verificar se o código acima foi executado conforme o esperado, navegue até a UI do Atlas e certifique-se de ver duas coleções, a saber,text-embedding-ada-002 e text-embedding-3-small, no banco de dadosrasgas_evals:
Enquanto estiver na interface do usuário do Atlas, crie índices vetoriais para ambas as coleções. A definição do índice vetorial especifica o caminho para o campo de incorporação, as dimensões e a métrica de similaridade a serem usados ao recuperar documentos usando a pesquisa vetorial. Certifique-se de que o nome do índice seja
vector_index
para cada coleção e que a definição do índice tenha a seguinte aparência:1 { 2 "fields": [ 3 { 4 "numDimensions": 1536, 5 "path": "embedding", 6 "similarity": "cosine", 7 "type": "vector" 8 } 9 ] 10 }
O número de dimensões de incorporação em ambas as definições de índice é 1536 , pois ada-002 e 3-small têm o mesmo número de dimensões.
Como primeira etapa no processo de avaliação, queremos garantir que estamos recuperando o contexto correto para o LLM. Embora existam vários fatores (fragmentação, reclassificação etc.) que podem afetar a recuperação, neste tutorial, faremos apenas experimentos com diferentes modelos de incorporação. Usaremos os mesmos modelos que usamos na etapa 5. Usaremos o LangChain para criar um armazenamento de vetor usando o MongoDB Atlas e o usaremos como um recuperador em nosso aplicativo RAG.
1 from langchain_openai import OpenAIEmbeddings 2 from langchain_mongodb import MongoDBAtlasVectorSearch 3 from langchain_core.vectorstores import VectorStoreRetriever 4 5 def get_retriever(model: str, k: int) -> VectorStoreRetriever: 6 """ 7 Given an embedding model and top k, get a vector store retriever object 8 9 Args: 10 model (str): Embedding model to use 11 k (int): Number of results to retrieve 12 13 Returns: 14 VectorStoreRetriever: A vector store retriever object 15 """ 16 embeddings = OpenAIEmbeddings(model=model) 17 18 vector_store = MongoDBAtlasVectorSearch.from_connection_string( 19 connection_string=MONGODB_URI, 20 namespace=f"{DB_NAME}.{model}", 21 embedding=embeddings, 22 index_name="vector_index", 23 text_key="text", 24 ) 25 26 retriever = vector_store.as_retriever( 27 search_type="similarity", search_kwargs={"k": k} 28 ) 29 return retriever
O código acima define uma função
get_retriever
que usa um modelo de incorporação (model
) e o número de documentos a serem recuperados (k
) como argumentos e retorna um objeto recuperador como saída. A função cria um armazenamento de vetores MongoDB Atlas usando a classeMongoDBAtlasVectorSearch
da integraçãolangchain-mongodb
. Especificamente, ele usa o métodofrom_connection_string
da classe para criar o armazenamento de vetores a partir da string de conexão do MongoDB que obtivemos na Etapa 2 acima. Também requer argumentos adicionais, como:- namespace: A combinação (banco de dados, collection) para utilizar como o armazenamento de vetor
- embedding: Modelo de incorporação a ser usado para gerar a inserção de consulta para recuperação
- index_name: o nome do índice de pesquisa vetorial do MongoDB Atlas (conforme definido na Etapa 5)
- text_key: O campo nos documentos de referência que contém o texto
Por fim, ele usa o método
as_retriever
em LangChain para usar o armazenamento de vetores como um recuperador.as_retriever
pode usar argumentos como search_type
, que especifica a métrica a ser usada para recuperar documentos. Aqui, escolhemos similarity
, pois queremos recuperar os documentos mais semelhantes a uma determinada query. Também podemos especificar argumentos de pesquisa adicionais, como k
, que é o número de documentos a serem recuperados.Para avaliar o retriever, usaremos as métricas
context_precision
e context_recall
da bibliotecaragas. Essas métricas usam o contexto recuperado, as respostas verdadeiras e as perguntas. Então, vamos primeiro reunir a lista de respostas e perguntas verdadeiras:1 QUESTIONS = df["question"].to_list() 2 GROUND_TRUTH = df["correct_answer"].tolist()
O trecho de código acima simplesmente converte as colunas
question
e correct_answer
da estrutura de dados que criamos na etapa 3 em listas. Reutilizaremos essas listas nas etapas a seguir.Finalmente, aqui está o código para avaliar o recuperador:
1 from datasets import Dataset 2 from ragas import evaluate, RunConfig 3 from ragas.metrics import context_precision, context_recall 4 import nest_asyncio 5 6 # Allow nested use of asyncio (used by RAGAS) 7 nest_asyncio.apply() 8 9 for model in EVAL_EMBEDDING_MODELS: 10 data = {"question": [], "ground_truth": [], "contexts": []} 11 data["question"] = QUESTIONS 12 data["ground_truth"] = GROUND_TRUTH 13 14 retriever = get_retriever(model, 2) 15 # Getting relevant documents for the evaluation dataset 16 for i in tqdm(range(0, len(QUESTIONS))): 17 data["contexts"].append( 18 [doc.page_content for doc in retriever.get_relevant_documents(QUESTIONS[i])] 19 ) 20 # RAGAS expects a Dataset object 21 dataset = Dataset.from_dict(data) 22 # RAGAS runtime settings to avoid hitting OpenAI rate limits 23 run_config = RunConfig(max_workers=4, max_wait=180) 24 result = evaluate( 25 dataset=dataset, 26 metrics=[context_precision, context_recall], 27 run_config=run_config, 28 raise_exceptions=False, 29 ) 30 print(f"Result for the {model} model: {result}")
O código acima faz o seguinte para cada um dos modelos que estamos avaliando:
- Cria um dicionário (
data
) comquestion
,ground_truth
econtexts
como chaves, correspondentes às perguntas no conjunto de dados de avaliação, suas respostas de verdade fundamental e contextos recuperados - Cria um
retriever
que recupera os dois documentos principais mais semelhantes a uma determinada query - Usa o método
get_relevant_documents
para obter os documentos mais relevantes para cada pergunta no conjunto de dados da avaliação e adicioná-los à listacontexts
no dicionáriodata
- Converte o dicionário
data
em um objeto Dataset - Cria uma configuração de tempo de execução para RAGAS para substituir suas configurações padrão de simultaneidade e repetição - tivemos que fazer isso para evitar os limites de taxado OpenAI, mas isso pode não ser um problema dependendo do seu nível de uso ou se você não estiver usando modelos OpenAI
- Usa o método
evaluate
da bibliotecaragas para obter as métricas de avaliação geral do conjunto de dados de avaliação
Os resultados da avaliação para incorporar modelos que comparamos são os seguintes em nosso conjunto de dados:
Modelo | Precisão do contexto | Contexto de recuperação |
---|---|---|
ada-002 | 0.9310 | 0.8561 |
3-small | 0.9116 | 0.8826 |
Com base nos números acima, ada-002 é melhor para recuperar os resultados mais relevantes no topo, mas 3-small é melhor para recuperar contextos que estão mais alinhados com as respostas verdadeiras. Portanto, concluímos que 3-small é o melhor modelo de incorporação para recuperação.
Agora que encontramos o melhor modelo para nosso retriever, vamos encontrar o melhor modelo de conclusão para o componente gerador em nosso aplicativo RAG.
Mas primeiro, vamos construir o nosso RAG "application. " Em LangChain, fazemo-lo utilizando cadeias. As cadeias no LangChain são uma sequência de chamadas para um LLM, uma ferramenta ou uma etapa de processamento de dados. Cada componente em uma cadeia é referido como um executável, e a maneira recomendada de compor cadeias é usando a Linguagem de ExpressãoLangChain (LCEL).
1 from langchain_openai import ChatOpenAI 2 from langchain_core.prompts import ChatPromptTemplate 3 from langchain_core.runnables import RunnablePassthrough 4 from langchain_core.runnables.base import RunnableSequence 5 from langchain_core.output_parsers import StrOutputParser 6 7 def get_rag_chain(retriever: VectorStoreRetriever, model: str) -> RunnableSequence: 8 """ 9 Create a basic RAG chain 10 11 Args: 12 retriever (VectorStoreRetriever): Vector store retriever object 13 model (str): Chat completion model to use 14 15 Returns: 16 RunnableSequence: A RAG chain 17 """ 18 # Generate context using the retriever, and pass the user question through 19 retrieve = { 20 "context": retriever 21 | (lambda docs: "\n\n".join([d.page_content for d in docs])), 22 "question": RunnablePassthrough(), 23 } 24 template = """Answer the question based only on the following context: \ 25 {context} 26 27 Question: {question} 28 """ 29 # Defining the chat prompt 30 prompt = ChatPromptTemplate.from_template(template) 31 # Defining the model to be used for chat completion 32 llm = ChatOpenAI(temperature=0, model=model) 33 # Parse output as a string 34 parse_output = StrOutputParser() 35 36 # Naive RAG chain 37 rag_chain = retrieve | prompt | llm | parse_output 38 return rag_chain
No código acima, definimos uma função
get_rag_chain
que usa um objetoretriever
e um nome de modelo de conclusão de chat (model
) como argumentos e retorna uma cadeia RAG como saída. A função cria os seguintes componentes que juntos compõem a cadeia RAG:- ** recuperar **: Recebe a entrada do usuário (uma pergunta) e a envia para o recuperador para obter documentos semelhantes; ele também formata a saída para corresponder ao formato de entrada esperado pelo próximo executável, que, nesse caso, é um dicionário com ** pergunta **
context
e **question
resposta ** como chaves; a chamada **RunnablePassthrough()** para a chave de pergunta indica que a entrada do usuário é simplesmente passada para o próximo estágio na chave de pergunta. - ** prompt: Cria um prompt preenchendo um modelo de prompt com o contexto e a pergunta do estágio de recuperação
- llm: especifica o modelo de bate-papo a ser usado para conclusão
- **parse_output ** : Um analisador de saída simples que analisa o resultado do LLM em uma string
Finalmente, ele cria uma cadeia RAG (
rag_chain
) usando a notação LCEL pipe ( | ) para agrupar os componentes acima.Para modelos de conclusão, avaliaremos a versão atualizada mais recente do gpt-3.5-turbo e uma versão mais antiga do GPT-3.5 HTTP, ou seja, gpt-3.5-turbo-1106. O código de avaliação do gerador é muito semelhante ao que tivemos na etapa 6 , exceto por ter etapas adicionais para inicializar a cadeia RAG e invocá-la para cada pergunta em nosso conjunto de dados de avaliação para gerar respostas:
1 from ragas.metrics import faithfulness, answer_relevancy 2 3 for model in ["gpt-3.5-turbo-1106", "gpt-3.5-turbo"]: 4 data = {"question": [], "ground_truth": [], "contexts": [], "answer": []} 5 data["question"] = QUESTIONS 6 data["ground_truth"] = GROUND_TRUTH 7 # Using the best embedding model from the retriever evaluation 8 retriever = get_retriever("text-embedding-3-small", 2) 9 rag_chain = get_rag_chain(retriever, model) 10 for i in tqdm(range(0, len(QUESTIONS))): 11 question = QUESTIONS[i] 12 data["answer"].append(rag_chain.invoke(question)) 13 data["contexts"].append( 14 [doc.page_content for doc in retriever.get_relevant_documents(question)] 15 ) 16 # RAGAS expects a Dataset object 17 dataset = Dataset.from_dict(data) 18 # RAGAS runtime settings to avoid hitting OpenAI rate limits 19 run_config = RunConfig(max_workers=4, max_wait=180) 20 result = evaluate( 21 dataset=dataset, 22 metrics=[faithfulness, answer_relevancy], 23 run_config=run_config, 24 raise_exceptions=False, 25 ) 26 print(f"Result for the {model} model: {result}")
Algumas alterações a serem observadas no código acima:
- O dicionário
data
tem uma chaveanswer
adicional para acumular respostas às perguntas em nosso conjunto de dados de avaliação. - Usamos o text-embedding-3-small para o recuperador, pois determinamos que esse é o melhor modelo de incorporação na Etapa 6.
- Estamos usando as métricas
faithfulness
eanswer_relevancy
para avaliar o gerador.
Os resultados da avaliação para os modelos de conclusão que comparamos são os seguintes em nosso conjunto de dados:
Modelo | Fidelidade | Relevância da resposta |
---|---|---|
gpt-3.5-turbo | 0.9714 | 0.9087 |
gpt-3.5-turbo-1106 | 0.9671 | 0.9105 |
Com base nos números acima, a versão mais recente do gpt-3.5-turbo produz resultados mais consistentes do que seu antecessor, enquanto a versão mais antiga produz respostas mais pertinentes ao prompt fornecido. Digamos que queremos usar o modelo mais "faithful ".
Se você não quiser escolher entre as métricas, considere criar métricas consolidadas usando uma soma ponderada após o fato ou personalizar os prompts usados para avaliação.
Por fim, vamos avaliar o desempenho geral do sistema usando os modelos de melhor desempenho:
1 from ragas.metrics import answer_similarity, answer_correctness 2 3 data = {"question": [], "ground_truth": [], "answer": []} 4 data["question"] = QUESTIONS 5 data["ground_truth"] = GROUND_TRUTH 6 # Using the best embedding model from the retriever evaluation 7 retriever = get_retriever("text-embedding-3-small", 2) 8 # Using the best completion model from the generator evaluation 9 rag_chain = get_rag_chain(retriever, "gpt-3.5-turbo") 10 for question in tqdm(QUESTIONS): 11 data["answer"].append(rag_chain.invoke(question)) 12 13 dataset = Dataset.from_dict(data) 14 run_config = RunConfig(max_workers=4, max_wait=180) 15 result = evaluate( 16 dataset=dataset, 17 metrics=[answer_similarity, answer_correctness], 18 run_config=run_config, 19 raise_exceptions=False, 20 ) 21 print(f"Overall metrics: {result}")
No código acima, usamos o modelotext-embedding-3-small para o retriever e o gpt-3.5-modelo para o gerador, para gerar respostas a perguntas em nosso conjunto de dados de avaliação. Usamos as métricas
answer_similarity
e answer_correctness
para medir o desempenho geral da cadeia RAG.A avaliação mostra que a cadeia RAG produz uma similaridade de resposta de 0.8873 e uma correção de resposta de 0.5922 em nosso conjunto de dados.
A exatidão parece um pouco baixa, então vamos investigar mais detalhadamente. Você pode converter os resultados do RAGAS para um dataframe do Pandas para realizar análises adicionais:
1 result_df = result.to_pandas() 2 result_df[result_df["answer_correctness"] < 0.7]
Para uma análise mais visual, também é possível criar um mapa de calor de perguntas versus métricas:
1 import seaborn as sns 2 import matplotlib.pyplot as plt 3 4 plt.figure(figsize=(10, 8)) 5 sns.heatmap( 6 result_df[1:10].set_index("question")[["answer_similarity", "answer_correctness"]], 7 annot=True, 8 cmap="flare", 9 ) 10 plt.show()
Ao investigar manualmente alguns dos resultados de baixa pontuação, observamos o seguinte:
- Algumas respostas de verdade no conjunto de dados de avaliação estavam de fato incorretas. Portanto, embora a resposta gerada pelo LLM estivesse correta, ela não correspondia à resposta verdadeira básica, resultando em uma pontuação baixa.
- Algumas respostas verdadeiras eram frases completas, enquanto a resposta gerada pelo LLM, embora factualmente correta, era uma única palavra, número, etc.
As resultados acima enfatizam a importância de verificar pontualmente as avaliações LLM, selecionando conjuntos de dados de avaliação precisos e representativos, e destacam ainda outro desafio com o uso de LLMs para avaliação.
A avaliação não deve ser um evento único. Cada vez que você deseja alterar um componente no sistema, você deve avaliar as alterações em relação às configurações existentes para avaliar como elas afetarão o desempenho. Então, depois que o aplicativo for implantado na produção, você também deverá ter uma maneira de monitorar o desempenho em tempo real e detectar alterações nele.
Neste tutorial, usamos o MongoDB Atlas como banco de dados vetorial para nosso aplicativo RAG. Você também pode usar o Atlas para monitorar o desempenho do seu aplicativo LLM por meio de Atlas Charts. Tudo que você precisa fazer é gravar os resultados da avaliação e quaisquer métricas de feedback (por exemplo, número de thumbs up, thumbs down, regenerações de respostas etc.) que você deseja rastrear em uma collection do MongoDB:
1 from datetime import datetime 2 3 result["timestamp"] = datetime.now() 4 collection = db["metrics"] 5 collection.insert_one(result)
No trecho de código acima, adicionamos um campo
timestamp
contendo o carimbo de data/hora atual ao resultado da avaliação final (result
) da etapa 8 e o escrevemos em uma collection chamada métricas no banco de dadosrasas_evals usando o insert_one
do PyMongo método. O dicionárioresult
inserido no MongoDB tem a seguinte aparência:1 { 2 "answer_similarity": 0.8873, 3 "answer_correctness": 0.5922, 4 "timestamp": 2024-04-07T23:27:30.655+00:00 5 }
Agora podemos criar um dashboard no Atlas Charts para visualizar os dados na collection demétricas:
Após a criação do dashboard, clique no botão Adicionar Gráfico e selecione a collection demétricas como fonte de dados para o gráfico. Arraste e solte os campos a serem incluídos, escolha um tipo de gráfico, adicione um título e uma descrição para o gráfico e salve-o no dashboard:
Veja como é o nosso painel de amostra:
Da mesma forma, quando seu aplicativo estiver em produção, você poderá criar um dashboard para qualquer métrica de feedback coletada.
Neste tutorial, examinamos alguns dos desafios da avaliação de aplicativos LLM, seguidos por um fluxo de trabalho passo a passo detalhado para avaliar um aplicativo LLM, incluindo a persistência e o rastreamento dos resultados da avaliação ao longo do tempo. Embora tenhamos usado RAG como nosso exemplo para avaliação, os conceitos e técnicas mostrados neste tutorial podem ser estendidos a outros aplicativos LLM, incluindo agentes.
Agora que você tem uma boa base sobre como avaliar aplicativos RAG, pode encarar como um desafio avaliar sistemas RAG a partir de alguns de nossos outros tutoriais:
Se você tiver mais dúvidas sobre as avaliações do LLM, entre em contato conosco em nossos fóruns da comunidade de IA generativa e fique atento ao próximo tutorial da série RAG. Os tutoriais anteriores da série podem ser encontrados abaixo:
Se quiser saber mais sobre como avaliar aplicações LLM, confira as seguintes referências:
- Yan, Ziyou. (outubro 2023). AI Engineer Summit - Blocos de construção para sistemas e produtos LLM. eugeneyan. com. https://eugeneyan.com/peak/ai-eng-summit/
- Sim, Ziyou. (2024 de março). Avaliações específicas de tarefas do LLM que fazem & não funcionam. eugeneyan.com. https://eugeneyan.com/writing/evals/
- Yan, Ziyou. (julho 2023). Padrões para construção de sistemas e produtos baseados em LLM. eugeneyan. com. https://eugeneyan.com/writing/llm-patterns/
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.