0Pricing
AI Engineering Academy · Lekcja

Obsługa wywołań narzędzi w strumieniowanych odpowiedziach

Parsuj strumieniowane odpowiedzi zawierające argumenty wywołania funkcji napływające token po tokenie, buforuj fragmenty JSON i uruchamiaj narzędzie dopiero po zakończeniu wywołania.

Obsługa wywołań narzędzi w strumieniowanych odpowiedziach to bezpłatna lekcja AI Engineering Academy na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Engineering Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Wywołania narzędzi docierają w strumieniach w inny sposób

Gdy LLM zdecyduje się wywołać funkcję, zmienia się struktura odpowiedzi. Zamiast ciągu content delta zawiera tablicę tool_calls. W strumieniowanej odpowiedzi argumenty wywołania funkcji docierają jednak token po tokenie jako fragmentaryczny ciąg JSON — kompletny obiekt JSON nie jest przesyłany w jednym fragmencie. Należy buforować te fragmenty i złożyć z nich kompletny JSON, zanim będzie można przeanalizować i wykonać wywołanie narzędzia.

# In a non-streaming response, tool call is complete:
# choice.message.tool_calls[0].function.arguments = '{"city": "Paris"}'

# In a streaming response, arguments arrive in pieces:
# chunk 1: delta.tool_calls[0].function.arguments = '{'
# chunk 2: delta.tool_calls[0].function.arguments = '"city"'
# chunk 3: delta.tool_calls[0].function.arguments = ': "'
# chunk 4: delta.tool_calls[0].function.arguments = 'Paris'
# chunk 5: delta.tool_calls[0].function.arguments = '"}'
# You must concatenate these before JSON.parse can work

Wykrywanie wywołania narzędzia w strumieniu

Sprawdzaj wartość finish_reason w każdym fragmencie, aby wiedzieć, kiedy oczekiwać wywołań narzędzi. Gdy finish_reason ma wartość 'tool_calls', model zdecydował o wywołaniu funkcji, a strumień dobiega końca. Gdy finish_reason ma wartość 'stop', model wygenerował zwykłą odpowiedź tekstową. Podczas strumieniowania sprawdzaj, czy chunk.choices[0].delta.tool_calls nie ma wartości None, aby rozpoznać fragmenty argumentów wywołań narzędzi.

async def detect_stream_type(messages, tools):
    stream = await async_client.chat.completions.create(
        model='gpt-4o-mini',
        messages=messages,
        tools=tools,
        stream=True,
    )
    response_type = 'text'
    async for chunk in stream:
        choice = chunk.choices[0]
        if choice.delta.tool_calls:  # tool call fragment arriving
            response_type = 'tool_call'
        if choice.finish_reason == 'tool_calls':
            print('Model wants to call a function')
        elif choice.finish_reason == 'stop':
            print('Normal text response')
    return response_type

Buforowanie argumentów wywołań narzędzi

Użyj słownika indeksowanego numerem wywołania narzędzia, aby gromadzić fragmenty argumentów z poszczególnych fragmentów. Indeks wskazuje, które wywołanie narzędzia jest strumieniowane — model może wywołać wiele funkcji w jednej odpowiedzi. Dla każdego fragmentu z niepustym przyrostem tool_calls dopisz fragment argumentu do odpowiedniego wpisu bufora oznaczonego indeksem wywołania narzędzia.

from collections import defaultdict

async def collect_streamed_tool_calls(messages, tools):
    stream = await async_client.chat.completions.create(
        model='gpt-4o-mini',
        messages=messages,
        tools=tools,
        stream=True,
    )

    tool_call_buffers = defaultdict(lambda: {'name': '', 'id': '', 'arguments': ''})
    text_buffer = ''

    async for chunk in stream:
        delta = chunk.choices[0].delta

        if delta.content:  # text content
            text_buffer += delta.content

        if delta.tool_calls:
            for tc in delta.tool_calls:
                idx = tc.index
                if tc.id:
                    tool_call_buffers[idx]['id'] = tc.id
                if tc.function.name:
                    tool_call_buffers[idx]['name'] += tc.function.name
                if tc.function.arguments:
                    tool_call_buffers[idx]['arguments'] += tc.function.arguments

    return text_buffer, dict(tool_call_buffers)

