Головна / Статті / Цикл чату без стану: вручну виклик API OpenAI у Python

Цикл чату без стану: вручну виклик API OpenAI у Python

Створіть багатокроковий чат за допомогою Python SDK від OpenAI, самостійно керуючи історією повідомлень, та зрозумійте, чому той самий цикл лежить в основі функцій пам’яті та агентів у LangChain.

1167 слів

Фреймворки на кшталт LangChain створюють враження, ніби моделі чату мають пам’ять, але API, що лежить в основі, нічого не пам’ятає. Кожен виклик є незалежним, а „розмова“ — це список повідомлень, який ваш код щоразу перебудовує та пересилає знову. Якщо написати цей цикл вручну за допомогою Python SDK від OpenAI, можна чітко побачити, що автоматизують фреймворки агентів, чому вартість токенів зростає під час розмови та які помилки потрібно передбачати.

Що насправді є кінцевою точкою хостованої моделі

API від OpenAI — це проста структура: постачальник запускає модель на своїх GPU та надає можливість виконання запитів через HTTPS. Ви надсилаєте текст, модель генерує токени за звичайним алгоритмом наступного токена, і ви платите за кожен токен у обох напрямках. Це має три наслідки:

  1. Вона є безстановою. Нічого з попередніх запитів не зберігається, тому кожен запит мусить містити все, що модель повинна знати.
  • Це звичайний HTTP. SDK обгортає запит POST, тож у разі проблеми ви можете переглянути первинний трафік.
  • Ви купуєте токени, а не відповіді. Довгий запит коштує грошей при кожному виклику, який його містить.
  • Ніколи не включайте ключ API у вихідний код. Клучі можуть протекти через історію Git, скріншоти та спільні ноутбуки, а їх витік означає, що хтось інший буде витрачати гроші з вашого рахунку. SDK автоматично читає OPENAI_API_KEY з середовища, тому ваш скрипт зовсім не потребує коду для обробки ключа. Використання API оплачується окремо від підписки ChatGPT, і новим рахункам зазвичай потрібен невеликий авансований баланс.

    Ролі: формат спільних повідомлень

    Запит містить список повідомлень, кожне з яких має певну роль:

    • system містить ваші інструкції, які модель враховує більш уважно.
    • user містить те, що написав користувач.
  • assistant зберігає попередні відповіді моделі, а у агентах — її виклики інструментів.
  • Цей формат використовується у всій екосистемі. Claude та Gemini застосовують ту саму ідею з невеликими відмінностями, Ollama її імітує, а SystemMessage, HumanMessage та AIMessage у LangChain є класами, які представляють ці ролі. Провайдер спрощує список до однієї послідовності токенів перед генерацією, тож ролі насправді є частиною структурованої інженерії запитів.

    Один запит

    У першому прикладі створюється клієнт, який читає ключ з середовища, надсилає системну інструкцію разом із одним запитанням та виводить відповідь разом із кількістю токенів запиту та результату з поля usage:

    from openai import OpenAI
    
    client = OpenAI()  # key from env
    
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": "You are a concise "
                           "Python assistant.",
            },
            {
                "role": "user",
                "content": "Why resend the whole "
                           "chat history each call?",
            },
        ],
        temperature=0,
    )
    print(resp.choices[0].message.content)
    u = resp.usage
    print(u.prompt_tokens, u.completion_tokens)
    

    gpt-4o-mini — це недорога модель, підходяща для навчання; перейти на більшу модель можна просто змінивши одну налаштування, хоча назви та ціни моделей змінюються, тому перевіряйте актуальний список. Значення temperature=0 мінімізує випадковість у виборі результатів, що є оптимальним значенням за замовчуванням для відповідей на запитання та пізніше для агентів, які використовують інструменти. Фіксуйте usage після кожного виклику — це ваш показник витрат.

    Ведення розмови самостійно

    Оскільки сервер забуває все, історію розмови контролює ваш код: після кожного виклику він зберігає відповідь, додає наступне запитання та знову надсилає все це. Наведений нижче функціонал реалізується за допомогою списку msgs на рівні модуля, який починається з системного повідомлення:

    msgs = [{
        "role": "system",
        "content": "You are a concise assistant.",
    }]
    
    def ask(text: str) -> str:
        msgs.append(
            {"role": "user", "content": text}
        )
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=msgs,  # full history
            temperature=0,
        )
        reply = resp.choices[0].message.content
        msgs.append({
            "role": "assistant",
            "content": reply,
        })
        return reply
    
    print(ask("Define a context window."))
    print(ask("Now for a five-year-old."))
    print(ask("Which answer was shorter?"))
    

    Третє запитання доводить цю ідею. Модель може порівнювати лише два варіанти відповідей, оскільки обидва знаходяться у msgs та надсилаються знову. Якщо видалити рядок, який додає відповідь асистента, модель не зрозуміє, що ви маєте на увазі.

    Ця функція ask() з’являється у багатьох варіантах. Веб-додаток ChatGPT по суті є її версією з інтерфейсом користувача. Клас RunnableWithMessageHistory у LangChain — це керована версія процедур додавання та повторного надсилання інформації. Внутрішній цикл агента також працює за цим же принципом, з додаванням викликів інструментів та їх результатів. Зверніть увагу на витрати: три повторних надсилання перетворюються на одне чи два, тож кількість токенів вхідних даних зростає з кожною зміною.

    Запуск прикладу

    Встановіть необхідні залежності, експортуйте ключ у своїй оболонці та запустіть скрипт. Показаний ключ є замінником; використовуйте свій ключ через оболонку чи менеджер секретів, ніколи не через файл, який було збережено до комітування:

    pip install -r requirements.txt
    export OPENAI_API_KEY="sk-..."
    python examples/part02_chat.py
    

    Скрипт здійснює один виклик на один оборот, потім запускає розмову на три обороти, і після кожного запиту повідомляє про використання токенів та приблизну вартість. Він негайно зупиняється, якщо ключ відсутній, і навмисно не використовується у CI, оскільки це потребує реальних грошей та справжнього ключа. Якщо ви хочете написати тести для такого коду, створіть мок клієнта.

    Проблеми, які потрібно врахувати з самого початку

    1. Відсутній ключ: виникає AuthenticationError з кодом HTTP 401, зазвичай тому, що змінна не встановлена в цьому середовищі, має помилку написання або містить зайвий пробіл. Перевіряйте це під час запуску та швидко припиняйте роботу.
    2. Обмеження швидкості: RateLimitError з кодом HTTP 429 означає занадто багато запитів або порожній баланс передплати. Агенти, які працюють у циклі, також можуть стикнутися з цим, тому вже зараз додайте можливість повторних спроб із затримкою.
  • Історія дій не спрацьовує ефективно в обох випадках. Якщо забути додати нові дані, модель забуде все; якщо додавати їх постійно, витрати токенів зростатимуть квадратично, поки не виникне помилка через обмежену довжину контексту. Реальні системи видаляють, узагальнюють або отримують лише актуальну історію, і саме ця ідея є основою технології RAG.
  • Значення Temperature 0 забезпечує стабільний вихід, але не його коректність. Перевіряйте все, що має значення.
  • Однакова структура у всіх постачальниках

    Claude приймає список повідомлень користувача та асистента, причому системний запит переміщується у окремий параметр верхнього рівня. Gemini використовує ту саму модель розмови у вигляді списку, де ролі позначаються як user та model. Ollama пропонує кінцеву точку, сумісну з OpenAI, тож цей код може використовувати локальну модель шляхом зміни базової URL та назви моделі; див. виклик Claude, GPT та Gemini через кінцеві точки, сумісні з OpenAI. Саме ця конвергенція дозволяє LangChain надавати єдиний інтерфейс для багатьох постачальників.

    Основні висновки

    • API чату є безстановим; ваш код керує розмовою та пересилає її заново.
    • Список повідомлень із позначеннями ролей є фактично стандартом між постачальниками.
    • Фіксуйте дані про використання під час кожного виклику, оскільки кількість токенів вхідного даних зростає з кожним кроком розмови.
  • Зберігайте ключі в середовищі та уникайте використання живих API-запитів у процесі CI.
  • Обробляйте помилки 401, 429 та проблеми з необмеженою історією запитів ще до створення агентів, оскільки цикли агентів посилюють усі три проблеми.