برای تست ایجنت لازم نیست کلید API داشته باشید. ماژول agents.testing در نسخه‌ی 0.22.3 از openai-agents یک مدل اسکریپت‌شده می‌دهد که هر نوبت مدل را از پیش تعیین می‌کند، در حافظه اجرا می‌شود و هیچ درخواستی به شبکه نمی‌فرستد. با سه خط تست می‌نویسید، کل حلقه‌ی ابزار را می‌بینید و یک تغییر ناخواسته در گردش کار به‌جای پاسخ مبهم، خطای نام‌گذاری‌شده می‌دهد.

مسئله‌ی ارتباط با API چرا تست ایجنت را گران می‌کند

تست معمولی یک ایجنت سه وابستگی دارد: کلید API، اعتبار مصرف و شبکه. اولی را می‌شود در متغیر محیطی گذاشت، دومی با هر اجرا دوباره خرج می‌شود و سومی در خط لوله‌ی CI شکننده‌ترین حلقه است. نتیجه‌ی عملی این است که تست‌ها یا کند می‌شوند یا در تست شبانه‌ی ناموفق به دلیل خطای گذرا رد می‌شوند، و تیم کم‌کم دوباره تست دستی می‌نویسد.

راه دوم این است که اصلا مدل واقعی صدا زده نشود. آنچه در یک تست باید واقعا بررسی شود رفتار گردش کار است: آیا ابزار درست انتخاب شد، آیا آرگومان‌ها درست ساخته شدند، آیا خروجی ابزار به مدل برگشت، آیا حلقه در تعداد درستی از نوبت‌ها بسته شد. این‌ها همگی به کدی که شما نوشته‌اید تعلق دارند، نه به مدلی که فردا عوض می‌شود.

مستندات تست SDK دقیقا همین مرز را نام‌گذاری کرده‌اند: ابزارهای تست برای رفتاری هستند که به اپلیکیشن و SDK شما تعلق دارد، و برای رفتاری که به مدل بیرونی، پروتکل شبکه یا سیستم صوتی تعلق دارد باید از آداپتور واقعی پرووایدر استفاده کنید. تست یکپارچه‌ی واقعی جای خود را دارد؛ در کنارش، نه به‌جایش.

نصب و نسخه‌ی واقعی که خواندم

پکیج openai-agents روی PyPI با نام همین است و آخرین نسخه‌ی پایدار آن در لحظه‌ی خواندن من 0.22.3 بود که در ۱۷ سپتامبر ۲۰۲۶ منتشر شده است. لیسانس MIT است و حداقل نسخه‌ی پایتون 3.10 را می‌خواهد.

$ python -m venv .venv
$ source .venv/bin/activate
$ pip install openai-agents
Successfully installed openai-agents-0.22.3

نکته‌ای که در راهنمای شروع سریع نوشته نشده: نصب این یک بسته نیست. در اجرای من 38 توزیع در یک محیط تازه نشست، از جمله openai==3.22.1، pydantic==2.13.5، mcp==2.2.0 و httpx2==2.13.1. اگر پروژه‌ی شما همین حالا یکی از این‌ها را با نسخه‌ی دیگری دارد، نصب در همان محیط می‌تواند نسخه‌ی موجود را جابه‌جا کند. محیط جدا، ارزان‌ترین راه پیشگیری است.

بستهنسخه‌ی نصب‌شدهنقش
openai-agents0.22.3خود SDK
openai3.22.1کلاینت HTTP
pydantic2.13.5ساخت اسکیمای ورودی ابزار
mcp2.2.0لایه‌ی MCP
httpx22.13.1لایه‌ی HTTP
websockets16.1.1ترنسپورت بلادرنگ

لحظه ی خواندن: ۱۰ مهر ۱۴۰۵. این اعداد در محیط تازه‌ی من اندازه‌گیری شده‌اند، نه از فهرست وابستگی‌های PyPI.

حلقه‌ی ابزار را با دو نوبت اسکریپت کنید

کل ایده در یک نوع داده است: ScriptedModel که یک صف از نوبت‌های مدل نگه می‌دارد. هر نوبت یک ModelStep است. برای پاسخ متنی از assistant_message و برای فراخوانی ابزار از function_call استفاده می‌کنید. دو نکته‌ی فنی مهم است که در نمونه‌های قدیمی وب نیست: مدل از طریق run_config تزریق می‌شود نه با آرگومان مستقیم، و نام پارامتر ابزار call_id است و بین فراخوانی و خروجی جفت می‌ماند.

