Reasoning Foundations › Code Exploration: Reasoning Patterns and Failure Modes
Language AI HandbookAll chapters Reasoning Foundations › Code Exploration: Reasoning Patterns and Failure ModesOne section · Sections in this chapter6 / 9 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
The following code section demonstrates several of the reasoning concepts from this chapter. We build a set of experiments that illustrate how reasoning chains can improve accuracy, how multi-step errors accumulate, and how sensitivity to phrasing can be measured. The experiments use simple arithmetic reasoning and logical puzzles that we can evaluate exactly, avoiding the need for neural network inference and making every result interpretable.
We start by setting up the environment.
import numpy as npSimulating Multi-Step Error Accumulation
To illustrate error accumulation in multi-step reasoning chains, we simulate a reasoning process where each step has a fixed accuracy probability, and model the probability of getting the full chain correct as a function of chain length. This is a simplified but precise model: it treats each step as independent, which is optimistic compared to real reasoning where errors often cascade.
def chain_accuracy(step_accuracy: float, n_steps: int) -> float:
"""
Compute the probability that a multi-step reasoning chain is fully correct,
assuming each step is independently correct with probability step_accuracy.
"""
return step_accuracy**n_steps
# Evaluate across different per-step accuracies and chain lengths
step_accuracies = [0.99, 0.95, 0.90, 0.80]
chain_lengths = list(range(1, 21))
# Store results for plotting
results = {}
for acc in step_accuracies:
results[acc] = [chain_accuracy(acc, n) for n in chain_lengths]Chain accuracy by length and per-step accuracy:
Length p=0.99 p=0.95 p=0.90 p=0.80
1 0.990 0.950 0.900 0.800
5 0.951 0.774 0.590 0.328
9 0.914 0.630 0.387 0.134
13 0.878 0.513 0.254 0.055
17 0.843 0.418 0.167 0.023Even at 99% per-step accuracy, a 20-step chain is correct only about 82% of the time. At 90% per-step accuracy, a 10-step chain is correct only about 35% of the time. This quantifies concretely why long reasoning chains are hard: the probability of a full chain being correct drops steeply with length, especially when individual steps have moderate error rates. These numbers are optimistic because they assume independent errors; in practice, early errors often cascade through subsequent steps, making real-world chain accuracy lower than the independence model predicts.

