본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 26. 12:39

에이전트 시리즈 (5): 의도 인식 (Intent Recognition) 및 라우팅 (Routing) — 에이전트가 사용자를 실제로 이해하게

요약

에이전트 시스템에서 사용자의 의도를 정확히 파악하고 적절한 전문 에이전트로 연결하는 의도 인식 및 라우팅 기술을 다룹니다. 기존 키워드 매칭 방식의 한계를 지적하며, LLM 분류기를 활용한 정교한 의도 계층 구축의 필요성을 설명합니다.

핵심 포인트

  • 의도 라우팅은 요청을 최적의 전문 에이전트에게 전달하는 핵심 계층임
  • 키워드 매칭은 유지보수 비용이 높고 자연어 대응력이 낮음
  • LLM 분류기를 통해 복잡한 사용자 의도를 정교하게 파악 가능
  • 안정적인 JSON 파싱을 위한 수동 추출 및 폴백 로직의 중요성

에이전트에게 의도 인식 (Intent Recognition)이 필요한 이유는 무엇인가요?

직관적인 접근 방식은 사용자의 입력을 LLM (Large Language Model)에 직접 전달하여 무엇을 할지 스스로 결정하게 하는 것입니다. 에이전트가 가진 도구(Tool)가 적고 사용 사례(Use case)가 단일하다면 이 방식은 잘 작동합니다.

하지만 에이전트가 검색 도구, 코드 도구, 계산기, 그리고 지식 베이스(Knowledge base)를 동시에 가지고 있을 때는 문제가 발생합니다. 동일한 LLM이라도 서로 다른 도구 세트와 시스템 프롬프트(System prompt)를 사용함에 따라, 동일한 작업에 대해 극적으로 다른 품질을 제공하게 됩니다.

전용 QA 에이전트는 지식 베이스를 사용하고, 출처를 인용하며, 개념을 깊이 있게 설명할 것입니다. 반면 동일한 질문을 받은 범용 에이전트는 빠른 웹 검색을 실행하고 짧은 스니펫(Snippet)만을 반환할 수도 있습니다. 의도 라우팅(Intent routing)은 각 요청을 이를 처리하기에 가장 적합한 에이전트에게 전달합니다.

이것이 바로 의도 계층(Intent layer)이 하는 역할입니다: 요청을 전문 에이전트에게 전달하기 전에 사용자가 실제로 무엇을 원하는지 결정하는 것입니다.

키워드 매칭 (Keyword Matching) vs. LLM 분류 (LLM Classification)

무언가를 구축하기 전에 실제 입력을 바탕으로 두 가지 접근 방식을 벤치마킹해 보겠습니다.

키워드 매칭의 문제점

전통적인 NLP (Natural Language Processing) 프로젝트에서의 고전적인 접근 방식입니다:

