Module 02 · Agent Evolution

From LLM to Agentic Agent:
The generational leap of intelligent agents

"Agent" is the hottest AI concept of 2024-2026. This chapter makes it concrete: what an Agent is, how an Agent is implemented with an LLM, the stages of Agent evolution, and what Agentic Agent means at the frontier.

1. What is an AI Agent?

A pragmatic definition:

AI Agent = Large Language Model (the brain) + Tools (the hands) + Planning (the decision) + Memory (the experience)

The key difference from a traditional LLM application is that the Agent has autonomous decision-making and execution. You give the Agent a goal; it breaks the task down, picks tools, calls APIs, reads the results, decides the next step, and continues until the goal is done.

1.1 Chatbot vs Agent — the essential difference

DimensionTraditional ChatbotAI Agent
Input / outputText → textGoal → multi-step actions
Decision-makingSingle inferenceContinuous planning, reflection, adjustment
Tool useNoneAPI calls, file I/O, code execution
DurationSecondsMinutes to hours, multi-iteration
Typical useQ&A, writingOffice automation, code generation, data analysis

1.2 The minimal Agent — code example

# The minimal Agent: let the LLM decide to use a calculator
from openai import OpenAI

client = OpenAI()
TOOLS = [{
    "type": "function",
    "function": {
        "name": "calculator",
        "description": "Evaluate a math expression. Supports + - * / and parentheses.",
        "parameters": {
            "type": "object",
            "properties": {
                "expression": {"type": "string"}
            }
        }
    }
}]

def calculator(expression: str) -> str:
    if not re.match(r'^[\d\s\+\-\*\/\(\)]+$', expression):
        return "Error: illegal characters"
    return str(eval(expression))

def run_agent(user_input, max_steps=5):
    messages = [{"role": "user", "content": user_input}]
    for step in range(max_steps):
        # 1. Let the LLM decide the next step
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=TOOLS,
        )
        msg = response.choices[0].message
        # 2. If the LLM wants to call a tool, run it
        if msg.tool_calls:
            tool_call = msg.tool_calls[0]
            result = calculator(tool_call.function.arguments)
            messages.append({"role": "tool", "content": result})
        else:
            # 3. LLM gives the final answer
            return msg.content
    return "Agent exceeded the maximum number of steps."

# Test
print(run_agent("A rectangle is 12m by 8m. Find its area and perimeter."))
# Output: The area is 96 square meters and the perimeter is 40 meters.

2. The four core Agent design patterns

In 2024, Andrew Ng summarized four patterns that distinguish an Agent from a plain LLM call. You can use any of them alone, or combine them.

2.1 Reflection

Have the model check its own work — after writing code, let the same (or a different) model review it; if it finds issues, rewrite.

The key insight: the model is sharper at "spotting other people's mistakes" than at "not making them in the first place". This is the asymmetry of cognitive load — when generating, the model juggles content, structure, style, and accuracy all at once; when reviewing, it can focus on one question.

def reflect_and_refine(task, max_iterations=3):
    output = llm.invoke(f"Please complete: {task}")
    for i in range(max_iterations):
        critique = llm.invoke(f"Review the following output for issues: {output}\nIf none, reply NO_ISSUES")
        if "NO_ISSUES" in critique:
            break
        output = llm.invoke(f"Apply this feedback: {critique}")
    return output

Result: on HumanEval, this "reflection" loop raised GPT-3.5's accuracy from 48% to 95% — surpassing GPT-4's zero-shot score of 67.5%.

2.2 Tool use

LLM knowledge has a cutoff date and cannot interact with the outside world. Tool use gives the LLM the ability to search fresh information, execute code, query databases, and call APIs.

Golden rules for tool design:

2.3 Planning

For complex tasks, the model should not just start executing — it should plan first, then execute, then adjust. Three main variants:

