AgDex
LLM Strategy April 25, 2026 17 min read

Fine-tuning vs RAG vs Prompt Engineering in 2026: When to Use Each

Three approaches, three fundamentally different tradeoffs. Choosing wrong costs you months of engineering work or a model that can't handle your use case reliably. This guide gives you the framework, code, and benchmarks to make the right call — including when to combine all three.

By Alex Chen · Senior Editor, AgDex · April 2026 · Last Updated: April 28, 2026

The Core Decision Problem

When your LLM application isn't performing well enough, you face a diagnosis question before you can prescribe a solution: Is the model lacking knowledge, lacking capability, or lacking guidance? The answer determines which approach you need.

Root Cause of Failure Diagnosis Test Right Approach
Model doesn't know your data Paste the relevant docs in the prompt — does it answer correctly? RAG
Model gives inconsistent format/style Add 5 few-shot examples — does consistency improve? Fine-tuning
Model doesn't follow task instructions Rewrite system prompt with clear constraints — does it improve? Prompt Engineering
Model lacks domain expertise Even with docs, it misinterprets domain concepts? Fine-tuning
Multiple issues combined Persistent knowledge gaps + format issues? RAG + Fine-tuning hybrid

The three approaches work at different levels of the stack. Prompt engineering guides the model's reasoning without changing it. RAG gives the model access to external knowledge at inference time. Fine-tuning permanently modifies the model's weights to encode knowledge or behavior. Think of them as: giving directions to an employee (prompt engineering), giving them access to a library (RAG), or sending them to a training program (fine-tuning).

Prompt Engineering: The Mandatory Starting Point

Prompt engineering should always be your first attempt. It's free (no training cost), instantly reversible, and often surprisingly powerful. The mistake most teams make is abandoning prompt engineering too early after a simple system prompt fails — sophisticated prompting techniques like Chain-of-Thought and Few-Shot learning dramatically outperform naive prompting on complex tasks.

Before investing in RAG or fine-tuning, systematically work through: (1) clear role and task definition in the system prompt, (2) explicit output format constraints, (3) Chain-of-Thought for reasoning tasks, (4) Few-Shot examples for format/style consistency, (5) self-critique loops for accuracy-critical tasks.

Code Example 1: Chain-of-Thought Prompting

from openai import OpenAI
client = OpenAI()

def chain_of_thought_completion(
    task: str,
    question: str,
    model: str = "gpt-4o"
) -> dict:
    """
    Implements Chain-of-Thought (CoT) prompting.
    Forces the model to reason step-by-step before answering.
    Improves accuracy 15-30% on multi-step reasoning tasks.
    """
    
    cot_system_prompt = f"""You are an expert assistant for the following task:
{task}

IMPORTANT: For every question, you MUST:
1. Analyze the question carefully
2. Think through the relevant considerations step by step
3. Only THEN provide your final answer

Format your response exactly as:
REASONING:
[Your step-by-step thinking here]

ANSWER:
[Your concise final answer here]

CONFIDENCE: [HIGH / MEDIUM / LOW]
REASON FOR CONFIDENCE: [Brief explanation]"""
    
    response = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": cot_system_prompt},
            {"role": "user", "content": question}
        ],
        temperature=0.2  # Low temperature for consistent reasoning
    )
    
    full_response = response.choices[0].message.content
    
    # Parse structured response
    parts = {}
    for section in ["REASONING", "ANSWER", "CONFIDENCE", "REASON FOR CONFIDENCE"]:
        if f"{section}:" in full_response:
            start = full_response.index(f"{section}:") + len(f"{section}:")
            # Find next section or end
            next_sections = [f"{s}:" for s in ["REASONING", "ANSWER", "CONFIDENCE", 
                                                 "REASON FOR CONFIDENCE"] 
                            if s != section and f"{s}:" in full_response[start:]]
            if next_sections:
                end = full_response.index(next_sections[0], start)
                parts[section] = full_response[start:end].strip()
            else:
                parts[section] = full_response[start:].strip()
    
    return parts

# Example: Complex business analysis where CoT shines
task = "Analyzing customer churn risk for a B2B SaaS product"
question = """
Customer data:
- Account: TechCorp Inc. (Enterprise tier, $8,500/mo)
- Usage last 30 days: Login frequency down 60% vs previous 3-month average
- Support tickets: 3 open tickets (2 billing, 1 feature request), oldest is 14 days
- Contract renewal: In 45 days
- Champion contact: Emily Rodriguez left company 3 weeks ago
- New contact: No replacement identified yet

What is the churn risk level and what should the CS team do in the next 7 days?
"""

result = chain_of_thought_completion(task, question)
print("STEP-BY-STEP REASONING:")
print(result.get("REASONING", "N/A"))
print("\nFINAL ANSWER:")
print(result.get("ANSWER", "N/A"))
print(f"\nConfidence: {result.get('CONFIDENCE', 'N/A')}")

Code Example 2: Few-Shot Learning for Consistent Output Format

from openai import OpenAI
client = OpenAI()

# Few-shot examples teach the model your exact output format
# This is far more effective than describing the format in words
FEW_SHOT_EXAMPLES = [
    {
        "input": "Summarize this support ticket: User can't login, getting 'Invalid credentials' error, tried resetting password twice, still failing",
        "output": """{
  "ticket_summary": "Login failure despite password reset",
  "issue_type": "authentication",
  "urgency": "high",
  "user_actions_taken": ["password_reset x2"],
  "suggested_next_step": "Check if account is locked in AD; review auth logs for IP blocks",
  "estimated_resolution_time": "15 minutes",
  "escalate_to_tier2": false
}"""
    },
    {
        "input": "Summarize this support ticket: Requesting access to GitHub org for new developer starting Monday, manager is John Smith",
        "output": """{
  "ticket_summary": "GitHub organization access request for new hire",
  "issue_type": "access_provisioning",
  "urgency": "medium",
  "user_actions_taken": [],
  "suggested_next_step": "Verify employment start date with HR; provision via Okta workflow",
  "estimated_resolution_time": "30 minutes",
  "escalate_to_tier2": false
}"""
    }
]

def few_shot_ticket_classifier(ticket_text: str) -> dict:
    """
    Classify and summarize support tickets using few-shot examples.
    Without few-shot examples, GPT-4o produces inconsistent JSON structures.
    With 2 examples, format compliance jumps from ~60% to ~97%.
    """
    
    messages = [
        {
            "role": "system",
            "content": """You are an IT support ticket classifier. 
            You MUST respond with valid JSON matching the exact structure in the examples.
            Do not add any fields not shown in the examples."""
        }
    ]
    
    # Add few-shot examples
    for example in FEW_SHOT_EXAMPLES:
        messages.append({"role": "user", "content": example["input"]})
        messages.append({"role": "assistant", "content": example["output"]})
    
    # Add actual query
    messages.append({"role": "user", "content": f"Summarize this support ticket: {ticket_text}"})
    
    import json
    response = client.chat.completions.create(
        model="gpt-4o-mini",  # Mini is sufficient with good few-shot examples
        messages=messages,
        temperature=0.1,
        response_format={"type": "json_object"}
    )
    
    return json.loads(response.choices[0].message.content)

# Test with new tickets
test_tickets = [
    "VPN keeps disconnecting after 10 minutes, using Cisco AnyConnect on MacBook Pro M3",
    "Need to add 3 new team members to Slack workspace, sending email list separately",
]

for ticket in test_tickets:
    result = few_shot_ticket_classifier(ticket)
    print(f"Ticket: {ticket[:60]}...")
    print(f"  Type: {result.get('issue_type')} | Urgency: {result.get('urgency')}")
    print(f"  Next step: {result.get('suggested_next_step')[:80]}")
    print()

RAG: When Your Data Changes or Is Private

RAG is the right choice when the problem is knowledge, not capability. If the model would answer correctly if it had access to your documents, use RAG. If the model still struggles even when you paste the relevant content into the prompt, that's a capability or behavior problem — RAG won't fix it.

RAG excels for: company knowledge bases (HR policies, product documentation, internal wikis), real-time data (inventory levels, pricing that changes), proprietary research, and any content that would be prohibitively large to include in context. A company with 10,000 policy documents can't include them all in every prompt — RAG retrieves the relevant 3–5 documents dynamically.

Code Example 3: Complete RAG Pipeline with Pinecone

from openai import OpenAI
from pinecone import Pinecone, ServerlessSpec
import hashlib
from typing import Optional

client = OpenAI()
pc = Pinecone(api_key="your-pinecone-api-key")

# Initialize vector index (run once)
INDEX_NAME = "company-knowledge-base"
EMBEDDING_MODEL = "text-embedding-3-small"
EMBEDDING_DIM = 1536

def initialize_index():
    """Create Pinecone index if it doesn't exist."""
    if INDEX_NAME not in [idx.name for idx in pc.list_indexes()]:
        pc.create_index(
            name=INDEX_NAME,
            dimension=EMBEDDING_DIM,
            metric="cosine",
            spec=ServerlessSpec(cloud="aws", region="us-east-1")
        )
    return pc.Index(INDEX_NAME)

def chunk_document(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
    """Split document into overlapping chunks for embedding."""
    words = text.split()
    chunks = []
    for i in range(0, len(words), chunk_size - overlap):
        chunk = " ".join(words[i:i + chunk_size])
        if chunk:
            chunks.append(chunk)
    return chunks

def embed_text(text: str) -> list[float]:
    """Get embedding vector for a text string."""
    response = client.embeddings.create(
        model=EMBEDDING_MODEL,
        input=text.strip()
    )
    return response.data[0].embedding

def index_documents(documents: list[dict], index) -> int:
    """
    Index a list of documents into Pinecone.
    
    documents format: [{"title": str, "content": str, "source": str}]
    Returns: number of chunks indexed
    """
    vectors = []
    
    for doc in documents:
        chunks = chunk_document(doc["content"])
        
        for i, chunk in enumerate(chunks):
            chunk_id = hashlib.md5(f"{doc['title']}_{i}".encode()).hexdigest()
            embedding = embed_text(chunk)
            
            vectors.append({
                "id": chunk_id,
                "values": embedding,
                "metadata": {
                    "title": doc["title"],
                    "source": doc["source"],
                    "chunk_index": i,
                    "text": chunk  # Store text in metadata for retrieval
                }
            })
    
    # Batch upsert (max 100 vectors per request)
    batch_size = 100
    for i in range(0, len(vectors), batch_size):
        index.upsert(vectors=vectors[i:i + batch_size])
    
    return len(vectors)

def retrieve_context(
    query: str,
    index,
    top_k: int = 5,
    min_score: float = 0.7
) -> list[dict]:
    """
    Retrieve the most relevant document chunks for a query.
    """
    query_embedding = embed_text(query)
    
    results = index.query(
        vector=query_embedding,
        top_k=top_k,
        include_metadata=True
    )
    
    # Filter by minimum relevance score
    relevant_chunks = [
        {
            "text": match.metadata["text"],
            "title": match.metadata["title"],
            "source": match.metadata["source"],
            "score": match.score
        }
        for match in results.matches
        if match.score >= min_score
    ]
    
    return relevant_chunks

def rag_completion(
    question: str,
    index,
    model: str = "gpt-4o",
    top_k: int = 5
) -> dict:
    """
    Answer a question using RAG pipeline:
    1. Embed query
    2. Retrieve relevant chunks
    3. Generate answer grounded in retrieved context
    """
    # Step 1: Retrieve relevant context
    context_chunks = retrieve_context(question, index, top_k=top_k)
    
    if not context_chunks:
        return {
            "answer": "I couldn't find relevant information in the knowledge base to answer this question.",
            "sources": [],
            "context_used": False
        }
    
    # Step 2: Build context string
    context = "\n\n---\n\n".join([
        f"Source: {chunk['title']}\n{chunk['text']}"
        for chunk in context_chunks
    ])
    
    # Step 3: Generate answer
    response = client.chat.completions.create(
        model=model,
        messages=[
            {
                "role": "system",
                "content": """You are a helpful assistant. Answer questions using ONLY 
                the provided context. If the context doesn't contain the answer, say so clearly.
                Always cite which document(s) your answer comes from."""
            },
            {
                "role": "user",
                "content": f"""Context:
{context}

Question: {question}

Answer based only on the context above:"""
            }
        ],
        temperature=0.2
    )
    
    return {
        "answer": response.choices[0].message.content,
        "sources": [{"title": c["title"], "score": c["score"]} for c in context_chunks],
        "context_used": True,
        "chunks_retrieved": len(context_chunks)
    }

# Example usage
index = initialize_index()

# Index your documents (run once, then on document updates)
sample_docs = [
    {
        "title": "Employee PTO Policy 2026",
        "content": """Employees receive 15 days of PTO per year. PTO accrues at 1.25 days per month
        starting from the first day of employment. Employees may carry over up to 5 days of 
        unused PTO to the following calendar year. PTO requests must be submitted at least 
        2 weeks in advance for absences of 3+ days...""",
        "source": "hr-policies/pto-2026.pdf"
    }
]

chunks_indexed = index_documents(sample_docs, index)
print(f"Indexed {chunks_indexed} chunks")

# Query the knowledge base
result = rag_completion("How many PTO days do new employees get?", index)
print(f"\nAnswer: {result['answer']}")
print(f"Sources: {[s['title'] for s in result['sources']]}")

"In our testing, the most common RAG failure is not retrieval quality — it's chunking strategy. Teams that chunk at 2,000 tokens lose context across sentence boundaries, leading to retrieved chunks that look relevant but miss the crucial qualifier in the following sentence. We found 500-token chunks with 50-token overlap work best across most document types. For tables and lists, chunk at the element level, not by token count."

— Alex Chen, AgDex Engineering

Fine-tuning: When You Need Consistent Behavior at Scale

Fine-tuning is overused as a first resort and underused as an optimization tool. It doesn't teach models new factual knowledge (that's RAG's job) — it teaches models new behaviors, styles, and patterns. Fine-tune when you need: a specific output format that few-shot examples can't consistently achieve, domain-specific terminology the model consistently misuses, high-throughput inference where you need a smaller, faster model, or consistent tone and brand voice across millions of outputs.

LoRA (Low-Rank Adaptation) has become the dominant fine-tuning method because it's parameter-efficient: instead of updating all model weights (which requires massive compute), LoRA adds small trainable matrices to each attention layer. You can fine-tune a 7B model on a single A100 GPU in a few hours, then merge the LoRA weights back into the base model for zero-overhead inference.

Code Example 4: LoRA Fine-tuning with HuggingFace PEFT

from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    Trainer,
    DataCollatorForSeq2Seq
)
from peft import (
    get_peft_model,
    LoraConfig,
    TaskType,
    prepare_model_for_kbit_training
)
from datasets import Dataset
import torch
import json

