아톨러브

XIII. Python 함수 자체를 분석하여, AI가 사용할 수 있는 Tool 설명서(schema)를 자동으로 만드는 방법 본문

AI, 클라우드, 문서, 자동화/AI_AGENT

XIII. Python 함수 자체를 분석하여, AI가 사용할 수 있는 Tool 설명서(schema)를 자동으로 만드는 방법

아톨 2026. 8. 25. 23:21
반응형

 

    #-------- 1. Type Hint 그리고   __annotations__

    def search(query:str, limit:int=3, safe:bool=True)->str:

        mode = "safe" if safe else "open"

        return f"'{query}' 대해 {limit}개의 결과물({mode})"

 

    print(search("환불 정책"))

    print(search("개점 시간", limit=1, safe=False))

    print(search.__annotations__)

 

    #-------- 2. History 분석

    def summarise(messages:list[dict], limit:int|None=None)->dict[str,int]:

        if limit is not None:

            messages=messages[-limit:]

 

        counts:dict[str,int]={}

        for m in messages:

            role=m["role"]

            counts[role]=counts.get(role,0)+1

        return counts

 

    history=[

        {"role":"user", "content":"a"},

        {"role":"assistant", "content":"b"},

        {"role":"user", "content":"c"},

        {"role":"tool", "content":"d"},

    ]

 

    print(summarise(history))

    print(summarise(history, limit=2))

 

    #-------- 3. Docstring

    def read_file(path:str, max_chars:int=2000)->str:

        """디스크에서 텍스트 파일을 읽어 내용을 반환합니다.

 

        사용자가 특정 파일 이름을 지정할 이것을 사용합니다.

        URL이나 아직 존재하지 않는 파일에는 그것을 사용하지 마십시오.

 

        인수(Args):

            path: 프로젝트 폴더를 기준으로 파일 경로

            max_chars: 잘리기 전에 반환할 최대 문자

        반환값(Returns):

            파일 내용 또는 ERROR 시작하는 오류 메시지

 

        # 위내용을 영어로(아래).

        Read a text file from disk and return its contents.

 

        Use this when the user refers to a specific file by name.

        Do not use it for URLs or for files that do not exist yet.

 

        Args:

            path: Path to the file, relative to the project folder.

            max_chars: Maximum characters to return before truncating.

       

        Returns:

            The file contents, or an error message starting with ERROR.

        """

        return f"contents of {path} (up to {max_chars} chars)"

 

    print(read_file.__doc__.strip().splitlines()[0])

    print("-----")

    print(read_file("notes.txt"))

 

    #-------- 4. 수동으로 Tool Schema 만들기

    import json

 

    schema={

        "name":"read_file",

        "description":"Read a file from disk and return its contents.",

        "input_schema":{

            "type":"object",

            "properties":{

                "path":{

                    "type":"string",

                    "description":"Path to the file, relative to the project folder.",

                },

                "max_chars":{

                    "type":"integer",

                    "description":"Maximum characters to return before truncating.",

                },

            },

            "required":["path"]

        },

    }

 

    print(json.dumps(schema, indent=2))

 

    #-------- 5. inspect.signature() 그리고 inspect.getdoc()

    import inspect

 

    def search(query:str, limit:int=3)->str:

        """Search the knowledge base for a phrase."""

        return "..."

 

    sig=inspect.signature(search)

 

    for name, param in sig.parameters.items():

        required=param.default is inspect.Parameter.empty

        print(f"{name:<8} type={param.annotation.__name__:<5} required={required}")

 

    print("summary: ", inspect.getdoc(search))

 

    #-------- 6. check_args() : Tool 실행 전, 인자검사

    import inspect

 

    def check_args(func, args):

        sig=inspect.signature(func)

        required={n for n, p in sig.parameters.items() if p.default is inspect.Parameter.empty}

        allowed=set(sig.parameters)

 

        missing=required-set(args)

        unknown=set(args)-allowed

 

        if missing:

            return f"missing required argument(s): {sorted(missing)}"

        if unknown:

            return f"unexpected argument(s): {sorted(unknown)}"

        return None

 

    def search(query:str, limit:int=3)->str:

        """Search the knowledge base."""

        return f"{limit} hits for {query}"

 

    for args in [{"query":"refunds"}, {"limit":5}, {"query":"x", "sort":"asc"}]:

        problem=check_args(search,args)

        print(args, "->", problem or search(**args))

 

    #-------- 7. Tool Schema 자동 생성

    import inspect

    import json

 

    JSON_TYPES = {

        str: "string",

        int: "integer",

        float: "number",

        bool: "boolean",

        list: "array",

        dict: "object",

    }

 

    def describe_tool(func):

        sig=inspect.signature(func)

        doc=inspect.getdoc(func) or "No description provided"

        summary=doc.strip().splitlines()[0]

        properties={}

        required=[]

 

        for name, param in sig.parameters.items():

            json_type=JSON_TYPES.get(param.annotation,"string")

            entry={"type":json_type, "description":f"{name}({json_type})"}

 

            if param.default is inspect.Parameter.empty:

                required.append(name)

            else:

                entry["description"] += f", defaults to {param.default!r}"

 

            properties[name] = entry

 

        return {

            "name":func.__name__,

            "description":summary,

            "input_schema":{

                "type":"object",

                "properties":properties,

                "required":required,

            },

        }

 

    def calculator(expr:str)->str:

        """Evaluate a simple arithmetic expression such as 20*5."""

        return str(eval(expr))

 

    def read_file(path:str, max_chars:int=2000)->str:

        """Read a text file from disk. Do not use this for URLs."""

        return f"contents of {path}"

 

    REGISTRY={"calculator":calculator, "read_file":read_file}

 

    schemas=[describe_tool(f) for f in REGISTRY.values()]

    print(json.dumps(schemas, indent=2))

 

