{"slug": "i-removed-the-llm-call-and-replaced-it-with-200-lines-of-template-code", "title": "I removed the LLM call and replaced it with 200 lines of template code", "summary": "A developer replaced an LLM call in a letter generator with 200 lines of deterministic template code, citing fixed output shape, legal-adjacent risk, and zero marginal cost as key reasons. The pure function approach eliminates hallucinations, enables instant offline rendering, and allows the tool to remain free without signup.", "body_md": "The feature was a letter generator. Somebody fills in a few fields and gets a finished\n\nletter of recommendation, resignation letter or notice letter, in plain text, ready to\n\npaste into an email.\n\nThe obvious build is a prompt and a model call. I wrote the deterministic version instead:\n\na pure function, about two hundred lines, no network, no key, no tokens. I want to lay out\n\nthe reasoning, because \"just call a model\" is the default now and the default is not always\n\nright.\n\n**1. The output is short and the shape is fixed.**\n\nA recommendation letter is a date block, a greeting, three or four paragraphs, a sign off\n\nand a name. There is no structural variation to discover. Generation is valuable when the\n\nspace of good outputs is large and you cannot enumerate it. Here the space is small enough\n\nto write down, and once you have written it down the model is doing an expensive\n\napproximation of a `switch`\n\nstatement.\n\n**2. It is a legal-adjacent document.**\n\nNot legal advice, but it goes into an employment record. A resignation letter that invents\n\na notice period, or a reference that invents a fact about a person, is a real problem for\n\nthe person who sent it. Templates cannot hallucinate. Everything specific in the output\n\neither came from a form field or is a sentence I wrote and can be held to.\n\n**3. Zero marginal cost changes what the product can be.**\n\nThis is the one that actually decided it. A model call costs money per use, and anything\n\nthat costs money per use needs an account, a rate limit and eventually a card. A pure\n\nfunction costs nothing, so the tool can stay open with no signup, forever, without a\n\nbusiness case. That is a product decision expressed as an architecture decision, and it\n\nonly works if the code path is free.\n\nThe whole engine is one exported function over one input type.\n\n```\nexport type LetterKind = 'resignation' | 'notice' | 'recommendation';\nexport type LetterTone = 'formal' | 'warm' | 'brief';\n\nexport function generateLetter(input: LetterInput): string\n```\n\nTone is not a prompt instruction, it is a dimension of the data. Two tiny functions carry\n\nmost of it:\n\n``` js\nfunction greeting(input: LetterInput, tone: LetterTone): string {\n  const name = input.recipientName.trim();\n  if (!name) return tone === 'warm' ? 'Hello,' : 'Dear Sir or Madam,';\n  if (tone === 'warm') return `Hi ${name},`;\n  return `Dear ${name},`;\n}\n\nfunction signOff(tone: LetterTone): string {\n  if (tone === 'warm') return 'With thanks,';\n  if (tone === 'brief') return 'Regards,';\n  return 'Sincerely,';\n}\n```\n\nThe bodies are arrays of paragraphs, assembled conditionally. A recommendation body opens\n\ndifferently depending on whether the writer told us how they know the subject:\n\n```\nparas.push(\n  rel\n    ? `I am pleased to recommend ${who} for the role of ${input.role}. ${rel}, which gave me a direct view of how they work.`\n    : `I am pleased to recommend ${who} for the role of ${input.role}, based on my direct experience of working with them at ${input.company}.`,\n);\n```\n\nThat ternary is the whole trick, repeated maybe fifteen times. It is not clever. Clever was\n\nnever the requirement.\n\n**Determinism, which means testability.** Same input, same bytes out. A snapshot test over\n\nthe full cross product of three kinds and three tones is nine assertions and runs in\n\nmilliseconds. Testing a model call means either mocking it, in which case you are testing\n\nyour mock, or asserting fuzzy properties of real output and paying for the privilege on\n\nevery CI run.\n\n**Offline and instant.** No spinner, no failure state, no retry logic, no timeout, no\n\n\"the service is busy\" copy to write and translate. The letter updates as the user types\n\nbecause rendering it is a function call.\n\n**A real validator instead of an implicit one.** With a model you tend to send whatever you\n\nhave and hope. With a template you have to decide what is required, which forces the\n\nproduct question into the open:\n\n``` js\nexport function missingFields(input: LetterInput): string[] {\n  const missing: string[] = [];\n  if (!input.senderName.trim()) missing.push('Your name');\n  if (!input.company.trim()) missing.push('Company');\n  if (input.kind === 'recommendation') {\n    if (!input.subjectName.trim()) missing.push('Who you are recommending');\n    if (!input.role.trim()) missing.push('Their role');\n  } else {\n    if (!input.role.trim()) missing.push('Your role');\n    if (!input.lastDay.trim()) missing.push('Last working day');\n  }\n  return missing;\n}\n```\n\nNote that the required set differs by kind. A recommendation has no last working day. A\n\nresignation has no subject. A single prompt would have blurred those together and produced\n\nsomething plausible for a missing field, which is worse than refusing.\n\n**Localisation is mechanical.** Nine strings per tone, translated once, correct forever. The\n\nsame feature backed by a model needs the prompt tuned per language and the output checked\n\nper language by someone who reads it.\n\nI am not going to pretend this scales to everything.\n\n**It cannot say the specific thing.** The generated paragraphs are competent and generic,\n\nand generic is exactly the part of a reference letter that carries no weight. A hiring\n\nmanager skims \"demonstrated consistent judgement\" and stops at \"she rewrote our billing\n\nreconciliation and the month-end close went from four days to one\".\n\nSo the engine has a `highlight`\n\nfield, free text, dropped verbatim into the middle of the\n\nletter. That is not a limitation I worked around, it is the correct division of labour. The\n\ntool writes the scaffolding nobody reads. The human writes the one sentence that does the\n\nwork. A model would have written a fluent guess at that sentence, and a fluent guess about\n\na real person is precisely the thing you do not want in a reference.\n\n**It cannot rewrite arbitrary prose.** Paste in three rambling paragraphs and ask for them\n\ntightened, and templates have nothing to offer. That is a genuine generation task and I\n\nwould use a model for it.\n\n**Adding a kind costs a function.** A new letter type means new code, not a new prompt\n\nstring. For three kinds that is fine. For thirty I would be rethinking it.\n\nReach for generation when the output space is large, variable, and you cannot enumerate the\n\ngood answers. Reach for templates when the output space is small, the shape is fixed, and\n\nbeing wrong is expensive.\n\nShort formal documents sit squarely in the second category, and the industry keeps building\n\nthem with the first tool because the first tool is what everyone is holding.\n\nThe engine described here runs the\n\n[letter of recommendation template](https://cvbooster.ai/recommendation-letter-generator)\n\ntool on the resume builder I maintain. No account, no card, no model call, and the plain\n\ntext output is free to copy. It renders in whatever time a string concatenation takes,\n\nwhich is the entire point.", "url": "https://wpnews.pro/news/i-removed-the-llm-call-and-replaced-it-with-200-lines-of-template-code", "canonical_source": "https://dev.to/thedolceway/i-removed-the-llm-call-and-replaced-it-with-200-lines-of-template-code-2lh0", "published_at": "2026-08-25 12:11:32+00:00", "updated_at": "2026-08-25 12:44:19.257807+00:00", "lang": "en", "topics": ["developer-tools"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/i-removed-the-llm-call-and-replaced-it-with-200-lines-of-template-code", "markdown": "https://wpnews.pro/news/i-removed-the-llm-call-and-replaced-it-with-200-lines-of-template-code.md", "text": "https://wpnews.pro/news/i-removed-the-llm-call-and-replaced-it-with-200-lines-of-template-code.txt", "jsonld": "https://wpnews.pro/news/i-removed-the-llm-call-and-replaced-it-with-200-lines-of-template-code.jsonld"}}