블로그 목록
AI

Vercel AI SDK로 툴 실행 전에 사용자 승인 받기 — 위험한 작업은 확인 버튼부터

에이전트에 툴을 붙이다 보면 슬슬 무서워지는 순간이 온다. 메일 보내기, 결제, DB 레코드 삭제 같은 툴을 LLM한테 그냥 쥐여주면, 모델이 오판한 순간 되돌릴 수 없는 일이 벌어진다. 조회 툴은 마음대로 돌려도 되지만, 실제로 뭔가를 바꾸는 툴은 사람이 한 번 확인하고 넘어가는 게 맞다.

처음엔 프롬프트로 "실행 전에 물어봐"라고 시켜봤는데, 이건 모델 재량이라 안 지킬 때가 있다. 확실하게 막으려면 코드로 게이트를 걸어야 한다. Vercel AI SDK v5는 이걸 위한 패턴이 아예 정해져 있다. 핵심은 툴에 execute를 안 붙이는 것이다.

execute 없는 툴은 실행을 멈춘다

execute 함수가 없으면 SDK는 툴 콜을 만들어놓고 실행은 안 한다. 대신 그 툴 콜이 input-available 상태로 클라이언트까지 흘러온다.

// app/api/chat/route.ts
import { streamText, tool } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';

const tools = {
  sendEmail: tool({
    description: '사용자에게 메일을 보낸다',
    inputSchema: z.object({ to: z.string(), subject: z.string() }),
    // execute를 일부러 뺀다 → 자동 실행되지 않음
  }),
};

프론트에서 확인 버튼을 그린다

useChat으로 받은 메시지 파트에서 이 툴 콜을 찾아 확인 UI를 띄운다. 사용자가 버튼을 누르면 addToolResult로 결과를 넣어준다.

'use client';
import { useChat } from '@ai-sdk/react';
import { lastAssistantMessageIsCompleteWithToolCalls } from 'ai';

const { messages, addToolResult } = useChat({
  // 툴 결과가 채워지면 자동으로 서버에 다시 보낸다
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
});

// 렌더링 중 sendEmail 파트를 만나면:
part.type === 'tool-sendEmail' && part.state === 'input-available' && (
  <div>
    <p>{part.input.to} 에게 메일을 보낼까요?</p>
    <button onClick={() => addToolResult({
      tool: 'sendEmail', toolCallId: part.toolCallId, output: '보냄',
    })}>보내기</button>
    <button onClick={() => addToolResult({
      tool: 'sendEmail', toolCallId: part.toolCallId, output: '취소됨',
    })}>취소</button>
  </div>
)

승인을 누르면 output이 결과로 들어가고, sendAutomaticallyWhen 덕분에 대화가 알아서 이어진다. 취소를 누르면 "취소됨"이라는 결과가 모델에게 전달되니, 모델은 그 사실을 알고 다음 답을 만든다.

실제 실행은 어디서?

실행 로직 자체는 승인 시점에 넣으면 된다. output을 만들기 전에 실제 메일 발송 API를 호출하거나, 서버에 별도 라우트를 두고 거기서 처리한 뒤 결과를 넣는 식이다. 중요한 건 모델이 아니라 사용자 클릭이 방아쇠라는 점이다.


정리하면, 되돌릴 수 없는 툴은 execute를 빼서 자동 실행을 막고, 프론트에서 확인 버튼 → addToolResult로 사람 손을 한 번 태우면 된다. 조회성 툴은 그대로 execute를 붙여 자동으로 돌리고, 위험한 툴만 골라 게이트를 걸면 에이전트가 훨씬 덜 무서워진다.