이 부분은 앞으로 실제 OpenAI API와 Tool Calling을 연결할 때 매우 중요합니다.


먼저 전체 그림부터 보겠습니다

우리가 이전까지는 Tool을 이렇게 만들었습니다.

 
def calculator(expr):
    return eval(expr)

TOOLS = {
    "calculator": calculator
}
 

그런데 AI 입장에서는 문제가 있습니다.

AI:
"calculator라는 Tool이 있군."

그런데...

"어떤 값을 넣어야 하지?"
"expr이라는 인자가 필요한가?"
"숫자인가? 문자열인가?"
"필수인가?"
 

그래서 AI에게 Tool 설명서를 줘야 합니다.

Tool 설명서
│
├── 이름: calculator
│
├── 설명:
│     수식을 계산합니다
│
└── 입력값
      │
      └── expr
            │
            ├── type: string
            └── required: true
 

이번 코드 전체는 바로 이 과정을 배우는 것입니다.


전체 학습 흐름

이번 코드를 순서대로 보면:

1. Type Hint
       ↓
2. 함수 Annotation
       ↓
3. 함수 Docstring
       ↓
4. 함수 Signature 검사
       ↓
5. 함수 인자 검증
       ↓
6. Python 함수 자동 분석
       ↓
7. Tool Schema 자동 생성
       ↓
8. 여러 Tool을 AI에게 전달
 

즉, 마지막 목표는:

Python 함수
     ↓
inspect
     ↓
자동 분석
     ↓
JSON Schema
     ↓
LLM에게 Tool 설명 전달
 

입니다.


1. 첫 번째 코드 — Type Hint

 
def search(query:str, limit:int=3, safe:bool=True)->str:
 

이 코드가 이번 학습의 시작입니다.

천천히 분해해 보겠습니다.

 
def search(
    query: str,
    limit: int = 3,
    safe: bool = True
) -> str:
 

각 부분의 의미

search
│
├── query
│     └── str
│
├── limit
│     ├── int
│     └── 기본값 3
│
├── safe
│     ├── bool
│     └── 기본값 True
│
└── 반환값
      └── str
 

query: str

 
query: str
 

뜻은:

query라는 변수는 문자열(str)을 사용하는 것이 좋습니다.

예:

 
search("환불 정책")
 

여기서:

