OptionalMLOpsVersion 1.0.0

DSPy Skill for Hermes Agent: Declarative LM Programming & Auto-Optimization

DSPy: declarative LM programs, auto-optimize prompts, RAG.

Written by Neura Market from the official Hermes Agent documentation for Dspy. Commands, paths, and version numbers are reproduced from the source unchanged.

Read the official documentation

DSPy Framework Reference

DSPy is a framework for declarative language model programming. It automates prompt engineering and optimizes LM pipelines using data-driven methods. Use DSPy when you need to build complex AI systems with multiple components, program LMs declaratively instead of manual prompt engineering, optimize prompts automatically, create modular and portable AI pipelines, improve model outputs systematically with optimizers, or build RAG systems, agents, or classifiers with better reliability.

Prerequisites

Before using DSPy, ensure you have:

  • A Python environment with pip
  • API keys for LM providers (or a local model server like Ollama)
  • Training data for optimization (can be generated)
  • Basic understanding of Python classes and functions
  • For RAG: a configured retriever (e.g., ChromadbRM with existing collection)

Installation

Install DSPy using pip. Choose the variant that matches your needs.

# Stable release
pip install dspy

# Latest development version
pip install git+https://github.com/stanfordnlp/dspy.git

# With specific LM providers
pip install dspy[openai]        # OpenAI
pip install dspy[anthropic]     # Anthropic Claude
pip install dspy[all]           # All providers

Core Concepts

Signatures

Signatures define the task structure. Use inline signatures for quick prototyping and class signatures for complex tasks. An inline signature uses the format "input_field -> output_field". A class signature provides detailed descriptions.

# Inline signature (simple)
qa = dspy.Predict("question -> answer")

# Class signature (detailed)
class Summarize(dspy.Signature):
    """Summarize text into key points."""
    text = dspy.InputField()
    summary = dspy.OutputField(desc="bullet points, 3-5 items")

summarizer = dspy.ChainOfThought(Summarize)

For multiple inputs, separate them with commas:

predictor = dspy.Predict("context, question -> answer")
result = predictor(context="Paris is the capital of France",
                   question="What is the capital?")

Modules

DSPy provides several built-in modules:

  • Predict: Basic prediction without reasoning steps
  • ChainOfThought: Generates reasoning steps automatically
  • ReAct: Agent-style reasoning with tool use
  • ProgramOfThought: Generates and executes code
  • TypedPredictor: Structured output with Pydantic models

Basic QA with Predict

import dspy

# Configure your language model
lm = dspy.Claude(model="claude-sonnet-4-5-20250929")
dspy.settings.configure(lm=lm)

# Define a signature (input → output)
class QA(dspy.Signature):
    """Answer questions with short factual answers."""
    question = dspy.InputField()
    answer = dspy.OutputField(desc="often between 1 and 5 words")

# Create a module
qa = dspy.Predict(QA)

# Use it
response = qa(question="What is the capital of France?")
print(response.answer)  # "Paris"

Chain of Thought

import dspy

lm = dspy.Claude(model="claude-sonnet-4-5-20250929")
dspy.settings.configure(lm=lm)

# Use ChainOfThought for better reasoning
class MathProblem(dspy.Signature):
    """Solve math word problems."""
    problem = dspy.InputField()
    answer = dspy.OutputField(desc="numerical answer")

# ChainOfThought generates reasoning steps automatically
cot = dspy.ChainOfThought(MathProblem)

response = cot(problem="If John has 5 apples and gives 2 to Mary, how many does he have?")
print(response.rationale)  # Shows reasoning steps
print(response.answer)     # "3"

ChainOfThought also works with inline signatures:

cot = dspy.ChainOfThought("question -> answer")
result = cot(question="Why is the sky blue?")
print(result.rationale)  # Reasoning steps
print(result.answer)     # Final answer

ReAct Agent

from dspy.predict import ReAct

class SearchQA(dspy.Signature):
    """Answer questions using search."""
    question = dspy.InputField()
    answer = dspy.OutputField()

