0Pricing
AI Engineering Academy · บทเรียน

การจัดการการเรียกใช้เครื่องมือในการตอบกลับแบบต่อเนื่อง

แยกวิเคราะห์การตอบกลับแบบต่อเนื่องที่มีอาร์กิวเมนต์การเรียกฟังก์ชันเข้ามาทีละโทเค็น เก็บชิ้นส่วน JSON ไว้ในบัฟเฟอร์ และเริ่มเรียกใช้เครื่องมือเมื่อการเรียกเสร็จสมบูรณ์เท่านั้น

การจัดการการเรียกใช้เครื่องมือในการตอบกลับแบบต่อเนื่อง เป็นบทเรียน AI Engineering Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน AI Engineering Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส AI Engineering Academy มีบทเรียนทั้งหมด 4 บทเรียน

การเรียกใช้เครื่องมือมาถึงแตกต่างกันในสตรีม

เมื่อ LLM ตัดสินใจเรียกใช้ฟังก์ชัน โครงสร้างการตอบกลับจะเปลี่ยนไป แทนที่จะมีสตริง content เดลตาจะมีอาร์เรย์ tool_calls แต่ในการตอบกลับแบบสตรีม อาร์กิวเมนต์ของการเรียกใช้ฟังก์ชันจะมาทีละส่วน ทีละโทเค็นในรูปแบบสตริง JSON บางส่วน คุณจะไม่ได้รับออบเจ็กต์ JSON ที่สมบูรณ์ภายในชังก์เดียว ดังนั้นต้องบัฟเฟอร์ส่วนย่อยเหล่านี้และประกอบ JSON ที่สมบูรณ์ขึ้นมาใหม่ก่อนจึงจะแยกวิเคราะห์และเรียกใช้เครื่องมือได้

# 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

การตรวจจับการเรียกใช้เครื่องมือในสตรีม

ตรวจสอบ finish_reason ของแต่ละส่วนย่อยเพื่อดูว่าควรคาดว่าจะมีการเรียกใช้เครื่องมือเมื่อใด เมื่อ finish_reason เป็น 'tool_calls' แสดงว่าโมเดลตัดสินใจเรียกใช้ฟังก์ชันและสตรีมกำลังจะสิ้นสุดลง เมื่อ finish_reason เป็น 'stop' แสดงว่าโมเดลสร้างคำตอบข้อความตามปกติ ระหว่างการสตรีม ให้ตรวจสอบว่า chunk.choices[0].delta.tool_calls ไม่ใช่ค่า None เพื่อระบุส่วนย่อยของอาร์กิวเมนต์ในการเรียกใช้เครื่องมือ

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

การบัฟเฟอร์อาร์กิวเมนต์การเรียกใช้เครื่องมือ

ใช้พจนานุกรมที่มีดัชนีการเรียกใช้เครื่องมือเป็นคีย์ เพื่อรวบรวมส่วนย่อยของอาร์กิวเมนต์จากแต่ละส่วนย่อย ดัชนีจะระบุว่ากำลังสตรีมการเรียกใช้เครื่องมือรายการใดอยู่ เนื่องจากโมเดลอาจเรียกใช้หลายฟังก์ชันในคำตอบเดียว สำหรับแต่ละส่วนย่อยที่มีเดลตาของ tool_calls ซึ่งไม่เป็นค่าว่าง ให้เพิ่มส่วนย่อยของอาร์กิวเมนต์ลงในรายการบัฟเฟอร์ที่ตรงกัน โดยใช้ดัชนีการเรียกใช้เครื่องมือเป็นคีย์

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)

การแยกวิเคราะห์และดำเนินการเรียกใช้เครื่องมือ

หลังจากสตรีมสิ้นสุดลงและคุณมีสตริงอาร์กิวเมนต์ที่สมบูรณ์แล้ว ให้แยกวิเคราะห์แต่ละรายการด้วย json.loads และส่งต่อไปยังฟังก์ชัน Python ที่เหมาะสม ดำเนินการเรียกใช้เครื่องมือตามลำดับที่ร้องขอ หรือดำเนินการแบบขนานหากไม่ขึ้นต่อกัน จากนั้นจัดรูปแบบผลลัพธ์เป็นข้อความตอบกลับจากเครื่องมือเพื่อส่งกลับไปในการเรียก 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

ลูปการเรียกใช้เครื่องมือแบบสตรีมที่สมบูรณ์

