top of page

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

Part of the Building AI Agents learning path.

bottom of page