Analizowanie i wykonywanie wywołań narzędzi

Po zakończeniu strumienia i uzyskaniu kompletnych ciągów argumentów przeanalizuj każdy z nich za pomocą json.loads i przekaż go do odpowiedniej funkcji języka Python. Wykonuj wywołania narzędzi w kolejności, w jakiej zostały zlecone (lub równolegle, jeśli są niezależne), a następnie sformatuj wyniki jako komunikaty odpowiedzi narzędzi, które wyślesz w kolejnym wywołaniu API.

import json

# Example tool registry
tools_registry = {
    'get_weather': lambda city, unit='celsius': {'temp': 22, 'desc': 'sunny', 'city': city},
    'search_docs': lambda query, top_k=3: [{'title': 'Doc 1', 'snippet': 'Relevant info...'}],
}

def execute_tool_calls(tool_call_buffers: dict) -> list[dict]:
    tool_messages = []
    for idx in sorted(tool_call_buffers.keys()):
        tc = tool_call_buffers[idx]
        func_name = tc['name']
        args = json.loads(tc['arguments'])

        if func_name in tools_registry:
            result = tools_registry[func_name](**args)
        else:
            result = {'error': f'Unknown function: {func_name}'}

        tool_messages.append({
            'role': 'tool',
            'tool_call_id': tc['id'],
            'content': json.dumps(result),
        })
    return tool_messages

Pełna pętla strumieniowania wywołań narzędzi

Kompletny przepływ strumieniowania wywołań narzędzi wymaga pętli konwersacji wieloturowej. Pierwsze żądanie może zwrócić wywołania narzędzi; należy je wykonać i dopisać wyniki do historii wiadomości, a drugie żądanie zwróci końcową odpowiedź tekstową. Pętla może wykonać się wielokrotnie, jeśli model zdecyduje się wykonać dodatkowe wywołania narzędzi na podstawie wyników wcześniejszych wywołań.

async def streaming_agent_loop(initial_messages, tools):
    messages = list(initial_messages)
    max_iterations = 5

    for iteration in range(max_iterations):
        text, tool_calls = await collect_streamed_tool_calls(messages, tools)

        if tool_calls:
            # Append assistant message with tool calls
            assistant_msg = {
                'role': 'assistant',
                'content': text or None,
                'tool_calls': [
                    {'id': tc['id'], 'type': 'function',
                     'function': {'name': tc['name'], 'arguments': tc['arguments']}}
                    for tc in tool_calls.values()
                ]
            }
            messages.append(assistant_msg)

            # Execute tools and append results
            tool_results = execute_tool_calls(tool_calls)
            messages.extend(tool_results)
        else:
            # No more tool calls — final text response
            print('Final answer:', text)
            return text

    return 'Max iterations reached'

Strumieniowanie tekstu podczas buforowania wywołań narzędzi

W praktyce należy natychmiast strumieniować tekst do klienta, jednocześnie buforując argumenty ewentualnych wywołań narzędzi. Wymaga to rozróżniania fragmentów zawierających content (strumieniuj natychmiast) od fragmentów zawierających tool_calls (buforuj do późniejszego wykonania). Narzędzia można wykonać i kontynuować działanie dopiero po zakończeniu strumienia i zebraniu kompletnych wywołań narzędzi.

async def stream_with_tools(messages, tools):
    stream = await async_client.chat.completions.create(
        model='gpt-4o-mini', messages=messages, tools=tools, stream=True
    )
    tool_buffers = defaultdict(lambda: {'name': '', 'id': '', 'arguments': ''})
    text_parts = []

    async for chunk in stream:
        delta = chunk.choices[0].delta
        finish = chunk.choices[0].finish_reason

        if delta.content:
            text_parts.append(delta.content)
            yield ('text', delta.content)   # stream to client immediately

        if delta.tool_calls:
            for tc in delta.tool_calls:
                if tc.id:    tool_buffers[tc.index]['id'] = tc.id
                if tc.function.name: tool_buffers[tc.index]['name'] += tc.function.name
                if tc.function.arguments: tool_buffers[tc.index]['arguments'] += tc.function.arguments

        if finish == 'tool_calls':
            yield ('tool_calls', dict(tool_buffers))  # signal tool execution needed

