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 documentationDSPy 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.Exampleobjects 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
Predictmodule 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
| Failure | Cause |
|---|---|
| Authentication errors | Missing or invalid API key |
| LM provider errors | Incorrect model name |
| Optimization fails | Training data without .with_inputs() |
| Evaluation errors | Metric function with wrong signature |
| Module crash | Assertion failure without backtrack handler |
| Retrieval errors | Retriever not configured or unavailable |
| Incomplete JSON | Saving a module that was not compiled |
| Load errors | Loading a module with different structure |
| Empty trace list | Tracing not enabled before prediction |
| Context window exceeded | Too 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.