def search_tool(query: str) -> str:
    """Search Wikipedia."""
    # Your search implementation
    return results

react = ReAct(SearchQA, tools=[search_tool])
result = react(question="When was Python created?")

ProgramOfThought

pot = dspy.ProgramOfThought("question -> answer")
result = pot(question="What is 15% of 240?")
# Generates: answer = 240 * 0.15

The ProgramOfThought module generates and executes code to compute the answer. In this example, it calculates 15% of 240.

Optimizers

DSPy provides three optimizers to improve module performance automatically.

BootstrapFewShot

from dspy.teleprompt import BootstrapFewShot

# Training data
trainset = [
    dspy.Example(question="What is 2+2?", answer="4").with_inputs("question"),
    dspy.Example(question="What is 3+5?", answer="8").with_inputs("question"),
]

# Define metric
def validate_answer(example, pred, trace=None):
    return example.answer == pred.answer

# Optimize
optimizer = BootstrapFewShot(metric=validate_answer, max_bootstrapped_demos=3)
optimized_qa = optimizer.compile(qa, trainset=trainset)

# Now optimized_qa performs better!

MIPRO

from dspy.teleprompt import MIPRO

optimizer = MIPRO(
    metric=validate_answer,
    num_candidates=10,
    init_temperature=1.0
)

optimized_cot = optimizer.compile(
    cot,
    trainset=trainset,
    num_trials=100
)

MIPRO iteratively improves prompts. With 10 candidates and 100 trials, it searches for optimal prompt configurations. This can be computationally expensive.

BootstrapFinetune

from dspy.teleprompt import BootstrapFinetune

optimizer = BootstrapFinetune(metric=validate_answer)
optimized_module = optimizer.compile(qa, trainset=trainset)

# Exports training data for fine-tuning

BootstrapFinetune exports training data for fine-tuning; it does not fine-tune the model itself.

Multi-Stage Pipelines

Build complex pipelines by inheriting from dspy.Module.

import dspy

class MultiHopQA(dspy.Module):
    def __init__(self):
        super().__init__()
        self.retrieve = dspy.Retrieve(k=3)
        self.generate_query = dspy.ChainOfThought("question -> search_query")
        self.generate_answer = dspy.ChainOfThought("context, question -> answer")

    def forward(self, question):
        # Stage 1: Generate search query
        search_query = self.generate_query(question=question).search_query

        # Stage 2: Retrieve context
        passages = self.retrieve(search_query).passages
        context = "\n".join(passages)

        # Stage 3: Generate answer
        answer = self.generate_answer(context=context, question=question).answer
        return dspy.Prediction(answer=answer, context=context)

# Use the pipeline
qa_system = MultiHopQA()
result = qa_system(question="Who wrote the book that inspired the movie Blade Runner?")

RAG System with Optimization

import dspy
from dspy.retrieve.chromadb_rm import ChromadbRM

# Configure retriever
retriever = ChromadbRM(
    collection_name="documents",
    persist_directory="./chroma_db"
)

class RAG(dspy.Module):
    def __init__(self, num_passages=3):
        super().__init__()
        self.retrieve = dspy.Retrieve(k=num_passages)
        self.generate = dspy.ChainOfThought("context, question -> answer")

    def forward(self, question):
        context = self.retrieve(question).passages
        return self.generate(context=context, question=question)

# Create and optimize
rag = RAG()

# Optimize with training data
from dspy.teleprompt import BootstrapFewShot

optimizer = BootstrapFewShot(metric=validate_answer)
optimized_rag = optimizer.compile(rag, trainset=trainset)

Configuring LM Providers

Anthropic Claude

import dspy

lm = dspy.Claude(
    model="claude-sonnet-4-5-20250929",
    api_key="your-api-key",  # Or set ANTHROPIC_API_KEY env var
    max_tokens=1000,
    temperature=0.7
)
dspy.settings.configure(lm=lm)

OpenAI

lm = dspy.OpenAI(
    model="gpt-4",
    api_key="your-api-key",
    max_tokens=1000
)
dspy.settings.configure(lm=lm)