import asyncio

from agents import Agent, RunConfig, Runner, function_tool
from agents.testing import ScriptedModel, ModelStep, assistant_message, function_call


@function_tool
def get_stock_price(symbol: str) -> str:
    # قیمت ثابت تا تست کاملا قطعی بماند
    return '{"symbol": "%s", "close": 191.25}' % symbol


stock_agent = Agent(
    name="Stock Agent",
    instructions="Answer stock questions by calling get_stock_price.",
    tools=[get_stock_price],
)

# هر نوبت مدل از پیش معلوم است: اول ابزار، بعد پاسخ
model = ScriptedModel(steps=[
    ModelStep(output=[function_call("get_stock_price", {"symbol": "AAPL"}, call_id="c1")]),
    ModelStep(output=[assistant_message("AAPL closed at 191.25.")]),
])


async def main():
    run_config = RunConfig(model=model, tracing_disabled=True)
    result = await Runner.run(stock_agent, "Price of AAPL?", run_config=run_config)
    print("final:", result.final_output)
    print("calls:", len(model.calls))


asyncio.run(main())

خروجی واقعی این اجرا روی Python 3.14.7 بدون هیچ کلیدی و بدون دسترسی به شبکه چنین بود:

$ ./.venv/bin/python stock_agent.py
final: AAPL closed at 191.25.
calls: 2

عدد 2 را بخوانید، نه متن پاسخ را. دو یعنی مدل یک بار ابزار خواست و یک بار پاسخ نهایی داد. اگر روزی روی همان تست عدد ۱ شد، یعنی ایجنت دیگر ابزار را صدا نزده و مستقیم جواب داده، و این دقیقا همان تغییر رفتاری است که در تست دستی دیده نمی‌شود.

قوی‌ترین بخش این ابزار خود پاسخ نیست؛ تاریخچه‌ی درخواست‌هایی است که SDK به مرز مدل فرستاد. هر فراخوانی در model.calls به شکل یک ModelCall ثبت می‌شود و ورودی آن همان چیزی است که مدل می‌بیند. در اجرای بالا، فراخوانی دوم ورودی‌ای داشت که از سه بخش تشکیل شده بود: پیام کاربر، خود فراخوانی ابزار با call_id برابر c1، و خروجی ابزار.

$ ./.venv/bin/python -c "print(model.calls[-1].input)"
[{'content': 'Price of AAPL?', 'role': 'user'},
 {'arguments': '{"symbol":"AAPL"}', 'call_id': 'c1',
  'name': 'get_stock_price', 'type': 'function_call', 'id': 'c1'},
 {'call_id': 'c1',
  'output': '{"symbol": "AAPL", "close": 191.25}',
  'type': 'function_call_output'}]

همین ساختار دقیقاً جایی است که بیشتر تست‌های ایجنت ضعیف می‌شوند. آن‌ها فقط متن نهایی را می‌سنجند و هرگز بررسی نمی‌کنند که آیا ابزار اصلا فراخوانی شد یا با چه آرگومانی. اینجا شما هر سه بخش را می‌بینید و می‌توانید روی آن‌ها ادعا کنید. در مقایسه‌ی حلقه‌ی ابزار در دو SDK معیار فقط تعداد نوبت‌ها بود، چون ورودی هر نوبت در دسترس نبود.

تستی که باید قرمز شود

تستی که هیچ‌وقت قرمز نمی‌شود یعنی تستی که نیست. این ابزار دو حالت خرابی جدا را نام‌گذاری کرده و هر دو را اجرا کردم تا مطمئن شوم واقعا خطا می‌دهند و فقط پیام چاپ نمی‌کنند.

حالت اول: اسکریپت خالی است ولی ایجنت می‌خواهد نوبت بگیرد. SDK به‌جای یک پاسخ مبهم، استثنایی با نام مشخص پرتاب می‌کند و اجرای تست همان‌جا متوقف می‌شود.

$ ./.venv/bin/python stock_agent_negative.py
--- empty script ---
raised UnexpectedModelCall: Unexpected non-streaming model call #1: no scripted steps remain.

حالت دوم: اسکریپت دو نوبت دارد ولی ایجنت بعد از نوبت اول تمام می‌کند. اجرا سبز می‌ماند و فقط متد assert_complete آن را می‌گیرد.

$ ./.venv/bin/python stock_agent_negative.py
--- leftover step ---
final: AAPL closed at 191.25.
model calls: 1
remaining steps: 1
assert_complete raised UnconsumedModelSteps: 1 scripted model step(s) were not consumed.

