← Back to Blog
AI EngineeringLLMsAI for PMs

Scaffolding Debt: How to Test Which Parts of Your LLM Pipeline Survive the Next Model

A new study found that for 37–63% of recent LLM techniques, a single prompt on a newer model matched or beat the complex version. Here's how to ablation-test your own pipeline so you can delete the workarounds you no longer need.

By JoinloopΒ·25 September 2026Β·11 min read
ShareLinkedInXReddit
πŸͺ“

Every production LLM system builds up workarounds. A second "verify your answer" call went in because the model hallucinated field names. A five-step decomposition chain went in because the model couldn't hold the whole task at once. A regex repair layer went in because JSON came back malformed 4% of the time. Each one fixed a real failure on the model you were using at the time.

Then the model changes. You bump the model ID, run a smoke test, see nothing broke, and ship. The workarounds stay. Nobody checks whether they're still pulling their weight, or whether they now make things worse.

That's scaffolding debt: pipeline complexity built to cover model weaknesses that no longer exist. It costs you latency, tokens, and debugging time on every request, and it can quietly lower quality.

The evidence: most workarounds don't outlive their model

A September 2026 paper from Salsabil, Dwyer, Elbaum and colleagues, What Survives the Next Model?, tested this systematically. They took 35 LLM-based techniques published at ICSE 2026, a top software engineering venue, and compared each one against the simplest possible baseline: a single, automatically generated prompt run on a newer-generation model.

The results:

  • In 37–63% of the papers (depending on how strictly you count), the single prompt on the newer model beat or matched the published technique.
  • Constructive tasks (code generation and program repair) were the most likely to be overtaken.
  • The techniques that survived tended to add information the model couldn't get on its own, such as program analysis results, execution feedback, or domain constraints. A stronger model makes that kind of technique more useful, not less.

The authors' point applies directly to product teams. A technique built as a workaround for a temporary model deficit has a shelf life. A technique that supplies missing information or verification improves as the model does.

Your pipeline is probably a mix of both, and you can't tell which is which by reading the code. You have to measure it.

Two kinds of scaffolding

Before running experiments, sort each pipeline component into one of two buckets. It's a hypothesis, not a verdict, but it tells you what to test first.

Component type What it does Example Expected fate on a stronger model
Crutch Compensates for a reasoning, formatting, or instruction-following weakness "Think step by step" chains, self-critique loops, JSON repair, task decomposition into 5 tiny calls, few-shot examples teaching the output format Often removable. Can hurt by over-constraining a capable model
Amplifier Gives the model information or feedback it can't produce itself Retrieval, test execution results, static analysis output, schema/constraint validation, tool access, user-specific context Usually keep. Value tends to grow with model capability
Guardrail Enforces a hard business or safety requirement PII redaction, output policy checks, allowlisted actions Keep regardless of performance. It's not there for quality

The third row matters: some components aren't there to improve quality, and they should never enter an ablation. Mark them out of scope up front.

The ablation harness

An ablation study removes one component at a time and measures what changes. For an LLM pipeline you need three things:

  1. A pipeline where each component can be switched off with a flag
  2. A fixed eval set with a scoring function (you should already have one; if not, start with our guide on LLM evals for production agents)
  3. A runner that crosses {model} Γ— {component configuration} and records quality, cost, and latency

Here is a minimal version in Python. It uses the OpenAI-compatible chat API shape so it works with most providers through a gateway. Swap in your own client and model IDs.

# ablate.py
import itertools, json, time, statistics
from dataclasses import dataclass, field, replace
from openai import OpenAI

client = OpenAI()  # or point base_url at your gateway

@dataclass(frozen=True)
class PipelineConfig:
    model: str
    decompose: bool = True        # crutch: split task into sub-steps
    few_shot: bool = True         # crutch: format-teaching examples
    self_critique: bool = True    # crutch: second "check your work" pass
    retrieval: bool = True        # amplifier: inject relevant docs
    run_tests: bool = True        # amplifier: execution feedback loop

