$npx -y skills add itsmostafa/llm-engineering-skills --skill agentsPatterns and architectures for building AI agents and workflows with LLMs. Use when designing systems that involve tool use, multi-step reasoning, autonomous decision-making, or orchestration of LLM-driven tasks.
| 1 | # Building Agents |
| 2 | |
| 3 | Agents are systems where LLMs dynamically direct their own processes and tool usage. This skill covers when to use agents vs workflows, common architectural patterns, and practical implementation guidance. |
| 4 | |
| 5 | ## Table of Contents |
| 6 | |
| 7 | - [Agents vs Workflows](#agents-vs-workflows) |
| 8 | - [Workflow Patterns](#workflow-patterns) |
| 9 | - [Agent Architectures](#agent-architectures) |
| 10 | - [ReAct Pattern](#react-pattern) |
| 11 | - [Tool Design](#tool-design) |
| 12 | - [External Context Protocols](#external-context-protocols) |
| 13 | - [Best Practices](#best-practices) |
| 14 | - [References](#references) |
| 15 | |
| 16 | ## Agents vs Workflows |
| 17 | |
| 18 | | Aspect | Workflows | Agents | |
| 19 | |--------|-----------|--------| |
| 20 | | **Control flow** | Predefined code paths | LLM determines next step | |
| 21 | | **Predictability** | High - deterministic steps | Lower - dynamic decisions | |
| 22 | | **Complexity** | Simpler to debug and test | More complex, harder to predict | |
| 23 | | **Best for** | Well-defined, repeatable tasks | Open-ended, adaptive problems | |
| 24 | |
| 25 | **Key principle**: Start with the simplest solution. Use workflows when the task is predictable; use agents when flexibility is required. |
| 26 | |
| 27 | ## Workflow Patterns |
| 28 | |
| 29 | ### 1. Prompt Chaining |
| 30 | |
| 31 | Decompose tasks into sequential LLM calls, where each step's output feeds the next. |
| 32 | |
| 33 | ```python |
| 34 | async def prompt_chain(input_text): |
| 35 | # Step 1: Extract key information |
| 36 | extracted = await llm.generate( |
| 37 | "Extract the main entities and relationships from: " + input_text |
| 38 | ) |
| 39 | |
| 40 | # Step 2: Analyze |
| 41 | analysis = await llm.generate( |
| 42 | "Analyze these entities for patterns: " + extracted |
| 43 | ) |
| 44 | |
| 45 | # Step 3: Generate output |
| 46 | return await llm.generate( |
| 47 | "Based on this analysis, provide recommendations: " + analysis |
| 48 | ) |
| 49 | ``` |
| 50 | |
| 51 | **Use when**: Tasks naturally decompose into fixed sequential steps. |
| 52 | |
| 53 | ### 2. Routing |
| 54 | |
| 55 | Classify inputs and direct them to specialized handlers. |
| 56 | |
| 57 | ```python |
| 58 | async def route_request(user_input): |
| 59 | # Classify the input |
| 60 | category = await llm.generate( |
| 61 | f"Classify this request into one of: [billing, technical, general]\n{user_input}" |
| 62 | ) |
| 63 | |
| 64 | handlers = { |
| 65 | "billing": handle_billing, |
| 66 | "technical": handle_technical, |
| 67 | "general": handle_general, |
| 68 | } |
| 69 | |
| 70 | return await handlers[category.strip()](user_input) |
| 71 | ``` |
| 72 | |
| 73 | **Use when**: Different input types need fundamentally different processing. |
| 74 | |
| 75 | ### 3. Parallelization |
| 76 | |
| 77 | Run multiple LLM calls concurrently for independent subtasks. |
| 78 | |
| 79 | ```python |
| 80 | import asyncio |
| 81 | |
| 82 | async def parallel_analysis(document): |
| 83 | # Run independent analyses in parallel |
| 84 | results = await asyncio.gather( |
| 85 | llm.generate(f"Summarize: {document}"), |
| 86 | llm.generate(f"Extract key facts: {document}"), |
| 87 | llm.generate(f"Identify sentiment: {document}"), |
| 88 | ) |
| 89 | |
| 90 | summary, facts, sentiment = results |
| 91 | return {"summary": summary, "facts": facts, "sentiment": sentiment} |
| 92 | ``` |
| 93 | |
| 94 | **Variants**: |
| 95 | - **Sectioning**: Break task into parallel subtasks |
| 96 | - **Voting**: Run same prompt multiple times, aggregate results |
| 97 | |
| 98 | ### 4. Orchestrator-Workers |
| 99 | |
| 100 | Central LLM decomposes tasks and delegates to worker LLMs. |
| 101 | |
| 102 | ```python |
| 103 | class Orchestrator: |
| 104 | async def run(self, task): |
| 105 | # Break down the task |
| 106 | subtasks = await self.plan(task) |
| 107 | |
| 108 | # Delegate to workers |
| 109 | results = [] |
| 110 | for subtask in subtasks: |
| 111 | worker_result = await self.delegate(subtask) |
| 112 | results.append(worker_result) |
| 113 | |
| 114 | # Synthesize results |
| 115 | return await self.synthesize(results) |
| 116 | |
| 117 | async def plan(self, task): |
| 118 | response = await llm.generate( |
| 119 | f"Break this task into subtasks:\n{task}\n\nReturn as JSON array." |
| 120 | ) |
| 121 | return json.loads(response) |
| 122 | |
| 123 | async def delegate(self, subtask): |
| 124 | return await llm.generate(f"Complete this subtask:\n{subtask}") |
| 125 | |
| 126 | async def synthesize(self, results): |
| 127 | return await llm.generate( |
| 128 | f"Combine these results into a coherent response:\n{results}" |
| 129 | ) |
| 130 | ``` |
| 131 | |
| 132 | **Use when**: Tasks require dynamic decomposition that can't be predetermined. |
| 133 | |
| 134 | ### 5. Evaluator-Optimizer |
| 135 | |
| 136 | One LLM generates, another evaluates and requests improvements. |
| 137 | |
| 138 | ```python |
| 139 | async def generate_with_feedback(task, max_iterations=3): |
| 140 | response = await llm.generate(f"Complete this task:\n{task}") |
| 141 | |
| 142 | for _ in range(max_iterations): |
| 143 | evaluation = await llm.generate( |
| 144 | f"Evaluate this response for quality and correctness:\n{response}\n" |
| 145 | "If improvements needed, specify them. Otherwise respond 'APPROVED'." |
| 146 | ) |
| 147 | |
| 148 | if "APPROVED" in evaluation: |
| 149 | return response |
| 150 | |
| 151 | response = await llm.generate( |
| 152 | f"Improve this response based on feedback:\n" |
| 153 | f"Original: {response}\nFeedback: {evaluation}" |
| 154 | ) |
| 155 | |
| 156 | return response |
| 157 | ``` |