legalcite-explain-7b-lora
A QLoRA adapter on Qwen/Qwen2.5-7B-Instruct that writes a short,
grounded explanation for a citation-support label. It does not decide
the label β the label comes from legalcite-support-base (the primary
encoder artifact), upstream of this model in the legalcite pipeline.
This adapter's only job is to justify a verdict someone else already
reached, from the same evidence that produced it.
Automated verification only. Not legal advice. Every citation flagged or cleared here must be independently confirmed by a licensed attorney before filing. Jurisdiction: US federal and state case law only.
This notice is printed on every output of the legalcite CLI and cannot
be suppressed by any flag or config value.
Intended use
Secondary, optional component: given (label, proposition, citation, case_name, cited_text), write one or two sentences explaining why the
cited text does or does not support the proposition, consistent with the
given label. Intended to make a legalcite finding easier for a human
reviewer to audit quickly β not to add independent verification weight.
Do not use this model for: deciding a support label (it doesn't have that capability by design β see "How this model was trained" below), legal advice, drafting argument text, or recommending alternative citations.
Important asymmetry to understand before trusting this model's output: if the upstream label is wrong, this model will still write a fluent, confident-sounding explanation justifying that wrong label β it was trained to explain a given verdict, not to independently check one. A well-written explanation is not evidence that the label is correct.
Usage
Not yet wired into the legalcite CLI. legalcite check currently
runs the encoder only β this adapter has to be invoked separately for
now, with a label you've already obtained (from the encoder, or
otherwise). This is an honest gap, not a design choice: the pipeline's
docstring has said "optional decoder explanation" since Stage E was
built, but CitationVerifier never actually calls a decoder. Wiring it
in is open work.
Loading the adapter
Requires peft, transformers, bitsandbytes, torch. The base model
downloads separately from this adapter (~15GB) the first time you run
this.
import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
ADAPTER = "vrushket/legalcite-explain-7b-lora"
BASE_MODEL = "Qwen/Qwen2.5-7B-Instruct"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16
)
tokenizer = AutoTokenizer.from_pretrained(ADAPTER)
base_model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL, quantization_config=bnb_config, device_map="auto"
)
model = PeftModel.from_pretrained(base_model, ADAPTER)
model.eval()
Generating an explanation
The prompt is ChatML: a fixed system message, then a user turn with the
label and evidence fields laid out plainly. Reusing the exact same
example verified in legalcite-support-base's model card (the encoder
classified it SUPPORTED at 0.99 confidence):
SYSTEM_PROMPT = (
"You are a citation-verification assistant. A citation-checking system has already "
"determined the label below. In one or two sentences, explain why the cited text does "
"or does not support the proposition, given that label. Do not decide or second-guess "
"the label. Do not give legal advice, and do not suggest an alternative citation."
)
label = "SUPPORTED"
proposition = (
"The right to privacy, as established in Roe v. Wade, 410 U.S. 113 (1973), "
"is broad enough to encompass a woman's decision whether to terminate her pregnancy."
)
citation_string = "410 U.S. 113"
case_name = "Roe v. Wade"
cited_text = (
"The Court held that the right to privacy, whether it be founded in the Fourteenth "
"Amendment's concept of personal liberty, is broad enough to encompass a woman's "
"decision whether or not to terminate her pregnancy."
)
user_prompt = (
f"Label: {label}\nProposition: {proposition}\n"
f"Citation: {citation_string} ({case_name})\nCited text: {cited_text}"
)
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
]
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
output_ids = model.generate(**inputs, max_new_tokens=150, do_sample=False)
explanation = tokenizer.decode(
output_ids[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True
).strip()
print(explanation)
# The cited text supports this proposition. It states: "The Court held that the right
# to privacy, whether it be founded in the Fourteenth Amendment's concept of personal
# liberty, is broad enough to encompass a..."
Or, with the package installed, reuse the exact tested prompt-building code instead of hand-rolling it:
from legal_citation_check.models.explain import build_chat_messages
messages = build_chat_messages(label, proposition, citation_string, case_name, cited_text)
Note that legal_citation_check.models.explain.load_model_and_tokenizer
is the training-time loader (it applies a fresh LoRA config via
get_peft_model, it does not load saved adapter weights) β for
inference against these published weights, use PeftModel.from_pretrained
as shown above, not that function.
How this model was trained
Base model and adapter
Qwen/Qwen2.5-7B-Instruct, 4-bit NF4 quantization (bfloat16 compute
dtype), LoRA rank 32 / alpha 64 / dropout 0.05, targeting
q_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_proj. Prompt is
ChatML-formatted: a system message instructing the model to explain
(never decide) the label and never give legal advice, a user turn with
the (label, proposition, citation, case_name, cited_text) fields, and
an assistant turn with the target explanation. Loss is computed only on
the assistant turn (DataCollatorForCompletionOnlyLM).
Why the training targets are templated, not human-written
There is no human-written explanation anywhere in this project's
dataset. The fix: examples.jsonl (produced during encoder training
data synthesis) already carries a perturbation field recording why a
given negative example was constructed (wrong_case_name,
fabricated_cite, real_cite_wrong_quote, etc.) β available at
training-target-generation time, but never part of what this model
actually sees as input, at training or inference. Each label/perturbation
combination maps to a literal template ("the cited text does/does not
say X"), never a legal conclusion β Stage D's gate explicitly fails any
explanation that crosses that line. This means the training signal is
grounded and correctly-labeled, but the range of phrasing and reasoning
styles it teaches is bounded by what a modest set of templates can
express β see "Known limitations" below.
Training data size
15,000 training examples / 2,000 eval examples, uniform-randomly subsampled from the ~356K-example labeled set (a random sample from an already label-balanced set preserves the same label proportions). This is a deliberate resource trade-off, not a data-availability limit: a real training run on the full set measured ~53 seconds per optimizer step regardless of how batch size vs. gradient accumulation was split (same total compute either way), projecting to roughly 70 wall-clock hours against a target of single-digit hours. The subsampled run took 3.56 hours for 3 epochs across 3x RTX 6000 Ada (48GB, PCIe, no NVLink).
Real training bugs fixed along the way
Documented here because they shaped the final training config, not just
as trivia: (1) a DDP model-loading race where concurrent from_pretrained
calls across ranks intermittently left one process with an empty model,
fixed with accelerate's main_process_first() barrier; (2) NCCL's
peer-to-peer transport hangs indefinitely on this PCIe-only, no-NVLink
hardware (NCCL_P2P_DISABLE=1 required, not optional); (3) DDP +
gradient checkpointing's default reentrant autograd is incompatible with
a frozen-base/LoRA model (RuntimeError: Expected to mark a variable ready only once), fixed with non-reentrant checkpointing; (4) the
originally-specified max_seq_length=4096 was ~2.5x the real data's
actual max (measured: p50=1010, p90=1235, p99=1487, max=1603 tokens),
causing both a CUDA OOM and much of the excess training time.
Evaluation
Training/eval loss
Eval loss dropped from 0.0063 (epoch 1) to 0.0041 (epoch 3) β consistent with the model learning the templated target patterns well, including on held-out examples (different citations from the training set, same underlying template distribution).
Faithfulness gate (spec: "explanations judged faithful to the
retrieved text on 100 samples; any explanation asserting a legal conclusion beyond 'the cited text does or does not say X' is a failure")
Ran an automated proxy for this gate β not the full human-judgment process the spec describes β on 100 held-out test examples: generated an explanation for each, checked for a fixed list of legal-conclusion-style phrases ("you should", "we recommend", "the law requires", "is unconstitutional", etc.).
Result: 0/100 forbidden-phrase violations. Spot-checked generations
were correctly label-consistent and grounded in the actual retrieved
text β SUPPORTED explanations quoted real evidence, FABRICATED/
wrong_case_name-style UNSUPPORTED explanations correctly named the
mismatch, on examples the adapter had not seen during training.
This is not the same as true human-judged faithfulness. The automated check catches an explicit list of forbidden phrases; it cannot catch a subtler legal-conclusion violation phrased differently, and it says nothing about explanation quality beyond the binary faithfulness question. Genuine human review of the 100-sample gate, as the spec describes, has not yet been done.
Known limitations
- This model cannot verify its own explanations. It is trained to justify whatever label it's given; a wrong upstream label produces a confidently-wrong explanation, not a flagged disagreement.
- Templated training targets bound the range of explanation styles.
The seven templates (one per label/perturbation combination) are
literal and grounded, but not stylistically diverse β real-world
explanations for cases not resembling any of the five perturbation
types (or genuinely novel
SUPPORTED/NOT_VERIFIABLEphrasing patterns) are less certain to generalize well, in the same spirit as the encoder card's documented generalization gap on out-of-distribution proposition phrasing. - Trained on 15K of the ~356K available labeled examples, for tractable training time on this hardware β not because more data wouldn't plausibly help; revisiting this with more compute/time budget or a lighter base model is open future work.
- The faithfulness gate result is an automated proxy, not the human-judged 100-sample review the spec calls for.
- Requires the base model at inference time. This repository is a
LoRA adapter only (
323MB); running it requires15GB) loaded alongside it, typically in 4-bit.Qwen/Qwen2.5-7B-Instruct( - Inherits every limitation of the upstream encoder and existence
check β see
legalcite-support-base's model card, in particular the documented out-of-distribution generalization gap on synthetically-phrased or question-form propositions, and the CourtListener existence-check rate limit (125 requests/day).
Not legal advice
This tool assists a legal professional in checking their own work. It
does not tell anyone what the law is, and does not create an
attorney-client relationship. This model writes an explanation of an
existing label β never a recommendation, never "you should cite X
instead." Every citation flagged or cleared by the legalcite pipeline
must be independently confirmed by a licensed attorney before filing.
License
Apache-2.0 (adapter weights). Base model Qwen/Qwen2.5-7B-Instruct is
licensed separately by Alibaba Cloud β check its own license before
redistribution. Training data: derived from CC0 (Caselaw Access Project)
and CourtListener public data; explanation targets are templated, not
sourced from any third-party text.