Valutazione con Arize Pheonix
Questa guida mostra come utilizzare Arize Pheonix per valutare una pipeline Retrieval-Augmented Generation (RAG) costruita su Milvus.
Il sistema RAG combina un sistema di recupero con un modello generativo per generare nuovo testo sulla base di un prompt dato. Il sistema recupera prima i documenti rilevanti da un corpus utilizzando Milvus e poi utilizza un modello generativo per generare nuovo testo sulla base dei documenti recuperati.
Arize Pheonix è un framework che aiuta a valutare le pipeline RAG. Esistono strumenti e framework che aiutano a costruire queste pipeline, ma valutarle e quantificarne le prestazioni può essere difficile. È qui che entra in gioco Arize Pheonix.
Prerequisiti
Prima di eseguire questo notebook, assicuratevi di avere installato le seguenti dipendenze:
$ pip install --upgrade pymilvus openai requests tqdm pandas "arize-phoenix>=4.29.0" nest_asyncio
Se si utilizza Google Colab, per abilitare le dipendenze appena installate potrebbe essere necessario riavviare il runtime (fare clic sul menu "Runtime" nella parte superiore dello schermo e selezionare "Restart session" dal menu a discesa).
In questo esempio utilizzeremo OpenAI come LLM. È necessario preparare la chiave api OPENAI_API_KEY
come variabile d'ambiente.
import os
# os.environ["OPENAI_API_KEY"] = "sk-*****************"
Definire la pipeline RAG
Definiamo la classe RAG che utilizza Milvus come archivio vettoriale e OpenAI come LLM. La classe contiene il metodo load
, che carica i dati di testo in Milvus, il metodo retrieve
, che recupera i dati di testo più simili alla domanda data, e il metodo answer
, che risponde alla domanda data con la conoscenza recuperata.
from typing import List
from tqdm import tqdm
from openai import OpenAI
from pymilvus import MilvusClient
class RAG:
"""
RAG(Retrieval-Augmented Generation) class built upon OpenAI and Milvus.
"""
def __init__(self, openai_client: OpenAI, milvus_client: MilvusClient):
self._prepare_openai(openai_client)
self._prepare_milvus(milvus_client)
def _emb_text(self, text: str) -> List[float]:
return (
self.openai_client.embeddings.create(input=text, model=self.embedding_model)
.data[0]
.embedding
)
def _prepare_openai(
self,
openai_client: OpenAI,
embedding_model: str = "text-embedding-3-small",
llm_model: str = "gpt-4o-mini",
):
self.openai_client = openai_client
self.embedding_model = embedding_model
self.llm_model = llm_model
self.SYSTEM_PROMPT = """
Human: You are an AI assistant. You are able to find answers to the questions from the contextual passage snippets provided.
"""
self.USER_PROMPT = """
Use the following pieces of information enclosed in <context> tags to provide an answer to the question enclosed in <question> tags.
<context>
{context}
</context>
<question>
{question}
</question>
"""
def _prepare_milvus(
self, milvus_client: MilvusClient, collection_name: str = "rag_collection"
):
self.milvus_client = milvus_client
self.collection_name = collection_name
if self.milvus_client.has_collection(self.collection_name):
self.milvus_client.drop_collection(self.collection_name)
embedding_dim = len(self._emb_text("demo"))
self.milvus_client.create_collection(
collection_name=self.collection_name,
dimension=embedding_dim,
metric_type="IP",
consistency_level="Strong",
)
def load(self, texts: List[str]):
"""
Load the text data into Milvus.
"""
data = []
for i, line in enumerate(tqdm(texts, desc="Creating embeddings")):
data.append({"id": i, "vector": self._emb_text(line), "text": line})
self.milvus_client.insert(collection_name=self.collection_name, data=data)
def retrieve(self, question: str, top_k: int = 3) -> List[str]:
"""
Retrieve the most similar text data to the given question.
"""
search_res = self.milvus_client.search(
collection_name=self.collection_name,
data=[self._emb_text(question)],
limit=top_k,
search_params={"metric_type": "IP", "params": {}}, # inner product distance
output_fields=["text"], # Return the text field
)
retrieved_texts = [res["entity"]["text"] for res in search_res[0]]
return retrieved_texts[:top_k]
def answer(
self,
question: str,
retrieval_top_k: int = 3,
return_retrieved_text: bool = False,
):
"""
Answer the given question with the retrieved knowledge.
"""
retrieved_texts = self.retrieve(question, top_k=retrieval_top_k)
user_prompt = self.USER_PROMPT.format(
context="\n".join(retrieved_texts), question=question
)
response = self.openai_client.chat.completions.create(
model=self.llm_model,
messages=[
{"role": "system", "content": self.SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
)
if not return_retrieved_text:
return response.choices[0].message.content
else:
return response.choices[0].message.content, retrieved_texts
Inizializziamo la classe RAG con i client OpenAI e Milvus.
openai_client = OpenAI()
milvus_client = MilvusClient(uri="./milvus_demo.db")
my_rag = RAG(openai_client=openai_client, milvus_client=milvus_client)
Per quanto riguarda l'argomento di MilvusClient
:
- L'impostazione di
uri
come file locale, ad esempio./milvus.db
, è il metodo più conveniente, poiché utilizza automaticamente Milvus Lite per memorizzare tutti i dati in questo file. - Se si dispone di una grande quantità di dati, è possibile configurare un server Milvus più performante su docker o kubernetes. In questa configurazione, utilizzare l'uri del server, ad esempio
http://localhost:19530
, comeuri
. - Se si desidera utilizzare Zilliz Cloud, il servizio cloud completamente gestito per Milvus, regolare
uri
etoken
, che corrispondono all'endpoint pubblico e alla chiave Api di Zilliz Cloud.
Eseguire la pipeline RAG e ottenere i risultati
Utilizziamo la guida allo sviluppo di Milvus come conoscenza privata nel nostro RAG, che è una buona fonte di dati per una semplice pipeline RAG.
Scaricarla e caricarla nella pipeline RAG.
import urllib.request
import os
url = "https://raw.githubusercontent.com/milvus-io/milvus/master/DEVELOPMENT.md"
file_path = "./Milvus_DEVELOPMENT.md"
if not os.path.exists(file_path):
urllib.request.urlretrieve(url, file_path)
with open(file_path, "r") as file:
file_text = file.read()
text_lines = file_text.split("# ")
my_rag.load(text_lines)
Creating embeddings: 100%|██████████| 47/47 [00:12<00:00, 3.84it/s]
Definiamo una domanda di interrogazione sul contenuto della documentazione della guida allo sviluppo. Quindi utilizziamo il metodo answer
per ottenere la risposta e i testi contestuali recuperati.
question = "what is the hardware requirements specification if I want to build Milvus and run from source code?"
my_rag.answer(question, return_retrieved_text=True)
('The hardware requirements specification to build and run Milvus from source code are:\n\n- 8GB of RAM\n- 50GB of free disk space',
['Hardware Requirements\n\nThe following specification (either physical or virtual machine resources) is recommended for Milvus to build and run from source code.\n\n```\n- 8GB of RAM\n- 50GB of free disk space\n```\n\n##',
'Building Milvus on a local OS/shell environment\n\nThe details below outline the hardware and software requirements for building on Linux and MacOS.\n\n##',
"Software Requirements\n\nAll Linux distributions are available for Milvus development. However a majority of our contributor worked with Ubuntu or CentOS systems, with a small portion of Mac (both x86_64 and Apple Silicon) contributors. If you would like Milvus to build and run on other distributions, you are more than welcome to file an issue and contribute!\n\nHere's a list of verified OS types where Milvus can successfully build and run:\n\n- Debian/Ubuntu\n- Amazon Linux\n- MacOS (x86_64)\n- MacOS (Apple Silicon)\n\n##"])
Ora prepariamo alcune domande con le corrispondenti risposte di verità. Otteniamo le risposte e i contesti dalla nostra pipeline RAG.
from datasets import Dataset
import pandas as pd
question_list = [
"what is the hardware requirements specification if I want to build Milvus and run from source code?",
"What is the programming language used to write Knowhere?",
"What should be ensured before running code coverage?",
]
ground_truth_list = [
"If you want to build Milvus and run from source code, the recommended hardware requirements specification is:\n\n- 8GB of RAM\n- 50GB of free disk space.",
"The programming language used to write Knowhere is C++.",
"Before running code coverage, you should make sure that your code changes are covered by unit tests.",
]
contexts_list = []
answer_list = []
for question in tqdm(question_list, desc="Answering questions"):
answer, contexts = my_rag.answer(question, return_retrieved_text=True)
contexts_list.append(contexts)
answer_list.append(answer)
df = pd.DataFrame(
{
"question": question_list,
"contexts": contexts_list,
"answer": answer_list,
"ground_truth": ground_truth_list,
}
)
rag_results = Dataset.from_pandas(df)
df
/Users/eureka/miniconda3/envs/zilliz/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
from .autonotebook import tqdm as notebook_tqdm
Answering questions: 100%|██████████| 3/3 [00:03<00:00, 1.04s/it]
domanda | contesti | risposta | verità_terra | |
---|---|---|---|---|
0 | quali sono le specifiche dei requisiti hardware... | [Requisiti hardware] Le seguenti specifiche... | Le specifiche dei requisiti hardware per... | Se si vuole costruire Milvus ed eseguirlo da sorgente... |
1 | Qual è il linguaggio di programmazione usato per scrivere... | [CMake & Conan] La libreria di algoritmi di Milvus... | Il linguaggio di programmazione utilizzato per scrivere Knowher... | Il linguaggio di programmazione utilizzato per scrivere Knowher... |
2 | Che cosa si deve garantire prima di eseguire la copertura del codice... | [Copertura del codice] Prima di inviare il pull ... | Prima di eseguire la copertura del codice, occorre assicurarsi ... | Prima di eseguire la copertura del codice, è necessario ... |
Valutazione con Arize Phoenix
Utilizziamo Arize Phoenix per valutare la nostra pipeline RAG (retrieval-augmented generation), concentrandoci su due metriche chiave:
Valutazione delle allucinazioni: Determina se il contenuto è reale o allucinatorio (informazioni non fondate sul contesto), garantendo l'integrità dei dati.
- Spiegazione dell'allucinazione: Spiega perché una risposta è fattuale o meno.
Valutazione AQ: Valuta l'accuratezza delle risposte del modello alle richieste di input.
- Spiegazione AQ: Spiega perché una risposta è corretta o meno.
Panoramica sul tracciamento di Phoenix
Phoenix fornisce un tracing compatibile con OTEL per le applicazioni LLM, con integrazioni per framework come Langchain, LlamaIndex e SDK come OpenAI e Mistral. Il tracciamento cattura l'intero flusso di richieste, offrendo approfondimenti su:
- Latenza dell'applicazione: Identificare e ottimizzare le invocazioni LLM lente e le prestazioni dei componenti.
- Utilizzo dei token: Disaggregazione del consumo di token per l'ottimizzazione dei costi.
- Eccezioni di runtime: Cattura i problemi critici come il rate-limiting.
- Documenti recuperati: Analizzare il recupero dei documenti, il punteggio e l'ordine.
Utilizzando il tracing di Phoenix, è possibile identificare i colli di bottiglia, ottimizzare le risorse e garantire l'affidabilità del sistema in diversi framework e linguaggi.
import phoenix as px
from phoenix.trace.openai import OpenAIInstrumentor
# To view traces in Phoenix, you will first have to start a Phoenix server. You can do this by running the following:
session = px.launch_app()
# Initialize OpenAI auto-instrumentation
OpenAIInstrumentor().instrument()
🌍 To view the Phoenix app in your browser, visit http://localhost:6006/
📖 For more information on how to use Phoenix, check out https://docs.arize.com/phoenix
Testo Alt
import nest_asyncio
from phoenix.evals import HallucinationEvaluator, OpenAIModel, QAEvaluator, run_evals
nest_asyncio.apply() # This is needed for concurrency in notebook environments
# Set your OpenAI API key
eval_model = OpenAIModel(model="gpt-4o")
# Define your evaluators
hallucination_evaluator = HallucinationEvaluator(eval_model)
qa_evaluator = QAEvaluator(eval_model)
# We have to make some minor changes to our dataframe to use the column names expected by our evaluators
# for `hallucination_evaluator` the input df needs to have columns 'output', 'input', 'context'
# for `qa_evaluator` the input df needs to have columns 'output', 'input', 'reference'
df["context"] = df["contexts"]
df["reference"] = df["contexts"]
df.rename(columns={"question": "input", "answer": "output"}, inplace=True)
assert all(
column in df.columns for column in ["output", "input", "context", "reference"]
)
# Run the evaluators, each evaluator will return a dataframe with evaluation results
# We upload the evaluation results to Phoenix in the next step
hallucination_eval_df, qa_eval_df = run_evals(
dataframe=df,
evaluators=[hallucination_evaluator, qa_evaluator],
provide_explanation=True,
)
run_evals |██████████| 6/6 (100.0%) | ⏳ 00:03<00:00 | 1.64it/s
results_df = df.copy()
results_df["hallucination_eval"] = hallucination_eval_df["label"]
results_df["hallucination_explanation"] = hallucination_eval_df["explanation"]
results_df["qa_eval"] = qa_eval_df["label"]
results_df["qa_explanation"] = qa_eval_df["explanation"]
results_df.head()
input | contesti | uscita | verità_di_terra | contesto | riferimento | allucinazione_eval | spiegazione_allucinazione | qa_eval | qa_spiegazione | |
---|---|---|---|---|---|---|---|---|---|---|
0 | quali sono i requisiti hardware specificati... | [Requisiti hardware] Le seguenti specifiche... | Le specifiche dei requisiti hardware per... | Se si vuole costruire Milvus ed eseguirlo da sorgente... | [Requisiti hardware] Le seguenti specifiche... | [Requisiti hardware] Le seguenti specifiche... | fattuale | Per determinare se la risposta è fatt... | corretta | Per determinare se la risposta è corretta, abbiamo bisogno di... |
1 | Qual è il linguaggio di programmazione utilizzato per scrivere... | [CMake e Conan] La libreria di algoritmi di Mil... | Il linguaggio di programmazione utilizzato per scrivere Knowher... | Il linguaggio di programmazione utilizzato per scrivere Knowher... | [CMake & Conan] La libreria di algoritmi di Mil... | [CMake & Conan] La libreria di algoritmi di Mil... | fattuale | Per determinare se la risposta è fatt... | corretto | Per determinare se la risposta è corretta, abbiamo bisogno... |
2 | Di cosa ci si deve assicurare prima di eseguire ... | [Copertura del codice] Prima di inviare il pull ... | Prima di eseguire la copertura del codice, bisogna assicurarsi ... | Prima di eseguire la copertura del codice, si dovrebbe fare ... | [Prima di inviare il pull ... | [Prima di inviare il pull... | fattuale | Il testo di riferimento specifica che prima di... | corretto | Per determinare se la risposta è corretta, abbiamo bisogno di... |