아톨러브

XII. Python 프로그램이 인터넷 너머의 API 서버와 대화하는 방법 → 오류, 재시도 ...→ 결국 실제 LLM 호출 함수 call_model()로 발전시키는 과정 본문

AI, 클라우드, 문서, 자동화/AI_AGENT

XII. Python 프로그램이 인터넷 너머의 API 서버와 대화하는 방법 → 오류, 재시도 ...→ 결국 실제 LLM 호출 함수 call_model()로 발전시키는 과정

아톨 2026. 8. 25. 19:57
반응형

 

    #-------- 1. HTTP 상태 코드

    # 200 OK, it worked

    # 400 bad request, your JSON or parameters are wrong

    # 401 unauthorised, the API key is missing or invalid

    # 403 forbidden, the key is valid but not allowed to do this

    # 404 not found, check the URL

    # 429 too many requests, slow down and retry later

    # 500 server error on their side

    # 503 service unavailable, retry later

 

    #-------- 2. GET 요청 보내기

    import requests

 

    response = requests.get("https://httpbin.org/json", timeout=10)

 

    print("status: ", response.status_code)

    print("content type: ", response.headers["content-type"])

 

    data = response.json()

    print("top level keys: ", list(data))

 

    # status:  200

    # content type:  application/json

    # top level keys:  ['slideshow']

 

    #-------- 3. POST 요청 + JSON 보내기

    import os

    import requests

 

    API_KEY = os.environ.get("MODEL_API_KEY", "test-key")

 

    payload = {

        "model":"some-model",

        "max_tokens":256,

        "messages":[{"role":"user", "content":"다섯 단어로 인사를 하세요."}],

    }

 

    headers = {

        "Authorization":f"Bearer {API_KEY}",

        "Content-Type":"application/json",

    }

 

    response = requests.post(

        "https://httpbin.org/post",

        json=payload,

        headers=headers,

        timeout=30,

    )

 

    echo = response.json()

    print("status: ", response.status_code)

    print("sent model: ", echo["json"]["model"])

    print("sent message: ", echo["json"]["messages"][0]["content"])

    print("auth header seen: ", echo["headers"]["Authorization"][:11] + "...")

 

    #-------- 4. HTTP 상태 코드 확인하기

    import requests

 

    for url in ["https://httpbin.org/status/200", "https://httpbin.org/status/404"]:

        r = requests.get(url, timeout=10)

        print(f"{url.split('/')[-1]}: ok={r.ok} code={r.status_code}")

 

        try:

            r.raise_for_status()

            print("진행~")

        except requests.HTTPError as e:

            print(f"중지되었습니다: {e.response.status_code} error")

 

    #-------- 5. 실패하면 Retry 하기

    import requests

    import time

 

    RETRY_CODES = {429, 500, 502, 503, 504}

 

    def get_with_retry(url, tries=3, timeout=10):

        for attempt in range(1, tries+1):

            try:

                r = requests.get(url, timeout=timeout)

            except requests.Timeout:

                print(f"attempt {attempt}: timed out")

            else:

                if r.status_code not in RETRY_CODES:

                    return r

                print(f"시도 {attempt}: {r.status_code} 수신, 재시도")

            if attempt < tries:

                time.sleep(0.5*(2**(attempt-1)))

        return None

 

    result = get_with_retry("https://httpbin.org/status/503")

    print("최종 결과: ", result)

 

    ok = get_with_retry("https://httpbin.org/status/200")

    print("좋은 신호로 돌아옴: ", ok.status_code)

 

    #-------- 6. Streaming 응답 받기

    import requests

 

    with requests.get("https://httpbin.org/stream/3", stream=True, timeout=30) as r:

        for number, line in enumerate(r.iter_lines(), start=1):

            if line:

                print(f"청크 {number}: {len(line)} bytes 수신되었습니다.")

    print("stream 종료되었습니다.")

 

    #-------- 7. 모든 것을 합쳐 call_model() 만들기

    import os

    import time

    import requests

 

    API_URL="https://httpbin.org/post"  

    API_KEY=os.environ.get("MODEL_API_KEY", "test_key")

    RETRY_CODES={429, 500, 502, 503, 504}

 

    def call_model(messages, model="some-model", max_tokens=512, tries=3):

        payload={"model":model, "max_tokens":max_tokens, "messages":messages}

        headers={

            "Authorization":f"Bearer {API_KEY}",

            "Content-Type":"application/json",

        }

 

        for attempt in range(1, tries+1):

            try:

                r = requests.post(API_URL, json=payload, headers=headers, timeout=30)

            except requests.RequestException as e:

                if attempt==tries:

                    return {"ok":False, "text":None, "error":f"network: {e}"}

                time.sleep(0.5*(2**(attempt-1)))

                continue

 

            if r.status_code in RETRY_CODES and attempt < tries:

                wait=float(r.headers.get("Retry-After", 0.5*(2**(attempt-1))))

                print(f"{r.status_code} received, waiting {wait}s")

                time.sleep(wait)

                continue

 

            if not r.ok:

                return {"ok":False, "text":None, "error":f"http {r.status_code}"}

 

            try:

                data = r.json()

                print("data: ", data)

            except ValueError:

                return {"ok":False, "text":None, "error":"response was not JSON"}

 

            #httpbin echoes the request back; a real API returns generated text here

            text = data["json"]["messages"][-1]["content"]

            print("text: ", text)

            return {"ok":True, "text":text, "error":None}

        return {"ok":False, "text":None, "error":"exhausted retries"}

 

    result = call_model([{"role":"user", "content":"우리의 환불정책은 무엇인가요?"}])

    print("ok: ", result["ok"])

    print("text: ", result["text"])

    print("error: ", result["error"])

 