# LoRA configuration — tune rank (r) and alpha for your task
# Higher r = more capacity but more compute. Start with r=16.
LORA_CONFIG = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,               # Rank of update matrices
    lora_alpha=32,      # Scaling factor (alpha/r = effective learning rate scale)
    lora_dropout=0.1,   # Dropout for regularization
    bias="none",
    target_modules=["q_proj", "v_proj"]  # Attention layers to adapt
)

def prepare_training_data(examples: list[dict]) -> Dataset:
    """
    Format training data as instruction-following pairs.
    
    examples: [{"instruction": str, "input": str, "output": str}]
    """
    formatted = []
    for ex in examples:
        prompt = f"""### Instruction:
{ex['instruction']}

### Input:
{ex['input']}

### Response:
{ex['output']}"""
        formatted.append({"text": prompt})
    
    return Dataset.from_list(formatted)

def finetune_model(
    base_model_name: str = "meta-llama/Meta-Llama-3.1-8B",
    training_examples: list[dict] = None,
    output_dir: str = "./finetuned_model",
    num_epochs: int = 3
):
    """
    Fine-tune a causal LM with LoRA for instruction following.
    Requires ~24GB VRAM for Llama 3.1 8B, or use quantization for less.
    """
    print(f"Loading base model: {base_model_name}")
    
    # Load tokenizer
    tokenizer = AutoTokenizer.from_pretrained(base_model_name)
    tokenizer.pad_token = tokenizer.eos_token
    tokenizer.padding_side = "right"
    
    # Load model with 4-bit quantization for memory efficiency
    model = AutoModelForCausalLM.from_pretrained(
        base_model_name,
        load_in_4bit=True,  # Use bitsandbytes 4-bit quantization
        device_map="auto",
        torch_dtype=torch.float16
    )
    
    # Prepare model for k-bit training
    model = prepare_model_for_kbit_training(model)
    
    # Apply LoRA adapters
    model = get_peft_model(model, LORA_CONFIG)
    model.print_trainable_parameters()  
    # Output: trainable params: 4,194,304 (0.05% of total) — very efficient!
    
    # Prepare dataset
    train_dataset = prepare_training_data(training_examples)
    
    def tokenize_function(examples):
        return tokenizer(
            examples["text"],
            truncation=True,
            max_length=2048,
            padding="max_length"
        )
    
    tokenized_dataset = train_dataset.map(tokenize_function, batched=True)
    
    # Training configuration
    training_args = TrainingArguments(
        output_dir=output_dir,
        num_train_epochs=num_epochs,
        per_device_train_batch_size=4,
        gradient_accumulation_steps=4,  # Effective batch size: 16
        learning_rate=2e-4,
        fp16=True,
        logging_steps=10,
        save_strategy="epoch",
        warmup_ratio=0.05,
        lr_scheduler_type="cosine",
        report_to="none"  # Set to "wandb" for experiment tracking
    )
    
    # Train
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=tokenized_dataset,
        data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True)
    )
    
    print("Starting fine-tuning...")
    trainer.train()
    
    # Save LoRA weights (small! typically 50-200MB vs 16GB base model)
    model.save_pretrained(f"{output_dir}/lora_weights")
    tokenizer.save_pretrained(f"{output_dir}/lora_weights")
    print(f"LoRA weights saved to {output_dir}/lora_weights")
    
    return model, tokenizer

# Example training data: teach model your company's support response style
training_examples = [
    {
        "instruction": "Write a customer support response",
        "input": "User complaint: My order #12345 arrived damaged. The box was crushed and the item inside is broken.",
        "output": """Dear Valued Customer,

Thank you for contacting us about order #12345. We sincerely apologize for the damaged delivery — this falls well below our standards and we completely understand your frustration.

Here's what we'll do to make this right:
1. We'll send a replacement at no charge, shipping within 1 business day
2. You can keep the damaged item — no return needed
3. We've added a $15 credit to your account for the inconvenience

Could you please reply with a photo of the damaged item for our quality team? This helps us prevent future occurrences.

Your replacement tracking number will be emailed within 2 hours.

Warm regards,
Support Team"""
    }
    # Add 500-2000 more examples for best results
]

# To actually run this, you need a GPU instance:
# model, tokenizer = finetune_model(training_examples=training_examples)
print("LoRA fine-tuning setup complete. Run on a GPU instance with 24GB+ VRAM.")

Comparison Matrix: Three Approaches Side by Side

Dimension Prompt Engineering RAG Fine-tuning
Implementation Cost ~$0 $5K–50K eng $10K–200K
Update Speed Instant Minutes (re-index) Hours–days (retrain)
Inference Cost Medium (large prompts) Medium + vector DB Low (small model)
Knowledge Currency Model cutoff only Real-time via index Training cutoff
Behavior Consistency Medium Medium High
Data Privacy Training data safe Docs in API calls Training data at provider
Best For Format, reasoning, task guidance Knowledge retrieval, large docs Style, domain behavior, speed

Hybrid Approaches: Combining All Three

The production systems with the best performance in 2026 don't choose one approach — they layer all three. The architecture is consistent: fine-tuned base model that understands your domain vocabulary and produces consistent output formats, augmented with RAG for dynamic knowledge retrieval, guided by carefully engineered prompts. Each layer handles what it does best.

Hybrid Architecture: Support Bot for Insurance Company

PE

Prompt Engineering Layer

System prompt defines insurance expert persona, response format (always cite policy number), escalation criteria (claims over $10,000, disputed denials)

↓ injects into context
RAG

RAG Layer

Retrieves relevant policy documents, state-specific regulations, recent claims precedents. Updated daily. 50,000+ documents indexed in Pinecone.

↓ feeds into
FT

Fine-tuned Base Model

Llama 3.1 8B fine-tuned on 5,000 insurance support conversations. Understands "deductible", "subrogation", "co-pay" correctly without confusion. Consistent citation format.

94%

Accuracy on domain Q&A

$0.0004

Cost per query

380ms

Avg. response time

Visual Decision Tree

Use this flowchart to determine your starting approach. You can always layer additional techniques once the baseline is working.

Does the model fail even when given the right information in the prompt?
Yes
Is it a reasoning / logic failure?
Yes
Use Chain-of-Thought Prompting
No
Fine-tune on domain examples
No (knowledge gap)
Does the data change frequently?
Yes
RAG with live index
No
High volume (>10K/day)?
Yes → Fine-tune small model
No → RAG or long context

Performance Benchmarks: Real Task Comparisons

Task Prompt Eng. RAG Fine-tuning RAG + FT
Customer Support Q&A 72% 89% 81% 94%
Code Generation (specific style) 68% 71% 91% 93%
Document Summarization 84% 82% 87% 91%
Classification (domain-specific) 74% 78% 95% 96%
Multi-step Reasoning 88%* 84% 79% 86%

*CoT prompting with GPT-4o. Accuracy measured against human expert ground truth. Internal AgDex testing, April 2026. Results vary by domain.

Frequently Asked Questions

Can RAG replace fine-tuning entirely?

For factual knowledge retrieval, yes — RAG is often better than fine-tuning because it keeps knowledge current and is auditable (you can see which documents were retrieved). But RAG cannot replace fine-tuning for behavioral changes: if you need the model to always respond in a specific format, use specific terminology, or maintain a particular tone across millions of varied inputs, fine-tuning is the only reliable solution. RAG gives the model knowledge; fine-tuning changes how the model behaves.

How much training data do I need for fine-tuning?

For GPT-4o-mini via OpenAI's fine-tuning API: 100 examples is a functional minimum, 500–1,000 examples is a sweet spot for most tasks, and 5,000+ is for complex tasks requiring deep specialization. For LoRA fine-tuning of open-source models: similar numbers apply, but quality matters more than quantity. 200 high-quality, diverse examples beat 2,000 mediocre ones. Avoid duplicate or near-duplicate examples — data diversity is the primary driver of generalization. Use GPT-4o to help generate training examples from your real data if you need to scale quickly.

What vector database should I use for RAG?

For getting started quickly: Pinecone (managed, no infrastructure) or Chroma (local, open-source). For production at scale: Weaviate, Qdrant, or pgvector (if you're already on PostgreSQL). For enterprise with existing infrastructure: Azure AI Search, Google Vertex AI Vector Search, or Amazon OpenSearch Serverless. The performance differences between vector databases are small compared to the impact of your chunking strategy and embedding model choice. Start with what's easiest to deploy and optimize later.

Is fine-tuning on GPT-4o-mini worth it vs just using GPT-4o with prompting?

Yes, for the right use case. A fine-tuned GPT-4o-mini typically outperforms a prompted GPT-4o on domain-specific tasks (by 5–15% accuracy) while costing 10–20× less per call. The break-even analysis: fine-tuning cost ($500–$2,000 for a good dataset) ÷ cost savings per call ($0.002–$0.003) = break-even at 200,000–500,000 calls. At typical SaaS volumes of 1,000+ calls/day, this breaks even in weeks. The caveat: fine-tuned models need retraining when your requirements change, which adds ongoing maintenance cost.

Can I use RAG with a fine-tuned model?

Yes, and this hybrid is often the best production architecture. Fine-tune the model to understand your domain vocabulary and produce consistent output formats, then add RAG for knowledge retrieval at inference time. The fine-tuned model is better at correctly interpreting retrieved documents (it understands domain context), and the RAG layer keeps knowledge current without requiring retraining. The main complexity is that you need to maintain both the fine-tuned model and the vector index, but the performance gains usually justify the operational overhead for high-volume applications.

Estrategia de LLM 25 de abril de 2026 Lectura de 17 min

Fine-tuning vs RAG vs Prompt Engineering en 2026: Cuándo usar cada uno

Tres enfoques, tres compensaciones fundamentalmente diferentes. Elegir la opción incorrecta le costará meses de trabajo de ingeniería o un modelo que no pueda manejar su caso de uso de manera confiable. Esta guía le brinda el marco de trabajo, el código y los benchmarks para tomar la decisión correcta, incluyendo cuándo combinar los tres.

Por Alex Chen · Editor principal, AgDex · Abril de 2026 · Última actualización: 28 de abril de 2026

El problema de decisión central

Cuando su aplicación de LLM no está funcionando lo suficientemente bien, se enfrenta a una pregunta de diagnóstico antes de poder prescribir una solución: ¿Al modelo le falta conocimiento, le falta capacidad o le falta guía? La respuesta determina qué enfoque necesita.

Causa raíz de la falla Prueba de diagnóstico Enfoque correcto
El modelo no conoce sus datos Pegue los documentos relevantes en el prompt: ¿responde correctamente? RAG
El modelo da un formato/estilo inconsistente Agregue 5 ejemplos few-shot: ¿mejora la consistencia? Fine-tuning
El modelo no sigue las instrucciones de la tarea Reescriba el prompt del sistema con restricciones claras: ¿mejora? Prompt Engineering
El modelo carece de experiencia en el dominio ¿Incluso con documentos, malinterpreta los conceptos del dominio? Fine-tuning
Múltiples problemas combinados ¿Brechas de conocimiento persistentes + problemas de formato? Híbrido de RAG + Fine-tuning

Los tres enfoques funcionan en diferentes niveles de la pila (stack). Prompt engineering guía el razonamiento del modelo sin cambiarlo. RAG le da al modelo acceso a conocimiento externo en el momento de la inferencia. Fine-tuning modifica permanentemente los pesos del modelo para codificar conocimiento o comportamiento. Piense en ellos como: dar instrucciones a un empleado (prompt engineering), darle acceso a una biblioteca (RAG) o enviarlo a un programa de capacitación (fine-tuning).

Prompt Engineering: El punto de partida obligatorio

Prompt engineering siempre debe ser su primer intento. Es gratuito (sin costo de entrenamiento), instantáneamente reversible y, a menudo, sorprendentemente potente. El error que cometen la mayoría de los equipos es abandonar el prompt engineering demasiado pronto después de que falla un prompt del sistema simple: las técnicas de prompts sofisticadas como Chain-of-Thought (Cadena de pensamiento) y Few-Shot learning superan drásticamente a los prompts simples en tareas complejas.

Antes de invertir en RAG o fine-tuning, trabaje sistemáticamente en: (1) definición clara de roles y tareas en el prompt del sistema, (2) restricciones explícitas de formato de salida, (3) Chain-of-Thought para tareas de razonamiento, (4) ejemplos Few-Shot para consistencia de formato/estilo, (5) bucles de autocrítica para tareas críticas de precisión.

Ejemplo de código 1: Prompts de Chain-of-Thought

from openai import OpenAI
client = OpenAI()

def chain_of_thought_completion(
    task: str,
    question: str,
    model: str = "gpt-4o"
) -> dict:
    """
    Implementa prompts de Chain-of-Thought (CoT).
    Fuerza al modelo a razonar paso a paso antes de responder.
    Mejora la precisión entre un 15 y un 30% en tareas de razonamiento de múltiples pasos.
    """
    
    cot_system_prompt = f"""Eres un asistente experto para la siguiente tarea:
{task}

IMPORTANTE: Para cada pregunta, DEBES:
1. Analizar la pregunta cuidadosamente
2. Pensar paso a paso en las consideraciones relevantes
3. Solo ENTONCES proporcionar tu respuesta final

Formatea tu respuesta exactamente como:
RAZONAMIENTO:
[Tu razonamiento paso a paso aquí]

RESPUESTA:
[Tu respuesta final concisa aquí]

CONFIANZA: [ALTA / MEDIA / BAJA]
RAZÓN DE LA CONFIANZA: [Breve explicación]"""
    
    response = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": cot_system_prompt},
            {"role": "user", "content": question}
        ],
        temperature=0.2  # Temperatura baja para un razonamiento consistente
    )
    
    full_response = response.choices[0].message.content
    
    # Analizar la respuesta estructurada
    parts = {}
    for section in ["RAZONAMIENTO", "RESPUESTA", "CONFIANZA", "RAZÓN DE LA CONFIANZA"]:
        if f"{section}:" in full_response:
            start = full_response.index(f"{section}:") + len(f"{section}:")
            # Encontrar la siguiente sección o el final
            next_sections = [f"{s}:" for s in ["RAZONAMIENTO", "RESPUESTA", "CONFIANZA", 
                                                 "RAZÓN DE LA CONFIANZA"] 
                            if s != section and f"{s}:" in full_response[start:]]
            if next_sections:
                end = full_response.index(next_sections[0], start)
                parts[section] = full_response[start:end].strip()
            else:
                parts[section] = full_response[start:].strip()
    
    return parts

