Module 4: Chain-of-Thought and Reasoning

7. Limitations and Anti-Patterns of CoT

Overview

CoT is not a silver bullet. There are situations where CoT doesn't just fail to help — it actively makes the results worse. This capsule covers: when CoT is counterproductive, the phenomenon of "fabricated reasoning" (persuasive but false reasoning), hallucinations specific to reasoning, and the most common anti-patterns you must avoid in production.

Estimated time: 60-75 minutes


Limitation 1: Tasks Where CoT Doesn't Help (or Makes Things Worse)

Creative Tasks

CoT makes creative thinking rigid. When you ask the model to "reason step by step" on a creative task, you impose a linear structure that kills serendipity.

from openai import OpenAI

client = OpenAI()

def compare_creative_with_without_cot(creative_task: str) -> dict:
    """
    Demonstrates how CoT can damage creative quality.
    """
    # Without CoT
    r_without = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": creative_task}],
        temperature=0.8,  # High temperature for creativity
        max_tokens=200
    )
    
    # With CoT (probably worse for creativity)
    r_with = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": f"{creative_task}\n\nThink step by step."}],
        temperature=0.8,
        max_tokens=400
    )
    
    return {
        "without_cot": r_without.choices[0].message.content,
        "with_cot": r_with.choices[0].message.content
    }


# Examples where CoT does damage
CREATIVE_TASKS = [
    "Write a haiku about winter",
    "Generate 5 creative names for an artificial intelligence startup",
    "Invent an original metaphor to describe anxiety",
    "Write the first paragraph of a noir novel set in the future",
]

if __name__ == "__main__":
    for task in CREATIVE_TASKS[:2]:
        print(f"\n=== {task} ===")
        result = compare_creative_with_without_cot(task)
        print(f"\nWITHOUT CoT:\n{result['without_cot']}")
        print(f"\nWITH CoT:\n{result['with_cot'][:200]}...")
        print("\nObservation: The CoT version tends to be more mechanical and less imaginative")

Why does this happen? CoT activates the model's "systematic analysis" mode. Creativity requires non-linear connections between concepts, and the "step by step" format inhibits those connections.

Opinion or Subjective Preference Tasks

SUBJECTIVE_TASKS = [
    "Is React or Vue better for a web project?",
    "What's the best book about programming?",
    "Should I use PostgreSQL or MongoDB for my app?",
]

# CoT on these questions creates an illusion of objectivity
# The model reasons as if there were a "correct" answer when there isn't one
# This can create a false sense of certainty in the user

Simple Classification with Clear Criteria

# ❌ Using CoT for this is a waste of tokens
unnecessary_cot_prompt = """
Classify whether the following number is even or odd: 42

Think step by step:
"""
# Response: "Step 1: The number is 42. Step 2: Even numbers are divisible by 2. 
# Step 3: 42 / 2 = 21, with no remainder. Step 4: Therefore, 42 is even."
# → 50 extra tokens for something that can be answered in 3 tokens

# ✅ No CoT for simple tasks
direct_prompt = "Is 42 even or odd? Answer in one word."

Direct Factual Questions

FACTUAL_QUESTIONS = [
    "Capital of France?",
    "What is 2 + 2?",
    "Which programming language did Guido van Rossum create?",
]

# CoT adds tokens with no value: "France is a European country... its historical capital... 
# therefore: Paris"
# The direct answer "Paris" is just as correct and 5x cheaper

Limitation 2: Fabricated CoT (Fabricated Reasoning)

The most dangerous phenomenon: the model generates reasoning that sounds right, looks right, but arrives at an incorrect conclusion.

Types of Fabricated Reasoning

Type 1: A Disguised Calculation Error

# The model shows "work" but makes internal mistakes

INCORRECT_REASONING = """
Problem: What is 23 × 47?

Reasoning:
We decompose it: 23 × 47 = 23 × 40 + 23 × 7
23 × 40 = 920  ✓ (correct)
23 × 7 = 151   ✗ (incorrect, it should be 161)
920 + 151 = 1071  ✗ (incorrect final answer, the correct one is 1081)
"""

# The problem: the reasoning "looks" right (it has steps, it has operations)
# but it has an error in step 2 that propagates

