Skip to content

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, the failure limit 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 up to n completions from the inner program, one after the other, 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 sets the number of failures that the search can take. A failure is an error of the inner program or an exception of the reward function. A low reward is not a failure: the search goes on with the next attempt, up to n attempts. With failCount = Some(FailureCount(1)), the search takes one failure, and the second failure gives a Left, also when an earlier attempt has a reward. With no failCount, the limit is n, so each attempt runs. When no attempt succeeds, the result is a Left with the last error.

Refine takes the same parameter. It also runs the attempts in sequence, and it gives advice from each attempt to the next attempt.

Judging with a language model

private val judge = ChainOfThought(
  Signature.of[FactualityJudge](instructions = "Determine if a statement is factually accurate.")
)

def call(question: String)(using ctx: RuntimeContext): Either[DspyError, String] =
  // An error of the judge is not a reward of 0. The exception counts as a failure of the attempt.
  def reward(answer: String): Double =
    judge((statement = answer)).fold(
      error => throw RuntimeException(error.message),
      r => if r.output.is_factual then 1.0 else 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 call a language model. Here judge is a ChainOfThought over a FactualityJudge signature that maps a statement to a boolean is_factual. The instructions argument gives the task text that the model gets. The reward runs the judge on the candidate answer and gives 1.0 when the judge says that the answer is factual. The reward signature is (input, prediction) only, so the judge call uses the RuntimeContext of the enclosing call. Refine runs the inner program until the reward reaches the threshold.

An error of the judge is not a reward of 0. The reward throws an exception, and Refine counts it as a failure of the attempt. When the judge fails on each attempt, the result is a Left.

Running it

OPENAI_API_KEY=sk-... sbt "examples/runMain dspy4s.examples.tutorials.output_refinement.bestOfNAndRefineMain"

Notes

Full source: BestOfNAndRefine.scala