Agents From First Principles 08: AI Agent Picks the First Solution? Add Search Instead of One-Shot Generation
An AI agent often fails for a surprisingly ordinary reason:
it commits too early.
It finds one plausible next action, follows it, and then spends the rest of the run trying to make that first choice work.
That can look intelligent because the agent keeps reasoning, calling tools, revising plans, and explaining itself.
But underneath, the trajectory may be almost completely determined by an early mistake.
A coding agent chooses the wrong implementation strategy and spends twenty tool calls repairing it.
A research agent finds the first plausible explanation and gathers only evidence that supports it.
A browser agent chooses the wrong navigation path and keeps trying to recover from increasingly confusing page state.
A data agent commits to the wrong transformation and then compensates with more transformations.
The problem is not necessarily that the model cannot produce a better solution.
The better solution may simply never get explored.
The next step in our agent stack is therefore search.
Not web search.
Search over possible agent trajectories.
Instead of:
problem
↓
first plausible action
↓
second plausible action
↓
final answer
we allow multiple possibilities to remain alive:
state 0
/ | \
A B C
/ \ / \ / \
A1 A2 B1 B2 C1 C2
Then we evaluate, prune, and continue only with promising branches.
This sounds like a large conceptual jump.
It is actually a small extension of several techniques we have already built.
We already know how to:
- generate multiple candidates,
- score candidates,
- critique candidates,
- track state,
- verify outcomes,
- bound loops,
- record trajectories.
Search simply changes where we branch and which branches we continue.
That makes it one of the most useful transitions from a reactive agent into a deliberate agent.
The Search Problem Behind Many Agent Failures
Suppose we ask a coding agent to fix a failing test.
A simple agent might do this:
inspect error
↓
choose likely file
↓
edit file
↓
run tests
↓
repair consequences
That is one trajectory.
But there may have been several plausible hypotheses at the first step:
failure
├── implementation bug
├── bad test fixture
├── stale generated file
└── configuration mismatch
If the agent chooses implementation bug immediately, the rest of the run may be spent inside the wrong branch.
A search-based agent can preserve alternatives:
failure
↓
generate hypotheses
↓
score / test cheaply
↓
expand top hypotheses
↓
execute stronger checks
↓
commit later
The key idea is simple:
Delay commitment when several plausible trajectories exist and cheap evidence can distinguish them.
Search is useful when uncertainty is about what path to follow, not merely how to phrase an answer.
Best-of-N Was Search, but Only at the End
Earlier in this series we built Best-of-N:
prompt
↓
answer A
answer B
answer C
↓
score
↓
select best
That is already a form of search.
But the candidates are usually complete outputs.
Suppose each complete solution requires ten expensive tool calls.
Generating five complete trajectories costs roughly five times as much work.
A more efficient approach is to branch earlier:
initial state
↓
three possible approaches
↓
cheap evaluation
↓
keep two
↓
expand each
↓
keep one
This is incremental search.
We spend compute where evidence suggests it is useful.
The Smallest Search Agent
We can start with a deliberately tiny abstraction.
from dataclasses import dataclass, field
@dataclass
class SearchNode:
state: dict
actions: list[str] = field(default_factory=list)
score: float = 0.0
depth: int = 0
Each node represents one possible point in the agent trajectory.
The search loop needs only four operations:
select
expand
evaluate
prune
A simple beam-style search looks like this:
def search(initial_state, expand, evaluate, beam_width=3, max_depth=5):
frontier = [SearchNode(state=initial_state)]
for depth in range(max_depth):
children = []
for node in frontier:
for action, next_state in expand(node.state):
child = SearchNode(
state=next_state,
actions=node.actions + [action],
depth=node.depth + 1,
)
child.score = evaluate(child)
children.append(child)
if not children:
break
children.sort(key=lambda n: n.score, reverse=True)
frontier = children[:beam_width]
return max(frontier, key=lambda n: n.score)
Nothing here requires an LLM.
The agent-specific pieces are hidden behind:
expand(state)
and:
evaluate(node)
That separation matters.
Search is a runtime mechanism.
The LLM may propose expansions or score nodes, but it is not the search algorithm itself.
Search Is About Partial Solutions
The major conceptual difference from Best-of-N is this:
We evaluate unfinished trajectories.
For a coding agent, a partial node might be:
Hypothesis: cache invalidation bug
Evidence: failure disappears when cache disabled
Next action: inspect cache key construction
For a research agent:
Hypothesis: productivity decline is driven by demographic composition
Evidence collected: two supporting sources
Missing evidence: sector-level decomposition
For a browser agent:
Current page: checkout
Progress: address entered
Alternative next actions:
- click continue
- fix postcode validation
The search algorithm decides which unfinished states deserve more compute.
Search Requires a State Representation
Search becomes difficult if the agent state is only a giant conversation transcript.
A node should contain the information necessary to continue the trajectory.
For example:
state = {
"goal": "Fix failing parser test",
"hypothesis": "delimiter handling regression",
"files_read": ["parser.py", "tests/test_parser.py"],
"tests": {
"test_pipe_delimiter": "failed",
},
"changes": [],
"remaining_budget": 8,
}
Now two branches can diverge cleanly.
Branch A:
hypothesis = parser logic regression
Branch B:
hypothesis = test fixture regression
Without explicit state, branches easily contaminate one another.
This is another reason structured runtime state matters more as agent sophistication increases.
Expansion: How Do We Create Alternatives?
The expand() function generates possible next moves.
At the simplest level:
def expand(state):
return model.propose_actions(state, n=3)
But we can make expansion more deliberate.
For example:
one conservative action
one diagnostic action
one alternative hypothesis
That creates diversity by construction.
A coding agent might expand with:
1. inspect the most likely implementation
2. run a discriminating test
3. inspect configuration or environment
A research agent might generate:
1. strengthen current hypothesis
2. seek contradictory evidence
3. investigate a competing explanation
This is much better than asking:
Give me three more ideas.
The search policy should generate meaningfully different branches.
Duplicate Branches Waste Search Budget
A common search failure is fake diversity.
Suppose the model proposes:
A: inspect parser.py
B: inspect the parser implementation
C: open parser.py and examine delimiter logic
Those are not three branches.
They are one branch written three ways.
We need branch deduplication.
A cheap version can normalize action signatures:
def action_fingerprint(action: dict) -> tuple:
return (
action["tool"],
tuple(sorted(action.get("args", {}).items())),
)
Semantic branches may require stronger deduplication:
embedding similarity
state similarity
shared changed files
shared hypothesis label
A useful metric is:
unique_branch_ratio = unique_branches / generated_branches
If you generate 20 candidates but only 4 are meaningfully distinct, your search width is not really 20.
Evaluation: The Hardest Part of Search
Generating alternatives is usually easier than evaluating unfinished ones.
The evaluation function is the search heuristic.
Bad heuristic:
score = llm("How promising is this approach?")
Better evaluation combines evidence.
For a coding agent:
+ tests fixed
+ failing tests reduced
+ static checks pass
+ patch size reasonable
- new failures introduced
- unsupported assumptions
For a research agent:
+ independent sources
+ claim coverage
+ source quality
+ contradictory evidence addressed
- unsupported claims
- duplicated sources
For browser automation:
+ task fields completed
+ navigation closer to target
- validation errors
- repeated page states
The evaluator does not need to predict the final result perfectly.
It only needs to rank partial trajectories well enough that promising branches receive more compute.
Objective Signals Should Dominate When Available
If a branch can be tested, test it.
For code:
run targeted tests
For SQL:
execute query against fixtures
For browser tasks:
inspect DOM state
For data transformations:
run schema validation
For planning:
check constraints
A model’s opinion about whether a branch is promising should generally be weaker evidence than direct environment feedback.
This gives us a search policy like:
objective verifier
↓
heuristic metrics
↓
learned scorer
↓
LLM judge
The exact order depends on the application, but the principle is consistent:
Use the cheapest reliable evidence first.
Beam Search: Keep the Best Few Paths Alive
One of the simplest useful search strategies is beam search.
Instead of keeping every branch, retain the top k after each expansion.
beam width = 2
root
/ | \
A B C
↓ ↓
A1 B1
/ \ / \
A2 A3 B2 B3
↓ ↓
top two
This controls combinatorial explosion.
If each node generates four children and we search five levels deep, exhaustive search explores:
4⁵ = 1024 terminal trajectories
A beam width of three keeps only three active nodes per depth.
That turns exponential growth into something much closer to:
beam_width × branching_factor × depth
This is not free, but it is manageable.
Search Width and Search Depth Are Different Budgets
There are two basic ways to spend more inference.
Breadth:
try more alternatives
Depth:
follow each alternative further
Different tasks want different allocations.
For a coding diagnosis problem, breadth may matter initially:
several plausible root causes
Once one root cause has strong evidence, depth becomes more useful:
implement + test + refine
For research, we may deliberately maintain breadth longer because competing hypotheses are valuable.
Search policy therefore includes a compute-allocation decision.
Search Explosion: The Most Obvious Failure
Naive search can become catastrophically expensive.
Suppose:
branching factor = 5
search depth = 8
Full expansion creates:
5⁸ = 390,625
terminal trajectories.
That is not an agent architecture.
That is a token furnace.
Real search agents need explicit controls:
max nodes
max depth
beam width
max model calls
max tool calls
wall-clock budget
cost budget
no-improvement patience
For example:
class SearchBudget:
def __init__(self, max_nodes=50, max_depth=6, max_calls=40):
self.max_nodes = max_nodes
self.max_depth = max_depth
self.max_calls = max_calls
Search should always know how much search it is allowed to perform.
When Should Search Stop?
A search agent should not continue merely because branches remain.
Useful stopping conditions include:
verified solution found
score threshold reached
no improvement for N expansions
all branches invalid
budget exhausted
branches converged
expected gain below cost
The last one is especially important.
Suppose:
beam width 1 → 74% success
beam width 2 → 81%
beam width 4 → 82%
beam width 8 → 82.3%
Search width eight may be unjustified.
The relevant metric is not merely quality.
It is quality relative to additional inference cost.
Search Should Earn Its Compute
A useful experiment sweeps search budget.
strategy success model calls latency
------------------------------------------------
one-shot 70% 1 2s
Best-of-3 78% 3 4s
beam-2 83% 8 10s
beam-4 84% 16 18s
beam-8 84% 31 35s
Now we can see the knee in the curve.
The question is not:
Does search help?
The question is:
Where does additional search stop paying for itself?
A Better Search Metric: Cost Per Verified Success
Suppose two systems have:
Agent A
success = 80%
cost/run = $0.10
Agent B
success = 90%
cost/run = $0.40
A useful comparison is:
cost per successful task
Approximately:
A = 0.10 / 0.80 = $0.125
B = 0.40 / 0.90 = $0.444
Agent B is more capable but much less efficient.
Depending on the application, either may be correct.
For an expensive production incident, the extra compute may be trivial.
For millions of low-value support requests, it may be unacceptable.
Search Is Not the Same as Retry
Retry says:
that failed
try again
Search says:
before committing, preserve multiple plausible trajectories
Retry is reactive.
Search is exploratory.
They can be combined, but they solve different problems.
Search Is Not the Same as Critique and Revision
Critique/revision usually follows one trajectory:
draft
↓
critique
↓
revision
↓
revision
Search keeps alternatives alive:
draft
/ | \
A B C
↓ ↓
A1 B1
Critique/revision is primarily depth.
Search adds breadth.
A strong agent often uses both:
generate branches
↓
select promising branches
↓
revise each
↓
verify
Search Is Not Yet MCTS
You will often see search agents described immediately in terms of Monte Carlo Tree Search.
That is an advanced technique.
We do not need it yet.
The core idea is already visible with beam search:
generate alternatives
evaluate partial states
allocate compute selectively
prune weak branches
MCTS adds more sophisticated selection and value propagation.
We will reserve that for Advanced Agents From First Principles.
The simpler search mechanism is easier to debug and often sufficient.
Application: Coding Agents
Coding agents are one of the clearest uses of search.
A bug rarely has exactly one plausible explanation.
A useful search state may include:
hypothesis
files inspected
tests executed
patches attempted
failures introduced
current verifier score
A branch could represent:
Hypothesis A: parser implementation regression
Hypothesis B: fixture mismatch
Hypothesis C: dependency-version change
Cheap branch evaluation:
run targeted test
inspect blame/diff
check environment version
Expensive evaluation:
implement patch
run full suite
The search agent should use cheap evidence before paying for expensive modifications.
Common coding-agent failure
agent edits code immediately
Better:
agent first generates competing diagnoses
↓
runs discriminating checks
↓
commits to strongest diagnosis
↓
edits code
This reduces destructive trial-and-error.
Application: Research Agents
Research agents benefit from search for a different reason.
The danger is not only wrong execution.
It is premature hypothesis commitment.
Instead of:
question
↓
first plausible explanation
↓
find supporting sources
use:
question
↓
competing hypotheses
↓
source search per hypothesis
↓
contradictory evidence
↓
rank explanations
A branch can represent a hypothesis plus its evidence set.
Search scores might include:
source quality
independence
claim coverage
contradiction handling
unresolved questions
This is particularly useful in technical investigations, economic analysis, literature reviews, and debugging research claims.
Application: Browser Agents
Browser agents operate in a partially observable environment.
A button label may be ambiguous.
Several navigation strategies may exist.
Search can preserve alternative interpretations:
checkout page
├── continue button advances
├── validation error blocks progress
└── address selector must be completed first
Instead of repeatedly clicking the same thing, the agent can evaluate branches using DOM state.
Useful node score:
required fields completed
validation errors reduced
page progress increased
repeated states penalized
Search is particularly useful when navigation is reversible or cheap to simulate.
It is much less attractive when actions have irreversible side effects.
Application: Data and Analytics Agents
Suppose an agent must repair a broken data pipeline.
Several transformations may plausibly fix a schema mismatch:
cast column
rename field
change join key
fill missing values
Search can evaluate them against deterministic validators.
candidate transformation
↓
run on fixture
↓
schema validation
↓
row-count invariants
↓
quality score
This is a particularly strong application because the environment provides objective feedback.
The agent does not need to guess whether the branch worked.
Application: Planning and Scheduling Agents
Search is useful when there are several valid plans under constraints.
For example:
schedule tasks
allocate workers
respect dependencies
minimize lateness
A branch represents a partial plan.
The evaluator can reject constraint violations immediately.
hard constraints → prune
soft objectives → score
This is often much better than asking an LLM to produce one final schedule and then hoping it satisfies everything.
Application: DevOps and Incident Agents
Incident response often involves competing hypotheses:
CPU saturation
bad deployment
network issue
database contention
cache failure
A search agent can prioritize diagnostic actions that distinguish hypotheses.
inspect metrics
check deployment diff
query DB locks
inspect upstream latency
The best search action may not fix the system immediately.
It may simply produce the most information.
That introduces an important idea:
Some actions are valuable because they reduce uncertainty.
This will become even more important in advanced agent search.
Search Actions Can Be Informational or Transformational
We can divide actions into two broad classes.
Informational
read file
run diagnostic
search docs
inspect metrics
query database
Transformational
edit file
deploy
submit form
change database
send message
Search generally works best when informational actions are cheap and transformational actions are expensive or risky.
A strong policy may prefer information-gathering branches early.
uncertain state
↓
cheap diagnostic actions
↓
confidence rises
↓
commit to transformation
This is a practical way to make agents less impulsive.
Branching on Irreversible Actions Is Dangerous
Search assumes branches can be evaluated independently.
That is easy in simulation.
It is harder in the real world.
You cannot:
send three alternative refunds
and later keep the best one.
For irreversible tools, search should happen before execution.
For example:
generate three refund plans
↓
validate policy
↓
select one
↓
execute once
This is a crucial boundary.
Search over plans or simulated effects when real actions cannot be undone safely.
Search Needs Provenance
Every branch should know where it came from.
@dataclass
class SearchNode:
id: str
parent_id: str | None
depth: int
action: dict | None
observation: dict | None
score: float
state: dict
This gives us a trajectory tree.
Now debugging can answer:
Which branch produced this result?
Why was another branch pruned?
Which evaluator score caused the decision?
Where did branches diverge?
Without lineage, search becomes very difficult to inspect.
Log the Search Frontier
A normal agent trace logs one trajectory.
A search agent should log the frontier.
For every expansion:
node ID
parent ID
depth
action
score
verification results
prune reason
cost
Useful summary metrics include:
nodes generated
nodes expanded
nodes pruned
unique branches
max depth
average branching factor
score improvement by depth
verified successes
search cost
Now search becomes observable software rather than invisible model reasoning.
Search Failure Mode: Weak Heuristic
Suppose the correct branch appears early but gets a mediocre partial score.
The search prunes it.
The final result fails.
This is search error caused by the evaluator.
We can measure something similar to the oracle@N idea from Best-of-N.
If a successful trajectory existed somewhere in generated branches but was pruned, the generator was not the main problem.
The search policy was.
Useful diagnostic:
oracle_generated_success
vs
selected_success
The gap measures search-selection failure.
Search Failure Mode: Search Becomes Model Self-Confirmation
If the same model:
generates branches
scores branches
explains why the winner is best
then diversity may be superficial.
The same bias can dominate every stage.
Whenever possible, introduce independent evidence:
tests
runtime output
retrieved facts
constraints
learned scorer
separate evaluator
Search quality depends on the independence of its feedback.
Search Failure Mode: Exploration Without Exploitation
An agent can also search too much.
It keeps generating new ideas instead of finishing one.
A
B
C
D
E
F
...
This is the opposite of premature commitment.
We need a balance:
explore alternatives
↓
identify promising region
↓
exploit / deepen
Beam search handles this crudely by pruning.
More advanced algorithms handle it explicitly.
For now, a simple policy is enough:
wide early
narrow later
Adaptive Beam Width
We do not need the same search width at every depth.
For example:
depth 0 → keep 5 hypotheses
depth 1 → keep 3
depth 2 → keep 2
depth 3 → keep 1
This mirrors how humans often solve problems:
consider several explanations
collect evidence
narrow possibilities
commit
It also reduces cost.
Confidence Is Not Enough
A model saying:
I am 95% confident
should not automatically collapse the search frontier.
Confidence can be poorly calibrated.
Better collapse conditions are evidence-based:
one hypothesis explains all observations
competing branches fail deterministic checks
verification threshold reached
score margin is stable across evaluators
Search should close because the evidence discriminated between branches, not merely because the model sounded certain.
A Practical Search-Agent Skeleton
Here is a compact version tying the pieces together.
from dataclasses import dataclass, field
from typing import Any, Callable
import uuid
@dataclass
class Node:
state: dict[str, Any]
score: float = 0.0
depth: int = 0
parent_id: str | None = None
id: str = field(default_factory=lambda: str(uuid.uuid4()))
action: dict[str, Any] | None = None
observation: dict[str, Any] | None = None
def beam_search(
initial_state: dict,
propose: Callable,
execute: Callable,
evaluate: Callable,
is_success: Callable,
beam_width: int = 3,
branch_factor: int = 3,
max_depth: int = 5,
):
frontier = [Node(state=initial_state)]
all_nodes = list(frontier)
for _ in range(max_depth):
children = []
for node in frontier:
actions = propose(node.state, branch_factor)
for action in actions:
observation, next_state = execute(node.state, action)
child = Node(
state=next_state,
depth=node.depth + 1,
parent_id=node.id,
action=action,
observation=observation,
)
child.score = evaluate(child)
children.append(child)
all_nodes.append(child)
if is_success(child):
return child, all_nodes
if not children:
break
children.sort(key=lambda n: n.score, reverse=True)
frontier = children[:beam_width]
return max(frontier, key=lambda n: n.score), all_nodes
This deliberately omits many production concerns.
But the mechanism is visible:
branch
execute or simulate
evaluate
prune
repeat
Search Before Side Effects
The skeleton above calls execute() on every branch.
That is only safe if actions are reversible, simulated, read-only, isolated, or otherwise branch-safe.
In production, often use:
propose plan
↓
simulate / validate
↓
score
↓
select
↓
execute chosen action once
For coding agents, separate worktrees or sandboxes can make branches executable.
For database agents, use transactions or fixtures.
For browser agents, use independent sessions where appropriate.
For irreversible business actions, do not execute competing branches in production.
Real Software Application Matrix
| Software | Search branches represent | Cheap evidence | Expensive commitment |
|---|---|---|---|
| Coding agent | bug hypotheses / patch strategies | targeted tests, static analysis, diff inspection | source edits, large test suite |
| Research agent | competing explanations | source retrieval, claim checks | final synthesis |
| Browser agent | navigation interpretations | DOM inspection, validation state | submission / purchase |
| Data agent | transformation strategies | fixture validation, schema checks | production write |
| DevOps agent | incident hypotheses | metrics, logs, health checks | restart / deploy / rollback |
| Planning agent | partial schedules or plans | constraint checks | committing resources |
| Support agent | resolution strategies | policy lookup, account state | refund / cancellation / escalation |
The same search mechanism appears in each system.
What changes is:
state
branch representation
evaluator
side-effect boundary
success condition
If Your Agent Picks the First Plausible Solution
A practical diagnostic sequence:
1. Check whether alternatives actually exist
If the task has one obvious deterministic path, search is unnecessary.
2. Generate alternatives before acting
Ask for distinct strategies, not paraphrases.
3. Measure branch diversity
If branches collapse to the same action, fix expansion first.
4. Add cheap discriminating tests
The search heuristic needs evidence.
5. Delay irreversible actions
Search plans before executing side effects.
6. Bound width and depth
Never allow unbounded expansion.
7. Log why branches were pruned
If the right answer was generated and discarded, the evaluator is the problem.
8. Compare against simpler baselines
Always benchmark against:
one-shot
Best-of-N
critique/revision
Search should earn its complexity.
Experiment: Does Search Actually Help?
Build a dataset of tasks with known success criteria.
Compare:
A: one-shot trajectory
B: Best-of-3 final answers
C: critique/revision
D: beam search width 2
E: beam search width 4
Hold constant where possible:
model
tool set
prompt information
verification rules
task set
Measure:
verified success
oracle generated success
pruning regret
model calls
tool calls
nodes expanded
latency
cost per successful task
Also inject tasks where the first plausible strategy is deliberately wrong.
That tests whether the system genuinely benefits from preserving alternatives.
Search Changes the Meaning of Agent Intelligence
Without search, the model often determines the trajectory in a single sequence.
model choice
↓
next state
↓
model choice
With search, the runtime becomes more important.
model proposes alternatives
↓
runtime preserves branches
↓
environment provides evidence
↓
runtime allocates compute
↓
model proposes next expansions
The intelligence of the system is no longer located entirely in the next-token prediction.
It is partly in the control structure around the model.
That is one of the central ideas of agent engineering.
Do You Actually Need Search?
Use search when:
multiple plausible paths exist
+
early choices strongly affect later outcomes
+
partial trajectories can be evaluated
+
additional inference is affordable
Do not add search merely because it sounds sophisticated.
If:
the task has a deterministic workflow
use normal code.
If:
the final answer is cheap to sample
Best-of-N may be enough.
If:
the first draft is usually close
critique/revision may be enough.
If:
multiple distinct paths must remain alive
search begins to justify itself.
Where We Are Now
Our agent has grown considerably.
structured actions
↓
Best-of-N
↓
critique + revision
↓
planning
↓
loop control
↓
tool routing
↓
memory
↓
search
But there is still a dangerous weakness.
The agent may produce an impressive trajectory, call the right tools, search several branches, and confidently tell us:
Done.
while the task has actually failed.
The next step is therefore one of the most important in the entire series:
Do not ask the agent whether it succeeded. Verify the result outside the model.
Next:
Agents From First Principles 09: AI Agent Says It Worked When It Didn’t? Verify the Result Outside the LLM.