Core Concepts

Core Concepts

dspy-go is built around three fundamental concepts that work together to create powerful LLM applications: Signatures, Modules, and Programs. Understanding these building blocks will help you build reliable, maintainable AI systems.

Signatures

Signatures define the contract between your application and the LLM. They specify what inputs the LLM needs and what outputs it should produce.

Why Signatures Matter

Instead of crafting prompts manually, signatures let you:

  • Define clear expectations - What goes in, what comes out
  • Type safety - Strong typing ensures correctness
  • Reusability - Use the same signature across different modules
  • Optimization - Optimizers can improve signatures automatically

Creating a Signature

Signatures are created using the core.NewSignature() function:

signature := core.NewSignature(
    []core.InputField{
        {Field: core.NewField("question",
            core.WithDescription("The question to answer"))},
        {Field: core.NewField("context",
            core.WithDescription("Background information"))},
    },
    []core.OutputField{
        {Field: core.NewField("answer",
            core.WithDescription("A concise, accurate answer"))},
        {Field: core.NewField("confidence",
            core.WithDescription("Confidence level: high/medium/low"))},
    },
).WithInstruction("You are a helpful assistant. Answer the question accurately using the provided context.")

You can also create simple signatures without descriptions:

signature := core.NewSignature(
    []core.InputField{
        {Field: core.NewField("question")},
    },
    []core.OutputField{
        {Field: core.NewField("answer")},
    },
)

Field Descriptions Matter

Important: Field descriptions aren’t just documentation—they directly influence prompt quality and LLM behavior.

// ❌ Weak description
{Field: core.NewField("sentiment")}

// ✅ Strong description
{Field: core.NewField("sentiment",
    core.WithDescription("The emotional tone: Positive, Negative, or Neutral"))}

// ✅ Even better - guides the LLM's reasoning
{Field: core.NewField("sentiment",
    core.WithDescription("Analyze the emotional tone. Consider word choice, context, and intensity. Return: Positive, Negative, or Neutral"))}

Modules

Modules are the execution engines that take your signatures and turn them into working LLM applications. They encapsulate different reasoning patterns and behaviors.

Core Modules

1. Predict - Direct Prediction

The simplest module. Makes a single prediction based on your signature.

predictor := modules.NewPredict(QuestionAnswerSignature{})

result, err := predictor.Process(ctx, map[string]interface{}{
    "question": "What is the capital of France?",
    "context": "France is a country in Western Europe.",
})

// result["answer"] = "Paris"
// result["confidence"] = "high"

When to use: Simple, single-step tasks where you need direct answers.

2. ChainOfThought - Step-by-Step Reasoning

Implements chain-of-thought reasoning, breaking down complex problems into steps.

cot := modules.NewChainOfThought(signature)

result, err := cot.Process(ctx, map[string]interface{}{
    "question": "If a train travels 120 miles in 2 hours, how far will it go in 5 hours?",
})

// result includes both:
// - "rationale": "Speed = 120/2 = 60 mph. Distance = 60 * 5 = 300 miles"
// - "answer": "300 miles"

When to use: Math problems, logical reasoning, multi-step analysis.

Key insight: ChainOfThought automatically adds a “rationale” field to guide the LLM’s thinking process.

3. ReAct - Reasoning + Acting

Combines reasoning with tool use. The LLM can call tools to gather information before answering.

// Create tools
calculator := tools.NewCalculatorTool()
searchTool := tools.NewSearchTool()

// Create registry
registry := tools.NewInMemoryToolRegistry()
registry.Register(calculator)
registry.Register(searchTool)

// Create ReAct module
react := modules.NewReAct(signature, registry, 5) // max 5 iterations

result, err := react.Process(ctx, map[string]interface{}{
    "question": "What is the population of Tokyo divided by 1000?",
})

// ReAct will:
// 1. Reason: "I need to find Tokyo's population"
// 2. Act: Call search tool
// 3. Reason: "Now I need to divide by 1000"
// 4. Act: Call calculator
// 5. Answer: "14,000 (approximately)"

