A hands-on learning project demonstrating core prompt engineering techniques using Spring AI and a configurable LLM backend (OpenAI-compatible API).
Prompt engineering is the practice of deliberately crafting the input text you send to a language model in order to reliably produce the output you want.
A language model doesn't "understand" your intent - it predicts the most statistically likely continuation of your input. The way you phrase, structure, and contextualize your prompt directly shapes what that continuation looks like. Prompt engineering is the discipline of exploiting that mechanism intentionally.
It sits at the intersection of:
- Linguistics - how phrasing and framing affect interpretation
- Cognitive science - how reasoning can be scaffolded through instruction
- Software engineering - how prompts can be composed, parameterized, and evaluated systematically
Good prompt engineering reduces hallucinations, improves output consistency, controls format and tone, and can dramatically improve task accuracy - without changing the model or fine-tuning anything.
Each technique is implemented as a standalone class in src/main/java/sk/mkrajcovic/springai_prompt_engineering/techniques/ and can be toggled on/off in SpringaiPromptEngineeringApplication.
1. Zero-Shot Prompting - ZeroShot.java
Ask the model to perform a task with no examples - relying entirely on its pre-trained knowledge.
When to use: Straightforward tasks the model has likely seen during training (classification, translation, summarization). Minimizes prompt length.
Key characteristics:
- No input-output examples provided
- Task is defined purely through instruction
- Output format is specified explicitly
Example from code:
Classify the sentiment of the movie review.
Possible labels: POSITIVE / NEUTRAL / NEGATIVE
Review: "Her" is a disturbing study...
Output requirements:
- Return exactly one label.
- Use uppercase.
- No explanation.
Temperature tip: Low temperature (
0.1) is used here to get deterministic, consistent classification output.
2. Few-Shot Prompting - FewShot.java
Provide the model with one or more input-output examples to demonstrate the expected pattern before presenting the actual task.
When to use: Tasks requiring specific output formats, handling edge cases, or when the task definition is ambiguous without demonstration. Typically 3-5 examples.
Key characteristics:
- Examples are injected as parameterized template variables
- The model infers the pattern from examples and applies it to new input
- One-shot (1 example) vs. few-shot (3-5 examples) depending on task complexity
Example from code: Three pizza order examples are built programmatically and injected into the prompt template, teaching the model to parse natural language orders into structured JSON (PizzaOrder).
3. System Prompting - SystemPrompting.java
Set a persistent behavioral framework for the model that applies across all interactions - separate from the specific task instruction.
When to use: When you need to define who the model is, how it behaves globally, what tone it uses, or what constraints it operates under - independently of any single user query.
Key characteristics:
- System prompt = behavioral framework ("who the model is")
- User prompt = specific task instruction + input ("what to do now")
- System prompt persists as a "mission statement" throughout the conversation
Example from code:
// System prompt - behavioral framework:
"You are a professional film critic assistant specializing in sentiment analysis.
Always respond with structured, concise classifications.
Never include explanations unless explicitly asked."
// User prompt - specific task:
"Classify the following review as POSITIVE, NEUTRAL, or NEGATIVE..."
Common mistake: Putting task instructions in the system prompt. The system prompt should define how the model operates, not what to do with this specific input.
4. Role Prompting - Role.java
Assign the model a specific identity, expertise level, or persona to influence the style, tone, depth, and framing of its responses.
When to use: When you want the model to respond from a particular professional perspective, adopt a communication style, or simulate domain expertise.
Key characteristics:
- Role is defined in the system prompt
- Can include expertise depth ("10 years of experience"), persona ("enthusiastic"), and style ("in a humorous style")
- These three dimensions together unlock the full power of role prompting
Example from code:
"You are an enthusiastic local travel guide with 10 years of experience.
I will write to you about my location and you will suggest 3 places to visit
near me in a humorous style."
5. Contextual Prompting - Contextual.java
Provide background information as a context parameter to enrich the model's understanding of the specific situation without cluttering the main instruction.
When to use: When the task is generic but the answer needs to be tailored to a specific domain, audience, or set of constraints.
Key characteristics:
- Context is injected as a template parameter (
{context}) - Keeps the main instruction clean and reusable
- Helps the model understand domain, audience, or background facts
Example from code:
"Suggest 3 topics to write an article about...
Context: You are writing for a blog about retro 80's arcade video games."
6. Step-Back Prompting - StepBack.java
Break complex requests into two phases: first acquire general background knowledge, then use that knowledge to answer the specific question.
When to use: Complex reasoning tasks, problems requiring specialized domain knowledge, or when you want more comprehensive responses rather than immediate shallow answers.
Key characteristics:
- Phase 1: Ask a broader, more general question to establish foundational knowledge
- Phase 2: Feed that knowledge as context into the specific task
- Reduces errors caused by the model jumping to conclusions without sufficient grounding
Example from code:
// Phase 1 - step back:
"What are 5 fictional key settings that contribute to a challenging
and engaging level storyline in a first-person shooter video game?"
// Phase 2 - use the knowledge:
"Write a one paragraph storyline for a new FPS level.
Context: {step-back}"
7. Chain of Thought (CoT) - ChainOfThought.java
Encourage the model to show its intermediate reasoning steps before producing a final answer, similar to how humans solve problems on paper.
When to use: Mathematical problems, logical reasoning, multi-step deductions. Makes errors visible and correctable.
Two variants implemented:
Zero-shot CoT - trigger reasoning with a magic phrase:
"When I was 3 years old, my partner was 3 times my age.
Now, I am 20 years old. How old is my partner?
Let's think step by step!"
One-shot CoT - provide a worked example first, then the actual question:
Q: [example question]
A: [step-by-step worked solution with the answer]
Q: [actual question]; Let's think step by step.
A:
8. Self-Consistency - SelfConsistency.java
Run the same prompt multiple times with higher temperature (more variation), then aggregate results via majority voting to produce a more reliable final answer.
When to use: High-stakes classification or reasoning tasks where a single model call may be unreliable. Trades cost/latency for accuracy.
Key characteristics:
- Same prompt executed N times (5 in this example)
- Higher temperature (
1.0) encourages diverse reasoning paths - Final answer determined by majority vote across all runs
- Particularly effective combined with Chain of Thought reasoning
Example from code: An email from "Harry the Hacker" is classified as IMPORTANT or NOT_IMPORTANT five times. The majority classification wins.
9. Tree of Thoughts (ToT) - TreeOfThoughts.java
An advanced reasoning framework that explores multiple reasoning paths simultaneously, evaluates their promise, and selects the most advantageous branch - treating problem-solving as a search tree.
When to use: Complex problems with multiple possible approaches, strategic planning, or when the solution requires exploring alternatives before committing to a path.
Three-phase pipeline implemented:
- Generate - produce multiple candidate approaches (3 chess opening moves with ratings)
- Evaluate - analyze and select the most promising candidate (best move by strategic criteria)
- Expand - project future states from the selected branch (next 3 moves for both players)
Note: This is a simplified ToT implementation. Full ToT involves recursive branching and backtracking, which requires more complex orchestration beyond a single prompt chain.
10. Automatic Prompt Engineering (APE) - AutomaticPromptEngineering.java
Use the AI itself to generate and evaluate alternative prompt instructions, finding the most effective formulation for a given task - without manual trial and error.
When to use: Optimizing prompts for production systems, when manual prompt engineering has plateaued, or when you need to systematically improve prompt quality at scale.
Two-phase pipeline:
- Generation phase - produce N diverse prompt instruction variants at high temperature
- Evaluation phase - meta-evaluate: the model ranks its own candidates by quality criteria and selects the best
Example from code:
// Phase 1 - generate variants (temperature=1.0 for diversity):
"Generate 10 different prompt instructions that could be used to instruct
a chatbot to correctly understand and process t-shirt purchase orders."
// Phase 2 - meta-evaluate:
"Rank them by: clarity, specificity, robustness to varied user input,
and chatbot training effectiveness. Select the best candidate."
Evaluation strategies (from most to least automated):
| Strategy | Description |
|---|---|
| Meta-evaluation | Model critiques/ranks its own outputs - used in this example |
| Few/Zero-shot benchmarks | Test variants against labeled datasets |
| Task-specific metrics | Accuracy, F1-score, BLEU (requires reference data) |
| Automated scoring | Log-probability of correct answers, hallucination detection |
| A/B testing | Real-world user engagement or task success rates |
| User feedback loops | Human ratings guide prompt selection |
Note: This document covers best practices derived from the techniques demonstrated in this project. Prompt engineering is a broad discipline - not all practices, activities, and methodologies are covered here.
Zero-shot works for simple tasks, but for anything requiring a specific format, edge case handling, or nuanced output - give the model examples. Even one well-chosen example (one-shot) dramatically improves consistency. Three to five examples (few-shot) handle more complex patterns.
One task per prompt. Don't chain multiple unrelated instructions into a single prompt - the model will deprioritize some of them. If you need multiple things done, use multiple prompts (as demonstrated in Step-Back, ToT, and APE).
Don't say "give me the answer." Say:
- "Return exactly one label in uppercase."
- "Respond with valid JSON matching this structure."
- "No explanation. No punctuation."
Vague output instructions produce vague output. Spring AI's .entity(MyClass.class) enforces structured output at the framework level - use it.
Tell the model what to do, not what not to do. Negative constraints ("don't include explanations") are less reliable than positive instructions ("return only the label").
| Constraint | Instruction |
|---|---|
| "Don't be verbose" | "Respond in one sentence" |
| "Don't use bullet points" | "Write in paragraph form" |
| "Don't explain your reasoning" | "Return only the final answer" |
Always set maxTokens when you know the expected output size. This prevents runaway responses, reduces cost, and signals to the model the expected response length. For classification tasks, 5-50 tokens is sufficient. For reasoning tasks, 250-500. For generation tasks, set accordingly.
.options(ChatOptions.builder()
.maxTokens(50))Temperature controls the randomness of the model's output. It's one of the most impactful levers you have:
| Temperature | Behavior | Use for |
|---|---|---|
0.0 - 0.2 |
Deterministic, focused | Classification, extraction, structured output |
0.3 - 0.6 |
Balanced | Q&A, summarization, analysis |
0.7 - 0.9 |
Creative, varied | Brainstorming, storytelling, diverse suggestions |
1.0 |
High variation | Self-consistency sampling, APE generation, exploring edge cases |
1.0 - 2.0 |
Maximum variation (OpenAI models only) | Extreme diversity sampling, stress-testing prompt robustness |
Provider limits: Anthropic (Claude) caps temperature at
1.0. OpenAI (GPT models) allows up to2.0. Values above1.0produce increasingly chaotic output and are rarely useful in practice.
Rule of thumb: Low temperature when you want the right answer. High temperature when you want diverse answers to choose from.
- Java 21+
- Maven
- An OpenAI-compatible API key (or local Ollama instance)
Edit src/main/resources/application.properties:
spring.ai.openai.api-key=your_api_key
spring.ai.openai.chat.model=gpt-4o-mini
# For a custom LiteLLM proxy:
# spring.ai.openai.base-url=https://your-litellm-proxy/v1./mvnw spring-boot:runToggle individual techniques on/off by commenting lines in SpringaiPromptEngineeringApplication.runExamplesOfPromptingTechniques().
Enable debug logging to see full prompt/response pairs from the SimpleLoggerAdvisor:
logging.level.org.springframework.ai.chat.client.advisor=DEBUGsrc/main/java/sk/mkrajcovic/
├── springai_prompt_enineering/
│ └── SpringaiPromptEngineeringApplication.java # Entry point, runs all techniques
└── springai_prompt_engineering/
└── techniques/
├── ZeroShot.java
├── FewShot.java
├── SystemPrompting.java
├── Role.java
├── Contextual.java
├── StepBack.java
├── ChainOfThought.java
├── SelfConsistency.java
├── TreeOfThoughts.java
└── AutomaticPromptEngineering.java