A hands-on introduction

From LLMs to Your Own Coding Agent

LLM basics → coding agent → local demo

LLM basicsAgent loopNode.jsClaude / Gemini / GPTOllama + Qwen3.5

← / → navigate · S notes · F fullscreen · ESC overview

About me

Kha Do

Đỗ Nguyên Kha · Teaching Assistant @ FIT-HCMUS

Former Blockchain EngineerFTX JPLiquidQUOINECustodiemDK Bank

[email protected] · kha.do

© FIT-HCMUS · Semester 3, 2025–2026

Disclaimer

  • For learning & research only.
  • Not investment or legal advice.
  • Code is simplified — use at your own risk.

Kha Do · FIT-HCMUS · [email protected] — inspired by Stanford CS146S "The Modern Software Developer".

What we'll cover

🧠

Part 1

Foundations

🛠️

Part 2

Build it in Node.js

💻

Part 3

Offline & free

Part 1

Foundations

What is an LLM?

A Large Language Model is trained on huge text to do one thing well:

Given some text, predict the next token.
  • Token ≈ word / word-piece
  • Scores possible next tokens, then picks one
  • Repeat → sentences, code, answers
The cat sat on the ? LLM scores every possible next token P(next) mat 62% floor 15% rug 9% sofa 6% 8% picks → mat …then repeats ↺

Tokens — how a model reads text

Models read tokens: chunks of text, not whole words.

I  love  building  token izers ! → 6 tokens

One word can split into several tokens; leading spaces count too.

✂️

≈ ¾ word

~100 tokens ≈ 75 English words. Code often costs more.

💰

Pay per token

Cloud APIs bill input + output. More context = more cost.

🪟

Context budget

The context window is the agent's working memory.

Why the same prompt gives different answers

The model returns a distribution, not one fixed answer. Sampling picks; temperature controls variety.

❄️

temperature = 0

Repeatable · deterministic. Best for code, tools, structured output.

🔥

temperature ≈ 0.7 – 1.0

Varied · creative, different each run. Good for ideas and writing.

Same prompt, different reply? That's sampling. For coding agents, keep temperature low.

How an LLM works — step by step

One token at a time:

text 1 ✂️ Tokenize text → tokens 2 🔢 Embed tokens → vectors 3 👁️ Attention weigh the context 4 📊 Predict next-token probs 5 🎲 Sample pick one token next token 6 · append the new token, then repeat from step 1 ↺
Example — "The cat sat on the ___"   ✂️ [The][ cat][ sat][ on][ the]  →  🔢 vectors  →  👁️ "sat on the" hints a surface  →  📊 mat 62% · floor 15% · rug 9%  →  🎲 picks "mat"   ↺ append it, predict the next word…

How does it "learn"? Three stages

A text-predictor becomes an assistant in stages:

Raw text & code 1 📚 Pre-training read the internet, books, code → learn language & facts 2 🎓 Fine-tuning instruction → answer pairs → learn to follow requests 3 👍 RLHF humans rank answers → helpful, honest, harmless Assistant chat-ready & aligned
Example, same fact through each stage:
📚 Pre-train: reads "Paris is the capital of France" across the web → soaks up the fact  ·  🎓 Fine-tune: shown Q: Capital of France? → A: Paris. → learns to answer, not just autocomplete  ·  👍 RLHF: humans prefer the clear, polite, correct reply → learns tone & safety

Key concepts to know

🪟Context window

How much text it can see at once.

e.g. ~200K tokens ≈ a 500-page book in one go.

⚙️System prompt

Hidden instructions: role, rules, style.

e.g. system: "You are a terse Python expert. Reply in code only."

💰Tokens = cost

Input and output both count.

e.g. ~1,000 tokens ≈ 750 English words ≈ a short email.
context window — what the model sees right now …the cat sat on the… ← older text falls off not written yet →

Honest limits

🌀Hallucination

Confidently wrong.

invents Array.shuffle()

📅Knowledge cutoff

May miss new info.

a library from last week

🫥No memory

Resend context each call.

new chat → forgets you

🚫Can't act

Needs tools to act.

writes npm test, can't run it

Language matters less than you'd think

To an LLM, language is mostly tokens + examples. If it has seen enough, it can help.

