メインコンテンツへスキップ
Retrieval Augmented Generation (RAG) は、独自のナレッジベースにアクセスできる生成 AI アプリケーションを構築する際によく用いられる手法です。 Evals hero

このガイドで学ぶこと

このガイドでは、次のことを行う方法を説明します。
  • ナレッジベースを構築する
  • 関連するドキュメントを検索するリトリーバルステップを含む RAG アプリケーションを作成する
  • Weave を使ってリトリーバルステップを追跡する
  • コンテキストの精度を測定するために LLM ジャッジを用いて RAG アプリケーションを評価する
  • カスタムスコアリング関数を定義する

前提条件

  • W&B アカウント
  • Python 3.8 以上または Node.js 18 以上
  • 必要なパッケージがインストールされていること:
    • Python: pip install weave openai
    • TypeScript: npm install weave openai
  • 環境変数として設定した OpenAI の APIキー

ナレッジベースを構築する

まず、記事の埋め込みベクトルを計算します。通常は、記事に対してこの処理を一度だけ実行し、その埋め込みベクトルとメタデータをデータベースに保存しますが、ここでは説明を簡単にするために、スクリプトが実行されるたびに処理を行います。
from openai import OpenAI
import weave
from weave import Model
import numpy as np
import json
import asyncio

articles = [
    "Novo Nordisk and Eli Lilly rival soars 32 percent after promising weight loss drug results Shares of Denmarks Zealand Pharma shot 32 percent higher in morning trade, after results showed success in its liver disease treatment survodutide, which is also on trial as a drug to treat obesity. The trial “tells us that the 6mg dose is safe, which is the top dose used in the ongoing [Phase 3] obesity trial too,” one analyst said in a note. The results come amid feverish investor interest in drugs that can be used for weight loss.",
    "Berkshire shares jump after big profit gain as Buffetts conglomerate nears $1 trillion valuation Berkshire Hathaway shares rose on Monday after Warren Buffetts conglomerate posted strong earnings for the fourth quarter over the weekend. Berkshires Class A and B shares jumped more than 1.5%, each. Class A shares are higher by more than 17% this year, while Class B has gained more than 18%. Berkshire was last valued at $930.1 billion, up from $905.5 billion where it closed on Friday, according to FactSet. Berkshire on Saturday posted fourth-quarter operating earnings of $8.481 billion, about 28 percent higher than the $6.625 billion from the year-ago period, driven by big gains in its insurance business. Operating earnings refers to profits from businesses across insurance, railroads and utilities. Meanwhile, Berkshires cash levels also swelled to record levels. The conglomerate held $167.6 billion in cash in the fourth quarter, surpassing the $157.2 billion record the conglomerate held in the prior quarter.",
    "Highmark Health says its combining tech from Google and Epic to give doctors easier access to information Highmark Health announced it is integrating technology from Google Cloud and the health-care software company Epic Systems. The integration aims to make it easier for both payers and providers to access key information they need, even if its stored across multiple points and formats, the company said. Highmark is the parent company of a health plan with 7 million members, a provider network of 14 hospitals and other entities",
    "Rivian and Lucid shares plunge after weak EV earnings reports Shares of electric vehicle makers Rivian and Lucid fell Thursday after the companies reported stagnant production in their fourth-quarter earnings after the bell Wednesday. Rivian shares sank about 25 percent, and Lucids stock dropped around 17 percent. Rivian forecast it will make 57,000 vehicles in 2024, slightly less than the 57,232 vehicles it produced in 2023. Lucid said it expects to make 9,000 vehicles in 2024, more than the 8,428 vehicles it made in 2023.",
    "Mauritius blocks Norwegian cruise ship over fears of a potential cholera outbreak Local authorities on Sunday denied permission for the Norwegian Dawn ship, which has 2,184 passengers and 1,026 crew on board, to access the Mauritius capital of Port Louis, citing “potential health risks.” The Mauritius Ports Authority said Sunday that samples were taken from at least 15 passengers on board the cruise ship. A spokesperson for the U.S.-headquartered Norwegian Cruise Line Holdings said Sunday that 'a small number of guests experienced mild symptoms of a stomach-related illness' during Norwegian Dawns South Africa voyage.",
    "Intuitive Machines lands on the moon in historic first for a U.S. company Intuitive Machines Nova-C cargo lander, named Odysseus after the mythological Greek hero, is the first U.S. spacecraft to soft land on the lunar surface since 1972. Intuitive Machines is the first company to pull off a moon landing — government agencies have carried out all previously successful missions. The company's stock surged in extended trading Thursday, after falling 11 percent in regular trading.",
    "Lunar landing photos: Intuitive Machines Odysseus sends back first images from the moon Intuitive Machines cargo moon lander Odysseus returned its first images from the surface. Company executives believe the lander caught its landing gear sideways on the moon's surface while touching down and tipped over. Despite resting on its side, the company's historic IM-1 mission is still operating on the moon.",
]