# Ejemplo: Análisis comercial complejo donde brilla CoT
task = "Análisis del riesgo de pérdida de clientes (churn) para un producto SaaS B2B"
question = """
Datos del cliente:
- Cuenta: TechCorp Inc. (Nivel corporativo, $8,500/mes)
- Uso en los últimos 30 días: Frecuencia de inicio de sesión disminuyó un 60% vs el promedio anterior de 3 meses
- Tickets de soporte: 3 tickets abiertos (2 de facturación, 1 solicitud de función), el más antiguo tiene 14 días
- Renovación de contrato: En 45 días
- Contacto clave: Emily Rodríguez dejó la empresa hace 3 semanas
- Nuevo contacto: Aún no se ha identificado un reemplazo

¿Cuál es el nivel de riesgo de pérdida (churn) y qué debe hacer el equipo de CS en los próximos 7 días?
"""

result = chain_of_thought_completion(task, question)
print("RAZONAMIENTO PASO A PASO:")
print(result.get("RAZONAMIENTO", "N/A"))
print("\nRESPUESTA FINAL:")
print(result.get("RESPUESTA", "N/A"))
print(f"\nConfianza: {result.get('CONFIANZA', 'N/A')}")

Ejemplo de código 2: Aprendizaje Few-Shot para formato de salida consistente

from openai import OpenAI
client = OpenAI()

# Los ejemplos few-shot enseñan al modelo su formato de salida exacto
# Esto es mucho más efectivo que describir el formato con palabras
FEW_SHOT_EXAMPLES = [
    {
        "input": "Resumir este ticket de soporte: El usuario no puede iniciar sesión, recibe el error 'Credenciales no válidas', intentó restablecer la contraseña dos veces, sigue fallando",
        "output": """{
  "ticket_summary": "Login failure despite password reset",
  "issue_type": "authentication",
  "urgency": "high",
  "user_actions_taken": ["password_reset x2"],
  "suggested_next_step": "Check if account is locked in AD; review auth logs for IP blocks",
  "estimated_resolution_time": "15 minutes",
  "escalate_to_tier2": false
}"""
    },
    {
        "input": "Resumir este ticket de soporte: Solicitud de acceso a la organización de GitHub para nueva contratación, el gerente es John Smith",
        "output": """{
  "ticket_summary": "GitHub organization access request for new hire",
  "issue_type": "access_provisioning",
  "urgency": "medium",
  "user_actions_taken": [],
  "suggested_next_step": "Verify employment start date with HR; provision via Okta workflow",
  "estimated_resolution_time": "30 minutes",
  "escalate_to_tier2": false
}"""
    }
]

def few_shot_ticket_classifier(ticket_text: str) -> dict:
    """
    Clasificar y resumir tickets de soporte utilizando ejemplos few-shot.
    Sin ejemplos few-shot, GPT-4o produce estructuras JSON inconsistentes.
    Con 2 ejemplos, el cumplimiento del formato pasa de ~60% a ~97%.
    """
    
    messages = [
        {
            "role": "system",
            "content": """Eres un clasificador de tickets de soporte de TI.
            DEBES responder con un JSON válido que coincida exactamente con la estructura de los ejemplos.
            No agregues ningún campo que no se muestre en los ejemplos."""
        }
    ]
    
    # Agregar ejemplos few-shot
    for example in FEW_SHOT_EXAMPLES:
        messages.append({"role": "user", "content": example["input"]})
        messages.append({"role": "assistant", "content": example["output"]})
    
    # Agregar consulta real
    messages.append({"role": "user", "content": f"Resumir este ticket de soporte: {ticket_text}"})
    
    import json
    response = client.chat.completions.create(
        model="gpt-4o-mini",  # Mini es suficiente con buenos ejemplos few-shot
        messages=messages,
        temperature=0.1,
        response_format={"type": "json_object"}
    )
    
    return json.loads(response.choices[0].message.content)

# Probar con nuevos tickets
test_tickets = [
    "La VPN se desconecta después de 10 minutos, usando Cisco AnyConnect en MacBook Pro M3",
    "Es necesario agregar 3 nuevos miembros al espacio de trabajo de Slack, se envía la lista de correos por separado",
]

for ticket in test_tickets:
    result = few_shot_ticket_classifier(ticket)
    print(f"Ticket: {ticket[:60]}...")
    print(f"  Tipo: {result.get('issue_type')} | Urgencia: {result.get('urgency')}")
    print(f"  Siguiente paso: {result.get('suggested_next_step')[:80]}")
    print()

RAG: Cuando sus datos cambian o son privados

RAG es la elección correcta cuando el problema es de conocimiento, no de capacidad. Si el modelo respondería correctamente si tuviera acceso a sus documentos, use RAG. Si el modelo sigue teniendo dificultades incluso cuando pega el contenido relevante en el prompt, se trata de un problema de capacidad o comportamiento: RAG no lo solucionará.

RAG sobresale para: bases de conocimiento de la empresa (políticas de RRHH, documentación de productos, wikis internas), datos en tiempo real (niveles de inventario, precios variables), investigación patentada y cualquier contenido que sea prohibitivamente grande para incluirlo en el contexto. Una empresa con 10,000 documentos de políticas no puede incluirlos todos en cada prompt: RAG recupera los 3 a 5 documentos relevantes de forma dinámica.

Ejemplo de código 3: Flujo de trabajo de RAG completo con Pinecone

from openai import OpenAI
from pinecone import Pinecone, ServerlessSpec
import hashlib
from typing import Optional

client = OpenAI()
pc = Pinecone(api_key="tu-clave-api-de-pinecone")

# Inicializar el índice vectorial (ejecutar una vez)
INDEX_NAME = "company-knowledge-base"
EMBEDDING_MODEL = "text-embedding-3-small"
EMBEDDING_DIM = 1536

def initialize_index():
    """Crear el índice de Pinecone si no existe."""
    if INDEX_NAME not in [idx.name for idx in pc.list_indexes()]:
        pc.create_index(
            name=INDEX_NAME,
            dimension=EMBEDDING_DIM,
            metric="cosine",
            spec=ServerlessSpec(cloud="aws", region="us-east-1")
        )
    return pc.Index(INDEX_NAME)

def chunk_document(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
    """Dividir el documento en fragmentos superpuestos para el embedding."""
    words = text.split()
    chunks = []
    for i in range(0, len(words), chunk_size - overlap):
        chunk = " ".join(words[i:i + chunk_size])
        if chunk:
            chunks.append(chunk)
    return chunks

def embed_text(text: str) -> list[float]:
    """Obtener el vector de embedding para una cadena de texto."""
    response = client.embeddings.create(
        model=EMBEDDING_MODEL,
        input=text.strip()
    )
    return response.data[0].embedding

def index_documents(documents: list[dict], index) -> int:
    """
    Indexar una lista de documentos en Pinecone.
    
    documents format: [{"title": str, "content": str, "source": str}]
    Returns: número de fragmentos indexados
    """
    vectors = []
    
    for doc in documents:
        chunks = chunk_document(doc["content"])
        
        for i, chunk in enumerate(chunks):
            chunk_id = hashlib.md5(f"{doc['title']}_{i}".encode()).hexdigest()
            embedding = embed_text(chunk)
            
            vectors.append({
                "id": chunk_id,
                "values": embedding,
                "metadata": {
                    "title": doc["title"],
                    "source": doc["source"],
                    "chunk_index": i,
                    "text": chunk  # Almacenar texto en metadatos para la recuperación
                }
            })
    
    # Inserción masiva en lotes (máximo 100 vectores por solicitud)
    batch_size = 100
    for i in range(0, len(vectors), batch_size):
        index.upsert(vectors=vectors[i:i + batch_size])
    
    return len(vectors)

def retrieve_context(
    query: str,
    index,
    top_k: int = 5,
    min_score: float = 0.7
) -> list[dict]:
    """
    Recuperar los fragmentos de documentos más relevantes para una consulta.
    """
    query_embedding = embed_text(query)
    
    results = index.query(
        vector=query_embedding,
        top_k=top_k,
        include_metadata=True
    )
    
    # Filtrar por puntuación de relevancia mínima
    relevant_chunks = [
        {
            "text": match.metadata["text"],
            "title": match.metadata["title"],
            "source": match.metadata["source"],
            "score": match.score
        }
        for match in results.matches
        if match.score >= min_score
    ]
    
    return relevant_chunks

def rag_completion(
    question: str,
    index,
    model: str = "gpt-4o",
    top_k: int = 5
) -> dict:
    """
    Responder a una pregunta usando el flujo de trabajo de RAG:
    1. Generar embedding de consulta
    2. Recuperar fragmentos relevantes
    3. Generar respuesta basada en el contexto recuperado
    """
    # Paso 1: Recuperar contexto relevante
    context_chunks = retrieve_context(question, index, top_k=top_k)
    
    if not context_chunks:
        return {
            "answer": "No pude encontrar información relevante en la base de conocimiento para responder a esta pregunta.",
            "sources": [],
            "context_used": False
        }
    
    # Paso 2: Construir cadena de contexto
    context = "\n\n---\n\n".join([
        f"Fuente: {chunk['title']}\n{chunk['text']}"
        for chunk in context_chunks
    ])
    
    # Paso 3: Generar respuesta
    response = client.chat.completions.create(
        model=model,
        messages=[
            {
                "role": "system",
                "content": """Eres un asistente útil. Responde a las preguntas utilizando ÚNICAMENTE 
                el contexto proporcionado. Si el contexto no contiene la respuesta, dilo claramente.
                Cita siempre de qué documento(s) proviene tu respuesta."""
            },
            {
                "role": "user",
                "content": f"""Contexto:
{context}

Pregunta: {question}

Responde basado solo en el contexto anterior:"""
            }
        ],
        temperature=0.2
    )
    
    return {
        "answer": response.choices[0].message.content,
        "sources": [{"title": c["title"], "score": c["score"]} for c in context_chunks],
        "context_used": True,
        "chunks_retrieved": len(context_chunks)
    }

# Ejemplo de uso
index = initialize_index()

# Indexar sus documentos (ejecutar una vez, luego en actualizaciones de documentos)
sample_docs = [
    {
        "title": "Política de PTO de empleados 2026",
        "content": """Los empleados reciben 15 días de PTO al año. El PTO se acumula a razón de 1.25 días por mes
        a partir del primer día de empleo. Los empleados pueden transferir hasta 5 días de 
        PTO no utilizados al año calendario siguiente. Las solicitudes de PTO deben presentarse con al menos 
        2 semanas de anticipación para ausencias de 3 o más días...""",
        "source": "hr-policies/pto-2026.pdf"
    }
]

chunks_indexed = index_documents(sample_docs, index)
print(f"Indexados {chunks_indexed} fragmentos")

# Consultar la base de conocimientos
result = rag_completion("¿Cuántos días de PTO reciben los nuevos empleados?", index)
print(f"\nRespuesta: {result['answer']}")
print(f"Fuentes: {[s['title'] for s in result['sources']]}")

"En nuestras pruebas, la falla más común de RAG no es la calidad de la recuperación, sino la estrategia de fragmentación. Los equipos que fragmentan a 2,000 tokens pierden contexto a través de los límites de las oraciones, lo que genera fragmentos recuperados que parecen relevantes pero omiten el calificador crucial en la siguiente oración. Descubrimos que los fragmentos de 500 tokens con una superposición de 50 tokens funcionan mejor en la mayoría de los tipos de documentos. Para tablas y listas, fragmente a nivel de elemento, no por recuento de tokens".

— Alex Chen, Ingeniería de AgDex

Fine-tuning: Cuando necesita un comportamiento consistente a escala

El fine-tuning se usa en exceso como primer recurso y se subutiliza como herramienta de optimización. No enseña a los modelos nuevos conocimientos fácticos (ese es el trabajo de RAG); les enseña nuevos comportamientos, estilos y patrones. Utilice fine-tuning cuando necesite: un formato de salida específico que los ejemplos few-shot no pueden lograr de manera consistente, terminología específica del dominio que el modelo usa incorrectamente de manera constante, inferencia de alto rendimiento donde necesita un modelo más pequeño y rápido, o un tono y voz de marca consistentes en millones de salidas.

LoRA (Low-Rank Adaptation) se ha convertido en el método de fine-tuning dominante porque es eficiente en parámetros: en lugar de actualizar todos los pesos del modelo (lo que requiere un cómputo masivo), LoRA agrega pequeñas matrices entrenables a cada capa de atención. Puede ajustar un modelo de 7B en una sola GPU A100 en unas pocas horas y luego fusionar los pesos de LoRA de regreso en el modelo base para una inferencia con sobrecarga cero.

Ejemplo de código 4: Ajuste fino de LoRA con HuggingFace PEFT

from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    Trainer,
    DataCollatorForSeq2Seq
)
from peft import (
    get_peft_model,
    LoraConfig,
    TaskType,
    prepare_model_for_kbit_training
)
from datasets import Dataset
import torch
import json

# Configuración de LoRA: ajuste el rango (r) y el alfa para su tarea
# Un r mayor = más capacidad pero más cómputo. Comience con r=16.
LORA_CONFIG = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,               # Rango de matrices de actualización
    lora_alpha=32,      # Factor de escala (alpha/r = escala de tasa de aprendizaje efectiva)
    lora_dropout=0.1,   # Dropout para regularización
    bias="none",
    target_modules=["q_proj", "v_proj"]  # Capas de atención a adaptar
)

