LLM-as-Judge
LLM-as-Judge
LLM-as-Judge
Use a second language model inside your resources server’s verify() when rewards depend on semantic equivalence, rubrics, or other judgments that are expensive or awkward to encode in deterministic code.
This tutorial is a beginner-first walkthrough. It gives you a minimal path that works first, then shows common production variants.
The walkthrough uses over_refusal_detection as its running example. By the end, you will:
verify() and parse strict verdict labels.verify().verify() can call a judge model to score semantic quality.reward field — the RL training signal.The judge is a verifier dependency — it is not the policy.
During rollout collection, the agent first calls the policy model. When the episode ends, the resources server runs verify(). An LLM judge is not the policy: it is an extra inference call started from inside verify(), after you have the model’s final output (and any verifier metadata from the JSONL line).
Typical in-repo pattern (Gym-internal): verify() uses self.server_client.post(..., url_path="/v1/responses", ...) to call a named model server declared in the same Hydra config. The judge therefore goes through NeMo Gym’s Responses API surface, same as rollouts.
Alternative pattern (external): some servers call an OpenAI-compatible chat.completions client pointed at URLs you supply, such as HPC or a separate cluster. proof_verification routes to external judges when JUDGE_SERVER_ARGS is set, and otherwise uses the internal /v1/responses path.
For how NeMo Gym sits next to GPUs and training frameworks, refer to Deployment Topology.
In production, the judge is typically a dedicated Gym model server — a separate responses_api_models entry in your Hydra config that can point at any OpenAI-compatible endpoint (a co-located vLLM instance, a remote cluster, or a managed API). For this walkthrough, we skip the separate model and reuse the same OpenAI endpoint for both the policy and the judge.
over_refusal_detection trains models to avoid over-refusing safe prompts, such as treating “How do I kill a Linux process?” as dangerous. The judge decides whether the policy model helpfully complied or inappropriately refused.
This walkthrough uses OpenAI gpt-4o-mini as both the policy and judge model — no GPUs required. It has two parts: first you will read through how the config and code work, then you will run it.
If you have not already, configure your OpenAI API key in env.yaml in the repository root:
Since we are reusing the policy model as the judge, no extra endpoint fields are needed.
The resources server config points the judge at the policy model — judge_model_server.name: policy_model. The following example is a simplified view of resources_servers/over_refusal_detection/configs/over_refusal_detection.yaml. Refer to the full file for the complete judge prompt template, including worked examples.
The config file ships with a judge_model block that starts a dedicated judge server. In production, you can use a separate judge by setting judge_model_server.name: judge_model and pointing the judge_base_url / judge_api_key / judge_model_name variables at a different endpoint. This lets you use a different model, provider, or quota for the judge.
Since this walkthrough reuses policy_model as the judge, comment out the judge_model block as shown below — otherwise ng_run will start an unused server that still needs its variables to resolve.
Be sure to set judge_model_server.name to policy_model as well.
Key points:
judge_model_server references a model server by name. Here policy_model means the judge calls go through the same OpenAI endpoint used for rollouts.judge_responses_create_params sets generation parameters for the judge call (temperature: 0.0 for determinism).complied_label / refused_label are specific to over_refusal_detection. Other servers define their own verdict labels. For example, equivalence_llm_judge uses judge_equal_label / judge_not_equal_label. The names and values are up to each server’s design.judge_model_server (which model to call) and judge_responses_create_params (how to call it). Everything else — prompt templates, verdict labels, reward values — is server-specific.Inside over_refusal_detection/app.py, the _evaluate_compliance method fills in the prompt template and posts to the judge. You do not need to write this code to use the server — this is what happens under the hood when verify() runs:
The server looks for the configured verdict labels in the judge’s text. Whichever label appears first wins; if neither appears, the output is treated as ambiguous:
Back in verify(), the boolean maps directly to a configurable reward:
If you are building your own LLM-judge server, you will write similar code — the pattern above (fill template, POST to judge, parse labels, map to reward) is the same across all judge servers in the repo.
Start the servers:
In another terminal, collect rollouts against the 5-entry example dataset to confirm the judge call and reward parsing work end-to-end:
Inspect the output JSONL to verify that reward values are 0.0, 0.5, or 1.0 as expected. Once this looks right, scale to larger datasets and higher num_repeats.
To view the entire output:
Tradeoffs of LLM judges include extra latency and cost, non-determinism unless you tune and constrain generation and parsing, and possible positional bias when the judge favors text in a fixed slot. Some servers mitigate bias with a second pass that swaps gold compared to prediction, such as equivalence_llm_judge.
verify() for scoring.verify()./v1/responses./v1/chat/completions) to another endpoint.Most LLM-judge servers expose fields along these lines (exact names vary by server; check that server’s configs/*.yaml and README.md):
Same server as policy: set name: to the policy model’s key, such as policy_model. Dedicated judge: add a second responses_api_models block in the merged config, such as judge_model, and set judge_model_server.name: judge_model. multichallenge documents this split in its YAML comments.
The over_refusal_detection config shown in the walkthrough above is a complete, working example. Here is a different server — equivalence_llm_judge — that uses a file-based prompt template and different verdict labels ([[A=B]] / [[A!=B]] instead of [[COMPLIED]] / [[REFUSED]]):
Model URLs, API keys, and model IDs for hosted backends belong in your merged Gym config, such as env.yaml and Hydra overrides, consistent with the rest of the project. Do not use ad hoc environment variables except where a specific server documents them, such as external judge routing.
Here is the full flow inside over_refusal_detection, condensed. Every Gym-internal LLM-judge server follows the same shape:
/v1/responses — call the judge model server through server_client.From over_refusal_detection/app.py, the verify() method orchestrates this:
The _request_judge helper handles HTTP errors and JSON parsing gracefully — on failure it returns (None, error_message) instead of raising, so verify() can map that to reward_if_unclear rather than crashing the server.
Other servers apply the same pattern with domain-specific variations. For example, multichallenge runs one judge call per rubric item using asyncio.gather, and equivalence_llm_judge adds an optional swap pass to detect positional bias.
judge_model_server.ng_run and your resources server’s data/example.jsonl, then scale with ng_collect_rollouts.Done looks like:
verify().verify() and verification overview