Ollama (Local Models)

lm = dspy.OllamaLocal(
    model="llama3.1",
    base_url="http://localhost:11434"
)
dspy.settings.configure(lm=lm)

Ollama requires a running local server at the specified base_url.

Multiple Models

Use dspy.settings.context to temporarily switch between models for different tasks.

# Different models for different tasks
cheap_lm = dspy.OpenAI(model="gpt-3.5-turbo")
strong_lm = dspy.Claude(model="claude-sonnet-4-5-20250929")

# Use cheap model for retrieval, strong model for reasoning
with dspy.settings.context(lm=cheap_lm):
    context = retriever(question)

with dspy.settings.context(lm=strong_lm):
    answer = generator(context=context, question=question)

Structured Output with TypedPredictor

from pydantic import BaseModel, Field

class PersonInfo(BaseModel):
    name: str = Field(description="Full name")
    age: int = Field(description="Age in years")
    occupation: str = Field(description="Current job")

class ExtractPerson(dspy.Signature):
    """Extract person information from text."""
    text = dspy.InputField()
    person: PersonInfo = dspy.OutputField()

extractor = dspy.TypedPredictor(ExtractPerson)
result = extractor(text="John Doe is a 35-year-old software engineer.")
print(result.person.name)  # "John Doe"
print(result.person.age)   # 35

Assertion-Driven Optimization

import dspy
from dspy.primitives.assertions import assert_transform_module, backtrack_handler

class MathQA(dspy.Module):
    def __init__(self):
        super().__init__()
        self.solve = dspy.ChainOfThought("problem -> solution: float")

    def forward(self, problem):
        solution = self.solve(problem=problem).solution

        # Assert solution is numeric
        dspy.Assert(
            isinstance(float(solution), float),
            "Solution must be a number",
            backtrack=backtrack_handler
        )

        return dspy.Prediction(solution=solution)

Assertion backtracking requires importing backtrack_handler. Without it, assertion failures may crash the module.

Self-Consistency

import dspy
from collections import Counter

class ConsistentQA(dspy.Module):
    def __init__(self, num_samples=5):
        super().__init__()
        self.qa = dspy.ChainOfThought("question -> answer")
        self.num_samples = num_samples

    def forward(self, question):
        # Generate multiple answers
        answers = []
        for _ in range(self.num_samples):
            result = self.qa(question=question)
            answers.append(result.answer)

        # Return most common answer
        most_common = Counter(answers).most_common(1)[0][0]
        return dspy.Prediction(answer=most_common)

Self-consistency requires multiple LM calls per question. The number of samples is controlled by num_samples.

Retrieval with Reranking

class RerankedRAG(dspy.Module):
    def __init__(self):
        super().__init__()
        self.retrieve = dspy.Retrieve(k=10)
        self.rerank = dspy.Predict("question, passage -> relevance_score: float")
        self.answer = dspy.ChainOfThought("context, question -> answer")

    def forward(self, question):
        # Retrieve candidates
        passages = self.retrieve(question).passages

        # Rerank passages
        scored = []
        for passage in passages:
            score = float(self.rerank(question=question, passage=passage).relevance_score)
            scored.append((score, passage))

        # Take top 3
        top_passages = [p for _, p in sorted(scored, reverse=True)[:3]]
        context = "\n\n".join(top_passages)

        # Generate answer
        return self.answer(context=context, question=question)

Reranking requires a separate Predict module for scoring each passage.

Evaluation

Define metric functions and use the Evaluate class.

def exact_match(example, pred, trace=None):
    """Exact match metric."""
    return example.answer.lower() == pred.answer.lower()

def f1_score(example, pred, trace=None):
    """F1 score for text overlap."""
    pred_tokens = set(pred.answer.lower().split())
    gold_tokens = set(example.answer.lower().split())

    if not pred_tokens:
        return 0.0

    precision = len(pred_tokens & gold_tokens) / len(pred_tokens)
    recall = len(pred_tokens & gold_tokens) / len(gold_tokens)

    if precision + recall == 0:
        return 0.0

    return 2 * (precision * recall) / (precision + recall)
