Skip to content
yisusvii Blog
Go back

Advanced Prompt Engineering Techniques in Spring AI

Suggest Changes

Prompt engineering is the art and science of crafting inputs that guide Large Language Models (LLMs) to produce accurate, useful, and reliable outputs. While the concept sounds simple—just write better prompts—mastering it requires understanding both the capabilities and limitations of AI models.

Spring AI provides powerful abstractions for prompt management, making it easier to implement sophisticated prompting strategies in enterprise Java applications. In this comprehensive guide, we’ll explore advanced techniques that can dramatically improve the quality and consistency of your AI-powered features.

The Foundation: Understanding Prompt Anatomy

Before diving into advanced techniques, let’s establish a solid foundation of how prompts work with Spring AI.

┌─────────────────────────────────────────────────────────────────────┐
│                      Prompt Structure                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │ SYSTEM MESSAGE (Role Definition)                               │ │
│  │ "You are a senior software architect..."                       │ │
│  └────────────────────────────────────────────────────────────────┘ │
│                              │                                       │
│                              ▼                                       │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │ CONTEXT (Background Information)                               │ │
│  │ "The application uses Spring Boot 3.x and PostgreSQL..."      │ │
│  └────────────────────────────────────────────────────────────────┘ │
│                              │                                       │
│                              ▼                                       │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │ EXAMPLES (Few-Shot Learning)                                   │ │
│  │ "Example 1: Input: ... Output: ..."                            │ │
│  └────────────────────────────────────────────────────────────────┘ │
│                              │                                       │
│                              ▼                                       │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │ INSTRUCTIONS (Task Definition)                                 │ │
│  │ "Analyze the code and identify security vulnerabilities..."   │ │
│  └────────────────────────────────────────────────────────────────┘ │
│                              │                                       │
│                              ▼                                       │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │ OUTPUT FORMAT (Structure Constraints)                          │ │
│  │ "Return a JSON object with the following schema..."            │ │
│  └────────────────────────────────────────────────────────────────┘ │
│                              │                                       │
│                              ▼                                       │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │ USER INPUT (Actual Query/Data)                                 │ │
│  │ "Here is the code to analyze: ..."                             │ │
│  └────────────────────────────────────────────────────────────────┘ │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Spring AI Prompt Templates

Spring AI provides template-based prompting that integrates with Spring’s resource management:

@Service
public class PromptTemplateService {
    
    private final ChatClient chatClient;
    private final ObjectMapper objectMapper;
    
    @Value("classpath:/prompts/code-review.st")
    private Resource codeReviewPromptResource;
    
    public PromptTemplateService(ChatClient.Builder builder, ObjectMapper objectMapper) {
        this.chatClient = builder.build();
        this.objectMapper = objectMapper;
    }
    
    public CodeReviewResult reviewCode(String code, String language) {
        PromptTemplate promptTemplate = new PromptTemplate(codeReviewPromptResource);
        
        Prompt prompt = promptTemplate.create(Map.of(
            "code", code,
            "language", language,
            "currentDate", LocalDate.now().toString()
        ));
        
        ChatResponse response = chatClient.prompt(prompt)
            .call()
            .chatResponse();
        
        // Parse JSON response into typed object
        return objectMapper.readValue(
            response.getResult().getOutput().getContent(),
            CodeReviewResult.class
        );
    }
}

Prompt Template File (code-review.st)

You are an expert {language} code reviewer with 15 years of experience.
Today's date is {currentDate}.

## Your Task
Review the following code and provide detailed feedback on:
1. Code quality and maintainability
2. Potential bugs or edge cases
3. Security vulnerabilities
4. Performance considerations
5. Adherence to best practices

## Code to Review
```{language}
{code}

Output Format

Provide your review in the following JSON format: { “overallScore”: <1-10>, “summary”: "", “issues”: [ { “severity”: “critical|high|medium|low”, “category”: "", “line”: , “description”: "", “suggestion”: "" } ], “positives”: [""], “recommendations”: [""] }


## Technique 1: Chain-of-Thought Prompting

Chain-of-Thought (CoT) prompting encourages the model to break down complex problems into steps, improving reasoning accuracy.

### Implementation

```java
@Service
public class ChainOfThoughtService {
    
