Chapter 13 · Planning, Iteration, and Error Handling
Chapter goals: Master the
planning=True"plan-then-execute" mechanism, understand howmax_iterandmax_retry_limitconstrain agent behavior, learn to usestep_callbackandusage_metricsto 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.
# 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:
# 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):
| Parameter | Default | Meaning |
|---|---|---|
max_iter | 20 | Max think-act iterations per task; once reached, the agent must deliver its current best answer |
max_retry_limit | 2 | Max retries when a task execution error occurs; beyond this, the task fails outright |
# 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_iteris not an error — the agent delivers a "best-effort" partial answer, so pair it withexpected_outputvalidation to judge whether the result is usable; max_retry_limitonly 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):
# 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:
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:
# 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_iterto 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_outputexplicitly 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不合格.
# 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=Truehas AgentPlanner generate a step-by-step plan before execution and inject it into task descriptions; default planning model isgpt-4o-mini; third-party model scenarios must explicitly setplanning_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_callbackcaptures per-step process,Task(callback=...)validates products,usage_metricsmonitors 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
- Add
planning=Trueand a DeepSeekplanning_llmto the structured output example from Chapter 12; observe the Step-by-Step Plan in logs vs without planning. - Write a task deliberately prone to tool looping (e.g., "search indefinitely until finding the perfect answer"); run with
max_iter=3andmax_iter=20respectively; compare total_tokens viausage_metrics. - 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 observemax_retry_limittaking effect.
Next we upgrade "black-box callbacks" into a full Chapter 14 · Callbacks, Logging, and Event Listening.