우리가 만들 Agent 구조에서 보면:

User
 ↓
Agent
 ↓
call_model()          ← 이번 코드의 최종 목적
 ↓
HTTP Request
 ↓
OpenAI API
 ↓
HTTP Response
 ↓
JSON
 ↓
Agent가 이해할 수 있는 데이터
 

순서대로 보겠습니다.


전체 코드의 큰 흐름

이번 코드는 사실 6단계의 실습입니다.

1. GET 요청 보내기
        ↓
2. POST 요청 + JSON 보내기
        ↓
3. HTTP 상태 코드 확인하기
        ↓
4. 실패하면 Retry 하기
        ↓
5. Streaming 응답 받기
        ↓
6. 모든 것을 합쳐 call_model() 만들기
 

마지막 call_model()이 우리가 실제 Agent에서 사용할 핵심입니다.


1. HTTP 상태 코드

맨 처음 주석입니다.

 
# 200 OK, it worked
# 400 bad request, your JSON or parameters are wrong
# 401 unauthorised, the API key is missing or invalid
# 403 forbidden, the key is valid but not allowed to do this
# 404 not found, check the URL
# 429 too many requests, slow down and retry later
# 500 server error on their side
# 503 service unavailable, retry later
 

이것은 서버와 통신했을 때 받는 HTTP 상태 코드입니다.

Agent를 만들 때 굉장히 중요합니다.


상태 코드 그림

Python 프로그램
      │
      │ HTTP 요청
      ▼
┌───────────────┐
│    API 서버    │
└───────┬───────┘
        │
        │ HTTP 응답
        ▼
┌────────────────┐
│ status_code    │
│                │
│ 200            │ 성공
│ 400            │ 요청 오류
│ 401            │ 인증 오류
│ 429            │ 너무 많은 요청
│ 500            │ 서버 오류
│ 503            │ 서버 일시 장애
└────────────────┘
 

Agent에서 중요한 분류

저는 이것을 두 그룹으로 나누어 기억하는 것을 추천합니다.

