🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Tool Design - 에이전트 최적화 툴 설계 - 슬라이드 1/8
A

AI Generated

2025. 12. 27. · 3 Views

Tool Design - 에이전트 최적화 툴 설계

AI 에이전트가 사용하는 도구(Tool)를 효과적으로 설계하는 방법을 알아봅니다. 결정론적 시스템과 비결정론적 에이전트 사이의 계약 개념부터 MCP 툴 모범 사례까지, 실무에서 바로 적용할 수 있는 툴 설계 원칙을 다룹니다.


목차

  1. Tools_as_Contracts
  2. Tool_Consolidation
  3. Clarity_over_Abstraction
  4. Token_Efficiency
  5. Recoverable_Failures
  6. MCP_Tool_Best_Practices
  7. Optimal_Tool_Set_Design

1. Tools as Contracts

어느 날 김개발 씨는 회사에서 새로운 AI 에이전트 프로젝트에 투입되었습니다. 선배 박시니어 씨가 던진 첫 질문은 예상 밖이었습니다.

"에이전트가 사용할 도구를 설계할 때 가장 중요한 게 뭔지 알아요?"

Tools as Contracts란 결정론적 시스템(일반 프로그램)과 비결정론적 에이전트(AI) 사이의 명확한 계약을 의미합니다. 마치 회사와 직원 사이의 업무 계약서처럼, 도구가 무엇을 하고 언제 사용하며 어떤 입출력을 가지는지 명확히 정의해야 합니다.

이 계약이 모호하면 에이전트는 혼란에 빠지고, 예측 불가능한 결과를 만들어냅니다.

다음 코드를 살펴봅시다.

// Tool을 계약(Contract)으로 정의하는 예시
interface ToolContract {
  name: string;           // 도구의 고유 이름
  description: string;    // 언제, 왜 사용하는지 명확히
  parameters: {
    type: "object";
    properties: Record<string, ParameterSchema>;
    required: string[];   // 필수 입력값 명시
  };
  returns: ReturnSchema;  // 출력 형식도 계약의 일부
}

// 좋은 예: 명확한 계약
const searchFileTool: ToolContract = {
  name: "search_file",
  description: "코드베이스에서 파일명 패턴으로 파일을 검색합니다. 파일 내용 검색이 아닌 파일명 검색에만 사용하세요.",
  parameters: {
    type: "object",
    properties: {
      pattern: { type: "string", description: "glob 패턴 (예: **/*.ts)" }
    },
    required: ["pattern"]
  },
  returns: { type: "array", items: { type: "string" } }
};

김개발 씨는 입사 후 첫 AI 에이전트 프로젝트에 배정받았습니다. LLM 기반 에이전트가 다양한 도구를 호출해서 업무를 수행하는 시스템이었습니다.

처음에는 간단해 보였지만, 도구를 설계하기 시작하면서 예상치 못한 문제들이 터져나왔습니다. 박시니어 씨가 다가와 말했습니다.

"에이전트는 사람과 달라요. 우리가 '대충 이런 느낌'으로 이해하는 것들을 에이전트는 전혀 이해하지 못합니다." 그렇다면 Tool을 Contract로 본다는 것은 정확히 무엇일까요?

쉽게 비유하자면, 이것은 마치 회사와 외주 업체 간의 계약서와 같습니다. 계약서에 "웹사이트 만들어주세요"라고만 적혀있다면 어떻게 될까요?

외주 업체는 자기 마음대로 해석해서 전혀 다른 결과물을 만들어올 수 있습니다. 반면 "반응형 웹사이트, 3페이지, React 사용, 2주 내 납품"처럼 명확히 정의되어 있다면 오해의 여지가 줄어듭니다.

AI 에이전트와 도구 사이의 관계도 마찬가지입니다. 에이전트는 비결정론적입니다.

같은 입력에도 다른 행동을 할 수 있고, 맥락에 따라 판단이 달라집니다. 반면 도구는 결정론적입니다.

같은 입력에는 항상 같은 출력을 내놓아야 합니다. 이 두 세계가 만나는 지점에서 계약이 필요합니다.

좋은 Tool Contract는 네 가지를 명확히 정의합니다. 첫째, what - 이 도구가 정확히 무엇을 하는가.

둘째, when - 언제 이 도구를 사용해야 하는가. 셋째, inputs - 어떤 입력값이 필요한가.

넷째, outputs - 어떤 결과를 반환하는가. 위 코드를 살펴보면, search_file 도구는 description에 "파일명 검색에만 사용하세요"라고 명시했습니다.

이것이 바로 when을 정의한 것입니다. 만약 이 설명이 없다면, 에이전트는 파일 내용 검색에도 이 도구를 사용하려 할 수 있습니다.

실제 현업에서는 어떤 문제가 발생할까요? 김개발 씨는 처음에 description을 "파일을 검색합니다"라고만 적었습니다.

그랬더니 에이전트가 이 도구를 파일 내용 검색, 파일명 검색, 심지어 데이터베이스 검색에까지 사용하려 했습니다. 계약이 모호했기 때문입니다.

