· 12 min read

New to LLMs? An Engineering Guide to In-Context Learning, RAG, Fine-Tuning, and Agents

A comprehensive, practical architecture guide demystifying Large Language Models: comparing prompt engineering, Retrieval-Augmented Generation (RAG), and fine-tuning, alongside tool use, LangChain workflows, and when to avoid autonomous agents.

Noureddine Driouech
Noureddine Driouech
AI Software Engineer
#LLMs #RAG #AI Engineering #LangChain #Architecture
New to LLMs? An Engineering Guide to In-Context Learning, RAG, Fine-Tuning, and Agents
Table of Contents

The explosion of Large Language Models (LLMs) has created an overwhelming amount of noise. Every week brings a flurry of new model releases, buzzwords, and framework abstractions. If you are a developer looking to build real systems rather than toys, the critical question is: how do you systematically adapt, control, and orchestrate these models to solve deterministic business problems?

In this guide, we break down the core mechanics of LLM application architecture from first principles. We will contrast the three core techniques for steering model behavior—In-Context Learning, Retrieval-Augmented Generation (RAG), and Fine-Tuning—explore the mechanics of Tool Calling, walk through a real LangChain implementation, and dissect when you should (and more importantly, should not) build autonomous agents.


1. What Exactly Is an Agent? (Model vs. System)

Before discussing advanced pipelines, we must untangle two concepts that are frequently conflated: the model and the agent.

+-----------------------------------------------------------+
|                        AI AGENT                           |
|                                                           |
|  +-------------------+       +-------------------------+  |
|  |    Large/Small    | <---> | Short & Long-Term Memory|  |
|  |  Language Model   |       |   (KV Cache / Vector)   |  |
|  +-------------------+       +-------------------------+  |
|           ^                                               |
|           | (Tool Invocations & Observability)            |
|           v                                               |
|  +-----------------------------------------------------+  |
|  |     Deterministic Tools & External Environments     |  |
|  | (REST APIs, SQL DBs, File Systems, Calculators)     |  |
|  +-----------------------------------------------------+  |
+-----------------------------------------------------------+

The Model: The Next-Token Predictor

A Large Language Model (whether GPT-4o, Claude 3.5 Sonnet, Llama 3, or Mistral) is fundamentally a massive probabilistic neural network. Given a sequence of tokens, its objective is to predict the probability distribution of the next token:

$$P(w_t \mid w_1, w_2, \dots, w_{t-1})$$

A raw model is stateless, frozen in time (its training cut-off), and computationally isolated. It cannot execute SQL queries, query your private CRM, verify mathematical arithmetic with 100% precision, or know what time it is right now.

The Agent: The Orchestrated System

An Agent is an architectural pattern built around the model. It wraps the core LLM with:

  1. Planning & Reasoning Loops: Directing the model to evaluate a goal, break it into sub-tasks, and reason over intermediate outcomes (e.g., the ReAct pattern: Reason + Act).
  2. Deterministic Tools: Interfaces allowing the model to invoke external software: web scrapers, database drivers, bash terminals, or custom internal APIs.
  3. Memory Systems: State persistence spanning short-term conversation context and long-term vector indexing.
  4. Execution Guardrails: Deterministic validation layers that inspect model outputs before executing actions.

2. Choosing Your Foundation: Large vs. Small, Open vs. Closed

Selecting a foundation model is an engineering tradeoff between latency, token cost, reasoning depth, and data sovereignty.

Model ClassRepresentative ExamplesPrimary StrengthsPrimary WeaknessesBest Use Cases
Frontier Closed-WeightsClaude 3.5 Sonnet, GPT-4oState-of-the-art coding, complex multi-step reasoning, massive context windows.Vendor lock-in, data sent to external cloud, per-token billing, latency spikes.High-ambiguity reasoning, multi-language code generation, complex planning.
Open-Weights (Local/Self-Hosted)Llama 3.1 (8B/70B), Mistral Nemo, Qwen 2.5Zero data leakage, full fine-tuning freedom, reproducible offline latency.Hardware requirements (VRAM), self-managed infrastructure.Enterprise privacy, air-gapped on-premise deployments, cost-effective high-volume batch tasks.
Small Language Models (SLMs)Phi-3 Mini (3.8B), Gemma 2 (2B/9B)Sub-10ms response times, runnable on edge/mobile devices, ultra-low resource usage.Limited multi-step reasoning, struggles with complex zero-shot instruction following.Classification, entity extraction, intent routing, semantic filtering.