query
↓
"환불 정책"
↓
str
 

limit: int = 3

 
limit: int = 3
 

뜻:

자료형 → int
기본값 → 3
 

따라서:

 
search("환불 정책")
 

이면:

query = "환불 정책"
limit = 3
 

자동으로 됩니다.

하지만:

 
search("환불 정책", limit=10)
 

하면:

limit = 10
 

으로 바뀝니다.


safe: bool = True

 
safe: bool = True
 

뜻:

safe
│
├── True
└── False
 

두 가지 값 중 하나를 사용하는 Boolean 값이라는 뜻입니다.


-> str

 
def search(...) -> str:
 

이 함수가 최종적으로 문자열을 반환한다는 의미입니다.

예:

 
return "검색 결과입니다."
 

중요한 점

Type Hint는 일반적으로 Python이 자동으로 강제하지 않습니다.

예:

 
def search(query: str):
    ...
 

라고 해도:

 
search(123)
 

이 무조건 즉시 오류가 나는 것은 아닙니다.

즉:

Type Hint
=
"이 값을 이런 타입으로 사용하는 것이 좋습니다"
+
도구가 함수 구조를 이해하는 데 도움
 

입니다.


함수 실행

 
def search(query:str, limit:int=3, safe:bool=True)->str:
    mode = "safe" if safe else "open"
    return f"'{query}'에 대해 {limit}개의 결과물({mode})"
 

삼항 조건문

 
mode = "safe" if safe else "open"
 

이것은 전에 배운 조건문을 한 줄로 쓴 것입니다.

풀어서 쓰면:

 
if safe:
    mode = "safe"
else:
    mode = "open"
 

입니다.


실행 1

 
search("환불 정책")
 

기본값:

query = "환불 정책"
limit = 3
safe = True
 

따라서:

mode = "safe"
 

결과:

'환불 정책'에 대해 3개의 결과물(safe)
 

실행 2

 
search("개점 시간", limit=1, safe=False)
 
query = "개점 시간"
limit = 1
safe = False
 

따라서:

mode = "open"
 

결과:

'개점 시간'에 대해 1개의 결과물(open)
 

2. __annotations__

 
print(search.__annotations__)
 

이것이 이번 코드에서 중요한 부분입니다.

함수를 만들 때:

 
def search(
    query: str,
    limit: int = 3,
    safe: bool = True
) -> str:
 

Python은 Annotation 정보를 저장해 둡니다.

대략:

 
{
    "query": str,
    "limit": int,
    "safe": bool,
    "return": str
}
 

입니다.

구조를 그림으로 보면:

search 함수
│
├── query → str
├── limit → int
├── safe → bool
└── return → str
 

왜 이것이 중요한가?

앞으로 우리가 만들고 싶은 것은:

Python 함수

def search(query: str, limit: int = 3):
 

를 자동으로 읽어서:

 
{
  "name": "search",
  "parameters": {
    "query": {
      "type": "string"
    },
    "limit": {
      "type": "integer"
    }
  }
}
 

로 바꾸는 것입니다.

즉:

Python Type Hint → AI Tool Schema

로 발전하게 됩니다.


3. 두 번째 코드 — History 분석

 
def summarise(
    messages:list[dict],
    limit:int|None=None
)->dict[str,int]:
 

이번에는 Type Hint가 조금 더 발전했습니다.


messages:list[dict]

 
messages: list[dict]
 

뜻:

messages는 Dictionary가 들어 있는 List입니다.

우리가 지금까지 Agent에서 사용한 History와 정확히 같습니다.

 
history = [
    {"role":"user", "content":"a"},
    {"role":"assistant", "content":"b"},
    {"role":"user", "content":"c"},
    {"role":"tool", "content":"d"},
]
 

그림으로 보면:

history
│
├── [0]
│     ├── role = user
│     └── content = a
│
├── [1]
│     ├── role = assistant
│     └── content = b
│
├── [2]
│     ├── role = user
│     └── content = c
│
└── [3]
      ├── role = tool
      └── content = d
 

limit: int | None = None