    private final ChatClient chatClient;
    
    public AnalysisResult analyzeWithCoT(String problem) {
        String cotPrompt = """
            You are an expert problem solver. When analyzing problems, 
            you always think step by step.
            
            ## Problem
            %s
            
            ## Instructions
            Analyze this problem using the following approach:
            
            1. **Understanding**: First, restate the problem in your own words 
               to ensure you understand it correctly.
            
            2. **Decomposition**: Break down the problem into smaller sub-problems 
               or components that need to be addressed.
            
            3. **Analysis**: For each component, analyze the relevant factors, 
               constraints, and considerations.
            
            4. **Reasoning**: Work through the logic step by step, showing your 
               thinking process clearly.
            
            5. **Synthesis**: Combine your findings into a coherent solution.
            
            6. **Verification**: Check your solution against the original problem 
               to ensure it addresses all requirements.
            
            7. **Conclusion**: Provide your final answer with confidence level.
            
            Show all your work and reasoning.
            """.formatted(problem);
        
        return chatClient.prompt()
            .user(cotPrompt)
            .call()
            .entity(AnalysisResult.class);
    }
}

Zero-Shot CoT (Simpler Approach)

public String solveWithZeroShotCoT(String problem) {
    String prompt = """
        %s
        
        Let's think through this step by step:
        """.formatted(problem);
    
    return chatClient.prompt()
        .user(prompt)
        .call()
        .content();
}

Technique 2: Few-Shot Learning

Provide examples to guide the model’s output format and reasoning style.

@Service
public class FewShotPromptService {
    
    private final ChatClient chatClient;
    
    private static final String FEW_SHOT_TEMPLATE = """
        You are an API documentation generator. Given a Java method signature, 
        generate comprehensive JavaDoc documentation.
        
        ## Examples
        
        ### Example 1
        **Input:**
        ```java
        public User findUserById(Long id) throws UserNotFoundException
        ```
        
        **Output:**
        ```java
        /**
         * Retrieves a user by their unique identifier.
         *
         * @param id the unique identifier of the user to retrieve, must not be null
         * @return the User object matching the given ID
         * @throws UserNotFoundException if no user exists with the specified ID
         * @since 1.0
         */
        public User findUserById(Long id) throws UserNotFoundException
        ```
        
        ### Example 2
        **Input:**
        ```java
        public List<Order> getOrdersByCustomer(String customerId, LocalDate from, LocalDate to)
        ```
        
        **Output:**
        ```java
        /**
         * Retrieves all orders placed by a specific customer within a date range.
         * <p>
         * The date range is inclusive on both ends. If no orders are found within
         * the specified criteria, an empty list is returned.
         *
         * @param customerId the unique identifier of the customer, must not be null or empty
         * @param from the start date of the range (inclusive), must not be null
         * @param to the end date of the range (inclusive), must not be null and must be 
         *           after or equal to {@code from}
         * @return a list of orders matching the criteria, never null
         * @since 1.2
         */
        public List<Order> getOrdersByCustomer(String customerId, LocalDate from, LocalDate to)
        ```
        
        ### Example 3
        **Input:**
        ```java
        public void processPayment(PaymentRequest request) throws PaymentException, ValidationException
        ```
        
        **Output:**
        ```java
        /**
         * Processes a payment transaction based on the provided request.
         * <p>
         * This method handles the complete payment lifecycle including validation,
         * authorization, and capture. The operation is idempotent when using the
         * same idempotency key in the request.
         *
         * @param request the payment request containing transaction details, must not be null
         * @throws PaymentException if the payment fails due to insufficient funds, 
         *         declined card, or gateway errors
         * @throws ValidationException if the request contains invalid or missing required fields
         * @see PaymentRequest
         * @see PaymentResult
         * @since 2.0
         */
        public void processPayment(PaymentRequest request) throws PaymentException, ValidationException
        ```
        
        ## Your Task
        Now generate documentation for this method:
        
        **Input:**
        ```java
        %s
        ```
        
        **Output:**
        """;
    
    public String generateDocumentation(String methodSignature) {
        return chatClient.prompt()
            .user(FEW_SHOT_TEMPLATE.formatted(methodSignature))
            .call()
            .content();
    }
}