_KEYWORD_RULES = {
    "search":    ["search", "latest", "news", "find"],
    "code":      ["code", "write a", "function", "implement", "bug"],
...

실제 자연어 입력(중국어)을 대상으로 테스트한 결과입니다:

△ [명확한 의도 - 검색]
  Input:    "What's new in the latest version of LangChain?"
  Keyword → unknown         ← 키워드 일치 없음
...

키워드 매칭의 치명적인 결함: 사용자가 우연히 규칙 테이블에 있는 정확한 키워드를 사용할 때만 작동한다는 점입니다. "Has LangChain released a new version?" 또는 "implement a sort for me"와 같은 문장은 unknown을 생성합니다. 이 규칙 테이블을 유지 관리하는 것은 매우 비용이 많이 듭니다. 새로운 비즈니스 시나리오와 사용자가 표현하는 모든 새로운 방식마다 수동 업데이트가 필요하기 때문입니다.

LLM 분류기 (LLM Classifier)

키워드 규칙을 LLM 기반 분류기로 교체하고, JSON 출력을 수동으로 파싱(Parsing)합니다:

def llm_classify(text: str, history: list[str] | None = None) -> IntentResult:
    system_prompt = f"""당신은 의도 분류기 (Intent Classifier)입니다. 사용자 입력을 다음 5가지 의도 중 하나로 분류하세요:
  search    — 정보, 최신 뉴스, 현재 사건 검색
...

with_structured_output 대신 수동 JSON 파싱 (Manual JSON Parsing)을 사용하는 이유는 무엇인가요?

테스트 결과, GLM-4-Flash의 with_structured_output JSON 스키마 (JSON Schema) 모드는 때때로 일부 필드만 반환하여 Pydantic ValidationError를 발생시키곤 합니다. 정규 표현식 추출 (Regex Extraction)과 폴백 로직 (Fallback Logic)을 사용한 수동 파싱이 더 견고하며, 이러한 패턴은 프로덕션 시스템 (Production Systems)에서 흔히 사용됩니다.

LangGraph 의도 라우터 (Intent Router): 전문 에이전트 (Specialist Agents)로의 배정

의도가 식별되면, LangGraph의 조건부 엣지 (Conditional Edges)는 라우팅 (Routing)을 위한 자연스러운 메커니즘이 됩니다.

그래프 구조 (Graph Structure)

START
  │
  ▼
...

구현 (Implementation):

class RouterState(TypedDict):
    user_input:           str
    conversation_history: list[str]
...

각 전문 에이전트 (Specialist Agent)는 팩토리 함수 (Factory Function)를 통해 생성되며, 특정 도구 세트 (Tool Set) 및 시스템 프롬프트 (System Prompt)에 바인딩됩니다:

def _make_specialist_node(node_name: str, tools_list: list, system_text: str):
    specialist = create_react_agent(model=specialist_llm, tools=tools_list)

...

라우팅 결과 (Routing Results)

[Search]  "LangGraph의 최신 버전에서 새로워진 점은 무엇인가요?"
  [classify]  intent=search  confidence=80%
  [route]  → search_agent
...

신뢰도 임계값 (Confidence Thresholds) 및 명확화 (Clarification)

의도 분류 (Intent Classification)는 이진적 (Binary)이지 않습니다. "그냥 고쳐줘"와 같은 문구는 진정으로 모호합니다. **추측하지 말고, 질문하십시오 (Not guessing, just asking)**가 더 나은 전략입니다.

신뢰도 임계값 (Confidence Threshold)은 라우팅 동작을 제어합니다:

# 신뢰도(confidence) < 0.6 일 때 → LLM은 clarify 의도를 반환해야 함
# clarify_node: 도구 없이, 명확하게 하는 질문(Clarifying Question)만 생성

...

데모의 실제 결과:

[낮은 신뢰도 - 모호함]  "그냥 고쳐줘"
  confidence: 50%  intent: clarify
  Clarification: 무엇을 고쳐드릴까요?
...

반면, 신뢰도가 높은 명확한 입력의 경우:

반면, 신뢰도가 높은 명확한 입력의 경우:

[High confidence]  "What's 2 to the power of 10?"
  confidence: 100%  intent: calculate  → 즉시 계산됨

...

다중 턴 의도 추적 (Multi-Turn Intent Tracking): 컨텍스트가 모호한 지침을 명확하게 만듦

이것은 데모에서 가장 가치 있는 발견입니다.

시나리오 A: 코드 대화가 진행되다가 모호한 지침이 도착하는 경우:

# 기존 대화 기록:
User: "리스트의 평균을 계산하는 Python 함수를 작성해 줘"
Agent: "def average(lst): return sum(lst) / len(lst) if lst else 0.0"
...

구절 "just optimize it" (중국어 세 글자): 분류:

Input: "just optimize it"
  ✗ 기록 없음 → clarify (50%)   입력 불분명, 의도 파악 불가.
  ✓ 기록 있음 → code   (80%)  사용자가 대화 컨텍스트를 기반으로 코드 최적화를 요청함.

"Change comments to English": 분류:

Input: "change comments to English"
  ✗ 기록 없음 → search (50%)   (JSON 파싱 실패, 성능 저하)
  ✓ 기록 있음 → code (100%)  사용자가 코드 수정을 요청하며, 명확하게 코딩 작업임.

시나리오 B: 계산 계속 진행:

# 기록: 사용자가 "2의 10제곱은 무엇인가요?"라고 질문 → 에이전트가 "1024"로 답변함

Input: "now multiply that by 3"
...

구현: 마지막 4개의 대화 턴을 분류 프롬프트에 주입합니다:

history_section = "\n\n최근 대화 기록:\n" + "\n".join(f"  {h}" for h in history[-4:])

system_prompt = f"""당신은 의도 분류기입니다... 
...

실제 실행에서 흥미로운 발견들

5가지 데모를 모두 실행하면서 문서화할 가치가 있는 몇 가지 행동을 발견했습니다:

발견 1: JSON 파싱 실패가 잘못된 라우팅을 유발함

Demo 5 Turn 2에서, "it으로 가장 간단한 Hello World Agent를 작성하는 것을 도와줘"라는 명백한 코드 요청이 GLM-4-Flash가 이 입력에 대해 유효한 JSON 출력을 생성하지 못했기 때문에 clarify_agent로 라우팅되었습니다. 폴백 로직은 키워드도 찾지 못해 결국 clarify (30%)를 반환했습니다.

2회차: "그것을 사용하여 가장 단순한 Hello World 에이전트를 작성하도록 도와줘"
[classify] intent=clarify confidence=30%
reasoning: (LLM 출력을 파싱할 수 없음)
...

흥미롭게도, 명확화 질문(clarification question) 자체는 논리적으로 정확했습니다. 모델은 요청을 이해했지만, 단지 유효한 JSON을 생성하는 데 실패했을 뿐입니다.

발견 2: 잘못된 라우팅(Routing), 하지만 전문 에이전트(Specialist Agent)가 이를 구제함

"리스트의 평균을 계산하는 Python 함수를 작성해줘"라는 문장은 JSON 파싱 성능 저하로 인해 calculate (50%)로 잘못 분류되어 calculator_agent로 전송되었습니다. calculator_agent는 계산기 도구(calculator tool)만 가지고 있지만, 그럼에도 불구하고 LLM을 직접 사용하여 실제 함수로 답변했습니다:

[route]  → calculator_agent (code_agent였어야 함)
[answer] def calculate_average(numbers):
             if not numbers:
...

교훈: 잘못된 라우팅이 발생하더라도, 좋은 시스템 프롬프트(System Prompt)를 가진 전문 에이전트는 어느 정도의 결함 허용(Fault Tolerance) 능력을 갖추고 있습니다. 하지만 이에 의존하지 마십시오. 분류 계층(Classification Layer)에서 신뢰성을 수정해야 합니다.

발견 3: 모델의 언어 표류 (Model Language Drift)

사용자 입력이 중국어임에도 불구하고 일부 reasoning 출력이 영어로 나오는 경우가 있었습니다 ("The user is asking for a mathematical calculation."). GLM-4-Flash는 때때로 영어로 사고합니다. 이것이 기능에 영향을 미치지는 않지만, 만약 reasoning을 사용자에게 노출할 계획이라면 출력 언어를 정규화(Normalize)해야 합니다.

발견 4: 대화 기록(Conversation History)은 가장 큰 품질 승수(Quality Multiplier)임

전체 데모를 통틀어 가장 영향력 있는 개선 사항은 분류 프롬프트(Classification Prompt)에 대화 기록을 주입하는 것이었습니다. "그냥 최적화해줘"라는 요청에 대해 → 대화 기록이 전혀 없을 때 → 매번 clarify 발생; 코드 대화 기록이 있을 때 → code (80%) 발생. 실제 운영 시스템(Production Systems)에서 이는 다음을 의미합니다: 대화 기록은 단순히 LLM이 더 잘 대답하도록 돕는 것에 그치지 않고, 의도 라우팅(Intent Routing)의 정확도를 직접적으로 향상시킵니다.

운영 아키텍처: 3계층 분류 + OOD 거부 + 데이터 루프

위의 데모는 의도 분류 (Intent Classification)를 위해 대형 모델 (Large Model)을 직접 사용합니다. 이는 학습이나 프로토타이핑에는 괜찮지만, 실제 운영 시스템(Production Systems)이 작동하는 방식은 아닙니다.

실제 산업 현장의 의도 인식 (Intent Recognition)은 다음과 같은 3계층 깔때기 (Three-layer funnel) 구조를 사용합니다:

User Input
    │
    ▼
...

Layer 1: 규칙 기반 라우팅 (Rule Routing, <1ms)

첫 번째 계층은 오직 **의미론적으로 확실하고 고정된 표현 (Semantically certain, fixed-expression)**을 가진 명령만을 처리합니다. 의미론적 이해 (Semantic understanding) 없이, 단순히 문자열 매칭 (String matching) 또는 유한 상태 머신 (FSM) 전이를 사용합니다:

RULE_ROUTES = {
    r"^transfer to human$|^speak to agent$|^I want to complain$": "human_handoff",
    r"^open .+ app$|^launch .+": "app_launch",
...

장점: LLM 호출 없음, 1ms 미만의 지연 시간 (Latency), 완전히 예측 가능함, 로직의 감사 (Auditable) 가능.

사용 사례: 고객 서비스에서의 "상담원 연결 (transfer to human)", 스마트 어시스턴트의 단축 명령, 토글 (Toggle) 작업.

Layer 2: 미세 조정된 소형 언어 모델 (Fine-tuned Small Language Model, 10~50ms)

Layer 1을 통과한 요청은 **미세 조정된 소형 언어 모델 (Fine-tuned SLM)**로 전달됩니다. 일반적으로 5B에서 7B 파라미터 규모를 가집니다:

  • 모델 옵션: Qwen2.5-7B-Instruct, GLM-4-9B, Llama-3.1-8B 미세 조정 모델
  • 학습 데이터: 주석이 달린 운영 로그 (Annotated production logs) + 데이터 증강 (Data augmentation); 운영 수준의 정확도에 도달하기 위해 수천 개에서 수만 개의 샘플 필요
  • 배포 비용: 단일 A10 (24GB VRAM)으로 7B 모델을 서빙할 수 있으며, 배치 추론 (Batched inference) 시 100+ QPS 달성 가능
  • 정확도: 일상적인 의도의 90% 이상을 92~97%의 정확도로 커버
# 의사 코드 (Pseudocode): 로컬에 배포된 미세 조정된 SLM 호출
def slm_classify(text: str, history: list[str]) -> IntentResult:
    response = slm_client.chat(
...

핵심 지표: 평균 지연 시간 20~50ms, 대형 모델 대비 80% 이상의 비용 절감.

Layer 3: 대형 모델 폴백 (Large Model Fallback, 100~500ms)

SLM의 신뢰도 (Confidence)가 낮거나, 입력값이 분포 외 데이터 (Out-of-distribution, OOD)이거나, 요청이 복잡한 다중 의도 (Multi-intent) 시나리오를 포함하는 경우, **대형 모델 (Large Model)**로 에스컬레이션(Escalate)합니다:

  • 트리거 조건 (Trigger condition): SLM 신뢰도 (Confidence) < 0.6, 또는 입력값이 학습 분포 (Training distribution)를 벗어난 경우
  • 트래픽 점유율 (Traffic share): 일반적으로 요청의 5~10%만이 에스컬레이션(Escalate)됨
  • 비용 제어 (Cost control): 에스컬레이션 비율이 낮기 때문에 전체 비용은 관리 가능한 수준으로 유지됨

3계층 비교 (Three-layer comparison):

계층 (Layer)지연 시간 (Latency)비용 (Cost)트래픽 점유율 (Traffic Share)사용 사례 (Use Case)
규칙 라우팅 (Rule Routing)<1ms최소 (Minimal)~5%고정 명령, 빠른 동작
...

OOD 거부 (OOD Rejection): 범위를 벗어난 요청 필터링

OOD (Out-of-Distribution) 거부는 종종 간과되지만 매우 중요한 요소입니다. 이는 시스템의 서비스 범위(Service scope)를 벗어나는 요청을 식별하고 거부하는 역할을 합니다.

전형적인 시나리오: 사용자가 쇼핑 어시스턴트에게 "시를 써줘"라고 말하는 경우입니다. 이는 유효한 자연어이지만, 서비스 범위 내에 있지 않습니다. OOD 거부 기능이 없다면, 이 요청은 낮은 신뢰도로 분류되어 잘못된 경로로 라우팅될 수 있습니다.

# OOD 거부 접근 방식 (OOD rejection approaches)

# 접근 방식 A: 임베딩 유사도 임계값 (embedding similarity threshold)
...

거부 응답은 에러 메시지가 아니라 친절하고 안내하는 방식이어야 합니다:

"죄송합니다. 현재 저는 [XX 관련] 질문에 대해서만 도움을 드릴 수 있습니다.
요청하신 내용은 제 서비스 범위를 벗어납니다.
다음과 같은 방법을 시도해 보세요: [관련 제안] 또는 [상담원 연결]"

데이터-평가 루프 (Data-Eval Loop): 지속적인 개선

의도 인식 (Intent recognition) 시스템을 배포하는 것은 시작일 뿐입니다. 지속적인 개선은 데이터 플라이휠 (Data flywheel)에 달려 있습니다:

실시간 트래픽 (Live Traffic)
    │
    ▼
...

3가지 핵심 관행 (Three core practices):

① 일일 실패 사례 수정 (Daily bad case fixes): 운영 로그(불만 사항, 재시도, 상담원 에스컬레이션 등)에서 라우팅 오류를 추출하고, 이를 주석 처리(Annotate)하여...

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0