ParadigmCharacteristicsBest for
Plan-and-ExecuteBuild a full plan first, then execute step by stepClear goals, predictable path
ReActReason-act-observe loop; replan every stepExploratory, high-uncertainty
Tree-of-ThoughtExplore multiple paths in parallel, pick the bestCreative, complex problems

2.4 Multi-Agent collaboration

When a task crosses domains and demands many specialized skills, a single Agent often becomes a bottleneck. There are four common multi-Agent topologies:

3. Comparing mainstream Agent frameworks

By 2025-2026, the four hottest Agent frameworks on GitHub are:

3.1 LangGraph (from the LangChain team)

Core abstraction: a state-graph (StateGraph) execution engine.

from langgraph.graph import StateGraph, END
from typing import TypedDict

class State(TypedDict):
    messages: list
    next_step: str

workflow = StateGraph(State)
workflow.add_node("think", think_node)
workflow.add_node("act", act_node)
workflow.add_node("observe", observe_node)
workflow.add_conditional_edges("think", router, {"act": "act", "end": END})
workflow.add_edge("act", "observe")
workflow.add_edge("observe", "think")
app = workflow.compile()

Highlights: maximum flexibility — you define every flow. 10k+ stars on GitHub. The learning curve is steeper, but production features are the most complete (LangSmith integration, checkpointer persistence, human-in-the-loop).

3.2 CrewAI

Core abstraction: Role + Task + Crew.

from crewai import Agent, Task, Crew

researcher = Agent(
    role="Market researcher",
    goal="Deeply analyze the target market and surface trends and opportunities",
    backstory="You are a senior market researcher with 10 years of experience",
)
writer = Agent(
    role="Report writer",
    goal="Turn research findings into a clear business report",
    backstory="You are a top-tier business report writer",
)

crew = Crew(agents=[researcher, writer], tasks=[research_task, write_task])
result = crew.kickoff()

Highlights: 25k+ stars; fastest to start (15 lines and you are running). Role-based + task pipelines — perfect for business teams building Agents quickly.

3.3 AutoGen (Microsoft)

Core abstraction: Agents "chat" with each other like people.

from autogen import AssistantAgent, UserProxyAgent

assistant = AssistantAgent(
    name="Coding assistant",
    llm_config={"config_list": [{"model": "gpt-4o"}]},
)
user = UserProxyAgent(name="User", human_input_mode="TERMINATE")
user.initiate_chat(assistant, message="Write me a data analysis script")

Highlights: 42k+ stars. The most natural human-in-the-loop design — well suited to frequent human-AI collaboration. Debugging is more involved.

3.4 Google ADK (Agent Development Kit)

Released in 2025, Google's Agent SDK targets "production-readiness", with deep Gemini integration and a built-in A2A (Agent-to-Agent) protocol.

Framework selection decision tree

Are you building an Agent system?
│
├─ Need fine-grained flow control / RAG / state management? ─→ LangGraph
│
├─ Multi-role workflow (research / analysis / writing)? ──────→ CrewAI
│
├─ Frequent human-in-the-loop (coding assistant / data Q&A)? ─→ AutoGen
│
├─ Quick prototype, demo in days? ───────────────────────────→ CrewAI → LangGraph
│
└─ Production system, observability and deployment required? → LangGraph

4. Agentic Agent: from single Agent to autonomous systems

"Agentic" is not a new word, but in 2024-2025 it became the industry's central narrative. Its core meaning: moving from "using LLMs as a tool" to "building autonomous, continuously-running intelligent systems".

4.1 OpenAI's Agent roadmap

In January 2025, OpenAI announced the Agent era with two products:

Sam Altman called this "the third layer of AGI — the Agent layer".

4.2 Anthropic Computer Use

In October 2024, Anthropic let Claude use a computer like a person — looking at the screen, moving the cursor, clicking buttons, typing text. The Computer Use update to Claude 3.5 Sonnet opened the door to "AI directly operating GUIs".

4.3 Google Gemini 2.5 Computer Use