When to use: Questions requiring external data, calculations, or API calls.

4. MultiChainComparison - Multi-Perspective Analysis

Compares multiple reasoning attempts and synthesizes a comprehensive answer.

multiChain := modules.NewMultiChainComparison(signature, 3, 0.7)

completions := []map[string]interface{}{
    {"rationale": "Cost-focused approach...", "solution": "Reduce expenses"},
    {"rationale": "Growth-focused approach...", "solution": "Invest in marketing"},
    {"rationale": "Balanced approach...", "solution": "Optimize both"},
}

result, err := multiChain.Process(ctx, map[string]interface{}{
    "problem": "How should we improve business performance?",
    "completions": completions,
})

// result contains synthesized recommendation considering all perspectives

When to use: Complex decisions requiring multiple viewpoints.

5. Refine - Quality Improvement

Runs multiple attempts with different parameters and selects the best result.

rewardFn := func(inputs, outputs map[string]interface{}) float64 {
    // Score based on answer length, completeness, etc.
    answer := outputs["answer"].(string)
    return calculateQualityScore(answer)
}

refine := modules.NewRefine(
    modules.NewPredict(signature),
    modules.RefineConfig{
        N:         5,          // 5 attempts
        RewardFn:  rewardFn,
        Threshold: 0.8,        // Stop if quality > 0.8
    },
)

result, err := refine.Process(ctx, inputs)
// Returns the highest-quality result

When to use: When quality is critical and you want the best possible answer.

6. Parallel - Batch Processing

Wraps any module for concurrent execution across multiple inputs.

baseModule := modules.NewPredict(signature)

parallel := modules.NewParallel(baseModule,
    modules.WithMaxWorkers(4),              // 4 concurrent workers
    modules.WithReturnFailures(false),      // Skip failures
)

batchInputs := []map[string]interface{}{
    {"question": "What is 2+2?"},
    {"question": "What is the capital of France?"},
    {"question": "What is the speed of light?"},
}

result, err := parallel.Process(ctx, map[string]interface{}{
    "batch_inputs": batchInputs,
})

// Process all inputs concurrently
results := result["results"].([]map[string]interface{})

When to use: Batch processing, bulk operations, performance optimization.

Module Composition

Modules can be composed to create sophisticated workflows:

// Combine ChainOfThought with Refine for high-quality reasoning
cotModule := modules.NewChainOfThought(signature)
refinedCot := modules.NewRefine(cotModule, refineConfig)

// Wrap with Parallel for batch high-quality reasoning
parallelRefinedCot := modules.NewParallel(refinedCot, parallelConfig)

Programs

Programs orchestrate multiple modules into complete workflows. They define how data flows through your system.

Creating a Program

program := core.NewProgram(
    map[string]core.Module{
        "retriever": retrieverModule,
        "generator": generatorModule,
    },
    func(ctx context.Context, inputs map[string]interface{}) (map[string]interface{}, error) {
        // Step 1: Retrieve relevant documents
        retrieverResult, err := retrieverModule.Process(ctx, inputs)
        if err != nil {
            return nil, err
        }

        // Step 2: Generate answer using retrieved documents
        generatorInputs := map[string]interface{}{
            "question": inputs["question"],
            "documents": retrieverResult["documents"],
        }

        return generatorModule.Process(ctx, generatorInputs)
    },
)

// Execute the program
result, err := program.Execute(ctx, map[string]interface{}{
    "question": "What are the benefits of Go for LLM applications?",
})

RAG (Retrieval-Augmented Generation) Example

A complete RAG pipeline demonstrating program composition:

// Define signatures
retrievalSig := core.NewSignature(
    []core.InputField{
        {Field: core.NewField("query")},
    },
    []core.OutputField{
        {Field: core.NewField("documents")},
    },
)

generationSig := core.NewSignature(
    []core.InputField{
        {Field: core.NewField("question")},
        {Field: core.NewField("documents")},
    },
    []core.OutputField{
        {Field: core.NewField("answer")},
    },
)

