CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-dev-langchain4j--langchain4j

Build LLM-powered applications in Java with support for chatbots, agents, RAG, tools, and much more

Overview
Eval results
Files

prompts.mddocs/

Prompts and Templates

Prompt system for creating reusable text templates with variable substitution. Supports single and structured prompts with automatic date/time injection and conversion to chat messages.

Capabilities

Prompt Class

Core prompt wrapper representing text input to an LLM.

package dev.langchain4j.model.input;

import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;

/**
 * Represents a prompt (an input text sent to the LLM)
 * Usually contains instructions, context, and user input
 */
public class Prompt {
    /**
     * Create a new Prompt
     * @param text The text of the prompt
     */
    public Prompt(String text);

    /**
     * Get the text of the prompt
     * @return The prompt text
     */
    public String text();

    /**
     * Convert to SystemMessage
     * @return SystemMessage with this prompt's text
     */
    public SystemMessage toSystemMessage();

    /**
     * Convert to UserMessage
     * @return UserMessage with this prompt's text
     */
    public UserMessage toUserMessage();

    /**
     * Convert to UserMessage with username
     * @param userName User name to associate
     * @return UserMessage with username and text
     */
    public UserMessage toUserMessage(String userName);

    /**
     * Convert to AiMessage
     * @return AiMessage with this prompt's text
     */
    public AiMessage toAiMessage();

    /**
     * Create prompt from text
     * @param text Prompt text
     * @return Prompt instance
     */
    public static Prompt from(String text);
}

Thread Safety

Immutable: Prompt instances are immutable and thread-safe. Once created, the text cannot be modified. Multiple threads can safely share and access the same Prompt instance without synchronization.

Conversion Methods: The conversion methods (toSystemMessage(), toUserMessage(), toAiMessage()) create new Message objects each time they are called. These methods are thread-safe and can be called concurrently.

Common Pitfalls

  • Null Text: Passing null to the constructor will result in a NullPointerException. Always validate input before creating a Prompt.
  • Empty Text: Empty strings are valid but may produce unexpected results when sent to LLMs. Consider validation before prompt creation.
  • Mismatched Message Types: Converting to the wrong message type can confuse the LLM. Use toSystemMessage() for instructions, toUserMessage() for user input, and toAiMessage() for AI responses in multi-turn conversations.

Edge Cases

  • Empty Prompts: Prompt.from("") creates a valid Prompt with empty text. This may cause API errors with some LLM providers.
  • Whitespace-Only Prompts: Prompts containing only whitespace are valid but may be rejected by LLMs or produce nonsensical responses.
  • Special Characters: Unicode characters, emojis, and control characters are preserved as-is. Some LLM providers may have limitations on supported character sets.
  • Large Prompts: No size limit is enforced by the Prompt class, but LLM providers have token limits. Always check provider-specific constraints.

Performance Notes

  • Creation Overhead: Prompt creation is lightweight with minimal overhead. The constructor simply stores the text string.
  • Conversion Cost: Message conversion methods create new objects but are still lightweight operations (typically < 1ms).
  • Memory: Each Prompt instance stores the full text in memory. For large-scale applications with many prompts, consider prompt pooling or lazy loading strategies.

Exception Handling

// Exceptions thrown by Prompt class
- NullPointerException: When text parameter is null in constructor or from() method
- No checked exceptions are thrown

Exception Examples:

// Throws NullPointerException
Prompt prompt = Prompt.from(null);

// Valid but potentially problematic
Prompt empty = Prompt.from(""); // No exception, but may fail at LLM API level

Related APIs

  • PromptTemplate: For creating reusable prompts with variable substitution
  • StructuredPrompt: For defining structured multi-line prompts with annotations
  • UserMessage, SystemMessage, AiMessage: Message types in dev.langchain4j.data.message package
  • ChatMemory: For managing conversation history including prompts and responses
  • AiServices: High-level API that uses prompts internally with @UserMessage and @SystemMessage annotations

Usage Example:

import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.data.message.UserMessage;

// Create a prompt
Prompt prompt = Prompt.from("What is the capital of France?");

// Get text
String text = prompt.text();

// Convert to message types
UserMessage userMessage = prompt.toUserMessage();
SystemMessage systemMessage = prompt.toSystemMessage();

PromptTemplate Class

Template system for creating reusable prompts with variable substitution. Supports placeholders in {{variable_name}} format.

package dev.langchain4j.model.input;

import java.time.Clock;
import java.util.Map;

/**
 * Represents a template of a prompt that can be reused multiple times
 * Variables defined as {{variable_name}} are replaced with actual values
 * Special variables {{current_date}}, {{current_time}}, {{current_date_time}}
 * are automatically filled with current date/time values
 */
public class PromptTemplate {
    /**
     * Create a new PromptTemplate
     * @param template Template string with {{variable}} placeholders
     */
    public PromptTemplate(String template);

    /**
     * Create PromptTemplate with custom clock
     * @param template Template string with {{variable}} placeholders
     * @param clock Clock to use for date/time variables
     */
    public PromptTemplate(String template, Clock clock);

    /**
     * Create PromptTemplate with name
     * @param template Template string with {{variable}} placeholders
     * @param name Template name for identification
     */
    public PromptTemplate(String template, String name);

    /**
     * Create PromptTemplate with name and clock
     * @param template Template string with {{variable}} placeholders
     * @param name Template name for identification
     * @param clock Clock to use for date/time variables
     */
    public PromptTemplate(String template, String name, Clock clock);

    /**
     * Get the template string
     * @return Template string
     */
    public String template();

    /**
     * Apply single value to template with {{it}} placeholder
     * @param value Value to inject for {{it}}
     * @return Prompt with variable replaced
     */
    public Prompt apply(Object value);

    /**
     * Apply multiple variables to template
     * @param variables Map of variable names to values
     * @return Prompt with all variables replaced
     */
    public Prompt apply(Map<String, Object> variables);