박시니어 씨는 조언했습니다. "에이전트를 믿을 수 없는 외부인이라고 생각하세요.

모든 것을 문서화하고, 가정하지 마세요." 김개발 씨는 고개를 끄덕였습니다. Tool은 단순한 함수가 아니라, 두 세계를 연결하는 계약이었던 것입니다.

이 계약이 명확할수록 에이전트는 더 예측 가능하고 안정적으로 동작합니다.

실전 팁

💡 - description에 "언제 사용하면 안 되는지"도 명시하면 오용을 줄일 수 있습니다

  • required 필드를 활용해 필수 입력값과 선택 입력값을 명확히 구분하세요

2. Tool Consolidation

김개발 씨는 에이전트를 위해 30개의 도구를 만들었습니다. 파일 읽기, 파일 쓰기, 파일 삭제, 파일 이동, 파일 복사...

각각 별도의 도구로 분리했습니다. 그런데 에이전트가 자꾸 엉뚱한 도구를 선택하는 문제가 발생했습니다.

Tool Consolidation은 비슷한 기능의 도구들을 하나로 통합하여 에이전트의 선택 부담을 줄이는 전략입니다. 마치 스위스 아미 나이프처럼, 하나의 도구가 관련된 여러 기능을 수행할 수 있게 만드는 것입니다.

도구가 너무 많으면 에이전트는 선택 장애에 빠지고, 모호성이 증가합니다.

다음 코드를 살펴봅시다.

// 나쁜 예: 도구가 너무 많이 분리됨
const tools_bad = [
  { name: "read_file", description: "파일을 읽습니다" },
  { name: "read_file_lines", description: "파일의 특정 줄을 읽습니다" },
  { name: "read_file_head", description: "파일의 처음 N줄을 읽습니다" },
  { name: "read_file_tail", description: "파일의 마지막 N줄을 읽습니다" },
];

// 좋은 예: 통합된 도구
const readFileTool = {
  name: "read_file",
  description: "파일을 읽습니다. 전체, 특정 범위, 처음/끝 N줄 읽기 모두 지원합니다.",
  parameters: {
    type: "object",
    properties: {
      path: { type: "string", description: "파일 경로" },
      mode: {
        type: "string",
        enum: ["full", "lines", "head", "tail"],
        description: "읽기 모드: full(전체), lines(범위), head(처음N줄), tail(끝N줄)"
      },
      start: { type: "number", description: "시작 줄 (lines 모드)" },
      end: { type: "number", description: "끝 줄 (lines 모드)" },
      count: { type: "number", description: "줄 수 (head/tail 모드)" }
    },
    required: ["path"]
  }
};

김개발 씨는 처음에 "기능별로 도구를 분리하는 게 좋은 설계 아닌가요?"라고 생각했습니다. 단일 책임 원칙처럼, 각 도구는 하나의 일만 해야 한다고 배웠기 때문입니다.

하지만 박시니어 씨는 고개를 저었습니다. "에이전트 세계에서는 조금 다릅니다.

에이전트는 수십 개의 도구 중에서 하나를 선택해야 해요. 비슷한 도구가 많을수록 혼란스러워집니다." 이것을 이해하기 위해 음식점을 비유로 들어보겠습니다.

어떤 음식점 메뉴판에 "아메리카노", "아이스 아메리카노", "뜨거운 아메리카노", "연한 아메리카노", "진한 아메리카노"가 따로따로 적혀있다면 어떨까요? 손님은 혼란스러울 것입니다.

반면 "아메리카노 (온도/농도 선택 가능)"이라고 적혀있다면 훨씬 명확합니다. Tool Consolidation은 바로 이 원칙을 따릅니다.

관련된 기능은 하나의 도구로 통합하고, 옵션으로 세부 동작을 선택하게 하는 것입니다. 위 코드에서 나쁜 예시는 4개의 별도 도구를 만들었습니다.

에이전트는 파일을 읽을 때마다 "어떤 도구를 써야 하지?"라고 고민해야 합니다. read_fileread_file_lines의 차이가 뭐지?

그냥 읽으면 전체가 읽히나? 이런 모호성이 오류를 만듭니다.

좋은 예시는 하나의 read_file 도구에 mode 파라미터를 추가했습니다. 에이전트는 "파일을 읽어야 하니까 read_file 도구를 쓰면 되겠다"라고 명확하게 판단할 수 있습니다.

세부적인 읽기 방식은 파라미터로 지정하면 됩니다. 그렇다면 얼마나 통합해야 할까요?

박시니어 씨는 경험에서 우러나온 조언을 해주었습니다. "10개에서 20개 사이가 적당해요.

그보다 적으면 기능이 부족하고, 그보다 많으면 선택이 어려워집니다." 통합의 기준은 개념적 유사성입니다. 파일 읽기 관련 기능은 하나로, 파일 쓰기 관련 기능은 하나로, 검색 관련 기능은 하나로 묶습니다.

