Strona główna / Artykuły / Rozróżnienie elementów a wiadomości w API odpowiedzi OpenAI

Rozróżnienie elementów a wiadomości w API odpowiedzi OpenAI

Wyjaśnia, w jaki sposób API Responses firmy OpenAI przekształca wyniki modelu na elementy zamiast wiadomości, oraz dlaczego ta zmiana ma znaczenie dla wywoływania narzędzi i prac w modelach agentowych.

1735 słów

Dopisywanie treści do czatu: oparte na wiadomościach

Dopisywanie treści do czatu od dawna jest standardowym formatem do komunikacji z modelami językowymi. Typowa interakcja wygląda w ten sposób:

response = client.chat.completions.create(
    model="...",
    messages=[
        {
            "role": "user",
            "content": "Explain RAG"
        }
    ]
)

Wysyłasz listę wiadomości, a model zwraca następną wiadomość asystenta. To dość proste. Z kolei API Responses od samego początku wygląda nieco inaczej.

response = client.responses.create(
    model="...",
    input="Explain RAG"
)

Na pierwszy rzut oka może to wydawać się po prostu innym punktem końcowym, w którym input zastępuje messages. Jednak prawdziwa różnica leży w abstrakcji, którą wykorzystuje każde z tych API. Dopisywanie treści do czatu opiera się na wiadomościach, natomiast API Responses jest zorganizowane wokół elementów i odpowiedzi. Gdy do gry wchodzą narzędzia i agenci, ta różnica staje się znacznie ważniejsza.

Rozmowa w ramach Chat Completions to po prostu tablica obiektów wiadomości.

messages = [
    {
        "role": "system",
        "content": "Act as a helpful AI assistant."
    },
    {
        "role": "user",
        "content": "What is a vector database?"
    }
]

Każda wiadomość zawiera rolę oraz pewien treść.

Typowe role, które można zobaczyć, to:

  • system
  • user
  • assistant

Ruch w takiej rozmowie można sobie wyobrazić w ten sposób:

Messages (list)-> Model -> Assistant Message

Model przyjmuje dotychczasową rozmowę i generuje następną wiadomość asystenta. Minimalny cykl rozmowy wygląda w ten sposób:

# Human message appended to the messages list
messages.append({
    "role": "user",
    "content": user_input
})
response = client.chat.completions.create(
    model="...",
    messages=messages
)
# AI message appended to the messages list
messages.append(
    response.choices[0].message
)

Pliki aplikacji są odpowiedzialne za przechowywanie historii wiadomości, czy to w pamięci, w bazie danych, lub w dowolny inny sposób wybrany przez użytkownika. Każda nowa wiadomość od użytkownika jest dodawana do tej listy, cała lista jest wysyłana do modelu, a odpowiedź modelu jest z kolei dodawana z powrotem. Ten wzorzec naturalnie odpowiada sposobowi działania interfejsów czatowych.

User Message (str)->
Message History (list[dict])->
Model (llm)->
Assistant Message (str)->
Message History (list[dict])

Dla generowania zwykłego tekstu oraz typowych przypadków użycia w stylu czatu ta konfiguracja jest w pełni wystarczająca. Jednak jej ograniczenia zaczynają się ujawniać, gdy aplikacja oparta na LLM musi robić coś więcej niż tylko prowadzić rozmowę.

Aplikacje LLM = nie tylko aplikacje do czatowania

Weźmy taką prośbę:

Znajdź najnowsze osiągnięcia w dziedzinie sztucznej inteligencji, stwórz podsumowanie tych najważniejszych i wyślij to podsumowanie do mojej skrzynki pocztowej.

Aby spełnić tę prośbę, model musi uruchomić wyszukiwanie w internecie oraz połączyć się z usługą e-mail.

Nagle ścieżka wykonywania obejmuje coś więcej niż zwykłą wymianę wiadomości między użytkownikiem a asystentem.

User Request ->
Model ->
Web Search ->
Search Results ->
Summarize (Model)->
Send Mail ->
Final Response

Bardziej złożone aplikacje mogą polegać na kilku różnych narzędziach połączonych ze sobą łańcuchem.