    /**
     * Create PromptTemplate from string
     * @param template Template string
     * @return PromptTemplate instance
     */
    public static PromptTemplate from(String template);

    /**
     * Create PromptTemplate with name
     * @param template Template string
     * @param name Template name
     * @return PromptTemplate instance
     */
    public static PromptTemplate from(String template, String name);

    /**
     * Create PromptTemplate with clock
     * @param template Template string
     * @param clock Clock for date/time variables
     * @return PromptTemplate instance
     */
    public static PromptTemplate from(String template, Clock clock);

    /**
     * Create PromptTemplate with name and clock
     * @param template Template string
     * @param name Template name
     * @param clock Clock for date/time variables
     * @return PromptTemplate instance
     */
    public static PromptTemplate from(String template, String name, Clock clock);
}

Thread Safety

Reusability: PromptTemplate instances are immutable and thread-safe. Once created, the template string and configuration (name, clock) cannot be modified. A single PromptTemplate can be safely shared across multiple threads and reused for multiple apply() calls.

Apply Method: The apply() method is thread-safe and can be called concurrently by multiple threads. Each call produces a new independent Prompt instance.

Clock Configuration: When using a custom Clock, ensure the Clock implementation itself is thread-safe. The standard Clock.systemDefaultZone() and Clock.fixed() are thread-safe.

Best Practice for Reusability:

// Define template once as a constant or singleton
private static final PromptTemplate GREETING_TEMPLATE =
    PromptTemplate.from("Hello {{name}}, welcome to {{service}}!");

// Reuse safely across threads and requests
public Prompt createGreeting(String name, String service) {
    return GREETING_TEMPLATE.apply(Map.of(
        "name", name,
        "service", service
    ));
}

Common Pitfalls

  • Missing Variables: If a variable in the template is not provided in the apply() map, it will remain as {{variable_name}} in the output. This will confuse the LLM. Always ensure all required variables are provided.
// INCORRECT - missing 'country' variable
PromptTemplate template = PromptTemplate.from("Capital of {{country}}?");
Prompt prompt = template.apply(Map.of("name", "Alice"));
// Result: "Capital of {{country}}?" - NOT what you want!

// CORRECT
Prompt prompt = template.apply(Map.of("country", "France"));
// Result: "Capital of France?"
  • Case Sensitivity: Variable names are case-sensitive. {{Name}} and {{name}} are different variables.
PromptTemplate template = PromptTemplate.from("Hello {{Name}}");
// WRONG - case mismatch
template.apply(Map.of("name", "Alice")); // Result: "Hello {{Name}}"
// CORRECT
template.apply(Map.of("Name", "Alice")); // Result: "Hello Alice"
  • Typos in Variable Names: A typo in either the template or the map key will result in unreplaced placeholders.

  • Null Values: Passing null values in the map will result in "null" being inserted as a string. Validate inputs before applying.

template.apply(Map.of("name", null)); // Result: "Hello null"
  • Single Variable with Wrong Method: Using apply(Object value) requires the template to use {{it}} as the placeholder.
// INCORRECT
PromptTemplate template = PromptTemplate.from("Translate {{text}}");
template.apply("Hello"); // Result: "Translate {{text}}" - wrong!

// CORRECT - use {{it}} for single variable
PromptTemplate template = PromptTemplate.from("Translate {{it}}");
template.apply("Hello"); // Result: "Translate Hello"

Edge Cases

  • Empty Templates: PromptTemplate.from("") creates a valid template that produces empty prompts.

  • No Variables: Templates without any {{}} placeholders work fine and simply return the template text as-is.

  • Nested Braces: Variable syntax requires exactly double braces. {variable} (single) and {{{variable}}} (triple) are treated as literal text, not placeholders.

PromptTemplate template = PromptTemplate.from("Price: {amount}"); // NOT replaced
template.apply(Map.of("amount", "10")); // Result: "Price: {amount}"
  • Special Characters in Variable Names: Variable names should be alphanumeric with underscores. Special characters may cause parsing issues.
// Supported
{{variable_name}}
{{userName}}
{{item1}}

// Potentially problematic
{{user-name}}   // hyphen may cause issues
{{user.name}}   // dot may cause issues
{{user name}}   // spaces will cause issues
  • Escaping Braces: Currently there is no escape mechanism for literal {{ or }} in templates. If you need literal double braces in output, this is not supported directly.

  • Empty Variable Values: Empty strings are replaced successfully, resulting in gaps in the output.

PromptTemplate template = PromptTemplate.from("Hello {{name}} there");
template.apply(Map.of("name", "")); // Result: "Hello  there" (double space)
  • Complex Objects: The apply() method calls .toString() on variable values. Ensure your objects have meaningful toString() implementations.
class Person {
    String name;
    // No toString() override - will print: Person@hashcode
}

// Problematic
template.apply(Map.of("user", new Person()));
// Result: "Hello Person@a1b2c3d4"

// Better - use primitives/strings or override toString()
template.apply(Map.of("user", person.name));

Performance Notes

  • Template Parsing Overhead: Template parsing happens once during construction. The template is analyzed to identify all {{variable}} placeholders and store their positions. This is a one-time cost.

  • Apply() Performance: Each apply() call performs string replacement for all variables. For large templates with many variables, this involves multiple string operations. Performance is generally O(n) where n is the template length.

  • Optimization Strategy: For high-throughput scenarios, create PromptTemplate instances once and reuse them rather than recreating on each request.

// INEFFICIENT - parsing overhead on every request
public Prompt createPrompt(String name) {
    return PromptTemplate.from("Hello {{name}}").apply(Map.of("name", name));
}

