직접 실행하는 AI Agent 개발 가이드(샘플소스) - 02
중년개발자
@loxo
약 4시간 전
직접 실행하는 AI Agent 개발 가이드
이 문서는 Client → Agent Server → LiteLLM → vLLM → LLM 구조를 실제로 실행할 수 있는 최소 예제로 설명합니다. 주문번호를 조회하는 작은 Agent를 통해 모델 호출, 도구 호출, 안전한 서버 통제의 원리를 살펴봅니다.
1. 전체 구조와 역할
| 구성 요소 | 역할 | 이 예제의 구현 |
|---|---|---|
| Client | 질문을 보내고 답을 보여주는 사용자 화면 | app.py의 간단한 HTML |
| Agent Server | 규칙·권한·실행 순서와 도구 실행을 통제 | FastAPI와 run_agent() |
| LiteLLM | 서로 다른 모델 서버를 공통 API로 호출 | litellm.acompletion() |
| vLLM | GPU에서 오픈소스 LLM을 빠르게 실행 | Docker 컨테이너 |
| LLM | 언어를 이해하고 답 또는 도구 호출을 생성 | Qwen/Qwen2.5-3B-Instruct |
| Tool | 실제 업무 데이터를 조회하거나 행동 수행 | get_order() |
2. 핵심 용어
AI Agent
AI Agent는 단순히 문장을 만드는 챗봇보다 넓은 시스템입니다. 사용자 요청을 이해하고, 필요하면 도구를 고르고 실행하며, 그 결과를 바탕으로 답변합니다. 이 예제에서는 “A100 주문 상태 알려줘”라는 요청을 받으면 get_order 도구를 사용해 주문 정보를 조회합니다.
AI Serving과 vLLM
AI Serving은 학습된 모델을 실제 서비스가 호출할 수 있도록 GPU 서버에서 실행하는 일입니다. vLLM은 오픈소스 LLM을 효율적으로 서빙하고 OpenAI 호환 API로 제공하는 도구입니다. 여러 사람이 동시에 요청해도 GPU를 효율적으로 쓰도록 돕습니다.
LiteLLM
LiteLLM은 모델 연결을 표준화하는 중간 계층입니다. Agent 코드에서는 같은 호출 방식으로 vLLM, OpenAI 호환 서버, 또는 다른 모델 제공자를 사용할 수 있습니다. 모델 교체나 장애 시 대체 모델 연결도 한곳에서 관리하기 쉬워집니다.
Hugging Face
Hugging Face는 AI 모델과 데이터셋을 공유·다운로드하는 플랫폼입니다. AI 분야의 GitHub와 앱스토어를 합친 곳처럼 이해하면 됩니다. vLLM은 여기의 Qwen/Qwen2.5-3B-Instruct 같은 모델을 내려받아 실행합니다. 일부 제한 모델은 다운로드 전 약관 동의와 HUGGING_FACE_HUB_TOKEN이 필요합니다. 이 토큰은 비밀값이므로 코드나 Git 저장소에 넣지 말고 서버 환경 변수로만 관리해야 합니다.
3. 요청 처리 순서
- 사용자가 Client에서 질문을 입력합니다.
- Client가 FastAPI의
/chatAPI에 메시지를 보냅니다. - Agent Server가 시스템 규칙, 사용자 질문, 허용된 도구 정의를 묶습니다.
- LiteLLM이 vLLM의 OpenAI 호환 API로 모델 요청을 보냅니다.
- LLM은 바로 답하거나
get_order도구 호출을 요청합니다. - Agent Server는 허용된 도구인지, 인자가 올바른지 확인한 뒤 도구를 실행합니다.
- 도구 결과를 다시 LLM에 전달하고, 자연어 최종 답변을 받습니다.
- Client가 최종 답변을 사용자에게 표시합니다.
중요한 원칙은 LLM이 직접 DB를 수정하거나 임의 명령을 실행하지 않게 하는 것입니다. 모델은 “어떤 도구가 필요하다”는 요청만 만들고, 실제 실행과 검증은 Agent Server가 담당합니다.
4. 프로젝트 파일 구성
ai-agent-starter/
├── app.py # Client + FastAPI Agent Server + 도구 실행
├── requirements.txt # Python 라이브러리
├── .env.example # 환경 변수 예시
└── docker-compose.vllm.yml # vLLM GPU 서버 실행 설정5. 가장 빠른 실행: 모델 없이 흐름 확인
아래 코드는 기본적으로 DEMO_MODE=true입니다. GPU나 LLM이 없어도 웹 화면, Agent API, 주문 도구 실행 흐름을 바로 확인할 수 있습니다. 이 모드는 실제 AI 추론이 아니라 테스트용 규칙 기반 응답입니다.
cd /Users/loxo/Documents/Codex/2026-08-05/a/outputs/ai-agent-starter
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
uvicorn app:app --reload --port 8080브라우저에서 http://localhost:8080을 열고 A100 주문 상태 알려줘 또는 B200 주문 상태 알려줘를 입력합니다.
6. 실제 vLLM 연결
vLLM은 일반적으로 NVIDIA GPU가 장착된 Linux 서버에서 운영합니다. Apple Silicon Mac에서는 vLLM 서버를 GPU Linux 서버에 두고, Mac의 Agent Server가 그 주소를 호출하도록 구성하는 편이 일반적입니다.
GPU Linux 서버에서 실행합니다.
export HUGGING_FACE_HUB_TOKEN=hf_...
docker compose -f docker-compose.vllm.yml up그 후 .env에서 아래처럼 데모 모드를 끕니다. Agent Server와 vLLM이 같은 서버에 없다면 LLM_API_BASE를 원격 GPU 서버 주소로 바꿉니다.
DEMO_MODE=false
LLM_API_BASE=http://localhost:8000/v1
LLM_MODEL=Qwen/Qwen2.5-3B-Instruct
LLM_API_KEY=local-token7. 전체 소스 코드
requirements.txt
fastapi>=0.115,<1.0
uvicorn[standard]>=0.30,<1.0
litellm>=1.60,<2.0
python-dotenv>=1.0,<2.0.env.example
# Application server port
APP_PORT=8080
# LiteLLM sends requests to any OpenAI-compatible model server.
# For vLLM, use http://localhost:8000/v1
LLM_API_BASE=http://localhost:8000/v1
LLM_MODEL=Qwen/Qwen2.5-3B-Instruct
LLM_API_KEY=local-token
# Set true to run the entire UI/API flow without a model server.
DEMO_MODE=truedocker-compose.vllm.yml
services:
vllm:
image: vllm/vllm-openai:latest
ports:
- "8000:8000"
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
environment:
- HUGGING_FACE_HUB_TOKEN=${HUGGING_FACE_HUB_TOKEN}
command: --model Qwen/Qwen2.5-3B-Instruct --dtype auto --max-model-len 4096 --enable-auto-tool-choice --tool-call-parser hermes
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]app.py
"""A minimal AI Agent: browser -> FastAPI -> LiteLLM -> vLLM -> tool."""
import json
import os
from typing import Any
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, Field
load_dotenv()
# ================================================================
# [Agent Server] FastAPI 애플리케이션과 요청/응답 계약
# 역할: 사용자 요청을 받고, Agent 실행과 도구 사용을 통제합니다.
# ================================================================
app = FastAPI(title="Minimal AI Agent")
DEMO_MODE = os.getenv("DEMO_MODE", "false").lower() == "true"
# ================================================================
# [LLM] 사용할 언어 모델의 이름과 접속 정보
# 역할: 질문을 이해하고, 답변 또는 도구 호출 요청을 생성합니다.
# 예: Qwen/Qwen2.5-3B-Instruct
# ================================================================
MODEL = os.getenv("LLM_MODEL", "Qwen/Qwen2.5-3B-Instruct")
# ================================================================
# [vLLM] GPU에서 LLM을 실행하고 OpenAI 호환 API를 제공하는 AI Serving 계층
# 역할: 아래 API_BASE가 가리키는 서버입니다. 실행 설정은 docker-compose.vllm.yml에 있습니다.
# ================================================================
API_BASE = os.getenv("LLM_API_BASE", "http://localhost:8000/v1")
API_KEY = os.getenv("LLM_API_KEY", "local-token")
class ChatRequest(BaseModel):
message: str = Field(min_length=1, max_length=4000)
# ================================================================
# [Tool] 업무 데이터를 읽거나 행동을 수행하는 안전한 서버 기능
# 역할: LLM이 요청한 실제 업무 작업을 실행합니다.
# 실제 서비스에서는 DB/API를 호출하며 권한 검사가 필요합니다.
# ================================================================
# 실제 서비스에서는 파라미터화된 DB 또는 업무 API 호출로 교체합니다.
ORDERS = {
"A100": {"customer": "Kim", "status": "배송 중", "amount_krw": 35000},
"B200": {"customer": "Lee", "status": "배송 완료", "amount_krw": 78000},
}
def get_order(order_id: str) -> dict[str, Any]:
"""허용된 도구입니다. 사용자 입력으로 임의 SQL이나 명령을 실행하지 않습니다."""
result = ORDERS.get(order_id.upper())
return result or {"error": f"주문 {order_id}를 찾을 수 없습니다."}
TOOLS = [{
"type": "function",
"function": {
"name": "get_order",
"description": "주문 번호로 주문 상태와 금액을 조회합니다.",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string", "description": "예: A100"}},
"required": ["order_id"],
"additionalProperties": False,
},
},
}]
SYSTEM = """당신은 주문 지원 AI Agent입니다. 한국어로 짧고 정확하게 답하세요.
주문 상태가 필요한 경우 반드시 get_order 도구를 호출하세요. 도구 결과에 없는 사실을 만들지 마세요."""
# ================================================================
# [LiteLLM] 모델 서버 호출을 하나의 공통 API로 통합하는 계층
# 역할: vLLM, 외부 API 등 어떤 OpenAI 호환 모델 서버에도 같은 방식으로 요청합니다.
# ================================================================
async def call_model(messages: list[dict[str, Any]], tools: list[dict[str, Any]] | None = None):
"""LiteLLM이 vLLM 등 OpenAI 호환 모델 서버 호출을 표준화합니다."""
from litellm import acompletion
request: dict[str, Any] = {
"model": f"openai/{MODEL}",
"api_base": API_BASE,
"api_key": API_KEY,
"messages": messages,
"temperature": 0.2,
}
if tools:
request["tools"] = tools
request["tool_choice"] = "auto"
return await acompletion(**request)
def demo_answer(message: str) -> str:
"""GPU 없이 실행하기 위한 데모 응답이며, 실제 LLM은 아닙니다."""
for order_id in ORDERS:
if order_id.lower() in message.lower():
order = get_order(order_id)
return f"데모 응답: 주문 {order_id}는 {order['status']}이며 금액은 {order['amount_krw']:,}원입니다."
return "데모 응답입니다. `A100 주문 상태` 또는 `B200 주문 상태`를 입력해 보세요."
# ================================================================
# [Agent Server] Agent 실행 오케스트레이션
# 역할: LLM 호출 → 도구 요청 검증 → Tool 실행 → 최종 답변 생성을 순서대로 조정합니다.
# ================================================================
async def run_agent(user_message: str) -> str:
if DEMO_MODE:
return demo_answer(user_message)
messages: list[dict[str, Any]] = [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": user_message},
]
first = await call_model(messages, TOOLS)
assistant_message = first.choices[0].message
messages.append(assistant_message.model_dump(exclude_none=True))
# 모델이 고른 도구를 검증한 뒤, 서버가 실제 실행합니다.
for tool_call in assistant_message.tool_calls or []:
if tool_call.function.name != "get_order":
raise HTTPException(status_code=400, detail="허용되지 않은 도구 요청입니다.")
try:
args = json.loads(tool_call.function.arguments)
result = get_order(str(args["order_id"]))
except (KeyError, json.JSONDecodeError) as error:
raise HTTPException(status_code=400, detail="도구 인자가 올바르지 않습니다.") from error
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": "get_order",
"content": json.dumps(result, ensure_ascii=False),
})
if not assistant_message.tool_calls:
return assistant_message.content or "응답을 생성하지 못했습니다."
final = await call_model(messages)
return final.choices[0].message.content or "응답을 생성하지 못했습니다."
@app.get("/health")
async def health():
return {"ok": True, "demo_mode": DEMO_MODE, "model": MODEL, "api_base": API_BASE}
@app.post("/chat")
async def chat(request: ChatRequest):
return {"answer": await run_agent(request.message)}
# ================================================================
# [Client] 사용자가 질문을 입력하고 답변을 확인하는 웹 화면
# 역할: /chat API로 질문을 전송하고, 받은 답변을 화면에 보여줍니다.
# ================================================================
@app.get("/", response_class=HTMLResponse)
async def home():
return """<!doctype html><html lang='ko'><meta charset='utf-8'>
<title>Minimal AI Agent</title><style>body{font:16px system-ui;max-width:720px;margin:48px auto;padding:0 16px}input,button{font:inherit;padding:10px}input{width:68%}#result{white-space:pre-wrap;padding:16px;background:#f4f4f5;border-radius:8px;margin-top:16px}</style>
<h1>주문 지원 AI Agent</h1><p>A100 또는 B200의 주문 상태를 물어보세요.</p>
<input id='message' value='A100 주문 상태 알려줘'><button onclick='send()'>보내기</button><div id='result'>준비되었습니다.</div>
<script>async function send(){const message=document.querySelector('#message').value;const r=await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message})});const d=await r.json();document.querySelector('#result').textContent=d.answer||d.detail||'오류';}</script></html>"""8. 코드가 동작하는 원리
도구 정의: TOOLS
TOOLS는 모델이 사용할 수 있는 기능의 계약서입니다. 도구 이름, 설명, 받을 수 있는 입력 형식을 JSON Schema로 정의합니다. 모델은 이 정의를 보고 get_order({"order_id": "A100"}) 같은 호출을 요청합니다.
도구 실행: get_order()
모델의 요청을 그대로 실행하지 않습니다. run_agent()가 도구 이름이 허용 목록에 있는지 확인하고, JSON 인자를 검증한 뒤에만 get_order()를 실행합니다. 실제 서비스에서는 이 자리에 권한 검사와 파라미터화된 DB 쿼리 또는 사내 API 호출을 넣습니다.
모델 연결: call_model()
call_model()은 LiteLLM을 통한 유일한 모델 호출 창구입니다. 모델을 바꾸거나 timeout, retry, fallback, 토큰 사용량 기록을 추가할 때 이 함수에 집중하면 됩니다.
Agent 실행 루프: run_agent()
이 함수가 Agent의 핵심입니다. 모델에 질문과 도구 정의를 전달하고, 도구 요청을 받으면 검증 후 실행하고, 그 결과를 다시 모델에 보내 최종 자연어 답변을 얻습니다.
9. 운영 서비스로 확장할 때 추가할 기능
| 기능 | 구현 방향 |
|---|---|
| 인증·권한 | OAuth/JWT 검증 후 사용자별 도구와 문서 접근 권한 적용 |
| 대화 이력 | PostgreSQL에 사용자·대화 ID·메시지를 저장 |
| RAG | 문서 분할 → 임베딩 → Vector DB 검색 → 검색 문서를 prompt에 포함 |
| 스트리밍 | FastAPI StreamingResponse와 LiteLLM streaming 연결 |
| 안전성 | 입력 길이 제한, 도구별 JSON 검증, 민감정보 마스킹, 중요 작업 승인 |
| 관측성 | 요청 ID, 응답 시간, 도구 성공률, 토큰·비용을 로그와 대시보드에 기록 |
| 장애 대응 | LiteLLM의 retry/fallback으로 대체 모델 또는 서버로 자동 전환 |
10. 최종 설계 원칙
- Client에는 모델 API 키나 데이터베이스 권한을 두지 않습니다.
- Agent Server가 권한, 업무 규칙, 도구 호출의 최종 통제권을 가집니다.
- LiteLLM은 모델 제공자와 실행 서버를 바꿔도 Agent 코드를 안정적으로 유지하게 합니다.
- vLLM은 사내 또는 GPU 서버에서 오픈소스 모델을 빠르게 제공하는 AI Serving 계층입니다.
- LLM의 출력만 믿고 데이터 삭제, 결제, 메일 발송 같은 행동을 실행하지 않습니다. 서버 검증과 사용자 승인 단계를 둡니다.