qa: capacity metrics — prompt-char accounting for strict-rate vs input-size analysis
Per-call accounting of prompt capacity so we can answer 'does
context size affect strict-rate?' at aggregate scale.
Per-call result dict gains:
"prompt_chars": {
"system_prompt": <int>, # system message chars
"grounding_reminder": <int>, # restated rule message (if any)
"user_question": <int>, # the original question text
"evidence_or_context":<int>, # rendered evidence map / wikitext context
"messages_total": <int>, # sum of all messages[].content
}
"answer_chars": <int> # rendered answer length
Char-level for now: model-agnostic, fast, and a workable proxy
for prompt tokens (~4 chars/token English prose, ~2.5 for JSON-
evidence-block heavy contexts). Token-exact accounting can layer
on later if a tokenizer is cheap enough to wire in per-call.
Surfaces in three places:
(1) result["prompt_chars"] — for programmatic callers. Cache hits
return prompt_chars too (computed before lookup).
(2) `aborist query` human render — one-liner showing the breakdown
and the answer length, so an operator can tell at a glance
whether STRICT came from a 5KB tight prompt or a 60KB stuffed
context.
(3) bench/qa_sweep.py — per-row prompt_chars_{total,evidence,system,
question} + answer_chars columns in the JSONL, plus a new
"strict-rate by prompt size" section in the markdown summary
bucketing runs by capacity (<8KB, 8-16KB, 16-32KB, 32-64KB,
>=64KB) so the bench reveals whether input size correlates
with verdict quality across modes.
Never enters cache_key — runtime measurements, not policy.
This commit is contained in:
parent
2d6a86b991
commit
f927298353
3 changed files with 83 additions and 0 deletions
|
|
@ -581,6 +581,19 @@ def _render_query_human(result: dict, question: str) -> str:
|
|||
)
|
||||
lines.append("")
|
||||
|
||||
pc = result.get("prompt_chars") or {}
|
||||
if pc:
|
||||
# Compact one-liner — operator at a glance: did STRICT come
|
||||
# from a tight prompt or a context-stuffed one?
|
||||
lines.append(
|
||||
f"capacity: prompt {pc.get('messages_total', 0):,} chars "
|
||||
f"(sys {pc.get('system_prompt', 0):,} + "
|
||||
f"reminder {pc.get('grounding_reminder', 0):,} + "
|
||||
f"evidence {pc.get('evidence_or_context', 0):,} + "
|
||||
f"question {pc.get('user_question', 0):,}) → "
|
||||
f"answer {result.get('answer_chars', 0):,} chars"
|
||||
)
|
||||
|
||||
cache_key = (result.get("cache_key") or "")[:8]
|
||||
lines.append(f"cache_key: {cache_key}… <run with --json for full record>")
|
||||
return "\n".join(lines)
|
||||
|
|
|
|||
|
|
@ -1435,6 +1435,26 @@ def query(
|
|||
messages.append({"role": "user", "content": grounding_reminder})
|
||||
messages.append({"role": "user", "content": _user_payload(question)})
|
||||
|
||||
# Capacity metrics. Char-level for now — a fast model-agnostic proxy
|
||||
# for prompt size (rule of thumb: ~4 chars/token for English prose,
|
||||
# ~2.5 for JSON-evidence-block heavy contexts). Surfaced in the
|
||||
# result dict so the bench can correlate strict-rate with input
|
||||
# size and the operator can tell at a glance whether a STRICT
|
||||
# verdict came from a tight 5KB prompt or a 50KB context-stuffed
|
||||
# one. Never enters cache_key — these are runtime measurements,
|
||||
# not policy.
|
||||
if answer_mode in ("claim_lattice_pointer", "claim_lattice"):
|
||||
evidence_or_context_chars = len(rendered_evidence)
|
||||
else:
|
||||
evidence_or_context_chars = len(context)
|
||||
prompt_chars = {
|
||||
"system_prompt": len(sys_prompt or ""),
|
||||
"grounding_reminder": len(grounding_reminder or ""),
|
||||
"user_question": len(question or ""),
|
||||
"evidence_or_context": evidence_or_context_chars,
|
||||
"messages_total": sum(len(m["content"]) for m in messages),
|
||||
}
|
||||
|
||||
context_root = _context_root([h.document_root for h in chosen])
|
||||
mhash = model_profile_hash(model_id, revision, quantization)
|
||||
|
||||
|
|
@ -1580,6 +1600,8 @@ def query(
|
|||
# writes populate it correctly. Acceptable degradation
|
||||
# since governance_policy_hash invalidated prior records.
|
||||
"partially_verified_quotes": [],
|
||||
"prompt_chars": prompt_chars,
|
||||
"answer_chars": len(cached["answer_text"] or ""),
|
||||
"timings": {
|
||||
"search_ms": search_ms,
|
||||
"context_ms": context_ms,
|
||||
|
|
@ -1943,6 +1965,8 @@ def query(
|
|||
# never threaded into run_dag_root.
|
||||
"pointer_id_distribution": verdict.get("pointer_id_distribution"),
|
||||
"lazy_anchor_ratio": verdict.get("lazy_anchor_ratio"),
|
||||
"prompt_chars": prompt_chars,
|
||||
"answer_chars": len(answer_text or ""),
|
||||
"timings": {
|
||||
"search_ms": search_ms,
|
||||
"context_ms": context_ms,
|
||||
|
|
|
|||
|
|
@ -128,6 +128,15 @@ def _run_one(
|
|||
"deflection_kind": deflection["kind"],
|
||||
"subject_anchor": deflection["subject_anchor"],
|
||||
"subject_in_answer": deflection["subject_in_answer"],
|
||||
# Capacity metrics — char-level proxy for prompt-token budget.
|
||||
# Surfaces "did STRICT come from a tight 5KB prompt or a 50KB
|
||||
# context-stuffed one?" at aggregate scale. Lets the bench
|
||||
# bucket strict-rate by input-size band.
|
||||
"prompt_chars_total": (result.get("prompt_chars") or {}).get("messages_total", 0),
|
||||
"prompt_chars_evidence": (result.get("prompt_chars") or {}).get("evidence_or_context", 0),
|
||||
"prompt_chars_system": (result.get("prompt_chars") or {}).get("system_prompt", 0),
|
||||
"prompt_chars_question": (result.get("prompt_chars") or {}).get("user_question", 0),
|
||||
"answer_chars": result.get("answer_chars", 0),
|
||||
"error": err,
|
||||
}
|
||||
|
||||
|
|
@ -227,6 +236,43 @@ def _render_markdown(
|
|||
f"{deflections}/{b['n']} |"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("## strict-rate by prompt size")
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"Buckets capacity in `messages_total` chars (system + reminder + "
|
||||
"evidence/context + question). ~4 chars/token English prose; "
|
||||
"~2.5 chars/token JSON-evidence-heavy. Tells you whether the "
|
||||
"model strict-rate degrades with input size."
|
||||
)
|
||||
lines.append("")
|
||||
buckets = [
|
||||
("<8KB", 0, 8000),
|
||||
("8-16KB", 8000, 16000),
|
||||
("16-32KB", 16000, 32000),
|
||||
("32-64KB", 32000, 64000),
|
||||
(">=64KB", 64000, 10**9),
|
||||
]
|
||||
lines.append("| mode | bucket | runs | STRICT | strict-rate | mean evidence chars | mean answer chars |")
|
||||
lines.append("|------|--------|------|--------|-------------|---------------------|-------------------|")
|
||||
for mode in modes:
|
||||
for label, lo, hi in buckets:
|
||||
sub = [
|
||||
r for r in rows
|
||||
if r["answer_mode"] == mode
|
||||
and lo <= (r.get("prompt_chars_total") or 0) < hi
|
||||
and not r["error"] and r["status"] != "error"
|
||||
]
|
||||
if not sub:
|
||||
continue
|
||||
n_b = len(sub)
|
||||
n_strict = sum(1 for r in sub if r["audit_mode"] == "STRICT")
|
||||
mean_ev = sum((r.get("prompt_chars_evidence") or 0) for r in sub) / n_b
|
||||
mean_ans = sum((r.get("answer_chars") or 0) for r in sub) / n_b
|
||||
lines.append(
|
||||
f"| {mode} | {label} | {n_b} | {n_strict} | "
|
||||
f"{n_strict / n_b:.2f} | {int(mean_ev)} | {int(mean_ans)} |"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("## per question")
|
||||
lines.append("")
|
||||
lines.append(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue