Chapter 24
fine tuning agents guide
Enhancing AI Agents Through Fine-Tuning Guide
Overview
This tutorial demonstrates how to leverage supervised fine-tuning (SFT) to create more capable and efficient AI agents. While we'll use a banking example for demonstration, the techniques and principles covered here apply to any scenario where you need to enhance an AI agent's capabilities through fine-tuning.
What is Fine-Tuning?
Fine-tuning is the process of taking a pre-trained AI model—one that has already learned general language patterns from vast amounts of data—and further training it on a smaller, specialized dataset.
This additional training helps the model adapt to specific domains, tasks, or communication styles, making it more effective and reliable for targeted applications. Fine-tuning allows you to customize the model’s behavior, vocabulary, and output format to better suit your unique needs.
Why Fine-Tune AI Agents?
Fine-tuning offers several powerful advantages for AI agent development:
-
Domain Expertise:
-
Internalizes domain-specific regulations and compliance requirements
-
Reduces hallucinations in specialized contexts
-
-
Consistent Style & Tone:
-
Learns to maintain your brand voice consistently
-
Provides uniform responses across all interactions
-
-
Structured Outputs:
- Reliably generates specific formats (JSON, SQL, Markdown)
-
Reduced Prompt Engineering:
-
Eliminates need for lengthy system prompts
-
Significantly reduces token usage and costs
-
-
Enhanced Efficiency:
-
Smaller tuned models can replace larger base models
-
Faster response times for specialized tasks
-
Who Should Consider Fine-Tuning?
-
Teams building specialized AI agents for specific domains
-
Applications where response speed and cost matter
-
Systems handling complex domain-specific tasks
Method Overview
-
Data Preparation: Creating high-quality training datasets that capture desired behaviors
-
Supervised Fine-Tuning: Adapting foundation models to specific requirements
-
Integration: Building efficient agent workflows with fine-tuned models
-
Evaluation: Measuring performance improvements across key metrics
This approach provides a robust framework for enhancing AI agents through fine-tuning, enabling them to perform specialized tasks more effectively while maintaining efficiency and reliability.
Conclusion
This tutorial shows how to leverage fine-tuning to create more capable AI agents. Whether you need domain expertise, consistent formatting, or improved efficiency, fine-tuning can help your agents perform better while reducing costs and complexity. The techniques demonstrated here can be applied across any industry or use case where specialized agent behavior is desired.
Written by Zohar Mosseri
📖 For more background on fine-tuning AI models and their applications, check out our detailed blog post: Fine-tuning AI models
DiamantAI is a top 0.1% newsletter for AI with over 25,000 subscribers, focusing on AI techniques, breakthroughs, and tutorials.
Example Use Case: Banking Assistant
To demonstrate these concepts, we'll create a banking customer support assistant. This example will show how to:
- Structure training data for domain-specific knowledge
- Fine-tune a model for specialized tasks
- Integrate the fine-tuned model into a practical application
1. Setup and Imports
Install and import necessary libraries
%pip install --upgrade openai langchain langgraph==0.3.1 tiktoken pandas scikit-learn
import os, json, time
from openai import OpenAI
from dotenv import load_dotenv
# Load environment variables and set OpenAI API key
load_dotenv()
os.environ["OPENAI_API_KEY"] = os.getenv('OPENAI_API_KEY')
# Initialize OpenAI client
client = OpenAI()2. Prepare Training & Validation Files for Fine-tuning
OpenAI expects UTF‑8 encoded JSONL files for fine-tuning, with one JSON object per line. Each example should follow the chat format with system, user, and assistant messages.
Understanding the Training Data Format
The quality of your fine-tuned model depends heavily on your training data. Here's what you need to know:
-
File Format: JSONL (JSON Lines) with UTF-8 encoding
-
Message Structure: Each example must contain a sequence of messages with roles and content
-
Required Fields: Each message needs
roleandcontentfields -
Valid Roles:
system,user, andassistant
Example Format
{
"messages": [
{"role": "system", "content": "You are a helpful domain expert assistant."},
{"role": "user", "content": "What is the policy on this matter?"},
{"role": "assistant", "content": "According to our policies..."}
]
}Creating Training Data: A Banking Example
To demonstrate these principles, let's create a training dataset using a banking knowledge base. This example can be adapted for any domain-specific knowledge:
import json
import random
import pathlib
import pandas as pd
# Create directory for our fine-tuning data
DATA_DIR = pathlib.Path("bank_finetune_data")
DATA_DIR.mkdir(exist_ok=True)
# --- General Approach to Creating Training Data ---
# 1. Use your domain knowledge base (docs, FAQs, policies, etc.)
# 2. Structure content into clear topics
# 3. Create diverse, well-phrased Q&A pairs
# Example using banking domain
kb_docs = [
{
"title": "Account Types",
"content": {
"checking": "We offer three types of checking accounts: Basic (no minimum balance), Premium ($2,500 minimum, no fees), and Student (no fees with valid student ID).",
"savings": "Our savings accounts include Regular (0.5% APY), High-Yield (1.5% APY with $10,000 minimum), and Goal-Based savings with customizable targets.",
"business": "Business accounts feature unlimited transactions, merchant services integration, and dedicated support. Available in Standard and Premium tiers."
}
},
# Additional examples below - uncomment to create a more comprehensive training set
# For tutorial purposes, we'll start with a single example to demonstrate the process
# {
# "title": "Security Protocols",
# "content": {
# "authentication": "We use multi-factor authentication, biometric verification, and device recognition. Login attempts are limited to 3 tries before temporary lockout.",
# "fraud_prevention": "Real-time transaction monitoring, instant fraud alerts, and zero liability protection for unauthorized charges.",
# "data_protection": "End-to-end encryption for all transactions, secure socket layer (SSL) protection, and regular security audits."
# }
# },
# {
# "title": "Digital Banking",
# "content": {
# "mobile_features": "Mobile check deposit, peer-to-peer payments, bill pay, and account aggregation across institutions.",
# "online_services": "Budget tracking, spending analysis, electronic statements, and customizable alerts.",
# "technical_requirements": "Compatible with iOS 13+ and Android 8+. Requires secure internet connection and updated browser versions."
# }
# }
]
def generate_diverse_examples(kb_doc):
"""
Generate multiple training examples with diverse phrasings and scenarios.
Following best practices for high-quality training data.
"""
examples = []
content = kb_doc["content"]
# Generate examples for each subtopic
for subtopic, details in content.items():
# Multiple question variations
questions = [
f"Can you explain {subtopic}?",
f"What should I know about {subtopic}?",
f"Tell me about your {subtopic}",
f"How does {subtopic} work?",
f"I need information regarding {subtopic}",
f"What are the details of {subtopic}?"
]
# Multiple response variations with consistent style
responses = [
f"Here's what you need to know about {subtopic}: {details}",
f"Regarding {subtopic}: {details} Let me know if you need any clarification.",
f"I'll explain {subtopic}. {details} Is there anything specific you'd like to know more about?",
f"{details} This information about {subtopic} is current as of today. Please check our website for any updates."
]
# Generate combinations with system message variations
system_messages = [
"You are a knowledgeable banking assistant focused on providing accurate, compliant information.",
"You are a helpful financial services expert committed to clear, precise communication.",
"You are a banking specialist dedicated to providing detailed, accurate responses."
]
# Create multiple examples for each subtopic
for system_msg in system_messages:
for question in questions:
for response in responses:
examples.append({
"messages": [
{"role": "system", "content": system_msg},
{"role": "user", "content": question},
{"role": "assistant", "content": response}
]
})
return examples
# Generate comprehensive training dataset
examples = []
for doc in kb_docs:
examples.extend(generate_diverse_examples(doc))
# Shuffle examples to ensure good distribution
random.shuffle(examples)
print(f"Generated {len(examples)} diverse training examples")Data Quality Best Practices for Fine-Tuning
The success of your fine-tuned model depends on well-structured training data. Key principles:
-
Data Requirements
- At least 10 examples per behavior
- Balance types and reserve 10–20% for validation
-
Quality Guidelines
- Clear intent and response in each example
- Vary phrasing, keep formatting and style consistent
- Reflect real-world usage
-
Common Pitfalls
- Avoid sensitive info, inconsistent styles, duplicates, and overfitting
Remember: Quality over quantity—a small set of high-quality examples outperforms a large set of poor ones.
Split into training and validation sets
A crucial step in fine-tuning is dividing your dataset into training and validation sets. The training set teaches the model, while the validation set helps measure its performance on unseen data. We'll use a 80/20 split - a common ratio that provides enough validation data while maximizing training examples.
# Randomly shuffle examples to ensure even distribution between sets
random.shuffle(examples)
# Calculate split point for 80% training, 20% validation
split_index = int(0.8 * len(examples))
# Split examples into training and validation sets
train_examples, validation_examples = examples[:split_index], examples[split_index:]
# Save to JSONL files
train_file = DATA_DIR / "train.jsonl"
validation_file = DATA_DIR / "validation.jsonl"
with open(train_file, "w", encoding="utf-8") as f:
for example in train_examples:
f.write(json.dumps(example, ensure_ascii=False) + "\n")
with open(validation_file, "w", encoding="utf-8") as f:
for example in validation_examples:
f.write(json.dumps(example, ensure_ascii=False) + "\n")
print(f"✍️ Created {len(train_examples)} training examples and {len(validation_examples)} validation examples")
# Create a DataFrame for easy visualization of example structure
df = pd.DataFrame([
{
"system": ex["messages"][0]["content"],
"user": ex["messages"][1]["content"],
"assistant": ex["messages"][2]["content"]
}
for ex in train_examples[:3]
])
# Display first 3 examples showing system, user, and assistant messages
dfValidating the JSONL format
Before uploading, let's validate our files to ensure they meet OpenAI's requirements:
def validate_jsonl(file_path):
"""Validate that the file is proper JSONL format for fine-tuning."""
with open(file_path, 'r', encoding='utf-8') as f:
line_count = 0
for line in f:
line_count += 1
try:
data = json.loads(line)
# Check if it has the messages field
if 'messages' not in data:
return False, f"Line {line_count} missing 'messages' field"
# Check message format
for msg in data['messages']:
if 'role' not in msg or 'content' not in msg:
return False, f"Line {line_count} has message missing 'role' or 'content'"
if msg['role'] not in ['system', 'user', 'assistant']:
return False, f"Line {line_count} has invalid role: {msg['role']}"
except json.JSONDecodeError:
return False, f"Line {line_count} is not valid JSON"
return True, f"Validated {line_count} examples"
# Validate our files
train_valid, train_msg = validate_jsonl(train_file)
val_valid, val_msg = validate_jsonl(validation_file)
print(f"Training file: {'✅' if train_valid else '❌'} {train_msg}")
print(f"Validation file: {'✅' if val_valid else '❌'} {val_msg}")3. Upload your JSONL files to OpenAI and Launch Fine-tuning
We'll upload our files programmatically using the OpenAI Python client:
import openai
import os
from pathlib import Path
# Initialize OpenAI client
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
def upload(path):
"""Upload a file to OpenAI for fine-tuning"""
with open(path, "rb") as file:
resp = client.files.create(
file=file,
purpose="fine-tune" # Specify the file will be used for fine-tuning
)
print(f"Uploaded {Path(path).name} → {resp.id}")
return resp.id
# Upload our training and validation files
# These IDs will be used when creating the fine-tuning job
train_file_id = upload(train_file)
validation_file_id = upload(validation_file)You can also upload them through the UI:
-
Navigate to the dashboard > fine-tuning.
-
Click + Create.
-
Under Training data, upload your JSONL files.
4. Launch the fine‑tuning job
Create fine-tuning job with the base model and the JSONL files
job = client.fine_tuning.jobs.create(
# The ID of the JSONL file containing training examples
training_file=train_file_id,
# The ID of the JSONL file containing validation examples (optional but recommended)
validation_file=validation_file_id,
model="gpt-4o-mini-2024-07-18", # base model
suffix="banking-support" # custom suffix for the fine-tuned model
)
# Print job information for monitoring
print(f"Fine-tuning job created with ID: {job.id}")
print(f"Job status: {job.status}")
print(f"Monitor progress at: https://platform.openai.com/finetune")5. Monitor job status
import time
# Continuously check job status every 30 seconds
while True:
# Retrieve current status of the fine-tuning job
status = client.fine_tuning.jobs.retrieve(job.id).status
print("Status:", status)
if status in ("succeeded", "failed", "cancelled"):
break # Exit loop if job reaches a terminal state
# Wait 30 seconds before next status check
time.sleep(30)
# Once complete, get the identifier for the fine-tuned model
fine_tuned_model = openai.fine_tuning.jobs.retrieve(job.id).fine_tuned_model # e.g. 'ft:gpt‑4o-mini:banking-support:2025-05-12-10-30-02'
print("✅ Fine‑tuned model:", fine_tuned_model)Understanding Fine-Tuning Metrics
You can watch progress and loss curves in the fine-tuning tab of the OpenAI dashboard. The fine-tuning process typically takes a few minutes to several hours depending on dataset size and model complexity.
When fine-tuning, you'll see two key graphs:
Loss Curves
The loss curves show how well the model is learning over time:
-
Training Loss (🟢 Green): Model's error on training data (should decrease)
-
Validation Loss (🟣 Purple): Error on unseen data (should follow training loss)
-
If validation loss increases while training loss decreases = overfitting
Accuracy Curves
These show the model's prediction accuracy over time:
-
Training Accuracy (🟢 Green): Should steadily increase
-
Validation Accuracy (🟣 Purple): Should follow training curve
-
If they diverge = potential overfitting
What to Look For
✅ Good Signs
-
Both metrics improving steadily
-
Training and validation curves following similar trends
⚠️ Warning Signs
-
Validation loss increasing while training loss decreases
-
Large gaps between training and validation metrics
-
Erratic or unstable curves

