③ Agent Core

Course path (9 chapters)
  1. LLM
  2. RAG
  3. Agent Core
  4. Retrieval Engineering
  5. LangChain & LangGraph
  6. MCP & A2A
  7. OpenClaw & Hermes
  8. Multi-agent & KG
  9. Multimodal
Chapter 3

Agent Core

From chatbot to acting agent — before MCP, before LangGraph

Prerequisites: LLM + RAG.

Agents teach action. RAG gives memory; Agent loop gives behavior.
Why Agent

Why LLM alone is not an Agent

Chatbot (1 pass)

User → LLM → text reply. No side effects. May hallucinate facts not in weights.

Agent (N cycles)

User → plan → tool/RAG → observe → plan → … → answer. Grounded + actionable.

User LLM Tools RAG Env Agent = LLM orchestrating Memory + Tools + Environment feedback
ReAct

ReAct framework

Reason + Act — paper: Yao et al., 2022

Thought: Decompose task; decide if a tool is needed
Action: Emit tool name + JSON arguments (or final answer)
Observation: Tool result appended to message history

Loop until the model emits a final answer (no more tool_calls). Typical max iterations: 5–15.

ChatReAct Agent
1 LLM call1–N LLM calls
Static contextContext grows with observations
Cannot verifyCan look up, compute, call APIs
Worked example

ReAct trace: book a flight

Thought: User wants NYC→LON next Friday. I need flight search, not memory.
Action: search_flights(origin="NYC", dest="LON", date="2025-06-06")
Observation: [{"id":"BA112","price":620,"dep":"18:30"}, ...]
Thought: Cheapest reasonable option is BA112. Confirm with user policy (economy only).
Action: book_flight(flight_id="BA112", passenger="Alice")
Observation: {"pnr":"X7K9P2","status":"confirmed"}
Action: Reply: "Booked BA112, confirmation X7K9P2."

Minimal loop (pure Python)

MAX_STEPS = 10
messages = [{"role": "user", "content": user_query}]

for step in range(MAX_STEPS):
    response = client.chat.completions.create(
        model="gpt-4o-mini", messages=messages, tools=TOOLS)
    msg = response.choices[0].message
    messages.append(msg)

    if not msg.tool_calls:
        return msg.content  # final answer

    for call in msg.tool_calls:
        result = run_tool(call.function.name, json.loads(call.function.arguments))
        messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
Tools

Tool calling in practice

1. Define JSON Schema (OpenAI format)

tools = [{
  "type": "function",
  "function": {
    "name": "search_docs",
    "description": "Search internal knowledge base",
    "parameters": {
      "type": "object",
      "properties": {
        "query": {"type": "string"},
        "top_k": {"type": "integer", "default": 5}
      },
      "required": ["query"]
    }
  }
}]

2. Tool design checklist

RuleWhy
One job per toolModel picks correctly
Clear descriptionWhen to use vs other tools
Return structured JSONEasy for model to parse
Handle errors gracefullyReturn error string as Observation, don't crash
Idempotent when possibleSafe if model retries

3. Connect RAG as a tool (not inline)

def search_docs(query: str, top_k: int = 5) -> str:
    chunks = retriever.invoke(query)[:top_k]
    return json.dumps([{"id": c.metadata["id"], "text": c.page_content} for c in chunks])

Agent decides when to retrieve — useful for multi-step tasks ("compare refund vs exchange policy" → two searches).

Intent

Intent recognition & routing

First gate: send query to the right subsystem

IntentSignalsRoute
Knowledge QA"what is", "explain", policy questionsRAG
Transaction"book", "cancel", "update order"Tool agent
Chitchatgreetings, off-topicDirect LLM (no tools)
Escalationangry, "speak to human"Handoff tool

Router implementation (LLM classifier)

ROUTER_PROMPT = '''Classify intent. Reply JSON only: {"intent": "...", "confidence": 0.0-1.0}
Intents: knowledge_qa | transaction | chitchat | escalation'''

def route(user_msg: str) -> str:
    r = llm.invoke(ROUTER_PROMPT + "\nUser: " + user_msg)
    intent = json.loads(r.content)["intent"]
    if intent == "knowledge_qa":
        return rag_chain.invoke(user_msg)
    if intent == "transaction":
        return agent_loop.invoke(user_msg)
    return llm.invoke(user_msg)  # chitchat

Alternative routers

Fallback: if confidence < 0.7, ask clarifying question instead of wrong route.
Context

Multi-turn context management

Token budget assembly (priority order)

  1. System rules & safety instructions (never drop)
  2. Current task state JSON (slots, pending tool results)
  3. Retrieved docs / RAG chunks (top-k by relevance)
  4. Recent conversation turns (newest first until budget full)
  5. Summary of older turns (if summarize strategy)

Sliding window + summarize pattern

TOKEN_BUDGET = 12000

def build_context(history, retrieved, system):
    msgs = [system]
    msgs += retrieved  # RAG chunks as system or tool messages
    recent, old = split_at_token_limit(history, TOKEN_BUDGET * 0.6)
    if old:
        summary = llm.invoke(f"Summarize this conversation:\n{old}")
        msgs.append({"role": "system", "content": f"Earlier summary: {summary}"})
    msgs += recent
    return trim_to_budget(msgs, TOKEN_BUDGET)
StrategyBest forTradeoff
Sliding windowShort support chatsForgets old facts
SummarizeLong sessionsSummary loss
Retrieve history"What did I say Tuesday?"Needs vector index of turns
Structured stateForms, booking flowsRequires schema design
Lab

Hands-on lab: build a ReAct agent

~60 min — no LangGraph required

pip install openai python-dotenv
  1. Two tools: search_faq (hard-coded dict or RAG) + create_ticket (append to JSON file).
  2. Implement the loop from #react-example; cap at 8 steps; log each Thought/Action/Observation.
  3. Add router: chitchat bypasses tools; FAQ questions must call search_faq first.
  4. Test cases: (a) "Hi" → no tool; (b) "Refund policy?" → search; (c) "File ticket for broken item" → create_ticket after confirm.
  5. Context test: 10-turn conversation; verify turn 11 still knows order ID from turn 3 (sliding window or summary).
  6. Failure injection: return tool error — agent should recover and explain to user.

Success criteria

MetricTarget
Tool selection accuracy≥ 90% on 10 labeled queries
Grounded answersFAQ answers cite retrieved chunk
Loop terminationNo infinite tool loops

Next: tune retrieval in Retrieval Engineering; frameworks in LangChain & LangGraph.

Evaluation

Agent evaluation

Offline eval (before deploy)

MetricHow to measureTarget
Tool selection accuracyLabel 20 queries; check correct tool chosen≥ 90%
Argument validityJSON schema parse + business rules100% parseable
Task success rateHuman or LLM judge: goal met?Domain-specific
Steps to completionAvg ReAct iterationsLower = cheaper
# Simple tool accuracy eval
def eval_tool_choice(test_cases, agent_fn):
    correct = 0
    for case in test_cases:
        trace = agent_fn(case["query"], return_trace=True)
        if trace["tools_called"] == case["expected_tools"]:
            correct += 1
    return correct / len(test_cases)

Online eval (production)

Benchmarks: SWE-bench (code), WebArena (web), AgentBench. Cross-course matrix: Evaluation Guide.