Recent Posts
Recent Comments
반응형
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
Archives
Today
Total
관리 메뉴

오늘도 공부

6. AI Agent가 사용하는 Tool 스키마 본문

AI/Agent

6. AI Agent가 사용하는 Tool 스키마

행복한 수지아빠 2026. 2. 23. 11:44
반응형

 

 

GitHub - MiniMax-AI/Mini-Agent: A minimal yet professional single agent demo project that showcases the core execution pipeline

A minimal yet professional single agent demo project that showcases the core execution pipeline and production-grade features of agents. - MiniMax-AI/Mini-Agent

github.com

 

"""Demo: Using Tool schemas with base Tool class.

This example demonstrates how to use the Tool base class and its schema methods.
"""

import asyncio
from pathlib import Path
from typing import Any

import yaml

from mini_agent import LLMClient, LLMProvider
from mini_agent.schema import Message
from mini_agent.tools.base import Tool, ToolResult


def load_config():
    """Load config from config.yaml."""
    config_path = Path("mini_agent/config/config.yaml")
    with open(config_path, encoding="utf-8") as f:
        return yaml.safe_load(f)


class WeatherTool(Tool):
    """Example weather tool."""

    @property
    def name(self) -> str:
        return "get_weather"

    @property
    def description(self) -> str:
        return "Get current weather information for a location. Returns temperature and conditions."

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "City and state, e.g. 'San Francisco, CA' or 'London, UK'",
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit (celsius or fahrenheit)",
                },
            },
            "required": ["location"],
        }

    async def execute(self, **kwargs) -> ToolResult:
        """Mock execute method."""
        return ToolResult(success=True, content="Weather data")


class SearchTool(Tool):
    """Example search tool."""

    @property
    def name(self) -> str:
        return "search_web"

    @property
    def description(self) -> str:
        return "Search the web for information about a topic"

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Search query string",
                },
                "max_results": {
                    "type": "integer",
                    "description": "Maximum number of results to return (1-10)",
                },
            },
            "required": ["query"],
        }

    async def execute(self, **kwargs) -> ToolResult:
        """Mock execute method."""
        return ToolResult(success=True, content="Search results")


class CalculatorTool(Tool):
    """Example calculator tool."""

    @property
    def name(self) -> str:
        return "calculator"

    @property
    def description(self) -> str:
        return "Perform arithmetic calculations"

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "Mathematical expression to evaluate, e.g. '2 + 2' or '10 * 5'",
                }
            },
            "required": ["expression"],
        }

    async def execute(self, **kwargs) -> ToolResult:
        """Mock execute method."""
        return ToolResult(success=True, content="Calculation result")


class TranslateTool(Tool):
    """Example translate tool."""

    @property
    def name(self) -> str:
        return "translate"

    @property
    def description(self) -> str:
        return "Translate text from one language to another"

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "text": {
                    "type": "string",
                    "description": "Text to translate",
                },
                "target_language": {
                    "type": "string",
                    "description": "Target language code (e.g. 'en', 'es', 'fr')",
                },
            },
            "required": ["text", "target_language"],
        }

    async def execute(self, **kwargs) -> ToolResult:
        """Mock execute method."""
        return ToolResult(success=True, content="Translation result")


async def demo_tool_schemas():
    """Demonstrate using Tool objects with LLM."""
    config = load_config()

    print("=" * 60)
    print("Method 1: Using Tool Objects with LLM")
    print("=" * 60)

    # Create tool instances
    weather_tool = WeatherTool()
    search_tool = SearchTool()

    # Create client
    client = LLMClient(
        api_key=config["api_key"],
        provider=LLMProvider.ANTHROPIC,
        model="MiniMax-M2.5",
    )

    # Test with a query that should trigger weather tool
    messages = [
        Message(
            role="user",
            content="What's the weather like in Tokyo? I want it in celsius.",
        )
    ]

    print("\nQuery: What's the weather like in Tokyo? I want it in celsius.")
    print("\nAvailable tools:")
    print(f"  1. {weather_tool.name}: {weather_tool.description}")
    print(f"  2. {search_tool.name}: {search_tool.description}")

    # Pass Tool objects directly to generate
    response = await client.generate(
        messages,
        tools=[weather_tool, search_tool],  # Using Tool objects
    )

    print(f"\nResponse content: {response.content}")

    if response.thinking:
        print(f"\nThinking: {response.thinking}")

    if response.tool_calls:
        print(f"\nTool calls made: {len(response.tool_calls)}")
        for tool_call in response.tool_calls:
            print(f"  - Function: {tool_call.function.name}")
            print(f"    Arguments: {tool_call.function.arguments}")


