Skip to content
This page has been auto-translated and may contain errors.View in English

도구 사용

docs.scrimba.com

도구 사용(종종 함수 호출이라고 불림)은 언어 모델이 자신의 텍스트 상자를 벗어나는 방법입니다: 모델이 실행할 수 있는 함수 목록을 제공하고, 모델이 언제 사용할지 결정합니다. 모델은 절대 직접 실행하지 않으며, 호출을 요청할 뿐이고, 당신의 코드가 실제 작업을 수행합니다.

모델 자체는 상자 안에 갇혀있습니다. 오늘의 날씨를 확인하거나, 데이터베이스에서 사용자를 조회하거나, 이메일을 보낼 수 없습니다. 학습 시점에 동결된 것에서만 텍스트를 예측할 뿐으로, LLM의 작동 방식의 동일한 그림입니다. 도구 사용은 상자 밖으로 나가도록 하는 방법이며, 에이전트가 구축되는 기반입니다.

기억할 핵심 단어는 "요청"입니다. 모델이 어떤 함수를 원하는지, 어떤 인수를 사용하는지 알려줍니다. 당신의 코드가 실행하고 결과를 돌려줍니다. 당신은 항상 제어권을 유지합니다.

모델은 텍스트만 생성하므로, 자신으로서는 라이브 데이터를 가져오거나, 데이터베이스에 접근하거나, 작업을 트리거할 수 없습니다. 도구 사용은 그 간격을 좁힙니다: 함수 집합을 설명하고, 모델이 구조화된 호출 요청을 내보내고, 당신의 코드가 실행하고, 결과를 다시 피드백합니다. 모델은 조율하고, 당신의 코드는 실행합니다.

이 장은 당신이 구축할 모든 에이전트 아래의 메커니즘입니다. 루프, 도구 스키마, 오류 경로를 여기서 올바르게 이해하면, 에이전트는 이 같은 패턴을 반복하는 것이 됩니다.

도구 사용은 확률 텍스트 생성기가 결정적 코드와 만나는 접점이며, 생산 환경의 대부분의 문제는 그 접점 바로 위에 있습니다. 모델이 구조화된 호출을 예측하고, 당신의 코드가 실행하고, 결과가 더 많은 컨텍스트로 다시 피드백됩니다. 이것을 어렵게 만드는 모든 것 - 신뢰할 수 없는 인수 검증, 멱등성, 추가 왕복, 환각된 호출, 관찰성 - 은 그 하나의 인수 인계에서 비롯됩니다.

새로운 수학은 없습니다. 확률적 시스템이 당신의 시스템에서 실제 부작용을 트리거하도록 하는 엔지니어링 규율이며, 전체 에이전트 장이 기반하는 기질입니다.

루프

도구 사용은 단일 호출이 아닌 **왕복**입니다:

  1. 사용자의 메시지를 모델이 사용할 수 있는 도구 목록과 함께 보냅니다.
  2. 모델이 두 가지 방식 중 하나로 응답합니다: 일반 답변, 또는 도구 호출 요청(원하는 인수 포함).
  3. 도구를 요청했다면, 당신의 코드가 그 함수를 실행하고 결과를 모델로 다시 보냅니다.
  4. 모델이 결과를 사용하여 최종 답변을 작성합니다.

이것이 전체 패턴입니다. 모델이 요청하고, 당신의 코드가 실행합니다. 단계 2와 3은 모델이 여러 도구가 필요한 경우 반복할 수 있으며, 이것이 몇 장 뒤의 에이전트의 씨앗입니다.

Juno루프 도구 사용은 루프입니다: 메시지와 도구 목록을 보내고, 모델이 답하거나 인수와 함께 도구 호출을 요청하고, 당신의 코드가 함수를 실행하고, 결과를 다시 보내 모델이 사용하도록 합니다. 모델은 코드를 절대 실행하지 않으며, 호출만 요청하므로 당신이 제어권을 유지합니다. 중간 단계를 반복하면 에이전트의 시작이 됩니다.

루프는 네 가지 단계를 가집니다: 메시지와 도구 목록을 보내기, 모델이 답변하거나 호출 요청, 당신의 코드가 함수 실행, 결과가 다시 가기, 모델이 계속 진행. 초보자가 종종 놓치는 부분은 이것이 거의 한 번의 라운드가 아니라는 것입니다. 모델이 도구를 요청하고, 당신의 결과를 읽은 다음, 본 것을 바탕으로 다른 도구를 요청할 수 있으며, 계속됩니다.

그래서 실제 형태는 **다중 턴 루프**입니다: 도구 호출이 없는 응답이 돌아올 때까지 모델을 호출하고 요청한 도구를 실행하기를 계속합니다. 그 최종 도구 호출 없는 응답이 사용자를 위한 답변입니다.

python
messages = [{"role": "user", "content": user_text}]

