TRACK COMPLETE
You have completed all 24 modules of the Open Source AI Agents track. You built a ReAct agent, a RAG pipeline, a multi-agent system, evaluation harnesses, production APIs, and cost-optimized inference — all with open source models. This module shows you where the ecosystem is heading and what to build next.
M24: What's Next — The Open Source Frontier
The models you used throughout this track are the floor, not the ceiling. In this module you will survey the current model landscape, learn to fine-tune a model on your own data, use reasoning models for hard problems, and choose a 90-day path that deepens your skills in the direction that matters most for your work.
Learning Objectives
- Navigate the 2026 open source model landscape: workhorses, reasoning models, and multimodal models
- Call a multimodal model via the OpenAI-compatible Ollama API (Python and TypeScript)
- Understand LoRA and QLoRA — what they do, when to use them, and how to run Unsloth
- Use DeepSeek-R1 for chain-of-thought reasoning and strip <think> blocks for end users
- Choose when to adopt LangChain, CrewAI, or AutoGen versus staying manual
- Pick a 90-day learning path aligned with your goals
- Scaffold your next agent project with the reference directory structure
The Open Source Model Landscape (2026)
Two years ago, the open source model landscape was a single shelf: Llama 2, Mistral 7B, and a handful of derivatives. If you needed something the 7B could not handle, you either upgraded to a 70B (expensive) or switched to a cloud API (expensive differently).
The pain: the 7B made consistent factual errors on your domain, could not reason through multi-step math, and could not look at an image. You were stuck with a hammer when the job needed a Swiss Army knife.
The mapping today: the open source ecosystem now has specialized models for reasoning (DeepSeek-R1DeepSeek-R1 is an open source reasoning model that outputs a chain-of-thought "thinking" block before its final answer. The reasoning traces are visible in the response content wrapped in <think>...</think> tags, making it useful for debugging complex inference chains.), vision (LLaVALLaVA (Large Language and Vision Assistant) is a multimodal model that accepts both images and text. It runs locally via Ollama with `ollama pull llava` and exposes the same OpenAI-compatible chat completions API with image support.), compact on-device inference (Phi-4 mini), and massive capability at 7B scale (Qwen 2.5). You can now match the right model to the right job within a single Ollama installation.
Current Generation: What You Have Been Using
These are the stable, well-documented workhorses that appeared throughout this course:
| Model | Pull command | Strengths | Context | Best for |
|---|---|---|---|---|
| mistral:7b | ollama pull mistral |
Tool use, instruction following | 32K | Agents, structured output |
| llama3.1:8b | ollama pull llama3.1 |
Coding, reasoning, general | 128K | Code generation, RAG |
| qwen2.5:7b | ollama pull qwen2.5 |
Multilingual, long context | 128K | Document analysis, i18n apps |
| phi3:mini | ollama pull phi3:mini |
Compact, fast, good reasoning | 4K | Edge devices, low-latency |
Newer and Emerging Models
These are the models released in 2024–2025 that represent a significant capability leap:
| Model | Architecture | Key capability | Pull command |
|---|---|---|---|
| llama4-scout | MoEMixture of Experts (MoE) architecture routes each token to a subset of "expert" sub-networks instead of activating the full model. A 17B MoE model may use only 3.4B active parameters per forward pass, giving large-model quality at small-model speed. 17B (3.4B active) | Natively multimodal, fast inference | ollama pull llama4:scout |
| llama4-maverick | MoE 400B (17B active) | Near-frontier reasoning + vision | ollama pull llama4:maverick |
| deepseek-r1:7b | Dense transformer + GRPO training | Chain-of-thought reasoning built in | ollama pull deepseek-r1:7b |
| mistral-small2 | Dense 22B | Huge quality jump from 7B series | ollama pull mistral-small2 |
| phi4:mini | Dense 3.8B | Reasoning-dense, tiny footprint | ollama pull phi4:mini |
| gemma3:9b | Dense 9B (Google) | Strong instruction following | ollama pull gemma3:9b |
The capability gap between open source and frontier API models has shrunk dramatically. DeepSeek-R1-7B matches GPT-4o on many reasoning benchmarks. Llama 4 Scout processes images natively. You now have access to models that, 18 months ago, would have required a $200/month API budget — running entirely on your laptop.
Multimodal Models via Ollama
A multimodalA multimodal model accepts multiple types of input — most commonly text + images. The model encodes each modality through separate encoders, then a fusion layer combines them before the language model produces a response. Vision capability is not a separate API call; the image tokens are injected directly into the context. model accepts both text and images (and sometimes audio or video) as input within a single inference call. Ollama supports several vision-capable models via the standard OpenAI-compatible chat completions API — you send images as base64-encoded data URIs alongside text in the message content array.
Available Vision Models
- LLaVA — the original. Strong general image understanding.
ollama pull llava - Moondream2 — lightweight vision model, runs on 4 GB VRAM.
ollama pull moondream - Bakllava — Mistral 7B backbone + LLaVA visual encoder.
ollama pull bakllava - Llama 4 Scout — natively multimodal, best quality.
ollama pull llama4:scout
import base64
from openai import OpenAI
# WHY: same client you've used throughout this track — no new dependencies
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
# WHAT: read and encode the image as a base64 data URI
def encode_image(path: str) -> str:
with open(path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
def analyze_image(image_path: str, question: str) -> str:
b64 = encode_image(image_path)
# WHAT: content is an array — one image_url item, one text item
# GOTCHA: the image_url.url must be a valid data URI with the correct MIME type
response = client.chat.completions.create(
model="llava", # or "llama4:scout" for better quality
messages=[
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{b64}"
}
},
{
"type": "text",
"text": question
}
]
}
],
max_tokens=512
)
return response.choices[0].message.content
# Example usage
if __name__ == "__main__":
result = analyze_image("screenshot.png", "What UI elements are visible? List them.")
print(result)
import OpenAI from "openai";
import fs from "fs";
import path from "path";
// WHY: same Ollama endpoint — TypeScript SDK works identically
const client = new OpenAI({
baseURL: "http://localhost:11434/v1",
apiKey: "ollama",
});
function encodeImage(filePath: string): string {
const buffer = fs.readFileSync(filePath);
return buffer.toString("base64");
}
async function analyzeImage(
imagePath: string,
question: string
): Promise {
const b64 = encodeImage(imagePath);
const ext = path.extname(imagePath).slice(1).toLowerCase();
const mimeType = ext === "png" ? "image/png" : "image/jpeg";
// WHAT: identical content array structure as Python
// GOTCHA: ensure the file exists before calling — no streaming fallback here
const response = await client.chat.completions.create({
model: "llava",
messages: [
{
role: "user",
content: [
{
type: "image_url",
image_url: { url: `data:${mimeType};base64,${b64}` },
},
{ type: "text", text: question },
],
},
],
max_tokens: 512,
});
return response.choices[0]?.message?.content ?? "";
}
// Example
analyzeImage("screenshot.png", "Describe what you see in this image.")
.then(console.log)
.catch(console.error);
You sent a JPEG to LLaVA using the exact same client.chat.completions.create() call you have used for text throughout this track. The model tokenizes the image using a CLIP-style visual encoder and injects those tokens into the context window alongside your question. No new SDK. No separate endpoint. Just a different content shape.
Fine-Tuning with LoRA and QLoRA
You run a 7B model for customer support at a medical device company. It gives reasonable answers — but it keeps confusing "510(k) clearance" with "CE marking," mixing up your product model numbers, and using generic language when your documentation has very specific phrasing. Every response needs editing before it goes to a customer.
The pain: the base model was trained on the general internet, not on your 800 FDA submission documents, your internal style guide, and three years of validated Q&A pairs. It cannot learn your domain vocabulary from prompts alone — the context window is not the same as learned weights.
The mapping: fine-tuning on 500–1000 domain-specific examples teaches the model your terminology, output format, and reasoning patterns. The model is still 7B parameters — you are not rebuilding it. You are adding thin trainable layers (LoRA) on top of frozen weights, costing 1–2 hours on an A10 GPU and producing a model that makes the right calls 80% of the time instead of 40%.
LoRALow-Rank Adaptation (LoRA) injects small trainable matrices (rank-r decompositions) into the frozen attention layers of a pretrained model. Only these adapter matrices — roughly 0.1–1% of total parameters — are updated during fine-tuning. The base model weights are never changed, so you can stack or swap LoRA adapters on a single base model. (Low-Rank Adaptation) adds small trainable matrices to the attention layers of a frozen model. During fine-tuning, only these matrices — typically 0.1–1% of total parameters — are updated. This means you can fine-tune a 7B model on a single A10 (24 GB VRAM) instead of needing a multi-GPU cluster. QLoRAQLoRA (Quantized LoRA) runs LoRA fine-tuning on a 4-bit quantized base model, cutting VRAM requirements roughly in half again. A 7B model fine-tune that needs 16 GB VRAM with standard LoRA needs only 6–8 GB with QLoRA. The quantization affects only the base weights; the LoRA adapters train in full precision (bf16). goes further: it runs LoRA on a 4-bit quantizedQuantization reduces the numerical precision of model weights (e.g., from 32-bit float to 4-bit integer), shrinking memory footprint by 4–8x. Inference quality degrades slightly but is often indistinguishable from full precision on most tasks. Ollama uses GGUF quantization formats like Q4_K_M, Q5_K_M, Q8_0. base model, so a 7B fine-tune fits in 6–8 GB VRAM — a consumer RTX 3060.
The Unsloth Workflow
UnslothUnsloth is a Python library that makes LoRA and QLoRA fine-tuning 2–5x faster than the standard HuggingFace PEFT implementation, while using 60% less VRAM. It hand-optimizes the backward pass for modern GPU architectures and integrates directly with HuggingFace Trainer and TRL's SFTTrainer. is currently the fastest LoRA/QLoRA implementation available. On an A10, it runs fine-tuning 2–5x faster than standard HuggingFace PEFT with 60% less VRAM. The workflow ends with a GGUFGGUF (GPT-Generated Unified Format) is the quantized model format used by llama.cpp and Ollama. After fine-tuning with Unsloth, you export to GGUF and register the model in Ollama via a Modelfile, which lets you run `ollama run my-fine-tuned-model` exactly like any built-in model. export that registers directly in Ollama:
# pip install unsloth trl datasets
from unsloth import FastLanguageModel
from trl import SFTTrainer
from transformers import TrainingArguments
from datasets import load_dataset
# WHAT: load Mistral 7B in 4-bit (QLoRA) — fits in 6-8 GB VRAM
# WHY: load_in_4bit=True triggers the quantized base model
# GOTCHA: first run downloads ~4 GB; ensure HF_HOME has space
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/mistral-7b-bnb-4bit",
max_seq_length=4096,
load_in_4bit=True,
)
# WHAT: attach LoRA adapters to attention projection matrices
# WHY: q_proj and v_proj are the highest-impact targets for most tasks
# GOTCHA: larger r (rank) = more capacity but more VRAM; r=16 is a good default
model = FastLanguageModel.get_peft_model(
model,
r=16, # rank of the adapter matrices
lora_alpha=16, # scaling factor — usually equal to r
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_dropout=0.0, # 0 is fine for most fine-tuning tasks
bias="none",
)
# WHAT: load your training data (JSONL with "text" field containing full conversations)
dataset = load_dataset("json", data_files="my_domain_data.jsonl", split="train")
# WHAT: train with HuggingFace SFTTrainer (supervised fine-tuning)
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
dataset_text_field="text",
max_seq_length=4096,
args=TrainingArguments(
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
warmup_steps=10,
num_train_epochs=3,
learning_rate=2e-4,
logging_steps=25,
output_dir="./lora-output",
fp16=True,
),
)
trainer.train()
# WHAT: export to GGUF so Ollama can load it
# WHY: GGUF is the format ollama uses natively — no conversion step needed
# GOTCHA: q4_k_m gives the best size/quality tradeoff for most tasks
model.save_pretrained_gguf(
"my-domain-model",
tokenizer,
quantization_method="q4_k_m"
)
print("Exported: my-domain-model-unsloth.Q4_K_M.gguf")
The LoRA/QLoRA training ecosystem (Unsloth, HuggingFace PEFT, TRL) is Python-only. TypeScript is not applicable here. Once the fine-tuned model is exported to GGUF and registered in Ollama, you call it from TypeScript exactly like any other model — the distinction only applies during the training step.
Registering the Fine-Tuned Model in Ollama
After the GGUF export, create a Modelfile in the same directory:
# Modelfile — place in the same directory as the .gguf file
# WHAT: points Ollama at the exported GGUF file
FROM ./my-domain-model-unsloth.Q4_K_M.gguf
# WHAT: system prompt baked into every conversation
# WHY: reduces per-request token cost; sets domain context permanently
SYSTEM """You are a specialized assistant for [Your Domain].
Use precise domain terminology. Always cite relevant standards when applicable.
Never use generic language when domain-specific phrasing exists."""
# WHAT: set inference parameters
PARAMETER temperature 0.3
PARAMETER top_p 0.9
PARAMETER num_ctx 4096
# Register with Ollama — runs once
ollama create my-domain-model -f Modelfile
# Test immediately
ollama run my-domain-model "What is a 510(k) clearance?"
# Use via API exactly like any other model
# client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
# response = client.chat.completions.create(model="my-domain-model", ...)
You trained a 7B model on your domain data using QLoRA (less than 8 GB VRAM), exported it to GGUF format, and registered it in Ollama with a custom system prompt baked in. From this point forward, ollama run my-domain-model behaves exactly like any built-in model — but it knows your domain.
Reasoning Models: DeepSeek-R1
A reasoning modelA reasoning model is trained (via GRPO or similar reinforcement learning from outcome feedback) to produce a step-by-step thinking trace before its final answer. DeepSeek-R1 outputs this trace inside <think>...</think> tags. The thinking tokens are part of the model's context but can be stripped before showing the response to users. produces a visible chain-of-thought before its final answer. DeepSeek-R1 was trained using Group Relative Policy Optimization (GRPO) — a reinforcement learning method that rewards correct final answers, which causes the model to develop structured reasoning strategies. The thinking trace appears in the response content wrapped in <think>...</think> tags.
When to Use DeepSeek-R1
- Use it for: multi-step math, logic puzzles, code debugging, complex planning, anything that requires holding multiple constraints simultaneously
- Avoid it for: simple Q&A, high-volume classification, real-time chat (it is slower — the thinking adds 200–800 tokens before the answer)
import re
from openai import OpenAI
# Pull once before first run: ollama pull deepseek-r1:7b
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
def think_and_answer(problem: str, show_thinking: bool = False) -> dict:
"""
Call DeepSeek-R1.
Returns {"thinking": str, "answer": str}.
Set show_thinking=True to debug reasoning; False for end-user responses.
"""
response = client.chat.completions.create(
model="deepseek-r1:7b",
messages=[
{
"role": "user",
"content": problem
}
],
max_tokens=2048,
temperature=0.6 # R1 works well at 0.5-0.7 for reasoning tasks
)
full_content = response.choices[0].message.content or ""
# WHAT: extract the thinking trace
# GOTCHA: some R1 variants use different tag formats — check with your version
think_match = re.search(r"<think>(.*?)</think>", full_content, re.DOTALL)
thinking = think_match.group(1).strip() if think_match else ""
# WHAT: strip thinking tags to get the clean final answer
answer = re.sub(r"<think>.*?</think>", "", full_content, flags=re.DOTALL).strip()
return {
"thinking": thinking if show_thinking else "[hidden]",
"answer": answer
}
# Example: a logic problem that benefits from chain-of-thought
result = think_and_answer(
"A farmer has 17 sheep. All but 9 die. How many are left? "
"Explain your reasoning step by step.",
show_thinking=True # set to False for production
)
print("THINKING:\n", result["thinking"])
print("\nANSWER:\n", result["answer"])
import OpenAI from "openai";
// Pull once: ollama pull deepseek-r1:7b
const client = new OpenAI({
baseURL: "http://localhost:11434/v1",
apiKey: "ollama",
});
interface ThinkResult {
thinking: string;
answer: string;
}
async function thinkAndAnswer(
problem: string,
showThinking: boolean = false
): Promise<ThinkResult> {
const response = await client.chat.completions.create({
model: "deepseek-r1:7b",
messages: [{ role: "user", content: problem }],
max_tokens: 2048,
temperature: 0.6,
});
const fullContent = response.choices[0]?.message?.content ?? "";
// WHAT: extract the thinking trace between <think> tags
// GOTCHA: use DOTALL equivalent — the thinking block can span many lines
const thinkMatch = fullContent.match(/<think>([\s\S]*?)<\/think>/);
const thinking = thinkMatch ? thinkMatch[1].trim() : "";
// WHAT: remove the thinking block to get the clean answer
const answer = fullContent
.replace(/<think>[\s\S]*?<\/think>/g, "")
.trim();
return {
thinking: showThinking ? thinking : "[hidden]",
answer,
};
}
// Example
thinkAndAnswer(
"If I have 3 red balls and 2 blue balls, and I remove one without looking, " +
"what is the probability it was red?",
true
).then(({ thinking, answer }) => {
console.log("THINKING:\n", thinking);
console.log("\nANSWER:\n", answer);
});
The <think> block can be 200–800 tokens of internal reasoning. Do not display this to end users — it looks like the model is "talking to itself" and can confuse non-technical users. Use show_thinking=False in production. However, keep show_thinking=True in your development and eval environments — the thinking trace is your primary debugging tool for understanding why the model reached a wrong answer.
Framework Landscape: Do You Need One?
A joiner learning woodworking does not start with a CNC machine. The CNC is faster and more precise — once you know what shapes you need. But if you start with the CNC, you skip learning to read the grain, feel the resistance, and understand why a joint holds. When the CNC breaks (and it will), you are helpless.
Frameworks are the CNC machines of agent development. LangChain, CrewAI, and AutoGen are faster for shipping common patterns — but when something goes wrong (and it will — wrong tools called, infinite loops, memory leaks, hallucinated tool schemas), you need to understand what the framework is doing underneath to debug it.
Throughout this track you built every component manually: the ReAct loop, the multi-agent handoff, the RAG pipeline. That knowledge is now your competitive advantage. You can read LangChain source code and understand it. You can adopt CrewAI for a crew-based pattern and know exactly which abstraction maps to which primitive you already understand.
Framework Tradeoffs
| Approach | Control | Complexity | Best pattern | When to choose |
|---|---|---|---|---|
| Manual (this course) | Full | You own it all | Any custom architecture | Always start here |
| LangChain | Medium | Complex internals, magic chains | Linear pipelines, integrations | Need 50+ integrations fast |
| CrewAI | Medium-low | Role/Task DSL to learn | Role-based multi-agent | Crew/Task model fits your problem |
| AutoGen | Medium | Conversation-first abstraction | Research / exploratory agents | Multi-agent chat patterns |
| LlamaIndex | High | Index/retrieval abstractions | RAG and knowledge bases | Complex retrieval patterns |
Build manual first (you have done this). Then adopt frameworks surgically: use LlamaIndex when your RAG pipeline becomes complex enough that the manual version is brittle. Use CrewAI when you have a clear crew/role mental model that maps directly to its DSL. Never start a new agent project with a framework — you will not understand what it is doing and debugging will be an order of magnitude harder.
Your 90-Day Roadmap
Three distinct paths forward, depending on your goal. Click a path to see the week-by-week breakdown.
Path B: Swap client to Anthropic, take main course M12–M22, add prompt caching and Claude hooks.
Path C: Take MCP Track, add M18–M21 eval + monitoring, harden your production agent.
Your Starter Project Structure
Copy this directory structure as your scaffold for any new agent project. Every component maps to a module in this track:
Final Exercise: Extend Your Agent
This is the final exercise in the Open Source Track. Choose one extension and implement it in your agent from Module 12. There is no single expected output — this is open-ended exploration. Click an option to see setup instructions.
Knowledge Check
Five questions covering the content of this module. Select an answer to see immediate feedback.
1. You need to run a fine-tune on a 7B model but only have 8 GB of VRAM. Which technique makes this possible?
2. DeepSeek-R1 outputs <think>...</think> blocks. What should you do with them in a production user-facing app?
3. Which architecture does Llama 4 Scout use that lets it achieve large-model quality at smaller active parameter counts?
4. What file format does Unsloth export to that allows you to register a fine-tuned model with Ollama?
5. According to the framework landscape section, what is the recommended order of operations when starting a new agent project?
Open Source Track — Complete!
You have built everything from scratch: a local dev environment, structured output, tool calling, a ReAct agent, RAG, multi-agent systems, evaluation, tracing, monitoring, a production API, and cost optimization — all running on your own hardware with open source models. That is a complete AI engineering foundation.