def detect_math_errors(reasoning: str) -> list[str]:
    """
    Analyzes a CoT chain of reasoning and detects math errors.
    Extracts every operation and verifies each one.
    """
    import re
    errors = []
    
    # Look for multiplications
    for match in re.finditer(r'(\d+)\s*[×x\*]\s*(\d+)\s*=\s*(\d+)', reasoning):
        a, b, result = int(match.group(1)), int(match.group(2)), int(match.group(3))
        correct = a * b
        if correct != result:
            errors.append(f"Error: {a} × {b} = {result} (correct: {correct})")
    
    # Look for additions
    for match in re.finditer(r'(\d+)\s*\+\s*(\d+)\s*=\s*(\d+)', reasoning):
        a, b, result = int(match.group(1)), int(match.group(2)), int(match.group(3))
        correct = a + b
        if correct != result:
            errors.append(f"Error: {a} + {b} = {result} (correct: {correct})")
    
    return errors


errors = detect_math_errors(INCORRECT_REASONING)
print(f"Errors found: {errors}")
# Output: ['Error: 23 × 7 = 151 (correct: 161)']

Type 2: An Unjustified Logical Leap

FABRICATED_LOGIC = """
Problem: Are all swans white?

Reasoning:
- The swans I've seen are white.
- Zoology books show white swans.
- The cultural association of "swan" implies whiteness.
- White swans are the most common.
- Therefore, all swans are white. ✓

ANSWER: Yes, all swans are white.
"""

# The error: black swans (Cygnus atratus) exist in Australia
# The reasoning uses anecdotal evidence and generalizes incorrectly
# This is an invalid inductive leap (Hume's problem of induction)

Type 3: An Invented Premise

INVENTED_PREMISE = """
Problem: How long does it take water to boil at 2000 meters of altitude?

Reasoning:
- At sea level, water boils at 100°C.
- At higher altitude, atmospheric pressure drops.
- At 2000 meters, water boils at exactly 93.4°C.  ← INVENTED PREMISE
- With a lower boiling temperature, the process is faster.
- Therefore, water boils in roughly 8 minutes.  ← INCORRECT CONCLUSION
"""

# The model invented "exactly 93.4°C" with no evidence
# The real temperature at 2000m is ~93.3°C, but more importantly:
# "it will boil faster" is incorrect; it takes the same or longer because heat 
# transfer in water is similar

Detecting Fabricated Reasoning

def evaluate_reasoning(problem: str, proposed_reasoning: str) -> dict:
    """
    Uses a second LLM to evaluate whether the reasoning is valid.
    
    This function implements the "external auditor" pattern to
    detect fabricated reasoning.
    """
    evaluation_prompt = f"""You are a critical auditor of reasoning. Your job is to find flaws,
not to confirm that everything is fine. Be specific about any problem.

PROBLEM: {problem}

PROPOSED REASONING:
{proposed_reasoning}

Analyze step by step:
1. Is every mathematical operation correct? (verify it manually)
2. Are there unjustified logical leaps?
3. Are facts assumed that were not given in the problem?
4. Does the conclusion necessarily follow from the premises?

VERDICT: VALID / INVALID / PARTIALLY_VALID
CONFIDENCE: High/Medium/Low
ERRORS FOUND: [specific list, or "None"]"""

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": evaluation_prompt}],
        temperature=0,
        max_tokens=500
    )
    output = response.choices[0].message.content
    
    import re
    verdict_match = re.search(r'VERDICT:\s*(VALID|INVALID|PARTIALLY_VALID)', output)
    
    return {
        "evaluation": output,
        "verdict": verdict_match.group(1) if verdict_match else "UNDETERMINED"
    }

Limitation 3: Hallucinations in Reasoning

Hallucinations in CoT are especially dangerous because they come wrapped in reasoning that looks solid.

Types of Hallucination in CoT

HALLUCINATION_TYPES = {
    "invented_data": {
        "description": "The model cites statistics, dates or facts that don't exist or are wrong",
        "example": "According to the 2019 Harvard study, 73% of projects fail because of...",
        "mitigation": "Ask for 'use only information from the context', or verify externally"
    },
    "fake_quotes": {
        "description": "The model attributes quotes or claims to people/papers that never said them",
        "example": "As Einstein said: 'Insanity is doing the same thing and expecting different results'",
        "mitigation": "Never assume a quote is real; check it against primary sources"
    },
    "unprovided_premises": {
        "description": "The model introduces information that was not given in the problem",
        "example": "Shipping takes 3 days... (the problem said nothing about shipping)",
        "mitigation": "Explicit instruction: 'Use only the data provided in the problem'"
    },
    "overgeneralization": {
        "description": "It extrapolates patterns from specific cases to general ones",
        "example": "Since the last 5 years have been positive, next year will be too",
        "mitigation": "Ask it to identify its assumptions explicitly"
    }
}


