Skip to content

Chapter 13 · Planning, Iteration, and Error Handling ​

Chapter goals: Master the planning=True "plan-then-execute" mechanism, understand how max_iter and max_retry_limit constrain agent behavior, learn to use step_callback and usage_metrics to diagnose failures, and build intuition for common故障 patterns like tool loops and context overflow.

13.1 Why Planning Exists ​

In previous chapters, the crew's execution order is entirely determined by the task list, but how each task is done internally is up to the agent's on-the-fly judgment. For multi-step complex tasks (e.g., "research → compare → write report"), on-the-fly judgment easily skips steps or gets the order wrong.

CrewAI's planning (Planning) feature makes "think before doing" an explicit phase: when enabled, before every kickoff, all crew information is first handed to an AgentPlanner, which produces a step-by-step plan and appends it to each task's description.

python
# planning_demo.py —— minimal planning example
import os
from crewai import Agent, Task, Crew, Process, LLM

llm = LLM(
    model="openai/deepseek-chat",              # third-party OpenAI-compatible model
    base_url="https://api.deepseek.com/v1",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    temperature=0.7,
)

researcher = Agent(
    role="Senior Researcher",
    goal="Deeply research the latest advances in large language models",
    backstory="You have ten years of AI research experience, skilled at information retrieval and synthesis.",
    llm=llm,
)

writer = Agent(
    role="Report Writer",
    goal="Write a structured report based on research materials",
    backstory="A technical writing expert producing clearly organized reports.",
    llm=llm,
)

t1 = Task(
    description="Comprehensively research AI large language model advances over the past year.",
    expected_output="A list of 10 key points, each summarizing one advance in one sentence.",
    agent=researcher,
)
t2 = Task(
    description="Expand the previous task's results into a complete report.",
    expected_output="A Markdown report with section headings, no code fences.",
    agent=writer,
)

my_crew = Crew(
    agents=[researcher, writer],
    tasks=[t1, t2],
    process=Process.sequential,
    planning=True,          # the key: enable planning
)
result = my_crew.kickoff()
print(result.raw)

At runtime, logs will show Planning the crew execution, followed by a完整 plan like "Task Number 1: ... Step-by-Step Plan:" — this plan gets injected into the corresponding task's description.

Watch the default planning model

The official docs explicitly note: when planning is enabled, CrewAI defaults to gpt-4o-mini as the planning LLM, which requires a valid OpenAI API Key in the environment. If your team统一 uses a third-party model, you must explicitly set planning_llm, or the planning phase will fail with an auth error.

13.2 Specifying planning_llm: Route Planning Through Your Model ​

planning_llm accepts either a string (following LiteLLM provider rules) or an LLM instance. Per course conventions, route it to DeepSeek as well:

python
# Option 1: string form (provider/model)
my_crew = Crew(
    agents=[researcher, writer],
    tasks=[t1, t2],
    process=Process.sequential,
    planning=True,
    planning_llm="openai/deepseek-chat",   # openai/ prefix + env var key
)

# Option 2: explicit LLM instance for precise base_url and temperature control
plan_llm = LLM(
    model="openai/deepseek-chat",
    base_url="https://api.deepseek.com/v1",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    temperature=0.2,        # plans need determinism, lower temperature
)
my_crew2 = Crew(
    agents=[researcher, writer],
    tasks=[t1, t2],
    planning=True,
    planning_llm=plan_llm,
)

Two practical tips:

  • Lower planning model temperature (0–0.3): plans need determinism, not creativity;
  • No need for the strongest model: planning only does task decomposition; a tier below the execution model is usually sufficient.

13.3 Iteration Cap max_iter and Retry Cap max_retry_limit ​

Agent task execution is a "think → act → observe" loop. Two parameters control this loop's runaway boundary (both are Agent constructor parameters):

ParameterDefaultMeaning
max_iter20Max think-act iterations per task; once reached, the agent must deliver its current best answer
max_retry_limit2Max retries when a task execution error occurs; beyond this, the task fails outright
python
# Configure different runaway boundaries for agents of different nature
analyst = Agent(
    role="Data Analyst",
    goal="Perform multi-dimensional analysis on sales data",
    backstory="Proficient in data analysis, habitually validates conclusions repeatedly.",
    llm=llm,
    max_iter=30,          # complex analysis allows more iterations
    max_retry_limit=3,    # one extra retry chance for analysis-type tasks
)

coder = Agent(
    role="SQL Generator",
    goal="Convert natural language to SQL",
    backstory="I only output SQL.",
    llm=llm,
    max_iter=5,           # tighten the cap for simple deterministic tasks to prevent token-burning loops
    max_retry_limit=1,    # fail fast, expose the problem instead of masking it
)

Heuristics:

  • Tool-heavy exploratory agents → loosen max_iter (25–40), otherwise the agent gets forcibly wrapped before finding the answer;
  • Format-conversion agents → tighten max_iter (3–8); such tasks shouldn't need multiple iterations;
  • Reaching max_iter is not an error — the agent delivers a "best-effort" partial answer, so pair it with expected_output validation to judge whether the result is usable;
  • max_retry_limit only triggers on actual exceptions; "bad answer but no error" does not trigger retries.

13.4 Catching Intermediate Steps with step_callback ​

