Optimize an AI agent to sound human, judged by an AI detector LaunchDarkly's agent optimization feature was used to automatically tune an AI email-drafting agent to sound human, judged by the AI detector GPTZero. The developer built a workflow where Claude drafts replies and generates prompt variations, while GPTZero scores AI-likeness, driving the optimizer to reduce the score. Each iteration costs about $0.002, and the reusable GPTZero integration can be adapted for other external scorers. You can tell when an LLM wrote an email. The "I hope this email finds you well" opener, the three polite paragraphs answering a one-line question. I wanted a reply-drafting agent that didn't do that, and "don't sound like an AI" turned out to be hard to put in a prompt. Banning a few phrases is easy. The rest is judgment, and a single prompt that holds across a friendly dinner invite and a recruiter cold-email took more iterations than I'd guessed. This is not only an email problem. Some platforms down-rank content that reads as AI-generated, so teams publishing at scale have a real stake in prose that clears a detector, even when a human wrote it. The workflow here applies to any of that. So I stopped hand-tuning and let LaunchDarkly agent optimization https://launchdarkly.com/docs/home/agentcontrol search for the prompt. You give it a judge that scores "better," and it generates prompt variations and keeps the ones that beat the bar. For the reasoning behind the feature, read the agent optimization announcement https://launchdarkly.com/blog/agentcontrol-agent-optimization/ . This tutorial is the how. If you don't have an account yet, sign up for LaunchDarkly https://app.launchdarkly.com/signup to follow along. Two pieces do the work here. Claude claude-haiku-4-5-20251001 runs both roles: it drafts the replies, and it writes each new candidate prompt when the loop asks for one. Scoring comes from GPTZero, which isn't a language model at all but a closed AI detector. I wired it in inverted, so the score is the probability a reply reads as AI and the optimizer drives it down. I went with a detector instead of an LLM-as-a-judge for a reason: grading one model's prose by asking another model whether it sounds human is exactly the call language models are unreliable at, and a tool trained for that one question gives a number you can defend. A run is cheap. Each iteration costs around $0.002 and a few seconds, so a full run lands near a penny or two, and the loop tries variations I'd never sit down and type by hand. This tutorial runs from a saved configYou bootstrap the agent, the judge, and the optimization, then work in the UI. Every iteration streams to the optimization Results tab, the winner lands on the agent Variations tab, and you tune thresholds and inputs on the optimization itself. The code does two things: it scores AI-likeness with GPTZero, and it runs the optimization. email-agent , seeded with one deliberately thin instruction ai-likeness , scored by GPTZero in code rather than by a prompt email-agent-opt , that runs as a candidate generator and streams to the Results tabThe GPTZero integration is the reusable part. The same shape works for any external scorer you might bring, whether a moderation API, a classifier you host, or a scoring endpoint of your own, so what you learn here isn't limited to email or to AI detection. The companion repo is agent-optimization-sample https://github.com/launchdarkly-labs/agent-optimization-sample . Clone it to follow along. .env Install the project and its dependencies: Terminal uv sync .env holds LD SDK KEY, LD API KEY, LD PROJECT KEY, ANTHROPIC API KEY, AI LIKENESS API KEY The repo aliases the short LD names to the LAUNCHDARKLY names the SDK expects, so the short names in .env are enough. Agent optimization runs an iterative loop against an AgentControl config https://launchdarkly.com/docs/home/agentcontrol/create . It measures your current variation as a baseline, generates candidate variations, and scores each against your acceptance criteria. The loop has a simple shape: ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ Define │────▶│ Explore │────▶│ Commit │ │ "Better" │ │ candidates │ │ a winner │ └──────▲──────┘ └──────────────┘ └──────┬──────┘ │ │ └────────────────────────────────────────┘ Define better. You set acceptance criteria with a judge and a threshold. A judge scores a response on one dimension. You reference a judge https://launchdarkly.com/docs/home/agentcontrol/judges saved as an AgentControl config by its key. Explore candidates. Each iteration drafts against your inputs, scores the result, and writes the next candidate from what the scores tell it. The threshold here is a gate that keeps the loop generating. When a candidate clears it, the optimizer re-runs that same prompt against a few more of your input samples and keeps it only if it passes those too, so a prompt that got lucky on one message doesn't win. Even then, clearing the gate doesn't certify that a candidate is good enough to ship. A fuller eval decides that, later. Commit the winner. The recommended variation shows up in LaunchDarkly, and with autoCommit it publishes back to the agent's Variations tab https://launchdarkly.com/docs/home/agentcontrol/create-variation so you can read what the optimizer wrote. You also pick an evaluation mode. Exploratory mode infers quality from the judge alone, which suits open-ended inputs that have no single correct output. Expected Output mode scores against known-correct answers. Replies have no single right answer, so this tutorial stays in Exploratory mode. The technique behind itAgent optimization is an instance of OPRO Optimization by PROmpting , introduced in Google DeepMind's Large Language Models as Optimizers . A model reads the history of prompts and their scores, then writes the next candidate to try. It searches over prompts, not model weights, so each candidate is cheap to run and nothing is ever trained. The companion repo keeps the moving parts in small files, so each piece is easy to find and swap: bootstrap.py : seeds the three LaunchDarkly objects, the email-agent config, the ai-likeness judge, and the email-agent-opt optimization. It's safe to re-run, and it prints links to the configs and the Results tab. optimize from config.py : the one run command. It reads the saved optimization, runs it, streams each iteration to the Results tab, and prints the tab's link at the end. optimize.py : the two callbacks the run needs. handle agent call drafts replies on Claude, and writes the next candidate prompt on Claude too when the SDK asks for one. handle judge call scores AI-likeness with GPTZero and hands the optimizer the per-reply detector output. detector.py : the GPTZero client. score with response returns the number the judge gates on and the full GPTZero JSON. messages.py : the synthetic input messages. gptzero test.py : a standalone probe for scoring a draft by hand. clients.py and env.py : the LaunchDarkly and Anthropic clients, built once each, and the .env loader.The saved optimization holds what you're optimizing for: the judge, the threshold, the inputs, and the model choices. The code holds how the work happens, drafting on Claude and scoring with GPTZero. You edit the what in the UI and the how in code, and the run command brings the two together. One command seeds everything this tutorial needs. bootstrap.py creates three objects in LaunchDarkly, and it's idempotent, so anything that already exists is left alone: email-agent : the agent {"replies": ... } envelope and the {{messages}} variable, and nothing about tone. That's on purpose. It leaves the humanization, the part you want optimized, to the optimizer. ai-likeness : the inverted judge config. GPTZero scores it from code, which leaves the judge prompt as a placeholder. email-agent-opt : the saved optimization the Results tab runs. Thresholds, inputs, and model choices all live here.The baseline is thin on tone but carries the output contract the parser needs, on the Claude model the agent drafts with: bootstrap.py variation { "key": "baseline", "name": "Baseline", "model": {"modelName": "claude-haiku-4-5-20251001", "parameters": {}}, "instructions": "You are an email assistant. Write a reply to each message below. " 'Return ONLY a JSON object {"replies": "