🗣️

Human language

Vietnamese or English: same model. English is usually strongest.

💻

Programming language

Popular stacks work better; niche or new stacks need more context.

📥

Fill the gaps

New API? Feed it docs: fetch/curl, MCP, or search.

LLMs lower the language barrier, not the need for engineering judgment.

From LLM → Coding Agent

💬

Plain LLM

You ask → it replies with text.

It can write code, but not run it or inspect files.

🤖

Coding Agent

An LLM in a loop with tools and a goal.

Reads files, edits code, runs commands, observes, retries.

Coding agent = LLM + tools + a loop + a goal. That's the whole trick.

Anatomy of a coding agent

Think: a fast junior dev with tools and feedback. Five parts:

🧠

Brain

The LLM. Decides what to do next.

🖐️

Tools

Functions it can call: read, write, run.

🔁

Loop

Runs think→act→observe until done.

📝

Memory

The message history sent each turn.

🎯

Goal

The user's task + the system prompt rules.

No tools → chatbot. No loop → one-shot script. Together → agent.

The agent loop

┌──▶ 1. Think — send goal + history to the LLM
       2. Decide — LLM replies with an answer OR a tool call
       3. Act — your code runs the tool (read file, run cmd…)
       4. Observe — feed the tool's result back to the LLM
└── 5. Repeat until the LLM says "done" (no more tool calls)

Tools = functions you expose. The model asks; your code executes.

The agent loop — visualized

👤 User the task / goal 🧠 LLM Think & decide next step Need a tool? 🖐️ Your code runs it read_file · write_file · run_command …then observe the result ✅ Final answer back to the user yes no — done tool result → appended to history, loop again ↺

The orange arrow is the loop: tool result → history → next model call.

How a "tool call" actually works

Key point: the model never runs code. It asks in structured text.

1. You tell the model which tools exist (name + description + params)
2. Instead of plain text, the model can reply: "call read_file with {path:'app.js'}"
3. Your code sees that request and actually runs the function
4. You paste the result back into the conversation
5. The model reads the result and decides the next step

Tool call = structured request. Your code decides what runs.

The flow, step by step 🎬

Task: "Add subtract() to math.js and prove it works."

USER Add subtract() to math.js and prove it works.
AGENT Let me see the file first. → calls read_file({ path: "math.js" })
TOOL read_file → "export const add = (a, b) => a + b;"
AGENT I'll append subtract. → calls write_file({ path: "math.js", content: "…" })
TOOL write_file → "Wrote 84 chars to math.js"
AGENT Now verify it. → calls run_command({ cmd: "node -e 'import(\"./math.js\").then(m=>console.log(m.subtract(5,3)))'" })
TOOL run_command → "2"
AGENT ✅ Done — added subtract(); calling subtract(5, 3) returned 2 as expected.

Each tool result is appended, so the model sees what happened.

Why the loop is powerful: self-correction

The power is not one perfect answer. It is observe → retry.

AGENT Run the tests. → calls run_command({ cmd: "node test.js" })
TOOL run_command → "❌ ReferenceError: subtract is not defined"
AGENT Ah — I forgot to export it. Fixing the file. → calls write_file(…)
TOOL write_file → "Wrote 96 chars to math.js"
AGENT Re-run the tests. → calls run_command({ cmd: "node test.js" })
TOOL run_command → "✅ All 3 tests passed"
AGENT ✅ Fixed the missing export; all tests pass now.

Feed the error back → the model can debug itself.

Terminology — the conversation

Every message has a role. History is the agent's memory.

⚙️

system

Behavior, rules, and context.

🙋

user

The request, plus tool results.

🤖

assistant

Text answer or tool request.

Loop = append input → ask model → run tool → append result → repeat.

Part 2

Build a coding agent in Node.js

~80 lines. Cloud now, local later.

What we're about to build

mini-agent — your own coding agent
Create hello.js that prints 1 to 10, then run it.
wrote hello.js
ran node hello.js
1 2 3 4 5 6 7 8 9 10
Done — created the file and ran it. Output looks correct.
Ask your agent to do something…Send

In ~80 lines of Node.js: read, edit, run, then reply.

Step 0 — Project setup

