Triton Inference Server로 함수 호출(Function Calling) 구현하기

Triton Inference Server로 함수 호출(Function Calling) 구현하기

이 튜토리얼은 함수 호출(Function Calling) 을 다뤄요. 대규모 언어 모델(LLM)을 외부 도구에 쉽게 연결하는 일반적인 접근 방식이죠. 이 방식은 AI 에이전트에 효과적인 도구 사용과 외부 API와의 원활한 상호작용을 가능하게 해서, 역량과 실용적 응용 범위를 크게 넓혀 줍니다.

함수 호출이 뭔가요?

함수 호출은 LLM이 다음을 할 수 있게 하는 능력을 말해요.

  • 질문에 답하거나 작업을 수행하는 데 특정 함수나 도구가 필요한지 인식.
  • 그 함수를 호출하기 위한 인자를 담은 구조화된 출력 생성.
  • 함수 호출 결과를 응답에 통합.

함수 호출은 LLM이 내재 지식 너머의 특정 계산이나 데이터 검색이 필요한 더 복잡한 작업(예: 멀티 에이전트 시스템의 에이전트 오케스트레이션)을 수행하게 하는 강력한 메커니즘이에요. 특정 함수가 필요할 때를 인식함으로써 LLM은 기능을 동적으로 확장해 실제 애플리케이션에서 더 다재다능하게 쓸 수 있습니다.

튜토리얼 개요

이 튜토리얼은 이 기능에 미리 파인튜닝된 Hermes-2-Pro-Llama-3-8B 모델로 함수 호출을 시연해요. 최신 주가 정보를 제공하고 최근 회사 뉴스를 요약하는 기본 주식 리포트 에이전트를 만들 거예요.

사전 준비: Hermes-2-Pro-Llama-3-8B

진행 전에 Hermes-2-Pro-Llama-3-8B 모델을 이 단계를 따라 Triton Inference Server와 TensorRT-LLM 백엔드로 성공적으로 배포했는지 확인하세요.

중요: 도커 컨테이너를 시작할 때 tutorials 폴더를 /tutorials에 마운트했는지 확인하세요.

함수 정의

주식 리포트 에이전트를 위해 세 개의 함수를 정의할게요.

  1. get_current_stock_price: 주어진 심볼의 현재 주가를 가져옴.
  2. get_company_news: 주어진 주식 심볼의 회사 뉴스와 보도자료를 가져옴.
  3. final_answer: no-op으로 쓰이며 최종 응답을 나타냄.

각 함수는 이름, 설명, 입력 파라미터 스키마를 포함해요.

TOOLS = [
   {
       "type": "function",
       "function": {
           "name": "get_current_stock_price",
           "description": "Get the current stock price for a given symbol.\n\nArgs:\n  symbol (str): The stock symbol.\n\nReturns:\n  float: The current stock price, or None if an error occurs.",
           "parameters": {
               "type": "object",
               "properties": {"symbol": {"type": "string"}},
               "required": ["symbol"],
           },
       },
   },
   {
       "type": "function",
       "function": {
           "name": "get_company_news",
           "description": "Get company news and press releases for a given stock symbol.\n\nArgs:\nsymbol (str): The stock symbol.\n\nReturns:\npd.DataFrame: DataFrame containing company news and press releases.",
           "parameters": {
               "type": "object",
               "properties": {"symbol": {"type": "string"}},
               "required": ["symbol"],
           },
       },
   },
   {
       "type": "function",
       "function": {
           "name": "final_answer",
           "description": "Return final generated answer",
           "parameters": {
               "type": "object",
               "properties": {"final_response": {"type": "string"}},
               "required": ["final_response"],
           },
       },
   },
]

이 함수 정의들은 프롬프트를 통해 모델에 전달되어, 대화 중에 모델이 이들을 인식하고 적절히 활용하게 해 줍니다.

실제 구현은 client_utils.py를 참고하세요.

프롬프트 엔지니어링

프롬프트 엔지니어링은 함수 호출에서 핵심적인 요소예요. LLM이 특정 함수를 언제·어떻게 활용할지 인식하도록 안내하죠. 프롬프트를 신중히 구성하면 LLM의 역할, 목표, 접근할 수 있는 도구를 정의해 정확하고 효율적인 작업 수행을 보장할 수 있습니다.

이 작업에서는 system_prompt_schema.yml 파일에 담긴 샘플 프롬프트 구조를 구성했어요. 이 파일은 다음을 꼼꼼히 정리해 둡니다.

  • 역할(Role): LLM이 수행할 것으로 기대되는 구체적인 역할.
  • 목표(Objective): 상호작용의 목표 또는 원하는 결과를 명확히 함.
  • 도구(Tools): LLM이 목표를 달성하는 데 사용할 수 있는 함수·도구 목록.
  • 스키마(Schema): 각 도구·함수를 호출하기 위한 구조와 형식.
  • 지침(Instructions): LLM이 의도한 경로를 따르고 도구를 적절히 활용하도록 보장하는 명확한 가이드라인.

프롬프트 엔지니어링을 활용하면 LLM이 복잡한 작업을 수행하고 함수 호출을 응답에 자연스럽게 통합하는 능력을 높여 다양한 애플리케이션에서 유용성을 극대화할 수 있어요.

모두 조합하기

먼저 Triton SDK 컨테이너를 시작해요.

# SDK 컨테이너 예시
docker run --rm -it --net host --shm-size=2g \
    --ulimit memlock=-1 --ulimit stack=67108864 --gpus all \
    -v /path/to/tutorials/:/tutorials \
    -v /path/to/tutorials/repo:/tutorials \
    nvcr.io/nvidia/tritonserver:<xx.yy>-py3-sdk