def anti_hallucination_prompt(problem: str, context: str = "") -> str:
    """
    A prompt template designed to minimize hallucinations in the reasoning.
    """
    anti_hallucination_instructions = """
STRICT RULES:
- Use only data explicitly provided in the problem or the context
- If you need information that is NOT in the problem, say "MISSING DATA: [what you need]"
- Do not cite studies, statistics or facts unless they are in the context
- When you make assumptions, mark them explicitly as "ASSUMPTION: [what you're assuming]"
- If there is ambiguity, identify it before reasoning: "AMBIGUITY: [what can be read two ways]"
"""
    
    prompt = f"""{anti_hallucination_instructions}

{'Available context:' + chr(10) + context + chr(10) if context else ''}

Problem: {problem}

Reasoning (following the rules):"""
    
    return prompt

CoT Anti-Patterns: The 7 Most Common

Anti-Pattern 1: CoT for Everything (Overuse)

# ❌ BAD: CoT on every call regardless of the task
class BadChatbot:
    def answer(self, question: str) -> str:
        return self._call_with_cot(question)  # Always CoT
    
    def _call_with_cot(self, question: str) -> str:
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": f"{question}\n\nThink step by step."}],
            temperature=0, max_tokens=500
        )
        return response.choices[0].message.content

# Result: 3-5x more tokens on questions that don't need it
# "What are your opening hours?" → 200 tokens of reasoning to say "9am-6pm"


# ✅ GOOD: CoT only when it adds value
class SmartChatbot:
    COT_TASKS = ["calculat", "reason", "analyz", "compare", "evaluate", "optimiz", "decid"]
    NON_COT_TASKS = ["what is", "what time", "where is", "what's the name", "capital of"]
    
    def answer(self, question: str) -> str:
        if self._needs_cot(question):
            return self._call_with_cot(question)
        else:
            return self._call_direct(question)
    
    def _needs_cot(self, question: str) -> bool:
        question_lower = question.lower()
        # Positive if there are words suggesting complex reasoning
        has_cot = any(t in question_lower for t in self.COT_TASKS)
        # Negative if it's clearly factual/simple
        is_simple = any(t in question_lower for t in self.NON_COT_TASKS)
        return has_cot and not is_simple
    
    def _call_with_cot(self, question: str) -> str:
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": f"{question}\n\nThink step by step."}],
            temperature=0, max_tokens=500
        )
        return response.choices[0].message.content
    
    def _call_direct(self, question: str) -> str:
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": question}],
            temperature=0, max_tokens=100
        )
        return response.choices[0].message.content

Anti-Pattern 2: Reasoning Without Verification in Production

# ❌ BAD: In production, blindly trusting CoT with no verification
def calculate_discount_BAD(price: float, discount_pct: float) -> float:
    """A discount system that uses CoT with no verification."""
    problem = f"If a product costs ${price} and has a {discount_pct}% discount, how much do you have to pay?"
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": f"{problem}\n\nThink step by step."}],
        temperature=0, max_tokens=300
    )
    
    import re
    text = response.choices[0].message.content
    match = re.search(r'\$?([\d.]+)', text)
    return float(match.group(1)) if match else price  # Returns the wrong price if it fails


# ✅ GOOD: Verify with deterministic code for critical calculations
def calculate_discount_GOOD(price: float, discount_pct: float) -> dict:
    """A system with deterministic verification for critical operations."""
    # Deterministic calculation (always correct)
    correct_final_price = price * (1 - discount_pct / 100)
    
    # CoT for the user-facing explanation (not for the calculation)
    problem = f"Explain to the customer how we computed their discount: original price ${price}, discount {discount_pct}%, final price ${correct_final_price:.2f}"
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": problem}],
        temperature=0, max_tokens=150
    )
    
    return {
        "final_price": correct_final_price,  # NEVER use the LLM's number for this
        "user_explanation": response.choices[0].message.content
    }

Anti-Pattern 3: Trusting Persuasive Reasoning

