Free lesson · GenAI Application Engineering
Manage prompt template versions with Langfuse
Build a PromptVersionManager interfacing with Langfuse's prompt management API for template storage and versioning. Implement register_prompt() creating a template in Langfuse with name, template string with {{variable}} placeholders, and metadata. Create get_prompt(name, version=None) fetching a specific or latest production version, returning a PromptTemplate Pydantic model with template, version, variables, and created_at. Build configure_ab_test() setting up traffic splitting between two versions with configurable weights (80/20) and rollout_id. Implement select_variant() using deterministic hashing on user_id + rollout_id for consistent assignment. Create track_variant_performance() tagging Langfuse traces with variant_id. Build promote_winner() setting the winner as production. Implement FastAPI dependency get_active_prompt() injecting the correct version into handlers.
Course: Full-Stack GenAI Applications · Chapter 16 · Observability with Langfuse & OpenTelemetry
Free to read — no subscription required.
Introduction
When you change a system prompt in production, a single-word edit can shift answer quality by double-digit percentages — and without versioning you cannot rollback a regression, attribute a latency spike to the prompt vs. the model, or run a trustworthy A/B test. Teams that ship prompts as plain strings in application code end up unable to tell which wording produced last week's quality dip. By the end of this lesson you'll be able to register prompt templates in Langfuse, fetch specific versions at request time, route traffic between versions with weighted rollouts, and tag every trace with the exact prompt version that produced it so dashboards can compare variants.
Key Terminology
- Prompt template: a named, server-stored string with double-brace
variableplaceholders that Langfuse versions and your application fetches at request time instead of hard-coding. - Version label: a movable tag (e.g.
"production","staging") that points at a specific integer version of a prompt; promoting a candidate means moving the label, not redeploying code. - Weighted rollout: a traffic-splitting policy that routes a configured fraction of requests to each prompt version so latency, cost, and feedback can be compared across variants.
Concepts
Why Prompt Versioning Matters at Scale
In production GenAI systems, a single-word change in a system prompt can shift answer accuracy by double-digit percentages. Without versioning, you face three concrete problems. First, rollback becomes impossible—if a new prompt degrades quality, you cannot revert to the prior wording because nobody recorded it. Second, performance attribution breaks—when latency spikes or cost increases, you cannot determine whether the prompt change or a model update caused the regression. Third, A/B testing requires manual plumbing—engineers build ad-hoc randomization logic, forget to log which variant ran, and end up with unanalyzable experiment data.
Langfuse addresses all three by storing every prompt version server-side, exposing a fetch API that returns the template text along with its version number, and accepting that version number as metadata on generation spans. The architecture separates prompt storage from application deployment: you update prompts through the Langfuse UI or API, and your running application picks up the new version on the next request without a redeploy.
Building Version-Segmented Dashboards
Once traces carry version metadata, the Langfuse dashboard becomes a powerful experiment analysis tool. Navigate to the Traces view and add a filter on metadata.prompt_version to isolate traffic for each variant. Key metrics to compare include median latency (version 2's "detailed reasoning" instruction typically increases completion time by 15-30%), mean token usage (longer instructions and outputs increase cost), and user feedback scores if you have implemented the feedback correlation pipeline from another goal of this chapter. A statistically significant difference in feedback scores across 500+ traces per variant provides strong evidence for promoting or retiring a prompt version.
To automate this analysis, query the Langfuse API's /api/public/traces endpoint with filters on the prompt name and version, aggregate token counts and scores in a Pandas DataFrame, and run a two-sample t-test. When the candidate version shows improvement at p < 0.05, update its labels to include "production" and remove the label from the old version—Langfuse ensures only one version per prompt carries a given label. Your application's next get_prompt call with label="production" will automatically pick up the promoted version, completing the rollout cycle without touching application code.
Code Walkthrough
Now that you understand why versioning matters and how dashboards segment by version, let's wire those concepts into runnable code — a manager class that fetches versioned templates and a tracing hook that tags every generation with its prompt version.
Architecture of the Prompt Version Manager
The following diagram illustrates how the PromptVersionManager sits between your application's inference pipeline and the Langfuse backend. When a request arrives, the manager selects a prompt version based on rollout weights, fetches the template from Langfuse, renders it with request-specific variables, and tags the resulting trace with version metadata for downstream analytics.
Code snippet mermaid
Loading diagram...
- Line 1: Declares a Mermaid flowchart with top-down (TD) layout direction.
- Line 2: Defines the entry node A labeled "Incoming Request" and connects it via an arrow to node B labeled "PromptVersionManager".
- Line 3: Connects B to a decision diamond
Clabeled "A/B Rollout Selection", indicating a branching point based on traffic splitting. - Lines 4-5: Define the two weighted branches from the A/B decision: 70% of traffic routes to node
D(the stable production prompt v3) and 30% routes to nodeE(the candidate prompt v4 being tested). - Lines 6-7: Merge both branches by connecting nodes
DandEinto a single nodeFlabeled "Fetch Template from Langfuse API", meaning regardless of which version was selected, the prompt template is retrieved from Langfuse. - Line 8: Connects F to node G labeled "Render Template with Variables", where the fetched prompt template is hydrated with runtime variable values.
- Line 9: Connects G to node H labeled "LiteLLM Completion Call", where the rendered prompt is sent to an LLM via the LiteLLM abstraction layer.
- Line 10: Connects H to node I labeled "Langfuse Trace with version metadata", recording the LLM call as an observability trace in Langfuse that includes which prompt version was used.
- Line 11: Connects I to the terminal node J labeled "Quality Dashboard - Segmented by Version", where traced results are visualized on a dashboard that segments quality metrics by prompt version for A/B comparison analysis.
This flow ensures that every LLM call carries the version identifier through the entire observability pipeline. The Langfuse dashboard can then filter traces by prompt_version to compare latency distributions, token counts, cost, and user feedback scores across variants.
Building the PromptVersionManager Class
The first building block is a class that wraps the Langfuse Python SDK to provide registration, fetch, and weighted rollout in one place. register_prompt creates a new version of a named template and optionally tags it with labels like "production" or "staging"; Langfuse auto-increments the integer version per name. get_prompt fetches a specific version (by integer) or whichever version currently carries a given label — passing None for both signals the caller wants whatever currently carries the production label. select_version_ab performs cumulative-weight selection between configured ABRolloutConfig variants, fetches the chosen version, renders the template with request variables, and returns a metadata dictionary suitable for attaching to a Langfuse trace. It validates that weights sum to 1.0 within a floating-point tolerance, raising ValueError if misconfigured — a silent traffic misallocation (e.g., weights summing to 0.8) would leave 20% of requests falling through to the fallback, invalidating experiment results.
Code snippet python
1import random 2from dataclasses import dataclass 3from typing import Optional 4 5from langfuse import Langfuse 6 7@dataclass 8class ABRolloutConfig: 9 version: int 10 weight: float # 0.0 to 1.0 11 12class PromptVersionManager: 13 """Manages prompt template versions via Langfuse API.""" 14 15 def __init__(self, public_key: str, secret_key: str, host: str): 16 self.client = Langfuse( 17 public_key=public_key, 18 secret_key=secret_key, 19 host=host, 20 ) 21 22 def register_prompt( 23 self, 24 name: str, 25 template: str, 26 labels: Optional[list[str]] = None, 27 config: Optional[dict] = None, 28 ) -> int: 29 """Create a new prompt version; returns the version number.""" 30 prompt = self.client.create_prompt( 31 name=name, 32 prompt=template, 33 labels=labels or [], 34 config=config or {}, 35 type="text", 36 ) 37 return prompt.version 38 39 def get_prompt( 40 self, 41 name: str, 42 version: Optional[int] = None, 43 label: Optional[str] = None, 44 ) -> tuple[str, int]: 45 """Fetch prompt template and version number.""" 46 kwargs = {"name": name} 47 if version is not None: 48 kwargs["version"] = version 49 elif label is not None: 50 kwargs["label"] = label 51 prompt = self.client.get_prompt(**kwargs) 52 return prompt.prompt, prompt.version 53 54 def select_version_ab( 55 self, 56 name: str, 57 variants: list[ABRolloutConfig], 58 request_variables: dict, 59 ) -> tuple[str, dict]: 60 """Select a prompt version by A/B weight and render it.""" 61 total = sum(v.weight for v in variants) 62 if abs(total - 1.0) > 1e-6: 63 raise ValueError( 64 f"Weights must sum to 1.0, got {total:.4f}" 65 ) 66 67 roll = random.random() 68 cumulative = 0.0 69 selected_version = variants[-1].version 70 71 for variant in variants: 72 cumulative += variant.weight 73 if roll < cumulative: 74 selected_version = variant.version 75 break 76 77 template, version = self.get_prompt( 78 name=name, version=selected_version 79 ) 80 81 for var_name, value in request_variables.items(): 82 placeholder = "{" + "{" + var_name + "}" + "}" 83 template = template.replace(placeholder, str(value)) 84 85 metadata = { 86 "prompt_name": name, 87 "prompt_version": version, 88 "ab_roll": round(roll, 6), 89 "ab_variants": {v.version: v.weight for v in variants}, 90 } 91 return template, metadata
- Lines 1-5: Import
randomfor probabilistic selection,dataclassfor the lightweight container,Optionalfor parameter annotations, and theLangfuseSDK client. - Lines 8-11: Define
ABRolloutConfigwith two fields:version(integer prompt version in Langfuse) andweight(a float controlling traffic proportion). - Lines 14-22: The
__init__instantiates aLangfuseclient using the public key, secret key, and host URL — these three values uniquely identify your Langfuse project. - Lines 24-38: The
register_promptmethod callsself.client.create_promptwith the template string, an optional list of labels (e.g.,["production"]), and an optional config dictionary for model parameters. The method returns the integer version number assigned by Langfuse, which auto-increments with each call for the same prompt name. - Lines 40-53: The
get_promptmethod builds a keyword arguments dictionary dynamically. Whenversionis not None, it fetches that exact version. Whenlabelis not None, it fetches the version carrying that label. If both are None, Langfuse returns the latest production-labeled version by default. The method returns a tuple of the template string and version number — both needed for downstream trace tagging. - Lines 55-69: The
select_version_abmethod first validates that all weights sum to 1.0 within a tolerance of1e-6. If the check fails, it raises a ValueError with the actual total — preventing silent traffic misallocation that would invalidate experiment results. - Lines 71-78: A single call to
random.random()generates a uniform float in[0.0, 1.0). The loop accumulates weights until the cumulative sum exceeds the roll value, selecting the corresponding version. If no variant triggers the break (a floating-point edge case), the last variant serves as fallback. - Lines 80-86: The selected version number is passed to
get_prompt, which fetches the exact template text from Langfuse. A simple loop then substitutes each Mustache-style double-brace placeholder in the template with the corresponding request value, converting each to a string viastr(). - Lines 88-93: The metadata dictionary captures everything needed for trace correlation: prompt name, selected version, the raw roll value (useful for debugging selection logic), and the full variant configuration. This dictionary gets passed directly to Langfuse's trace or generation span.
Correlating Traces with Prompt Versions
The final piece connects the A/B selection output to Langfuse's tracing infrastructure. Every LLM call must carry the prompt version metadata so that dashboards can segment metrics by variant. The following snippet demonstrates end-to-end usage: it initializes the manager, registers two prompt versions, configures a 70/30 rollout, executes a LiteLLM completion call inside a Langfuse @observe() decorated function, and passes the version metadata as generation-level attributes. The @observe() decorator from the langfuse.decorators module automatically creates a trace for the function invocation, and calling langfuse_context.update_current_observation within the function attaches the prompt version metadata to the active span, making it queryable in the Langfuse UI under the generation's metadata tab.
Code snippet python
1from langfuse.decorators import observe, langfuse_context 2import litellm 3 4manager = PromptVersionManager( 5 public_key="pk-lf-...", 6 secret_key="sk-lf-...", 7 host="https://cloud.langfuse.com", 8) 9 10v1 = manager.register_prompt( 11 name="rag_answer", 12 template="Context: {{context}}\nQuestion: {{question}}\nAnswer concisely.", 13 labels=["production"], 14) 15v2 = manager.register_prompt( 16 name="rag_answer", 17 template="Context: {{context}}\nQuestion: {{question}}\nProvide a detailed answer with reasoning.", 18) 19 20rollout = [ 21 ABRolloutConfig(version=v1, weight=0.7), 22 ABRolloutConfig(version=v2, weight=0.3), 23] 24 25@observe(name="rag_pipeline") 26def answer_question(context: str, question: str) -> str: 27 rendered, meta = manager.select_version_ab( 28 name="rag_answer", 29 variants=rollout, 30 request_variables={"context": context, "question": question}, 31 ) 32 33 langfuse_context.update_current_observation( 34 metadata=meta, 35 ) 36 37 response = litellm.completion( 38 model="gpt-4o-mini", 39 messages=[{"role": "user", "content": rendered}], 40 metadata={"prompt_version": meta["prompt_version"]}, 41 ) 42 return response.choices[0].message.content
- Lines 1-2: Import the
@observedecorator andlangfuse_contextfor trace manipulation, pluslitellmfor the model call. - Lines 5-9: Instantiate the
PromptVersionManagerwith Langfuse project credentials. In production, these values come from environment variables, never hardcoded. - Lines 11-19: Register two versions of the
"rag_answer"prompt. Versionv1receives the"production"label and uses concise instruction phrasing. Versionv2omits the label (it becomes a candidate) and instructs the model to provide detailed reasoning. Eachregister_promptcall returns the auto-assigned version integer. - Lines 21-24: Build the rollout configuration: 70% of traffic uses
v1(production), 30% usesv2(candidate). Adjusting these weights requires only changing the float values—no code redeployment needed if you externalize the config to a YAML file or environment variable. - Lines 27-28: The
@observedecorator wrapsanswer_questionin a Langfuse trace named"rag_pipeline". Every call to this function automatically starts a new trace with timing, input/output capture, and a unique trace ID. - Lines 29-33: Inside the function,
select_version_abpicks a version, fetches and renders the template, and returns the metadata dictionary. - Lines 35-37:
langfuse_context.update_current_observationattaches the version metadata to the active trace span. This is the critical correlation step—without it, the Langfuse dashboard cannot filter or group traces by prompt version. - Lines 39-43: The
litellm.completioncall sends the rendered prompt to the model. Themetadataparameter on the LiteLLM call passes the prompt version through to Langfuse's generation span via LiteLLM's built-in Langfuse callback integration, providing a second correlation point at the generation level in addition to the trace level.
Do's and Don'ts
Do's
- ✓Do use
register_promptwith explicit labels such as"production"or"staging"every time you change a template — Langfuse auto-increments the integer version per name, giving you a distinct rollback target for each wording change and lettingget_promptresolve the correct template by label at request time without hardcoding version integers into application code. - ✓Do validate that
ABRolloutConfigweights sum exactly to 1.0 within floating-point tolerance before invokingselect_version_ab— the method raisesValueErroron a detected mismatch, but any code path that silently swallows that exception lets a misconfigured sum (e.g., 0.7 + 0.1 = 0.8) route the remaining 20% of traffic to an uncontrolled fallback, skewing cohort sizes and making the v3-vs-v4 comparison statistically invalid. - ✓Do attach the metadata dictionary returned by
select_version_abto every Langfuse trace — theprompt_versionfield in that dict is the sole key the quality dashboard uses to segment latency distributions, token counts, cost, and user feedback scores across variants; traces emitted without it collapse all A/B traffic into a single unlabeled bucket, making the experiment unreadable.
Don'ts
- ✗Don't ship prompt templates as plain strings in application code — without passing them through
register_prompt, there is no Langfuse-managed integer version to rollback to, no label to promote between environments, and no way to isolate whether a latency spike or quality regression originated in a model change or a wording change. - ✗Don't hardcode integer version numbers in
get_promptcalls for production traffic — use the"production"label instead; hardcoded integers require a code deploy every time you want to promote a new version, whereas reassigning the Langfuse label takes effect at the next request with no application code change. - ✗Don't skip rendering the template with
request_variablesbefore sending it to LiteLLM — fetching a raw template fromget_promptand forwarding it directly passes literal placeholder tokens to the model, producing completions that reference unresolved variable names rather than actual request content, and the resulting traces carry prompt metadata that no longer matches what the model actually received.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Application Engineering subscription.
From · cancel anytime
More free lessons in Full-Stack GenAI Applications
- Ch 10Build Llama Guard 4 content classifier
- Ch 14Build a semantic cache with Redis + embedding similarity
- Ch 16Build OpenTelemetry distributed trace pipelines
- Ch 16Manage prompt template versions with LangfuseYou are here
- Ch 16Use Pydantic AI + Logfire as an alternative observability stack
- Ch 18Deploy FastAPI to Cloud Run with auto-scaling
- Ch 18Deploy MCP tool servers as sidecars with external-secrets-operator