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
| Dimension | Traditional Chatbot | AI Agent |
|---|---|---|
| Input / output | Text → text | Goal → multi-step actions |
| Decision-making | Single inference | Continuous planning, reflection, adjustment |
| Tool use | None | API calls, file I/O, code execution |
| Duration | Seconds | Minutes to hours, multi-iteration |
| Typical use | Q&A, writing | Office 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:
- One tool, one job.
- No more than 5 parameters.
- Clearly distinguish read vs. write actions (writes need human confirmation).
- Return structured error messages.
2.3 Planning
For complex tasks, the model should not just start executing — it should plan first, then execute, then adjust. Three main variants:
| Paradigm | Characteristics | Best for |
|---|---|---|
| Plan-and-Execute | Build a full plan first, then execute step by step | Clear goals, predictable path |
| ReAct | Reason-act-observe loop; replan every step | Exploratory, high-uncertainty |
| Tree-of-Thought | Explore multiple paths in parallel, pick the best | Creative, 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:
- Sequential pipeline: Researcher → Analyst → Writer → Reviewer.
- Debate: Pro Agent vs. Con Agent, judged by a Referee Agent.
- Hierarchical: a Manager Agent dispatches tasks, Worker Agents execute them.
- Group chat: all Agents share a workspace and talk freely.
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:
- Operator: based on the Computer Use paradigm — drives a browser to book tickets, shop, fill forms, etc.
- Deep Research: based on the Knowledge Research paradigm — autonomously gathers information and produces deep research reports.
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:
- Send the model a request (user goal + current GUI screenshot)
- The model analyzes and emits a function_call (specific UI action)
- Execute the function_call (in browser or mobile)
- 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
- Domain knowledge: document chunks retrieved by the RAG system.
- Tool data: descriptions and usage guides for available tools.
- Session state: conversation history, user preferences, current task state.
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:
- Zero-shot: 1×
- Agentic (5 rounds of reflection): 6×
- Multi-Agent (3 Agents): 18×
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
- ✅ Agent = LLM + Tools + Planning + Memory; the core capability is autonomous decision-making.
- ✅ Four design patterns: Reflection, Tool Use, Planning, Multi-Agent.
- ✅ Framework selection: LangGraph (fine-grained control), CrewAI (fast to start), AutoGen (human-in-the-loop), Google ADK (enterprise production).
- ✅ Agentic Agent era: from using single Agents as tools to autonomous intelligent systems.
- ✅ What truly determines Agent effectiveness is context engineering, not the model itself.
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
- Input: a GitHub PR URL (e.g.
https://github.com/owner/repo/pull/123) - Behavior: pull diff → run ruff + bandit → use an LLM for a synthesized review → POST the comment to the PR
- Features: large PRs are auto-chunked; humans can approve/reject at key nodes; failures are retryable; state is recoverable
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)
Step 7: advanced extensions
- GitHub Webhook integration: expose
POST /webhookwith FastAPI; triggerapp.streamonpull_requestevents; usethread_id = f"{repo}-{pr}"for idempotency. - Persistent checkpointer: replace
MemorySaverwithPostgresSaver(langgraph-checkpoint-postgres) — the Agent resumes from the last interrupt on restart. - Add memory: pair
langgraph.storewithInMemoryStore; remember the common issues found on this repo; prepend "Last time you found X here — it still exists" to the prompt. - Multi-Agent collaboration: split into
security_agent(security only),perf_agent(performance only),style_agent(style only) and aggregate them in anorchestratornode. - Token control: add
langchain.callbacks.get_openai_callback()inllm_review; if usage exceeds a threshold, chunk-and-summarize before reviewing.
Common issues & debugging
- Interrupt stuck: resume with
app.invoke(Command(resume="approve"), config). - GitHub 401: verify the
GITHUB_TOKENhas thereposcope. - Diff too long: split
state["diff"][:6000]into segments; review each, then merge. - Graph visualization: use
app.get_graph().draw_mermaid_png()to render the diagram.