跳到正文
DEV Community· IgnazCole6453·· 3 小時前AI 評分35

使用 Chat Completions API 摘要發票文字的 7 種方法:入帳前把關

7 Ways to Summarize Invoice Text with Chat Completions APIs — Before Posting

AI 導讀

Node.js 透過 Chat Completions API 摘要發票文字時,應採用具型別的資料擷取流程,並在送交應付帳款系統入帳前驗證欄位。對含糊或缺漏資料,須保留原文、標示原因並轉交覆核;批次處理適用於可等待的工作,但不能取代驗證或人工覆核。

正文

A Node.js text summarization API using chat completions should reject ambiguous invoice fields before an accounts-payable system posts them. The deciding constraint is structured output correctness: a fluent summary that moves a currency symbol, drops a tax line, or turns an invoice number into a date is a failed extraction.

TL;DR: Treat summarization as a typed extraction pipeline. Define the contract, preserve the source text, validate every response, evaluate field-level errors, and quarantine uncertain invoices. Batch work can reduce request pressure for non-urgent workloads, but it does not replace validation or review.

1. Define the invoice contract before you write a prompt

For a game studio, supplier invoices often mix software licenses, outsourced art, localization, cloud usage, and hardware. The useful output is not a paragraph. It is a record that another system can inspect. Start with the smallest contract that supports the posting decision.

from dataclasses import dataclass
from decimal import Decimal
from typing import Optional

@dataclass
class InvoiceFields:
    supplier_name: str
    invoice_number: str
    invoice_date: str
    currency: str
    subtotal: Decimal
    tax: Optional[Decimal]
    total: Decimal
    purchase_order: Optional[str]

I keep dates as strings until they pass an explicit date parser, and I do not let a missing purchase order become an empty string by accident. Missing, unreadable, and not-applicable are different states. That distinction matters when a supplier invoice is routed to a human.

The first experiment is deliberately boring: run the same small invoice set through a strict schema and a free-form summary, then compare field-level correctness. The free-form version may read better. That is not the winning metric.

2. How should Node.js summarize invoice text through a chat completions API?

An extraction system needs a refusal path. Consider this fragment from a fictional invoice:

Northstar Localization
Invoice: NL-8841
Work period: 03/04/2026
Localization review: 1,200
Tax: --
Total due: 1,200

The date could mean March 4 or April 3, depending on the supplier's convention. The currency is absent. A summarizer can still produce a polished answer, but a posting service should return an uncertainty reason and preserve the raw value.

def posting_decision(fields: dict) -> str:
    required = ("supplier_name", "invoice_number", "invoice_date", "currency", "total")
    missing = [name for name in required if not fields.get(name)]
    if missing:
        return f"review: missing {', '.join(missing)}"
    if fields.get("invoice_date_ambiguous"):
        return "review: ambiguous invoice date"
    return "ready"

This is the failure mode I design around. A model response is an input to a decision function, not the decision itself. Keep the original text beside the parsed record, attach a reason code, and make review reversible.

Whole-document exact match is too blunt. A record can have nine correct fields and one wrong total. I use a weighted score for investigation, while keeping hard gates for fields that must never drift.

def field_score(expected: dict, actual: dict) -> dict:
    weights = {
        "invoice_number": 3,
        "currency": 3,
        "total": 5,
        "tax": 4,
        "supplier_name": 2,
    }
    result = {}
    for field, weight in weights.items():
        result[field] = {
            "correct": expected.get(field) == actual.get(field),
            "weight": weight,
        }
    return result

Track exact match for identifiers, normalized numeric equality for amounts, and a separate outcome for abstentions. Sample invoices should include split tax lines, credit notes, comma and period separators, multiple currencies, scanned text, and invoices with no purchase-order number. Five hand-picked happy paths are not an evaluation set. The contract above has eight fields; that is enough surface area to expose drift without pretending a single score explains the ledger.

The notebook-to-prod handoff is where many teams lose the plot. I first thought a passing notebook sample meant the extractor was ready. It did not: the missing-currency case was more informative than another clean invoice. Freeze a small labeled set, run it in CI, and record the schema version with every result. When the prompt or model changes, compare the new confusion cases instead of celebrating a single aggregate score.

That gate matters.

4. Keep the runtime boring and the prompt cost visible

The runtime needs a generic interface with a clear boundary: text in, validated object out, diagnostics alongside it. The rest of the application should not know which completion service produced the candidate.

from typing import Protocol

class TextRuntime(Protocol):
    def summarize(self, text: str, schema_name: str) -> dict:
        ...

def extract_invoice(runtime: TextRuntime, text: str) -> dict:
    candidate = runtime.summarize(text, schema_name="supplier_invoice_v1")
    validate_invoice(candidate)
    return candidate

I count input and output tokens, truncation events, retries, and review rates per invoice class. A shorter prompt is not automatically cheaper if it causes a second attempt or a manual correction. For overnight reconciliation, a batch-oriented workflow can be a reasonable operational choice; the public batch guidance describes asynchronous processing and a separate completion window, so it is a poor fit for an approval screen that needs an immediate answer.

Do not hide retries inside a helper that returns only a dict. Emit a request identifier, schema version, latency, and validation result. Those four details make a bad invoice reproducible. The limitation is clear: this design is a poor fit for an instant approval screen if the chosen runtime is asynchronous; use a synchronous path there, and reserve batch processing for work that can wait. That is the trade-off between throughput and immediate feedback.

5. Compare failure boundaries, not polished demos

If you test hosted APIs, local models, or a self-managed runtime, use the same corpus and the same acceptance contract. Compare how each option handles malformed output, unsupported documents, rate limits, long invoices, and a missing currency. Measure operator review time as well as model accuracy.

The practical choice is often a boundary decision. A managed service can reduce infrastructure work; a self-hosted model can offer different control over deployment and data movement; a local fallback can change latency and maintenance responsibilities. A local fallback is a poor fit when the team cannot own model updates and capacity planning. That limitation is a staffing decision, not a model-quality claim. None of those properties proves that invoice fields are correct.

Before copying an implementation, measure four things: field-level accuracy on difficult invoices, abstention quality, end-to-end review rate, and token volume per accepted invoice. Then set a release gate for the fields that can create a financial error. The summary is useful only after the record survives that gate.

References

來源:DEV Community · dev.to