W tym momencie model jest aktywnym uczestnikiem szerszego procesu realizacji, a nie tylko generatorem tekstu. Część tego, co wytwarza, nigdy nie ma być dostarczana końcowemu użytkownikowi — istnieje wyłącznie w celach przetwarzania wewnętrznego. Model może wywołać narzędzie, a wynik tego wywołania może wymagać dalszej obróbki, co może skutkować kolejnym wywołaniem narzędzia. Dopiero gdy wszystko to zostanie rozwiązane, model wytwarza ostateczną odpowiedź, a nawet ta odpowiedź może nie przybrać postaci wiadomości tekstowej.

Przepływ ten nie może już być sprowadzony do:

Messages (list)-> Model -> Assistant Message

Obecnie istnieją pośrednie wyniki oraz działania na poziomie aplikacji, które muszą zostać zachowane jako część kontekstu, i właśnie w tym miejscu abstrakcja oparta wyłącznie na wiadomościach zaczyna okazywać się zbyt wąska.

Każdy wynik modelu != wiadomość

Gdy model zostanie połączony z narzędziami, większość tego, co wytwarza, to wywołanie funkcji, a nie tekst konwersacyjny.

Weźmy to jako przykład:

Function Call
Name: get_weather
Arguments:
{
    "city": "Bengaluru"
}

To nie jest coś przeznaczonego do czytania przez użytkownika — to instrukcja skierowana do twojej aplikacji. Twój kod uruchamia odpowiednią funkcję i przekazuje wynik z powrotem do modelu, a ten wynik może być lub nie być przeznaczony dla użytkownika.

W praktyce model może wytwarzać podczas działania co najmniej dwa różne rodzaje wyjść:

  1. Wywołanie funkcji
  2. Wiadomość

Łączenie tych dwóch elementów pod jednym nazwaniem „wiadomość asystenta” nie odzwierciedla tego, co faktycznie dzieje się podczas wykonywania. To niezgodność stanowi główny problem projektowy, który ma rozwiązać API Responses.

API Responses: inna abstrakcja

Zamiast być zorganizowaną wokół wymiany wiadomości, API Responses opiera się na koncepcji odpowiedzi, która może łączyć w sobie kilka elementów wyjściowych.

response = client.responses.create(
    model="...",
    input="Explain LangGraph"
)
print(response.output)
# response.output is a list of output items.

Dla prostej instrukcji tekstowej wyjściem może być pojedyncza wiadomość. Jednak w przypadku zadań wymagających użycia agentów lub narzędzi, odpowiedź może zawierać kilka różnych typów elementów. Uproszczony widok tej struktury wygląda następująco:

Response
| Reasoning Item
| Function Call Item
| Message Item

W tym modelu wiadomość staje się tylko jednym z kilku możliwych typów wyjściowych, a nie całością tego, co reprezentuje odpowiedź.

Różnica:

Różnica:

Messages (list)-> Model -> Assistant Message

Dopisywanie treści w czacie

Input -> Model -> Response

Response:
| Output Item
| Output Item
| Output Item

API Responses

W przypadku Chat Completions to sama rozmowa jest modelem, natomiast API Responses traktuje wykonywanie modelu jako odpowiedź składającą się z elementów wynikowych. Przy prostym uzupełnianiu tekstu ta różnica prawie nie ma znaczenia. Staje się istotna, gdy pojawiają się narzędzia, modele zdolne do rozumowania lub zintegrowane procesy pracy.

Wiadomości vs elementy

Prawdziwa różnica między tymi dwoma API objawia się w sposobie, w jaki każde z nich strukturuje swoją odpowiedź.

W Chat Completions wszystko skupia się wokół wiadomości.

print(response.choices[0].message.content)
# The generated text is inside the assistant message.
# response
# | choices
#     | message
#         | content

API Responses organizuje swoje wyniki inaczej.

print(response.output)
# A simplified structure:
# response
# | output
#   | reasoning
#   | function_call
#   | message
# The important difference is that output is not a list of messages,
# but output items.

Wiadomość to tylko jeden rodzaj elementu; wywołanie funkcji to inny rodzaj; modele obsługujące rozumowanie mogą również wytwarzać elementy oparte na rozumowaniu. To zmienia sposób, w jaki postrzegamy to, co model faktycznie zwraca.

Chat Completions
Model Output = Assistant Message

Responses API
Model Output = List of Output Items

- The structure which is useful for tool calling.

Wywołania narzędzi jako elementy wynikowe