그룹 1: 내가 고쳐야 하는 오류

400
401
403
404
 

예를 들어:

400 → JSON 구조가 틀림
401 → API Key가 틀림
403 → 권한 없음
404 → URL이 틀림
 

이런 것은 단순 재시도를 해도 대부분 해결되지 않습니다.

틀린 API Key
    ↓
재시도
    ↓
틀린 API Key
    ↓
재시도
    ↓
틀린 API Key
 

계속 실패합니다.


그룹 2: 잠시 후 다시 시도할 가치가 있는 오류

429
500
502
503
504
 

예:

503 Service Unavailable
 

서버가 잠시 바쁜 경우:

지금 실패
 ↓
0.5초 대기
 ↓
다시 요청
 ↓
성공 가능
 

이것이 뒤에서 배우는 Retry입니다.


2. 첫 번째 코드 — GET 요청

 
import requests
 

requests란?

Python에서 인터넷 서버와 통신할 때 많이 사용하는 라이브러리입니다.

Python
   │
   │ requests
   ▼
Internet
   │
   ▼
API Server
 

대표적으로:

 
requests.get()
 

읽어오기

 
requests.post()
 

데이터 보내기

입니다.


요청 보내기

 
response = requests.get(
    "https://httpbin.org/json",
    timeout=10
)
 

작은 블록으로 보겠습니다.

requests.get()

 
requests.get(URL)
 

뜻:

이 URL에 GET 요청을 보내라.


URL

 
"https://httpbin.org/json"
 

httpbin.org는 HTTP 요청을 연습하기 좋은 테스트 서버입니다.

실제 OpenAI API 대신:

Python
 ↓
httpbin
 ↓
응답 확인
 

을 연습하는 것입니다.


timeout=10

 
timeout=10
 

뜻:

최대 10초만 기다려라.

만약 서버가 응답하지 않는다면:

요청
 ↓
1초
 ↓
5초
 ↓
10초
 ↓
응답 없음
 ↓
Timeout Exception
 

이 됩니다.

이것이 중요한 이유는 API 서버가 문제가 생겼을 때 Agent가 영원히 멈추는 것을 방지하기 위해서입니다.


3. 응답 객체 response

 
response = requests.get(...)
 

여기서 response는 단순 문자열이 아닙니다.

대략 이런 구조입니다.

response
│
├── status_code
│
├── headers
│
├── text
│
├── json()
│
├── ok
│
└── raise_for_status()
 

즉:

 
response.status_code
 

성공 여부

 
response.headers
 

응답 헤더

 
response.json()
 

JSON 데이터 변환

등을 제공합니다.


4. status_code 출력

 
print("status: ", response.status_code)
 

예:

status: 200
 

이것은:

서버와 통신이 성공했다.

라는 뜻입니다.


5. headers

 
print(
    "content type: ",
    response.headers["content-type"]
)
 

서버 응답에는 여러 정보가 들어 있습니다.

HTTP Response
│
├── Headers
│     │
│     ├── Content-Type
│     ├── Date
│     └── Server
│
└── Body
      │
      └── 실제 데이터
 

여기서:

Content-Type: application/json
 

이면:

서버가 JSON 데이터를 보내고 있습니다.

라는 뜻입니다.


잠재적인 문제점

현재 코드는:

 
response.headers["content-type"]
 

입니다.

만약 해당 헤더가 없으면:

KeyError
 

가 발생할 수 있습니다.

조금 더 안전하게 하려면:

 
response.headers.get("content-type")
 

를 사용할 수 있습니다.

하지만 학습용 코드로는 현재 방식도 괜찮습니다.


6. JSON 변환

 
data = response.json()
 

서버가 보내온 데이터가 예를 들어:

 
{
    "slideshow": {
        "title": "Sample"
    }
}
 

라고 하면:

 
response.json()
 

은 Python Dictionary로 바꿔줍니다.

 
data = {
    "slideshow": {
        "title": "Sample"
    }
}
 