def docs_to_embeddings(docs: list) -> list:
    openai = OpenAI()
    document_embeddings = []
    for doc in docs:
        response = (
            openai.embeddings.create(input=doc, model="text-embedding-3-small")
            .data[0]
            .embedding
        )
        document_embeddings.append(response)
    return document_embeddings

article_embeddings = docs_to_embeddings(articles) # 注意: 通常はこの処理を一度だけ実行し、埋め込みとメタデータをデータベースに保存します

RAG アプリを作成する

次に、検索用関数 get_most_relevant_documentweave.op() デコレーターでラップし、Model クラスを作成します。weave.init('<team-name>/rag-quickstart') を呼び出して、後から確認できるように関数のすべての入力と出力のトラッキングを開始します。チーム名を指定しない場合、出力はあなたの W&B のデフォルトチームまたはエンティティ に記録されます。
from openai import OpenAI
import weave
from weave import Model
import numpy as np
import asyncio

@weave.op()
def get_most_relevant_document(query):
    openai = OpenAI()
    query_embedding = (
        openai.embeddings.create(input=query, model="text-embedding-3-small")
        .data[0]
        .embedding
    )
    similarities = [
        np.dot(query_embedding, doc_emb)
        / (np.linalg.norm(query_embedding) * np.linalg.norm(doc_emb))
        for doc_emb in article_embeddings
    ]
    # 最も類似しているドキュメントのインデックスを取得する
    most_relevant_doc_index = np.argmax(similarities)
    return articles[most_relevant_doc_index]

class RAGModel(Model):
    system_message: str
    model_name: str = "gpt-3.5-turbo-1106"

    @weave.op()
    def predict(self, question: str) -> dict: # 注: `question` は後で評価行からデータを選択する際に使用されます
        from openai import OpenAI
        context = get_most_relevant_document(question)
        client = OpenAI()
        query = f"""Use the following information to answer the subsequent question. If the answer cannot be found, write "I don't know."
        Context:
        \"\"\"
        {context}
        \"\"\"
        Question: {question}"""
        response = client.chat.completions.create(
            model=self.model_name,
            messages=[
                {"role": "system", "content": self.system_message},
                {"role": "user", "content": query},
            ],
            temperature=0.0,
            response_format={"type": "text"},
        )
        answer = response.choices[0].message.content
        return {'answer': answer, 'context': context}

# チーム名とプロジェクト名を設定します
weave.init('<team-name>/rag-quickstart')
model = RAGModel(
    system_message="You are an expert in finance and answer questions related to finance, financial services, and financial markets. When responding based on provided information, be sure to cite the source."
)
model.predict("What significant result was reported about Zealand Pharma's obesity trial?")

LLM ジャッジを使った評価

アプリケーションを評価するシンプルな方法がない場合の一つのアプローチとして、LLM を使ってそのさまざまな側面を評価させることができます。ここでは、LLM ジャッジを使って、コンテキストが与えられた回答を導くうえで有用だったかどうかを検証するようにプロンプトし、コンテキスト精度を測ろうとする例を示します。このプロンプトは、人気のある RAGAS フレームワーク をもとに拡張したものです。

