Agent Prompt Engineering
Why This Matters
Prompt engineering is the interface between human intent and AI capability. A poorly crafted prompt wastes tokens, produces inaccurate outputs, and frustrates users. Mastering prompt engineering transforms your agent from a mediocre responder into a precise, reliable system that consistently delivers high-quality results.
Real-World Analogy: Think of prompt engineering like giving directions to a taxi driver. Vague instructions ("go somewhere nice") yield unpredictable results. Precise instructions ("take Highway 101 north, exit at Elm Street, building with blue awning") get you exactly where you need to go, every time.
Prompt Engineering Architecture
Prompt Builder System
from dataclasses import dataclass, field
from typing import Any, Optional
from enum import Enum
import json
import logging
logger = logging.getLogger(__name__)
class PromptStyle(Enum):
ZERO_SHOT = "zero_shot"
FEW_SHOT = "few_shot"
CHAIN_OF_THOUGHT = "chain_of_thought"
ZERO_SHOT_COT = "zero_shot_cot"
SELF_CONSISTENCY = "self_consistency"
@dataclass
class Example:
input: str
output: str
reasoning: Optional[str] = None
@dataclass
class PromptTemplate:
name: str
style: PromptStyle
system_prompt: str
task_description: str
examples: list[Example] = field(default_factory=list)
output_format: str = ""
constraints: list[str] = field(default_factory=list)
class PromptBuilder:
def __init__(self):
self.templates: dict[str, PromptTemplate] = {}
self.variables: dict[str, str] = {}
def register_template(self, template: PromptTemplate) -> None:
self.templates[template.name] = template
logger.debug(f"Registered prompt template: {template.name}")
def set_variable(self, key: str, value: str) -> None:
self.variables[key] = value
def build_prompt(
self, template_name: str, user_input: str, context: Optional[dict] = None,
) -> str:
template = self.templates.get(template_name)
if not template:
raise ValueError(f"Template {template_name} not found")
parts = [f"System: {template.system_prompt}", "", f"Task: {template.task_description}", ""]
if template.style == PromptStyle.FEW_SHOT and template.examples:
parts.append("Examples:")
for i, example in enumerate(template.examples, 1):
parts.extend([f"\nExample {i}:", f"Input: {example.input}", f"Output: {example.output}"])
if example.reasoning:
parts.append(f"Reasoning: {example.reasoning}")
parts.append("")
elif template.style in [PromptStyle.CHAIN_OF_THOUGHT, PromptStyle.ZERO_SHOT_COT]:
parts.extend(["Let's think step by step:", ""])
if template.output_format:
parts.extend([f"Output Format: {template.output_format}", ""])
if template.constraints:
parts.append("Constraints:")
parts.extend([f"- {c}" for c in template.constraints])
parts.append("")
processed_input = self._process_variables(user_input, context)
parts.extend([f"Input: {processed_input}", ""])
if template.style == PromptStyle.CHAIN_OF_THOUGHT:
parts.append("Reasoning:")
return "\n".join(parts)
def _process_variables(self, text: str, context: Optional[dict] = None) -> str:
processed = text
for key, value in self.variables.items():
processed = processed.replace(f"{{{key}}}", value)
if context:
for key, value in context.items():
processed = processed.replace(f"{{{key}}}", str(value))
return processed
def build_cot_prompt(self, question: str) -> str:
return f"""Let's solve this step by step.
Question: {question}
Step 1: Understand the problem
Step 2: Identify key information
Step 3: Apply relevant concepts
Step 4: Calculate or reason
Step 5: Verify the answer
Let me work through this:"""
def build_few_shot_prompt(self, task: str, examples: list[Example], query: str) -> str:
parts = [f"Task: {task}\n", "Examples:"]
for i, example in enumerate(examples, 1):
parts.extend([f"\n{i}. Input: {example.input}", f" Output: {example.output}"])
parts.extend([f"\nNow, given this input:\n{query}", "\nOutput:"])
return "\n".join(parts)
def get_available_templates(self) -> list[str]:
return list(self.templates.keys())
Chain-of-Thought Engine
from dataclasses import dataclass, field
from typing import Any, Optional, Callable
from enum import Enum
import asyncio
import logging
logger = logging.getLogger(__name__)
class ReasoningStrategy(Enum):
LINEAR = "linear"
TREE = "tree"
SELF_CONSISTENCY = "self_consistency"
@dataclass
class ReasoningStep:
step_number: int
thought: str
confidence: float = 0.8
dependencies: list[int] = field(default_factory=list)
@dataclass
class ReasoningPath:
steps: list[ReasoningStep]
final_answer: str
overall_confidence: float
class ChainOfThoughtEngine:
def __init__(self, strategy: ReasoningStrategy = ReasoningStrategy.LINEAR):
self.strategy = strategy
self.max_steps = 10
self.confidence_threshold = 0.7
async def generate_reasoning(
self, question: str, context: Optional[dict] = None, llm_caller: Optional[Callable] = None,
) -> ReasoningPath:
if self.strategy == ReasoningStrategy.LINEAR:
return await self._linear_reasoning(question, context, llm_caller)
elif self.strategy == ReasoningStrategy.TREE:
return await self._tree_reasoning(question, context, llm_caller)
else:
return await self._self_consistency_reasoning(question, context, llm_caller)
async def _linear_reasoning(
self, question: str, context: Optional[dict], llm_caller: Optional[Callable],
) -> ReasoningPath:
steps = []
if llm_caller:
reasoning_prompt = f"Let's solve this step by step.\n\nQuestion: {question}\n\nReasoning:"
response = await llm_caller(reasoning_prompt)
steps.append(ReasoningStep(step_number=1, thought=response, confidence=0.85))
else:
for i, thought in enumerate([
"Analyzing the problem components...",
"Evaluating available information...",
"Developing solution strategy...",
"Executing solution steps...",
"Verifying the result against requirements...",
], 1):
steps.append(ReasoningStep(step_number=i, thought=thought, confidence=0.8 + (i * 0.02)))
overall_confidence = sum(s.confidence for s in steps) / len(steps)
return ReasoningPath(steps=steps, final_answer="Based on step-by-step analysis.", overall_confidence=overall_confidence)
async def _tree_reasoning(
self, question: str, context: Optional[dict], llm_caller: Optional[Callable],
) -> ReasoningPath:
steps = [
ReasoningStep(step_number=1, thought="Breaking down into sub-problems...", confidence=0.8),
ReasoningStep(step_number=2, thought="Exploring multiple solution paths...", confidence=0.75),
ReasoningStep(step_number=3, thought="Evaluating each path's feasibility...", confidence=0.85),
ReasoningStep(step_number=4, thought="Selecting optimal path...", confidence=0.9),
]
return ReasoningPath(steps=steps, final_answer="Optimal solution via tree exploration.", overall_confidence=0.825)
async def _self_consistency_reasoning(
self, question: str, context: Optional[dict], llm_caller: Optional[Callable],
) -> ReasoningPath:
num_paths = 3
paths = [await self._linear_reasoning(question, context, llm_caller) for _ in range(num_paths)]
answer_counts: dict[str, int] = {}
for path in paths:
answer_counts[path.final_answer] = answer_counts.get(path.final_answer, 0) + 1
most_common = max(answer_counts.items(), key=lambda x: x[1])
return ReasoningPath(
steps=[ReasoningStep(step_number=1, thought=f"Generated {num_paths} reasoning paths", confidence=0.9)],
final_answer=most_common[0],
overall_confidence=most_common[1] / num_paths,
)
Mathematical Foundations
Prompt Effectiveness Score:
Few-Shot Learning Gain:
Chain-of-Thought Accuracy Boost:
Token Efficiency:
Reasoning Confidence:
Performance Considerations
| Technique | Latency | Cost | Accuracy | Best For |
|---|---|---|---|---|
| Zero-Shot | Low | Low | Medium | Simple tasks |
| Few-Shot (3 examples) | Medium | Medium | High | Classification |
| Chain-of-Thought | High | High | Very High | Complex reasoning |
| Zero-Shot CoT | Medium | Low | High | General reasoning |
| Self-Consistency | Very High | Very High | Highest | Critical decisions |
| System Prompt Only | Low | Low | Low | Basic operations |
Security Considerations
- Prompt injection defense: Separate system and user prompts with clear delimiters
- Input validation: Sanitize user inputs before including in prompts
- Output filtering: Validate LLM outputs against expected formats
- Token budget limits: Prevent abuse through token consumption controls
- Model access controls: Restrict which models can be used for sensitive operations
- Audit logging: Log all prompt constructions for security review
Interview Questions
1. What is the difference between few-shot and zero-shot prompting?
Answer: Few-shot prompting provides input/output examples to demonstrate the task pattern, helping the model understand requirements. Zero-shot prompting relies on instructions alone without examples. Few-shot typically achieves 10-30% higher accuracy but uses more tokens. Zero-shot is faster and cheaper but may require more careful instruction design. Use few-shot for complex tasks, zero-shot for simple or cost-sensitive applications.
2. How does chain-of-thought prompting improve reasoning?
Answer: Chain-of-thought prompting encourages step-by-step reasoning by asking the model to "think step by step." This: 1) Breaks complex problems into manageable steps, 2) Makes reasoning explicit and debuggable, 3) Reduces logical errors, 4) Improves accuracy on math and logic tasks. The model's intermediate reasoning helps it arrive at more accurate final answers.
3. What is self-consistency in prompt engineering?
Answer: Self-consistency generates multiple reasoning paths for the same question and uses majority voting for the final answer. Implementation: 1) Generate N responses with different temperatures, 2) Extract answers from each, 3) Vote on most common answer. This improves reliability by reducing variance in individual responses. Best for critical tasks where accuracy is paramount.
4. How do you optimize prompts for cost efficiency?
Answer: Cost optimization strategies: 1) Minimize token count while maintaining clarity, 2) Use system prompts for reusable instructions, 3) Cache effective prompts, 4) A/B test prompt variations, 5) Use cheaper models for simpler tasks, 6) Implement prompt compression. Measure cost per correct answer, not just cost per request.
5. What are common prompt engineering mistakes?
Answer: Common mistakes: 1) Overly complex instructions, 2) Missing output format specification, 3) Inconsistent examples, 4) Ignoring edge cases, 5) Not testing diverse inputs, 6) Over-relying on one technique, 7) Ignoring token limits, 8) Not iterating based on failures. The key is systematic testing and refinement.
6. How do you handle prompt injection in agent systems?
Answer: Prompt injection defense: 1) Separate system and user prompts clearly, 2) Validate and sanitize user inputs, 3) Use delimiters for user content, 4) Implement output filtering, 5) Monitor for unusual patterns, 6) Use instruction hierarchy (system > user). Never trust user input blindly—treat it as potentially adversarial.
7. How do you evaluate prompt quality systematically?
Answer: Evaluation approach: 1) Define clear success metrics, 2) Create diverse test cases, 3) Measure accuracy, consistency, and cost, 4) Test edge cases and adversarial inputs, 5) Compare against baselines, 6) Use human evaluation for subjective quality, 7) A/B test in production. Track metrics over time and iterate.
8. What is the role of temperature in prompt engineering?
Answer: Temperature controls randomness: 0 = deterministic (highest accuracy), 1 = most random (most creative). For factual tasks: use low temperature (0-0.3). For creative tasks: use higher temperature (0.7-1.0). For code generation: use low temperature. Temperature affects both accuracy and diversity—find the balance for your use case.
Common Pitfalls
| Pitfall | Solution |
|---|---|
| No clear output format | Specify expected structure explicitly |
| Inconsistent examples | Ensure examples demonstrate pattern clearly |
| Overly long prompts | Optimize for token efficiency |
| No testing framework | Build systematic evaluation pipeline |
| Ignoring edge cases | Test with diverse inputs including adversarial |
| Single technique reliance | Combine multiple approaches strategically |
| No cost tracking | Monitor tokens and costs per operation |
| Static prompts | Iterate based on performance data |
Summary with Key Takeaways
- System prompts define agent behavior, role, and constraints
- Few-shot examples demonstrate task patterns and improve accuracy
- Chain-of-thought reasoning improves complex problem solving
- Self-consistency increases reliability through multiple paths
- Prompt optimization is iterative—test, measure, refine
- Cost efficiency requires balancing tokens, accuracy, and speed
- Security means treating user input as potentially adversarial
- Evaluation must be systematic and ongoing
KnowledgeCheck
-
What is few-shot prompting?
- a) Using no examples
- b) Providing input/output examples
- c) Using only system prompts
- d) Generating multiple responses
-
What does chain-of-thought prompting encourage?
- a) Faster responses
- b) Step-by-step reasoning
- c) Lower costs
- d) Higher creativity
-
What is self-consistency?
- a) Using same prompt repeatedly
- b) Generating multiple paths and voting
- c) Keeping responses consistent
- d) Using consistent temperature
-
What is the recommended temperature for factual tasks?
- a) 0.0 - 0.3
- b) 0.5 - 0.7
- c) 0.7 - 1.0
- d) 1.0 - 1.5
-
What is prompt injection?
- a) Adding more examples
- b) Malicious input overriding instructions
- c) Improving prompt quality
- d) Reducing token count
-
What should prompts always include?
- a) Maximum tokens
- b) Clear output format
- c) High temperature
- d) Many examples
Answers: 1-b, 2-b, 3-b, 4-a, 5-b, 6-b