# Persuasive reasoning (the kind that sounds good) ≠ correct reasoning

PERSUASIVE_BUT_FALSE_REASONING = """
Problem: If I flipped a coin 9 times and it came up heads every time,
what's the probability that the tenth flip comes up heads?

Reasoning:
- The first 9 flips came up heads, which is extremely improbable (1/512)
- This suggests the coin may be biased toward heads
- With such a pronounced bias, the tenth is more likely to be heads too
- The probability of heads is greater than 50%

Answer: ~75% probability of heads

Why it's false:
- A fair coin has memory: 0. Every flip is independent.
- The probability is always 50% if the coin is fair.
- The "the coin may be biased" reasoning would only be valid if you 
  had evidence that the coin isn't fair.
- This is called the "Gambler's Fallacy"
"""


def detect_logical_fallacies(argument: str) -> dict:
    """
    Identifies common logical fallacies in a piece of reasoning.
    """
    prompt = f"""Identify whether there are logical fallacies in this reasoning.

Reasoning: {argument}

Fallacies to check for:
- Gambler's fallacy (treating independent events as dependent)
- Hasty generalization (small n → general conclusion)
- Post hoc ergo propter hoc (correlation = causation)
- Slippery slope (A → B → C with no justification)
- Appeal to authority with no evidence
- Affirming the consequent

List the fallacies you find with an explanation, or "No fallacies detected"."""
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
        max_tokens=400
    )
    
    output = response.choices[0].message.content
    has_fallacies = "no fallacies" not in output.lower()
    
    return {
        "analysis": output,
        "has_fallacies": has_fallacies
    }

Anti-Pattern 4: CoT That's Too Long

# ❌ BAD: With no step limit, the model can produce 20 paragraphs for a simple calculation
unlimited_prompt = """
What is 15% of 280?

Think step by step.
"""
# The model may explain the concept of percentages, the history of percentages,
# multiple calculation methods, verifications, etc. → 400+ tokens

# ✅ GOOD: Cap the steps explicitly
def calculate_with_controlled_cot(problem: str, max_steps: int = 5) -> str:
    """CoT with an explicit step limit for concise answers."""
    prompt = f"""{problem}

Solve it in AT MOST {max_steps} concise steps. 
Format:
Step 1: [action]
...
Answer: [result]"""
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
        max_tokens=200  # A token limit as well
    )
    return response.choices[0].message.content

Anti-Pattern 5: Ignoring Uncertainty

# ❌ BAD: The model asserts with certainty when there's real uncertainty
uncertain_problem = "Will the stock market go up tomorrow?"
# Without CoT: "Yes, it'll go up" or "No, it'll go down"
# With CoT: It generates 10 paragraphs of analysis that end in an equally uncertain conclusion

# ✅ GOOD: Use CoT that includes an estimate of the uncertainty
def reason_with_uncertainty(question: str) -> dict:
    """Reasoning that explicitly models the uncertainty."""
    prompt = f"""Answer the question using step-by-step reasoning.
IMPORTANT: Be honest about the uncertainty. If you cannot know something for sure, say so.

Answer structure:
1. Analysis: [reasoning with the available data]
2. Assumptions: [what you have to assume in order to answer]
3. Uncertainty factors: [what you don't know that affects the answer]
4. Answer: [with a confidence level: HIGH/MEDIUM/LOW/VERY_LOW]

Question: {question}"""
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
        max_tokens=500
    )
    return {"answer_with_uncertainty": response.choices[0].message.content}

Anti-Pattern 6: Using CoT for Memorization

# ❌ BAD: CoT for questions the model simply "remembers"
memorization_problem = "How many planets does the solar system have?"
# With CoT: "The solar system was formed X years ago... it has rocky and gas planets...
# The IAU redefined 'planet' in 2006... currently there are 8 planets. Answer: 8"
# Without CoT: "8"

# The difference: CoT adds tokens, introduces the possibility of an error in the reasoning,
# and the result is identical

# RULE: If the answer comes from memorization (it requires no steps), do NOT use CoT


