跳到正文
DEV Community· ElowenVeil9067·· 2 小時前AI 評分56

Node.js 媒體目錄文字摘要的結構化 JSON 輸出 Schema 解説

Node.js Structured JSON Output: Text Summarization Schema Explained for Media Catalogs

AI 導讀

Node.js 媒體目錄摘要可用精簡 Schema 約束 JSON 輸出,並在應用程式邊界驗證結果、按租戶記錄用量。建議欄位包括 summary、keywords、language 和 sourceVersion;工作佇列及儲存流程應核對來源版本,避免發布過期結果,並以冪等寫入和用量事件處理重試及成本歸屬。

正文

Short answer: for Node.js text summarization, use structured JSON output with a small schema, then validate it at the application boundary and record usage beside the tenant that caused it. The useful unit is not a pretty summary. It is a catalog enrichment event that can be retried, audited, and charged to the right account.

I use a revenue-per-hour lens for this kind of feature. A media catalog may contain a clean title, a two-line description, a pasted press release, or a paragraph with three different products mixed together. The summarizer has to produce predictable fields without hiding the work behind an opaque monthly total. That changes the design.

Four fields are enough for the first contract.

How should structured JSON output handle text summarization with a schema?

Start with a contract small enough to test. For a catalog item, I would keep summary, keywords, language, and sourceVersion. Each field answers a different operational question: what can the editor display, what can search index, which language was detected, and which input revision produced the result.

The schema is a boundary, not a prompt decoration. A valid JSON document can still be useless: an empty summary, 400 keywords, or a sourceVersion copied from an old retry all pass a weak type check. Put semantic limits in the application as well as shape constraints in the schema.

The input should be treated as data. Do not concatenate a description and tenant instructions into one unmarked string and hope the model knows which text has authority. The runtime request can carry a fixed task and a clearly delimited description. The output parser should accept one object, reject trailing prose, and preserve the raw response for a review queue when policy allows it.

A practical contract might look like this:

type CatalogSummary = {
  summary: string;
  keywords: string[];
  language: string;
  sourceVersion: string;
};

type SummaryRequest = {
  tenantId: string;
  itemId: string;
  sourceVersion: string;
  description: string;
};

TypeScript gives the handler a useful shape, but it does not prove that a runtime response matches that shape. Use a JSON Schema validator or an equivalent runtime validator at the boundary. Keep the schema version in configuration and include it in the event record. A changed field is a data migration, even if the code review looks small.

Where does tenant cost attribution belong?

The tenant identifier belongs on the job before the model call, not after the result is saved. A result has no reliable owner if the worker has to infer ownership from mutable catalog state. Put the attribution key, item identifier, input revision, schema version, and attempt number into the job envelope.

Then collect usage as an event. The event can contain input and output units when the runtime supplies them, a request identifier, a status, and a timestamp. Keep the accounting path separate from the display path. The editor needs a summary quickly; the finance view needs a durable record that explains why a retry exists.

Here is the smallest Node.js-shaped worker I would ship first. The callRuntime function is intentionally generic. It hides no vendor-specific behavior and leaves the response usage explicit.

type RuntimeResult = {
  text: string;
  usage?: { inputUnits?: number; outputUnits?: number };
  requestId?: string;
};

declare function callRuntime(input: {
  instruction: string;
  description: string;
}): Promise<RuntimeResult>;

declare function parseSummary(text: string): CatalogSummary;
declare function saveSummary(args: {
  tenantId: string;
  itemId: string;
  sourceVersion: string;
  schemaVersion: string;
  summary: CatalogSummary;
}): Promise<void>;
declare function appendUsageEvent(event: Record<string, unknown>): Promise<void>;

async function enrichCatalogItem(request: SummaryRequest): Promise<CatalogSummary> {
  const startedAt = Date.now();
  const schemaVersion = "catalog-summary-v1";

  const result = await callRuntime({
    instruction:
      "Return exactly one JSON object with summary, keywords, language, and sourceVersion.",
    description: request.description,
  });

  const summary = parseSummary(result.text);
  if (summary.sourceVersion !== request.sourceVersion) {
    throw new Error("summary source version does not match the job");
  }

  await saveSummary({
    tenantId: request.tenantId,
    itemId: request.itemId,
    sourceVersion: request.sourceVersion,
    schemaVersion,
    summary,
  });

  await appendUsageEvent({
    tenantId: request.tenantId,
    itemId: request.itemId,
    schemaVersion,
    requestId: result.requestId,
    inputUnits: result.usage?.inputUnits,
    outputUnits: result.usage?.outputUnits,
    durationMs: Date.now() - startedAt,
    status: "accepted",
  });

  return summary;
}