while True:
    response = client.chat.completions.create(
        model=MODEL, messages=messages, tools=tools,
    )
    msg = response.choices[0].message
    messages.append(msg)              # 모델이 한 것을 기록

    if not msg.tool_calls:            # 원하는 도구가 없음: 이것이 답변
        return msg.content

    for call in msg.tool_calls:       # 요청한 모든 호출을 실행, 첫 번째만 아님
        args = json.loads(call.function.arguments)
        result = tool_impls[call.function.name](args)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })

루프 조건은 안전 질문입니다. 계속 도구를 요청하는 모델은 자체적으로 빠져나올 수 없으므로, 프로덕션에서 반복을 제한하고 제한에 도달하면 명확한 오류로 중지합니다. 루프는 모델이 도구 호출이 없는 응답을 반환할 때 종료됩니다.

Juno루프 루프는 도구를 보내고, 모델이 답변하거나 호출을 요청하고, 당신이 실행하고, 결과를 다시 보내고, 반복하는 것입니다. 핵심 움직임은: 모델이 도구 호출이 없는 응답을 반환할 때까지 계속되므로, 하나의 if 문이 아닌 실제 루프로 작성합니다. 그리고 반복을 제한합니다. 도구를 계속 요청하는 모델은 당신의 비용으로 기꺼이 영원히 회전하기 때문입니다.

루프는 기계적으로 작습니다: 도구로 모델을 호출하고, 요청한 것을 실행하고, 결과를 추가하고, 도구 호출이 없는 응답을 반환할 때까지 반복합니다. 흥미로운 부분은 모든 반복이 전체 모델 **왕복**이라는 것이고, 왕복이 당신의 지연시간과 토큰 예산이 가는 곳입니다.

각 턴은 전체 증가하는 메시지 히스토리를 다시 보내므로, 5개의 도구 호출이 필요한 작업은 프롬프트 비용을 5배로 지불하고, 결과가 축적되면서 매 턴 입력이 커집니다. 레버:

  • 왕복 수를 최소화하여 호출당 더 많은 작업을 수행하는 도구를 제공합니다.
  • 도구 결과를 간결하게 유지하여 다시 보낸 히스토리가 부풀어지지 않도록 합니다.
  • 독립적인 도구 호출을 동시에 실행하여 벽시계 지연시간이 가장 느린 호출을 추적하도록 합니다(합이 아닌).
python
MAX_TURNS = 8

for turn in range(MAX_TURNS):
    response = client.chat.completions.create(
        model=MODEL, messages=messages, tools=tools,
    )
    msg = response.choices[0].message
    messages.append(msg)

    if not msg.tool_calls:
        return msg.content

    # 모델은 한 턴에 여러 호출을 요청할 수 있음; 실행한 후 계속
    for call in msg.tool_calls:
        messages.append(run_tool(call))   # 검증됨, 로깅됨, 오류 래핑됨

raise RuntimeError("tool loop did not converge within MAX_TURNS")

하드 캡은 선택 사항이 아닙니다. 없으면, 혼란스러운 모델이 무한정 루프할 수 있으며, 실패 모드는 깨끗한 오류가 아닌 느리고 비용이 많이 드는 요청입니다. 제한하면, 제한에 도달했을 때, 크게 실패하고 모델이 뭘 하고 있었는지 볼 수 있도록 추적을 로깅합니다.

Juno루프 루프는 작성하기는 간단하고 실행하기는 비쌉니다: 모든 턴은 전체 다시 보낸 히스토리에 대한 전체 모델 호출이므로, 5개의 도구 호출은 프롬프트 비용을 5배로 지불하는 것을 의미합니다. 결과를 간결하게 유지하고, 독립적인 호출을 동시에 실행하고, 여러 수다떤 도구보다 더 적은 지방 도구를 선호합니다. 그리고 턴을 하드 캡합니다. 모델이 영원히 루프하기로 결정하는 날, 당신은 5자리 청구서가 아닌 로그에 깨끗한 오류를 원합니다.

실제로 일어나는 것

도구 사용은 모델이 새로운 힘을 얻은 것처럼 느껴질 수 있으며, 그렇지 않았습니다. 내부적으로, 모델은 항상 하는 한 가지를 하고 있습니다: 텍스트를 예측합니다. 그것이 사는 상자에 대해 아무것도 변하지 않았습니다.

요청에 도구 정의를 포함하면, 모델이 예측하는 컨텍스트에 추가합니다. 모델은 이러한 도구가 주어졌을 때, 올바른 계속은 때때로 "Oslo의 get_weather 호출"을 의미하는 특별히 형식된 메시지가 아닌 일반 문장인 예시로 훈련되었습니다. 따라서 당신의 질문이 도구를 유용해 보이게 할 때, 가장 가능성 높은 다음 출력은 그 구조화된 **도구 호출 메시지**이며, API는 이를 tool_calls로 표면화합니다.

