본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 06. 29. 21:22

도구의 정의와 제시 ― LLM에게 도구를 부여하고 호출하게 하는 메커니즘【프롬프트로 풀어보는 AI 에이전트 #4】

요약

AI 에이전트가 사용할 수 있는 도구를 정의하고 LLM에게 전달하는 메커니즘을 다룹니다. agent-zero의 자연어 설명 방식과 Hermes의 JSON 스키마 방식을 비교하며 도구 구현과 설명의 차이를 분석합니다.

핵심 포인트

  • 도구 정의는 실제 코드 구현과 LLM을 위한 설명 두 가지 측면이 있음
  • agent-zero는 도구 구현과 자연어 설명서를 분리하여 관리함
  • Hermes는 OpenAI의 function-calling JSON 스키마를 활용함
  • 에이전트의 확장성을 위해 도구 목록을 기계적으로 구성하는 것이 중요함

연재 「프롬프트로 풀어보는 AI 에이전트」 제4회 (제3장).

실재하는 3가지 AI 에이전트 OSS를 실제 코드와 실제 프롬프트를 원전에서 인용하며 분석하여,

최종적으로 「스스로 AI 에이전트를 만들 수 있는」 상태를 목표로 하는 연재입니다.

이 장의 전제 (제1장·제2장 복습)

지금까지 실행형 에이전트의 두 가지 부품을 얻었습니다. 제1장에서는 본체의 루프 ―― Reason–Act–Observe를 반복하는 run_agent ―― 를 필사할 수 있는 형태로 구성했고, 제2장에서는 루프의 맨 앞에 두는

SYSTEM_PROMPT

(인격·행동 규범)을 파일로 외재화했습니다. 전제 지식은 이 두 가지로 충분합니다. 하지만 두 장 모두에서 일부러 추상적인 상태로 남겨둔 빈틈이 있습니다. 제1장의 최소 구현에 있었던 다음 두 줄입니다.

tool_calls = parse_tool_calls(reply) # 응답에서 도구 호출을 추출
obs = run_tool(call["name"], call["args"]) # ★실행은 이 코드(LLM이 아님)로 수행

run_tool은 이름과 인수를 받으면 동작합니다. 하지만, LLM은 애초에 「어떤 도구가・어떤 인수로 사용 가능한지」를 어디서 알게 될까요? 제1장에서는 "도구란 외부 함수·명령어이며, LLM은 의도를 텍스트로 반환할 뿐이다"라고 정의했고, 제2장에서는 "시스템 프롬프트에 도구 목록을 넣어둔다"라고 한마디 언급했을 뿐이었습니다. 본 장에서는 그 "도구를 LLM에게 어떻게 정의하고, 어떻게 목록을 제시할 것인가"를 정면으로 다룹니다.

본 장의 목표는 명확합니다. 독자가 직접 만든 도구를 하나 정의하고, LLM이 이를 호출하게 만드는 최소 사례를 가져가는 것입니다. 이를 위해 도구 정의의 "스키마 (Schema, 타입 규약)"와 "LLM에 대한 제시 방법"을 agent-zero와 Hermes의 실제 코드로 대조합니다.

agent-zero: 도구의 **구현 (.py)**과 LLM을 위한 **설명서 (.md)**를 별도의 파일로 나누고, 모든 설명서를 모아서 프롬프트에 흘려보낸다.

Hermes: OpenAI의 function-calling 스키마 (JSON 딕셔너리)로 도구를 정의하고, API의 tools= 인수에 그대로 전달한다.

같은 "LLM에게 도구를 부여하는 것"이라도, 한쪽은 자연어 설명서를 보여주고, 다른 한쪽은 JSON 함수 스키마를 전달한다. 이 대조가 본 장의 핵심입니다.

도구를 「정의한다」는 것 ―― 두 가지 대상

먼저 용어 하나를 정리하겠습니다. 본 연재에서 **도구 (Tool)**란, LLM 스스로는 할 수 없는 조작 (웹 검색, 파일 읽기/쓰기, 셸 실행 등)을 대행하는 프로그램 측의 함수나 명령어를 가리킵니다 (제1장에서 정의 완료). LLM은 "이 도구를 이 인수로 사용하고 싶다"라는 의도를 텍스트로 반환할 뿐이며, 실제로 동작시키는 것은 당신의 코드입니다.