@dataclass
class Result:
    score: float
    tokens: int
    latency_s: float

def call(model, messages):
    r = client.chat.completions.create(model=model, messages=messages)
    return r.choices[0].message.content, r.usage.total_tokens

def run_pipeline(cfg: PipelineConfig, example: dict) -> Result:
    start, tokens = time.time(), 0
    system = "You are a careful senior engineer."
    user = example["task"]

    if cfg.retrieval:
        user += "\n\nRelevant context:\n" + example["context"]
    if cfg.few_shot:
        user = FEW_SHOT_BLOCK + "\n\n" + user

    if cfg.decompose:
        plan, t = call(cfg.model, [
            {"role": "system", "content": system},
            {"role": "user", "content": f"List the sub-steps to solve:\n{user}"},
        ])
        tokens += t
        user += f"\n\nFollow this plan:\n{plan}"

    answer, t = call(cfg.model, [
        {"role": "system", "content": system},
        {"role": "user", "content": user},
    ])
    tokens += t

    if cfg.run_tests:
        feedback = example["test_fn"](answer)  # returns None if passing
        if feedback:
            answer, t = call(cfg.model, [
                {"role": "system", "content": system},
                {"role": "user", "content": user},
                {"role": "assistant", "content": answer},
                {"role": "user", "content": f"Tests failed:\n{feedback}\nFix it."},
            ])
            tokens += t

    if cfg.self_critique:
        answer, t = call(cfg.model, [
            {"role": "system", "content": system},
            {"role": "user", "content": f"Task:\n{user}\n\nDraft:\n{answer}\n\n"
                                        "Review the draft and return a corrected final version."},
        ])
        tokens += t

    score = example["score_fn"](answer)
    return Result(score, tokens, time.time() - start)

ABLATABLE = ["decompose", "few_shot", "self_critique", "retrieval", "run_tests"]

def configs_for(model):
    full = PipelineConfig(model=model)
    yield "full", full
    for name in ABLATABLE:                     # leave-one-out
        yield f"-{name}", replace(full, **{name: False})
    yield "bare", replace(full, **{n: False for n in ABLATABLE})  # single prompt

def main(models, dataset, repeats=3):
    rows = []
    for model in models:
        for label, cfg in configs_for(model):
            results = [run_pipeline(cfg, ex) for ex in dataset for _ in range(repeats)]
            rows.append({
                "model": model, "config": label,
                "score": statistics.mean(r.score for r in results),
                "score_sd": statistics.pstdev(r.score for r in results),
                "tokens": statistics.mean(r.tokens for r in results),
                "latency_s": statistics.mean(r.latency_s for r in results),
            })
            print(json.dumps(rows[-1]))
    return rows

FEW_SHOT_BLOCK, context, test_fn and score_fn are placeholders for your own pipeline's pieces. The structure is what matters:

  • Leave-one-out tells you each component's marginal contribution given everything else is on.
  • Bare is the paper's baseline: one prompt, no scaffolding. If bare on the new model gets close to full on the old model, you have a lot of debt.
  • Repeats matter because LLM outputs vary. Three runs per example is the minimum, and you should look at the standard deviation before trusting any difference.

The full grid is models Γ— (components + 2) configurations. With 5 components and 2 models that's 14 configs. At 200 examples Γ— 3 repeats, that's 8,400 pipeline runs, so use a batch API where you can.

Reading the results

Put the output in a table with the old model and new model side by side. A typical result looks something like this (illustrative numbers):

Config Old model score New model score New model tokens New model p50 latency
full 0.71 0.80 9,400 14.2s
βˆ’decompose 0.64 0.82 6,100 9.8s
βˆ’few_shot 0.66 0.80 7,900 12.9s
βˆ’self_critique 0.69 0.81 6,800 10.1s
βˆ’retrieval 0.52 0.58 7,000 11.5s
βˆ’run_tests 0.60 0.69 8,200 10.4s
bare 0.41 0.61 1,900 3.1s

