Build a Planning Agent — Building AI Agents, Part 6
- Contributor
- 5 days ago
- 3 min read
Updated: 3 hours ago
Building AI Agents · Part 6
For complex tasks, jumping straight to tool calls produces unreliable results. A planning step — where the agent decides what to do before doing it — improves reliability significantly.
What You'll Build
An agent that plans before acting: task → plan → execute steps → verify.
Step 1: The Plan-Then-Execute Pattern (5 min)
Phase 1: PLAN
- Decompose task into steps
- Identify tools needed for each
- Note dependencies
Phase 2: EXECUTE
- Run each step
- Verify result
- Adjust if needed
Phase 3: REPORT
- Final answer
- What was done
- Any caveats
The plan is explicit. Steps can be reviewed.
Step 2: Generate the Plan (20 min)
def plan_task(task):
plan_prompt = f"""
Task: {task}
Available tools:
{format_tools(TOOLS)}
Generate a step-by-step plan. For each step:
- What to do
- Which tool to use
- What input
- What you expect to get
- Why this step is needed
Output as JSON list.
"""
response = call_llm(plan_prompt)
plan = json.loads(response)
return plan
The plan is a data structure you can inspect.
Step 3: Validate the Plan (15 min)
Before executing:
def validate_plan(plan):
issues = []
for i, step in enumerate(plan):
if step["tool"] not in [t["name"] for t in TOOLS]:
issues.append(f"Step {i}: unknown tool {step['tool']}")
if not step.get("expected"):
issues.append(f"Step {i}: no expected outcome")
# Check input matches schema
# ...
return issues
Plan with issues gets revised before execution.
Step 4: Execute Step-by-Step (20 min)
def execute_plan(plan):
results = []
for i, step in enumerate(plan):
# Execute
try:
result = TOOL_FN[step["tool"]](**step["input"])
except Exception as e:
return on_step_failure(plan, i, str(e), results)
# Verify against expectation
if not matches_expected(result, step["expected"]):
return on_unexpected_result(plan, i, result, results)
results.append({"step": i, "result": result})
return synthesize_answer(plan, results)
Each step verified before proceeding.
Step 5: Handle Step Failures (15 min)
def on_step_failure(plan, failed_step, error, prior_results):
# Ask the agent: what now?
prompt = f"""
Plan was:
{format_plan(plan)}
Step {failed_step} failed: {error}
Prior results:
{format_results(prior_results)}
Options:
1. Revise the plan from here
2. Try the step differently
3. Give up and explain
What should we do?
"""
decision = call_llm(prompt)
# Parse decision; act accordingly
The agent reasons about how to recover. Sometimes it'll revise; sometimes give up cleanly.
Step 6: Handle Unexpected Results (15 min)
The step succeeded but didn't return what was expected:
def on_unexpected_result(plan, step_idx, actual, prior_results):
prompt = f"""
Step {step_idx} was: {plan[step_idx]}
Expected: {plan[step_idx]['expected']}
Actual: {actual}
Should we:
1. Continue with the actual result (maybe expectation was wrong)
2. Revise the plan
3. Give up
"""
# Same pattern as failure handling
Step 7: Synthesize the Final Answer (15 min)
def synthesize_answer(plan, results):
prompt = f"""
Task: {plan[0]['task']} # Or however task is referenced
Steps executed:
{format_steps_and_results(plan, results)}
Compose a clear final answer for the user based on these results.
"""
return call_llm(prompt)
The agent composes a clean answer from execution data.
Step 8: Log Everything (15 min)
Planning agents produce more data per task:
def execute_planned_task(task):
plan = plan_task(task)
log_event("plan_generated", {"task": task, "plan": plan})
issues = validate_plan(plan)
if issues:
log_event("plan_invalid", {"issues": issues})
# Revise or fail
for i, step in enumerate(plan):
log_event("step_started", {"step_index": i, "step": step})
# ...
log_event("step_completed", {"step_index": i, "result": result})
log_event("task_complete", {"task": task, "final": final_answer})
return final_answer
Full visibility. Critical for debugging.
Step 9: Compare With Non-Planning Agent (varies)
For complex tasks:
# Non-planning
result_a = agent(task)
# Planning
result_b = planning_agent(task)
Measure:
Quality (was the right answer reached?)
Cost (planning agent uses more tokens)
Latency (planning agent is slower)
For complex tasks, planning often wins. For simple ones, it's wasteful overhead.
Step 10: Choose When to Plan (10 min)
Don't plan every task. Quick heuristic:
def needs_planning(task):
# Quick LLM call
decision = call_llm(f"""
Task: {task}
Is this a complex multi-step task that benefits from planning?
Or a simple task that can be done directly?
Respond: complex or simple
""")
return "complex" in decision.lower()
def agent(task):
if needs_planning(task):
return planning_agent(task)
else:
return simple_agent(task)
Right level of process for the task.
What You Just Did
You built a planning agent. For complex tasks, the explicit plan improves reliability and provides debuggability. For simple tasks, you skip the overhead.
Common Failure Modes
Plan everything. Wasteful for simple tasks.
Bad plans go straight to execution. Validate before executing.
No replanning. First plan failure means full failure.
Verbose plans. Plan output takes more tokens than execution. Compress.
Plans without verification. Step "succeeded" but result was wrong; no check.
Continue the Building AI Agents path
Previous — Part 5: Handle Agent Failures
Part of the Building AI Agents learning path.


