go_bunzee

AI Agent 만들기 생각보다 잘 안 된다 - 계획과 실제 구현에서 벌어지는 오류들 | 매거진에 참여하세요

questTypeString.01quest1SubTypeString.04
publish_date : 26.03.10

AI Agent 만들기 생각보다 잘 안 된다 - 계획과 실제 구현에서 벌어지는 오류들

#AI에이전트 #구현 #오류 #툴선택 #LLM #비용 #OpenClaw #오케스트라 #UI렌더링 #화면내업데이트

content_guide

1편 https://letspl.me/quest/2354/shortcut

3편 https://letspl.me/quest/2356/shortcut

AI Agent 구조는 이렇게 구성된다.

User
 ↓
Orchestrator
 ↓
LLM
 ↓
Tools
 ↓
Memory

다른건 알겠는데 Orchestrator는 실제로 무엇을 할까?

간단히 말하면

Agent의 실행을 관리하는 엔진이다.

LLM은 생각을 하고 Tool은 행동을 한다.

하지만 그 사이에서 어떤 순서로 실행할지 어떤 Tool을 호출할지 언제 멈출지

를 관리하는 것이 Orchestrator다.

실제 Orchestrator가 하는 일

실제 서비스에서 Orchestrator는 보통 다음 역할을 한다. 아래 다섯 가지가 핵심이다.


  1. 1. 요청 분석
    2. Agent loop 실행
    3. Tool 호출 관리
    4. 병렬 작업 관리
    5. 결과 정리 및 전달


실제 실행 흐름

사용자가 이렇게 요청한다고 해보자.

“다음 주 도쿄 출장 일정 만들어줘”

Orchestrator는 보통 이런 순서로 동작한다.

1 user request
2 planner 실행
3 task 생성
4 tool 실행
5 결과 수집
6 결과 정리
7 사용자 응답 생성

이걸 코드 흐름으로 단순화하면 이렇게 된다.

tasks = planner(user_request)

results = []

for task in tasks:
    tool = select_tool(task)
    result = tool.run(task)
    results.append(result)

response = llm.generate(results)

물론 실제 시스템은 이것보다 훨씬 복잡하다.

특히 Tool 실행 방식이 중요하다.


Tool은 항상 순차 실행일까?

많은 예시에서는 Tool이 순차적으로 실행되는 것처럼 보인다.

flight search
↓
hotel search
↓
calendar event

하지만 실제 서비스에서는 병렬 실행이 꽤 많다.

예를 들어 여행 일정 생성이라면

search_flight
search_hotel
search_restaurant

이 세 개는 서로 의존성이 없다.

그래서 보통 이렇게 실행한다.

          search_flight
         /
Planner →
         \
          search_hotel

          search_restaurant

병렬 Tool 실행이다. Python 예시로 보면 이런 구조가 된다.

results = await asyncio.gather(
    search_flight(),
    search_hotel(),
    search_restaurant()
)

이렇게 하면 응답 속도가 훨씬 빨라진다.

Tool 결과는 어떻게 사용자에게 전달될까

여기서 또 하나 중요한 질문이 있다.

Tool 실행 결과는 보통 이런 형태다.

flight API result
hotel API result
restaurant API result

하지만 사용자는 이런 데이터를 원하지 않는다.

사용자가 원하는 것은 보통 이런 형태다.

“서울 → 도쿄 항공편은 ANA 10:30 출발이 가장 저렴합니다.
추천 호텔은 신주쿠 지역의 XXX 호텔입니다.”

API 결과 → 사용자 메시지 로 변환해야 한다.

이 역할도 보통 Orchestrator가 담당한다.

결과 정리 단계

보통 구조는 이렇게 된다.

Tools
 ↓
Raw results
 ↓
Result Aggregator
 ↓
LLM response generation

예를 들어

flight API
hotel API
restaurant API

결과를 모아서

LLM에게 이렇게 전달한다.