조금 중요합니다.

 
limit: int | None = None
 

뜻:

limit에는

int
또는
None

둘 중 하나가 들어올 수 있다.
 

예:

 
summarise(history, limit=3)
 

또는:

 
summarise(history, limit=None)
 

가능합니다.


예전 방식

Python에서는 예전에는 보통:

 
Optional[int]
 

또는:

 
Union[int, None]
 

를 사용했습니다.

하지만 현재는:

 
int | None
 

처럼 간단하게 쓸 수 있습니다.


반환값

 
-> dict[str, int]
 

뜻:

Dictionary

Key → str
Value → int
 

예:

 
{
    "user": 2,
    "assistant": 1,
    "tool": 1
}
 

입니다.


함수 내부

 
if limit is not None:
    messages = messages[-limit:]
 

이 부분은 Agent History와 직접 연결됩니다.

예:

 
messages = [
    A,
    B,
    C,
    D
]
 

그리고:

 
limit = 2
 

이면:

 
messages[-2:]
 

결과:

[C, D]
 

입니다.


왜 Agent에서 중요한가?

Agent는 대화가 길어질 수 있습니다.

1번째 메시지
2번째 메시지
3번째 메시지
...
100번째 메시지
...
1000번째 메시지
 

이 모든 것을 LLM에 보내면:

  • 비용 증가
  • 응답 속도 감소
  • Context Window 문제

가 발생합니다.

그래서:

 
messages = messages[-limit:]
 

처럼 최근 기록만 유지할 수 있습니다.

이것이 이전에 배운:

History Trim

과 연결됩니다.


counts

 
counts:dict[str,int] = {}
 

뜻:

counts는 Dictionary

Key → 문자열
Value → 정수
 

처음에는:

 
{}
 

빈 Dictionary입니다.


for 반복

 
for m in messages:
 

예:

 
messages = [
    {"role":"user"},
    {"role":"assistant"},
    {"role":"user"}
]
 

실행:

1회차
m = {"role":"user"}

2회차
m = {"role":"assistant"}

3회차
m = {"role":"user"}
 

role 추출

 
role = m["role"]
 

예:

 
m = {
    "role":"user",
    "content":"a"
}
 

그러면:

role = "user"
 

가장 중요한 부분

 
counts[role] = counts.get(role, 0) + 1
 

이 부분을 천천히 보겠습니다.


첫 번째 user

처음:

 
counts = {}
 
role = "user"
 

그러면:

 
counts.get("user", 0)
 

"현재 user라는 Key가 있니?"

없습니다.

그러면 기본값:

0
 

따라서:

 
counts["user"] = 0 + 1
 

결과:

 
{
    "user": 1
}
 

assistant

role = "assistant"
 

현재:

 
{
    "user": 1
}
 

assistant 없음.

 
counts.get("assistant", 0)
 

0
 

따라서:

 
counts["assistant"] = 1
 

결과:

 
{
    "user": 1,
    "assistant": 1
}
 

다시 user

 
counts.get("user", 0)
 

1
 

따라서:

 
counts["user"] = 1 + 1
 

2
 

최종:

 
{
    "user": 2,
    "assistant": 1,
    "tool": 1
}
 

4. Docstring