def prepare_training_data(examples: list[dict]) -> Dataset:
    """
    Formatear los datos de entrenamiento como pares de seguimiento de instrucciones.
    
    examples: [{"instruction": str, "input": str, "output": str}]
    """
    formatted = []
    for ex in examples:
        prompt = f"""### Instruction:
{ex['instruction']}

### Input:
{ex['input']}

### Response:
{ex['output']}"""
        formatted.append({"text": prompt})
    
    return Dataset.from_list(formatted)

def finetune_model(
    base_model_name: str = "meta-llama/Meta-Llama-3.1-8B",
    training_examples: list[dict] = None,
    output_dir: str = "./finetuned_model",
    num_epochs: int = 3
):
    """
    Ajustar un LM causal con LoRA para el seguimiento de instrucciones.
    Requiere aproximadamente 24 GB de VRAM para Llama 3.1 8B, o use cuantificación para menos.
    """
    print(f"Cargando modelo base: {base_model_name}")
    
    # Cargar tokenizador
    tokenizer = AutoTokenizer.from_pretrained(base_model_name)
    tokenizer.pad_token = tokenizer.eos_token
    tokenizer.padding_side = "right"
    
    # Cargar el modelo con cuantificación de 4 bits para eficiencia de memoria
    model = AutoModelForCausalLM.from_pretrained(
        base_model_name,
        load_in_4bit=True,  # Usar cuantificación de 4 bits de bitsandbytes
        device_map="auto",
        torch_dtype=torch.float16
    )
    
    # Preparar el modelo para el entrenamiento de k-bits
    model = prepare_model_for_kbit_training(model)
    
    # Aplicar adaptadores LoRA
    model = get_peft_model(model, LORA_CONFIG)
    model.print_trainable_parameters()  
    # Salida: parámetros entrenables: 4,194,304 (0.05% del total): ¡muy eficiente!
    
    # Preparar conjunto de datos
    train_dataset = prepare_training_data(training_examples)
    
    def tokenize_function(examples):
        return tokenizer(
            examples["text"],
            truncation=True,
            max_length=2048,
            padding="max_length"
        )
    
    tokenized_dataset = train_dataset.map(tokenize_function, batched=True)
    
    # Configuración de entrenamiento
    training_args = TrainingArguments(
        output_dir=output_dir,
        num_train_epochs=num_epochs,
        per_device_train_batch_size=4,
        gradient_accumulation_steps=4,  # Tamaño de lote efectivo: 16
        learning_rate=2e-4,
        fp16=True,
        logging_steps=10,
        save_strategy="epoch",
        warmup_ratio=0.05,
        lr_scheduler_type="cosine",
        report_to="none"  # Establecer en "wandb" para el seguimiento de experimentos
    )
    
    # Entrenar
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=tokenized_dataset,
        data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True)
    )
    
    print("Iniciando fine-tuning...")
    trainer.train()
    
    # Guardar pesos de LoRA (¡pequeños! típicamente 50-200MB frente a un modelo base de 16GB)
    model.save_pretrained(f"{output_dir}/lora_weights")
    tokenizer.save_pretrained(f"{output_dir}/lora_weights")
    print(f"Pesos de LoRA guardados en {output_dir}/lora_weights")
    
    return model, tokenizer

# Datos de entrenamiento de ejemplo: enseñe al modelo el estilo de respuesta de soporte de su empresa
training_examples = [
    {
        "instruction": "Escribir una respuesta de soporte al cliente",
        "input": "Queja del usuario: Mi pedido #12345 llegó dañado. La caja estaba aplastada y el artículo de adentro está roto.",
        "output": """Estimado cliente,

Gracias por contactarnos acerca del pedido #12345. Nos disculpamos sinceramente por la entrega dañada; esto está muy por debajo de nuestros estándares y entendemos completamente su frustración.

Esto es lo que haremos para solucionar esto:
1. Enviaremos un reemplazo sin cargo, con envío en 1 día hábil
2. Puede quedarse con el artículo dañado, no es necesario devolverlo
3. Hemos agregado un crédito de $15 a su cuenta por las molestias

¿Podría responder con una foto del artículo dañado para nuestro equipo de calidad? Esto nos ayuda a prevenir futuros incidentes.

El número de seguimiento de su reemplazo se enviará por correo electrónico en un plazo de 2 horas.

Atentamente,
Equipo de soporte"""
    }
    # Agregue de 500 a 2000 ejemplos más para obtener mejores resultados
]

# Para ejecutar esto, necesita una instancia de GPU:
# model, tokenizer = finetune_model(training_examples=training_examples)
print("Configuración de ajuste fino de LoRA completada. Ejecútelo en una instancia de GPU con más de 24 GB de VRAM.")

Matriz de comparación: Tres enfoques lado a lado

Dimensión Prompt Engineering RAG Fine-tuning
Costo de implementación ~$0 $5K–50K ing $10K–200K
Velocidad de actualización Instantáneo Minutos (reindexar) Horas–días (reentrenar)
Costo de inferencia Medio (prompts grandes) Medio + BD vectorial Bajo (modelo pequeño)
Actualidad del conocimiento Solo fecha de corte del modelo Tiempo real a través del índice Fecha de corte de entrenamiento
Consistencia del comportamiento Medio Medio Alto
Privacidad de los datos Datos de entrenamiento seguros Docs en llamadas a la API Datos de entrenamiento en el proveedor
Ideal para Formato, razonamiento, guía de tareas Recuperación de conocimiento, documentos grandes Estilo, comportamiento del dominio, velocidad

Enfoques híbridos: Combinando los tres

Los sistemas en producción con el mejor rendimiento en 2026 no eligen un solo enfoque: superponen los tres. La arquitectura es consistente: un modelo base ajustado (fine-tuned) que comprende el vocabulario de su dominio y produce formatos de salida consistentes, aumentado con RAG para la recuperación dinámica de conocimiento, guiado por prompts cuidadosamente diseñados. Cada capa maneja lo que mejor sabe hacer.

Arquitectura híbrida: Bot de soporte para compañía de seguros

PE

Capa de Prompt Engineering

El prompt del sistema define la personalidad del experto en seguros, el formato de respuesta (siempre citar el número de póliza) y los criterios de escalación (reclamaciones de más de $10,000, denegaciones en disputa)

↓ inyecta en el contexto
RAG

Capa de RAG

Recupera documentos de pólizas relevantes, regulaciones específicas del estado, precedentes recientes de reclamaciones. Actualizado diariamente. Más de 50,000 documentos indexados en Pinecone.

↓ alimenta a
FT

Modelo base con Fine-tuning

Llama 3.1 8B ajustado (fine-tuned) en 5,000 conversaciones de soporte de seguros. Entiende "deducible", "subrogación", "copago" correctamente y sin confusión. Formato de cita consistente.

94%

Precisión en P&R del dominio

$0.0004

Costo por consulta

380ms

Tiempo prom. de respuesta

Árbol de decisión visual

Utilice este diagrama de flujo para determinar su enfoque inicial. Siempre puede superponer técnicas adicionales una vez que la línea base esté funcionando.

¿El modelo falla incluso cuando se le da la información correcta en el prompt?
¿Es una falla de razonamiento / lógica?
Use prompts de Chain-of-Thought
No
Ajuste fino (fine-tune) en ejemplos del dominio
No (brecha de conoc.)
¿Los datos cambian con frecuencia?
RAG con índice en vivo
No
¿Alto volumen (>10K/día)?
Sí → Ajustar modelo pequeño
No → RAG o contexto largo

Benchmarks de rendimiento: Comparaciones de tareas reales

Tarea Prompt Eng. RAG Fine-tuning RAG + FT
P&R de soporte al cliente 72% 89% 81% 94%
Generación de código (estilo específico) 68% 71% 91% 93%
Resumen de documentos 84% 82% 87% 91%
Clasificación (específica del dominio) 74% 78% 95% 96%
Razonamiento de múltiples pasos 88%* 84% 79% 86%

*Prompts de CoT con GPT-4o. Precisión medida contra la verdad fundamental de expertos humanos. Pruebas internas de AgDex, abril de 2026. Los resultados varían según el dominio.

Preguntas frecuentes

¿Puede RAG reemplazar por completo al fine-tuning?

Para la recuperación de conocimiento fáctico, sí: RAG suele ser mejor que el fine-tuning porque mantiene el conocimiento actualizado y es auditable (puede ver qué documentos se recuperaron). Pero RAG no puede reemplazar al fine-tuning para cambios de comportamiento: si necesita que el modelo responda siempre en un formato específico, use terminología específica o mantenga un tono particular en millones de entradas variadas, el fine-tuning es la única solución confiable. RAG le da conocimiento al modelo; el fine-tuning cambia cómo se comporta el modelo.

¿Cuánta información de entrenamiento necesito para el fine-tuning?

Para GPT-4o-mini a través de la API de ajuste fino de OpenAI: 100 ejemplos es el mínimo funcional, 500-1,000 ejemplos es el punto ideal para la mayoría de las tareas y 5,000+ es para tareas complejas que requieren una especialización profunda. Para el ajuste fino de LoRA de modelos de código abierto: se aplican números similares, pero la calidad importa más que la cantidad. 200 ejemplos diversos y de alta calidad superan a 2,000 mediocres. Evite ejemplos duplicados o casi duplicados: la diversidad de datos es el principal impulsor de la generalización. Utilice GPT-4o para ayudar a generar ejemplos de entrenamiento a partir de sus datos reales si necesita escalar rápidamente.

¿Qué base de datos vectorial debería usar para RAG?

Para comenzar rápidamente: Pinecone (administrado, sin infraestructura) o Chroma (local, de código abierto). Para producción a escala: Weaviate, Qdrant o pgvector (si ya está en PostgreSQL). Para empresas con infraestructura existente: Azure AI Search, Google Vertex AI Vector Search o Amazon OpenSearch Serverless. Las diferencias de rendimiento entre las bases de datos vectoriales son pequeñas en comparación con el impacto de su estrategia de fragmentación y la elección del modelo de embedding. Comience con lo que sea más fácil de implementar y optimice más adelante.

¿Vale la pena el fine-tuning en GPT-4o-mini frente a usar solo GPT-4o con prompts?

Sí, para el caso de uso correcto. Un GPT-4o-mini ajustado suele superar a un GPT-4o guiado por prompts (entre un 5 y un 15% de precisión) en tareas específicas del dominio, mientras que cuesta entre 10 y 20 veces menos por llamada. El análisis de punto de equilibrio: costo del ajuste fino ($500–$2,000 para un buen conjunto de datos) ÷ ahorro de costos por llamada ($0.002–$0.003) = punto de equilibrio en 200,000–500,000 llamadas. A los volúmenes típicos de SaaS de más de 1,000 llamadas al día, esto se amortiza en semanas. La advertencia: los modelos ajustados necesitan un reentrenamiento cuando cambian sus requisitos, lo que agrega un costo de mantenimiento continuo.

¿Puedo usar RAG con un modelo ajustado (fine-tuned)?

Sí, y este híbrido suele ser la mejor arquitectura de producción. Ajuste el modelo para que comprenda el vocabulario de su dominio y produzca formatos de salida consistentes, luego agregue RAG para la recuperación de conocimiento en el momento de la inferencia. El modelo ajustado es mejor para interpretar correctamente los documentos recuperados (comprende el contexto del dominio) y la capa RAG mantiene el conocimiento actualizado sin requerir reentrenamiento. La principal complejidad es que debe mantener tanto el modelo ajustado como el índice vectorial, pero las ganancias de rendimiento generalmente justifican la sobrecarga operativa para aplicaciones de alto volumen.

🔧 Herramientas relacionadas

LLM-Strategie 25. April 2026 17 Min. Lesedauer

Fine-Tuning vs. RAG vs. Prompt Engineering im Jahr 2026: Wann man welches verwendet

Drei Ansätze, drei grundlegend verschiedene Kompromisse. Die falsche Wahl kostet Sie Monate an Entwicklungsarbeit oder ein Modell, das Ihren Anwendungsfall nicht zuverlässig bewältigen kann. Dieser Leitfaden bietet Ihnen den Rahmen, den Code und die Benchmarks, um die richtige Entscheidung zu treffen – einschließlich der Frage, wann man alle drei kombiniert.

Von Alex Chen · Leitender Redakteur, AgDex · April 2026 · Zuletzt aktualisiert: 28. April 2026

Das Kernproblem der Entscheidung

Wenn Ihre LLM-Anwendung nicht gut genug funktioniert, stehen Sie vor einer Diagnosefrage, bevor Sie eine Lösung vorschreiben können: Fehlt es dem Modell an Wissen, an Fähigkeiten oder an Anleitung? Die Antwort bestimmt, welchen Ansatz Sie benötigen.

Fehlerursache Diagnosetest Richtiger Ansatz
Modell kennt Ihre Daten nicht Fügen Sie die relevanten Dokumente in den Prompt ein – antwortet es richtig? RAG
Modell liefert inkonsistentes Format/Stil Fügen Sie 5 Few-Shot-Beispiele hinzu – verbessert sich die Konsistenz? Fine-tuning
Modell folgt den Aufgabenanweisungen nicht Schreiben Sie den System-Prompt mit klaren Einschränkungen um – verbessert es sich? Prompt Engineering
Dem Modell fehlt Fachwissen Selbst mit Dokumenten interpretiert es Bereichskonzepte falsch? Fine-tuning
Mehrere Probleme kombiniert Hartnäckige Wissenslücken + Formatierungsprobleme? RAG + Fine-Tuning Hybrid

Die drei Ansätze arbeiten auf unterschiedlichen Ebenen des Stacks. Prompt Engineering leitet das Denken des Modells, ohne es zu verändern. RAG gibt dem Modell zur Inferenzzeit Zugriff auf externes Wissen. Fine-Tuning modifiziert permanent die Gewichte des Modells, um Wissen oder Verhalten zu codieren. Stellen Sie sich das so vor: einem Mitarbeiter Anweisungen geben (Prompt Engineering), ihm Zugriff auf eine Bibliothek gewähren (RAG) oder ihn zu einer Schulung schicken (Fine-Tuning).