// EFFICIENT - parse once, reuse many times
private static final PromptTemplate TEMPLATE = PromptTemplate.from("Hello {{name}}");
public Prompt createPrompt(String name) {
    return TEMPLATE.apply(Map.of("name", name));
}
  • Memory Considerations: Each PromptTemplate stores the original template string and metadata. For applications with thousands of unique templates, consider template caching strategies or loading from external resources.

  • Date/Time Variables: When using {{current_date}}, {{current_time}}, or {{current_date_time}}, these are evaluated at apply() time, not at template creation time.

PromptTemplate template = PromptTemplate.from("Report for {{current_date}}");
// Each apply() call uses the current date at that moment
Prompt p1 = template.apply(Map.of()); // 2025-01-15
// ... time passes ...
Prompt p2 = template.apply(Map.of()); // 2025-01-16 (different date)

Exception Handling

// Exceptions thrown by PromptTemplate
- NullPointerException: When template string is null in constructor or from() methods
- IllegalArgumentException: When template syntax is invalid (implementation-specific)
- No checked exceptions are thrown

Exception Examples:

// Throws NullPointerException
PromptTemplate template = PromptTemplate.from(null);

// Valid - missing variables don't throw exceptions, they remain unreplaced
PromptTemplate template = PromptTemplate.from("Hello {{name}}");
Prompt prompt = template.apply(Map.of()); // No exception, but produces "Hello {{name}}"

// apply() with null map throws NullPointerException
template.apply(null); // Throws NullPointerException

Related APIs

  • Prompt: The output type produced by apply() methods
  • StructuredPrompt + StructuredPromptProcessor: Alternative approach for complex multi-line prompts with type-safe variables
  • @UserMessage and @SystemMessage annotations: Used in AiServices to define inline prompt templates
  • ChatMemory: Stores conversation history including prompts
  • Clock: Java's Clock class for controlling date/time variable values (useful for testing)

Usage Example:

import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.input.Prompt;
import java.util.Map;

// Simple template with single variable
PromptTemplate template = PromptTemplate.from(
    "What is the capital of {{country}}?"
);
Prompt prompt = template.apply(Map.of("country", "France"));

// Template with multiple variables
PromptTemplate multiTemplate = PromptTemplate.from(
    "Hello {{name}}, today is {{current_date}}. How can I help with {{topic}}?"
);
Prompt multiPrompt = multiTemplate.apply(Map.of(
    "name", "Alice",
    "topic", "Java programming"
));
// current_date is automatically injected

// Single variable template using {{it}}
PromptTemplate simpleTemplate = PromptTemplate.from(
    "Translate the following to French: {{it}}"
);
Prompt simplePrompt = simpleTemplate.apply("Hello, how are you?");

StructuredPrompt Annotation

Annotation for creating structured prompts from Java classes with automatic variable extraction.

package dev.langchain4j.model.input.structured;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Annotation for defining structured prompts
 * Annotate a class to define a multi-line prompt template
 * Class fields become template variables
 */
@Target(TYPE)
@Retention(RUNTIME)
public @interface StructuredPrompt {
    /**
     * Prompt template lines
     * @return Array of template lines
     */
    String[] value();

    /**
     * Delimiter to join template lines
     * @return Delimiter string (default: newline)
     */
    String delimiter() default "\n";
}

Thread Safety

Annotation Metadata: The annotation itself is just metadata read at runtime via reflection. Multiple threads can safely read the same annotation.

Instances of Annotated Classes: The thread-safety of instances depends on your class design. If your prompt class has mutable fields, you need to ensure thread safety yourself. For best practices, make fields final and the class immutable.

// Thread-safe immutable design
@StructuredPrompt({"Hello {{name}}"})
class ThreadSafePrompt {
    final String name; // final = immutable

    public ThreadSafePrompt(String name) {
        this.name = name;
    }
}

// NOT thread-safe - mutable fields
@StructuredPrompt({"Hello {{name}}"})
class UnsafePrompt {
    String name; // mutable!

    public void setName(String name) {
        this.name = name; // race conditions possible
    }
}

StructuredPromptProcessor: The toPrompt() method uses reflection to read field values. This is thread-safe for reading, but if multiple threads are modifying the same object's fields while processing, race conditions can occur.

Common Pitfalls

  • Field Name Mismatches: Variable names in the template must exactly match field names in the class (case-sensitive).
@StructuredPrompt({"Hello {{Name}}"}) // Capital N
class Prompt {
    String name; // lowercase n - MISMATCH!
}
// Result: "Hello {{Name}}" - variable not replaced
  • Private Fields: StructuredPromptProcessor uses reflection to access fields. It can access private fields, but this may fail in environments with restricted reflection (e.g., some Java modules).
@StructuredPrompt({"Hello {{name}}"})
class Prompt {
    private String name; // Works in most cases, but be aware of reflection restrictions
}
  • Non-String Field Types: Fields can be any type - toString() is called automatically. Ensure your objects have meaningful toString() implementations.
@StructuredPrompt({"User {{user}}"})
class Prompt {
    Person user; // Person must have a good toString() method
}
  • Null Fields: If a field is null, "null" will be inserted as a string. Always validate or provide defaults.
@StructuredPrompt({"Hello {{name}}"})
class Prompt {
    String name = null; // Will produce: "Hello null"
}
  • Missing Delimiter: The default delimiter is \n. If you need different line joining (e.g., space, comma), specify it explicitly.
@StructuredPrompt(
    value = {"Part1", "Part2"},
    delimiter = " " // Join with space instead of newline
)
  • Static Fields: Static fields are not used for variable substitution. Only instance fields are processed.
@StructuredPrompt({"Hello {{name}}"})
class Prompt {
    static String name = "Alice"; // NOT used - static fields ignored
    String actualName = "Bob";    // This would work if named 'name'
}

Edge Cases

  • Empty Array: @StructuredPrompt({}) produces an empty prompt.

  • Single-Line Prompt: Can use with just one string: @StructuredPrompt({"Hello {{name}}"}) - no newlines.

  • No Variables: Templates without placeholders work fine: @StructuredPrompt({"Hello World"}).

  • Inherited Fields: Fields inherited from parent classes are included in variable substitution.