하지만 "파일 읽기"와 "데이터베이스 쿼리"를 하나로 묶는 것은 과도한 통합입니다. 김개발 씨는 30개였던 도구를 15개로 줄였습니다.

그랬더니 에이전트의 도구 선택 정확도가 눈에 띄게 향상되었습니다. 때로는 적은 것이 더 많은 것이었습니다.

실전 팁

💡 - 도구 이름이 동사+명사 형태로 비슷하다면 통합 대상입니다 (read_file, read_lines → read)

  • enum 타입의 mode 파라미터를 활용하면 통합이 쉬워집니다

3. Clarity over Abstraction

김개발 씨는 프로그래밍에서 추상화의 중요성을 배웠습니다. 그래서 도구 설명도 추상적으로 작성했습니다.

"데이터를 처리합니다"라고요. 하지만 에이전트는 이 도구를 전혀 엉뚱한 상황에 사용했습니다.

Clarity over Abstraction은 에이전트 도구 설계에서 추상화보다 명확성을 우선해야 한다는 원칙입니다. 일반 프로그래밍에서는 추상화가 미덕이지만, LLM 에이전트는 구체적이고 명확한 설명을 더 잘 이해합니다.

모호한 추상화는 오히려 혼란을 야기합니다.

다음 코드를 살펴봅시다.

// 나쁜 예: 과도한 추상화
const badTool = {
  name: "process_data",
  description: "데이터를 처리합니다",  // 너무 추상적!
  parameters: {
    type: "object",
    properties: {
      data: { type: "any" },  // 타입도 모호함
      options: { type: "object" }  // 어떤 옵션인지 불명확
    }
  }
};

// 좋은 예: 명확한 정의
const goodTool = {
  name: "format_json",
  description: "JSON 문자열을 보기 좋게 정렬합니다. 들여쓰기와 줄바꿈을 추가하여 가독성을 높입니다. 이미 정렬된 JSON이나 유효하지 않은 JSON에는 사용하지 마세요.",
  parameters: {
    type: "object",
    properties: {
      json_string: {
        type: "string",
        description: "정렬할 JSON 문자열. 유효한 JSON이어야 합니다."
      },
      indent_size: {
        type: "number",
        description: "들여쓰기 공백 수. 기본값 2, 최대 8",
        default: 2
      }
    },
    required: ["json_string"]
  }
};

박시니어 씨는 김개발 씨의 도구 정의를 보고 한숨을 쉬었습니다. "프로그래밍할 때 추상화가 좋다고 배웠죠?

하지만 에이전트 세계에서는 정반대예요." 왜 그럴까요? 일반 프로그래밍에서 추상화가 좋은 이유는 코드를 작성하는 개발자가 내부 구현을 이해하고 있기 때문입니다.

processData()라는 추상적인 함수명을 보더라도, 개발자는 문서를 읽거나 코드를 살펴보며 정확한 동작을 파악할 수 있습니다. 하지만 LLM 에이전트는 다릅니다.

에이전트는 오직 도구의 이름설명만 보고 판단합니다. 코드를 들여다보지 않습니다.

따라서 설명이 모호하면 잘못된 도구를 선택하거나, 올바른 도구를 잘못된 방식으로 사용합니다. 마치 외국인에게 길을 알려주는 상황과 같습니다.

"저기 가세요"라고 추상적으로 말하면 어디로 가야 할지 모릅니다. "이 길 따라 100미터 직진한 뒤 편의점에서 좌회전하세요"라고 구체적으로 말해야 합니다.

Clarity over Abstraction 원칙은 네 가지를 명확히 하라고 말합니다. 첫째, what - 이 도구가 정확히 무엇을 하는가.

"데이터를 처리합니다"가 아니라 "JSON 문자열을 들여쓰기하여 정렬합니다"처럼 구체적으로 적어야 합니다. 둘째, when - 언제 이 도구를 사용해야 하는가.

"필요할 때"가 아니라 "압축된 JSON을 읽기 좋게 만들 때"처럼 상황을 명시해야 합니다. 더 나아가 "언제 사용하면 안 되는지"도 적으면 좋습니다.

셋째, inputs - 어떤 입력이 필요한가. 타입만 적지 말고, 입력값의 형식과 제약조건도 설명해야 합니다.

"유효한 JSON이어야 합니다", "최대 8까지 가능합니다" 같은 정보가 중요합니다. 넷째, outputs - 무엇을 반환하는가.

성공 시 어떤 형태의 결과가 나오는지, 실패 시 어떤 에러가 나오는지 알려주면 에이전트가 결과를 올바르게 해석할 수 있습니다. 위 코드의 나쁜 예시를 보세요.

process_data라는 이름, "데이터를 처리합니다"라는 설명, any 타입의 파라미터. 에이전트는 이 도구를 언제 써야 할지 전혀 알 수 없습니다.

모든 데이터 관련 작업에 이 도구를 남용하게 될 것입니다. 좋은 예시는 모든 것이 명확합니다.

