●SHEETS — Fill with Gemini expands to 28 more languages, so you can build spreadsheets in your own language with natural prompts●ALPHAEVOLVE — Gemini Enterprise makes AlphaEvolve generally available, autonomously discovering optimized code solutions●SPARK — Gemini Spark, a personal AI agent for macOS, organizes folders and runs workflows across Workspace●OMNI — Gemini Omni blends text, photos, and video for high-quality creation, plus custom AI avatars of you●GEMINI35 — Gemini 3.5 Pro slips to July 17 for a full rebuild, introducing a 2M-token context and Deep Think●DEEPTHINK — The 3.5 Pro rebuild targets better math reasoning, SVG scene generation, and image quality●SHEETS — Fill with Gemini expands to 28 more languages, so you can build spreadsheets in your own language with natural prompts●ALPHAEVOLVE — Gemini Enterprise makes AlphaEvolve generally available, autonomously discovering optimized code solutions●SPARK — Gemini Spark, a personal AI agent for macOS, organizes folders and runs workflows across Workspace●OMNI — Gemini Omni blends text, photos, and video for high-quality creation, plus custom AI avatars of you●GEMINI35 — Gemini 3.5 Pro slips to July 17 for a full rebuild, introducing a 2M-token context and Deep Think●DEEPTHINK — The 3.5 Pro rebuild targets better math reasoning, SVG scene generation, and image quality
A minimal evolutionary search loop with Gemini: propose, evaluate, select — prompted by AlphaEvolve's GA
With AlphaEvolve reaching GA, I built the smallest possible evolutionary search loop on the Gemini API: generate candidates, score them with a fitness function, and select the best. Sandboxed evaluation, diversity, and budget control — from real solo-dev use.
I was rewriting one more number in a spreadsheet of weights. They were the coefficients of a tiny scoring function that decides which Google Play reviews I should reply to first — star rating, body length, whether a crash-related word appears, days since posting. I tuned the four weights by hand until the ranking matched my gut. The more I aligned one case, the more another slid out of place. That night I poured half a day into the back-and-forth.
The same week, I read that AlphaEvolve had reached general availability in Gemini Enterprise. The model searches for candidate solutions on the server side, executes code in a safely isolated client environment to score them, and repeats — inching toward an optimum. My hands stopped mid-edit. What I had just been doing by hand was exactly this: the manual version of propose, evaluate, and select.
This article is the record of turning that realization into a tool for solo development. Rather than reproducing AlphaEvolve itself, I extract only its core and build the smallest evolutionary search loop that runs on the Gemini API. I'll leave the working code and the traps I actually stepped in.
What AlphaEvolve really demonstrates is automating the search itself
The heart of AlphaEvolve is not asking the model to write "the answer" once. It is running a loop: have the model produce candidate answers, score them mechanically, then use the good ones as seeds for the next batch. In the vocabulary of evolutionary computation, it has all four pieces — individuals (candidates), fitness (scores), selection, and mutation (next-generation creation).
If I strip it down to what a solo developer can reuse, the transferable core maps out like this.
Evolutionary element
Its counterpart in this loop
Who handles it
Individual
A Python code string for the scoring function
Generated by Gemini
Fitness
Ranking agreement on my data (Kendall's τ)
My own evaluator
Selection
Keep the top k
My own loop
Mutation / crossover
Show the elites, ask for improved variants
Generated by Gemini
Obviously I can't hold the large-scale distributed search or the Google-managed isolated runtime that the Enterprise AlphaEvolve provides. But the skeleton — propose, evaluate, select — is something one person can assemble in an evening. And for small optimization problems (coefficient tuning, discovering a heuristic, exploring prompt structure), that skeleton alone proved to be enough.
Where hand-tuning quietly ran out of road
Turning weights by hand has three silent limits.
First, the search space is exponential. Even linear weights over four features form an infinite space of continuous values, and once you reach for nonlinear terms — logs, thresholds, products — you quickly leave what intuition can track.
Second, evaluation stays vague. If you keep nudging on "it feels a little better," you overfit to the handful of cases you happened to be looking at. Without a number, you can't tell when you've regressed.
Third, no tacit knowledge survives. Only the final coefficients remain in the code, and why those values fade even from my own memory. Touch it six months later and the reasons are gone.
Evolutionary search inverts all three. The machine does the searching, the function turns evaluation into a number, and good candidates persist as code. All the human decides is what counts as good — the definition of fitness.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
First, fix the axis of evaluation. Here I prepare a small labeled dataset and measure the agreement between "the priority I assigned" and "the ranking a candidate function produces" with Kendall's τ. τ ranges from −1 to 1; closer to 1 means the rankings match perfectly.
# fitness.py — fix the evaluation axis firstfrom itertools import combinations# A small hand-labeled dataset (features, priority I assigned: higher = handle sooner)# features = (rating, body_len, has_crash_word, days_since)DATASET = [ ((1, 180, 1, 0), 5.0), # low rating, crash report, same day -> top priority ((2, 240, 1, 1), 4.6), ((1, 40, 0, 0), 4.0), # low rating but short ((3, 320, 0, 2), 2.8), ((5, 30, 0, 0), 0.6), # high rating, short -> defer ((4, 210, 0, 5), 1.4), ((2, 90, 1, 9), 3.2), # crash but old ((5, 400, 0, 1), 1.0),]def kendall_tau(order_a, order_b): n = len(order_a) concordant = discordant = 0 for i, j in combinations(range(n), 2): sa = order_a[i] - order_a[j] sb = order_b[i] - order_b[j] if sa * sb > 0: concordant += 1 elif sa * sb < 0: discordant += 1 total = concordant + discordant return 0.0 if total == 0 else (concordant - discordant) / totaldef fitness(score_fn): """Fitness of a candidate (-1..1). Higher = closer to my priorities.""" truth = [label for _, label in DATASET] preds = [score_fn(*feats) for feats, _ in DATASET] return kendall_tau(truth, preds)
Deciding fitness first is what matters. If this is vague, no matter how fast the later search spins, it will climb the wrong hill at full speed. My first attempt used a plain correlation coefficient for agreement and failed. For a ranking problem you want a rank-agreement metric; a correlation on the raw values gets dragged around by outliers.
Safely evaluating code the model wrote
This is the part that demands the most care. You cannot simply exec the Python code Gemini generates. Infinite loops, file writes, network calls — let any one of them through and autonomous operation turns straight into an incident.
What I chose was a four-layer guard: shrink builtins to a minimum, forbid import, put a time limit on execution, and swallow exceptions as a score of zero.
# sandbox.py — isolated evaluation of model-generated codeimport signalimport mathSAFE_BUILTINS = { "abs": abs, "min": min, "max": max, "round": round, "len": len, "float": float, "int": int, "pow": pow,}SAFE_GLOBALS = {"__builtins__": SAFE_BUILTINS, "math": math}class Timeout(Exception): passdef _raise_timeout(signum, frame): raise Timeout()def compile_candidate(code_str, time_limit_sec=1): """Build score(rating, body_len, has_crash_word, days_since) from a code string. Dangerous or invalid candidates return None; the caller treats that as fitness 0.""" if "import" in code_str or "__" in code_str or "open(" in code_str: return None local_ns = {} try: exec(compile(code_str, "<candidate>", "exec"), dict(SAFE_GLOBALS), local_ns) except Exception: return None fn = local_ns.get("score") if not callable(fn): return None def guarded(*args): signal.signal(signal.SIGALRM, _raise_timeout) signal.setitimer(signal.ITIMER_REAL, time_limit_sec) try: return float(fn(*args)) except (Timeout, Exception): return 0.0 finally: signal.setitimer(signal.ITIMER_REAL, 0) return guarded
String inspection alone is not airtight. Swapping __builtins__ still leaves decorator-based escapes, and a signal-based timeout only works on the main thread. Still, within the range one person runs at their desk, this four-layer guard dropped essentially every harmful candidate to zero. If you put this on always-on production, the correct move is to add a separate process and resource limits (resource.setrlimit). This is exactly where the principle shows through: build the stop mechanism and the isolation before you build the convenience.
Letting Gemini do the mutation — why LLM-guided mutation works
I hand the job of producing the next generation to Gemini. The key is not to ask "write a scoring function" in the abstract, but to show the current best candidates with their fitness, then ask for improvements. Mutating toward a reasoned direction rather than at random — that is the essence of LLM-guided mutation.
With structured output, I receive an array of candidate code strings.
# propose.py — seed on the elites, generate several improved variantsfrom google import genaifrom google.genai import typesclient = genai.Client() # reads the GEMINI_API_KEY environment variableRESPONSE_SCHEMA = { "type": "object", "properties": { "candidates": { "type": "array", "items": {"type": "string"}, "minItems": 4, "maxItems": 8, } }, "required": ["candidates"],}PROMPT_TMPL = """You are a lab assistant for heuristic optimization.Goal: improve a Python function that defines score(rating, body_len, has_crash_word, days_since).Fitness is Kendall's tau (higher is better, max 1.0).Current elite individuals (code and fitness):{elites}Constraints:- no import statements and no double underscores- only arithmetic, the math module, and min/max/abs/round- the function must be named scoreGiven these, propose {n} improved variants that take different directions."""def propose(elites, n=6, temperature=0.9): elites_txt = "\n\n".join( f"# fitness={fit:.3f}\n{code}" for code, fit in elites ) prompt = PROMPT_TMPL.format(elites=elites_txt, n=n) resp = client.models.generate_content( model="gemini-flash-latest", contents=prompt, config=types.GenerateContentConfig( temperature=temperature, response_mime_type="application/json", response_schema=RESPONSE_SCHEMA, ), ) import json return json.loads(resp.text)["candidates"]
Why does this beat random mutation? Because Gemini understands what the features mean. Raise priority when has_crash_word is 1; decay it as days_since grows — it weaves these sensible directions in from the very first move of the search. Targets that random search would need hundreds of generations to hit, it reaches in a handful. You do not need the heavy gemini-2.5-pro here. Candidate generation is a volume step, so I balanced cost and speed with gemini-flash-latest (the identity of 3.5 Flash now that it is GA).
Premature convergence and diversity — temperature and restarts
LLM-guided mutation has a side effect. It converges on good candidates fast, but the population starts to look alike just as quickly. That is premature convergence. Once the elites fill up with the same shape of function, you can't move forward.
As countermeasures I added three things. First, reject duplicate individuals by hashing normalized code, so synonymous code can't pad the population. Second, jitter the generation temperature across generations — when improvement stalls, raise it from 0.9 to 1.2 to widen the search. Third, every few generations, mix in one fully random restart, an island-model touch.
Here is the trajectory when I actually ran 8 individuals for 6 generations.
Gen
Best τ
Population mean τ
Dupes rejected
Cumulative tokens
0 (initial)
0.43
0.11
0
~4,900
1
0.57
0.30
1
~10,200
2
0.64
0.41
2
~15,800
3
0.71
0.55
3
~21,300
4
0.79
0.66
5
~27,100
5
0.79
0.72
6
~32,600
At generation 4 the best τ reached 0.79, and generation 5 brought no update. The mean τ keeps rising, so the population is converging on the best individual. I stopped there. Pushing further would only grow token spend. What mattered was that I could decide when to stop from a number — a sense I never got from hand-tuning.
Budget and guardrails — build the stop mechanism first
In a loop that runs on its own, the first thing to build is not the mechanism that starts it but the one that reliably stops it. The caps I set were these.
Generation cap: 8 by default. Most small problems plateau by then.
No-improvement stop: end if the best fitness hasn't updated for 2 consecutive generations.
Token budget: 60k tokens per run. Exceed it and return the current best.
Hold-out check: hide 20% of the data from the search and re-measure τ there at the end.
The fourth one works quietly. Evaluate only on the data used for search and the population overfits to those few cases. In my run, against τ 0.79 on the search data, the held-out data gave 0.68. That gap is the material for discounting the "apparent" quality. If the gap is too large, it's a sign to add data or penalize function complexity (favor shorter code).
The cost side was realistic too. Roughly 33k tokens per run, a few yen at the gemini-flash-latest rate. If that buys back half a day of manual work, the amount isn't worth hesitating over. As an indie developer running several apps solo, I find these "tools that make small optimizations cheap to repeat" compound over time.
Choosing between hand-tuning, DSPy, and the evolutionary loop
Finally, how to pick the tool. Evolutionary search is not universal; matching it to the shape of the problem is the practical stance.
Situation
Fitting tool
Why
2–3 features, clear relationships
Hand-tuning
You can guess the answer before running a search
Optimizing prompt wording
Auto-optimizers like DSPy
Machinery specialized for prompt space exists
Small code search with numeric evaluation
This evolutionary loop
Propose, evaluate, select runs cleanly
Optimization needing scale and distribution
AlphaEvolve (Enterprise)
Delegate search scale and safe execution
The dividing line is a single question: can you write fitness as a number you trust? If you can, the evolutionary loop works with surprising honesty. If you can't, start by putting the evaluation axis into words — that is a problem prior to any tool.
The biggest thing I took from this loop was not the optimal coefficients themselves. It was that writing out "what counts as good" as a function forced my own criteria into the open. Priorities that had hidden inside intuition now sit in front of me as code.
As a next step, picture one number you're currently adjusting "by feel." If you can score its quality on about ten data points, it is already a problem you can put on this loop. I hope it gives you a place to start.
Share
Thank You for Reading
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.