6. Implementing Your Fine-Tuned Model
This section demonstrates how to integrate your fine-tuned model into a production environment using LangGraph. You'll learn how to:
-
Connect your specialized model to existing tools and systems
-
Structure the interaction flow
-
Handle domain-specific queries effectively
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langgraph.prebuilt import create_react_agent
# Define example tool to demonstrate fine-tuned model integration
@tool
def account_lookup(account_id: str) -> str:
"""Look up account information by account ID (requires authentication)."""
return f"Account {account_id} information is available after authentication."
@tool
def transfer_payment(source_account: str, destination_account: str, amount: float) -> str:
"""Transfer payment from one account to another."""
return f"Payment of {amount} from {source_account} to {destination_account} has been initiated."
# Use our fine-tuned model
domain_expert = ChatOpenAI(model=fine_tuned_model, temperature=0.0)
# Create the agent with our tools
agent = create_react_agent(
model=domain_expert, # Use our fine-tuned model
tools=[account_lookup, transfer_payment]
)
# Test the agent with some example queries
test_queries = [
"What account types do you offer?",
"What are the fees for wire transfers?",
"I need information about overdraft protection"
]
# Run the agent on a test query
for query in test_queries:
response = agent.invoke({"messages": [HumanMessage(content=query)]})
print("\nQuery:", query)
print("\nResponse:")
# Extract and print only the final assistant response
for msg in reversed(response["messages"]):
# Check if it's an AIMessage and has non-empty content
if hasattr(msg, "content") and msg.content and msg.__class__.__name__ == "AIMessage":
print(msg.content)
breakTip: if you later need multiple specialised models, simply point each LangGraph node to a different fine tuned model.
7. Benefits of Fine-Tuning: A Comparative Analysis
Let's compare how a fine-tuned model performs against a base LLM in two key areas:
1. Domain Expertise & Security
Query: "I want to transfer $50,000 to account 1234567"
Base LLM:
For your safety, please make sure you are using a secure and verified banking platform to perform this action. If you’d like, I can draft a sample confirmation message or explain the common steps to transfer funds through your bank or app. Would you like me to do that?
Fine-Tuned Model:
For your security, I'll need to:
- Verify your identity through our authentication process
- Confirm the destination account details
- Check if this transfer requires additional fraud prevention measures (required for transfers over $10,000)
Please provide your account ID to begin the secure verification process.
2. Structured Responses & Product Knowledge
Query: "What savings accounts do you offer?"
Base LLM:
Banks typically offer various types of savings accounts with different interest rates and minimum balance requirements. You should check with your specific bank for their offerings.
Fine-Tuned Model:
{
"savings_products": {
"regular_savings": {
"rate": "0.5% APY",
"min_balance": "$0",
"features": ["Mobile banking", "24/7 support"]
},
"high_yield_savings": {
"rate": "1.5% APY",
"min_balance": "$10,000",
"features": ["Premium interest rate", "Priority support"]
}
}
}Takeaway:
These improvements would be difficult to achieve through prompt engineering alone, showing the value of fine-tuning for creating specialized AI agents.
8. Best Practices for Fine-Tuning
| Topic | Recommendation |
|---|---|
| Train/Validation Split | Always reserve at least 10% of your data (ideally ≥40 examples) for validation. This helps you monitor overfitting and ensures your model generalizes well to new queries. |
| Early Stopping | Enabled by default. Training will automatically stop if the validation loss stops improving, preventing overfitting and saving compute costs. |
| Example Truncation | Any sample longer than the model’s context window is auto‑truncated. Keep messages concise and relevant for best results. |
| Evaluation Metrics | The OpenAI dashboard shows token‑level accuracy. For deeper analysis, consider running BLEU or exact-match metrics on your own test set. |
9. Next Steps
-
Automate Data Refresh: Schedule regular (e.g., nightly) re-training with new domain data to keep the model up-to-date with evolving knowledge and requirements.
-
Add Compliance & Audit Nodes: Integrate additional LangGraph nodes (potentially with their own fine-tuned models) to automatically check responses for compliance and flag sensitive topics.
-
Monitor agent performance and costs with tools like LangSmith, and continuously evaluate real user queries to iterate and improve your training data and workflow.