async def demo_multiple_tools():
    """Demonstrate using multiple Tool instances."""
    config = load_config()

    print("\n" + "=" * 60)
    print("Method 2: Using Multiple Tool Instances")
    print("=" * 60)

    # Create tool instances
    calculator_tool = CalculatorTool()
    translate_tool = TranslateTool()

    client = LLMClient(
        api_key=config["api_key"],
        provider=LLMProvider.ANTHROPIC,
        model="MiniMax-M2.5",
    )

    messages = [Message(role="user", content="Calculate 15 * 23 for me")]

    print("\nQuery: Calculate 15 * 23 for me")
    print("\nAvailable tools:")
    print("  1. calculator (Tool)")
    print("  2. translate (Tool)")

    response = await client.generate(messages, tools=[calculator_tool, translate_tool])

    print(f"\nResponse content: {response.content}")

    if response.thinking:
        print(f"\nThinking: {response.thinking}")

    if response.tool_calls:
        print(f"\nTool calls made: {len(response.tool_calls)}")
        for tool_call in response.tool_calls:
            print(f"  - Function: {tool_call.function.name}")
            print(f"    Arguments: {tool_call.function.arguments}")


async def demo_tool_schema_methods():
    """Demonstrate Tool schema conversion methods."""
    print("\n" + "=" * 60)
    print("Method 3: Tool Schema Conversion Methods")
    print("=" * 60)

    weather_tool = WeatherTool()

    print("\nTool to Anthropic schema (to_schema):")
    anthropic_schema = weather_tool.to_schema()
    print(f"  {anthropic_schema}")

    print("\nTool to OpenAI schema (to_openai_schema):")
    openai_schema = weather_tool.to_openai_schema()
    print(f"  {openai_schema}")

    print("\nSchema methods allow flexible tool usage with different LLM providers.")


async def main():
    """Run all demos."""
    print("\n🚀 Tool Schema Demo - Using Tool Base Class\n")

    try:
        # Demo 1: Tool objects with LLM
        await demo_tool_schemas()

        # Demo 2: Multiple tools
        await demo_multiple_tools()

        # Demo 3: Schema methods
        await demo_tool_schema_methods()

        print("\n✅ All demos completed successfully!")

    except Exception as e:
        print(f"\n❌ Error: {e}")
        import traceback

        traceback.print_exc()


if __name__ == "__main__":
    asyncio.run(main())

06_tool_schema_demo.py 해설: 도구 스키마(JSON Schema)는 에이전트의 “행동 공간”이다

이 글은 examples/06_tool_schema_demo.py를 기반으로, Mini Agent에서 Tool 스키마가 어떤 역할을 하는지(그리고 왜 에이전트 동작을 공부할 때 스키마가 핵심인지) 설명합니다.

이 예제는 “Agent 전체 루프”가 아니라 Tool 정의와 스키마 변환을 눈으로 확인하는 데 초점이 있습니다.

TL;DR

  • Tool은 name/description/parameters/execute()로 구성되는 “도구 계약”입니다.
  • parameters는 JSON Schema로, 모델이 “어떤 인자를 만들어야 하는지”를 학습하는 유일한 공식 문서입니다.
  • 이 예제는 tool call을 “생성”하는 것까지 보여주며, 실제 실행/피드백 루프는 Agent가 담당합니다.

준비/실행 방법

이 예제는 실제 API 호출을 합니다. mini_agent/config/config.yaml이 필요합니다.

uv run python examples/06_tool_schema_demo.py

이 예제가 보여주는 3가지 방법

Method 1: Tool 객체를 LLM에 직접 넘기기

demo_tool_schemas()는 WeatherTool과 SearchTool을 만들고:

  • client.generate(messages, tools=[weather_tool, search_tool]) 형태로 Tool 객체를 전달합니다.
  • LLMClient는 provider에 맞춰 Tool을 스키마로 변환해 전송합니다.

여기서 관찰할 포인트:

  • 모델이 “get_weather”를 호출하려고 tool_calls를 생성하는가?
  • arguments를 스키마에 맞춰 만드는가? (예: location, unit)