mkdir mini-agent && cd mini-agent
npm init -y
npm pkg set type=module          # enable ES modules (import/export)

# install ONE provider SDK (they're interchangeable — pick yours):
npm install openai               # OpenAI GPT  (also talks to Ollama!)
npm install @anthropic-ai/sdk    # Anthropic Claude
npm install @google/genai        # Google Gemini

Requires Node 18+. Put keys in env vars, never code: export OPENAI_API_KEY=sk-...

Step 1 — Say hello to a model

Same pattern in every SDK: messages in, text out.

OpenAI (GPT)

import OpenAI from "openai";
const openai = new OpenAI();

const r = await openai.chat.completions
  .create({
    model: "gpt-5.5",
    messages: [{ role: "user",
      content: "Hi in 3 words" }],
  });
console.log(
  r.choices[0].message.content);

Anthropic (Claude)

import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();

const r = await anthropic.messages
  .create({
    model: "claude-sonnet-5",
    max_tokens: 1024,
    messages: [{ role: "user",
      content: "Hi in 3 words" }],
  });
console.log(r.content[0].text);

Google Gemini

import { GoogleGenAI }
  from "@google/genai";
const ai = new GoogleGenAI({});

const r = await ai.models
  .generateContent({
    model: "gemini-2.5-flash",
    contents: "Hi in 3 words",
  });
console.log(r.text);

Keys come from env vars. We'll use the OpenAI SDK; the same code drives Ollama / Qwen3 later.

OpenAI-compatible endpoints are common. Change baseURL, keep the agent loop. Advanced features can still differ.

Step 2 — Describe the tools

A tool = name + description + parameters. The model reads this.

const tools = [
  {
    name: "read_file",
    description: "Read a file",
    parameters: { path: "string" },
  },
  {
    name: "write_file",
    description: "Write text to a file",
    parameters: { path: "string", content: "string" },
  },
  {
    name: "run_command",
    description: "Run a shell command",
    parameters: { cmd: "string" },
  },
];

Simplified; real SDKs use JSON Schema.

Step 2b — Implement the tools

Plain Node functions: the agent's hands.

import fs from "node:fs/promises";
import { execSync } from "node:child_process";

const handlers = {
  read_file:   ({ path })          => fs.readFile(path, "utf8"),
  write_file:  ({ path, content }) => fs.writeFile(path, content),
  run_command: ({ cmd })           => execSync(cmd).toString(),
};

run_command is real shell access. Sandbox or require approval.

Step 2c — Call the model with tools 🎬

Before the loop: one model call with tools.

const messages = [
  { role: "user", content: "Create hello.js that prints 'hi'." },
];

// Hand the model the tool schemas from Step 2:
const reply = await callModel(messages, tools);

console.log(reply.tool_calls);
// → [{ name: "write_file",
//      args: { path: "hello.js", content: "console.log('hi')" } }]

The model did not create the file; it requested write_file(). Loop this → agent. ↓

Step 3 — The agent loop

The whole flow, top to bottom.

const messages = [
  { role: "system", content: "You are a coding agent. Use tools to finish the task." },
  { role: "user",   content: "Create hello.js that prints 1 to 10, then run it." },
];

while (true) {
  // 1. Ask the model what to do next
  const reply = await callModel(messages, tools);
  messages.push(reply);

  // 2. No tool requested -> the model is done
  if (!reply.tool_calls) {
    console.log(reply.content);
    break;
  }

  // 3. Run each requested tool, add the result back to the history
  for (const call of reply.tool_calls) {
    const result = await handlers[call.name](call.args);
    messages.push({ role: "tool", content: String(result) });
  }
  // 4. Loop again -> the model now sees the result
}

Step 4 — What happens when it runs

user:  Create hello.js that prints 1 to 10, then run it.

model: (tool) write_file  path=hello.js
model: (tool) run_command cmd=node hello.js
tool:  1 2 3 4 5 6 7 8 9 10
model: Done. Created hello.js and ran it.

That is a working coding agent — one loop, a few tools.

Step 5 — Put it all together

agent.js wires together four pieces:

import OpenAI from "openai";

const openai = new OpenAI();                       // 1 · the model client

const tools = [ /* read_file, write_file, run_command — Step 2 */ ];

