Building composable prediction modules with Predict, ChainOfThought, ReAct, and custom modules
Scope: Predict, ChainOfThought, ReAct, ProgramOfThought, custom modules, composition Lines: ~450 Last Updated: 2025-10-25
Activate this skill when:
Definition: Modules are composable building blocks that replace hand-written prompts
Purpose:
Key insight: Modules separate strategy (how to prompt) from task (what to ask)
Predict: Basic prediction
ChainOfThought: Multi-step reasoning
ReAct: Reasoning + Acting
ProgramOfThought: Code generation + execution
Pattern: Small modules → Complex systems
forward() methodimport dspy
# Configure LM
lm = dspy.LM("openai/gpt-4o-mini")
dspy.configure(lm=lm)
# Simple prediction
predictor = dspy.Predict("question -> answer")
# Use module
result = predictor(question="What is the capital of France?")
print(result.answer) # "Paris"
# With typed signature
classifier = dspy.Predict("text -> category, confidence: float")
result = classifier(text="This movie was amazing!")
print(f"{result.category} ({result.confidence})")
When to use:
import dspy
class QASignature(dspy.Signature):
"""Answer questions with reasoning."""
question = dspy.InputField(desc="Question to answer")
answer = dspy.OutputField(desc="Answer to the question")
# Add chain-of-thought reasoning
cot = dspy.ChainOfThought(QASignature)
# Module automatically adds reasoning step
result = cot(question="If it takes 5 machines 5 minutes to make 5 widgets, how long for 100 machines to make 100 widgets?")
print("Reasoning:", result.reasoning) # Shows step-by-step thinking
print("Answer:", result.answer) # "5 minutes"
When to use:
How it works:
reasoning field to signatureimport dspy
class SearchQA(dspy.Signature):
"""Answer questions using search results."""
question = dspy.InputField()
answer = dspy.OutputField()
# Define tools that ReAct can use
def search(query: str) -> str:
"""Search for information."""
# Implement actual search (e.g., Google, Wikipedia)
return f"Search results for: {query}"
def calculate(expression: str) -> float:
"""Evaluate mathematical expressions."""
return eval(expression) # Use safely in production!
# Create ReAct module with tools
react = dspy.ReAct(
SearchQA,
tools=[search, calculate],
max_iters=5, # Maximum thought-action cycles
)
# ReAct will:
# 1. Think about the question
# 2. Decide which tool to use
# 3. Execute the tool
# 4. Observe results
# 5. Repeat until answer found
result = react(question="What is the population of the capital of France?")
print(result.answer)
When to use:
How it works:
import dspy
class MathProblem(dspy.Signature):
"""Solve math problems by generating Python code."""
problem = dspy.InputField(desc="Math problem to solve")
solution = dspy.OutputField(desc="Numerical solution")
# ProgramOfThought generates and executes code
pot = dspy.ProgramOfThought(MathProblem)
result = pot(problem="What is 15% of 240 plus 30% of 150?")
print("Code:", result.code) # Generated Python code
print("Solution:", result.solution) # Executed result
When to use:
Benefits:
import dspy
class RAGModule(dspy.Module):
"""Custom Retrieval-Augmented Generation module."""
def __init__(self, num_passages=3):
super().__init__()
self.num_passages = num_passages
# Sub-modules
self.retrieve = dspy.Retrieve(k=num_passages)
self.generate = dspy.ChainOfThought("context, question -> answer")
def forward(self, question):
# Step 1: Retrieve relevant passages
passages = self.retrieve(question).passages
# Step 2: Format context
context = "\n\n".join(passages)
# Step 3: Generate answer with context
result = self.generate(context=context, question=question)
return result
# Use custom module
rag = RAGModule(num_passages=5)
result = rag(question="What is DSPy?")
print(result.answer)
When to use:
Structure:
dspy.Module__init__()forward() methodimport dspy
class MultiStepQA(dspy.Module):
"""Answer questions using multi-step reasoning."""
def __init__(self):
super().__init__()
# Step 1: Break down question
self.decompose = dspy.Predict("question -> sub_questions: list[str]")
# Step 2: Answer sub-questions
self.answer_sub = dspy.ChainOfThought("question -> answer")
# Step 3: Synthesize final answer
self.synthesize = dspy.ChainOfThought("question, sub_answers -> final_answer")
def forward(self, question):
# Decompose into sub-questions
decomp = self.decompose(question=question)
# Answer each sub-question
sub_answers = []
for sub_q in decomp.sub_questions:
ans = self.answer_sub(question=sub_q)
sub_answers.append(f"Q: {sub_q}\nA: {ans.answer}")
# Synthesize final answer
result = self.synthesize(
question=question,
sub_answers="\n\n".join(sub_answers)
)
return dspy.Prediction(
sub_questions=decomp.sub_questions,
sub_answers=sub_answers,
answer=result.final_answer
)
# Use composed module
qa = MultiStepQA()
result = qa(question="Compare and contrast Python and JavaScript")
print("Sub-questions:", result.sub_questions)
print("Final answer:", result.answer)
Benefits:
import dspy
class RobustPredictor(dspy.Module):
"""Predictor with automatic retry on failure."""
def __init__(self, signature, max_retries=3):
super().__init__()
self.predictor = dspy.ChainOfThought(signature)
self.max_retries = max_retries
def forward(self, **kwargs):
last_error = None
for attempt in range(self.max_retries):
try:
result = self.predictor(**kwargs)
# Validate result (example: check if answer is non-empty)
if hasattr(result, 'answer') and result.answer.strip():
return result
except Exception as e:
last_error = e
print(f"Attempt {attempt + 1} failed: {e}")
# All retries failed
raise Exception(f"All {self.max_retries} attempts failed. Last error: {last_error}")
# Use robust predictor
predictor = RobustPredictor("question -> answer")
result = predictor(question="What is DSPy?")
When to use:
import dspy
from concurrent.futures import ThreadPoolExecutor
class EnsemblePredictor(dspy.Module):
"""Run multiple predictors in parallel and combine results."""
def __init__(self, signature, num_predictors=3):
super().__init__()
# Create multiple predictor instances
self.predictors = [
dspy.ChainOfThought(signature)
for _ in range(num_predictors)
]
self.vote = dspy.Predict("answers: list[str] -> best_answer: str")
def forward(self, **kwargs):
# Execute predictors in parallel
with ThreadPoolExecutor(max_workers=len(self.predictors)) as executor:
futures = [
executor.submit(pred, **kwargs)
for pred in self.predictors
]
results = [f.result() for f in futures]
# Extract answers
answers = [r.answer for r in results]
# Vote on best answer
best = self.vote(answers=answers)
return dspy.Prediction(
all_answers=answers,
answer=best.best_answer
)
# Use ensemble
ensemble = EnsemblePredictor("question -> answer", num_predictors=5)
result = ensemble(question="What is the meaning of life?")
print("All answers:", result.all_answers)
print("Best answer:", result.answer)
When to use:
| Module | Reasoning | Speed | Accuracy | Use Case | |--------|-----------|-------|----------|----------| | Predict | None | Fastest | Good | Simple tasks | | ChainOfThought | Sequential | Fast | Better | Complex reasoning | | ReAct | Iterative + Tools | Slow | Best (with tools) | Multi-step + retrieval | | ProgramOfThought | Code gen | Medium | Excellent (math) | Computations |
class MyModule(dspy.Module):
def __init__(self):
super().__init__()
# Define sub-modules
def forward(self, **kwargs):
# Implement logic
return dspy.Prediction(...)
✅ DO: Use Predict for simple tasks
✅ DO: Use ChainOfThought for complex reasoning
✅ DO: Use ReAct when you need external information
✅ DO: Compose small modules into larger systems
✅ DO: Return dspy.Prediction from custom modules
❌ DON'T: Use ChainOfThought for all tasks (overkill for simple ones)
❌ DON'T: Use ReAct without proper tools
❌ DON'T: Create monolithic custom modules
❌ DON'T: Forget to call super().__init__() in custom modules
# Simple predictor
pred = dspy.Predict("input -> output")
# Reasoning predictor
cot = dspy.ChainOfThought("question -> answer")
# With tools
react = dspy.ReAct(signature, tools=[tool1, tool2])
# Custom module
class Custom(dspy.Module):
def __init__(self):
super().__init__()
self.pred = dspy.Predict(sig)
def forward(self, x):
return self.pred(input=x)
❌ Using ChainOfThought for everything: Slower and more expensive
# Bad - overkill for simple classification
classifier = dspy.ChainOfThought("text -> category")
✅ Use Predict for simple tasks:
# Good
classifier = dspy.Predict("text -> category")
❌ Not composing modules: Monolithic, hard to optimize
# Bad - everything in one module
class Monolith(dspy.Module):
def forward(self, x):
# 100 lines of complex logic
pass
✅ Compose smaller modules:
# Good
class Pipeline(dspy.Module):
def __init__(self):
super().__init__()
self.step1 = Module1()
self.step2 = Module2()
self.step3 = Module3()
❌ Ignoring module return format: Breaks composition
# Bad
def forward(self, x):
result = self.pred(x)
return result.answer # Returns string, not Prediction
✅ Return Prediction objects:
# Good
def forward(self, x):
result = self.pred(x)
return dspy.Prediction(answer=result.answer, confidence=0.9)
dspy-signatures.md - Defining signatures for modulesdspy-optimizers.md - Optimizing module parametersdspy-rag.md - Building RAG pipelines with modulesdspy-assertions.md - Adding validation to modulesdspy-evaluation.md - Evaluating module performanceLast Updated: 2025-10-25 Format Version: 1.0 (Atomic)