In 2025, Google released Gemini 2.5 Computer Use Preview, using a cyclic interaction model:

  1. Send the model a request (user goal + current GUI screenshot)
  2. The model analyzes and emits a function_call (specific UI action)
  3. Execute the function_call (in browser or mobile)
  4. Capture a new screenshot, feed it back to the model, start a new cycle

On WebArena, Online-Mind2Web, and Mobile Control, it achieves lower latency than competitors.

4.4 GitHub Agentic Workflows

In early 2026, GitHub released Agentic Workflows as a technical preview, letting developers define automation goals in Markdown so coding Agents can run them through GitHub Actions:

# Daily Repo Status Report
Create a daily status report for maintainers. Include:
- Recent activity (issues, PRs, discussions, releases)
- Progress tracking, goal reminders
- Project status and recommendations
- Actionable next steps
Keep it concise and link to the relevant items.

Built-in multi-layer safety (sandbox, read-by-default, safe output review) — embodying the "Continuous AI" vision.

5. Context engineering: the real core of the Agent era

In late 2025, the industry converged on a consensus: what determines Agent effectiveness is not the model itself but context quality. Andrew Ng said in 2025: "Context engineering matters more than prompt engineering."

5.1 The three contexts an Agent needs

5.2 The rise of tool retrieval

When an enterprise has hundreds of tools, stuffing all their descriptions into the prompt makes the LLM "indecisive". The fix: build an index over tool descriptions and dynamically retrieve the top 3 tools for the current task.

Even a simple BM25 keyword search makes a strong baseline for this job.

6. Production pitfalls

6.1 State management going off the rails

Use a unified schema + session isolation (thread_id) to avoid "returning user A's order to user B".

6.2 Cost explosion

Agent loops amplify token consumption exponentially. Typical multipliers:

Fix: tiered model strategy (cheap models for simple tasks, expensive ones for complex ones) + context-window management + hard budget caps.

6.3 Hallucination amplification

In an Agent loop, one hallucination becomes a "fact" feeding the next step, multiplying the error. Fix: verify tool-call results, double-verify critical nodes.

6.4 Latency

User patience is ~3 seconds. Multi-step serial Agent loops easily exceed 10s. Fix: stream output + fast paths (skip the loop for simple questions) + parallelize independent tasks.

7. Chapter summary

Hands-on: build a production-grade Agent with LangGraph — an intelligent PR-review Agent

This section uses LangGraph to build an Agent that automatically reviews GitHub Pull Requests, from zero. It has 5 nodes (load diff → static check → LLM review → generate comment → write back to GitHub), with state persistence + human-in-the-loop interrupts. The full code is around 350 lines and actually runs locally.

Project requirements

Step 1: environment setup

pip install langgraph langchain-anthropic langchain-openai httpx tenacity python-dotenv
# verify
python -c "import langgraph, langchain_anthropic; print('langgraph', langgraph.__version__)"

Env vars (in .env):

ANTHROPIC_API_KEY=sk-ant-xxx
GITHUB_TOKEN=ghp_xxx

Steps 2 + 3: define state + nodes (full code: pr_agent.py, ~280 lines)

"""LangGraph PR Review Agent.

Usage:
    python pr_agent.py https://github.com/owner/repo/pull/123
"""
from __future__ import annotations

from typing import Annotated, Literal, TypedDict

from langchain_anthropic import ChatAnthropic
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import interrupt
from pydantic import BaseModel
from tenacity import retry, stop_after_attempt, wait_exponential


# ---------- 1. State definition ----------
class PRState(TypedDict, total=False):
    """Global state passed through the entire graph."""
    pr_url: str
    owner: str
    repo: str
    pr_number: int
    diff: str
    files: list[str]
    lint_issues: list[str]
    review_comments: list[str]
    final_report: str
    needs_human: bool
    error: str
    attempts: int