모델이 나가서 뭔가를 실행한 것이 아닙니다. 도구 호출이 올바른 움직임이라고 예측하고 당신의 코드가 읽는 방법을 아는 형식으로 호출 요청을 작성했습니다.

그러면 모델이 아닌 당신이 함수를 실행합니다. 실제 결과를 가져와 메시지에 더 많은 컨텍스트로 넣고, 모델이 그 확대된 컨텍스트에서 최종 답변을 예측합니다. 전체 기능은 당신의 코드가 중간의 실제 작업을 하는 두 가지 일반적인 예측입니다.

당신이 모델의 텍스트 상자와 현실 세계 사이의 다리입니다. 그 그림을 유지하고 장의 나머지, 에이전트 포함이 더 이상 신비로워 보이지 않습니다: AI가 취하는 모든 행동은 당신의 코드가 수행하기로 선택하는 예측된 요청입니다.

Juno실제로 일어나는 것 도구 사용은 여전히 순수 예측입니다: 모델은 컨텍스트의 도구 정의가 주어졌을 때, 가능성 높은 출력이 때때로 "이 함수를 이 인수로 호출"을 의미하는 구조화된 메시지인 예시로 훈련되었으며, API는 이를 tool_calls로 반환합니다. 모델은 절대 아무것도 실행하지 않으며, 요청을 예측합니다. 당신의 코드가 함수를 실행하고 결과를 다음 예측을 위해 더 많은 컨텍스트로 피드백하므로, 당신이 모델과 현실 세계 사이의 다리입니다.

도구 사용은 모델에 새로 붙인 기능이 아니라, LLM의 작동 방식에서 구조화된 목표를 향한 동일한 다음 토큰 예측입니다. 도구 정의는 컨텍스트로 들어가고, 도구가 맞을 때 가장 높은 확률의 계속은 산문이 아닌 형식화된 호출입니다. API가 그 계속을 구문 분석하고 tool_calls로 반환합니다.

유용한 결과: 도구 호출은 **제약 생성**으로, 모델이 당신이 제공한 스키마와 일치시키기 위해 생성하는 출력입니다. 이것은 구조화된 출력과 동일한 메커니즘으로, 당신이 정의한 형태로 JSON을 반환하도록 모델에 요청합니다.

그들 사이의 선은 의도입니다. 구조화된 출력은 "이 형태로 데이터를 주고 중지합니다". 도구 사용은 "이 형태로 데이터를 주면 뭔가 실행하고 결과를 다시 피드백할 수 있습니다".

당신이 모델에서 구문 분석된 객체만 필요하면, 구조화된 출력을 사용합니다. 모델이 행동하고 당신의 코드가 반환하는 것에 반응해야 할 때 도구 사용을 합니다.

호출이 예측되고 실행되지 않으므로, 당신의 코드는 당신의 시스템에 실제로 접촉하는 유일한 것입니다. 모델이 제안하고, 당신의 코드가 처리하고, 그 경계가 실제 일이 일어나기 전에 모든 검사를 하는 곳입니다.

Juno실제로 일어나는 것 도구 호출은 산문 대신 스키마를 목표로 하는 동일한 다음 토큰 예측입니다: API가 그것을 tool_calls로 표면화합니다. 구조화된 출력과 동일한 메커니즘인 제약 생성입니다. 차이는 의도입니다: 구조화된 출력이 데이터를 주고 중지하고, 도구 사용이 데이터를 주면 뭔가 실행하고 반응합니다. 어느 쪽이든 모델은 제안하고, 당신의 코드는 실제 무언가에 접촉하는 유일한 것입니다.

도구 호출은 예측된 텍스트이지, 실행이 아니며, 그것을 구별하는 것이 당신을 보호합니다. 모델이 컨텍스트의 도구에 주어진 가장 높은 확률의 계속을 내보내고, API가 그것을 구조화된 호출로 디코딩하고, 당신의 코드가 그것을 수용할지 결정합니다. 기계적으로 그것은 구조화된 출력의 부작용 첨부입니다.

그 프레임을 통해 신뢰 경계를 정확하게 설정합니다. 도구 호출의 인수는 모델 출력이므로, **신뢰할 수 없는 입력**입니다: 확률 시스템이 생성한 텍스트, 사용자가 양식에 입력한 것과 동일한 입장을 가집니다. 그렇게 대합니다.

모델은 또한 루프 중간에 읽은 콘텐츠로 조종될 수 있으므로, 웹페이지나 문서의 텍스트를 도구가 반환하면, 그 텍스트에 숨어있는 프롬프트 주입이 모델을 공격자가 선택한 인수로 다른 도구를 요청하도록 조종할 수 있습니다. 방어는 시스템 프롬프트의 정중한 지시가 아닌 구조적입니다: 엄격한 스키마에 대해 인수를 검증하고, 각 도구를 필요한 가장 좁은 권한으로 범위 지정하고, 도구의 권한이 그 입력에 영향을 줄 수 있는 가장 신뢰할 수 없는 당사자에게 부여하는 것을 초과하지 않도록 합니다.