from dspy.evaluate import Evaluate

# Create evaluator
evaluator = Evaluate(
    devset=testset,
    metric=exact_match,
    num_threads=4,
    display_progress=True
)

# Evaluate model
score = evaluator(qa_system)
print(f"Accuracy: {score}")

# Compare optimized vs unoptimized
score_before = evaluator(qa)
score_after = evaluator(optimized_qa)
print(f"Improvement: {score_after - score_before:.2%}")

The improvement from 2% to 22000% is possible depending on the task and data quality.

Save and Load Optimized Models

# Save
optimized_qa.save("models/qa_v1.json")

# Load
loaded_qa = dspy.ChainOfThought("question -> answer")
loaded_qa.load("models/qa_v1.json")

Saving a module that was not compiled may produce incomplete JSON. Loading a module with a different structure than saved causes errors.

Enable Tracing for Debugging

# Enable tracing
dspy.settings.configure(lm=lm, trace=[])

# Run prediction
result = qa(question="...")

# Inspect trace
for call in dspy.settings.trace:
    print(f"Prompt: {call['prompt']}")
    print(f"Response: {call['response']}")

Tracing must be enabled before prediction by setting trace=[] in settings. If not enabled, the trace list remains empty.

Best Practices

Development Workflow

# Start with Predict
qa = dspy.Predict("question -> answer")

# Add reasoning if needed
qa = dspy.ChainOfThought("question -> answer")

# Add optimization when you have data
optimized_qa = optimizer.compile(qa, trainset=data)

Signature Design

# ❌ Bad: Vague
class Task(dspy.Signature):
    input = dspy.InputField()
    output = dspy.OutputField()

# ✅ Good: Descriptive
class SummarizeArticle(dspy.Signature):
    """Summarize news articles into 3-5 key points."""
    article = dspy.InputField(desc="full article text")
    summary = dspy.OutputField(desc="bullet points, 3-5 items")

Training Data

# Create diverse training examples
trainset = [
    dspy.Example(question="factual", answer="...").with_inputs("question"),
    dspy.Example(question="reasoning", answer="...").with_inputs("question"),
    dspy.Example(question="calculation", answer="...").with_inputs("question"),
]

# Use validation set for metric
def metric(example, pred, trace=None):
    return example.answer in pred.answer

Training data without .with_inputs() causes optimization to fail. The metric function must accept (example, pred, trace=None) and return a score.

Constraints and Caveats

  • Inline signatures are for quick prototyping only; class signatures are recommended for complex tasks.
  • Optimizers require training data (dspy.Example objects with .with_inputs()).
  • For multiple models, use dspy.settings.context(lm=...) to temporarily switch; not global.
  • Assertion backtracking requires import of backtrack_handler.
  • Self-consistency requires multiple LM calls per question.
  • Reranking requires a separate Predict module for scoring each passage.
  • Save/load uses JSON format; module structure must match when loading.
  • Tracing must be enabled before prediction by setting trace=[] in settings.
  • Ollama requires a running local server at the specified base_url.
  • BootstrapFinetune exports training data for fine-tuning, does not fine-tune itself.
  • MIPRO may be computationally expensive due to multiple trials and candidates.
  • Using too many bootstrapped demos may exceed LM context window.

Failure Modes

FailureCause
Authentication errorsMissing or invalid API key
LM provider errorsIncorrect model name
Optimization failsTraining data without .with_inputs()
Evaluation errorsMetric function with wrong signature
Module crashAssertion failure without backtrack handler
Retrieval errorsRetriever not configured or unavailable
Incomplete JSONSaving a module that was not compiled
Load errorsLoading a module with different structure
Empty trace listTracing not enabled before prediction
Context window exceededToo many bootstrapped demos

Additional Resources

For more detailed examples and advanced usage, refer to the following documentation files: references/modules.md, references/optimizers.md, and references/examples.md.

More MLOps skills