함수 도구
함수 도구 (Function Tools)
함수 도구는 모델이 행동을 취하고 응답을 만들어내는 데 도움이 될 추가 정보를 가져오는 메커니즘이에요. 에이전트가 필요로 할 모든 맥락을 지시사항에 다 넣기가 비현실적이거나 불가능할 때, 또는 응답 생성에 필요한 로직 일부를 (AI 기반이 아닐 수도 있는) 다른 도구로 위임해 에이전트의 행동을 더 결정적이고 안정적으로 만들고 싶을 때 유용해요.
모델이 그 함수를 최종 행동으로 호출하게 하고 그 결과를 모델로 되돌려보내지 않으려면, 출력 함수(output function) 를 대신 쓰면 돼요.
에이전트에 도구를 등록하는 방법은 여러 가지가 있어요:
@agent.tool데코레이터 — 에이전트 컨텍스트(context) 에 접근해야 하는 도구용@agent.tool_plain데코레이터 — 에이전트 컨텍스트(context) 에 접근할 필요가 없는 도구용Agent의tools키워드 인자 — 일반 함수나Tool인스턴스를 받을 수 있어요
더 고급 사용이 필요하면, toolsets 기능으로 (여러분이 만들거나 MCP 서버 나 다른 서드파티 가 제공하는) 도구 모음을 관리하고 Agent 의 toolsets 키워드 인자로 한 번에 등록할 수 있어요. 내부적으로 모든 tools 와 toolsets 는 모델에 제공되는 하나의 결합 툴셋(combined toolset) 으로 모여요.
함수 도구 vs RAG
함수 도구는 기본적으로 RAG(Retrieval-Augmented Generation)의 "R" 이에요 — 모델이 추가 정보를 요청할 수 있게 해서 할 수 있는 일을 확장해 주죠.
Pydantic AI 도구와 RAG의 주된 의미론적 차이는, RAG가 벡터 검색과 동의어인 반면 Pydantic AI 도구는 더 범용적이라는 거예요. 벡터 검색이 필요하면 embeddings 지원으로 여러 제공자에 걸쳐 임베딩을 만들 수 있어요.
함수 도구 vs 구조화된 출력 (Structured Outputs)
이름이 말해주듯 함수 도구는 모델의 "tools"·"functions" API를 써서 모델이 호출할 수 있는 게 뭔지 알려줘요. 도구·함수는 기본 tool 출력 모드 에서 구조화된 출력 의 스키마를 정의하는 데도 쓰여요. 그래서 모델이 여러 도구에 접근할 수 있는데, 그중 일부는 함수 도구를 호출하고 다른 것들은 런을 끝내고 최종 출력을 만들어내요.
데코레이터로 등록하기
@agent.tool 이 기본 데코레이터로 여겨져요. 대부분의 경우 도구가 에이전트 컨텍스트(context) 에 접근해야 하기 때문이에요.
둘 다 쓰는 예시를 볼게요:
dice_game.py
import random
from pydantic_ai import Agent, RunContext
agent = Agent(
'google:gemini-3-flash-preview', # (1)
deps_type=str, # (2)
instructions=(
"You're a dice game, you should roll the die and see if the number "
"you get back matches the user's guess. If so, tell them they're a winner. "
"Use the player's name in the response."
),
)
@agent.tool_plain # (3)
def roll_dice() -> str:
"""Roll a six-sided die and return the result."""
return str(random.randint(1, 6))
@agent.tool # (4)
def get_player_name(ctx: RunContext[str]) -> str:
"""Get the player's name."""
return ctx.deps
dice_result = agent.run_sync('My guess is 4', deps='Anne') # (5)
print(dice_result.output)
#> Congratulations Anne, you guessed correctly! You're a winner!
이건 꽤 단순한 작업이라 빠르고 저렴한 Gemini flash 모델을 쓸 수 있어요.
예시를 단순하게 유지하려고 사용자 이름을 문자열 의존성으로 넘겨요.
이 도구는 어떤 컨텍스트도 필요 없고 그냥 난수를 돌려줘요. 이런 경우엔 동적 지시사항을 쓸 수도 있을 거예요.
이 도구는 플레이어 이름이 필요하므로 RunContext 로 의존성(여기선 그냥 플레이어 이름)에 접근해요.
플레이어 이름을 의존성으로 넘기며 에이전트를 실행해요.
(이 예시는 완결돼 있어 그대로 실행할 수 있어요.)
그 게임의 메시지를 찍어서 무슨 일이 있었는지 볼게요:
dice_game_messages.py
from dice_game import dice_result
print(dice_result.all_messages())
"""
[
ModelRequest(
parts=[
UserPromptPart(
content='My guess is 4',
timestamp=datetime.datetime(...),
)
],
timestamp=datetime.datetime(...),
instructions="You're a dice game, you should roll the die and see if the number you get back matches the user's guess. If so, tell them they're a winner. Use the player's name in the response.",
run_id='...',
conversation_id='...',
),
ModelResponse(
parts=[
ToolCallPart(
tool_name='roll_dice', args={}, tool_call_id='pyd_ai_tool_call_id'
)
],
usage=RequestUsage(cost=Decimal('0.000033'), input_tokens=54, output_tokens=2),
model_name='gemini-3-flash-preview',
timestamp=datetime.datetime(...),
run_id='...',
conversation_id='...',
),
ModelRequest(
parts=[
ToolReturnPart(
tool_name='roll_dice',
content='4',
tool_call_id='pyd_ai_tool_call_id',
timestamp=datetime.datetime(...),
)
],
timestamp=datetime.datetime(...),
instructions="You're a dice game, you should roll the die and see if the number you get back matches the user's guess. If so, tell them they're a winner. Use the player's name in the response.",
run_id='...',
conversation_id='...',
),
ModelResponse(
parts=[
ToolCallPart(
tool_name='get_player_name', args={}, tool_call_id='pyd_ai_tool_call_id'
)
],
usage=RequestUsage(cost=Decimal('0.0000395'), input_tokens=55, output_tokens=4),
model_name='gemini-3-flash-preview',
timestamp=datetime.datetime(...),
run_id='...',
conversation_id='...',
),
ModelRequest(
parts=[
ToolReturnPart(
tool_name='get_player_name',
content='Anne',
tool_call_id='pyd_ai_tool_call_id',
timestamp=datetime.datetime(...),
)
],
timestamp=datetime.datetime(...),
instructions="You're a dice game, you should roll the die and see if the number you get back matches the user's guess. If so, tell them they're a winner. Use the player's name in the response.",
run_id='...',
conversation_id='...',
),
ModelResponse(
parts=[
TextPart(
content="Congratulations Anne, you guessed correctly! You're a winner!"
)
],
usage=RequestUsage(cost=Decimal('0.000064'), input_tokens=56, output_tokens=12),
model_name='gemini-3-flash-preview',
timestamp=datetime.datetime(...),
run_id='...',
conversation_id='...',
),
]
"""
이걸 다이어그램으로 나타낼 수 있어요:
sequenceDiagram
participant Agent
participant LLM
Note over Agent: Send prompts
Agent ->> LLM: System: "You're a dice game..."<br>User: "My guess is 4"
activate LLM
Note over LLM: LLM decides to use<br>a tool
LLM ->> Agent: Call tool<br>roll_dice()
deactivate LLM
activate Agent
Note over Agent: Rolls a six-sided die
Agent -->> LLM: ToolReturn<br>"4"
deactivate Agent
activate LLM
Note over LLM: LLM decides to use<br>another tool
LLM ->> Agent: Call tool<br>get_player_name()
deactivate LLM
activate Agent
Note over Agent: Retrieves player name
Agent -->> LLM: ToolReturn<br>"Anne"
deactivate Agent
activate LLM
Note over LLM: LLM constructs final response
LLM ->> Agent: ModelResponse<br>"Congratulations Anne, ..."
deactivate LLM
Note over Agent: Game session complete
에이전트 인자로 등록하기
데코레이터 외에도 Agent 생성자 의 tools 인자로 도구를 등록할 수 있어요. 도구를 재사용하고 싶을 때 유용하고, 도구에 대한 더 세밀한 제어도 가능해요.
dice_game_tool_kwarg.py
import random
from pydantic_ai import Agent, RunContext, Tool
instructions = """You're a dice game, you should roll the die and see if the number
you get back matches the user's guess. If so, tell them they're a winner.
Use the player's name in the response.
"""
def roll_dice() -> str:
"""Roll a six-sided die and return the result."""
return str(random.randint(1, 6))
def get_player_name(ctx: RunContext[str]) -> str:
"""Get the player's name."""
return ctx.deps
agent_a = Agent(
'google:gemini-3-flash-preview',
deps_type=str,
tools=[roll_dice, get_player_name], # (1)
instructions=instructions,
)
agent_b = Agent(
'google:gemini-3-flash-preview',
deps_type=str,
tools=[ # (2)
Tool(roll_dice, takes_ctx=False),
Tool(get_player_name, takes_ctx=True),
],
instructions=instructions,
)
dice_result = {}
dice_result['a'] = agent_a.run_sync('My guess is 6', deps='Yashar')
dice_result['b'] = agent_b.run_sync('My guess is 4', deps='Anne')
print(dice_result['a'].output)
#> Tough luck, Yashar, you rolled a 4. Better luck next time.
print(dice_result['b'].output)
#> Congratulations Anne, you guessed correctly! You're a winner!
Agent 생성자로 도구를 등록하는 가장 단순한 방법은 함수 목록을 넘기는 거예요. 함수 시그니처를 검사해서 그 도구가 RunContext 를 받는지 결정해요.
agent_a 와 agent_b 는 똑같아요 — 하지만 Tool 을 쓰면 도구 정의를 재사용하고 도구 정의 방식을 더 세밀하게 제어할 수 있어요. 예를 들어 이름·설명을 설정하거나 커스텀 prepare 메서드를 쓰는 식이죠.
(이 예시는 완결돼 있어 그대로 실행할 수 있어요.)
도구 출력 (Tool Output)
도구는 Pydantic이 JSON으로 직렬화할 수 있는 것이면 뭐든 돌려줄 수 있어요. 멀티모달 콘텐츠와 메타데이터를 포함한 고급 출력 옵션은 Advanced Tool Features 를 보세요.
도구 스키마 (Tool Schema)
함수 파라미터는 함수 시그니처에서 추출되고, RunContext 를 제외한 모든 파라미터가 그 도구 호출의 스키마를 만드는 데 쓰여요.
더 좋은 건, Pydantic AI가 함수에서 docstring을 추출하고 (griffe 덕분에) docstring에서 파라미터 설명을 추출해 스키마에 추가해요.
Griffe는 google, numpy, sphinx 스타일 docstring 에서 파라미터 설명을 추출할 수 있어요. Pydantic AI는 docstring을 보고 사용할 형식을 추론하지만, docstring_format 으로 명시적으로 설정할 수도 있어요. require_parameter_descriptions=True 를 설정해 파라미터 설명을 강제할 수도 있어요. 파라미터 설명이 빠지면 UserError 가 발생해요.
docstring의 세 부분이 모델에 닿아요: 앞쪽 설명(leading description), 파라미터 설명, returns 섹션의 첫 번째 항목. Raises, Examples, Notes, Warnings, Yields 같은 Griffe가 파싱할 수 있는 다른 섹션은 버려져요. 그래서 모델이 행동해야 할 건 앞쪽 설명, 파라미터 설명, 첫 returns 항목 중 하나에 들어가야 해요.
도구의 스키마를 보여주기 위해 여기선 FunctionModel 을 써서 모델이 받게 될 스키마를 출력해요:
tool_schema.py
from pydantic_ai import Agent, ModelMessage, ModelResponse, TextPart
from pydantic_ai.models.function import AgentInfo, FunctionModel
agent = Agent()
@agent.tool_plain(docstring_format='google', require_parameter_descriptions=True)
def foobar(a: int, b: str, c: dict[str, list[float]]) -> str:
"""Get me foobar.
Args:
a: apple pie
b: banana cake
c: carrot smoothie
"""
return f'{a} {b} {c}'
def print_schema(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
tool = info.function_tools[0]
print(tool.description)
#> Get me foobar.
print(tool.parameters_json_schema)
"""
{
'additionalProperties': False,
'properties': {
'a': {'description': 'apple pie', 'type': 'integer'},
'b': {'description': 'banana cake', 'type': 'string'},
'c': {
'additionalProperties': {'items': {'type': 'number'}, 'type': 'array'},
'description': 'carrot smoothie',
'type': 'object',
},
},
'required': ['a', 'b', 'c'],
'type': 'object',
}
"""
return ModelResponse(parts=[TextPart('foobar')])
agent.run_sync('hello', model=FunctionModel(print_schema))
(이 예시는 완결돼 있어 그대로 실행할 수 있어요.)
도구에 JSON 스키마에서 객체로 나타낼 수 있는 단일 파라미터(dataclass, TypedDict, pydantic 모델 등)가 있으면, 그 도구의 스키마는 그 객체 하나로 단순화돼요.
여기선 TestModel.last_model_request_parameters 로 모델에 전달될 도구 스키마를 들여다보는 예시예요.
single_parameter_tool.py
from pydantic import BaseModel
from pydantic_ai import Agent
from pydantic_ai.models.test import TestModel
agent = Agent()
class Foobar(BaseModel):
"""This is a Foobar"""
x: int
y: str
z: float = 3.14
@agent.tool_plain
def foobar(f: Foobar) -> str:
return str(f)
test_model = TestModel()
result = agent.run_sync('hello', model=test_model)
print(result.output)
#> {"foobar":"x=0 y='a' z=3.14"}
print(test_model.last_model_request_parameters.function_tools)
"""
[
ToolDefinition(
name='foobar',
parameters_json_schema={
'properties': {
'x': {'type': 'integer'},
'y': {'type': 'string'},
'z': {'default': 3.14, 'type': 'number'},
},
'required': ['x', 'y'],
'title': 'Foobar',
'type': 'object',
},
description='This is a Foobar',
toolset_id='<agent>',
)
]
"""
(이 예시는 완결돼 있어 그대로 실행할 수 있어요.)
도구 호출 디버깅 (Debugging Tool Calls)
도구의 동작을 이해하는 건 에이전트 개발에서 중요해요. 에이전트를 Logfire 로 계측하면 다음을 볼 수 있어요:
- 각 도구에 전달된 인자
- 각 도구가 돌려준 값
- 각 도구 실행에 걸린 시간
- 발생한 모든 오류
이 가시성은 에이전트가 왜 특정 결정을 했는지 이해하고 도구 구현의 문제를 찾는 데 도움을 줘요.
도구에서 후속 메시지 주입하기
도구는 RunContext.enqueue 로 대화에 추가 메시지를 밀어 넣을 수 있어요 — 도구가 후속 맥락을 추가하거나, 에이전트의 계획을 리다이렉트하거나, 모델이 반응해야 할 이벤트를 드러낼 때 유용해요. 전체 패턴은 Injecting messages mid-run 을 보세요.
See Also
더 많은 도구 기능·통합이 궁금하면:
- Advanced Tool Features — 커스텀 스키마, 동적 도구, 도구 실행·재시도
- Toolsets — 도구 모음 관리
- Native Tools — LLM 제공자가 제공하는 네이티브 도구
- Common Tools — 바로 쓸 수 있는 도구 구현
- Third-Party Tools — MCP·LangChain 등 도구 라이브러리와의 통합
- Deferred Tools — 승인 또는 외부 실행이 필요한 도구