Prompt Engineering: Der obligatorische Ausgangspunkt

Prompt Engineering sollte immer Ihr erster Versuch sein. Es ist kostenlos (keine Trainingskosten), sofort umkehrbar und oft überraschend effektiv. Der Fehler, den die meisten Teams machen, besteht darin, das Prompt Engineering zu früh aufzugeben, nachdem ein einfacher System-Prompt fehlgeschlagen ist. Anspruchsvolle Prompt-Techniken wie Chain-of-Thought und Few-Shot-Learning übertreffen naive Prompts bei komplexen Aufgaben dramatisch.

Bevor Sie in RAG oder Fine-Tuning investieren, arbeiten Sie systematisch Folgendes ab: (1) klare Rollen- und Aufgabendefinition im System-Prompt, (2) explizite Ausgabeformatbeschränkungen, (3) Chain-of-Thought für Logikaufgaben, (4) Few-Shot-Beispiele für Format-/Stilkonsistenz, (5) Selbstkritikschleifen für präzisionskritische Aufgaben.

Code-Beispiel 1: Chain-of-Thought-Prompting

from openai import OpenAI
client = OpenAI()

def chain_of_thought_completion(
    task: str,
    question: str,
    model: str = "gpt-4o"
) -> dict:
    """
    Implementiert Chain-of-Thought-Prompting (CoT).
    Zwingt das Modell, vor der Antwort Schritt für Schritt zu denken.
    Verbessert die Genauigkeit bei mehrstufigen Logikaufgaben um 15–30 %.
    """
    
    cot_system_prompt = f"""Sie sind ein Experte für die folgende Aufgabe:
{task}

WICHTIG: Bei jeder Frage MÜSSEN Sie:
1. Die Frage sorgfältig analysieren
2. Die relevanten Überlegungen Schritt für Schritt durchdenken
3. Erst DANN Ihre endgültige Antwort geben

Formatieren Sie Ihre Antwort genau wie folgt:
BEGRÜNDUNG:
[Ihr Schritt-für-Schritt-Denken hier]

ANTWORT:
[Ihre prägnante endgültige Antwort hier]

VERTRAUEN: [HOCH / MITTEL / NIEDRIG]
GRUND FÜR VERTRAUEN: [Kurze Erklärung]"""
    
    response = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": cot_system_prompt},
            {"role": "user", "content": question}
        ],
        temperature=0.2  # Niedrige Temperatur für konsistentes Denken
    )
    
    full_response = response.choices[0].message.content
    
    # Strukturierte Antwort parsen
    parts = {}
    for section in ["BEGRÜNDUNG", "ANTWORT", "VERTRAUEN", "GRUND FÜR VERTRAUEN"]:
        if f"{section}:" in full_response:
            start = full_response.index(f"{section}:") + len(f"{section}:")
            # Nächsten Abschnitt oder Ende finden
            next_sections = [f"{s}:" for s in ["BEGRÜNDUNG", "ANTWORT", "VERTRAUEN", 
                                                 "GRUND FÜR VERTRAUEN"] 
                            if s != section and f"{s}:" in full_response[start:]]
            if next_sections:
                end = full_response.index(next_sections[0], start)
                parts[section] = full_response[start:end].strip()
            else:
                parts[section] = full_response[start:].strip()
    
    return parts

# Beispiel: Komplexe Geschäftsanalyse, bei der CoT glänzt
task = "Analyse des Kundenabwanderungsrisikos (Churn) für ein B2B-SaaS-Produkt"
question = """
Kundendaten:
- Konto: TechCorp Inc. (Enterprise-Stufe, 8.500 $/Monat)
- Nutzung letzte 30 Tage: Login-Häufigkeit um 60 % gesunken im Vergleich zum vorherigen 3-Monats-Durchschnitt
- Support-Tickets: 3 offene Tickets (2 Abrechnung, 1 Feature-Anfrage), das älteste ist 14 Tage alt
- Vertragsverlängerung: In 45 Tagen
- Hauptansprechpartner: Emily Rodriguez hat das Unternehmen vor 3 Wochen verlassen
- Neuer Ansprechpartner: Bisher kein Ersatz identifiziert

Wie hoch ist das Abwanderungsrisiko und was sollte das CS-Team in den nächsten 7 Tagen tun?
"""

result = chain_of_thought_completion(task, question)
print("SCHRITT-FÜR-SCHRITT-DENKEN:")
print(result.get("BEGRÜNDUNG", "N/A"))
print("\nENDGÜLTIGE ANTWORT:")
print(result.get("ANTWORT", "N/A"))
print(f"\nVertrauen: {result.get('VERTRAUEN', 'N/A')}")

Code-Beispiel 2: Few-Shot-Learning für konsistentes Ausgabeformat

from openai import OpenAI
client = OpenAI()

# Few-Shot-Beispiele lehren das Modell Ihr exaktes Ausgabeformat
# Dies ist weitaus effektiver, als das Format in Worten zu beschreiben
FEW_SHOT_EXAMPLES = [
    {
        "input": "Fassen Sie dieses Support-Ticket zusammen: Benutzer kann sich nicht anmelden, erhält die Fehlermeldung 'Ungültige Anmeldedaten', hat das Passwort zweimal zurückgesetzt, schlägt immer noch fehl",
        "output": """{
  "ticket_summary": "Login failure despite password reset",
  "issue_type": "authentication",
  "urgency": "high",
  "user_actions_taken": ["password_reset x2"],
  "suggested_next_step": "Check if account is locked in AD; review auth logs for IP blocks",
  "estimated_resolution_time": "15 minutes",
  "escalate_to_tier2": false
}"""
    },
    {
        "input": "Fassen Sie dieses Support-Ticket zusammen: Zugriffsanfrage für die GitHub-Organisation für neue Mitarbeiter, der Manager ist John Smith",
        "output": """{
  "ticket_summary": "GitHub organization access request for new hire",
  "issue_type": "access_provisioning",
  "urgency": "medium",
  "user_actions_taken": [],
  "suggested_next_step": "Verify employment start date with HR; provision via Okta workflow",
  "estimated_resolution_time": "30 minutes",
  "escalate_to_tier2": false
}"""
    }
]

def few_shot_ticket_classifier(ticket_text: str) -> dict:
    """
    Klassifizieren und Zusammenfassen von Support-Tickets mithilfe von Few-Shot-Beispielen.
    Ohne Few-Shot-Beispiele erzeugt GPT-4o inkonsistente JSON-Strukturen.
    Mit 2 Beispielen steigt die Formatkonformität von ~60 % auf ~97 %.
    """
    
    messages = [
        {
            "role": "system",
            "content": """Sie sind ein IT-Support-Ticket-Klassifizierer.
            Sie MÜSSEN mit validem JSON antworten, das genau der Struktur in den Beispielen entspricht.
            Fügen Sie keine Felder hinzu, die nicht in den Beispielen gezeigt werden."""
        }
    ]
    
    # Few-Shot-Beispiele hinzufügen
    for example in FEW_SHOT_EXAMPLES:
        messages.append({"role": "user", "content": example["input"]})
        messages.append({"role": "assistant", "content": example["output"]})
    
    # Tatsächliche Abfrage hinzufügen
    messages.append({"role": "user", "content": f"Fassen Sie dieses Support-Ticket zusammen: {ticket_text}"})
    
    import json
    response = client.chat.completions.create(
        model="gpt-4o-mini",  # Mini ist ausreichend mit guten Few-Shot-Beispielen
        messages=messages,
        temperature=0.1,
        response_format={"type": "json_object"}
    )
    
    return json.loads(response.choices[0].message.content)

# Test mit neuen Tickets
test_tickets = [
    "VPN trennt sich nach 10 Minuten ständig, Verwendung von Cisco AnyConnect auf dem MacBook Pro M3",
    "Müssen 3 neue Teammitglieder zum Slack-Workspace hinzufügen, E-Mail-Liste wird separat gesendet",
]

for ticket in test_tickets:
    result = few_shot_ticket_classifier(ticket)
    print(f"Ticket: {ticket[:60]}...")
    print(f"  Typ: {result.get('issue_type')} | Dringlichkeit: {result.get('urgency')}")
    print(f"  Nächster Schritt: {result.get('suggested_next_step')[:80]}")
    print()

RAG: Wenn sich Ihre Daten ändern oder privat sind

RAG ist die richtige Wahl, wenn das Problem im Wissen und nicht in der Fähigkeit liegt. Wenn das Modell richtig antworten würde, wenn es Zugriff auf Ihre Dokumente hätte, verwenden Sie RAG. Wenn das Modell immer noch Probleme hat, selbst wenn Sie den relevanten Inhalt in den Prompt einfügen, handelt es sich um ein Fähigkeits- oder Verhaltensproblem – RAG wird es nicht beheben.

RAG eignet sich hervorragend für: Wissensdatenbanken von Unternehmen (HR-Richtlinien, Produktdokumentation, interne Wikis), Echtzeitdaten (Lagerbestände, sich ändernde Preise), proprietäre Forschung und alle Inhalte, die für den Kontext unerschwinglich groß wären. Ein Unternehmen mit 10.000 Richtliniendokumenten kann diese nicht alle in jeden Prompt aufnehmen – RAG ruft die relevanten 3–5 Dokumente dynamisch ab.

Code-Beispiel 3: Vollständige RAG-Pipeline mit Pinecone

from openai import OpenAI
from pinecone import Pinecone, ServerlessSpec
import hashlib
from typing import Optional

client = OpenAI()
pc = Pinecone(api_key="tu-dile-api-de-pinecone")

# Vektorindex initialisieren (einmal ausführen)
INDEX_NAME = "company-knowledge-base"
EMBEDDING_MODEL = "text-embedding-3-small"
EMBEDDING_DIM = 1536

def initialize_index():
    """Pinecone-Index erstellen, falls nicht vorhanden."""
    if INDEX_NAME not in [idx.name for idx in pc.list_indexes()]:
        pc.create_index(
            name=INDEX_NAME,
            dimension=EMBEDDING_DIM,
            metric="cosine",
            spec=ServerlessSpec(cloud="aws", region="us-east-1")
        )
    return pc.Index(INDEX_NAME)

def chunk_document(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
    """Dokument in überlappende Abschnitte (Chunks) für Embeddings aufteilen."""
    words = text.split()
    chunks = []
    for i in range(0, len(words), chunk_size - overlap):
        chunk = " ".join(words[i:i + chunk_size])
        if chunk:
            chunks.append(chunk)
    return chunks

def embed_text(text: str) -> list[float]:
    """Embedding-Vektor für eine Textzeichenkette abrufen."""
    response = client.embeddings.create(
        model=EMBEDDING_MODEL,
        input=text.strip()
    )
    return response.data[0].embedding

def index_documents(documents: list[dict], index) -> int:
    """
    Liste von Dokumenten in Pinecone indexieren.
    
    documents format: [{"title": str, "content": str, "source": str}]
    Returns: Anzahl der indexierten Abschnitte
    """
    vectors = []
    
    for doc in documents:
        chunks = chunk_document(doc["content"])
        
        for i, chunk in enumerate(chunks):
            chunk_id = hashlib.md5(f"{doc['title']}_{i}".encode()).hexdigest()
            embedding = embed_text(chunk)
            
            vectors.append({
                "id": chunk_id,
                "values": embedding,
                "metadata": {
                    "title": doc["title"],
                    "source": doc["source"],
                    "chunk_index": i,
                    "text": chunk  # Text in Metadaten speichern für den Abruf
                }
            })
    
    # Batch-Upsert (max. 100 Vektoren pro Anfrage)
    batch_size = 100
    for i in range(0, len(vectors), batch_size):
        index.upsert(vectors=vectors[i:i + batch_size])
    
    return len(vectors)

def retrieve_context(
    query: str,
    index,
    top_k: int = 5,
    min_score: float = 0.7
) -> list[dict]:
    """
    Die relevantesten Dokumentenabschnitte für eine Anfrage abrufen.
    """
    query_embedding = embed_text(query)
    
    results = index.query(
        vector=query_embedding,
        top_k=top_k,
        include_metadata=True
    )
    
    # Nach minimalem Relevanz-Score filtern
    relevant_chunks = [
        {
            "text": match.metadata["text"],
            "title": match.metadata["title"],
            "source": match.metadata["source"],
            "score": match.score
        }
        for match in results.matches
        if match.score >= min_score
    ]
    
    return relevant_chunks

def rag_completion(
    question: str,
    index,
    model: str = "gpt-4o",
    top_k: int = 5
) -> dict:
    """
    Eine Frage mithilfe der RAG-Pipeline beantworten:
    1. Generar embedding de consulta
    2. Recuperar fragmentos relevantes
    3. Generar respuesta basada en el contexto recuperado
    """
    # Schritt 1: Relevanten Kontext abrufen
    context_chunks = retrieve_context(question, index, top_k=top_k)
    
    if not context_chunks:
        return {
            "answer": "Ich konnte in der Wissensdatenbank keine relevanten Informationen finden, um diese Frage zu beantworten.",
            "sources": [],
            "context_used": False
        }
    
    # Schritt 2: Kontextzeichenkette erstellen
    context = "\n\n---\n\n".join([
        f"Quelle: {chunk['title']}\n{chunk['text']}"
        for chunk in context_chunks
    ])
    
    # Schritt 3: Antwort generieren
    response = client.chat.completions.create(
        model=model,
        messages=[
            {
                "role": "system",
                "content": """Sie sind ein hilfreicher Assistent. Beantworten Sie Fragen NUR unter Verwendung des bereitgestellten Kontexts. Wenn der Kontext die Antwort nicht enthält, sagen Sie dies deutlich.
                Zitieren Sie immer, aus welchen Dokumenten Ihre Antwort stammt."""
            },
            {
                "role": "user",
                "content": f"""Kontext:
{context}

Frage: {question}

Antworten Sie nur basierend auf dem obigen Kontext:"""
            }
        ],
        temperature=0.2
    )
    
    return {
        "answer": response.choices[0].message.content,
        "sources": [{"title": c["title"], "score": c["score"]} for c in context_chunks],
        "context_used": True,
        "chunks_retrieved": len(context_chunks)
    }

# Beispiel für die Verwendung
index = initialize_index()

# Ihre Dokumente indexieren (einmal ausführen, dann bei Dokumentenaktualisierungen)
sample_docs = [
    {
        "title": "Mitarbeiter-PTO-Richtlinie 2026",
        "content": """Mitarbeiter erhalten 15 Tage PTO (bezahlten Urlaub) pro Jahr. PTO wird mit 1,25 Tagen pro Monat
        ab dem ersten Beschäftigungstag angespart. Mitarbeiter können bis zu 5 Tage nicht genutzten PTO in das folgende Kalenderjahr
        übertragen. PTO-Anträge müssen mindestens 2 Wochen im Voraus für Abwesenheiten von 3+ Tagen eingereicht werden...""",
        "source": "hr-policies/pto-2026.pdf"
    }
]

chunks_indexed = index_documents(sample_docs, index)
print(f"Indexiert {chunks_indexed} Chunks")

# Wissensdatenbank abfragen
result = rag_completion("Wie viele PTO-Tage erhalten neue Mitarbeiter?", index)
print(f"\nAntwort: {result['answer']}")
print(f"Quellen: {[s['title'] for s in result['sources']]}")

"In unseren Tests ist der häufigste RAG-Fehler nicht die Abrufqualität, sondern die Chunking-Strategie. Teams, die Chunks mit 2.000 Token erstellen, verlieren den Kontext über Satzgrenzen hinweg. Dies führt dazu, dass abgerufene Abschnitte zwar relevant aussehen, aber die entscheidende Einschränkung im folgenden Satz verpassen. Wir haben festgestellt, dass 500-Token-Chunks mit 50-Token-Überlappung bei den meisten Dokumenttypen am besten funktionieren. Bei Tabellen und Listen sollten Sie Chunks auf Elementebene und nicht nach Token-Anzahl erstellen."

— Alex Chen, AgDex Engineering

Fine-Tuning: Wenn Sie konsistentes Verhalten in großem Maßstab benötigen

Fine-Tuning wird zu oft als erster Ausweg und zu selten als Optimierungswerkzeug genutzt. Es vermittelt Modellen kein neues Faktenwissen (das ist die Aufgabe von RAG) – es lehrt Modelle neue Verhaltensweisen, Stile und Muster. Nutzen Sie Fine-Tuning, wenn Sie Folgendes benötigen: ein bestimmtes Ausgabeformat, das Few-Shot-Beispiele nicht konsistent erreichen können, domänenspezifische Terminologie, die das Modell ständig falsch verwendet, Inferenz mit hohem Durchsatz, bei der Sie ein kleineres, schnelleres Modell benötigen, oder einen konsistenten Ton und eine einheitliche Markenstimme über Millionen von Ausgaben hinweg.

LoRA (Low-Rank Adaptation) hat sich als dominierende Fine-Tuning-Methode etabliert, da sie parametereffizient ist: Anstatt alle Modellgewichte zu aktualisieren (was enorme Rechenleistung erfordert), fügt LoRA jeder Attention-Schicht kleine trainierbare Matrizen hinzu. Sie können ein 7B-Modell auf einer einzigen A100-GPU in wenigen Stunden feintunen und die LoRA-Gewichte dann ohne zusätzlichen Inferenz-Overhead wieder mit dem Basismodell verschmelzen.

Code-Beispiel 4: LoRA-Fine-Tuning mit HuggingFace PEFT

from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    Trainer,
    DataCollatorForSeq2Seq
)
from peft import (
    get_peft_model,
    LoraConfig,
    TaskType,
    prepare_model_for_kbit_training
)
from datasets import Dataset
import torch
import json