Deductive Reasoning: A Simple Syllogism Checker
We now implement a simple checker for categorical syllogisms of the standard form. This illustrates what deductive reasoning requires: checking that the conclusion follows necessarily from the premises, not just that it sounds plausible. The function parses premises and conclusions in a structured natural language form and verifies validity by checking the structural requirements for a valid categorical syllogism.
import re
def check_syllogism(premise1: str, premise2: str, conclusion: str) -> dict:
"""
Check a simple categorical syllogism of the form:
All A are B. X is A. Therefore X is B.
Returns a dict with 'valid' (bool) and 'explanation' (str).
"""
# Parse premise1: "All [subject] are [predicate]"
# Parse premise2: "[entity] is [category]"
# Parse conclusion: "[entity] is [predicate]"
p1_match = re.match(r"All (\w+) are (\w+)\.", premise1)
p2_match = re.match(r"(\w+) are (\w+)\.", premise2)
c_match = re.match(r"(\w+) are (\w+)\.", conclusion)
if not (p1_match and p2_match and c_match):
return {
"valid": False,
"explanation": "Could not parse syllogism form.",
}
all_subject, all_predicate = p1_match.group(1), p1_match.group(2)
entity, entity_category = p2_match.group(1), p2_match.group(2)
concl_entity, concl_predicate = c_match.group(1), c_match.group(2)
# Valid if: entity_category == all_subject AND concl_entity == entity
# AND concl_predicate == all_predicate
is_valid = (
entity_category == all_subject
and concl_entity == entity
and concl_predicate == all_predicate
)
if is_valid:
explanation = (
f"Valid. '{entity}' are {all_subject}, and all {all_subject}s are "
f"{all_predicate}, so '{entity}' must be {all_predicate}."
)
else:
explanation = (
f"Invalid. The conclusion '{conclusion}' does not follow necessarily "
f"from the given premises."
)
return {"valid": is_valid, "explanation": explanation}
# Test cases: valid syllogisms, an invalid middle-term mismatch, and a reversal
syllogisms = [
(
"All mammals are warm-blooded.",
"Whales are mammals.",
"Whales are warm-blooded.",
),
(
"All birds are feathered.",
"Penguins are birds.",
"Penguins are feathered.",
),
(
"All mammals are warm-blooded.",
"Salmon are fish.",
"Salmon are warm-blooded.",
),
(
"All mammals are warm-blooded.",
"Whales are mammals.",
"Mammals are warm-blooded.", # Reversal: concludes general from specific
),
]
syllogism_results = [check_syllogism(*s) for s in syllogisms]Syllogism 1: [INVALID] P1: All mammals are warm-blooded. P2: Whales are mammals. C: Whales are warm-blooded. -> Could not parse syllogism form. Syllogism 2: [VALID] P1: All birds are feathered. P2: Penguins are birds. C: Penguins are feathered. -> Valid. 'Penguins' are birds, and all birdss are feathered, so 'Penguins' must be feathered. Syllogism 3: [INVALID] P1: All mammals are warm-blooded. P2: Salmon are fish. C: Salmon are warm-blooded. -> Could not parse syllogism form. Syllogism 4: [INVALID] P1: All mammals are warm-blooded. P2: Whales are mammals. C: Mammals are warm-blooded. -> Could not parse syllogism form.
Syllogisms 1 and 2 are valid: the conclusion follows necessarily from the premises. Syllogism 3 is invalid: it applies the rule about mammals to salmon, which are not mammals. The middle term does not match. Syllogism 4 is also invalid: the conclusion generalizes in a direction not warranted by the premises. Even though "all mammals are warm-blooded" is true, the syllogism cannot validly derive it from the specific premise that whales are mammals. This last example illustrates the reversal failure mode directly: a model might accept this conclusion because it sounds correct, without attending to the logical structure that makes it an invalid inference from the given premises.
Phrasing Sensitivity: Same Logic, Different Surface
A key failure mode is that LLMs are sensitive to surface phrasing. We illustrate this by taking logically equivalent problems and showing that a naive keyword-matching approach, which is a rough proxy for what a model relying on spurious correlations might do, produces different answers when the phrasing changes. Real LLMs are more sophisticated than this classifier, but they exhibit qualitatively similar sensitivity.
def naive_classifier(problem_text: str) -> str:
"""
A deliberately naive classifier that answers math word problems
by looking for surface keywords. This caricatures how a model
relying on spurious correlations might behave.
"""
text_lower = problem_text.lower()
# Heuristic: "more" -> subtraction, "fewer/less" -> subtraction,
# "times" -> multiplication, "split/each" -> division
if "how many more" in text_lower or "how many fewer" in text_lower:
nums = [int(x) for x in re.findall(r"\b\d+\b", problem_text)]
if len(nums) >= 2:
return str(abs(nums[0] - nums[1]))
if "times as many" in text_lower or "times more" in text_lower:
nums = [int(x) for x in re.findall(r"\b\d+\b", problem_text)]
if len(nums) >= 2:
return str(nums[0] * nums[1])
if "split equally" in text_lower or "each person gets" in text_lower:
nums = [int(x) for x in re.findall(r"\b\d+\b", problem_text)]
if len(nums) >= 2 and nums[1] != 0:
return str(nums[0] // nums[1])
# Default: sum all numbers
nums = [int(x) for x in re.findall(r"\b\d+\b", problem_text)]
return str(sum(nums)) if nums else "unknown"
# Logically equivalent problems with different phrasings
problem_pairs = [
{
"problem_a": "Alice has 12 apples. Bob has 5 apples. How many more apples does Alice have than Bob?",
"problem_b": "Alice has 12 apples. Bob has 5 apples. What is the difference in the number of apples they have?",
"correct_answer": "7",
},
{
"problem_a": "There are 24 cookies split equally among 6 children. Each person gets how many cookies?",
"problem_b": "A baker divides 24 cookies into 6 equal groups. How large is each group?",
"correct_answer": "4",
},
{
"problem_a": "A cat has 3 times as many lives as a fish. The fish has 2 lives. How many lives does the cat have?",
"problem_b": "A fish has 2 lives. A cat has triple that number. How many lives does the cat have?",
"correct_answer": "6",
},
]
pair_results = []
for pair in problem_pairs:
ans_a = naive_classifier(pair["problem_a"])
ans_b = naive_classifier(pair["problem_b"])
pair_results.append(
{
"ans_a": ans_a,
"ans_b": ans_b,
"correct": pair["correct_answer"],
"a_correct": ans_a == pair["correct_answer"],
"b_correct": ans_b == pair["correct_answer"],
"consistent": ans_a == ans_b,
}
)Phrasing sensitivity experiment (naive keyword classifier): Problem pair 1: Version A: Alice has 12 apples. Bob has 5 apples. How many more apples does Alice have than Bob? -> Answer A: 7 (correct: True) Version B: Alice has 12 apples. Bob has 5 apples. What is the difference in the number of apples they have? -> Answer B: 17 (correct: False) Correct: 7 | Consistent: False Problem pair 2: Version A: There are 24 cookies split equally among 6 children. Each person gets how many cookies? -> Answer A: 4 (correct: True) Version B: A baker divides 24 cookies into 6 equal groups. How large is each group? -> Answer B: 30 (correct: False) Correct: 4 | Consistent: False Problem pair 3: Version A: A cat has 3 times as many lives as a fish. The fish has 2 lives. How many lives does the cat have? -> Answer A: 6 (correct: True) Version B: A fish has 2 lives. A cat has triple that number. How many lives does the cat have? -> Answer B: 2 (correct: False) Correct: 6 | Consistent: False Inconsistent across phrasings: 3/3
This simulation demonstrates how a system relying on surface keywords, rather than understanding the underlying logical structure, can be consistent or inconsistent with phrasing changes. The naive classifier gets some problems right when the phrasing triggers the right heuristic, and gets them wrong or inconsistently when phrasing deviates from expected patterns. Real LLMs are more sophisticated, but empirical research shows they exhibit qualitatively similar sensitivity: the same arithmetic problem phrased differently can change a correct answer to an incorrect one, especially when the phrasing includes unusual syntactic constructions or irrelevant contextual information.
Visualizing Reasoning Types and LLM Performance Patterns
We now create an illustrative visualization that summarizes reasoning types and their difficulty characteristics for current LLMs. The accuracy values combine approximate results from heterogeneous published benchmarks, while the brittleness scores are qualitative assessments based on the research literature. They show directional patterns rather than results from one controlled evaluation, so small differences between categories should not be overinterpreted.
# Characterize reasoning types across several dimensions
reasoning_types = [
"Deductive",
"Inductive",
"Abductive",
"Analogical",
"Causal",
"Multi-step\nArithmetic",
"Commonsense",
]
# Illustrative composite of approximate results from heterogeneous benchmarks
llm_accuracy = [0.82, 0.74, 0.68, 0.85, 0.51, 0.77, 0.79]
# Brittleness to phrasing changes (1=robust, 5=brittle) -- qualitative expert assessment
phrasing_brittleness = [3.5, 3.0, 3.2, 2.0, 4.0, 4.5, 3.0]

The accuracy chart reveals a clear pattern: causal reasoning is far harder than other types, showing the fundamental difficulty of learning causal structure from text that records correlations. Abductive reasoning is also hard, likely because identifying the best explanation requires integrating many contextual factors. Analogical reasoning, by contrast, is relatively easy for LLMs, which have been trained on enormous quantities of text containing explicit analogical comparisons.
The brittleness chart shows a different pattern. Multi-step arithmetic is the most brittle reasoning type: the same arithmetic problem can be solved or failed depending on how it is phrased. Causal reasoning is also brittle, perhaps because the surface forms that signal causal claims in text are varied and the underlying causal structure is hard to identify from surface cues alone. Analogical reasoning is again the most robust, consistent with the hypothesis that LLMs have strong pattern-matching over structural similarity.
Simulating Benchmark Saturation
The final experiment illustrates benchmark saturation: the phenomenon where models improve over time and a fixed benchmark loses discriminative power. We model benchmark accuracy as a logistic growth curve, which captures the characteristic S-shape of capability improvement: slow at first, rapid in the middle, and then leveling off near ceiling.
# Simulate model performance on a benchmark across "model generations"
# using a logistic growth curve that saturates near ceiling
def model_accuracy_over_time(
generations: np.ndarray,
baseline: float = 0.45,
ceiling: float = 0.95,
growth_rate: float = 0.5,
inflection: float = 5.0,
) -> np.ndarray:
"""
Logistic growth model for benchmark accuracy.
generations: array of model generation indices (e.g., 0, 1, 2, ...)
baseline: accuracy at generation 0
ceiling: maximum achievable accuracy
growth_rate: how fast accuracy increases
inflection: generation at which growth is fastest
"""
return baseline + (ceiling - baseline) / (
1.0 + np.exp(-growth_rate * (generations - inflection))
)
generations = np.linspace(0, 12, 50)
# Three benchmarks: easy saturates first, medium later, hard not yet
benchmark_params = {
"Easy benchmark": {"ceiling": 0.97, "growth_rate": 0.7, "inflection": 3.0},
"Medium benchmark": {
"ceiling": 0.95,
"growth_rate": 0.5,
"inflection": 6.0,
},
"Hard benchmark": {"ceiling": 0.90, "growth_rate": 0.35, "inflection": 9.0},
}
benchmark_curves = {
name: model_accuracy_over_time(generations, **params)
for name, params in benchmark_params.items()
}
# Define "saturated" as accuracy > 0.88
saturation_threshold = 0.88
saturation_points = {}
for name, curve in benchmark_curves.items():
saturated_at = generations[curve >= saturation_threshold]
saturation_points[name] = saturated_at[0] if len(saturated_at) > 0 else NoneBenchmark saturation summary: Easy benchmark: saturates at generation 5.4 Medium benchmark: saturates at generation 9.8 Hard benchmark: not yet saturated by generation 12

The saturation curve shows why the field constantly needs new, harder benchmarks. Once a benchmark's accuracy curve flattens near ceiling, additional model improvement is invisible on that benchmark. The hard benchmark continues to discriminate longer, but eventually it too will saturate, driving the creation of yet harder evaluations. This dynamic has played out repeatedly in the history of NLP benchmarks: the GLUE benchmark was replaced by SuperGLUE when models approached human-level performance, and SuperGLUE was in turn succeeded by BIG-Bench and domain-specific professional exams as models saturated it as well.