도구 이름 format_json에서 이미 목적이 드러나고, 설명은 정확한 동작과 제한사항을 알려주며, 파라미터는 타입과 기본값, 제약조건까지 명시되어 있습니다. 김개발 씨는 모든 도구 설명을 다시 작성했습니다.

추상적인 표현을 구체적인 표현으로 바꾸고, 사용 조건과 제한사항을 추가했습니다. 그 결과 에이전트의 도구 활용 정확도가 크게 향상되었습니다.

실전 팁

💡 - 도구 설명에 "~하지 마세요" 형태의 제한사항을 추가하면 오용을 줄일 수 있습니다

  • 파라미터의 default 값을 명시하면 에이전트가 선택적 파라미터를 더 잘 활용합니다

4. Token Efficiency

김개발 씨는 도구의 응답을 최대한 상세하게 만들었습니다. 파일 읽기 결과에 메타데이터, 히스토리, 관련 파일 목록까지 모두 포함시켰습니다.

그런데 복잡한 작업에서 에이전트가 자꾸 맥락을 잃어버리는 문제가 발생했습니다.

Token Efficiency는 LLM의 컨텍스트 윈도우를 효율적으로 사용하기 위해 도구 응답을 최적화하는 전략입니다. 도구가 너무 많은 정보를 반환하면 에이전트의 컨텍스트가 빠르게 소진되어 앞서 수행한 작업을 잊어버립니다.

필요한 정보만 간결하게 반환하는 것이 핵심입니다.

다음 코드를 살펴봅시다.

// 나쁜 예: 과도한 정보 반환
const readFileResultBad = {
  content: "파일 내용...",
  metadata: {
    size: 1024,
    created: "2024-01-01",
    modified: "2024-01-02",
    owner: "user",
    permissions: "644",
    encoding: "utf-8",
    mimeType: "text/plain",
    // 수십 개의 추가 메타데이터...
  },
  relatedFiles: ["file1.ts", "file2.ts", ...],
  gitHistory: [/* 커밋 히스토리 */]
};

// 좋은 예: 필요한 정보만 반환 + 옵션으로 추가 정보
interface ReadFileParams {
  path: string;
  include_metadata?: boolean;  // 필요할 때만 메타데이터 포함
  line_numbers?: boolean;      // 필요할 때만 줄 번호 포함
}

const readFileResultGood = {
  content: "파일 내용...",  // 기본은 핵심 정보만
  // metadata는 요청 시에만 포함
};

// 출력 형식 옵션 제공
const searchTool = {
  name: "search_code",
  parameters: {
    output_format: {
      type: "string",
      enum: ["minimal", "normal", "detailed"],
      description: "minimal: 파일 경로만, normal: 경로+줄번호, detailed: 경로+줄번호+컨텍스트"
    }
  }
};

박시니어 씨는 화면을 가리키며 설명했습니다. "에이전트가 사용하는 LLM에는 컨텍스트 윈도우라는 것이 있어요.

한 번에 처리할 수 있는 텍스트 양의 한계입니다." 이것은 마치 책상 위의 공간과 같습니다. 책상이 넓으면 많은 서류를 펼쳐놓고 작업할 수 있지만, 공간에는 한계가 있습니다.

서류가 너무 많아지면 이전에 펼쳐둔 중요한 서류가 밀려나 바닥에 떨어집니다. 그러면 그 내용을 잊어버리게 됩니다.

에이전트도 마찬가지입니다. 도구가 너무 많은 정보를 반환하면 컨텍스트 윈도우가 빠르게 채워집니다.

10번의 도구 호출 후 에이전트는 처음에 무엇을 하려고 했는지 잊어버릴 수 있습니다. Token Efficiency는 이 문제를 해결합니다.

첫 번째 원칙은 기본값을 최소화하는 것입니다. 도구의 기본 응답에는 반드시 필요한 정보만 포함합니다.

파일 읽기 도구라면 파일 내용만 반환하면 됩니다. 생성 날짜, 권한, 소유자 같은 메타데이터는 대부분의 경우 필요하지 않습니다.

두 번째 원칙은 옵션으로 확장하는 것입니다. 추가 정보가 필요한 경우를 위해 파라미터를 제공합니다.

include_metadata: true를 지정하면 메타데이터도 함께 반환하는 식입니다. 이렇게 하면 필요할 때만 추가 토큰을 소비합니다.

세 번째 원칙은 출력 형식 옵션을 제공하는 것입니다. 위 코드의 search_code 도구처럼 output_format 파라미터를 두면, 에이전트가 상황에 따라 적절한 상세도를 선택할 수 있습니다.

빠르게 파일 목록만 필요하면 "minimal", 자세한 분석이 필요하면 "detailed"를 선택합니다. 실제 효과는 어떨까요?

김개발 씨가 응답 크기를 최적화한 후, 같은 작업에서 에이전트가 사용하는 토큰 수가 40% 줄었습니다. 더 중요한 것은, 복잡한 멀티스텝 작업에서 에이전트가 맥락을 유지하는 능력이 크게 향상되었다는 점입니다.