Dynamic Few-Shot Selection

Select the most relevant examples based on the input:

@Service
public class DynamicFewShotService {
    
    private final VectorStore exampleStore;
    private final ChatClient chatClient;
    
    public String generateWithDynamicExamples(String task, String input) {
        // Find similar examples from vector store
        List<Document> similarExamples = exampleStore.similaritySearch(
            SearchRequest.query(input).withTopK(3)
        );
        
        // Build prompt with selected examples
        StringBuilder promptBuilder = new StringBuilder();
        promptBuilder.append("## Task\n").append(task).append("\n\n");
        promptBuilder.append("## Examples\n\n");
        
        for (int i = 0; i < similarExamples.size(); i++) {
            Document example = similarExamples.get(i);
            promptBuilder.append("### Example ").append(i + 1).append("\n");
            promptBuilder.append("**Input:** ").append(example.getMetadata().get("input")).append("\n");
            promptBuilder.append("**Output:** ").append(example.getMetadata().get("output")).append("\n\n");
        }
        
        promptBuilder.append("## Your Input\n").append(input).append("\n\n");
        promptBuilder.append("## Your Output\n");
        
        return chatClient.prompt()
            .user(promptBuilder.toString())
            .call()
            .content();
    }
}

Technique 3: Structured Output with JSON Mode

Ensure consistent, parseable outputs by specifying exact JSON schemas:

@Service
public class StructuredOutputService {
    
    private final ChatClient chatClient;
    private final ObjectMapper objectMapper;
    
    public StructuredOutputService(ChatClient.Builder builder, ObjectMapper objectMapper) {
        this.chatClient = builder.build();
        this.objectMapper = objectMapper;
    }
    
    public SentimentAnalysis analyzeSentiment(String text) {
        String prompt = """
            Analyze the sentiment of the following text and extract key information.
            
            ## Text
            %s
            
            ## Output Schema
            You MUST respond with a valid JSON object matching this exact schema:
            {
                "sentiment": "positive" | "negative" | "neutral" | "mixed",
                "confidence": <number between 0 and 1>,
                "emotions": [
                    {
                        "emotion": "<emotion name>",
                        "intensity": <1-10>
                    }
                ],
                "keyPhrases": ["<phrase1>", "<phrase2>", ...],
                "summary": "<one sentence summary of the sentiment>",
                "languageDetected": "<ISO 639-1 code>"
            }
            
            IMPORTANT: Return ONLY the JSON object, no additional text or markdown.
            """.formatted(text);
        
        ChatResponse response = chatClient.prompt()
            .user(prompt)
            .options(ChatOptionsBuilder.builder()
                .withResponseFormat(ResponseFormat.JSON)
                .build())
            .call()
            .chatResponse();
        
        return objectMapper.readValue(
            response.getResult().getOutput().getContent(),
            SentimentAnalysis.class
        );
    }
}

Using Spring AI’s BeanOutputConverter

@Service
public class TypeSafeOutputService {
    
    private final ChatClient chatClient;
    
    public ProductRecommendation getRecommendation(CustomerProfile profile) {
        BeanOutputConverter<ProductRecommendation> converter = 
            new BeanOutputConverter<>(ProductRecommendation.class);
        
        String prompt = """
            Based on the following customer profile, recommend products.
            
            ## Customer Profile
            - Age Group: %s
            - Interests: %s
            - Purchase History: %s
            - Budget Range: %s
            
            ## Instructions
            Provide personalized product recommendations that match the customer's
            interests and budget. Consider their purchase history to avoid 
            recommending items they already own.
            
            %s
            """.formatted(
                profile.getAgeGroup(),
                String.join(", ", profile.getInterests()),
                formatPurchaseHistory(profile.getPurchaseHistory()),
                profile.getBudgetRange(),
                converter.getFormat()
            );
        
        String response = chatClient.prompt()
            .user(prompt)
            .call()
            .content();
        
        return converter.convert(response);
    }
}