중요: 이 예제는 tool_calls를 “실제로 실행”하지 않습니다.
따라서 모델이 tool call을 내면, content가 비어있거나 “툴 결과를 기다리는” 상태로 끝날 수 있습니다.
실제 실행 루프는 Agent.run()이 담당합니다.

Method 2: 여러 Tool을 동시에 제공하기

demo_multiple_tools()는 CalculatorTool과 TranslateTool을 동시에 제공합니다.

핵심 학습 포인트:

  • 도구가 많아지면 모델이 “어떤 도구를 써야 하는지”를 판단해야 합니다.
  • 이때 description과 parameters의 품질이 거의 전부를 결정합니다.

Method 3: 스키마 변환 메서드(to_schema / to_openai_schema)

demo_tool_schema_methods()는 weather_tool.to_schema()와 to_openai_schema()를 출력합니다.

이 프로젝트의 Tool 베이스(mini_agent/tools/base.py)는:

  • Anthropic 형식: {name, description, input_schema}
  • OpenAI 형식: {type:"function", function:{name, description, parameters}}

으로 변환하는 헬퍼를 제공합니다.

Tool 스키마를 “에이전트 행동 설계”로 보기

에이전트 관점에서 Tool 스키마는 사실상 “행동 API 문서”입니다.

1) name: 라우팅 키

name은 모델이 선택하는 함수명이며, 런타임은 이 문자열로 tools[name]을 찾아 실행합니다.
따라서:

  • 짧고 명확한 동사형(예: read_file, bash, record_note)
  • 안정적인 naming(중간에 바꾸면 모델 프롬프트/기억과 충돌)

이 중요합니다.

2) description: 도구 선택 품질

모델은 도구를 고를 때 description을 강하게 참고합니다.
좋은 description은:

  • 무엇을 하는지(what)
  • 언제 써야 하는지(when)
  • 어떤 제약이 있는지(limitations)

를 짧게 담습니다.

3) parameters(JSON Schema): 인자 생성 품질

모델이 arguments를 “그럴싸하게” 만드는 게 아니라, 스키마에 맞춰 만들어야 합니다.

실습 팁:

  • required를 명확히 써서 필수 인자를 강제
  • enum으로 선택지를 좁혀 hallucination을 줄임
  • 숫자라면 minimum/maximum/default 같은 제약 추가

이런 제약은 모델을 답답하게 만드는 게 아니라, 오히려 성공률을 올리는 레일입니다.

“tool call 생성”과 “tool 실행”은 완전히 다른 단계

이 예제가 자주 주는 착각이 하나 있습니다.

  • Tool을 전달하면 모델이 tool_calls를 만들어준다
  • 하지만 그걸 실행하는 건 별도의 런타임 책임이다

이 프로젝트에서는:

  • LLMClient.generate(...)는 “tool_calls를 포함한 응답”까지만 담당
  • Agent.run()이 tool_calls를 실제로 실행하고 tool 결과를 메시지 히스토리에 추가

라는 역할 분리가 되어 있습니다.

즉, “에이전트”를 만들려면:

  1. tool schema를 모델에 보여주고
  2. tool_calls를 받으면 실행하고
  3. tool 결과를 다시 모델에게 넣고
  4. tool_calls가 없어질 때까지 반복

이 루프가 필요합니다.

실습 과제(공부용)

  1. WeatherTool 스키마를 더 빡세게 만들기
    unit에 default를 넣거나, location 포맷 예시를 더 추가해 모델의 arguments 품질이 좋아지는지 확인하세요.
  2. OpenAI provider로도 실행해보기
    LLMClient(provider=LLMProvider.OPENAI, ...)로 바꾸고 tool_calls가 동일하게 생성되는지 비교하세요.
  3. “실제 실행”을 붙여보기
    이 예제에서 tool_calls를 받으면 tool.execute(**args)를 호출하고, 그 결과를 다시 모델에 넣는 간단한 루프를 만들어보세요.
    그 순간 06_tool_schema_demo.py는 사실상 “미니 Agent”가 됩니다.

추가

Tool 스키마와 실행 루프를 실제로 합쳐 “자율 수행”을 만들고 싶다면:

  • examples/02_simple_agent.py (가장 단순한 Agent 루프)
  • examples/04_full_agent.py (도구/메모리/MCP까지 통합)

을 함께 참고하는 게 가장 빠른 길입니다.

반응형