# LoRA-Konfiguration – Passen Sie Rang (r) und Alpha für Ihre Aufgabe an
# Höheres r = mehr Kapazität, aber mehr Rechenleistung. Beginnen Sie mit r=16.
LORA_CONFIG = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,               # Rang der Update-Matrizen
    lora_alpha=32,      # Skalierungsfaktor (alpha/r = effektive Lernratenskalierung)
    lora_dropout=0.1,   # Dropout zur Regularisierung
    bias="none",
    target_modules=["q_proj", "v_proj"]  # Attention-Schichten, die angepasst werden sollen
)

def prepare_training_data(examples: list[dict]) -> Dataset:
    """
    Trainingsdaten als Instruktionsfolgemuster-Paare formatieren.
    
    examples: [{"instruction": str, "input": str, "output": str}]
    """
    formatted = []
    for ex in examples:
        prompt = f"""### Instruction:
{ex['instruction']}

### Input:
{ex['input']}

### Response:
{ex['output']}"""
        formatted.append({"text": prompt})
    
    return Dataset.from_list(formatted)

def finetune_model(
    base_model_name: str = "meta-llama/Meta-Llama-3.1-8B",
    training_examples: list[dict] = None,
    output_dir: str = "./finetuned_model",
    num_epochs: int = 3
):
    """
    Ein kausales LM mit LoRA für die Befolgung von Anweisungen feintunen.
    Erfordert ca. 24 GB VRAM für Llama 3.1 8B, oder verwenden Sie Quantisierung für weniger.
    """
    print(f"Cargando modelo base: {base_model_name}")
    
    # Tokenizador laden
    tokenizer = AutoTokenizer.from_pretrained(base_model_name)
    tokenizer.pad_token = tokenizer.eos_token
    tokenizer.padding_side = "right"
    
    # Modell mit 4-Bit-Quantisierung laden für Speichereffizienz
    model = AutoModelForCausalLM.from_pretrained(
        base_model_name,
        load_in_4bit=True,  # Verwenden von bitsandbytes 4-Bit-Quantisierung
        device_map="auto",
        torch_dtype=torch.float16
    )
    
    # Modell für das k-Bit-Training vorbereiten
    model = prepare_model_for_kbit_training(model)
    
    # LoRA-Adapter anwenden
    model = get_peft_model(model, LORA_CONFIG)
    model.print_trainable_parameters()  
    # Trainierbare Parameter: 4.194.304 (0,05 % der Gesamtzahl) – sehr effizient!
    
    # Trainingsdatensatz vorbereiten
    train_dataset = prepare_training_data(training_examples)
    
    def tokenize_function(examples):
        return tokenizer(
            examples["text"],
            truncation=True,
            max_length=2048,
            padding="max_length"
        )
    
    tokenized_dataset = train_dataset.map(tokenize_function, batched=True)
    
    # Trainingskonfiguration
    training_args = TrainingArguments(
        output_dir=output_dir,
        num_train_epochs=num_epochs,
        per_device_train_batch_size=4,
        gradient_accumulation_steps=4,  # Effektive Batchgröße: 16
        learning_rate=2e-4,
        fp16=True,
        logging_steps=10,
        save_strategy="epoch",
        warmup_ratio=0.05,
        lr_scheduler_type="cosine",
        report_to="none"  # Auf "wandb" zur Testverfolgung festlegen
    )
    
    # Trainieren
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=tokenized_dataset,
        data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True)
    )
    
    print("Iniciando fine-tuning...")
    trainer.train()
    
    # LoRA-Gewichte speichern (klein! typischerweise 50–200 MB im Vergleich zum 16-GB-Basismodell)
    model.save_pretrained(f"{output_dir}/lora_weights")
    tokenizer.save_pretrained(f"{output_dir}/lora_weights")
    print(f"Pesos de LoRA guardados en {output_dir}/lora_weights")
    
    return model, tokenizer

# Beispiel-Trainingsdaten: Bringen Sie dem Modell den Stil für Support-Antworten Ihres Unternehmens bei
training_examples = [
    {
        "instruction": "Schreiben Sie eine Kundensupport-Antwort",
        "input": "Benutzerbeschwerde: Meine Bestellung #12345 kam beschädigt an. Der Karton war zerdrückt und der Artikel darin ist kaputt.",
        "output": """Sehr geehrte Kundin, sehr geehrter Kunde,

vielen Dank, dass Sie sich bezüglich der Bestellung #12345 an uns gewandt haben. Wir entschuldigen uns aufrichtig für die beschädigte Lieferung – dies entspricht keineswegs unseren Standards und wir verstehen Ihren Ärger vollkommen.

Folgendes werden wir tun, um dies zu korrigieren:
1. Wir senden Ihnen kostenlos Ersatz, der Versand erfolgt innerhalb von 1 Werktag
2. Sie können den beschädigten Artikel behalten – keine Rücksendung erforderlich
3. Wir haben Ihrem Konto eine Gutschrift von 15 $ für die Unannehmlichkeiten hinzugefügt

Könnten Sie bitte mit einem Foto des beschädigten Artikels für unser Qualitätsteam antworten? Dies hilft uns, zukünftige Vorfälle zu verhindern.

Die Sendungsverfolgungsnummer für Ihren Ersatz wird Ihnen innerhalb von 2 Stunden per E-Mail zugesandt.

Mit freundlichen Grüßen,
Ihr Support-Team"""
    }
    # Fügen Sie 500 bis 2000 weitere Beispiele für bessere Ergebnisse hinzu
]

# Um dies tatsächlich auszuführen, benötigen Sie eine GPU-Instanz:
# model, tokenizer = finetune_model(training_examples=training_examples)
print("LoRA-Fine-Tuning-Setup abgeschlossen. Führen Sie es auf einer GPU-Instanz mit mehr als 24 GB VRAM aus.")

Vergleichsmatrix: Drei Ansätze im Vergleich

Dimension Prompt Engineering RAG Fine-tuning
Implementierungskosten ~$0 $5K–50K Entw. $10K–200K
Aktualisierungsgeschwindigkeit Sofort Minuten (neu indexieren) Stunden-Tage (neu trainieren)
Inferenzkosten Mittel (große Prompts) Mittel + Vektor-DB Niedrig (kleines Modell)
Wissensaktualität Nur Modell-Cutoff Echtzeit über Index Training-Cutoff
Verhaltenskonsistenz Mittel Mittel Hoch
Datenschutz Trainingsdaten sicher Dokumente in API-Aufrufen Trainingsdaten beim Anbieter
Bestens geeignet für Format, Logik, Aufgabenführung Wissensabruf, große Dokumente Stil, Domänenverhalten, Geschwindigkeit

Hybride Ansätze: Alle drei kombinieren

Die Produktionssysteme mit der besten Leistung im Jahr 2026 wählen nicht einen einzigen Ansatz – sie schichten alle drei übereinander. Die Architektur ist einheitlich: Ein feingetuntes Basismodell, das Ihr Fachvokabular versteht und konsistente Ausgabeformate erzeugt, erweitert um RAG für den dynamischen Wissensabruf und gesteuert durch sorgfältig gestaltete Prompts. Jede Ebene übernimmt das, was sie am besten kann.

Hybride Architektur: Support-Bot für ein Versicherungsunternehmen

PE

Prompt-Engineering-Ebene

Der System-Prompt definiert die Persona eines Versicherungsexperten, das Antwortformat (immer die Policennummer zitieren) und Eskalationskriterien (Schäden über 10.000 $, strittige Ablehnungen)

↓ injiziert in Kontext
RAG

RAG-Ebene

Ruft relevante Policendokumente, bundesstaatliche Vorschriften und jüngste Schadenspräzedenzfälle ab. Täglich aktualisiert. Über 50.000 Dokumente in Pinecone indexiert.

↓ speist in
FT

Feingetuntes Basismodell

Llama 3.1 8B feingetunt mit 5.000 Versicherungssupport-Gesprächen. Versteht „Selbstbeteiligung“, „Forderungsübergang“, „Zuzahlung“ korrekt und ohne Verwirrung. Konsistentes Zitierformat.

94%

Genauigkeit bei Fach-F&A

$0.0004

Kosten pro Anfrage

380ms

Avg. Antwortzeit

Visueller Entscheidungsbaum

Verwenden Sie dieses Flussdiagramm, um Ihren Ausgangsansatz zu bestimmen. Sie können jederzeit weitere Techniken hinzufügen, sobald die Basis funktioniert.

Scheitert das Modell selbst dann, wenn es die richtigen Informationen im Prompt erhält?
Ja
Handelt es sich um einen Logik- / Denkfehler?
Ja
Chain-of-Thought-Prompting nutzen
Nein
Feintuning mit Domänenbeispielen
Nein (Wissenslücke)
Ändern sich die Daten häufig?
Ja
RAG mit Live-Index
Nein
Hohes Volumen (>10K/Tag)?
Ja → Kleines Modell feintunen
Nein → RAG oder langer Kontext

Leistungs-Benchmarks: Vergleiche realer Aufgaben

Aufgabe Prompt Eng. RAG Fine-tuning RAG + FT
Kundensupport F&A 72% 89% 81% 94%
Codegenerierung (spezifischer Stil) 68% 71% 91% 93%
Dokumentenzusammenfassung 84% 82% 87% 91%
Klassifizierung (domänenspezifisch) 74% 78% 95% 96%
Mehrstufiges Denken 88%* 84% 79% 86%

*CoT-Prompting mit GPT-4o. Genauigkeit gemessen an der Ground Truth menschlicher Experten. Interne AgDex-Tests, April 2026. Ergebnisse variieren je nach Domäne.

Häufig gestellte Fragen

Kann RAG das Fine-Tuning vollständig ersetzen?

Für den Abruf von Faktenwissen ja – RAG ist oft besser als Fine-Tuning, da es das Wissen aktuell hält und überprüfbar ist (Sie können sehen, welche Dokumente abgerufen wurden). RAG kann jedoch das Fine-Tuning für Verhaltensänderungen nicht ersetzen: Wenn das Modell immer in einem bestimmten Format antworten soll, eine bestimmte Terminologie verwenden oder einen bestimmten Ton bei Millionen unterschiedlicher Eingaben beibehalten soll, ist Fine-Tuning die einzige zuverlässige Lösung. RAG gibt dem Modell Wissen; Fine-Tuning ändert, wie sich das Modell verhält.

Wie viele Trainingsdaten benötige ich für das Fine-Tuning?