주의할 점도 있습니다. 너무 간결하게 만들면 에이전트가 필요한 정보를 얻지 못해 추가 도구 호출을 해야 할 수 있습니다.

이것도 토큰 낭비입니다. 균형을 찾는 것이 중요합니다.

박시니어 씨는 마무리했습니다. "컨텍스트 윈도우를 현금이라고 생각하세요.

아껴 쓸수록 더 복잡한 작업을 수행할 수 있습니다."

실전 팁

💡 - 응답에 줄임표(...)를 사용해 긴 내용을 요약하고 "전체 보기" 옵션을 제공하세요

  • 자주 사용되는 도구일수록 응답 크기 최적화의 효과가 큽니다

5. Recoverable Failures

김개발 씨가 만든 도구에서 에러가 발생했습니다. 에러 메시지는 "Error: Something went wrong"이었습니다.

에이전트는 이 메시지를 보고 어떻게 해야 할지 몰라 그냥 작업을 포기해버렸습니다.

Recoverable Failures는 에이전트가 실패에서 복구할 수 있도록 안내하는 에러 메시지 설계 원칙입니다. 단순히 "실패했다"고 알려주는 것이 아니라, 왜 실패했는지, 어떻게 하면 성공할 수 있는지를 구체적으로 안내해야 합니다.

좋은 에러 메시지는 에이전트를 올바른 방향으로 이끕니다.

다음 코드를 살펴봅시다.

// 나쁜 예: 복구 불가능한 에러 메시지
const badError = {
  error: true,
  message: "File operation failed"  // 왜? 어떻게 해결하지?
};

// 좋은 예: 복구 가능한 에러 메시지
const goodError = {
  error: true,
  code: "FILE_NOT_FOUND",
  message: "파일을 찾을 수 없습니다: /src/utils/helper.ts",
  suggestion: "파일 경로를 확인하세요. 비슷한 파일: /src/utils/helpers.ts, /src/util/helper.ts",
  recoverable: true,
  recovery_actions: [
    "search_file 도구로 정확한 파일 경로를 검색하세요",
    "list_directory 도구로 /src/utils/ 디렉토리 내용을 확인하세요"
  ]
};

// 에러 타입별 복구 가이드 제공
const errorHandlers: Record<string, ErrorResponse> = {
  PERMISSION_DENIED: {
    message: "파일 접근 권한이 없습니다",
    suggestion: "sudo 권한이 필요하거나 파일 소유자를 확인하세요"
  },
  INVALID_JSON: {
    message: "유효하지 않은 JSON 형식입니다",
    suggestion: "JSON 문법을 확인하세요. 흔한 실수: 후행 쉼표, 따옴표 누락"
  },
  RATE_LIMITED: {
    message: "요청 한도를 초과했습니다",
    suggestion: "30초 후에 다시 시도하세요",
    retry_after: 30
  }
};

"Error: Something went wrong" 김개발 씨는 이 에러 메시지를 보고 당황했습니다. 사람도 이해하기 어려운데, 에이전트가 이것을 보고 무엇을 할 수 있을까요?

당연히 아무것도 할 수 없었습니다. 박시니어 씨가 설명했습니다.

"에이전트에게 에러는 대화예요. 뭐가 잘못됐고 어떻게 고칠 수 있는지 알려줘야 합니다." 좋은 에러 메시지는 선생님과 같습니다.

학생이 문제를 틀렸을 때 "틀렸어"라고만 하면 학생은 어떻게 고쳐야 할지 모릅니다. 하지만 "부호를 잘못 봤네.

여기는 마이너스가 아니라 플러스야"라고 하면 학생은 즉시 고칠 수 있습니다. Recoverable Failures 원칙은 에러 메시지에 세 가지를 포함하라고 말합니다.

첫째, 무엇이 잘못됐는지입니다. "파일을 찾을 수 없습니다: /src/utils/helper.ts"처럼 구체적인 실패 원인을 알려줍니다.

에러 코드(FILE_NOT_FOUND)도 함께 제공하면 에이전트가 에러 유형을 분류하기 쉬워집니다. 둘째, 왜 잘못됐는지입니다.

"비슷한 파일: /src/utils/helpers.ts"처럼 가능한 원인을 제시합니다. 오타인지, 경로가 틀린 건지, 권한 문제인지 힌트를 줍니다.

셋째, 어떻게 해결할 수 있는지입니다. "search_file 도구로 정확한 파일 경로를 검색하세요"처럼 구체적인 다음 액션을 제안합니다.

에이전트는 이 제안을 따라 문제를 해결할 수 있습니다. 위 코드를 보면, 좋은 예시는 recovery_actions 배열로 복구 방법을 여러 개 제시합니다.

에이전트는 첫 번째 방법이 안 되면 두 번째를 시도할 수 있습니다. errorHandlers 객체는 에러 유형별로 미리 정의된 복구 가이드를 제공합니다.

RATE_LIMITED 에러에는 retry_after 값까지 포함되어 있어서, 에이전트가 정확히 언제 재시도해야 하는지 알 수 있습니다. 실제 효과는 놀라웠습니다.