def is_factual_question(question: str) -> bool:
    """
    Heuristic to detect factual questions that don't benefit from CoT.
    """
    factual_indicators = [
        question.strip().startswith("What is"),
        question.strip().startswith("Who"),
        question.strip().startswith("When"),
        question.strip().startswith("Where"),
        "name of" in question.lower(),
        "capital of" in question.lower(),
        "founder of" in question.lower(),
    ]
    
    reasoning_indicators = [
        "why" in question.lower(),
        "how" in question.lower() and "calculat" in question.lower(),
        any(op in question for op in ["+", "-", "×", "÷", "%"]),
        "if...then" in question.lower(),
        "how much" in question.lower() and "discount" in question.lower(),
    ]
    
    return sum(factual_indicators) > sum(reasoning_indicators)

Anti-Pattern 7: Not Validating the Output Format

# ❌ BAD: Assuming that CoT always ends with the answer in the expected format
def parse_answer_BAD(cot_output: str) -> float:
    """Fragile: assumes the last number is the answer."""
    import re
    numbers = re.findall(r'\d+\.?\d*', cot_output)
    return float(numbers[-1])  # It may capture a year, a step number, etc.


# ✅ GOOD: An explicit format instruction + robust parsing
def solve_with_robust_format(problem: str) -> dict:
    """CoT with a controlled output format and robust parsing."""
    prompt = f"""{problem}

Think step by step.

IMPORTANT: At the end of your reasoning, write EXACTLY this on a new line:
RESULT: [number or answer, with no extra text]

Do not use "RESULT:" anywhere else in your response."""
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
        max_tokens=600
    )
    output = response.choices[0].message.content
    
    import re
    # Look for the RESULT: at the end of the text
    match = re.search(r'^RESULT:\s*(.+?)$', output, re.MULTILINE | re.IGNORECASE)
    
    if not match:
        # Fallback: make a second call to extract the answer
        extraction_response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "user", "content": prompt},
                {"role": "assistant", "content": output},
                {"role": "user", "content": "Give me ONLY the final result in the format: RESULT: [number]"}
            ],
            temperature=0,
            max_tokens=50
        )
        extraction_output = extraction_response.choices[0].message.content
        match = re.search(r'RESULT:\s*(.+?)$', extraction_output, re.IGNORECASE)
    
    return {
        "reasoning": output,
        "result": match.group(1).strip() if match else None,
        "parse_success": match is not None
    }

Diagnostic Table: Should I Use CoT?

def cot_diagnosis(task: str) -> dict:
    """
    Decision tree for determining whether to use CoT, and which kind.
    """
    task_lower = task.lower()
    
    # Clear anti-patterns: do NOT use CoT
    if any(t in task_lower for t in ["write a", "create a", "generate a poem", "invent"]):
        return {
            "use_cot": False,
            "reason": "Creative task: CoT can make the output rigid",
            "alternative": "A direct prompt with a high temperature"
        }
    
    if any(t in task_lower for t in ["capital of", "what year", "who invented", "when was born"]):
        return {
            "use_cot": False,
            "reason": "A direct factual question: CoT adds tokens with no value",
            "alternative": "A direct prompt with temperature=0"
        }
    
    if any(t in task_lower for t in ["your opinion", "which do you prefer", "which is better", "recommend me"]):
        return {
            "use_cot": False,
            "reason": "Subjective opinion: there is no 'correct' answer",
            "alternative": "List pros and cons; don't use CoT to make the decision"
        }
    
    # Cases where CoT definitely helps
    if any(t in task_lower for t in ["what is", "calculate", "solve", "equation"]):
        return {
            "use_cot": True,
            "type": "zero_shot_cot",
            "reason": "A math problem: CoT improves accuracy significantly"
        }
    
    if any(t in task_lower for t in ["why", "analyze", "evaluate", "is it valid"]):
        return {
            "use_cot": True,
            "type": "manual_cot_or_pipeline",
            "reason": "It requires complex reasoning: CoT makes the process auditable"
        }
    
    # Default case: light CoT
    return {
        "use_cot": True,
        "type": "light_zero_shot_cot",
        "reason": "A task with some complexity: CoT can help",
        "note": "Monitor whether it actually improves things in your specific case"
    }


# Tests
tasks = [
    "Write a horror story",
    "Calculate the compound interest on $1000 at 5% over 3 years",
    "Capital of Mexico?",
    "Is the argument valid: If P then Q, Q, therefore P?",
    "What's the best JavaScript framework?",
    "Analyze the causes of the First World War",
]

for task in tasks:
    d = cot_diagnosis(task)
    status = "✓ CoT" if d["use_cot"] else "✗ No CoT"
    print(f"{status:12} | {task[:50]:50} | {d['reason'][:60]}")