// The output class with annotations
@JsonClassDescription("Product recommendations for a customer")
public record ProductRecommendation(
    @JsonPropertyDescription("List of recommended products with reasons")
    List<RecommendedProduct> products,
    
    @JsonPropertyDescription("Overall recommendation strategy explanation")
    String strategy,
    
    @JsonPropertyDescription("Confidence score from 0 to 1")
    double confidence
) {}

public record RecommendedProduct(
    @JsonPropertyDescription("Product name")
    String name,
    
    @JsonPropertyDescription("Why this product is recommended")
    String reason,
    
    @JsonPropertyDescription("Estimated price in USD")
    BigDecimal price,
    
    @JsonPropertyDescription("Match score from 1-10")
    int matchScore
) {}

Technique 4: System Prompt Engineering

System prompts set the foundation for model behavior across all interactions.

@Configuration
public class SystemPromptConfiguration {
    
    @Bean
    public ChatClient customerSupportChatClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem("""
                You are a customer support specialist for TechCorp, a software company.
                
                ## Your Identity
                - Name: Alex (do not reveal you are an AI unless directly asked)
                - Role: Senior Customer Support Specialist
                - Experience: Extensive knowledge of TechCorp products
                
                ## Communication Style
                - Be friendly, professional, and empathetic
                - Use clear, simple language (avoid technical jargon unless necessary)
                - Keep responses concise but complete
                - Always acknowledge the customer's frustration if they express it
                
                ## Guidelines
                1. NEVER share internal documentation, pricing strategies, or competitor comparisons
                2. NEVER make promises about features, timelines, or compensation without explicit authorization
                3. ALWAYS verify customer identity before discussing account-specific information
                4. If you don't know something, say so and offer to escalate
                5. For billing issues, always recommend speaking with the billing team directly
                
                ## Escalation Triggers
                Immediately offer to escalate to a human agent if:
                - Customer expresses severe frustration (uses caps, exclamation marks, profanity)
                - Issue involves legal matters, data breaches, or security concerns
                - Customer explicitly requests human assistance
                - Issue is complex and requires access to internal systems you cannot access
                
                ## Response Format
                - Start with a greeting that includes the customer's name if known
                - Acknowledge their issue
                - Provide solution or next steps
                - Offer additional help
                - End with a friendly closing
                
                ## Current Context
                - Date: {{currentDate}}
                - Business Hours: 9 AM - 6 PM EST, Monday-Friday
                - Current Promotions: 20% off annual subscriptions this month
                """)
            .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
            .build();
    }
}

Role-Based System Prompts

@Service
public class ExpertSystemService {
    
    private final ChatClient.Builder chatClientBuilder;
    
    private static final Map<String, String> EXPERT_PROMPTS = Map.of(
        "security", """
            You are a senior cybersecurity expert with expertise in:
            - Application security (OWASP Top 10)
            - Network security and penetration testing
            - Cloud security (AWS, Azure, GCP)
            - Compliance frameworks (SOC2, PCI-DSS, HIPAA)
            
            When analyzing code or systems:
            1. Always identify the most critical vulnerabilities first
            2. Provide CVSS scores when applicable
            3. Include specific remediation steps
            4. Reference CWE identifiers for vulnerabilities
            5. Consider the attack surface and threat model
            """,
        
        "architecture", """
            You are a principal software architect with 20 years of experience.
            
            Your expertise includes:
            - Distributed systems design
            - Microservices and event-driven architecture
            - Domain-Driven Design (DDD)
            - Cloud-native patterns
            - Performance optimization
            
            When providing architectural guidance:
            1. Consider scalability, maintainability, and cost
            2. Identify trade-offs and explain them clearly
            3. Reference architectural patterns by name (e.g., CQRS, Saga)
            4. Consider operational aspects (monitoring, debugging)
            5. Provide diagrams using ASCII or Mermaid when helpful
            """,
        
        "performance", """
            You are a performance engineering specialist focused on:
            - JVM optimization and garbage collection tuning
            - Database query optimization
            - Caching strategies
            - Load testing and capacity planning
            - Profiling and bottleneck identification
            
            When analyzing performance issues:
            1. Start with measurements, not assumptions
            2. Identify the limiting resource (CPU, memory, I/O, network)
            3. Calculate theoretical limits and compare to actual
            4. Recommend specific, measurable optimizations
            5. Consider the cost-benefit of each optimization
            """
    );
    