따라서 정신 모형은 동료가 아닌 요청 라우터입니다. 모델이 제안된 인수를 가진 함수로 라우팅합니다. 당신의 코드가 인증하고, 권한을 부여하고, 검증하고, 그 이후에만 실행합니다. "그 이후에만"의 하류는 이 장이 실력을 발휘하는 곳입니다.

Juno실제로 일어나는 것 도구 호출은 API가 당신을 위해 디코딩하는 예측된 텍스트이며, 구조화된 출력의 부작용 첨부이며, 모델이 절대 실행하는 것이 아닙니다. 그래서 인수는 신뢰할 수 없는 입력이며, 낯선 사람이 채운 양식 필드와 동일한 입장을 가지며, 도구의 반환된 텍스트에 숨어있는 프롬프트 주입이 다음 호출을 리다이렉트할 수 있습니다. 경계에서 검증하고, 범위 지정하고, 권한을 부여합니다. 모델이 요청을 라우팅하고, 당신의 코드가 레버에 손을 올려놓은 유일한 것입니다.

도구 정의

각 도구를 모델에 설명합니다: 이름, 기능, 사용하는 인수. 설명은 당신을 위한 문서가 아니라, 언제 도구를 사용하는지에 대한 모델 지시이므로 프롬프트처럼 작성합니다.

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "현재 도시의 날씨를 가져옵니다. 사용자가 날씨에 대해 물을 때 사용합니다.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "도시 이름, 예: 'Seoul'"},
                },
                "required": ["city"],
            },
        },
    },
]

parameters 블록은 **스키마**로, 구조화된 출력과 동일한 아이디어입니다: 모델이 생성해야 하는 인수를 정의합니다. 모델이 get_weather 호출을 결정하면, 이 형태와 일치하는 city 인수를 다시 보냅니다. 모호한 설명("날씨를 가져옵니다")은 모델이 잘못된 순간에 도구를 사용하도록 합니다. 명확한 것("사용자가 날씨에 대해 물을 때 사용합니다")은 잘 안내합니다.

Juno도구 정의 도구 정의는 이름, 설명, 그리고 인수에 대한 parameters 스키마를 가집니다. 설명은 정말 프롬프트입니다: 도구를 언제 호출할지 결정하도록 모델에 알려주므로, 그것처럼 작성합니다. parameters 스키마는 구조화된 출력과 동일한 메커니즘으로, 모델이 보내는 인수를 제약합니다.

도구 정의는 모델이 읽는 세 부분을 가집니다: 이름, 설명, parameters 스키마. 설명이 사람들이 경시하는 부분입니다. 당신의 팀원을 위한 주석이 아니라, 모델이 도구를 호출할 때 **프롬프트**이므로, 프롬프트처럼 작성합니다: 도구가 무엇을 하는지, 언제 사용할지, 언제 사용하지 않을지 말합니다.

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_orders",
            "description": (
                "고객의 계정 이메일로 주문을 조회합니다. "
                "사용자가 기존 주문에 대해 물을 때만 사용합니다. "
                "상품 질문이나 환불에는 사용하지 마십시오."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "email": {"type": "string", "description": "검색할 계정 이메일"},
                    "status": {
                        "type": "string",
                        "enum": ["pending", "shipped", "delivered", "cancelled"],
                        "description": "선택적 상태 필터",
                    },
                },
                "required": ["email"],
                "additionalProperties": False,
            },
        },
    },
]

스키마를 조이고 모델을 조입니다:

  • required는 도구가 실행할 수 없이는 필요한 인수를 강제합니다.
  • enum은 필드를 고정 집합으로 제한하므로 모델이 다섯 번째 상태를 발명할 수 없습니다.
  • additionalProperties: false는 정의하지 않은 필드를 거부합니다.

스키마는 당신의 첫 번째 방어선입니다: 좁을수록, 모델이 도구를 잘못 호출할 수 있는 방법이 더 적습니다.

이 도구 형태는 OpenAI 스타일입니다; 정확한 키는 제공자에 따라 다르지만(Anthropic 및 다른 공급자는 스키마를 다르게 중첩합니다), 이름, 설명, 스키마 이 세 부분은 어디서나 동일합니다.

Juno도구 정의 이름, 설명, 그리고 parameters 스키마, 이것이 도구입니다. 설명은 docstring이 아닌 프롬프트입니다: 도구를 호출할 때와 하지 않을 때를 모델에 알리거나, 잘못된 순간에 호출할 것입니다. 스키마를 required, enum, 추가 속성 없음으로 조입니다. 왜냐하면 좁은 스키마는 모델이 도구를 잘못 호출할 수 있는 더 적은 방법이기 때문입니다. 정확한 JSON 키는 제공자에 따라 변하지만, 그 세 부분은 변하지 않습니다.