For high-throughput systems, the winning architecture is rarely a single monolithic frontier model. Instead, modern systems utilize model routing: a fast, inexpensive Small Language Model routes and classifies incoming queries, escalating to a frontier model only when complex multi-step synthesis is required.


3. The 3 Methods to Tailor an LLM: The Student Analogy

When an out-of-the-box model lacks domain knowledge or fails to follow your required formatting, you have three primary engineering strategies.

Think of an LLM as a university student taking an exam:

  1. In-Context Learning (Prompting) is handing the student a quick cheat-sheet with a few worked examples right before they write the answer.
  2. Retrieval-Augmented Generation (RAG) is an open-book exam, where the student is sitting in a library and can look up exact reference textbooks on demand.
  3. Fine-Tuning is putting the student through three months of intensive drills to fundamentally alter their habits, tone, and internalized knowledge.
                      KNOWLEDGE & CONTROL STRATEGIES
                                    │
         ┌──────────────────────────┼──────────────────────────┐
         ▼                          ▼                          ▼
┌──────────────────┐      ┌──────────────────┐      ┌──────────────────┐
│   IN-CONTEXT     │      │   RETRIEVAL-     │      │   FINE-TUNING    │
│    LEARNING      │      │    AUGMENTED     │      │     (PEFT/LoRA)  │
│  (Few-Shot /     │      │   GENERATION     │      │                  │
│   System Prompt) │      │     (RAG)        │      │                  │
└────────┬─────────┘      └────────┬─────────┘      └────────┬─────────┘
         │                         │                         │
         ▼                         ▼                         ▼
• Zero infrastructure     • Dynamic live data        • Internalizes style
• Fast experimentation    • Verifiable citations     • Fast inference
• Limited context window  • Zero training required  • High training cost
• Vulnerable to distraction• Requires vector search  • Does not fix facts

Strategy 1: In-Context Learning (ICL)

In-context learning relies entirely on providing the model with instructions, system constraints, and few-shot input/output pairs directly inside the prompt context window.

SYSTEM_PROMPT = """
You are a senior PostgreSQL DBA. Convert natural language queries into valid ANSI SQL.
Return ONLY valid SQL enclosed in ```sql code blocks.

Example 1:
Input: "Find active users who logged in during the last 7 days."
Output:
```sql
SELECT id, email, last_login_at 
FROM users 
WHERE is_active = true 
  AND last_login_at >= NOW() - INTERVAL '7 days';

"""


- **When to use**: Quick prototyping, lightweight formatting, classification tasks with clear rule sets.
- **Limitations**: Context windows are finite and costly. As prompt size grows, the model's "needle-in-a-haystack" recall degrades, and latency increases linearly with input token count.

### Strategy 2: Retrieval-Augmented Generation (RAG)
Rather than stuffing an entire documentation corpus into the prompt, RAG decouples **knowledge storage** from **language reasoning**.
1. Documents are chunked (e.g., 500-token chunks with 50-token overlap) and converted into dense vector embeddings.
2. When a user asks a question, the query is embedded using the same encoder.
3. A vector store performs approximate nearest neighbor search (e.g., Cosine similarity or HNSW) combined with keyword search (BM25) to retrieve the top $K$ relevant chunks.
4. The retrieved chunks are dynamically injected into the model's prompt as grounded reference context.

- **When to use**: Dynamic, frequently updating datasets (legal documents, technical manuals, internal wikis), and any scenario requiring strict source citations to eliminate hallucinations.
- **Limitations**: Incurs retrieval latency, requires maintaining an indexing pipeline, and retrieval quality is only as good as your chunking and reranking strategy.

