학습 자료

실전 도구 — 외부 API 연결


Article

14강의 runCalculator 는 순수 자바스크립트 함수였다. 인터넷에 나갈 일이 없었다. 그런데 실무에서 Claude 에게 맡기고 싶은 일은 대부분 바깥 세상의 데이터다. 최신 정보, 다른 서비스의 상태, 우리 DB 밖의 무언가. 오늘은 진짜 외부 API 를 부르는 도구를 만든다. npm 레지스트리에서 패키지 정보를 실시간으로 가져온다.

왜 npm 레지스트리인가

키 발급 없이, 누구나 바로 따라 할 수 있는 실제 공개 API 가 필요했다. https://registry.npmjs.org/{패키지명} 은 인증 없이 JSON 을 돌려주는 진짜 프로덕션 API 다. 우리가 지금까지 npm i 할 때마다 이미 이 서버와 통신해온 것이다.

13·14강 도구 vs 오늘 도구
  1. calculator (13·14강)입력을 받아 우리 코드 안에서 즉시 계산 — 네트워크 없음
  2. get_npm_package_info (오늘)입력을 받아 실제 서버에 요청을 보내고 응답을 기다린다
구조(tool_use → 실행 → tool_result)는 똑같다. runCalculator 자리에 fetch 가 들어갈 뿐이다.

도구를 정의하고, 진짜 fetch 로 실행한다

ai-lab-b 저장소에 lesson-15 폴더를 만들고 14강과 같은 방식으로 세팅한다. index.ts 를 만든다.

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const tools: Anthropic.Tool[] = [
  {
    name: "get_npm_package_info",
    description:
      "npm 패키지의 최신 버전·설명·라이선스를 실제 npm 레지스트리에서 조회한다. 패키지 이름을 확실히 모르면 짐작하지 말고 이 도구로 확인한다.",
    input_schema: {
      type: "object",
      properties: {
        package_name: {
          type: "string",
          description: "조회할 npm 패키지 이름. 예: \"react\", \"@anthropic-ai/sdk\"",
        },
      },
      required: ["package_name"],
    },
  },
];

async function getNpmPackageInfo(packageName: string): Promise<string> {
  const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
  const res = await fetch(url);

  if (!res.ok) {
    return JSON.stringify({ error: `패키지 "${packageName}" 를 찾을 수 없다 (HTTP ${res.status})` });
  }

  const data = await res.json();
  const latest = data["dist-tags"]?.latest;
  const latestVersionInfo = data.versions?.[latest];

  return JSON.stringify({
    name: data.name,
    latest_version: latest,
    description: data.description ?? null,
    license: latestVersionInfo?.license ?? null,
  });
}

runCalculator 와 모양이 같다. 입력을 받아 결과 문자열을 돌려준다. 다른 건 그 안에서 await fetch(url) 로 진짜 네트워크를 탄다는 것, 그리고 실패할 수 있다는 것이다. res.ok 가 거짓이면(패키지가 없으면) 예외를 던지는 대신, 에러 내용을 담은 JSON 문자열을 그대로 tool_result 로 돌려준다.

되먹임 루프는 14강과 똑같다

ask 함수는 14강의 while 루프를 그대로 쓴다. 도구 이름만 get_npm_package_info 로 바뀌고, 실행부만 await getNpmPackageInfo(...) 로 바뀐다.

async function ask(question: string) {
  console.log(`\n질문: "${question}"`);

  const messages: Anthropic.MessageParam[] = [{ role: "user", content: question }];

  let response = await client.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 500,
    tools,
    messages,
  });

  console.log(`[1차 응답] stop_reason: ${response.stop_reason}`);

  while (response.stop_reason === "tool_use") {
    messages.push({ role: "assistant", content: response.content });

    const toolResults: Anthropic.ToolResultBlockParam[] = [];

    for (const block of response.content) {
      if (block.type === "tool_use" && block.name === "get_npm_package_info") {
        const input = block.input as { package_name: string };
        const result = await getNpmPackageInfo(input.package_name);
        console.log(`  [실행] get_npm_package_info(${JSON.stringify(input)})`);
        console.log(`  [결과] ${result}`);
        toolResults.push({
          type: "tool_result",
          tool_use_id: block.id,
          content: result,
        });
      }
    }

    messages.push({ role: "user", content: toolResults });

    response = await client.messages.create({
      model: "claude-haiku-4-5",
      max_tokens: 500,
      tools,
      messages,
    });

    console.log(`[다음 응답] stop_reason: ${response.stop_reason}`);
  }

  for (const block of response.content) {
    if (block.type === "text") {
      console.log(`[최종 답] ${block.text}`);
    }
  }
}