When debugging complex crews, seeing only the final output is insufficient — you need to observe what the agent did at each step. step_callback fires after every step of every agent (agent-level settings override crew-level settings):

python
# Log intermediate steps as JSONL for post-hoc analysis
import json
import os
from datetime import datetime
from crewai import Agent, Task, Crew, LLM

llm = LLM(
    model="openai/deepseek-chat",
    base_url="https://api.deepseek.com/v1",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
)

def step_logger(step):
    """Fires after each agent step; step is the step's output object."""
    record = {
        "ts": datetime.now().isoformat(timespec="seconds"),
        "step": str(step)[:500],       # truncate to prevent ultra-long lines
    }
    with open("steps.jsonl", "a", encoding="utf-8") as f:
        f.write(json.dumps(record, ensure_ascii=False) + "\n")

researcher = Agent(role="Researcher", goal="Research", backstory="Expert", llm=llm)
t1 = Task(description="List three AI trends worth watching in 2024.",
          expected_output="three key points.", agent=researcher)

crew = Crew(agents=[researcher], tasks=[t1], step_callback=step_logger)
crew.kickoff()

Paired with it is Task(task_callback=...): fires when the entire task completes, receiving the task output object — suitable for "task-level" auditing (Chapter 14 compares both systematically). Recommended combo during debugging: Crew-level step_callback for process + Task-level callback for products.

Another lightweight option is verbose=True: prints execution logs directly to the console. Open during development; close in production and use callbacks/events for persistence instead.

13.5 usage_metrics: Cost and Scale Monitoring ​

Each kickoff() return value carries a usage_metrics attribute aggregating token consumption and request count for this run:

python
result = crew.kickoff()
m = result.usage_metrics
print(f"Total tokens: {m.total_tokens}")
print(f"Prompt tokens: {m.prompt_tokens}, Completion tokens: {m.completion_tokens}")
print(f"Successful requests: {m.successful_requests}")

Piping this into your monitoring is straightforward:

python
# Append each run's usage to CSV for trend charts
import csv

with open("usage.csv", "a", newline="") as f:
    csv.writer(f).writerow([
        datetime.now().isoformat(),
        m.total_tokens, m.prompt_tokens,
        m.completion_tokens, m.successful_requests,
    ])

If one day total_tokens jumps several-fold, check three things first: is some agent furiously calling tools within max_iter? Is context growing unchecked? Did planning/memory introduce extra LLM calls?

13.6 Common Failure Modes and Remedies ​

Mode 1: Tool Loop. The agent repeatedly calls the same search tool, always feeling "not enough information." Remedies:

  • Tighten max_iter to force convergence within the cap;
  • Write "at most 3 searches" in the task description — LLMs respect such instructions;
  • Check whether tool return content is empty or erroring — empty results cause the agent to keep retrying with different keywords.

Mode 2: Context Overflow. An upstream task output a 8000-word report, all stuffed into the next task's context, exceeding the model window and throwing an error. Remedies:

  • Have upstream expected_output explicitly cap length (e.g., "under 300 words summary");
  • Use structured output (output_pydantic, see Chapter 12) instead of long text passing;
  • Switch to a third-party model with a longer context window, or enable memory for summarization compression (Chapter 10).

Mode 3: Silent Degradation. No error is raised after reaching max_iter, but the answer is incomplete. Remedy: add a guardrail to critical tasks or validate output in callbacks (length, required fields) — throw an exception to trigger a retry if不合格.

python
# A simple output quality gate
def quality_gate(task_output):
    if len(task_output.raw) < 100:
        raise ValueError("Output too short,疑似 degradation. Requires retry.")
    return task_output

t1 = Task(
    description="Write a product analysis essay.",
    expected_output="An analysis essay of at least 200 words.",
    agent=writer,
    callback=quality_gate,     # validate on task completion
)

Chapter Summary ​

  • planning=True has AgentPlanner generate a step-by-step plan before execution and inject it into task descriptions; default planning model is gpt-4o-mini; third-party model scenarios must explicitly set planning_llm;
  • max_iter (default 20) caps per-task iterations, delivering a "best-effort" answer once reached; max_retry_limit (default 2) only triggers on exceptions;
  • step_callback captures per-step process, Task(callback=...) validates products, usage_metrics monitors token cost;
  • Tool loops are governed by tightening iteration counts and explicit search budgets; context overflow is governed by capping upstream output length and using structured output.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. When planning=True is enabled without specifying planning_llm, what model does CrewAI use for planning?

2. Regarding the difference between max_iter and max_retry_limit, which is correct?

3. When is step_callback invoked?

4. Downstream tasks frequently hit context overflow from upstream's long output. Most targeted combo remedy?

🛠️ Hands-On Exercises ​

  1. Add planning=True and a DeepSeek planning_llm to the structured output example from Chapter 12; observe the Step-by-Step Plan in logs vs without planning.
  2. Write a task deliberately prone to tool looping (e.g., "search indefinitely until finding the perfect answer"); run with max_iter=3 and max_iter=20 respectively; compare total_tokens via usage_metrics.
  3. Upgrade the quality_gate: in addition to length validation, also check via regex whether the output contains specified required section headings — throw an exception on miss and observe max_retry_limit taking effect.

Next we upgrade "black-box callbacks" into a full Chapter 14 · Callbacks, Logging, and Event Listening.