กระบวนการเรียกใช้เครื่องมือแบบสตรีมที่สมบูรณ์จำเป็นต้องมี ลูปการสนทนาหลายรอบ คำขอแรกอาจส่งคืนการเรียกใช้เครื่องมือ คุณจะดำเนินการเรียกใช้เหล่านั้นและเพิ่มผลลัพธ์ลงในประวัติข้อความ จากนั้นคำขอที่สองจะส่งคืนคำตอบข้อความสุดท้าย ลูปนี้อาจทำซ้ำหลายครั้งหากโมเดลเลือกเรียกใช้เครื่องมือเพิ่มเติมโดยอิงจากผลลัพธ์ก่อนหน้า

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'

การสตรีมข้อความขณะบัฟเฟอร์การเรียกใช้เครื่องมือ

ในทางปฏิบัติ คุณควร สตรีมข้อความไปยังไคลเอ็นต์ทันที พร้อมกับบัฟเฟอร์อาร์กิวเมนต์ของการเรียกใช้เครื่องมือไปพร้อมกัน ซึ่งจำเป็นต้องแยกแยะระหว่างส่วนย่อยที่มี content ซึ่งควรสตรีมทันที กับส่วนย่อยที่มี tool_calls ซึ่งควรบัฟเฟอร์ไว้เพื่อดำเนินการภายหลัง คุณจะดำเนินการเครื่องมือและทำงานต่อได้ก็ต่อเมื่อสตรีมสิ้นสุดลงและการเรียกใช้เครื่องมือเสร็จสมบูรณ์แล้ว

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

การดำเนินการเรียกใช้เครื่องมือแบบขนาน

เมื่อโมเดลส่งคืนการเรียกใช้เครื่องมือหลายรายการพร้อมกัน ซึ่งเป็นความสามารถที่เรียกว่า การเรียกใช้ฟังก์ชันแบบขนาน ให้ดำเนินการแบบขนานด้วย asyncio.gather แทนการดำเนินการตามลำดับ การดำเนินการตามลำดับเพิ่มความหน่วงโดยไม่จำเป็น หากโมเดลเรียกใช้ API สภาพอากาศและค้นหาฐานข้อมูลพร้อมกัน ก็ไม่มีเหตุผลที่จะต้องรอให้รายการหนึ่งเสร็จก่อนจึงเริ่มอีกรายการ

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)

การสตรีมคำตอบสุดท้ายหลังใช้เครื่องมือ

หลังจากดำเนินการเรียกใช้เครื่องมือและเพิ่มผลลัพธ์ลงในประวัติข้อความแล้ว ให้ส่ง คำขอแบบสตรีมครั้งที่สอง เพื่อรับคำตอบสุดท้ายจากโมเดล สตรีมคำตอบนี้ไปยังไคลเอ็นต์โดยตรง รูปแบบสองคำขอนี้ ซึ่งประกอบด้วยคำขอเริ่มต้นที่มีการเรียกใช้เครื่องมือและคำขอติดตามผลที่มีผลลัพธ์จากเครื่องมือ เป็นวงรอบการทำงานมาตรฐานของเอเจนต์ และคำขอทั้งสองรายการสามารถสตรีมข้อความไปยังส่วนติดต่อผู้ใช้ได้

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

การแสดงความคืบหน้าของการเรียกใช้เครื่องมือในส่วนติดต่อผู้ใช้

ผู้ใช้ควรเห็น สิ่งที่เอเจนต์กำลังดำเนินการ ระหว่างรอให้การเรียกใช้เครื่องมือเสร็จสิ้น ก่อนดำเนินการเครื่องมือ ให้สตรีมเหตุการณ์สถานะไปยังไคลเอ็นต์ โดยระบุว่ากำลังเรียกใช้ฟังก์ชันใดและใช้อาร์กิวเมนต์อะไร หลังดำเนินการเสร็จแล้ว ให้สตรีมสถานะเสร็จสมบูรณ์ ความโปร่งใสนี้ช่วยเพิ่มความรู้สึกว่าระบบตอบสนองได้ดีขึ้นอย่างมาก และช่วยให้ผู้ใช้ตรวจสอบการใช้เครื่องมือที่ไม่คาดคิดได้

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...

การจัดการข้อผิดพลาดในสตรีมการเรียกใช้เครื่องมือ