도구 정의는 한 모자를 쓴 두 개의 프롬프트입니다. 설명은 모델이 도구를 언제 호출하는지 조종하고, 스키마는 어떤 인수를 보낼 수 있는지 제약하며, 둘 다 내부 문서가 아닌 모델 대면 지시입니다. 스키마는 엄격할 수 있는 곳이며, 여기서의 엄격함은 당신이 가진 가장 저렴한 환각 제어입니다.

python
{
    "name": "issue_refund",
    "description": (
        "특정 주문 항목에 대해 환불을 발급합니다. "
        "사용자와 주문 ID 및 금액을 확인한 후에만 사용합니다. "
        "주문 총액을 초과하는 금액에는 절대 호출하지 마십시오."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {"type": "string", "pattern": "^ord_[0-9]{8}$"},
            "amount_cents": {"type": "integer", "minimum": 1, "maximum": 100000},
            "reason": {"type": "string", "enum": ["damaged", "wrong_item", "late"]},
        },
        "required": ["order_id", "amount_cents", "reason"],
        "additionalProperties": False,
    },
}

제약 필드(하나에 enum, pattern, minimum/maximum, 또는 required 설정이 있는 필드)는 모델이 내보낼 수 있는 것을 좁히지만, 제한을 명확하게 읽습니다: 많은 제공자가 스키마의 구조를 검증하지만, 값은 여전히 예측에서 나오므로, 타이트한 스키마는 형식이 잘못된 호출을 줄이지만 호출이 올바르거나 안전하다는 것을 증명하지는 않습니다. order_id가 패턴과 일치한다는 것은 그 주문이 존재하거나 이 사용자에 속한다는 것을 의미하지 않습니다. 그래서 스키마는 필터이지, 인증 검사가 아닙니다.

당신은 여전히 모든 인수를 실제 상태에 대해 검증합니다: 주문이 존재하고, 금액이 총액을 초과하지 않으며, 호출자가 소유하고 있는지 확인합니다. 부작용이 실행되기 전에 당신의 코드에서. 스키마 검증이 잘못된 형태를 포착합니다; 오직 당신의 코드만 잘못된 작업을 포착합니다. (스키마 키와 각 제공자가 제약을 얼마나 엄격하게 적용하는지는 다르므로, 가정하기보다는 당신의 제공자의 동작을 확인합니다.)

더 레버: 도구 수. 대략 12개 도구를 지나면, 선택 정확도가 떨어지고 모델이 잘못된 것에 도달하기 시작하며, 모든 정의도 컨텍스트에 앉아 모든 호출에서 토큰을 태웁니다. 전체 API 표면을 한 번에 노출하기보다는 활성 도구 집합을 작은 것으로 유지하고 작업과 관련된 것으로 유지합니다.

Juno도구 정의 설명은 호출할 때를 제어하고, 스키마는 보낼 수 있는 것을 제어하며, 둘 다 프롬프트입니다. enum, pattern, 경계로 스키마를 잠그지만, 유효한 형태를 유효한 작업으로 착각하지 마십시오: 잘 형식화된 order_id는 여전히 신뢰할 수 없으므로, 부작용이 실행되기 전에 실제 상태에 대해 다시 검증합니다. 그리고 도구 집합을 작게 유지하십시오. 왜냐하면 12개를 지나면 모델이 잘못 선택하고 모든 정의가 컨텍스트를 압박하기 때문입니다. 스키마가 필터이고, 당신의 코드가 권한을 부여합니다.

호출 처리

모델이 도구를 원하면, 응답이 최종 답변 대신 tool_calls를 포함합니다. 요청된 함수와 인수를 읽고, 실제 함수를 실행하고, 결과를 tool 메시지로 다시 보냅니다. 그러면 모델을 다시 호출하여 완료할 수 있습니다.

python
import json

# 도구의 실제 구현
def get_weather(city):
    # 실제 앱에서는 날씨 API를 호출합니다; 여기서는 가짜입니다
    return {"city": city, "tempC": 18, "condition": "cloudy"}

messages = [{"role": "user", "content": "Seoul의 날씨는 어떨까요?"}]

# 1. 첫 호출: 도구 제공
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
tool_calls = response.choices[0].message.tool_calls
tool_call = tool_calls[0] if tool_calls else None

if tool_call:
    # 2. 모델의 인수로 요청된 함수 실행
    args = json.loads(tool_call.function.arguments)
    result = get_weather(args["city"])

    # 3. 모델의 요청과 당신의 결과를 다시 보냅니다
    messages.append(response.choices[0].message)         # 어시스턴트의 도구 요청
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps(result),
    })

    # 4. 모델이 결과를 사용하여 답변할 수 있도록 다시 호출
    response = client.chat.completions.create(model=MODEL, messages=messages)

