OpenRouter API로 함수 호출
모델이 알 수 없는 현재 재고를 외부 함수로 조회하고, 호출 요청·실행·결과 반환을 셀별로 확인한다.
이번 실습의 흐름
- 질문: “무선 마우스는 지금 몇 개 남아 있나요?”
- 도구:
get_stock하나. - 현재 재고는 매장의 운영 자료. 모델의 학습된 지식이나 질문의 문장만으로 확인할 수 없는 정보.
- 먼저 파이썬으로 재고를 조회한 뒤 같은 함수를 모델에 연결.
- 실습에서는 실제 매장 대신 노트북의 사전을 재고표로 사용. 재고 수량은 모델의 질문·도구 설명에 넣지 않고 함수 실행 결과로만 전달.
- 한 번의 함수 호출과 최종 답변만 다룸. 반복 실행 구조를 만들 필요 없음.
함수 호출의 요청과 응답
Mermaid 다이어그램 원문
sequenceDiagram
autonumber
participant L as LLM
participant A as 애플리케이션
participant F as 도구 함수
A->>L: 작업 요청과 도구 정의
L-->>A: 도구 호출 요청(함수 이름·인자)
A->>F: 함수 실행(인자)
F-->>A: 실행 결과
A->>L: 호출 기록과 실행 결과
L-->>A: 최종 답변대화형 다이어그램을 불러오기 전에는 원문으로 표시됩니다.
- 애플리케이션: 사용자 요청을 받고 모델·도구 호출과 결과 전달을 관리하는 프로그램.
- 위에서 아래로 진행되는 시간 순서. 실선은 요청·실행, 점선은 반환.
- 모델에 도구를 알려 주는 첫 요청과, 도구 실행 결과를 전달하는 두 번째 요청을 구분.
실습 준비
-
환경: Colab 또는 Jupyter. 증강 섹션의 실습은 같은 노트북·런타임에서 순서대로 실행.
-
설치·불러오기·API 연결은 여기서 한 번만 준비. 뒤 자료는
client,model,json,Client,FastMCP,Monty를 그대로 사용. 런타임을 재시작했다면 이 준비 코드부터 다시 실행. -
OpenRouter API 키와 사용 가능한 크레딧 필요. API 호출에는 사용량에 따른 비용 발생.
-
모형:
openai/gpt-4.1-mini. 함수 호출을 지원하는 고정 모형을 사용하며, 다른 모형으로 바꿀 때는 도구 호출 지원 여부 확인. -
키 입력란에는 OpenRouter에서 발급받은 키 입력. 입력한 키를 화면·코드·공유 노트북에 출력하지 않기.
-
openai: OpenRouter API 호출.fastmcp: MCP 도구 연결.pydantic-monty: 코드 실행 샌드박스.
%pip install -q openai fastmcp==4.1.4 pydantic-monty==1.4.0
import json
from getpass import getpass
from openai import OpenAI
from fastmcp import Client, FastMCP
from pydantic_monty import Monty
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=getpass("OpenRouter API 키: "),
)
model = "openai/gpt-4.1-mini"
재고 조회 함수 만들기
inventory: 상품 이름과 현재 재고 수량을 짝지은 파이썬 사전.def: 함수 정의.product_name: 조회할 상품 이름.str,dict: 입력은 문자열, 반환값은 사전이라는 설명.inventory.get(): 상품 이름에 해당하는 수량 조회. 등록되지 않은 이름이면None반환.return: 상품 이름과 조회 결과를 함수 밖으로 반환.
inventory = {
"무선 마우스": 7,
"기계식 키보드": 0,
"USB 허브": 12,
}
def get_stock(product_name: str) -> dict:
"""실습용 재고표에서 상품의 현재 재고를 조회."""
return {
"product_name": product_name,
"stock": inventory.get(product_name),
}
get_stock("무선 마우스") # 상품 이름으로 현재 재고 조회
실행 결과
{'product_name': '무선 마우스', 'stock': 7}
- 위 값은 파이썬 함수를 직접 실행한 결과. 아직 모델을 사용하지 않은 단계.
get_stock("기계식 키보드")
실행 결과
{'product_name': '기계식 키보드', 'stock': 0}
- 수량
0: 등록된 상품이지만 현재 품절.
get_stock("웹캠")
실행 결과
{'product_name': '웹캠', 'stock': None}
None: 재고표에 등록되지 않은 상품. 수량0과 구분.- 실제 서비스에서는 이 함수 안에서 데이터베이스나 재고 관리 API를 조회하도록 연결.
도구 설명 전달하기
- 함수의 파이썬 코드를 모델에 보내는 대신 이름·설명·입력 형식을 전달.
parameters: 함수가 받을 인자의 JSON Schema. 이미 배운 자료형·필수 항목 표기를 사용.description: 조회할 정보와 입력할 상품 이름을 설명. 재고 수량 자체는 포함하지 않음.- 아래 블록은 그대로 실행하고
name,description,properties만 우선 확인.
stock_tool = {
"type": "function",
"function": {
"name": "get_stock",
"description": "상품의 현재 재고를 조회한다. stock이 0이면 품절, null이면 등록되지 않은 상품이다.",
"parameters": {
"type": "object",
"properties": {
"product_name": {
"type": "string",
"description": "조회할 상품 이름. 예: 무선 마우스, 기계식 키보드, USB 허브"
}
},
"required": [
"product_name"
],
"additionalProperties": False
}
}
}
모델에 호출 요청하기
tool_choice: 이 단계에서는 지정한 함수 호출을 요청. 모델의 자유로운 도구 선택보다 호출 흐름을 먼저 확인하기 위한 설정.parallel_tool_calls=False: 이 실습에서는 한 번에 하나의 도구 호출만 요청.
messages = [
{"role": "system", "content": "재고는 도구 결과로 답하세요. stock이 null이면 등록되지 않은 상품이라고 안내하세요."},
{"role": "user", "content": "무선 마우스는 지금 몇 개 남아 있나요?"},
]
response = client.chat.completions.create(
model=model,
messages=messages,
tools=[stock_tool],
tool_choice={"type": "function", "function": {"name": "get_stock"}},
parallel_tool_calls=False,
)
assistant_message = response.choices[0].message
tool_call = assistant_message.tool_calls[0] # 첫 번째 함수 호출 요청
tool_call.function.name # 요청한 함수 이름
tool_call.function.arguments # JSON 문자열로 전달된 인자
- 확인: 함수 이름이
get_stock인지, 상품 이름이 질문의 대상을 반영했는지 살펴보기. - 호출 요청이 반환됐다고 재고 조회가 실행된 것은 아님.
인자를 읽고 실제 실행하기
json.loads(): JSON 문자열을 파이썬 사전으로 변환.arguments["product_name"]: 인자 사전에서 조회할 상품 이름을 꺼내는 표현.- 실제 실행할 함수는 이 실습에서 정의한
get_stock으로 고정.
arguments = json.loads(tool_call.function.arguments)
arguments
result = get_stock(arguments["product_name"])
result
결과를 모델에 돌려주기
- 대화에는 사용자의 질문뿐 아니라 모델의 호출 요청도 포함.
role="tool": 도구에서 반환된 결과.tool_call_id: 어떤 호출 요청에 대한 결과인지 연결하는 식별자.json.dumps(): 파이썬 값을 결과 메시지에 넣을 JSON 문자열로 변환.
messages.append(assistant_message.model_dump(exclude_none=True))
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result, ensure_ascii=False),
})
final_response = client.chat.completions.create(
model=model,
messages=messages,
tools=[stock_tool],
tool_choice="none", # 조회 결과로 답하고 추가 도구 호출은 하지 않음
)
final_response.choices[0].message.content
- 확인: 상품 이름과 재고 수량이
result와 일치하는가? 수량을 추측하거나 없는 상품을 품절로 설명하지 않는가? - 답변 문장은 실행마다 달라질 수 있음. 문장 모양보다 조회 결과의 반영 여부 확인.
재고가 바뀌면 답변도 바뀔까?
- 재고표에서 무선 마우스 수량을 변경. 이 셀은 매장의 재고가 바뀐 상황을 단순화한 예.
inventory["무선 마우스"] = 2 # 재고표의 현재 수량 변경
get_stock("무선 마우스")
실행 결과
{'product_name': '무선 마우스', 'stock': 2}
- 질문을 그대로 두고
messages를 만드는 셀부터 최종 답변 셀까지 다시 실행. - 모델을 다시 학습하거나 질문에 새 수량을 적지 않아도, 새 조회 결과를 전달해 달라진 재고를 안내 가능.
- 확인: 최종 답변이 이전 수량 대신 변경된 수량을 반영하는가?
다른 상품으로 바꿔 보기
- 질문을 “기계식 키보드는 지금 몇 개 남아 있나요?”로 변경.
- 이어서 “웹캠은 지금 몇 개 남아 있나요?”로 변경.
- 질문을 바꿀 때마다
messages를 만드는 셀부터 다시 실행. 이전 질문·호출 기록을 새 실험에 섞지 않기. - 함수 이름 → 상품 이름 → 실행 결과 → 최종 답변 순서로 비교. 품절과 미등록 상품을 구분하는지 확인.
출처: OpenRouter 함수 호출.
퀴즈
현재 재고를 안내할 때 조회 도구가 필요한 이유는?
퀴즈를 풀려면 대화형 기능을 불러와야 합니다.