Für GPT-4o-mini über die Fine-Tuning-API von OpenAI: 100 Beispiele sind das funktionale Minimum, 500–1.000 Beispiele sind der ideale Bereich für die meisten Aufgaben, und 5.000+ sind für komplexe Aufgaben erforderlich, die eine tiefe Spezialisierung erfordern. Für das LoRA-Fine-Tuning von Open-Source-Modellen: Ähnliche Zahlen gelten, aber die Qualität ist wichtiger als die Quantität. 200 qualitativ hochwertige, vielfältige Beispiele übertreffen 2.000 mittelmäßige Beispiele. Vermeiden Sie doppelte oder fast doppelte Beispiele – Datenvielfalt ist der Haupttreiber für die Generalisierung. Verwenden Sie GPT-4o, um Trainingsbeispiele aus Ihren echten Daten zu generieren, wenn Sie schnell skalieren müssen.

Welche Vektordatenbank sollte ich für RAG verwenden?

Für einen schnellen Einstieg: Pinecone (verwaltet, keine Infrastruktur) oder Chroma (lokal, Open-Source). Für die Produktion im großen Maßstab: Weaviate, Qdrant oder pgvector (wenn Sie bereits auf PostgreSQL setzen). Für Unternehmen mit bestehender Infrastruktur: Azure AI Search, Google Vertex AI Vector Search oder Amazon OpenSearch Serverless. Die Leistungsunterschiede zwischen Vektordatenbanken sind im Vergleich zu den Auswirkungen Ihrer Chunking-Strategie und der Wahl des Embedding-Modells gering. Beginnen Sie mit dem, was am einfachsten zu implementieren ist, und optimieren Sie später.

Lohnt sich das Fine-Tuning von GPT-4o-mini im Vergleich zur einfachen Verwendung von GPT-4o mit Prompting?

Ja, für den richtigen Anwendungsfall. Ein feingetuntes GPT-4o-mini übertrifft in der Regel ein über Prompts gesteuertes GPT-4o bei domänenspezifischen Aufgaben (um 5–15 % Genauigkeit) und kostet 10–20-mal weniger pro Aufruf. Die Break-Even-Analyse: Kosten für Fine-Tuning ($500–$2.000 für einen guten Datensatz) ÷ Kosteneinsparungen pro Aufruf ($0.002–$0.003) = Break-Even bei 200.000–500.000 Aufrufen. Bei typischen SaaS-Volumina von 1.000+ Aufrufen/Tag amortisiert sich dies in wenigen Wochen. Der Haken: Feingetunte Modelle müssen neu trainiert werden, wenn sich Ihre Anforderungen ändern, was laufende Wartungskosten verursacht.

Kann ich RAG mit einem feingetunten Modell verwenden?

Ja, und dieser Hybrid ist oft die beste Produktionsarchitektur. Feintunen Sie das Modell, damit es Ihr Domänenvokabular versteht und konsistente Ausgabeformate erzeugt, und fügen Sie dann RAG für den Wissensabruf zur Inferenzzeit hinzu. Das feingetunte Modell kann die abgerufenen Dokumente besser interpretieren (es versteht den Kontext der Domäne) und die RAG-Ebene hält das Wissen auf dem neuesten Stand, ohne dass ein erneutes Training erforderlich ist. Die Hauptkomplexität besteht darin, dass Sie sowohl das feingetunte Modell als auch den Vektorindex pflegen müssen, aber die Leistungsgewinne rechtfertigen in der Regel den betrieblichen Aufwand für hochvolumige Anwendungen.

LLM戦略 2026年4月25日 17分で読了

2026年のファインチューニング vs RAG vs プロンプトエンジニアリング:それぞれの使い分け

3つのアプローチ、3つの根本的に異なるトレードオフ。間違った選択をすれば、数か月のエンジニアリング作業を無駄にするか、ユースケースを確実に処理できないモデルになります。本ガイドでは、3つすべてを組み合わせるタイミングを含め、正しい判断を下すためのフレームワーク、コード、ベンチマークを提供します。

著者:Alex Chen · シニアエディター、AgDex · 2026年4月 · 最終更新: 2026年4月28日

核心的な意思決定の問題

When your LLM application isn't performing well enough, you face a diagnosis question before you can prescribe a solution: Is the model lacking knowledge, lacking capability, or lacking guidance? The answer determines which approach you need.

失敗の根本原因 診断テスト 正しいアプローチ
モデルがデータを把握していない プロンプトに関連ドキュメントを貼り付けて、正確に回答しますか? RAG
モデルの出力フォーマット/スタイルが一貫しない Few-Shotの例を5つ追加して、一貫性は向上しますか? Fine-tuning
モデルがタスク指示に従わない 明確な制約でシステムプロンプトを書き直して、改善しますか? Prompt Engineering
モデルにドメイン専門知識が不足 ドキュメントがあっても、ドメインの概念を誤解しますか? Fine-tuning
複数の問題の複合 持続的な知識ギャップ+フォーマットの問題? RAG+ファインチューニングのハイブリッド

3つのアプローチはスタックの異なるレベルで機能します。 Prompt engineering モデルを変更せずに推論を導きます. RAG 推論時にモデルに外部ナレッジへのアクセスを提供します. Fine-tuning ナレッジや挙動をエンコードするためにモデルの重みを恒久的に変更します. これらを次のように考えてください:従業員に指示を与える(プロンプトエンジニアリング)、図書館へのアクセスを与える(RAG)、研修プログラムに参加させる(ファインチューニング)。

プロンプトエンジニアリング:必須の出発点

Prompt engineering should always be your first attempt. It's free (no training cost), instantly reversible, and often surprisingly powerful. The mistake most teams make is abandoning prompt engineering too early after a simple system prompt fails — sophisticated prompting techniques like Chain-of-Thought and Few-Shot learning dramatically outperform naive prompting on complex tasks.

Before investing in RAG or fine-tuning, systematically work through: (1) clear role and task definition in the system prompt, (2) explicit output format constraints, (3) Chain-of-Thought for reasoning tasks, (4) Few-Shot examples for format/style consistency, (5) self-critique loops for accuracy-critical tasks.

コード例1:Chain-of-Thoughtプロンプティング

from openai import OpenAI
client = OpenAI()

def chain_of_thought_completion(
    task: str,
    question: str,
    model: str = "gpt-4o"
) -> dict:
    """
    Chain-of-Thought(CoT)プロンプティングを実装します。
    回答前にモデルにステップバイステップで推論させます。
    マルチステップ推論タスクで精度を15〜30%向上させます。
    """
    
    cot_system_prompt = f"""あなたは以下のタスクの専門アシスタントです:
{task}

重要:すべての質問に対して、必ず以下を実行してください:
1. 質問を注意深く分析する
2. 関連する考慮事項をステップバイステップで検討する
3. その後初めて最終回答を提供する

回答は正確に以下の形式で:
REASONING:
[ここにステップバイステップの思考過程]

ANSWER:
[ここに簡潔な最終回答]

CONFIDENCE: [HIGH / MEDIUM / LOW]
REASON FOR CONFIDENCE: [簡潔な説明]"""
    
    response = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": cot_system_prompt},
            {"role": "user", "content": question}
        ],
        temperature=0.2  # Low temperature for consistent reasoning
    )
    
    full_response = response.choices[0].message.content
    
    # Parse structured response
    parts = {}
    for section in ["REASONING", "ANSWER", "CONFIDENCE", "REASON FOR CONFIDENCE"]:
        if f"{section}:" in full_response:
            start = full_response.index(f"{section}:") + len(f"{section}:")
            # Find next section or end
            next_sections = [f"{s}:" for s in ["REASONING", "ANSWER", "CONFIDENCE", 
                                                 "REASON FOR CONFIDENCE"] 
                            if s != section and f"{s}:" in full_response[start:]]
            if next_sections:
                end = full_response.index(next_sections[0], start)
                parts[section] = full_response[start:end].strip()
            else:
                parts[section] = full_response[start:].strip()
    
    return parts

# 例:CoTが威力を発揮する複雑なビジネス分析
task = "Analyzing customer churn risk for a B2B SaaS product"
question = """
Customer data:
- Account: TechCorp Inc. (Enterprise tier, $8,500/mo)
- Usage last 30 days: Login frequency down 60% vs previous 3-month average
- Support tickets: 3 open tickets (2 billing, 1 feature request), oldest is 14 days
- Contract renewal: In 45 days
- Champion contact: Emily Rodriguez left company 3 weeks ago
- New contact: No replacement identified yet

What is the churn risk level and what should the CS team do in the next 7 days?
"""

result = chain_of_thought_completion(task, question)
print("ステップバイステップの推論:")
print(result.get("REASONING", "N/A"))
print("\n最終回答:")
print(result.get("ANSWER", "N/A"))
print(f"\nConfidence: {result.get('CONFIDENCE', 'N/A')}")

コード例2:一貫した出力フォーマットのためのFew-Shot学習

from openai import OpenAI
client = OpenAI()

# Few-shotの例がモデルに正確な出力フォーマットを教えます
# これは言葉でフォーマットを説明するよりはるかに効果的です
FEW_SHOT_EXAMPLES = [
    {
        "input": "Summarize this support ticket: User can't login, getting 'Invalid credentials' error, tried resetting password twice, still failing",
        "output": """{
  "ticket_summary": "Login failure despite password reset",
  "issue_type": "authentication",
  "urgency": "high",
  "user_actions_taken": ["password_reset x2"],
  "suggested_next_step": "Check if account is locked in AD; review auth logs for IP blocks",
  "estimated_resolution_time": "15 minutes",
  "escalate_to_tier2": false
}"""
    },
    {
        "input": "Summarize this support ticket: Requesting access to GitHub org for new developer starting Monday, manager is John Smith",
        "output": """{
  "ticket_summary": "GitHub organization access request for new hire",
  "issue_type": "access_provisioning",
  "urgency": "medium",
  "user_actions_taken": [],
  "suggested_next_step": "Verify employment start date with HR; provision via Okta workflow",
  "estimated_resolution_time": "30 minutes",
  "escalate_to_tier2": false
}"""
    }
]

def few_shot_ticket_classifier(ticket_text: str) -> dict:
    """
    Few-shotの例を使用してサポートチケットを分類・要約します。
    Few-shotの例なしでは、GPT-4oは一貫性のないJSON構造を生成します。
    2つの例で、フォーマット準拠率は約60%から約97%に向上します。
    """
    
    messages = [
        {
            "role": "system",
            "content": """You are an IT support ticket classifier. 
            You MUST respond with valid JSON matching the exact structure in the examples.
            Do not add any fields not shown in the examples."""
        }
    ]
    
    # Few-shotの例を追加
    for example in FEW_SHOT_EXAMPLES:
        messages.append({"role": "user", "content": example["input"]})
        messages.append({"role": "assistant", "content": example["output"]})
    
    # 実際のクエリを追加
    messages.append({"role": "user", "content": f"Summarize this support ticket: {ticket_text}"})
    
    import json
    response = client.chat.completions.create(
        model="gpt-4o-mini",  # 良いFew-shotの例があればMiniで十分
        messages=messages,
        temperature=0.1,
        response_format={"type": "json_object"}
    )
    
    return json.loads(response.choices[0].message.content)

# Test with new tickets
test_tickets = [
    "VPN keeps disconnecting after 10 minutes, using Cisco AnyConnect on MacBook Pro M3",
    "Need to add 3 new team members to Slack workspace, sending email list separately",
]

for ticket in test_tickets:
    result = few_shot_ticket_classifier(ticket)
    print(f"Ticket: {ticket[:60]}...")
    print(f"  Type: {result.get('issue_type')} | Urgency: {result.get('urgency')}")
    print(f"  Next step: {result.get('suggested_next_step')[:80]}")
    print()

RAG: When Your Data Changes or Is Private

RAG is the right choice when the problem is knowledge, not capability. If the model would answer correctly if it had access to your documents, use RAG. If the model still struggles even when you paste the relevant content into the prompt, that's a capability or behavior problem — RAG won't fix it.

RAG excels for: company knowledge bases (HR policies, product documentation, internal wikis), real-time data (inventory levels, pricing that changes), proprietary research, and any content that would be prohibitively large to include in context. A company with 10,000 policy documents can't include them all in every prompt — RAG retrieves the relevant 3–5 documents dynamically.

Code Example 3: Complete RAG Pipeline with Pinecone

from openai import OpenAI
from pinecone import Pinecone, ServerlessSpec
import hashlib
from typing import Optional

client = OpenAI()
pc = Pinecone(api_key="your-pinecone-api-key")

# Initialize vector index (run once)
INDEX_NAME = "company-knowledge-base"
EMBEDDING_MODEL = "text-embedding-3-small"
EMBEDDING_DIM = 1536

def initialize_index():
    """Create Pinecone index if it doesn't exist."""
    if INDEX_NAME not in [idx.name for idx in pc.list_indexes()]:
        pc.create_index(
            name=INDEX_NAME,
            dimension=EMBEDDING_DIM,
            metric="cosine",
            spec=ServerlessSpec(cloud="aws", region="us-east-1")
        )
    return pc.Index(INDEX_NAME)

