Instructor چیست؟ آموزش Structured Output با Python و API درواره
Instructor کتابخانهای سبک برای تبدیل پاسخ مدلهای زبانی به اشیای معتبر Pydantic است. در این آموزش یک سیستم استخراج اطلاعات با Python، FastAPI و API درواره میسازیم.
پاسخ متنی مدلهای زبانی برای چت، تولید محتوا و پاسخگویی به کاربر مناسب است؛ اما بسیاری از برنامهها به متن آزاد نیاز ندارند. آنها باید اطلاعات را در قالبی مشخص و قابلاستفاده در کد دریافت کنند.
برای مثال، یک سیستم پشتیبانی ممکن است بخواهد از متن هر پیام این اطلاعات را استخراج کند:
- موضوع درخواست
- اولویت
- احساس کاربر
- خلاصه
- واحد مسئول
- نیاز یا عدم نیاز به بررسی انسانی
درخواست از مدل برای تولید JSON بهتنهایی کافی نیست. ممکن است مدل:
- یک فیلد را حذف کند.
- نام فیلد را تغییر دهد.
- مقدار نامعتبر برگرداند.
- پیش یا پس از JSON توضیح اضافه کند.
- نوع داده را رعایت نکند.
- ساختار تودرتو را ناقص تولید کند.
Instructor کتابخانهای برای دریافت خروجیهای ساختاریافته و Type-safe از مدلهای زبانی است. این کتابخانه با استفاده از Pydantic، ساختار خروجی را تعریف و پاسخ مدل را اعتبارسنجی میکند.
در این آموزش یک پروژه عملی میسازیم که:
- Instructor را نصب و پیکربندی میکند.
- به API سازگار درواره متصل میشود.
- متن فارسی را به داده ساختاریافته تبدیل میکند.
- خروجی را با Pydantic اعتبارسنجی میکند.
- در صورت خروجی نامعتبر Retry انجام میدهد.
- فهرستها و مدلهای تودرتو را استخراج میکند.
- Validation اختصاصی اجرا میکند.
- از اجرای Async پشتیبانی میکند.
- از طریق FastAPI به یک API قابلاستفاده تبدیل میشود.
- برای تست، مدیریت خطا و محیط Production آماده میشود.
Instructor چیست؟
Instructor یک کتابخانه سبک برای دریافت خروجی ساختاریافته از مدلهای زبانی است. توسعهدهنده نوع پاسخ موردانتظار را با یک مدل Pydantic تعریف میکند و Instructor فرایند تبدیل Schema، دریافت پاسخ، Parse کردن، Validation و Retry را مدیریت میکند.
بر اساس مستندات رسمی Instructor، این کتابخانه قابلیتهایی مانند موارد زیر را ارائه میدهد:
- خروجی Type-safe
- اعتبارسنجی با Pydantic
- Retry خودکار
- مدلهای تودرتو
- استخراج فهرست
- Streaming
- اجرای Async
- پشتیبانی از Providerهای مختلف
- Validation مبتنی بر Context
فرایند کلی Instructor به این شکل است:
متن ورودی
↓
تعریف مدل Pydantic
↓
ارسال درخواست به مدل هوش مصنوعی
↓
دریافت خروجی ساختاریافته
↓
اعتبارسنجی با Pydantic
↓
Retry در صورت نامعتبر بودن
↓
شیء معتبر پایتون
نتیجه نهایی فقط یک رشته JSON نیست، بلکه نمونهای از کلاس Pydantic است و میتوان مستقیماً از فیلدهای آن در منطق برنامه استفاده کرد.
چرا از Instructor استفاده کنیم؟
میتوان مستقیماً از مدل خواست که JSON تولید کند:
پاسخ را فقط بهشکل JSON برگردان.
این روش برای یک نمونه آزمایشی ساده قابلاستفاده است، اما در پروژه واقعی با مشکلاتی مواجه میشود:
- Schema بهشکل دقیق تعریف نشده است.
- Parse کردن پاسخ را باید خودتان انجام دهید.
- Validation نوع دادهها دستی است.
- مدیریت فیلدهای حذفشده بر عهده برنامه است.
- Retry خروجی نامعتبر باید جداگانه نوشته شود.
- پیام خطای Validation باید دوباره به مدل ارسال شود.
- مدلهای تودرتو کد بیشتری نیاز دارند.
- نگهداری Schemaهای متعدد دشوار میشود.
Instructor این مراحل را در یک رابط نزدیک به OpenAI Python SDK قرار میدهد.
نمونه ساده:
person = client.chat.completions.create(
model=model_id,
response_model=Person,
messages=[
{
"role": "user",
"content": "امیر ۴۹ سال دارد و در تهران زندگی میکند.",
}
],
)
اکنون person یک نمونه معتبر از کلاس Person است:
print(person.name)
print(person.age)
print(person.city)
Instructor چه تفاوتی با Pydantic دارد؟
Pydantic یک کتابخانه عمومی برای مدلسازی و اعتبارسنجی داده در پایتون است. Instructor از Pydantic برای تعریف خروجی موردانتظار مدل هوش مصنوعی استفاده میکند.
Pydantic بهتنهایی این داده را بررسی میکند:
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
Instructor همین مدل را به فرایند فراخوانی LLM متصل میکند:
person = client.chat.completions.create(
model=model_id,
response_model=Person,
messages=[
{
"role": "user",
"content": "نام و سن فرد را استخراج کن.",
}
],
)
بهطور خلاصه:
- Pydantic ساختار و اعتبار داده را مدیریت میکند.
- Instructor مدل زبانی را وادار میکند دادهای سازگار با آن ساختار تولید کند.
- API درواره دسترسی به مدل هوش مصنوعی را فراهم میکند.
تفاوت Instructor با PydanticAI
Instructor و PydanticAI هر دو از Pydantic استفاده میکنند، اما هدف یکسانی ندارند.
| ویژگی | Instructor | PydanticAI |
|---|---|---|
| هدف اصلی | دریافت خروجی ساختاریافته | ساخت Agent و Workflow هوش مصنوعی |
| پیچیدگی | سبکتر | گستردهتر |
| Tool Calling | هدف اصلی نیست | قابلیت اصلی |
| Agent Loop | ندارد | دارد |
| Dependency Injection | محدود به Validation Context | پشتیبانی کامل برای Agent |
| Structured Output | قابلیت اصلی | یکی از قابلیتها |
| کاربرد مناسب | استخراج و طبقهبندی داده | Agentهای چندمرحلهای |
اگر فقط میخواهید یک ورودی را به خروجی معتبر Pydantic تبدیل کنید، Instructor معمولاً انتخاب سادهتری است.
اگر پروژه به Tool Calling، Agent Loop، Dependency Injection و Workflow چندمرحلهای نیاز دارد، PydanticAI انتخاب کاملتری خواهد بود.
Instructor چه تفاوتی با فراخوانی مستقیم API دارد؟
در فراخوانی مستقیم API باید پاسخ را بهشکل متن دریافت و سپس پردازش کنید:
response = openai_client.chat.completions.create(
model=model_id,
messages=messages,
)
raw_text = response.choices[0].message.content
پس از آن باید:
- JSON را استخراج کنید.
- آن را با
json.loadsتبدیل کنید. - خطای Parse را مدیریت کنید.
- داده را به مدل Pydantic بدهید.
- خطای Validation را مدیریت کنید.
- در صورت نیاز درخواست را تکرار کنید.
در Instructor بخش بزرگی از این فرایند با response_model انجام میشود:
result = instructor_client.chat.completions.create(
model=model_id,
response_model=DesiredOutput,
messages=messages,
)
فراخوانی مستقیم برای پاسخ متنی ساده مناسب است. Instructor زمانی ارزشمند میشود که ساختار خروجی بخشی از قرارداد برنامه باشد.
پیشنیازهای آموزش
برای اجرای مثالها به موارد زیر نیاز دارید:
- Python نسخه ۳.۱۰ یا جدیدتر
- محیط مجازی پایتون
- کلید API درواره
- شناسه یک مدل سازگار
- آشنایی مقدماتی با Python
- آشنایی مقدماتی با Pydantic
برای مشاهده شناسه، قابلیت و قیمت جاری مدلها، صفحه مدلهای درواره را بررسی کنید.
قابلیت مدلها در تولید JSON، Tool Calling و رعایت Schema یکسان نیست. پیش از استفاده در Production، مدل انتخابشده را روی دادههای واقعی پروژه آزمایش کنید.
ساخت پروژه
یک پوشه جدید ایجاد کنید:
mkdir instructor-darvareh
cd instructor-darvareh
محیط مجازی را بسازید:
python -m venv .venv
فعالسازی در Linux و macOS:
source .venv/bin/activate
فعالسازی در Windows PowerShell:
.venv\Scripts\Activate.ps1
نصب Instructor
بستههای موردنیاز را نصب کنید:
pip install instructor openai pydantic python-dotenv
برای بخش FastAPI:
pip install fastapi "uvicorn[standard]"
نمونه فایل requirements.txt:
instructor
openai
pydantic
python-dotenv
fastapi
uvicorn[standard]
در محیط Production بهتر است نسخه وابستگیها را پس از آزمایش Pin کنید.
ساخت فایل تنظیمات محیطی
در ریشه پروژه یک فایل .env بسازید:
DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID
فایل .gitignore:
.venv/
.env
__pycache__/
*.pyc
.pytest_cache/
آدرس پایه API درواره ثابت است:
https://api.darvareh.ir/v1
اتصال Instructor به API درواره
از آنجا که API درواره با ساختار OpenAI-compatible قابلاستفاده است، ابتدا یک نمونه از OpenAI Client با base_url درواره ایجاد میکنیم و سپس آن را با Instructor ترکیب میکنیم.
فایل client.py:
import os
import instructor
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
api_key = os.getenv("DARVAREH_API_KEY")
model_id = os.getenv("DARVAREH_MODEL_ID")
if not api_key:
raise RuntimeError(
"DARVAREH_API_KEY is not configured"
)
if not model_id:
raise RuntimeError(
"DARVAREH_MODEL_ID is not configured"
)
openai_client = OpenAI(
api_key=api_key,
base_url="https://api.darvareh.ir/v1",
)
instructor_client = instructor.from_openai(
openai_client,
mode=instructor.Mode.JSON,
)
در این کد:
- کلید API از متغیر محیطی خوانده میشود.
- شناسه مدل خارج از کد نگهداری میشود.
- درخواستها به API درواره ارسال میشوند.
- Instructor پاسخ را مطابق مدل Pydantic دریافت میکند.
- حالت JSON برای سازگاری عمومیتر انتخاب شده است.
بسته به مدل انتخابشده، ممکن است حالتهای دیگر Instructor نیز قابلاستفاده باشند. Mode را بدون آزمایش روی مدل و مسیر API تغییر ندهید.
ساخت اولین استخراجکننده اطلاعات
فایل extract_person.py را ایجاد کنید:
from pydantic import BaseModel, Field
from client import instructor_client, model_id
class Person(BaseModel):
name: str = Field(
description="نام شخص"
)
age: int | None = Field(
default=None,
ge=0,
le=120,
description="سن شخص در صورت وجود",
)
city: str | None = Field(
default=None,
description="شهر محل سکونت در صورت وجود",
)
person = instructor_client.chat.completions.create(
model=model_id,
response_model=Person,
messages=[
{
"role": "system",
"content": (
"اطلاعات را فقط از متن کاربر استخراج کن. "
"اگر یک مقدار در متن وجود ندارد، آن را null قرار بده."
),
},
{
"role": "user",
"content": (
"امیر ۴۹ سال دارد و در تهران زندگی میکند."
),
},
],
)
print(person)
print(person.model_dump())
خروجی بهشکل یک شیء Pydantic در دسترس است:
print(person.name)
print(person.age)
print(person.city)
دیگر نیازی به اجرای دستی json.loads یا بررسی وجود فیلدها ندارید.
طراحی درست Response Model
کیفیت خروجی Instructor فقط به مدل زبانی وابسته نیست. طراحی مدل Pydantic نیز تأثیر زیادی دارد.
یک Response Model مناسب باید:
- نام فیلدهای روشن داشته باشد.
- برای هر فیلد Description مشخص ارائه دهد.
- مقادیر محدود را با
Literalیا Enum تعریف کند. - فیلدهای اختیاری را واقعاً Optional کند.
- محدودیت طول و بازه داشته باشد.
- ساختار بیش از حد پیچیده نداشته باشد.
- با اطلاعات موجود در ورودی سازگار باشد.
نمونه نامناسب:
class Result(BaseModel):
value: str
data: str
status: str
این فیلدها مبهم هستند و مدل نمیداند چه چیزی باید داخل آنها قرار گیرد.
نمونه بهتر:
from typing import Literal
from pydantic import BaseModel, Field
class SupportTicket(BaseModel):
category: Literal[
"technical",
"billing",
"account",
"feature_request",
"other",
]
priority: Literal[
"low",
"medium",
"high",
]
summary: str = Field(
min_length=5,
max_length=300,
description="خلاصه کوتاه درخواست به زبان فارسی",
)
requires_human_review: bool = Field(
description=(
"اگر اطلاعات ناکافی یا موضوع حساس است، true باشد"
)
)
پروژه عملی: تحلیل تیکت پشتیبانی
فایل ticket_analyzer.py:
from typing import Literal
from pydantic import BaseModel, Field
from client import instructor_client, model_id
class TicketAnalysis(BaseModel):
category: Literal[
"technical",
"billing",
"account",
"feature_request",
"general",
]
priority: Literal[
"low",
"medium",
"high",
]
sentiment: Literal[
"positive",
"neutral",
"negative",
]
summary: str = Field(
min_length=5,
max_length=300,
)
suggested_team: Literal[
"technical_support",
"finance",
"customer_success",
"product",
]
requires_human_review: bool
confidence: float = Field(
ge=0,
le=1,
)
def analyze_ticket(message: str) -> TicketAnalysis:
return instructor_client.chat.completions.create(
model=model_id,
response_model=TicketAnalysis,
max_retries=2,
messages=[
{
"role": "system",
"content": (
"پیام پشتیبانی را فقط براساس اطلاعات موجود "
"تحلیل کن. در صورت ابهام، confidence را کاهش "
"بده و requires_human_review را true قرار بده."
),
},
{
"role": "user",
"content": message,
},
],
)
analysis = analyze_ticket(
"سلام، از دیشب تمام درخواستهای API من با Timeout مواجه میشوند."
)
print(analysis.model_dump())
اکنون منطق برنامه میتواند مستقیماً از نتیجه استفاده کند:
if analysis.priority == "high":
send_to_priority_queue(analysis)
if analysis.requires_human_review:
assign_to_human_agent(analysis)
مدل فقط اطلاعات را پیشنهاد میدهد. تصمیم نهایی کسبوکار، مانند ارجاع تیکت یا اولویت صف، بهتر است در کد برنامه کنترل شود.
استخراج فهرست با Instructor
فرض کنید یک متن شامل چند محصول است و میخواهیم همه آنها را استخراج کنیم.
from pydantic import BaseModel, Field
from client import instructor_client, model_id
class Product(BaseModel):
name: str
price: int | None = Field(
default=None,
ge=0,
)
available: bool | None = None
class ProductList(BaseModel):
products: list[Product]
text = """
کیبورد مکانیکی با قیمت ۴ میلیون و ۵۰۰ هزار تومان موجود است.
ماوس بیسیم با قیمت ۱ میلیون و ۸۰۰ هزار تومان موجود است.
پایه لپتاپ فعلاً موجود نیست و قیمت آن در متن ذکر نشده است.
"""
result = instructor_client.chat.completions.create(
model=model_id,
response_model=ProductList,
max_retries=2,
messages=[
{
"role": "system",
"content": (
"فقط اطلاعات صریح متن را استخراج کن. "
"مقادیر ناموجود را حدس نزن."
),
},
{
"role": "user",
"content": text,
},
],
)
for product in result.products:
print(product.model_dump())
قرار دادن فهرست داخل یک مدل ریشهای معمولاً امکان توسعه Schema را آسانتر میکند. بعداً میتوانید فیلدهایی مانند تعداد رکورد، هشدار یا زبان متن را نیز اضافه کنید.
مدلهای تودرتو
برای استخراج داده پیچیده میتوان از مدلهای Nested استفاده کرد.
from pydantic import BaseModel, Field
class ContactInfo(BaseModel):
email: str | None = None
phone: str | None = None
class Address(BaseModel):
city: str | None = None
street: str | None = None
postal_code: str | None = None
class Customer(BaseModel):
full_name: str
contact: ContactInfo
address: Address
interests: list[str] = Field(
default_factory=list
)
فراخوانی:
customer = instructor_client.chat.completions.create(
model=model_id,
response_model=Customer,
messages=[
{
"role": "system",
"content": (
"اطلاعات مشتری را استخراج کن. "
"هیچ مقدار ناموجودی را حدس نزن."
),
},
{
"role": "user",
"content": (
"علی رضایی از شیراز است. ایمیل او "
"ali@example.com است و به برنامهنویسی "
"و هوش مصنوعی علاقه دارد."
),
},
],
)
Schemaهای بسیار عمیق میتوانند رعایت ساختار را برای برخی مدلها دشوار کنند. اگر خروجی پیچیده است، آن را به چند مرحله کوچکتر تقسیم کنید.
استفاده از Enum
برای مقادیر محدود میتوان از Enum استفاده کرد:
from enum import Enum
from pydantic import BaseModel
class Priority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class Ticket(BaseModel):
title: str
priority: Priority
Enum از تولید مقادیری مانند urgent, critical یا normal خارج از قرارداد برنامه جلوگیری میکند.
اعتبارسنجی اختصاصی با Pydantic
معتبر بودن نوع داده همیشه کافی نیست. ممکن است بخواهیم قواعد بیشتری اعمال کنیم.
from pydantic import BaseModel, Field, field_validator
class ArticleSummary(BaseModel):
title: str = Field(
min_length=5,
max_length=100,
)
summary: str = Field(
min_length=30,
max_length=500,
)
keywords: list[str] = Field(
min_length=3,
max_length=8,
)
@field_validator("keywords")
@classmethod
def normalize_keywords(
cls,
values: list[str],
) -> list[str]:
cleaned = []
for value in values:
keyword = value.strip()
if keyword and keyword not in cleaned:
cleaned.append(keyword)
if len(cleaned) < 3:
raise ValueError(
"at least three unique keywords are required"
)
return cleaned
اگر پاسخ مدل با این قواعد سازگار نباشد، Validation شکست میخورد. با فعالبودن Retry، Instructor میتواند خطا را به مدل بازگرداند تا پاسخ اصلاح شود.
جزئیات این قابلیت در مستندات رسمی Validation در Instructor ارائه شده است.
Retry خودکار چگونه کار میکند؟
ممکن است مدل در اولین تلاش خروجی نامعتبر تولید کند. برای مثال:
- مقدار Enum اشتباه باشد.
- فیلد ضروری حذف شود.
- متن بیش از حد طولانی باشد.
- عدد خارج از بازه باشد.
- Validator اختصاصی خطا بدهد.
Instructor میتواند اطلاعات خطای Validation را در اختیار مدل قرار دهد و از آن بخواهد پاسخ را اصلاح کند.
result = instructor_client.chat.completions.create(
model=model_id,
response_model=TicketAnalysis,
max_retries=2,
messages=[
{
"role": "user",
"content": user_message,
}
],
)
Retry را محدود نگه دارید. مقدار زیاد میتواند:
- هزینه را افزایش دهد.
- Latency را بالا ببرد.
- خطای دائمی را پنهان کند.
- تعداد درخواستها را غیرقابلپیشبینی کند.
اگر مدل پس از یک یا دو Retry همچنان Schema را رعایت نمیکند، بهتر است:
- Schema را سادهتر کنید.
- Description فیلدها را بهبود دهید.
- Prompt را روشنتر کنید.
- مدل مناسبتری انتخاب کنید.
- نمونه را برای بررسی انسانی ارسال کنید.
راهنمای رسمی Retry در Instructor جزئیات بیشتری درباره این فرایند ارائه میدهد.
تفاوت Validation ساختاری و معنایی
Pydantic میتواند بررسی کند:
- فیلد وجود دارد.
- مقدار یک عدد است.
- عدد در بازه مشخص قرار دارد.
- رشته طول قابلقبولی دارد.
- مقدار عضو Enum است.
- ساختار Nested معتبر است.
اما Pydantic بهتنهایی نمیتواند تضمین کند:
- اطلاعات با واقعیت مطابقت دارد.
- خلاصه تمام نکات مهم را پوشش داده است.
- قیمت از منبع درست استخراج شده است.
- مدل اطلاعاتی را جعل نکرده است.
- طبقهبندی از نظر کسبوکار صحیح است.
بنابراین دو سطح Validation نیاز داریم:
Validation ساختاری
با Pydantic، Schema، نوع داده و Validatorها انجام میشود.
Validation محتوایی
با روشهایی مانند موارد زیر انجام میشود:
- مقایسه با منبع
- بررسی در پایگاه داده
- قواعد کسبوکار
- اجرای کد
- Evals
- بازبینی انسانی
- مدل داور مستقل
Structured Output شکل داده را کنترل میکند، نه حقیقت آن را.
Validation مبتنی بر Context
گاهی اعتبار یک مقدار به اطلاعات زمان اجرا وابسته است. برای مثال، دسته محصول باید عضو فهرستی باشد که از پایگاه داده دریافت شده است.
from pydantic import (
BaseModel,
ValidationInfo,
field_validator,
)
class ClassifiedProduct(BaseModel):
product_name: str
category: str
@field_validator("category")
@classmethod
def category_must_be_allowed(
cls,
value: str,
info: ValidationInfo,
) -> str:
allowed_categories = info.context.get(
"allowed_categories",
[],
)
if value not in allowed_categories:
raise ValueError(
f"category must be one of {allowed_categories}"
)
return value
فراخوانی:
result = instructor_client.chat.completions.create(
model=model_id,
response_model=ClassifiedProduct,
context={
"allowed_categories": [
"computer",
"mobile",
"accessories",
]
},
max_retries=2,
messages=[
{
"role": "user",
"content": (
"کیبورد مکانیکی را در دسته مناسب قرار بده."
),
}
],
)
این قابلیت برای موارد زیر مناسب است:
- دستههای پویا
- فهرست مجاز کشورها
- محصولات موجود
- Policyهای کسبوکار
- گزینههای وابسته به کاربر
- اعتبارسنجی براساس سند مرجع
مستندات Reask و Context Validation این قابلیت را توضیح میدهد.
مدیریت مقدار ناموجود
یکی از اشتباهات رایج این است که تمام فیلدها اجباری تعریف شوند، حتی زمانی که متن ممکن است اطلاعات لازم را نداشته باشد.
مدل نامناسب:
class Person(BaseModel):
name: str
age: int
city: str
اگر متن فقط نام را داشته باشد، مدل ممکن است سن و شهر را حدس بزند.
مدل بهتر:
class Person(BaseModel):
name: str
age: int | None = None
city: str | None = None
در Prompt نیز صریحاً بنویسید:
اگر اطلاعات یک فیلد در متن وجود ندارد، مقدار آن را null قرار بده.
هیچ اطلاعاتی را حدس نزن.
Optional بودن فیلد باید با واقعیت دادههای پروژه هماهنگ باشد.
افزودن شواهد به خروجی
برای کاهش ابهام میتوان از مدل خواست علاوه بر مقدار استخراجشده، بخش پشتیبان متن را نیز برگرداند.
from pydantic import BaseModel
class ExtractedValue(BaseModel):
value: str | None
evidence: str | None
نمونه کاملتر:
class InvoiceData(BaseModel):
invoice_number: ExtractedValue
customer_name: ExtractedValue
total_amount: ExtractedValue
payment_status: ExtractedValue
اکنون برنامه میتواند بررسی کند که Evidence واقعاً در متن ورودی وجود دارد. این روش جایگزین Validation مستقل نیست، اما بررسی خروجی را سادهتر میکند.
دریافت متادیتای پاسخ
گاهی علاوه بر شیء Parseشده، به اطلاعات پاسخ خام یا Usage نیاز دارید. Instructor برای این هدف قابلیت create_with_completion ارائه میدهد.
result, completion = (
instructor_client.chat.completions.create_with_completion(
model=model_id,
response_model=TicketAnalysis,
messages=[
{
"role": "user",
"content": user_message,
}
],
)
)
print(result.model_dump())
print(completion)
ساختار دقیق Usage به مدل و Provider وابسته است. پیش از اتکا به فیلدهای متادیتا، خروجی مسیر انتخابشده را بررسی کنید.
اجرای Async
در FastAPI یا برنامههایی با درخواستهای همزمان، از Async Client استفاده کنید.
فایل async_client.py:
import os
import instructor
from dotenv import load_dotenv
from openai import AsyncOpenAI
load_dotenv()
api_key = os.getenv("DARVAREH_API_KEY")
if not api_key:
raise RuntimeError(
"DARVAREH_API_KEY is not configured"
)
async_openai_client = AsyncOpenAI(
api_key=api_key,
base_url="https://api.darvareh.ir/v1",
)
async_instructor_client = instructor.from_openai(
async_openai_client,
mode=instructor.Mode.JSON,
)
استفاده:
from async_client import async_instructor_client
from client import model_id
async def analyze_message(
message: str,
) -> TicketAnalysis:
return await (
async_instructor_client.chat.completions.create(
model=model_id,
response_model=TicketAnalysis,
max_retries=2,
messages=[
{
"role": "user",
"content": message,
}
],
)
)
در Endpointهای Async از Client همزمان استفاده نکنید؛ زیرا میتواند Event Loop را مسدود کند.
پردازش چند ورودی بهصورت همزمان
برای تحلیل چند متن میتوان از asyncio.gather همراه با محدودیت همزمانی استفاده کرد.
import asyncio
semaphore = asyncio.Semaphore(5)
async def analyze_with_limit(
message: str,
) -> TicketAnalysis:
async with semaphore:
return await analyze_message(message)
async def analyze_many(
messages: list[str],
) -> list[TicketAnalysis]:
tasks = [
analyze_with_limit(message)
for message in messages
]
return await asyncio.gather(*tasks)
محدودیت همزمانی از ارسال ناگهانی تعداد زیادی درخواست جلوگیری میکند. مقدار مناسب را براساس محدودیت API، Latency، هزینه و ظرفیت برنامه انتخاب کنید.
ساخت API با FastAPI
ساختار پروژه:
instructor-darvareh/
├── .env
├── .gitignore
├── client.py
├── async_client.py
├── schemas.py
├── services.py
├── main.py
└── requirements.txt
فایل schemas.py:
from typing import Literal
from pydantic import BaseModel, Field
class AnalyzeRequest(BaseModel):
message: str = Field(
min_length=5,
max_length=5000,
)
class TicketAnalysis(BaseModel):
category: Literal[
"technical",
"billing",
"account",
"feature_request",
"general",
]
priority: Literal[
"low",
"medium",
"high",
]
summary: str = Field(
min_length=5,
max_length=300,
)
requires_human_review: bool
confidence: float = Field(
ge=0,
le=1,
)
class AnalyzeResponse(BaseModel):
data: TicketAnalysis
فایل services.py:
from async_client import async_instructor_client
from client import model_id
from schemas import TicketAnalysis
async def analyze_ticket(
message: str,
) -> TicketAnalysis:
return await (
async_instructor_client.chat.completions.create(
model=model_id,
response_model=TicketAnalysis,
max_retries=2,
messages=[
{
"role": "system",
"content": (
"پیام پشتیبانی را فقط براساس متن "
"کاربر تحلیل کن. اطلاعاتی را که در "
"متن وجود ندارد حدس نزن."
),
},
{
"role": "user",
"content": message,
},
],
)
)
فایل main.py:
from fastapi import FastAPI, HTTPException
from openai import (
APIConnectionError,
APITimeoutError,
AuthenticationError,
RateLimitError,
)
from schemas import (
AnalyzeRequest,
AnalyzeResponse,
)
from services import analyze_ticket
app = FastAPI(
title="Darvareh Instructor API",
version="1.0.0",
)
@app.get("/health")
async def health() -> dict[str, str]:
return {"status": "ok"}
@app.post(
"/analyze-ticket",
response_model=AnalyzeResponse,
)
async def analyze_ticket_endpoint(
body: AnalyzeRequest,
) -> AnalyzeResponse:
try:
result = await analyze_ticket(body.message)
return AnalyzeResponse(data=result)
except AuthenticationError as exc:
raise HTTPException(
status_code=500,
detail="Model service is not configured",
) from exc
except RateLimitError as exc:
raise HTTPException(
status_code=429,
detail="Request rate limit reached",
) from exc
except (APITimeoutError, APIConnectionError) as exc:
raise HTTPException(
status_code=503,
detail="Model service is temporarily unavailable",
) from exc
except Exception as exc:
raise HTTPException(
status_code=502,
detail="Unable to generate a valid response",
) from exc
اجرای برنامه:
uvicorn main:app --reload
مستندات Swagger:
http://127.0.0.1:8000/docs
نمونه درخواست:
curl -X POST "http://127.0.0.1:8000/analyze-ticket" \
-H "Content-Type: application/json" \
-d '{"message":"شارژ کیف پول من انجام شده اما موجودی تغییر نکرده است."}'
نمونه پاسخ:
{
"data": {
"category": "billing",
"priority": "high",
"summary": "شارژ انجام شده اما موجودی کیف پول تغییر نکرده است.",
"requires_human_review": true,
"confidence": 0.96
}
}
مدیریت خطاها
فراخوانی Instructor ممکن است در چند سطح شکست بخورد:
خطای تنظیمات
- کلید API وجود ندارد.
- شناسه مدل تنظیم نشده است.
- آدرس Base URL اشتباه است.
این خطاها در زمان Startup بررسی شوند.
خطای Provider
- Rate Limit
- Timeout
- اختلال شبکه
- خطای موقت سرویس
- مدل ناموجود
- دسترسی نامعتبر
خطای Structured Output
- مدل JSON معتبر تولید نمیکند.
- Schema را رعایت نمیکند.
- Validation شکست میخورد.
- تعداد Retry به پایان میرسد.
خطای منطق برنامه
- ورودی کاربر بیش از حد طولانی است.
- دستههای مجاز خالی هستند.
- داده مرجع وجود ندارد.
- خروجی با قواعد کسبوکار سازگار نیست.
پیام خام Provider، Prompt داخلی یا جزئیات زیرساخت را مستقیماً به کاربر نمایش ندهید. جزئیات لازم را در Log داخلی ثبت و یک پیام عمومی مناسب برگردانید.
Retry را در چند لایه تکرار نکنید
ممکن است Retry در این بخشها وجود داشته باشد:
- OpenAI Client
- Instructor Validation
- Service Layer
- Queue Worker
- API Gateway
اگر هر لایه چند بار Retry کند، تعداد واقعی فراخوانیها میتواند بهشکل غیرمنتظره افزایش یابد.
برای طراحی مناسب:
- Retry خروجی نامعتبر را به Instructor بسپارید.
- Retry خطاهای موقت شبکه را در یک لایه مشخص انجام دهید.
- برای هر درخواست Timeout کلی تعیین کنید.
- تعداد تلاشها را ثبت کنید.
- برای خطاهای دائمی Retry انجام ندهید.
- هزینه تمام تلاشها را محاسبه کنید.
انتخاب مدل مناسب در درواره
برای Instructor، فقط کیفیت پاسخ متنی اهمیت ندارد. مدل باید بتواند Schema را با ثبات مناسبی رعایت کند.
معیارهای انتخاب مدل:
- کیفیت تولید JSON
- پشتیبانی از Structured Output
- رعایت فیلدهای ضروری
- کیفیت زبان فارسی
- عملکرد روی ساختارهای Nested
- دقت طبقهبندی
- نرخ Validation موفق
- تعداد Retry
- سرعت پاسخ
- هزینه
- Context Window
مدلها را روی Dataset یکسان آزمایش کنید. برای مثال:
TEST_CASES = [
{
"input": "درخواستهای API من Timeout میشوند.",
"expected_category": "technical",
},
{
"input": "مبلغ شارژ کیف پول ثبت نشده است.",
"expected_category": "billing",
},
{
"input": "لطفاً خروجی Excel اضافه کنید.",
"expected_category": "feature_request",
},
]
فهرست و قیمت جاری مدلها در صفحه مدلهای درواره در دسترس است.
تست بدون فراخوانی مدل
بخشهایی را که به LLM وابسته نیستند جداگانه تست کنید.
نمونه تست Schema:
from schemas import TicketAnalysis
def test_ticket_analysis_schema():
result = TicketAnalysis(
category="technical",
priority="high",
summary="درخواستهای API با Timeout مواجه میشوند.",
requires_human_review=True,
confidence=0.9,
)
assert result.category == "technical"
assert result.priority == "high"
نمونه تست Validator:
import pytest
from pydantic import ValidationError
def test_invalid_confidence():
with pytest.raises(ValidationError):
TicketAnalysis(
category="technical",
priority="high",
summary="نمونه پیام معتبر",
requires_human_review=False,
confidence=2,
)
در تست Endpoint نیز تابع analyze_ticket را Mock کنید تا Unit Test به مدل واقعی و هزینه API وابسته نباشد.
Evals روی مدل واقعی
Unit Test ساختار برنامه را بررسی میکند، اما کیفیت مدل را نمیسنجد. برای این کار به Evals نیاز دارید.
معیارهای مناسب:
- نرخ خروجی معتبر
- دقت دستهبندی
- دقت استخراج فیلدها
- نرخ مقادیر ساختگی
- تعداد Retry
- Latency
- Token مصرفی
- هزینه هر نمونه
- کیفیت فارسی
- نرخ ارجاع صحیح به انسان
Dataset باید شامل نمونههای متنوع باشد:
- متن کامل
- متن ناقص
- متن مبهم
- چند موضوع در یک پیام
- غلط املایی
- فارسی محاورهای
- ترکیب فارسی و انگلیسی
- پیام بسیار کوتاه
- پیام طولانی
- نمونه خارج از دستههای تعریفشده
معماری پیشنهادی Production
کاربر یا سیستم داخلی
↓
API Gateway
↓
احراز هویت و Rate Limit
↓
FastAPI
↓
Extraction Service
↓
Instructor
↓
OpenAI-compatible Client
↓
API درواره
↓
مدل انتخابشده
پس از دریافت خروجی:
خروجی Pydantic
↓
قواعد کسبوکار
↓
بررسی داده مرجع
↓
ذخیره در پایگاه داده
↓
Workflow یا بررسی انسانی
Instructor باید در لایه Backend قرار گیرد. فرانتاند فقط به API برنامه شما متصل میشود.
ساختار پیشنهادی پروژه بزرگتر
app/
├── api/
│ └── routes/
│ └── extraction.py
├── schemas/
│ ├── tickets.py
│ ├── invoices.py
│ └── products.py
├── services/
│ ├── instructor_client.py
│ ├── ticket_extractor.py
│ └── invoice_extractor.py
├── validators/
│ ├── categories.py
│ └── evidence.py
├── repositories/
│ ├── ticket_repository.py
│ └── product_repository.py
├── evals/
│ ├── datasets/
│ └── run_evals.py
├── core/
│ ├── config.py
│ ├── logging.py
│ └── exceptions.py
└── main.py
Schemaهای مختلف را داخل یک فایل بزرگ قرار ندهید. هر دامنه مانند تیکت، فاکتور و محصول بهتر است مدلها و Validationهای مستقل داشته باشد.
بهینهسازی هزینه
Structured Output ممکن است به دلیل Retry یا Schema طولانی، مصرف بیشتری نسبت به یک درخواست متنی ساده داشته باشد.
برای کنترل هزینه:
- Schema را فقط به فیلدهای ضروری محدود کنید.
- Descriptionها را روشن اما کوتاه بنویسید.
- تعداد Retry را محدود کنید.
- متنهای طولانی را پیش از ارسال بخشبندی کنید.
- برای وظایف ساده از مدل متناسب استفاده کنید.
- پاسخ خام طولانی درخواست نکنید.
- Dataset واقعی برای مقایسه مدلها بسازید.
- نرخ موفقیت در اولین تلاش را ثبت کنید.
- درخواستهای تکراری را Cache کنید.
- عملیات قابلحل با Regex یا کد را به LLM نسپارید.
- برای هر نوع استخراج سقف هزینه تعیین کنید.
اگر یک مقدار با یک Parser قطعی و ساده قابل استخراج است، استفاده از LLM ضرورت ندارد.
چه زمانی از Instructor استفاده کنیم؟
Instructor برای این کاربردها مناسب است:
- استخراج اطلاعات از متن
- طبقهبندی تیکت
- تحلیل بازخورد مشتری
- تبدیل متن به JSON
- استخراج اطلاعات محصول
- پردازش رزومه
- ساخت Metadata
- استخراج موجودیتها
- تحلیل فرمهای متنی
- تبدیل درخواست کاربر به تنظیمات برنامه
- ایجاد خروجی سازگار با پایگاه داده
- تولید پاسخ Type-safe برای Backend
چه زمانی استفاده نکنیم؟
ممکن است Instructor انتخاب مناسبی نباشد اگر:
- فقط پاسخ متنی برای نمایش به کاربر نیاز دارید.
- ساختار با Regex یا Parser ساده قابل استخراج است.
- Workflow به Tool Calling پیچیده نیاز دارد.
- Agent باید چند مرحله تصمیمگیری کند.
- خروجی کاملاً قطعی است و LLM ضرورتی ندارد.
- مدل انتخابشده Schema را با ثبات کافی رعایت نمیکند.
- Latency بسیار پایین مهمتر از Validation و Retry است.
برای Agentهای چندمرحلهای، PydanticAI یا فریمورکهای Agent ممکن است مناسبتر باشند.
اشتباهات رایج
تعریف Schema بسیار پیچیده
مدل Nested با تعداد زیادی فیلد اختیاری و شرطی میتواند نرخ خطا را افزایش دهد. خروجی پیچیده را به چند مرحله تقسیم کنید.
اجباریکردن اطلاعات ناموجود
اگر داده ممکن است در متن وجود نداشته باشد، فیلد را Optional تعریف کنید. در غیر این صورت مدل ممکن است مقدار بسازد.
اعتماد کامل به Validation
Pydantic معتبر بودن شکل داده را بررسی میکند، نه صحت واقعی آن را.
Retry بیش از حد
Retry زیاد هزینه و زمان پاسخ را افزایش میدهد و ممکن است ضعف مدل یا Schema را پنهان کند.
Descriptionهای مبهم
فیلدهایی مانند value, status و data بدون توضیح کافی، مدل را دچار ابهام میکنند.
استفاده از مدل بدون Eval
عملکرد مناسب روی چند نمونه دستی برای Production کافی نیست.
ترکیب استخراج و تصمیم نهایی
مدل میتواند اولویت یا دسته را پیشنهاد کند، اما عملیات مهم باید تحت کنترل قواعد Backend باشد.
استفاده از Client همزمان در FastAPI
در Endpointهای Async از AsyncOpenAI و Client غیرهمزمان Instructor استفاده کنید.
ذخیره مستقیم خروجی در پایگاه داده
پیش از ذخیره، قواعد کسبوکار، ارتباط رکوردها و محدودیتهای پایگاه داده را نیز بررسی کنید.
چکلیست انتشار
- هدف استخراج دقیقاً مشخص شده است.
- مدل Pydantic فقط فیلدهای ضروری را دارد.
- Description تمام فیلدها روشن است.
- مقادیر محدود با Enum یا Literal تعریف شدهاند.
- فیلدهای ناموجود Optional هستند.
- Validation ساختاری و محتوایی از هم تفکیک شدهاند.
- تعداد Retry محدود شده است.
- Timeout کلی وجود دارد.
- خطاهای Provider مدیریت میشوند.
- مدل و Prompt نسخهبندی شدهاند.
- Dataset ارزیابی ساخته شده است.
- کیفیت زبان فارسی آزمایش شده است.
- نرخ خروجی معتبر اندازهگیری میشود.
- تعداد Retry و هزینه ثبت میشوند.
- خروجی پیش از ذخیره با قواعد Backend بررسی میشود.
- کلید API در متغیر محیطی نگهداری میشود.
- Client غیرهمزمان در FastAPI استفاده میشود.
- مسیر بررسی انسانی برای نمونههای مبهم وجود دارد.
پرسشهای متداول
Instructor چیست؟
Instructor کتابخانهای برای دریافت خروجی ساختاریافته، معتبر و Type-safe از مدلهای زبانی با استفاده از Pydantic است.
آیا Instructor یک مدل هوش مصنوعی است؟
خیر. Instructor یک کتابخانه میان برنامه شما و API مدل است. مدل هوش مصنوعی از طریق Provider یا API درواره فراخوانی میشود.
آیا میتوان Instructor را به API درواره متصل کرد؟
بله. با ساخت یک نمونه OpenAI یا AsyncOpenAI و تنظیم base_url روی https://api.darvareh.ir/v1 میتوان از Instructor همراه با API درواره استفاده کرد.
response_model چیست؟
response_model یک کلاس Pydantic است که شکل، نوع فیلدها و قواعد Validation خروجی مدل را مشخص میکند.
آیا Instructor همیشه JSON معتبر برمیگرداند؟
Instructor پاسخ را Parse و Validate میکند و میتواند در صورت نامعتبر بودن Retry انجام دهد؛ اما موفقیت نهایی به مدل، Mode، Prompt و پیچیدگی Schema وابسته است.
تفاوت Instructor با درخواست «فقط JSON بده» چیست؟
Instructor علاوه بر درخواست JSON، پاسخ را با مدل Pydantic بررسی، خطاهای ساختاری را شناسایی و در صورت تنظیم Retry برای اصلاح به مدل بازمیگرداند.
آیا Instructor از زبان فارسی پشتیبانی میکند؟
Instructor به زبان خاصی وابسته نیست. کیفیت استخراج فارسی به مدل انتخابشده و طراحی Prompt بستگی دارد.
آیا Instructor از Async پشتیبانی میکند؟
بله. میتوان آن را همراه با AsyncOpenAI در FastAPI و برنامههای غیرهمزمان استفاده کرد.
آیا Instructor صحت اطلاعات را تضمین میکند؟
خیر. Instructor اعتبار ساختاری داده را بررسی میکند. صحت واقعی اطلاعات باید با منبع، پایگاه داده، قواعد کسبوکار یا بررسی انسانی تأیید شود.
Instructor بهتر است یا PydanticAI؟
برای استخراج مستقیم داده ساختاریافته، Instructor معمولاً سادهتر است. برای Agentهای دارای Tool، Dependency Injection و اجرای چندمرحلهای، PydanticAI امکانات گستردهتری دارد.
آیا همه مدلهای درواره با Instructor سازگارند؟
قابلیت مدلها متفاوت است. مدلی را انتخاب کنید که تولید JSON یا Structured Output قابلقبولی داشته باشد و آن را روی Dataset پروژه آزمایش کنید.
جمعبندی
Instructor راهکاری سبک و کاربردی برای تبدیل پاسخ مدلهای زبانی به اشیای معتبر Pydantic است. بهجای Parse کردن دستی JSON، مدیریت فیلدهای حذفشده و نوشتن Retryهای پراکنده، میتوان Schema خروجی را بهشکل یک مدل پایتون تعریف کرد.
در این آموزش یاد گرفتیم چگونه:
- Instructor را نصب کنیم.
- آن را به API درواره متصل کنیم.
- خروجی Type-safe دریافت کنیم.
- فهرستها و مدلهای تودرتو بسازیم.
- Validator اختصاصی تعریف کنیم.
- Retry را مدیریت کنیم.
- Validation Context به برنامه اضافه کنیم.
- Client غیرهمزمان بسازیم.
- Instructor را با FastAPI ترکیب کنیم.
- برای تست، Evals و Production آماده شویم.
برای شروع، یک کاربرد محدود مانند طبقهبندی تیکت یا استخراج اطلاعات محصول انتخاب کنید. ابتدا Schema سادهای بسازید و سپس مدلهای مختلف را روی Dataset واقعی خود از نظر دقت، نرخ Validation موفق، تعداد Retry، هزینه و Latency مقایسه کنید.
برای دریافت کلید API و استفاده از مدلهای مختلف هوش مصنوعی میتوانید به درواره مراجعه کنید. شناسه مدلها، قابلیتها و قیمت جاری آنها نیز در صفحه مدلهای درواره در دسترس است.
منابع
- مستندات رسمی Instructor
- راهنمای رسمی Structured Outputs با OpenAI Client
- مستندات Validation در Instructor
- راهنمای Retry Mechanisms
- راهنمای Reask و Context Validation
- API Reference رسمی Instructor
- مخزن رسمی Instructor در GitHub
- صفحه رسمی Instructor در PyPI
- مستندات رسمی Pydantic
مقالات مرتبط
- PydanticAI چیست؟ آموزش ساخت AI Agent با پایتون و API درواره
- آموزش Structured Output و JSON Schema
- Structured Outputs چیست؟
- API سازگار با OpenAI چیست؟
- ساخت AI Agent با Python، FastAPI و درواره
- Tool Calling چیست؟
- Function Calling چیست؟
- آموزش کامل FastAPI با پایتون و هوش مصنوعی
- ارزیابی مدلهای هوش مصنوعی و Evals
- کاهش هزینه استفاده از API هوش مصنوعی
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.