await ask("@anthropic-ai/sdk 패키지 최신 버전이 몇이야? 라이선스도 알려줘.");
await ask("존재하지 않을 것 같은 패키지 이름 \"this-package-should-not-exist-xyz-12345\" 정보를 조회해줘.");

루프 코드를 한 글자도 안 바꿨다. tool_use → 실행 → tool_result → 반복이라는 뼈대는, 도구 안에서 순수 계산을 하든 네트워크를 타든 상관없다. 이게 14강에서 만든 구조를 재사용 가능하게 만든 이유다.

실제로 돌려본다

npx tsx --env-file=.env index.ts
터미널
  • 질문: "@anthropic-ai/sdk 패키지 최신 버전이 몇이야? 라이선스도 알려줘."
  • [1차 응답] stop_reason: tool_use
  • [실행] get_npm_package_info({"package_name":"@anthropic-ai/sdk"})
  • [결과] {"name":"@anthropic-ai/sdk","latest_version":"0.112.3","description":"The official TypeScript library for the Anthropic API","license":"MIT"}
  • [다음 응답] stop_reason: end_turn
  • [최종 답] @anthropic-ai/sdk 패키지 정보를 알려드립니다:
  • - **최신 버전**: 0.112.3
  • - **라이선스**: MIT
  • MIT 라이선스이므로 상업적 사용, 수정, 배포 등이 비교적 자유롭게 가능합니다!
  • 질문: "존재하지 않을 것 같은 패키지 이름 \"this-package-should-not-exist-xyz-12345\" 정보를 조회해줘."
  • [1차 응답] stop_reason: tool_use
  • [실행] get_npm_package_info({"package_name":"this-package-should-not-exist-xyz-12345"})
  • [결과] {"error":"패키지 \"this-package-should-not-exist-xyz-12345\" 를 찾을 수 없다 (HTTP 404)"}
  • [다음 응답] stop_reason: end_turn
  • [최종 답] 예상대로입니다!
  • **결과: 패키지를 찾을 수 없습니다** (HTTP 404 에러)
  • 패키지 이름 "this-package-should-not-exist-xyz-12345"는 npm 레지스트리에 존재하지 않습니다.
  • 실제로 존재하는 패키지 정보를 조회하고 싶으시면 패키지 이름을 알려주세요! 예를 들어: react, lodash, express, @angular/core

첫 질문은 실제 존재하는 패키지라 정상 응답이 왔다. @anthropic-ai/sdk 의 진짜 최신 버전과 라이선스를, Claude 가 우리에게 지어낸 게 아니라 방금 npm 서버에서 받아온 값 그대로 전달했다. 두 번째 질문은 일부러 존재하지 않을 이름을 넣었다. 우리 도구는 404 를 에러로 던지지 않고 {"error": "..."} 문자열을 tool_result 로 돌려줬고, Claude 는 그 에러 내용을 읽고 당황하지 않고 자연스럽게 "못 찾았다"고 설명한 뒤 대안까지 제시했다.

성공 케이스 vs 실패 케이스, 둘 다 tool_result 다

지금까지 만든 것

my-ai-assistant/ (ai-lab-b, lesson-15)
  • my-ai-assistant
    • index.tsget_npm_package_info — 진짜 fetch 를 타는 도구
    • .env
    • package.json

정리하면

도구 안에서 무엇을 하든(순수 계산이든 네트워크 요청이든) tool_use → 실행 → tool_result 되먹임이라는 뼈대는 그대로다. 실패조차 예외로 던지지 말고 tool_result 로 돌려주면, Claude 가 그 실패까지 자연스럽게 다뤄준다.

다시 짚어보기
  1. 01

    진짜 외부 API(npm 레지스트리)를 부르는 도구를 만들었다

    fetch 로 registry.npmjs.org 에 요청해 실제 최신 버전·라이선스를 받아왔다.

  2. 02

    14강의 되먹임 루프를 코드 변경 없이 재사용했다

    tool_use → 실행 → tool_result 구조는 도구 내부가 계산이든 네트워크든 그대로 통한다.

    npx tsx --env-file=.env index.ts
  3. 03

    실패(404)도 예외로 던지지 않고 tool_result 로 돌려줬다

    Claude 가 에러 내용을 읽고 당황하지 않고 안내 문구로 마무리하는 것까지 확인했다.

Part 4(도구 사용)가 끝났다. 12강에서 인자 없는 도구로 시작해, 13강에서 촘촘한 스키마를, 14강에서 되먹임 루프를, 오늘 진짜 외부 API 연결까지 왔다. 다음 Part 5부터는 방향이 바뀐다. 도구로 "행동"을 시키는 대신, 내 문서를 검색해 답하게 만든다. 16강에서 문서를 읽어 청크로 쪼개는 것부터 시작한다.

실전 도구 — 외부 API 연결 — 디코드랩(DCODELAB)