const handlers = { /* read_file, write_file, run_command — Step 2b */ };  // 2 · the tool functions

const messages = [                                 // 3 · the conversation
  { role: "system", content: "You are a coding agent. Use tools to finish the task." },
  { role: "user",   content: process.argv[2] },    // task from the command line
];

while (true) {                                     // 4 · the loop
  const res   = await openai.chat.completions.create({ model: "gpt-5.5", messages, tools });
  const reply = res.choices[0].message;
  messages.push(reply);

  if (!reply.tool_calls) { console.log(reply.content); break; }   // done

  for (const call of reply.tool_calls) {           // run tools, feed results back
    const args   = JSON.parse(call.function.arguments);
    const result = await handlers[call.function.name](args);
    messages.push({ role: "tool", tool_call_id: call.id, content: String(result) });
  }
}

Run it: node agent.js "Create hello.js that prints 1 to 10, then run it". It works; now make it reliable ↓

Step 6 — Add rules + stop condition

A bare prompt drifts. Add constraints and a stop condition.

const SYSTEM = `You are a coding agent. Use tools to finish the task.

Rules:                     // ← guide the model
- Work only inside ./workspace.
- Read before editing.
- Ask approval before destructive commands.

Done:                      // ← when to stop calling tools
- Verify after changes.
- Final answer only after verification.
- If stuck, explain why and stop.`;

Also cap the loop: while (n++ < 20) { … }. Rules guide; caps protect.

⚠️ It's a demo — real agents need a sandbox

run_command runs model-chosen commands on your machine: fine for learning, risky for real work.

Why that's risky

  • Hallucination → destructive command
  • Prompt injection → leak keys or run hostile code
  • No undo — it already ran

What a sandbox gives you

  • Isolation — container/VM, no real files or keys
  • Limits — network, time, resources, non-root
  • Control — allow-list + human approval

Course demo: use a throwaway folder. Real use: sandbox the tools.

Choosing a model — cloud or local

☁️

Best cloud

Claude / GPT-5.5 — top coding & agents · paid

💸

Cheap cloud

Gemini Flash / DeepSeek — low cost · free tiers

🖥️

Local

Ollama + Qwen3.5 — free · offline · private

Same loop: swap SDK / baseURL, keep the agent logic.

Note: chat-only models without tool calling are poor agent choices. Price table next is reference only.

Price reference — paid models (reference · skim)

ProviderModelInput $/1MOutput $/1MNote
AnthropicClaude Opus 4.8$5.00$25.00most capable
AnthropicClaude Sonnet 5$3.00$15.00balanced
AnthropicClaude Haiku 4.5$1.00$5.00fast & cheap
OpenAIGPT-5.5$5.00$30.00flagship
OpenAIGPT-5.4$2.50$15.00balanced
GoogleGemini 2.5 Flash$0.30$2.50cheap · free tier
GoogleGemini 2.5 Pro$1.25$10.00stronger
xAIGrok 4~$2.00~$6.00
DeepSeekDeepSeek V3$0.27$1.10very cheap
MistralMistral Large$2.00$6.00EU
Ollama + Qwen3.5qwen3.5:4b (local)$0$0free & offline

Approx. list prices / 1M tokens — verify on provider docs before class.

Part 3

Offline & free with Ollama + Qwen3.5

Run a capable model locally: no API key, no bill, no data leaves your laptop.

Why run a model locally?

Why developers keep a local model around:

💸

Free

No per-token bill, ever.

🔒

Private

Your code never leaves the machine.

✈️

Offline

Works on a plane or air-gapped.

♾️

Unlimited

Experiment as much as you want.

Ollama runs open models locally. Qwen3 is strong at tool calling, which agents need.

Local trade-offs — be realistic

Free and private, with trade-offs:

⚙️

Hardware

Bigger models need more RAM/GPU.

📉

Capability

Small models trail frontier cloud models.

🔧

Maintenance

You manage models and updates.

For learning and everyday coding tasks, a small local Qwen3 is enough. Use cloud APIs or coder models for harder jobs.

Free local models — what to demo

