Output Refinement with BestOfN and Refine¶
This example wraps a ChainOfThought program in BestOfN and Refine to score its
outputs against a reward function and keep the best result. It covers reward
functions over predictions, early stopping with failCount, and a reward that
calls a language model to judge the output.
For a short introduction to these two modules, see Modules.
Scoring with a reward function¶
val bestOf3 = BestOfN(
module = qa,
n = AttemptCount(3),
rewardFn = (_, pred) => oneWord(pred.output.answer),
threshold = 1.0
)
def call(question: String)(using RuntimeContext): Either[DspyError, String] =
bestOf3((question = question)).map(_.output.answer)
BestOfN samples n completions from the inner program in parallel and returns the
one with the highest reward. The reward is (input, prediction) => Double. Because the
inner program is a ChainOfThought("question -> answer"), its prediction carries the
augmented output (reasoning, answer), so the reward reads pred.output.answer. The
threshold lets sampling stop early: once a completion scores at or above it, that
completion is returned without drawing the rest. Here oneWord returns 1.0 only when
the answer is a single word.
Stopping early on failures¶
val bestOf3 = BestOfN(
module = qa,
n = AttemptCount(3),
rewardFn = (_, pred) => oneWord(pred.output.answer),
threshold = 1.0,
failCount = Some(FailureCount(1))
)
failCount caps how many completions may score below the threshold before sampling
gives up and returns the best result seen so far. With failCount = Some(1), a single
sub-threshold completion ends the search. This bounds the number of model calls when the
program is unlikely to reach the threshold. Refine accepts the same parameter and
refines sequentially, feeding each attempt's score into the next try.
Judging with a language model¶
private val judge = ChainOfThought(Signature.of[FactualityJudge])
def call(question: String)(using ctx: RuntimeContext): Either[DspyError, String] =
def reward(answer: String): Double =
judge((statement = answer)).map(r => if r.output.is_factual then 1.0 else 0.0).getOrElse(0.0)
val refinedQa = Refine(
module = qa,
n = AttemptCount(3),
rewardFn = (_, pred) => reward(pred.output.answer),
threshold = 1.0
)
refinedQa((question = question)).map(_.output.answer)
A reward function can itself call a language model. Here judge is a ChainOfThought
over a FactualityJudge signature that maps a statement to a boolean is_factual. The
reward runs the judge on the candidate answer and returns 1.0 when it is judged
factual. The reward signature is (input, prediction) only, with no implicit context
threaded in, so the judge call captures the ambient RuntimeContext from the enclosing
call. Refine then drives the inner program until the judged reward reaches the
threshold.
Running it¶
OPENAI_API_KEY=sk-... sbt "examples/runMain dspy4s.examples.tutorials.output_refinement.bestOfNAndRefineMain"
Notes¶
Full source: BestOfNAndRefine.scala