การดำเนินการเครื่องมืออาจล้มเหลวได้ เช่น API ส่งคืนข้อผิดพลาด ฟังก์ชันโยนข้อยกเว้น หรือการแยกวิเคราะห์ JSON ล้มเหลว ให้ดักจับข้อยกเว้นระหว่างการดำเนินการเครื่องมือเสมอ และส่งคืนคำตอบข้อผิดพลาดที่มีโครงสร้างให้โมเดล จากนั้นโมเดลอาจตัดสินใจลองใหม่ด้วยอาร์กิวเมนต์อื่น เรียกใช้เครื่องมือสำรอง หรืออธิบายให้ผู้ใช้ทราบว่าการดำเนินการที่ร้องขอล้มเหลว อย่าปล่อยให้ข้อยกเว้นจากเครื่องมือที่ไม่ได้ดักจับทำให้ลูปการสตรีมล้มเหลว

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'])),
}

การเปรียบเทียบแบบสตรีมกับไม่สตรีมสำหรับเอเจนต์

สำหรับแอปพลิเคชันแบบเอเจนต์ การสตรีมเพิ่มความซับซ้อน แต่ก็มอบคุณค่าด้านประสบการณ์ผู้ใช้เป็นอย่างมาก หากไม่ใช้การสตรีม ผู้ใช้จะไม่เห็นสิ่งใดระหว่างลูปการเรียกใช้เครื่องมือหลายขั้นตอนที่อาจใช้เวลา 10–30 วินาที แต่เมื่อใช้การสตรีม ผู้ใช้จะเห็นข้อความระหว่างทาง การแจ้งเตือนการเรียกใช้เครื่องมือ และคำตอบสุดท้ายปรากฏขึ้นทีละโทเค็น โดยทั่วไป ความซับซ้อนของโค้ดที่เพิ่มขึ้นคุ้มค่าสำหรับแอปพลิเคชันแบบโต้ตอบ แต่เอเจนต์เบื้องหลังที่ทำงานโดยอัตโนมัติสามารถใช้แบบไม่สตรีมเพื่อให้โค้ดเรียบง่ายขึ้นได้

ตรวจสอบความเข้าใจ

ทดสอบความเข้าใจเกี่ยวกับการเรียกใช้เครื่องมือในคำตอบแบบสตรีมจากบทเรียนนี้

สรุปบทเรียน

ในบทเรียนนี้ คุณได้เรียนรู้ว่า อาร์กิวเมนต์การเรียกใช้เครื่องมือจะมาถึงในรูปแบบ ส่วนย่อยของ JSON ในคำตอบแบบสตรีม และต้องบัฟเฟอร์ตามดัชนีการเรียกใช้เครื่องมือก่อนแยกวิเคราะห์ จากนั้น ดำเนินการเครื่องมือหลังสตรีมเสร็จสิ้น แล้วส่งคำขอแบบสตรีมครั้งที่สองเพื่อรับคำตอบสุดท้าย และการ ดำเนินการแบบขนาน ด้วย asyncio.gather ช่วยลดความหน่วงเมื่อโมเดลเรียกใช้หลายฟังก์ชันพร้อมกัน ให้ดักจับข้อผิดพลาดจากการดำเนินการเครื่องมือเสมอเพื่อป้องกันไม่ให้ลูปของเอเจนต์ล้มเหลว ต่อไป เราจะนำการแคชคำตอบมาใช้เพื่อลดค่าใช้จ่ายของ API

คำถามที่พบบ่อย

บทเรียน “การจัดการการเรียกใช้เครื่องมือในการตอบกลับแบบต่อเนื่อง” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การจัดการการเรียกใช้เครื่องมือในการตอบกลับแบบต่อเนื่อง” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส AI Engineering Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส AI Engineering Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การจัดการการเรียกใช้เครื่องมือในการตอบกลับแบบต่อเนื่อง”

แยกวิเคราะห์การตอบกลับแบบต่อเนื่องที่มีอาร์กิวเมนต์การเรียกฟังก์ชันเข้ามาทีละโทเค็น เก็บชิ้นส่วน JSON ไว้ในบัฟเฟอร์ และเริ่มเรียกใช้เครื่องมือเมื่อการเรียกเสร็จสมบูรณ์เท่านั้น คุณปฏิบัติ AI Engineering Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน AI Engineering Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน AI Engineering Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “การจัดการการเรียกใช้เครื่องมือในการตอบกลับแบบต่อเนื่อง” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน AI Engineering Academy นี้ได้ไหม

ได้ บทเรียน AI Engineering Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. ทำความเข้าใจการส่งโทเค็นแบบต่อเนื่อง
  2. การรับข้อมูลแบบต่อเนื่องด้วย Python SDK
  3. การส่งข้อมูลแบบต่อเนื่องใน FastAPI ด้วยเหตุการณ์ที่ส่งจากเซิร์ฟเวอร์
  4. การจัดการการเรียกใช้เครื่องมือในการตอบกลับแบบต่อเนื่อง
← กลับไปที่ AI Engineering Academy