class BasePrompt {
    String baseName;
}

@StructuredPrompt({"{{baseName}} - {{derivedName}}"})
class DerivedPrompt extends BasePrompt {
    String derivedName;
    // Both baseName and derivedName can be used
}
  • Multiple Classes with Same Template: You can annotate multiple classes with the same template structure for different use cases.

  • Complex Delimiters: You can use multi-character delimiters: delimiter = " | ".

@StructuredPrompt(
    value = {"Name", "Age", "Location"},
    delimiter = " | "
)
// Result: "Name | Age | Location"
  • Special Characters in Templates: Unicode, emojis, and special characters in the annotation value array are preserved.

Performance Notes

  • Reflection Overhead: StructuredPromptProcessor.toPrompt() uses reflection to read field values and process the annotation. This has measurable overhead compared to manual string building or PromptTemplate.

  • First-Call Caching: Some reflection operations may be cached by the JVM on subsequent calls, improving performance after the first invocation.

  • Compilation Impact: Annotations have no runtime cost until accessed via reflection. The annotation itself doesn't slow down class loading.

  • Performance Comparison:

    • Fastest: Static strings or pre-built prompts
    • Medium: PromptTemplate with apply()
    • Slowest: StructuredPrompt with reflection
  • Optimization Strategy: For high-throughput scenarios, consider caching the result of toPrompt() if the same prompt object is used repeatedly, or use PromptTemplate instead.

// Less efficient - reflection on every call
public Prompt getPrompt(String name, String topic) {
    MyPrompt p = new MyPrompt(name, topic);
    return StructuredPromptProcessor.toPrompt(p); // Reflection overhead
}

// More efficient - use PromptTemplate for repeated operations
private static final PromptTemplate TEMPLATE =
    PromptTemplate.from("Name: {{name}}, Topic: {{topic}}");

public Prompt getPrompt(String name, String topic) {
    return TEMPLATE.apply(Map.of("name", name, "topic", topic));
}

Exception Handling

// Exceptions thrown by StructuredPrompt and StructuredPromptProcessor
- IllegalArgumentException: When object is not annotated with @StructuredPrompt
- NullPointerException: When null is passed to toPrompt()
- IllegalAccessException: Wrapped in RuntimeException if field access fails (rare)
- No checked exceptions are thrown

Exception Examples:

// Throws IllegalArgumentException - not annotated
class NotAnnotated {
    String field;
}
StructuredPromptProcessor.toPrompt(new NotAnnotated());

// Throws NullPointerException
StructuredPromptProcessor.toPrompt(null);

// No exception - null fields produce "null" strings
@StructuredPrompt({"{{name}}"})
class Prompt {
    String name = null;
}
StructuredPromptProcessor.toPrompt(new Prompt()); // Returns Prompt with text "null"

Related APIs

  • StructuredPromptProcessor: Required for processing @StructuredPrompt annotations
  • PromptTemplate: Alternative approach that doesn't require reflection
  • Prompt: The output type produced by StructuredPromptProcessor
  • @UserMessage / @SystemMessage: Alternative annotations used in AiServices for inline prompts
  • AiServices: Can work with structured prompts for complex conversational patterns

Usage Example:

import dev.langchain4j.model.input.structured.StructuredPrompt;
import dev.langchain4j.model.input.structured.StructuredPromptProcessor;

// Define structured prompt class
@StructuredPrompt({
    "You are a helpful assistant for {{taskType}}.",
    "User: {{userName}}",
    "Context: {{context}}",
    "Question: {{question}}"
})
class MyPrompt {
    String taskType;
    String userName;
    String context;
    String question;

    public MyPrompt(String taskType, String userName,
                    String context, String question) {
        this.taskType = taskType;
        this.userName = userName;
        this.context = context;
        this.question = question;
    }
}

// Use structured prompt
MyPrompt promptData = new MyPrompt(
    "technical support",
    "Alice",
    "Application crashes on startup",
    "How can I fix this?"
);

Prompt prompt = StructuredPromptProcessor.toPrompt(promptData);
String text = prompt.text();
// Result:
// You are a helpful assistant for technical support.
// User: Alice
// Context: Application crashes on startup
// Question: How can I fix this?

StructuredPromptProcessor

Processor for converting structured prompt objects to Prompt instances.

package dev.langchain4j.model.input.structured;

import dev.langchain4j.model.input.Prompt;

/**
 * Processor for structured prompts
 * Converts annotated objects to Prompt instances
 */
public class StructuredPromptProcessor {
    /**
     * Convert structured prompt object to Prompt
     * @param structuredPrompt Object annotated with @StructuredPrompt
     * @return Prompt with variables replaced
     */
    public static Prompt toPrompt(Object structuredPrompt);
}

Thread Safety

Static Method: The toPrompt() method is static and stateless. It is inherently thread-safe and can be called concurrently from multiple threads.

Input Object Thread Safety: The thread-safety of processing depends on the input object. If the same object is being modified by another thread during processing, race conditions can occur. For safe concurrent usage, ensure input objects are immutable or properly synchronized.

Reflection Caching: Internal reflection operations may be cached by the JVM, which is handled thread-safely by the Java reflection API.

Common Pitfalls

  • Forgetting the Annotation: Passing an object not annotated with @StructuredPrompt will throw an IllegalArgumentException.
class NotAnnotated {
    String name;
}
// WRONG - throws IllegalArgumentException
StructuredPromptProcessor.toPrompt(new NotAnnotated());
  • Null Input: Passing null throws NullPointerException. Always validate inputs.
