مدلهای استخراج
مدل استخراج، شواهد ارسالی را به مقدارهای نامدار تبدیل میکند که برنامه میتواند بررسی کند. Clairic از Cloudflare، با شناسه cf.clairic در /v1/extraction از طریق AvalAI، محاسبه نوعدار، برآورد و پیشبینی را به این الگو اضافه میکند. مدل میتواند شناسه صورتحساب، مهلت پرداخت، مبلغ همراه با حمل یا زمان موردانتظار پرداخت را پیشنهاد دهد. این ادعاها از یک جنس نیستند و بررسیهای متفاوتی میخواهند.
مرجع API استخراج قالب کامل درخواست و پاسخ و مثالهای curl، Python و Node.js را ارائه میکند. این راهنما گردش کار برنامه را توضیح میدهد: استخراج ← بررسی با کد و رکورد معتبر ← بازبینی ← اقدام بر اساس مجوز برنامه. داده و پاسخ صورتحساب این راهنما ساختگیاند. این صفحه، اعتبارسنجی زنده AvalAI یا دقت سنجیدهشده در محیط عملیاتی را گزارش نمیکند.
خروجی مناسب را انتخاب کنید
| نیاز | رویکرد مدل / API | مسئولیت برنامه |
|---|---|---|
| شناسه عیناً موجود در سند | text در استخراج | بررسی بخش منبع، شکل شناسه و تطبیق با رکورد. |
| مبلغ عددی یا تاریخ تقویمی | number / date در استخراج | بررسی محاسبه، ارز، تاریخ معتبر و شرایط معتبر قرارداد. |
| چند نام، مبلغ یا تاریخ | list در استخراج | بررسی هر عضو، تکرار، کامل بودن و شواهد منبع. |
| زمان، تاریخ و زمان یا مدت | time، datetime یا duration در استخراج | تعیین قالب، منطقه زمانی، ابهام و قواعد واحد. |
| مقدار نامطمئن آینده | estimate / forecast در استخراج | حفظ عدم قطعیت و مقایسه با نتیجههای بعدی. |
| دسته، شرط یا معیار امتیازدهی مشخص | Decisions / SystemOne | بررسی پاسخ مجاز و انتخاب سیاست بازبینی یا مسیریابی. |
| خلاصه، توضیح یا شیء تودرتوی دلخواه | مدل متنی مناسب با خروجی ساختیافته | بررسی قالب و محتوای واقعی. |
fields در استخراج، خروجی نوعدار را توصیف میکند و JSON Schema دلخواه نیست. choices در تصمیمگیری، مجموعه پاسخ ازپیشتعیینشده را بیان میکند. زمان پرداخت پیشبینیشده، نه شرط عیناً استخراجشده است و نه امتیاز تصمیمگیری. این معناها را در رکورد ذخیرهشده و رابط کاربری روشن نگه دارید.
شواهد و فیلدها را با هم طراحی کنید
مرز سندها، شناسه منبع و شرایط مرتبط را در شواهد حفظ کنید. state در این نقطه پایانی باید متن باشد؛ رکورد ساختاریافته را روشن قالببندی کنید و شیء بومی JSON نفرستید. ارز، تاریخ مرجع و تفاوت تاریخ صدور با تاریخ پرداخت را حذف نکنید. متن نامرتبطی را که میتواند توجه را از فیلد درخواستی دور کند کاهش دهید.
با instructions نقش فیلد را تعریف کنید. «شماره سفارش خرید»، po_number را از invoice_number جدا میکند؛ «مبلغ قابلپرداخت شامل حمل» محاسبه کل موردنظر را توضیح میدهد. هر فیلد یک هدف روشن داشته باشد. از مدل نخواهید برای کامل شدن رکورد، داده ناموجود منبع را بسازد.
محتوای سند، شاهد غیرقابلاعتماد است. ممکن است متن گمراهکننده یا دستور جاسازیشده داشته باشد. مجوز حساب، رکورد معتبر تأمینکننده، کلید محرمانه، مقصد پرداخت و اقدام قابلاجرا را بیرون از آن شواهد نگه دارید. قالب و امتیاز اطمینان، قابلاعتماد بودن سند را ثابت نمیکنند.
برای تصویر از PNG/JPEG/WebP جاسازیشده با محدودیت بالادستی توضیحدادهشده در مرجع استفاده کنید. خطای تشخیص متن، ورودی فقط تصویری، تصویر کموضوح و متن غیر ASCII را جداگانه آزمایش کنید. فرض موقعیت متنی را به ناحیه پیکسلی تعمیم ندهید. صوت، ویدئو، PDF و URL تصویر راه دور، ورودی مستندشده این مسیر استخراج نیستند.
مشاهده، محاسبه و پیشبینی را جدا نگه دارید
منبع شاهد و نوع فیلد را همراه هر مقدار ذخیره کنید. شناسه متنی را میتوان با زیررشته دقیق سنجید؛ مبلغ محاسبهشده باید با محاسبه قطعی مقایسه شود؛ تاریخ پرداخت آینده به سنجش با نتیجه بعدی نیاز دارد. تجزیه موفق پاسخ، این بررسیها را هممعنا نمیکند.
Clairic میتواند value: null همراه با guess، span و confidence بالا برگرداند. فقط مقدار اصلی value را پس از بررسی بپذیرید. شماره صورتحساب محتمل نباید به شماره سفارش خرید تبدیل شود، صرفاً چون فیلد سفارش خرید ناموجود آن را حدس زده است.
p_null، اطمینان و بازه پیشبینی را نشانههای جداگانه بدانید. قالب عمومی آنها رابطه کالیبراسیون برنامه را تعریف نمیکند. آستانه را با داده برچسبخورده گردش کار خود انتخاب کنید. تنظیم null_threshold دفعات خودداری مدل از پاسخ را تغییر میدهد؛ دقت واقعی بهتر را ثابت نمیکند. فراداده ناموجود را ناموجود نگه دارید؛ صفر یا اطمینان ساختگی جایگزین آن نکنید.
مثال کامل بررسی صورتحساب
برنامه زیر، همان درخواست ششفیلدی مرجع را به کار میبرد. فقط از کتابخانه استاندارد Python استفاده میکند، یک درخواست با مهلت اتصال 120 ثانیه میفرستد و ready_for_review یا review_required را چاپ میکند. هیچ پرداخت، نوشتن در پایگاه داده یا اعلانی انجام نمیدهد. حتی نامزد معتبر نیز به فرد بازبین یا فرایند تأییدشده بازبینی برنامه سپرده میشود.
کد را با نام invoice_extraction.py ذخیره کنید. با python3 invoice_extraction.py --self-check، اعتبارسنج را بدون کلید API یا درخواست شبکه آزمایش کنید. اگر AVALAI_API_KEY از قبل تنظیم شده باشد، python3 invoice_extraction.py یک درخواست ساختگی با احتمال هزینه میفرستد. خودآزمایی برنامه، آزمایش سازگاری مسیر زنده نیست.
داده معتبر آزمایشی، شناسه موردانتظار، تاریخ صدور، مهلت پرداخت، تعداد، قیمت و حمل را ثابت میکند. در گردش کار واقعی، این مقدارها را از سامانه سفارش خرید یا تأمینکننده دریافت کنید؛ اگر همه مقدارهای مقایسه را از همان پاسخ مدل بگیرید، بررسی دوری میشود. شرط اطمینان 0.95 در مثال، انتخاب توضیحی برنامه است و تضمین کالیبرهشده AvalAI نیست.
import copy
import json
import math
import os
import sys
from datetime import date, timedelta
from decimal import Decimal, InvalidOperation
from urllib.error import HTTPError
from urllib.request import Request, urlopen
payload = {
"model": "cf.clairic",
"state": "Invoice INV-2291 from Northwind Supply, issued 2026-09-02, net 30. 4 pallets at $250.00 each, shipping $80.00. Contact: Dana Ruiz, Priya Shah.",
"fields": {
"invoice_number": {"type": "text"},
"due_date": {
"type": "date",
"instructions": "Issue date plus the net payment days",
},
"total": {
"type": "number",
"decimals": 2,
"unit": "USD",
"instructions": "Amount due including shipping",
},
"contacts": {"type": "list", "items": "text"},
"po_number": {"type": "text", "instructions": "Purchase order number"},
"days_to_pay": {
"type": "estimate",
"min": 0,
"max": 120,
"step": 1,
"instructions": "Days after issue until this invoice is paid",
},
},
}
trusted = {
"invoice_number": "INV-2291",
"issue_date": date(2026, 9, 2),
"net_days": 30,
"quantity": Decimal("4"),
"unit_price": Decimal("250.00"),
"shipping": Decimal("80.00"),
}
def validate_candidate(result):
reasons = []
if not isinstance(result, dict) or result.get("model") != "cf.clairic":
return {
"status": "review_required",
"reasons": ["unexpected_model_or_envelope"],
}
if result.get("truncated") is not False:
reasons.append("truncated_or_unknown_evidence")
answers = result.get("answers")
if not isinstance(answers, dict):
return {"status": "review_required", "reasons": reasons + ["missing_answers"]}
def accepted(name, kind):
answer = answers.get(name)
if (
not isinstance(answer, dict)
or answer.get("type") != kind
or "value" not in answer
):
reasons.append(f"{name}:malformed_answer")
return None
value = answer["value"]
if value is None:
reasons.append(f"{name}:unresolved")
return None
confidence = answer.get("confidence")
if (
isinstance(confidence, bool)
or not isinstance(confidence, (int, float, Decimal))
or not math.isfinite(confidence)
or not 0.95 <= confidence <= 1
):
reasons.append(f"{name}:confidence_review")
return value
invoice_number = accepted("invoice_number", "text")
if invoice_number is not None:
if (
not isinstance(invoice_number, str)
or invoice_number != trusted["invoice_number"]
):
reasons.append("invoice_number:trusted_record_mismatch")
span = answers["invoice_number"].get("span")
valid_span = (
isinstance(span, list)
and len(span) == 2
and all(type(i) is int for i in span)
and 0 <= span[0] <= span[1] <= len(payload["state"])
)
if not valid_span or payload["state"][span[0] : span[1]] != invoice_number:
reasons.append("invoice_number:invalid_source_span")
due_date = accepted("due_date", "date")
if due_date is not None:
try:
if not isinstance(due_date, str):
raise ValueError("date must be text")
parsed_date = date.fromisoformat(due_date)
expected_date = trusted["issue_date"] + timedelta(days=trusted["net_days"])
if parsed_date.isoformat() != due_date or parsed_date != expected_date:
reasons.append("due_date:trusted_terms_mismatch")
except ValueError:
reasons.append("due_date:invalid_calendar_date")
total = accepted("total", "number")
if total is not None:
try:
if isinstance(total, bool) or not isinstance(total, (int, float, Decimal)):
raise InvalidOperation
amount = Decimal(str(total))
expected_total = (
trusted["quantity"] * trusted["unit_price"] + trusted["shipping"]
)
if not amount.is_finite() or amount < 0 or amount != expected_total:
reasons.append("total:trusted_arithmetic_mismatch")
except (InvalidOperation, ValueError):
reasons.append("total:invalid_number")
contacts = accepted("contacts", "list")
if contacts is not None and (
not isinstance(contacts, list)
or not all(
isinstance(item, str) and item.strip() and item in payload["state"]
for item in contacts
)
or len(set(contacts)) != len(contacts)
):
reasons.append("contacts:invalid_or_duplicate_items")
# در این داده سفارش خرید وجود ندارد؛ حدس، شماره پذیرفتهشده نیست.
po = answers.get("po_number")
if (
not isinstance(po, dict)
or po.get("type") != "text"
or "value" not in po
or po["value"] is not None
):
reasons.append("po_number:unexpected_value")
# پیشبینی را از شرایط پرداخت و بررسی قطعی جدا نگه دارید.
prediction = answers.get("days_to_pay")
if (
not isinstance(prediction, dict)
or prediction.get("type") != "estimate"
or "value" not in prediction
):
reasons.append("days_to_pay:malformed_prediction")
return {
"status": "review_required" if reasons else "ready_for_review",
"reasons": reasons,
"candidate": {
"invoice_number": invoice_number,
"due_date": due_date,
"total": total,
"contacts": contacts,
},
"prediction": prediction,
}
def self_check():
fixture = {
"model": "cf.clairic",
"truncated": False,
"answers": {
"invoice_number": {
"type": "text",
"value": "INV-2291",
"span": [8, 16],
"confidence": 1,
},
"due_date": {"type": "date", "value": "2026-10-02", "confidence": 1},
"total": {"type": "number", "value": 1080, "confidence": 1},
"contacts": {
"type": "list",
"value": ["Dana Ruiz", "Priya Shah"],
"confidence": 1,
},
"po_number": {
"type": "text",
"value": None,
"guess": "INV-2291",
"confidence": 1,
},
"days_to_pay": {"type": "estimate", "value": 30, "p10": 21, "p90": 38},
},
}
assert validate_candidate(fixture)["status"] == "ready_for_review"
fractional = copy.deepcopy(fixture)
fractional["answers"]["total"]["confidence"] = Decimal("0.9999")
assert validate_candidate(fractional)["status"] == "ready_for_review"
for field, value in [
("total", True),
("total", 1),
("total", float("nan")),
("due_date", "2026-02-30"),
("po_number", "INV-2291"),
]:
invalid = copy.deepcopy(fixture)
invalid["answers"][field]["value"] = value
assert validate_candidate(invalid)["status"] == "review_required"
invalid = copy.deepcopy(fixture)
invalid["answers"]["invoice_number"]["value"] = None
invalid["answers"]["invoice_number"]["guess"] = "INV-2291"
assert validate_candidate(invalid)["status"] == "review_required"
invalid = copy.deepcopy(fixture)
invalid["truncated"] = True
assert validate_candidate(invalid)["status"] == "review_required"
print("self-check passed; no network request")
if __name__ == "__main__":
if sys.argv[1:] == ["--self-check"]:
self_check()
elif sys.argv[1:]:
raise SystemExit("usage: invoice_extraction.py [--self-check]")
else:
request = Request(
"https://api.avalai.ir/v1/extraction",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {os.environ['AVALAI_API_KEY']}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urlopen(request, timeout=120) as response:
result = json.load(response, parse_float=Decimal)
request_id = response.headers.get("avalai-request-id")
except HTTPError as error:
print("avalai-request-id:", error.headers.get("avalai-request-id"))
print(
f"HTTP {error.code}: {error.read().decode('utf-8', errors='replace')}"
)
raise
print("avalai-request-id:", request_id)
print(json.dumps(validate_candidate(result), indent=2, default=str))مثال، نبود سفارش خرید اختیاری را null نگه میدارد و days_to_pay را وارد شرایط پرداخت نمیکند. اگر سیاست خرید شما سفارش خرید را الزامی میداند، قاعده بازبینی آن را اضافه کنید. متن آزمایشی ASCII است؛ بررسی موقعیت آن، رفتار شمارش Unicode را ثابت نمیکند. بررسی مخاطبها فقط اعضای ارسالی را میسنجد، نه کامل بودن فهرست یا هویت در دفترچه مخاطب معتبر.
پیشبینی برای بازبینی حفظ میشود و گردش کار پرداخت از آن استفاده نمیکند. برنامه پیشبینی همچنین باید نوع مقدار، متناهی بودن خلاصهها، ترتیب بازهها، همراستایی شبکه و تعداد گام، و گام ناموجود یا null را بررسی کند. قالب عمومی خروجی، ساختار تودرتوی پیشبینی را گسترده نگه میدارد؛ پیش از پیادهسازی این بررسیها، نمایش واقعی مسیر را مشخص کنید.
پیش از استفاده عملیاتی ارزیابی کنید
نمونههای برچسبخوردهای از سند درست، فیلد ناموجود، شناسه متعارض، سطر تکراری، ابهام ارز، تاریخ ناممکن، Unicode، خطای تشخیص متن و دستور تزریقشده بسازید. دقت مقدارهای پذیرفتهشده و نرخ فیلدهای نامشخص یا نیازمند بازبینی را جداگانه بسنجید. سامانهای که همه فیلدها را پر میکند، ممکن است از سامانهای که در نبود شاهد بهدرستی خودداری میکند ضعیفتر باشد.
مبلغ و تاریخ محاسبهشده را با محاسبه و قواعد تقویمی معتبر مقایسه کنید. برای برآورد و پیشبینی، نتیجه را پیش از رخ دادن پیامد حفظ کنید و خطای بعدی، پوشش بازه، رفتار بیرون محدوده و تغییر در طول زمان را بسنجید. دقت خوب استخراج، کیفیت پیشبینی را ثابت نمیکند. وضعیت پیشنمایش یعنی پس از تغییر ارائهدهنده، سازگاری و رفتار باید دوباره بررسی شوند.
شناسه مدل درخواستی، تعریف فیلدها، زمان مرجع، شناسه منبع، شناسه HTTP/درخواست، وضعیت کوتاه شدن، فراداده خام پاسخ، مصرف و نتیجه اعتبارسنج را حفظ کنید. کلید API و بدنه حساس سند را در گزارش مشترک قرار ندهید. بازگرداندن همان شناسه مدل، نسخه ثابت بالادستی را مشخص نمیکند.
نتیجه ناقص و شکست را مدیریت کنید
پاسخ HTTP 200 همچنان به بررسی پوشش پاسخ، نوع جواب و مقدار نیاز دارد. فیلد ناموجود، نوع ناشناخته، JSON نامعتبر، null غیرمنتظره، کوتاه شدن و موقعیت نامعتبر باید به بازبینی برسند؛ جایگزین مبلغ صفر یا شناسه خالی نیستند. پاسخ خام را برای بررسی مجاز حفظ کنید و بیصدا اصلاح نکنید.
خطاهای درخواست 400، 413 و 422 را پیش از تکرار اصلاح کنید. بدنه 429 را برای کد سهمیه پایانیافته بررسی کنید؛ تلاش مجدد اعتبار تمامشده را افزایش نمیدهد. برای شکست موقت 429/503، فقط هنگامی سیاست تلاش مجدد محدود به کار ببرید که تکرار استنتاج مناسب باشد و هزینه ورودی بیشتر را حساب کنید. کلاینتهای مرجع و این برنامه یک تلاش انجام میدهند تا شکست اولیه آشکار بماند.
اجرای پرداخت، تغییر تأمینکننده، اعلان به مشتری و بهروزرسانی پایگاه داده، پس از اعتبارسنجی و بازبینی، مجوز و کنترل تکرار اقدام خود را میخواهند. آنها را بیرون از حلقه تلاش مجدد استنتاج نگه دارید. مرجع استخراج، مدیریت خطا و Cloudflare را ببینید.