들어가며: 에이전트, 정말 필요한가?
LLM 애플리케이션을 만들 때, 대부분의 개발자들이 가장 먼저 떠올리는 것은 '강력한 에이전트'입니다. 하지만 막상 어떤 프레임워크를 쓸지 고민하다 보면 시간이 훅 지나가버리죠. CrewAI, LangGraph, Microsoft Agent Framework... 선택지가 너무 많아서 오히려 코드 작성은 시작도 못 하는 경우가 많습니다.
하지만 잠깐, 정말 에이전트 프레임워크가 필요한가요? 저는 다양한 도메인에서 LLM 애플리케이션을 만들어오면서 중요한 교훈을 얻었습니다. 많은 유용한 LLM 앱에서 실제로 안정적으로 동작하는 것은 자율적인 에이전트가 아니라 **워크플로우(Workflow)**라는 점입니다. 그리고 워크플로우를 만들 때는 프레임워크조차 필요 없을 수 있습니다.
이 글에서는 순수 파이썬과 로컬 함수, 구조화된 출력, OpenAI Responses API를 사용하여 LLM 워크플로우를 프로토타이핑하는 방법을 보여드리겠습니다. 이상치 탐지 문제를 실전 예제로 다루면서, 왜 워크플로우가 먼저 고려해야 할 추상화인지 설명합니다.
핵심 주장: 에이전트는 유연하지만, 많은 실무 문제는 고정된 절차로 해결 가능합니다. 워크플로우는 투명성과 안정성을 제공하며, 프레임워크 없이도 충분히 구현할 수 있습니다.

LLM 워크플로우 설계의 4가지 핵심 요소
1. 제어 흐름(Control Flow)
워크플로우의 뼈대입니다. 입력부터 출력까지의 이동 경로를 그래프로 표현합니다. 각 노드는 코드 기반 처리 또는 LLM 호출이고, 엣지는 정보 전달 방식(정적/조건부)을 결정합니다. 핵심은 코드가 그래프를 소유하고, LLM은 특정 노드에 국한된다는 점입니다.
2. 역할 지시(Role Instructions)
각 LLM 노드에 어떤 역할을 부여할지 정의합니다. 시스템 프롬프트에 페르소나, 수행 작업, 주의사항, 도메인 규칙을 명시합니다.
3. 프롬프트 빌더(Prompt Builders)
LLM이 현재 워크플로우 상태에서 필요한 컨텍스트를 조립하는 함수입니다. 동적 값을 받아 프롬프트 템플릿에 주입하고, 최종 프롬프트를 생성합니다. 컨텍스트 윈도우를 제어할 수 있는 핵심 지점입니다.
4. 구조화된 출력(Structured Output)
LLM 출력을 미리 정의된 스키마(JSON 또는 Pydantic 모델)로 강제합니다. 이는 LLM 단계 간의 계약(Contract)으로, 다운스트림 파싱을 안정적으로 만듭니다.
실전: 순수 파이썬으로 데이터 품질 조사 워크플로우 만들기
Iris 데이터셋을 사용해 이상치를 탐지하고, LLM으로 원인을 진단하는 워크플로우를 구축해보겠습니다. 총 3단계로 구성됩니다.
Step 1: LLM 호출 헬퍼 작성
import os
import time
from pydantic import BaseModel
from openai import AzureOpenAI
client = AzureOpenAI(
api_key=os.environ["OPENAI_API_KEY"],
azure_endpoint=os.environ["OPENAI_API_BASE"],
api_version=os.environ["OPENAI_API_VERSION"],
)
llm_telemetry = [] # 성능 측정용
def call_llm(
step_name: str,
instructions: str,
prompt: str,
output_schema: type[BaseModel],
) -> BaseModel:
"""LLM을 호출하고 구조화된 출력을 반환"""
model = "gpt-5.4-mini"
started = time.perf_counter()
response = client.responses.parse(
model=model,
instructions=instructions,
input=prompt,
reasoning={"effort": "medium"},
text_format=output_schema,
)
usage = response.usage.model_dump() if response.usage else {}
llm_telemetry.append(
{
"step": step_name,
"schema": output_schema.__name__,
"model": model,
"prompt_chars": len(prompt),
"elapsed_s": round(time.perf_counter() - started, 2),
"input_tokens": usage.get("input_tokens"),
"output_tokens": usage.get("output_tokens"),
"total_tokens": usage.get("total_tokens"),
}
)
return response.output_parsed
Step 2: 이상치 스크리닝 (코드 기반)
import pandas as pd
from scipy import stats
def screen_suspicious_samples(df: pd.DataFrame, species_col: str, features: list[str]) -> pd.DataFrame:
"""종별 특성의 z-score를 계산하여 이상치 후보를 찾음"""
anomaly_scores = []
for _, row in df.iterrows():
species = row[species_col]
subset = df[df[species_col] == species]
z_scores = {}
for feature in features:
mean = subset[feature].mean()
std = subset[feature].std()
z_scores[feature] = abs((row[feature] - mean) / std) if std > 0 else 0
anomaly_scores.append(max(z_scores.values()))
df = df.copy()
df["anomaly_score"] = anomaly_scores
return df.sort_values("anomaly_score", ascending=False)
Step 3: LLM 조사자와 설명자
from typing import Literal
from pydantic import BaseModel, Field
class InvestigationDecision(BaseModel):
status: Literal["need_more_evidence", "enough_evidence"] = Field(..., description="더 많은 증거가 필요한지 여부")
reasoning: str = Field(..., description="증거 기반의 간단한 근거")
tool_name: Literal["compare_row_to_species_profile", "find_nearest_neighbors"] | None = Field(None, description="호출할 로컬 함수 이름")
species: Literal["setosa", "versicolor", "virginica"] | None = Field(None, description="비교할 종 프로필")
k: int | None = Field(None, description="최근접 이웃 수")
# 조사 루프
for round_id in range(1, MAX_ROUNDS + 1):
decision = call_llm(
step_name=f"investigator_row_{row_id}_round_{round_id}",
output_schema=InvestigationDecision,
instructions=INVESTIGATOR_INSTRUCTIONS,
prompt=build_investigation_prompt(flagged_row, collected_evidence),
)
if decision.status == "enough_evidence":
break
tool_result = execute_tool_call(row_id, decision)
collected_evidence.append(tool_result)