    public String consultExpert(String expertType, String query) {
        String systemPrompt = EXPERT_PROMPTS.getOrDefault(
            expertType.toLowerCase(),
            "You are a helpful technical assistant."
        );
        
        ChatClient expertClient = chatClientBuilder
            .defaultSystem(systemPrompt)
            .build();
        
        return expertClient.prompt()
            .user(query)
            .call()
            .content();
    }
}

Technique 5: Self-Consistency Prompting

Generate multiple responses and aggregate for more reliable outputs:

@Service
public class SelfConsistencyService {
    
    private final ChatClient chatClient;
    private final ObjectMapper objectMapper;
    private static final int NUM_SAMPLES = 5;
    
    public SelfConsistencyService(ChatClient.Builder builder, ObjectMapper objectMapper) {
        this.chatClient = builder.build();
        this.objectMapper = objectMapper;
    }
    
    public ConsensusResult getConsensusAnswer(String question) {
        // Generate multiple independent responses
        List<String> responses = IntStream.range(0, NUM_SAMPLES)
            .parallel()
            .mapToObj(i -> generateResponse(question))
            .collect(Collectors.toList());
        
        // Aggregate responses
        return aggregateResponses(question, responses);
    }
    
    private String generateResponse(String question) {
        return chatClient.prompt()
            .user("""
                Answer the following question. Think through it carefully.
                
                Question: %s
                
                Provide your answer directly without excessive explanation.
                """.formatted(question))
            .options(ChatOptionsBuilder.builder()
                .withTemperature(0.7) // Higher temperature for diversity
                .build())
            .call()
            .content();
    }
    
    private ConsensusResult aggregateResponses(String question, List<String> responses) {
        String aggregationPrompt = """
            You have been given multiple responses to the same question.
            Analyze these responses and determine the consensus answer.
            
            ## Question
            %s
            
            ## Responses
            %s
            
            ## Task
            1. Identify the most common answer or theme across responses
            2. Note any significant disagreements
            3. Provide the consensus answer with confidence level
            
            Return your analysis as JSON:
            {
                "consensusAnswer": "<the agreed-upon answer>",
                "confidence": <0-1>,
                "agreementLevel": "unanimous|strong|moderate|weak|no_consensus",
                "dissenting Views": ["<any significantly different answers>"],
                "reasoning": "<why this is the consensus>"
            }
            """.formatted(
                question,
                IntStream.range(0, responses.size())
                    .mapToObj(i -> "Response " + (i + 1) + ": " + responses.get(i))
                    .collect(Collectors.joining("\n\n"))
            );
        
        String result = chatClient.prompt()
            .user(aggregationPrompt)
            .options(ChatOptionsBuilder.builder()
                .withTemperature(0.0) // Low temperature for consistent aggregation
                .build())
            .call()
            .content();
        
        return objectMapper.readValue(result, ConsensusResult.class);
    }
}

Technique 6: Prompt Chaining

Break complex tasks into sequential steps with specialized prompts:

@Service
public class PromptChainingService {
    
    private final ChatClient chatClient;
    
    public BusinessAnalysisReport analyzeBusinessRequirements(String requirements) {
        // Step 1: Extract key entities and actors
        ExtractedEntities entities = extractEntities(requirements);
        
        // Step 2: Identify use cases
        List<UseCase> useCases = identifyUseCases(requirements, entities);
        
        // Step 3: Analyze technical requirements
        TechnicalRequirements techReqs = analyzeTechnicalRequirements(requirements, useCases);
        
        // Step 4: Generate architecture recommendations
        ArchitectureRecommendation architecture = recommendArchitecture(techReqs);
        
        // Step 5: Compile final report
        return compileReport(entities, useCases, techReqs, architecture);
    }
    
    private ExtractedEntities extractEntities(String requirements) {
        String prompt = """
            Extract all business entities, actors, and their relationships from 
            these requirements.
            
            Requirements:
            %s
            
            Identify:
            1. Main entities (nouns that represent data/concepts)
            2. Actors (users, systems, external parties)
            3. Relationships between entities
            4. Attributes of each entity
            
            Return as structured JSON.
            """.formatted(requirements);
        
        return chatClient.prompt()
            .user(prompt)
            .call()
            .entity(ExtractedEntities.class);
    }
    