Flight results: ...
Hotel results: ...
Restaurant results: ...

Create a travel itinerary.

LLM이 최종 사용자 응답을 생성한다.

실제 서비스에서는 UI 업데이트가 따로 존재한다

Agent 시스템을 실제 제품에 붙이면
또 하나의 단계가 추가된다. UI 업데이트 단계다.

예를 들어 Chat UI라면 이런 흐름이 된다.

User message
 ↓
Agent processing
 ↓
Tool execution
 ↓
Intermediate events
 ↓
UI streaming

예를 들어 사용자가 화면에서 보게 되는 것은 이런 흐름이다.

AI is searching flights...
AI is checking hotels...
AI is generating itinerary...

이런 메시지는 보통 Orchestrator가 이벤트 형태로 전달한다.

event: TOOL_START
event: TOOL_RESULT
event: AGENT_STEP
event: FINAL_RESPONSE

이 이벤트를 WebSocket이나 SSE로 프론트엔드에 전달한다.

실제 사용자가보는 화면은 아래와 같을 것이다.

그래서 백엔드에서 돌아가는 프로세스

User
 ↓
Frontend (React / Next.js)
 ↓
API Gateway
 ↓
Agent Service
 ↓
Orchestrator
 ↓
Tools
 ↓
External APIs

실제 UI에서 유저들이 보는 화면

User:
출장 일정 만들어줘

AI:
✈️ 항공편 검색 중...

AI:
🏨 호텔 검색 중...

AI:
일정을 생성했습니다.

즉 Agent 시스템은 단순히

LLM → Tool 이 아니라 Event 기반 시스템이 된다.

그래서 Orchestrator가 중요한 이유

결국 Agent 시스템에서 가장 중요한 컴포넌트는 LLM이 아니다.

Orchestrator다.

LLM은 생각을 하지만

Orchestrator는 실행 흐름 관리 Tool 스케줄, 병렬 처리, 상태 관리 , UI 이벤트 전달

을 모두 담당한다.

그래서 실제 제품을 만들다 보면 이런 결론에 도달한다.

Agent 시스템은 사실
LLM 서비스가 아니라 Workflow Engine이다.

그러면 Tool이 많아질수록 Agent는 더 똑똑해질까?

처음 Agent를 만들 때 가장 먼저 하는 일은 Tool을 붙이는 것이다.

예를 들어 여행 Agent를 만든다고 하자.

대충 이런 Tool들이 생긴다.

search_flight()
search_hotel()
search_restaurant()
create_calendar_event()
send_email()
get_weather()

사용자가 이렇게 말한다고 해보자.

“다음 주 도쿄 출장 일정 만들어줘”

개발자의 기대는 이렇다.

1. 항공편 검색
2. 호텔 검색
3. 일정 생성
4. 캘린더 등록

하지만 실제로는 이런 일이 벌어지기도 한다.

LLM output

{
 "tool": "send_email",
 "arguments": {
   "to": "user",
   "content": "Your trip plan is ready"
 }
}

아직 아무것도 안 했는데 이메일부터 보내버린다. 이유는 간단하다.

LLM은 논리적으로 계획을 세우는 시스템이 아니라
확률적으로 다음 토큰을 생성하는 모델이기 때문이다.

즉 Agent는 항상 조금 이상하게 행동할 가능성을 가지고 있다.

생각과는 다르게 Tool 선택은 생각보다 어렵다

Agent 시스템에서 실제로 가장 어려운 부분은 Tool 선택이다.

예를 들어 Tool이 이렇게 있다고 해보자.

search_flight()
search_hotel()
search_trip()
search_travel_plan()

사람이 보면 차이가 명확하다.

하지만 LLM 입장에서는 설명이 비슷하면 구분이 어렵다.

그래서 이런 일이 발생한다.

사용자 요청

“서울에서 도쿄 가는 비행기 찾아줘”

LLM 선택

{
 "tool": "search_trip"
}