ModelToolsCodeSizeBest for
Qwen3.5 4B★★★★★★~3.4 GBM4 / 16 GB demo
Qwen3 4B★★★★★☆~2.5 GBbackup — lighter
Qwen3 Coder 30B★★★★★★~19 GB32 GB+ only
Devstral / gpt-oss★★★★★★20B+needs RAM
Gemma 3★☆☆★★☆1–27Bweak tools · not agents

★ = relative strength · "Tools" = tool-calling reliability.

Why this pick

  • Qwen3.5 4B: best default for M4 / 16 GB
  • Qwen3 4B: safer backup if RAM is tight
  • Coder models are better at code, but too heavy for class demo
  • OpenAI-compatible endpoint → same agent loop

Use qwen3.5:4b live. Keep qwen3:4b ready as backup.

What hardware do you actually need?

Rule of thumb at Q4: RAM ≈ model size (B) × ~0.7 GB. GPU helps speed, not correctness.

ModelRAM / VRAM (Q4)Runs comfortably onFeel
qwen3.5:4b~4–6 GBM-series / 16 GB laptopbest demo default
qwen3:4b~3–4 GB8 GB+ laptoplighter backup
qwen3:8b~6–8 GB16 GB laptopbetter, slower
qwen3-coder:30b~20+ GB32 GB+ / GPUreal coding work

GPU = speed

CPU works, just fewer tokens/sec.

Apple Silicon shines

Unified memory helps 8–14B models run smoothly.

Start small

Demo with 4B; size up after class.

Step 1 — Install Ollama

macOS

# download the app from ollama.com
# ...or with Homebrew:
brew install ollama

Linux

curl -fsSL https://ollama.com/install.sh | sh

Windows

# download & run the installer
# from ollama.com/download

Verify it's running

ollama --version
# prints installed Ollama version

# Ollama runs a local server at:
#   http://localhost:11434
# (the desktop app starts it automatically;
#  otherwise run:  ollama serve )

Your Node agent talks to this local server.

Step 2 — Download & run Qwen3.5

# M4 / 16 GB demo pick
ollama pull qwen3.5:4b

# chat with it right in your terminal
ollama run qwen3.5:4b
>>> Write a Python function that reverses a string.

# keep a lighter backup ready:
ollama pull qwen3:4b

# strong coding model, but not for 16 GB demo:
ollama pull qwen3-coder:30b   # needs 32 GB+ / GPU
Model~Size on diskMin RAMBest for
qwen3.5:4b~3.4 GB16 GBM4 / 16 GB live demo
qwen3:4b~2.5 GB8 GBBackup, lighter
qwen3:8b~5 GB16 GBBetter quality, slower
qwen3-coder:30b~19 GB32 GB+Coding quality, not class demo

Step 3 — Ollama speaks "OpenAI"

Ollama exposes an OpenAI-compatible API. Change baseURL; keep the loop.

# quick test with plain curl:
curl http://localhost:11434/v1/chat/completions -d '{
  "model": "qwen3.5:4b",
  "messages": [{ "role": "user", "content": "Say hi in 3 words" }]
}'

Same request shape → same SDK → same agent loop.

Step 4 — Point your agent at Qwen3.5

Change two lines and the Part 2 agent runs locally:

import OpenAI from "openai";

const openai = new OpenAI({
  baseURL: "http://localhost:11434/v1",  // ← point at Ollama
  apiKey: "ollama",                      // ← required by SDK, but ignored
});

// ...the ENTIRE agent loop is unchanged — just switch the model:
const res = await openai.chat.completions.create({
  model: "qwen3.5:4b",                   // ← M4 / 16 GB demo pick
  messages,
  tools,                                 // Qwen3.5 supports tool calling
});

Same 80-line agent. Local, free per token, private.

What agents are great at

A fast junior dev — best on well-scoped, verifiable tasks.

  • 📖Read an unfamiliar codebase
  • 🧪Write tests
  • 🧹Small refactors
  • 🛠️Internal scripts & tools
  • 🐞Debug errors that have logs
  • 📝Update docs / README
  • 🔍First-pass PR review
Not a replacement for a senior engineer — a very fast junior dev.

A workflow that actually works

Short leash: small steps, verify every turn.

1. 🎯State the goal clearly
2. 📖Make it read the files first
3. 🗺️Ask for a short plan
4. ✏️Let it make a small change
5. 🧪Make it run the tests
6. 👀Review the diff
7. Commit when it's green