// WRONG - throws NullPointerException
StructuredPromptProcessor.toPrompt(null);
  • Misunderstanding Scope: This processor only works with @StructuredPrompt. It does not work with @UserMessage or @SystemMessage annotations (those are handled by AiServices).

  • Expecting Validation: The processor does not validate that all variables in the template are present as fields. Missing fields result in unreplaced {{variable}} placeholders in the output.

Edge Cases

  • Empty Object Fields: All fields being null or empty results in a prompt with "null" or "" substituted.

  • Complex Object Graphs: Only direct fields of the annotated class are processed. Nested object properties are not automatically expanded.

@StructuredPrompt({"User: {{user.name}}"}) // This does NOT work
class Prompt {
    User user; // user.name is NOT automatically extracted
}

// Must do this instead:
@StructuredPrompt({"User: {{userName}}"})
class Prompt {
    String userName; // Flatten the structure
}
  • Collections and Arrays: If a field is a List or Array, its toString() representation is used (e.g., "[item1, item2]").
@StructuredPrompt({"Items: {{items}}"})
class Prompt {
    List<String> items = List.of("A", "B");
}
// Result: "Items: [A, B]"
  • Multiple Annotations: A class can only have one @StructuredPrompt annotation. Multiple annotations would be a compilation error.

Performance Notes

  • Reflection Cost: Each call to toPrompt() involves reflection to read the annotation and field values. This is slower than PromptTemplate or direct string building.

  • No Caching: By default, annotation metadata is read each time. For repeated calls with the same class, consider caching the template structure (advanced optimization).

  • Object Creation Cost: Creating new instances of annotated classes has normal Java object creation overhead.

  • Recommendation: Use StructuredPrompt for complex, maintainable prompt structures where readability matters more than raw performance. For high-frequency, simple prompts, use PromptTemplate.

Exception Handling

// Exceptions thrown by StructuredPromptProcessor.toPrompt()
- IllegalArgumentException: When object is not annotated with @StructuredPrompt
- NullPointerException: When null object is passed
- RuntimeException: Wrapping reflection errors (IllegalAccessException, etc.) - rare

Exception Examples:

import dev.langchain4j.model.input.structured.StructuredPromptProcessor;

// IllegalArgumentException - missing annotation
class Plain { String name; }
try {
    StructuredPromptProcessor.toPrompt(new Plain());
} catch (IllegalArgumentException e) {
    // Handle: Object must be annotated with @StructuredPrompt
}

// NullPointerException - null input
try {
    StructuredPromptProcessor.toPrompt(null);
} catch (NullPointerException e) {
    // Handle: Input cannot be null
}

Related APIs

  • @StructuredPrompt: The annotation this processor requires
  • Prompt: The return type of toPrompt()
  • PromptTemplate: Alternative approach without reflection
  • AiServices + @UserMessage/@SystemMessage: Alternative for integrating prompts directly into service interfaces

Special Variables

PromptTemplate automatically provides these special variables:

  • {{current_date}} - Current date (LocalDate.now())
  • {{current_time}} - Current time (LocalTime.now())
  • {{current_date_time}} - Current date and time (LocalDateTime.now())

Example:

PromptTemplate template = PromptTemplate.from(
    "Today is {{current_date}}. Generate a report for {{topic}}."
);
Prompt prompt = template.apply(Map.of("topic", "sales"));
// Result: "Today is 2025-01-15. Generate a report for sales."

Customizing Date/Time with Clock:

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;

// Fixed clock for testing
Clock fixedClock = Clock.fixed(Instant.parse("2025-06-15T10:30:00Z"), ZoneId.of("UTC"));
PromptTemplate template = PromptTemplate.from(
    "Report for {{current_date}}",
    fixedClock
);
Prompt prompt = template.apply(Map.of());
// Result: "Report for 2025-06-15" (always same date)

Variable Validation Patterns

Pre-Apply Validation

Always validate variables before applying to templates to avoid unreplaced placeholders and runtime errors.

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class PromptValidator {
    /**
     * Validate that all required variables are present and non-null
     */
    public static void validateVariables(Map<String, Object> variables, Set<String> requiredKeys) {
        for (String key : requiredKeys) {
            if (!variables.containsKey(key)) {
                throw new IllegalArgumentException("Missing required variable: " + key);
            }
            if (variables.get(key) == null) {
                throw new IllegalArgumentException("Variable cannot be null: " + key);
            }
        }
    }

    /**
     * Validate and sanitize string inputs
     */
    public static Map<String, Object> sanitizeInputs(Map<String, Object> variables) {
        Map<String, Object> sanitized = new HashMap<>();
        for (Map.Entry<String, Object> entry : variables.entrySet()) {
            Object value = entry.getValue();
            if (value == null) {
                sanitized.put(entry.getKey(), ""); // Replace null with empty string
            } else if (value instanceof String) {
                // Trim whitespace
                sanitized.put(entry.getKey(), ((String) value).trim());
            } else {
                sanitized.put(entry.getKey(), value);
            }
        }
        return sanitized;
    }
}

// Usage
PromptTemplate template = PromptTemplate.from("Hello {{name}}, topic: {{topic}}");
Map<String, Object> vars = Map.of("name", "Alice", "topic", "Java");

// Validate before applying
PromptValidator.validateVariables(vars, Set.of("name", "topic"));
Prompt prompt = template.apply(vars);

Post-Apply Validation

Check the resulting prompt for unreplaced variables.

public class PromptChecker {
    /**
     * Check if prompt contains any unreplaced variables
     * @return true if prompt is valid (no {{}} placeholders remain)
     */
    public static boolean isValid(Prompt prompt) {
        String text = prompt.text();
        return !text.contains("{{") && !text.contains("}}");
    }

    /**
     * Get list of unreplaced variables
     */
    public static List<String> getUnreplacedVariables(Prompt prompt) {
        List<String> unreplaced = new ArrayList<>();
        String text = prompt.text();
        Pattern pattern = Pattern.compile("\\{\\{([^}]+)\\}\\}");
        Matcher matcher = pattern.matcher(text);
        while (matcher.find()) {
            unreplaced.add(matcher.group(1));
        }
        return unreplaced;
    }
}