When CoT Can Actively Make the Results Worse

There is research showing that CoT can reduce accuracy in certain cases:

SituationWhy CoT makes it worseSolution
Small models (<7B)They lack the capacity for coherent reasoningUse a bigger model, or few-shot
Exact memorization tasksThe reasoning can "distract" from the correct factA direct prompt
Problems with an obvious trapCoT over-analyzes and lands on the wrong conclusionBe direct
Classification with simple criteriaReasoning can create artificial confusionA direct prompt
Generating short codeReasoning about the code can introduce bugsJust ask for the code
# A concrete example: CoT can make things worse on small models
def test_cot_vs_direct_small_model(problem: str) -> None:
    """
    Demonstrates that CoT can be neutral/negative on less capable models.
    Note: gpt-4o-mini is capable of CoT. For smaller models the effect varies.
    """
    # With CoT
    r_cot = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": f"{problem}\n\nThink step by step."}],
        temperature=0, max_tokens=500
    )
    
    # Without CoT
    r_direct = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": problem}],
        temperature=0, max_tokens=100
    )
    
    print("With CoT:", r_cot.choices[0].message.content[:200])
    print("\nWithout CoT:", r_direct.choices[0].message.content)
    print("\nCompare manually which one is more correct for this specific problem")

Exercises

Exercise 1: Identify tasks that should NOT use CoT

From the following list, classify which ones should NOT use CoT and explain why:

a) Translate "Hello World" into Spanish
b) Solve the quadratic equation x² - 5x + 6 = 0
c) Generate the name of an innovative product
d) Verify whether "All cats are animals. Misi is a cat. Is Misi an animal?" is valid
e) Answer "When was Apple founded?"

See solution
(a) Translation → NO CoT
- A direct task: translation requires no reasoning, it's linguistic mapping
- "Hola Mundo" - no benefit from showing the steps
- Use a direct prompt

(b) Quadratic equation → YES CoT
- It requires multiple steps: compute the discriminant, apply the formula, simplify
- CoT: b²-4ac = 25-24 = 1. x = (5±1)/2. x₁ = 3, x₂ = 2
- Without CoT: the model may give a wrong answer without showing the process

(c) Product name → NO CoT
- A creative task: CoT produces mechanical, unoriginal names
- "Step 1: Identify the industry. Step 2: Look for related words..." → a generic result
- Better: a creative prompt with a high temperature

(d) Logical reasoning → YES CoT
- It requires identifying the premises, applying the syllogism, checking validity
- Without CoT the model can slip up; with CoT it's more accurate and verifiable

(e) Factual question → NO CoT
- "1976" or "April 1, 1976"
- CoT would add: "Apple is a technology company... founded by Wozniak and Jobs...
  the exact year was 1976." → 3x more tokens for the same answer

Exercise 2: Detect fabricated reasoning

The following CoT output contains an error. Find it:

Problem: A store sells 240 products a day. In 5 days, how many products does it sell?

Reasoning:
- Daily sales: 240 products
- Days: 5
- Total: 240 × 5 = 1,200
- Verification: 1,200 / 5 = 240 ✓
- Answer: 1,200 products
See solution

Surprise: This reasoning IS correct. 240 × 5 = 1,200, and the verification is correct too.

This exercise has a trap: the point is that you should not detect an error where there isn't one. The "hunt for errors" bias can lead you to question correct reasoning.

Lesson: Detecting fabricated reasoning must be objective, not biased toward finding errors. Verify each step mathematically:

  • 240 × 5 = 1,200? Yes ✓
  • 1,200 / 5 = 240? Yes ✓
# A robust implementation of objective verification
import re

def verify_operations_in_reasoning(text: str) -> dict:
    errors = []
    correct_ops = []
    
    for match in re.finditer(r'(\d+)\s*[×x\*]\s*(\d+)\s*=\s*([\d,]+)', text):
        a = int(match.group(1))
        b = int(match.group(2))
        stated = int(match.group(3).replace(',', ''))
        correct = a * b
        
        if correct == stated:
            correct_ops.append(f"{a} × {b} = {stated} ✓")
        else:
            errors.append(f"{a} × {b} = {stated} ✗ (correct: {correct})")
    
    return {
        "correct_operations": correct_ops,
        "errors": errors,
        "reasoning_valid": len(errors) == 0
    }

