AI 에이전트 개발 실전 #2 좋은 도구 설계하기
1편에서 루프를 단단하게 만들었습니다. 그런데 같은 루프를 쓰는 두 에이전트의 성능이 크게 갈린다면, 원인은 대부분 도구입니다. Claude가 보는 세상은 우리가 준 도구 목록이 전부라서, 도구가 모호하면 에이전트도 모호하게 움직입니다. 이번 글에서는 도구를 설계하는 원칙을 정리합니다.
description은 모델이 읽는 문서입니다 #
도구의 description은 주석이 아닙니다. Claude가 “이 도구를 지금 쓸지"를 판단하는 유일한 근거입니다. 그래서 도구가 무엇을 하는지만 적으면 부족하고, 언제 써야 하는지까지 적어야 합니다.
# 부족한 예 — 무엇을 하는지만 적었다
{
"name": "search_orders",
"description": "주문을 검색한다.",
...
}
# 좋은 예 — 언제 쓰는지, 무엇이 나오는지, 한계까지
{
"name": "search_orders",
"description": (
"주문 번호, 고객 이름, 날짜 범위로 주문을 검색한다. "
"사용자가 특정 주문의 상태나 내역을 물으면 먼저 이 도구로 주문을 찾는다. "
"최근 90일 이내의 주문만 검색되며, 최대 20건을 반환한다."
),
...
}특히 최신 모델은 도구를 신중하게 골라 쓰는 경향이 있어서, “사용자가 X를 물으면 이 도구를 먼저 부른다"처럼 트리거 조건을 명시하는 것이 도구 사용률을 눈에 띄게 올립니다.
스키마는 좁게, 설명은 파라미터마다 #
input_schema는 자유도를 주는 곳이 아니라 줄이는 곳입니다. 받을 수 있는 값이 정해져 있으면 enum으로 못 박고, 꼭 필요한 파라미터만 required에 넣고, 파라미터마다 description을 답니다.
{
"name": "update_order_status",
"description": "주문 상태를 변경한다. 상태 변경 요청을 받았을 때만 사용한다.",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "주문 번호. 예: ORD-2026-0001",
},
"status": {
"type": "string",
"enum": ["paid", "shipped", "delivered", "cancelled"],
"description": "변경할 상태. 이 네 가지 외의 값은 없다.",
},
},
"required": ["order_id", "status"],
},
}status를 자유 문자열로 두면 Claude는 “배송중”, “shipping”, “SHIPPED” 같은 변형을 만들어 냅니다. enum으로 막으면 이 문제가 통째로 사라집니다. 같은 이유로, 날짜는 “YYYY-MM-DD 형식"처럼 형식을 설명에 못 박는 것이 좋습니다.
에러 메시지도 설계 대상입니다 #
1편에서 도구 에러를 is_error 결과로 돌려주기로 했습니다. 그 메시지의 품질이 곧 에이전트의 복구 능력입니다. Claude는 에러 메시지를 읽고 다음 행동을 정하므로, 다음 행동을 정할 수 있는 정보를 담아야 합니다.
# 부족한 예 — 읽어도 다음 행동을 정할 수 없다
return "에러가 발생했습니다."
# 좋은 예 — 무엇이 잘못됐고 어떻게 고치는지
return (
"주문 번호 'ORD-9999'를 찾을 수 없습니다. "
"search_orders 도구로 고객 이름이나 날짜로 먼저 주문을 검색해 보세요."
)사람에게 보여 주는 에러 메시지를 만들 때와 기준이 같습니다. 차이라면 에이전트는 그 메시지를 정말로 읽고 그대로 행동한다는 점입니다. 고치는 방법을 적어 주면 높은 확률로 그 방법을 따릅니다.
도구의 위험도를 분류합니다 #
도구를 추가하기 전에 한 가지를 자문하는 것이 좋습니다. 이 도구가 잘못 호출되면 무슨 일이 생기는지입니다.
| 분류 | 예 | 다루는 방법 |
|---|---|---|
| 읽기 | 검색, 조회 | 자유롭게 호출하게 둔다 |
| 쓰기 (되돌릴 수 있음) | 상태 변경, 임시 저장 | 입력 검증 후 실행 |
| 쓰기 (되돌리기 어려움) | 결제, 삭제, 메일 발송 | 실행 전 사람의 승인을 거친다 |
되돌리기 어려운 도구는 호출 자체를 막는 것이 아니라, 실행 직전에 확인 단계를 끼워 넣습니다. 도구 실행 함수에서 분기하면 됩니다.
DANGEROUS_TOOLS = {"send_email", "delete_order", "refund_payment"}
def execute_tool(block) -> dict:
if block.name in DANGEROUS_TOOLS:
if not confirm_with_human(block.name, block.input):
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": "사용자가 이 작업을 승인하지 않았습니다. 다른 방법을 찾아보세요.",
"is_error": True,
}
...거부도 에러 결과로 돌려주는 점에 주목해 주세요. Claude는 “승인되지 않았다"는 사실을 알고 계획을 수정합니다. 7편의 실전 프로젝트에서 이 승인 흐름을 실제로 구현합니다.
도구 개수와 겹침을 관리합니다 #
도구가 많아질수록 Claude의 선택은 어려워집니다. 특히 설명이 겹치는 도구가 문제입니다. search_orders와 find_order가 같이 있으면 어느 쪽을 쓸지 매번 흔들립니다. 점검 기준은 두 가지입니다.
- 겹치는 도구는 합치거나 지웁니다. 비슷한 일을 하는 도구 두 개보다, 파라미터로 분기하는 도구 하나가 낫습니다.
- 안 쓰는 도구는 빼고 줍니다. 이번 작업과 무관한 도구가 목록에 있으면 그만큼 잘못 고를 여지가 생깁니다. 작업 종류별로 도구 목록을 다르게 구성하는 것도 방법입니다.
도구 하나하나가 좋아도 목록 전체가 혼란스러우면 에이전트는 헤맵니다. 도구 목록은 전체를 한눈에 보면서 다듬어야 합니다.
흔히 걸려 넘어지는 곳 #
- description을 개발자 기준으로 쓴다 — “내부 주문 API 래퍼” 같은 설명은 Claude에게 아무 정보가 없습니다. 이 도구로 무엇을 알 수 있고 언제 쓰는지를 사용자 관점에서 적습니다.
- 모든 파라미터를 required로 만든다 — 선택 파라미터까지 필수로 두면 Claude가 모르는 값을 지어내서 채웁니다. 정말 필요한 것만 필수로 둡니다.
- 에러를 숨긴다 — 도구가 실패했는데 빈 결과를 돌려주면, Claude는 “결과가 없다"고 잘못 학습한 채 진행합니다. 실패는 실패라고 알려야 복구가 시작됩니다.
마무리 #
이번 글에서는 에이전트의 성능을 좌우하는 도구 설계를 다뤘습니다.
description에는 무엇을 하는지에 더해 언제 쓰는지를 적습니다. 트리거 조건이 도구 사용률을 올립니다.- 스키마는
enum과required로 좁히고, 에러 메시지에는 다음 행동을 정할 정보를 담습니다. - 도구를 위험도로 분류하고, 되돌리기 어려운 도구에는 사람의 승인을 끼워 넣습니다.
다음 글인 “AI 에이전트 개발 실전 #3 계획 세우기와 자기 수정"에서는 도구를 잘 갖춘 에이전트가 여러 단계 작업을 어떻게 계획하고, 중간 실패를 어떻게 스스로 바로잡는지를 다룹니다.