Handy prompt: Read first, don't edit yet. Make the smallest change. Run tests after.

Guardrails you'll see in industry

The riskier the action, the more of these you add.

🧱

Workspace boundary

Command approval

🕵️

Secret redaction

🧪

Tests before final

👀

Human reviews the diff

🚫

No blind deploy

More risk → more sandbox, approval, and review. Never let an agent touch prod unwatched.

Recap — the mental model

  • 🧠LLM = next-token predictor at scale.
  • 🤖Agent = LLM + tools + a loop + a goal.
  • 🔁Loop: think → tool → act → observe → repeat → answer.
  • 💻Node.js MVP: tools + loop + tool results.
  • 🔀Cloud ↔ local: change baseURL and model.
  • 🖥️Ollama + Qwen3.5 = the same agent, free, private, offline.

The "secret sauce" of great agents

Our loop works. Production agents add reliability layers:

🧭

Context engineering

🧩

MCP tools

🌿

Sub-agents

🛡️

Permissions

🗜️

Compaction

🔧

Sharp tools

Inspired by Stanford CS146S — The Modern Software Developer · themodernsoftware.dev

Roadmap — from mini-agent to "Claude Code"

Level up the same loop, one layer at a time:

1. 🏗️MVP loop — read · write · run · the while-loop  ← you built this
2. 🔧Better tools — edit (str-replace) · grep · glob · git · MCP servers
3. 🛡️Safety — approval prompts · sandbox · command allow-list
4. 🧭Context — system reminders · compaction · sub-agents
5. 🎨UX — streaming · TUI with diffs · Markdown render · sessions & memory
A Claude Code–class agent 🎉

Layers are additive; the core loop stays the same. Study: Claude Code, Aider, Cline.

Where to go next

You have the mental model. Try three things tonight:

🔑

Build it

Run the agent with your own API key.

🔌

Go offline

Point baseURL at Qwen3 on Ollama.

🛠️

Extend it

Add one tool: git, search, or line edit.

📚 Docs

ollama.com/library · docs.claude.com · platform.openai.com/docs · ai.google.dev

🔍 Study real agents

Claude Code · Aider · Cline · OpenCode — study their loops.

Assignment · 1 / 2

📝 Homework — what to build

Core requirements

  • CLI tool — chat in the terminal
  • Powered by Ollama + Qwen3 (local; Qwen3.5 if your laptop can)
  • Switch model via config.json
  • Run & demo locally
{
  "provider": "ollama",
  "baseURL":  "http://localhost:11434/v1",
  "model":    "qwen3:4b",
  "apiKey":   "ollama"
}

Tools to implement

  • File: read_file · write_file · list_files · edit_file · create_dir · delete_file
    ↳ stay inside workspace; block escaping paths
  • run_command — run a shell command
    ask approval before running
  • Session — keep conversation in memory
Assignment · 2 / 2

📝 Homework — submit & grading

Demo & submit

  • 🎥 Record a short YouTube demo
  • 📤 Submit the YouTube link + source code on Moodle

Rules

  • Any programming language
  • AI coding assistants allowed
  • Free tiers / local models allowed

Grading (100%)

Core agent loop (Ollama + Qwen3)35%
File tools working & confined to workspace25%
Session (in-memory) + run_command approval15%
Model switch via config.json10%
Demo video & submission15%

References & resources

🧰Build stack

  • Node.js — nodejs.org
  • reveal.js — revealjs.com
  • Ollama — ollama.com/library

🤖Models & SDKs

  • Claude — docs.claude.com
  • OpenAI — platform.openai.com/docs
  • Gemini — ai.google.dev
  • Qwen3 — github.com/QwenLM/Qwen3

📚Concepts

  • MCP — modelcontextprotocol.io
  • Stanford CS146S — themodernsoftware.dev

🔍Real agents to study

Claude Code · claude.com/claude-code  ·  Aider · aider.chat  ·  Cline · github.com/cline/cline  ·  OpenCode · opencode.ai

Model names & prices change — verify on official docs before class.

That's a wrap

Now go build one 🚀

LLM → Agent → Node.js → Ollama + Qwen3.5.

Questions?

Kha Do · FIT-HCMUS · [email protected]