reasoning_text = """240 × 5 = 1,200. Verification: 1,200 / 5 = 240"""
print(verify_operations_in_reasoning(reasoning_text))

Exercise 3: The blind-trust anti-pattern

Write a system that uses CoT for price calculations, but that mathematically verifies the answer before using it in production.

See solution
from openai import OpenAI
import re

client = OpenAI()

def calculate_price_with_validation(
    base_price: float,
    discount_pct: float,
    tax_pct: float
) -> dict:
    """
    Computes the final price with:
    1. CoT for the user-facing explanation
    2. Mathematical verification independent of the LLM's result
    """
    # The correct calculation (deterministic, always right)
    discounted_price = base_price * (1 - discount_pct / 100)
    correct_final_price = discounted_price * (1 + tax_pct / 100)
    
    # CoT for the explanation
    prompt = f"""Explain the final price calculation step by step.
Base price: ${base_price}
Discount: {discount_pct}%
Tax: {tax_pct}%

Show each step. At the end write: FINAL PRICE: $[number]"""
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0, max_tokens=300
    )
    output = response.choices[0].message.content
    
    # Extract the LLM's price
    match = re.search(r'FINAL PRICE:\s*\$?([\d.]+)', output)
    llm_price = float(match.group(1)) if match else None
    
    # Validate that the LLM arrived at the correct result
    discrepancy = None
    if llm_price is not None:
        difference = abs(llm_price - correct_final_price)
        if difference > 0.01:  # A one-cent tolerance
            discrepancy = f"The LLM computed ${llm_price:.2f}, the correct value is ${correct_final_price:.2f}"
    
    return {
        "final_price": correct_final_price,  # ALWAYS use the deterministic calculation
        "llm_price": llm_price,
        "discrepancy_detected": discrepancy,
        "user_explanation": output,
        "validation": "OK" if discrepancy is None else "ERROR_DETECTED"
    }


# Test with several scenarios
scenarios = [
    (100, 20, 16),   # $100, 20% discount, 16% VAT → $80 × 1.16 = $92.80
    (250, 10, 0),    # $250, 10% discount, no VAT → $225
    (1000, 30, 10),  # $1000, 30% discount, 10% VAT → $700 × 1.10 = $770
]

for price, disc, tax in scenarios:
    r = calculate_price_with_validation(price, disc, tax)
    status = "✓" if r["validation"] == "OK" else f"⚠️  {r['discrepancy_detected']}"
    print(f"${price} -{disc}% +{tax}% = ${r['final_price']:.2f} | {status}")

Exercise 4: Build the complete decision tree

Implement a function that, given any type of task, decides: no CoT / zero-shot CoT / manual CoT / pipeline, with a justification.

See solution
from enum import Enum

class CoTStrategy(Enum):
    NO_COT = "No CoT"
    ZERO_SHOT = "Zero-Shot CoT"
    MANUAL_COT = "Manual CoT (Few-Shot)"
    PIPELINE = "Multi-Step Pipeline"

def select_strategy(task: str, context: dict = None) -> dict:
    """
    A complete decision tree for selecting a CoT strategy.
    
    Args:
        task: Description of the task
        context: {'token_budget': 'low/medium/high', 'criticality': 'low/high'}
    """
    context = context or {}
    task_lower = task.lower()
    
    # Level 1: Is the task creative or opinion-based?
    is_creative = any(w in task_lower for w in ['write', 'create', 'generate', 'invent', 'design a text'])
    is_opinion = any(w in task_lower for w in ['best', 'recommend', 'prefer', 'opinion'])
    
    if is_creative or is_opinion:
        return {
            "strategy": CoTStrategy.NO_COT,
            "reason": f"{'Creative' if is_creative else 'Opinion'} task: CoT makes it rigid/biased",
            "temperature": 0.7 if is_creative else 0.3
        }
    
    # Level 2: Is it directly factual?
    is_factual = any(w in task_lower for w in ['capital of', 'when was', 'who invented', 'translate'])
    
    if is_factual:
        return {
            "strategy": CoTStrategy.NO_COT,
            "reason": "A factual question: it requires no reasoning",
            "temperature": 0
        }
    
    # Level 3: Does it require multiple sub-tasks or inspection?
    is_complex = (
        len(task) > 300 or
        task.count('\n') > 5 or
        any(w in task_lower for w in ['first...then', 'several stages', 'analyze and generate'])
    )
    
    if is_complex:
        return {
            "strategy": CoTStrategy.PIPELINE,
            "reason": "A complex problem with multiple sub-tasks",
            "n_stages": 4,
            "temperature": 0
        }
    
    # Level 4: Is there a specific domain that requires exact reasoning?
    is_domain_specific = any(w in task_lower for w in [
        'code', 'debug', 'algorithm', 'contract', 'legal'
    ])
    
    if is_domain_specific:
        return {
            "strategy": CoTStrategy.MANUAL_COT,
            "reason": "A specific domain: reasoning examples improve consistency",
            "n_examples": 2,
            "temperature": 0
        }
    
    # Default: Zero-Shot CoT for moderate reasoning
    return {
        "strategy": CoTStrategy.ZERO_SHOT,
        "reason": "A reasoning task: Zero-Shot CoT is the optimal balance",
        "temperature": 0
    }