// Create modules
retriever := modules.NewPredict(retrievalSig)
generator := modules.NewChainOfThought(generationSig)

// Compose into RAG program
ragProgram := core.NewProgram(
    map[string]core.Module{
        "retriever": retriever,
        "generator": generator,
    },
    func(ctx context.Context, inputs map[string]interface{}) (map[string]interface{}, error) {
        // Retrieval phase
        docs, err := retriever.Process(ctx, map[string]interface{}{
            "query": inputs["question"],
        })
        if err != nil {
            return nil, err
        }

        // Generation phase
        return generator.Process(ctx, map[string]interface{}{
            "question": inputs["question"],
            "documents": docs["documents"],
        })
    },
)

// Use the program
answer, err := ragProgram.Execute(ctx, map[string]interface{}{
    "question": "How does dspy-go handle tool management?",
})

Program Optimization

Key feature: Programs can be optimized automatically:

// Create MIPRO optimizer
optimizer := optimizers.NewMIPRO(
    metricFunc,
    optimizers.WithNumTrials(10),
)

// Optimize the entire program
optimizedProgram, err := optimizer.Compile(ctx, ragProgram, dataset, nil)

// optimizedProgram now has improved prompts and parameters

Putting It All Together

Here’s a complete example showing Signatures, Modules, and Programs working together:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/XiaoConstantine/dspy-go/pkg/core"
    "github.com/XiaoConstantine/dspy-go/pkg/llms"
    "github.com/XiaoConstantine/dspy-go/pkg/modules"
)

func main() {
    // 1. Configure LLM
    llm, err := llms.NewGeminiLLM("", core.ModelGoogleGeminiPro)
    if err != nil {
        log.Fatal(err)
    }
    core.SetDefaultLLM(llm)

    // 2. Define Signature
    signature := core.NewSignature(
        []core.InputField{
            {Field: core.NewField("text",
                core.WithDescription("The text to analyze"))},
        },
        []core.OutputField{
            {Field: core.NewField("summary",
                core.WithDescription("A concise summary"))},
            {Field: core.NewField("sentiment",
                core.WithDescription("Overall sentiment: Positive/Negative/Neutral"))},
            {Field: core.NewField("key_points",
                core.WithDescription("Main takeaways as bullet points"))},
        },
    ).WithInstruction("Analyze the provided text thoroughly and extract key insights.")

    // 3. Create Modules
    analyzer := modules.NewChainOfThought(signature)

    // 4. Create Program
    program := core.NewProgram(
        map[string]core.Module{"analyzer": analyzer},
        func(ctx context.Context, inputs map[string]interface{}) (map[string]interface{}, error) {
            return analyzer.Process(ctx, inputs)
        },
    )

    // 5. Execute
    result, err := program.Execute(context.Background(), map[string]interface{}{
        "text": "dspy-go provides a systematic approach to building LLM applications...",
    })
    if err != nil {
        log.Fatal(err)
    }

    // 6. Use Results
    fmt.Printf("Summary: %s\n", result["summary"])
    fmt.Printf("Sentiment: %s\n", result["sentiment"])
    fmt.Printf("Key Points: %s\n", result["key_points"])
}

Best Practices

Signature Design

DO:

  • Write detailed, specific field descriptions
  • Include examples in descriptions when helpful
  • Use meaningful field names
  • Specify expected output formats

DON’T:

  • Leave descriptions empty
  • Use vague instructions
  • Change signatures frequently (breaks optimization)
  • Over-complicate with too many fields

Module Selection

Use CaseRecommended Module
Simple Q&APredict
Math/LogicChainOfThought
Tool UseReAct
Quality-CriticalRefine
Batch ProcessingParallel
Complex DecisionsMultiChainComparison

Program Structure

DO:

  • Keep workflows simple and linear when possible
  • Handle errors at each step
  • Log intermediate results for debugging
  • Use context for cancellation and timeouts

DON’T:

  • Create circular dependencies
  • Ignore error handling
  • Make programs too deeply nested
  • Forget to pass context through

What’s Next?

Now that you understand the core concepts, explore:

Deep Dives