The Problem
Agent prompt strings concatenated inline — "You are a " + role + " assistant. The user asked: " + query — are impossible to review, test independently, or version. A one-character change to a prompt buried in a 500-line agent file can silently break behavior. As prompts grow to hundreds of tokens with multiple sections, inline construction becomes unmanageable.
The Solution
Store each prompt section as a named PromptTemplate with a version string and a text/template body. A Registry compiles templates at registration time — syntax errors surface immediately rather than at runtime. Registry.Render(name, vars) performs substitution with missingkey=error, so a forgotten variable is a compile-time-equivalent panic, not a silent blank. A PromptBuilder assembles multi-section prompts by rendering and concatenating named templates in order.
Structure
Registration and Validation
Templates are registered at package init or server startup via MustRegister(). text/template.Parse() runs immediately — a malformed template panics early rather than failing during a live LLM call.
flowchart TD Registry["Registry MustRegister() / Render()"] System["system template v1.0.0"] UserTurn["user_turn template v1.0.0"] ToolResult["tool_result template v1.0.0"] Builder["PromptBuilder Build()"] Final["Final Prompt String"] LLM["LLM"] System -.->|"registered"| Registry UserTurn -.->|"registered"| Registry ToolResult -.->|"registered"| Registry Builder -->|"Render(system, vars)"| Registry Builder -->|"Render(user_turn, vars)"| Registry Builder -->|"Render(tool_result, vars)"| Registry Registry --> Final Final --> LLM
Implementation
package main
// PromptTemplate holds a named, versioned text/template for prompt construction.
type PromptTemplate struct {
Name string
Version string
Raw string
}
// RenderedPrompt is the final prompt string after variable substitution.
type RenderedPrompt struct {
TemplateName string
Content string
}
// TemplateVars is a free-form map of variable substitutions.
type TemplateVars map[string]any Real-World Analogy
Legal document templates: a law firm maintains versioned contract templates with placeholder clauses. The paralegal fills in the blanks (client name, date, jurisdiction) for each engagement. The underlying template is reviewed, approved, and versioned independently from the data that populates it. Changing the indemnification clause means updating one template file, not hunting through every engagement letter.
Pros and Cons
| Pros | Cons |
|---|---|
| Prompt text is reviewable, diffable, and versionable as plain files | text/template syntax is unfamiliar to non-Go contributors |
missingkey=error surfaces missing vars at render time, not silently | Template logic (conditionals, ranges) can grow complex and hard to test |
| Registry validates syntax at startup, not during a live LLM call | Each new section type requires a new template registration |
PromptBuilder decouples assembly order from template content | Templates are strings — IDE support for embedded syntax is limited |
Best Practices
- Version every template (
Version: "1.0.0") and log the version alongside every LLM call — prompt regressions are easier to diagnose when you know which version was active. - Keep templates as logic-free as possible — use Go code for conditionals, not
{{if}}blocks inside templates. - Write unit tests that call
Registry.Render()with known vars and assert the output string — prompts are testable code. - Store template raw strings as
.tmplfiles in anembed.FSso they can be reviewed in pull requests without touching Go source. - Use
TemplateVars(amap[string]anyalias) consistently — resist the temptation to pass typed structs, which break the uniform Render signature.
When to Use
- Any agent with more than one prompt section or more than two variable substitutions.
- Teams where non-engineers need to review or edit prompt content.
- Systems that run A/B experiments on prompt wording.
When NOT to Use
- Trivial single-turn agents with a one-sentence system prompt — a string constant is fine.
- Prompts with no variable substitution at all — just use a
const.