그런데 search_trip은 패키지 여행 검색 API였다.

이런 일이 꽤 자주 발생한다.

나쁜 예

search_trip
search_travel
search_flight
search_transport

좋은 예

flight_search
hotel_search
restaurant_search

Tool 이름조차도 LLM이 이해하기 쉽게 설계해야 한다.

Agent는 같은 행동을 반복한다

Agent를 처음 실행하면 꽤 자주 보는 장면이 있다.

Agent 로그가 이렇게 찍힌다.

Step 1
search_hotel

Step 2
search_hotel

Step 3
search_hotel

계속 같은 행동을 한다. 왜 이런 일이 생길까?

Agent의 기본 구조는 보통 이런 loop다.

plan
action
observation
plan

문제는 observation이 충분히 명확하지 않으면
LLM이 이렇게 판단한다. “아직 목표를 달성하지 못했다.”

그래서 같은 행동을 반복한다.

예를 들어

호텔 검색 → 결과 없음

Agent 생각

아직 호텔 못 찾음
→ 다시 호텔 검색

이 loop가 계속 반복된다. 그래서 실제 서비스에서는 반드시 이런 제한을 둔다.

max_steps = 10
timeout = 30s

Agent가 똑똑해서가 아니라 폭주를 막기 위해서다.

존재하지 않는 Tool을 호출하기도 한다

이건 처음 보면 꽤 당황스러운 문제다.

Tool 목록이 이렇게 있다고 하자.

search_flight
search_hotel
create_calendar_event

그런데 Agent 로그에 이런 것이 찍힌다.

{
 "tool": "book_flight"
}

문제는 book_flight라는 Tool은 존재하지 않는다.

LLM이 “있을 것 같은 API”를 만들어낸 것이다.

이 현상은 사실 hallucination의 한 형태다. LLM은 없는 것도 자연스럽게 만들어낸다.

그래서 실제 시스템에서는 보통 이런 레이어가 추가된다.

LLM
 ↓
Tool Validator
 ↓
Tool Execution

비용이 예상보다 훨씬 커진다

Agent 시스템을 처음 만들면 대부분 이런 구조를 생각한다.

User
 ↓
LLM
 ↓
Tool

하지만 실제로는 이렇게 된다.

LLM (plan)
Tool
LLM (analysis)
Tool
LLM (evaluation)

한 작업에 LLM 호출이 여러 번 들어간다.

예를 들어 여행 일정 생성 하나가

LLM 호출 6회
API 호출 5회

이런 구조가 된다.

그래서 Agent 시스템은 생각보다 비용이 빨리 증가한다.

이 때문에 실제 서비스에서는

  • - step limit

  • - cost limit

  • - caching

같은 장치가 들어간다.

그래서 실제 Agent 시스템은 이렇게 바뀐다

이 문제들을 겪다 보면 대부분 이런 결론에 도달한다.

Agent를 완전히 자유롭게 두면 시스템이 불안정해진다.

그래서 실제 서비스에서는 보통 이렇게 만든다.

Planner
↓
Task Queue
↓
Executor
↓
Tool Validation
↓
Guardrails

완전히 자율적인 Agent가 아니라 통제된 Agent 시스템이다.

그런데 최근 조금 다른 접근이 등장하고 있다

지금까지의 Agent 시스템은 대부분 이런 구조였다.

LLM
→ Tool 호출

하지만 최근 등장한 시스템들은 컴퓨터 자체를 조작하는 Agent를 만들기 시작했다.

예를 들어

  • 1. 브라우저를 직접 조작하고

  • 2. 파일을 생성하고

  • 3. 스크립트를 실행한다.

대표적인 사례가 OpenClaw 같은 시스템이다.

이 구조는 기존 Agent보다 훨씬 강력하지만 동시에 훨씬 위험하기도 하다.

이 이야기는 다음 글에서 조금 더 자세히 다뤄보려고 한다.