Class: Insika::Evals::Pairwise
- Inherits:
-
Object
- Object
- Insika::Evals::Pairwise
- Defined in:
- lib/insika/evals/pairwise.rb
Overview
PAIRWISE AGAINST THE INCUMBENT — the number that answers "can we replace it". An absolute 0.72 says a reply cleared a bar we invented; it says nothing about whether the system already answering 403,231 chats would have done better with the same customer.
Same opening, two transcripts, one question: which one served the customer
better? Three outcomes (better / comparable / worse), plus two the panel can
produce and must not hide — split when the judges disagree and unknown when
none of them answered in a readable way.
Three honesty rules, each of them the difference between a number people should trust and one they should not:
- Anonymous. The judge sees "A" and "B" and is never told which one is Insika. Told, it would have an opinion about the new system rather than about the conversations.
- Both orders, every judge. Position bias is THE known failure of pairwise
LLM judging, and it is the same species as the kill criterion ("
bettertracks length, or politeness"). So each judge is asked twice with the transcripts swapped, and a verdict that FLIPS with presentation order is reported ascomparablewithorder_dependent: true— a preference that depends on which one was printed first is not a preference. - A split panel stays split. Averaging "better" and "worse" into "comparable" invents agreement that nobody expressed.
Cost: 2 provider calls per judge per case. This is why it never runs as part of the gate and is opt-in on the CLI.
Defined Under Namespace
Classes: Verdict
Constant Summary collapse
- BETTER =
"better"- COMPARABLE =
"comparable"- WORSE =
"worse"- SPLIT =
"split"- UNKNOWN =
"unknown"
Class Method Summary collapse
-
.reference_transcript(messages) ⇒ Object
The incumbent's half.
-
.transcript(user_turns, turns) ⇒ Object
The replayed conversation as the judge reads it: the user turns we sent, interleaved with the answers the deployment published.
Instance Method Summary collapse
-
#compare(golden:, turns:) ⇒ Object
golden + the run's [TurnResult] -> Verdict, or nil when the case carries no reference (most of them: a pair is curated by a human, like the case itself).
-
#initialize(asks:) ⇒ Pairwise
constructor
asks: [->(prompt) { "
" }] — the SAME panel the rubric judge uses (JudgePanel builds both), because "the judges the operator configured" is one decision, not two.
Constructor Details
#initialize(asks:) ⇒ Pairwise
asks: [->(prompt) { "
53 54 55 56 |
# File 'lib/insika/evals/pairwise.rb', line 53 def initialize(asks:) @asks = Array(asks).compact raise ArgumentError, "a pairwise comparison needs at least one `ask`" if @asks.empty? end |
Class Method Details
.reference_transcript(messages) ⇒ Object
The incumbent's half. Human turns are NOT flagged to the judge: what it grades
is the conversation as the customer received it, and telling it "a person wrote
this one" is an invitation to grade the author instead. The fact is carried to
the READER as vs: human-assisted instead, which is where it changes a decision.
84 85 86 87 88 89 |
# File 'lib/insika/evals/pairwise.rb', line 84 def self.reference_transcript() Array().map do |m| speaker = m["role"].to_s == "user" ? "customer" : "assistant" "#{speaker}: #{m['text'].to_s.strip}" end.join("\n") end |
.transcript(user_turns, turns) ⇒ Object
The replayed conversation as the judge reads it: the user turns we sent,
interleaved with the answers the deployment published. turns may be shorter
than user_turns (an errored turn aborts the replay) — zip on what ran.
74 75 76 77 78 |
# File 'lib/insika/evals/pairwise.rb', line 74 def self.transcript(user_turns, turns) Array(turns).each_with_index.map do |t, i| ["customer: #{user_turns[i]}", "assistant: #{t.output_text.to_s.strip}"] end.flatten.join("\n") end |
Instance Method Details
#compare(golden:, turns:) ⇒ Object
golden + the run's [TurnResult] -> Verdict, or nil when the case carries no reference (most of them: a pair is curated by a human, like the case itself).
60 61 62 63 64 65 66 67 68 69 |
# File 'lib/insika/evals/pairwise.rb', line 60 def compare(golden:, turns:) return nil unless golden.reference? ours = Pairwise.transcript(golden.user_turns, turns) theirs = Pairwise.reference_transcript(golden.) return nil if ours.strip.empty? panel = @asks.map { |ask| judge_once(ask, ours, theirs) } combine(panel, vs: golden.human_assisted? ? "human-assisted" : "agent") end |