여기서 "도구를 정의한다"는 것에는 사실 대상(Target)이 다른 두 가지 작업이 포함됩니다.

구현 (코드 대상): 도구가 실제로 무엇을 하는가. web_search(query)가 배후에서 검색 API를 호출하는 것과 같은 본체. 이것은 일반적인 함수입니다.

설명 (LLM 대상): 그 도구가 "어떤 이름으로・무엇을 하며・어떤 인수를 취하는가"를 LLM에게 가르쳐주는 정보. LLM은 이것을 읽고 나서야 비로소 도구의 존재와 사용법을 알게 됩니다.

최소 구현의 run_tool은 (1)만을 가지고 있었습니다. SYSTEM_PROMPT에 "calculatorweb_search를 사용할 수 있습니다"라고 일본어로 써두면 일단 동작은 하겠지만, 그것은 (2)를 수기로 직접 작성했을 뿐입니다. 도구가 늘어날 때마다 프롬프트를 손으로 다시 쓰는 것은 불가능합니다.

따라서 실행형 에이전트는 (2)를 기계적으로 구성하는 메커니즘을 가집니다. 이 (2) ―― 「LLM에 대한 제시」를 어떤 형식으로・어떻게 자동 생성할 것인가야말로 본 장에서 두 프레임워크가 갈라지는 급소입니다. agent-zero는 "설명을 .md 파일에 작성하여 모두 모은다", Hermes는 "설명을 JSON 스키마로 작성하여 API에 전달한다". 순서대로 살펴보겠습니다.

실물로 확인하기 ① ― agent-zero: 구현(.py)과 설명서(.md)를 분리하여 {{tools}}에 흘려보내기

agent-zero (agent0ai/agent-zero @ f9d8167)

