Defend a tradeoff
Chapters and challenges so far taught you to build, debug, and design LLM systems. This one trains the meta-skill that separates senior engineers from the rest: taking a real fork in the road — RAG vs fine-tune, build vs buy, agent vs pipeline, sync vs batch, small model vs big — and defending a decision with evidence. Not the tool you like; the call you can put on the record, with the criteria you weighed, the thing you give up, and the signal that would change your mind. A decision you can’t defend is a guess.
Learning objectives
- Turn a hard technical fork into a defensible decision, not a preference.
- Enumerate real options, then score them on explicit, weighted criteria.
- State what you give up and the conditions under which the decision flips.
- Defend five staff-level forks: RAG vs fine-tune, build vs buy, agent vs pipeline, sync vs batch, small+scaffolding vs big model.
- Grade a tradeoff defense the way an interviewer or a design review would.
1 · The decision framework essential
Every one of the forks below is worked with the same six-step frame. Interviewers and design reviews are really testing whether you have a frame — the specific answer matters less than whether the reasoning is legible. Learn the loop once; apply it to anything.
This is the whole lesson in one picture: the repeatable loop that turns a hard fork into a decision you can defend. Read it left to right — every fork below is just this frame filled in with different options.
- Goal + constraints — start here, not with the tool you like. Write down what success is and the hard limits (a latency SLA, a budget, a compliance rule). The constraints do most of the deciding.
- Enumerate options — list 2–4 real alternatives. One option and a strawman isn't a decision; it's a justification you wrote after the fact.
- Score on criteria — rate each option on the same short list: quality, cost, latency, maintenance, risk. Same axes every time, so the comparison is honest.
- Weight by what matters — the key step. The criteria are not equal for this context: a chat UI weights latency high, a nightly batch job weights it at zero. The weighting is where the real judgement lives.
- Decide — the pick now falls out of the weighted scores. If it doesn't, your weights are wrong or you're overriding them on a hunch — fix that before you commit.
- Name the tradeoff — the step juniors skip: say what you gave up and the specific signal that would flip the decision. Without it you have a preference, not a decision.
In short: Whenever a fork stalls you, drop back to these six boxes. The specific answer matters less than being able to walk the loop out loud — that legibility is the whole skill.
The criteria are almost always the same short list — quality, cost, latency, maintenance/ops, risk, time-to-ship — and the whole skill is in the weighting. The right answer for a nightly batch job is the wrong answer for a chat UI because the weights differ, not because the options changed. Make the weights explicit and the decision defends itself.
2 · Fork — RAG vs fine-tune essential
Decision: default to RAG. For a docs Q&A bot that must answer from a corpus that changes weekly, RAG puts the knowledge in context at query time; fine-tuning bakes it into weights you'd have to re-train every time the docs change. Fine-tuning changes behavior (style, format, a narrow skill) — it's a poor way to add knowledge. This is the XT4 decision, defended out loud.
| Criterion | Weight | RAG | Fine-tune | Big prompt (long-context) |
|---|---|---|---|---|
| Quality on fresh facts | high | strong (cites sources) | weak (stale at train time) | strong but token-heavy |
| Cost per call | med | moderate (retrieval + context) | low (small tuned model) | high (huge context every call) |
| Time to ship | high | days | weeks (data prep dominates) | hours |
| Maintenance | med | re-index, not re-train | re-tune when base/docs change | trivial |
| Freshness | high | update the index | requires re-training | paste new docs |
The defense: weight freshness and time-to-ship high (the docs change weekly, we need it this sprint) and RAG dominates — it wins the two heaviest criteria and ties on quality. The scorer below makes that ranking concrete rather than asserted.
decide.pydef score_options(options, weights):
"""options: {name: {criterion: 0..5}} (higher = better on that criterion).
weights: {criterion: weight}. Returns options ranked best-first."""
ranked = []
for name, scores in options.items():
total = sum(scores[c] * w for c, w in weights.items())
ranked.append((name, round(total, 1)))
ranked.sort(key=lambda x: x[1], reverse=True)
return ranked
# Fork: RAG vs fine-tune vs long-context prompt for a docs Q&A bot.
weights = {"quality": 3, "cost": 2, "latency": 1, "maintenance": 2, "time_to_ship": 3}
options = {
"RAG": {"quality": 4, "cost": 4, "latency": 3, "maintenance": 4, "time_to_ship": 5},
"Fine-tune": {"quality": 4, "cost": 2, "latency": 5, "maintenance": 1, "time_to_ship": 1},
"Big prompt": {"quality": 3, "cost": 1, "latency": 2, "maintenance": 5, "time_to_ship": 5},
}
for name, total in score_options(options, weights):
print(f"{name:12} {total}")
print("decision:", score_options(options, weights)[0][0])
RAG 46
Big prompt 38
Fine-tune 26
decision: RAG
This tiny program turns "I think RAG is better" into a ranking you can put on a slide. It scores each option against weighted criteria and prints them best-first — the decision becomes arithmetic instead of an assertion. It's pure stdlib, so it runs with a plain python decide.py.
score_options(options, weights)takes two dicts: each option's 0–5 rating on every criterion (higher = better), and how much each criterion weighs in this context.sum(scores[c] * w for c, w in weights.items())is the whole idea — multiply each rating by that criterion's weight and add them up. A great score on a criterion you weighted low barely moves the total; that's intentional.ranked.sort(key=lambda x: x[1], reverse=True)orders the options by weighted total, highest first, so the winner isranked[0].- The
weightshere encode this fork's reality:qualityandtime_to_shipat 3 (we need it this sprint, on fresh docs) outweighlatencyat 1. Change those weights and you change the winner — which is the point.
What the output means: Three lines ranked best-first — RAG 46, Big prompt 38, Fine-tune 26 — then decision: RAG. RAG wins because it takes the two heaviest criteria; the number is the defense.
Try this: Bump latency's weight to 5 and drop time_to_ship to 1 (pretend this is a latency-critical, no-deadline path). Watch Fine-tune climb — proof that the decision lives in the weighting, not the options.
3 · Fork — build vs buy a component intermediate
Decision: buy the undifferentiated component; build only your moat. For a vector database, an eval harness, or an LLM gateway, the honest question is: is this the thing customers pay us for? Almost never. A managed vector DB, a hosted eval tool, or an off-the-shelf gateway is battle-tested, maintained by someone else, and lets your small team spend its scarce build-hours on the product no one else can ship.
| Criterion | Build it | Buy it (managed) |
|---|---|---|
| Fits our exact need | perfect | 80–90% (config, not custom) |
| Time to first value | weeks–months | hours |
| Who fixes it at 3am | you | the vendor's on-call |
| Ongoing maintenance | yours forever | theirs |
| Cost at scale | fixed infra + salaries | per-usage (can exceed build) |
| Is it our moat? | only if it's the product | rarely |
The defense: engineering time is the scarcest resource on a small team. Every week spent building a vector DB is a week not spent on the differentiated product. Undifferentiated heavy lifting is precisely what you pay a vendor for — you're buying focus, not just software. This is the same capability-vs-control weighing as PE4's API-vs-self-host call, one layer up the stack.
4 · Fork — single agent vs pipeline of steps advanced
Decision: pipeline by default; reach for an agent only where the path is genuinely open-ended. A fixed pipeline (classify → retrieve → answer → validate) is deterministic, cheap, testable step-by-step, and easy to debug — you know exactly which stage failed. An agentic loop that plans its own tool calls buys flexibility for tasks whose shape you can't predict, at the cost of determinism, token spend, and debuggability.
| Criterion | Weight | Fixed pipeline | Single agent (tool loop) |
|---|---|---|---|
| Determinism | high | fixed path, reproducible | path varies per run |
| Debuggability | high | known failing stage | trace a nondeterministic loop |
| Cost / latency | med | bounded (N steps) | unbounded (loops, retries) |
| Flexibility | med | only the paths you built | handles the unforeseen |
| Eval surface | high | each stage unit-tested | end-to-end only, harder |
The defense: most production tasks have a knowable shape. Weight determinism, debuggability, and eval surface high — as any team running this in production must — and the pipeline wins decisively. "Agent" is not a maturity level; it's a tool for open-ended tasks, and you pay for its flexibility in every dimension a pipeline is strong. When you do need an agent, contain it: bounded steps, a tool allowlist, and a validation stage on the way out — a pipeline wrapped around the loop.
5 · Fork — sync/realtime vs batch professional
Decision: route by whether a human is blocked on this exact response. If someone is waiting (chat, an API endpoint) it must be synchronous — no discount is worth a user staring at a spinner. If nothing is blocked (overnight classification, backfills, bulk summarization) batch it and take the ~50% token discount. This is the AP1 decision, defended with the gate question.
| Criterion | Weight (chat) | Weight (bulk) | Sync/realtime | Batch |
|---|---|---|---|---|
| Latency | critical | irrelevant | seconds | minutes–hours |
| Cost per token | low | critical | full price | ~50% off |
| Is a human blocked? | yes | no | required | forbidden |
| Throughput / $ | low | critical | poor | excellent |
The defense: the same model, same prompt, two delivery modes — the decision is entirely about which criterion you weight. A chat turn weights latency critical, so the 50% saving is irrelevant. A 50,000-row nightly job weights throughput-per-dollar critical and latency at zero, so batching is close to free money. Naming the weights is the defense; the flip is built into the question "is anyone blocked?"
6 · Fork — small model + scaffolding vs big model tech-lead
Decision: push the cost/quality frontier — a small model with good scaffolding beats a big model raw, until it doesn't. A cheap, fast model wrapped in retrieval, tight prompts, validation, and a retry/repair loop often matches a frontier model on the common cases at a fraction of the cost and latency. The frontier model still wins on the hardest reasoning — so the lead's move is usually both: small model for the easy majority, escalate the hard tail to the big one (the hybrid from PE4).
The build-vs-buy-style flip here is a pure break-even: a big model has near-zero fixed cost but a high per-call price; a small tuned model (or a self-hosted one) carries an upfront build and cheaper calls. Below some volume the big model is simply cheaper; above it, the small one wins. Don't argue it — compute it.
flip.pydef when_finetune_flips(rag_infra_monthly, ft_fixed_cost, ft_infra_monthly,
rag_cost_per_call, ft_cost_per_call, months=12):
"""Big-model/RAG path: low fixed cost, higher per-call. Small-tuned path: big
upfront build, cheaper per-call. Return the monthly volume where the cheap
per-call path wins over the horizon."""
per_call_gap = rag_cost_per_call - ft_cost_per_call # expensive path pays more/call
infra_gap = (rag_infra_monthly - ft_infra_monthly) * months
if per_call_gap <= 0:
return "cheaper-per-call path never wins at these rates"
calls_total = (ft_fixed_cost - infra_gap) / (per_call_gap * months)
if calls_total <= 0:
return "cheaper-per-call path already wins from call 1"
return f"small/tuned model wins above ~{round(calls_total):,} calls/month"
print(when_finetune_flips(
rag_infra_monthly=300, ft_fixed_cost=40_000, ft_infra_monthly=200,
rag_cost_per_call=0.004, ft_cost_per_call=0.0008))
small/tuned model wins above ~1,010,417 calls/month
This helper answers the one question that settles a small-vs-big (or build-vs-buy) cost argument: at what volume does the cheaper-per-call option actually win? One path has a big upfront cost but cheap calls; the other has no upfront cost but pricey calls. It's stdlib — run it with python flip.py.
per_call_gap = rag_cost_per_call - ft_cost_per_callis how much more the expensive-per-call path pays on every single call. This is the money that slowly repays the upfront build.infra_gapcompares the two paths' ongoing monthly infra over the horizon — a credit (or debit) applied before we count calls.- The guard
if per_call_gap <= 0catches the case where the "cheaper" path isn't actually cheaper per call — then it never wins on cost and no volume changes that. calls_total = (ft_fixed_cost - infra_gap) / (per_call_gap * months)is the break-even: the monthly volume at which the per-call savings, over the horizon, finally cover the upfront build.
What the output means: small/tuned model wins above ~1,010,417 calls/month — below ~1M calls/month the big-model/RAG path is cheaper (no build cost); above it, the cheap-per-call path pays for itself. State the number, don't argue the preference.
Try this: Cut ft_fixed_cost to 5000 (a cheap adapter instead of a full build) and re-run. The break-even plummets — showing that the flip point is driven by the upfront cost you're trying to amortize, which is exactly the lever a lead negotiates down.
The defense: at low volume the frontier model is both cheaper (no build cost) and higher quality — start there, ship this week. The break-even (~1M calls/month here) is where the small-model investment pays for itself; below it, building your own is premature optimization. A lead states the number, not a preference, and revisits it as volume grows — the same discipline as PE4's API-vs-self-host break-even.
7 · Tech-lead — defending it in the room tech-lead
A lead's job in a design review is not to be right — it's to make the decision legible and falsifiable. Walk the frame out loud: "here are the two or three options I weighed; here are the criteria and how I weighted them for this context; the evidence points to X; we're giving up Y; and the signal that would flip me is Z." That last clause is what separates a defense from a rationalization — a decision you'd never reverse under any evidence was never reasoned, it was chosen.
| Dimension | Meets bar | Above bar |
|---|---|---|
| Options enumerated | Names 2+ real alternatives, not one and a strawman. | Enumerates the true option space and says why the losers lost. |
| Criteria explicit & weighted | Lists the criteria (cost/latency/quality/maintenance/risk). | Weights them for this context and justifies the weighting. |
| Decision follows from evidence | The pick is consistent with the criteria. | The pick is derived from the weighted scores — reproducible, not asserted. |
| Tradeoff acknowledged | States at least one thing given up. | Names the full cost of the choice honestly, without hedging it away. |
| Knows when it flips | Can say a condition that would change the decision. | Names a specific, measurable flip condition (a volume, a latency SLA, a scale). |
Score each dimension Meets or Above. All five at least Meets = a defense that survives a design review. Any dimension you can't hit means you have a preference, not a decision — go back to the frame in §1 and fill the gap before you commit.
Your teammate says "let's fine-tune a model on our docs so it just knows the answers." The docs change weekly. In one sentence, defend the counter-decision.
Show answer
A reviewer accepts your "use a fixed pipeline, not an agent" decision but asks: "when would you change your mind?" Why is having a crisp answer more important than the answer itself?
Show answer
Exercise AC5.1 — Defend a real fork
Context: A defensible decision beats a confident opinion. Running the §1 framework on a real fork — goal, options, weighted criteria, and a measurable flip condition — is what turns ‘I prefer’ into ‘we decided.’
Your task: Take a fork you're facing (or one of the five) and produce a one-paragraph defense using the framework, then grade it against the rubric.
Requirements:
- Write the goal + constraints and enumerate 2–3 real options
- Score the options on weighted criteria with
decide.py - Where cost is the axis, find the break-even with
flip.py - Write the defense: decision, evidence, what you give up, and the measurable condition that flips it
- Grade against the rubric — if any dimension isn't at least ‘Meets’, you have a preference, not a decision
💡 Hint: The measurable flip condition is the part that makes it a decision — if you can't name what would change your mind, you haven't decided yet.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: ‘I think we should use RAG’ is a preference; a decision is falsifiable. The difference is whether anyone can check the constraint and the reversal condition.
Your task: Rewrite ‘I think we should use RAG’ into a defensible decision statement and say what it must contain.
Requirements:
- State the four required parts: the choice, the driving constraint, the rejected alternative, and the reversal condition
- Anchor the choice on a binding constraint (e.g. weekly-changing knowledge that must cite sources)
- Explicitly reject the alternative on that constraint (fine-tuning can't update fast or cite)
- Make it falsifiable — anyone can check whether the constraint holds and whether the reversal condition has been met
💡 Hint: The reversal condition is what turns an opinion into a decision: name the measurable thing that would make you switch.
Show solution
A defensible decision names the choice, the driving constraint, the rejected alternative, and the reversal condition.
Rewrite: ‘We will use RAG rather than fine-tuning because our knowledge changes weekly and must cite sources (the binding constraint); fine-tuning cannot update fast enough or cite. We will revisit if freshness stops mattering and per-query latency becomes the dominant cost.’
‘I think’ is a preference; the rewrite is a decision because it is falsifiable — anyone can check whether the constraint holds and whether the reversal condition has been met.
Context: RAG vs fine-tuning is the most common architecture fork, and it's usually settled by one question about what you're actually changing — knowledge or behavior.
Your task: Give the tradeoff table that decides RAG vs fine-tuning and the one question that usually settles it.
Requirements:
- Compare on the load-bearing dimensions: knowledge freshness, citations/provenance, behavior/format/tone, and per-query cost/latency
- Show RAG wins on freshness and citations; fine-tune wins on behavior/format and per-query cost
- State the settling question: are we changing what the model knows or how it behaves?
- Note they compose — fine-tune for form, RAG for facts
💡 Hint: Map the axis to the choice: changing knowledge points to RAG, changing behavior points to fine-tune.
Show solution
| Dimension | RAG | Fine-tune |
|---|---|---|
| Knowledge freshness | Update the index instantly | Retrain to update |
| Citations / provenance | Natural (returns sources) | None — baked into weights |
| Behavior / format / tone | Weak lever | Strong lever |
| Per-query cost/latency | Higher (retrieval + long context) | Lower (no retrieval) |
The settling question: ‘Are we changing what the model knows or how it behaves?’ Knowledge that changes → RAG. Fixed behavior/format/style → fine-tune. They also compose: fine-tune for form, RAG for facts.
Context: Single agent vs fixed pipeline is a reliability-vs-flexibility fork. Most problems labelled ‘agent’ are really pipelines with one uncertain step, so you should be able to defend both directions.
Your task: Say when you replace a flexible single agent with a fixed pipeline of steps, and defend both directions.
Requirements:
- Argue the pipeline case: well-understood stable tasks — testable, cheaper, lower-latency, predictable, per-step evals
- Argue the agent case: open-ended or per-request-varying paths you can't pre-wire
- Name the tradeoff explicitly (flexibility for reliability/observability)
- State the default: start with a pipeline and introduce agentic freedom only at the steps that genuinely need it
💡 Hint: Favor the pipeline when the steps are known and ordered; reach for the agent only where the next step depends on intermediate findings.
Show solution
Pipeline wins when the task is well-understood and stable: each step is testable, cheaper, lower-latency, and predictable. You trade flexibility for reliability and observability — you know exactly where it failed.
Agent wins when the path is open-ended or varies per request: you cannot pre-wire every branch, so you pay for flexibility with unpredictability and harder debugging.
| Favor pipeline when | Favor agent when |
|---|---|
| Steps are known & ordered | Steps depend on intermediate findings |
| Reliability & cost matter most | Coverage of long-tail requests matters most |
| You need per-step evals | The task space is too large to enumerate |
Default: start with a pipeline; introduce agentic freedom only at the steps that genuinely need it. Most ‘agent’ problems are pipelines with one uncertain step.
Context: Defending a decision under a pointed performance attack is a real skill: you concede the true cost, then re-anchor on the dimension the decision actually rests on — without getting defensive.
Your task: Defend a RAG choice on the merits when a senior engineer says ‘your RAG adds 200 ms of retrieval latency for nothing — just fine-tune.’
Requirements:
- Concede the real cost honestly (retrieval adds latency, and name the mitigations: caching, streaming)
- Re-anchor on the binding constraint the decision rests on (freshness + citations), not the attacked dimension (latency)
- Show you considered the alternative by stating the reversal condition (you'd switch if freshness stopped mattering)
- Separate the tunable dimension they attacked from the binding one that drove the choice
💡 Hint: You win by making the binding constraint explicit, not by out-arguing them on latency.
Show solution
Acknowledge the true part, then re-anchor on the binding constraint:
- Concede the real cost: ‘You're right that retrieval adds latency — ~200 ms, and we can cut it with caching and streaming.’
- Re-anchor: ‘But the reason we're not fine-tuning is freshness and citations, not latency. Our KB changes weekly and legal requires source links. A fine-tune is stale the day after training and can't cite.’
- Show you considered it: ‘If freshness stopped mattering, I'd switch — that's the reversal condition I wrote down.’
The move: separate the dimension they attacked (latency, tunable) from the dimension the decision actually rests on (freshness, binding). You win by making the constraint explicit, not by defending latency.
Context: Sync vs batch is decided by one question — is a human blocked waiting? — but the answer that usually wins in production is a hybrid that gets batch economics and realtime freshness.
Your task: Choose sync per-request inference vs nightly batch for a scoring feature and give the hybrid that often wins.
Requirements:
- State the decision driver: does the consumer need the answer at request time, and how fresh must it be?
- Contrast the two (sync: user waits, seconds-fresh, per-request cost; batch: consumed later, hours-fresh fine, amortized cost)
- Describe the winning hybrid: pre-compute the predictable bulk in batch, serve from cache, fall back to sync only on cache-miss
- Name the cost of the hybrid (two code paths) so the tradeoff is honest
💡 Hint: Pre-compute the 90% you can predict overnight and reserve sync inference for the new or changed entities.
Show solution
Decision driver: does the consumer need the answer at request time, and how fresh must it be?
| Sync/realtime | Batch |
|---|---|
| User waits on the result now | Result consumed later / in bulk |
| Freshness = seconds | Freshness = hours/day is fine |
| Pay per-request cost & latency | Amortize cost, batch discounts, no latency SLA |
The hybrid that usually wins: pre-compute in batch for the predictable bulk (all known users overnight), serve those instantly from a cache, and fall back to sync inference only for cache-misses (new/changed entities). You get batch economics for 90% of traffic and realtime freshness where it matters — the best of both, at the cost of two code paths.
Context: ‘Just use the biggest model’ is the safe-sounding default; defending a smaller model plus scaffolding requires a decision doc that proposes the experiment which settles it, not an argument.
Your task: Defend a small-model-plus-scaffolding design against ‘just use the biggest model’ with a decision doc: claim, evidence, tradeoffs, and how you'd prove it.
Requirements:
- State the claim: a small/mid model wrapped in retrieval + validation + a router that escalates only hard queries beats big-model-everywhere on cost and reliability at equal quality
- Give a comparison across cost/request, latency, factuality, failure modes, and eng complexity
- Concede the real cost (more moving parts to maintain) rather than hiding it
- Propose to prove it: build both, run the golden eval, commit only if quality matches at materially lower cost
- Set a reversal condition (if the big model's price drops below the scaffolding's blended cost, collapse to the simple design)
💡 Hint: Defend a tradeoff by proposing the experiment that settles it — ask for the week to run the eval, and let the numbers, not the debate, decide.
Show solution
Claim: a small/mid model wrapped in scaffolding (retrieval for facts, structured-output validation, a router that escalates only hard queries to the big model) beats ‘big model everywhere’ on cost and reliability at equal quality for our task.
| Dimension | Big model everywhere | Small + scaffolding |
|---|---|---|
| Cost / request | High, on every call | Low; big model only on escalation |
| Latency | Higher | Lower for the common path |
| Factuality | Priors, can hallucinate | Grounded in retrieval + validated schema |
| Failure modes | Opaque | Localized to a component you can test |
| Eng complexity | Low | Higher (more moving parts) |
How to prove it (not argue it): build both, run the golden eval, and compare quality, cost/request, and p95 latency. Commit to the scaffolding only if it matches the big model's quality at materially lower cost. Concede complexity is a real cost — scaffolding is more to maintain — and set the reversal condition: if the big model's price drops below the scaffolding's blended cost, collapse to the simple design.
Defense in the room: ‘Biggest model is the safe default and I'm not against it — I'm asking for one week to run the eval. If scaffolding doesn't win on the numbers, we ship the big model and I'll have proven it.’ You defend a tradeoff by proposing the experiment that settles it.
✓ Checkpoint — you can move on when you can…
- Run the six-step framework: goal+constraints → options → criteria → weights → decide → name the tradeoff.
- Defend RAG-by-default and say the four conditions under which fine-tune wins.
- Argue buy-the-undifferentiated-component, and when the answer flips to build.
- Justify pipeline-over-agent on determinism/debuggability, and when to reach for an agent.
- Route sync vs batch from "is a human blocked?" and compute a small-vs-big break-even.
- Grade a defense on all five rubric dimensions — including a specific, measurable flip condition.