    private List<UseCase> identifyUseCases(String requirements, ExtractedEntities entities) {
        String prompt = """
            Based on these requirements and identified entities, define use cases.
            
            ## Requirements
            %s
            
            ## Identified Entities
            %s
            
            For each use case, specify:
            1. Name and ID
            2. Primary actor
            3. Preconditions
            4. Main success scenario (steps)
            5. Alternative flows
            6. Postconditions
            7. Business rules
            
            Return as structured JSON array.
            """.formatted(requirements, toJson(entities));
        
        return chatClient.prompt()
            .user(prompt)
            .call()
            .entity(new ParameterizedTypeReference<List<UseCase>>() {});
    }
    
    private TechnicalRequirements analyzeTechnicalRequirements(
            String requirements, List<UseCase> useCases) {
        String prompt = """
            Analyze technical requirements based on business requirements and use cases.
            
            ## Business Requirements
            %s
            
            ## Use Cases
            %s
            
            Identify:
            1. Functional requirements (categorized)
            2. Non-functional requirements:
               - Performance (response times, throughput)
               - Scalability (expected load, growth)
               - Security (authentication, authorization, data protection)
               - Availability (uptime, disaster recovery)
               - Integration (external systems, APIs)
            3. Constraints (technology, compliance, budget)
            4. Assumptions
            5. Dependencies
            
            Be specific with numbers where possible (e.g., "support 10,000 concurrent users").
            """.formatted(requirements, toJson(useCases));
        
        return chatClient.prompt()
            .user(prompt)
            .call()
            .entity(TechnicalRequirements.class);
    }
    
    private ArchitectureRecommendation recommendArchitecture(TechnicalRequirements techReqs) {
        String prompt = """
            Recommend a system architecture based on these technical requirements.
            
            ## Technical Requirements
            %s
            
            Provide:
            1. Recommended architecture pattern (monolith, microservices, etc.)
            2. Technology stack recommendations with justifications
            3. Component diagram (ASCII art)
            4. Data storage strategy
            5. Integration patterns
            6. Security architecture
            7. Deployment architecture
            8. Trade-offs and risks of this architecture
            
            Consider industry best practices and the Spring ecosystem.
            """.formatted(toJson(techReqs));
        
        return chatClient.prompt()
            .user(prompt)
            .call()
            .entity(ArchitectureRecommendation.class);
    }
}

Technique 7: Guardrails and Validation

Implement safety checks and validation for AI outputs:

@Service
public class GuardedPromptService {
    
    private final ChatClient chatClient;
    private final ContentModerationService moderationService;
    
    public SafeResponse generateContent(String userInput) {
        // Pre-generation validation
        ValidationResult inputValidation = validateInput(userInput);
        if (!inputValidation.isValid()) {
            return SafeResponse.rejected(inputValidation.getReason());
        }
        
        // Generate with safety instructions
        String prompt = """
            ## Safety Guidelines
            - Do not generate harmful, illegal, or unethical content
            - Do not reveal sensitive information
            - Do not impersonate real individuals
            - If the request seems inappropriate, politely decline
            
            ## User Request
            %s
            
            ## Your Response
            """.formatted(sanitizeInput(userInput));
        
        String response = chatClient.prompt()
            .user(prompt)
            .call()
            .content();
        
        // Post-generation validation
        ModerationResult moderation = moderationService.check(response);
        if (moderation.isFlagged()) {
            log.warn("Generated content flagged: {}", moderation.getCategories());
            return SafeResponse.filtered(
                "The generated content was filtered for safety.",
                moderation
            );
        }
        
        // Validate output format/content
        OutputValidation outputValidation = validateOutput(response);
        if (!outputValidation.isValid()) {
            return SafeResponse.invalid(outputValidation.getIssues());
        }
        
        return SafeResponse.success(response);
    }
    