Równoległe wykonywanie wywołań narzędzi

Gdy model zwraca jednocześnie wiele wywołań narzędzi (funkcja nazywana równoległym wywoływaniem funkcji), wykonuj je równolegle za pomocą asyncio.gather, zamiast robić to sekwencyjnie. Wykonywanie sekwencyjne niepotrzebnie zwiększa opóźnienie — jeśli model jednocześnie wywołuje API pogodowe i wyszukiwanie w bazie danych, nie ma powodu czekać na zakończenie jednego z nich przed rozpoczęciem drugiego.

import asyncio

async def execute_tool_calls_parallel(tool_call_buffers: dict) -> list[dict]:
    async def execute_one(idx, tc):
        func_name = tc['name']
        args = json.loads(tc['arguments'])
        if func_name in async_tools_registry:
            result = await async_tools_registry[func_name](**args)
        else:
            result = {'error': f'Unknown function: {func_name}'}
        return {
            'role': 'tool',
            'tool_call_id': tc['id'],
            'content': json.dumps(result),
        }

    tasks = [execute_one(idx, tc) for idx, tc in sorted(tool_call_buffers.items())]
    return await asyncio.gather(*tasks)

Strumieniowanie końcowej odpowiedzi po użyciu narzędzi

Po wykonaniu wywołań narzędzi i dopisaniu wyników do historii wiadomości wykonaj drugie żądanie strumieniowane, aby uzyskać końcową odpowiedź modelu. Przekazuj tę odpowiedź bezpośrednio do klienta w strumieniu. Ten schemat dwóch żądań (początkowe żądanie z wywołaniami narzędzi oraz kolejne żądanie z wynikami działania narzędzi) jest standardowym cyklem tury agenta, a oba żądania mogą strumieniować tekst do interfejsu użytkownika.

async def full_tool_calling_stream(question: str, tools: list):
    messages = [{'role': 'user', 'content': question}]

    # First request: may produce tool calls
    tool_buffers = {}
    text1 = ''
    async for event_type, data in stream_with_tools(messages, tools):
        if event_type == 'text':
            text1 += data
            yield data  # stream partial text if any
        elif event_type == 'tool_calls':
            tool_buffers = data

    if tool_buffers:
        # Execute tools, then get final streaming answer
        tool_results = await execute_tool_calls_parallel(tool_buffers)
        messages += [{  # assistant tool call message
            'role': 'assistant',
            'tool_calls': [
                {'id': tc['id'], 'type': 'function',
                 'function': {'name': tc['name'], 'arguments': tc['arguments']}}
                for tc in tool_buffers.values()
            ]
        }] + tool_results

        # Second request: final answer streams directly
        async for token in token_stream(messages):  # from earlier lesson
            yield token

Wyświetlanie postępu wywołań narzędzi w interfejsie użytkownika

Użytkownicy powinni widzieć co robi agent podczas oczekiwania na zakończenie wywołań narzędzi. Przed wykonaniem narzędzi wysyłaj strumieniowo do klienta zdarzenie statusu informujące, która funkcja jest wywoływana i z jakimi argumentami. Po wykonaniu wyślij status zakończenia. Taka przejrzystość znacznie poprawia postrzeganą responsywność i pomaga użytkownikom diagnozować nieoczekiwane użycie narzędzi.

import json

async def stream_with_progress(question, tools):
    messages = [{'role': 'user', 'content': question}]
    tool_buffers = {}

    async for event_type, data in stream_with_tools(messages, tools):
        if event_type == 'tool_calls':
            tool_buffers = data

    for tc in tool_buffers.values():
        args = json.loads(tc['arguments'])
        yield f'data: {json.dumps({"type": "tool_start", "function": tc["name"], "args": args})}\n\n'

        result = tools_registry.get(tc['name'], lambda **kw: {})(** args)

        yield f'data: {json.dumps({"type": "tool_done", "function": tc["name"]})}\n\n'

    # Then stream final answer...

Obsługa błędów w strumieniach wywołań narzędzi