Weźmy prostą funkcję, która podaje informacje o pogodzie (klasyczny przykład używany wszędzie).

def get_weather(city: str):
    return f"The weather in {city} is 28°C"
# The weather is ofcoure hardcoded.

Można udostępnić tę funkcję modelowi jako definicję narzędzia.

tools = [
    {
        "type": "function",
        "name": "get_weather",
        "description": "Get the current weather for a city",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string"
                }
            },
            "required": ["city"]
        }
    }
]

Ta definicja narzędzia zostaje włączona do Twojego żądania.

response = client.responses.create(
    model="...",
    tools=tools,
    input="What is the weather in Bengaluru?"
)

Od tego momentu model ma dwa możliwe kierunki działania.

Option 1: Generate a message
Option 2: Call get_weather

Ponieważ rzeczywista wartość pogody nie jest wbudowana w definicję funkcji, model potrzebuje aktualnych danych, dlatego wysyła element wywołania funkcji zamiast odpowiadać bezpośrednio.

Function Call Item
name: get_weather
arguments:
{
    "city": "Bengaluru"
}

Można znaleźć to wywołanie funkcji, przeglądając elementy wynikowe odpowiedzi.

for item in response.output:
    if item.type == "function_call":
        print(item.name)
        print(item.arguments)

Uruchomienie tego daje:

get_weather
{"city":"Bengaluru"}

W tym momencie model jeszcze nie wydał ostatecznej odpowiedzi – poprosił jedynie o wykonanie określonej akcji; uruchomienie samej funkcji należy do Twojej aplikacji.

result = get_weather("Bengaluru")

Ten wynik musi następnie zostać zwrócony do modelu.

Wynik wywołania funkcji

Wynik działania narzędzia reprezentuje się za pomocą elementu typu function_call_output.

tool_output = {
    "type": "function_call_output",
    "call_id": item.call_id,
    "output": result
}

Pole call_id łączy ten wynik z konkretnym wywołaniem funkcji, które je zażądało.

Function Call
| call_id: call_123
Application Executes Tool
Function Call Output
| call_id: call_123

To dopasowanie staje się niezbędne, gdy jednocześnie uruchamia się kilka narzędzi, na przykład zapytanie o pogodę w dwóch różnych miastach jednocześnie.

Bazowy cykl wykonywania narzędzi

Aplikacje zbudowane wokół narzędzi zazwyczaj działają według powtarzalnego cyklu.

User Input ->
Model ->
Response Output Items ->
Check for Function Calls ->
Execute Functions ->
Create Function Call Outputs ->
Model ->
Final Response

Oto w przybliżeniu, jak to wygląda w kodzie.

response = client.responses.create(
    model="...",
    input=user_input,
    tools=tools
)
while True:
    function_calls = [
        item
        for item in response.output
        if item.type == "function_call"
    ]
    if not function_calls:
        break
    tool_outputs = []
    for call in function_calls:
        result = execute_tool(
            call.name,
            call.arguments
        )
        tool_outputs.append({
            "type": "function_call_output",
            "call_id": call.call_id,
            "output": result
        })
    response = client.responses.create(
        model="...",
        previous_response_id=response.id,
        input=tool_outputs,
        tools=tools
    )

Ten cykl będzie się powtarzał tak długo, jak model będzie zwracał wywołania funkcji. Gdy przestanie żądać ich wykonania, odpowiedź będzie zawierać ostateczny wynik modelu.

Model ->
Function Call ->
Tool Result ->
Model ->
Function Call ->
Tool Result ->
Model ->
Message

To pętlo stanowi podstawę większości implementacji agentów wywołujących narzędzia.

Wniosek

API Responses to nie jest po prostu przebudowaną interfejsem Chat Completions. Oferuje strukturę, która dokładniej odzwierciedla sposób działania współczesnych aplikacji opartych na LLM, gdy zaangażowane są narzędzia, rozumowanie oraz różne typy wyjść. Chat Completions pozostaje solidnym wyborem dla prostych przypadków użycia w rozmowach. Jednak gdy proces pracy zaczyna przybierać charakter agencji, API Responses jest lepszym rozwiązaniem. Wiadomości zajmują się rozmową; elementy – wykonywaniem zadań. To właśnie jest cała idea.

Literatura pokrewna