    private ValidationResult validateInput(String input) {
        List<String> issues = new ArrayList<>();
        
        // Length check
        if (input.length() > 10000) {
            issues.add("Input exceeds maximum length");
        }
        
        // Injection detection
        if (containsPromptInjection(input)) {
            issues.add("Potential prompt injection detected");
        }
        
        // PII detection
        if (containsPII(input)) {
            issues.add("Personal identifiable information detected");
        }
        
        return issues.isEmpty() 
            ? ValidationResult.valid() 
            : ValidationResult.invalid(issues);
    }
    
    private boolean containsPromptInjection(String input) {
        List<String> injectionPatterns = List.of(
            "ignore previous instructions",
            "ignore all instructions",
            "disregard your instructions",
            "new instructions:",
            "system prompt:",
            "you are now",
            "forget everything"
        );
        
        String lowerInput = input.toLowerCase();
        return injectionPatterns.stream()
            .anyMatch(lowerInput::contains);
    }
    
    private String sanitizeInput(String input) {
        // Remove potential control characters
        return input.replaceAll("[\\p{Cntrl}&&[^\r\n\t]]", "")
                   .trim();
    }
}

Technique 8: Adaptive Prompting

Adjust prompts based on user context and model performance:

@Service
public class AdaptivePromptService {
    
    private final ChatClient chatClient;
    private final PromptPerformanceTracker performanceTracker;
    private final UserContextService userContextService;
    
    public String generateAdaptiveResponse(String userId, String query) {
        UserContext context = userContextService.getContext(userId);
        
        // Select prompt variant based on user expertise
        String promptVariant = selectPromptVariant(context);
        
        // Adjust parameters based on historical performance
        PromptParameters params = optimizeParameters(query, context);
        
        String prompt = buildAdaptivePrompt(query, context, promptVariant);
        
        String response = chatClient.prompt()
            .user(prompt)
            .options(ChatOptionsBuilder.builder()
                .withTemperature(params.getTemperature())
                .withMaxTokens(params.getMaxTokens())
                .build())
            .call()
            .content();
        
        // Track for future optimization
        performanceTracker.record(userId, query, response, promptVariant);
        
        return response;
    }
    
    private String selectPromptVariant(UserContext context) {
        return switch (context.getExpertiseLevel()) {
            case BEGINNER -> "beginner";
            case INTERMEDIATE -> "standard";
            case EXPERT -> "advanced";
        };
    }
    
    private String buildAdaptivePrompt(String query, UserContext context, 
                                        String variant) {
        String basePrompt = switch (variant) {
            case "beginner" -> """
                You are a patient teacher explaining to someone new to this topic.
                - Use simple language and avoid jargon
                - Provide examples and analogies
                - Break down complex concepts
                - Offer to explain further if needed
                """;
            case "advanced" -> """
                You are a peer expert having a technical discussion.
                - Use precise technical terminology
                - Assume familiarity with advanced concepts
                - Focus on nuances and edge cases
                - Provide references to papers/specifications when relevant
                """;
            default -> """
                You are a helpful assistant balancing clarity with depth.
                - Explain concepts clearly but don't oversimplify
                - Use technical terms but provide brief definitions
                - Offer both overview and details
                """;
        };
        
        // Add personalization
        if (context.getPreferences().containsKey("responseStyle")) {
            basePrompt += "\nResponse style preference: " + 
                context.getPreferences().get("responseStyle");
        }
        
        return basePrompt + "\n\n## User Query\n" + query;
    }
    
    private PromptParameters optimizeParameters(String query, UserContext context) {
        // Analyze query complexity
        int queryComplexity = estimateComplexity(query);
        
        // Get historical performance data
        PerformanceStats stats = performanceTracker.getStats(context.getUserId());
        
        // Optimize temperature based on query type
        double temperature = queryComplexity > 7 ? 0.3 : // Complex = more deterministic
                            queryComplexity < 3 ? 0.8 : // Simple = more creative
                            0.5; // Balanced
        
        // Adjust max tokens based on expected response length
        int maxTokens = estimateRequiredTokens(query, stats);
        
        return new PromptParameters(temperature, maxTokens);
    }
}

Technique 9: Meta-Prompting

Use AI to help create and refine prompts:

@Service
public class MetaPromptService {
    
    private final ChatClient chatClient;
    