주의사항 및 비판적 시각
워크플로우의 한계
이 접근법은 문제가 명확하고 절차가 어느 정도 정해져 있을 때 강력합니다. 하지만 다음과 같은 한계가 있습니다:
- 유연성 부족: 문제가 개방적이거나 예상치 못한 상황이 많다면, 워크플로우는 오히려 제약이 됩니다.
- 유지보수 비용: 분기와 조건이 많아지면 오히려 복잡도가 증가할 수 있습니다. 이 경우 에이전트가 더 나은 선택일 수 있습니다.
- 도메인 지식 의존: 워크플로우 설계는 도메인에 대한 깊은 이해가 필요합니다. 초보자에게는 진입 장벽이 될 수 있습니다.
프레임워크의 가치
프레임워크는 프로덕션 환경에서 실패 처리, 관측성, 인간 개입, 지속성 등을 제공합니다. 하지만 아이디어 검증 단계에서는 오버엔지니어링일 수 있습니다. 워크플로우가 실제로 동작하는지 확인한 후, 필요할 때 프레임워크로 전환하는 것이 효율적입니다.
한국 개발 생태계에서의 적용 맥락
국내 IT 환경에서는 특히 SI 프로젝트에서 레거시 시스템과의 통합이 중요합니다. 워크플로우 기반 접근법은 기존 코드베이스에 LLM을 자연스럽게 통합할 수 있어 좋습니다. 또한, 금융권에서는 설명 가능성이 중요한데, 워크플로우는 각 단계의 입출력이 명확하여 감사(Audit)에 유리합니다. 다만, 국내에서는 LLM API 비용 문제가 있을 수 있으므로, 사내 LLM 또는 오픈소스 모델과 함께 사용하는 방안을 고려해야 합니다.

결론: 단순하게 시작하고, 필요할 때 복잡도를 더하라
많은 유용한 LLM 애플리케이션에서 필요한 것은 자율 에이전트나 무거운 프레임워크가 아닙니다. 명확한 워크플로우와 제어 흐름, 역할 지시, 프롬프트 빌더, 구조화된 출력, 그리고 이를 연결하는 순수 파이썬 코드입니다.
손으로 작성한 워크플로우는 나중에 에이전트나 프레임워크로 전환할 때도 그대로 활용할 수 있습니다. 제어 흐름, 프롬프트, 스키마, 도구 정의는 모두 재사용 가능합니다.
다음 단계 학습 방향:
- OpenAI Responses API의 다양한 기능(스트리밍, 함수 호출)을 더 깊이 학습해보세요.
- LangGraph와 같은 프레임워크의 핵심 개념을 이해하고, 언제 전환할지 판단 기준을 세워보세요.
- 실제 프로젝트에 워크플로우를 적용하고, 관측성(Observability)을 위한 로깅을 추가해보세요.
원문 참고: 이 글은 Towards Data Science의 원문을 기반으로, 한국 개발자 관점에서 재구성했습니다.