1. 에이전트의 도구: 실제로 무엇인가
앞선 모듈에서 여러분은 이미 Apps SDK 관점에서 도구를 봤습니다 — ChatGPT가 여러분의 App을 통해 호출하는 ‘백엔드 함수’처럼요. 이제 시야를 바꿔서, Agents SDK의 에이전트 관점에서 도구를 보고, 에이전트가 무엇을 호출할지 어떻게 고르는지와 오류를 어떻게 다루는지 알아봅니다.
일반적인 백엔드에서는 보통 ‘엔드포인트’, ‘컨트롤러 메서드’, ‘서비스 함수’ 같은 범주로 생각합니다. 에이전트 세계에서 행동의 기본 단위는 도구(tool)가 됩니다. 에이전트의 tools와 mcp-tools는 서로 다르지만 일부는 겹칩니다.
엄밀히 말해, ChatGPT Agents SDK 문맥에서 도구란 모델이 실행을 요청할 수 있는 함수의 설명입니다. 모델 자체가 코드를 실행하진 않습니다. 모델은 구조화된 요청(보통 JSON)을 생성하고, 런타임(여러분의 코드, MCP 서버 또는 Agents SDK)이 실제로 그 작업을 수행한 뒤 결과를 반환합니다.
ChatGPT Agents SDK 생태계에서 도구는 구성으로 설명됩니다. 도구에는 name, description, parameters(인자에 대한 JSON Schema)가 있습니다. 에이전트는 이 도구 목록을 자신의 컨텍스트에 보관하고, 추론(reasoning) 과정에서 어떤 도구를 어떤 인자로 호출할지 결정합니다.
에이전트(또는 호스트로서의 ChatGPT)는 이 리스트를 받아 자신의 컨텍스트에 ‘기억’해 두고, 추론 과정에서 사용자 요청에 대해 어떤 도구를 어떤 인자로 호출할지를 결정합니다. 그래서 스펙에서 “tools are a contract”라는 만트라가 반복됩니다 — 도구는 모델과 여러분의 코드 사이의 계약이지, 단순한 “Python/TS의 함수”가 아닙니다.
고전적인 API와 비교해 볼 수 있습니다. 라우트 /api/gifts/search는 순수한 문법입니다: URL, 메서드, 바디 포맷. 반면 tool search_gifts는 의미입니다: “수령인 프로필과 예산으로 선물을 검색한다.” 도구 설명은 사람을 위한 문장형 프롬프트와 유사하지만, LLM을 대상으로 구조화되어 있습니다.
2. 도구의 유형: LLM 에이전트가 구체적으로 무엇을 할 수 있는가
‘모든 걸 다 하는 함수’의 혼돈에 빠지지 않으려면, 도구를 몇 가지 전형적인 범주로 보는 것이 유용합니다. 이는 SDK의 형식적 타입 구분이 아니라, 여러분에게 큰 도움이 될 아키텍처적 사고방식입니다.
우리 백엔드에서 LLM 에이전트의 도구는 보통 세 가지 출처가 있습니다.
- 로컬 비즈니스 도구. 여러분의 백엔드에 사는 것들입니다: DB 작업, 도메인 로직(필터링, 추천, 스코어링). 예를 들어 GiftGenius에서는 자체 PostgreSQL 테이블에서 상품을 가져오거나 “이 선물이 이 사람에게 얼마나 잘 맞을지” 개인화 스코어링을 계산하는 도구가 있을 수 있습니다.
- MCP 도구. 여기서 MCP 서버는 도구(tools) 공급자 역할을 합니다. 함수, 리소스, 프롬프트를 등록하고 이를 클라이언트(ChatGPT, LLM 에이전트)에게 제공합니다. MCP를 통해 노출된 도구는 외부 API 호출, 파일 작업, 프롬프트 템플릿 제공 등을 수행할 수 있습니다.
- 통합(Integration) 도구. 외부 세계와 여러분을 연결하는 모든 것들입니다: ACP/커머스(주문 생성 및 체크아웃), 이메일 발송, 웹훅, CRM 기록. 이런 도구들은 외부 시스템의 상태를 변경하기 때문에 더 위험하며, 보안과 멱등성 관점에서 특히 엄격하게 다뤄야 합니다.
행위 특성에 따른 또 다른 유용한 분류도 있습니다. LLM 도구 연구에서는 보통 데이터 조회 도구(검색, RAG, get_*), 부작용이 있는 동작 도구(create_order, send_email), 순수 계산 도구(calculate_loan), 그리고 시스템/제어 도구(handoff_to_human, finish_task)를 구분합니다.
이를 명확히 하기 위해 작은 표를 보겠습니다.
| 범주 | GiftGenius 예시 | 부작용 | 위험도 |
|---|---|---|---|
| Data Retrieval | |
아니오 | 낮음 |
| Action / Mutating | |
예 | 높음 |
| Computation | |
아니오 | 중간 |
| System / Control | |
아니오 | 논리적 |
아키텍처 관점에서 가장 중요한 점: 읽기 전용 도구는 다수이면서 저렴해야 하고, 상태를 변경하는 도구는 드물고 극도로 신중해야 하며, 로깅과 멱등성을 갖추고 자주 사용자 확인을 받아야 합니다.
이후에는 주로 데이터 조회 도구와 Action 도구에 대해 이야기하겠습니다. GiftGenius의 로직이 바로 여기에 기반하기 때문입니다.
3. JSON Schema: 모델과 여러분의 코드 사이의 계약
이제 도구가 어떻게 기술되는지 더 깊이 들어가 보겠습니다. ChatGPT Agents SDK(및 Apps SDK)에서 도구 파라미터를 설명하는 표준 포맷은 JSON Schema입니다. object 타입, 그 properties, 필드 타입, 필수 필드, 제약 등을 기술합니다.
중요한 점은: 여기서 JSON Schema는 단지 검증을 위한 것이 아닙니다. 이것은 모델을 위한 프롬프트의 일부입니다. OpenAI의 공식 도구 설계 가이드에서는 도구의 동작 품질이 필드, 필드명, 주석(설명)이 얼마나 자세하고 모호하지 않은지에 크게 좌우된다고 명시합니다.
과정 계획에서 이미 본 GiftGenius 예시를 살펴봅시다.
{
"name": "search_gifts",
"description": "수령인 유형, 관심사 및 예산에 따라 선물을 찾습니다.",
"parameters": {
"type": "object",
"properties": {
"recipient_type": {
"type": "string",
"description": "선물의 수령인이 누구인지(예: '남성', '여성', '어린이')."
},
"interests": {
"type": "array",
"items": { "type": "string" },
"description": "주요 관심사(스포츠, 도서, 기술 등)."
},
"budget": {
"type": "number",
"description": "사용자 통화 기준 최대 예산."
}
},
"required": ["recipient_type", "budget"]
}
}
여기에는 몇 가지 중요한 포인트가 있습니다.
- 첫째, name과 description입니다. 모델에게는 이게 “언제 이 도구를 써야 하는지”에 대한 주된 신호입니다. 의미적 라우팅 문서에서는 도구 설명이 사실상 모델을 위한 API라고 강조합니다. 도구 이름을 func1로 하고 “뭔가 유용한 일을 함”이라고 쓰면, 모델은 언제 호출해야 할지 이해하지 못합니다. 반대로 search_gifts라는 이름에 명확한 설명을 붙이면 선택이 훨씬 쉬워집니다.
- 둘째, parameters입니다. 필드 이름과 설명은 매우 중요합니다. LLM에게 recipient_type은 type보다 훨씬 명확합니다. “선물의 수령인이 누구인지…” 같은 좋은 설명은 여기에 포장 형식 같은 게 아니라 ‘수령인의 유형’을 넣어야 한다는 점을 모델에게 알려 줍니다.
- 셋째, required입니다. 이것은 여러분 쪽의 유효성 검사뿐 아니라 모델을 위한 힌트이기도 합니다. 모델은 필수 필드를 채우려고 하며, 컨텍스트에서 명확하지 않은 경우 선택 필드는 생략합니다. 그 결과 ‘빈’ 또는 잘못된 도구 호출이 줄어듭니다.
Apps SDK의 공식 가이드는 도구를 좁고 단일 책임으로 만들고, 명확한 이름과 설명을 제공하며, 서로 다른 작업을 한데 묶으려는 “선물 관련 모든 것을 해줘” 같은 도구는 피하라고 권장합니다.
4. GiftGenius 도구 설계: 스키마에서 코드까지
GiftGenius에 거의 모든 시나리오에서 필요한 에이전트의 핵심 도구 두 개를 추가해 봅시다.
- suggest_gifts(profile, budget) — 후보 목록을 제공합니다.
- get_gift_details(gift_id) — 특정 선물의 상세 정보를 보여 줍니다.
suggest_gifts와 get_gift_details는 앞선 분류에서 본 전형적인 로컬 비즈니스 도구이며, 주로 Data Retrieval 범주에 속합니다.
suggest_gifts의 스키마
먼저 순수 JSON Schema를 작성하고, 이후 백엔드/에이전트 런타임의 TypeScript 코드에서 어떻게 보일 수 있는지 살펴보겠습니다.
{
"name": "suggest_gifts",
"description": "수령인 프로필과 예산을 바탕으로 선물 목록을 추천합니다.",
"parameters": {
"type": "object",
"properties": {
"age": {
"type": "integer",
"minimum": 0,
"maximum": 120,
"description": "수령인의 나이(세)."
},
"relationship": {
"type": "string",
"enum": ["friend", "coworker", "partner", "family"],
"description": "수령인과의 관계: 친구, 동료, 파트너, 가족."
},
"interests": {
"type": "array",
"items": { "type": "string" },
"description": "수령인의 관심사(스포츠, 도서, 기술 등)."
},
"budget": {
"type": "number",
"minimum": 1,
"description": "사용자 통화 기준 최대 예산."
}
},
"required": ["budget"]
}
}
여기서 relationship에 enum을 사용해 모델이 임의의 문자열, 예컨대 "나쁜 동료" 같은 값을 만들어 코드로 넘기지 못하게 합니다. 스키마를 이렇게 꼼꼼히 설계하면 모델(허용 가능한 값을 볼 수 있음)과 개발자(런타임의 놀람이 줄어듦) 모두에게 도움이 됩니다.
이제 Node.js의 MCP 서버가 있고, 가상의 McpServer를 사용한다고 가정해 봅시다. 도구 등록은 다음과 같을 수 있습니다.
// MCP 서버에서 도구를 등록하는 단순 예시
server.registerTool(
{
name: "suggest_gifts",
description: "프로필과 예산을 바탕으로 선물을 추천합니다.",
inputSchema: suggestGiftsSchema
},
async (input, ctx) => {
const gifts = await findGiftsInDb(input, ctx.userLocale);
return { items: gifts }; // 이후 에이전트가 보게 될 JSON
}
);
코드는 많이 단순화했지만, 핵심은 명확합니다. 한쪽에는 계약 설명(이름, 설명, 스키마), 다른 한쪽에는 구현이 있습니다.
get_gift_details의 스키마
두 번째 도구는 거의 모든 상품 목록 화면에서 필요합니다.
{
"name": "get_gift_details",
"description": "선물 ID로 선물의 전체 정보를 가져옵니다.",
"parameters": {
"type": "object",
"properties": {
"gift_id": {
"type": "string",
"description": "GiftGenius 데이터베이스의 선물 UUID."
}
},
"required": ["gift_id"]
}
}
등록은 비슷합니다.
server.registerTool(
{
name: "get_gift_details",
description: "선물에 대한 상세 정보를 반환합니다.",
inputSchema: getGiftDetailsSchema
},
async ({ gift_id }) => {
const gift = await db.gifts.findById(gift_id);
if (!gift) return { notFound: true };
return { gift };
}
);
유의하세요. 여기서 도구가 notFound: true를 반환할 수 있음을 바로 보여 줍니다. 이는 의미적 오류(비즈니스 오류)의 시작입니다. 아래에서 더 이야기합니다. 에이전트는 “선물을 찾지 못함”을 보고 판단할 수 있습니다. 예를 들어 다른 id를 시도하거나 사용자에게 다른 상품을 선택하도록 제안할 수 있습니다.
5. 에이전트는 어떤 도구를 호출할지 어떻게 고르는가
이제 가장 흥미로운 주제, 라우팅입니다. 전통적인 웹 애플리케이션에서는 라우팅이 고정적입니다: URL → 특정 컨트롤러. ChatGPT Apps와 에이전트의 세계에서는 도구 선택이 의미 기반이면서 확률적으로 이뤄집니다.
상위 수준의 사이클은 다음과 같이 나타낼 수 있습니다.
flowchart TD
U[User message] --> M["모델(에이전트)"]
M -->|요청 분석| C{툴이 필요한가?}
C -->|아니오| T[텍스트 응답]
C -->|예| S[도구 선택]
S --> K[JSON 인자 구성]
K --> R[도구 실행]
R --> M2[모델이 결과를 확인]
M2 --> T2[최종 응답 또는 다음 단계]
각 단계에서 에이전트는 다음을 봅니다.
- 첫째, system 지시(에이전트의 역할, 제약);
- 둘째, 대화 기록;
- 마지막으로, 도구 목록(tools)과 각 도구의 name, description, inputSchema.
새 사용자 메시지가 오면, 모델은 요청의 의미를 도구 설명과 비교합니다(의미적 매칭). 요청이 “동료에게 줄 50달러 이하의 선물 추천”이라면, suggest_gifts의 설명이 get_gift_details보다 훨씬 관련성이 높으므로, 에이전트는 높은 확률로 이를 선택합니다.
공식 가이드는 라우팅 품질에 크게 영향을 주는 두 가지를 강조합니다.
- 첫째, 의미가 겹치는 도구를 피해야 합니다. search_gifts와 find_gifts가 비슷하게 설명되어 있으면 모델은 혼란스러워합니다.
- 둘째, 도구의 단일 책임 원칙을 지키세요. 한 도구에는 한 가지 분명한 작업만 — “선물 추천 + 주문 생성 + 메일 발송” 같은 조합형 도구는 피하세요.
여러 LLM 에이전트에는 도구 선택 모드를 제어하는 메커니즘이 있습니다. 예를 들어 “auto”(모델이 도구 필요 여부를 스스로 결정), “required”(반드시 도구 호출), “none”(도구 비활성) 등이 있습니다. 이는 복잡한 워크플로(다단계 시나리오)에서 유용합니다. 예컨대 특정 단계에서는 suggest_gifts를 강제로 호출하게 하고, 모델이 수다만 떨지 못하게 할 수 있습니다.
GiftGenius에서의 의미 기반 라우팅 예시
에이전트에 최소 두 개의 도구 suggest_gifts, get_gift_details가 있다고 가정합시다.
- 사용자: “동료에게 줄 30달러 이하의 선물을 추천해 줘. 그 친구는 보드게임을 좋아해.”
- 에이전트는 요청에 “선물 추천”이라는 목적과 예산/관심사 정보가 있음을 봅니다. suggest_gifts 설명이 완벽히 맞습니다 — 이 도구를 호출합니다.
- 도구는 id, 이름, 간단한 설명과 함께 다섯 개 선물 목록을 반환합니다.
- 사용자가 “세 번째 옵션 자세히 알려줘.”라고 하면, 에이전트는 “세 번째 옵션”을 이전 결과의 id에 매칭하고, 이번에는 get_gift_details가 의미상 적합하므로 이를 호출합니다.
중요한 점: 코드 어디에도 “요청에 ‘추천’이라는 단어가 있으면 suggest_gifts를 호출하라”고 명시하지 않았습니다. 이 선택은 설명과 대화 기록을 바탕으로 모델이 스스로 수행합니다. 개발자인 여러분의 책임은 모델과 사람 모두에게 선택이 명확해지도록 만드는 것입니다.
6. 도구의 오류: 500이 아니라 모델을 위한 신호
기억하세요. get_gift_details에서 이미 notFound: true를 보여 주었습니다. 이는 에이전트가 단순 500을 받는 대신, 비즈니스 오류를 보고 의미 있게 처리해야 한다는 예시입니다.
이제 가장 아픈 부분으로 가봅시다. 일반적인 REST API에서는 백엔드 내부 어딘가에서 무언가가 실패하면 500 Internal Server Error를 반환하고, 스택 트레이스를 로그에 남기고, 그다음은 사용자가 somehow 처리합니다. 에이전트의 경우 이런 접근은 잘 작동하지 않습니다.
Agents SDK의 실용 가이드와 자료는 도구의 오류를 관찰 가능한 이벤트로 다루라고 권장합니다. 이를 흔히 “Error as Observation” 패턴이라고 부릅니다.
거칠게 말하면, 설명 없이 “크래시”해서는 안 되며, 모델이 행동을 조정할 수 있도록(요청 재구성, 사용자에게 질문, 다른 도구 시도 등) 무엇이 잘못되었는지 설명하는 구조화된 응답을 반환해야 합니다.
오류 유형은 보통 세 가지로 나눕니다.
- 인자 유효성 오류. 모델이 잘못된 파라미터를 생성할 수 있습니다. 필수 필드를 빠뜨리거나, 숫자 대신 문자열을 넣거나, 허용 범위를 벗어나는 값 등을 줄 수 있습니다. 스키마와 유효성 검사를 단지 예외를 던지는 용도만이 아니라, 의미 있는 응답을 위해 사용하세요. 예를 들어 어떤 필드가 왜 잘못됐는지 반환합니다.
- 비즈니스 오류. “상품을 찾지 못함”, “지역 미지원”, “해당 유형에 예산이 너무 적음” 같은 완전히 예상 가능한 상황입니다. API 관점에서는 오류이지만, 크래시가 아니라 식별 가능한 코드와 메시지를 가진 정상 응답의 일부로 반환해야 합니다.
- 시스템 오류. 외부 서비스 타임아웃, 네트워크 문제, DB 장애 등입니다. 이 경우 에이전트에게는 “서비스가 일시적으로 불가, 나중에 다시 시도”처럼 조심스럽고 일반화된 메시지면 충분합니다. 스택 트레이스, 테이블 이름 등 모델에 불필요하고 보안상 위험할 수 있는 세부는 전달하지 마세요.
Agents SDK의 공식 자료에서는 예외를 위로 던지는 대신, 모델이 보게 될 오류 텍스트를 정갈하게 구성할 수 있도록 failure_error_function 같은 전용 메커니즘을 제안하기도 합니다.
‘친화적인’ 오류의 구조
에이전트의 도구(여러분의 백엔드)에서는 모든 오류를 다음과 같은 객체 형태로 반환하기로 합의할 수 있습니다.
type ToolError = {
code: string; // 'VALIDATION_ERROR', 'OUT_OF_STOCK', ...
message: string; // 모델용
retryable: boolean;
};
그리고 도구의 결과는 다음과 같은 합집합으로 정의할 수 있습니다.
type SuggestGiftsResult =
| {
ok: true;
items: GiftSummary[];
}
| {
ok: false;
error: ToolError;
};
모델(또는 에이전트 런타임)은 이런 JSON을 보고 판단할 수 있습니다. retryable: true라면 약간의 변경으로 재시도할 수 있고, 비즈니스 레벨의 재시도 불가 오류라면 사용자에게 무엇이 문제인지 설명하며 돌아가는 편이 낫습니다.
7. 예시: 유효성 검사, 비즈니스 오류, 시스템 오류
우리의 백엔드/에이전트 도구로 돌아와 같은 아이디어를 코드에서 어떻게 구현할 수 있는지 보겠습니다.
유효성 오류
suggest_gifts 호출이 들어왔는데, 모델이 실수로 음수 예산을 보냈다고 가정해 봅시다.
async function handleSuggestGifts(input: SuggestGiftsInput)
: Promise<SuggestGiftsResult> {
if (input.budget <= 0) {
return {
ok: false,
error: {
code: "VALIDATION_ERROR",
message: "budget는 양의 숫자여야 합니다.",
retryable: false
}
};
}
const items = await findGiftsInDb(input);
return { ok: true, items };
}
여기서는 의도적으로 예외를 던지지 않고 구조화된 오류를 반환합니다. 에이전트는 요청을 재고할 수 있습니다. 예컨대 통화를 혼동했다고 판단해 사용자에게 물어보거나, 해당 예산으로는 추천할 수 없다고 인정할 수도 있습니다.
비즈니스 오류
이번에는 get_gift_details 예시입니다. 지정한 id의 선물이 없을 수 있습니다.
async function handleGetGiftDetails(input: { gift_id: string }) {
const gift = await db.gifts.findById(input.gift_id);
if (!gift) {
return {
ok: false,
error: {
code: "GIFT_NOT_FOUND",
message: "해당 식별자의 선물을 찾을 수 없습니다.",
retryable: false
}
};
}
return { ok: true, gift };
}
모델의 응답에서는 “선택한 선물이 더 이상 제공되지 않는 것 같습니다. 비슷한 카테고리의 대안을 몇 가지 제안해 드릴까요?”와 같은 문장을 기대할 수 있습니다. 이를 위해 에이전트는 SQL 오류나 스택 트레이스를 볼 필요가 없습니다 — 인지 가능한 code와 message면 충분합니다.
시스템 오류
마지막으로 시스템 오류 예시입니다. 여러분의 도구가 가끔 ‘다운’되는 외부 배송 API를 호출한다고 합시다.
async function handleEstimateDelivery(input: EstimateDeliveryInput) {
try {
const eta = await callDeliveryApi(input);
return { ok: true, eta_days: eta };
} catch (e) {
return {
ok: false,
error: {
code: "DELIVERY_SERVICE_UNAVAILABLE",
message: "배송 서비스가 일시적으로 사용 불가합니다.",
retryable: true
}
};
}
}
에이전트는 “지금은 배송 서비스가 불안정한 것 같아요. 그래도 선물은 보여 드리되, 정확한 배송 시간은 달라질 수 있습니다. 계속 진행할까요?”라고 판단할 수 있습니다.
8. 도구의 보안과 멱등성(도구 관점의 빠른 훑어보기)
보안과 권한에 대한 본격적인 이야기는 별도 주제로 다루겠지만, 에이전트의 도구는 이와 너무 밀접해 살짝 짚고 넘어가겠습니다.
첫째, 읽기 도구와 쓰기 도구를 분리해야 합니다. 설명, 스키마, 권한에서 어떤 도구가 데이터를 읽기만 하며 절대적으로 안전한지, 어떤 도구가 결제를 발생시키거나 주문을 변경하는지 등을 명시하세요. 문서와 포럼에서는 에이전트 시나리오에서 ReadOnly와 Mutating 도구의 분리를 분명히 권장합니다.
둘째, 상태 변경 도구에는 멱등성을 고려해야 합니다. 에이전트나 MCP 클라이언트가 호출을 재시도할 수 있고(예: 네트워크 오류 때문에), create_order가 한 번 대신 두 번 주문을 만들게 하고 싶지는 않을 겁니다. 전형적인 패턴은 다음과 같습니다.
- 도구 인자로 전달되는 idempotency-key;
- 실행 전 동일 작업이 존재하는지 확인;
- 단계를 “주문 초안 생성”과 “주문 확정”으로 분리.
이 모든 것은 여러분이 도구의 계약을 어떻게 설계하느냐와 밀접히 연결됩니다. JSON Schema에 idempotency-key 필드가 없다면, 나중에 멱등성을 추가하기 훨씬 고통스러울 것입니다.
9. Agents SDK 한눈 보기: 에이전트 런타임에서는 어떻게 보이는가
이 섹션은 TypeScript 지향 Agents SDK로 작업하려는 분들을 위한 간단한 개요입니다. 강의의 주력은 MCP지만, Agents SDK가 도구를 어떻게 인식하고 런타임에서 전형적인 도구가 어떻게 생겼는지 이해하는 것도 유용합니다.
공식 문서에서는 보통 ‘함수형 도구’ 같은 개념을 설명합니다. 구성 객체(또는 tool(...) 같은 helper)와 타입으로 기술된 임의의 함수가 자동으로 도구로 변환될 수 있고, SDK가 JSON Schema와 설명을 생성합니다.
개념적으로는 우리가 이미 논의한 것과 같습니다. 함수 이름, 파라미터, 주석/description이 도구의 이름, 스키마, 설명 역할을 합니다. 차이는 스키마를 위한 보일러플레이트를 SDK나 보조 라이브러리(Zod 또는 JSON Schema)가 대부분 처리해 준다는 점입니다.
가상의 예시(의사 TypeScript, 단순화):
type Gift = {
id: string;
title: string;
// ...
};
const suggestGifts = tool({
name: "suggest_gifts",
description: "수령인 유형과 예산을 기준으로 선물 목록을 추천합니다.",
parameters: {
type: "object",
properties: {
recipient_type: {
type: "string",
description: "선물의 수령인이 누구인지(예: '남성', '여성', '어린이')."
},
budget: {
type: "number",
description: "사용자 통화 기준 최대 예산."
}
},
required: ["recipient_type", "budget"]
}
}, async (args: { recipient_type: string; budget: number }): Promise<Gift[]> => {
// 내부 — 귀하의 도메인 로직
return findGifts(args.recipient_type, args.budget);
});
SDK(또는 여러분의 helper tool)는 parameters 객체를 기반으로 JSON Schema를 만들고 이를 에이전트에게 전달합니다. 런타임은 인자의 유효성 검사와 마샬링을 처리합니다. 개념적으로는 TypeScript 기반 MCP 서버에서 수동으로 하던 것을, 이제는 도구가 에이전트 런타임에 바로 ‘연결’되어 수행하는 셈입니다.
여기서 중요한 것은 helper tool의 구문을 외우는 게 아니라, 견고한 타입 + 명확한 description/주석 = 품질 좋은 도구라는 생각을 잡는 것입니다.
모든 것을 종합하면, 좋은 에이전트 도구란 잘 설계된 JSON Schema, 모델을 위한 명확한 설명, 깔끔한 오류 처리까지 갖춘 좁고 명확한 기능입니다. 의미 기반 라우팅은 도구 간 의미가 겹치지 않아야 제대로 작동합니다. 그리고 상태를 변경하는 작업은 반드시 안전하고 멱등적이어야 하며, 그렇지 않으면 프로덕션에서 에이전트는 금세 ‘놀라움’의 원천이 됩니다.
10. 에이전트 도구 설계 시 자주 하는 실수
실수 №1: 지나치게 넓은 “do_everything” 도구.
가끔 manage_gifts 하나에 “선물 검색, 상세 보기, 주문 생성, 메일 발송”을 모두 넣고 싶어집니다. 그러면 모델이 힘들어집니다. 설명은 모호해지고 의미 기반 라우팅은 퇴화하며, 단순 검색이면 되는 곳에서도 이 도구를 ‘일단’ 호출하기 시작합니다. 각 작업은 하나의 도구로 분리하고, 한 개의 잘 정의된 책임만 갖게 하세요.
실수 №2: 의미가 서로 겹치는 도구.
search_gifts와 find_gifts가 모두 “관심사로 선물 검색”을 한다면, 모델은 임의로 둘 중 하나를 고를 것입니다. 그 결과 동일한 요청이 어떤 때는 이 도구로, 어떤 때는 저 도구로 가는 불안정한 동작이 생깁니다. 각 이름과 설명이 의미 공간에서 고유한 ‘틈새’를 가지도록 하세요.
실수 №3: 부실하거나 누락된 설명과 스키마 필드.
이름이 func1, 설명은 “Does something”, 파라미터는 data: string — 에이전트를 바보로 만드는 전형적인 방법입니다. 모델은 독심술사가 아니며 여러분의 소스 코드를 읽을 수 없습니다. 모델은 스키마의 description, properties와 그 description에 의존합니다. recipient_type이 무엇인지 설명하지 않으면, 모델은 추측하다가 틀립니다.
실수 №4: happy-path만 상정하고 오류를 무시.
많은 도구 구현은 “항상 인자가 올바르고 서비스가 이용 가능할 것”이라고 가정합니다. 현실에서는 모델이 잘못된 파라미터를 쉽게 만들고, 외부 서비스는 다운되며, DB는 때로 “timeout”이라고 말합니다. 오류 포맷을 고민하지 않고 에이전트에게 의미 있는 메시지를 반환하지 않으면, 에이전트는 행동을 조정하지 못하고 조용히 실패하거나 헛소리를 할 것입니다.
실수 №5: 날것의 500과 스택 트레이스를 LLM에 던짐.
REST API에서는 디버깅을 위해 전체 스택 트레이스를 로깅하곤 합니다. 에이전트 문맥에서 모델에게 스택 트레이스를 주는 것은 쓸모없을 뿐(모델은 여러분의 특정 라이브러리의 SQLException이 뭔지 모릅니다)만 아니라 잠재적으로 위험합니다(구현 세부 및 민감 정보 노출). 예외를 잡아 로그에는 자세히 기록하되, 모델에게는 code와 message만 깔끔하게 전달하세요.
실수 №6: 상태 변경 도구에 멱등성 부재.
idempotency-key 없는 create_order는 네트워크 장애와 자동 재시도 환경에서 이중 주문으로 직결됩니다. 커머스 시나리오에서 동작하는 에이전트라면 돈이 걸린 도구는 재호출이 추가 청구나 중복을 만들지 않도록 설계되어야 합니다.
실수 №7: 스키마나 설명에 비밀/기술 세부를 담음.
개발자가 습관적으로 description에 “내부에서 https://internal-api.example.com 의 서비스 X를 호출함”이라고 쓰는 경우가 있습니다. 모델에게도 사용자에게도 이 정보는 필요 없습니다. 스키마와 설명은 프롬프트의 일부로 모델 컨텍스트에 남습니다. 내부 서비스 URL, 비공개 테이블 이름, 비밀은 절대 넣지 마세요.
실수 №8: 잘 고른 필드 대신, 무작정 모든 것을 도구에 전달.
“사용자 프롬프트 전체를 문자열로 넘기고 내부에서 알아서 처리하자”는 유혹에 빠지기 쉽습니다. 그러면 JSON Schema를 통한 구조화의 이점을 잃습니다. 모델은 로직에 중요한 요청의 어떤 부분을 알아야 하는지 이해하지 못하고, 유효성 검사와 예측 가능성도 사라집니다. 요청에서 명시적 필드(budget, interests, user_location)를 추출해 계약의 일부로 설명하는 편이 좋습니다.
GO TO FULL VERSION