スコアリング関数の定義

Build an Evaluation pipeline tutorial と同様に、アプリをテストするためのサンプル行(row)の集合とスコアリング関数を定義します。スコアリング関数は 1 行を受け取り、その行を評価します。入力引数は、その行内の対応するキーと一致している必要があるため、ここでの question は行の辞書から取得されます。output はモデルの出力です。モデルへの入力は、その入力引数に基づいてサンプルから取得されるため、この例でも question が使用されます。この例では async 関数を使用して、並列で高速に実行できるようにしています。async の簡単な入門を確認したい場合は、こちらを参照してください。
from openai import OpenAI
import weave
import asyncio

@weave.op()
async def context_precision_score(question, output):
    context_precision_prompt = """Given question, answer and context verify if the context was useful in arriving at the given answer. Give verdict as "1" if useful and "0" if not with json output.
    Output in only valid JSON format.

    question: {question}
    context: {context}
    answer: {answer}
    verdict: """
    client = OpenAI()

    prompt = context_precision_prompt.format(
        question=question,
        context=output['context'],
        answer=output['answer'],
    )

    response = client.chat.completions.create(
        model="gpt-4-turbo-preview",
        messages=[{"role": "user", "content": prompt}],
        response_format={ "type": "json_object" }
    )
    response_message = response.choices[0].message
    response = json.loads(response_message.content)
    return {
        "verdict": int(response["verdict"]) == 1,
    }

questions = [
    {"question": "What significant result was reported about Zealand Pharma's obesity trial?"},
    {"question": "How much did Berkshire Hathaway's cash levels increase in the fourth quarter?"},
    {"question": "What is the goal of Highmark Health's integration of Google Cloud and Epic Systems technology?"},
    {"question": "What were Rivian and Lucid's vehicle production forecasts for 2024?"},
    {"question": "Why was the Norwegian Dawn cruise ship denied access to Mauritius?"},
    {"question": "Which company achieved the first U.S. moon landing since 1972?"},
    {"question": "What issue did Intuitive Machines' lunar lander encounter upon landing on the moon?"}
]
evaluation = weave.Evaluation(dataset=questions, scorers=[context_precision_score])
asyncio.run(evaluation.evaluate(model)) # 注意: 評価するための model を定義する必要があります

Optional: Defining a Scorer class

一部のアプリケーションでは、カスタム評価クラスを作成したくなる場合があります。たとえば、標準化された LLMJudge クラスを、特定のパラメータ(例: チャットモデル、プロンプト)、各行ごとの特定のスコアリング方法、および集計スコアの特定の計算方法を備えて作成したいケースです。Weave では、すぐに使える Scorer クラスのリストを提供しているほか、カスタム Scorer を簡単に作成できます。以下の例では、カスタム class CorrectnessLLMJudge(Scorer) の作成方法を示します。 大まかには、カスタム Scorer を作成する手順は次のとおりです。
  1. weave.flow.scorer.Scorer を継承するカスタムクラスを定義する
  2. score 関数を上書きし、その関数の各呼び出しを追跡したい場合は @weave.op() を追加する
    • この関数では、モデルの予測が渡される output 引数を定義する必要があります。モデルが “None” を返す可能性がある場合は、型を Optional[dict] にします。
    • 残りの引数は一般的な Anydict にすることもできますし、weave.Evaluate クラスを使ってモデルを評価するときに利用されるデータセットから特定のカラムを選択することもできます。その場合は、preprocess_model_input が使われているときには、その処理後の 1 行分のカラム名またはキーと完全に同じ名前にする必要があります。
  3. Optional: 集計スコアの計算方法をカスタマイズするために summarize 関数を上書きする。デフォルトでは、カスタム関数を定義しない場合、Weave は weave.flow.scorer.auto_summarize 関数を使用します。
    • この関数には @weave.op() デコレータを付与する必要があります。
