LLM basicsAgent loopNode.jsClaude / Gemini / GPTOllama + Qwen3.5
← / → navigate · S notes · F fullscreen · ESC overview
Former Blockchain EngineerFTX JPLiquidQUOINECustodiemDK Bank
© FIT-HCMUS · Semester 3, 2025–2026
Kha Do · FIT-HCMUS · [email protected] — inspired by Stanford CS146S "The Modern Software Developer".
Part 1
Part 2
Part 3
A Large Language Model is trained on huge text to do one thing well:
Given some text, predict the next token.
Models read tokens: chunks of text, not whole words.
One word can split into several tokens; leading spaces count too.
~100 tokens ≈ 75 English words. Code often costs more.
Cloud APIs bill input + output. More context = more cost.
The context window is the agent's working memory.
The model returns a distribution, not one fixed answer. Sampling picks; temperature controls variety.
Repeatable · deterministic. Best for code, tools, structured output.
Varied · creative, different each run. Good for ideas and writing.
Same prompt, different reply? That's sampling. For coding agents, keep temperature low.
One token at a time:
[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…
A text-predictor becomes an assistant in stages:
Q: Capital of France? → A: Paris. → learns to answer, not just autocomplete
· 👍 RLHF: humans prefer the clear, polite, correct reply → learns tone & safety
How much text it can see at once.
e.g. ~200K tokens ≈ a 500-page book in one go.Hidden instructions: role, rules, style.
e.g.system: "You are a terse Python expert. Reply in code only."
Input and output both count.
e.g. ~1,000 tokens ≈ 750 English words ≈ a short email.Confidently wrong.
inventsArray.shuffle()
May miss new info.
a library from last weekResend context each call.
new chat → forgets youNeeds tools to act.
writesnpm test, can't run it
To an LLM, language is mostly tokens + examples. If it has seen enough, it can help.
Vietnamese or English: same model. English is usually strongest.
Popular stacks work better; niche or new stacks need more context.
New API? Feed it docs: fetch/curl, MCP, or search.
LLMs lower the language barrier, not the need for engineering judgment.
You ask → it replies with text.
It can write code, but not run it or inspect files.
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.
Think: a fast junior dev with tools and feedback. Five parts:
The LLM. Decides what to do next.
Functions it can call: read, write, run.
Runs think→act→observe until done.
The message history sent each turn.
The user's task + the system prompt rules.
No tools → chatbot. No loop → one-shot script. Together → agent.
Tools = functions you expose. The model asks; your code executes.
The orange arrow is the loop: tool result → history → next model call.
Key point: the model never runs code. It asks in structured text.
{path:'app.js'}"Tool call = structured request. Your code decides what runs.
Task: "Add subtract() to math.js and prove it works."
subtract() to math.js and prove it works.read_file({ path: "math.js" })write_file({ path: "math.js", content: "…" })run_command({ cmd: "node -e 'import(\"./math.js\").then(m=>console.log(m.subtract(5,3)))'" })subtract(); calling subtract(5, 3) returned 2 as expected.Each tool result is appended, so the model sees what happened.
The power is not one perfect answer. It is observe → retry.
run_command({ cmd: "node test.js" })export it. Fixing the file. → calls write_file(…)run_command({ cmd: "node test.js" })export; all tests pass now.Feed the error back → the model can debug itself.
Every message has a role. History is the agent's memory.
Behavior, rules, and context.
The request, plus tool results.
Text answer or tool request.
Loop = append input → ask model → run tool → append result → repeat.
~80 lines. Cloud now, local later.
node hello.jsIn ~80 lines of Node.js: read, edit, run, then reply.
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-...
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.
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.
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.
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. ↓
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
}
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.
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 ↓
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.
run_command runs model-chosen commands on your machine: fine for learning, risky for real work.
Course demo: use a throwaway folder. Real use: sandbox the tools.
Claude / GPT-5.5 — top coding & agents · paid
Gemini Flash / DeepSeek — low cost · free tiers
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.
| Provider | Model | Input $/1M | Output $/1M | Note |
|---|---|---|---|---|
| Anthropic | Claude Opus 4.8 | $5.00 | $25.00 | most capable |
| Anthropic | Claude Sonnet 5 | $3.00 | $15.00 | balanced |
| Anthropic | Claude Haiku 4.5 | $1.00 | $5.00 | fast & cheap |
| OpenAI | GPT-5.5 | $5.00 | $30.00 | flagship |
| OpenAI | GPT-5.4 | $2.50 | $15.00 | balanced |
| Gemini 2.5 Flash | $0.30 | $2.50 | cheap · free tier | |
| Gemini 2.5 Pro | $1.25 | $10.00 | stronger | |
| xAI | Grok 4 | ~$2.00 | ~$6.00 | — |
| DeepSeek | DeepSeek V3 | $0.27 | $1.10 | very cheap |
| Mistral | Mistral Large | $2.00 | $6.00 | EU |
| Ollama + Qwen3.5 | qwen3.5:4b (local) | $0 | $0 | free & offline |
Approx. list prices / 1M tokens — verify on provider docs before class.
Run a capable model locally: no API key, no bill, no data leaves your laptop.
Why developers keep a local model around:
No per-token bill, ever.
Your code never leaves the machine.
Works on a plane or air-gapped.
Experiment as much as you want.
Ollama runs open models locally. Qwen3 is strong at tool calling, which agents need.
Free and private, with trade-offs:
Bigger models need more RAM/GPU.
Small models trail frontier cloud models.
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.
| Model | Tools | Code | Size | Best for |
|---|---|---|---|---|
| Qwen3.5 4B | ★★★ | ★★★ | ~3.4 GB | M4 / 16 GB demo |
| Qwen3 4B | ★★★ | ★★☆ | ~2.5 GB | backup — lighter |
| Qwen3 Coder 30B | ★★★ | ★★★ | ~19 GB | 32 GB+ only |
| Devstral / gpt-oss | ★★★ | ★★★ | 20B+ | needs RAM |
| Gemma 3 | ★☆☆ | ★★☆ | 1–27B | weak tools · not agents |
★ = relative strength · "Tools" = tool-calling reliability.
Use qwen3.5:4b live. Keep qwen3:4b ready as backup.
Rule of thumb at Q4: RAM ≈ model size (B) × ~0.7 GB. GPU helps speed, not correctness.
| Model | RAM / VRAM (Q4) | Runs comfortably on | Feel |
|---|---|---|---|
| qwen3.5:4b | ~4–6 GB | M-series / 16 GB laptop | best demo default |
| qwen3:4b | ~3–4 GB | 8 GB+ laptop | lighter backup |
| qwen3:8b | ~6–8 GB | 16 GB laptop | better, slower |
| qwen3-coder:30b | ~20+ GB | 32 GB+ / GPU | real coding work |
CPU works, just fewer tokens/sec.
Unified memory helps 8–14B models run smoothly.
Demo with 4B; size up after class.
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.
# 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 disk | Min RAM | Best for |
|---|---|---|---|
| qwen3.5:4b | ~3.4 GB | 16 GB | M4 / 16 GB live demo |
| qwen3:4b | ~2.5 GB | 8 GB | Backup, lighter |
| qwen3:8b | ~5 GB | 16 GB | Better quality, slower |
| qwen3-coder:30b | ~19 GB | 32 GB+ | Coding quality, not class demo |
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.
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.
A fast junior dev — best on well-scoped, verifiable tasks.
Not a replacement for a senior engineer — a very fast junior dev.
Short leash: small steps, verify every turn.
Handy prompt: Read first, don't edit yet. Make the smallest change. Run tests after.
The riskier the action, the more of these you add.
More risk → more sandbox, approval, and review. Never let an agent touch prod unwatched.
baseURL and model.Our loop works. Production agents add reliability layers:
Inspired by Stanford CS146S — The Modern Software Developer · themodernsoftware.dev
Level up the same loop, one layer at a time:
Layers are additive; the core loop stays the same. Study: Claude Code, Aider, Cline.
You have the mental model. Try three things tonight:
Run the agent with your own API key.
Point baseURL at Qwen3 on Ollama.
Add one tool: git, search, or line edit.
ollama.com/library · docs.claude.com · platform.openai.com/docs · ai.google.dev
Claude Code · Aider · Cline · OpenCode — study their loops.
config.json{
"provider": "ollama",
"baseURL": "http://localhost:11434/v1",
"model": "qwen3:4b",
"apiKey": "ollama"
}
read_file · write_file · list_files · edit_file · create_dir · delete_filerun_command — run a shell command| Core agent loop (Ollama + Qwen3) | 35% |
| File tools working & confined to workspace | 25% |
Session (in-memory) + run_command approval | 15% |
Model switch via config.json | 10% |
| Demo video & submission | 15% |
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.
LLM → Agent → Node.js → Ollama + Qwen3.5.
Questions?