7. top level keys

 
print("top level keys: ", list(data))
 

data가:

 
{
    "slideshow": {
        ...
    }
}
 

이면:

 
list(data)
 

는:

 
["slideshow"]
 

입니다.

즉:

data
 │
 └── "slideshow"
 

여기서 중요한 Python 문법입니다.

Dictionary를 list()로 감싸면?

 
list(data)
 

는 기본적으로 Dictionary의 Key들을 가져옵니다.

예:

 
person = {
    "name": "Kim",
    "age": 50
}

print(list(person))
 

결과:

['name', 'age']
 

첫 번째 실습의 핵심

GET 요청
 ↓
Response 객체 받음
 ↓
status_code 확인
 ↓
headers 확인
 ↓
JSON → Python Dictionary
 

이것이 API 통신의 가장 기본입니다.


8. 두 번째 코드 — POST로 JSON 보내기

이제 더 중요합니다.

 
import os
import requests
 

두 개를 가져옵니다.

os
 ↓
환경변수

requests
 ↓
인터넷 API 통신
 

9. API_KEY

 
API_KEY = os.environ.get(
    "MODEL_API_KEY",
    "test-key"
)
 

이것은 우리가 전에 공부한 환경변수입니다.

뜻:

환경변수 MODEL_API_KEY가 있으면
        ↓
그 값을 사용

없으면
        ↓
"test-key" 사용
 

예:

MODEL_API_KEY=abc123
 

이면:

 
API_KEY = "abc123"
 

없으면:

 
API_KEY = "test-key"
 

10. payload

 
payload = {
    "model":"some-model",
    "max_tokens":256,
    "messages":[
        {
            "role":"user",
            "content":"다섯 단어로 인사를 하세요."
        }
    ],
}
 

이 부분은 매우 중요합니다.

왜냐하면 실제 OpenAI API를 호출할 때도 이런 식으로:

Python Dictionary → JSON 요청 데이터

를 만드는 과정이 필요하기 때문입니다.

구조를 그림으로 보면:

payload
│
├── model
│     └── "some-model"
│
├── max_tokens
│     └── 256
│
└── messages
      │
      └── List
            │
            └── Dictionary
                  │
                  ├── role
                  │     └── user
                  │
                  └── content
                        └── "다섯 단어로..."
 

즉:

 
payload["messages"][0]["content"]
 

이 가능합니다.

하나씩 보면:

payload
   ↓
messages
   ↓
첫 번째 메시지 [0]
   ↓
content
 

11. headers

 
headers = {
    "Authorization": f"Bearer{API_KEY}",
    "Content-Type": "application/json",
}
 

HTTP 요청에는 크게 두 가지가 있습니다.

HTTP Request
│
├── Headers
│
└── Body
 

Authorization

 
"Authorization": f"Bearer {API_KEY}"
 

일반적으로 의미는:

이 요청은 이 API Key를 가진 사용자가 보낸 것입니다.

입니다.


주의할 점

첫 번째 코드에는:

 
f"Bearer{API_KEY}"
 

라고 되어 있습니다.

여기에는 Bearer와 API Key 사이에 공백이 없습니다.

Bearerabc123
 

일반적인 형식은:

Bearer abc123
 

즉:

 
f"Bearer {API_KEY}"
 

입니다.

아래 마지막 call_model() 코드에서는 제대로 수정되어 있습니다.

 
"Authorization": f"Bearer {API_KEY}"
 

이 부분은 실제 API에서는 매우 중요합니다.


12. POST 요청

 
response = requests.post(
    "https://httpbin.org/post",
    json=payload,
    headers=headers,
    timeout=30,
)
 

이제 전체 구조입니다.

Python

payload
   │
   ├── model
   ├── max_tokens
   └── messages

headers
   │
   ├── Authorization
   └── Content-Type

        ↓

