Lesson 6: Advanced Prompting Techniques | AI Engineering
Module 2: Prompt Engineering

Advanced Prompting Techniques

Few-shot learning, chain-of-thought reasoning, self-consistency, and structured output extraction

Lesson 6 of 20 ⏱ 25 minutes 🎯 Intermediate

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:"""
When CoT helps most: Arithmetic, symbolic reasoning, commonsense reasoning, multi-hop questions, code debugging. It helps least on simple factual recall or classification.

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}")
Important: Even with JSON mode, the model can still hallucinate values. JSON mode only guarantees valid JSON structure. Always validate the content with your own business logic.

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))
Extension ideas: Add confidence thresholds that trigger human review, support for multi-page documents via chunking, and a caching layer for repeated similar documents.

📌 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_format for 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