일반 LLM API 호출은 "질문 → 답변"의 단방향입니다. Agent는 다릅니다 — LLM이 스스로 도구(Tool)를 선택해서 실행하고, 결과를 보고 다음 행동을 결정하는 루프를 반복합니다. 서버 코드를 짜고, DB를 조회하고, 외부 API를 호출하는 AI를 만들 수 있게 됩니다.
ReAct = Reasoning + Acting. LLM이 생각(Thought)하고, 행동(Action)하고, 결과를 관찰(Observation)한 뒤 다시 생각하는 패턴입니다.
Tool Calling은 LLM API가 공식 지원하는 기능입니다. 개발자가 "이런 함수가 있어" 라고 JSON 스키마로 도구를 선언하면, LLM이 필요할 때 해당 도구를 호출하라고 신호를 보냅니다. 실제 함수 실행은 백엔드 서버가 하고, 결과를 다시 LLM에 돌려주면 됩니다.
// Tool 정의 (JSON Schema)
{
"name": "get_order_status",
"description": "주문 ID로 주문 상태를 조회합니다",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "조회할 주문 ID"
}
},
"required": ["order_id"]
}
}
// LLM 응답 (Tool 호출 요청)
{
"type": "tool_use",
"name": "get_order_status",
"input": { "order_id": "ORD-12345" }
}
// 백엔드가 실행 후 결과를 다시 전달
{
"type": "tool_result",
"content": "{\"status\": \"배송중\", \"eta\": \"2026-06-18\"}"
}
// 무한 루프 방지를 위해 maxSteps 필수!
public String runAgent(String userMessage) {
List<Message> messages = new ArrayList<>();
messages.add(userMsg(userMessage));
for (int step = 0; step < MAX_STEPS; step++) {
var response = llm.call(messages, TOOLS);
if (response.isTextOnly()) {
return response.getText(); // 완료
}
// Tool 호출 처리
for (ToolUse tu : response.getToolUses()) {
String result = executeTool(
tu.getName(), tu.getInput());
messages.add(toolResult(tu.getId(), result));
}
messages.add(assistantMsg(response));
}
return "최대 단계 초과"; // Circuit Breaker
}
private String executeTool(String name, JsonNode input) {
return switch (name) {
case "get_order_status" -> {
String id = input.get("order_id").asText();
// ✅ 권한 체크: 현재 사용자가 이 주문의 소유자인가?
if (!orderService.isOwner(currentUserId, id))
yield "{\"error\": \"권한 없음\"}";
yield orderService.getStatus(id);
}
case "send_email" -> {
// ✅ 위험 도구: Human-in-the-loop 게이트
yield requireApproval(name, input);
}
default -> "{\"error\": \"알 수 없는 도구\"}";
};
}
일반 API 호출은 개발자가 명시적으로 어떤 함수를 호출할지 코드에 작성합니다. Tool Calling은 LLM이 상황을 판단해서 어떤 도구가 필요한지 스스로 결정합니다. 사용자가 "내 주문 어디까지 왔어?"라고 하면 LLM이 get_order_status를 선택하고, "환불해줘"라고 하면 request_refund를 선택합니다 — 분기 로직을 LLM이 대신합니다. 단, 이 때문에 어떤 도구가 호출될지 100% 예측이 어렵다는 점이 전통적인 서비스와 다른 점이고, 권한 체크·비용 제한이 필수인 이유입니다.
세 가지 위험이 있습니다. 첫째, 무한 루프입니다. LLM이 도구 결과를 오해해서 같은 도구를 계속 호출할 수 있습니다. max_steps와 토큰 예산으로 반드시 제어해야 합니다. 둘째, 권한 없는 도구 실행입니다. LLM이 사용자 요청 범위를 벗어난 도구를 호출할 수 있습니다. 도구마다 권한 체크와 화이트리스트를 적용해야 합니다. 셋째, 비용 폭탄입니다. 도구 결과가 길어서 컨텍스트가 커지거나 루프가 반복되면 토큰 비용이 기하급수적으로 증가합니다.
룰 기반 봇은 개발자가 모든 분기("주문 조회이면 A, 환불이면 B")를 미리 코딩합니다. 새로운 케이스가 생기면 코드를 수정해야 합니다. ReAct Agent는 자연어 의도를 LLM이 해석하고 적절한 도구를 스스로 선택합니다. 미리 작성하지 않은 복잡한 조합("주문 조회 후 배송이 7일 넘으면 자동으로 보상 쿠폰 발행")도 처리할 수 있습니다. 대신 결정 과정이 LLM의 확률적 추론에 의존하므로 예측 가능성이 낮아집니다.
Agent = LLM이 도구를 선택해서 실행하는 루프. 구현 시 max_steps·권한 체크·비용 제한 세 가지가 없으면 프로덕션에서 반드시 사고난다.