다음 코드입니다.

 
def read_file(path:str, max_chars:int=2000)->str:
    """디스크에서 텍스트 파일을 읽어 그 내용을 반환합니다.
 

함수 안에 들어 있는 여러 줄 문자열입니다.

이것을 Docstring이라고 합니다.


일반 문자열과 Docstring의 차이

일반:

 
text = "hello"
 

Docstring:

 
def hello():
    """이 함수는 인사합니다."""
 

Docstring은 함수 설명으로 사용됩니다.


__doc__

 
print(read_file.__doc__)
 

하면 함수의 설명을 가져올 수 있습니다.

즉:

read_file 함수
│
├── 이름
│
├── 코드
│
└── Docstring
 

입니다.


이번 코드

 
print(
    read_file.__doc__
    .strip()
    .splitlines()[0]
)
 

체인처럼 연결되어 있습니다.

하나씩 보겠습니다.


read_file.__doc__

Docstring 전체를 가져옵니다.

디스크에서 텍스트 파일을 읽어...
...
 

.strip()

앞뒤 공백과 줄바꿈 제거


.splitlines()

줄 단위로 나눕니다.

[
    "디스크에서 텍스트 파일을 읽어 그 내용을 반환합니다.",
    "",
    "사용자가 특정 파일 이름을 지정할 때..."
]
 

[0]

첫 번째 줄

디스크에서 텍스트 파일을 읽어 그 내용을 반환합니다.
 

왜 Tool에서 Docstring이 중요한가?

AI에게:

Tool 이름: read_file
 

만 알려주면 부족합니다.

AI가:

이걸 언제 사용하지?
 

라고 생각할 수 있습니다.

그래서:

Tool: read_file

설명:
Read a text file from disk.

언제 사용하는가:
사용자가 특정 파일 이름을 언급했을 때

언제 사용하지 않는가:
URL에는 사용하지 말 것
 

같은 설명을 제공합니다.

이 설명이 바로 LLM의 Tool 선택 능력을 높입니다.


5. 수동으로 Tool Schema 만들기

 
schema = {
    "name":"read_file",
    "description":"Read a file from disk and return its contents.",
 

이제 본격적으로 AI에게 Tool 설명서를 만들기 시작합니다.

구조:

schema
│
├── name
│
├── description
│
└── input_schema
 

name

 
"name": "read_file"
 

AI가 Tool을 호출할 때:

"read_file라는 Tool을 사용하고 싶습니다."
 

라고 지정할 수 있습니다.


description

 
"description":
"Read a file from disk and return its contents."
 

AI에게:

이 Tool은 파일을 읽는 도구입니다.

라고 알려줍니다.


input_schema

 
"input_schema": {
    "type":"object",
 

뜻:

입력값 전체가 Object 구조입니다.

Python 관점에서는 거의:

 
dict
 

라고 생각하면 됩니다.

예:

 
{
    "path":"notes.txt",
    "max_chars":2000
}
 

properties

 
"properties": {
 

실제 입력값의 구조를 정의합니다.

properties
│
├── path
│
└── max_chars
 

path

 
"path": {
    "type":"string",
    "description":"Path to the file..."
}
 

AI에게:

path라는 값을 넣어야 합니다.

자료형:
string

설명:
프로젝트 폴더 기준 파일 경로
 

max_chars

 
"max_chars": {
    "type":"integer"
}
 
max_chars
↓
integer
 

입니다.


required

 
"required":["path"]
 

뜻:

반드시 필요한 인자

path
 

반면:

max_chars
 

는 기본값이 있으므로 선택사항입니다.


왜 Schema가 필요한가?

AI에게 이렇게 말하는 것과:

read_file라는 함수가 있어.
 

이렇게 말하는 것은 차이가 큽니다.

 
{
  "name": "read_file",
  "description": "Read a file from disk",
  "input_schema": {
    "properties": {
      "path": {
        "type": "string"
      },
      "max_chars": {
        "type": "integer"
      }
    },
    "required": [
      "path"
    ]
  }
}
 

AI는:

무슨 Tool인가?
→ read_file

언제 사용하는가?
→ 파일을 읽을 때

무슨 값을 넣어야 하는가?
→ path

어떤 타입인가?
→ string

필수인가?
→ yes
 

를 알 수 있습니다.


6. inspect.signature()

이제 굉장히 재미있는 부분입니다.

 
import inspect
 

inspect는 Python 코드 자체를 조사하는 도구입니다.


 
def search(query:str, limit:int=3)->str:
 

inspect를 사용하면 Python이 함수에게 묻습니다.

"너 이름이 뭐야?"
"어떤 인자가 필요해?"
"기본값은 뭐야?"
"자료형은 뭐야?"
"Docstring은 뭐야?"
 

signature

 
sig = inspect.signature(search)
 

대략:

sig

(
    query: str,
    limit: int = 3
) -> str
 

라는 함수 구조 정보를 가져옵니다.


parameters

 
sig.parameters
 

대략:

{
    "query": Parameter(...),
    "limit": Parameter(...)
}
 

입니다.


반복문

 
for name, param in sig.parameters.items():
 

첫 번째:

name = "query"
param = query의 상세 정보
 

두 번째:

name = "limit"
param = limit의 상세 정보
 

필수 인자 판단

 
required = (
    param.default is inspect.Parameter.empty
)
 

이 부분이 매우 중요합니다.


query

 
query: str
 

기본값이 없습니다.

따라서:

param.default
↓
Parameter.empty
 

그래서:

 
required = True
 

limit

 
limit: int = 3
 

기본값이 있습니다.

param.default
↓
3
 

따라서:

 
required = False
 

출력

 
print(
    f"{name:<8} "
    f"type={param.annotation.__name__:<5} "
    f"required={required}"
)
 

여기서:

 
{name:<8}
 

이전에 배운 format specifier입니다.

<8
↓
왼쪽 정렬
8칸
 

예:

query    type=str   required=True
limit    type=int   required=False
 

보기 좋게 정렬됩니다.


7. inspect.getdoc()

 
print(
    "summary: ",
    inspect.getdoc(search)
)
 

이 함수의 Docstring을 가져옵니다.

 
def search(...):
    """Search the knowledge base for a phrase."""
 

결과:

Search the knowledge base for a phrase.
 

지금까지 중요한 연결

여기까지:

Python 함수

def search(
    query: str,
    limit: int = 3
) -> str:
    """Search the knowledge base."""
 

에서 자동으로:

inspect

↓
이름: search
↓
인자: query, limit
↓
타입: str, int
↓
기본값: 없음, 3
↓
Docstring: Search the knowledge base
 

를 얻었습니다.

이 정보를 나중에 JSON으로 만들면 Tool Schema가 됩니다.


8. check_args()

이제 실제 Tool 실행 전에 인자를 검사합니다.

 
def check_args(func, args):
 

입력:

func
↓
실행할 함수

args
↓
AI가 보내온 인자
 

예:

 
func = search

args = {
    "query": "refunds"
}
 

함수 구조 조사

 
sig = inspect.signature(func)
 

예:

 
search(query:str, limit:int=3)
 

를 분석합니다.


required 만들기

 
required = {
    n
    for n, p in sig.parameters.items()
    if p.default is inspect.Parameter.empty
}
 

이것은 Set Comprehension입니다.

결과:

 
{"query"}
 

입니다.

왜냐하면:

query
→ 기본값 없음
→ 필수

limit
→ 기본값 3
→ 선택
 

이기 때문입니다.


allowed

 
allowed = set(sig.parameters)
 

결과:

 
{
    "query",
    "limit"
}
 

즉:

이 함수가 허용하는 인자 목록

입니다.


missing

 
missing = required - set(args)
 

예:

 
args = {
    "limit": 5
}
 

그러면:

required
=
{"query"}

set(args)
=
{"limit"}
 

따라서:

{"query"} - {"limit"}

=
{"query"}
 

즉:

query가 빠졌다!
 

입니다.


unknown

 
unknown = set(args) - allowed
 

예:

 
args = {
    "query":"x",
    "sort":"asc"
}
 

그러면:

set(args)
=
{"query", "sort"}

allowed
=
{"query", "limit"}
 

따라서:

{"query", "sort"}
-
{"query", "limit"}

=
{"sort"}
 

즉:

sort라는 예상하지 않은 인자가 있다.
 

입니다.


실행 결과

정상

 
{"query":"refunds"}
 

 
search(query="refunds")
 

3 hits for refunds
 

필수 인자 없음

 
{"limit":5}
 

missing required argument(s): ['query']
 

이상한 인자

 
{
    "query":"x",
    "sort":"asc"
}
 

unexpected argument(s): ['sort']
 

이것이 Agent에서 왜 중요한가?

LLM이 Tool을 호출할 때:

 
{
    "tool": "search",
    "arguments": {
        "query": "환불 정책",
        "sort": "price"
    }
}
 

라고 할 수 있습니다.

하지만 실제 함수는:

 
def search(query, limit=3):
 

입니다.

sort라는 인자는 없습니다.

따라서 실행 전에:

LLM
 ↓
Tool 요청
 ↓
check_args()
 ↓
정상?
 │
 ├── YES → Tool 실행
 │
 └── NO
       ↓
     Error Observation
       ↓
     다시 LLM
 

이 구조가 됩니다.


9. 마지막 코드 — Tool Schema 자동 생성

이제 이번 코드의 핵심입니다.

 
JSON_TYPES = {
    str: "string",
    int: "integer",
    float: "number",
    bool: "boolean",
    list: "array",
    dict: "object",
}
 

Python Type과 JSON Type을 연결하는 Dictionary입니다.


구조

Python Type
      ↓
JSON Type

str
 ↓
string

int
 ↓
integer

float
 ↓
number

bool
 ↓
boolean

list
 ↓
array

dict
 ↓
object
 

왜 필요한가?

Python 함수는:

 
def search(
    query: str,
    limit: int
):
 

이지만 AI Tool Schema는:

 
{
    "query": {
        "type": "string"
    },
    "limit": {
        "type": "integer"
    }
}
 

를 원합니다.

따라서 변환기가 필요합니다.

Python
str
 ↓
JSON_TYPES
 ↓
string
 

describe_tool()

 
def describe_tool(func):
 

이 함수의 목표는 단 하나입니다.

Python 함수
      ↓
describe_tool()
      ↓
Tool Schema Dictionary
 

함수 Signature 가져오기

 
sig = inspect.signature(func)
 

예:

 
def read_file(
    path: str,
    max_chars: int = 2000
):
 

sig
│
├── path
│     ├── annotation = str
│     └── default = 없음
│
└── max_chars
      ├── annotation = int
      └── default = 2000
 

Docstring 가져오기

 
doc = inspect.getdoc(func) or "No description provided"
 

뜻:

Docstring이 있으면
↓
사용

없으면
↓
"No description provided"
 

첫 번째 줄만 사용

 
summary = doc.strip().splitlines()[0]
 

예:

 
"""Read a text file from disk.
Do not use this for URLs."""
 

첫 번째 줄:

Read a text file from disk.
 

이것을 Tool 설명으로 사용합니다.


properties와 required 준비

 
properties = {}
required = []
 

처음에는:

properties = {}

required = []
 

입니다.


모든 Parameter 분석

 
for name, param in sig.parameters.items():
 

예:

 
def read_file(
    path: str,
    max_chars: int = 2000
)
 

첫 번째:

name = path
 

두 번째:

name = max_chars
 

Python Type → JSON Type

 
json_type = JSON_TYPES.get(
    param.annotation,
    "string"
)
 

예:

path
annotation = str

JSON_TYPES[str]

↓

string
 

max_chars
annotation = int

JSON_TYPES[int]

↓

integer
 

entry 만들기

 
entry = {
    "type": json_type,
    "description": f"{name}({json_type})"
}
 

예:

 
path
 

 
{
    "type": "string",
    "description": "path(string)"
}
 

필수 인자인가?

 
if param.default is inspect.Parameter.empty:
 

예:

 
path: str
 

기본값 없음.

 
required.append(name)
 

 
required = [
    "path"
]
 

기본값이 있다면?

 
max_chars: int = 2000
 

 
entry["description"] += (
    f", defaults to {param.default!r}"
)
 

여기서:

 
!r
 

는 전에 배운 repr()과 관련됩니다.

예:

2000
 

은 그대로 표시됩니다.

문자열이라면:

'hello'
 

처럼 표현됩니다.

결과:

max_chars(integer), defaults to 2000
 

properties에 저장

 
properties[name] = entry
 

첫 번째:

 
properties = {
    "path": {
        "type":"string",
        "description":"path(string)"
    }
}
 

두 번째까지 추가하면:

 
properties = {
    "path": {
        "type":"string",
        "description":"path(string)"
    },

    "max_chars": {
        "type":"integer",
        "description":"max_chars(integer), defaults to 2000"
    }
}
 

최종 반환

 
return {
    "name": func.__name__,
    "description": summary,
    "input_schema": {
        "type": "object",
        "properties": properties,
        "required": required,
    },
}
 

결과 구조:

Tool Schema
│
├── name
│
├── description
│
└── input_schema
      │
      ├── type
      │
      ├── properties
      │      │
      │      ├── path
      │      └── max_chars
      │
      └── required
 

실제 Tool 두 개

 
def calculator(expr:str)->str:
 

Tool 1:

calculator
│
└── expr
      └── string
 

 
def read_file(
    path:str,
    max_chars:int=2000
)->str:
 

Tool 2:

read_file
│
├── path
│     └── string
│
└── max_chars
      └── integer
 

Registry

 
REGISTRY = {
    "calculator": calculator,
    "read_file": read_file
}
 

우리가 이미 배운 구조입니다.

REGISTRY
│
├── calculator
│       ↓
│   calculator 함수
│
└── read_file
        ↓
    read_file 함수
 

모든 Tool 자동 분석

 
schemas = [
    describe_tool(f)
    for f in REGISTRY.values()
]
 

이것은 List Comprehension입니다.

풀어서 쓰면:

 
schemas = []

for f in REGISTRY.values():
    schema = describe_tool(f)
    schemas.append(schema)
 

입니다.


전체 흐름

REGISTRY
│
├── calculator
│       ↓
│   describe_tool()
│       ↓
│   Tool Schema
│
└── read_file
        ↓
    describe_tool()
        ↓
    Tool Schema


↓


schemas = [
    calculator schema,
    read_file schema
]
 

최종 결과의 의미

결국 Python 코드:

 
def calculator(expr: str) -> str:
    """Evaluate a simple arithmetic expression."""
 

를 AI가 이해할 수 있는:

 
{
    "name": "calculator",
    "description": "Evaluate a simple arithmetic expression.",
    "input_schema": {
        "type": "object",
        "properties": {
            "expr": {
                "type": "string"
            }
        },
        "required": [
            "expr"
        ]
    }
}
 

로 자동 변환한 것입니다.


이번 코드가 Agent 전체에서 어디에 있는가?

매우 중요합니다.

이제 Agent 전체 구조를 보면:

Python Tool

def calculator(expr: str):
    ...

        │
        ▼

Type Hint
Docstring
Signature

        │
        ▼

inspect

        │
        ▼

describe_tool()

        │
        ▼

Tool Schema

        │
        ▼

LLM에게 전달

        │
        ▼

LLM:

"calculator를 사용해야겠다"

        │
        ▼

{
    "tool": "calculator",
    "arguments": {
        "expr": "7 * 24"
    }
}

        │
        ▼

check_args()

        │
        ├── 문제 있음
        │       ↓
        │     Error
        │
        ▼

REGISTRY

        │
        ▼

TOOLS["calculator"](**args)

        │
        ▼

calculator("7 * 24")

        │
        ▼

Observation

        │
        ▼

History

        │
        ▼

LLM 재호출
 

이번 코드의 가장 중요한 핵심 3가지

① Type Hint는 단순한 설명이 아니다

처음에는:

 
query: str
 

이 단순히:

query는 문자열입니다.

라고 알려주는 용도로 보입니다.

하지만 Agent에서는:

Type Hint
↓
inspect
↓
자동 분석
↓
JSON Schema
↓
LLM이 Tool 사용법 이해
 

로 연결됩니다.


② Docstring은 사람뿐 아니라 AI에게도 중요하다

 
def read_file(...):
    """Read a text file from disk."""
 

이 설명은:

사람
↓
이 함수가 뭔지 이해

AI
↓
언제 이 Tool을 사용할지 판단
 

하는 데 사용됩니다.


③ Python 함수에서 AI Tool을 자동으로 만들 수 있다

이것이 이번 코드의 가장 중요한 목적입니다.

우리가 직접:

 
schema = {
    ...
}
 

를 일일이 만들 수도 있지만,

함수
↓
inspect
↓
자동 Schema 생성
 

이 가능해집니다.

 

반응형