For a short summarization task, does an added reasoning field improve the result enough to justify its latency and token cost? This experiment compares the programs under the same examples and judge.
The experiment uses one signature, two DSPy.rb modules, five Wikipedia excerpts, and the same LLM judge for every output.
flowchart LR
subgraph Examples
E1["Wikipedia<br/>Articles"]
end
subgraph Predictors["Same Signature, Different Modules"]
P["DSPy::Predict<br/>(direct)"]
C["DSPy::ChainOfThought<br/>(reasoning first)"]
end
subgraph Judge["LLM Judge (gpt-4.1)"]
J["EvaluateSummary<br/>faithfulness | relevance<br/>coherence | fluency"]
end
subgraph Results
R["Compare<br/>Scores"]
end
E1 --> P --> J
E1 --> C --> J
J --> R
style P fill:#e8f5e9,stroke:#81c784
style C fill:#e3f2fd,stroke:#64b5f6
style J fill:#fff3e0,stroke:#ffb74d
One Task, Two Modules
The signature keeps the task fixed while the module changes how DSPy executes it.
class Summarize < DSPy::Signature
description "Summarize the given text concisely while preserving key concepts"
input do
const :text, String, description: "Text to summarize"
end
output do
const :summary, String,
description: "Concise summary preserving key concepts (2-3 sentences)"
end
end
DSPy.configure do |config|
config.lm = DSPy::LM.new(
"openai/gpt-4o-mini",
api_key: ENV["OPENAI_API_KEY"]
)
end
direct = DSPy::Predict.new(Summarize)
reasoned = DSPy::ChainOfThought.new(Summarize)
direct_result = direct.call(text: source_text)
reasoned_result = reasoned.call(text: source_text)
This is a module comparison, not a prompt-string comparison. DSPy constructs the provider requests from the same signature; ChainOfThought adds its reasoning strategy.
Define Acceptable Summary Behavior
There is no reference summary in this experiment. Another model scores each output for faithfulness, relevance, coherence, and fluency. The types make the judge’s inputs and outputs explicit:
class EvaluatorMindset < T::Enum
enums do
Critical = new("critical")
Balanced = new("balanced")
Generous = new("generous")
end
end
class GroundedSummary < T::Struct
const :source_text, String
const :summary, String
end
class EvaluateSummary < DSPy::Signature
description "Evaluate summary quality using G-Eval criteria according to the specified mindset."
input do
const :grounded_summary, GroundedSummary
const :mindset, EvaluatorMindset
end
output do
const :faithfulness, Integer,
description: "Score 1-5: Is the summary factually accurate?"
const :relevance, Integer,
description: "Score 1-5: Does it capture the most important information?"
const :coherence, Integer,
description: "Score 1-5: Is it well-structured with logical flow?"
const :fluency, Integer,
description: "Score 1-5: Is it grammatically correct and readable?"
const :overall_score, Float,
description: "Overall quality score from 1.0 to 5.0"
end
end
The judge turns our definition of acceptable behavior into a metric result. It returns both a threshold decision and a normalized score:
def create_llm_judge_metric(judge_lm, mindset: EvaluatorMindset::Critical)
judge = DSPy::ChainOfThought.new(EvaluateSummary)
judge.configure { |config| config.lm = judge_lm }
lambda do |example, prediction|
evaluation = judge.call(
grounded_summary: GroundedSummary.new(
source_text: example.input_values[:text],
summary: prediction.summary
),
mindset: mindset
)
{
passed: evaluation.overall_score >= 3.5,
score: evaluation.overall_score / 5.0,
faithfulness: evaluation.faithfulness,
relevance: evaluation.relevance,
coherence: evaluation.coherence,
fluency: evaluation.fluency
}
rescue StandardError => e
{ passed: false, score: 0.0, error: e.message }
end
end
The 3.5 threshold and the judge’s rubric are part of the experiment. They are not objective properties of summarization. A deployment decision would need judge calibration against human ratings and repeated runs to measure variance.
Run Both Programs Against The Same Evidence
examples = wikipedia_articles.map do |document|
DSPy::Example.new(
signature_class: Summarize,
input: { text: document[:text] },
expected: { summary: "" }
)
end
judge_lm = DSPy::LM.new(
"openai/gpt-4.1",
api_key: ENV["OPENAI_API_KEY"]
)
metric = create_llm_judge_metric(judge_lm)
direct_result = DSPy::Evals.new(
DSPy::Predict.new(Summarize),
metric: metric
).evaluate(examples)
reasoned_result = DSPy::Evals.new(
DSPy::ChainOfThought.new(Summarize),
metric: metric
).evaluate(examples)
The placeholder expected summary is unused by this metric. It is present because DSPy::Example records the task’s expected output shape.
What This Run Found
We ran both modules on five Wikipedia articles: Photosynthesis, Byzantine Empire, Machine Learning, Great Barrier Reef, and the French Revolution. gpt-4o-mini generated the summaries; gpt-4.1 judged them.
Predict avg score: 93.0%
ChainOfThought avg score: 96.0%
Improvement: +3.0 percentage points
| Dimension | Predict | ChainOfThought | Difference |
|---|---|---|---|
| Faithfulness | 4.4/5 | 4.8/5 | +0.4 |
| Relevance | 4.4/5 | 4.4/5 | 0.0 |
| Coherence | 4.8/5 | 5.0/5 | +0.2 |
| Fluency | 5.0/5 | 5.0/5 | 0.0 |
In this run, ChainOfThought scored three percentage points higher, with the difference concentrated in faithfulness and coherence. Five documents and one judge run are not enough to establish a general advantage. The result is evidence for a follow-up experiment, not proof that ChainOfThought improves summaries.
The next useful run would add more documents, repeat judge calls, inspect disagreements manually, and record latency and token cost. A three-point score difference may disappear under that variance or cost more than it is worth.
Run It Yourself
export OPENAI_API_KEY=your-key
bundle exec ruby examples/summarization_comparison.rb
Use DSPY_SUMMARIZER_MODEL and DSPY_JUDGE_MODEL to compare other model combinations. Keep the examples and judge fixed when comparing modules, then change one experimental variable at a time.