는, 제2장에서 보았듯이 거동을 prompts/*.md로 외재화하는 사상이었습니다. 도구도 예외는 아닙니다. 하나의 도구가

두 개의 파일로 나뉘어 있습니다.

tools/<name>.py

―― 도구의 구현 (Implementation) (Python 코드)

prompts/agent.system.tool.<name>.md

―― 해당 도구의 설명서 (Manual) (LLM에게 보여줄 자연어)

이 분리가 agent-zero 도구 시스템의 핵심입니다. 구현은 코드, LLM에 대한 설명은 텍스트. 양자는 이름(<name>)으로 대응됩니다.

LLM에게 보여주는 「설명서」 ―― .md 파일

먼저 설명서 측을 보겠습니다. 제1장·제2장에서 몇 번인가 등장했던 response 도구(최종 답변을 반환하는 도구)의 설명서는 다음과 같습니다.

### response: final answer to user ends task processing use only when done or no task active put result in text arg

――
prompts/agent.system.tool.response.md @f9d8167, L1-4

### response:도구 이름 (Tool name), 이어지는 행이 무엇을 하는 도구인지에 대한 설명, put result in text arg인자(Argument) 지정(text 인자에 결과를 넣어라)입니다. LLM은 이 텍스트를 읽고 「response라는 도구가 있으며, text 인자에 최종 답변을 넣으면 사용할 수 있다」라고 이해합니다.

인자의 구조가 조금 더 명확한 예로, 웹 검색(Web search) 도구의 설명서를 보겠습니다.

### search_engine find live news, prices, and other real-time web data arg: query (keyword-based text search query) returns urls, titles, and descriptions

――
prompts/agent.system.tool.search_engine.md @f9d8167, L1-4

### search_engine (이름) / find live news... (용도) / arg: query`` (인자 이름과 설명) / returns urls, titles... (반환값 설명). 도구 정의에 필요한 4요소 ―― 이름·용도·인자·반환값 ―― 이 자연어 몇 줄로 작성되어 있음을 알 수 있습니다. 이것이 agent-zero에서의 「도구 스키마 (Tool schema)」입니다. 타입 시스템(Type system)이 아니라, LLM이 읽고 이해하는 산문입니다.

.md에는 내용이 이어지며, 실제 호출 예시(JSON)까지 실려 있습니다.

example: ~~~json { "thoughts": ["I need current information rather than relying on memory."], "headline": "Searching the web", "tool_name": "search_engine", "tool_args": { "query": "LiteLLM latest release notes changelog" } } ~~~

――
prompts/agent.system.tool.search_engine.md @f9d8167, L14-24

제1장에서 보았던 「thoughts + tool_name + tool_args의 JSON을 반환하라」는 응답 계약(communication.md)에, 이 도구 고유의 사용 예시가 **원샷 예시 (One-shot example)**로서 매립되어 있습니다. LLM은 「이 도구를 사용할 때는 tool_namesearch_engine, tool_args.query에 키워드를 넣으면 된다」라고 구체적인 예시를 통해 배울 수 있는 것입니다. 참고로 원문의 코드 펜스(Code fence)는 ~~~json (물결표)로 작성되어 있습니다.

구현 측 ―― .py 파일

설명서(.md)에 대응하는 구현(.py)은 별도의 파일입니다. search_engine의 실체는 다음과 같습니다.

class SearchEngine(Tool):
    async def execute(self, query="", **kwargs):
        searxng_result = await self.searxng_search(query)
        ...

―― tools/search_engine.py

@ f9d8167
, L12-22 (발췌)

Tool을 상속하여 execute(self, query="", ...)를 구현한다 ―― 이것이 전부입니다. .md 측에서 arg: query라고 선언한 인수가 execute의 키워드 인자 query로 전달됩니다. 반환값은 제1장에서 보았던 Response (break_loop=False이므로 루프는 지속)입니다. "무엇을 할 것인가"는 이 .py 파일에, "LLM에게 어떻게 보여줄 것인가"는 .md 파일에라고 관심사가 깔끔하게 분리되어 있습니다.

response 도구의 구현 역시, 제1장에서 인용했듯이 불과 몇 줄뿐이었습니다.

class ResponseTool(Tool):
    async def execute(self, **kwargs):
        return Response(message=self.args["text"] if "text" in self.args else self.args["message"], break_loop=True)

―― tools/response.py

@ f9d8167
, L4-7

.md (put result in text arg)와 .py (self.args["text"])가 text라는 인자 이름으로 서로 맞물려 있는 것을 볼 수 있습니다.

LLM에 대한 제시 ―― {{tools}} 플레이스홀더에 모든 설명서를 주입하기

그렇다면, 이렇게 흩어져 있는 .md 설명서들은 어떻게 하나의 시스템 프롬프트 (System Prompt)로 합쳐지는 것일까요? 핵심은 **{{tools}}라는 플레이스홀더 (Placeholder)**입니다. 도구 목록의 틀을 정의하고 있는 것이 다음 파일입니다.

## available tools use ONLY the tools listed below. match names exactly. do NOT invent tool names. Action names are not tool names. There is no top-level multior batch tool; call one listed tool at a time. If a tool has an action namedmulti, keep that action inside tool_args.action for that specific tool. {{tools}}

―― prompts/agent.system.tools.md

@ f9d8167
, L1-4

도입부 3줄이 "사용 가능한 도구만 사용하라, 이름을 정확히 맞춰라, 도구 이름을 지어내지 마라"라는 목록 전체에 대한 주의 사항입니다. 그리고 마지막의 {{tools}} (L4)가 여기에 모든 도구의 설명서가 삽입되는 구멍입니다. 제2장에서 보았던 {{ include }}와 동일한 agent-zero의 템플릿 메커니즘입니다.

이 구멍을 채우는 것이 전용 확장 (Extension) 스크립트입니다.

@extensible
async def build_prompt(agent: Agent) -> str:
    # collect tool files from all prompt directories
    ...

―― extensions/python/system_prompt/_11_tools_prompt.py

@ f9d8167
, L27-58 (발췌. 중앙 부분을 ...으로 생략)

로직을 따라가 보면, 도구 제시의 전체상이 여기에 응축되어 있습니다.

  • prompts 디렉토리로부터 (L31-33). agent.system.tool.*.md 패턴과 일치하는 모든 파일을 수집하는 get_unique_filenames_in_dirs가 파일명으로 유니크화하면서 glob으로 가져옵니다. - 수집된 각 .mdread_prompt

로 읽어 들여 리스트에 쌓습니다 (L39-44). - 그것들을 "\n\n"로 연결하여 하나의 문자열 tools_str로 만듭니다 (L48). - agent.system.tools.md를 읽으면서, tools=tools_str을 전달하여 {{tools}}를 해당 문자열로 치환합니다 (L49).

즉, agent-zero의 도구 제시란, **"agent.system.tool.*.md 라는 파일을 두기만 하면, 그것이 자동으로 수집되어 {{tools}} 위치에 나열된다"**는 의미입니다. 목록에 등록할 필요도, 시스템 프롬프트를 다시 쓸 필요도 없습니다. 파일의 존재 자체가 등록이 됩니다.

이것이 메커니즘입니다. 새로운 도구를 추가하고 싶다면, tools/foo.py (구현)와 prompts/agent.system.tool.foo.md (설명서)라는 2개의 파일을 두기만 하면 됩니다.

이름에서 구현으로 ―― 호출의 연결

LLM이 설명서를 읽고 "tool_name": "search_engine"이라는 JSON을 반환했을 때, 그 이름이 어떻게 구현(.py)으로 연결되는가? 제1장에서 보았던 process_tools가 이 역할을 담당합니다.

async def process_tools(self, msg: str):
# agent 메시지에서 도구 사용 요청을 검색
tool_request = extract_tools.json_parse_dirty(msg)
...

―― agent.py @ f9d8167, L867-880 (발췌)

LLM의 응답 JSON을 파싱하여 tool_name을 추출하고 (L869, L878), 그 이름으로 구현 파일을 찾아 로드합니다.

def get_tool(
self,
name: str,
...

―― agent.py @ f9d8167, L1006-1031 (발췌)

get_tool은 LLM이 반환한 name으로부터 tools/<name>.py를 찾아 (L1021), 그 안의 Tool 서브클래스를 로드하고 (L1025, L1030), 인스턴스화합니다. 찾을 수 없다면 Unknown 도구(= "그런 도구는 없다"라고 LLM에게 반환)로 폴백(fallback)합니다. 설명서(.md)와 구현(.py)을 잇는 것은 "파일명 = 도구 이름"이라는 규약뿐입니다. LLM은 .md로 이름을 알고, 런타임은 동일한 이름으로 .py를 가져옵니다. 중앙 레지스트리(등록부)는 존재하지 않으며, 파일 시스템이 등록부 역할을 수행합니다. 여기까지가 agent-zero의 방식입니다. 도구의 스키마(Schema) = 자연어 .md 설명서, 제시 = 모든 .md를 모아 {{tools}}에 주입, 구현과의 결합 = 파일명 규약. 타입 검증도 JSON 스키마도 없이, 모든 것이 텍스트와 파일 배치만으로 성립됩니다.

실물로 확인하기 ② ― Hermes: JSON 함수 스키마로 정의하고, tools= 로 전달하기

Hermes (NousResearch/hermes-agent @ 6928692)는 대조적입니다. 제1장에서 보았듯이 Hermes는 **LLM 네이티브 Function Calling (함수 호출)**을 사용합니다. 도구 정의 또한 그 방식에 맞춰, OpenAI function-calling 형식의 JSON 스키마로 작성합니다. 자연어 설명서가 아닌, 기계 판독 가능한 타입 정의입니다.

도구의 스키마 ―― dict 리터럴의 function 스키마

Hermes의 도구 스키마는 Python의 딕셔너리(dict) 리터럴로 작성됩니다. 웹 검색 도구의 정의는 다음과 같습니다.

WEB_SEARCH_SCHEMA = {
"name": "web_search",
"description": "정보를 위해 웹을 검색합니다. 기본적으로 제목, URL, 설명을 포함하여 최대 5개의 결과를 반환합니다. 쿼리는 설정된 백엔드로 전달되므로, 백엔드가 지원하는 경우 site:domain, filetype:pdf, intitle:word, -term, "exact phrase"와 같은 연산자가 작동할 수 있습니다.",
...

―― tools/web_tools.py

@ 6928692

, L1286-1306

agent-zero의 search_engine.md

(몇 줄의 산문)과, 동일한 웹 검색 도구의 정의를 비교해 보세요. 정보는 대응합니다 ―― name (이름) = 도구 이름, description (설명) = 용도, parameters (매개변수) = 인자. 하지만 Hermes 측은 JSON Schema라는 구조화된 형식입니다. query"type": "string"이며 필수(required) 항목이고, limit"type": "integer"이며 범위(minimum/maximum)와 기본값(default)까지 타입으로 선언되어 있습니다.

이것이 agent-zero와의 가장 눈에 띄는 차이점입니다. **agent-zero는 "LLM에게 읽히는 설명문", Hermes는 "API에 전달하는 타입 정의"**입니다. 후자는 OpenAI Chat Completions API의 tools 파라미터가 그대로 받아들일 수 있는 형태({"type": "function", "function": {...}}function 부분)로 되어 있습니다.

등록 ―― register()로 중앙 레지스트리에 모으기

agent-zero에서는 "파일을 두는 것만으로" 등록되었지만, Hermes는 중앙 레지스트리(등록부)에 명시적으로 등록합니다. 스키마를 정의한 바로 아래에서 register()를 호출합니다.

registry.register(
name="web_search",
toolset="web",
...

―― tools/web_tools.py

@ 6928692

, L1325-1334

단 한 번의 register() 호출에 도구의 모든 정보가 집약되어 있습니다.

  • name ―― 도구 이름 (web_search)
  • schema ―― 앞서 언급한 JSON 스키마 (LLM에게 보여줄 정의)
  • handler ―― 구현(implementation)으로의 연결. LLM이 반환한 args를 실제 함수인 web_search_tool로 전달하는 람다(lambda). agent-zero가 "파일명으로 .py를 가져오는" 방식이라면, Hermes는 "handler에 실제 함수를 직접 연결하는" 방식입니다.
  • check_fn ―― 이 도구를 지금 사용할 수 있는지 판별하는 함수 (후술). check_web_api_key는 검색 API 키가 설정되었는지 여부를 반환합니다.
  • toolset ―― 논리적 그룹 이름 (후술). web 그룹에 속함.

이 스키마(LLM용 정의)와 handler(구현)가 한 곳에서 결합되는 구조가 Hermes 도구 정의의 핵심입니다. 레지스트리(tools/registry.py)의 ToolEntry가 이것들을 1개 도구당 1개의 엔트리로 보유합니다.

check_fn ―― "사용 가능한 도구만" LLM에게 보여주기

이 부분이 Hermes의 도구 제시(tool presentation)에서 중요한 장치입니다. check_fnFalse를 반환하는 도구는 애초에 LLM에게 제시되지 않습니다. 레지스트리가 LLM용 정의 목록을 구성하는 get_definitions

를 확인합니다.

def get_definitions(self, tool_names: Set[str], quiet: bool = False) -> List[dict]:
...
for name in sorted(tool_names):
...

―― tools/registry.py

@ 6928692
, L337-384 (발췌)

요점은 두 가지입니다.

  • check_fn()이 False를 반환하는 도구는 애초에 LLM에게 제시되지 않습니다 (L358-364). 예를 들어 검색 API 키가 설정되지 않았다면, check_web_api_key가 False를 반환하고, web_search는 LLM에게 보여줄 목록에서 제외됩니다. "환경적으로 사용할 수 없는 도구는 LLM에게 존재를 알리지 않는다" ―― 사용할 수 없는 도구를 호출하게 하여 실패하게 만드는 낭비를 제시 단계에서 방지하고 있습니다.
  • 살아남은 도구를 {"type": "function", "function": <schema>} 형태로 정형화하여 반환합니다 (L383). 이것이 OpenAI function-calling의 도구 정의 그 자체입니다.

이 정형화된 리스트가 최종적으로 LLM API 호출의 tools= 인자로 전달됩니다. Hermes의 경우, transport별 build_kwargs (Anthropic / Bedrock / OpenAI 계열)가 준비되어 있으며, 모두 도구 정의를 tools=tools_for_api 형태로 API에 전달합니다. Anthropic 루트에서의 전형적인 모습은 다음과 같습니다.

return _transport.build_kwargs(
model=agent.model,
messages=anthropic_messages,
...

―― agent/chat_completion_helpers.py

@ 6928692
, L539-542 (발췌)

tools_for_apiagent.tools, 즉 get_definitions (경유하여 get_tool_definitions, model_tools.py @ 6928692, L264)로부터 얻은 JSON 스키마 목록입니다 (Bedrock 루트는 동일 파일 L562, OpenAI 계열의 chat_completions 루트는 L619 / L719 / L751에서도 동일한 tools=tools_for_api 형태로 전달됩니다). agent-zero가 "설명서를 시스템 프롬프트(System Prompt) 텍스트에 매립"하는 방식이라면, Hermes는 "스키마를 API의 전용 인자로 전달"합니다. 제시 경로 자체가 다릅니다. 전자는 프롬프트 본문의 일부이고, 후자는 메시지와는 별개의 채널인 구조화 데이터(Structured Data)입니다.

toolset ―― 도구의 논리적 그룹화

도구가 늘어나면 "어떤 상황에서 어떤 도구군을 내보낼 것인가"에 대한 관리가 필요합니다. Hermes는 이를 **toolset (툴셋)**이라는 논리적 그룹으로 다룹니다. register()toolset="web"이 바로 이것이었습니다. toolset의 정의는 TOOLSETS라는 딕셔너리에 집약되어 있습니다.

TOOLSETS = {
...
"web": {
...

―― toolsets.py

@ 6928692
, L88-94 (발췌)

web이라는 이름의 그룹에 web_searchweb_extract가 속하며, includes를 통해 다른 toolset을 포함하여 합성할 수도 있습니다 (예를 들어 debugging 그룹은 ["web", "file"]을 include 하여 해당 도구들의 모든 도구를 포함합니다). 상황(CLI / 각 메시징 플랫폼 / webhook 등)에 따라 "어떤 toolset을 활성화할 것인가"를 전환하면 LLM에게 제시되는 도구 집합이 바뀝니다 ―― 도구의 선별적 제시 (出し分け) 단위가 바로 toolset입니다.

정리하자면 Hermes의 유형은 다음과 같습니다. 도구의 스키마 = JSON의 function 정의, 제시 = check_fn으로 필터링하여 tools=에 전달, 구현과의 결합 = handler의 직접 결합, 선별적 제시 = toolset 그룹. 모든 것이 구조화 데이터와 레지스트리(Registry)로 관리되고 있습니다.

2자의 대조 ―― 같은 "도구를 부여하기", 제시 형식이 근본부터 다르다

agent-zero와 Hermes는 "LLM에게 도구를 정의하고 호출하게 한다"는 목적을 공유하면서도, 그 구현 방식이 4가지 점에서 갈립니다.

관점최소 구현agent-zero (f9d8167)Hermes (6928692)
스키마 형식SYSTEM_PROMPT에 수기 작성자연어 .md 설명서 (이름·용도·인자·예시를 산문으로)JSON Schema dict (name / description / parameters, 타입·필수·범위 포함)
LLM 제시 경로시스템 프롬프트 본문모든 .md를 모아 {{tools}} 플레이스홀더(Placeholder)에 연결 (프롬프트 본문의 일부)get_definitions로 정형화하여 API의 tools= 인자로 전달 (본문과는 별도의 채널)
구현과의 결합run_tool 내의 분기파일명 규칙 (tools/<name>.py를 이름으로 동적 로드)register(handler=...)로 실제 함수를 직접 결합
등록 방법코드에 직접 작성.md + .py를 두기만 하면 됨 (파일의 존재 = 등록)중앙 레지스트리(Registry)에 register()로 명시적 등록
구분 제시 / 가용성없음배치된 .md는 항상 전부 제시check_fn으로 환경적으로 사용 가능한 도구만 제시 + toolset으로 그룹 단위로 구분 제시

설계 차이의 핵심은 "도구 정의를 LLM이 읽는 산문(Prose)으로 작성할 것인가 / API에 전달할 타입(Type)으로 작성할 것인가"입니다.

agent-zero는 제2장에서부터 일관된 "동작은 코드가 아니라 프롬프트로 결정된다"는 사상을 도구에도 관철합니다. 도구의 설명은 LLM이 읽는 텍스트이며, .md 파일을 하나 추가하는 것만으로 도구가 늘어납니다. 모델의 function-calling 대응 여부에 의존하지 않고, .md의 문구 하나만으로 제시를 완전히 제어할 수 있는 대신, 인자의 타입 체크는 LLM의 해석에 맡깁니다 (응답 계약의 JSON을 json_parse_dirty로 느슨하게 파싱하는 세계입니다).

Hermes는 API 네이티브의 function calling을 활용하는 방식입니다. 스키마가 JSON이므로 타입·필수·범위를 기계적으로 선언할 수 있으며, check_fn

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0