print(response.choices[0].message.content)
# "Seoul은 현재 18도이고 흐립니다."

인수는 **JSON 문자열**로 도착하므로, json.loads를 사용하여 실제 객체로 변환한 다음, 사용하기 전에 검사합니다. 히스토리에 두 메시지를 추가합니다: 모델의 도구 요청과 당신의 tool 결과, tool_call_id로 연결된. 최종 호출은 원시 날씨 데이터를 자연스러운 문장으로 바꿉니다.

Juno호출 처리 모델이 도구를 원하면, 응답이 텍스트 대신 tool_calls를 전달합니다. JSON 문자열에서 인수를 구문 분석하고, 실제 함수를 실행하고, 일치하는 tool_call_id로 태그된 두 메시지를 다시 푸시합니다: 모델의 요청과 당신의 결과. 최종 모델 호출이 원시 결과를 자연스러운 답변으로 바꿉니다.

모델이 도구를 원하면, 응답이 content 대신 tool_calls를 전달합니다. 각 호출이 함수 이름, id, JSON 문자열로 인수를 제공합니다. 당신이 구문 분석하고, 실행하고, tool 메시지를 추가하고, 일치하는 tool_call_id로 태그하면 모델이 어떤 요청이 어떤 결과를 답하는지 알 수 있습니다.

올바르게 얻을 가치가 있는 부분은 실패입니다. 당신의 도구가 던질 것입니다: 나쁜 인수, 404, 타임아웃. 본능은 예외를 버블링하는 것이지만, 그것은 전체 턴을 끝냅니다. 더 나은 움직임은 **오류를 데이터로 반환**하는 것입니다: 오류를 포착하고 도구 결과로 다시 손으로, 모델이 무엇이 잘못되었는지 읽고 복구하거나, 수정된 인수로 재시도하거나, 사용자에게 할 수 없었다고 말할 수 있습니다.

python
def run_tool(call):
    try:
        args = json.loads(call.function.arguments)
        result = tool_impls[call.function.name](args)
        content = json.dumps(result)
    except Exception as err:
        # 예외가 아닌 데이터로 실패를 손으로 전달
        content = json.dumps({"error": str(err)})
    return {"role": "tool", "tool_call_id": call.id, "content": content}

오류를 도구 메시지로 반환하는 것이 도구 루프를 부서지기 쉬운 것이 아닌 탄력적으로 만듭니다. 형식이 잘못된 이메일로 search_orders를 호출하는 모델이 {"error": "invalid email"}를 다시 받고 사용자에게 수정하도록 요청할 수 있으며, 전체 요청이 500하지 않습니다. 응답 형태(tool_calls, tool_call_id, tool 역할)는 OpenAI 스타일이며 제공자에 따라 다르지만, 패턴은, 구문 분석, 실행, 결과 또는 오류를 반환하며, 모든 것을 따릅니다.

Juno호출 처리 응답이 tool_calls를 전달합니다: JSON 인수를 구문 분석하고, 함수를 실행하고, 일치하는 tool_call_id를 가진 tool 메시지를 추가합니다. 실패에서 움직임: 예외를 버블링하게 하고 턴을 종료하지 말고, 포착하고 {"error": ...}를 도구 결과로 다시 손으로 전달하여 모델이 복구하거나 재시도할 수 있습니다. 정확한 필드 이름은 제공자에 따라 다르지만, 구문 분석, 실행, 결과 또는 오류를 반환하는 것은 어디서나 동일합니다.

구문 분석과 발송이 지루한 부분입니다. 위험한 부분은 인수를 받고 부작용을 실행하는 사이의 모든 것입니다. 왜냐하면 인수는 모델 출력이고 함수는 실제 일을 하기 때문입니다.

세 가지 습관이 데모와 프로덕션에서 실행할 수 있는 도구를 구분합니다:

  • 첫째, 스키마가 아닌 실제 상태에 대해 실행하기 전에 검증합니다: 스키마는 이미 통과했거나 당신이 여기 없었을 테니까, 주문이 존재하고, 사용자가 소유하고, 금액이 범위 내인지 확인합니다.
  • 둘째, **멱등성**을 위해 구축하세요. 이것은 동일한 작업을 두 번 실행하는 것이 한 번 실행하는 것과 동일한 영향을 갖는 속성입니다. 모델은 재시도하고, 루프는 다시 발사하고, 네트워크는 복제하므로, 쓰기 도구에는 멱등성 키(종종 tool_call_id에서 파생됨)가 필요하여 반복이 이중 청구나 이중 송신을 하지 않습니다.
  • 셋째, 오류를 데이터로 반환하고, 모델이 행동할 수 있을 만큼 구조화되어 있어서, 복구 가능한 실패는 재시도가 되고 복구할 수 없는 것은 사용자에게 깨끗한 메시지가 됩니다.
