Scattered Prompt Strings is an AI code smell, a sign of trouble in LLM systems, in the Change preventers group. Prompt fragments assembled with string concatenation across many files.
An automated SDR writes the first message to leads. The final prompt is assembled by concatenation across four files (anonymized):
# persona.pyBASE = "You are Julia, a consultant at the Company. "# tone.pydef tone(segment): return "Be formal. " if segment == "manufacturing" else "Be casual! "# campaigns/q3.pyCAMPAIGN = "Mention the launch discount. "# writer.pyprompt = BASE + tone(lead.segment) + CAMPAIGN + f"Lead: {lead.name}, {lead.company}. " \ + ("Do not mention price. " if lead.is_enterprise else "")
An enterprise retail lead assembles this at run time, and nobody reviewed this text:
You are Julia, a consultant at the Company. Be casual! Mention thelaunch discount. Lead: Ana, North Store. Do not mention price.
“Mention the discount” and “do not mention price” in the same paragraph. The outbound message oscillates: one day it cites a 12% discount, the next it says sales will follow up. Ordinary retail leads only get the casual tone and the discount. Nobody can see the prompt that was actually sent without running the code with that lead.
Signs of Scattered Prompt Strings:
The prompt is assembled with +, f-strings, or join across several files.
Pieces of the prompt depend on conditions spread through the code.
There is no place where you can read the complete prompt.
Combinations of conditions produce contradictory instructions.
Tone or content changes depending on which code path assembled the message.
flowchart LR
A["persona.py"]:::tool --> J["1. Concatenation"]:::tool
B["tone.py"]:::tool --> J
C["campaigns/q3.py"]:::tool --> J
D["writer.py with ifs"]:::tool --> J
J --> M(["2. A prompt nobody has read in full"]):::llm
M --> OUT(["3. Message with varying tone"]):::io
Text fragments come from several files and conditions.
The final prompt only exists at run time.
Output varies by combination, sometimes with contradictory instructions.
One or two declared variables in a single file are not this smell. The smell starts when the full text does not fit in your head without executing.
The fastest way to confirm: log the assembled prompt on every request, for a day. If you cannot point at one file and say “this is what the model saw”, the text is scattered. On Julia’s SDR, Ana’s log would be the first time anyone read the two opposite sentences on the same screen.
Reasons
Reusing fragments looks like good design. The persona is the same in several places, so it becomes a constant. The constant grows an if, the if grows a file.
Each campaign adds a fragment. The code grows by addition, like the prompt. Q3 lands in campaigns/q3.py with nobody opening the final text.
The prompt treated as a plain string. There is no template tool, so people use whatever the language offers: + and f-strings.
Condition in code, instruction in text.if lead.is_enterprise mixes control flow with prose. Combinations grow as a product, not a sum.
Cost of ignoring
Unpredictable behavior. Rare combinations produce contradictory prompts. The SDR promises a discount and refuses price in the same sentence.
Impossible to review. Nobody approves a prompt that only exists once assembled. The tone.py PR looks harmless and changes every segment.
Impossible to version. A change to tone.py alters every prompt that uses it, with no record of which combination went out.
Slow debugging. To understand an answer, you have to rebuild that case’s prompt, with that lead, in that campaign.
In Brazilian bank support on WhatsApp, the same pattern shows up: one file for “formal” tone, another for the Pix product, another for compliance “do not promise a deadline”. The enterprise Pix customer gets three policies at once, and one of them contradicts another.
Always. The fragments become one template, with a name, a version, and declared variables. Conditions become variables or separate templates. Target: Prompt Template Registry.
The template declares what may vary. Anything not on the list does not enter by concatenation:
# sdr.first_message@3You are {{persona_name}}, a consultant at {{company}}.Tone: {{tone}}.Campaign: {{campaign}}.Lead: {{lead_name}}, {{lead_company}}.Price: {{price_policy}}.
Two incompatible campaigns become two templates (sdr.retail@3 and sdr.enterprise@3), not an if that glues opposite sentences. The trace stores name and version. At review time, a person reads the whole file.
A test the concatenator never had: render the template with an enterprise retail lead and paste the text on the PR. If review cannot say “yes, this is what the model sees”, some fragment is still missing.
Payoff
The complete prompt readable in one place.
Every version recorded, with who changed it and why.
Declared variables, with no hidden combinations.
The trace stores the template name and version for each message.
When to ignore
One or two simple variables. A prompt with the customer’s name inserted is not scattered.
A one-file prototype. If the whole prompt is in one place, even if assembled with an f-string, the problem is smaller.
Is it this smell?
Quick answers
What is Scattered Prompt Strings?
Scattered Prompt Strings is an AI code smell, a sign of trouble in LLM systems, in the Change preventers group. Prompt fragments assembled with string concatenation across many files.
How do you fix Scattered Prompt Strings?
With Extract Versioned Template. Each refactoring has safe step-by-step mechanics with an eval before and after.
Where does Scattered Prompt Strings show up in practice?