김개발 씨가 에러 메시지를 개선한 후, 에이전트의 작업 완료율이 25% 향상되었습니다. 이전에는 에러가 나면 포기했던 작업들을 이제는 스스로 해결할 수 있게 된 것입니다.

주의할 점은 에러 메시지도 토큰을 소비한다는 것입니다. 너무 장황한 에러 메시지는 컨텍스트 윈도우를 낭비합니다.

핵심 정보만 간결하게 담는 것이 좋습니다. 박시니어 씨는 마무리했습니다.

"좋은 에러 메시지는 에이전트의 회복력을 높여줍니다. 실패해도 다시 일어설 수 있게 해주는 거죠."

실전 팁

💡 - 비슷한 이름의 대안을 제시하면 오타로 인한 실패를 쉽게 복구할 수 있습니다

  • recoverable 필드로 에이전트가 재시도 가치가 있는지 판단할 수 있게 하세요

6. MCP Tool Best Practices

김개발 씨는 MCP(Model Context Protocol) 서버에서 제공하는 도구들을 사용하게 되었습니다. 그런데 같은 이름의 도구가 여러 서버에 있었습니다.

read_file 도구를 호출했더니 엉뚱한 서버의 도구가 실행되어 예상치 못한 결과가 나왔습니다.

MCP 툴 모범 사례는 여러 MCP 서버가 제공하는 도구들을 올바르게 사용하는 방법입니다. 핵심은 완전 정규화된 이름(Fully Qualified Name)을 사용하는 것입니다.

서버 이름과 도구 이름을 함께 명시하여 모호성을 제거합니다.

다음 코드를 살펴봅시다.

// MCP 도구 호출 시 완전 정규화된 이름 사용
// 형식: server_name:tool_name

// 나쁜 예: 도구 이름만 사용
const badCall = {
  tool: "read_file",  // 어느 서버의 read_file인지 모호함
  params: { path: "/src/index.ts" }
};

// 좋은 예: 서버 이름을 포함한 완전 정규화된 이름
const goodCall = {
  tool: "filesystem:read_file",  // filesystem 서버의 read_file
  params: { path: "/src/index.ts" }
};

// MCP 서버 설정 예시
const mcpServers = {
  "filesystem": {
    command: "mcp-server-filesystem",
    tools: ["read_file", "write_file", "list_directory"]
  },
  "github": {
    command: "mcp-server-github",
    tools: ["read_file", "create_issue", "list_repos"]  // 같은 이름의 도구!
  },
  "database": {
    command: "mcp-server-postgres",
    tools: ["query", "insert", "update"]
  }
};

// 에이전트에게 MCP 도구 사용법 안내
const toolUsageGuide = `
MCP 도구를 호출할 때는 반드시 완전 정규화된 이름을 사용하세요:
- 로컬 파일 읽기: filesystem:read_file
- GitHub 파일 읽기: github:read_file
- 데이터베이스 조회: database:query
`;

MCP는 LLM 에이전트가 다양한 외부 시스템과 상호작용할 수 있게 해주는 프로토콜입니다. 파일 시스템, GitHub, 데이터베이스, Slack 등 다양한 MCP 서버가 도구를 제공합니다.

문제는 서로 다른 서버가 같은 이름의 도구를 가질 수 있다는 것입니다. 김개발 씨가 겪은 상황을 보겠습니다.

그는 로컬 파일을 읽으려고 read_file 도구를 호출했습니다. 하지만 시스템에는 filesystem 서버의 read_file과 GitHub 서버의 read_file 두 개가 있었습니다.

에이전트는 GitHub 서버의 도구를 선택했고, 로컬 파일 대신 GitHub 저장소의 파일을 읽으려 해서 에러가 발생했습니다. 이 문제를 해결하는 방법이 완전 정규화된 이름(Fully Qualified Name)입니다.

완전 정규화된 이름은 server_name:tool_name 형식을 따릅니다. read_file 대신 filesystem:read_file이라고 명시하면 모호성이 사라집니다.

에이전트는 정확히 어느 서버의 어떤 도구를 호출해야 하는지 알 수 있습니다. 이것은 마치 사람 이름과 같습니다.

"철수야"라고 부르면 같은 반에 철수가 두 명 있을 때 혼란스럽습니다. "3반 김철수"라고 부르면 명확해집니다.

도구 설명에도 이 정보를 포함해야 합니다. 위 코드의 toolUsageGuide처럼 에이전트가 참고할 수 있는 가이드를 시스템 프롬프트에 포함시키면 좋습니다.

MCP 도구 설계 시 고려할 사항이 더 있습니다. 첫째, 서버 이름을 의미있게 짓습니다.

server1, server2 대신 filesystem, github, database처럼 역할이 드러나는 이름을 사용합니다. 둘째, 도구 이름의 일관성을 유지합니다.

같은 서버 내에서는 비슷한 명명 규칙을 따릅니다. read_file, write_file, delete_file처럼 동사_명사 형식을 일관되게 사용합니다.

