Constructive Prompt Injection
LLMs are not universal functions. They are token predictors shaped by RLHF. This limits what they should do, not what they can do. An LLM can count the r's in "strawberry" — sometimes. Code counts them every time. The question is never "can the model handle this?" It's "is the model the cheapest substrate for this task?"
Flow control, routing, state management, counting, branching — these are all tasks where deterministic code is cheaper, faster, and more reliable. The model's substrate advantage is language: understanding intent, generating natural text, reasoning about ambiguous meaning, synthesizing across contexts. Using an LLM for flow control is using a paintbrush to hammer nails. It'll leave a mark, but not the one you wanted.
The leak
A shopping assistant. The user has items in their cart. The first attempt puts everything in the prompt:
You are a shopping assistant. The user's cart contains: {items}.
Their account type is: {account_type}.
If the cart total > $100 and account_type is "premium", offer free shipping.
If the cart total > $100 and account_type is "standard", suggest upgrading.
If the cart total < $100, suggest items to reach the free shipping threshold.
Respond helpfully to: "{user_message}"
The model can handle this. But should it? The orchestrator already knows the cart total and the account type. The branching is deterministic. Asking the model to evaluate if total > $100 and type == "premium" is misplaced.
Constructive prompt injection
Pull the branch into the orchestrator. The model gets a different prompt depending on the state — it never sees the condition:
def build_cart_prompt(cart, account_type, user_message):
total = sum(item.price for item in cart.items)
if total > 100 and account_type == "premium":
context = "This order qualifies for free shipping."
elif total > 100:
context = "Mention the premium plan includes free shipping."
else:
gap = 100 - total
context = f"${gap:.0f} more qualifies for free shipping."
return f"""
You are a shopping assistant.
Cart: {format_items(cart.items)}
{context}
Customer says: "{user_message}"
"""
The before prompt had three conditionals the model had to parse and branch on. The after prompt has none — the orchestrator already resolved the state into a single line of context. The model's job is now purely linguistic: respond naturally given the situation it's been told about.
This is constructive prompt injection. Security researchers use the term to mean an attacker hijacking model behavior by injecting instructions. The orchestration pattern is the same mechanism with opposite intent: the orchestrator injects resolved state, constraints, and scoped context to steer the model toward predictable outcomes.
Adversarial prompt injection subverts alignment. Orchestrated prompt injection is alignment.
Every build() function in a well-structured agent system is an injection point — a place where the orchestration layer composes runtime state into instructions that constrain the model's output space. The tighter the injection, the steeper the gradient, the more reliable the output. What "prompt engineering" actually is: not crafting the perfect static instruction, but building systems that compose the right prompt dynamically.
The bootstrapping problem
There's an irony worth noting: if you use an LLM to write your prompts today, it tends to produce the "before" version. The model writes prompts the way it writes code — with conditionals, branches, "if this then that." It doesn't distinguish between logic it should execute and logic the orchestrator should handle before the prompt is composed.
This may improve as models get better at reasoning about their own substrate. But for now, the common workflow — ask the model to generate a prompt, then use that prompt — has a bias toward leaking orchestration logic into the token prediction layer. The branching prompt works. It just works unreliably and expensively. Knowing to pull the branches out is, for the moment, a human judgment call.
The consistent finding from practice: you get better results by reducing what you ask the model to do, not expanding it. If it needs to pick among 15 tools, use an orchestrator heuristic to narrow to 3. The gradient is steeper. If it needs to branch, branch in code and give the model whichever branch won. It's the write-time intelligence principle applied to every invocation: do the structural work where it's cheap and verifiable, then hand the model a single, focused task.
The composition discipline
The practical discipline: every prompt in a production system should be dynamically composed, not statically written. A build() function takes structured state and returns a prompt. The orchestrator calls the right build() function for the current phase, with the current context. The model receives a prompt that's been scoped, narrowed, and loaded with exactly the context it needs.
state machine → selects phase → selects prompt builder →
injects state → composes prompt → model generates →
structured output → state machine advances
The model never sees the state machine. It never picks the phase. It never decides what context is relevant. All of that is deterministic. The model does the part that requires language — and only that part.
This is how you get small models to perform like large ones, unreliable calls to become reliable, and expensive inference to become cheap. Not by making the model smarter. By making the prompt steeper.
These rules — resolve known values in code, branch before the prompt, narrow option sets, compose context from state — are themselves injectable. A meta-prompt that enforces them would produce "after" versions instead of "before" versions. The article is its own first application.
