Agent를 기본 아키텍처로 두면 설계가 쉽게 과해진다.
"요즘은 Agent가 대세니까 이 기능도 Agent로 만들자"
이 접근은 위험하다. Agent는 특정 문제를 해결하는 구조이지, 모든 LLM 앱의 상위 호환이 아니다.
좋은 기준은 이것이다.
답변만 필요한가?
외부 정보를 검색해야 하는가?
실행할 Tool이 필요한가?
Tool 결과가 다음 행동을 바꾸는가?
사람 승인과 상태 관리가 필요한가?
정해진 입력을 받아 정해진 방식으로 출력하면 chain이 낫다.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
llm = ChatOpenAI(model="gpt-5-nano", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", "기술 개념을 정의, 이유, 예시 순서로 설명하세요."),
("user", "{question}")
])
chain = prompt | llm | StrOutputParser()
이런 업무는 Agent가 필요 없다.
흐름이 고정되어 있으면 고정된 구조가 더 안정적이다.
자연어에서 필드를 뽑아 다음 시스템에 넘기는 문제는 Agent보다 structured output이 핵심일 수 있다.
from typing import Literal
from pydantic import BaseModel, Field
class TicketRequest(BaseModel):
equipment_id: str = Field(description="설비 ID")
symptom: str = Field(description="이상 증상")
priority: Literal["low", "medium", "high"]
requested_action: str
필요한 것이 "행동 선택"이 아니라 "안정적인 구조화"라면 굳이 Agent loop를 만들 필요가 없다.
user text -> structured extraction -> deterministic handler
이 구조가 더 테스트하기 쉽고, 실패 지점도 선명하다.
문서 기반 Q&A는 기본 RAG로 충분한 경우가 많다.
def rag_answer(question: str) -> str:
docs = retriever.invoke(question)
return answer_chain.invoke({
"question": question,
"context": format_docs(docs),
})
다음 조건이면 기본 RAG를 먼저 선택한다.
Agentic RAG는 검색 경로 선택, 재검색, fallback이 필요할 때 검토한다.
검색 품질이 낮은데 Agent부터 붙이면 원인을 가린다. 먼저 문서 파싱, chunking, embedding, retriever, 평가셋을 봐야 한다.
Tool이 있다고 항상 Agent는 아니다.
단발 Tool 호출은 tool-using chatbot으로 충분할 수 있다.
user asks weather
-> call weather_api once
-> answer
반면 Tool 결과가 다음 행동을 바꾸면 Agent 설계가 필요해진다.
user request
-> lookup order
-> if delivered: check return policy
-> if return allowed: ask approval
-> if approved: create return ticket
기준은 Tool 존재가 아니라 control loop다.
decide -> act -> observe -> decide next
Agent를 검토할 만한 조건은 아래와 같다.
한 번의 요청으로 끝나지 않고 중간 상태를 들고 다녀야 한다.
from typing import Literal, NotRequired, TypedDict
class AgentState(TypedDict):
request: str
extracted_fields: NotRequired[dict]
tool_result: NotRequired[dict]
approval_status: NotRequired[Literal["pending", "approved", "rejected"]]
retry_count: int
final_answer: NotRequired[str]
질문 유형이나 Tool 결과에 따라 다음 경로가 달라진다.
def route(state: AgentState) -> str:
if state.get("approval_status") == "pending":
return "wait_for_human"
if state["retry_count"] > 2:
return "fallback"
if state.get("tool_result"):
return "generate"
return "call_tool"
실제 업무 변경으로 이어지는 Tool은 승인 대기가 필요할 수 있다.
최종 답변만으로는 원인을 알기 어렵다.
Agent에는 trace가 거의 필수다.

Supervisor -> Planner -> Researcher -> Reviewer -> Writer
이 구조가 항상 나쁜 것은 아니다. 하지만 역할 경계가 흐리면 hop만 늘어난다.
징후:
배송/환불/교환 분류
규칙이나 작은 classifier로 충분하면 Agent는 과하다.
문서가 잘못 파싱됐거나 chunking이 나쁘면 Agentic RAG가 해결해주지 않는다.
먼저 볼 것:
Tool이 외부 시스템을 변경한다면 권한, 승인, 로그가 필요하다.
LLM selected tool != safe to execute
| 문제 | 추천 구조 |
|---|---|
| 고정된 답변 생성 | Chain |
| 자연어를 JSON으로 추출 | Structured Output |
| 문서 기반 Q&A | RAG |
| 단발 외부 정보 조회 | Tool-using chatbot |
| Tool 결과에 따라 다음 행동 변경 | Agent |
| 승인, 재시도, fallback 필요 | LangGraph/HITL Agent |
| 역할 경계가 분명하고 병렬 이점 있음 | Multi-Agent |
1. chain으로 해결 가능한가?
2. structured output만 있으면 되는가?
3. fixed RAG pipeline으로 충분한가?
4. Tool 호출이 단발성인가?
5. Tool result가 다음 action을 바꾸는가?
6. state를 여러 turn 또는 node에서 공유해야 하는가?
7. retry/fallback/stop condition이 필요한가?
8. human approval이 필요한가?
9. trace와 테스트를 준비했는가?
10. 복잡도가 늘어나는 만큼 품질 개선 근거가 있는가?
Agent는 만능 패턴이 아니다.
Agent를 쓰면 유연성이 생기지만 동시에 비용, latency, test surface, observability requirement도 늘어난다.
단순한 문제는 단순하게 푸는 편이 낫다.
Chain -> Structured Output -> RAG -> Tool-using chatbot -> Agent -> Multi-Agent
이 순서로 필요한 만큼만 복잡도를 올리는 것이 실무적으로 더 안전하다.