Routing tests tell you the agent picked the right topic. They say nothing about whether the answer was correct, complete, on-tone — or invented. To ship, you need to score responses, not just match them.
CarePoint Health, a regional healthcare provider, runs a patient-services agent that answers questions about appointments, billing, insurance coverage and pre-visit preparation. The routing suite from Part 1 is green: the right topics fire, the right actions run.
Then a compliance reviewer reads twenty transcripts and rejects three of them. In one, the agent told a patient their colonoscopy prep "requires fasting from midnight" — the grounded Knowledge article said six hours, and the agent had blended in generic advice. In another, the answer was correct but incomplete: it gave the copay amount without the deductible caveat, which the policy document states in the same paragraph. The third had a tone problem — a patient asking about an overdue bill for a deceased family member got a chirpy upsell for paperless billing.
All three passed routing. All three would have been publishable failures. CarePoint's conclusion: correct routing with a wrong answer is still a wrong answer, and in healthcare it's a reportable one. So they built a response-quality evaluation on top of the routing suite. This post shows how.
For beginners: an LLM's response is free text. You can't assert equality against an expected string, because two perfect answers can be worded completely differently. The standard technique is a rubric — a small set of named quality dimensions with pass criteria — scored either by humans (accurate, slow, expensive) or by another LLM given strict grading instructions, known as LLM-as-judge. Testing Center's own outcome expectation works this way under the hood; here we build an explicit version you control.
CarePoint scores every test response on five dimensions. Each is a yes/no gate, not a 1–10 scale — vague scales produce vague judges. The rubric:
The judge is only as good as its instructions. Three rules that made CarePoint's judge reliable: give it the grounding source (not just the answer), force a per-dimension verdict with a quoted justification, and demand structured JSON so Apex can parse it.
You are a strict quality auditor for a healthcare service agent.
Judge the AGENT RESPONSE only against the GROUNDING SOURCE.
Do not use your own medical or billing knowledge.
USER QUESTION:
{{question}}
GROUNDING SOURCE (the only permitted facts):
{{source}}
AGENT RESPONSE:
{{response}}
Evaluate these dimensions. For each, answer "pass" or "fail"
and quote the exact words that justify your verdict.
1. accuracy: every number, time, duration and policy term in the
response matches the source exactly.
2. completeness: every caveat or condition the source attaches to
the quoted facts is present in the response.
3. groundedness: the response contains no factual claim that is
absent from the source.
4. tone: plain and empathetic; no promotional language.
5. compliance: no diagnosis, no treatment advice, no coverage
promise; required disclaimers present.
Return only JSON:
{"accuracy":{"verdict":"","evidence":""},
"completeness":{"verdict":"","evidence":""},
"groundedness":{"verdict":"","evidence":""},
"tone":{"verdict":"","evidence":""},
"compliance":{"verdict":"","evidence":""},
"overall":"pass|fail"}
The colonoscopy failure is a good trace of why the source must be in the prompt. Without it, a judge model "knows" fasting-from-midnight is common advice and passes the answer. With the source present and the instruction "do not use your own knowledge", it fails groundedness and quotes the six-hour line as evidence.
CarePoint runs the judge through the Models API from the aiplatform Apex namespace — the same gateway their agent uses, so everything stays behind the Einstein Trust Layer. The scorer is an invocable, which means Flow can call it too (for sampled scoring of live conversations later):
public with sharing class AgentResponseJudge {
public class Request {
@InvocableVariable(required=true label='User Question')
public String question;
@InvocableVariable(required=true label='Grounding Source')
public String source;
@InvocableVariable(required=true label='Agent Response')
public String response;
}
public class Result {
@InvocableVariable(label='Verdict JSON')
public String verdictJson;
@InvocableVariable(label='Overall Pass')
public Boolean overallPass;
}
@InvocableMethod(label='Judge Agent Response'
description='Scores an agent response against its grounding source.')
public static List<Result> judge(List<Request> requests) {
List<Result> results = new List<Result>();
for (Request req : requests) {
aiplatform.ModelsAPI.createGenerations_Request genReq =
new aiplatform.ModelsAPI.createGenerations_Request();
genReq.modelName = 'sfdc_ai__DefaultGPT4Omni';
aiplatform.ModelsAPI_GenerationRequest body =
new aiplatform.ModelsAPI_GenerationRequest();
body.prompt = buildJudgePrompt(req.question, req.source, req.response);
genReq.body = body;
aiplatform.ModelsAPI.createGenerations_Response genRes =
new aiplatform.ModelsAPI().createGenerations(genReq);
Result r = new Result();
r.verdictJson = genRes.Code200.generation.generatedText;
Map<String, Object> parsed =
(Map<String, Object>) JSON.deserializeUntyped(r.verdictJson);
r.overallPass = 'pass'.equals((String) parsed.get('overall'));
results.add(r);
}
return results;
}
private static String buildJudgePrompt(String question,
String source, String response) {
return JUDGE_TEMPLATE
.replace('{{question}}', question)
.replace('{{source}}', source)
.replace('{{response}}', response);
}
private static final String JUDGE_TEMPLATE = '...'; // the prompt above
}
A driver script feeds it the evaluation set — question, source article body, and the response captured from a batch run — and writes verdicts to a custom object, Agent_Eval_Result__c, one record per case per dimension. A simple report then answers the questions that matter: pass rate by dimension, by topic, over time.
modelName. If you must use the same family, the source-quoting requirement is your main defense against self-agreement.An unvalidated judge is just a second opinion with confidence. CarePoint calibrated theirs in an afternoon:
That loop — human labels, measure agreement, tighten the rubric where the judge and humans disagree — is the whole method. Repeat it whenever you change the judge prompt or model. And keep a small human sample forever: CarePoint's reviewers still read ten random transcripts a week, which is how they'll spot the failure modes the rubric doesn't cover yet.
Thresholds, agreed with compliance and enforced later in CI:
You now have the two halves of agent quality: behavior tests (Part 1, Part 2) and response scoring (this post). The obvious next question: when someone edits a topic instruction six months from now, what reruns all of this automatically? Next in the series: regression testing and CI for agents — including the one-line change that silently broke refund routing at an e-commerce company. The adversarial cousin of this post is Part 5, guardrail and red-team testing.
For the Models API plumbing used above in a different context, see Call LLMs from Apex with the Models API. The full series lives on the Agentforce Testing & Evaluation track.
Sources: Access Models API with Apex (Developer Guide) · Models API (Developer Guide) · Agentforce Testing Center (Salesforce Help) · Einstein Trust Layer (Trailhead)