← Back to Building CNS 2.0: A Developer's Guide

Chapter 7: Advanced Optimization with DSPy

Evolving CNS 2.0 from prompt engineering to programmatic optimization using DSPy

From Brittle Prompting to Robust Programming

Throughout this guide, we’ve often assumed a developer would write fixed, static prompts to instruct the LLMs in our system. This “prompt engineering” is the standard way of working with LLMs, but it has critical weaknesses: a prompt that works well on one model (e.g., GPT-4) may fail completely on another (e.g., Llama 3), and optimizing it is a manual, time-consuming, and often unscientific process of trial and error.

To build a truly robust and adaptive system, we must evolve from prompting to programming. This is where DSPy comes in. DSPy is a framework that fundamentally reframes the problem. Instead of hand-crafting prompts, we:

  1. Define the task we want to perform (e.g., “extract claims from a document”).
  2. Define a metric for success (e.g., “how well do the extracted claims match a gold-standard example?”).

The DSPy “compiler” then does the hard work of generating and optimizing the best possible prompts and few-shot examples for our specific model and use case. This transforms the brittle art of prompt engineering into a systematic, programmatic optimization process.

Solving a “Major Research Challenge”: Narrative Ingestion

The CNS 2.0 research proposal is candid about the difficulty of the first step in the workflow: converting unstructured text into a well-formed SNO. In Section 3.1, it states:

“A critical prerequisite for the CNS ecosystem is the ability to generate SNOs from unstructured source materials (e.g., academic papers, intelligence reports). This process, a form of advanced argumentation mining, is a major research challenge in itself.”

Manually engineering a fixed prompt to reliably extract a central hypothesis, multiple sub-claims, and their logical relationships from diverse documents is exactly the kind of brittle, complex task where traditional prompt engineering fails and DSPy excels. Instead of guessing the right prompt, we can use DSPy to find it programmatically.

Defining the Ingestion Task with DSPy

First, we define the input (document_text) and the desired structured output (central_hypothesis, claims) using a DSPy Signature. This is an abstract definition of the task, independent of any specific prompt.

# Assume dspy is installed and configured, and Pydantic models are defined
import dspy
from typing import List
from pydantic import BaseModel, Field

class ExtractedClaim(BaseModel):
    """Pydantic model for a single extracted claim."""
    claim_text: str = Field(description="The text of the claim.")
    relationship_to_hypothesis: str = Field(description="How this claim relates to the central hypothesis (e.g., 'supports', 'refutes').")

class DocumentToSNO(dspy.Signature):
    """Extracts the central hypothesis and a structured list of claims from a document."""
    document_text: str = dspy.InputField(desc="The full text of the source document.")
    central_hypothesis: str = dspy.OutputField(desc="A single, concise sentence summarizing the main argument.")
    claims: List[ExtractedClaim] = dspy.OutputField(desc="A structured list of key claims and their relationship to the hypothesis.")

Next, we define a metric function that scores how well an LLM’s prediction matches a hand-labeled example. By providing partial credit (a graded metric), we give the optimizer a much richer signal to learn from.

def graded_sno_structure_metric(example, pred, trace=None) -> float:
    """
    A graded metric that gives partial credit for correctly extracting parts of the SNO.
    This provides a much better learning signal to the DSPy optimizer than a simple 0/1 score.
    """
    score = 0.0
    # Award marks for correctly identifying the hypothesis
    if example.central_hypothesis.lower() in pred.central_hypothesis.lower():
        score += 0.5

    # Award marks for each correctly identified claim
    # (In a real scenario, this would involve more sophisticated semantic matching)
    pred_claims_text = {c.claim_text for c in pred.claims}
    for gold_claim in example.claims:
        if gold_claim.claim_text in pred_claims_text:
            score += 0.5 / len(example.claims)

    return score

With a few labeled examples of documents and their ideal SNO structures, we can use a DSPy optimizer (like BootstrapFewShot) to “compile” a module that contains the best possible prompt for the ingestion task. This turns a “major research challenge” into a solvable optimization problem.

The Ultimate Goal: A Self-Optimizing Synthesis Engine

The true power of combining CNS 2.0 and DSPy is realized when we turn the system’s critical judgment upon itself. We can use our own Critic Pipeline as the metric to optimize the Synthesis Engine. This creates a powerful feedback loop where the system learns to generate syntheses that it itself considers to be high-quality.

The diagram below illustrates this self-optimizing loop. The goal is to “compile” a SynthesisModule that is optimized to produce SNOs that score highly on our CriticPipeline metric.

A diagram showing the self-optimizing loop where the DSPy Optimizer compiles a Synthesis Module, which generates a candidate SNO that is then scored by our own CNS Critic Pipeline, with the score being fed back to the optimizer.

How the Self-Optimizing Loop Works