python
def run_tool(call):
    name, args = call.function.name, json.loads(call.function.arguments)
    log.info("tool_call", name=name, args=redact(args), call_id=call.id)  # 관찰성

    try:
        validate(name, args)                       # 실제 상태 검사, 나쁜 입력에 대해 발생
        result = TOOLS[name](args, idem_key=call.id)  # 재시도에서 멱등성
        content = json.dumps(result)
    except ValidationError as err:
        content = json.dumps({"error": "invalid", "detail": str(err)})  # 모델이 재시도할 수 있음
    except Exception as err:
        log.exception("tool_failed", call_id=call.id)
        content = json.dumps({"error": "tool_failed"})  # 모델에 내부 항목 누수 없음

    return {"role": "tool", "tool_call_id": call.id, "content": content}

두 가지 더 많은 프로덕션 현실:

  • 관찰성: 어떤 도구가 실행되었는지, 어떤 인수로(비밀을 수정), 반환된 것을 로깅합니다. 에이전트가 잘못되면 도구 호출 추적이 실제로 무엇을 했는지 재구성하는 유일한 방법이기 때문입니다.
  • 파괴적인 도구: 삭제, 지불, 이메일링 등 돌이킬 수 없는 것의 경우, 모델의 호출이 최종 권한이 되지 않도록 합니다. 확인, 무엇이 일어날지 보고하는 드라이 런, 또는 인간 승인 단계 뒤에 게이트하고, 자격 증명을 범위하여 모델이 잘못되거나 하이재킹되더라도 폭발 반경이 제한됩니다.

스키마 검증 호출은 당신의 코드가 승인할 때까지 신뢰할 수 없습니다. (필드 이름과 재시도 봉투는 OpenAI 스타일입니다; 규율은 어떤 제공자든 이월됩니다.)

Juno호출 처리 구문 분석은 지루한 부분입니다. 인수와 부작용 사이: 스키마가 아닌 실제 상태에 대해 검증하고, tool_call_id에서 쓰기를 멱등성으로 만드십시오. 왜냐하면 재시도가 두 번 발사될 것이기 때문이고, 오류를 모델이 행동할 수 있고 당신의 내부 항목을 누수하지 않는 데이터로 반환합니다. 모든 호출과 인수를 로깅하여 에이전트가 실제로 무엇을 했는지 재구성할 수 있습니다. 그리고 파괴적인 것 모두 확인이나 범위 자격 증명 뒤에 게이트하세요. 왜냐하면 스키마 검증 환불은 당신의 코드가 그렇다고 말할 때까지 낯선 사람의 요청이기 때문입니다.

실제로

재사용 가능한 함수와 동일한 흐름. 모델과 당신의 시스템 사이에 있는 유일한 것은 당신이 작성하고 제어하는 코드입니다:

python
import json

tool_impls = {"get_weather": lambda args: get_weather(args["city"])}

def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
    calls = response.choices[0].message.tool_calls
    if not calls:
        return response.choices[0].message.content  # 모델이 직접 답변함

    call = calls[0]
    args = json.loads(call.function.arguments)
    result = tool_impls[call.function.name](args)

    messages.append(response.choices[0].message)
    messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})

    response = client.chat.completions.create(model=MODEL, messages=messages)
    return response.choices[0].message.content

이것이 하나의 도구 호출을 처리합니다. 모델은 또한 한 번에 여러 개를 요청할 수 있으며, **병렬 도구 호출**이라고 불리며, tool_calls의 모든 항목을 루핑하여 처리합니다. 그리고 모델이 도구를 호출하는 루프에서 그것을 유지하면, 매 턴마다 다음 단계를 결정하고, 당신은 에이전트를 얻으며, 정확히 다음 장이 가는 곳입니다.

Juno실제로 함수로 래핑되면, 도구 사용은 도구를 선택하는 하나의 모델 호출, 당신의 코드가 실행하는 것, 결과를 답변으로 바꾸는 두 번째 호출입니다. 모델과 당신의 시스템 사이에 있는 유일한 것은 당신이 작성한 코드이므로, 당신이 제어권을 유지합니다. 이것을 루프하여 모델이 완료되면 당신은 에이전트를 얻습니다.

실제 **도구 핸들러**에서 당신은 다중 턴 루프, 병렬 호출, 오류 반환을 하나의 함수로 결합합니다. 모델이 단일 턴에서 여러 도구를 요청할 수 있으며, 루프는 요청을 중지할 때까지 실행됩니다.

python
import json

MAX_TURNS = 6

def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    for _ in range(MAX_TURNS):
        response = client.chat.completions.create(
            model=MODEL, messages=messages, tools=tools,
        )
        msg = response.choices[0].message
        messages.append(msg)

        if not msg.tool_calls:
            return msg.content

        for call in msg.tool_calls:        # 모든 병렬 호출을 처리
            messages.append(run_tool(call))  # 구문 분석, 실행, 결과 또는 오류를 반환

    return "죄송합니다. 그것을 완료할 수 없습니다."  # 턴 캡 도달