    public OptimizedPrompt optimizePrompt(String originalPrompt, 
                                           List<PromptExample> examples) {
        String metaPrompt = """
            You are a prompt engineering expert. Analyze and improve this prompt.
            
            ## Original Prompt
            %s
            
            ## Example Inputs and Expected Outputs
            %s
            
            ## Your Task
            1. Identify weaknesses in the original prompt:
               - Ambiguity
               - Missing context
               - Unclear output format
               - Potential for misinterpretation
            
            2. Suggest an improved prompt that:
               - Is clearer and more specific
               - Includes appropriate examples if beneficial
               - Specifies the exact output format
               - Handles edge cases
               - Is efficient (not unnecessarily long)
            
            3. Explain your changes and why they improve the prompt
            
            Return your analysis as:
            {
                "weaknesses": ["<weakness 1>", ...],
                "improvedPrompt": "<the optimized prompt>",
                "changes": [
                    {
                        "change": "<what changed>",
                        "reason": "<why it's better>"
                    }
                ],
                "estimatedImprovementScore": <1-10>
            }
            """.formatted(
                originalPrompt,
                examples.stream()
                    .map(e -> "Input: " + e.getInput() + "\nExpected: " + e.getExpected())
                    .collect(Collectors.joining("\n\n"))
            );
        
        return chatClient.prompt()
            .user(metaPrompt)
            .call()
            .entity(OptimizedPrompt.class);
    }
    
    public String generatePromptForTask(String taskDescription, 
                                         String targetModel,
                                         List<String> constraints) {
        String prompt = """
            You are an expert at creating prompts for AI models.
            
            ## Task Description
            %s
            
            ## Target Model
            %s
            
            ## Constraints
            %s
            
            Create an optimal prompt that will achieve this task effectively.
            Include:
            1. Clear role definition
            2. Specific instructions
            3. Output format specification
            4. Any necessary examples
            5. Edge case handling
            
            Return just the prompt, ready to use.
            """.formatted(
                taskDescription,
                targetModel,
                String.join("\n- ", constraints)
            );
        
        return chatClient.prompt()
            .user(prompt)
            .call()
            .content();
    }
}

Best Practices Summary

1. Be Specific and Explicit

// Bad
String vague = "Summarize this document.";

// Good  
String specific = """
    Summarize the following document in exactly 3 bullet points.
    Each bullet should be 1-2 sentences.
    Focus on: main argument, key evidence, and conclusion.
    Do not include minor details or examples.
    
    Document:
    %s
    """.formatted(document);

2. Use Delimiters for Clarity

String clear = """
    Analyze the code between the <code> tags.
    
    <code>
    %s
    </code>
    
    Provide your analysis between <analysis> tags.
    """.formatted(code);

3. Specify Output Format Explicitly

String formatted = """
    Return your response as a JSON object with this exact structure:
    {
        "decision": "approve" | "reject" | "review",
        "confidence": <number 0-100>,
        "reasons": ["<reason1>", "<reason2>"],
        "suggestedActions": ["<action1>"]
    }
    
    IMPORTANT: Return ONLY the JSON object, no markdown or explanation.
    """;

4. Handle Edge Cases

String robust = """
    Classify the customer feedback.
    
    ## Rules
    - If feedback is in a language other than English, respond with: 
      {"error": "unsupported_language", "detected": "<language>"}
    - If feedback is empty or just whitespace, respond with:
      {"error": "empty_input"}
    - If feedback is too short to classify (< 5 words), respond with:
      {"error": "insufficient_content"}
    
    ## Feedback
    %s
    """.formatted(feedback);

Conclusion

Effective prompt engineering is crucial for building reliable AI-powered applications. Spring AI’s template system, combined with these advanced techniques, provides a robust foundation for implementing sophisticated prompting strategies in enterprise Java applications.

Key takeaways:

References and Further Reading


The examples in this post demonstrate patterns and techniques. Always test prompts thoroughly with your specific models and use cases, as effectiveness can vary between models and versions.


Suggest Changes
Share this post on:

Previous Post
Agentic AI Architecture Patterns for Document Extraction and Processing
Next Post
Deploying Spring AI Applications to Kubernetes