تفاوت این دو حالت مهم است. خطای اول را SDK خودش پرتاب می‌کند و به‌عنوان خطای تست متوقف می‌شوید. خطای دوم پیش‌دستی نیاز دارد: اگر assert_complete را صدا نزنید، یک نوبت اسکریپت‌شده‌ی مصرف‌نشده بی‌سروصدا باقی می‌ماند و شما فکر می‌کنید پوشش کامل است. این دقیقا همان شکافی است که یک تست سبز ولی بی‌فایده می‌سازد، و دلیل اینکه این متد باید در انتهای هر تست بیاید نه در صورت تیک.

مرز کار را کجا بگذارید

این ابزار جایگزین تست یکپارچه نیست و نباید باشد. یک تقسیم کار که در اجرای من معنادار بود:

  • گردش کار، ابزار، handoff، گاردیل: با مدل اسکریپت‌شده تست کنید. این‌ها کد شما هستند و تست قطعی‌شان سریع‌تر و ارزان‌تر است.
  • سریال‌سازی درخواست به پرووایدر: با آداپتور واقعی و یک ترنسپورت شبکه‌ی کنترل‌شده تست کنید. مدل اسکریپت‌شده اصلا درخواستی نمی‌سازد، پس چیزی برای بررسی ندارد.
  • کیفیت پاسخ مدل: اصلا موضوع این ابزار نیست. این را با ارزیابی جداگانه انجام دهید، نه با انتظار ثابت در تست واحد.

همین نکته در راهنمای تست SDK هم آمده است و دلیل نوشتن این بخش را می‌سازد: مدل اسکریپت‌شده درباره‌ی گردش کار حرف می‌زند، نه درباره‌ی هوش. اگر از پاسخ اسکریپت‌شده‌ی خودتان انتظار دارید کیفیت مدل را بسنجد، دارید دقیقا همان اشتباهی را می‌کنید که این ابزار برای جلوگیری از آن ساخته شده.

یک تله‌ی عملی هم هست: ردیابی (tracing) به‌صورت پیش‌فرض فعال است. اگر یک کلید OpenAI در محیط داشته باشید، پردازنده‌ی پیش‌فرض ردیابی تلاش می‌کند داده‌ی تست را آپلود کند. در هر اجرای تست tracing_disabled=True را در RunConfig بگذارید؛ هم تست را سریع‌تر می‌کند و هم داده‌ی تست را از مسیر شما بیرون نمی‌فرستد.

جمع‌بندی

بزرگ‌ترین برد این کتابخانه این است که تست ایجنت را از وابستگی به شبکه جدا می‌کند. یک نصب، دوازده خط اسکریپت، و تستی که در هزارم ثانیه و بدون کلید اجرا می‌شود. دو حالت خرابی جدا هم نام‌گذاری شده‌اند: UnexpectedModelCall وقتی نوبتی بیشتر خواسته شود و UnconsumedModelSteps وقتی نوبتی کمتر مصرف شود.

مرز را روشن نگه دارید: آنچه SDK و اپلیکیشن شما مالکشان هستند با مدل اسکریپت‌شده تست می‌شود، و آنچه مال پرووایدر است با آداپتور واقعی. اگر مرز را رعایت کنید، تست شما هم سریع است و هم درباره‌ی چیزی حرف می‌زند که کنترلش می‌کنید.

منابع

  1. مستندات تست OpenAI Agents SDK: مرز ابزار و مرز پرووایدر — خوانده در ۱۰ مهر ۱۴۰۵
  2. مرجع کلاس‌های testing: ScriptedModel، ModelStep، ModelCall و استثناها — خوانده در ۱۰ مهر ۱۴۰۵
  3. متادیتای PyPI برای openai-agents: نسخه 0.22.3، تاریخ انتشار و وابستگی‌ها — خوانده در ۱۰ مهر ۱۴۰۵
  4. مخزن openai/openai-agents-python
  5. راهنمای شروع سریع SDK — خوانده در ۱۰ مهر ۱۴۰۵
  6. صفحه‌ی مدل‌ها و مرز پرووایدر در SDK — خوانده در ۱۰ مهر ۱۴۰۵
  7. مرجع ماژول asyncio در مستندات پایتون
  8. توکن و پنجره‌ی زمینه: عددهای پشت صورتحساب — برای اینکه چرا شمارش نوبت و توکن در تست یکسان نیست