스트리밍 응답에서 도구 호출 처리
함수 호출 인수가 토큰 단위로 도착하는 스트리밍 응답을 파싱하고 JSON 조각을 버퍼링한 뒤, 호출이 완전히 끝났을 때만 도구 실행을 시작합니다.
스트리밍 응답에서 도구 호출 처리은(는) CoddyKit의 무료 AI Engineering Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 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 델타가 null이 아닌 각 청크에 대해 도구 호출 인덱스를 키로 하는 적절한 버퍼 항목에 인수 조각을 추가하십시오.
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)도구 사용 후 최종 답변 스트리밍하기
도구 호출을 실행하고 결과를 메시지 기록에 추가한 후에는 두 번째 스트리밍 요청을 보내 모델의 최종 답변을 받으십시오. 이 응답을 클라이언트에 직접 스트리밍하십시오. 이 두 요청 방식(도구 호출이 포함된 최초 요청과 도구 결과가 포함된 후속 요청)은 표준 에이전트 턴 주기이며, 두 요청 모두 텍스트를 UI로 스트리밍할 수 있습니다.
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 tokenUI에 도구 호출 진행 상황 표시하기
도구 호출이 완료되기를 기다리는 동안 사용자는 에이전트가 무엇을 하고 있는지 확인할 수 있어야 합니다. 도구를 실행하기 전에 어떤 함수를 어떤 인수와 함께 호출하는지 나타내는 상태 이벤트를 클라이언트로 스트리밍하십시오. 실행이 끝난 후에는 완료 상태를 스트리밍하십시오. 이러한 투명성은 사용자가 느끼는 응답성을 크게 높이고 예상하지 못한 도구 사용을 디버깅하는 데 도움이 됩니다.
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'])),
}에이전트의 스트리밍과 비스트리밍 비교
에이전트 기반 애플리케이션에서 스트리밍은 복잡성을 높이지만 상당한 UX 가치를 제공합니다. 스트리밍이 없으면 10~30초가 걸릴 수 있는 다단계 도구 호출 루프가 진행되는 동안 사용자는 아무것도 볼 수 없습니다. 스트리밍을 사용하면 중간 텍스트, 도구 호출 알림, 토큰 단위로 나타나는 최종 답변을 확인할 수 있습니다. 추가되는 코드 복잡성은 대개 대화형 애플리케이션에 사용할 가치가 있지만, 자율적으로 실행되는 백그라운드 에이전트는 더 단순한 코드를 위해 비스트리밍 방식을 사용할 수 있습니다.
빠른 확인
이 레슨에서 학습한 스트리밍 응답의 도구 호출에 대한 이해도를 확인해 보십시오.
레슨 요약
이 레슨에서는 다음을 학습했습니다. 스트리밍 응답에서 도구 호출 인수는 JSON 조각으로 도착하므로 파싱하기 전에 도구 호출 인덱스별로 버퍼링해야 합니다. 스트림이 완료된 후 도구를 실행하고 최종 답변을 받기 위해 두 번째 스트리밍 요청을 보내야 합니다. 또한 모델이 여러 함수를 동시에 호출할 때는 asyncio.gather를 사용한 병렬 실행으로 지연 시간을 최소화할 수 있습니다. 에이전트 루프가 중단되지 않도록 도구 실행 오류는 항상 포착하십시오. 다음에는 API 비용을 줄이기 위해 응답 캐싱을 구현합니다.
자주 묻는 질문
“스트리밍 응답에서 도구 호출 처리” 강의는 무료인가요?
네 — “스트리밍 응답에서 도구 호출 처리” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Engineering Academy 강의 전체를 잠금 해제할 수 있습니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“스트리밍 응답에서 도구 호출 처리”에서 뭘 배우나요?
함수 호출 인수가 토큰 단위로 도착하는 스트리밍 응답을 파싱하고 JSON 조각을 버퍼링한 뒤, 호출이 완전히 끝났을 때만 도구 실행을 시작합니다. 브라우저에서 직접 실행하는 실습 코드로 AI Engineering Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
AI Engineering Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 AI Engineering Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“스트리밍 응답에서 도구 호출 처리” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 AI Engineering Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 AI Engineering Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 토큰 스트리밍 이해
- Python SDK로 스트림 소비
- Server-Sent Events를 사용한 FastAPI 스트리밍
- 스트리밍 응답에서 도구 호출 처리