fix(ask): judge every question in one request
The state dominates a System One request and each question is scored in isolation against it, so sending context choices separately only re-sent the state per request and added round trips. A live ask carried 22,957 input tokens over two requests for one question; one request carries it once. Drops the now unused AskOptions.MaxParallel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ public enum ContextScope
|
||||
Documents,
|
||||
}
|
||||
|
||||
public sealed record AskOptions(int K = 6, int DocChars = 12_000, int StateChars = 60_000, int MaxParallel = 8);
|
||||
public sealed record AskOptions(int K = 6, int DocChars = 12_000, int StateChars = 60_000);
|
||||
|
||||
public sealed record AskRequest(IReadOnlyList<string> Queries, IReadOnlyDictionary<string, AskQuestion>? Questions, AskOptions Options);
|
||||
|
||||
@@ -52,7 +52,7 @@ public sealed record AskResult(
|
||||
/// <summary>
|
||||
/// wiki_ask.py as a use case: rank every query, unite the top notes into one context (per-document
|
||||
/// and total budget), expand context choices over the paragraph or document ids plus NONE, add the
|
||||
/// covered gate, judge (every context choice in its own request, the rest batched), and attach the
|
||||
/// covered gate, judge every question in one request, and attach the
|
||||
/// answering paragraphs' text to the results. Judgements and quotations, never prose.
|
||||
/// </summary>
|
||||
public sealed class AskUseCase(RankUseCase rank, IDocumentCatalog catalog, IChunker chunker)
|
||||
@@ -69,7 +69,7 @@ public sealed class AskUseCase(RankUseCase rank, IDocumentCatalog catalog, IChun
|
||||
var questions = request.Questions ?? DefaultQuestions(request.Queries);
|
||||
var (expanded, scopes) = Expand(questions, context, request.Queries, judge.MaxChoiceOptions);
|
||||
|
||||
var (answers, usage) = await JudgeInParallelAsync(judge, context, expanded, scopes, request.Options.MaxParallel, ct);
|
||||
var (answers, usage) = await JudgeAsync(judge, context, expanded, ct);
|
||||
var resolved = Resolve(answers, scopes, context);
|
||||
double? covered = answers.TryGetValue(CoveredId, out var c) && c is Judgement.Noul n ? Math.Round(n.Probability, 3) : null;
|
||||
return new AskResult(request.Queries, context, meta, resolved, covered, usage, judge.ModelId, Stopwatch.GetElapsedTime(started));
|
||||
@@ -183,9 +183,12 @@ public sealed class AskUseCase(RankUseCase rank, IDocumentCatalog catalog, IChun
|
||||
return (expanded, scopes);
|
||||
}
|
||||
|
||||
private static async Task<(IReadOnlyDictionary<string, Judgement> Answers, JudgeUsage Usage)> JudgeInParallelAsync(
|
||||
IJudge judge, IReadOnlyList<ContextDocument> context, IReadOnlyDictionary<string, TypedQuestion> questions,
|
||||
IReadOnlyDictionary<string, ContextScope> scopes, int maxParallel, CancellationToken ct)
|
||||
/// <summary>
|
||||
/// One request for every question: the state dominates the request, and System One scores each
|
||||
/// question in isolation against it, so splitting only re-sends the state and adds round trips.
|
||||
/// </summary>
|
||||
private static async Task<(IReadOnlyDictionary<string, Judgement> Answers, JudgeUsage Usage)> JudgeAsync(
|
||||
IJudge judge, IReadOnlyList<ContextDocument> context, IReadOnlyDictionary<string, TypedQuestion> questions, CancellationToken ct)
|
||||
{
|
||||
var state = new
|
||||
{
|
||||
@@ -196,29 +199,8 @@ public sealed class AskUseCase(RankUseCase rank, IDocumentCatalog catalog, IChun
|
||||
paragraphs = d.Paragraphs.Select(p => new { id = p.Id, heading = p.Heading, text = p.Text }),
|
||||
}),
|
||||
};
|
||||
var groups = new List<Dictionary<string, TypedQuestion>>();
|
||||
foreach (var (id, q) in questions.Where(kv => scopes.ContainsKey(kv.Key)))
|
||||
groups.Add(new Dictionary<string, TypedQuestion> { [id] = q });
|
||||
var rest = questions.Where(kv => !scopes.ContainsKey(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
if (rest.Count > 0) groups.Add(rest);
|
||||
|
||||
using var gate = new SemaphoreSlim(Math.Max(1, maxParallel));
|
||||
var tasks = groups.Select(async g =>
|
||||
{
|
||||
await gate.WaitAsync(ct);
|
||||
try { return await judge.JudgeAsync(new JudgeRequest(state, g), ct); }
|
||||
finally { gate.Release(); }
|
||||
}).ToList();
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
var answers = new Dictionary<string, Judgement>();
|
||||
var usage = JudgeUsage.None;
|
||||
foreach (var r in results)
|
||||
{
|
||||
foreach (var (id, a) in r.Answers) answers[id] = a;
|
||||
usage = usage.Add(r.Usage);
|
||||
}
|
||||
return (answers, usage);
|
||||
var result = await judge.JudgeAsync(new JudgeRequest(state, questions), ct);
|
||||
return (result.Answers, result.Usage);
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, AskAnswer> Resolve(
|
||||
|
||||
@@ -81,10 +81,10 @@ public class AskUseCaseTests
|
||||
Assert.Equal("udm-dns#p1", where.Top[0].Id);
|
||||
Assert.Contains("answers every query", where.Top[0].Text);
|
||||
Assert.Equal("UDM intercepts DNS", where.Top[0].Title);
|
||||
// the context choice went alone, the noul questions together
|
||||
Assert.Equal(2, _judge.Requests.Count);
|
||||
Assert.Contains(_judge.Requests, r => r.Questions.Count == 1 && r.Questions.ContainsKey("where_1"));
|
||||
Assert.Equal(2, result.Usage.Requests);
|
||||
// every question rides in the one request that carries the state
|
||||
var sentToJudge = Assert.Single(_judge.Requests);
|
||||
Assert.Equal(3, sentToJudge.Questions.Count);
|
||||
Assert.Equal(1, result.Usage.Requests);
|
||||
Assert.Equal("fake-judge", result.JudgeModel);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user