This process allows the system to programmatically discover what makes a “good” synthesis from its own perspective. The core idea is to use our CriticPipeline—the embodiment of the system’s values—as the objective function for the DSPy optimizer. This creates a powerful feedback loop where the system learns to generate syntheses that it itself considers to be high-quality, effectively teaching its generative components to align with its evaluative components. Here is a step-by-step breakdown:

  1. Define the Task: We define a ChiralPairToSynthesis signature that tells the LLM its goal: take two conflicting narratives and output a new, higher-order hypothesis.
  2. Prompt Generation: The DSPy Optimizer (BootstrapFewShot) creates a candidate prompt and few-shot examples for the SynthesisModule.
  3. Candidate Generation: The SynthesisModule uses this prompt to call an LLM, which generates a synthesized_hypothesis (a string).
  4. Instantiation: Our custom metric function, critic_pipeline_metric, takes this raw string and instantiates a full StructuredNarrativeObject from it. This is where the abstract output of the LLM becomes a concrete, evaluable part of our CNS ecosystem.
  5. Self-Evaluation: The candidate SNO is passed through our complete, multi-component CriticPipeline from Chapter 3. The pipeline calculates a final, holistic trust_score.
  6. Feedback: This trust_score is returned to the DSPy Optimizer. The optimizer uses this score to judge how “good” its generated prompt was.
  7. Iteration: The optimizer repeats this process, learning to generate prompts that produce SNOs that our own system rates highly.

The CriticPipeline as a Metric

The bridge between DSPy’s optimization and our system’s judgment is the critic_pipeline_metric function. It wraps our entire evaluation workflow into a single function that DSPy can use to score its attempts.

def critic_pipeline_metric(cns_workflow_manager, example, pred, trace=None) -> float:
    """
    Uses the entire CNS critic pipeline to evaluate the quality of a synthesized hypothesis.
    This function is the bridge between DSPy's optimization and our system's own judgment.
    """
    try:
        # Step 1: Extract the predicted hypothesis from the DSPy prediction object.
        synthesized_hypothesis = pred.synthesized_hypothesis

        # Step 2: Perform basic validation. An invalid or trivial output gets the worst score.
        if not isinstance(synthesized_hypothesis, str) or len(synthesized_hypothesis) < 20:
            return 0.0

        # Step 3: Instantiate a candidate SNO from the LLM's generated hypothesis.
        # This turns the raw text output into a rich, structured object.
        candidate_sno = StructuredNarrativeObject(central_hypothesis=synthesized_hypothesis)
        candidate_sno.compute_hypothesis_embedding(cns_workflow_manager.embedding_model)

        # Step 4: Prepare the context for evaluation. The Novelty Critic needs to see
        # the existing SNO population to do its job.
        context = {'sno_population': cns_workflow_manager.sno_population}

        # Step 5: THE CORE OF THE LOOP. Run the candidate SNO through our complete,
        # multi-component critic pipeline from Chapter 3.
        evaluation_result = cns_workflow_manager.critic_pipeline.evaluate_sno(candidate_sno, context)

        # Step 6: The final, holistic trust_score produced by our pipeline is the metric.
        # DSPy's optimizer will now tune the synthesizer's prompts to maximize this score.
        trust_score = evaluation_result.get('trust_score', 0.0)
        return trust_score

    except Exception as e:
        # Penalize any prompt that produces an output that breaks our system.
        logger.error(f"Critic pipeline metric failed: {e}")
        return 0.0

Ethical Consideration: The Power and Peril of Metrics

The self-optimizing loop is powerful, but it contains a critical ethical risk. The optimizer will relentlessly maximize the score from the critic_pipeline_metric, and the old adage "you get what you measure" applies with force.

If our metric is flawed, the system could learn to produce undesirable outputs. For example, if our training data contains biased narratives and our metric only rewards "coherence" and "novelty," the DSPy optimizer could learn to generate highly coherent and novel but deeply biased syntheses. It would be optimizing for a plausible-sounding output, not a fair or accurate one.

This highlights the immense responsibility placed on the developer to design metrics that explicitly account for fairness. A metric that is blind to bias will create a system that is blind to injustice.

Defining and measuring fairness is a complex challenge. For a detailed analysis, see the research project on Bias, Fairness, and Accountability.

Compiling the Self-Optimizing Synthesizer

With the signature, module, and metric defined, we can now “compile” our SynthesisModule. The optimizer will learn to generate hypotheses that are well-grounded, logical, and novel according to the system’s own internal criteria.

# ... (Code for defining the SynthesisModule and training examples remains the same) ...

# This is the compilation step. DSPy runs a series of experiments. Over many
# iterations, it finds the prompt that maximizes the trust score, effectively
# teaching the synthesizer what our own critic pipeline values.
optimized_synthesis_module = optimizer.compile(SynthesisModule(), trainset=synthesis_train_examples)

Conclusion: From Blueprint to a Dynamic System

This guide has walked through the entire process of translating the CNS 2.0 research proposal from a theoretical blueprint into a practical, working system. We have built each component step-by-step, shown how to assemble them into an autonomous system, and laid out the path to a robust, scalable production deployment.

Finally, by integrating DSPy, we have shown a path from a static system to a dynamic one—a system that can programmatically optimize and improve its own reasoning capabilities. This closing of the loop, where the system’s own judgment is used to refine its generative components, represents a key step toward the goal of automated, robust, and continuously improving knowledge discovery.