跳到正文
DEV Community· SeraphinaLyn7139·· 3 小時前AI 評分34

Node.js 如何使用 JSON Schema 實現結構化摘要:詳解

Use JSON Schema for Structured Summarization in Node.js Explained

AI 導讀

使用 JSON Schema 建立結構化摘要時,應把輸出視為有版本的數據契約,而非提示詞格式技巧。產品目錄資料須依次通過架構驗證、業務驗證及來源追溯檢查;只接受通過驗證的物件,並隔離不合格資料。把推理服務、目錄服務和人工審核流程的責任分開,可避免修改欄位時連帶改變其他規則。

正文

Short answer: treat structured summarization as a versioned data contract, not as a prompt formatting trick. For a fintech product catalog, the useful output is the record that passes schema validation, business validation, and provenance checks in that order. A fluent summary that silently turns 12-month warranty into 12 months or invents a missing fee is a data-quality incident, even when the model call returned HTTP 200.

The operational rule is narrow: accept only a validated object, quarantine anything that fails, and make schema changes explicit. Keep the model behind an interface so the catalog pipeline does not inherit one endpoint's request shape, retry semantics, or interpretation of JSON Schema. That decision leaves room to change the inference service later, but it does not relax the contract at the database boundary.

Why does a catalog summary need a contract?

A messy description is not a trustworthy source merely because it is text. Consider a consumer-loan listing containing an annual percentage rate, a promotional rate, an early-repayment condition, and a sentence that applies only to new customers. A summary intended for search may need a short description, normalized features, eligibility flags, and evidence spans. Those fields have different failure modes. Missing evidence should remain missing; it should not become a confident sentence.

Reject early.

The first tempting implementation is to validate only after persistence, because that keeps the request path short. The trade-off is a larger blast radius: one malformed object can reach search indexes, customer-facing copy, and an audit export before anyone sees it. I prefer spending a little latency on validation at the boundary, then keeping the rejected payload available for replay with its source identifier and schema version. That is a capacity decision as much as a correctness decision; the queue must be sized for the work validation creates.

I start with a small schema and a rejection path. The schema answers shape questions: which keys exist, which values are arrays, and which fields are required. A second validator answers domain questions: an APR cannot be negative, an evidence span must occur in the original description, and promotional cannot be true without an associated condition. JSON Schema is useful here, but it is not a substitute for those domain rules.

This separation also makes ownership legible. The inference adapter owns transport and decoding. The catalog service owns business validation and persistence. The review queue owns human correction. When all three concerns sit in a prompt, a change to one field can quietly alter the meaning of the other two.

How should Node.js use JSON Schema for structured summarization?

The example below keeps the response contract in Go types and sends a schema-shaped request to a configurable inference endpoint. The endpoint is deliberately an interface boundary; the catalog service does not need to know which hosted or self-managed runtime is behind it. In production, the schema string should be checked into the same change as the Go type and evaluated against representative fixtures.

package catalog

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

type ProductSummary struct {
    Title       string   `json:"title"`
    Summary     string   `json:"summary"`
    Features    []string `json:"features"`
    Eligibility []string `json:"eligibility"`
    Evidence    []string `json:"evidence"`
    NeedsReview bool     `json:"needs_review"`
}

const productSummarySchema = `{\n  "type": "object",\n  "additionalProperties": false,\n  "required": ["title", "summary", "features", "eligibility", "evidence", "needs_review"],\n  "properties": {\n    "title": {"type": "string"},\n    "summary": {"type": "string"},\n    "features": {"type": "array", "items": {"type": "string"}},\n    "eligibility": {"type": "array", "items": {"type": "string"}},\n    "evidence": {"type": "array", "items": {"type": "string"}},\n    "needs_review": {"type": "boolean"}\n  }\n}`

type requestPayload struct {
    Input  string `json:"input"`
    Schema string `json:"json_schema"`
}

func Summarize(ctx context.Context, client *http.Client, endpoint, source string) (ProductSummary, error) {
    payload, err := json.Marshal(requestPayload{Input: source, Schema: productSummarySchema})
    if err != nil {
        return ProductSummary{}, fmt.Errorf("encode request: %w", err)
    }

    request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
    if err != nil {
        return ProductSummary{}, fmt.Errorf("build request: %w", err)
    }
    request.Header.Set("Content-Type", "application/json")

    response, err := client.Do(request)
    if err != nil {
        return ProductSummary{}, fmt.Errorf("inference request: %w", err)
    }
    defer response.Body.Close()
    if response.StatusCode < 200 || response.StatusCode >= 300 {
        return ProductSummary{}, fmt.Errorf("inference status: %s", response.Status)
    }

    var result ProductSummary
    if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
        return ProductSummary{}, fmt.Errorf("decode structured result: %w", err)
    }
    if err := validateDomain(result, source); err != nil {
        return ProductSummary{}, err
    }
    return result, nil
}

