Few-Shot Examples
Learning Outcomes
- Explain what few-shot learning means and why it works for code generation
- Provide effective input/output examples that guide AI behaviour
- Determine how many examples are needed for different tasks
- Prioritise example quality over quantity
- Choose between zero-shot and few-shot approaches based on task complexity
Lesson Plan
| Segment | Duration | Topic |
|---|---|---|
| Intro | 3 min | The power of "show, don't tell" |
| Explain | 8 min | What few-shot learning means for AI coding |
| Demo | 10 min | Providing input/output examples in practice |
| Explain | 7 min | How many examples is enough |
| Demo | 8 min | Quality vs quantity of examples |
| Practice | 7 min | Zero-shot vs few-shot decision making |
| Wrap-up | 2 min | Key takeaways, preview next lesson |
Before You Begin
Pre-work:
- Complete Lessons 1-3 (Anatomy, Role/Context, Step-by-Step)
- Think of a coding pattern you repeat often (API handlers, component structures, test files)
- Find 2-3 existing files in your project that follow the same pattern
Shopping List:
- Any AI coding tool (Claude Code, Cursor, or Codex CLI)
- A project with established patterns you'd like AI to follow
- Examples of your code that demonstrate your preferred style
Few-shot learning is the technique of showing AI examples of what you want before asking it to produce new output. Instead of describing your desired format, style, or pattern in words, you demonstrate it.
The concept:
- Zero-shot: "Write a test for this function" (no examples)
- One-shot: "Here's an example test for a similar function. Write one for this new function."
- Few-shot: "Here are 2-3 examples of tests in our codebase. Follow the same pattern for this new function."
Why few-shot works better for code:
Code has precise structural requirements — indentation, naming conventions, import patterns, error handling styles. Describing these in prose is tedious and error-prone. Showing them is instant and unambiguous.
Compare these two approaches:
Descriptive (zero-shot):
Write an Express route handler that:
- Uses async/await with try/catch
- Destructures parameters from req.params
- Calls a service method, not direct database access
- Returns JSON with a { data } wrapper on success
- Returns { error: message } on failure with appropriate status code
- Uses 201 for creation, 200 for retrieval
- Logs errors with the request ID from req.id
Demonstrative (one-shot):
Here's an example route handler from our codebase:
// GET /api/products/:id
const getProduct = async (req, res) => {
try {
const { id } = req.params;
const product = await productService.findById(id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
return res.status(200).json({ data: product });
} catch (err) {
logger.error({ requestId: req.id, error: err.message });
return res.status(500).json({ error: 'Internal server error' });
}
};
Now write a similar handler for GET /api/orders/:id using orderService.findById.
The second approach communicates the same information — naming style, error handling, response format, logging — but without needing to spell out each rule. AI extracts the pattern from the example.
The most powerful form of few-shot prompting shows explicit input/output pairs: "Given THIS, produce THAT."
Pattern: Input -> Output mapping
For code transformations:
Convert JavaScript functions to TypeScript with proper types.
Example 1:
Input:
function add(a, b) {
return a + b;
}
Output:
function add(a: number, b: number): number {
return a + b;
}
Example 2:
Input:
function getUser(id) {
return db.query('SELECT * FROM users WHERE id = ?', [id]);
}
Output:
interface User {
id: string;
email: string;
name: string;
createdAt: Date;
}
async function getUser(id: string): Promise<User | null> {
return db.query<User>('SELECT * FROM users WHERE id = ?', [id]);
}
Now convert this function:
Input:
function createOrder(userId, items, discount) {
const total = items.reduce((sum, item) => sum + item.price, 0);
const finalTotal = total - (total * discount);
return db.insert('orders', { userId, items, total: finalTotal });
}
Notice how the examples teach AI:
- Add type annotations to all parameters and return values
- Add interface definitions when the type is complex
- Add async/await when the function involves I/O
- The level of type specificity you expect
Pattern: Style transfer
For making AI match your project's conventions:
Write a React component following our project conventions.
Example component (Button.tsx):
import { type FC, type MouseEvent } from 'react';
import { clsx } from 'clsx';
import styles from './Button.module.css';
interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
}
export const Button: FC<ButtonProps> = ({
label,
variant = 'primary',
disabled = false,
onClick,
}) => {
return (
<button
className={clsx(styles.button, styles[variant])}
disabled={disabled}
onClick={onClick}
>
{label}
</button>
);
};
Now create a TextInput component with: value, onChange, placeholder,
error message, and disabled state.
Pattern: Test generation
Write tests following the pattern used in our codebase.
Example test (product.test.ts):
describe('ProductService', () => {
let service: ProductService;
let mockRepo: jest.Mocked<ProductRepository>;
beforeEach(() => {
mockRepo = createMockRepo();
service = new ProductService(mockRepo);
});
describe('findById', () => {
it('returns the product when found', async () => {
const expected = createTestProduct({ id: '123' });
mockRepo.findById.mockResolvedValue(expected);
const result = await service.findById('123');
expect(result).toEqual(expected);
expect(mockRepo.findById).toHaveBeenCalledWith('123');
});
it('returns null when not found', async () => {
mockRepo.findById.mockResolvedValue(null);
const result = await service.findById('nonexistent');
expect(result).toBeNull();
});
});
});
Now write tests for OrderService with methods: create, findById, updateStatus.
Research and practical experience converge on a simple guideline: 2-3 examples is the sweet spot for most coding tasks.
Why 2-3 works:
- 1 example might be treated as a single case, not a pattern. AI might follow it exactly rather than generalising.
- 2 examples establish a pattern. AI can identify what varies and what stays constant.
- 3 examples confirm the pattern and show edge cases. Diminishing returns begin here.
- 4+ examples rarely help and consume context window space that could be used for the actual task.
When 1 example is enough:
- The pattern is simple and obvious (naming convention, file structure)
- You're just showing format/style, not logic
- The example is comprehensive (covers happy path and error case)
Follow this file naming convention:
Example: getUserById.ts -> getUserById.test.ts
Create test files for: createOrder.ts, validatePayment.ts, sendNotification.ts
When you need 2 examples:
- The pattern has variation you want to demonstrate
- There are different cases that should be handled differently
Example 1 (single resource):
GET /api/products/:id -> getProductById(req, res)
Example 2 (collection with filters):
GET /api/products?category=X&sort=Y -> getProducts(req, res)
Now create handlers for the /api/orders endpoints (both single and collection).
When you need 3 examples:
- The pattern has branching logic or multiple variants
- You need to show edge cases or exception handling
- The difference between examples is subtle
Example 1 (simple validation):
Input: { name: "" }
Output: { valid: false, errors: [{ field: "name", message: "Required" }] }
Example 2 (type validation):
Input: { age: "not-a-number" }
Output: { valid: false, errors: [{ field: "age", message: "Must be a number" }] }
Example 3 (cross-field validation):
Input: { startDate: "2024-03-01", endDate: "2024-02-01" }
Output: { valid: false, errors: [{ field: "endDate", message: "Must be after startDate" }] }
Now write validators for the order schema: { quantity, unitPrice, discount, shippingDate }
The diversity principle:
Your examples should be DIFFERENT from each other. Showing three nearly identical examples teaches nothing new. Choose examples that demonstrate different aspects of the pattern:
- Example 1: The simple/happy path
- Example 2: A variation or option
- Example 3: An edge case or error handling
A single excellent example outperforms five mediocre ones. Here's what makes an example high-quality:
Characteristics of a GOOD example:
- Representative — it genuinely reflects the pattern you want repeated
- Complete — it includes imports, error handling, edge cases
- Clean — it follows best practices (no hacks, workarounds, or TODOs)
- Annotated — brief comments explain non-obvious choices (optional but helpful)
Characteristics of a BAD example:
- Outdated — uses deprecated APIs or old patterns you've since abandoned
- Exceptional — it's a special case, not representative of the general pattern
- Incomplete — missing error handling, imports, or edge cases
- Messy — contains workarounds, commented-out code, or inconsistencies
Demonstration:
Bad example to provide:
// TODO: refactor this
const handleSubmit = (e) => {
e.preventDefault()
// const oldValidation = validateForm(data) // removed
if (data.email && data.password) { // basic check for now
fetch('/api/login', {
method: 'POST',
body: JSON.stringify(data)
}).then(r => r.json()).then(d => {
if(d.token) {
localStorage.setItem('token', d.token) // move to context later
navigate('/dashboard')
}
}).catch(e => console.log(e)) // fix error handling
}
}
AI will replicate the bad patterns: no types, TODO comments, console.log errors, no proper error handling, localStorage directly in the handler.
Good example to provide:
const handleSubmit = async (e: FormEvent<HTMLFormElement>): Promise<void> => {
e.preventDefault();
setIsLoading(true);
setError(null);
const validation = validateLoginForm(formData);
if (!validation.valid) {
setErrors(validation.errors);
setIsLoading(false);
return;
}
try {
const response = await authService.login(formData.email, formData.password);
authContext.setToken(response.token);
navigate('/dashboard');
} catch (err) {
if (err instanceof AuthError) {
setError(err.userMessage);
} else {
setError('An unexpected error occurred. Please try again.');
logger.error('Login failed', { error: err });
}
} finally {
setIsLoading(false);
}
};
AI will replicate the good patterns: proper types, validation before submission, service abstraction, typed error handling, loading state management, finally block.
Curating your example library:
Keep a collection of "golden examples" — files in your project that represent your best patterns:
prompts/examples/
route-handler.ts # Ideal Express route
react-component.tsx # Ideal component structure
unit-test.test.ts # Ideal test file
service-class.ts # Ideal service pattern
migration.sql # Ideal migration format
When you need AI to follow your conventions, paste the relevant golden example.
Not every prompt needs examples. Here's a decision framework:
Use ZERO-SHOT (no examples) when:
- The task is standard and well-understood ("Write a binary search function")
- You're asking for something AI has seen millions of times in training
- You want AI to use its own best practices rather than follow yours
- The output format is obvious (a function, a class, a test)
- You're exploring and don't have a fixed pattern in mind
Zero-shot works fine:
"Write a debounce utility function in TypeScript with configurable delay."
AI knows what debounce is, what it returns, and how it's typically implemented.
Use ONE-SHOT (1 example) when:
- You have a specific format you want replicated
- Your naming or structural convention differs from the "standard"
- The task is clear but you want a particular style
One-shot is better:
"Generate a migration file like this example:
exports.up = (knex) => knex.schema.alterTable('users', (t) => {
t.string('avatar_url', 500).nullable();
t.index('avatar_url');
});
exports.down = (knex) => knex.schema.alterTable('users', (t) => {
t.dropIndex('avatar_url');
t.dropColumn('avatar_url');
});
Now create a migration that adds 'phone_number' (string, 20 chars, nullable, indexed)
and 'phone_verified' (boolean, default false) to the users table."
Use FEW-SHOT (2-3 examples) when:
- The pattern has variation that needs demonstrating
- Your conventions are unusual or project-specific
- The task involves transformation with multiple input types
- You've tried zero-shot and got inconsistent results
Few-shot is necessary:
"Convert API responses to our frontend models.
Example 1:
API: { user_id: "123", first_name: "Jane", created_at: "2024-01-01T..." }
Model: { userId: "123", firstName: "Jane", createdAt: new Date("2024-01-01T...") }
Example 2:
API: { order_id: "456", line_items: [...], total_cents: 2500 }
Model: { orderId: "456", lineItems: [...], totalAmount: 25.00 }
Convert this:
API: { product_id: "789", display_name: "Widget", price_cents: 999, is_active: true }
"
The examples teach: snake_case to camelCase, _cents to dollars, _at strings to Date objects, _id stays as string. Describing all these rules in prose would be longer and less clear.
Decision flowchart:
- Is this a standard, well-known task? -> Zero-shot
- Do you need a specific format? -> One-shot
- Does the pattern have variation? -> Few-shot (2 examples)
- Are there edge cases to demonstrate? -> Few-shot (3 examples)
- Did zero-shot give poor results? -> Add examples and retry
Questions & Answers
Key Takeaways
- Show, don't tell — one example communicates more than paragraphs of description, especially for style and convention.
- 2-3 examples is the sweet spot — enough to establish a pattern without wasting context space.
- Quality beats quantity — one clean, representative example outperforms five sloppy ones.
- Use diverse examples — show different cases to teach the pattern, not a single case.
- Zero-shot first, few-shot as recovery — try without examples, add them when results aren't right.
Next Steps: In Lesson 5 — Constraints & Guardrails, we'll learn how to bound AI's output with positive and negative constraints that prevent common failure modes.