Files
aio-01d4-stu/Last3Mon/77/LAB-01-Q-READ-AND-MEASURE.md

752 lines
45 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Lab 01 · 실행 기록을 읽고, 지표로 만들기
- 관련 강의: [1단계 · 실행 기록 읽기](../../Lecture/77/01-STAGE1-READ-THE-LOG.md)
- 소요 시간: 50분 (2교시 한 시간) · 예제 1 10분 · 예제 2 15분 · 예제 3 7분 · 과제 1 8분 · 과제 2 10분
- 준비: [실습 준비](../../Lecture/77/00-DAY77-PREP.md)의 `doctor` → `ready`, 기준 보고서 `base-report` 게시
- 목표: 두 주 동안의 실행 기록을 먼저 **눈으로** 읽고, 팀이 쓰던 기준 보고서의 숫자가 왜 다른지 찾는다. 같은 질문에 늘 같은 숫자로 답하는 **지표 보고서**를 만들고(지표는 정의가 먼저), 경계(하루 · 빈 기간 · 거르기)에서 재 보고, 날짜별 · 릴리스별 · 단계별 · 비용으로 나눠 본다.
```mermaid
flowchart LR
E1["예제 1<br/>손으로 읽기 ·<br/>기준 보고서"] --> E2["예제 2<br/>지표 보고서"] --> E3["예제 3<br/>경계에서 재 보기"] --> T1["과제 1<br/>날짜별 · 릴리스별"] --> T2["과제 2<br/>단계별 · 비용"]
```
> 복사해 붙일 요청문은 없습니다. 막히면 `▶` 를 하나씩 펼칩니다. 연습이 끝나면 `uv run tools/flow.py ops --reset`.
## 시작하기 · 터미널 두 개
| 터미널 | 하는 일 |
| --- | --- |
| ① 에이전트 | `cd ~/aio-01d4-stu/Last3Mon/77` 후 `codex` (또는 `claude`, `agy`). 내가 쓴 요청문을 보낸다 |
| ② 확인 | `cd ~/aio-01d4-stu/Last3Mon/77` 에서 `logs` · `call` · `push` · `test`를 **내가** 실행한다 |
`cp templates/OPS-ANALYSIS-TEMPLATE.md my-work/OPS.md` 후 표를 채워 갑니다. 쓴 요청문은 `my-work/PROMPTS.md`에 모읍니다.
## 예제 1 · 손으로 읽기 · 팀의 기준 보고서 (10분, 에이전트 없이)
### 이 실습의 의도
**한 줄로:** 두 주 기록을 직접 훑어보고, 팀이 지난달 급히 만든 기준 보고서(`base-report`)에 같은 기간을 물어 **두 숫자가 다른지** 봅니다. 고치는 것은 예제 2부터이고, 여기서는 **읽고 비교해 적기만** 합니다.
> **생활 속 예 · 가게의 영수증 더미와 장부.** 가게 계산대 옆에 두 주 치 영수증이 쌓여 있습니다. 사장님이 "두 주 동안 몇 건 팔았지?"라고 물으면, 알바생 둘이 서로 다른 답을 할 수 있습니다. 한 사람은 마지막 날 영수증을 빼고 셌고, 한 사람은 넣고 셌다면요. 예제 1은 영수증(실행 기록)을 직접 넘겨 보고, 팀 장부(기준 보고서)가 어떻게 셌는지 비교하는 실습입니다.
**상황.** 데이터 API에 IT 헬프데스크의 두 주(10/19~10/30, 평일 10일) 실행 기록 666줄이 있습니다. 한 줄이 헬프데스크가 질문 하나에 답한 한 번의 실행입니다. 팀에는 지난달 만든 보고서 워크플로우 `base-report`가 있고, 운영 회의에서 이 숫자를 써 왔습니다. 그런데 이 보고서가 맞는지 아무도 확인하지 않았습니다.
**실험 넷.**
| 실험 | 명령 | 볼 것 |
| --- | --- | --- |
| ① 두 주를 훑기 | `logs --from 2026-10-19 --to 2026-10-30` | 실행 수 · 오류 수 · 갈래(`route`)마다 수, 마지막 다섯 줄 |
| ② 하루만 | `logs --from 2026-10-26 --to 2026-10-26` | 월요일 하루의 실행 · 오류 — 두 주의 오류와 비교 |
| ③ 기준에 두 주 | `call base-report requests/two-weeks.json` | ①과 같은 숫자인가? 어떤 칸이 있고 없나? |
| ④ 기준에 하루 | `call base-report requests/monday.json` | ②와 같은가? |
`logs`는 데이터 API의 기록을 **그대로** 세어 보여 주는 도구이고, `base-report`는 팀이 만든 워크플로우가 **자기 방식으로** 센 결과입니다. 둘이 다르면 어느 한쪽의 "세는 방법"이 다른 것입니다.
**끝나면 알게 되는 것.** 같은 기록이라도 **무엇을 넣고 빼는지 · 무엇으로 나누는지 · 몇 자리로 끊는지**에 따라 숫자가 달라집니다. 그것을 정해 둔 것이 지표의 **정의**이고, 예제 2에서 정의대로 다시 셉니다.
**적을 곳.** `my-work/OPS.md` 1.1 표 (질문 · 내 짐작 · 기준 보고서 · 다른 까닭).
```bash
# ① 두 주를 훑기
uv run tools/flow.py logs --from 2026-10-19 --to 2026-10-30 --last 5
# ② 하루만 (숫자만, --last 0)
uv run tools/flow.py logs --from 2026-10-26 --to 2026-10-26 --last 0
# ③ · ④ 팀의 기준 보고서에 같은 기간을
uv run tools/flow.py call base-report requests/two-weeks.json # {"from": "2026-10-19", "to": "2026-10-30"}
uv run tools/flow.py call base-report requests/monday.json # {"from": "2026-10-26", "to": "2026-10-26"}
```
### 이해하기
**실행 기록 한 줄에 무엇이 있나.** [AGENTS.md](AGENTS.md) 2절 "실행 기록 한 줄"입니다. 오늘 질문에 쓰는 칸은 이것뿐입니다.
| 칸 | 뜻 | 오늘 쓰는 곳 |
| --- | --- | --- |
| `started_at` | 실행이 시작된 때 (`2026-10-26T09:05:39`) | 기간 · 날짜별 · 시간대별 |
| `status` | `success` · `error` | 오류 수 · 오류율 |
| `route` | `자동 답변` · `담당자 연결` · `잠시 후`, 오류면 `null` | 갈래별 · 답 못 함 |
| `duration_ms` | 한 실행에 걸린 시간 | p50 · p95 |
| `error_node` · `error_message` | 오류가 난 노드 · 그 말 | 실패 묶음 (Lab 02) |
| `topic` · `release` · `repeat` | 질문 주제 · 버전 · 10분 안에 다시 물음 | 릴리스별 · 다시 묻는 주제 |
| `steps` · `cost_usd` · `llm_calls` | 단계마다 시간 · 비용 · LLM 호출 수 | 과제 2 |
**지표는 "정의 + 계산"입니다.** "오류율"이라는 말만으로는 숫자가 하나로 정해지지 않습니다. 기간의 끝날을 넣나? 0.0315로 쓰나 3.15로 쓰나? 몇 자리에서 끊나? 느린 정도는 평균인가 p95인가? 이것을 미리 정해 둔 것이 정의이고, 정의가 같아야 지난주와 이번 주, 내 숫자와 옆 사람 숫자를 비교할 수 있습니다.
```mermaid
flowchart LR
L[("실행 기록 666줄")] --> A["logs<br/>그대로 셈"]
L --> B["base-report<br/>팀의 방식으로 셈"]
A --> C{"같은 숫자?"}
B --> C
C -->|다르면| D["세는 방법(정의)이<br/>어디서 다른가"]
```
**기대 결과.** `logs`의 두 주 · 하루 숫자와 기준 보고서의 숫자가 **다릅니다.** 어느 칸이 왜 다른지 `OPS.md` 1.1에 적습니다. 무엇이 다른지는 아래 실행 결과를 보기 전에 먼저 찾아 봅니다.
<details><summary>▶ 1단계 · 두 숫자가 다르면 무엇부터 보나</summary>
차이가 **얼마**인지 봅니다. 두 주 666과 기준의 숫자의 차이가 어느 하루의 실행 수와 같다면? `logs --from 2026-10-30 --to 2026-10-30 --last 0`으로 확인해 봅니다.
</details>
<details><summary>▶ 2단계 · 기준의 오류율은 무엇으로 나눴나</summary>
`logs`는 오류율을 `3.2%`로 보여 줍니다. 기준의 `error_rate`는 어떤 모양인가요? 백분율인가 비율인가, 몇 자리인가? 하루만 물었을 때의 `null`은 무엇을 0으로 나눈 것일까요?
</details>
### 실행 결과 · 손으로 읽기 · 기준 보고서
수업 준비 때(10/31) 강사 노트북에서 실제로 나온 출력입니다. 두 주 기록은 바뀌지 않으므로 숫자는 누가 돌려도 같습니다 (`call`의 시간만 다름).
이 예제는 **고치지 않습니다.** 같은 기간을 `logs`(그대로 셈)와 기준 보고서(팀의 방식)에 물어 비교합니다. 고치는 것은 예제 2입니다.
#### ① · ② `logs`로 두 주 · 하루
실행한 명령:
```bash
uv run tools/flow.py logs --from 2026-10-19 --to 2026-10-30 --last 5
uv run tools/flow.py logs --from 2026-10-26 --to 2026-10-26 --last 0
```
출력:
```text
666 run(s) · errors 21 (3.2%)
route: 자동 답변 591 · 담당자 연결 34 · 오류 21 · 잠시 후 20
2026-10-30T17:05:38 success 자동 답변 1311ms 프린터 인쇄 대기열이 안 비워져요
2026-10-30T17:11:46 success 자동 답변 1175ms 메일 메일함 용량이 꽉 찼대요
2026-10-30T17:20:51 success 담당자 연결 79ms 와이파이 회의실 와이파이가 안 잡혀요
2026-10-30T17:24:39 success 자동 답변 1205ms 메일 첨부 파일이 너무 커서 안 보내져요
2026-10-30T17:59:20 success 자동 답변 1076ms 메일 메일함 용량이 꽉 찼대요
70 run(s) · errors 16 (22.9%)
route: 자동 답변 50 · 오류 16 · 담당자 연결 4
```
**왜 이렇게 나왔나**
- 두 주 666건 중 오류 21건(3.2%). 그런데 **하루(10/26 월요일)에 16건**이 몰려 있습니다. 그날의 오류율은 22.9%로, 두 주 평균의 7배쯤입니다. "두 주 동안 오류율 3%"라는 말만으로는 이 사실이 보이지 않습니다.
- 마지막 다섯 줄은 10/30(금) 저녁의 실행입니다. 담당자 연결 한 줄(와이파이, 79ms)이 눈에 띕니다. 답을 찾지 못해 바로 사람에게 넘긴 실행입니다 (Lab 02).
#### ③ · ④ 기준 보고서에 같은 기간
실행한 명령:
```bash
uv run tools/flow.py call base-report requests/two-weeks.json
uv run tools/flow.py call base-report requests/monday.json
```
출력:
```text
HTTP 200 · 1.2s
{ "total": 602, "errors": 21, "error_rate": 3.488372093023256, "avg_ms": 1249.2392026578073,
"by_route": { "자동 답변": 529, "담당자 연결": 32, "잠시 후": 20 } }
HTTP 200 · 0.1s
{ "total": 0, "errors": 0, "error_rate": null, "avg_ms": null, "by_route": {} }
```
**왜 이렇게 나왔나**
| 칸 | `logs` | 기준 보고서 | 다른 까닭 |
| --- | --- | --- | --- |
| 두 주 실행 | 666 | **602** | 666 − 602 = 64 = 10/30 하루의 실행. 기준은 `started_at < to`로 걸러 **끝날을 뺐습니다** (`2026-10-30T09:…`는 `2026-10-30`보다 큰 글자) |
| 오류율 | 3.2% (0.0315) | **3.488…** | 백분율(×100)이고, 끊지 않았고, 분모가 602 |
| 느린 정도 | (p95는 예제 2) | 평균 1249.2ms | 평균만 있음. 대부분이 겪는 시간을 말하지 못함 |
| 오류로 끝난 실행 | 갈래 `오류` 21 | 갈래에 **없음** | `route`가 `null`인 줄을 건너뜀. 갈래를 더하면 581 ≠ 602 |
| 하루 (from = to) | 70 · 오류 16 | **0 · `null`** | 끝날을 빼니 하루를 물으면 아무것도 남지 않고, 0을 0으로 나눠 `null` |
- 기준 보고서는 "고장"이 아닙니다. 200을 주고, 숫자도 그럴듯합니다. **세는 방법이 다를 뿐**인데, 운영 회의에서는 이 숫자로 "두 주에 602건"이라고 말해 왔습니다.
- 이 차이를 `OPS.md` 1.1에 적습니다. 예제 2에서 정의를 정하고, 예제 3에서 경계를 더 재 봅니다.
## 예제 2 · 지표 보고서 (15분)
### 이 실습의 의도
**한 줄로:** 예제 1에서 본 차이를 없애기 위해, AGENTS.md 3절 정의대로 세는 **내 보고서**(`my-log-report`)를 만듭니다. Lab 01의 중심 실습이고, 오늘 만드는 모든 것이 이 보고서를 바탕으로 합니다.
> **생활 속 예 · 가게의 장부 쓰는 법.** 알바생마다 세는 법이 다르면 장부를 믿을 수 없습니다. 그래서 가게는 "마감일 영수증도 넣는다, 금액은 원 단위까지, 환불도 따로 적는다"처럼 **장부 쓰는 법**을 벽에 붙여 둡니다. 예제 2는 그 쓰는 법(정의)을 요청문에 그대로 넣어, 누가 만들어도 같은 숫자가 나오는 보고서를 만드는 실습입니다.
| 예제 1에서 본 것 | 정의 (AGENTS.md 3절) | 확인하는 케이스 |
| --- | --- | --- |
| 마지막 날이 빠짐 | 날짜는 **포함** (`from ≤ 날짜 ≤ to`, 데이터 API가 거름) | 두 주 · 첫 주 · 둘째 주 · 하루 |
| 오류율이 백분율 · 끊지 않음 | `errors / total`, 소수 4자리, 0건이면 0 | 두 주 · 둘째 주 |
| 평균만 있음 | `p50_ms` · `p95_ms` = 오름차순 `ceil(p × n)`번째 | 두 주 · 둘째 주 |
| 오류가 갈래에 없음 | `route`가 `null`이면 `"오류"` | 두 주 · 갈래별 |
**순서.** 같은 테스트를 **기준에 먼저** 돌려 0/5를 봄 (케이스마다 무엇이 틀렸는지) → 요청문을 써서 에이전트가 새 파일을 만들게 함 → push → 5/5.
**끝나면 알게 되는 것.** 정의를 요청문에 적으면 에이전트도 같은 숫자를 낸다는 것, p95가 평균과 어떻게 다른지, 숫자는 LLM이 아니라 Code가 세야 하는 까닭.
**만들 것**: 기간(`from` · `to`)을 받아 실행 기록을 가져와 지표를 계산해 돌려주는 워크플로우.
```bash
uv run tools/flow.py test cases/lab01.json --webhook base-report -v # 고치기 전 (기준에 같은 테스트)
```
| 조건 | 값 |
| --- | --- |
| 파일 · 웹훅 | `my-work/log-report.json` (새로) · `my-log-report` |
| 지표 | `total`, `errors`, `error_rate`, `p50_ms`, `p95_ms`, `by_route` — **정의는 [AGENTS.md](AGENTS.md) 3절** |
| 계산 | Code 노드 하나 (LLM 없음) |
| 기대 결과 | `cases/lab01.json` (두 주 · 갈래별 · 첫 주 · 둘째 주 · 하루) |
### 이해하기
**지금 무엇이 문제인가.** 기준 보고서는 틀린 계산을 한 것이 아니라 **다른 정의**로 셌습니다. 그래서 고치는 일은 "버그 찾기"보다 "정의를 정해 그대로 세기"입니다. 정의가 요청문에 없으면 에이전트도 자기 방식(평균, 보간한 p95, 백분율…)으로 셉니다.
```mermaid
flowchart LR
subgraph 기준
A1(["from · to"]) --> B1["로그 전체를 가져와<br/>started_at < to 로 거름"] --> C1["평균 · 백분율<br/>오류 갈래 없음"]
end
subgraph 내 보고서
A2(["from · to · tag · source"]) --> B2["데이터 API가 거름<br/>(날짜 포함)"] --> C2["Code: 정의대로<br/>p50 · p95 · 소수 4자리 · 오류 갈래"]
end
```
**p95란.** 실행 시간을 짧은 것부터 줄 세웠을 때 95%째에 있는 값입니다. "100명 중 95명은 이 시간 안에 답을 받았다"는 뜻입니다. 오늘 정의는 **가장 가까운 순위**(nearest-rank)입니다: n개를 오름차순으로 놓고 `ceil(0.95 × n)`번째. 예를 들어 값이 10개면 `ceil(9.5)` = 10번째, 666개면 `ceil(632.7)` = 633번째입니다. 평균은 아주 느린 몇 건에 끌려가고, 대부분의 사용자가 겪는 시간을 말하지 못합니다.
| 같은 기록 | 평균 | p50 | p95 |
| --- | --- | --- | --- |
| 1초 × 9건 + 10초 × 1건 | 1.9초 | 1초 | 10초 |
**왜 Code인가.** LLM에게 666줄을 주고 "오류율이 얼마야?"라고 물으면 틀릴 수 있고(세는 일은 LLM이 약함), 줄이 많을수록 돈이 듭니다 (75일차). 숫자는 Code가 정확하게, 빠르게(0.1초 안팎) 셉니다. LLM은 Lab 02 과제 2에서 **센 결과를 글로 설명**할 때만 씁니다.
**테스트 케이스.**
| 케이스 | 보내는 것 | 기대 |
| --- | --- | --- |
| 두 주 전체 | `two-weeks` | `total` 666 · `errors` 21 · `error_rate` 0.0315 · `p50_ms` 1205 · `p95_ms` 2537 |
| 두 주 · 갈래별 | `two-weeks` | 자동 답변 591 · 담당자 연결 34 · 잠시 후 20 · 오류 21 |
| 첫 주 | `week1` | 309 · 0 · 0 |
| 둘째 주 | `week2` | 357 · 21 · 0.0588 · p95 2598 |
| 10월 26일 하루 | `monday` | 70 · 16 |
**기대 결과.** 고치기 전(기준에 같은 테스트) 0/5, 고친 뒤 5/5.
<details><summary>▶ 1단계 · 정의를 요청문에</summary>
"p95를 구해 줘"만 쓰면 에이전트마다 계산이 다릅니다 (평균? 보간?). 3절의 정의를 그대로 요청문에 넣거나 "AGENTS.md 3절 정의대로"라고 씁니다. 반올림 자리, 0건일 때도.
</details>
<details><summary>▶ 2단계 · 거르기는 누가</summary>
기간 · `tag` · `source`로 거르는 일은 데이터 API(`POST /logs/query`)가 합니다 ([AGENTS.md](AGENTS.md) 2절). 워크플로우는 웹훅 본문을 그대로 넘기면 됩니다. 기준처럼 다 가져와서 직접 거르면 정의가 두 곳에 생깁니다.
</details>
<details><summary>▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대</summary>
요청문에 꼭 들어갈 것: **① 파일 · 웹훅 ② 넘길 본문과 기본값 ③ 지표와 정의(반올림 · 0건) ④ 백분위 정의 ⑤ LLM 없음 · 기대 결과**. 아래 빈칸 4개(`____①`~`____④`)를 채워 `my-work/PROMPTS.md`에 쓴 뒤 에이전트에게 보냅니다.
```text
my-work/log-report.json 을 새로 만들어 줘. 웹훅 POST ____①, 이름 "77 Log report (my)".
Get logs(HTTP POST http://data-api:8800/logs/query) 에 웹훅 본문의 from · to · tag · source 를 넘기고, source 가 없으면 ____②.
Compute metrics(Code) 하나에서 AGENTS.md 3절 정의대로 total · errors · error_rate · p50_ms · p95_ms · by_route 를 계산해 줘.
error_rate 는 errors / total 을 소수 ____③자리 반올림, 0건이면 0.
p50 · p95 는 duration_ms 를 오름차순으로 놓고 ____④번째 값 (1부터 셈, 오류 실행도 포함). route 가 null 이면 by_route 의 "오류".
LLM 은 쓰지 않는다. 기대 결과 cases/lab01.json.
```
빈칸 힌트 (답이 아니라 **어디서 찾고 무엇을 생각하나**):
- ① AGENTS.md 3절 계약 표의 "로그 보고서" 줄입니다. 테스트 파일 `cases/lab01.json` 맨 위의 `webhook`과 같아야 합니다.
- ② `source`의 세 값(`all` · `seed` · `live`) 중, 요청에 `source`가 없을 때 두 주 기록과 오늘 기록을 **모두** 보는 값.
- ③ 3절 지표 표의 `error_rate` 줄. 테스트의 기대값 0.0315를 보면 몇 자리인지 보입니다.
- ④ 3절 지표 표의 `p50_ms` · `p95_ms` 줄의 굵은 글씨입니다. 위 "p95란"의 예와 같이.
채운 요청문은 수업 뒤 공개되는 정답본의 요청문 예와 비교합니다.
</details>
```bash
uv run tools/flow.py push my-work/log-report.json
uv run tools/flow.py test cases/lab01.json -v
uv run tools/flow.py call my-log-report requests/week2.json
```
### 실행 결과 · 지표 보고서
수업 준비 때(10/31) 강사 노트북에서 정답 워크플로우로 실제로 나온 출력입니다. 지표 숫자는 두 주 기록을 정의대로 센 것이라 누구에게나 같고, 시간만 조금씩 다릅니다.
#### 고치기 전 · 기준(`base-report`)에 같은 테스트
실행한 명령:
```bash
uv run tools/flow.py test cases/lab01.json --webhook base-report -v
```
출력 (결과 줄만, `-v`의 요청 · 응답은 줄임):
```text
✘ 두 주 전체 · 0.2s · total = 602 (expected 666); error_rate = 3.488372093023256 (expected 0.0315); p50_ms = null (expected 1205); p95_ms = null (expected 2537)
✘ 두 주 · 갈래별 · 0.1s · by_route.자동 답변 = 529 (expected 591); by_route.담당자 연결 = 32 (expected 34); by_route.오류 = null (expected 21)
✘ 첫 주 · 0.1s · total = 245 (expected 309)
✘ 둘째 주 · 0.1s · total = 293 (expected 357); error_rate = 7.167235494880546 (expected 0.0588); p95_ms = null (expected 2598)
✘ 10월 26일 하루 · 0.1s · total = 0 (expected 70); errors = 0 (expected 16)
Passed 0/5
```
**왜 이렇게 나왔나 (고치기 전)**
- **`total`이 모두 작음:** 602 · 245 · 293 · 0. 기간마다 **끝날의 실행 수만큼** 빠졌습니다 (첫 주 309 − 245 = 64 = 10/23, 둘째 주 357 − 293 = 64 = 10/30). 하루를 물으면 끝날이 곧 그날이라 0입니다.
- **`error_rate`가 3.488… · 7.167…:** 백분율에, 반올림이 없고, 분모도 줄어든 `total`입니다. 기대값 0.0315는 "비율, 소수 4자리"입니다.
- **`p50_ms` · `p95_ms`가 `null`:** 기준에는 그런 칸이 없습니다 (`avg_ms`만). `null`은 "그 칸이 없음"입니다.
- **`by_route.오류`가 `null`:** 오류로 끝난 실행을 갈래에서 건너뛰었습니다. 자동 답변 529 · 담당자 연결 32도 끝날이 빠진 만큼 작습니다.
- 0/5는 **예상한 결과**입니다. 다섯 케이스의 ✘가 모두 예제 1에서 찾은 정의의 차이 넷으로 설명됩니다.
#### 고친 뒤 · 내 `my-log-report`
실행한 명령:
```bash
uv run tools/flow.py push my-work/log-report.json
uv run tools/flow.py test cases/lab01.json -v
uv run tools/flow.py call my-log-report requests/week2.json
```
출력 (결과 줄과 `call`):
```text
✔ 두 주 전체 · 0.9s
✔ 두 주 · 갈래별 · 0.1s
✔ 첫 주 · 0.2s
✔ 둘째 주 · 0.1s
✔ 10월 26일 하루 · 0.0s
Passed 5/5
HTTP 200 · 0.1s
{ "total": 357, "errors": 21, "error_rate": 0.0588, "p50_ms": 1171, "p95_ms": 2598,
"by_route": { "오류": 21, "자동 답변": 299, "담당자 연결": 17, "잠시 후": 20 } }
```
**고친 뒤 이렇게 나온 까닭**
| 케이스 | 고치기 전 (기준) | 고친 뒤 | 무엇이 바꿨나 |
| --- | --- | --- | --- |
| 두 주 실행 | 602 | 666 | 거르기를 데이터 API에 맡김 (날짜 **포함**) |
| 두 주 오류율 | 3.488… | 0.0315 | `errors / total`, 소수 4자리 |
| 두 주 p50 · p95 | 없음 | 1205 · 2537ms | 오름차순 `ceil(p × n)`번째 |
| 오류 갈래 | 없음 | 21 | `null` → `"오류"` |
| 하루 | 0 | 70 · 오류 16 | 끝날 포함 |
- **둘째 주 응답을 읽으면:** 357건 중 오류 21, 오류율 0.0588(5.88%). 갈래를 더하면 21 + 299 + 17 + 20 = 357로 `total`과 맞습니다. 기준에서는 맞지 않던 합입니다.
- **p50 1171 · p95 2598:** 절반은 1.2초 안에, 95%는 2.6초 안에 답을 받았습니다. 첫 주 p95(2525)와 비교하면 조금 느려졌습니다. 평균 하나로는 이런 비교가 어렵습니다.
- **0.1초 안팎:** 666줄을 Code 하나에서 셌습니다. LLM은 부르지 않았습니다.
- 이제 이 보고서가 오늘의 **기준 숫자**입니다. 과제 · Lab 02 · Lab 03은 모두 이 파일에 칸을 더해 갑니다.
## 예제 3 · 경계에서 재 보기 (7분, 에이전트 없이)
### 이 실습의 의도
**한 줄로:** 예제 2의 내 보고서와 기준 보고서에 **경계의 질문**(하루만 · 빈 기간 · 주말 · 기간 없음 · `source`)을 같이 던져, 평소에는 안 보이던 정의의 차이를 드러냅니다. 고치는 실습이 아니라 **재 보는** 실습입니다.
> **생활 속 예 · 줄자의 눈금 끝.** 줄자로 1m를 잴 때 "끝 눈금을 넣나"는 보통 상관없어 보입니다. 하지만 1cm짜리를 재면 그 한 칸이 전부입니다. 장부도 그렇습니다. 두 주를 셀 때는 그럴듯하던 장부가 "오늘 하루" · "손님이 없던 날"을 물으면 0이나 오류를 냅니다. 예제 3은 끝 눈금처럼 작은 경계에서 두 장부를 재 보는 실습입니다.
**상황.** 보통 기간(두 주 · 한 주)에서는 기준 보고서도 그럴듯한 숫자를 냅니다. 그런데 운영에서는 "오늘 하루만", "이번 주 아직 0건", "오늘 실습 기록만"처럼 경계의 질문을 자주 합니다. 같은 테스트 `cases/lab01-edges.json`을 두 보고서에 돌립니다.
| 경계 | 보내는 것 | 기대 (정의대로) |
| --- | --- | --- |
| 하루 · from = to | `monday` | 70건 · 오류 16 (끝날 포함) |
| 빈 기간 | 11/02~11/06 (기록 없음) | 200 · `total` 0 · `error_rate` 0 · `p95_ms` 0 |
| 주말 | 10/24~10/25 | 0건 |
| 기간을 비우면 | `{}` | 666 (전체) |
| `source: seed` | 두 주 · 두 주 기록만 | 666 |
| `source: live` | 오늘 실습 기록만 (아직 없음) | 0 · 오류율 0 |
**끝나면 알게 되는 것.** 정의는 경계에서 시험받는다는 것, 0으로 나누기 · 끝날 포함 · 거르기를 **처음부터** 정해 둬야 한다는 것. 그리고 테스트에 경계 케이스를 넣는 까닭.
**적을 곳.** `my-work/OPS.md` 1.2 표 (경계 · 내 보고서 · 기준 보고서 · 기준이 틀린 까닭).
```bash
uv run tools/flow.py test cases/lab01-edges.json -v # 내 보고서
uv run tools/flow.py test cases/lab01-edges.json --webhook base-report # 같은 케이스를 기준에
```
### 이해하기
**경계(edge)란.** 값의 범위 끝에 있는 경우입니다: 0건, 1일, 첫날 · 마지막 날, 거르기를 했을 때 아무것도 남지 않는 경우. 프로그램의 실수는 대부분 경계에서 나옵니다. 보통 경우만 시험하면 "잘 된다"고 믿게 됩니다.
| 경계 | 정의가 없으면 생기는 일 |
| --- | --- |
| 끝날 | `<`와 `≤`의 차이로 하루가 통째로 빠짐 |
| 0건 | `0 / 0` → `NaN` → JSON의 `null` (화면에 빈칸, 다음 계산이 깨짐) |
| 거르기 | `source` · `tag`를 무시하면 늘 전체가 나옴 (오늘 실습 기록만 보고 싶어도) |
**`--webhook base-report`.** 같은 케이스 파일을 다른 웹훅에 돌리는 옵션입니다. 테스트 파일은 그대로 두고 두 워크플로우를 비교할 때 씁니다.
**기대 결과.** 내 보고서 6/6. 기준 보고서는 대부분 ✘ — 어느 케이스가 왜 ✘인지가 이 예제의 답입니다.
<details><summary>▶ 1단계 · `source: live`에 기준이 666을 낸다면</summary>
기준 보고서가 데이터 API에 무엇을 보내는지 봅니다: `uv run tools/flow.py diff reference/baseline-report.json my-work/log-report.json`. `Get logs` 노드의 본문을 비교해 보세요.
</details>
### 실행 결과 · 경계에서 재 보기
수업 준비 때(10/31) 강사 노트북에서 실제로 나온 출력입니다.
이 예제는 **고치지 않습니다.** 같은 케이스 파일을 내 보고서와 기준 보고서에 돌려 비교합니다. 기준을 고치지 않는 것은, 오늘은 기준과 **비교하는** 연습이기 때문입니다.
#### 내 보고서 (`my-log-report`)
실행한 명령:
```bash
uv run tools/flow.py test cases/lab01-edges.json -v
```
출력 (결과 줄만):
```text
✔ before: POST http://localhost:8800/logs-reset · HTTP 200 · 0.0s · {"cleared": 0}
✔ 하루 · from = to (포함) · 0.0s
✔ 빈 기간 · 0건이어도 200 · 0.1s
✔ 주말 · 실행이 없는 날 · 0.0s
✔ 기간을 비우면 전체 · 0.1s
✔ source seed · 두 주 기록만 · 0.0s
✔ source live · 오늘 실습 기록 (아직 없음) · 0.0s
Passed 6/6
```
**왜 이렇게 나왔나:** 첫 줄 `before`는 테스트가 먼저 오늘 실습 기록을 비운 것입니다 (`live`가 0이어야 하므로). 내 보고서는 거르기를 데이터 API에 맡기고, 0건이면 나누지 않으므로 경계에서도 정의대로입니다. 빈 기간 · 주말은 `total` 0 · `error_rate` 0 · `p95_ms` 0으로 200을 줍니다.
#### 같은 케이스를 기준에 (`base-report`)
실행한 명령:
```bash
uv run tools/flow.py test cases/lab01-edges.json --webhook base-report
```
출력:
```text
✘ 하루 · from = to (포함) · 0.1s · total = 0 (expected 70); errors = 0 (expected 16)
✘ 빈 기간 · 0건이어도 200 · 0.1s · error_rate = null (expected 0); p95_ms = null (expected 0)
✔ 주말 · 실행이 없는 날 · 0.1s
✔ 기간을 비우면 전체 · 0.0s
✘ source seed · 두 주 기록만 · 0.1s · total = 602 (expected 666)
✘ source live · 오늘 실습 기록 (아직 없음) · 0.1s · total = 666 (expected 0); error_rate = 3.153153153153153 (expected 0)
Passed 2/6
```
**왜 이렇게 나왔나 (케이스마다)**
| 경계 | 기준의 결과 | 까닭 |
| --- | --- | --- |
| 하루 | 0건 | `started_at < to`로 끝날을 뺌 → 하루를 물으면 그날이 통째로 빠짐 |
| 빈 기간 | 오류율 · p95 `null` | `0 / 0` = `NaN` → JSON으로 `null`. p95 칸은 원래 없음 |
| 주말 ✔ | 0건 | 실행이 없는 날이라 어떻게 세도 0 — **우연히** 맞음 |
| 기간 없음 ✔ | 666 | 거를 것이 없으면 끝날 문제도 없음 — 역시 우연 |
| `source: seed` | 602 | `source`는 맞았지만 끝날 문제가 그대로 |
| `source: live` | **666** · 3.15… | 기준은 `Get logs`에 늘 `{source: 'all'}`만 보내고 본문의 `source`를 **무시**. 오늘 기록만 묻는데 두 주 전체를 셈 |
- 기준이 맞은 두 케이스(주말 · 기간 없음)는 정의가 맞아서가 아니라 **경계가 문제를 가려서** 맞았습니다. 보통 경우만 시험하면 이렇게 "잘 된다"고 믿게 됩니다.
- `source: live`의 666은 Lab 03에서 위험합니다. "고친 뒤 오늘 기록만" 보려고 해도 두 주 전체를 보게 되기 때문입니다. 경계 케이스를 테스트에 넣어 두면 이런 일을 미리 막습니다.
## 과제 1 · 날짜별 · 릴리스별 (8분)
### 이 실습의 의도
**한 줄로:** 두 주 합계를 **날짜로** 나눠 오류가 언제 몰렸는지 보고, **릴리스로** 나눠 새 버전의 답이 도움이 됐는지(다시 묻는 비율) 봅니다.
> **생활 속 예 · 가계부의 월 합계와 날짜별 칸.** 한 달 지출이 늘었다는 것만으로는 무엇을 줄일지 모릅니다. 날짜별 칸을 보면 "15일에 한 번 크게 썼다"가 보입니다. 새로 바꾼 장보기 앱도 "그 앱으로 산 날만" 따로 봐야 좋아졌는지 압니다. 과제 1은 보고서에 날짜별 칸과 버전별 칸을 더하는 실습입니다.
팀장이 요청했습니다.
> "둘째 주에 오류가 늘었다는데, 매일 그런 건지 어느 날만 그런 건지 알고 싶어요. 그리고 지난주에 새 버전(1.1.0)과 카나리(1.2.0)를 내보냈잖아요. 버전마다 사람들이 같은 걸 다시 묻는 비율도 봐 주세요."
| 조건 | 값 |
| --- | --- |
| 날짜별 | `by_day` — 날짜마다 `total` · `errors` · `error_rate`, 날짜순 ([AGENTS.md](AGENTS.md) 3절) |
| 릴리스별 | `by_release` — `자동 답변`인 실행만, 릴리스마다 `answers` · `repeats` · `repeat_rate` (3절) |
| 기대 결과 | `cases/lab01-challenge.json`, `lab01.json`도 그대로 |
**먼저 생각해 보기 (2분)**: 다시 묻는 비율을 왜 `자동 답변`인 실행만으로 셀까요? 1.2.0은 몇 건이었나요 — 그 비율을 믿어도 될까요?
```bash
uv run tools/flow.py test cases/lab01-challenge.json -v # 고치기 전
```
### 이해하기
**묶어서 세기(group by).** 같은 칸(날짜, 릴리스)의 값이 같은 줄끼리 모아 각각 셉니다. 오늘 보고서의 지표 대부분은 "무엇으로 묶고, 무엇을 세고, 무엇으로 나누나"의 세 가지로 정해집니다.
| 지표 | 묶는 칸 | 세는 것 | 나누는 것 |
| --- | --- | --- | --- |
| `by_day` | `started_at` 앞 10자 | 실행 · 오류 | 그날 실행 |
| `by_release` | `release` (자동 답변만) | 답 · 다시 물음 | 그 릴리스의 답 |
**분모를 고르는 일.** "다시 묻는 비율"의 분모가 전체 실행이면, 답을 못 한 실행(담당자 연결 · 오류)도 들어가 버전의 **답의 질**과 상관없는 것이 섞입니다. 답을 준 실행만 분모로 해야 "그 버전의 답이 도움이 됐나"를 잽니다.
**표본 크기.** 1.2.0은 카나리(일부 사용자만)라 답이 8건뿐입니다. 8건 중 5건이면 62.5%지만, 몇 건만 달라져도 크게 바뀝니다. 비율 옆에는 늘 **건수**를 같이 봅니다 (74일차 카나리).
**기대 결과.** 고치기 전 0/6, 고친 뒤 6/6. 10/26은 70건 중 오류 16, 날짜는 평일 10일.
<details><summary>▶ 1단계 · 날짜순으로</summary>
묶은 결과는 객체(`{"2026-10-19": {...}, ...}`)입니다. 키를 정렬해서 만들면 화면에도 날짜순으로 나옵니다.
</details>
<details><summary>▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대</summary>
요청문에 꼭 들어갈 것: **① 묶는 칸 ② 거르는 조건(자동 답변만) ③ 세는 것 ④ 앞의 지표를 두기**. 아래 빈칸 4개를 채워 `my-work/PROMPTS.md`에 쓴 뒤 에이전트에게 보냅니다.
```text
my-work/log-report.json 의 Compute metrics 에 by_day 와 by_release 를 더해 줘.
by_day: started_at 앞 ____①자(날짜)로 묶어 {"2026-10-26": {total, errors, error_rate}}, 날짜순.
by_release: route 가 "____②" 인 실행만 release 로 묶어 {answers, repeats, repeat_rate}.
repeats 는 repeat 가 ____③ 인 수, repeat_rate = repeats / answers (소수 4자리).
앞의 지표는 ____④ 둔다. 기대 결과 cases/lab01-challenge.json, cases/lab01.json 도.
```
빈칸 힌트:
- ① `started_at`(`2026-10-26T09:05:39`)에서 날짜까지가 몇 글자인지 셉니다.
- ② `route`의 세 값 중 "답을 준" 갈래입니다.
- ③ AGENTS.md 2절 `repeat`의 값 중 "다시 물었다"는 쪽.
- ④ 같은 파일에 더하는 것이라 예제 2의 지표를 어떻게 하나요? `lab01` 5/5를 지켜야 합니다.
</details>
```bash
uv run tools/flow.py push my-work/log-report.json
uv run tools/flow.py test cases/lab01-challenge.json -v
uv run tools/flow.py test cases/lab01.json
```
### 실행 결과 · 날짜별 · 릴리스별
수업 준비 때(10/31) 강사 노트북에서 실제로 나온 출력입니다.
#### 고치기 전 · 예제 2의 보고서
실행한 명령:
```bash
uv run tools/flow.py test cases/lab01-challenge.json -v
```
출력 (결과 줄만):
```text
✘ 날짜별 · 월요일 · 0.1s · by_day.2026-10-26.total = null (expected 70); by_day.2026-10-26.errors = null (expected 16); by_day.2026-10-26.error_rate = null (expected 0.2286)
✘ 날짜별 · 목요일 · 0.1s · by_day.2026-10-29.total = null (expected 97); by_day.2026-10-29.errors = null (expected 5)
✘ 날짜는 10일 · 0.1s · by_day has none of ['"2026-10-19"']
✘ 릴리스별 다시 묻기 · 1.2.0 · 0.1s · by_release has none of ['"1.2.0": {"answers": 8, "repeats": 5, "repeat_rate": 0.625}']
✘ 릴리스별 다시 묻기 · 1.0.0 · 1.1.0 · 0.1s · by_release has none of ['"1.0.0": {"answers": 402, "repeats": 19, "repeat_rate": 0.0473}']
✘ 1.1.0 · 0.1s · by_release has none of ['"1.1.0": {"answers": 181, "repeats": 14, "repeat_rate": 0.0773}']
Passed 0/6
```
**왜 이렇게 나왔나 (고치기 전)**
- 예제 2의 보고서에는 `by_day` · `by_release` 칸이 아직 없습니다. 날짜 케이스는 `null`, 릴리스 케이스는 찾을 글자가 없다(`has none of`)고 합니다.
- 릴리스 케이스가 `equals`가 아니라 글자 찾기(`contains`)인 까닭: 키 `1.2.0`에 점이 있어 `by_release.1.2.0.answers`처럼 내려갈 수 없습니다. 그래서 JSON 글자 `"1.2.0": {"answers": 8, …}`를 통째로 찾습니다 (AGENTS.md 7절).
- 0/6은 **예상한 결과**입니다.
#### 고친 뒤
실행한 명령:
```bash
uv run tools/flow.py push my-work/log-report.json
uv run tools/flow.py test cases/lab01-challenge.json -v
uv run tools/flow.py test cases/lab01.json
```
출력 (결과 줄만):
```text
✔ 날짜별 · 월요일 · 1.0s
✔ 날짜별 · 목요일 · 0.1s
✔ 날짜는 10일 · 0.2s
✔ 릴리스별 다시 묻기 · 1.2.0 · 0.1s
✔ 릴리스별 다시 묻기 · 1.0.0 · 1.1.0 · 0.1s
✔ 1.1.0 · 0.0s
Passed 6/6
(lab01 5/5)
```
**고친 뒤 이렇게 나온 까닭**
| 볼 것 | 값 | 읽는 법 |
| --- | --- | --- |
| 10/26 (월) | 70건 · 오류 16 · 0.2286 | 두 주 오류 21건 중 16건이 이날 |
| 10/29 (목) | 97건 · 오류 5 | 나머지 5건은 목요일 |
| 날짜 | 평일 10일 (10/24 · 25 없음) | 주말에는 실행이 없음 |
| 1.0.0 | 답 402 · 다시 물음 19 · 4.73% | 기준 버전 |
| 1.1.0 | 답 181 · 다시 물음 14 · 7.73% | 새 버전 — 다시 묻는 비율이 올랐음 |
| 1.2.0 (카나리) | 답 **8** · 다시 물음 5 · 62.5% | 비율은 높지만 8건뿐 |
- 팀장의 첫 질문 "매일 그런가, 어느 날만 그런가"의 답: **월요일 하루**(16건)와 목요일(5건)입니다. 둘째 주 다른 날에는 오류가 없습니다. 어느 **시간**인지는 Lab 02 예제 2에서 봅니다.
- 둘째 질문 "버전마다 다시 묻는 비율"의 답: 1.1.0이 1.0.0보다 높고(7.73% > 4.73%), 1.2.0은 62.5%지만 **8건으로는 판단하기 이릅니다.** 비율과 함께 건수(`answers`)를 꼭 보고합니다.
- 1.0.0 + 1.1.0 + 1.2.0의 답 = 402 + 181 + 8 = 591 = 두 주 `자동 답변` 수. 분모를 "답을 준 실행"으로 정했으니 맞습니다.
- `lab01` 5/5: 같은 파일에 칸을 더했지만 앞의 지표는 그대로입니다.
## 과제 2 · 어느 단계가 느린가 · 얼마 들었나 (10분)
### 이 실습의 의도
**한 줄로:** 실행 전체의 p95만으로는 **어디가** 느린지 모릅니다. 단계(FAQ 검색 · LLM 답)마다 시간을 재고, 두 주 동안 LLM에 얼마를 썼는지(비용 · 실행당 비용 · 호출 수)를 보고서에 더합니다.
> **생활 속 예 · 택배의 구간별 시간.** 택배가 늦었다고 화내기 전에 배송 조회를 보면 "집하 → 허브 → 배송" 구간마다 시간이 찍혀 있습니다. 허브에서 하루를 묵었다면 기사님을 재촉해도 소용없습니다. 과제 2는 헬프데스크의 한 번의 실행을 구간(단계)으로 나눠 재고, 택배비(LLM 비용)도 함께 적는 실습입니다.
운영팀이 물었습니다.
> "p95가 2.5초쯤인데, FAQ 검색이 느린 건가요 LLM이 느린 건가요? 그리고 두 주에 LLM 비용이 얼마나 들었는지, 한 번에 얼마쯤인지도요."
| 조건 | 값 |
| --- | --- |
| 단계별 | `steps.faq_ms` · `steps.answer_ms` — 그 단계 값이 0보다 큰 실행만, `{runs, p50, p95}` ([AGENTS.md](AGENTS.md) 3절) |
| 비용 | `cost_usd`(합, 소수 5자리) · `cost_per_run`(실행당, 소수 6자리) · `llm_calls`(합) |
| 기대 결과 | `cases/lab01-challenge2.json`, `lab01` · `lab01-challenge`도 그대로 |
**먼저 생각해 보기 (2분)**: `잠시 후`로 끝난 실행은 FAQ도 LLM도 거치지 않아 두 단계 모두 0ms입니다. 이 실행들을 단계 p50에 넣으면 무슨 일이 생길까요?
```bash
uv run tools/flow.py test cases/lab01-challenge2.json -v # 고치기 전
```
### 이해하기
**단계(step) 시간.** 한 실행의 `duration_ms`는 여러 단계의 합입니다. 오늘 헬프데스크는 `faq_ms`(FAQ 검색)와 `answer_ms`(LLM 답)를 기록합니다. 전체가 느릴 때 어느 단계가 늘었는지 보면 **고칠 곳**이 정해집니다 (72일차 timeout, 75일차 비용).
```mermaid
flowchart LR
Q(["질문"]) --> F["FAQ 검색<br/>faq_ms"] --> A{{"LLM 답<br/>answer_ms"}} --> R(["답"])
F -.->|없음| N["담당자 연결<br/>answer_ms 0"]
```
**거치지 않은 단계는 뺀다.** `잠시 후`(20건)는 두 단계 모두 0, `담당자 연결`(34건)은 답 단계가 0입니다. 0을 넣으면 "빠른 실행"처럼 보여 단계의 진짜 시간이 작게 나옵니다. 그래서 단계마다 **값이 0보다 큰** 실행만 모으고, 몇 건을 모았는지(`runs`)도 함께 씁니다.
**기간을 나누면 다른 문제가 보인다.** 두 주 전체의 FAQ 단계 p95는 작지만, 하루만 떼어 보면 다를 수 있습니다. 케이스에 "월요일 · FAQ 단계가 느림"이 있는 까닭입니다.
**비용.** 기록마다 `cost_usd`(그 실행의 LLM 비용)가 있습니다. 합은 두 주 예산을, 실행당 비용은 "질문 하나에 얼마"를 말합니다 (75일차 비용 · 예산). 0건이면 나누지 않고 0.
**기대 결과.** 고치기 전 0/5, 고친 뒤 5/5. 테스트 줄 끝에 `596 calls · $0.078360`처럼 보이는 것은 응답의 `llm_calls` · `cost_usd`를 flow.py가 함께 보여 주는 것입니다.
<details><summary>▶ 1단계 · 백분위 함수를 다시 쓰기</summary>
예제 2의 p50 · p95 계산(오름차순 · `ceil(p × n)`번째)을 단계 값에도 그대로 씁니다. 같은 정의를 두 번 쓰지 말고 같은 함수를 부르게 요청합니다.
</details>
<details><summary>▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대</summary>
요청문에 꼭 들어갈 것: **① 단계 값을 모으는 조건 ② 비용 반올림 ③ 호출 수 ④ 앞의 테스트**. 아래 빈칸 4개를 채워 `my-work/PROMPTS.md`에 쓴 뒤 에이전트에게 보냅니다.
```text
my-work/log-report.json 의 Compute metrics 에 steps 와 비용을 더해 줘.
steps: faq_ms · answer_ms 마다, steps 의 그 값이 ____① 실행만 모아 {runs, p50, p95} (백분위 정의는 앞과 같음, 없으면 runs 0 · p50 0 · p95 0).
cost_usd: 실행마다 cost_usd 의 합, 소수 ____②자리 반올림. cost_per_run: cost_usd / total, 소수 6자리 (0건이면 0).
llm_calls: 실행마다 llm_calls 의 ____③.
앞의 지표는 그대로. 기대 결과 cases/lab01-challenge2.json, ____④ 도 그대로 통과.
```
빈칸 힌트:
- ① 위 "먼저 생각해 보기"의 답입니다. 거치지 않은 단계의 값은 얼마로 기록되나요?
- ② 3절의 `cost_usd` 줄. 기대값 0.07836의 자리 수를 셉니다.
- ③ 실행마다 `llm_calls`는 0 또는 1입니다. 두 주 전체로는?
- ④ 같은 파일을 고쳤으니 다시 돌려 볼 테스트 두 개.
</details>
```bash
uv run tools/flow.py push my-work/log-report.json
uv run tools/flow.py test cases/lab01-challenge2.json -v
uv run tools/flow.py test cases/lab01.json
uv run tools/flow.py test cases/lab01-challenge.json
```
### 실행 결과 · 단계별 · 비용
수업 준비 때(10/31) 강사 노트북에서 실제로 나온 출력입니다.
#### 고치기 전 · 과제 1까지의 보고서
실행한 명령:
```bash
uv run tools/flow.py test cases/lab01-challenge2.json -v
```
출력 (결과 줄만):
```text
✘ 두 주 · 단계별 p95 · 0.1s · steps.faq_ms.p95 = null (expected 39); steps.answer_ms.p95 = null (expected 2509); steps.answer_ms.p50 = null (expected 1179)
✘ 두 주 · 단계마다 잰 실행 수 · 0.1s · steps.faq_ms.runs = null (expected 646); steps.answer_ms.runs = null (expected 596)
✘ 월요일 · FAQ 단계가 느림 · 0.1s · steps.faq_ms.p95 = null (expected 1000); steps.answer_ms.p95 = null (expected 2482)
✘ 두 주 · 비용 · 0.1s · cost_usd = null (expected 0.07836); cost_per_run = null (expected 0.000118); llm_calls = null (expected 596)
✘ 빈 기간 · 비용 0 · 0.0s · cost_usd = null (expected 0); cost_per_run = null (expected 0); llm_calls = null (expected 0); steps.faq_ms.p95 = null (expected 0)
Passed 0/5
```
**왜 이렇게 나왔나 (고치기 전)**
- `steps` · `cost_usd` · `cost_per_run` · `llm_calls` 칸이 아직 없어 모두 `null`입니다. 기록 한 줄에는 `steps`와 `cost_usd`가 있지만, 보고서가 아직 세지 않았습니다.
- 0/5는 **예상한 결과**입니다.
#### 고친 뒤
실행한 명령:
```bash
uv run tools/flow.py push my-work/log-report.json
uv run tools/flow.py test cases/lab01-challenge2.json -v
uv run tools/flow.py test cases/lab01.json
uv run tools/flow.py test cases/lab01-challenge.json
```
출력 (결과 줄만):
```text
✔ 두 주 · 단계별 p95 · 0.9s · 596 calls · $0.078360
✔ 두 주 · 단계마다 잰 실행 수 · 0.1s · 596 calls · $0.078360
✔ 월요일 · FAQ 단계가 느림 · 0.2s · 50 calls · $0.006450
✔ 두 주 · 비용 · 0.1s · 596 calls · $0.078360
✔ 빈 기간 · 비용 0 · 0.0s · 0 calls · $0.000000
Passed 5/5
(lab01 5/5 · lab01-challenge 6/6)
```
**고친 뒤 이렇게 나온 까닭**
| 볼 것 | 두 주 | 월요일만 | 읽는 법 |
| --- | --- | --- | --- |
| FAQ 단계 p95 | **39ms** (646건) | **1000ms** | 평소 FAQ는 빠름. 월요일에는 timeout(1초)에 걸린 실행이 p95를 차지 |
| 답 단계 p50 · p95 | 1179 · **2509ms** (596건) | p95 2482ms | 평소 느린 것은 LLM 답 단계 |
| 비용 | 0.07836달러 | 0.00645달러 | 두 주 LLM 비용 |
| 실행당 비용 | 0.000118달러 | | 질문 하나에 약 0.0001달러 |
| LLM 호출 | 596 | 50 | 답을 준 실행마다 한 번 |
- **운영팀 질문의 답:** 평소 p95 2.5초의 대부분은 **LLM 답 단계**(p95 2509ms)입니다. FAQ 검색은 39ms입니다. 그런데 **월요일만** 떼어 보면 FAQ 단계 p95가 1000ms — 그날은 FAQ 검색이 문제였습니다. 같은 지표도 기간을 나누면 다른 고칠 곳이 보입니다 (Lab 02 · 03).
- **`runs` 646 · 596:** FAQ 단계는 `잠시 후` 20건을 뺀 646건, 답 단계는 답을 준 591건 + 답 도중 실패한 5건 = 596건입니다. 0인 값을 넣었다면 단계 p50이 작게 나왔을 것입니다.
- **테스트 줄 끝의 `596 calls · $0.078360`:** 응답에 `llm_calls` · `cost_usd`가 생기자 flow.py가 함께 보여 주는 것입니다 (75일차 기능). 빈 기간은 `0 calls · $0.000000`.
- `lab01` 5/5 · `lab01-challenge` 6/6: 앞의 지표는 그대로입니다.
## 막혔을 때
| 증상 | 할 일 |
| --- | --- |
| `total`이 맞지 않음 | `Get logs`가 웹훅 본문의 `from` · `to`를 넘기는지 (직접 거르지 않음), `source` 기본값이 `all`인지 |
| `p95_ms`가 1~2 다름 | 오름차순 · `ceil(p × n)`번째(1부터 셈) · 오류 실행도 포함하는지 |
| `error_rate`가 0.03149… | 소수 4자리 반올림 |
| 빈 기간에 `null` | 0건이면 나누지 않고 0 |
| `by_route.오류`가 없음 | `route`가 `null`인 실행을 `"오류"`로 |
| `by_release`를 `equals`로 못 찾음 | 키에 점(`1.2.0`)이 있어 케이스는 `contains`로 JSON 글자를 찾음 — 모양을 정확히 `{"answers", "repeats", "repeat_rate"}` 순서로 |
| 단계 p50이 너무 작음 | 0인 단계 값을 뺐는지 |
| 계속 안 됨 | `cp reference/lab01-example2-report.json my-work/log-report.json` 후 push (예제 2까지) |
## 확인
- [ ] 예제 1: `OPS.md` 1.1 (내 짐작 · 기준 보고서 · 다른 까닭).
- [ ] 예제 2: `cases/lab01.json` 5/5 (고치기 전 기준에 0/5).
- [ ] 예제 3: `cases/lab01-edges.json` 내 보고서 6/6, 기준이 ✘인 까닭을 `OPS.md` 1.2에.
- [ ] 과제 1: `cases/lab01-challenge.json` 6/6, `lab01`도 통과.
- [ ] 과제 2: `cases/lab01-challenge2.json` 5/5, `lab01` · `lab01-challenge`도 통과, `OPS.md` 2절.
- [ ] `my-work/PROMPTS.md`.