{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/eugeneyan-com/2024-10-27-aligneval-building-an-app-to-make-evals-easy-fun-and/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "Go to [aligneval.com](https://aligneval.com) to start building your own LLM-evaluator; sample data included.\n\n> **Update**: Now [integrated with LangSmith](https://blog.langchain.com/introducing-align-evals/) and [featured on VentureBeat](https://venturebeat.com/ai/langchains-align-evals-closes-the-evaluator-trust-gap-with-prompt-level-calibration)! 🎉\n\nEvery AI-powered product needs evals. But let’s face it—they’re a pain to build, hard to scale, and most teams get them wrong. As a result, many AI-powered experiences are bottlenecked on evals, sometimes delaying launches by weeks or even months.\n\nI’ve spent the past year or so wrestling with product/task-specific evals. Testing different ways to [detect](https://eugeneyan.com/writing/abstractive/) hallucinations, [finetune](https://eugeneyan.com/writing/finetuning/) evaluators, and [evaluate](https://eugeneyan.com/writing/llm-evaluators/) LLM-based evaluators. There were dead ends. There were rabbit holes. But I learned [what works and what doesn’t](https://eugeneyan.com/writing/evals/).\n\nAnd that’s why I’m excited to introduce [AlignEval](https://aligneval.com/), an app that makes evals easy and… fun? (Okay, I’ll settle for less painful.) It also tries to automate part of the process. AlignEval makes building LLM-evaluators as straightforward as four simple steps:\n\n- Upload a CSV file with columns for input and output.\n- Look at the data and label samples as pass or fail.\n- Define eval criteria, run the LLM-evaluator, check results.\n- Optimize the LLM-evaluator with dev-test splits.\n\nBy the way, if you want to learn more about evals, my friends Hamel and Shreya are hosting their *final* cohort of “AI Evals for Engineers and PMs” in July. Here’s a [35% discount code](https://maven.com/parlance-labs/evals?promoCode=eugene-is-all-you-need).\n\n## Align AI to human. Calibrate human to AI. Repeat.\n\nThe key insight is that aligning AI to human preferences is only half the battle. To build effective evals, we must also **calibrate human criteria to AI output**.\n\nMany teams make the mistake of crafting elaborate eval criteria without first **looking at the data**. It’s like theorizing about user experience and defects from the ivory tower, without doing error analysis. From [Who Validates the Validators](https://arxiv.org/abs/2404.12272): “*It is impossible to completely determine evaluation criteria prior to human judging of LLM outputs.*”\n\nThis leads to two types of bad criteria. First, irrelevant criteria that are a waste of time, such as generic metrics (e.g., helpfulness) or very low probability defects (e.g., grammar, spelling). Second, unrealistic, unattainable criteria that the technology isn’t ready for, such as autonomous agents back in 2023. Either way, teams squander effort that would have been better invested in evaluating actual defects that occur with moderate frequency.\n\n(I think out-of-the-box eval solutions fuel this problem, pushing generic criteria that are easy to plug and play but ignore the idiosyncrasies of your domain, product, and data.)\n\nThe way to solve this—and build useful evals—is to work backward from the data. That’s what [AlignEval](https://aligneval.com/) tries to do by putting data and metrics first, not preset criteria or LLMs.\n\n## Getting started: Upload some data\n\n**To get started, upload a CSV file containing the following columns:**\n\n- **id**: Lets you match labeled and evaluated samples to your data\n- **input**: The context provided to the LLM to generate the output (e.g., text to classify, news to summarize, retrieved documents for answering questions)\n- **output**: What the LLM generates (e.g., classification label, summary, answer)\n- **label**: Your judgment on whether the output passes (0) or fails (1)\n\n![Instructions to upload a CSV file.](https://eugeneyan.com/assets/upload.webp \"Instructions to upload a CSV file.\")\n\nInstructions to upload a CSV file.\n\nIf you don’t have a CSV handy, [download a sample](https://aligneval.com/data/fib50.csv) based on the [Factual Inconsistency Benchmark](https://huggingface.co/datasets/r-three/fib). It contains 50 news articles, each with two summaries. One is factually consistent while the other is not. Perfect for AlignEval’s binary labeling task.\n\nAfter uploading the CSV, you’ll see the data in a flexible table view. Adjust the width of the browser to control how wide or narrow you want the table and columns to be.\n\n![How the table view looks after uploading a CSV file.](https://eugeneyan.com/assets/table.webp \"How the table view looks after uploading a CSV file.\")\n\nHow the table view looks after uploading a CSV file.\n\n## Labeling mode: Look at the data\n\n**In labeling mode, our only job is to look at the data.** AlignEval simplifies the process to a comparison between input and output fields. And for each sample, we only have to make a binary decision—pass or fail.\n\n![Instructions for labeling mode.](https://eugeneyan.com/assets/labeling.webp \"Instructions for labeling mode.\")\n\nInstructions for labeling mode.\n\nWhy binary labels? It leads to more accurate data, takes less time, and keeps cognitive load low. [DoorDash adopted a similar approach](https://eugeneyan.com/writing/content-moderation/#collecting-ground-truth-via-human-in-the-loop). Similarly, [Llama2](https://arxiv.org/abs/2307.09288) focused on collecting binary human preferences. The Llama2 authors also [shared at a meetup](https://www.youtube.com/live/CzR3OrOkM9w?si=gTrb6M44O941wrqb&t=913) that collecting binary preferences was much faster than writing samples for supervised finetuning.\n\nWhat’s important is that we look at the data with an open mind, and not write criteria based on our priors. We should also resist the urge to prematurely define criteria. We need to first immerse ourselves in the data. This reduces the risk of hunting for nonexistent defects or chasing unrealistic expectations. By understanding what the LLM actually generates, we can define more meaningful, better-calibrated criteria.\n\n[YouTube video player](https://www.youtube.com/watch?v=nnhMnNh_ZvU)\n\nLook at your data remix, courtesy of Jason Liu. Sign up for his next RAG course [here](https://maven.com/s/course/b9b1f32583).\n\nAfter labeling 20 rows, we unlock evaluation mode. (While evaluation mode is available after 20 labels, I think it’s too little to understand the data well enough to define criteria, and too small a sample for evaluation. I suggest aiming for 50-100 before writing criteria and running evals. The more familiar you are with your data, the better your evals will be.)\n\n## Evaluation mode: Write criteria, evaluate the evaluator\n\n**After unlocking evaluation mode, we can now write our task-specific evaluation criteria.** Keep it simple: Evaluate on a single dimension and return either 0 (pass) or 1 (fail).\n\n![Instructions for evaluation mode.](https://eugeneyan.com/assets/evaluation.webp \"Instructions for evaluation mode.\")\n\nInstructions for evaluation mode.\n\nHere’s the prompt I use for factual inconsistency classification. We start with only two sentences that define what a pass or fail looks like. We can refine it after running the LLM-evaluator and examining the explanations and predictions.\n\n![Writing the prompt and selecting models and fields.](https://eugeneyan.com/assets/prompt.webp \"Writing the prompt and selecting models and fields.\")\n\nWriting the prompt and selecting models and fields.\n\nNext, we choose our model (gpt-4o-mini or claude-3-haiku) and input fields (both input and output, or output only). For most tasks, (e.g., classification, summarization, Q&A), we want to compare the output to the input. Nonetheless, for some tasks, like evaluating style guide adherence or tone of voice, we only need the output. Focusing solely on the output reduces token cost and latency while potentially improving performance.\n\n**It’s also crucial to evaluate our LLM-evaluators against the labeled data.** Hit “Evaluate” and we’ll see metrics in the top right corner: sample size, recall, precision, F1, Cohen’s $\\\\kappa$, and counts for true and false positives/negatives.\n\n![Metrics to evaluate the LLM-evaluator.](https://eugeneyan.com/assets/metrics.webp \"Metrics to evaluate the LLM-evaluator.\")\n\nMetrics to evaluate the LLM-evaluator.\n\nIf our sample size is low (e.g., 20), the metrics can fluctuate a lot, even when we rerun the same LLM-evaluator. This is due to LLM stochasticity. We can improve stability by labeling more data. Aim for at least 50 labeled samples which also unlocks optimization mode.\n\n## Optimization mode: Semi-automated improvements 🤞\n\n**Optimization mode is where the magic (hopefully) happens.** Click “Optimize” and let AlignEval do its thing to improve your LLM-evaluator. While it’s still in early beta, it’s achieved decent improvements for several use cases. (And no, it’s not dspy.)\n\n![Instructions for optimization mode.](https://eugeneyan.com/assets/optimization.webp \"Instructions for optimization mode.\")\n\nInstructions for optimization mode.\n\nUnder the hood, optimization splits the labeled data into dev and test sets. (Since we’re not training a model, we call it a development split instead of a training split.) It then runs $n$ trials on the dev set to improve F1, using the same LLM and fields from evaluation mode. After $n$ trials, the improved LLM-evaluator is evaled on the unseen test set. In the table below, F1 starts at 0.571, increases to 0.722 at trial 5 on dev, then gets F1=0.727 on test.\n\n![Metrics from optimization trials on all, dev, and test splits.](https://eugeneyan.com/assets/optimize-table.webp \"Metrics from optimization trials on all, dev, and test splits.\")\n\nMetrics from optimization trials on all, dev, and test splits.\n\nSometimes, the dev and test metrics diverge significantly. With the small sample size (25 per split, 50 in total), this likely stems from the dev split being unrepresentative of the full data distribution, including the test split. Thus, the LLM-evaluator may overfit on the dev set and fail to generalize to the test set.\n\nTo improve generalization across the dev and test splits, we can label more data, ensuring that it’s diverse and representative. To be diverse is to be balanced in both 0 and 1 labels, and the variety of inputs and outputs. To be representative is to be similar to real-world examples. We can also run multiple LLM-evaluators in parallel and ensemble their scores. [PoLL](https://arxiv.org/abs/2404.18796) showed that an ensemble of three smaller LLM-evaluators outperformed gpt-4.\n\nFor a visual walkthrough, here’s a demo where I look at and label 50 rows of data lol.\n\n[YouTube video player](https://www.youtube.com/watch?v=OJJ7aHIBtPw)\n\n## Behind the scenes: How AlignEval was built\n\nAs a frontend development newbie (this is my first TypeScript app), I relied on the wisdom of the crowd, polling [Twitter](https://x.com/eugeneyan/status/1828447283811402006) and [LinkedIn](https://www.linkedin.com/posts/eugeneyan_if-you-were-building-a-small-web-app-today-activity-7234224078675439617-HX75/) for advice. I also [built the same app five times](https://eugeneyan.com/writing/web-frameworks/) to get a feel for several frameworks. Specifically, I tried FastHTML, Next.js, SvelteKit, and FastAPI + HTML (my go-to for prototyping).\n\nFrom early prototypes, Next.js seemed to be a good fit. Intuitive, scalable, and the perfect excuse to learn TypeScript. And thanks to Cursor, my beginner questions were answered easily: What’s the difference between `const` and `let`? What is a `prop`? It also made it easier to build UX components and fix bugs 2-3x faster, keeping velocity and motivation high.\n\nFor the backend, Python + FastAPI was the obvious choice. Python’s ecosystem of data analysis and machine learning made it easier to experiment with optimization mode.\n\nWhen it came to LLMs, I went with the smallest models from the biggest labs: gpt-4o-mini and claude-3-haiku (soon to be claude-3.5-haiku). They’re cheap, fast, and good enough for binary classification LLM-evaluators. It’s the perfect combo for a free app. And with each release, these small models just keep getting more and more capable.\n\nFor gpt-4o-mini, I used the [structured output](https://platform.openai.com/docs/guides/structured-outputs) functionality that’s still in beta. It allows users to define the desired output schema using Zod (JavaScript) or Pydantic (Python). Here’s an example in Javascript and its equivalent in Python:\n\n```javascript\nimport { z } from \"zod\";\n\nconst EvaluationResponse = z.object({\n  explanation: z.string(),\n  prediction: z.string().refine((val) => val === '0' || val === '1')\n});\n```\n\n```python\nfrom pydantic import BaseModel\n\nclass EvaluationResponse(BaseModel):\n  explanation: str\n  prediction: str\n```\n\nFor claude-3-haiku, I [defined the XML output](https://eugeneyan.com/writing/prompting/#structured-input-and-output) via the prompt. From what I’ve seen on the job and in AlignEval, this works 99.9% of the time. Here’s the prompt I used for AlignEval.\n\n```plaintext\nEvaluate the output based on the provided criteria. First, in the <sketchpad> provided, think through your evaluation step by step.\n\nThen, provide a binary prediction (0 or 1) within <prediction>.\n```\n\nTo decide on hosting, I conducted a [Twitter poll](https://x.com/eugeneyan/status/1828450401311764741) and consulted documentation for options like Dokku, Coolify, Vercel, etc. [Hamel’s write-up on Dokku](https://hamel.dev/blog/posts/dokku/) was particularly helpful. While I initially considered hosting the app on Hetzner with Dokku (another opportunity to learn something new), I ultimately decided to take the lazy path (read: prioritize ruthlessly) and start with hosting on Railway. It was a good decision.\n\nHosting on Railway has been a breeze. After setting up the project via their web UI and linking it via the CLI (`railway link`), pushing new updates is as simple as `railway up`. Each update takes one to two minutes. Railway also comes with several database options, including Postgres and Redis. While a VPS might be cheaper in the long run, at this stage, [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it). Fiddling with a VPS would have been a distraction from improving the UX, implementing features, and figuring out optimization mode.\n\n![My simple Railway setup.](https://eugeneyan.com/assets/railway.webp \"My simple Railway setup.\")\n\nMy simple Railway setup\n\n> If you’d like to get started with Railway, please use my [affiliate code](https://railway.app?referralCode=fMY11k)—it’ll help with hosting costs and keep AlignEval running!\n\nGoing from a laptop prototype to a public beta took some effort. The work included:\n\n- Splitting into Next.js frontend, FastAPI backend, migrating from SQLite to Postgres\n- Implementing real-time feedback for labeling, evaluation, and optimization\n- Adding features to simplify labeling (pass/fail buttons), customize models and fields (dropdowns, integration with model providers), and more\n- Improving the UI (as rough as it looks now, it was way worse before lol)\n- Squashing lots of bugs (and probably missing many more)\n\nHere’s the to-do list I had; there were many more tasks and bugs that I neglected to track. Because I timeboxed the effort to October, a few items were not completed. But if AlignEval gains traction and proves useful, I might revisit the list.\n\n```markdown\n## Todos\n- [x] Split deployment into frontend and backend\n  - https://help.railway.app/questions/how-to-expose-a-fast-api-backend-service-a1712631\n  - https://help.railway.app/questions/econnrefused-when-calling-another-servic-5662d969#p-1\n  - ~~[ ] Or consider something like this: https://vercel.com/templates/next.js/nextjs-fastapi-starter~~ (only works with Vercel)\n- [x] Add external database (Railway Postgres)\n- [x] Write interface to ensure compatibility between SQLite and Postgres\n- [x] Connect label-app to Railway Postgres\n- [x] Connect label-hpo to Railway Postgres\n- [x] Show feedback to users while optimization is running\n- [x] Update table names to use CSV file name\n- [x] Add support for multiple tables\n- [x] Replace tablename parameter with filename parameter\n- [x] Uploading a CSV file with an existing name should return the existing table\n- [x] Add llm-evaluator optimization to label-hpo\n- [x] Add dropdown to select evaluation fields\n- [x] Add ability to delete optimization table, with password\n- [x] Add progress bar to gamify labeling, evaluation and optimization\n- [x] Add buttons to click pass and fail\n- [x] Add usage count (upload (files, rows), evaluate (files, rows), optimize (files, trials))\n- [x] Add info button intro next to header to explain the site\n- [x] Update favicon\n- [x] Add support for OpenAI models\n- [x] Add balanced val-test split for optimization\n- [x] Add columns to optimization metrics: split (dev, test), model, evaluation_fields\n- [x] Set max upload rows to env variable\n- [ ] Fix bug where alert pops up: \"Failed to handle optimization: Unexpected token '<', \"<!DOCTYPE \"... is not valid JSON\"\n  - Still pops up every now and then\n- [x] Fix bug where evaluation alert pops up even when optimization is unlocked\n- [x] Add popup to see full prompt\n- [x] Add button to copy prompt\n- [x] Add completion bar to optimization sticky\n- [x] Make site look retro and like a game\n- [x] Prompt users to rename sample data before downloading\n- [x] Update delete optimization to reset optimization, and set password to Twitter handle\n- [x] Add \"don't show me this again\" checkbox\n- [ ] Improve how we poll for data\n- [ ] Add button to donate with Stripe payment\n- [ ] Add button to optimize more with Stripe payment\n\n## Todos (OE)\n- [x] Separate data definitions to definitions.ts\n- [x] Separate fetch data queries to data.ts\n- [x] Separate data mutations to actions.ts\n- [x] Separate components into component.tsx under ui which are imported in page.tsx\n```\n\nBuilding AlignEval has been a ton of fun, and writing about the journey was a great way to reflect. It’s easily my favorite project of the year, because I got to build an app and learn a lot along the way. (That said, there’s still November and December to try to beat this.)\n\nIf you’re tired of spending days or weeks on evals and aligning LLM-evaluators, try [AlignEval](http://aligneval.com). It’s designed to streamline your workflow and focus on what matters most:\n\n- Looking at the data and adding labels\n- Defining meaningful criteria\n- Evaluating the LLM-evaluator\n- Semi-automatically optimizing it\n\nHere’s the [code](https://github.com/eugeneyan/align-app) in case you want to deploy your own instance. It’s my first TypeScript project, so feedback welcome! How could I have organized it better? What best practices should I have followed? And beyond the code, what have you found useful when building LLM-evaluators? Comment below or [DM me](https://x.com/eugeneyan).\n\nThanks to the incredible folks who made discussing this topic so much fun, and provided helpful feedback at early demos: Shreya Shankar, Hamel Husain, Kyle Corbitt, David Corbitt, Saumya Gandhi, Swyx, Eugene Cheah, and Dennis Taylor. And thanks to Hamel Husain for reading early drafts of this piece.\n\nBy the way, if you want to learn more about evals, my friends Hamel and Shreya are hosting their *final* cohort of “AI Evals for Engineers and PMs” in July. Here’s a [35% discount code](https://maven.com/parlance-labs/evals?promoCode=eugene-is-all-you-need).\n\n## Further reading\n\n- [Your AI Product Needs Evals](https://hamel.dev/blog/posts/evals/)\n- [Task-Specific LLM Evals that Do & Don’t Work](https://eugeneyan.com/writing/evals/)\n- [Who Validates the Validators](https://arxiv.org/abs/2404.12272)\n- [Evaluating the Effectiveness of LLM-Evaluators](https://eugeneyan.com/writing/llm-evaluators/)\n- [Creating a LLM-as-a-Judge That Drives Business Results](https://hamel.dev/llm-judge)\n\nIf you found this useful, please cite this write-up as:\n\n> Yan, Ziyou. (Oct 2024). AlignEval: Building an App to Make Evals Easy, Fun, and Automated. eugeneyan.com. https://eugeneyan.com/writing/aligneval/.\n\nor\n\n```\n@article{yan2024aligneval,\n  title   = {AlignEval: Building an App to Make Evals Easy, Fun, and Automated},\n  author  = {Yan, Ziyou},\n  journal = {eugeneyan.com},\n  year    = {2024},\n  month   = {Oct},\n  url     = {https://eugeneyan.com/writing/aligneval/}\n}\n```\n\nShare on:\n\n![](https://eugeneyan.com/assets/icon-twitter.svg)\n\n![](https://eugeneyan.com/assets/icon-linkedin.svg)\n\n![](https://eugeneyan.com/assets/bluesky.svg)\n\n![](https://eugeneyan.com/assets/icon-facebook.svg)\n\n![](https://eugeneyan.com/assets/icon-mail.svg)\n\nJoin **11,800+** readers getting updates on machine learning, RecSys, LLMs, and engineering.\n",
  "authors": [],
  "captured_at": "2026-09-27T19:29:15.293681927+00:00",
  "category": "programming",
  "content_html": "<p>Go to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://aligneval.com\">aligneval.com</a> to start building your own LLM-evaluator; sample data included.</p>\n<blockquote>\n<p><strong>Update</strong>: Now <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://blog.langchain.com/introducing-align-evals/\">integrated with LangSmith</a> and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://venturebeat.com/ai/langchains-align-evals-closes-the-evaluator-trust-gap-with-prompt-level-calibration\">featured on VentureBeat</a>! 🎉</p>\n</blockquote>\n<p>Every AI-powered product needs evals. But let’s face it—they’re a pain to build, hard to scale, and most teams get them wrong. As a result, many AI-powered experiences are bottlenecked on evals, sometimes delaying launches by weeks or even months.</p>\n<p>I’ve spent the past year or so wrestling with product/task-specific evals. Testing different ways to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://eugeneyan.com/writing/abstractive/\">detect</a> hallucinations, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://eugeneyan.com/writing/finetuning/\">finetune</a> evaluators, and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://eugeneyan.com/writing/llm-evaluators/\">evaluate</a> LLM-based evaluators. There were dead ends. There were rabbit holes. But I learned <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://eugeneyan.com/writing/evals/\">what works and what doesn’t</a>.</p>\n<p>And that’s why I’m excited to introduce <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://aligneval.com/\">AlignEval</a>, an app that makes evals easy and… fun? (Okay, I’ll settle for less painful.) It also tries to automate part of the process. AlignEval makes building LLM-evaluators as straightforward as four simple steps:</p>\n<ul>\n<li>Upload a CSV file with columns for input and output.</li>\n<li>Look at the data and label samples as pass or fail.</li>\n<li>Define eval criteria, run the LLM-evaluator, check results.</li>\n<li>Optimize the LLM-evaluator with dev-test splits.</li>\n</ul>\n<p>By the way, if you want to learn more about evals, my friends Hamel and Shreya are hosting their <em>final</em> cohort of “AI Evals for Engineers and PMs” in July. Here’s a <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://maven.com/parlance-labs/evals?promoCode=eugene-is-all-you-need\">35% discount code</a>.</p>\n<h2>Align AI to human. Calibrate human to AI. Repeat.</h2>\n<p>The key insight is that aligning AI to human preferences is only half the battle. To build effective evals, we must also <strong>calibrate human criteria to AI output</strong>.</p>\n<p>Many teams make the mistake of crafting elaborate eval criteria without first <strong>looking at the data</strong>. It’s like theorizing about user experience and defects from the ivory tower, without doing error analysis. From <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2404.12272\">Who Validates the Validators</a>: “<em>It is impossible to completely determine evaluation criteria prior to human judging of LLM outputs.</em>”</p>\n<p>This leads to two types of bad criteria. First, irrelevant criteria that are a waste of time, such as generic metrics (e.g., helpfulness) or very low probability defects (e.g., grammar, spelling). Second, unrealistic, unattainable criteria that the technology isn’t ready for, such as autonomous agents back in 2023. Either way, teams squander effort that would have been better invested in evaluating actual defects that occur with moderate frequency.</p>\n<p>(I think out-of-the-box eval solutions fuel this problem, pushing generic criteria that are easy to plug and play but ignore the idiosyncrasies of your domain, product, and data.)</p>\n<p>The way to solve this—and build useful evals—is to work backward from the data. That’s what <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://aligneval.com/\">AlignEval</a> tries to do by putting data and metrics first, not preset criteria or LLMs.</p>\n<h2>Getting started: Upload some data</h2>\n<p><strong>To get started, upload a CSV file containing the following columns:</strong></p>\n<ul>\n<li><strong>id</strong>: Lets you match labeled and evaluated samples to your data</li>\n<li><strong>input</strong>: The context provided to the LLM to generate the output (e.g., text to classify, news to summarize, retrieved documents for answering questions)</li>\n<li><strong>output</strong>: What the LLM generates (e.g., classification label, summary, answer)</li>\n<li><strong>label</strong>: Your judgment on whether the output passes (0) or fails (1)</li>\n</ul>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/upload.webp\" alt=\"Instructions to upload a CSV file.\" title=\"Instructions to upload a CSV file.\" loading=\"eager\" decoding=\"async\" fetchpriority=\"high\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Instructions to upload a CSV file.</p>\n<p>If you don’t have a CSV handy, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://aligneval.com/data/fib50.csv\">download a sample</a> based on the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/datasets/r-three/fib\">Factual Inconsistency Benchmark</a>. It contains 50 news articles, each with two summaries. One is factually consistent while the other is not. Perfect for AlignEval’s binary labeling task.</p>\n<p>After uploading the CSV, you’ll see the data in a flexible table view. Adjust the width of the browser to control how wide or narrow you want the table and columns to be.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/table.webp\" alt=\"How the table view looks after uploading a CSV file.\" title=\"How the table view looks after uploading a CSV file.\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>How the table view looks after uploading a CSV file.</p>\n<h2>Labeling mode: Look at the data</h2>\n<p><strong>In labeling mode, our only job is to look at the data.</strong> AlignEval simplifies the process to a comparison between input and output fields. And for each sample, we only have to make a binary decision—pass or fail.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/labeling.webp\" alt=\"Instructions for labeling mode.\" title=\"Instructions for labeling mode.\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Instructions for labeling mode.</p>\n<p>Why binary labels? It leads to more accurate data, takes less time, and keeps cognitive load low. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://eugeneyan.com/writing/content-moderation/#collecting-ground-truth-via-human-in-the-loop\">DoorDash adopted a similar approach</a>. Similarly, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2307.09288\">Llama2</a> focused on collecting binary human preferences. The Llama2 authors also <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.youtube.com/live/CzR3OrOkM9w?si=gTrb6M44O941wrqb&amp;t=913\">shared at a meetup</a> that collecting binary preferences was much faster than writing samples for supervised finetuning.</p>\n<p>What’s important is that we look at the data with an open mind, and not write criteria based on our priors. We should also resist the urge to prematurely define criteria. We need to first immerse ourselves in the data. This reduces the risk of hunting for nonexistent defects or chasing unrealistic expectations. By understanding what the LLM actually generates, we can define more meaningful, better-calibrated criteria.</p>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.youtube.com/watch?v=nnhMnNh_ZvU\">YouTube video player</a></p>\n<p>Look at your data remix, courtesy of Jason Liu. Sign up for his next RAG course <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://maven.com/s/course/b9b1f32583\">here</a>.</p>\n<p>After labeling 20 rows, we unlock evaluation mode. (While evaluation mode is available after 20 labels, I think it’s too little to understand the data well enough to define criteria, and too small a sample for evaluation. I suggest aiming for 50-100 before writing criteria and running evals. The more familiar you are with your data, the better your evals will be.)</p>\n<h2>Evaluation mode: Write criteria, evaluate the evaluator</h2>\n<p><strong>After unlocking evaluation mode, we can now write our task-specific evaluation criteria.</strong> Keep it simple: Evaluate on a single dimension and return either 0 (pass) or 1 (fail).</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/evaluation.webp\" alt=\"Instructions for evaluation mode.\" title=\"Instructions for evaluation mode.\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Instructions for evaluation mode.</p>\n<p>Here’s the prompt I use for factual inconsistency classification. We start with only two sentences that define what a pass or fail looks like. We can refine it after running the LLM-evaluator and examining the explanations and predictions.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/prompt.webp\" alt=\"Writing the prompt and selecting models and fields.\" title=\"Writing the prompt and selecting models and fields.\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Writing the prompt and selecting models and fields.</p>\n<p>Next, we choose our model (gpt-4o-mini or claude-3-haiku) and input fields (both input and output, or output only). For most tasks, (e.g., classification, summarization, Q&amp;A), we want to compare the output to the input. Nonetheless, for some tasks, like evaluating style guide adherence or tone of voice, we only need the output. Focusing solely on the output reduces token cost and latency while potentially improving performance.</p>\n<p><strong>It’s also crucial to evaluate our LLM-evaluators against the labeled data.</strong> Hit “Evaluate” and we’ll see metrics in the top right corner: sample size, recall, precision, F1, Cohen’s <span class=\"math\">κ</span>, and counts for true and false positives/negatives.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/metrics.webp\" alt=\"Metrics to evaluate the LLM-evaluator.\" title=\"Metrics to evaluate the LLM-evaluator.\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Metrics to evaluate the LLM-evaluator.</p>\n<p>If our sample size is low (e.g., 20), the metrics can fluctuate a lot, even when we rerun the same LLM-evaluator. This is due to LLM stochasticity. We can improve stability by labeling more data. Aim for at least 50 labeled samples which also unlocks optimization mode.</p>\n<h2>Optimization mode: Semi-automated improvements 🤞</h2>\n<p><strong>Optimization mode is where the magic (hopefully) happens.</strong> Click “Optimize” and let AlignEval do its thing to improve your LLM-evaluator. While it’s still in early beta, it’s achieved decent improvements for several use cases. (And no, it’s not dspy.)</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/optimization.webp\" alt=\"Instructions for optimization mode.\" title=\"Instructions for optimization mode.\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Instructions for optimization mode.</p>\n<p>Under the hood, optimization splits the labeled data into dev and test sets. (Since we’re not training a model, we call it a development split instead of a training split.) It then runs <span class=\"math\">n</span> trials on the dev set to improve F1, using the same LLM and fields from evaluation mode. After <span class=\"math\">n</span> trials, the improved LLM-evaluator is evaled on the unseen test set. In the table below, F1 starts at 0.571, increases to 0.722 at trial 5 on dev, then gets F1=0.727 on test.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/optimize-table.webp\" alt=\"Metrics from optimization trials on all, dev, and test splits.\" title=\"Metrics from optimization trials on all, dev, and test splits.\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Metrics from optimization trials on all, dev, and test splits.</p>\n<p>Sometimes, the dev and test metrics diverge significantly. With the small sample size (25 per split, 50 in total), this likely stems from the dev split being unrepresentative of the full data distribution, including the test split. Thus, the LLM-evaluator may overfit on the dev set and fail to generalize to the test set.</p>\n<p>To improve generalization across the dev and test splits, we can label more data, ensuring that it’s diverse and representative. To be diverse is to be balanced in both 0 and 1 labels, and the variety of inputs and outputs. To be representative is to be similar to real-world examples. We can also run multiple LLM-evaluators in parallel and ensemble their scores. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2404.18796\">PoLL</a> showed that an ensemble of three smaller LLM-evaluators outperformed gpt-4.</p>\n<p>For a visual walkthrough, here’s a demo where I look at and label 50 rows of data lol.</p>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.youtube.com/watch?v=OJJ7aHIBtPw\">YouTube video player</a></p>\n<h2>Behind the scenes: How AlignEval was built</h2>\n<p>As a frontend development newbie (this is my first TypeScript app), I relied on the wisdom of the crowd, polling <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/eugeneyan/status/1828447283811402006\">Twitter</a> and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.linkedin.com/posts/eugeneyan_if-you-were-building-a-small-web-app-today-activity-7234224078675439617-HX75/\">LinkedIn</a> for advice. I also <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://eugeneyan.com/writing/web-frameworks/\">built the same app five times</a> to get a feel for several frameworks. Specifically, I tried FastHTML, Next.js, SvelteKit, and FastAPI + HTML (my go-to for prototyping).</p>\n<p>From early prototypes, Next.js seemed to be a good fit. Intuitive, scalable, and the perfect excuse to learn TypeScript. And thanks to Cursor, my beginner questions were answered easily: What’s the difference between <code>const</code> and <code>let</code>? What is a <code>prop</code>? It also made it easier to build UX components and fix bugs 2-3x faster, keeping velocity and motivation high.</p>\n<p>For the backend, Python + FastAPI was the obvious choice. Python’s ecosystem of data analysis and machine learning made it easier to experiment with optimization mode.</p>\n<p>When it came to LLMs, I went with the smallest models from the biggest labs: gpt-4o-mini and claude-3-haiku (soon to be claude-3.5-haiku). They’re cheap, fast, and good enough for binary classification LLM-evaluators. It’s the perfect combo for a free app. And with each release, these small models just keep getting more and more capable.</p>\n<p>For gpt-4o-mini, I used the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://platform.openai.com/docs/guides/structured-outputs\">structured output</a> functionality that’s still in beta. It allows users to define the desired output schema using Zod (JavaScript) or Pydantic (Python). Here’s an example in Javascript and its equivalent in Python:</p>\n<pre><code class=\"language-javascript\"><span class=\"code-snippet\" data-language=\"JavaScript\"><span class=\"syntax-source syntax-js\"><span class=\"syntax-meta syntax-import syntax-js\"><span class=\"syntax-keyword syntax-control syntax-import-export syntax-js\">import</span> <span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">z</span> <span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span> <span class=\"syntax-keyword syntax-control syntax-import-export syntax-js\">from</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&quot;</span>zod<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n\n<span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">EvaluationResponse</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-object syntax-js\">z</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">object</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-meta syntax-object-literal syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span>\n  <span class=\"syntax-meta syntax-object-literal syntax-key syntax-js\">explanation</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-js\">:</span> <span class=\"syntax-variable syntax-other syntax-object syntax-js\">z</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">string</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span>\n  <span class=\"syntax-meta syntax-object-literal syntax-key syntax-js\">prediction</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-js\">:</span> <span class=\"syntax-variable syntax-other syntax-object syntax-js\">z</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">string</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">refine</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-meta syntax-function syntax-declaration syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-js\">(</span><span class=\"syntax-variable syntax-parameter syntax-function syntax-js\">val</span><span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-js\">)</span><span class=\"syntax-meta syntax-function syntax-declaration syntax-js\"> </span><span class=\"syntax-storage syntax-type syntax-function syntax-arrow syntax-js\">=&gt;</span></span> <span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">val</span> <span class=\"syntax-keyword syntax-operator syntax-comparison syntax-js\">===</span> <span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>0<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-js\">||</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">val</span> <span class=\"syntax-keyword syntax-operator syntax-comparison syntax-js\">===</span> <span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>1<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span>\n<span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n</span></span></code></pre>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-meta syntax-statement syntax-import syntax-python\"><span class=\"syntax-keyword syntax-control syntax-import syntax-from syntax-python\">from</span></span><span class=\"syntax-meta syntax-statement syntax-import syntax-python\"><span class=\"syntax-meta syntax-import-source syntax-python\"> <span class=\"syntax-meta syntax-import-path syntax-python\"><span class=\"syntax-meta syntax-import-name syntax-python\">pydantic</span></span> <span class=\"syntax-meta syntax-statement syntax-import syntax-python\"><span class=\"syntax-keyword syntax-control syntax-import syntax-python\">import</span></span></span></span><span class=\"syntax-meta syntax-statement syntax-import syntax-python\"></span><span class=\"syntax-meta syntax-statement syntax-import syntax-python\"> <span class=\"syntax-meta syntax-generic-name syntax-python\">BaseModel</span></span>\n\n<span class=\"syntax-meta syntax-class syntax-python\"><span class=\"syntax-storage syntax-type syntax-class syntax-python\">class</span> <span class=\"syntax-entity syntax-name syntax-class syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">EvaluationResponse</span></span><span class=\"syntax-meta syntax-class syntax-inheritance syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-inheritance syntax-begin syntax-python\">(</span></span></span><span class=\"syntax-meta syntax-class syntax-inheritance syntax-python\"><span class=\"syntax-entity syntax-other syntax-inherited-class syntax-python\">BaseModel</span><span class=\"syntax-punctuation syntax-section syntax-inheritance syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-class syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-class syntax-begin syntax-python\">:</span></span>\n  <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">explanation</span></span><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-variable syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">str</span></span>\n  <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">prediction</span></span><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-variable syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">str</span></span>\n</span></span></code></pre>\n<p>For claude-3-haiku, I <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://eugeneyan.com/writing/prompting/#structured-input-and-output\">defined the XML output</a> via the prompt. From what I’ve seen on the job and in AlignEval, this works 99.9% of the time. Here’s the prompt I used for AlignEval.</p>\n<pre><code class=\"language-plaintext\"><span class=\"code-snippet\">Evaluate the output based on the provided criteria. First, in the &lt;sketchpad&gt; provided, think through your evaluation step by step.\n\nThen, provide a binary prediction (0 or 1) within &lt;prediction&gt;.\n</span></code></pre>\n<p>To decide on hosting, I conducted a <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/eugeneyan/status/1828450401311764741\">Twitter poll</a> and consulted documentation for options like Dokku, Coolify, Vercel, etc. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://hamel.dev/blog/posts/dokku/\">Hamel’s write-up on Dokku</a> was particularly helpful. While I initially considered hosting the app on Hetzner with Dokku (another opportunity to learn something new), I ultimately decided to take the lazy path (read: prioritize ruthlessly) and start with hosting on Railway. It was a good decision.</p>\n<p>Hosting on Railway has been a breeze. After setting up the project via their web UI and linking it via the CLI (<code>railway link</code>), pushing new updates is as simple as <code>railway up</code>. Each update takes one to two minutes. Railway also comes with several database options, including Postgres and Redis. While a VPS might be cheaper in the long run, at this stage, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it\">YAGNI</a>. Fiddling with a VPS would have been a distraction from improving the UX, implementing features, and figuring out optimization mode.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/railway.webp\" alt=\"My simple Railway setup.\" title=\"My simple Railway setup.\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>My simple Railway setup</p>\n<blockquote>\n<p>If you’d like to get started with Railway, please use my <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://railway.app?referralCode=fMY11k\">affiliate code</a>—it’ll help with hosting costs and keep AlignEval running!</p>\n</blockquote>\n<p>Going from a laptop prototype to a public beta took some effort. The work included:</p>\n<ul>\n<li>Splitting into Next.js frontend, FastAPI backend, migrating from SQLite to Postgres</li>\n<li>Implementing real-time feedback for labeling, evaluation, and optimization</li>\n<li>Adding features to simplify labeling (pass/fail buttons), customize models and fields (dropdowns, integration with model providers), and more</li>\n<li>Improving the UI (as rough as it looks now, it was way worse before lol)</li>\n<li>Squashing lots of bugs (and probably missing many more)</li>\n</ul>\n<p>Here’s the to-do list I had; there were many more tasks and bugs that I neglected to track. Because I timeboxed the effort to October, a few items were not completed. But if AlignEval gains traction and proves useful, I might revisit the list.</p>\n<pre><code class=\"language-markdown\"><span class=\"code-snippet\" data-language=\"Markdown\"><span class=\"syntax-text syntax-html syntax-markdown\"><span class=\"syntax-meta syntax-block-level syntax-markdown\"><span class=\"syntax-markup syntax-heading syntax-2 syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-heading syntax-begin syntax-markdown\">##</span> </span><span class=\"syntax-markup syntax-heading syntax-2 syntax-markdown\"><span class=\"syntax-entity syntax-name syntax-section syntax-markdown\">Todos</span><span class=\"syntax-meta syntax-whitespace syntax-newline syntax-markdown\">\n</span></span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"> <span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\">[x] Split deployment into frontend and backend\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\">  </span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> <span class=\"syntax-markup syntax-underline syntax-link syntax-markdown\">https://help.railway.app</span><span class=\"syntax-markup syntax-underline syntax-link syntax-markdown\">/questions/how-to-expose-a-fast-api-backend-service-a1712631</span>\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\">  </span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> <span class=\"syntax-markup syntax-underline syntax-link syntax-markdown\">https://help.railway.app</span><span class=\"syntax-markup syntax-underline syntax-link syntax-markdown\">/questions/econnrefused-when-calling-another-servic-5662d969#p-1</span>\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\">  </span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> ~~[ ] Or consider something like this: <span class=\"syntax-markup syntax-underline syntax-link syntax-markdown\">https://vercel.com</span><span class=\"syntax-markup syntax-underline syntax-link syntax-markdown\">/templates/next</span><span class=\"syntax-markup syntax-underline syntax-link syntax-markdown\">.</span><span class=\"syntax-markup syntax-underline syntax-link syntax-markdown\">js/nextjs-fastapi-starter</span>~~ (only works with Vercel)\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Add external database (Railway Postgres)\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Write interface to ensure compatibility between SQLite and Postgres\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Connect label-app to Railway Postgres\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Connect label-hpo to Railway Postgres\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Show feedback to users while optimization is running\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Update table names to use CSV file name\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Add support for multiple tables\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Replace tablename parameter with filename parameter\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Uploading a CSV file with an existing name should return the existing table\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Add llm-evaluator optimization to label-hpo\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Add dropdown to select evaluation fields\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Add ability to delete optimization table, with password\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Add progress bar to gamify labeling, evaluation and optimization\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Add buttons to click pass and fail\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Add usage count (upload (files, rows), evaluate (files, rows), optimize (files, trials))\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Add info button intro next to header to explain the site\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Update favicon\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Add support for OpenAI models\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Add balanced val-test split for optimization\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Add columns to optimization metrics: split (dev, test), model, evaluation_fields\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [x] Set max upload rows to env variable\n</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-bullet syntax-markdown\"><span class=\"syntax-punctuation syntax-definition syntax-list_item syntax-markdown\">-</span></span><span class=\"syntax-markup syntax-list syntax-unnumbered syntax-markdown\"><span class=\"syntax-meta syntax-paragraph syntax-list syntax-markdown\"> [ ] Fix bug where alert pops up: &quot;Failed to handle optimization: Unexpected token &#39;<span class=\"syntax-meta syntax-other syntax-valid-bracket syntax-markdown\">&lt;</span>&#39;, &quot;<span class=\"syntax-meta syntax-tag syntax-sgml syntax-html\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-html\">&lt;!</span><span class=\"syntax-meta syntax-tag syntax-sgml syntax-doctype syntax-html\"><span class=\"syntax-entity syntax-name syntax-tag syntax-doctype syntax-html\">DOCTYPE</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-doctype syntax-identifiers-and-DTDs syntax-html\">&quot;... is not valid JSON&quot;</span>\n  - Still pops up every now and then\n- [x] Fix bug where evaluation alert pops up even when optimization is unlocked\n- [x] Add popup to see full prompt\n- [x] Add button to copy prompt\n- [x] Add completion bar to optimization sticky\n- [x] Make site look retro and like a game\n- [x] Prompt users to rename sample data before downloading\n- [x] Update delete optimization to reset optimization, and set password to Twitter handle\n- [x] Add <span class=\"syntax-string syntax-quoted syntax-double syntax-doctype syntax-identifiers-and-DTDs syntax-html\">&quot;don&#39;t show me this again&quot;</span> checkbox\n- [ ] Improve how we poll for data\n- [ ] Add button to donate with Stripe payment\n- [ ] Add button to optimize more with Stripe payment\n\n## Todos (OE)\n- [x] Separate data definitions to definitions.ts\n- [x] Separate fetch data queries to data.ts\n- [x] Separate data mutations to actions.ts\n- [x] Separate components into component.tsx under ui which are imported in page.tsx\n</span></span></span></span></span></span></code></pre>\n<p>Building AlignEval has been a ton of fun, and writing about the journey was a great way to reflect. It’s easily my favorite project of the year, because I got to build an app and learn a lot along the way. (That said, there’s still November and December to try to beat this.)</p>\n<p>If you’re tired of spending days or weeks on evals and aligning LLM-evaluators, try <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"http://aligneval.com\">AlignEval</a>. It’s designed to streamline your workflow and focus on what matters most:</p>\n<ul>\n<li>Looking at the data and adding labels</li>\n<li>Defining meaningful criteria</li>\n<li>Evaluating the LLM-evaluator</li>\n<li>Semi-automatically optimizing it</li>\n</ul>\n<p>Here’s the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/eugeneyan/align-app\">code</a> in case you want to deploy your own instance. It’s my first TypeScript project, so feedback welcome! How could I have organized it better? What best practices should I have followed? And beyond the code, what have you found useful when building LLM-evaluators? Comment below or <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/eugeneyan\">DM me</a>.</p>\n<p>Thanks to the incredible folks who made discussing this topic so much fun, and provided helpful feedback at early demos: Shreya Shankar, Hamel Husain, Kyle Corbitt, David Corbitt, Saumya Gandhi, Swyx, Eugene Cheah, and Dennis Taylor. And thanks to Hamel Husain for reading early drafts of this piece.</p>\n<p>By the way, if you want to learn more about evals, my friends Hamel and Shreya are hosting their <em>final</em> cohort of “AI Evals for Engineers and PMs” in July. Here’s a <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://maven.com/parlance-labs/evals?promoCode=eugene-is-all-you-need\">35% discount code</a>.</p>\n<h2>Further reading</h2>\n<ul>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://hamel.dev/blog/posts/evals/\">Your AI Product Needs Evals</a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://eugeneyan.com/writing/evals/\">Task-Specific LLM Evals that Do &amp; Don’t Work</a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2404.12272\">Who Validates the Validators</a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://eugeneyan.com/writing/llm-evaluators/\">Evaluating the Effectiveness of LLM-Evaluators</a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://hamel.dev/llm-judge\">Creating a LLM-as-a-Judge That Drives Business Results</a></li>\n</ul>\n<p>If you found this useful, please cite this write-up as:</p>\n<blockquote>\n<p>Yan, Ziyou. (Oct 2024). AlignEval: Building an App to Make Evals Easy, Fun, and Automated. eugeneyan.com. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://eugeneyan.com/writing/aligneval/\">https://eugeneyan.com/writing/aligneval/</a>.</p>\n</blockquote>\n<p>or</p>\n<pre><code><span class=\"code-snippet\">@article{yan2024aligneval,\n  title   = {AlignEval: Building an App to Make Evals Easy, Fun, and Automated},\n  author  = {Yan, Ziyou},\n  journal = {eugeneyan.com},\n  year    = {2024},\n  month   = {Oct},\n  url     = {https://eugeneyan.com/writing/aligneval/}\n}\n</span></code></pre>\n<p>Share on:</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/icon-twitter.svg\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/icon-linkedin.svg\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/bluesky.svg\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/icon-facebook.svg\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/icon-mail.svg\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Join <strong>11,800+</strong> readers getting updates on machine learning, RecSys, LLMs, and engineering.</p>\n",
  "content_markdown": "Go to [aligneval.com](https://aligneval.com) to start building your own LLM-evaluator; sample data included.\n\n> **Update**: Now [integrated with LangSmith](https://blog.langchain.com/introducing-align-evals/) and [featured on VentureBeat](https://venturebeat.com/ai/langchains-align-evals-closes-the-evaluator-trust-gap-with-prompt-level-calibration)! 🎉\n\nEvery AI-powered product needs evals. But let’s face it—they’re a pain to build, hard to scale, and most teams get them wrong. As a result, many AI-powered experiences are bottlenecked on evals, sometimes delaying launches by weeks or even months.\n\nI’ve spent the past year or so wrestling with product/task-specific evals. Testing different ways to [detect](https://eugeneyan.com/writing/abstractive/) hallucinations, [finetune](https://eugeneyan.com/writing/finetuning/) evaluators, and [evaluate](https://eugeneyan.com/writing/llm-evaluators/) LLM-based evaluators. There were dead ends. There were rabbit holes. But I learned [what works and what doesn’t](https://eugeneyan.com/writing/evals/).\n\nAnd that’s why I’m excited to introduce [AlignEval](https://aligneval.com/), an app that makes evals easy and… fun? (Okay, I’ll settle for less painful.) It also tries to automate part of the process. AlignEval makes building LLM-evaluators as straightforward as four simple steps:\n\n- Upload a CSV file with columns for input and output.\n- Look at the data and label samples as pass or fail.\n- Define eval criteria, run the LLM-evaluator, check results.\n- Optimize the LLM-evaluator with dev-test splits.\n\nBy the way, if you want to learn more about evals, my friends Hamel and Shreya are hosting their *final* cohort of “AI Evals for Engineers and PMs” in July. Here’s a [35% discount code](https://maven.com/parlance-labs/evals?promoCode=eugene-is-all-you-need).\n\n## Align AI to human. Calibrate human to AI. Repeat.\n\nThe key insight is that aligning AI to human preferences is only half the battle. To build effective evals, we must also **calibrate human criteria to AI output**.\n\nMany teams make the mistake of crafting elaborate eval criteria without first **looking at the data**. It’s like theorizing about user experience and defects from the ivory tower, without doing error analysis. From [Who Validates the Validators](https://arxiv.org/abs/2404.12272): “*It is impossible to completely determine evaluation criteria prior to human judging of LLM outputs.*”\n\nThis leads to two types of bad criteria. First, irrelevant criteria that are a waste of time, such as generic metrics (e.g., helpfulness) or very low probability defects (e.g., grammar, spelling). Second, unrealistic, unattainable criteria that the technology isn’t ready for, such as autonomous agents back in 2023. Either way, teams squander effort that would have been better invested in evaluating actual defects that occur with moderate frequency.\n\n(I think out-of-the-box eval solutions fuel this problem, pushing generic criteria that are easy to plug and play but ignore the idiosyncrasies of your domain, product, and data.)\n\nThe way to solve this—and build useful evals—is to work backward from the data. That’s what [AlignEval](https://aligneval.com/) tries to do by putting data and metrics first, not preset criteria or LLMs.\n\n## Getting started: Upload some data\n\n**To get started, upload a CSV file containing the following columns:**\n\n- **id**: Lets you match labeled and evaluated samples to your data\n- **input**: The context provided to the LLM to generate the output (e.g., text to classify, news to summarize, retrieved documents for answering questions)\n- **output**: What the LLM generates (e.g., classification label, summary, answer)\n- **label**: Your judgment on whether the output passes (0) or fails (1)\n\n![Instructions to upload a CSV file.](https://eugeneyan.com/assets/upload.webp \"Instructions to upload a CSV file.\")\n\nInstructions to upload a CSV file.\n\nIf you don’t have a CSV handy, [download a sample](https://aligneval.com/data/fib50.csv) based on the [Factual Inconsistency Benchmark](https://huggingface.co/datasets/r-three/fib). It contains 50 news articles, each with two summaries. One is factually consistent while the other is not. Perfect for AlignEval’s binary labeling task.\n\nAfter uploading the CSV, you’ll see the data in a flexible table view. Adjust the width of the browser to control how wide or narrow you want the table and columns to be.\n\n![How the table view looks after uploading a CSV file.](https://eugeneyan.com/assets/table.webp \"How the table view looks after uploading a CSV file.\")\n\nHow the table view looks after uploading a CSV file.\n\n## Labeling mode: Look at the data\n\n**In labeling mode, our only job is to look at the data.** AlignEval simplifies the process to a comparison between input and output fields. And for each sample, we only have to make a binary decision—pass or fail.\n\n![Instructions for labeling mode.](https://eugeneyan.com/assets/labeling.webp \"Instructions for labeling mode.\")\n\nInstructions for labeling mode.\n\nWhy binary labels? It leads to more accurate data, takes less time, and keeps cognitive load low. [DoorDash adopted a similar approach](https://eugeneyan.com/writing/content-moderation/#collecting-ground-truth-via-human-in-the-loop). Similarly, [Llama2](https://arxiv.org/abs/2307.09288) focused on collecting binary human preferences. The Llama2 authors also [shared at a meetup](https://www.youtube.com/live/CzR3OrOkM9w?si=gTrb6M44O941wrqb&t=913) that collecting binary preferences was much faster than writing samples for supervised finetuning.\n\nWhat’s important is that we look at the data with an open mind, and not write criteria based on our priors. We should also resist the urge to prematurely define criteria. We need to first immerse ourselves in the data. This reduces the risk of hunting for nonexistent defects or chasing unrealistic expectations. By understanding what the LLM actually generates, we can define more meaningful, better-calibrated criteria.\n\n[YouTube video player](https://www.youtube.com/watch?v=nnhMnNh_ZvU)\n\nLook at your data remix, courtesy of Jason Liu. Sign up for his next RAG course [here](https://maven.com/s/course/b9b1f32583).\n\nAfter labeling 20 rows, we unlock evaluation mode. (While evaluation mode is available after 20 labels, I think it’s too little to understand the data well enough to define criteria, and too small a sample for evaluation. I suggest aiming for 50-100 before writing criteria and running evals. The more familiar you are with your data, the better your evals will be.)\n\n## Evaluation mode: Write criteria, evaluate the evaluator\n\n**After unlocking evaluation mode, we can now write our task-specific evaluation criteria.** Keep it simple: Evaluate on a single dimension and return either 0 (pass) or 1 (fail).\n\n![Instructions for evaluation mode.](https://eugeneyan.com/assets/evaluation.webp \"Instructions for evaluation mode.\")\n\nInstructions for evaluation mode.\n\nHere’s the prompt I use for factual inconsistency classification. We start with only two sentences that define what a pass or fail looks like. We can refine it after running the LLM-evaluator and examining the explanations and predictions.\n\n![Writing the prompt and selecting models and fields.](https://eugeneyan.com/assets/prompt.webp \"Writing the prompt and selecting models and fields.\")\n\nWriting the prompt and selecting models and fields.\n\nNext, we choose our model (gpt-4o-mini or claude-3-haiku) and input fields (both input and output, or output only). For most tasks, (e.g., classification, summarization, Q&A), we want to compare the output to the input. Nonetheless, for some tasks, like evaluating style guide adherence or tone of voice, we only need the output. Focusing solely on the output reduces token cost and latency while potentially improving performance.\n\n**It’s also crucial to evaluate our LLM-evaluators against the labeled data.** Hit “Evaluate” and we’ll see metrics in the top right corner: sample size, recall, precision, F1, Cohen’s $\\\\kappa$, and counts for true and false positives/negatives.\n\n![Metrics to evaluate the LLM-evaluator.](https://eugeneyan.com/assets/metrics.webp \"Metrics to evaluate the LLM-evaluator.\")\n\nMetrics to evaluate the LLM-evaluator.\n\nIf our sample size is low (e.g., 20), the metrics can fluctuate a lot, even when we rerun the same LLM-evaluator. This is due to LLM stochasticity. We can improve stability by labeling more data. Aim for at least 50 labeled samples which also unlocks optimization mode.\n\n## Optimization mode: Semi-automated improvements 🤞\n\n**Optimization mode is where the magic (hopefully) happens.** Click “Optimize” and let AlignEval do its thing to improve your LLM-evaluator. While it’s still in early beta, it’s achieved decent improvements for several use cases. (And no, it’s not dspy.)\n\n![Instructions for optimization mode.](https://eugeneyan.com/assets/optimization.webp \"Instructions for optimization mode.\")\n\nInstructions for optimization mode.\n\nUnder the hood, optimization splits the labeled data into dev and test sets. (Since we’re not training a model, we call it a development split instead of a training split.) It then runs $n$ trials on the dev set to improve F1, using the same LLM and fields from evaluation mode. After $n$ trials, the improved LLM-evaluator is evaled on the unseen test set. In the table below, F1 starts at 0.571, increases to 0.722 at trial 5 on dev, then gets F1=0.727 on test.\n\n![Metrics from optimization trials on all, dev, and test splits.](https://eugeneyan.com/assets/optimize-table.webp \"Metrics from optimization trials on all, dev, and test splits.\")\n\nMetrics from optimization trials on all, dev, and test splits.\n\nSometimes, the dev and test metrics diverge significantly. With the small sample size (25 per split, 50 in total), this likely stems from the dev split being unrepresentative of the full data distribution, including the test split. Thus, the LLM-evaluator may overfit on the dev set and fail to generalize to the test set.\n\nTo improve generalization across the dev and test splits, we can label more data, ensuring that it’s diverse and representative. To be diverse is to be balanced in both 0 and 1 labels, and the variety of inputs and outputs. To be representative is to be similar to real-world examples. We can also run multiple LLM-evaluators in parallel and ensemble their scores. [PoLL](https://arxiv.org/abs/2404.18796) showed that an ensemble of three smaller LLM-evaluators outperformed gpt-4.\n\nFor a visual walkthrough, here’s a demo where I look at and label 50 rows of data lol.\n\n[YouTube video player](https://www.youtube.com/watch?v=OJJ7aHIBtPw)\n\n## Behind the scenes: How AlignEval was built\n\nAs a frontend development newbie (this is my first TypeScript app), I relied on the wisdom of the crowd, polling [Twitter](https://x.com/eugeneyan/status/1828447283811402006) and [LinkedIn](https://www.linkedin.com/posts/eugeneyan_if-you-were-building-a-small-web-app-today-activity-7234224078675439617-HX75/) for advice. I also [built the same app five times](https://eugeneyan.com/writing/web-frameworks/) to get a feel for several frameworks. Specifically, I tried FastHTML, Next.js, SvelteKit, and FastAPI + HTML (my go-to for prototyping).\n\nFrom early prototypes, Next.js seemed to be a good fit. Intuitive, scalable, and the perfect excuse to learn TypeScript. And thanks to Cursor, my beginner questions were answered easily: What’s the difference between `const` and `let`? What is a `prop`? It also made it easier to build UX components and fix bugs 2-3x faster, keeping velocity and motivation high.\n\nFor the backend, Python + FastAPI was the obvious choice. Python’s ecosystem of data analysis and machine learning made it easier to experiment with optimization mode.\n\nWhen it came to LLMs, I went with the smallest models from the biggest labs: gpt-4o-mini and claude-3-haiku (soon to be claude-3.5-haiku). They’re cheap, fast, and good enough for binary classification LLM-evaluators. It’s the perfect combo for a free app. And with each release, these small models just keep getting more and more capable.\n\nFor gpt-4o-mini, I used the [structured output](https://platform.openai.com/docs/guides/structured-outputs) functionality that’s still in beta. It allows users to define the desired output schema using Zod (JavaScript) or Pydantic (Python). Here’s an example in Javascript and its equivalent in Python:\n\n```javascript\nimport { z } from \"zod\";\n\nconst EvaluationResponse = z.object({\n  explanation: z.string(),\n  prediction: z.string().refine((val) => val === '0' || val === '1')\n});\n```\n\n```python\nfrom pydantic import BaseModel\n\nclass EvaluationResponse(BaseModel):\n  explanation: str\n  prediction: str\n```\n\nFor claude-3-haiku, I [defined the XML output](https://eugeneyan.com/writing/prompting/#structured-input-and-output) via the prompt. From what I’ve seen on the job and in AlignEval, this works 99.9% of the time. Here’s the prompt I used for AlignEval.\n\n```plaintext\nEvaluate the output based on the provided criteria. First, in the <sketchpad> provided, think through your evaluation step by step.\n\nThen, provide a binary prediction (0 or 1) within <prediction>.\n```\n\nTo decide on hosting, I conducted a [Twitter poll](https://x.com/eugeneyan/status/1828450401311764741) and consulted documentation for options like Dokku, Coolify, Vercel, etc. [Hamel’s write-up on Dokku](https://hamel.dev/blog/posts/dokku/) was particularly helpful. While I initially considered hosting the app on Hetzner with Dokku (another opportunity to learn something new), I ultimately decided to take the lazy path (read: prioritize ruthlessly) and start with hosting on Railway. It was a good decision.\n\nHosting on Railway has been a breeze. After setting up the project via their web UI and linking it via the CLI (`railway link`), pushing new updates is as simple as `railway up`. Each update takes one to two minutes. Railway also comes with several database options, including Postgres and Redis. While a VPS might be cheaper in the long run, at this stage, [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it). Fiddling with a VPS would have been a distraction from improving the UX, implementing features, and figuring out optimization mode.\n\n![My simple Railway setup.](https://eugeneyan.com/assets/railway.webp \"My simple Railway setup.\")\n\nMy simple Railway setup\n\n> If you’d like to get started with Railway, please use my [affiliate code](https://railway.app?referralCode=fMY11k)—it’ll help with hosting costs and keep AlignEval running!\n\nGoing from a laptop prototype to a public beta took some effort. The work included:\n\n- Splitting into Next.js frontend, FastAPI backend, migrating from SQLite to Postgres\n- Implementing real-time feedback for labeling, evaluation, and optimization\n- Adding features to simplify labeling (pass/fail buttons), customize models and fields (dropdowns, integration with model providers), and more\n- Improving the UI (as rough as it looks now, it was way worse before lol)\n- Squashing lots of bugs (and probably missing many more)\n\nHere’s the to-do list I had; there were many more tasks and bugs that I neglected to track. Because I timeboxed the effort to October, a few items were not completed. But if AlignEval gains traction and proves useful, I might revisit the list.\n\n```markdown\n## Todos\n- [x] Split deployment into frontend and backend\n  - https://help.railway.app/questions/how-to-expose-a-fast-api-backend-service-a1712631\n  - https://help.railway.app/questions/econnrefused-when-calling-another-servic-5662d969#p-1\n  - ~~[ ] Or consider something like this: https://vercel.com/templates/next.js/nextjs-fastapi-starter~~ (only works with Vercel)\n- [x] Add external database (Railway Postgres)\n- [x] Write interface to ensure compatibility between SQLite and Postgres\n- [x] Connect label-app to Railway Postgres\n- [x] Connect label-hpo to Railway Postgres\n- [x] Show feedback to users while optimization is running\n- [x] Update table names to use CSV file name\n- [x] Add support for multiple tables\n- [x] Replace tablename parameter with filename parameter\n- [x] Uploading a CSV file with an existing name should return the existing table\n- [x] Add llm-evaluator optimization to label-hpo\n- [x] Add dropdown to select evaluation fields\n- [x] Add ability to delete optimization table, with password\n- [x] Add progress bar to gamify labeling, evaluation and optimization\n- [x] Add buttons to click pass and fail\n- [x] Add usage count (upload (files, rows), evaluate (files, rows), optimize (files, trials))\n- [x] Add info button intro next to header to explain the site\n- [x] Update favicon\n- [x] Add support for OpenAI models\n- [x] Add balanced val-test split for optimization\n- [x] Add columns to optimization metrics: split (dev, test), model, evaluation_fields\n- [x] Set max upload rows to env variable\n- [ ] Fix bug where alert pops up: \"Failed to handle optimization: Unexpected token '<', \"<!DOCTYPE \"... is not valid JSON\"\n  - Still pops up every now and then\n- [x] Fix bug where evaluation alert pops up even when optimization is unlocked\n- [x] Add popup to see full prompt\n- [x] Add button to copy prompt\n- [x] Add completion bar to optimization sticky\n- [x] Make site look retro and like a game\n- [x] Prompt users to rename sample data before downloading\n- [x] Update delete optimization to reset optimization, and set password to Twitter handle\n- [x] Add \"don't show me this again\" checkbox\n- [ ] Improve how we poll for data\n- [ ] Add button to donate with Stripe payment\n- [ ] Add button to optimize more with Stripe payment\n\n## Todos (OE)\n- [x] Separate data definitions to definitions.ts\n- [x] Separate fetch data queries to data.ts\n- [x] Separate data mutations to actions.ts\n- [x] Separate components into component.tsx under ui which are imported in page.tsx\n```\n\nBuilding AlignEval has been a ton of fun, and writing about the journey was a great way to reflect. It’s easily my favorite project of the year, because I got to build an app and learn a lot along the way. (That said, there’s still November and December to try to beat this.)\n\nIf you’re tired of spending days or weeks on evals and aligning LLM-evaluators, try [AlignEval](http://aligneval.com). It’s designed to streamline your workflow and focus on what matters most:\n\n- Looking at the data and adding labels\n- Defining meaningful criteria\n- Evaluating the LLM-evaluator\n- Semi-automatically optimizing it\n\nHere’s the [code](https://github.com/eugeneyan/align-app) in case you want to deploy your own instance. It’s my first TypeScript project, so feedback welcome! How could I have organized it better? What best practices should I have followed? And beyond the code, what have you found useful when building LLM-evaluators? Comment below or [DM me](https://x.com/eugeneyan).\n\nThanks to the incredible folks who made discussing this topic so much fun, and provided helpful feedback at early demos: Shreya Shankar, Hamel Husain, Kyle Corbitt, David Corbitt, Saumya Gandhi, Swyx, Eugene Cheah, and Dennis Taylor. And thanks to Hamel Husain for reading early drafts of this piece.\n\nBy the way, if you want to learn more about evals, my friends Hamel and Shreya are hosting their *final* cohort of “AI Evals for Engineers and PMs” in July. Here’s a [35% discount code](https://maven.com/parlance-labs/evals?promoCode=eugene-is-all-you-need).\n\n## Further reading\n\n- [Your AI Product Needs Evals](https://hamel.dev/blog/posts/evals/)\n- [Task-Specific LLM Evals that Do & Don’t Work](https://eugeneyan.com/writing/evals/)\n- [Who Validates the Validators](https://arxiv.org/abs/2404.12272)\n- [Evaluating the Effectiveness of LLM-Evaluators](https://eugeneyan.com/writing/llm-evaluators/)\n- [Creating a LLM-as-a-Judge That Drives Business Results](https://hamel.dev/llm-judge)\n\nIf you found this useful, please cite this write-up as:\n\n> Yan, Ziyou. (Oct 2024). AlignEval: Building an App to Make Evals Easy, Fun, and Automated. eugeneyan.com. https://eugeneyan.com/writing/aligneval/.\n\nor\n\n```\n@article{yan2024aligneval,\n  title   = {AlignEval: Building an App to Make Evals Easy, Fun, and Automated},\n  author  = {Yan, Ziyou},\n  journal = {eugeneyan.com},\n  year    = {2024},\n  month   = {Oct},\n  url     = {https://eugeneyan.com/writing/aligneval/}\n}\n```\n\nShare on:\n\n![](https://eugeneyan.com/assets/icon-twitter.svg)\n\n![](https://eugeneyan.com/assets/icon-linkedin.svg)\n\n![](https://eugeneyan.com/assets/bluesky.svg)\n\n![](https://eugeneyan.com/assets/icon-facebook.svg)\n\n![](https://eugeneyan.com/assets/icon-mail.svg)\n\nJoin **11,800+** readers getting updates on machine learning, RecSys, LLMs, and engineering.\n",
  "dateCreated": "2026-09-27T19:29:15.293681927+00:00",
  "date_created": "2024-10-27T00:00:00+00:00",
  "date_modified": "2024-10-27T00:00:00+00:00",
  "date_published": "2024-10-27T00:00:00+00:00",
  "description": "Look at and label your data, build and evaluate your LLM-evaluator, and optimize it against your labels.",
  "external_url": "https://eugeneyan.com//writing/aligneval/",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/eugeneyan-com/2024/10/2024-10-27-aligneval-building-an-app-to-make-evals-easy-fun-and.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/eugeneyan-com/2024/10/2024-10-27-aligneval-building-an-app-to-make-evals-easy-fun-and.md"
  },
  "id": "urn:aggr:item:bd0d843db43fc4688f8986b70239512785224d2e",
  "isBasedOn": {
    "@id": "https://eugeneyan.com//writing/aligneval/"
  },
  "mainEntity": {
    "@id": "https://eugeneyan.com//writing/aligneval/",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/eugeneyan-com/2024-10-27-aligneval-building-an-app-to-make-evals-easy-fun-and/#webpage"
    },
    "dateModified": "2024-10-27T00:00:00+00:00",
    "datePublished": "2024-10-27T00:00:00+00:00",
    "headline": "AlignEval: Building an App to Make Evals Easy, Fun, and Automated",
    "timeRequired": "PT13M",
    "url": "https://eugeneyan.com//writing/aligneval/",
    "wordCount": 2761
  },
  "name": "Archived snapshot: AlignEval: Building an App to Make Evals Easy, Fun, and Automated",
  "preview": null,
  "reading_minutes": 13,
  "replicated_at": null,
  "source": "eugeneyan-com",
  "source_name": "Eugene Yan",
  "summary": "Look at and label your data, build and evaluate your LLM-evaluator, and optimize it against your labels.",
  "tags": [
    "eval",
    "learning",
    "llm",
    "🛠",
    "🩷"
  ],
  "temporalCoverage": "2026-09-27T19:29:15.293681927+00:00",
  "title": "AlignEval: Building an App to Make Evals Easy, Fun, and Automated",
  "url": "https://aggr.aymericbeaumet.com/items/eugeneyan-com/2024-10-27-aligneval-building-an-app-to-make-evals-easy-fun-and/",
  "word_count": 2761
}