제공되는 클라이언트 스크립트는 pydanticyfinance 라이브러리를 쓰는데, SDK 컨테이너에는 포함되어 있지 않아요. 진행 전에 설치하세요.

pip install pydantic yfinance

제공되는 client.py를 다음과 같이 실행해요.

python3 /tutorials/AI_Agents_Guide/Function_Calling/artifacts/client.py --prompt "Tell me about Rivian. Include current stock price in your final response." -o 200

다음과 유사한 응답을 기대할 수 있어요.

+++++++++++++++++++++++++++++++++++++
RESPONSE: Rivian, with its current stock price of <CURRENT STOCK PRICE>, <NEWS SUMMARY>
+++++++++++++++++++++++++++++++++++++

LLM이 어떤 도구를 "호출"했는지 보려면 verbose 플래그를 추가하면 돼요.

python3 /tutorials/AI_Agents_Guide/Function_Calling/artifacts/client.py --prompt "Tell me about Rivian. Include current stock price in your final response." -o 200 --verbose

그러면 함수 호출의 단계별 과정이 보여요. 호출되는 도구, 각 도구에 전달되는 인자, 각 함수 호출의 응답, 최종 요약 응답을 포함해서요.

[b'\n{\n  "step": "1",\n  "description": "Get the current stock price for Rivian",\n  "tool": "get_current_stock_price",\n  "arguments": {\n    "symbol": "RIVN"\n  }\n}']
=====================================
Executing function: get_current_stock_price({'symbol': 'RIVN'})
Function response: <CURRENT STOCK PRICE>
=====================================
[b'\n{\n  "step": "2",\n  "description": "Get company news and press releases for Rivian",\n  "tool": "get_company_news",\n  "arguments": {\n    "symbol": "RIVN"\n  }\n}']
=====================================
Executing function: get_company_news({'symbol': 'RIVN'})
Function response: [<LIST OF RECENT NEWS TITLES>]
=====================================
[b'\n{\n  "step": "3",\n  "description": "Summarize the company news and press releases for Rivian",\n  "tool": "final_answer",\n  "arguments": {\n    "final_response": "Rivian, with its current stock price of  <CURRENT STOCK PRICE>, <NEWS SUMMARY>"\n  }\n}']


+++++++++++++++++++++++++++++++++++++
RESPONSE: Rivian, with its current stock price of  <CURRENT STOCK PRICE>, <NEWS SUMMARY>
+++++++++++++++++++++++++++++++++++++

팁: 이 튜토리얼에서는 모든 기능(도구 정의, 구현, 실행)이 클라이언트 쪽에 구현돼 있어요(client.py). 특히 함수를 미리 알고 있는 운영 시나리오에서는 이 로직을 서버 쪽에 구현하는 걸 고려하세요. 서버 쪽 구현의 권장 방식은 Triton ensemble 또는 BLS로 워크플로를 배포하는 거예요. 전처리 모델로 사용자 프롬프트와 시스템 프롬프트·가용 도구를 조합·포맷하고, 후처리 모델로 배포된 LLM에 대한 여러 호출을 관리해 최종 답변에 도달하게 합니다.

추가 최적화

출력 형식 강제

이 튜토리얼에서는 프롬프트 엔지니어링으로 특정 출력 형식을 강제하는 법을 보여줬어요. 원하는 구조는 다음과 같아요.

  {
    "step" : <Step number>
    "description": <Description of what the step does and its output>
    "tool": <Tool to use>,
    "arguments": {
        <Parameters to pass to the tool as a valid dict>
    }
  }

다만 출력이 이 스키마에서 벗어나는 경우가 있을 수 있어요. 예를 들어 다음 프롬프트 실행을 봐요.

python3 /tutorials/AI_Agents_Guide/Function_Calling/artifacts/client.py --prompt "How Rivian is doing?" -o 500 --verbose

이 실행은 잘못된 JSON 형식으로 실패할 수 있어요. verbose 출력을 보면 최종 LLM 응답이 기대한 JSON 형식 대신 평문 텍스트였음을 알 수 있죠.

{
  "step": "3",
  "description": <Description of what the step does and its output>
  "tool": "final_answer",
  "arguments": {
    "final_response": <Final Response>
  }
}

다행히 이 동작은 제약 디코딩으로 제어할 수 있어요. 특정 형식·내용 요구 사항을 충족하는 출력을 생성하도록 모델을 안내하는 기법이죠. 모델 출력을 효과적으로 관리하는 방법을 더 깊이 알고 싶다면 제약 디코딩에 관한 전용 튜토리얼을 적극 권장합니다.

팁: 최적의 결과를 위해 client_utils.py에 정의된 FunctionCall 클래스를 Logits 후처리기의 JSON 스키마로 사용하세요. 그러면 이 튜토리얼 전반에서 구축한 구조와 일치하는 일관되고 올바른 포맷의 출력이 보장돼요.

병렬 도구 호출

이 튜토리얼은 단일 턴의 강제 호출을 다뤄요. LLM이 단일 상호작용 안에서 특정 함수 호출을 하도록 유도하는 거죠. 이 방식은 즉시 필요한 정확한 동작이 있을 때 유용하며, 현재 대화의 일부로 함수가 실행되도록 보장합니다.

일부 함수 호출은 동시에 실행될 수 있어요. 이 기법은 독립적인 연산으로 나눌 수 있는 작업에 유용하며, 효율성을 높이고 응답 시간을 줄여 줍니다. 병렬 도구 호출 구현을 실습으로 직접 시도해 보시길 권장합니다.

참고

이 튜토리얼의 일부는 Hermes-Function-Calling을 기반으로 해요.