### Strategy 3: Fine-Tuning (Parameter-Efficient Fine-Tuning / LoRA)
Fine-tuning adjusts the actual weights of the neural network using supervised learning on thousands of curated instruction-response pairs. With **LoRA (Low-Rank Adaptation)**, developers freeze the base model weights and train lightweight adapter matrices, reducing training VRAM by up to 80%.

- **When to use**: Adapting the model's communication style, teaching it a specialized syntax or proprietary programming language, or distilling a large model's capabilities into a tiny 3B model for edge deployment.
- **The Critical Trap**: **Do not use fine-tuning to teach an LLM new dynamic facts.** Models struggle to reliably recall factual details from fine-tuning alone and continue to hallucinate. Use RAG for factual knowledge, and fine-tuning for form, style, and domain grammar.

---

## 4. Grounding and Actions: Tools and Function Calling

Hallucination occurs when an LLM is forced to compute facts or perform deterministic actions outside its internal weights. **Tool calling** (or function calling) solves this by transforming the LLM into a deterministic decision engine.

Instead of outputting freeform natural language, the model outputs a structured JSON payload identifying:
1. The specific tool function to invoke.
2. The precise arguments matching the tool's JSON schema.

+----------+ 1. Prompt + Tool Schema +-------+ | | ---------------------------------> | | | | <--------------------------------- | LLM | | | 2. Tool Call: +-------+ | Client | {“name”: “fetch_order”,
| Runtime | “args”: {“id”: 1042}}
| |
| | 3. Execute DB Query
| | ---------------------------------+
| | |
| | <--------------------------------+
| | 4. Return Raw DB Result
| |
| | 5. Grounded Prompt + Result +-------+ | | ---------------------------------> | | | | <--------------------------------- | LLM | | | 6. Final Human-Readable Answer+-------+ +----------+


### Why Structured Output Matters
Without schema enforcement, parsing model outputs in production is brittle. Modern APIs support strict JSON Schema validation (e.g., OpenAI Structured Outputs or Pydantic validation), ensuring the LLM's response matches your type contracts with zero parsing exceptions.

---

## 5. Orchestration in Practice: Building with LangChain

Let's look at a concrete, production-ready Python example using **LangChain** and **Pydantic**. We will define a real deterministic tool, equip a model with it, and execute a tool-calling pipeline:

```python
import os
from typing import Dict, Any
from pydantic import BaseModel, Field
from langchain_core.tools import tool
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

# 1. Define our tool's input schema using Pydantic
class ServerStatusInput(BaseModel):
    hostname: str = Field(description="The fully qualified domain name or IP of the server.")
    include_metrics: bool = Field(default=False, description="Whether to fetch CPU and memory metrics.")

# 2. Implement the deterministic tool function
@tool("check_server_status", args_schema=ServerStatusInput)
def check_server_status(hostname: str, include_metrics: bool = False) -> Dict[str, Any]:
    """Inspects the operational health of an enterprise infrastructure host."""
    # In production, this would make an authenticated call to Prometheus or Datadog
    mock_db = {
        "prod-api-01.internal": {"status": "ONLINE", "uptime_hours": 742, "cpu_load": "42%", "mem_usage": "61%"},
        "prod-db-01.internal": {"status": "DEGRADED", "uptime_hours": 12, "cpu_load": "94%", "mem_usage": "88%"},
    }
    
    server_data = mock_db.get(hostname, {"status": "UNKNOWN", "error": "Host not registered in inventory"})
    if not include_metrics and "cpu_load" in server_data:
        return {"hostname": hostname, "status": server_data["status"]}
    return {"hostname": hostname, **server_data}

# 3. Bind the tool to the LLM
llm = ChatOpenAI(model="gpt-4o", temperature=0.0)
llm_with_tools = llm.bind_tools([check_server_status])

# 4. Construct a structured chain
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are an automated Site Reliability Engineer assistant. Use available tools to diagnose infrastructure."),
    ("human", "{query}")
])

chain = prompt | llm_with_tools

# 5. Run the chain
response = chain.invoke({"query": "What is the current status and CPU load of prod-db-01.internal?"})

# The model decides to invoke the tool deterministically:
for tool_call in response.tool_calls:
    print(f"Tool invoked: {tool_call['name']}")
    print(f"Arguments: {tool_call['args']}")
    # Tool invoked: check_server_status
    # Arguments: {'hostname': 'prod-db-01.internal', 'include_metrics': True}

Notice what happened: the model did not guess or hallucinate the server metrics. It analyzed the query, matched the semantic intent to the check_server_status tool docstring, inferred include_metrics: True based on the request for CPU load, and emitted a validated function call.


6. The Hard Truth: When You Should (and Shouldn’t) Build an Agent

The AI ecosystem currently suffers from an obsession with fully autonomous multi-agent systems. Marketing demos often showcase swarms of agents delegating tasks to one another in recursive feedback loops.

In enterprise software engineering, unconstrained autonomous agents are usually an anti-pattern.

Why Autonomous Agents Break in Production

  1. Compounding Probability of Failure: If an agent executes 5 consecutive LLM calls to solve a problem, and each individual call has a 95% success rate:

    $$P(\text{success}) = 0.95^5 \approx 0.7738 \quad (77.4%)$$

    Nearly one in four user requests fails or enters an infinite looping pattern.

  2. Non-Deterministic Latency & Runaway Costs: An agent that decides to “think deeper” might execute 12 tool iterations, turning a 500ms interaction into a 45-second wait with a massive API bill.

  3. Debugging Nightmare: When an autonomous loop goes off the rails, tracing the root cause across nested non-deterministic prompts is exceptionally difficult.

The Agent Decision Matrix

                          CAN THE PATHWAY BE PREDETERMINED?
                                         │
                        ┌────────────────┴────────────────┐
                       YES                                NO
                        │                                 │
                        ▼                                 ▼
             ┌─────────────────────┐           ┌─────────────────────┐
             │ DETERMINISTIC CODE  │           │   DO YOU REQUIRE    │
             │   OR DIRECT DAG     │           │ OPEN-ENDED ACTIONS? │
             │  (FastAPI / Router) │           └──────────┬──────────┘
             └─────────────────────┘                      │
                                                 ┌────────┴────────┐
                                                YES               NO
                                                 │                 │
                                                 ▼                 ▼
                                      ┌──────────────────┐ ┌───────────────┐
                                      │ CONSTRAINED FSM  │ │ SIMPLE RAG /  │
                                      │   (LangGraph)    │ │ SINGLE TOOL   │
                                      └──────────────────┘ └───────────────┘

Use this checklist before reaching for an autonomous agent framework:

  • Do NOT build an agent if:

    • The sequence of steps is known in advance (e.g., Parse PDF $\rightarrow$ Extract Entities $\rightarrow$ Store in DB). Use a deterministic Python/TypeScript pipeline.
    • The task has zero tolerance for unexpected actions (e.g., executing financial transfers or updating live database records).
    • You simply need to retrieve relevant internal documents. A standard RAG pipeline is faster, cheaper, and more reliable.
  • DO build an agent if:

    • The exact number and sequence of steps cannot be predicted upfront (e.g., open-ended code debugging, exploratory data analysis across disparate systems).
    • The environment provides immediate, objective feedback (e.g., a test suite that passes or fails, or a compiler error output that the model can inspect and iterate upon).
    • You constrain the agent with a Finite State Machine (FSM) like LangGraph, enforcing maximum step limits, human-in-the-loop approvals, and hard fallback exits.

7. Architectural Takeaways for Builders

  1. Start with In-Context Learning: Nail your prompt contracts, system constraints, and few-shot examples first.
  2. Graduate to RAG for Dynamic Truth: Keep your models factual by indexing private knowledge into vector stores with hybrid semantic search.
  3. Reserve Fine-Tuning for Form, Not Facts: Fine-tune when you need strict style adherence, specialized token grammars, or cost compression on small models.
  4. Enforce Deterministic Boundaries: Never let an LLM directly touch your database or critical systems without typed validation schemas (Pydantic/Zod) and explicit tool contracts.
  5. Prefer Workflows Over Agents: Build deterministic directed acyclic graphs (DAGs) whenever possible. Leave open-ended autonomy for scenarios that genuinely demand it.