Python AI를 실무 흐름으로 이해하기
Python으로 LLM 애플리케이션과 AI Agent를 구축합니다. 비동기 LLM 호출, Pydantic 구조화 출력, ReAct 루프, FastAPI 스트리밍 서버, Agent 테스트까지 실무 흐름으로 정리했습니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
Python으로 LLM 애플리케이션과 AI Agent를 구축합니다. 비동기 LLM 호출, Pydantic 구조화 출력, ReAct 루프, FastAPI 스트리밍 서버, Agent 테스트까지 실무 흐름으로 정리했습니다.
포함된 Learning Path
이 가이드는 아래 경로의 한 단계입니다. 앞뒤 순서와 함께 학습해보세요.
Python으로 LLM 애플리케이션과 AI Agent를 구축합니다. 비동기 LLM 호출, Pydantic 구조화 출력, ReAct 루프, FastAPI 스트리밍 서버, Agent 테스트까지 실무 흐름으로 정리했습니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
모델과 프롬프트만 보지 않고, 데이터 흐름, 평가, 배포 이후의 운영 지표까지 한 번에 연결해서 봅니다.
글로 읽은 내용을 머릿속에 오래 남기려면 먼저 흐름을 그림으로 잡는 편이 좋습니다. 아래 두 그림은 Python AI를 학습할 때 계속 되돌아볼 수 있는 기준 지도입니다.
Python AI를 처음 펼칠 때는 세부 명령보다 큰 그림이 먼저입니다. 이 섹션에서는 앞으로 배울 개념들이 어떤 문제를 풀기 위해 등장했는지부터 잡아봅니다.
| 레이어 | 라이브러리 | 역할 |
|---|---|---|
| 모델 실행 | transformers, vLLM, Ollama | LLM 로딩 · 추론 · 서빙 |
| Agent 오케스트레이션 | LangChain, LlamaIndex, smolagents | 도구 호출 · 메모리 · 라우팅 |
| 구조화 출력 | Pydantic v2, instructor, outlines | 스키마 검증 · JSON 강제 |
| 벡터 검색 | ChromaDB, Pinecone, pgvector | RAG 임베딩 저장소 |
| API 서버 | FastAPI, LangServe | 스트리밍 엔드포인트 |
| 평가 & 추적 | RAGAS, LangSmith, deepeval | 품질 측정 · 트레이싱 |
여기서는 환경 구성 (uv)을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
# uv 설치 (macOS/Linux)
curl -LsSf https://astral.sh/uv/install.sh | sh
# AI Agent 프로젝트 초기화
uv init my-agent && cd my-agent
# 핵심 패키지 추가 (pyproject.toml 자동 업데이트)
uv add langchain langchain-openai langchain-community
uv add pydantic fastapi uvicorn chromadb
uv add instructor ragas langsmith
# 재현 가능한 환경 — lock 파일로 정확한 버전 고정
uv run python main.py # venv 자동 활성화
uv sync # lock 파일 기준 동기화여기서는 비동기 LLM 호출을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import asyncio
from openai import AsyncOpenAI
client = AsyncOpenAI()
# ── 기본 비동기 호출 ──────────────────────────────
async def call_llm(prompt: str, model: str = "gpt-4o-mini") -> str:
resp = await client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
)
return resp.choices[0].message.content
# ── 스트리밍 (토큰 단위 실시간 출력) ─────────────
async def stream_llm(prompt: str):
async with client.chat.completions.stream(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
) as stream:
async for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
# ── 여러 프롬프트 동시 병렬 호출 ─────────────────
async def batch_call(prompts: list[str]) -> list[str]:
return await asyncio.gather(*[call_llm(p) for p in prompts])
async def main():
prompts = ["Python이란?", "Go란?", "Rust란?"]
# 직렬: 순차 실행 (~3초) vs 병렬: 동시 실행 (~1초)
serial = [await call_llm(p) for p in prompts]
parallel = await batch_call(prompts) # 약 3배 빠름
asyncio.run(main())여기서는 Pydantic v2 구조화 출력을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
uv add instructorimport instructor
from openai import OpenAI
from pydantic import BaseModel, Field
from enum import Enum
client = instructor.from_openai(OpenAI())
class Priority(str, Enum):
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
class Task(BaseModel):
title: str = Field(description="작업 제목")
priority: Priority = Field(description="우선순위")
deadline: str | None = Field(description="마감일 YYYY-MM-DD")
subtasks: list[str] = Field(default_factory=list)
class ProjectPlan(BaseModel):
goal: str = Field(description="프로젝트 목표")
tasks: list[Task] = Field(description="작업 목록")
total_weeks: int = Field(description="예상 소요 기간(주)")
# LLM이 자유 텍스트 대신 검증된 Pydantic 객체를 반환
plan = client.chat.completions.create(
model="gpt-4o",
response_model=ProjectPlan,
messages=[{"role": "user",
"content": "AI 챗봇 서비스를 3개월 안에 출시하는 계획을 짜줘"}]
)
for task in plan.tasks:
print(f"[{task.priority.upper()}] {task.title} — 마감: {task.deadline}")
for sub in task.subtasks:
print(f" • {sub}")여기서는 Agent 루프 직접 구현을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import json, math
from openai import OpenAI
from typing import Callable
client = OpenAI()
# ── 도구 정의 ─────────────────────────────────────
def search_web(query: str) -> str:
"""웹에서 최신 정보를 검색합니다."""
return f"[검색 결과] {query}: 관련 최신 정보..." # 실제: Tavily API
def calculate(expression: str) -> str:
"""수식을 안전하게 계산합니다."""
try:
allowed = {k: v for k, v in math.__dict__.items() if not k.startswith("_")}
return str(eval(expression, {"__builtins__": {}}, allowed))
except Exception as e:
return f"오류: {e}"
TOOLS: dict[str, Callable] = {"search_web": search_web, "calculate": calculate}
SCHEMAS = [
{"type": "function", "function": {
"name": "search_web", "description": "웹 검색",
"parameters": {"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"]}}},
{"type": "function", "function": {
"name": "calculate", "description": "수식 계산",
"parameters": {"type": "object",
"properties": {"expression": {"type": "string"}},
"required": ["expression"]}}},
]
# ── ReAct 루프 ────────────────────────────────────
def run_agent(query: str, max_steps: int = 5) -> str:
messages = [{"role": "user", "content": query}]
for _ in range(max_steps):
resp = client.chat.completions.create(
model="gpt-4o", messages=messages, tools=SCHEMAS
)
msg = resp.choices[0].message
messages.append(msg)
if not msg.tool_calls: # 도구 없음 → 최종 답변
return msg.content
for tc in msg.tool_calls: # 도구 실행
args = json.loads(tc.function.arguments)
result = TOOLS[tc.function.name](**args)
print(f" [Tool] {tc.function.name}({args}) → {result[:60]}")
messages.append({"role": "tool", "tool_call_id": tc.id,
"content": result})
return "최대 스텝 초과"
print(run_agent("삼성전자 현재 주가에 2.5를 곱하면?"))여기서는 동시성 & 배치 처리을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import asyncio
from openai import AsyncOpenAI
client = AsyncOpenAI()
async def summarize_one(sem: asyncio.Semaphore, doc: str, idx: int) -> dict:
async with sem: # 동시 최대 N개 호출 제한
try:
resp = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"요약:\n{doc}"}],
max_tokens=200,
)
return {"idx": idx, "summary": resp.choices[0].message.content,
"ok": True}
except Exception as e:
return {"idx": idx, "error": str(e), "ok": False}
async def batch_summarize(docs: list[str], concurrency: int = 10) -> list[dict]:
sem = asyncio.Semaphore(concurrency)
results = await asyncio.gather(
*[summarize_one(sem, doc, i) for i, doc in enumerate(docs)]
)
return sorted(results, key=lambda x: x["idx"])
# 1,000개 문서를 직렬 대비 ~10배 빠르게 처리
docs = [f"문서 {i} 내용..." for i in range(100)]
results = asyncio.run(batch_summarize(docs))
print(f"완료: {sum(r['ok'] for r in results)}, "
f"실패: {sum(not r['ok'] for r in results)}")여기서는 FastAPI Agent 서버을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from openai import AsyncOpenAI
import json
app = FastAPI(title="AI Agent API")
client = AsyncOpenAI()
class ChatRequest(BaseModel):
message: str
model: str = "gpt-4o"
session_id: str = "default"
# ── SSE 스트리밍 엔드포인트 ──────────────────────
@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
async def generate():
async with client.chat.completions.stream(
model=req.model,
messages=[{"role": "user", "content": req.message}],
) as stream:
async for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
yield f"data: {json.dumps({'token': delta})}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(generate(), media_type="text/event-stream",
headers={"Cache-Control": "no-cache"})
# ── 구조화 출력 엔드포인트 ────────────────────────
class AnalysisResult(BaseModel):
sentiment: str
key_points: list[str]
confidence: float
@app.post("/analyze", response_model=AnalysisResult)
async def analyze(req: ChatRequest):
import instructor
inst = instructor.from_openai(AsyncOpenAI(), mode=instructor.Mode.JSON)
return await inst.chat.completions.create(
model=req.model, response_model=AnalysisResult,
messages=[{"role": "user", "content": f"분석:\n{req.message}"}]
)
# 실행: uvicorn agent_server:app --reload --port 8000여기서는 Agent 테스트을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import pytest
from unittest.mock import patch, MagicMock
from pydantic import BaseModel, ValidationError
from agent_loop import calculate
# ── 도구 단위 테스트 ──────────────────────────────
def test_calculate_basic():
assert calculate("2 + 3") == "5"
assert calculate("10 ** 2") == "100"
assert float(calculate("math.sqrt(2)")) == pytest.approx(1.414, rel=1e-3)
def test_calculate_blocks_dangerous():
assert "오류" in calculate("__import__('os').system('ls')")
# ── LLM 호출 Mock 테스트 ──────────────────────────
def test_agent_calls_tool_then_answers():
tool_call = MagicMock()
tool_call.function.name = "calculate"
tool_call.function.arguments = '{"expression": "2 + 2"}'
tool_call.id = "call_1"
msg1 = MagicMock(); msg1.tool_calls = [tool_call]; msg1.content = None
msg2 = MagicMock(); msg2.tool_calls = None; msg2.content = "4입니다"
r1 = MagicMock(); r1.choices[0].message = msg1
r2 = MagicMock(); r2.choices[0].message = msg2
with patch("agent_loop.client.chat.completions.create",
side_effect=[r1, r2]):
from agent_loop import run_agent
assert "4" in run_agent("2 더하기 2는?")
# ── 구조화 출력 스키마 검증 ───────────────────────
class AgentOut(BaseModel):
answer: str
sources: list[str]
confidence: float
def test_valid_schema():
out = AgentOut(answer="결과", sources=["doc.pdf"], confidence=0.9)
assert 0.0 <= out.confidence <= 1.0
def test_rejects_out_of_range():
with pytest.raises(ValidationError):
AgentOut(answer="", sources=[], confidence=1.5)Python AI 실무 설계은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 결정 지점 | 확인 질문 | 실무 기준 |
|---|---|---|
| 경계 | Python AI 코드에서 바뀌기 쉬운 부분은 어디인가? | 입출력, 설정, 외부 연동, 핵심 규칙을 분리합니다. |
| 상태 | 상태가 어디서 생성되고 어디서 사라지는가? | 상태 소유자와 수명 주기를 코드로 드러냅니다. |
| 장애 | 실패했을 때 호출자는 무엇을 받는가? | timeout, fallback, error contract를 먼저 정합니다. |
이 섹션은 Python AI 운영 기준을 실무 관점에서 정리합니다. 개념을 외우기보다, 어떤 상황에서 이 기준을 꺼내 쓸지에 초점을 맞춰보세요.
Python AI 검증 전략은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 품질 축 | 검증 방법 | 완료 기준 |
|---|---|---|
| 정확성 | 정상/실패 케이스를 자동화합니다. | 핵심 시나리오가 재현 가능하게 통과합니다. |
| 회귀 방지 | 버그 수정 시 동일 케이스를 테스트로 남깁니다. | 같은 장애가 다시 배포되지 않습니다. |
| 운영성 | 로그, 메트릭, 알림을 확인합니다. | 문제가 생겼을 때 원인 추적 경로가 있습니다. |