from weave import Scorer

class CorrectnessLLMJudge(Scorer):
    prompt: str
    model_name: str
    device: str

    @weave.op()
    async def score(self, output: Optional[dict], query: str, answer: str) -> Any:
        """pred, query, target を比較して予測の正確さをスコア付けする。
        Args:
            - output: 評価対象のモデルから提供される dict
            - query: データセットで定義された質問
            - answer: データセットで定義されたターゲット回答
        Returns:
            - 単一の dict {メトリクス名: 単一の評価値}"""

        # get_model は、指定されたパラメータ(OpenAI,HF...)に基づく汎用的なモデル取得関数として定義されている
        eval_model = get_model(
            model_name = self.model_name,
            prompt = self.prompt
            device = self.device,
        )
        # 評価を高速化するための非同期評価 - 非同期である必要はない
        grade = await eval_model.async_predict(
            {
                "query": query,
                "answer": answer,
                "result": output.get("result"),
            }
        )
        # 出力のパース - pydantic を使えばより堅牢に実装できる
        evaluation = "incorrect" not in grade["text"].strip().lower()

        # Weave に表示されるカラム名
        return {"correct": evaluation}

    @weave.op()
    def summarize(self, score_rows: list) -> Optional[dict]:
        """スコアリング関数によって各行に対して計算されたすべてのスコアを集約する。
        Args:
            - score_rows: dict のリスト。各 dict はメトリクスとスコアを持つ
        Returns:
            - 入力と同じ構造を持つネストされた dict"""

        # 何も提供されない場合は weave.flow.scorer.auto_summarize 関数が使われる
        # return auto_summarize(score_rows)

        valid_data = [x.get("correct") for x in score_rows if x.get("correct") is not None]
        count_true = list(valid_data).count(True)
        int_data = [int(x) for x in valid_data]

        sample_mean = np.mean(int_data) if int_data else 0
        sample_variance = np.var(int_data) if int_data else 0
        sample_error = np.sqrt(sample_variance / len(int_data)) if int_data else 0

        # 追加の "correct" レイヤーは必須ではないが、UI 上の構造を増やすのに役立つ
        return {
            "correct": {
                "true_count": count_true,
                "true_fraction": sample_mean,
                "stderr": sample_error,
            }
        }
これを scorer として使用するには、次のように初期化して Evaluationscorers 引数に渡します:
evaluation = weave.Evaluation(dataset=questions, scorers=[CorrectnessLLMJudge()])

まとめ:すべてを組み合わせる

RAG アプリで同じ結果を得るには、次のようにします。
  • LLM 呼び出しと検索ステップ関数を weave.op() でラップする
  • (任意)predict 関数とアプリの詳細を持つ Model サブクラスを作成する
  • 評価用のサンプルを収集する
  • 各サンプルをスコアリングする関数を作成する
  • Evaluation クラスを使ってサンプルに対して評価を実行する
NOTE: Evaluation の非同期実行により、OpenAI や Anthropic などのモデルでレート制限が発生することがあります。これを防ぐには、例えば WEAVE_PARALLELISM=3 のように環境変数を設定して、並列ワーカーの数を制限してください。 以下がコード全体です。
from openai import OpenAI
import weave
from weave import Model
import numpy as np
import json
import asyncio

# 評価に使用するサンプル
articles = [
    "Novo Nordisk and Eli Lilly rival soars 32 percent after promising weight loss drug results Shares of Denmarks Zealand Pharma shot 32 percent higher in morning trade, after results showed success in its liver disease treatment survodutide, which is also on trial as a drug to treat obesity. The trial “tells us that the 6mg dose is safe, which is the top dose used in the ongoing [Phase 3] obesity trial too,” one analyst said in a note. The results come amid feverish investor interest in drugs that can be used for weight loss.",
    "Berkshire shares jump after big profit gain as Buffetts conglomerate nears $1 trillion valuation Berkshire Hathaway shares rose on Monday after Warren Buffetts conglomerate posted strong earnings for the fourth quarter over the weekend. Berkshires Class A and B shares jumped more than 1.5%, each. Class A shares are higher by more than 17% this year, while Class B has gained more than 18%. Berkshire was last valued at $930.1 billion, up from $905.5 billion where it closed on Friday, according to FactSet. Berkshire on Saturday posted fourth-quarter operating earnings of $8.481 billion, about 28 percent higher than the $6.625 billion from the year-ago period, driven by big gains in its insurance business. Operating earnings refers to profits from businesses across insurance, railroads and utilities. Meanwhile, Berkshires cash levels also swelled to record levels. The conglomerate held $167.6 billion in cash in the fourth quarter, surpassing the $157.2 billion record the conglomerate held in the prior quarter.",
    "Highmark Health says its combining tech from Google and Epic to give doctors easier access to information Highmark Health announced it is integrating technology from Google Cloud and the health-care software company Epic Systems. The integration aims to make it easier for both payers and providers to access key information they need, even if it's stored across multiple points and formats, the company said. Highmark is the parent company of a health plan with 7 million members, a provider network of 14 hospitals and other entities",
    "Rivian and Lucid shares plunge after weak EV earnings reports Shares of electric vehicle makers Rivian and Lucid fell Thursday after the companies reported stagnant production in their fourth-quarter earnings after the bell Wednesday. Rivian shares sank about 25 percent, and Lucids stock dropped around 17 percent. Rivian forecast it will make 57,000 vehicles in 2024, slightly less than the 57,232 vehicles it produced in 2023. Lucid said it expects to make 9,000 vehicles in 2024, more than the 8,428 vehicles it made in 2023.",
    "Mauritius blocks Norwegian cruise ship over fears of a potential cholera outbreak Local authorities on Sunday denied permission for the Norwegian Dawn ship, which has 2,184 passengers and 1,026 crew on board, to access the Mauritius capital of Port Louis, citing “potential health risks.” The Mauritius Ports Authority said Sunday that samples were taken from at least 15 passengers on board the cruise ship. A spokesperson for the U.S.-headquartered Norwegian Cruise Line Holdings said Sunday that 'a small number of guests experienced mild symptoms of a stomach-related illness' during Norwegian Dawns South Africa voyage.",
    "Intuitive Machines lands on the moon in historic first for a U.S. company Intuitive Machines Nova-C cargo lander, named Odysseus after the mythological Greek hero, is the first U.S. spacecraft to soft land on the lunar surface since 1972. Intuitive Machines is the first company to pull off a moon landing — government agencies have carried out all previously successful missions. The company's stock surged in extended trading Thursday, after falling 11 percent in regular trading.",
    "Lunar landing photos: Intuitive Machines Odysseus sends back first images from the moon Intuitive Machines cargo moon lander Odysseus returned its first images from the surface. Company executives believe the lander caught its landing gear sideways on the surface of the moon while touching down and tipped over. Despite resting on its side, the company's historic IM-1 mission is still operating on the moon.",
]

def docs_to_embeddings(docs: list) -> list:
    openai = OpenAI()
    document_embeddings = []
    for doc in docs:
        response = (
            openai.embeddings.create(input=doc, model="text-embedding-3-small")
            .data[0]
            .embedding
        )
        document_embeddings.append(response)
    return document_embeddings

article_embeddings = docs_to_embeddings(articles) # 注意: 通常はこの処理を一度だけ実行し、埋め込みとメタデータをデータベースに保存します

# 検索ステップにデコレーターを追加する
@weave.op()
def get_most_relevant_document(query):
    openai = OpenAI()
    query_embedding = (
        openai.embeddings.create(input=query, model="text-embedding-3-small")
        .data[0]
        .embedding
    )
    similarities = [
        np.dot(query_embedding, doc_emb)
        / (np.linalg.norm(query_embedding) * np.linalg.norm(doc_emb))
        for doc_emb in article_embeddings
    ]
    # 最も類似したドキュメントのインデックスを取得する
    most_relevant_doc_index = np.argmax(similarities)
    return articles[most_relevant_doc_index]

