Advanced Prompting Techniques
Few-shot learning, chain-of-thought reasoning, self-consistency, and structured output extraction
Beyond Basic Prompting
Basic prompting gets you 70% of the way. Advanced techniques push you to 95%+ reliability. These patterns — developed through research at Google, OpenAI, and others — unlock reasoning capabilities that simple instructions can't achieve.
The Prompting Spectrum
| Technique | When to Use | Token Cost | Reliability |
|---|---|---|---|
| Zero-shot | Simple, well-defined tasks | Low | ⭐⭐ |
| Few-shot | Tasks needing format/style guidance | Medium | ⭐⭐⭐ |
| Chain-of-Thought | Reasoning, math, multi-step logic | High | ⭐⭐⭐⭐ |
| Self-Consistency | High-stakes decisions needing confidence | Very High | ⭐⭐⭐⭐⭐ |
Few-Shot Prompting
Provide examples of input-output pairs to teach the model your expected behavior without any fine-tuning. The model learns the pattern in-context.
Rules for Effective Few-Shot Examples
- Diverse coverage — Include edge cases, not just easy examples
- Consistent format — Every example must follow identical structure
- Relevant difficulty — Examples should match the complexity of real inputs
- 3-5 examples — Usually the sweet spot; more rarely helps and wastes tokens
- Order matters — Put the most representative example last (recency bias)
few_shot_prompt = """Classify each customer message into a category and extract the key entity.
EXAMPLE 1:
Message: "I haven't received my order #4521 that was supposed to arrive yesterday"
Category: SHIPPING
Entity: order #4521
Urgency: HIGH
EXAMPLE 2:
Message: "Can you tell me if the blue widget comes in size XL?"
Category: PRODUCT_INQUIRY
Entity: blue widget (size XL)
Urgency: LOW
EXAMPLE 3:
Message: "I was charged twice on my credit card for the same item"
Category: BILLING
Entity: duplicate charge
Urgency: HIGH
EXAMPLE 4:
Message: "Your app crashes every time I try to upload a photo on Android"
Category: TECHNICAL
Entity: photo upload (Android)
Urgency: MEDIUM
---
Now classify this message:
Message: "{customer_message}"
Category:
Entity:
Urgency:"""
def classify_message(message: str) -> dict:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": few_shot_prompt.format(customer_message=message)}],
temperature=0.0
)
# Parse structured output
lines = response.choices[0].message.content.strip().split('\n')
result = {}
for line in lines:
if ':' in line:
key, value = line.split(':', 1)
result[key.strip().lower()] = value.strip()
return result
Chain-of-Thought (CoT) Reasoning
Chain-of-thought prompting asks the model to show its reasoning steps before providing a final answer. This dramatically improves performance on math, logic, and multi-step problems.
Zero-Shot CoT: The Magic Phrase
Simply adding "Let's think step by step" to any prompt triggers reasoning behavior without examples:
# Zero-shot CoT — no examples needed
zero_shot_cot = """A store has 4 shelves. Each shelf holds 8 boxes.
Each box contains 6 items. The store receives a shipment that
doubles the items on the top 2 shelves only.
How many total items are in the store after the shipment?
Let's think step by step."""
# Model output:
# Step 1: Calculate initial items per shelf: 8 boxes × 6 items = 48 items
# Step 2: Calculate total initial items: 4 shelves × 48 = 192 items
# Step 3: Top 2 shelves doubled: 2 × 48 × 2 = 192 items on top shelves
# Step 4: Bottom 2 shelves unchanged: 2 × 48 = 96 items
# Step 5: Total = 192 + 96 = 288 items
# Answer: 288
Few-Shot CoT: Demonstrate Reasoning
cot_prompt = """Determine if the conclusion follows from the premises.
Show your reasoning step by step, then give VALID or INVALID.
EXAMPLE:
Premises:
- All engineers can code
- Sarah is an engineer
Conclusion: Sarah can code
Reasoning:
1. We know all engineers can code (universal statement)
2. Sarah is an engineer (specific instance)
3. Therefore, Sarah falls under "all engineers"
4. So Sarah can code (valid application of universal to specific)
Answer: VALID
EXAMPLE:
Premises:
- Some dogs are friendly
- Rex is a dog
Conclusion: Rex is friendly
Reasoning:
1. We know SOME dogs are friendly (not all)
2. Rex is a dog
3. But we don't know if Rex is in the "friendly" subset
4. The conclusion assumes all dogs are friendly, which isn't stated
Answer: INVALID
---
Premises:
{premises}
Conclusion: {conclusion}
Reasoning:"""
Self-Consistency (Multiple Samples + Majority Vote)
Generate multiple reasoning paths and take the majority answer. This is the most reliable technique for complex reasoning — it trades cost for accuracy.
from collections import Counter
def self_consistent_answer(prompt: str, n_samples: int = 5, temperature: float = 0.7) -> dict:
"""Generate multiple CoT responses and return majority answer."""
# Add CoT trigger
cot_prompt = prompt + "\n\nLet's think step by step. After reasoning, put your final answer on a new line starting with 'ANSWER:'"
responses = []
for _ in range(n_samples):
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": cot_prompt}],
temperature=temperature, # Higher temp for diverse reasoning paths
max_tokens=1000
)
responses.append(response.choices[0].message.content)
# Extract final answers
answers = []
for resp in responses:
lines = resp.strip().split('\n')
for line in reversed(lines):
if line.strip().startswith('ANSWER:'):
answers.append(line.split('ANSWER:')[1].strip())
break
# Majority vote
vote_counts = Counter(answers)
winner, count = vote_counts.most_common(1)[0]
return {
"answer": winner,
"confidence": count / len(answers),
"vote_distribution": dict(vote_counts),
"n_samples": n_samples,
"all_reasoning": responses # Keep for debugging
}
# Usage
result = self_consistent_answer(
"If a train travels 120km in 1.5 hours, then stops for 30 minutes, "
"then travels 80km in 1 hour, what is its average speed for the entire journey?"
)
print(f"Answer: {result['answer']} (confidence: {result['confidence']:.0%})")
When to Use Self-Consistency
- ✅ Math problems where a single CoT might make arithmetic errors
- ✅ Medical/legal classification where confidence matters
- ✅ Code generation — generate multiple solutions, test them all
- ❌ Simple factual questions (overkill)
- ❌ Creative tasks where there's no "correct" answer
- ❌ Latency-sensitive applications (parallel calls help but still slow)
JSON Mode & Structured Output
Modern APIs offer native JSON mode that guarantees valid JSON output. This eliminates the #1 failure mode in production LLM apps: output parsing errors.
# OpenAI JSON mode
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "You extract structured data from text. Always respond in JSON."
},
{
"role": "user",
"content": f"Extract all people, their roles, and companies from this text:\n\n{text}"
}
],
response_format={"type": "json_object"} # Guarantees valid JSON
)
data = json.loads(response.choices[0].message.content)
# Structured Outputs with schema (even stricter)
from pydantic import BaseModel
class Person(BaseModel):
name: str
role: str
company: str
confidence: float
class ExtractionResult(BaseModel):
people: list[Person]
raw_text_length: int
response = client.beta.chat.completions.parse(
model="gpt-4o",
messages=[
{"role": "system", "content": "Extract people from the text."},
{"role": "user", "content": text}
],
response_format=ExtractionResult # Pydantic model as schema!
)
result = response.choices[0].message.parsed # Already a Pydantic object
for person in result.people:
print(f"{person.name} — {person.role} at {person.company}")
Robust Output Parsing
When you can't use JSON mode (e.g., streaming, or non-OpenAI providers), you need defensive parsing:
import json
import re
from typing import Optional
def extract_json_from_response(text: str) -> Optional[dict]:
"""Extract JSON from LLM response, handling common issues."""
# Strategy 1: Try direct parse
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# Strategy 2: Extract from markdown code block
json_match = re.search(r'```(?:json)?\s*([\s\S]*?)```', text)
if json_match:
try:
return json.loads(json_match.group(1))
except json.JSONDecodeError:
pass
# Strategy 3: Find JSON object/array boundaries
for start_char, end_char in [('{', '}'), ('[', ']')]:
start = text.find(start_char)
if start == -1:
continue
# Find matching closing bracket
depth = 0
for i in range(start, len(text)):
if text[i] == start_char:
depth += 1
elif text[i] == end_char:
depth -= 1
if depth == 0:
try:
return json.loads(text[start:i+1])
except json.JSONDecodeError:
break
# Strategy 4: Fix common issues and retry
cleaned = text.strip()
cleaned = re.sub(r',\s*}', '}', cleaned) # trailing commas
cleaned = re.sub(r',\s*]', ']', cleaned)
cleaned = cleaned.replace("'", '"') # single quotes
try:
return json.loads(cleaned)
except json.JSONDecodeError:
return None
def parse_with_retry(prompt: str, max_retries: int = 2) -> dict:
"""Try to get valid JSON, retry with feedback if parsing fails."""
for attempt in range(max_retries + 1):
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.0
)
text = response.choices[0].message.content
result = extract_json_from_response(text)
if result is not None:
return result
# Add correction prompt for retry
prompt = f"""Your previous response was not valid JSON.
Please try again. Respond with ONLY a JSON object, no other text.
Original request: {prompt}"""
raise ValueError(f"Failed to get valid JSON after {max_retries + 1} attempts")
Complete Example: Receipt Extraction with CoT
Let's combine chain-of-thought reasoning with structured output to extract data from unstructured receipt text:
receipt_extraction_prompt = """You are an expert at extracting structured data from receipt text.
INSTRUCTIONS:
1. First, reason through the receipt text to identify all items, quantities, and prices
2. Handle edge cases: discounts, tax, tips, multiple quantities
3. Then output the structured data
RECEIPT TEXT:
---
{receipt_text}
---
Think through this step by step:
1. Identify the store/restaurant name
2. List each line item with quantity and unit price
3. Identify any discounts or promotions applied
4. Find subtotal, tax, and total
5. Note the payment method if visible
After your reasoning, output ONLY this JSON (no markdown):
{{
"store_name": "string",
"date": "YYYY-MM-DD or null",
"items": [
{{"name": "string", "quantity": int, "unit_price": float, "total_price": float}}
],
"subtotal": float,
"discount": float or null,
"tax": float,
"tip": float or null,
"total": float,
"payment_method": "string or null",
"confidence": float
}}"""
def extract_receipt(receipt_text: str) -> dict:
"""Extract structured data from receipt text using CoT + JSON."""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "You are a precise data extraction system. Always complete your JSON output."
},
{
"role": "user",
"content": receipt_extraction_prompt.format(receipt_text=receipt_text)
}
],
temperature=0.0,
max_tokens=2000
)
text = response.choices[0].message.content
# The reasoning comes first, JSON at the end
result = extract_json_from_response(text)
if result is None:
raise ValueError("Failed to extract JSON from response")
# Validate: total should roughly equal sum of items + tax - discount
items_sum = sum(item["total_price"] for item in result["items"])
expected_total = items_sum + result["tax"] - (result.get("discount") or 0) + (result.get("tip") or 0)
if abs(expected_total - result["total"]) > 0.02:
result["_validation_warning"] = f"Math check: items sum to {expected_total:.2f}, stated total is {result['total']:.2f}"
return result
# Test with messy receipt text
sample_receipt = """
TACO PALACE
123 Main St, Austin TX
03/15/2024 7:42 PM
2x Chicken Burrito $11.99 ea $23.98
1x Queso Grande $6.49
3x Street Tacos $3.50 ea $10.50
1x Horchata $3.99
SUBTOTAL $44.96
Loyalty Discount (10%) -$4.50
Tax (8.25%) $3.34
TOTAL $43.80
Paid: Visa ***4521
Thank you! See you next time!
"""
result = extract_receipt(sample_receipt)
print(json.dumps(result, indent=2))
🛠 Mini-Project: Data Extraction Pipeline
Build a pipeline that processes multiple document types (receipts, invoices, emails) using technique selection based on document complexity.
from enum import Enum
from dataclasses import dataclass
import json
class DocumentType(Enum):
RECEIPT = "receipt"
INVOICE = "invoice"
EMAIL = "email"
CONTRACT = "contract"
class ExtractionStrategy(Enum):
ZERO_SHOT = "zero_shot" # Simple, well-structured docs
FEW_SHOT = "few_shot" # Needs format guidance
COT = "chain_of_thought" # Complex reasoning needed
SELF_CONSISTENT = "self_consistent" # High-stakes, need confidence
@dataclass
class ExtractionConfig:
strategy: ExtractionStrategy
temperature: float
n_samples: int = 1 # >1 for self-consistency
examples: list[dict] = None
# Strategy selection based on document complexity
STRATEGY_MAP = {
DocumentType.RECEIPT: ExtractionConfig(
strategy=ExtractionStrategy.COT,
temperature=0.0
),
DocumentType.INVOICE: ExtractionConfig(
strategy=ExtractionStrategy.FEW_SHOT,
temperature=0.0,
examples=[
{"input": "Invoice #001...", "output": {"invoice_id": "001", "...": "..."}}
]
),
DocumentType.EMAIL: ExtractionConfig(
strategy=ExtractionStrategy.ZERO_SHOT,
temperature=0.1
),
DocumentType.CONTRACT: ExtractionConfig(
strategy=ExtractionStrategy.SELF_CONSISTENT,
temperature=0.5,
n_samples=3
),
}
class ExtractionPipeline:
def __init__(self, client):
self.client = client
def extract(self, text: str, doc_type: DocumentType) -> dict:
"""Extract structured data using the appropriate strategy."""
config = STRATEGY_MAP[doc_type]
if config.strategy == ExtractionStrategy.ZERO_SHOT:
return self._zero_shot_extract(text, doc_type)
elif config.strategy == ExtractionStrategy.FEW_SHOT:
return self._few_shot_extract(text, doc_type, config.examples)
elif config.strategy == ExtractionStrategy.COT:
return self._cot_extract(text, doc_type)
elif config.strategy == ExtractionStrategy.SELF_CONSISTENT:
return self._self_consistent_extract(text, doc_type, config.n_samples)
def _zero_shot_extract(self, text: str, doc_type: DocumentType) -> dict:
schema = self._get_schema(doc_type)
prompt = f"Extract data from this {doc_type.value}. Return JSON matching this schema:\n{schema}\n\nText:\n{text}"
return self._call_llm(prompt, temperature=0.0)
def _few_shot_extract(self, text: str, doc_type: DocumentType, examples: list) -> dict:
examples_text = "\n\n".join(
f"INPUT:\n{ex['input']}\nOUTPUT:\n{json.dumps(ex['output'])}"
for ex in examples
)
prompt = f"Extract data from documents. Examples:\n\n{examples_text}\n\nNow extract from:\n{text}"
return self._call_llm(prompt, temperature=0.0)
def _cot_extract(self, text: str, doc_type: DocumentType) -> dict:
schema = self._get_schema(doc_type)
prompt = (
f"Extract data from this {doc_type.value}.\n\n"
f"Think step by step:\n"
f"1. Identify the key sections\n"
f"2. Extract each field carefully\n"
f"3. Verify amounts/calculations\n"
f"4. Output JSON matching: {schema}\n\n"
f"Text:\n{text}"
)
return self._call_llm(prompt, temperature=0.0)
def _self_consistent_extract(self, text: str, doc_type: DocumentType, n: int) -> dict:
results = []
for _ in range(n):
result = self._cot_extract(text, doc_type)
results.append(result)
# For structured data, compare field by field
# Use majority vote on each field
if not results:
return {}
final = results[0].copy()
final["_confidence"] = self._calculate_agreement(results)
return final
def _calculate_agreement(self, results: list[dict]) -> float:
"""Calculate what fraction of fields agree across all samples."""
if len(results) <= 1:
return 1.0
keys = set().union(*[r.keys() for r in results])
agreements = 0
for key in keys:
values = [r.get(key) for r in results]
most_common = max(set(map(str, values)), key=list(map(str, values)).count)
agreements += sum(1 for v in values if str(v) == most_common) / len(values)
return agreements / len(keys)
def _call_llm(self, prompt: str, temperature: float) -> dict:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
temperature=temperature
)
return json.loads(response.choices[0].message.content)
def _get_schema(self, doc_type: DocumentType) -> str:
schemas = {
DocumentType.RECEIPT: '{"store": str, "items": [{name, qty, price}], "total": float}',
DocumentType.INVOICE: '{"invoice_id": str, "vendor": str, "line_items": [...], "due_date": str, "total": float}',
DocumentType.EMAIL: '{"from": str, "subject": str, "intent": str, "action_items": [...], "urgency": str}',
DocumentType.CONTRACT: '{"parties": [...], "effective_date": str, "terms": [...], "obligations": [...]}',
}
return schemas[doc_type]
# Usage
pipeline = ExtractionPipeline(client)
result = pipeline.extract(sample_receipt, DocumentType.RECEIPT)
print(json.dumps(result, indent=2))
📌 Key Takeaways
- Few-shot: 3-5 diverse examples teach format and style without fine-tuning
- Chain-of-Thought: "Think step by step" unlocks reasoning — use for math, logic, and multi-step problems
- Self-Consistency: Multiple samples + majority vote gives highest reliability at highest cost
- JSON Mode: Use
response_formatfor guaranteed valid structure — but still validate content - Defensive parsing: Always have fallback strategies for extracting JSON from free-text responses
- Strategy selection: Match technique complexity to document complexity — don't over-engineer simple tasks