# ---------- 2. Tool helpers ----------
def parse_pr_url(url: str) -> tuple[str, str, int]:
    """Parse https://github.com/owner/repo/pull/123."""
    m = re.match(r"https://github\.com/([^/]+)/([^/]+)/pull/(\d+)", url)
    if not m:
        raise ValueError(f"bad PR URL: {url}")
    return m.group(1), m.group(2), int(m.group(3))


@retry(stop=stop_after_attempt(3), wait=wait_exponential(min=1, max=10))
def github_get(path: str, token: str) -> dict:
    """GET helper for the GitHub API with retries."""
    r = httpx.get(
        f"https://api.github.com{path}",
        headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()


@retry(stop=stop_after_attempt(3), wait=wait_exponential(min=1, max=10))
def github_post(path: str, body: dict, token: str) -> dict:
    """POST helper for the GitHub API with retries."""
    r = httpx.post(
        f"https://api.github.com{path}",
        headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"},
        json=body,
        timeout=30,
    )
    r.raise_for_status()
    return r.json()


# ---------- 3. Node definitions ----------
def node_load_diff(state: PRState) -> PRState:
    """Node 1: fetch the PR diff + file list."""
    print("[node] load_diff")
    try:
        owner, repo, pr = parse_pr_url(state["pr_url"])
        diff = httpx.get(
            f"https://patch-diff.githubusercontent.com/raw/{owner}/{repo}/pull/{pr}.diff",
            headers={"Accept": "application/vnd.github.v3.diff"},
            timeout=30,
        ).text
        meta = github_get(f"/repos/{owner}/{repo}/pulls/{pr}/files", os.environ["GITHUB_TOKEN"])
        files = [f["filename"] for f in meta]
        print(f"  loaded {len(files)} files, diff {len(diff)} chars")
        return {"owner": owner, "repo": repo, "pr_number": pr, "diff": diff, "files": files}
    except Exception as e:
        return {"error": f"load_diff failed: {e}"}


def node_lint(state: PRState) -> PRState:
    """Node 2: run ruff + bandit on every Python file; collect issues."""
    print("[node] lint")
    if state.get("error"):
        return {}
    issues: list[str] = []
    for f in state.get("files", []):
        if not f.endswith(".py"):
            continue
        # Write to a temp file for the tool (use in-memory in real projects)
        with open(f, "r", encoding="utf-8", errors="ignore") as fp:
            src = fp.read()
        # ruff (skipped if not installed)
        try:
            r = subprocess.run(["ruff", "check", "--stdin-filename", f, "-"],
                               input=src, capture_output=True, text=True, timeout=15)
            if r.stdout.strip():
                issues.append(f"### ruff: {f}\n```\n{r.stdout}\n```")
        except FileNotFoundError:
            pass
        # bandit (optional)
        try:
            r = subprocess.run(["bandit", "-q", "-"], input=src, capture_output=True, text=True, timeout=15)
            if r.stdout.strip():
                issues.append(f"### bandit: {f}\n```\n{r.stdout}\n```")
        except FileNotFoundError:
            pass
    print(f"  collected {len(issues)} lint blocks")
    return {"lint_issues": issues}


def node_llm_review(state: PRState) -> PRState:
    """Node 3: use Claude to combine diff + lint into a structured review."""
    print("[node] llm_review")
    if state.get("error"):
        return {}
    llm = ChatAnthropic(model="claude-haiku-4-5", temperature=0)
    prompt = f"""You are a strict code reviewer. Based on the info below, produce a Markdown report with 3 sections:
## 1. Lint results
## 2. Security issues
## 3. Improvement suggestions

PR: {state['pr_url']}
Changed files: {', '.join(state.get('files', []))}

=== Lint output ===
{chr(10).join(state.get('lint_issues', [])) or '(none)'}

=== Diff (first 6000 chars) ===
{state.get('diff', '')[:6000]}
"""
    try:
        resp = llm.invoke(prompt)
        report = resp.content if isinstance(resp.content, str) else resp.content[0].text
        # Big PRs get a human checkpoint
        needs_human = len(state.get("diff", "")) > 20000
        print(f"  report={len(report)} chars needs_human={needs_human}")
        return {"final_report": report, "needs_human": needs_human, "attempts": state.get("attempts", 0) + 1}
    except Exception as e:
        return {"error": f"llm_review failed: {e}"}


def node_human_approve(state: PRState) -> PRState:
    """Node 4: Human-in-the-loop interrupt — wait for human approval."""
    if not state.get("needs_human"):
        return {}
    print("[node] human_approve: waiting for human...")
    decision = interrupt({
        "question": "The report is ready. POST it to GitHub?",
        "preview": state.get("final_report", "")[:2000],
    })
    if decision != "approve":
        return {"error": f"human rejected: {decision}"}
    return {}


def node_post_comment(state: PRState) -> PRState:
    """Node 5: POST the report as a PR comment."""
    print("[node] post_comment")
    if state.get("error"):
        return {}
    try:
        body = {
            "body": f"## 🤖 AI Code Review\n\n{state.get('final_report', '(empty)')}\n\n---\n*Generated by LangGraph PR Agent*",
        }
        github_post(
            f"/repos/{state['owner']}/{state['repo']}/issues/{state['pr_number']}/comments",
            body,
            os.environ["GITHUB_TOKEN"],
        )
        print("  ✓ posted to GitHub")
    except Exception as e:
        return {"error": f"post_comment failed: {e}"}
    return {}


# ---------- 4. Routing ----------
def route_after_review(state: PRState) -> Literal["human_approve", "post_comment", "__end__"]:
    """Decide the next step from state: error → end; needs human → interrupt; else → post."""
    if state.get("error"):
        return END
    if state.get("needs_human"):
        return "human_approve"
    return "post_comment"


# ---------- 5. Compile the graph ----------
def build_graph():
    g = StateGraph(PRState)
    g.add_node("load_diff", node_load_diff)
    g.add_node("lint", node_lint)
    g.add_node("llm_review", node_llm_review)
    g.add_node("human_approve", node_human_approve)
    g.add_node("post_comment", node_post_comment)

    g.add_edge(START, "load_diff")
    g.add_edge("load_diff", "lint")
    g.add_edge("lint", "llm_review")
    g.add_conditional_edges("llm_review", route_after_review, {
        "human_approve": "human_approve",
        "post_comment": "post_comment",
        END: END,
    })
    g.add_edge("human_approve", "post_comment")
    g.add_edge("post_comment", END)
    return g.compile(checkpointer=MemorySaver())


# ---------- 6. Entry point ----------
def main():
    import sys
    if len(sys.argv) != 2:
        print("usage: python pr_agent.py <pr_url>")
        sys.exit(1)
    app = build_graph()
    config = {"configurable": {"thread_id": sys.argv[1]}}
    print(f"=== Reviewing {sys.argv[1]} ===")
    for event in app.stream({"pr_url": sys.argv[1]}, config=config):
        print(event)
    print("=== Done ===")


if __name__ == "__main__":
    main()

Step 5: run it & test

# 1. Test with a real, small PR (fork a demo repo)
python pr_agent.py https://github.com/your-name/demo/pull/1

Expected output (terminal + GitHub):

=== Reviewing https://github.com/your-name/demo/pull/1 ===
[node] load_diff
  loaded 3 files, diff 1234 chars
[node] lint
  collected 1 lint blocks
[node] llm_review
  report=842 chars needs_human=False
[node] post_comment
  ✓ posted to GitHub
=== Done ===

Screenshot: the GitHub PR page should show an AI comment with the "🤖 AI Code Review" header and the 3-section report.

Step 6: graph structure (Mermaid)

START ↓ load_diff → lint → llm_review ↓
human_approve post_comment END (on error) ↓ ↓ (interrupt) END

Step 7: advanced extensions

Common issues & debugging

📚 Previous: 11.1 Agent vs. LLM · Next chapter: Overseas Top Three — OpenAI / Anthropic / Google compared