실무 Multi-Agent 오케스트레이션 24장 · 서비스 통합과 실행 2 / 9 ← 이전목차다음 → TechLead Cro

24장. 서비스 통합과 실행

FastAPI — 파이썬 함수를 주소로 만든다

한 줄 요약

FastAPI는 파이썬 함수를 웹 주소에 연결해 브라우저에서 호출할 수 있게 해 주는 웹 프레임워크입니다. 함수 위에 한 줄을 붙이면 그 함수가 해당 주소의 요청을 처리하고, 들어오는 입력은 4장에서 쓴 Pydantic이 처리 전에 검사합니다.


1. 함수 위에 한 줄

from fastapi import FastAPI

app = FastAPI(title="하루마켓 고객지원 멀티에이전트")

@app.get("/api/health")
def health():
    return {"ok": True, "customer": session.customer_id}
줄 뜻
app = FastAPI(...) 서버 프로그램 하나를 만든다
@app.get("/api/health") "/api/health 주소로 조회 요청(GET) 이 오면 아래 함수를 실행한다"
return {...} 돌려준 딕셔너리가 JSON으로 바뀌어 응답이 된다

이렇게 주소 하나와 함수 하나가 짝지어진 것을 엔드포인트(endpoint) 라고 부릅니다.

요청에는 종류가 있습니다. 이번 장에서는 두 가지만 씁니다.

종류 쓰임 데코레이터
GET 무언가를 가져온다. 브라우저 주소창에 주소를 치면 GET이다 @app.get(...)
POST 내용을 보내서 처리를 시킨다 @app.post(...)

2. 우리 서버의 엔드포인트 여덟 개

고객 쪽 셋과 상담원 쪽 다섯입니다.

종류와 주소 하는 일 누가 부르나
GET / 채팅 화면(index.html)을 내려 준다 고객의 브라우저 주소창
GET /api/health 서버가 살아 있는지, 누구로 로그인되어 있는지 알려 준다 채팅 화면(머리글 아래 표시), 점검할 때
POST /api/chat 문의를 받아 Supervisor를 실행하고 답을 흘려보낸다 채팅 화면
GET /admin 상담원 화면(admin.html)을 내려 준다 상담원의 브라우저 주소창
GET /api/actions 처리 내역 전체(배송지 변경, 교환 접수, 환불·취소 요청)를 돌려준다 상담원 화면
GET /api/approvals 그 가운데 승인을 기다리는 요청만 돌려준다 점검할 때, 다른 프로그램
POST /api/approvals/{action_id} 상담원의 승인 또는 반려를 받아 기록한다 상담원 화면의 승인·반려 버튼
GET /api/tickets 에이전트가 처리하지 못해 넘겨받은 문의(티켓) 목록을 돌려준다 상담원 화면

GET /과 GET /admin이 화면을 주고, 그 화면들이 나머지 주소를 부릅니다. 화면과 API를 한 서버가 함께 내어 주는 구조입니다.

상담원 쪽 주소의 함수는 모두 몇 줄입니다. 일은 21장의 haru_actions.py가 하고, 서버는 주소만 붙입니다.

@app.get("/api/approvals")
def approvals():
    """승인을 기다리는 환불·주문 취소 요청."""
    return pending_approvals()


@app.post("/api/approvals/{action_id}")
def approve(action_id: str, body: Decision):
    """상담원이 승인(approve=true) 또는 반려(approve=false)한다."""
    result = decide(action_id, approve=body.approve, by="상담원")
    log.info("[승인] %s → %s", action_id, result.get("status", result))
    return result
줄 뜻
"/api/approvals/{action_id}" 주소의 중괄호 자리에 온 값이 함수의 action_id 인자로 들어온다. /api/approvals/A00001이면 action_id는 "A00001"
body: Decision 함께 보낸 내용. 모양은 아래 3절의 Pydantic 모델이 정한다
decide(action_id, approve=..., by="상담원") 23장의 결과 기록 노드가 부르던 그 함수. 승인대기를 승인완료나 반려로 바꾼다

23장에서 Command(resume="approve")가 하던 일을, 여기서는 상담원 화면의 버튼이 보낸 요청이 합니다. 끝에서 불리는 함수는 같습니다.


3. 들어오는 입력을 문 앞에서 검사한다

/api/chat과 /api/approvals/{action_id}는 밖에서 오는 내용을 받습니다. 무엇이 올지 모르므로 모양부터 정해 둡니다.

from pydantic import BaseModel

class ChatRequest(BaseModel):
    session_id: str
    message: str


class Decision(BaseModel):
    approve: bool

@app.post("/api/chat")
def chat(req: ChatRequest):
    ...        # req.session_id, req.message 로 꺼내 쓴다
모델 어느 주소의 입력인가 칸
ChatRequest POST /api/chat 어느 대화인지(session_id), 문의 내용(message)
Decision POST /api/approvals/{action_id} 승인이면 true, 반려면 false (approve)