requests.post()

        ↓

HTTP Server
 

json=payload

여기서 매우 중요합니다.

 
json=payload
 

는 requests에게:

이 Python Dictionary를 JSON으로 변환해서 보내세요.

라는 의미입니다.

즉:

 
payload = {
    "model": "some-model"
}
 

인터넷으로:

 
{
    "model": "some-model"
}
 

형태로 전송됩니다.


13. 서버가 받은 데이터를 다시 확인

 
echo = response.json()
 

httpbin은 테스트 서버라서 재미있는 기능이 있습니다.

우리가 보낸 요청을 다시 돌려줍니다.

내가 보낸 것
      ↓
httpbin
      ↓
그대로 돌려줌
 

그래서:

 
echo["json"]["model"]
 

로 확인할 수 있습니다.


구조

echo
│
├── json
│     │
│     ├── model
│     │
│     └── messages
│            │
│            └── [0]
│                  │
│                  └── content
│
└── headers
       │
       └── Authorization
 

그래서:

 
echo["json"]["model"]
 

some-model
 

 
echo["json"]["messages"][0]["content"]
 

다섯 단어로 인사를 하세요.
 

14. HTTP 상태 코드 확인

다음 코드입니다.

 
for url in [
    "https://httpbin.org/status/200",
    "https://httpbin.org/status/404"
]:
 

우리가 배운 for문입니다.

두 개의 URL을 하나씩 꺼냅니다.

1회차

url
↓
https://httpbin.org/status/200
 

다음:

2회차

url
↓
https://httpbin.org/status/404
 

15. r.ok

 
print(
    f"{url.split('/')[-1]}: "
    f"ok={r.ok} "
    f"code={r.status_code}"
)
 

예:

200: ok=True code=200
404: ok=False code=404
 

r.ok는 간단하게:

성공 범위?
 

를 판단하는 Boolean 값입니다.

 
True
False
 

16. raise_for_status()

 
try:
    r.raise_for_status()
    print("진행~")
except requests.HTTPError as e:
    print(
        f"중지되었습니다: "
        f"{e.response.status_code} error"
    )
 

이 부분은 매우 중요합니다.


200인 경우

r.raise_for_status()
       ↓
오류 없음
       ↓
"진행~"
 

404인 경우

r.raise_for_status()
       ↓
HTTPError 발생
       ↓
except
       ↓
"중지되었습니다: 404 error"
 

왜 이렇게 사용하는가?

API 프로그램에서는:

 
r = requests.get(...)
 

만 했다고 성공한 것이 아닙니다.

예:

인터넷 연결 성공
        ↓
서버까지 도착
        ↓
하지만 서버가
404 반환
 

입니다.

그래서:

 
r.raise_for_status()
 

를 사용하면:

HTTP 오류를 Python Exception으로 바꿔줍니다.

이것이 편리한 이유입니다.

HTTP 오류
   ↓
Exception
   ↓
try / except로 통일해서 처리
 

17. Retry 코드

이제 Agent에서 정말 중요한 부분입니다.

 
RETRY_CODES = {
    429,
    500,
    502,
    503,
    504
}
 

이 Dictionary가 아니라 Set입니다.

 
{429, 500, 502}
 

이 형태는 Python Set입니다.


왜 Set을 사용하는가?

우리는 계속 확인합니다.

 
if r.status_code in RETRY_CODES:
 

즉:

이 상태 코드가 재시도 목록에 있는가?

Set은 이런 포함 여부 확인에 적합합니다.


18. get_with_retry()

 
def get_with_retry(
    url,
    tries=3,
    timeout=10
):
 

뜻:

url
↓
요청할 주소

tries=3
↓
최대 3번 시도

timeout=10
↓
각 요청당 최대 10초 기다림
 

19. for attempt

 
for attempt in range(1, tries+1):
 

예:

 
tries = 3
 

이면:

attempt

1
2
3
 

