← Blog

AI Skills Every Developer Needs in 2026

·7 min read·AISkillsLLMPrompt EngineeringRAGAgents

The AI tooling landscape has settled enough that the question is no longer "should I learn this" but "which parts actually matter for my work." This post is my honest breakdown — what's worth investing time in, what's overhyped, and what separates engineers who ship AI products from those who build demos.


The Core Skill Map

Skill AreaPriorityWhy It Matters
Prompt engineeringHighAffects output quality on everything you build
Embeddings & RAGHighMost real AI products need external knowledge
Tool use / AgentsHighUnlocks automation beyond chat
Evals & testingHighThe only way to ship AI reliably
Fine-tuningMediumUseful for specific domains, costly to maintain
Model architectureLowUnless you're at a lab, it's academic

1. Prompt Engineering

This sounds basic but most developers underinvest here. The difference between a prompt that works and one that's production-ready is significant.

What actually works:

# ❌ Vague
prompt = "Summarize this document."
 
# ✅ Specific, with constraints and format
prompt = """
Summarize the following document in 3 bullet points.
- Each bullet must be under 20 words
- Focus on actionable insights, not background
- If the document is too short to summarize meaningfully, say "N/A"
 
Document:
{document}
"""

Chain-of-thought for complex tasks:

prompt = """
You are analyzing a customer support ticket.
 
First, identify the problem type (bug / feature request / billing / other).
Then, assess urgency (high / medium / low) based on the user's language.
Finally, suggest the appropriate team to route it to.
 
Think through each step before giving your final answer.
 
Ticket: {ticket}
"""

Structured output:

Most LLMs support JSON mode or structured output now. Use it — parsing free-form text is fragile:

from anthropic import Anthropic
import json
 
client = Anthropic()
 
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=512,
    system="You always respond with valid JSON only, no prose.",
    messages=[{
        "role": "user",
        "content": f"Extract: name, email, company from: {text}"
    }]
)
 
data = json.loads(response.content[0].text)

2. Embeddings and RAG

Retrieval-Augmented Generation is how you give an LLM access to your data without fine-tuning. The architecture is simple:

  1. Chunk and embed your documents
  2. Store embeddings in a vector database
  3. At query time, embed the question and retrieve similar chunks
  4. Pass retrieved chunks + question to the LLM
from openai import OpenAI
import numpy as np
 
client = OpenAI()
 
def embed(text: str) -> list[float]:
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding
 
def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
 
# Index your docs
docs = ["FastAPI handles async by default.", "Django uses sync views by default."]
doc_embeddings = [embed(d) for d in docs]
 
# Query
query = "Which framework is better for async APIs?"
query_embedding = embed(query)
 
scores = [cosine_similarity(query_embedding, de) for de in doc_embeddings]
best = docs[np.argmax(scores)]
print(f"Most relevant: {best}")

For production, use a real vector DB:

  • pgvector — if you're already on PostgreSQL, lowest overhead
  • Pinecone — managed, scales well, good SDK
  • Qdrant — open source, strong filtering support
  • Weaviate — good for hybrid search (vector + keyword)

Chunking strategy matters more than the model:

def chunk_document(text: str, size: int = 512, overlap: int = 64) -> list[str]:
    words = text.split()
    chunks = []
    i = 0
    while i < len(words):
        chunk = " ".join(words[i:i + size])
        chunks.append(chunk)
        i += size - overlap
    return chunks

Overlap prevents important context from being split across chunk boundaries.


3. Tool Use and Agents

An agent is a loop: the model decides what to do, calls a tool, receives the result, and continues until the task is done.

def run_agent(task: str, tools: list, max_steps: int = 10):
    messages = [{"role": "user", "content": task}]
 
    for _ in range(max_steps):
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=2048,
            tools=tools,
            messages=messages
        )
 
        messages.append({"role": "assistant", "content": response.content})
 
        if response.stop_reason == "end_turn":
            return response.content[0].text
 
        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    result = execute_tool(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": str(result)
                    })
 
            messages.append({"role": "user", "content": tool_results})
 
    return "Max steps reached"

Keep tools small and well-named. One tool that does three things is harder for the model to reason about than three tools with clear purposes.


4. Evals — The Skill Nobody Talks About

This is the biggest differentiator between AI demos and AI products. Without evals, you're shipping blind.

A basic eval harness:

import json
from typing import Callable
 
def run_eval(
    test_cases: list[dict],
    fn: Callable,
    scorer: Callable
) -> dict:
    results = []
    for case in test_cases:
        output = fn(case["input"])
        score = scorer(output, case["expected"])
        results.append({
            "input": case["input"],
            "output": output,
            "expected": case["expected"],
            "score": score,
            "passed": score >= 0.8
        })
 
    passed = sum(1 for r in results if r["passed"])
    return {
        "total": len(results),
        "passed": passed,
        "pass_rate": passed / len(results),
        "results": results
    }
 
# Example test cases for a summarizer
test_cases = [
    {
        "input": "FastAPI is a modern Python web framework...",
        "expected": "Framework for building Python APIs"
    },
    # ...
]
 
report = run_eval(test_cases, my_summarizer, semantic_similarity_scorer)
print(f"Pass rate: {report['pass_rate']:.0%}")

Types of evals to build:

Eval TypeWhat It Tests
Unit evalsSingle input → expected output
Regression evalsNew model/prompt didn't break old cases
Human evalsPreference between two outputs
Red-team evalsAdversarial inputs, safety, jailbreaks

Run evals before every prompt change. They're not tests for the LLM — they're tests for your system.


5. Context Management

LLMs have context windows but long contexts get expensive and slower. Manage what goes in:

def build_context(
    system: str,
    history: list[dict],
    user_message: str,
    max_tokens: int = 4000
) -> list[dict]:
    # Estimate tokens (rough: 1 token ≈ 4 chars)
    budget = max_tokens
    budget -= len(system) // 4
    budget -= len(user_message) // 4
 
    # Include recent history that fits
    included = []
    for msg in reversed(history):
        cost = len(msg["content"]) // 4
        if budget - cost < 0:
            break
        included.insert(0, msg)
        budget -= cost
 
    return included

For long-running sessions, summarize old turns instead of truncating them:

summary = client.messages.create(
    model="claude-haiku-4-5-20251001",  # cheap model for summarization
    max_tokens=256,
    messages=[{
        "role": "user",
        "content": f"Summarize this conversation in 3 sentences:\n{old_history}"
    }]
).content[0].text

6. MCP — Model Context Protocol

MCP is the emerging standard for giving LLMs access to tools and data sources. If you're building internal tooling or integrations, it's worth understanding now.

# MCP server (Python)
from mcp.server import Server
from mcp.server.models import InitializationOptions
 
server = Server("my-tools")
 
@server.list_tools()
async def list_tools():
    return [
        Tool(name="search_db", description="Search the internal database")
    ]
 
@server.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "search_db":
        return search_internal_db(arguments["query"])

MCP servers can be connected to Claude Desktop, Cursor, or any MCP-compatible client — without writing custom integrations for each.


What to Focus On First

If you're just getting started with AI engineering, the order that gives the fastest ROI:

  1. Prompt engineering — immediate impact on everything
  2. Structured output + basic evals — makes your work reliable
  3. RAG with pgvector — covers 80% of "give the LLM my data" use cases
  4. Tool use / agents — once you have reliable primitives
  5. MCP — when you're building integrations or internal tooling

Skip fine-tuning until you've exhausted what good prompting + RAG can do. It's expensive to run and expensive to maintain.

The engineers who ship the best AI products aren't the ones who understand transformers — they're the ones who understand how to test, iterate, and fail fast.