How to read it:

  • Decompose was essential on the old model (removing it cost 7 points) and is slightly harmful on the new one. That's a classic expired crutch: the stronger model plans better than your hard-coded plan, and the extra step constrains it.
  • Few-shot and self-critique are neutral on the new model but cost tokens and seconds on every request. Remove them.
  • Retrieval and run_tests are amplifiers. Their drop is as large or larger on the new model. Keep them, and consider investing more in them.
  • Bare improved a lot but is still well below full, so scaffolding isn't worthless. The job is to keep the right parts.

The pruned pipeline (retrieval + tests only) would likely score about the same as full on the new model at roughly half the tokens and latency. Confirm that by running it as its own config. Don't assume components contribute additively, because they interact.

A decision checklist per component

For each component, after the ablation:

  • Score delta when removed is within noise (less than ~1 standard deviation across repeats)? β†’ candidate for removal
  • Score delta is positive when removed? β†’ remove; it's actively hurting
  • Cost and latency savings are meaningful at your traffic volume? β†’ prioritize this removal
  • Slice-level check: does removal hurt a specific segment (a language, a customer tier, long inputs) even if the average is flat? β†’ keep, or make it conditional for that slice
  • Failure-mode check: read 20 outputs where the pruned config lost. Are they new failure types your aggregate score doesn't capture?
  • Is it a guardrail? β†’ out of scope; keep it

Common pitfalls

Averages hide regressions. A component can be neutral on average and essential for 8% of traffic. Always break results down by the slices your business cares about before deleting anything.

Your eval set is from the old model's era. If you built your test cases around the old model's failures, they over-represent problems the new model may not have and miss the new model's own failure modes. Refresh the set with recent production traces before you ablate.

Changing the model and the pipeline in one release. If quality drops, you won't know which change caused it. Ship the model upgrade with the full pipeline first, confirm it's stable in production, then prune in a separate release.

Treating "bare wins" as "scaffolding is bad." The paper's clearest finding is that techniques adding information survive. Removing retrieval because a new model "knows more" is a common and expensive mistake. The model doesn't know your customer's data.

Only ablating on upgrade day. Providers update models behind stable aliases, and your traffic mix changes. Run the leave-one-out suite on a schedule (monthly is reasonable) and alert when a component's marginal contribution crosses zero.

Ignoring prompt interactions. A system prompt tuned for a decomposition step may read oddly without one. When you remove a component, spend 15 minutes reviewing the prompts around it, or you'll underestimate the pruned pipeline.

For PMs: make scaffolding a line item

If you own an AI feature, scaffolding debt is a roadmap problem, not just an engineering one. Three habits help:

  1. Tag every pipeline component with its reason for existing in the PRD or design doc: which failure it fixed, on which model, with what eval evidence. When the model changes, that list becomes your ablation plan.
  2. Budget an ablation pass into every model upgrade. It's typically one or two engineer-days plus eval compute, and it often pays back in inference cost within weeks.
  3. Track cost-per-successful-task, not just quality. A pipeline that scores 0.80 at 9,400 tokens loses to one that scores 0.80 at 4,000, and only a per-task cost metric makes that visible.

Next steps

  1. List every step in your main LLM pipeline and label it crutch, amplifier, or guardrail.
  2. Add a config flag to switch each non-guardrail step off.
  3. Run leave-one-out plus a bare baseline on your current model and the newest candidate, with at least three repeats.
  4. Remove anything that's neutral or harmful, confirm with a pruned-config run and a slice breakdown, and ship the pruning separately from the model upgrade.
  5. Schedule the suite to run again next month.

Model-specific details such as structured output modes, reasoning-effort settings, and context limits change often, so check your provider's current docs before assuming a workaround is still needed or no longer needed.

If you want to build evaluation and ablation habits like these on real systems with feedback from practitioners, that's core to Joinloop's AI engineering cohort. For product leaders deciding what an AI feature should cost and how to measure it, see the PM cohort.

ShareLinkedInXReddit

Want to build this yourself?

Join a live cohort and go from reading about AI to shipping it, with practitioners who do it daily.