# Demo
demo_tasks = [
    "Write a sonnet about technology",
    "Capital of Japan?",
    "Calculate the future value of $5000 at 7% a year over 10 years with compound interest",
    "Analyze this lease agreement, identify abusive clauses, propose alternatives",
    "Debug this Python code: def sum(a,b): return a-b",
]

print(f"{'Task':50} | {'Strategy':25} | Reason")
print("-" * 100)
for task in demo_tasks:
    d = select_strategy(task)
    print(f"{task[:50]:50} | {d['strategy'].value:25} | {d['reason'][:40]}")

Exercise 5: A hallucination detection system for reasoning

Implement a verifier that detects when the model uses information that was not in the original problem.

See solution
from openai import OpenAI

client = OpenAI()

def detectar_informacion_externa(
    problema_original: str,
    razonamiento_cot: str
) -> dict:
    """
    Detects when the reasoning includes information that was not provided.
    """
    prompt = f"""You are an auditor. Verify whether the reasoning uses ONLY the information from the problem.

ORIGINAL PROBLEM (source of truth):
{problema_original}

GENERATED REASONING:
{razonamiento_cot}

Identify: Is there information in the reasoning that is NOT in the original problem?
- Invented data (numbers, percentages, dates that were never mentioned)
- Undeclared assumptions
- Irrelevant real-world facts that the model "imported"
- Generalizations about the problem that go beyond the given data

Respond:
EXTERNAL_INFORMATION: [Yes/No]
LIST: [what information was added, or "None"]"""
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0, max_tokens=300
    )
    output = response.choices[0].message.content
    
    import re
    tiene_externa = "yes" in output.lower()[:100] or "external_information: yes" in output.lower()
    
    return {
        "tiene_informacion_externa": tiene_externa,
        "analisis": output
    }


# Test
problema = "Juan has 5 apples and María has 3. How many do they have together?"
razonamiento_limpio = "Juan: 5. María: 3. Total: 5 + 3 = 8 apples."
razonamiento_contaminado = "Juan has 5 apples. María has 3. An apple weighs roughly 200g, so together they have 1.6 kg of apples. In total they have 8 apples."

print("Clean reasoning:")
print(detectar_informacion_externa(problema, razonamiento_limpio))

print("\nContaminated reasoning:")
print(detectar_informacion_externa(problema, razonamiento_contaminado))

Summary

  • Don't use CoT for: creative tasks, subjective opinion, simple factual questions, direct memorization
  • Fabricated CoT: Persuasive reasoning that looks right but isn't; detect it by verifying the operations mathematically
  • Hallucinations in reasoning: The model can invent data, quotes or premises inside the reasoning; use the instruction "only data from the context"
  • Key anti-patterns: Overuse of CoT, blind trust with no verification, reasoning that's too long, ignoring uncertainty, bad output parsing
  • Golden rule: For critical calculations in production, verify the LLM's result with independent deterministic code

Additional resources

  1. The False Promise of Imitating Proprietary LLMs (Gudibande et al., 2023)
  2. Sycophancy to Subterfuge: Investigating Reward Tampering in Language Models
  3. Measuring Mathematical Problem Solving With the MATH Dataset
  4. TruthfulQA: Measuring How Models Mimic Human Falsehoods
  5. When Not to Trust Language Models (Kadavath et al., 2022)
  6. Calibration of Large Language Models Using Their Generations