Modules¶
A signature says what a task is. A module decides how a language model answers it. You wrap a signature in a module to get a runnable program.
dspy4s ships a few modules. They all share the same shape: construct one from a signature, then call it with its input type.
Predict¶
Predict answers the signature directly, in a single model call. You have
already used it in the Quickstart and on the
Signatures page:
It is the module to reach for when the task is a direct mapping from inputs to outputs.
ChainOfThought¶
ChainOfThought asks the model to reason before it answers. It adds a
reasoning: String field to the front of the output, so you get both the
explanation and the answer with direct dot access:
object QaReasoningExample:
val classify = ChainOfThought(Signature.fromString("question -> answer"))
/** Returns the reasoning and the answer. `ChainOfThought` prepends the `reasoning` output. */
def call(question: String)(using RuntimeContext): Either[DspyError, (String, String)] =
classify((question = question)).map(p => (p.output.reasoning, p.output.answer))
The signature did not change. Swapping Predict for ChainOfThought is the
only edit, and the extra reasoning field appears on the output.
Some models give their reasoning apart from their answer. To use that reasoning,
set nativeReasoning = true, and declare the capability on the model with
capabilities = Some(LmCapabilities.openAi.copy(reasoning = true)). The prompt then does
not ask for the field, the request has reasoning_effort, and the field takes
the reasoning of the reply. On a model with no such capability, the model writes
the field as text.
A signature can also have its own output of the type Reasoning, which is a
String with the same rule.
Verified snippet
This example is extracted from
Modules.scala.
ReAct¶
ReAct answers by calling tools in a loop: the model thinks, picks a tool, sees
the result, and repeats until it has an answer. It has its own page,
Tools & ReAct.
Refining outputs¶
Two modules wrap another module to improve its output against a reward function:
BestOfNsamples several completions, one after the other, and keeps the best one.Refinedoes the same sequentially, feeding each attempt's score back in.
Both take a reward function (input, prediction) => Double:
def bestOfN(question: String)(using RuntimeContext): Either[DspyError, String] =
val qa = ChainOfThought(Signature.fromString("question -> answer"))
BestOfN(
module = qa,
n = AttemptCount(3),
rewardFn = (_, pred) => if pred.output.answer.length == 1 then 1.0 else 0.0,
threshold = 1.0
)((question = question)).map(_.output.answer)
Other modules¶
RLM answers by writing Python code and running it in a sandbox. The inputs
are variables in that sandbox, so the model can explore a context that is too
large for one prompt. The sandbox runs on Deno. See
ReAct vs RLM.
Parallel runs several program calls concurrently.
Choosing a module¶
| Module | Strategy | Use when |
|---|---|---|
Predict |
One call, direct answer. | The task maps inputs to outputs. |
ChainOfThought |
Reason, then answer. | The task benefits from step-by-step thinking. |
ReAct |
Call tools in a loop. | The model needs external information or actions. |
RLM |
Write and run code in a sandbox. | The answer needs real computation, or the context is too large for one prompt. |
BestOfN / Refine |
Sample and rank against a reward. | You can score outputs and want the best. |
Parallel |
Run calls concurrently. | You have many independent calls. |
Modules are ordinary Scala values, so you can also compose them into larger programs. That is the next step.
Next: Tools & ReAct.