func validateDomain(result ProductSummary, source string) error {
    if result.Title == "" || result.Summary == "" {
        return fmt.Errorf("reject summary: required text is empty")
    }
    for _, evidence := range result.Evidence {
        if evidence == "" || !bytes.Contains([]byte(source), []byte(evidence)) {
            return fmt.Errorf("reject summary: evidence is not in source")
        }
    }
    return nil
}

var defaultHTTPClient = &http.Client{Timeout: 8 * time.Second}

There are two deliberate boundaries in this small example. The response is decoded into a closed Go shape, and evidence is checked against the source after decoding. Neither check proves that the prose is good, but both prevent an apparently successful call from becoming an untraceable catalog fact. An empty or malformed result should enter quarantine with the source identifier and schema version attached.

The limitation matters. JSON Schema can reject the wrong shape, but it cannot establish that an APR was interpreted correctly or that a summary omitted a material exception. Domain validators and source evidence add work and latency, and a review queue adds operational ownership. That is the price of making correctness observable; dropping those controls is only appropriate when the output is disposable and non-regulated.

What changes when the schema changes?

Schema evolution is where many otherwise tidy implementations become operationally expensive. Adding an optional field is usually compatible for readers that ignore unknown data; changing a string into an array is not. Renaming eligibility to requirements is also a migration, even if both names feel clearer. The model will follow the latest instructions, while stored records, replay jobs, dashboards, and reviewers may still speak the old dialect.

I use an explicit version next to every accepted result, for example catalog_summary_v2. The version is metadata, not another field that the model may omit. A deployment accepts the versions it can validate, writes only the current version, and keeps a converter for records that need replay. During rollout, fixtures cover both old and new payloads, including additional properties, missing arrays, null values, very long evidence, and contradictory source text.

Version it.

Suppose the first contract stores eligibility as a list of plain strings, then a compliance review requires each item to carry an evidence span and a jurisdiction. The safe change is not to make the old field mysteriously accept objects. Publish a new version with an explicit object shape, teach the reader to accept both versions, dual-run the validator on a fixture sample, and decide which existing rows require replay. Only after the new valid-record rate and review-queue age meet their objectives should the writer switch. This is slower than changing one prompt line, but the prompt line was never the real interface; the stored record was. The extra migration work is the trade-off for being able to explain exactly which rule produced a customer-visible field months later.

A useful capacity calculation belongs here. If the catalog receives 2,000 descriptions per hour and the review rate is 4%, the queue receives 80 cases per hour. If reviewers clear 100 cases per hour during the staffed window, the nominal margin is 20 cases per hour, but that is not an SLO. A burst, a deployment pause, or a second validation rule can remove the margin quickly. Track queue age and oldest-item age, not only average throughput.

Keep it boring.

How do you verify correctness before release?

Start with a fixed evaluation set owned by the catalog team: regulated terms, missing terms, conflicting terms, multilingual fragments, and descriptions that contain tempting but irrelevant numbers. Assert properties rather than a single preferred paragraph. Every accepted output must satisfy the schema, preserve required evidence, and obey domain constraints. Every rejected output must be observable and replayable without losing the original input.

The key SLO should describe usable records, not model availability alone. One example is the proportion of submitted descriptions that produce a schema-valid, domain-valid record within the processing window. Pair it with an error budget for quarantined records and a separate review-queue age objective. A runtime can be available while the catalog is unusable if it returns plausible objects that fail downstream checks.

Log a request identifier, schema version, validation result, latency, token accounting when available, and a content fingerprint rather than raw financial text by default. Redact sensitive fields before logs and restrict access to the replay store. OWASP identifies prompt injection and sensitive information disclosure as risks for applications built around large language models; a catalog pipeline still needs ordinary data-governance controls around those risks.

Rollback is a data operation

If a new schema or instruction set causes the valid-record SLO to fall, stop publishing the new version, route new inputs to the last accepted contract, and retain the failed inputs for analysis. Do not overwrite failed records with an empty summary. That turns a visible quality regression into silent data loss.

Rollback also needs a decision about already-published rows. If the change can alter regulated wording, mark affected records for replay or review using the schema version and deployment identifier. If it only adds an unused optional field, retaining the old rows may be acceptable. The decision belongs in the runbook before release, because incident pressure is a poor time to discover that the database cannot distinguish two meanings of summary.

The practical conclusion is modest: JSON Schema gives the pipeline a shared shape, while validation, evidence, versioning, and queue controls give that shape operational meaning. Choose the inference runtime by its observable contract and failure behavior, then keep the catalog's acceptance rules independent of that choice.

References

來源:DEV Community · dev.to