총 3번입니다.


20. try / except / else

 
try:
    r = requests.get(url, timeout=timeout)

except requests.Timeout:
    print(f"attempt {attempt}: timed out")

else:
    ...
 

여기서 else가 중요합니다.

구조:

try
 ↓
성공
 ↓
else 실행


try
 ↓
Exception
 ↓
except 실행
 

즉:

 
else:
 

는:

try에서 오류가 발생하지 않았을 때 실행

됩니다.


21. 재시도 여부

 
if r.status_code not in RETRY_CODES:
    return r
 

이 한 줄은 매우 중요한 판단입니다.

예를 들어:

200
 

은 RETRY_CODES에 없습니다.

따라서:

 
return r
 

성공 종료합니다.


그런데:

503
 

은:

 
RETRY_CODES = {
    429,
    500,
    502,
    503,
    504
}
 

안에 있습니다.

그래서:

재시도
 

합니다.


22. Exponential Backoff

 
time.sleep(
    0.5 * (2 ** (attempt - 1))
)
 

이 부분이 조금 어려울 수 있습니다.

하나씩 보겠습니다.

첫 번째 시도 실패

attempt = 1

0.5 × 2^(1-1)

0.5 × 1

= 0.5초
 

두 번째 시도 실패

attempt = 2

0.5 × 2^(2-1)

0.5 × 2

= 1초
 

세 번째 시도 실패

attempt = 3

0.5 × 2^(3-1)

0.5 × 4

= 2초
 

즉:

1번째 실패 → 0.5초
2번째 실패 → 1초
3번째 실패 → 2초
4번째 실패 → 4초
 

점점 기다리는 시간이 늘어납니다.

이것을 Exponential Backoff(지수 백오프)라고 합니다.


왜 그냥 계속 재시도하지 않는가?

만약 서버가:

너무 바쁩니다!
 

라고 했는데:

요청
요청
요청
요청
요청
요청
 

계속 보내면 서버는 더 힘들어집니다.

그래서:

실패
 ↓
잠시 기다림
 ↓
다시 요청
 ↓
더 오래 기다림
 ↓
다시 요청
 

하는 것입니다.


23. Streaming

다음 코드입니다.

 
with requests.get(
    "https://httpbin.org/stream/3",
    stream=True,
    timeout=30
) as r:
 

Streaming은:

서버가 결과 전체를 한 번에 보내지 않고 조금씩 보내는 방식

입니다.


일반 응답

서버
 │
 │████████████████
 │ 전체 데이터
 ▼
Python
 

Streaming

서버
 │
 │██
 ▼
Python

서버
 │
 │██
 ▼
Python

서버
 │
 │██
 ▼
Python
 

with

 
with requests.get(...) as r:
 

이것은:

작업이 끝나면 연결을 자동으로 정리하라.

는 의미입니다.

우리가 배운 파일과 비슷합니다.

 
with open("file.txt") as f:
    ...
 

파일 작업이 끝나면 자동으로 닫습니다.

HTTP도:

 
with requests.get(...) as r:
 

작업이 끝나면 연결을 정리합니다.


iter_lines()

 
for number, line in enumerate(
    r.iter_lines(),
    start=1
):
 

iter_lines():

응답 데이터를 한 줄씩 가져온다.
 

그리고:

 
enumerate(..., start=1)
 

로:

번호 + 데이터
 

를 동시에 받습니다.

예:

1번째 줄
2번째 줄
3번째 줄
 

24. 이제 핵심: call_model()

이제 앞의 모든 것을 합쳤습니다.

 
def call_model(
    messages,
    model="some-model",
    max_tokens=512,
    tries=3
):
 

이 함수는 Agent 전체 구조에서 여기입니다.

Agent.run()

   ↓

history

   ↓

call_model(messages)   ★
   │
   │
   ▼
HTTP POST
   │
   ▼
LLM API
   │
   ▼