The ordering matters. Save only after validation, and write the usage event with the same tenant key. In a production queue, I would make the persistence operation idempotent on tenantId, itemId, sourceVersion, and schemaVersion. A retry should not create a second catalog revision or make the usage dashboard claim two successful enrichments.

What happens when the JSON is valid but the job is wrong?

This is the failure I watch most closely. Syntax errors are loud. Wrong lineage is quiet. A worker can return valid JSON for an old description after an editor has already published a newer revision. The summary looks fine, so a basic health check passes while the catalog is stale.

Use a monotonic source revision or an immutable content hash. On write, compare the job revision with the current item revision. If they differ, mark the result stale and retain its usage event. Do not publish it. The work still consumed runtime capacity, and hiding that cost makes the next planning decision worse.

Consider a simple editorial sequence. A tenant imports a film description as revision 17. The worker receives it, and an editor replaces the description with revision 18 before the worker finishes. The response for revision 17 can be perfectly structured, use the expected language, and contain sensible keywords. It is still the wrong result. The write path should compare the job envelope with the current row, classify the response as stale, and leave revision 18 eligible for a fresh job. The usage record should retain 17, because the runtime work happened. This gives the operator two honest facts: the tenant has no published summary for the current text, and the older attempt consumed capacity. A dashboard that counts only accepted summaries hides both facts and makes a later cost review look better than the system actually behaved. That is why lineage and usage should share identifiers even though they serve different screens.\n\nStale is a valid outcome.

A second quiet failure is duplication. Queue delivery can be at-least-once, and a timeout does not prove that the remote request never completed. The retry policy should distinguish a rejected response, a validation failure, and an unknown outcome. Use an idempotency key where the runtime supports one; otherwise, make the catalog write idempotent and record the uncertainty.

Rate limiting belongs in this design too. HTTP status 429 means the server is refusing a request because the client sent too many requests in a given period; a worker should respond with bounded backoff and respect Retry-After when it is present. It should not treat every 429 as a malformed summary or retry forever.

function retryDelay(attempt: number, retryAfterSeconds?: number): number {
  const serverDelay = retryAfterSeconds === undefined
    ? 0
    : retryAfterSeconds * 1000;
  const exponentialDelay = Math.min(30_000, 500 * 2 ** attempt);
  const jitter = Math.floor(Math.random() * 250);
  return Math.max(serverDelay, exponentialDelay) + jitter;
}

The numbers here are policy examples, not universal defaults. Pick limits from queue latency and tenant expectations, then test them under a controlled burst. The important property is bounded work.

How do you test a schema without testing only the happy path?

A fixture with one polished description proves very little. Build fixtures from the actual shape of the catalog: missing punctuation, HTML fragments, duplicated paragraphs, multiple languages, long descriptions, empty descriptions, and text that asks the model to ignore the task. The expected result should test invariants, not a single generated sentence.

I would run four layers. First, validate the request envelope and reject a missing tenant or source revision before spending runtime units. Second, validate the returned object against the schema. Third, apply semantic checks such as maximum summary length and keyword count. Fourth, verify persistence and usage attribution with the same identifiers.

Keep a small golden set, but do not confuse it with a benchmark. Generated wording can change while the contract remains correct. Property-based tests are useful for the parser and for hostile strings. A replay test is useful for an incident: the same input revision and schema version should explain the stored result without requiring a live call.

Observability should answer three questions quickly: which tenant generated the work, which schema version accepted it, and where the request stopped. Log identifiers and status transitions. Avoid putting the full description in ordinary logs; catalog text may contain licensed copy or personal information. A count of validation failures is more useful than a giant log line.

What would I change after the first weekly release?

I would ship the smallest path weekly: one schema, one queue policy, one usage event, and one operator view showing accepted, stale, rejected, and retried jobs by tenant. That is enough to learn whether the feature improves catalog operations before adding a second enrichment task.

At larger volume, I would separate admission control from worker concurrency. A tenant can have a quota and a backlog without blocking every other tenant. I would also sample raw responses into restricted storage, with retention and access rules, instead of making them part of the default observability stream.

The trade-off is real. More fields in the event make investigations easier, but increase storage and privacy work. More aggressive retries improve completion for transient failures, but make cost spikes harder to predict. A strict schema reduces downstream surprises, but forces a migration when editors ask for a new field. I prefer explicit friction here. Outsource the undifferentiated work to the queue and validator; keep the decisions that affect tenant trust in application code.

The decision rule is simple: if a summary cannot be tied to a tenant, an input revision, a schema version, and an outcome, it is not ready for publication. Structured output is the visible part. Attribution and stale-write protection are what make the feature operable.

References

來源:DEV Community · dev.to