// Usage
Prompt prompt = template.apply(variables);
if (!PromptChecker.isValid(prompt)) {
    List<String> missing = PromptChecker.getUnreplacedVariables(prompt);
    throw new IllegalStateException("Unreplaced variables: " + missing);
}

Builder Pattern for Complex Prompts

public class SafePromptBuilder {
    private final PromptTemplate template;
    private final Map<String, Object> variables = new HashMap<>();
    private final Set<String> requiredVariables;

    public SafePromptBuilder(String templateString, Set<String> requiredVariables) {
        this.template = PromptTemplate.from(templateString);
        this.requiredVariables = requiredVariables;
    }

    public SafePromptBuilder withVariable(String key, Object value) {
        if (value == null) {
            throw new IllegalArgumentException("Variable value cannot be null: " + key);
        }
        variables.put(key, value);
        return this;
    }

    public Prompt build() {
        // Validate all required variables are present
        for (String required : requiredVariables) {
            if (!variables.containsKey(required)) {
                throw new IllegalStateException("Missing required variable: " + required);
            }
        }

        Prompt prompt = template.apply(variables);

        // Post-validation
        if (!PromptChecker.isValid(prompt)) {
            throw new IllegalStateException("Prompt contains unreplaced variables");
        }

        return prompt;
    }
}

// Usage
Prompt prompt = new SafePromptBuilder(
    "Hello {{name}}, help with {{topic}}",
    Set.of("name", "topic")
)
    .withVariable("name", "Alice")
    .withVariable("topic", "Java")
    .build();

Prompt Engineering Best Practices

1. Structure Your Prompts Clearly

Use clear sections and formatting to improve LLM understanding.

PromptTemplate structuredTemplate = PromptTemplate.from(
    """
    # Role
    You are an expert {{role}}.

    # Context
    {{context}}

    # Task
    {{task}}

    # Constraints
    - {{constraint1}}
    - {{constraint2}}

    # Output Format
    {{format}}
    """
);

2. Be Specific and Explicit

Avoid ambiguity by providing explicit instructions.

// VAGUE
PromptTemplate vague = PromptTemplate.from("Summarize {{text}}");

// BETTER
PromptTemplate specific = PromptTemplate.from(
    """
    Summarize the following text in exactly 3 bullet points.
    Each bullet point should be no more than 15 words.
    Focus on the main ideas and key takeaways.

    Text: {{text}}
    """
);

3. Provide Examples (Few-Shot Learning)

Include examples in your templates to guide the LLM.

PromptTemplate fewShot = PromptTemplate.from(
    """
    Extract the sentiment from the following reviews.

    Examples:
    Review: "This product is amazing!" → Sentiment: Positive
    Review: "Terrible experience, do not buy." → Sentiment: Negative
    Review: "It's okay, nothing special." → Sentiment: Neutral

    Review: {{review}} → Sentiment:
    """
);

4. Use Delimiters for Clarity

Separate different parts of the input with clear delimiters.

PromptTemplate withDelimiters = PromptTemplate.from(
    """
    Translate the text between the ### markers from {{source_lang}} to {{target_lang}}.

    ###
    {{text}}
    ###

    Translation:
    """
);

5. Specify Output Format

Tell the LLM exactly how you want the response formatted.

PromptTemplate jsonOutput = PromptTemplate.from(
    """
    Analyze the following text and extract key information.
    Return your response as a JSON object with these fields:
    - summary (string): One sentence summary
    - sentiment (string): positive/negative/neutral
    - key_points (array): List of key points

    Text: {{text}}

    JSON Response:
    """
);

6. Handle Edge Cases in Prompts

Design prompts that guide the LLM when inputs are unusual.

PromptTemplate edgeCaseHandling = PromptTemplate.from(
    """
    Answer the following question: {{question}}

    Instructions:
    - If the question is unclear, ask for clarification
    - If you don't know the answer, say "I don't have enough information"
    - If the question is inappropriate, politely decline to answer
    - Otherwise, provide a clear, concise answer
    """
);

7. Use Temperature and Context Windows Wisely

Remember that prompts interact with model parameters.

// For factual/deterministic tasks - use low temperature
PromptTemplate factual = PromptTemplate.from(
    "What is the capital of {{country}}?"
);
// Use with temperature = 0.0 to 0.3

// For creative tasks - use higher temperature
PromptTemplate creative = PromptTemplate.from(
    "Write a creative story about {{topic}}"
);
// Use with temperature = 0.7 to 1.0

8. System vs User Messages

Use different message types appropriately.

// System message - set behavior and context
Prompt systemPrompt = Prompt.from(
    "You are a helpful assistant specialized in {{domain}}. " +
    "Always provide accurate, well-sourced information."
).toSystemMessage();

// User message - specific request
Prompt userPrompt = Prompt.from(
    "Explain {{topic}} in simple terms"
).toUserMessage();

9. Prompt Chaining for Complex Tasks

Break complex tasks into multiple prompts.

// Step 1: Extract information
PromptTemplate extractTemplate = PromptTemplate.from(
    "Extract all dates, names, and locations from: {{text}}"
);

// Step 2: Analyze extracted information
PromptTemplate analyzeTemplate = PromptTemplate.from(
    "Given this extracted information: {{extracted}}, " +
    "provide a timeline of events"
);

// Execute in sequence
Prompt extract = extractTemplate.apply(Map.of("text", input));
String extractedInfo = llm.generate(extract.text());

Prompt analyze = analyzeTemplate.apply(Map.of("extracted", extractedInfo));
String timeline = llm.generate(analyze.text());

10. Version and Test Your Prompts

Treat prompts as code - version, test, and iterate.

