The fine-tuned model generates responses — but are they any good? Unlike ch6’s accuracy, free-form text has no single right answer to compare against. This unit extracts the model’s test-set responses and scores them the modern way: with another LLM as the judge.
7.7 Extracting and saving responses
First, the landscape. Instruction-tuned LLMs (chatbots) are evaluated via several approaches:
| Approach⇅ | How it works⇅ | Example⇅ | |
|---|---|---|---|
| Short-answer / multiple-choice benchmarks | Test general knowledge with questions that HAVE checkable answers | MMLU | + |
| Human preference comparison | Humans vote between two models' responses to the same prompt | LMSYS Chatbot Arena | + |
| Automated conversational benchmarks | Another LLM scores each response against a reference | AlpacaEval — the book's choice | + |
Before judging, collect the evidence — generate a response for every test entry and file it next to the reference answer:
from tqdm import tqdm
for i, entry in tqdm(enumerate(test_data), total=len(test_data)):
input_text = format_input(entry) #A
token_ids = generate( #B
model=model,
idx=text_to_token_ids(input_text, tokenizer).to(device),
max_new_tokens=256,
context_size=BASE_CONFIG["context_length"],
eos_id=50256
)
generated_text = token_ids_to_text(token_ids, tokenizer).replace(
"<|endoftext|>", "") # remove end-of-text token from output
response_text = generated_text[len(input_text):].replace(
"### Response:", "").strip() #C
test_data[i]["model_response"] = response_text #D
with open("instruction-data-with-response.json", "w") as file:
json.dump(test_data, file, indent=4) - #A The PROMPT half only (no response section) — inference completes what training practiced.
- #B ch5's generate() with eos_id=50256: generation stops at the end-of-text token the collate function deliberately left unmasked. The training curriculum and the stopping rule are the same signal.
- #C generate() returns prompt + continuation; slicing at len(input_text) and stripping the "### Response:" tag isolates the model's actual answer.
- #D Each test entry now carries instruction, input, reference output AND model_response — everything a judge needs, saved to JSON.
7.8 Evaluating with an LLM judge
LLM-as-a-judge llm-as-a-judge Evaluating a model's free-form responses by asking another LLM to score them against reference outputs (e.g. on a 0–100 scale). defined in ch. 7 — open in glossary : for each entry, a prompt presents the judge model with the task, the reference answer, and the model’s answer — and asks for a score:
for entry in test_data[:3]:
prompt = (
f"Given the input `{format_input(entry)}` "
f"and correct output `{entry['output']}`, "
f"score the model response `{entry['model_response']}`"
f" on a scale from 0 to 100, where 100 is the best score. "
)
print("\nDataset response:")
print(">>", entry['output'])
print("\nModel response:")
print(">>", entry["model_response"])
print("\nScore:")
print(">>", query_model(prompt)) #A
# Dataset response:
# >> The car is as fast as lightning.
#
# Model response:
# >> The car is as fast as a bullet.
#
# Score:
# >> I'd rate the model response "The car is as fast as a bullet."
# an 85 out of 100. #B - #A query_model sends the prompt to the judge (the locally-running Llama via Ollama, per the chapter summary) and returns its reply.
- #B A sensible verdict — but wrapped in prose. For automated averaging, the reply must be a bare number…
prompt = (
f"Given the input `{format_input(entry)}` "
f"and correct output `{entry['output']}`, "
f"score the model response `{entry[json_key]}`"
f" on a scale from 0 to 100, where 100 is the best score. "
f"Respond with the integer number only." #A
) - #A One added sentence turns chatty verdicts into machine-readable scores — collect them across the test set and report the average. Constraining output format is half the craft of using LLM judges.
Key idea — evaluation is a generation task too
When outputs are free-form, grading becomes a language task — so a language model does it. The judge sees what string matching can’t: that two different similes are both correct. The result is a scalable, repeatable score for an otherwise fuzzy question, closing the loop the chapter opened: data → batches → fine-tuning → measured instruction-following. The finale wraps up — and points at what lies beyond.📝 Check yourself: evaluating the instruction model
0 / 51.Why is evaluating an instruction-tuned model fundamentally harder than evaluating ch6's classifier?
2.In the extraction loop, why is generate() called with eos_id=50256?
3.After generation, why does the code slice generated_text[len(input_text):]?
4.How does the LLM-as-a-judge method score each test response?
5.Why append "Respond with the integer number only." to the judge prompt?