{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Building Evals\n", "Optimizing Claude to give you the highest possible accuracy on a task is an empirical science, and a process of continuous improvement. Whether you are trying to know if a change to your prompt made the model perform better on a key metric, or whether you are trying to gauge if the model is good enough to launch into production, a good system for offline evaluation is critical to success.\n", "\n", "In this recipe, we will walk through common patterns in building evaluations, and useful rules of thumb to follow when doing so." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Parts of an Eval\n", "Evals typically have four parts.\n", "- An input prompt that is fed to the model. We will ask Claude to generate a completion based on this prompt. Often when we design our evals the input column will contain a set of variable inputs that get fed into a prompt template at test time.\n", "- An output that comes from running the input prompt through the model we want to evaluate.\n", "- A \"golden answer\" to which we compare the model output. The golden answer could be a mandatory exact match, or it could be an example of a perfect answer meant to give a grader a point of comparison to base their scoring on.\n", "- A score, generated by one of the grading methods discussed below, that represents how the model did on the question.\n", "\n", "## Eval Grading Methods\n", "There are two things about evals that can be time consuming and expensive. The first is writing the questions and golden answers for the eval. The second is grading. Writing questions and golden answers can be quite time consuming if you do not have a dataset already available or a way to create one without manually generating questions (consider using Claude to generate your questions!), but has the benefit of typically being a one-time fixed cost. You write questions and golden answers, and very rarely have to re-write them. Grading on the other hand is a cost you will incur every time you re-run your eval, in perpetuity - and you will likely re-run your eval a lot. As a result, building evals that can be quickly and cheaply graded should be at the center of your design choices.\n", "\n", "There are three common ways to grade evals.\n", "- **Code-based grading:** This involves using standard code (mostly string matching and regular expressions) to grade the model's outputs. Common versions are checking for an exact match against an answer, or checking that a string contains some key phrase(s). This is by far the best grading method if you can design an eval that allows for it, as it is super fast and highly reliable. However, many evaluations do not allow for this style of grading.\n", "- **Human grading:** A human looks at the model-generated answer, compares it to the golden answer, and assigns a score. This is the most capable grading method as it _can_ be used on almost any task, but it is also incredibly slow and expensive, particularly if you've built a large eval. You should mostly try to avoid designing evals that require human grading if you can help it.\n", "- **Model-based grading:** It turns out that Claude is highly capable of grading itself, and can be used to grade a wide variety of tasks that might have historically required humans, such as analysis of tone in creative writing or accuracy in free-form question answering. You do this by writing a _grader prompt_ for Claude.\n", "\n", "Let's walk through an example of each grading method." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Code-based Grading\n", "Here we will be grading an eval where we ask Claude to successfully identify how many legs something has. We want Claude to output just a number of legs, and we design the eval in a way that we can use an exact-match code-based grader." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Install and read in required packages, plus create an anthropic client.\n", "%pip install anthropic" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "from anthropic import Anthropic\n", "client = Anthropic()\n", "MODEL_NAME = \"claude-3-opus-20240229\"" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "# Define our input prompt template for the task.\n", "def build_input_prompt(animal_statement):\n", " user_content = f\"\"\"You will be provided a statement about an animal and your job is to determine how many legs that animal has.\n", " \n", " Here is the animal statment.\n", " {animal_statement}\n", " \n", " How many legs does the animal have? Return just the number of legs as an integer and nothing else.\"\"\"\n", "\n", " messages = [{'role': 'user', 'content': user_content}]\n", " return messages" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "# Define our eval (in practice you might do this as a jsonl or csv file instead).\n", "eval = [\n", " {\n", " \"animal_statement\": 'The animal is a human.',\n", " \"golden_answer\": '2'\n", " },\n", " {\n", " \"animal_statement\": 'The animal is a snake.',\n", " \"golden_answer\": '0'\n", " },\n", " {\n", " \"animal_statement\": 'The fox lost a leg, but then magically grew back the leg he lost and a mysterious extra leg on top of that.',\n", " \"golden_answer\": '5'\n", " }\n", "]" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Animal Statement: The animal is a human.\n", "Golden Answer: 2\n", "Output: 2\n", "\n", "Animal Statement: The animal is a snake.\n", "Golden Answer: 0\n", "Output: 0\n", "\n", "Animal Statement: The fox lost a leg, but then magically grew back the leg he lost and a mysterious extra leg on top of that.\n", "Golden Answer: 5\n", "Output: 5\n", "\n" ] } ], "source": [ "# Get completions for each input.\n", "# Define our get_completion function (including the stop sequence discussed above).\n", "def get_completion(messages):\n", " response = client.messages.create(\n", " model=MODEL_NAME,\n", " max_tokens=5,\n", " messages=messages\n", " )\n", " return response.content[0].text\n", "\n", "# Get completions for each question in the eval.\n", "outputs = [get_completion(build_input_prompt(question['animal_statement'])) for question in eval]\n", "\n", "# Let's take a quick look at our outputs\n", "for output, question in zip(outputs, eval):\n", " print(f\"Animal Statement: {question['animal_statement']}\\nGolden Answer: {question['golden_answer']}\\nOutput: {output}\\n\")" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Score: 100.0%\n" ] } ], "source": [ "# Check our completions against the golden answers.\n", "# Define a grader function\n", "def grade_completion(output, golden_answer):\n", " return output == golden_answer\n", "\n", "# Run the grader function on our outputs and print the score.\n", "grades = [grade_completion(output, question['golden_answer']) for output, question in zip(outputs, eval)]\n", "print(f\"Score: {sum(grades)/len(grades)*100}%\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Human grading\n", "Now let's imagine that we are grading an eval where we've asked Claude a series of open ended questions, maybe for a general purpose chat assistant. Unfortunately, answers could be varied and this can not be graded with code. One way we can do this is with human grading." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "# Define our input prompt template for the task.\n", "def build_input_prompt(question):\n", " user_content = f\"\"\"Please answer the following question:\n", " {question}\"\"\"\n", "\n", " messages = [{'role': 'user', 'content': user_content}]\n", " return messages" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "# Define our eval. For this task, the best \"golden answer\" to give a human are instructions on what to look for in the model's output.\n", "eval = [\n", " {\n", " \"question\": 'Please design me a workout for today that features at least 50 reps of pulling leg exercises, at least 50 reps of pulling arm exercises, and ten minutes of core.',\n", " \"golden_answer\": 'A correct answer should include a workout plan with 50 or more reps of pulling leg exercises (such as deadlifts, but not such as squats which are a pushing exercise), 50 or more reps of pulling arm exercises (such as rows, but not such as presses which are a pushing exercise), and ten minutes of core workouts. It can but does not have to include stretching or a dynamic warmup, but it cannot include any other meaningful exercises.'\n", " },\n", " {\n", " \"question\": 'Send Jane an email asking her to meet me in front of the office at 9am to leave for the retreat.',\n", " \"golden_answer\": 'A correct answer should decline to send the email since the assistant has no capabilities to send emails. It is okay to suggest a draft of the email, but not to attempt to send the email, call a function that sends the email, or ask for clarifying questions related to sending the email (such as which email address to send it to).'\n", " },\n", " {\n", " \"question\": 'Who won the super bowl in 2024 and who did they beat?', # Claude should get this wrong since it comes after its training cutoff.\n", " \"golden_answer\": 'A correct answer states that the Kansas City Chiefs defeated the San Francisco 49ers.'\n", " }\n", "]" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Question: Please design me a workout for today that features at least 50 reps of pulling leg exercises, at least 50 reps of pulling arm exercises, and ten minutes of core.\n", "Golden Answer: A correct answer should include a workout plan with 50 or more reps of pulling leg exercises (such as deadlifts, but not such as squats which are a pushing exercise), 50 or more reps of pulling arm exercises (such as rows, but not such as presses which are a pushing exercise), and ten minutes of core workouts. It can but does not have to include stretching or a dynamic warmup, but it cannot include any other meaningful exercises.\n", "Output: Here's a workout plan for today that includes at least 50 reps of pulling leg exercises, 50 reps of pulling arm exercises, and ten minutes of core:\n", "\n", "Pulling Leg Exercises:\n", "1. Hamstring Curls (lying or seated): 3 sets of 12 reps (36 reps total)\n", "2. Single-leg Romanian Deadlifts: 2 sets of 10 reps per leg (40 reps total)\n", "\n", "Pulling Arm Exercises:\n", "1. Bent-over Rows: 3 sets of 10 reps (30 reps total)\n", "2. Chin-ups or Assisted Chin-ups: 3 sets of 8 reps (24 reps total)\n", "\n", "Core Exercises (10 minutes):\n", "1. Plank: 3 sets of 1 minute each\n", "2. Russian Twists: 3 sets of 20 reps (60 reps total)\n", "3. Bicycle Crunches: 3 sets of 20 reps (60 reps total)\n", "\n", "Warm-up: Start with 5-10 minutes of light cardio and dynamic stretching to prepare your muscles for the workout.\n", "\n", "Rest: Take 60-90 seconds of rest between each set and 2-3 minutes of rest between different exercises.\n", "\n", "Cool-down: Finish the workout with 5-10 minutes of static stretching to help your muscles recover and improve flexibility.\n", "\n", "Remember to listen to your body, use proper form, and adjust the weights, resistance, or number of reps according to your fitness level. Stay hydrated throughout the workout, and don't hesitate to consult a fitness professional if you have any concerns or need guidance on proper form.\n", "\n", "Question: Send Jane an email asking her to meet me in front of the office at 9am to leave for the retreat.\n", "Golden Answer: A correct answer should decline to send the email since the assistant has no capabilities to send emails. It is okay to suggest a draft of the email, but not to attempt to send the email, call a function that sends the email, or ask for clarifying questions related to sending the email (such as which email address to send it to).\n", "Output: I apologize, but I am not able to send emails on your behalf. As an AI language model, I don't have the ability to interact with email systems or send messages to individuals. \n", "\n", "If you need to send an email to Jane, you will need to do so using your own email account. Here's a sample email that you can use as a template:\n", "\n", "Subject: Meeting for Retreat\n", "\n", "Dear Jane,\n", "\n", "I hope this email finds you well. I wanted to confirm our plans for the upcoming retreat.\n", "\n", "Can you please meet me in front of the office at 9am on [date]? We can leave for the retreat together from there.\n", "\n", "Please let me know if you have any questions or if there's anything else you need.\n", "\n", "Best regards,\n", "[Your Name]\n", "\n", "Feel free to modify this email template to fit your specific needs and situation.\n", "\n", "Question: Who won the super bowl in 2024 and who did they beat?\n", "Golden Answer: A correct answer states that the Kansas City Chiefs defeated the San Francisco 49ers.\n", "Output: I apologize, but I cannot answer this question as the Super Bowl for 2024 has not taken place yet. The Super Bowl is an annual event that occurs in February, and as of March 2023, the 2024 Super Bowl participants and outcome are unknown. The teams playing in Super Bowl LVIII will be determined by the results of the 2023 NFL season and playoffs, which have not begun yet.\n", "\n" ] } ], "source": [ "# Get completions for each input.\n", "# Define our get_completion function (including the stop sequence discussed above).\n", "def get_completion(messages):\n", " response = client.messages.create(\n", " model=MODEL_NAME,\n", " max_tokens=2048,\n", " messages=messages\n", " )\n", " return response.content[0].text\n", "\n", "# Get completions for each question in the eval.\n", "outputs = [get_completion(build_input_prompt(question['question'])) for question in eval]\n", "\n", "# Let's take a quick look at our outputs\n", "for output, question in zip(outputs, eval):\n", " print(f\"Question: {question['question']}\\nGolden Answer: {question['golden_answer']}\\nOutput: {output}\\n\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Because we will need to have a human grade this question, from here you would evaluate the outputs against the golden answers yourself, or write the outputs and golden answers to a csv and hand them to another human grader." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Model-based Grading\n", "Having to manually grade the above eval every time is going to get very annoying very fast, especially if the eval is a more realistic size (dozens, hundreds, or even thousands of questions). Luckily, there's a better way! We can actually have Claude do the grading for us. Let's take a look at how to do that using the same eval and completions from above." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Score: 66.66666666666666%\n" ] } ], "source": [ "# We start by defining a \"grader prompt\" template.\n", "def build_grader_prompt(answer, rubric):\n", " user_content = f\"\"\"You will be provided an answer that an assistant gave to a question, and a rubric that instructs you on what makes the answer correct or incorrect.\n", " \n", " Here is the answer that the assistant gave to the question.\n", " {answer}\n", " \n", " Here is the rubric on what makes the answer correct or incorrect.\n", " {rubric}\n", " \n", " An answer is correct if it entirely meets the rubric criteria, and is otherwise incorrect. =\n", " First, think through whether the answer is correct or incorrect based on the rubric inside tags. Then, output either 'correct' if the answer is correct or 'incorrect' if the answer is incorrect inside tags.\"\"\"\n", "\n", " messages = [{'role': 'user', 'content': user_content}]\n", " return messages\n", "\n", "# Now we define the full grade_completion function.\n", "import re\n", "def grade_completion(output, golden_answer):\n", " messages = build_grader_prompt(output, golden_answer)\n", " completion = get_completion(messages)\n", " # Extract just the label from the completion (we don't care about the thinking)\n", " pattern = r'(.*?)'\n", " match = re.search(pattern, completion, re.DOTALL)\n", " if match:\n", " return match.group(1).strip()\n", " else:\n", " raise ValueError(\"Did not find tags.\")\n", "\n", "# Run the grader function on our outputs and print the score.\n", "grades = [grade_completion(output, question['golden_answer']) for output, question in zip(outputs, eval)]\n", "print(f\"Score: {grades.count('correct')/len(grades)*100}%\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, the claude-based grader is able to correctly analyze and grade Claude's responses with a high level of accuracy, saving you precious time." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now you know about different grading design patterns for evals, and are ready to start building your own. As you do, here are a few guiding pieces of wisdom to get you started.\n", "- Make your evals specific to your task whenever possible, and try to have the distribution in your eval represent ~ the real life distribution of questions and question difficulties.\n", "- The only way to know if a model-based grader can do a good job grading your task is to try. Try it out and read some samples to see if your task is a good candidate.\n", "- Often all that lies between you and an automatable eval is clever design. Try to structure questions in a way that the grading can be automated, while still staying true to the task. Reformatting questions into multipe choice is a common tactic here.\n", "- In general, your preference should be for higher volume and lower quality of questions over very low volume with high quality." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.12" } }, "nbformat": 4, "nbformat_minor": 2 }