셋째, 도구 설명에 서버 컨텍스트를 포함합니다. "filesystem 서버의 read_file은 로컬 파일 시스템에서 파일을 읽습니다"처럼 어떤 서버의 도구인지, 어떤 환경에서 동작하는지 명시합니다.

김개발 씨는 모든 MCP 도구 호출을 완전 정규화된 이름으로 변경했습니다. 그 후로 도구 혼동 문제는 완전히 사라졌습니다.

실전 팁

💡 - 시스템 프롬프트에 사용 가능한 MCP 서버와 도구 목록을 포함시키세요

  • 같은 기능의 도구가 여러 서버에 있다면, 각각의 차이점을 명확히 문서화하세요

7. Optimal Tool Set Design

드디어 실습 시간입니다. 김개발 씨는 지금까지 배운 모든 원칙을 적용해서 에이전트용 도구 세트를 설계하기로 했습니다.

목표는 10-20개의 최적화된 도구로 대부분의 개발 업무를 수행할 수 있게 하는 것입니다.

최적 툴 세트 설계는 앞서 배운 모든 원칙을 종합하여 실제로 사용할 도구들을 설계하는 과정입니다. 도구 수는 10-20개가 적당하며, 각 도구는 명확한 계약, 적절한 통합 수준, 효율적인 응답, 복구 가능한 에러를 갖춰야 합니다.

다음 코드를 살펴봅시다.

// 최적화된 개발 도구 세트 설계 (15개 도구)
const optimalToolSet = {
  // 1. 파일 작업 (통합된 3개 도구)
  file_read: {
    description: "파일 내용을 읽습니다. 전체/부분/줄번호 포함 읽기 지원",
    params: ["path", "mode?", "start_line?", "end_line?", "include_line_numbers?"]
  },
  file_write: {
    description: "파일에 내용을 씁니다. 생성, 덮어쓰기, 추가 모드 지원",
    params: ["path", "content", "mode: create|overwrite|append"]
  },
  file_edit: {
    description: "파일의 특정 부분을 수정합니다. 검색-교체 방식",
    params: ["path", "old_content", "new_content", "replace_all?"]
  },

  // 2. 검색 작업 (통합된 2개 도구)
  search_files: {
    description: "파일명 패턴으로 파일 검색. glob 패턴 지원",
    params: ["pattern", "path?", "max_results?"]
  },
  search_content: {
    description: "파일 내용에서 텍스트 검색. 정규식 지원",
    params: ["query", "path?", "file_pattern?", "output_format?"]
  },

  // 3. 코드 분석 (3개 도구)
  analyze_syntax: {
    description: "코드 문법 검사 및 에러 위치 반환",
    params: ["path", "language?"]
  },
  get_definitions: {
    description: "함수/클래스/변수 정의 위치 찾기",
    params: ["symbol", "path?"]
  },
  get_references: {
    description: "심볼의 모든 참조 위치 찾기",
    params: ["symbol", "path?"]
  },

  // 4. 실행 (2개 도구)
  run_command: {
    description: "셸 명령어 실행. 타임아웃 및 작업 디렉토리 지정 가능",
    params: ["command", "cwd?", "timeout?", "env?"]
  },
  run_tests: {
    description: "테스트 실행. 특정 파일/패턴 지정 가능",
    params: ["pattern?", "watch?", "coverage?"]
  },

  // 5. Git 작업 (통합된 2개 도구)
  git_status: {
    description: "저장소 상태 조회. 변경사항, 브랜치, 커밋 정보",
    params: ["include_diff?", "include_log?"]
  },
  git_commit: {
    description: "변경사항 스테이징 및 커밋",
    params: ["message", "files?", "amend?"]
  },

  // 6. 유틸리티 (3개 도구)
  format_code: {
    description: "코드 포맷팅. 언어별 포맷터 자동 선택",
    params: ["path", "language?", "config?"]
  },
  web_fetch: {
    description: "URL에서 데이터 가져오기. HTML, JSON, 텍스트 지원",
    params: ["url", "method?", "format?"]
  },
  ask_user: {
    description: "사용자에게 질문하고 응답 대기",
    params: ["question", "options?", "allow_custom?"]
  }
};

// 도구 세트 검증 함수
function validateToolSet(tools: ToolSet): ValidationResult {
  const issues: string[] = [];

  if (Object.keys(tools).length < 10) issues.push("도구가 너무 적습니다 (최소 10개)");
  if (Object.keys(tools).length > 20) issues.push("도구가 너무 많습니다 (최대 20개)");

  for (const [name, tool] of Object.entries(tools)) {
    if (!tool.description) issues.push(`${name}: description 누락`);
    if (tool.description.length < 20) issues.push(`${name}: description이 너무 짧음`);
  }

  return { valid: issues.length === 0, issues };
}

김개발 씨는 지금까지 배운 모든 원칙을 떠올렸습니다. Tools as Contracts, Tool Consolidation, Clarity over Abstraction, Token Efficiency, Recoverable Failures, MCP Best Practices.

이제 이것들을 실제로 적용해볼 차례입니다. 박시니어 씨는 조언했습니다.

