페이지 목차
이 페이지에서 검색
로컬 LLM
이 페이지에 묻기
로컬 LLM · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.
생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.
LOCAL · LLM
로컬 LLM
생성 수정
로컬 모델 실행과 GPU·메모리 이해부터 서빙·RAG·도구 호출·업무형 앱 구현까지 차례로 살펴봅니다.
42개 문서 · 1일차 · 8 / 42
7차시 · Ollama API 활용
지도 위치 : 1일차 · ④API 층 맛보기 (Ollama 고유 API) | 이전: 6차시 로컬 LLM 모델 및 Prompt 활용 | 다음: 8차시 Ollama 기반 AI 앱 구현 키워드 : REST API, Chat API, Python 연동, 요청 · 응답 처리, Streaming 응답
이 차시를 다 읽으면
- Ollama 서버를 REST API (주소 + 메서드 + JSON)로 읽고,
curl로 직접 말을 걸 수 있다. /api/generate와/api/chat의 차이를 prompt 하나 ↔ messages 배열 로 말할 수 있다.- 스트리밍 응답이 줄 단위 JSON(NDJSON) 이라는 것과, 26차시의 SSE와 무엇이 다른지 말할 수 있다.
- 마지막
done줄의 통계로 TPS를 계산 할 수 있다 (단위: 나노초). - 동기 ↔ 비동기 를 정의하고, "스트리밍은 빨라지는 게 아니라 먼저 보여 주는 것 ", "비동기는 처리량 을 늘리는 것"을 대비해 설명할 수 있다.
0. 이 차시의 판별 축
| # | 축 | 왼쪽 (0) | 오른쪽 (1) | 이 축으로 갈리는 것 | 큰 축 (README 5장) |
|---|---|---|---|---|---|
| 1 | 다 될 때까지 기다리는가 | 한 번에 받기 (stream: false) | 조각으로 받기 (stream: true) | 응답 전체를 기다림 ↔ 생성되는 대로 받음 | ⑥ 동기 ↔ 비동기 (3.3에서 정의) |
| 2 | 입력이 어떤 모양인가 | 글 한 덩어리 (prompt) | 역할별 메시지 배열 (messages) | /api/generate ↔ /api/chat | ⑦ 무상태 ↔ 상태 (8차시 예고) |
| 3 | 누구의 규격인가 | Ollama 고유 API (/api/...) | OpenAI 호환 API (/v1/...) | 이 엔진 전용 ↔ 엔진을 갈아 끼울 수 있음 | ② 추상 ↔ 구상 (20차시) |
5차시 1.2에서 "ollama run도 사실은 11434번 포트 의 서버에 요청을 보내는 클라이언트"라고 했습니다. 이번에는 그 요청을 우리가 직접 보냅니다. 터미널 명령 대신 프로그램이 서버와 말하는 방식, 즉 API 를 다루는 차시입니다. 8차시의 챗봇 앱도, 3일차의 vLLM 서버도 모두 이 방식 위에 섭니다.
1. Ollama 서버를 REST로 읽기
먼저 이 차시 전체에 나오는 단어들을 정의합니다.
API (Application Programming Interface) — [약속]인데, [ 프로그램이 다른 프로그램의 기능을 부르는 방법을 정해 둔다 ]는 점에서 사람이 쓰는 화면(UI)과 구별된다.
HTTP — [통신 규약]인데, [ 클라이언트가 요청을 하나 보내면 서버가 응답을 하나 돌려주는 한 번의 왕복 으로 대화한다]는 점에서 연결을 열어 두고 양쪽이 아무 때나 말하는 방식과 구별된다. 웹 브라우저가 페이지를 받아 오는 방식이 바로 이것이고, Ollama API도 같은 HTTP를 씁니다.
HTTP 요청과 응답은 각각 정해진 부품으로 이루어집니다.
| 요청의 부품 | 뜻 | 이 차시의 예 |
|---|---|---|
| 메서드 | 어떻게 — 가져오기(GET) · 보내서 처리시키기(POST) · 지우기(DELETE) 등 | POST |
| 주소 (URL) | 어디에, 무엇을 — http://호스트:포트/경로 | http://localhost:11434/api/chat |
| 헤더 | 부가 정보 — 본문 형식(Content-Type), 인증 키 등 | Content-Type: application/json |
| 본문 (body) | 보내는 내용. LLM API에서는 대개 JSON | {"model": ..., "messages": [...]} |
| 응답의 부품 | 뜻 | 예 |
|---|---|---|
| 상태 코드 | 결과를 숫자로 — 2xx 성공 , 4xx 요청한 쪽 잘못 , 5xx 서버 쪽 잘못 | 200 성공, 404 그런 모델(자원) 없음, 500 서버 내부 오류 |
| 헤더 | 응답 본문의 형식 등 | application/x-ndjson (3.1) |
| 본문 | 결과 JSON | 모델의 답 + 통계 |
JSON — 이름과 값을 {"이름": 값} 꼴로 적는 글 형식. 사람도 읽을 수 있고 거의 모든 프로그래밍 언어가 읽을 수 있어서 API의 공용어처럼 쓰입니다.
REST — [API 설계 방식]인데, [ 다룰 대상(자원)을 주소로 가리키고, 무엇을 할지는 HTTP 메서드로, 내용은 JSON 같은 표현으로 주고받는다 ]는 점에서 함수 이름을 그대로 불러 쓰는 방식과 구별된다. 한 줄로 "주소(무엇을) + 메서드(어떻게) + 표현(JSON)" 입니다.
엔드포인트 (Endpoint) — 메서드와 주소를 짝지은 API의 출입구 하나 . POST /api/chat이 엔드포인트 하나입니다.
1.1 엔드포인트 지도
| 메서드 · 주소 | 하는 일 | 5차시 명령과의 대응 |
|---|---|---|
POST /api/generate | 글 하나 → 이어서 생성 | — |
POST /api/chat | 메시지 배열 → 다음 assistant 메시지 | ollama run의 대화 |
GET /api/tags | 디스크에 있는 모델 목록 | ollama list |
GET /api/ps | 메모리에 올라간 모델 | ollama ps |
POST /api/show | 모델 정보 | ollama show |
POST /api/pull, DELETE /api/delete | 내려받기 · 지우기 | ollama pull · rm |
⚠️ 마지막 줄을 보세요. 지우기 API도 같은 포트에 있습니다. 5차시 4.2에서 0.0.0.0으로 열면 안 된다고 한 이유가 이 표 한 줄로 보입니다. 인증 없이 열면 남이 우리 모델을 지울 수 있습니다.
1.2 curl로 직접 — 한 번에 받기
curl http://localhost:11434/api/chat -d '{
"model": "qwen3:8b",
"messages": [
{"role": "system", "content": "당신은 쇼핑몰 고객센터 문의 분류 도우미입니다."},
{"role": "user", "content": "주문번호 20261128-001 신발이 일주일째 안 와요."}
],
"stream": false,
"options": {"temperature": 0, "num_ctx": 8192}
}'
🔍 읽는 포인트
| 부분 | REST의 무엇 (1절 첫머리) | 뜻 |
|---|---|---|
localhost:11434 | 주소 · 포트 | 내 컴퓨터의 Ollama 창구 |
/api/chat | 자원 | 채팅 API |
-d '...' | 메서드 POST + 표현(JSON) | -d가 있으면 curl은 POST로 보냄 |
"stream": false | — | ⭐ 다 만들고 한 번에 달라 (0절 축 1 왼쪽) |
"options": {...} | — | 6차시의 추론 파라미터 를 요청마다 지정 |
응답 (값은 예시 · 실측 아님 , 일부 필드 생략):
{
"model": "qwen3:8b",
"message": {"role": "assistant", "content": "분류: 배송지연\n주문번호: 20261128-001\n요약: ..."},
"done": true,
"done_reason": "stop",
"total_duration": 2900000000,
"load_duration": 15000000,
"prompt_eval_count": 58,
"prompt_eval_duration": 120000000,
"eval_count": 40,
"eval_duration": 800000000
}
🔍 읽는 포인트 : 답은 message.content에 있습니다. 나머지 숫자는 3.2절에서 읽습니다. done_reason이 "stop"이면 스스로 끝낸 것이고, "length"면 생성 상한(num_predict)에 걸려 잘린 것 입니다.
2. /api/generate ↔ /api/chat
2.1 차이
/api/generate | /api/chat | |
|---|---|---|
| 입력 | "prompt": "글 하나" (+ 선택적으로 "system") | "messages": [{role, content}, ...] |
| 출력 필드 | response | message.content |
| 채팅 템플릿 | 엔진이 적용 (4차시 3.2) | 엔진이 적용 — 역할별로 |
| 어울리는 일 | 한 번 묻고 끝 (요약 1건, 분류 1건) | 대화 , 역할 구분이 필요한 모든 것 |
도구 호출 (tools) | 없음 | 지원 (30차시) — 구조화 출력 format은 둘 다 (29차시) |
⭐ 실무 기본값은 /api/chat 입니다. 한 번짜리 요청도 system + user 두 개짜리 배열로 보내면 됩니다. 20차시의 OpenAI 호환 API도 messages 배열 모양이라, 지금 이 모양에 익숙해 두면 그대로 옮겨 갑니다.
2.2 ⚠️ messages 배열이 말해 주는 것
/api/chat에 이전 대화는 어디에도 저장되지 않습니다. 두 번째 질문을 보낼 때 첫 번째 질문과 답을 배열에 다시 넣어 보내지 않으면 모델은 모릅니다. 이 배열을 누가 들고 있느냐 가 8차시 전체의 주제입니다.
3. 스트리밍 — 조각으로 받기
3.1 NDJSON — 줄 하나에 JSON 하나
"stream": true(기본값)로 보내면 응답이 줄 단위로 흘러옵니다.
{"model":"qwen3:8b","message":{"role":"assistant","content":"분류"},"done":false}
{"model":"qwen3:8b","message":{"role":"assistant","content":":"},"done":false}
{"model":"qwen3:8b","message":{"role":"assistant","content":" 배송"},"done":false}
...
{"model":"qwen3:8b","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop","total_duration":...,"eval_count":...,"eval_duration":...}
NDJSON (Newline-Delimited JSON) — [데이터 전송 형식]인데, [ 한 줄에 완결된 JSON 하나씩, 줄바꿈으로 구분해 보낸다 ]는 점에서 data: 접두어와 빈 줄로 구분하는 SSE와 구별된다.
Ollama /api/chat 스트리밍 | SSE (26차시, OpenAI 호환 /v1) | |
|---|---|---|
| 한 조각의 모양 | {...} + 줄바꿈 | data: {...} + 빈 줄 |
| 끝 표시 | 마지막 JSON의 "done": true | data: [DONE] |
| 조각 안의 글 | message.content | choices[0].delta.content |
| Content-Type | application/x-ndjson | text/event-stream |
⚠️ 같은 Ollama라도 주소에 따라 형식이 다릅니다. /api/chat은 NDJSON, /v1/chat/completions는 SSE입니다. 한쪽용으로 짠 파싱 코드를 다른 쪽에 붙이면 조각을 하나도 못 읽거나 JSON 에러가 납니다. AI가 만든 코드가 스트리밍에서 깨지면 가장 먼저 의심할 곳입니다.
3.2 ⭐ 마지막 줄의 통계 — TPS 계산
마지막 done: true 줄(또는 stream: false 응답)에 붙는 숫자들입니다. 단위는 전부 나노초(10⁻⁹초) 입니다.
| 필드 | 뜻 | 2차시와의 대응 |
|---|---|---|
total_duration | 요청 전체 시간 | — |
load_duration | 모델을 메모리에 올리는 데 쓴 시간 | 5차시 3.2 — 첫 요청이면 큼 |
prompt_eval_count | 입력으로 처리한 토큰 수 | Prefill |
prompt_eval_duration | 입력 처리 시간 | Prefill |
eval_count | 생성한 토큰 수 | Decode |
eval_duration | 생성 시간 | Decode |
생성 속도 (TPS) = eval_count ÷ eval_duration × 1,000,000,000
1.2절 예시값: 40 ÷ 800,000,000 × 10⁹ = 50 tok/s (예시 · 실측 아님)
입력 처리 속도 = 58 ÷ 120,000,000 × 10⁹ ≈ 483 tok/s (예시 · 실측 아님)
⚠️ 나노초를 초로 착각하면 숫자가 10억 배 틀립니다. AI가 "TPS가 0.00000005입니다"라고 보고하면 단위 변환을 빼먹은 것입니다.
⭐ 이 숫자는 AI의 "빨라졌습니다"를 검수하는 첫 번째 증거 입니다. 추정이 아니라 엔진이 직접 잰 값 이기 때문입니다.
앞으로 다시 만납니다 — 14차시에서 이 통계와 클라이언트 쪽 시계로 TTFT · TPS · Latency 를 정식으로 재고, 워밍업 · 반복 · 중앙값 규칙을 붙입니다. 8차시에서는 대화가 길어질수록
prompt_eval_count가 어떻게 변하는지 봅니다.
3.3 ⭐ 스트리밍은 빨라지는 게 아니라 먼저 보여 주는 것
이 절에서 README 5장의 큰 축 ⑥을 정식으로 정의합니다.
동기 (Synchronous) — [작업 처리 방식]인데, [ 결과가 올 때까지 그 자리에서 멈춰 기다린 뒤 다음 일로 넘어간다 ]는 점에서 비동기와 구별된다. 전화 통화처럼, 상대가 답할 때까지 다른 일을 못 합니다.
비동기 (Asynchronous) — [작업 처리 방식]인데, [ 결과를 기다리는 동안 멈추지 않고 다른 일을 하다가, 결과가 오면 이어서 처리한다 ]는 점에서 동기와 구별된다. 문자 메시지처럼, 보내 놓고 답이 올 때까지 다른 일을 합니다.
LLM 앱의 시간은 대부분 기다리는 시간 입니다. 앱 서버는 요청을 보내 놓고 GPU가 답을 만드는 몇 초 동안 할 일이 없습니다. 이렇게 계산이 아니라 바깥(네트워크 · 디스크 · 다른 서버)의 응답을 기다리느라 오래 걸리는 일 을 I/O-bound 라고 하고, 반대로 계산 자체가 오래 걸리는 일 을 CPU-bound (또는 GPU에서 계산이 병목인 일)라고 합니다. 비동기가 효과를 보는 것은 I/O-bound 쪽입니다.
⚠️ 비동기는 빨라지는 것이 아닙니다. 요청 한 건이 걸리는 시간은 똑같습니다. 바뀌는 것은 기다리는 시간을 놀리지 않는다 는 점이고, 그래서 같은 시간에 처리하는 요청 수(처리량, Throughput) 가 늘어납니다.
동기: [요청A ── 기다림 3초 ──][요청B ── 기다림 3초 ──] → 6초에 2건
비동기: [요청A ── 기다림 3초 ──]
[요청B ── 기다림 3초 ──] → 3초에 2건 (각 건은 여전히 3초)
(기다리는 상대 — 여기서는 LLM 서버 — 가 두 요청을 동시에 받아 줄 수 있을 때의 그림입니다. 엔진이 동시에 몇 건을 처리하는지는 13 · 21 · 24차시에서 다룹니다.)
스트리밍도 "기다리지 않는" 쪽처럼 보이지만, 바뀌는 것이 다릅니다. 둘을 나란히 놓습니다.
| 한 건의 총 시간 | 서버 전체 처리량 | 바뀌는 것 | |
|---|---|---|---|
| 비동기 (앱 서버 쪽) | 같음 | 늘어남 | 기다리는 동안 다른 요청을 처리 |
| 스트리밍 (이 차시) | 같음 | 같음 | ⭐ 첫 글자가 보이는 시점 이 당겨짐 |
stream: false [────── 생성 3초 ──────]▶ 3초 뒤 답 전체가 한꺼번에
stream: true [▶첫 조각 ▶ ▶ ▶ ▶ ▶ ▶ ▶] 0.x초 뒤부터 글자가 흘러나옴, 끝은 똑같이 3초
사람이 느끼는 기다림은 첫 글자까지의 시간(TTFT, 2차시 4절) 이 좌우합니다. 그래서 상담원 화면에는 스트리밍이 맞고, 분류 결과를 받아 프로그램이 다음 단계로 넘기는 경우(분류 → DB 저장)에는 어차피 끝까지 받아야 하니 stream: false가 단순합니다.
4. Python 연동
4.1 공식 라이브러리 — ollama
import ollama
messages = [
{"role": "system", "content": "당신은 쇼핑몰 고객센터 문의 분류 도우미입니다."},
{"role": "user", "content": "주문번호 20261128-001 신발이 일주일째 안 와요."},
]
stream = ollama.chat(model="qwen3:8b", messages=messages, stream=True,
options={"temperature": 0, "num_ctx": 8192})
for chunk in stream:
print(chunk["message"]["content"], end="", flush=True)
if chunk["done"]:
tps = chunk["eval_count"] / chunk["eval_duration"] * 1e9
print(f"\n[생성 {chunk['eval_count']} 토큰, {tps:.1f} tok/s]")
🔍 읽는 포인트 : stream=True면 for로 조각을 하나씩 받습니다. 조각마다 message.content를 이어 붙여 출력 하고, done인 마지막 조각에서 3.2절의 계산을 합니다. pip install ollama가 필요하고, 앱 전용 환경(3차시 3.1의 llm-app)에 설치합니다.
4.2 라이브러리 없이 — requests로 NDJSON 읽기
import json, requests
body = {"model": "qwen3:8b", "messages": messages, "stream": True}
with requests.post("http://localhost:11434/api/chat", json=body, stream=True, timeout=120) as r:
r.raise_for_status()
for line in r.iter_lines(): # ⭐ 한 줄 = JSON 하나 (NDJSON)
if not line:
continue
part = json.loads(line)
print(part["message"]["content"], end="", flush=True)
🔍 읽는 포인트 : iter_lines()가 줄 단위로 끊어 주고, 각 줄을 json.loads로 엽니다 — 3.1절의 NDJSON을 코드로 옮긴 것이 전부입니다. SSE였다면 data: 를 떼고 [DONE]을 걸러야 합니다(26차시).
4.3 요청 · 응답 처리에서 챙길 것
| 상황 | 증상 | 처리 |
|---|---|---|
| Ollama가 안 떠 있음 | 연결 거부 (Connection refused) | systemctl status ollama (5차시 1.2) |
| 모델 이름 오타 · 미설치 | HTTP 404 + "error" 필드 | ollama list로 이름 확인 |
| 첫 요청이 오래 걸림 | 타임아웃 | load_duration이 큼 → 타임아웃을 넉넉히, 또는 keep_alive로 상주 (5차시 3.2) |
| 답이 중간에 끊김 | done_reason: "length" | num_predict · num_ctx 확인 (6차시 4절) |
Qwen3 답에 <think>가 섞임 | 생각 모드 | 버전에 따라 요청의 think 옵션 · /no_think — 공식 문서 확인 (4차시 3.3) |
| 요청마다 모델 유지 시간을 바꾸고 싶음 | — | 요청 JSON의 "keep_alive" |
5. 예고 — OpenAI 호환 /v1
Ollama는 같은 포트에 OpenAI와 같은 모양의 API 도 엽니다.
http://localhost:11434/api/chat ← Ollama 고유 (이 차시)
http://localhost:11434/v1/chat/completions ← OpenAI 호환 (20차시)
OpenAI 호환 API 는 OpenAI가 만든 API의 요청 · 응답 모양을 그대로 따른 것입니다. 이 모양이 사실상 업계 공용 규격이 되어, Ollama · vLLM을 비롯한 여러 엔진이 같은 모양의 /v1 주소를 엽니다. 그래서 /v1에 맞춰 짠 앱은 엔진이 무엇인지 모릅니다. 콘센트 규격(약속)만 맞으면 어느 회사 플러그(구현)든 꽂히는 것과 같습니다 — README 7장 문장 3, 큰 축 ② 추상 ↔ 구상.
고유 API는 Ollama의 기능(keep_alive, 상세 통계)을 다 쓰지만 Ollama에 묶이고 , /v1은 일부 기능을 포기하는 대신 vLLM으로 갈아 끼울 수 있습니다 (0절 축 3).
앞으로 다시 만납니다 — 20차시에서 같은 코드의
base_url만 바꿔 Ollama → vLLM으로 옮깁니다. 앱을 오래 쓸 생각이면 그쪽이 기본값이 됩니다.
6. 실습 — 맡기고 검수하기
지시
목표: Ollama API를 curl과 Python으로 직접 호출하고, 스트리밍 응답과 통계를 확인해줘.
맥락: Ollama 공식 API 문서(docs의 API 레퍼런스)와 ollama-python 저장소 README를 기준으로 해.
필드 이름을 기억으로 쓰지 말고 실제 응답에서 확인해.
제약: - 모델 qwen3:8b, temperature 0, num_ctx 8192, 생각 모드 끔.
- 같은 문의로 stream:false 한 번, stream:true 한 번.
- 첫 요청(로딩 포함)은 따로 표시해.
보고 형식:
① curl stream:false 응답 JSON 원문
② curl stream:true 응답의 처음 5줄과 마지막 1줄 원문
③ 표 [요청 | load_duration(초) | prompt_eval_count | eval_count | eval_duration(초) | TPS]
— 나노초를 초로 바꾼 식을 같이 적어
④ Python(ollama 라이브러리) 스트리밍 코드와 실행 화면
추측하지 말고 실행 출력 원문을 붙여.
받을 증거
- 응답 원문 (특히 ②의 줄 단위 모양 — NDJSON인지 눈으로 확인)
- ③ 표의 TPS 계산식
내가 직접 볼 것
| 볼 것 | 이걸로 아는 것 |
|---|---|
②가 data: 없이 {로 시작하는 줄들인가 | NDJSON이다 (SSE 아님) |
첫 요청과 두 번째 요청의 load_duration 차이 | 로딩이 첫 요청에만 들어간다 (5차시 3.2) |
| TPS를 내 손으로 한 번 계산해 표와 맞는가 | 단위(나노초) 변환이 맞는가 |
stream true/false의 total_duration이 비슷한가 | 스트리밍은 총 시간을 줄이지 않는다 (3.3) |
부록 A. 스스로 확인하기
| # | 질문 | 답이 있는 곳 |
|---|---|---|
| 1 | HTTP 요청 · 응답의 부품은? Ollama API를 REST의 세 요소로 읽으면? | 1절 첫머리, 1.2 |
| 2 | 지우기 API가 같은 포트에 있다는 것이 왜 중요한가? | 1.1 |
| 3 | /api/generate와 /api/chat의 입력 · 출력 필드 차이는? | 2.1 |
| 4 | 두 번째 질문을 보낼 때 첫 대화를 배열에 안 넣으면? | 2.2 |
| 5 | NDJSON과 SSE는 조각 모양 · 끝 표시가 어떻게 다른가? | 3.1 |
| 6 | 같은 Ollama에서 두 형식이 갈리는 기준은? | 3.1 |
| 7 | TPS 계산식과 단위는? | 3.2 |
| 8 | 동기와 비동기는 무엇으로 갈리는가? 스트리밍과 비동기는 각각 무엇을 바꾸는가? | 3.3 |
| 9 | done_reason이 "length"면 무슨 뜻인가? | 1.2, 4.3 |
| 10 | 고유 API와 /v1은 무엇을 주고 무엇을 포기하는가? | 5 |
부록 B. 자주 하는 실수
| 실수 | 바로잡기 |
|---|---|
| "스트리밍을 켜면 응답이 빨라진다" | ❌ 총 시간은 같음. 첫 글자가 먼저 보일 뿐 |
| "비동기로 바꾸면 한 건이 빨라진다" | ❌ 한 건의 시간은 같음. 늘어나는 것은 처리량 (3.3) |
"Ollama 스트리밍도 SSE니까 data:를 떼면 된다" | ❌ /api/chat은 NDJSON . /v1만 SSE |
"eval_duration이 800,000,000이니 엄청 느리다" | ❌ 단위가 나노초 . 0.8초 |
| "서버가 대화를 기억하니 새 질문만 보내면 된다" | ❌ API는 기억하지 않음. 배열 전체를 다시 (8차시) |
| "첫 요청 속도로 성능을 판단" | ⚠️ load_duration 포함. 워밍업은 버림 (14차시) |
"/api/generate로 대화를 만든다" | ⚠️ 역할 구분이 필요하면 /api/chat |
부록 C. NotebookLM 복습 프롬프트
이 7차시 노트만 근거로 6문제를 내줘. 비전공자 기준으로.
"/api/generate와 /api/chat", "NDJSON과 SSE의 차이", "나노초 단위 TPS 계산", "스트리밍 ↔ 비동기"는 반드시 포함해줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.
LangTailor