Response
   │
   ▼
Python Dictionary
 

25. 함수 입력

 
messages
 

Agent의 History입니다.

예:

 
messages = [
    {
        "role": "system",
        "content": "You are a helpful agent."
    },
    {
        "role": "user",
        "content": "7개의 가격을 계산해줘"
    }
]
 

즉:

History
   ↓
call_model()
   ↓
API
 

입니다.


26. payload 만들기

 
payload = {
    "model": model,
    "max_tokens": max_tokens,
    "messages": messages
}
 

이 구조는 매우 중요합니다.

call_model()
│
├── model
│
├── max_tokens
│
└── messages
        │
        └── Agent History
 

27. headers

 
headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}
 

이번에는 정확합니다.

Bearer + 공백 + API_KEY
 

28. RequestException

 
except requests.RequestException as e:
 

이것은 매우 좋은 구조입니다.

requests에서 발생할 수 있는 여러 네트워크 오류를 비교적 넓게 잡습니다.

예:

ConnectionError
Timeout
Request 문제
 

등입니다.


마지막 시도인가?

 
if attempt == tries:
 

예:

tries = 3

attempt = 3
 

이면:

마지막 시도
 

입니다.

그래서:

 
return {
    "ok": False,
    "text": None,
    "error": f"network: {e}"
}
 

결과 구조

성공하든 실패하든 비슷한 구조를 유지합니다.

result
│
├── ok
│
├── text
│
└── error
 

예:

 
{
    "ok": True,
    "text": "안녕하세요",
    "error": None
}
 

실패:

 
{
    "ok": False,
    "text": None,
    "error": "network error"
}
 

이런 구조를 유지하는 것은 Agent를 만들 때 매우 좋습니다.

왜냐하면 사용하는 쪽에서 항상:

 
result["ok"]
result["text"]
result["error"]
 

를 사용할 수 있기 때문입니다.


29. Retry-After

 
wait = float(
    r.headers.get(
        "Retry-After",
        0.5 * (2 ** (attempt - 1))
    )
)
 

서버가 이런 메시지를 줄 수 있습니다.

Retry-After: 5
 

뜻:

5초 후 다시 시도하세요.

그러면:

 
wait = 5
 

를 사용합니다.

없다면:

 
0.5 * (2 ** (attempt - 1))
 

을 사용합니다.

구조:

Retry-After 있음?
       │
    ┌──┴──┐
   YES    NO
    │      │
    ▼      ▼
서버가 준  Exponential
시간 사용   Backoff
 

30. r.ok 확인

 
if not r.ok:
    return {
        "ok": False,
        "text": None,
        "error": f"http {r.status_code}"
    }
 

예:

400
 

이라면:

 
r.ok = False
 

따라서:

실패 결과 반환
 

중요한 설계 포인트

여기서:

400
401
403
404
 

같은 오류는 재시도하지 않고 바로 반환합니다.

왜?

401
↓
API Key 틀림
↓
재시도해도 틀린 API Key
↓
계속 실패
 

그래서:

Permanent Error
→ 바로 종료
 

반면:

503
↓
서버 잠시 문제
↓
재시도 가치 있음
 

입니다.


31. JSON 파싱

 
try:
    data = r.json()
    print("data: ", data)

except ValueError:
    return {
        "ok": False,
        "text": None,
        "error": "response was not JSON"
    }
 

서버가 항상 JSON을 준다는 보장은 없습니다.

예:

 
<html>
Server Error
</html>
 

를 줄 수도 있습니다.

그러면:

 
r.json()
 

실패합니다.

그래서:

JSON 변환
   │
   ├── 성공
   │      ↓
   │    data
   │
   └── 실패
          ↓
        error 반환
 

입니다.


32. 현재 httpbin의 특별한 부분

 
text = data["json"]["messages"][-1]["content"]
 

이 부분은 실제 LLM 응답을 읽는 코드가 아닙니다.