public class PromptLibrary {
    // Version prompts
    public static final String SUMMARIZATION_V1 = "Summarize: {{text}}";
    public static final String SUMMARIZATION_V2 = """
        Provide a concise summary of the following text in 3-5 sentences.
        Focus on main ideas and key takeaways.

        Text: {{text}}
        """;

    private static final PromptTemplate CURRENT_SUMMARIZATION =
        PromptTemplate.from(SUMMARIZATION_V2);

    public static Prompt createSummarizationPrompt(String text) {
        return CURRENT_SUMMARIZATION.apply(Map.of("text", text));
    }
}

Testing Patterns

Unit Testing Prompts

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class PromptTemplateTest {
    @Test
    void testVariableReplacement() {
        PromptTemplate template = PromptTemplate.from("Hello {{name}}");
        Prompt prompt = template.apply(Map.of("name", "Alice"));
        assertEquals("Hello Alice", prompt.text());
    }

    @Test
    void testMissingVariable() {
        PromptTemplate template = PromptTemplate.from("Hello {{name}}");
        Prompt prompt = template.apply(Map.of("other", "value"));
        // Should remain unreplaced
        assertEquals("Hello {{name}}", prompt.text());
    }

    @Test
    void testMultipleVariables() {
        PromptTemplate template = PromptTemplate.from(
            "{{greeting}} {{name}}, topic: {{topic}}"
        );
        Prompt prompt = template.apply(Map.of(
            "greeting", "Hello",
            "name", "Bob",
            "topic", "Testing"
        ));
        assertEquals("Hello Bob, topic: Testing", prompt.text());
    }

    @Test
    void testSpecialVariables() {
        PromptTemplate template = PromptTemplate.from(
            "Date: {{current_date}}"
        );
        Prompt prompt = template.apply(Map.of());
        assertTrue(prompt.text().startsWith("Date: "));
        assertTrue(prompt.text().matches("Date: \\d{4}-\\d{2}-\\d{2}"));
    }

    @Test
    void testFixedClock() {
        Clock fixedClock = Clock.fixed(
            Instant.parse("2025-06-15T10:00:00Z"),
            ZoneId.of("UTC")
        );
        PromptTemplate template = PromptTemplate.from(
            "{{current_date}}",
            fixedClock
        );
        Prompt prompt = template.apply(Map.of());
        assertEquals("2025-06-15", prompt.text());
    }

    @Test
    void testNullValueHandling() {
        PromptTemplate template = PromptTemplate.from("Value: {{value}}");
        Prompt prompt = template.apply(Map.of("value", null));
        assertEquals("Value: null", prompt.text());
    }

    @Test
    void testEmptyTemplate() {
        PromptTemplate template = PromptTemplate.from("");
        Prompt prompt = template.apply(Map.of());
        assertEquals("", prompt.text());
    }
}

Testing Structured Prompts

class StructuredPromptTest {
    @StructuredPrompt({"Hello {{name}}"})
    static class SimplePrompt {
        String name;
        SimplePrompt(String name) { this.name = name; }
    }

    @Test
    void testStructuredPromptProcessing() {
        SimplePrompt promptData = new SimplePrompt("Alice");
        Prompt prompt = StructuredPromptProcessor.toPrompt(promptData);
        assertEquals("Hello Alice", prompt.text());
    }

    @Test
    void testMultiLineStructured() {
        @StructuredPrompt({
            "Line 1: {{var1}}",
            "Line 2: {{var2}}"
        })
        class MultiLine {
            String var1 = "Value1";
            String var2 = "Value2";
        }

        Prompt prompt = StructuredPromptProcessor.toPrompt(new MultiLine());
        String expected = "Line 1: Value1\nLine 2: Value2";
        assertEquals(expected, prompt.text());
    }

    @Test
    void testCustomDelimiter() {
        @StructuredPrompt(
            value = {"Part1", "Part2", "Part3"},
            delimiter = " | "
        )
        class CustomDelim {
        }

        Prompt prompt = StructuredPromptProcessor.toPrompt(new CustomDelim());
        assertEquals("Part1 | Part2 | Part3", prompt.text());
    }

    @Test
    void testMissingAnnotation() {
        class NotAnnotated {
            String field;
        }
        assertThrows(IllegalArgumentException.class, () -> {
            StructuredPromptProcessor.toPrompt(new NotAnnotated());
        });
    }

    @Test
    void testNullInput() {
        assertThrows(NullPointerException.class, () -> {
            StructuredPromptProcessor.toPrompt(null);
        });
    }
}

Integration Testing with LLMs

class PromptIntegrationTest {
    private ChatLanguageModel model; // Initialize with your model

    @Test
    void testPromptWithActualLLM() {
        PromptTemplate template = PromptTemplate.from(
            "What is the capital of {{country}}?"
        );
        Prompt prompt = template.apply(Map.of("country", "France"));

        String response = model.generate(prompt.text());

        assertTrue(response.toLowerCase().contains("paris"));
    }

    @Test
    void testConversationFlow() {
        // System message
        Prompt systemPrompt = Prompt.from(
            "You are a helpful math tutor"
        );

        // User question
        PromptTemplate questionTemplate = PromptTemplate.from(
            "Explain {{concept}} in simple terms"
        );
        Prompt userPrompt = questionTemplate.apply(
            Map.of("concept", "quadratic equations")
        );

        // Test the flow
        ChatMemory memory = MessageWindowChatMemory.withMaxMessages(10);
        memory.add(systemPrompt.toSystemMessage());
        memory.add(userPrompt.toUserMessage());

        // Generate response
        Response<AiMessage> response = model.generate(memory.messages());
        assertNotNull(response.content());
    }
}

Validation Testing

class PromptValidationTest {
    @Test
    void testVariableValidation() {
        Map<String, Object> variables = new HashMap<>();
        variables.put("name", "Alice");
        variables.put("topic", "Java");

        Set<String> required = Set.of("name", "topic", "level");

        assertThrows(IllegalArgumentException.class, () -> {
            PromptValidator.validateVariables(variables, required);
        });
    }

