Teams write refusal data as a list: these topics, do not answer. The model learns the list, passes the test built from the same list, and then behaves unpredictably on the request that sits one step to the side.
A list teaches categories. A boundary teaches judgment.
Lists do not generalize
If the negative examples are all extreme, the model learns a threshold far from where you wanted it. Everything milder than the extreme gets treated as acceptable, including the cases you cared about.
The rows that fix this are the mild ones: the request that is legal, plausible, and still something you would not want answered in this context.
Write the near miss on purpose
For each boundary, write three rows: clearly inside, clearly outside, and one that an experienced person would have to think about. The third is the training signal. The first two are the scaffolding.
Then write the reason next to the third. A refusal without a stated reason becomes a superstition the next model inherits.
What a refusal row holds
- The request, in its natural phrasing
- The decision: answer, decline, or answer with a constraint
- The reason, in one sentence
- For partial answers, the part that is allowed
Storing the partial case is what stops a model from treating a whole topic as forbidden when only one use of it is.
Measure both errors
Over-refusal and under-refusal cost different things and both cost. Report them separately, on separate slices, or a change that trades one for the other will look like an improvement.
Zovda builds these rows through Datuva, where the decision and its reason stay attached to the request. The corpus catalogue shows where boundary rows sit in each stage.
Tell us the boundary your product has to hold. We reply within one business day.
