Short answer: make the LLM's output a proposal, then admit it through a versioned closed-set contract that accepts one JSON object, rejects every label outside the active taxonomy, and commits one canonical result for each product revision. A Node.js worker can own the model call, but a deterministic boundary must own publication.
The least complex dependable design has four records: normalized product text, a taxonomy snapshot, the untrusted model response, and an accepted classification. This separation matters because syntactically valid JSON can still contain an obsolete category, a duplicate label, or a perfectly spelled label from the wrong taxonomy version. Those failures are more dangerous than a parse error: they look finished.
How can a Node.js LLM keep ecommerce product labels exact?
Define “exact” before choosing a prompt. For multi-label text classification, exactness has three independent dimensions: the response is exactly one JSON object; its labels member is an array of strings with no undeclared sibling fields; and every string belongs to the taxonomy version attached to the request. The third condition is the one a JSON parser can't prove by itself.
Use stable machine identifiers such as department.kitchen, material.stainless_steel, and use.outdoor; keep display names and translations outside the model contract. The request should identify the product revision, taxonomy version, and policy version, while the response should remain deliberately small:
{"labels":["department.kitchen","material.stainless_steel"]}
That's all.
An empty array should be legal when abstention is safer than invention. Whether it is legal for a particular catalog is a policy choice, not a decoding choice, and the policy must say whether parent and child labels may coexist. A label description should express the boundary that distinguishes neighbors; accessory alone is weak, whereas a description that distinguishes wearable accessories from appliance replacement parts gives the classifier a decision surface. Don't ask the model to infer rules the merchandising team hasn't written.
For a very large taxonomy, retrieve a smaller candidate set before classification. Embeddings are useful for relatedness and retrieval, but candidate retrieval introduces a hard accounting constraint: a label omitted from the candidate set cannot be selected later. Persist the candidate identifiers and retrieval-policy version alongside the final decision. This turns an apparently minor optimization into something that can be inspected when recall changes.
Treat publication as a ledger operation
The key invariant is not “the model answered once.” Networks, queues, and workers may deliver an operation more than once. The useful invariant is that one product revision under one taxonomy and policy produces at most one published classification. Construct an operation key from the product identifier, a digest of normalized source text, the taxonomy version, and the classification-policy version. A retry reuses that key; changed text or policy produces a new key.
This is an exactly-once mindset applied over at-least-once execution. It doesn't claim that transport is exactly once. Instead, the commit boundary checks the operation key, returns the already committed result for a duplicate, and prevents two late responses from silently overwriting each other. The audit event should record the operation key, input digest, candidate-set digest, configuration fingerprint, validation outcome, canonical labels, and timestamps. Raw product text and raw model output belong in access-controlled storage governed by the organization's retention and deletion rules; an audit trail is not permission to retain personal data forever.
Races still win.
Consider a concrete race. Revision 41 enters the queue, attempt one reaches the model, and the worker loses its lease before recording the reply. Attempt two starts with the same operation key. Meanwhile, revision 42 changes the product description. If each worker merely writes “latest tags,” the delayed revision 41 response can replace a valid revision 42 result. A conditional commit prevents it: revision 41 may populate only its own operation record, and publication advances only when the catalog row still names the matching product revision and policy. The response can be valid JSON and still lose that race. Correctly so.
Retries also need classification. A 429 is a transport-capacity signal; preserve operation identity, apply bounded delay, and honor retry guidance when the service supplies it. Unknown labels, extra JSON fields, and taxonomy mismatches are contract rejections. Blindly resending the same request can reproduce the same rejection while obscuring its frequency. Record the rejected attempt, then route it according to an explicit policy: a changed policy version, human review, or abstention. Never count attempts as completed products. Reconcile the set of requested operation keys against committed or terminally reviewed keys, because queue depth alone can't reveal a missing durable result.
Compliance changes the shape of the log. Product descriptions may carry seller contact details or other personal data, and generated output can repeat them even when the requested schema asks only for labels. Limit response size before decoding, separate restricted raw evidence from the compact accepted record, encrypt according to the applicable control environment, and make deletion executable. I'm not sure a universal retention period exists for this workload; legal basis, data classification, dispute windows, and local obligations determine it. The architecture should allow those decisions to change without erasing the minimal classification history required for reconciliation.
Validate before any tag becomes visible
In a Node.js system, the model adapter can call a small language-neutral validation service, or the same sequence can be implemented locally. The important part is behavioral identity across callers. The focused Go example below decodes one object, rejects unknown fields and trailing values, enforces membership, rejects duplicates rather than quietly repairing evidence, and sorts accepted labels so hashing and comparison remain stable.
package classification
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"sort"
)
type Result struct {
Labels []string `json:"labels"`
}
func Validate(raw []byte, allowed map[string]struct{}) (Result, error) {
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.DisallowUnknownFields()
var proposed Result
if err := decoder.Decode(&proposed); err != nil {
return Result{}, fmt.Errorf("decode classification: %w", err)
}
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
return Result{}, errors.New("expected exactly one JSON object")
}
seen := make(map[string]struct{}, len(proposed.Labels))
accepted := make([]string, 0, len(proposed.Labels))
for _, label := range proposed.Labels {
if _, exists := allowed[label]; !exists {
return Result{}, fmt.Errorf("label %q is outside the active taxonomy", label)
}
if _, duplicate := seen[label]; duplicate {
return Result{}, fmt.Errorf("duplicate label %q", label)
}
seen[label] = struct{}{}
accepted = append(accepted, label)
}
sort.Strings(accepted)
return Result{Labels: accepted}, nil
}
Do not autocorrect an unknown identifier or map it to the nearest known string. That destroys the distinction between what the classifier proposed and what the application accepted. Normalization belongs in taxonomy authoring and input preparation, before classification; accepted output should be canonical by construction. The same rule applies to duplicates. Rejecting them makes contract drift measurable, whereas silent deduplication can make a deteriorating response look healthy.
The validator is necessary, but it can't establish semantic quality. An allowed label may still be wrong. Build an adjudicated evaluation set with ambiguous examples, empty-label cases, overlapping categories, and rare labels; report per-label precision and recall rather than only aggregate accuracy. Test parser rejection separately from classification quality. Then shadow production traffic without publishing, compare accepted results with the current path, and observe latency, rejection class, abstention rate, candidate-set recall, and operation reconciliation. Your mileage may vary on the threshold for human review, but the threshold and escalation owner must exist before release.
Image-derived attributes are a separate pipeline. If product tagging later consumes images, deterministic preprocessing and versioned transformation settings become part of the input identity; image-processing documentation such as sharp's describes that layer, but it doesn't replace the text classification contract. Mixing unversioned image transformations into a text-input digest would make replay evidence misleading.
Choose the boundary, then migrate compactly
Closed-set generation fits a moderate taxonomy whose distinctions depend on prose. Retrieval followed by classification fits a taxonomy too large to present in full, with the catch that retrieval recall caps downstream recall. Deterministic rules are the better boundary when a statute, contract, exact SKU table, or approved eligibility matrix already decides the result. Human review remains appropriate when a wrong tag could hide a required warning or expose a restricted product. An LLM isn't suitable merely because the source field is text.
| Architecture | Appropriate constraint | Principal limitation | Evidence to retain |
|---|---|---|---|
| Closed-set classification | The complete choice set fits the request | Similar labels can remain semantically ambiguous | Taxonomy and policy versions |
| Retrieval plus classification | The taxonomy needs candidate narrowing | Omitted candidates cannot be recovered downstream | Candidate set and retrieval version |
| Deterministic rules | The mapping is formally specified | Nuanced language expands rule maintenance | Rule version and matched facts |
| Human review | Consequences or ambiguity exceed automation tolerance | Throughput and reviewer consistency | Reviewer, rationale, and revision |
Roll out by version, not by mutation. First freeze the taxonomy and policy used by an adjudicated test set. Next run replay tests and parser-adversarial cases, then shadow live work while keeping publication disabled. Enable a small cohort behind a policy flag, reconcile requested operations against durable outcomes, and preserve the previous policy as a rollback target. Finally, widen the cohort only after rare-label performance and contract rejection rates remain within written limits.
The result is intentionally unglamorous: Node.js coordinates the workflow, the LLM proposes a compact JSON value, deterministic code decides admissibility, and a conditional commit decides visibility. That division provides exact labels without pretending probabilistic classification itself is exact.
Top comments (0)