    @Test
    void testSanitization() {
        Map<String, Object> variables = Map.of(
            "name", "  Alice  ",
            "topic", "Java   "
        );

        Map<String, Object> sanitized = PromptValidator.sanitizeInputs(variables);

        assertEquals("Alice", sanitized.get("name"));
        assertEquals("Java", sanitized.get("topic"));
    }

    @Test
    void testUnreplacedVariableDetection() {
        Prompt prompt = Prompt.from("Hello {{name}}, topic: {{topic}}");

        List<String> unreplaced = PromptChecker.getUnreplacedVariables(prompt);

        assertEquals(2, unreplaced.size());
        assertTrue(unreplaced.contains("name"));
        assertTrue(unreplaced.contains("topic"));
    }
}

Snapshot Testing for Prompts

class PromptSnapshotTest {
    /**
     * Snapshot testing helps detect unintended prompt changes
     */
    @Test
    void testPromptSnapshot() {
        PromptTemplate template = PromptTemplate.from(
            """
            # Role
            You are an expert {{role}}.

            # Task
            {{task}}
            """
        );

        Prompt prompt = template.apply(Map.of(
            "role", "Java developer",
            "task", "Review this code"
        ));

        String expected = """
            # Role
            You are an expert Java developer.

            # Task
            Review this code
            """;

        assertEquals(expected, prompt.text());
    }
}

Performance Testing

class PromptPerformanceTest {
    @Test
    void testTemplateReusability() {
        // Create template once
        PromptTemplate template = PromptTemplate.from("Hello {{name}}");

        // Measure reuse performance
        long start = System.nanoTime();
        for (int i = 0; i < 10000; i++) {
            template.apply(Map.of("name", "User" + i));
        }
        long duration = System.nanoTime() - start;

        System.out.println("10000 applies took: " + duration / 1_000_000 + "ms");
        // Should be < 100ms for simple templates
        assertTrue(duration < 100_000_000);
    }

    @Test
    void comparePromptApproaches() {
        String name = "Alice";

        // Approach 1: Direct string
        long start1 = System.nanoTime();
        for (int i = 0; i < 10000; i++) {
            String prompt = "Hello " + name;
        }
        long time1 = System.nanoTime() - start1;

        // Approach 2: PromptTemplate
        PromptTemplate template = PromptTemplate.from("Hello {{name}}");
        long start2 = System.nanoTime();
        for (int i = 0; i < 10000; i++) {
            template.apply(Map.of("name", name));
        }
        long time2 = System.nanoTime() - start2;

        // Approach 3: StructuredPrompt
        @StructuredPrompt({"Hello {{name}}"})
        class P { String name; P(String n) { this.name = n; }}
        long start3 = System.nanoTime();
        for (int i = 0; i < 10000; i++) {
            StructuredPromptProcessor.toPrompt(new P(name));
        }
        long time3 = System.nanoTime() - start3;

        System.out.println("Direct: " + time1 / 1_000_000 + "ms");
        System.out.println("Template: " + time2 / 1_000_000 + "ms");
        System.out.println("Structured: " + time3 / 1_000_000 + "ms");

        // PromptTemplate should be faster than StructuredPrompt (reflection overhead)
        assertTrue(time2 < time3);
    }
}

Related APIs

Core Message Types

  • UserMessage (dev.langchain4j.data.message.UserMessage): Represents user input messages
  • SystemMessage (dev.langchain4j.data.message.SystemMessage): Represents system-level instructions
  • AiMessage (dev.langchain4j.data.message.AiMessage): Represents AI-generated responses

AiServices Integration

  • @UserMessage (dev.langchain4j.service.UserMessage): Annotation for defining user message templates in service interfaces
  • @SystemMessage (dev.langchain4j.service.SystemMessage): Annotation for defining system message templates
  • @V (dev.langchain4j.service.V): Annotation for marking parameters as template variables

Example:

interface Assistant {
    @SystemMessage("You are an expert in {{domain}}")
    @UserMessage("Explain {{topic}} in simple terms")
    String explain(@V("domain") String domain, @V("topic") String topic);
}

Memory Management

  • ChatMemory (dev.langchain4j.memory.ChatMemory): Stores conversation history including prompts
  • MessageWindowChatMemory (dev.langchain4j.memory.chat.MessageWindowChatMemory): Sliding window memory for recent messages

LLM Integration

  • ChatLanguageModel (dev.langchain4j.model.chat.ChatLanguageModel): Interface for chat-based LLMs that consume prompts
  • StreamingChatLanguageModel (dev.langchain4j.model.chat.StreamingChatLanguageModel): For streaming responses
  • ChatRequest / ChatResponse (dev.langchain4j.model.chat.request / dev.langchain4j.model.chat.response): Request/response objects that include prompts

Advanced Patterns

  • ConversationalChain (dev.langchain4j.chain.ConversationalChain): Deprecated - use AiServices instead
  • RetrievalAugmentor (dev.langchain4j.rag.RetrievalAugmentor): For RAG patterns where prompts include retrieved context
  • Guardrails (dev.langchain4j.service.guardrail): Input/output validation that can be applied to prompts

Utility Classes

  • Clock (java.time.Clock): For controlling date/time variables in templates (useful for testing)
  • Map (java.util.Map): For providing variables to templates
  • Pattern / Matcher (java.util.regex): For validating and parsing prompt content

Install with Tessl CLI

npx tessl i tessl/maven-dev-langchain4j--langchain4j@1.11.0

docs

ai-services.md

chains.md

classification.md

data-types.md

document-processing.md

embedding-store.md

guardrails.md

index.md

memory.md

messages.md

models.md

output-parsing.md

prompts.md

rag.md

request-response.md

spi.md

tools.md

README.md

tile.json