# アプリの詳細を含む Model サブクラスと、レスポンスを生成する predict 関数を作成する
class RAGModel(Model):
    system_message: str
    model_name: str = "gpt-3.5-turbo-1106"

    @weave.op()
    def predict(self, question: str) -> dict: # 注意: `question` は後で評価行からデータを選択する際に使用されます
        from openai import OpenAI
        context = get_most_relevant_document(question)
        client = OpenAI()
        query = f"""以下の情報を使用して、後続の質問に答えてください。答えが見つからない場合は、「わかりません」と書いてください。
        コンテキスト:
        \"\"\"
        {context}
        \"\"\"
        質問: {question}"""
        response = client.chat.completions.create(
            model=self.model_name,
            messages=[
                {"role": "system", "content": self.system_message},
                {"role": "user", "content": query},
            ],
            temperature=0.0,
            response_format={"type": "text"},
        )
        answer = response.choices[0].message.content
        return {'answer': answer, 'context': context}

# チーム名とプロジェクト名を設定する
weave.init('<team-name>/rag-quickstart')
model = RAGModel(
    system_message="あなたは金融の専門家であり、金融、金融サービス、および金融市場に関する質問に答えます。提供された情報に基づいて回答する際は、必ず出典を明記してください。"
)

# 質問と出力を使用してスコアを算出するスコアリング関数
@weave.op()
async def context_precision_score(question, output):
    context_precision_prompt = """質問、回答、コンテキストが与えられた場合、そのコンテキストが回答を導き出すのに役立ったかどうかを検証してください。役立った場合は「1」、そうでない場合は「0」を JSON 形式で出力してください。
    有効な JSON 形式のみで出力してください。

    question: {question}
    context: {context}
    answer: {answer}
    verdict: """
    client = OpenAI()

    prompt = context_precision_prompt.format(
        question=question,
        context=output['context'],
        answer=output['answer'],
    )

    response = client.chat.completions.create(
        model="gpt-4-turbo-preview",
        messages=[{"role": "user", "content": prompt}],
        response_format={ "type": "json_object" }
    )
    response_message = response.choices[0].message
    response = json.loads(response_message.content)
    return {
        "verdict": int(response["verdict"]) == 1,
    }

questions = [
    {"question": "Zealand Pharma の肥満治療試験について報告された重要な結果は何ですか?"},
    {"question": "Berkshire Hathaway の第4四半期における現金保有額はどれだけ増加しましたか?"},
    {"question": "Highmark Health による Google Cloud と Epic Systems の技術インテグレーションの目的は何ですか?"},
    {"question": "Rivian と Lucid の2024年の車両生産予測はどのようなものでしたか?"},
    {"question": "Norwegian Dawn クルーズ船がモーリシャスへの入港を拒否された理由は何ですか?"},
    {"question": "1972年以来、初めて米国の月面着陸を達成した企業はどこですか?"},
    {"question": "Intuitive Machines の月面着陸船が着陸時に遭遇した問題は何ですか?"}
]

# Evaluation オブジェクトを定義し、サンプルの質問とスコアリング関数を渡す
evaluation = weave.Evaluation(dataset=questions, scorers=[context_precision_score])
asyncio.run(evaluation.evaluate(model))

まとめ

このチュートリアルでは、この例で示した取得ステップのように、アプリケーションのさまざまなステップにオブザーバビリティを組み込む方法を紹介しました。また、アプリケーションの応答を自動評価するために、LLM ジャッジのような、より複雑なスコアリング関数を構築する方法も学びました。

次のステップ

エンジニア向けの実践的な RAG 手法をさらに発展的に学ぶには、RAG++ コースをチェックしてください。このコースでは、Weights & Biases、Cohere、Weaviate が提供する本番環境でそのまま使えるソリューションを通じて、パフォーマンスの最適化、コスト削減、アプリケーションの精度と関連性の向上方法を学ぶことができます。