세 가지 결정이 이것을 데모가 아닌 프로덕션 준비로 만듭니다. [0]이 아닌 tool_calls 모두에 대해 루프하거나, 모델이 요청한 호출을 조용히 놓칠 것입니다. MAX_TURNS를 캡하여 모델이 영원히 회전할 수 없습니다. 그리고 실패를 run_tool을 통해 오류 메시지로 라우팅하여 하나의 나쁜 호출이 전체 교환을 충돌시키지 않습니다.

단계를 하드코딩하는 것을 중단하고 매 턴마다 모델이 자신의 다음 움직임을 결정하게 할 때, 이 정확한 루프는 에이전트가 됩니다. 차이는 자율성이지, 아키텍처가 아닙니다.

Juno실제로 프로덕션 핸들러는 세 가지 것이 연결된 다중 턴 루프입니다: 첫 번째만이 아닌 tool_calls의 모든 항목에 대해 루프하고, 턴을 캡하여 영원히 회전할 수 없으며, 실패를 도구 메시지로 반환하여 하나의 나쁜 호출이 실행을 충돌시키지 않습니다. 동일한 루프, 더 많은 자율성, 그리고 당신은 에이전트를 가집니다. 아키텍처는 변하지 않고, 모델은 자신의 다음 단계를 결정할 수 있을 뿐입니다.

프로덕션 핸들러는 위의 모든 것을 접습니다: 경계 다중 턴 루프, 동시에 실행되는 병렬 호출, 검증되고 로깅된 실행, 데이터로 반환되는 오류. 남은 질문은 대부분의 팀이 건너뛰는 것입니다: 모델에 도구를 줄 때를 결정하지 않는 것.

도구는 작업이 자연 언어에 대한 모델의 판단에 정말 의존할 때 올바른 답변입니다: 어떤 주문이 사용자의 의미, 이것이 환불 경우로 계산되는지, 무엇을 검색할지. 경로가 **결정적**일 때 잘못된 답변입니다. 동일한 입력이 항상 동일한 호출을 생성합니다. 요청이 항상 동일한 인수로 동일한 호출을 트리거하면, 코드에 연결하고 왕복을 건너뛰세요; 당신은 환각 표면, 지연시간 홉, 토큰 비용을 제거할 수 있습니다. 모델에 도구를 건네는 것은 유연성을 구매하고 비결정성에 비용을 지불합니다. 유연성이 요점일 때만 사용합니다.

python
def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    for turn in range(MAX_TURNS):
        response = client.chat.completions.create(
            model=MODEL, messages=messages, tools=tools, tool_choice="auto",
        )
        msg = response.choices[0].message
        messages.append(msg)
        if not msg.tool_calls:
            return msg.content

        results = run_tools_concurrently(msg.tool_calls)  # 독립적인 호출은 병렬로
        messages.extend(results)

    log.warning("tool_loop_unconverged", turns=MAX_TURNS)
    return fallback_answer()

도구를 사용할 때 두 가지 레버:

  • 도구 호출 환각을 제약합니다: 모델이 정의하지 않은 도구에 호출을 발명하거나 인수를 위조할 수 있으므로, 알려지지 않은 도구 이름을 즉시 거부하고, tool_choice(도구 사용을 강제, 금지, 또는 해제하는 매개 변수)를 사용하여 하나가 필요할 때 도구를 필요하고 없어야 할 때 금지하기보다는 모든 턴을 우연에 남기기.
  • 관찰성 추적을 유지하세요. 도구, 인수, 결과, 턴 수. 자율 루프는 실제로 무엇을 했는지 재생할 수 있을 때만 디버깅할 수 있기 때문입니다.

이 핸들러는 에이전트의 문자 그대로의 씨앗입니다: 에이전트는 더 광범위한 도구 집합과 목표를 향해 호출을 연쇄할 수 있는 자유도를 가진 이 루프이며, 실패 모드가 여기서 그곳으로 확대되는 이유이기도 합니다. (SDK 표면은 OpenAI 스타일입니다; tool_choice와 메시지 봉투는 제공자에 따라 다릅니다.)

Juno실제로 전체 핸들러는 동시, 검증, 로깅된 도구 실행과 데이터로 반환되는 오류를 가진 경계 루프입니다. 건너뛴 질문은 도구를 주지 않을 때입니다: 호출이 결정적이면, 코드에 연결하고 왕복, 지연시간, 환각 표면을 놓칩니다. 모든 것이 희망이 아닌 강제 또는 금지로 도구를 사용하기 위해 `tool_choice`를 사용하고, 정의하지 않은 도구에 호출을 거부하고, 추적을 유지하세요. 왜냐하면 이것은 훈련 바퀴가 여전히 켜진 에이전트이고, 실패 모드는 여기에서만 커집니다.