Wykonanie narzędzia może się nie powieść — interfejsy API zwracają błędy, funkcje zgłaszają wyjątki, a analiza JSON może się nie udać. Zawsze przechwytuj wyjątki podczas wykonywania narzędzi i zwracaj modelowi ustrukturyzowaną odpowiedź błędu. Model może wtedy zdecydować o ponowieniu próby z innymi argumentami, wywołaniu narzędzia zastępczego lub wyjaśnieniu użytkownikowi, że żądana czynność się nie powiodła. Nigdy nie pozwalaj, aby nieprzechwycony wyjątek narzędzia zakończył działanie pętli strumieniowania.

def safe_execute_tool(func_name: str, args: dict) -> str:
    try:
        if func_name not in tools_registry:
            return json.dumps({'error': f'Function {func_name!r} not found'})
        result = tools_registry[func_name](**args)
        return json.dumps(result)
    except TypeError as e:
        return json.dumps({'error': f'Invalid arguments: {str(e)}'})
    except Exception as e:
        return json.dumps({'error': f'Execution failed: {str(e)}'})

# Tool result message with error handled
tool_message = {
    'role': 'tool',
    'tool_call_id': tc['id'],
    'content': safe_execute_tool(tc['name'], json.loads(tc['arguments'])),
}

Porównanie strumieniowania i braku strumieniowania w agentach

W aplikacjach agentowych strumieniowanie zwiększa złożoność, ale zapewnia znaczące korzyści w zakresie UX. Bez strumieniowania użytkownik nie widzi niczego podczas wieloetapowej pętli wywołań narzędzi, która może trwać od 10 do 30 sekund. Dzięki strumieniowaniu widzi tekst pośredni, powiadomienia o wywołaniach narzędzi oraz końcową odpowiedź pojawiającą się token po tokenie. Dodatkowa złożoność kodu jest zwykle warta poniesienia w aplikacjach interaktywnych, ale agenci działający autonomicznie w tle mogą używać braku strumieniowania, aby uprościć kod.

Szybkie sprawdzenie

Sprawdź swoją wiedzę na temat wywołań narzędzi w strumieniowanych odpowiedziach z tej lekcji.

Podsumowanie lekcji

W tej lekcji dowiedział się Pan / dowiedziała się Pani, że: argumenty wywołań narzędzi docierają w strumieniowanych odpowiedziach jako fragmenty JSON i przed analizą muszą być buforowane według indeksu wywołania narzędzia, po zakończeniu strumienia należy wykonać narzędzia, a następnie wysłać drugie strumieniowane żądanie po końcową odpowiedź, natomiast wykonywanie równoległe za pomocą asyncio.gather minimalizuje opóźnienie, gdy model jednocześnie wywołuje wiele funkcji. Zawsze przechwytuj błędy wykonywania narzędzi, aby zapobiec przerwaniu pętli agenta. W następnej części zaimplementujemy buforowanie odpowiedzi w celu obniżenia kosztów API.

Często zadawane pytania

Czy lekcja „Obsługa wywołań narzędzi w strumieniowanych odpowiedziach” jest bezpłatna?

Tak — pełny tekst „Obsługa wywołań narzędzi w strumieniowanych odpowiedziach” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Engineering Academy, przejdź na CoddyKit PRO. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Obsługa wywołań narzędzi w strumieniowanych odpowiedziach”?

Parsuj strumieniowane odpowiedzi zawierające argumenty wywołania funkcji napływające token po tokenie, buforuj fragmenty JSON i uruchamiaj narzędzie dopiero po zakończeniu wywołania. Ćwiczysz AI Engineering Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć AI Engineering Academy?

Nie wymagamy żadnego doświadczenia. AI Engineering Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Obsługa wywołań narzędzi w strumieniowanych odpowiedziach”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji AI Engineering Academy?

Tak. Każda lekcja AI Engineering Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Zrozumienie strumieniowania tokenów
  2. Obsługa strumieni za pomocą Python SDK
  3. Streaming w FastAPI z Server-Sent Events
  4. Obsługa wywołań narzędzi w strumieniowanych odpowiedziach
← Powrót do AI Engineering Academy