4장에서 Pydantic으로 LLM의 출력을 검사했습니다. 여기서는 같은 도구로 바깥에서 들어오는 입력을 검사합니다. 방향만 다르고 하는 일은 같습니다.

모양이 맞지 않는 요청은 우리 함수가 실행되기도 전에 FastAPI가 돌려보냅니다. session_id를 빼고 보내면 이런 응답이 옵니다(이 교재를 준비하며 실제로 받은 응답입니다).

{"detail":[{"type":"missing","loc":["body","session_id"],"msg":"Field required","input":{"message":"안녕"}}]}

응답 코드는 422입니다. "요청의 형식은 알아봤지만 내용이 약속과 다르다"는 뜻입니다. Supervisor는 불리지 않았고 LLM 비용도 나가지 않았습니다.

믿을 수 없는 것이 들어오는 자리마다 검사를 둡니다. LLM의 출력이 들어오는 자리(4장), 도구의 인자가 들어오는 자리(8장), 그리고 바깥의 요청이 들어오는 자리(24장).


4. 서버를 실행하는 것은 Uvicorn이다

FastAPI로 쓴 것은 "어떤 주소에 어떤 함수"라는 목록입니다. 실제로 포트를 열고 요청을 기다리는 일은 Uvicorn이라는 프로그램이 합니다.

$ python -m uvicorn app.main:app --reload --port 8000
조각 뜻
python -m uvicorn 지금 켜져 있는 환경(myenv)의 Uvicorn을 실행한다
app.main:app app 폴더의 main.py 안에 있는 app이라는 변수
--reload 코드를 고쳐 저장하면 서버가 자동으로 다시 시작된다. 개발할 때만 쓴다
--port 8000 8000번 포트에서 기다린다

포트(port) 는 한 컴퓨터 안에서 프로그램을 구분하는 번호입니다. 브라우저에서 http://localhost:8000을 열면 "내 컴퓨터(localhost)의 8000번에서 기다리는 프로그램"에게 요청이 갑니다.

app.main:app에서 앞의 app은 폴더 이름이고 뒤의 app은 변수 이름입니다. 이름이 같아서 헷갈리기 쉽습니다. 그리고 이 명령은 app 폴더가 보이는 자리, 곧 haru-market 폴더 맨 위에서 실행해야 합니다.


5. 서버가 시작될 때 한 번 일어나는 일

main.py의 맨 바깥에 이런 줄이 있습니다.

DEMO_CUSTOMER = "C003"
session = CustomerSession(DEMO_CUSTOMER)
supervisor = build_supervisor(session, verbose=False)
histories: dict[str, list] = {}          # session_id → messages

함수 밖에 있으므로 서버가 시작될 때 한 번만 실행됩니다. Supervisor와 전문 에이전트 셋을 미리 만들어 두고, 요청이 올 때마다 그것을 다시 씁니다.

반대로 처리 기록은 미리 읽어 두지 않습니다. load_actions()와 pending_approvals()는 불릴 때마다 memory_store/actions.json을 다시 읽습니다. 그래서 채팅 쪽에서 방금 등록한 승인 요청이 상담원 쪽 주소에 곧바로 보입니다.

두 가지를 짚어 둡니다.

  • 로그인 고객이 C003 한 명으로 고정되어 있습니다. 실제 로그인은 이 과정의 범위 밖입니다. 8장에서 만든 CustomerSession에 시연용 고객을 넣어 쓰는 것이고, 실제 서비스라면 로그인 정보에서 고객을 알아내 요청마다 세션을 만듭니다.
  • verbose=False로 만들었으므로 22장의 [위임 →] 줄은 터미널에 찍히지 않습니다. 그 정보는 이제 화면으로 갑니다.

핵심 정리

  • FastAPI는 함수 위에 @app.get / @app.post 한 줄을 붙여 주소로 만듭니다.
  • 우리 서버의 엔드포인트는 여덟 개입니다. 고객 쪽 화면·점검·채팅 셋, 상담원 쪽 화면·처리 내역·승인 대기·승인/반려·티켓 목록 다섯.
  • POST /api/approvals/{action_id}는 23장과 같은 decide() 를 부릅니다. 승인 게이트가 웹에서는 버튼이 됩니다.
  • 들어오는 입력은 Pydantic 모델로 모양을 정해 문 앞에서 검사합니다. 틀리면 422로 돌려보내고 함수는 실행되지 않습니다.
  • 서버를 실제로 띄우는 것은 Uvicorn입니다. app.main:app은 "app/main.py의 app 변수"입니다.
  • Supervisor는 서버가 시작될 때 한 번 만들어 두고 계속 씁니다.
← 이전 절왜 서비스로 만들어야 하는가 — 고객은 터미널을 쓰지 않는다다음 절 →세션과 대화 이력 — 브라우저마다 대화가 따로 있다
오명운 · macro@prag-ai.com