AI
Agentic Patterns
Ai Ml advanced v1.0.0
Agentic Patterns
Overview
Agentic AI patterns enable LLMs to take actions, use tools, and complete multi-step tasks autonomously. This skill covers agent architectures, tool integration, planning, and orchestration patterns.
Key Concepts
Agent Architecture
┌─────────────────────────────────────────────────────────────┐
│ Agent Architecture │
├─────────────────────────────────────────────────────────────┤
│ │
│ ReAct Pattern (Reasoning + Acting): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Input: "What's the weather in Paris?" │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ THOUGHT: I need │ │ │
│ │ │ to check weather │◄────────────┐ │ │
│ │ └────────┬─────────┘ │ │ │
│ │ │ │ │ │
│ │ ▼ │ │ │
│ │ ┌──────────────────┐ │ │ │
│ │ │ ACTION: Call │ │ │ │
│ │ │ weather_api │ │ │ │
│ │ └────────┬─────────┘ │ │ │
│ │ │ │ │ │
│ │ ▼ │ │ │
│ │ ┌──────────────────┐ │ │ │
│ │ │ OBSERVATION: │─────────────┘ │ │
│ │ │ {"temp": 18°C} │ (loop until done) │ │
│ │ └────────┬─────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ ANSWER: It's 18°C│ │ │
│ │ │ in Paris │ │ │
│ │ └──────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Multi-Agent Patterns: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • Supervisor: One agent delegates to specialists │ │
│ │ • Peer-to-Peer: Agents communicate directly │ │
│ │ • Hierarchical: Nested supervision │ │
│ │ • Pipeline: Sequential agent processing │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Best Practices
1. Define Clear Tool Contracts
Well-documented tools with specific purposes.
2. Implement Guardrails
Limit actions, validate inputs, confirm destructive operations.
3. Handle Failures Gracefully
Agents should recover from tool failures.
4. Log All Actions
Full observability for debugging.
5. Set Iteration Limits
Prevent infinite loops.
Code Examples
Example 1: Tool System
/**
* Tool definition and execution framework
*/
@Data
@Builder
public class Tool {
private String name;
private String description;
private JsonSchema parameters;
private ToolExecutor executor;
private ToolPermissions permissions;
}
public interface ToolExecutor {
ToolResult execute(Map<String, Object> parameters, ToolContext context);
}
@Data
@Builder
public class ToolResult {
private boolean success;
private Object output;
private String error;
private Map<String, Object> metadata;
}
@Service
public class ToolRegistry {
private final Map<String, Tool> tools = new ConcurrentHashMap<>();
public void registerTool(Tool tool) {
tools.put(tool.getName(), tool);
}
public Tool getTool(String name) {
return tools.get(name);
}
public List<Map<String, Object>> getToolDefinitions() {
return tools.values().stream()
.map(tool -> Map.of(
"type", "function",
"function", Map.of(
"name", tool.getName(),
"description", tool.getDescription(),
"parameters", tool.getParameters().toMap()
)
))
.toList();
}
}
// Example tool implementations
@Component
public class WebSearchTool implements ToolExecutor {
private final SearchService searchService;
@Override
public ToolResult execute(Map<String, Object> params, ToolContext context) {
String query = (String) params.get("query");
int numResults = (int) params.getOrDefault("num_results", 5);
try {
List<SearchResult> results = searchService.search(query, numResults);
return ToolResult.builder()
.success(true)
.output(results)
.build();
} catch (Exception e) {
return ToolResult.builder()
.success(false)
.error("Search failed: " + e.getMessage())
.build();
}
}
}
@Component
public class CodeExecutionTool implements ToolExecutor {
private final SandboxService sandbox;
@Override
public ToolResult execute(Map<String, Object> params, ToolContext context) {
String code = (String) params.get("code");
String language = (String) params.get("language");
// Validate permissions
if (!context.hasPermission("code_execution")) {
return ToolResult.builder()
.success(false)
.error("Code execution not permitted")
.build();
}
// Execute in sandbox
ExecutionResult result = sandbox.execute(code, language, Duration.ofSeconds(30));
return ToolResult.builder()
.success(result.getExitCode() == 0)
.output(Map.of(
"stdout", result.getStdout(),
"stderr", result.getStderr(),
"exit_code", result.getExitCode()
))
.build();
}
}
Example 2: ReAct Agent
@Service
public class ReActAgent {
private final LlmClient llmClient;
private final ToolRegistry toolRegistry;
private final int maxIterations = 10;
public AgentResponse run(String userInput, AgentContext context) {
List<Message> messages = new ArrayList<>();
messages.add(Message.system(buildSystemPrompt()));
messages.add(Message.user(userInput));
List<AgentStep> steps = new ArrayList<>();
for (int i = 0; i < maxIterations; i++) {
// Get LLM response with tool calling
CompletionResponse response = llmClient.complete(
CompletionRequest.builder()
.model("gpt-4")
.messages(messages)
.tools(toolRegistry.getToolDefinitions())
.toolChoice("auto")
.build()
);
// Check if done (no tool calls)
if (response.getToolCalls() == null || response.getToolCalls().isEmpty()) {
return AgentResponse.builder()
.answer(response.getContent())
.steps(steps)
.iterationsUsed(i + 1)
.build();
}
// Execute tool calls
for (ToolCall toolCall : response.getToolCalls()) {
String toolName = toolCall.getFunction().getName();
Map<String, Object> args = parseArgs(toolCall.getFunction().getArguments());
log.info("Agent calling tool: {} with args: {}", toolName, args);
Tool tool = toolRegistry.getTool(toolName);
if (tool == null) {
throw new UnknownToolException(toolName);
}
// Execute tool
ToolResult result = tool.getExecutor().execute(args, context.toToolContext());
// Record step
steps.add(AgentStep.builder()
.iteration(i)
.thought(response.getContent())
.action(toolName)
.actionInput(args)
.observation(result)
.build());
// Add to conversation
messages.add(Message.assistant(response.getContent(), response.getToolCalls()));
messages.add(Message.tool(toolCall.getId(),
objectMapper.writeValueAsString(result.getOutput())));
}
}
throw new MaxIterationsExceededException(maxIterations);
}
private String buildSystemPrompt() {
return """
You are a helpful assistant that can use tools to accomplish tasks.
When using tools:
1. Think about what information you need
2. Call the appropriate tool
3. Use the result to answer or call another tool
4. Only provide final answer when you have enough information
Available tools will be provided. Call them as needed.
""";
}
}
@Data
@Builder
class AgentStep {
private int iteration;
private String thought;
private String action;
private Map<String, Object> actionInput;
private ToolResult observation;
}
@Data
@Builder
class AgentResponse {
private String answer;
private List<AgentStep> steps;
private int iterationsUsed;
}
Example 3: Planning Agent
@Service
public class PlanningAgent {
private final LlmClient llmClient;
private final ReActAgent executor;
/**
* Plan-then-execute pattern
*/
public AgentResponse runWithPlanning(String task, AgentContext context) {
// Phase 1: Create plan
Plan plan = createPlan(task);
log.info("Created plan with {} steps", plan.getSteps().size());
// Phase 2: Execute each step
List<StepResult> results = new ArrayList<>();
StringBuilder context = new StringBuilder();
for (PlanStep step : plan.getSteps()) {
log.info("Executing step {}: {}", step.getNumber(), step.getDescription());
// Include context from previous steps
String stepPrompt = buildStepPrompt(step, context.toString());
try {
AgentResponse stepResult = executor.run(stepPrompt, agentContext);
results.add(new StepResult(step, stepResult, true));
context.append("\nStep ").append(step.getNumber())
.append(" result: ").append(stepResult.getAnswer());
} catch (Exception e) {
results.add(new StepResult(step, null, false));
// Decide whether to continue or replan
if (step.isCritical()) {
return handleCriticalFailure(task, plan, results, e);
}
}
}
// Phase 3: Synthesize final answer
return synthesizeAnswer(task, results);
}
private Plan createPlan(String task) {
String planningPrompt = """
Create a step-by-step plan to accomplish this task:
Task: %s
Return a JSON plan with this structure:
{
"goal": "The main goal",
"steps": [
{
"number": 1,
"description": "What to do",
"expectedOutput": "What we expect to get",
"dependencies": [],
"critical": true
}
]
}
Keep the plan concise (3-7 steps). Each step should be atomic.
""".formatted(task);
CompletionResponse response = llmClient.complete(
CompletionRequest.builder()
.model("gpt-4")
.messages(List.of(Message.user(planningPrompt)))
.temperature(0.0)
.responseFormat(ResponseFormat.JSON)
.build()
);
return objectMapper.readValue(response.getContent(), Plan.class);
}
/**
* Adaptive replanning on failure
*/
private AgentResponse handleCriticalFailure(
String task,
Plan originalPlan,
List<StepResult> completedSteps,
Exception error) {
String replanPrompt = """
The original plan failed at step %d.
Original task: %s
Original plan: %s
Completed steps and results:
%s
Error: %s
Create a new plan to complete the task, considering what was already done.
""".formatted(
completedSteps.size(),
task,
objectMapper.writeValueAsString(originalPlan),
formatCompletedSteps(completedSteps),
error.getMessage()
);
Plan newPlan = createPlan(replanPrompt);
// Continue with new plan (with recursion limit)
return executeRemainingPlan(newPlan, completedSteps);
}
}
@Data
class Plan {
private String goal;
private List<PlanStep> steps;
}
@Data
class PlanStep {
private int number;
private String description;
private String expectedOutput;
private List<Integer> dependencies;
private boolean critical;
}
Example 4: Multi-Agent Orchestration
@Service
public class MultiAgentOrchestrator {
private final Map<String, Agent> agents;
private final LlmClient llmClient;
public MultiAgentOrchestrator(
@Qualifier("researchAgent") Agent researchAgent,
@Qualifier("codeAgent") Agent codeAgent,
@Qualifier("reviewAgent") Agent reviewAgent) {
this.agents = Map.of(
"researcher", researchAgent,
"coder", codeAgent,
"reviewer", reviewAgent
);
}
/**
* Supervisor pattern: route to specialized agents
*/
public OrchestratorResponse supervise(String task) {
List<Message> conversation = new ArrayList<>();
conversation.add(Message.system(buildSupervisorPrompt()));
conversation.add(Message.user(task));
List<AgentExecution> executions = new ArrayList<>();
for (int round = 0; round < 10; round++) {
// Ask supervisor what to do
CompletionResponse decision = llmClient.complete(
CompletionRequest.builder()
.model("gpt-4")
.messages(conversation)
.temperature(0.0)
.responseFormat(ResponseFormat.JSON)
.build()
);
SupervisorDecision parsed = parseDecision(decision.getContent());
// Check if complete
if (parsed.getAction().equals("FINISH")) {
return OrchestratorResponse.builder()
.finalAnswer(parsed.getFinalAnswer())
.executions(executions)
.rounds(round + 1)
.build();
}
// Delegate to agent
String agentName = parsed.getAgent();
Agent agent = agents.get(agentName);
if (agent == null) {
conversation.add(Message.assistant(decision.getContent()));
conversation.add(Message.user("Unknown agent: " + agentName));
continue;
}
log.info("Delegating to {} agent: {}", agentName, parsed.getInstruction());
AgentResponse agentResponse = agent.run(
parsed.getInstruction(),
buildAgentContext(executions)
);
executions.add(AgentExecution.builder()
.agent(agentName)
.instruction(parsed.getInstruction())
.response(agentResponse)
.build());
// Add to conversation
conversation.add(Message.assistant(decision.getContent()));
conversation.add(Message.user(
"Agent %s completed. Result:\n%s".formatted(
agentName, agentResponse.getAnswer()
)
));
}
throw new MaxRoundsExceededException();
}
private String buildSupervisorPrompt() {
return """
You are a supervisor coordinating specialized agents to complete tasks.
Available agents:
- researcher: Good at finding information, searching web, reading docs
- coder: Good at writing and analyzing code
- reviewer: Good at reviewing work and finding issues
For each step, respond with JSON:
{
"action": "DELEGATE" or "FINISH",
"agent": "agent name if delegating",
"instruction": "what to tell the agent",
"reasoning": "why this step",
"finalAnswer": "if action is FINISH"
}
Delegate to the most appropriate agent for each subtask.
Use FINISH when the task is complete.
""";
}
/**
* Parallel agent execution
*/
public List<AgentResponse> runParallel(List<AgentTask> tasks) {
List<CompletableFuture<AgentResponse>> futures = tasks.stream()
.map(task -> CompletableFuture.supplyAsync(() ->
agents.get(task.getAgentType()).run(task.getInstruction(), task.getContext())
))
.toList();
return futures.stream()
.map(CompletableFuture::join)
.toList();
}
}
Example 5: Agent Memory
@Service
public class AgentMemoryService {
private final VectorStore vectorStore;
private final EmbeddingService embeddingService;
/**
* Store interaction in long-term memory
*/
public void remember(String agentId, MemoryEntry entry) {
float[] embedding = embeddingService.embed(entry.getContent());
vectorStore.insert(
UUID.randomUUID().toString(),
embedding,
Map.of(
"agent_id", agentId,
"type", entry.getType().name(),
"content", entry.getContent(),
"timestamp", entry.getTimestamp().toString(),
"importance", entry.getImportance(),
"metadata", objectMapper.writeValueAsString(entry.getMetadata())
)
);
}
/**
* Recall relevant memories for context
*/
public List<MemoryEntry> recall(String agentId, String query, int limit) {
float[] queryEmbedding = embeddingService.embed(query);
List<VectorMatch> matches = vectorStore.search(
queryEmbedding,
limit,
Map.of("agent_id", agentId)
);
return matches.stream()
.map(match -> MemoryEntry.builder()
.content(match.getMetadata().get("content").toString())
.type(MemoryType.valueOf(match.getMetadata().get("type").toString()))
.timestamp(Instant.parse(match.getMetadata().get("timestamp").toString()))
.importance(((Number) match.getMetadata().get("importance")).doubleValue())
.relevance(match.getScore())
.build())
.toList();
}
/**
* Build context with working + episodic memory
*/
public AgentContext buildContext(String agentId, String currentTask) {
// Short-term working memory (recent interactions)
List<MemoryEntry> recent = getRecentMemories(agentId, 5);
// Long-term episodic memory (relevant past experiences)
List<MemoryEntry> relevant = recall(agentId, currentTask, 5);
// Combine and deduplicate
Set<String> seen = new HashSet<>();
List<MemoryEntry> combined = new ArrayList<>();
for (MemoryEntry entry : recent) {
if (seen.add(entry.getContent())) {
combined.add(entry);
}
}
for (MemoryEntry entry : relevant) {
if (seen.add(entry.getContent())) {
combined.add(entry);
}
}
// Build context string
StringBuilder contextStr = new StringBuilder("Relevant context:\n");
for (MemoryEntry entry : combined) {
contextStr.append("- [").append(entry.getType()).append("] ")
.append(entry.getContent()).append("\n");
}
return AgentContext.builder()
.memories(combined)
.contextString(contextStr.toString())
.build();
}
/**
* Reflection: periodically consolidate and abstract memories
*/
@Scheduled(fixedRate = 3600000) // Every hour
public void reflect(String agentId) {
// Get recent memories
List<MemoryEntry> recent = getRecentMemories(agentId, 20);
if (recent.size() < 10) {
return; // Not enough to reflect on
}
// Ask LLM to identify patterns and create abstractions
String reflectionPrompt = """
Review these recent memories and identify:
1. Key patterns or recurring themes
2. Important learnings
3. High-level abstractions
Memories:
%s
Return JSON with insights to remember.
""".formatted(formatMemories(recent));
CompletionResponse response = llmClient.complete(
CompletionRequest.builder()
.model("gpt-4")
.messages(List.of(Message.user(reflectionPrompt)))
.build()
);
ReflectionResult reflection = parseReflection(response.getContent());
// Store high-level insights as new memories
for (String insight : reflection.getInsights()) {
remember(agentId, MemoryEntry.builder()
.content(insight)
.type(MemoryType.REFLECTION)
.timestamp(Instant.now())
.importance(0.9) // Reflections are high importance
.build());
}
}
}
@Data
@Builder
class MemoryEntry {
private String content;
private MemoryType type;
private Instant timestamp;
private double importance;
private double relevance;
private Map<String, Object> metadata;
}
enum MemoryType {
OBSERVATION, ACTION, RESULT, REFLECTION, FACT
}
Anti-Patterns
❌ No Action Limits
Always set maximum iterations/actions.
❌ Unvalidated Tool Inputs
Validate all parameters before execution.