"10개에서 20개 사이가 황금 비율이에요. 그 안에서 최대한 많은 작업을 커버할 수 있게 설계해보세요." 김개발 씨는 개발 업무를 크게 여섯 가지 범주로 나눴습니다.

파일 작업, 검색 작업, 코드 분석, 실행, Git 작업, 유틸리티. 각 범주별로 필요한 도구를 정의하되, 통합 원칙을 적용해서 최소화했습니다.

파일 작업을 보겠습니다. 원래는 읽기, 쓰기, 생성, 삭제, 이동, 복사, 이름 변경 등 수많은 도구가 필요할 것 같습니다.

하지만 실제로 에이전트가 가장 자주 하는 작업은 읽기, 쓰기, 수정 세 가지입니다. 삭제나 이동은 run_command로 처리할 수 있습니다.

그래서 세 개의 도구로 통합했습니다. 각 도구의 description을 보세요.

Clarity over Abstraction 원칙에 따라 무엇을 하는지, 어떤 모드를 지원하는지 명확히 적었습니다. file_read는 "전체/부분/줄번호 포함 읽기 지원"이라고 명시했습니다.

파라미터에는 물음표(?)가 붙은 것들이 있습니다. 이것은 선택적 파라미터입니다.

Token Efficiency 원칙에 따라 기본값으로 동작하되, 필요할 때 추가 옵션을 사용할 수 있게 했습니다. 검색 도구는 두 개로 나눴습니다.

search_files는 파일명 검색, search_content는 내용 검색입니다. 이 둘은 개념적으로 다르기 때문에 통합하면 오히려 혼란스럽습니다.

적절한 분리도 중요합니다. 코드 분석 도구 세 개는 IDE가 제공하는 핵심 기능을 반영합니다.

문법 검사, 정의로 이동, 참조 찾기. 이 세 가지가 있으면 대부분의 코드 탐색이 가능합니다.

validateToolSet 함수는 설계한 도구 세트를 검증합니다. 도구 수가 적절한지, 모든 도구에 description이 있는지, description이 충분히 상세한지 확인합니다.

이런 검증 과정을 거치면 누락이나 실수를 줄일 수 있습니다. 김개발 씨는 이 15개 도구 세트를 실제 프로젝트에 적용해봤습니다.

결과는 놀라웠습니다. 이전에 30개 도구를 사용할 때보다 에이전트의 도구 선택 정확도가 40% 향상되었고, 작업 완료율도 크게 높아졌습니다.

박시니어 씨는 마지막 조언을 건넸습니다. "도구 세트는 한 번에 완성되지 않아요.

사용하면서 계속 다듬어가야 합니다. 어떤 도구가 자주 잘못 사용되는지, 어떤 도구가 부족한지 관찰하고 개선하세요."

실전 팁

💡 - 도구 사용 로그를 분석해서 자주 함께 호출되는 도구들을 통합 대상으로 고려하세요

  • 에이전트가 자주 실패하는 도구는 description을 개선하거나 파라미터를 단순화하세요
  • 새 도구를 추가하기 전에 기존 도구의 옵션으로 해결할 수 있는지 먼저 검토하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#AI Engineering#Tool Design#Agent#MCP#LLM

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

Context Optimization 컨텍스트 최적화 기법

AI 에이전트와 대규모 언어 모델 활용 시 컨텍스트 윈도우를 효율적으로 관리하는 방법을 다룹니다. 토큰 비용 절감부터 캐시 최적화까지, 실무에서 바로 적용할 수 있는 핵심 기법들을 소개합니다.

Memory Systems 에이전트 메모리 아키텍처 완벽 가이드

AI 에이전트가 정보를 기억하고 활용하는 메모리 시스템의 핵심 아키텍처를 다룹니다. 벡터 스토어의 한계부터 Knowledge Graph, Temporal Knowledge Graph까지 단계별로 이해할 수 있습니다.

Multi-Agent Patterns 멀티 에이전트 아키텍처 완벽 가이드

여러 AI 에이전트가 협력하여 복잡한 작업을 수행하는 멀티 에이전트 시스템의 핵심 패턴을 다룹니다. 컨텍스트 격리부터 Supervisor, Swarm, Hierarchical 패턴까지 실무에서 바로 적용할 수 있는 아키텍처 설계 원칙을 배웁니다.

Context Compression 컨텍스트 압축 전략 완벽 가이드

LLM 애플리케이션에서 컨텍스트 윈도우를 효율적으로 관리하는 압축 전략을 다룹니다. Anchored Summarization부터 프로브 기반 평가까지, 토큰 비용을 최적화하면서 정보 품질을 유지하는 핵심 기법들을 실무 관점에서 설명합니다.

Context Degradation 컨텍스트 저하 패턴 진단

LLM의 컨텍스트 윈도우에서 발생하는 다양한 정보 손실과 왜곡 패턴을 진단하고, 이를 완화하는 실전 전략을 학습합니다. 프롬프트 엔지니어링의 핵심 난제를 풀어봅니다.