주석에도:

 
# httpbin echoes the request back;
# a real API returns generated text here
 

라고 되어 있습니다.

httpbin은:

내가 보낸 JSON
      ↓
그대로 돌려줌
 

그래서:

 
data["json"]["messages"][-1]["content"]
 

를 하면:

내가 마지막으로 보낸 사용자 메시지
 

가 다시 나옵니다.

예:

우리의 환불정책은 무엇인가요?
 

[-1]의 의미

 
messages[-1]
 

은 리스트의 마지막 요소입니다.

예:

 
messages = [
    {"role": "system"},
    {"role": "user"},
    {"role": "assistant"}
]
 

그러면:

messages[-1]
        ↓
마지막 요소
        ↓
{"role": "assistant"}
 

실제 Agent에서는 어떻게 바뀌는가?

현재:

httpbin

내 요청
 ↓
그대로 반환
 

실제 LLM API:

messages
    ↓
LLM
    ↓
AI가 생각하고 응답
    ↓
새로운 메시지 반환
 

그래서 나중에는 대략:

 
text = response.choices[0].message.content
 

또는 사용하는 API의 실제 응답 구조에 맞는 코드를 사용하게 됩니다.


전체 call_model() 실행 흐름

마지막으로 전체 흐름을 그림으로 정리해보겠습니다.

call_model(messages)
        │
        ▼
┌───────────────────┐
│ payload 생성       │
│ model             │
│ max_tokens        │
│ messages          │
└─────────┬─────────┘
          │
          ▼
┌───────────────────┐
│ headers 생성       │
│ Authorization     │
│ Content-Type      │
└─────────┬─────────┘
          │
          ▼
┌───────────────────┐
│ POST 요청          │
└─────────┬─────────┘
          │
          ├──── Timeout?
          │       │
          │       ▼
          │    Retry
          │
          ▼
    HTTP Response
          │
          ├──── 503?
          │       │
          │       ▼
          │    Retry
          │
          ├──── 400?
          │       │
          │       ▼
          │    Error 반환
          │
          ▼
        200 OK
          │
          ▼
     JSON 변환
          │
          ├── 실패 → Error
          │
          ▼
       Text 추출
          │
          ▼
{
    "ok": True,
    "text": "...",
    "error": None
}
 

이번 코드에서 발견한 중요한 문제점 3가지

① Bearer 뒤 공백

두 번째 코드:

 
"Authorization": f"Bearer{API_KEY}"
 

보다는:

 
"Authorization": f"Bearer {API_KEY}"
 

가 맞습니다.


② Retry-After는 항상 숫자가 아닐 수도 있음

현재:

 
wait = float(r.headers.get(...))
 

서버에 따라 Retry-After가 날짜 형식으로 올 가능성도 있어서 실제 서비스에서는 조금 더 안전하게 처리할 필요가 있습니다.

하지만 현재 학습 단계에서는 충분히 좋은 코드입니다.


③ 마지막 call_model()은 아직 진짜 모델을 호출하지 않는다

 
API_URL = "https://httpbin.org/post"
 

이기 때문입니다.

즉 지금은:

Agent
 ↓
가짜 API
 ↓
요청 확인
 

단계입니다.

다음 단계에서는:

Agent
 ↓
실제 OpenAI API
 ↓
GPT 응답
 

으로 바뀌게 됩니다.


이번 코드에서 가장 중요한 핵심

이번 코드 전체를 한 문장으로 요약하면:

LLM을 호출하는 것은 결국 Python이 HTTP POST 요청으로 JSON을 보내고, 서버가 돌려주는 JSON 응답을 안전하게 받아오는 과정이다.

그리고 Agent 관점에서는:

history
   ↓
call_model(history)
   ↓
HTTP POST
   ↓
LLM API
   ↓
Response
   ↓
JSON parsing
   ↓
text
   ↓
Agent
 

입니다.

반응형