def chunk_document(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
    """Split document into overlapping chunks for embedding."""
    words = text.split()
    chunks = []
    for i in range(0, len(words), chunk_size - overlap):
        chunk = " ".join(words[i:i + chunk_size])
        if chunk:
            chunks.append(chunk)
    return chunks

def embed_text(text: str) -> list[float]:
    """Get embedding vector for a text string."""
    response = client.embeddings.create(
        model=EMBEDDING_MODEL,
        input=text.strip()
    )
    return response.data[0].embedding

def index_documents(documents: list[dict], index) -> int:
    """
    Index a list of documents into Pinecone.
    
    documents format: [{"title": str, "content": str, "source": str}]
    Returns: number of chunks indexed
    """
    vectors = []
    
    for doc in documents:
        chunks = chunk_document(doc["content"])
        
        for i, chunk in enumerate(chunks):
            chunk_id = hashlib.md5(f"{doc['title']}_{i}".encode()).hexdigest()
            embedding = embed_text(chunk)
            
            vectors.append({
                "id": chunk_id,
                "values": embedding,
                "metadata": {
                    "title": doc["title"],
                    "source": doc["source"],
                    "chunk_index": i,
                    "text": chunk  # Store text in metadata for retrieval
                }
            })
    
    # Batch upsert (max 100 vectors per request)
    batch_size = 100
    for i in range(0, len(vectors), batch_size):
        index.upsert(vectors=vectors[i:i + batch_size])
    
    return len(vectors)

def retrieve_context(
    query: str,
    index,
    top_k: int = 5,
    min_score: float = 0.7
) -> list[dict]:
    """
    Retrieve the most relevant document chunks for a query.
    """
    query_embedding = embed_text(query)
    
    results = index.query(
        vector=query_embedding,
        top_k=top_k,
        include_metadata=True
    )
    
    # Filter by minimum relevance score
    relevant_chunks = [
        {
            "text": match.metadata["text"],
            "title": match.metadata["title"],
            "source": match.metadata["source"],
            "score": match.score
        }
        for match in results.matches
        if match.score >= min_score
    ]
    
    return relevant_chunks

def rag_completion(
    question: str,
    index,
    model: str = "gpt-4o",
    top_k: int = 5
) -> dict:
    """
    Answer a question using RAG pipeline:
    1. Embed query
    2. Retrieve relevant chunks
    3. Generate answer grounded in retrieved context
    """
    # Step 1: Retrieve relevant context
    context_chunks = retrieve_context(question, index, top_k=top_k)
    
    if not context_chunks:
        return {
            "answer": "I couldn't find relevant information in the knowledge base to answer this question.",
            "sources": [],
            "context_used": False
        }
    
    # Step 2: Build context string
    context = "\n\n---\n\n".join([
        f"Source: {chunk['title']}\n{chunk['text']}"
        for chunk in context_chunks
    ])
    
    # Step 3: Generate answer
    response = client.chat.completions.create(
        model=model,
        messages=[
            {
                "role": "system",
                "content": """You are a helpful assistant. Answer questions using ONLY 
                the provided context. If the context doesn't contain the answer, say so clearly.
                Always cite which document(s) your answer comes from."""
            },
            {
                "role": "user",
                "content": f"""Context:
{context}

Question: {question}

Answer based only on the context above:"""
            }
        ],
        temperature=0.2
    )
    
    return {
        "answer": response.choices[0].message.content,
        "sources": [{"title": c["title"], "score": c["score"]} for c in context_chunks],
        "context_used": True,
        "chunks_retrieved": len(context_chunks)
    }

# Example usage
index = initialize_index()

# Index your documents (run once, then on document updates)
sample_docs = [
    {
        "title": "Employee PTO Policy 2026",
        "content": """Employees receive 15 days of PTO per year. PTO accrues at 1.25 days per month
        starting from the first day of employment. Employees may carry over up to 5 days of 
        unused PTO to the following calendar year. PTO requests must be submitted at least 
        2 weeks in advance for absences of 3+ days...""",
        "source": "hr-policies/pto-2026.pdf"
    }
]

chunks_indexed = index_documents(sample_docs, index)
print(f"Indexed {chunks_indexed} chunks")

# Query the knowledge base
result = rag_completion("How many PTO days do new employees get?", index)
print(f"\nAnswer: {result['answer']}")
print(f"Sources: {[s['title'] for s in result['sources']]}")

"In our testing, the most common RAG failure is not retrieval quality — it's chunking strategy. Teams that chunk at 2,000 tokens lose context across sentence boundaries, leading to retrieved chunks that look relevant but miss the crucial qualifier in the following sentence. We found 500-token chunks with 50-token overlap work best across most document types. For tables and lists, chunk at the element level, not by token count."

— Alex Chen, AgDex Engineering

Fine-tuning: When You Need Consistent Behavior at Scale

Fine-tuning is overused as a first resort and underused as an optimization tool. It doesn't teach models new factual knowledge (that's RAG's job) — it teaches models new behaviors, styles, and patterns. Fine-tune when you need: a specific output format that few-shot examples can't consistently achieve, domain-specific terminology the model consistently misuses, high-throughput inference where you need a smaller, faster model, or consistent tone and brand voice across millions of outputs.

LoRA (Low-Rank Adaptation) has become the dominant fine-tuning method because it's parameter-efficient: instead of updating all model weights (which requires massive compute), LoRA adds small trainable matrices to each attention layer. You can fine-tune a 7B model on a single A100 GPU in a few hours, then merge the LoRA weights back into the base model for zero-overhead inference.

Code Example 4: LoRA Fine-tuning with HuggingFace PEFT

from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    Trainer,
    DataCollatorForSeq2Seq
)
from peft import (
    get_peft_model,
    LoraConfig,
    TaskType,
    prepare_model_for_kbit_training
)
from datasets import Dataset
import torch
import json

# LoRA configuration — tune rank (r) and alpha for your task
# Higher r = more capacity but more compute. Start with r=16.
LORA_CONFIG = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,               # Rank of update matrices
    lora_alpha=32,      # Scaling factor (alpha/r = effective learning rate scale)
    lora_dropout=0.1,   # Dropout for regularization
    bias="none",
    target_modules=["q_proj", "v_proj"]  # Attention layers to adapt
)

def prepare_training_data(examples: list[dict]) -> Dataset:
    """
    Format training data as instruction-following pairs.
    
    examples: [{"instruction": str, "input": str, "output": str}]
    """
    formatted = []
    for ex in examples:
        prompt = f"""### Instruction:
{ex['instruction']}

### Input:
{ex['input']}

### Response:
{ex['output']}"""
        formatted.append({"text": prompt})
    
    return Dataset.from_list(formatted)

def finetune_model(
    base_model_name: str = "meta-llama/Meta-Llama-3.1-8B",
    training_examples: list[dict] = None,
    output_dir: str = "./finetuned_model",
    num_epochs: int = 3
):
    """
    Fine-tune a causal LM with LoRA for instruction following.
    Requires ~24GB VRAM for Llama 3.1 8B, or use quantization for less.
    """
    print(f"Loading base model: {base_model_name}")
    
    # Load tokenizer
    tokenizer = AutoTokenizer.from_pretrained(base_model_name)
    tokenizer.pad_token = tokenizer.eos_token
    tokenizer.padding_side = "right"
    
    # Load model with 4-bit quantization for memory efficiency
    model = AutoModelForCausalLM.from_pretrained(
        base_model_name,
        load_in_4bit=True,  # Use bitsandbytes 4-bit quantization
        device_map="auto",
        torch_dtype=torch.float16
    )
    
    # Prepare model for k-bit training
    model = prepare_model_for_kbit_training(model)
    
    # Apply LoRA adapters
    model = get_peft_model(model, LORA_CONFIG)
    model.print_trainable_parameters()  
    # Output: trainable params: 4,194,304 (0.05% of total) — very efficient!
    
    # Prepare dataset
    train_dataset = prepare_training_data(training_examples)
    
    def tokenize_function(examples):
        return tokenizer(
            examples["text"],
            truncation=True,
            max_length=2048,
            padding="max_length"
        )
    
    tokenized_dataset = train_dataset.map(tokenize_function, batched=True)
    
    # Training configuration
    training_args = TrainingArguments(
        output_dir=output_dir,
        num_train_epochs=num_epochs,
        per_device_train_batch_size=4,
        gradient_accumulation_steps=4,  # Effective batch size: 16
        learning_rate=2e-4,
        fp16=True,
        logging_steps=10,
        save_strategy="epoch",
        warmup_ratio=0.05,
        lr_scheduler_type="cosine",
        report_to="none"  # Set to "wandb" for experiment tracking
    )
    
    # Train
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=tokenized_dataset,
        data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True)
    )
    
    print("Starting fine-tuning...")
    trainer.train()
    
    # Save LoRA weights (small! typically 50-200MB vs 16GB base model)
    model.save_pretrained(f"{output_dir}/lora_weights")
    tokenizer.save_pretrained(f"{output_dir}/lora_weights")
    print(f"LoRA weights saved to {output_dir}/lora_weights")
    
    return model, tokenizer

# Example training data: teach model your company's support response style
training_examples = [
    {
        "instruction": "Write a customer support response",
        "input": "User complaint: My order #12345 arrived damaged. The box was crushed and the item inside is broken.",
        "output": """Dear Valued Customer,

Thank you for contacting us about order #12345. We sincerely apologize for the damaged delivery — this falls well below our standards and we completely understand your frustration.

Here's what we'll do to make this right:
1. We'll send a replacement at no charge, shipping within 1 business day
2. You can keep the damaged item — no return needed
3. We've added a $15 credit to your account for the inconvenience

Could you please reply with a photo of the damaged item for our quality team? This helps us prevent future occurrences.

Your replacement tracking number will be emailed within 2 hours.

Warm regards,
Support Team"""
    }
    # Add 500-2000 more examples for best results
]

# To actually run this, you need a GPU instance:
# model, tokenizer = finetune_model(training_examples=training_examples)
print("LoRA fine-tuning setup complete. Run on a GPU instance with 24GB+ VRAM.")

Comparison Matrix: Three Approaches Side by Side

Dimension Prompt Engineering RAG Fine-tuning
Implementation Cost ~$0 $5K–50K eng $10K–200K
Update Speed Instant Minutes (re-index) Hours–days (retrain)
Inference Cost Medium (large prompts) Medium + vector DB Low (small model)
Knowledge Currency Model cutoff only Real-time via index Training cutoff
Behavior Consistency Medium Medium High
Data Privacy Training data safe Docs in API calls Training data at provider
Best For Format, reasoning, task guidance Knowledge retrieval, large docs Style, domain behavior, speed

Hybrid Approaches: Combining All Three

The production systems with the best performance in 2026 don't choose one approach — they layer all three. The architecture is consistent: fine-tuned base model that understands your domain vocabulary and produces consistent output formats, augmented with RAG for dynamic knowledge retrieval, guided by carefully engineered prompts. Each layer handles what it does best.

Hybrid Architecture: Support Bot for Insurance Company

PE

Prompt Engineering Layer

System prompt defines insurance expert persona, response format (always cite policy number), escalation criteria (claims over $10,000, disputed denials)

↓ injects into context
RAG

RAG Layer

Retrieves relevant policy documents, state-specific regulations, recent claims precedents. Updated daily. 50,000+ documents indexed in Pinecone.

↓ feeds into
FT

Fine-tuned Base Model

Llama 3.1 8B fine-tuned on 5,000 insurance support conversations. Understands "deductible", "subrogation", "co-pay" correctly without confusion. Consistent citation format.

94%

Accuracy on domain Q&A

$0.0004

Cost per query

380ms

Avg. response time

Visual Decision Tree

Use this flowchart to determine your starting approach. You can always layer additional techniques once the baseline is working.

Does the model fail even when given the right information in the prompt?
Yes
Is it a reasoning / logic failure?
Yes
Use Chain-of-Thought Prompting
No
Fine-tune on domain examples
No (knowledge gap)
Does the data change frequently?
Yes
RAG with live index
No
High volume (>10K/day)?
Yes → Fine-tune small model
No → RAG or long context

Performance Benchmarks: Real Task Comparisons

Task Prompt Eng. RAG Fine-tuning RAG + FT
Customer Support Q&A 72% 89% 81% 94%
Code Generation (specific style) 68% 71% 91% 93%
Document Summarization 84% 82% 87% 91%
Classification (domain-specific) 74% 78% 95% 96%
Multi-step Reasoning 88%* 84% 79% 86%

*CoT prompting with GPT-4o. Accuracy measured against human expert ground truth. Internal AgDex testing, 2026年4月. Results vary by domain.

よくある質問

RAGはファインチューニングを完全に置き換えられますか?

事実に基づくナレッジ検索については、はい — RAG is often better than fine-tuning because it keeps knowledge current and is auditable (you can see which documents were retrieved). But RAG cannot replace fine-tuning for behavioral changes: if you need the model to always respond in a specific format, use specific terminology, or maintain a particular tone across millions of varied inputs, fine-tuning is the only reliable solution. RAGはモデルにナレッジを与え、ファインチューニングはモデルの挙動を変更します。

ファインチューニングにはどれくらいのトレーニングデータが必要ですか?

For GPT-4o-mini via OpenAI's fine-tuning API: 100 examples is a functional minimum, 500–1,000 examples is a sweet spot for most tasks, and 5,000+ is for complex tasks requiring deep specialization. For LoRA fine-tuning of open-source models: similar numbers apply, but quality matters more than quantity. 200 high-quality, diverse examples beat 2,000 mediocre ones. Avoid duplicate or near-duplicate examples — data diversity is the primary driver of generalization. Use GPT-4o to help generate training examples from your real data if you need to scale quickly.

RAGにはどのベクトルデータベースを使うべきですか?

For getting started quickly: Pinecone (managed, no infrastructure) or Chroma (local, open-source). For production at scale: Weaviate, Qdrant, or pgvector (if you're already on PostgreSQL). For enterprise with existing infrastructure: Azure AI Search, Google Vertex AI Vector Search, or Amazon OpenSearch Serverless. The performance differences between vector databases are small compared to the impact of your chunking strategy and embedding model choice. Start with what's easiest to deploy and optimize later.

GPT-4o-miniのファインチューニングはGPT-4oのプロンプティングと比べて価値がありますか?

Yes, for the right use case. A fine-tuned GPT-4o-mini typically outperforms a prompted GPT-4o on domain-specific tasks (by 5–15% accuracy) while costing 10–20× less per call. The break-even analysis: fine-tuning cost ($500–$2,000 for a good dataset) ÷ cost savings per call ($0.002–$0.003) = break-even at 200,000–500,000 calls. At typical SaaS volumes of 1,000+ calls/day, this breaks even in weeks. The caveat: fine-tuned models need retraining when your requirements change, which adds ongoing maintenance cost.

ファインチューニングしたモデルでRAGを使えますか?

Yes, and this hybrid is often the best production architecture. Fine-tune the model to understand your domain vocabulary and produce consistent output formats, then add RAG for knowledge retrieval at inference time. The fine-tuned model is better at correctly interpreting retrieved documents (it understands domain context), and the RAG layer keeps knowledge current without requiring retraining. The main complexity is that you need to maintain both the fine-tuned model and the vector index, but the performance gains usually justify the operational overhead for high-volume applications.

🔧 Related Guides