Prompts: Template-Driven LLM Inputs
Part 2: Composition - Lesson 1
Stop hardcoding prompts. Start composing them.
Overview
You've been writing prompts like this:
const prompt = `You are a helpful assistant. The user asked: ${userInput}`;
const response = await llm.invoke(prompt);
This works, but it's fragile. What if you need:
- Different system messages for different use cases?
- To inject multiple variables?
- To reuse prompt patterns across your app?
- To validate inputs before sending to the LLM?
- To compose prompts from smaller pieces?
PromptTemplates solve all of these problems.
Why This Matters
The Problem: Prompt Chaos
Without templates, your code becomes a mess:
// Scattered throughout your codebase:
const prompt1 = `Translate to ${lang}: ${text}`;
const prompt2 = "Translate to " + language + ": " + input;
const prompt3 = `Translate to ${target_language}: ${user_text}`;
// Same logic, different implementations everywhere!
Problems:
- No consistency in prompt format
- Hard to test prompts in isolation
- Can't reuse prompt patterns
- Difficult to track what prompts are being used
- No validation of variables
The Solution: PromptTemplate
const translatePrompt = new PromptTemplate({
template: "Translate to {language}: {text}",
inputVariables: ["language", "text"]
});
const prompt = await translatePrompt.format({
language: "Spanish",
text: "Hello, world!"
});
// "Translate to Spanish: Hello, world!"
Benefits:
- β Reusable prompt patterns
- β Variable validation
- β Testable in isolation
- β Composable with other Runnables
- β Type-safe variable injection
Learning Objectives
By the end of this lesson, you will:
- β Build a PromptTemplate class that replaces variables
- β Create ChatPromptTemplate for structured messages
- β Implement Few-Shot prompts for examples
- β Build Pipeline prompts for composition
- β Use prompts as Runnables in chains
- β Understand LangChain's prompting patterns
Core Concepts
What is a PromptTemplate?
A PromptTemplate is a reusable prompt pattern with placeholders for variables.
Structure:
Template String: "Translate to {language}: {text}"
β
Variables Injected: { language: "Spanish", text: "Hello" }
β
Output: "Translate to Spanish: Hello"
The Prompt Hierarchy
BasePromptTemplate (abstract)
βββ PromptTemplate (string templates)
βββ ChatPromptTemplate (message templates)
βββ FewShotPromptTemplate (examples + template)
βββ PipelinePromptTemplate (compose templates)
βββ SystemMessagePromptTemplate (system message helper)
Each type serves a specific use case.
Key Operations
- Format: Replace variables with values
- Validate: Check required variables are provided
- Compose: Combine templates together
- Invoke: Use as a Runnable (returns formatted prompt)
Implementation Guide
Step 1: Base Prompt Template
Location: src/prompts/base-prompt-template.js
This is the abstract base class all prompts inherit from.
What it does:
- Defines the interface for all prompt templates
- Extends Runnable (so prompts work in chains)
- Provides validation logic
Key methods:
format(values)- Replace variables and return string/messagesformatPromptValue(values)- Return as PromptValue (for messages)_validateInput(values)- Check all required variables present
Implementation:
import { Runnable } from '../core/runnable.js';
/**
* Base class for all prompt templates
*/
export class BasePromptTemplate extends Runnable {
constructor(options = {}) {
super();
this.inputVariables = options.inputVariables || [];
this.partialVariables = options.partialVariables || {};
}
/**
* Format the prompt with given values
* @abstract
*/
async format(values) {
throw new Error('Subclasses must implement format()');
}
/**
* Runnable interface: invoke returns formatted prompt
*/
async _call(input, config) {
return await this.format(input);
}
/**
* Validate that all required variables are provided
*/
_validateInput(values) {
const provided = { ...this.partialVariables, ...values };
const missing = this.inputVariables.filter(
key => !(key in provided)
);
if (missing.length > 0) {
throw new Error(
`Missing required input variables: ${missing.join(', ')}`
);
}
}
/**
* Merge partial variables with provided values
*/
_mergePartialAndUserVariables(values) {
return { ...this.partialVariables, ...values };
}
}
Key insights:
- Extends
Runnableso prompts can be chained _callinvokesformat- this makes prompts work in pipelines- Validation ensures all variables are provided
- Partial variables are pre-filled defaults
Step 2: PromptTemplate
Location: src/prompts/prompt-template.js
The most common prompt template - replaces {variable} placeholders.
What it does:
- Takes a template string with
{placeholders} - Replaces placeholders with actual values
- Validates all variables are provided
Template syntax:
"Hello {name}, you are {age} years old."
Implementation:
import { BasePromptTemplate } from './base-prompt-template.js';
/**
* Simple string template with {variable} placeholders
*
* Example:
* const prompt = new PromptTemplate({
* template: "Translate to {language}: {text}",
* inputVariables: ["language", "text"]
* });
*
* await prompt.format({ language: "Spanish", text: "Hello" });
* // "Translate to Spanish: Hello"
*/
export class PromptTemplate extends BasePromptTemplate {
constructor(options = {}) {
super(options);
this.template = options.template;
// Auto-detect input variables if not provided
if (!options.inputVariables) {
this.inputVariables = this._extractInputVariables(this.template);
}
}
/**
* Format the template with provided values
*/
async format(values) {
this._validateInput(values);
const allValues = this._mergePartialAndUserVariables(values);
let result = this.template;
for (const [key, value] of Object.entries(allValues)) {
const regex = new RegExp(`\\{${key}\\}`, 'g');
result = result.replace(regex, String(value));
}
return result;
}
/**
* Extract variable names from template string
* Finds all {variable} patterns
*/
_extractInputVariables(template) {
const matches = template.match(/\{(\w+)\}/g) || [];
return matches.map(match => match.slice(1, -1));
}
/**
* Static helper to create from template string
*/
static fromTemplate(template, options = {}) {
return new PromptTemplate({
template,
...options
});
}
}
Usage example:
const prompt = new PromptTemplate({
template: "Translate to {language}: {text}",
inputVariables: ["language", "text"]
});
const formatted = await prompt.format({
language: "Spanish",
text: "Hello, world!"
});
// "Translate to Spanish: Hello, world!"
Step 3: ChatPromptTemplate
Location: src/prompts/chat-prompt-template.js
For structured conversations with system/user/assistant messages.
What it does:
- Creates arrays of Message objects
- Supports system, human, and AI message templates
- Returns properly formatted conversation structure
Message template syntax:
[
["system", "You are a {role}"],
["human", "My question: {question}"],
["ai", "Let me help with {topic}"]
]
Implementation:
import { BasePromptTemplate } from './base-prompt-template.js';
import { SystemMessage, HumanMessage, AIMessage } from '../core/message.js';
import { PromptTemplate } from './prompt-template.js';
/**
* Template for chat-based conversations
* Returns an array of Message objects
*
* Example:
* const prompt = ChatPromptTemplate.fromMessages([
* ["system", "You are a {role}"],
* ["human", "{input}"]
* ]);
*
* const messages = await prompt.format({
* role: "translator",
* input: "Hello"
* });
* // [SystemMessage(...), HumanMessage(...)]
*/
export class ChatPromptTemplate extends BasePromptTemplate {
constructor(options = {}) {
super(options);
this.promptMessages = options.promptMessages || [];
}
/**
* Format into array of Message objects
*/
async format(values) {
this._validateInput(values);
const allValues = this._mergePartialAndUserVariables(values);
const messages = [];
for (const [role, template] of this.promptMessages) {
const content = await this._formatMessageTemplate(template, allValues);
messages.push(this._createMessage(role, content));
}
return messages;
}
/**
* Format a single message template
*/
async _formatMessageTemplate(template, values) {
if (typeof template === 'string') {
const promptTemplate = new PromptTemplate({ template });
return await promptTemplate.format(values);
}
return template;
}
/**
* Create appropriate Message object for role
*/
_createMessage(role, content) {
switch (role.toLowerCase()) {
case 'system':
return new SystemMessage(content);
case 'human':
case 'user':
return new HumanMessage(content);
case 'ai':
case 'assistant':
return new AIMessage(content);
default:
throw new Error(`Unknown message role: ${role}`);
}
}
/**
* Static helper to create from message list
*/
static fromMessages(messages, options = {}) {
const promptMessages = messages.map(msg => {
if (Array.isArray(msg)) {
return msg; // [role, template]
}
throw new Error('Each message must be [role, template] array');
});
// Extract all input variables from all templates
const inputVariables = new Set();
for (const [, template] of promptMessages) {
if (typeof template === 'string') {
const matches = template.match(/\{(\w+)\}/g) || [];
matches.forEach(m => inputVariables.add(m.slice(1, -1)));
}
}
return new ChatPromptTemplate({
promptMessages,
inputVariables: Array.from(inputVariables),
...options
});
}
}
Usage example:
const chatPrompt = ChatPromptTemplate.fromMessages([
["system", "You are a {role} assistant"],
["human", "My question: {question}"]
]);
const messages = await chatPrompt.format({
role: "helpful",
question: "What's the weather?"
});
// Returns:
// [
// SystemMessage("You are a helpful assistant"),
// HumanMessage("My question: What's the weather?")
// ]
Step 4: Few-Shot Prompt Template
Location: src/prompts/few-shot-prompt.js
For including examples in your prompts (few-shot learning).
What it does:
- Takes a list of examples
- Formats each example with a template
- Combines examples with the main prompt
Structure:
Examples:
Input: 2+2 Output: 4
Input: 3+5 Output: 8
Prompt: Input: {input} Output:
Implementation:
import { BasePromptTemplate } from './base-prompt-template.js';
import { PromptTemplate } from './prompt-template.js';
/**
* Prompt template that includes examples (few-shot learning)
*
* Example:
* const fewShot = new FewShotPromptTemplate({
* examples: [
* { input: "2+2", output: "4" },
* { input: "3+5", output: "8" }
* ],
* examplePrompt: new PromptTemplate({
* template: "Input: {input}\nOutput: {output}",
* inputVariables: ["input", "output"]
* }),
* prefix: "Solve these math problems:",
* suffix: "Input: {input}\nOutput:",
* inputVariables: ["input"]
* });
*/
export class FewShotPromptTemplate extends BasePromptTemplate {
constructor(options = {}) {
super(options);
this.examples = options.examples || [];
this.examplePrompt = options.examplePrompt;
this.prefix = options.prefix || '';
this.suffix = options.suffix || '';
this.exampleSeparator = options.exampleSeparator || '\n\n';
}
/**
* Format the few-shot prompt
*/
async format(values) {
this._validateInput(values);
const allValues = this._mergePartialAndUserVariables(values);
const parts = [];
// Add prefix
if (this.prefix) {
const prefixTemplate = new PromptTemplate({ template: this.prefix });
parts.push(await prefixTemplate.format(allValues));
}
// Add formatted examples
if (this.examples.length > 0) {
const exampleStrings = await Promise.all(
this.examples.map(ex => this.examplePrompt.format(ex))
);
parts.push(exampleStrings.join(this.exampleSeparator));
}
// Add suffix
if (this.suffix) {
const suffixTemplate = new PromptTemplate({ template: this.suffix });
parts.push(await suffixTemplate.format(allValues));
}
return parts.join('\n\n');
}
}
Usage example:
const fewShotPrompt = new FewShotPromptTemplate({
examples: [
{ input: "happy", output: "sad" },
{ input: "tall", output: "short" }
],
examplePrompt: new PromptTemplate({
template: "Input: {input}\nOutput: {output}",
inputVariables: ["input", "output"]
}),
prefix: "Give the antonym of each word:",
suffix: "Input: {word}\nOutput:",
inputVariables: ["word"]
});
const prompt = await fewShotPrompt.format({ word: "hot" });
// Output:
// Give the antonym of each word:
//
// Input: happy
// Output: sad
//
// Input: tall
// Output: short
//
// Input: hot
// Output:
Step 5: Pipeline Prompt Template
Location: src/prompts/pipeline-prompt.js
For composing multiple prompts together.
What it does:
- Combines multiple prompt templates
- Pipes output of one template to input of another
- Enables modular prompt construction
Implementation:
import { BasePromptTemplate } from './base-prompt-template.js';
/**
* Compose multiple prompts into a pipeline
*
* Example:
* const pipeline = new PipelinePromptTemplate({
* finalPrompt: mainPrompt,
* pipelinePrompts: [
* { name: "context", prompt: contextPrompt },
* { name: "instructions", prompt: instructionPrompt }
* ]
* });
*/
export class PipelinePromptTemplate extends BasePromptTemplate {
constructor(options = {}) {
super(options);
this.finalPrompt = options.finalPrompt;
this.pipelinePrompts = options.pipelinePrompts || [];
// Collect all input variables
this.inputVariables = this._collectInputVariables();
}
/**
* Format by running pipeline prompts first, then final prompt
*/
async format(values) {
this._validateInput(values);
const allValues = this._mergePartialAndUserVariables(values);
// Format each pipeline prompt and collect results
const pipelineResults = {};
for (const { name, prompt } of this.pipelinePrompts) {
pipelineResults[name] = await prompt.format(allValues);
}
// Merge with original values and format final prompt
const finalValues = { ...allValues, ...pipelineResults };
return await this.finalPrompt.format(finalValues);
}
/**
* Collect input variables from all prompts
*/
_collectInputVariables() {
const vars = new Set(this.finalPrompt.inputVariables);
for (const { prompt } of this.pipelinePrompts) {
prompt.inputVariables.forEach(v => vars.add(v));
}
// Remove pipeline output names (they're generated)
this.pipelinePrompts.forEach(({ name }) => vars.delete(name));
return Array.from(vars);
}
}
Usage example:
const contextPrompt = new PromptTemplate({
template: "Context: {topic} is important because {reason}",
inputVariables: ["topic", "reason"]
});
const mainPrompt = new PromptTemplate({
template: "{context}\n\nQuestion: {question}",
inputVariables: ["context", "question"]
});
const pipeline = new PipelinePromptTemplate({
finalPrompt: mainPrompt,
pipelinePrompts: [
{ name: "context", prompt: contextPrompt }
]
});
const result = await pipeline.format({
topic: "AI",
reason: "it transforms industries",
question: "What are the risks?"
});
// Output:
// Context: AI is important because it transforms industries
//
// Question: What are the risks?
Step 6: System Message Prompt Template
Location: src/prompts/system-message-prompt.js
A specialized template for creating system messages with consistent formatting.
What it does:
- Creates SystemMessage objects with variable substitution
- Simplifies creating reusable system prompts
- Provides a cleaner API than ChatPromptTemplate for system-only messages
Why this matters: System messages set the behavior and context for LLMs. Having a dedicated template makes it easier to:
- Manage different system prompts for different use cases
- A/B test system message variations
- Inject context dynamically (user preferences, current date, etc.)
- Maintain consistent system message formatting
Implementation:
import { BasePromptTemplate } from './base-prompt-template.js';
import { SystemMessage } from '../core/message.js';
import { PromptTemplate } from './prompt-template.js';
/**
* Template specifically for system messages
* Returns a single SystemMessage object
*
* Example:
* const systemPrompt = new SystemMessagePromptTemplate({
* template: "You are a {role} assistant specialized in {domain}. {instructions}",
* inputVariables: ["role", "domain", "instructions"]
* });
*
* const message = await systemPrompt.format({
* role: "helpful",
* domain: "cooking",
* instructions: "Always provide recipe alternatives."
* });
* // SystemMessage("You are a helpful assistant specialized in cooking. Always provide recipe alternatives.")
*/
export class SystemMessagePromptTemplate extends BasePromptTemplate {
constructor(options = {}) {
super(options);
this.prompt = options.prompt || new PromptTemplate({
template: options.template,
inputVariables: options.inputVariables,
partialVariables: options.partialVariables
});
// Inherit input variables from inner prompt
if (!options.inputVariables) {
this.inputVariables = this.prompt.inputVariables;
}
}
/**
* Format into a SystemMessage object
*/
async format(values) {
this._validateInput(values);
const allValues = this._mergePartialAndUserVariables(values);
const content = await this.prompt.format(allValues);
return new SystemMessage(content);
}
/**
* Static helper to create from template string
*/
static fromTemplate(template, options = {}) {
return new SystemMessagePromptTemplate({
template,
...options
});
}
/**
* Create with partial variables pre-filled
* Useful for setting default context that can be overridden
*/
static fromTemplateWithPartials(template, partialVariables = {}, options = {}) {
const promptTemplate = new PromptTemplate({ template });
const inputVariables = promptTemplate.inputVariables.filter(
v => !(v in partialVariables)
);
return new SystemMessagePromptTemplate({
template,
inputVariables,
partialVariables,
...options
});
}
}
Usage Examples:
Example 1: Basic system message
const systemPrompt = SystemMessagePromptTemplate.fromTemplate(
"You are a {role} assistant. Always be {tone}."
);
const message = await systemPrompt.format({
role: "helpful",
tone: "professional"
});
// SystemMessage("You are a helpful assistant. Always be professional.")
Example 2: System message with partial variables (defaults)
const systemPrompt = SystemMessagePromptTemplate.fromTemplateWithPartials(
"You are a {role} assistant. Today is {date}. User preference: {preference}",
{
date: new Date().toLocaleDateString(), // Pre-filled default
preference: "concise responses" // Pre-filled default
}
);
// Only need to provide 'role' - others use defaults
const message1 = await systemPrompt.format({
role: "coding"
});
// SystemMessage("You are a coding assistant. Today is 11/20/2025. User preference: concise responses")
// Can override defaults if needed
const message2 = await systemPrompt.format({
role: "coding",
preference: "detailed explanations"
});
// SystemMessage("You are a coding assistant. Today is 11/20/2025. User preference: detailed explanations")
Key Benefits:
- Separation of Concerns: System prompts are separate from conversation flow
- Reusability: Create once, use in multiple chat templates
- A/B Testing: Easily swap system prompts to test effectiveness
- Dynamic Context: Inject runtime information (date, user prefs, etc.)
- Type Safety: Always returns SystemMessage (not just strings)
- Partial Variables: Set defaults that can be overridden
Common Use Cases:
- Role-based prompting: Different system messages for different assistant personalities
- Context injection: Add current date, user preferences, session info
- A/B testing: Test different instruction phrasings
- Domain switching: Same app, different domains (customer service vs technical support)
- Compliance: Ensure required disclaimers/policies in every system message
Prompts: Real-World Patterns
Real-World Examples
Example 1: Translation Service
import { PromptTemplate } from './prompts/prompt-template.js';
import { LlamaCppLLM } from './llm/llama-cpp-llm.js';
// Reusable translation prompt
const translationPrompt = new PromptTemplate({
template: `Translate the following text from {source_lang} to {target_lang}.
Text: {text}
Translation:`,
inputVariables: ["source_lang", "target_lang", "text"]
});
// Use in your app
const llm = new LlamaCppLLM({ modelPath: './model.gguf' });
// Build a reusable translation chain
const translationChain = translationPrompt.pipe(llm);
// Now you can easily translate
const spanish = await translationChain.invoke({
source_lang: "English",
target_lang: "Spanish",
text: "Hello, how are you?"
});
const french = await translationChain.invoke({
source_lang: "English",
target_lang: "French",
text: "Hello, how are you?"
});
Why this is better:
- β Prompt pattern defined once, used everywhere
- β Consistent formatting across translations
- β Easy to test prompt in isolation
- β Can swap LLM without changing prompt
Example 2: Customer Support Bot
import { ChatPromptTemplate } from './prompts/chat-prompt-template.js';
const supportPrompt = ChatPromptTemplate.fromMessages([
["system", `You are a customer support agent for {company}.
Company details:
- Product: {product}
- Return policy: {return_policy}
- Support hours: {support_hours}
Be helpful, professional, and concise.`],
["human", "{customer_message}"]
]);
// Use with partial variables for company info
const myCompanySupportPrompt = supportPrompt.partial({
company: "TechCorp",
product: "Cloud Storage",
return_policy: "30 days",
support_hours: "9am-5pm EST"
});
// Now only need customer message
const messages = await myCompanySupportPrompt.format({
customer_message: "How do I cancel my subscription?"
});
const response = await llm.invoke(messages);
Why this is better:
- β Company info defined once, reused everywhere
- β Partial variables reduce repetition
- β Easy to update company policy in one place
- β Structured conversation format
Example 3: Few-Shot Classification
import { FewShotPromptTemplate } from './prompts/few-shot-prompt.js';
import { PromptTemplate } from './prompts/prompt-template.js';
// Example formatter
const examplePrompt = new PromptTemplate({
template: "Text: {text}\nSentiment: {sentiment}",
inputVariables: ["text", "sentiment"]
});
// Few-shot sentiment classifier
const sentimentPrompt = new FewShotPromptTemplate({
examples: [
{ text: "I love this product!", sentiment: "positive" },
{ text: "This is terrible.", sentiment: "negative" },
{ text: "It's okay, I guess.", sentiment: "neutral" }
],
examplePrompt: examplePrompt,
prefix: "Classify the sentiment of the following texts:",
suffix: "Text: {input}\nSentiment:",
inputVariables: ["input"],
exampleSeparator: "\n\n"
});
const prompt = await sentimentPrompt.format({
input: "This exceeded my expectations!"
});
// The LLM now has examples to learn from!
const sentiment = await llm.invoke(prompt);
Why this is better:
- β Examples teach the LLM the task
- β More consistent classifications
- β Easy to add/remove examples
- β Can dynamically select relevant examples
Example 4: Modular Prompt Composition
import { PipelinePromptTemplate } from './prompts/pipeline-prompt.js';
import { PromptTemplate } from './prompts/prompt-template.js';
// Reusable context builder
const contextPrompt = new PromptTemplate({
template: `Domain: {domain}
Key concepts: {concepts}
Current date: {date}`,
inputVariables: ["domain", "concepts", "date"]
});
// Reusable instruction builder
const instructionPrompt = new PromptTemplate({
template: `Task: {task}
Format: {format}
Constraints: {constraints}`,
inputVariables: ["task", "format", "constraints"]
});
// Main prompt uses outputs from sub-prompts
const mainPrompt = new PromptTemplate({
template: `{context}
{instructions}
Input: {input}
Output:`,
inputVariables: ["context", "instructions", "input"]
});
// Compose them together
const composedPrompt = new PipelinePromptTemplate({
finalPrompt: mainPrompt,
pipelinePrompts: [
{ name: "context", prompt: contextPrompt },
{ name: "instructions", prompt: instructionPrompt }
]
});
// Use the composed prompt
const result = await composedPrompt.format({
domain: "Healthcare",
concepts: "diagnosis, treatment, patient care",
date: new Date().toISOString().split('T')[0],
task: "Analyze symptoms",
format: "JSON with confidence scores",
constraints: "HIPAA compliant, evidence-based",
input: "Patient reports fatigue and headaches"
});
Why this is better:
- β Modular prompt components
- β Reuse context/instructions across prompts
- β Easy to update individual sections
- β Cleaner prompt management
Advanced Patterns
Pattern 1: Conditional Prompts
class ConditionalPromptTemplate extends BasePromptTemplate {
constructor(options = {}) {
super(options);
this.condition = options.condition;
this.truePrompt = options.truePrompt;
this.falsePrompt = options.falsePrompt;
}
async format(values) {
const useTrue = this.condition(values);
const selectedPrompt = useTrue ? this.truePrompt : this.falsePrompt;
return await selectedPrompt.format(values);
}
}
// Usage
const prompt = new ConditionalPromptTemplate({
condition: (values) => values.userType === 'expert',
truePrompt: new PromptTemplate({
template: "Technical analysis: {query}"
}),
falsePrompt: new PromptTemplate({
template: "Explain in simple terms: {query}"
}),
inputVariables: ["userType", "query"]
});
Pattern 2: Dynamic Examples (Select Best Examples)
class DynamicFewShotPromptTemplate extends FewShotPromptTemplate {
constructor(options = {}) {
super(options);
this.exampleSelector = options.exampleSelector;
this.maxExamples = options.maxExamples || 3;
}
async format(values) {
// Select most relevant examples dynamically
const selectedExamples = await this.exampleSelector.select(
values,
this.maxExamples
);
// Temporarily replace examples
const originalExamples = this.examples;
this.examples = selectedExamples;
const result = await super.format(values);
// Restore original examples
this.examples = originalExamples;
return result;
}
}
// Example selector based on similarity
class SimilarityExampleSelector {
constructor(examples) {
this.examples = examples;
}
async select(input, k) {
// In real implementation, use embeddings for similarity
// For now, simple keyword matching
const scores = this.examples.map(ex => ({
example: ex,
score: this._similarity(ex.input, input.input)
}));
scores.sort((a, b) => b.score - a.score);
return scores.slice(0, k).map(s => s.example);
}
_similarity(a, b) {
// Simple word overlap
const wordsA = new Set(a.toLowerCase().split(' '));
const wordsB = new Set(b.toLowerCase().split(' '));
const intersection = new Set([...wordsA].filter(x => wordsB.has(x)));
return intersection.size / Math.max(wordsA.size, wordsB.size);
}
}
Pattern 3: Prompt with Validation
class ValidatedPromptTemplate extends PromptTemplate {
constructor(options = {}) {
super(options);
this.validators = options.validators || {};
}
async format(values) {
// Validate each input
for (const [key, validator] of Object.entries(this.validators)) {
if (key in values) {
const isValid = validator(values[key]);
if (!isValid) {
throw new Error(`Invalid value for ${key}: ${values[key]}`);
}
}
}
return await super.format(values);
}
}
// Usage
const emailPrompt = new ValidatedPromptTemplate({
template: "Send email to {email} about {subject}",
inputVariables: ["email", "subject"],
validators: {
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
subject: (value) => value.length > 0 && value.length <= 100
}
});
// This will throw an error
await emailPrompt.format({
email: "invalid-email",
subject: "Hi"
});
Pattern 4: Prompt Chaining
// Chain multiple prompts together
const summaryPrompt = new PromptTemplate({
template: "Summarize this text: {text}"
});
const bulletPrompt = new PromptTemplate({
template: "Convert this summary to bullet points:\n{summary}"
});
// Create a chain
const summaryChain = summaryPrompt.pipe(llm);
const bulletChain = bulletPrompt.pipe(llm);
// Use them together
async function summarizeAsBullets(text) {
const summary = await summaryChain.invoke({ text });
const bullets = await bulletChain.invoke({ summary });
return bullets;
}
Common Use Cases
Use Case 1: Multi-Language Support
const prompts = {
en: new PromptTemplate({
template: "Answer in English: {query}"
}),
es: new PromptTemplate({
template: "Responde en espaΓ±ol: {query}"
}),
fr: new PromptTemplate({
template: "RΓ©pondre en franΓ§ais: {query}"
})
};
function getPrompt(language) {
return prompts[language] || prompts.en;
}
const prompt = getPrompt(userLanguage);
const response = await prompt.pipe(llm).invoke({ query: "Hello" });
Use Case 2: A/B Testing Prompts
const variantA = new PromptTemplate({
template: "Explain {concept} simply"
});
const variantB = new PromptTemplate({
template: "Teach me about {concept} like I'm 5 years old"
});
// Randomly select variant
const prompt = Math.random() < 0.5 ? variantA : variantB;
// Track which variant was used
const config = {
metadata: { variant: prompt === variantA ? 'A' : 'B' }
};
const response = await prompt.pipe(llm).invoke({ concept: "gravity" }, config);
Use Case 3: Prompt Versioning
const prompts = {
v1: new PromptTemplate({
template: "Classify: {text}"
}),
v2: new PromptTemplate({
template: "Classify the sentiment of: {text}\nOptions: positive, negative, neutral"
}),
v3: ChatPromptTemplate.fromMessages([
["system", "You are a sentiment classifier. Only respond with: positive, negative, or neutral"],
["human", "{text}"]
])
};
// Use specific version
const currentPrompt = prompts.v3;
// Easy to roll back if needed
const response = await currentPrompt.pipe(llm).invoke({ text: "I love this!" });
Best Practices
β DO:
1. Keep prompts in separate files
// prompts/translation.js
export const translationPrompt = new PromptTemplate({
template: "Translate to {language}: {text}",
inputVariables: ["language", "text"]
});
// app.js
import { translationPrompt } from './prompts/translation.js';
2. Use partial variables for constants
const prompt = new PromptTemplate({
template: "Company: {company}\nQuery: {query}",
partialVariables: {
company: "MyCompany"
}
});
3. Test prompts in isolation
const formatted = await prompt.format({ input: "test" });
console.log(formatted);
// Verify output before sending to LLM
4. Version your prompts
// prompts/classifier/v2.js
export const classifierPromptV2 = ...
5. Document your templates
/**
* Email classification prompt
*
* Variables:
* - from: Email sender
* - subject: Email subject
* - body: Email body
*
* Returns: Category name
*/
export const emailPrompt = ...
β DON'T:
1. Hardcode prompts throughout codebase
// Bad
const result = await llm.invoke(`Translate to ${lang}: ${text}`);
2. Skip input validation
// Bad
const prompt = new PromptTemplate({ template: "..." });
await prompt.format({}); // Missing required variables!
3. Make prompts too complex
// Bad - too many nested conditions
const prompt = condition1 ? (condition2 ? prompt1 : prompt2) : ...
4. Forget to handle formatting errors
// Bad
const formatted = await prompt.format(values); // What if this throws?
// Good
try {
const formatted = await prompt.format(values);
} catch (error) {
console.error('Prompt formatting failed:', error);
// Handle gracefully
}
Integration with Framework
Using Prompts in Chains
import { PromptTemplate } from './prompts/prompt-template.js';
import { LlamaCppLLM } from './llm/llama-cpp-llm.js';
import { StringOutputParser } from './output-parsers/string-parser.js';
const prompt = new PromptTemplate({
template: "Summarize: {text}"
});
const llm = new LlamaCppLLM({ modelPath: './model.gguf' });
const parser = new StringOutputParser();
// Build chain: prompt -> llm -> parser
const chain = prompt.pipe(llm).pipe(parser);
// Use it
const summary = await chain.invoke({ text: "Long article..." });
Using Prompts with Memory
import { ChatPromptTemplate } from './prompts/chat-prompt-template.js';
import { BufferMemory } from './memory/buffer-memory.js';
const memory = new BufferMemory();
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant"],
["placeholder", "{chat_history}"],
["human", "{input}"]
]);
// Load history from memory
const chatHistory = await memory.loadMemoryVariables();
const messages = await prompt.format({
chat_history: chatHistory.history,
input: "What did we discuss?"
});
Testing Prompts
Unit Testing
import { describe, it, expect } from 'your-test-framework';
import { PromptTemplate } from './prompts/prompt-template.js';
describe('PromptTemplate', () => {
it('should format template with variables', async () => {
const prompt = new PromptTemplate({
template: "Hello {name}",
inputVariables: ["name"]
});
const result = await prompt.format({ name: "Alice" });
expect(result).toBe("Hello Alice");
});
it('should throw on missing variables', async () => {
const prompt = new PromptTemplate({
template: "Hello {name}",
inputVariables: ["name"]
});
await expect(prompt.format({})).rejects.toThrow();
});
it('should handle partial variables', async () => {
const prompt = new PromptTemplate({
template: "{greeting} {name}",
inputVariables: ["greeting", "name"],
partialVariables: { greeting: "Hello" }
});
const result = await prompt.format({ name: "Bob" });
expect(result).toBe("Hello Bob");
});
});
Exercises
Practice using prompt templates from simple to complex:
Exercise 17: Using a Basic PromptTemplate
Master variable replacement and template formatting fundamentals.
Starter code: exercises/17-prompt-template.js
Exercise 18: Using the ChatPromptTemplate
Create structured conversations with role-based messages.
Starter code: exercises/18-chat-prompt-template.js
Exercise 19: Using the FewShotPromptTemplate
Implement few-shot learning with examples for better LLM outputs.
Starter code: exercises/19-few-shot-prompt-template.js
Exercise 20: Using the PipelinePromptTemplate
Compose modular prompts by connecting multiple templates.
Starter code: exercises/20-pipeline-prompt-template.js
Summary
You've learned how to build a complete prompting system!
Key Takeaways
- PromptTemplate: Basic string templates with variable replacement
- ChatPromptTemplate: Structured conversations with role-based messages
- FewShotPromptTemplate: Include examples for better LLM performance
- PipelinePromptTemplate: Compose prompts modularly
- BasePromptTemplate: Foundation that makes prompts Runnables
What You Built
A production-ready prompt system that:
- β Replaces variables safely
- β Validates inputs
- β Composes with other Runnables
- β Supports chat and completion formats
- β Enables few-shot learning
- β Allows prompt composition
Next Steps
Now that you have reusable prompts, you need to parse LLM outputs into structured data.
β‘οΈ Next: Output Parsers
Learn how to extract structured data from LLM responses reliably.
Built with β€οΈ for learners who want to understand AI frameworks deeply
β Previous: Context | Tutorial Index | Next: Output Parsers β