
如何快速實(shí)現(xiàn)REST API集成以?xún)?yōu)化業(yè)務(wù)流程
在本指南中,我們將構(gòu)建一個(gè)基于 RAG 的 LLM 應(yīng)用程序,我們將在其中整合外部數(shù)據(jù)源以增強(qiáng) LLM 的功能。具體來(lái)說(shuō),我們將構(gòu)建一個(gè)可以回答有關(guān) Ray (一個(gè)用于生產(chǎn)和擴(kuò)展 ML 工作負(fù)載的 Python 框架)問(wèn)題的助手。
這里的目標(biāo)是讓開(kāi)發(fā)人員更容易采用 Ray,而且正如我們將在本指南中看到的那樣,幫助改進(jìn)我們的 Ray 文檔本身并為其他 LLM 應(yīng)用程序提供基礎(chǔ)。我們還將分享我們?cè)诖诉^(guò)程中遇到的挑戰(zhàn)以及我們?nèi)绾慰朔@些挑戰(zhàn)。
注意:我們已經(jīng)概括了整個(gè)指南,以便可以輕松擴(kuò)展它以在您自己的數(shù)據(jù)之上構(gòu)建基于 RAG 的 LLM 應(yīng)用程序。
除了構(gòu)建我們的 LLM 應(yīng)用程序之外,我們還將專(zhuān)注于擴(kuò)展和在生產(chǎn)中提供服務(wù)。與傳統(tǒng)機(jī)器學(xué)習(xí)甚至監(jiān)督式深度學(xué)習(xí)不同,從一開(kāi)始,規(guī)模就是 LLM 應(yīng)用程序的瓶頸。大型數(shù)據(jù)集、模型、計(jì)算密集型工作負(fù)載、服務(wù)要求等。隨著我們周?chē)氖澜绮粩喟l(fā)展,我們將開(kāi)發(fā)能夠處理任何規(guī)模的應(yīng)用程序。
我們還將專(zhuān)注于評(píng)估和性能。我們的應(yīng)用程序涉及許多可變的部分:嵌入模型、分塊邏輯、LLM 本身等,因此,重要的是我們要嘗試不同的配置以?xún)?yōu)化最佳質(zhì)量響應(yīng)。但是,評(píng)估和定量比較生成任務(wù)的不同配置并非易事。我們將分解應(yīng)用程序各個(gè)部分的評(píng)估(給定查詢(xún)的檢索、給定源的生成),還評(píng)估整體性能(端到端生成)并分享優(yōu)化配置的結(jié)果。
注意:在本指南中,我們將嘗試使用不同的 LLM(OpenAI、Llama 等)。您需要 OpenAI 憑據(jù)才能訪問(wèn) ChatGPT 模型和 Anyscale Endpoints(可用的公共和私有終端)來(lái)提供 + 微調(diào) OSS LLM。
OpenAI 憑據(jù):https://platform.openai.com/account/api-keysAnyscale Endpoints:https://www.anyscale.com/
在開(kāi)始構(gòu)建 RAG 應(yīng)用程序之前,我們需要首先創(chuàng)建包含已處理數(shù)據(jù)源的向量數(shù)據(jù)庫(kù)。
我們將首先將 Ray 文檔從網(wǎng)站加載到本地目錄:Ray 文檔:https://docs.ray.io/en/master/?
export EFS_DIR=/desired/output/directory
wget -e robots=off --recursive --no-clobber --page-requisites \
--html-extension --convert-links --restrict-file-names=windows \
--domains docs.ray.io --no-parent --accept=html \
-P $EFS_DIR https://docs.ray.io/en/master/
然后,我們將把文檔內(nèi)容加載到 Ray 數(shù)據(jù)集中,以便可以對(duì)它們執(zhí)行大規(guī)模操作(例如嵌入、索引等)。對(duì)于大型數(shù)據(jù)源、模型和應(yīng)用程序服務(wù)需求,規(guī)模是 LLM 應(yīng)用程序的首要任務(wù)。我們希望以這樣的方式構(gòu)建我們的應(yīng)用程序,使它們能夠隨著我們的需求增長(zhǎng)而擴(kuò)展,而無(wú)需我們稍后更改代碼。Ray 數(shù)據(jù)集:https://docs.ray.io/en/latest/data/data.html?
# Ray dataset
DOCS_DIR = Path(EFS_DIR, "docs.ray.io/en/master/")
ds = ray.data.from_items([{"path": path} for path in DOCS_DIR.rglob("*.html")
if not path.is_dir()])
print(f"{ds.count()} documents")
現(xiàn)在我們有了包含所有 html 文件路徑的數(shù)據(jù)集,我們將開(kāi)發(fā)一些可以適當(dāng)?shù)貜倪@些文件中提取內(nèi)容的函數(shù)。我們希望以一種通用的方式來(lái)執(zhí)行此操作,以便我們可以在所有文檔頁(yè)面中執(zhí)行此提取(這樣您就可以將其用于您自己的數(shù)據(jù)源)。我們的流程是首先識(shí)別 html 頁(yè)面中的章節(jié),然后提取它們之間的文本。我們將所有這些保存到一個(gè)字典列表中,該字典將章節(jié)內(nèi)的文本映射到具有章節(jié)錨點(diǎn) ID 的特定 url。
sample_html_fp = Path(EFS_DIR, "docs.ray.io/en/master/rllib/rllib-env.html")
extract_sections({"path": sample_html_fp})[0]
{'source': 'https://docs.ray.io/en/master/rllib/rllib-env.html#environments', 'text': '\nEnvironments#\nRLlib works with several different types of environments, including Farama-Foundation Gymnasium, user-defined, multi-agent, and also batched environments.\nTip\nNot all environments work with all algorithms. Check out the algorithm overview for more information.\n'}
我們可以使用 Ray Data 的 flat_map 僅用一行代碼將此提取過(guò)程(extract_section)并行應(yīng)用于數(shù)據(jù)集中的所有文件路徑。 flat_map:https://docs.ray.io/en/latest/data/api/doc/ray.data.Dataset.flat_map.html?
# Extract sections
sections_ds = ds.flat_map(extract_sections)
sections = sections_ds.take_all()
print (len(sections))
我們現(xiàn)在有了一個(gè)章節(jié)列表(包含每個(gè)章節(jié)的文本和來(lái)源),但我們現(xiàn)在還不應(yīng)該直接將其用作 RAG 應(yīng)用程序的上下文。每個(gè)章節(jié)的文本長(zhǎng)度各不相同,而且很多都是相當(dāng)大的塊。
如果我們使用這些大段文本,那么我們就會(huì)插入大量嘈雜/不需要的上下文,而且由于所有 LLM 都有最大上下文長(zhǎng)度,我們無(wú)法容納太多其他相關(guān)上下文。因此,我們將把每個(gè)部分中的文本拆分成較小的塊。直觀地說(shuō),較小的塊將封裝單個(gè)/幾個(gè)概念,與較大的塊相比,噪聲較少。我們現(xiàn)在將選擇一些典型的文本拆分值(例如,chunk_size=300)來(lái)創(chuàng)建我們的塊,但稍后我們將嘗試使用更廣泛的值。
from langchain.document_loaders import ReadTheDocsLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
# Text splitter
chunk_size = 300
chunk_overlap = 50
text_splitter = RecursiveCharacterTextSplitter(
separators=["\n\n", "\n", " ", ""],
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
)
# Chunk a sample section
sample_section = sections_ds.take(1)[0]
chunks = text_splitter.create_documents(
texts=[sample_section["text"]],
metadatas=[{"source": sample_section["source"]}])
print (chunks[0])
page_content='ray.tune.TuneConfig.search_alg#\nTuneConfig.search_alg: Optional[Union[ray.tune.search.searcher.Searcher, ray.tune.search.search_algorithm.SearchAlgorithm]] = None#' metadata={'source': 'https://docs.ray.io/en/master/tune/api/doc/ray.tune.TuneConfig.search_alg.html#ray-tune-tuneconfig-search-alg'}
雖然對(duì)數(shù)據(jù)集進(jìn)行分塊相對(duì)較快,但讓我們將分塊邏輯包裝到一個(gè)函數(shù)中,以便我們可以大規(guī)模應(yīng)用工作負(fù)載,從而使分塊速度與數(shù)據(jù)源的增長(zhǎng)一樣快:
def chunk_section(section, chunk_size, chunk_overlap):
text_splitter = RecursiveCharacterTextSplitter(
separators=["\n\n", "\n", " ", ""],
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len)
chunks = text_splitter.create_documents(
texts=[sample_section["text"]],
metadatas=[{"source": sample_section["source"]}])
return [{"text": chunk.page_content, "source": chunk.metadata["source"]} for chunk in chunks]
# Scale chunking
chunks_ds = sections_ds.flat_map(partial(
chunk_section,
chunk_size=chunk_size,
chunk_overlap=chunk_overlap))
print(f"{chunks_ds.count()} chunks")
chunks_ds.show(1)
5727 chunks
{'text': 'ray.tune.TuneConfig.search_alg#\nTuneConfig.search_alg: Optional[Union[ray.tune.search.searcher.Searcher, ray.tune.search.search_algorithm.SearchAlgorithm]] = None#', 'source': 'https://docs.ray.io/en/master/tune/api/doc/ray.tune.TuneConfig.search_alg.html#ray-tune-tuneconfig-search-alg'}
嵌入數(shù)據(jù)
現(xiàn)在我們已經(jīng)從各個(gè)部分創(chuàng)建了小塊,我們需要一種方法來(lái)識(shí)別與給定查詢(xún)最相關(guān)的塊。一種非常有效且快速的方法是使用預(yù)訓(xùn)練模型嵌入我們的數(shù)據(jù),并使用相同的模型嵌入查詢(xún)。然后,我們可以計(jì)算所有塊嵌入和我們的查詢(xún)嵌入之間的距離,以確定前 k 個(gè)塊。有許多不同的預(yù)訓(xùn)練模型可供選擇來(lái)嵌入我們的數(shù)據(jù),但最受歡迎的模型可以通過(guò) HuggingFace 的海量文本嵌入基準(zhǔn) (MTEB) 排行榜發(fā)現(xiàn)。這些模型通過(guò)諸如下一個(gè)/掩碼標(biāo)記預(yù)測(cè)等任務(wù)在非常大的文本語(yǔ)料庫(kù)上進(jìn)行了預(yù)訓(xùn)練,這使它們能夠?qū)W習(xí)在 N 維中表示子標(biāo)記并捕獲語(yǔ)義關(guān)系。我們可以利用這一點(diǎn)來(lái)表示我們的數(shù)據(jù)并確定用于回答給定查詢(xún)的最相關(guān)上下文。我們使用 Langchain 的嵌入包裝器(HuggingFaceEmbeddings 和 OpenAIEmbeddings)輕松加載模型并嵌入我們的文檔塊。
注意:嵌入并不是確定更相關(guān)塊的唯一方法。我們也可以使用 LLM 來(lái)決定!但是,由于 LLM 比這些嵌入模型大得多,并且具有最大上下文長(zhǎng)度,因此最好使用嵌入來(lái)檢索前 k 個(gè)塊。然后,我們可以在較少的 k 個(gè)塊上使用 LLM 來(lái)確定要用作上下文來(lái)回答查詢(xún)的 <k 個(gè)塊。我們還可以使用重新排名(例如 Cohere Rerank)來(lái)進(jìn)一步確定要使用的最相關(guān)塊。我們還可以將嵌入與傳統(tǒng)的信息檢索方法(例如關(guān)鍵字匹配)相結(jié)合,這對(duì)于匹配嵌入子標(biāo)記時(shí)可能丟失的唯一標(biāo)記很有用。
HuggingFace 的海量文本嵌入基準(zhǔn):
https://huggingface.co/spaces/mteb/leaderboard
HuggingFaceEmbeddings:
OpenAIEmbeddings:
Cohere Rerank:
https://txt.cohere.com/rerank/
from langchain.embeddings import OpenAIEmbeddings
from langchain.embeddings.huggingface import HuggingFaceEmbeddings
import numpy as np
from ray.data import ActorPoolStrategy
class EmbedChunks:
def __init__(self, model_name):
if model_name == "text-embedding-ada-002":
self.embedding_model = OpenAIEmbeddings(
model=model_name,
openai_api_base=os.environ["OPENAI_API_BASE"],
openai_api_key=os.environ["OPENAI_API_KEY"])
else:
self.embedding_model = HuggingFaceEmbeddings(
model_name=model_name,
model_kwargs={"device": "cuda"},
encode_kwargs={"device": "cuda", "batch_size": 100})
def __call__(self, batch):
embeddings = self.embedding_model.embed_documents(batch["text"])
return {"text": batch["text"], "source": batch["source"], "embeddings":
embeddings}
在這里,我們能夠使用 map_batches 按比例嵌入我們的塊。我們所要做的就是定義 batch_size 和計(jì)算(我們使用兩個(gè)工作器,每個(gè)工作器有 1 個(gè) GPU)。
# Embed chunks
embedding_model_name = "thenlper/gte-base"
embedded_chunks = chunks_ds.map_batches(
EmbedChunks,
fn_constructor_kwargs={"model_name": embedding_model_name},
batch_size=100,
num_gpus=1,
compute=ActorPoolStrategy(size=2))
# Sample (text, source, embedding) triplet
[{'text': 'External library integrations for Ray Tune#',
'source': 'https://docs.ray.io/en/master/tune/api/integration.html#external-library-integrations-for-ray-tune',
'embeddings': [
0.012108353897929192,
0.009078810922801495,
0.030281754210591316,
-0.0029687234200537205,
…]
}
現(xiàn)在我們有了嵌入的塊,我們需要將它們索引(存儲(chǔ))到某個(gè)地方,以便我們可以快速檢索它們進(jìn)行推理。雖然有許多流行的向量數(shù)據(jù)庫(kù)選項(xiàng),但我們將使用 Postgres 和 pgvector,因?yàn)樗?jiǎn)單且性能好。我們將創(chuàng)建一個(gè)表(文檔)并為每個(gè)嵌入的塊寫(xiě)入(文本、源、嵌入)三元組。Postgres 和 pgvector:https://github.com/pgvector/pgvector
class StoreResults:
def __call__(self, batch):
with psycopg.connect(os.environ["DB_CONNECTION_STRING"]) as conn:
register_vector(conn)
with conn.cursor() as cur:
for text, source, embedding in zip
(batch["text"], batch["source"], batch["embeddings"]):
cur.execute("INSERT INTO document (text, source, embedding)
VALUES (%s, %s, %s)", (text, source, embedding,),)
return {}
再次,我們可以利用 Ray Data map_batches 來(lái)并行執(zhí)行此索引:
map_batches :
https://docs.ray.io/en/latest/data/api/doc/ray.data.Dataset.map_batches.html?
# Index data
embedded_chunks.map_batches(
StoreResults,
batch_size=128,
num_cpus=1,
compute=ActorPoolStrategy(size=28),
).count()
在我們的向量數(shù)據(jù)庫(kù)中索引了嵌入的塊后,我們就可以針對(duì)給定的查詢(xún)執(zhí)行檢索了。首先,我們將使用與嵌入文本塊相同的嵌入模型來(lái)嵌入傳入的查詢(xún)。
# Embed query
embedding_model = HuggingFaceEmbeddings(model_name=embedding_model_name)
query = "What is the default batch size for map_batches?"
embedding = np.array(embedding_model.embed_query(query))
len(embedding)
768
然后,我們將通過(guò)提取與我們的嵌入式查詢(xún)最接近的嵌入塊來(lái)檢索前 k 個(gè)最相關(guān)的塊。我們使用余弦距離 (<=>),但有很多選項(xiàng)(https://github.com/pgvector/pgvector#vector-operators)可供選擇。一旦我們檢索到前 num_chunks,我們就可以收集每個(gè)塊的文本并將其用作上下文來(lái)生成響應(yīng)。
# Get context
num_chunks = 5
with psycopg.connect(os.environ["DB_CONNECTION_STRING"]) as conn:
register_vector(conn)
with conn.cursor() as cur:
cur.execute("SELECT * FROM document ORDER BY embedding <-> %s LIMIT %s", (embedding, num_chunks))
rows = cur.fetchall()
context = [{"text": row[1]} for row in rows]
sources = [row[2] for row in rows]
https://docs.ray.io/en/master/data/api/doc/ray.data.Dataset.map_batches.html#ray-data-dataset-map-batches
entire blocks as batches (blocks may contain different numbers of rows).
The actual size of the batch provided to fn may be smaller than
batch_size if batch_size doesn’t evenly divide the block(s) sent
to a given map task. Default batch_size is 4096 with “default”.
https://docs.ray.io/en/master/data/transforming-data.html#configuring-batch-size
The default batch size depends on your resource type. If you’re using CPUs,
the default batch size is 4096. If you’re using GPUs, you must specify an explicit batch size.
(cont…)
我們可以將所有這些組合成一個(gè)方便的函數(shù):
def semantic_search(query, embedding_model, k):
embedding = np.array(embedding_model.embed_query(query))
with psycopg.connect(os.environ["DB_CONNECTION_STRING"]) as conn:
register_vector(conn)
with conn.cursor() as cur:
cur.execute("SELECT * FROM document ORDER BY embedding <=> %s LIMIT %s", (embedding, k),)
rows = cur.fetchall()
semantic_context = [{"id": row[0], "text": row[1], "source": row[2]} for row in rows]
return semantic_context
現(xiàn)在,我們可以使用上下文從 LLM 生成響應(yīng)。如果沒(méi)有檢索到的相關(guān)上下文,LLM 可能無(wú)法準(zhǔn)確回答我們的問(wèn)題。隨著數(shù)據(jù)的增長(zhǎng),我們可以輕松地嵌入和索引任何新數(shù)據(jù),并能夠檢索它來(lái)回答問(wèn)題。
from rag.generate import prepare_response
from rag.utils import get_client
def generate_response(
llm, temperature=0.0, stream=True,
system_content="", assistant_content="", user_content="",
max_retries=1, retry_interval=60):
"""Generate response from an LLM."""
retry_count = 0
client = get_client(llm=llm)
messages = [{"role": role, "content": content} for role, content in [
("system", system_content),
("assistant", assistant_content),
("user", user_content)] if content]
while retry_count <= max_retries:
try:
chat_completion = client.chat.completions.create(
model=llm,
temperature=temperature,
stream=stream,
messages=messages,
)
return prepare_response(chat_completion, stream=stream)
except Exception as e:
print(f"Exception: {e}")
time.sleep(retry_interval) # default is per-minute rate limits
retry_count += 1
return ""
注意:我們使用 0.0 的溫度來(lái)啟用可重復(fù)的實(shí)驗(yàn),但您應(yīng)該根據(jù)您的用例進(jìn)行調(diào)整。對(duì)于需要始終以事實(shí)為依據(jù)的用例,我們建議使用非常低的溫度值,而更具創(chuàng)造性的任務(wù)可以從更高的溫度下受益。
# Generate response
query = "What is the default batch size for map_batches?"
response = generate_response(
llm="meta-llama/Llama-2-70b-chat-hf",
temperature=0.0,
stream=True,
system_content="Answer the query using the context provided. Be succinct.",
user_content=f"query: {query}, context: {context}")
The default batch size for map_batches is 4096.
讓我們將上下文檢索和響應(yīng)生成結(jié)合在一起,形成一個(gè)方便的查詢(xún)代理,我們可以使用它輕松生成響應(yīng)。這將負(fù)責(zé)設(shè)置我們的代理(嵌入和 LLM 模型)以及上下文檢索,并將其傳遞給我們的 LLM 以生成響應(yīng)。
class QueryAgent:
def __init__(self, embedding_model_name="thenlper/gte-base",
llm="meta-llama/Llama-2-70b-chat-hf", temperature=0.0,
max_context_length=4096, system_content="", assistant_content=""):
# Embedding model
self.embedding_model = get_embedding_model(
embedding_model_name=embedding_model_name,
model_kwargs={"device": "cuda"},
encode_kwargs={"device": "cuda", "batch_size": 100})
# Context length (restrict input length to 50% of total length)
max_context_length = int(0.5*max_context_length)
# LLM
self.llm = llm
self.temperature = temperature
self.context_length = max_context_length - get_num_tokens(system_content + assistant_content)
self.system_content = system_content
self.assistant_content = assistant_content
def __call__(self, query, num_chunks=5, stream=True):
# Get sources and context
context_results = semantic_search(
query=query,
embedding_model=self.embedding_model,
k=num_chunks)
# Generate response
context = [item["text"] for item in context_results]
sources = [item["source"] for item in context_results]
user_content = f"query: {query}, context: {context}"
answer = generate_response(
llm=self.llm,
temperature=self.temperature,
stream=stream,
system_content=self.system_content,
assistant_content=self.assistant_content,
user_content=trim(user_content, self.context_length))
# Result
result = {
"question": query,
"sources": sources,
"answer": answer,
"llm": self.llm,
}
return result
有了這個(gè),我們只需幾行就可以使用我們的 RAG 應(yīng)用程序:
llm = "meta-llama/Llama-2-7b-chat-hf"
agent = QueryAgent(
embedding_model_name="thenlper/gte-base",
llm=llm,
max_context_length=MAX_CONTEXT_LENGTHS[llm],
system_content="Answer the query using the context provided. Be succinct.")
result = agent(query="What is the default batch size for map_batches?")
print("\n\n", json.dumps(result, indent=2))
The default batch size for map_batches
is 4096
{
"question": "What is the default batch size for map_batches?",
"sources": [
"ray.data.Dataset.map_batches — Ray 2.7.1",
"Transforming Data — Ray 2.7.1",
"Ray Data Internals — Ray 2.7.1",
"Dynamic Request Batching — Ray 2.7.1",
"Image Classification Batch Inference with PyTorch — Ray 2.7.1"
],
"answer": "The default batch size for map_batches
is 4096",
"llm": "meta-llama/Llama-2-7b-chat-hf"
}
到目前為止,我們已經(jīng)為 RAG 應(yīng)用程序的各個(gè)部分選擇了典型/任意值。但是,如果我們要更改某些內(nèi)容,例如分塊邏輯、嵌入模型、LLM 等,我們?nèi)绾沃牢覀儞碛斜纫郧案玫呐渲茫肯襁@樣的生成任務(wù)很難進(jìn)行定量評(píng)估,因此我們需要開(kāi)發(fā)可靠的方法來(lái)進(jìn)行評(píng)估。由于我們的應(yīng)用程序中有許多可變組件,因此我們需要執(zhí)行單元/組件和端到端評(píng)估。組件評(píng)估可能涉及單獨(dú)評(píng)估我們的檢索(是我們檢索到的一組塊中的最佳來(lái)源)和評(píng)估我們的 LLM 響應(yīng)(給定最佳來(lái)源,LLM 是否能夠產(chǎn)生高質(zhì)量的答案)。對(duì)于端到端評(píng)估,我們可以評(píng)估整個(gè)系統(tǒng)的質(zhì)量(給定數(shù)據(jù)源,響應(yīng)的質(zhì)量如何)。我們將要求我們的評(píng)估員 LLM 使用上下文對(duì)回答的質(zhì)量進(jìn)行 1-5 之間的評(píng)分,但是,我們也可以讓它為其他維度生成分?jǐn)?shù),例如幻覺(jué)(僅使用提供的上下文中的信息生成的答案)、毒性等。注意:我們可以將分?jǐn)?shù)限制為二進(jìn)制(0/1),這可能更容易解釋?zhuān)ɡ纾卮鹨凑_要么不正確)。但是,我們?cè)诜謹(jǐn)?shù)中引入了更高的方差,以便更深入、更細(xì)致地了解 LLM 如何對(duì)回答進(jìn)行評(píng)分(例如,LLM 對(duì)回答的偏見(jiàn))。
檢索系統(tǒng)和 LLM 的組件評(píng)估(左),總體評(píng)估(右)。
評(píng)估器我們將從確定評(píng)估器開(kāi)始。給定查詢(xún)的響應(yīng)和相關(guān)上下文,我們的評(píng)估器應(yīng)該是一種值得信賴(lài)的方法來(lái)評(píng)分/評(píng)估響應(yīng)的質(zhì)量。但在確定評(píng)估器之前,我們需要一個(gè)問(wèn)題數(shù)據(jù)集和答案的來(lái)源。我們可以使用此數(shù)據(jù)集要求不同的評(píng)估者提供答案,然后對(duì)他們的答案進(jìn)行評(píng)分(例如,分?jǐn)?shù)在 1-5 之間)。然后,我們可以檢查此數(shù)據(jù)集以確定我們的評(píng)估者是否公正,并且對(duì)分配的分?jǐn)?shù)有合理的推理。注意:我們正在評(píng)估我們的 LLM 在給定相關(guān)上下文的情況下生成響應(yīng)的能力。這是一個(gè)組件級(jí)評(píng)估(quality_score (LLM)),因?yàn)槲覀儧](méi)有使用檢索來(lái)獲取相關(guān)上下文。我們將從手動(dòng)創(chuàng)建數(shù)據(jù)集開(kāi)始(如果您無(wú)法手動(dòng)創(chuàng)建數(shù)據(jù)集,請(qǐng)繼續(xù)閱讀)。我們有一個(gè)用戶(hù)查詢(xún)列表和回答查詢(xún)的理想來(lái)源 datasets/eval-dataset-v1.jsonl。我們將使用上面的 LLM 應(yīng)用程序通過(guò) GPT-4 為每個(gè)查詢(xún)/源對(duì)生成參考答案。
datasets/eval-dataset-v1.jsonl:
https://github.com/ray-project/llm-applications/blob/main/datasets/eval-dataset-v1.jsonl
with open(Path(ROOT_DIR, "datasets/eval-dataset-v1.jsonl"), "r") as f:
data = [json.loads(item) for item in list(f)]
[{'question': 'I’m struggling a bit with Ray Data type conversions when I do map_batches. Any advice?',
'source': 'https://docs.ray.io/en/master/data/transforming-data.html'},
…
{'question': 'Is Ray integrated with DeepSpeed?',
'source': 'https://docs.ray.io/en/master/ray-air/examples/gptj_deepspeed_fine_tuning.html#fine-tuning-the-model-with-ray-air-a-name-train-a'}]
每個(gè)數(shù)據(jù)點(diǎn)都有一個(gè)問(wèn)題,并且標(biāo)記的源具有與問(wèn)題答案相關(guān)的精確上下文:
# Sample
uri = "https://docs.ray.io/en/master/data/transforming-data.html#configuring-batch-format"
fetch_text(uri=uri)
'\nConfiguring batch format#\nRay Data represents batches as dicts of NumPy ndarrays or pandas DataFrames. …'
我們可以從此上下文中提取文本并將其傳遞給我們的 LLM 以生成問(wèn)題的答案。我們還將要求它對(duì)查詢(xún)的響應(yīng)質(zhì)量進(jìn)行評(píng)分。為此,我們定義了一個(gè)繼承自 QueryAgent 的 QueryAgentWithContext,不同之處在于我們提供上下文,它不需要檢索它。
class QueryAgentWithContext(QueryAgent):
def __call__(self, query, context):
user_content = f"query: {query}, context: {context}"
response = generate_response(
llm=self.llm,
temperature=self.temperature,
stream=True,
system_content=self.system_content,
assistant_content=self.assistant_content,
user_content=user_content[: self.context_length])
return response
現(xiàn)在,我們可以創(chuàng)建一個(gè)包含問(wèn)題、來(lái)源、答案、分?jǐn)?shù)和推理的數(shù)據(jù)集。我們可以檢查它以確定我們的評(píng)估器是否高質(zhì)量。
根據(jù)它提供的分?jǐn)?shù)和推理,我們發(fā)現(xiàn) GPT-4 是一款高質(zhì)量的評(píng)估器。我們對(duì)其他 LLM(例如 Llama-2-70b)進(jìn)行了同樣的評(píng)估,發(fā)現(xiàn)它們?nèi)狈m當(dāng)?shù)耐评恚⑶曳浅?犊亟o出了自己的答案。
EVALUATOR = "gpt-4"
注意:更徹底的評(píng)估還會(huì)通過(guò)要求評(píng)估者比較以下不同 LLM 的回答來(lái)測(cè)試以下內(nèi)容:
冷啟動(dòng)我們可能并不總是有準(zhǔn)備好的問(wèn)題數(shù)據(jù)集和隨時(shí)可用的最佳來(lái)源來(lái)回答該問(wèn)題。為了解決這個(gè)冷啟動(dòng)問(wèn)題,我們可以使用 LLM 查看我們的文本塊并生成特定塊將回答的問(wèn)題。這為我們提供了高質(zhì)量的問(wèn)題和答案的確切來(lái)源。但是,這種數(shù)據(jù)集生成方法可能會(huì)有點(diǎn)嘈雜。生成的問(wèn)題可能并不總是與用戶(hù)可能提出的問(wèn)題高度一致。我們所說(shuō)的最佳來(lái)源的特定塊也可能在其他塊中具有該確切信息。盡管如此,在我們收集 + 手動(dòng)標(biāo)記高質(zhì)量數(shù)據(jù)集的同時(shí),這仍然是開(kāi)始我們的開(kāi)發(fā)過(guò)程的好方法。
# Prompt
num_questions = 3
system_content = f"""
Create {num_questions} questions using only the context provided.
End each question with a '?' character and then in a newline write the answer to that question using only the context provided.
Separate each question/answer pair by a newline.
"""
# Generate questions
synthetic_data = []
for chunk in chunks[:1]: # small samples
response = generate_response(
llm="gpt-4",
temperature=0.0,
system_content=system_content,
user_content=f"context: {chunk.page_content}")
entries = response.split("\n\n")
for entry in entries:
question, answer = entry.split("\n")
synthetic_data.append({"question": question, "source": chunk.metadata["source"], "answer": answer})
synthetic_data[:3]
[{'question': 'What can you use to monitor and debug your Ray applications and clusters?',
'source': 'https://docs.ray.io/en/master/ray-observability/reference/index.html#reference',
'answer': 'You can use the API and CLI documented in the references to monitor and debug your Ray applications and clusters.'},
{'question': 'What are the guides included in the references?',
'source': 'https://docs.ray.io/en/master/ray-observability/reference/index.html#reference',
'answer': 'The guides included in the references are State API, State CLI, and System Metrics.'},
{'question': 'What are the two types of interfaces mentioned for monitoring and debugging Ray applications and clusters?',
'source': 'https://docs.ray.io/en/master/ray-observability/reference/index.html#reference',
'answer': 'The two types of interfaces mentioned for monitoring and debugging Ray applications and clusters are API and CLI.'}]
本文章轉(zhuǎn)載微信公眾號(hào)@Ray中文社區(qū)
對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力
一鍵對(duì)比試用API 限時(shí)免費(fèi)