Files

6408 lines
219 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.
# 72일차 Lab 테스트 결과 · 실제로 만든 시나리오와 `test -v` 결과
> **정답 자료** · 72일차 n8n 워크플로우를 **만드는 프롬프트(codex) → push → `call -v` · `bench` · `test -v` 결과**를 학생이 가는 순서대로 한 문서에 모았습니다. 수업이 끝난 뒤 공개합니다.
- 실행: 2026-10-24, 실습을 Lab마다 5개(예제 3 + 과제 2)로 늘린 뒤 강사 노트북의 65일차 n8n 2.42.6 + 72일차 판 데이터 API(고장 스위치 · 캐시), OpenAI `gpt-4.1-mini`, 케이스 파일마다 **한 번씩** (Lab 02 과제 1만 `--repeat 3`), 모두 45단계
- 명령: `~/aio-01d4-stu/Last3Mon/72`에서 `uv run tools/flow.py test <케이스 파일> -v`, 한 번 부르기는 `call ... -v`, 시간 재기는 `bench ... -n 5`
- 아래 단계마다 **실행한 명령**과 **화면에 나온 출력 전부**를 그대로 붙였습니다. 같은 출력이 `NN-<이름>.txt`에, `test`의 보고서가 `NN-<이름>.json`(케이스마다 `request` · `response` · `problems` · `seconds`)에 있습니다 (`call` · `bench` · `fault` · `llm` · `doctor`만 한 단계는 보고서 없음).
- LLM 문장과 **시간은 실행마다 다릅니다.** 이번 실행에서 정답이 아닌 이유로 실패한 케이스는 셋이고 모두 같은 까닭입니다: **검토 LLM이 접수 답의 "처리 중"을 다른 말("승인 대기 중" 등)로 바꿔 씀** (02 · 12 · 15단계). 검토 LLM을 지우는 Lab 02 과제 1 뒤로는 나오지 않습니다.
## `-v`가 보여 주는 것
```text
✔ 다른 말로 묻는 VPN → FAQ 번호 캐시 · 0.1s ← 케이스 이름 · 시간 (틀리면 틀린 곳)
→ request POST /webhook/my-helpdesk ← 보낸 요청 JSON
{ "question": "VPN 연결이 계속 끊어지는데요", "user": "최유진" }
← response HTTP 200 ← 받은 응답 JSON
{ "route": "자동 답변", ..., "cached": true, "cache_by": "faq", "llm_calls": 0 }
```
오늘은 응답의 `timings` · `steps` · `llm_calls` · `cached` · `cache_by` · `reason`을 봐야 **왜 빨라졌는지, 어느 길로 갔는지** 알 수 있습니다.
## 이 문서 하나로 따라가기
단계마다 **① codex에 넣는 프롬프트 → ② codex가 `my-work/*.json`을 쓰고 `check` → ③ 내가 `push` → ④ 내가 `test -v` · `bench`** 순서입니다. 재기만 하는 예제(Lab 01 예제 1 · 3, Lab 02 예제 3)와 고장을 먼저 겪는 단계는 프롬프트가 없습니다.
```mermaid
flowchart LR
P["① 프롬프트<br/>터미널 ① codex"] --> C["② codex<br/>my-work/*.json · check"] --> U["③ push<br/>터미널 ②"] --> T["④ test -v · bench<br/>요청 · 응답 · 시간"]
T -->|✘| X["실패 줄 · 보고서 경로를<br/>codex에 그대로"] --> C
```
- **프롬프트는 정답본(`LAB-0x-A-*.md`)의 요청문 예**를 그대로 썼습니다 (실습 문서 빈칸 채우기 뼈대의 `____`를 채운 글자).
- **이 기록의 워크플로우는 수업 준비 때 만든 정답 파일**입니다. 프롬프트를 codex에 보내 다시 만들어 재지는 않았습니다 (68일차부터 수업 준비는 정답 워크플로우만 실행).
- 학생은 `push my-work/helpdesk.json`, 이 기록은 같은 웹훅(`my-helpdesk`)의 정답 파일을 push했습니다. 그래서 `test` 명령과 결과 모양은 같습니다.
- 시간을 보는 케이스(`max_seconds`)는 노트북 · 네트워크 · OpenAI 응답에 따라 흔들립니다. 실패하면 먼저 같은 테스트를 한 번 더 돌려 봅니다.
## 시나리오 한눈에
```mermaid
flowchart LR
L1["Lab 01 (00~11)<br/>기준 p50 2.69초 · LLM 98%<br/>과제 1 0/4 → 4/4<br/>과제 2 0/4 → 4/4"] --> L2["Lab 02 (12~27)<br/>캐시 0.05초<br/>과제 1 1/5 → 15/15 · p50 2.22 → 1.12초<br/>과제 2 4/6 → 6/6"]
L2 --> L3["Lab 03 (28~44)<br/>예제 1 · 2 · 3 모두 고치기 전 → 4/4<br/>과제 1 4/4 · 과제 2 3/4 → 4/4<br/>doctor ready"]
```
| 단계 | 먼저 올린 것 (`push`) | 테스트 · 재기 | 결과 | 파일 |
| --- | --- | --- | --- | --- |
| 00 | 기준 `reference/baseline-helpdesk.json` | `call` + `bench` × 3 | 첫 호출 4.4초 · VPN p50 2.69 / p95 2.98초 | `00-lab01-baseline` |
| 01 | (같음) | `lab01 --webhook base-helpdesk` | **1/4** (시간 칸 없음) | `01-lab01-baseline` |
| 02 | 예제 2 `reference/lab01-example2-timings.json` | `lab01` | 3/4 (접수 답에서 검토 LLM이 "처리 중"을 바꿈) | `02-lab01` |
| 03 | (같음) | `call` VPN | `timings` · `llm_calls: 3` | `03-lab01-call` |
| 04 | (같음) | 예제 3 `bench` × 3 + `call` × 2 | VPN 2.91 · 접수 2.99 · 점심 2.48초 (p50), LLM 98% | `04-lab01-kinds` |
| 05 | (같음, 과제 1 전) | `lab01-challenge` | **0/4** | `05-lab01-challenge-before` |
| 06 | 과제 1 [`72-lab01-challenge-timings.json`](72-lab01-challenge-timings.json) | `lab01-challenge` | 4/4 | `06-lab01-challenge` |
| 07 | (같음) | `lab01` | 4/4 | `07-lab01-after-challenge` |
| 08 | (같음, 과제 2 전) | `lab01-challenge2` | **0/4** | `08-lab01-challenge2-before` |
| 09 | 과제 2 [`72-lab01-challenge2-budget.json`](72-lab01-challenge2-budget.json) | `lab01-challenge2` | 4/4 | `09-lab01-challenge2` |
| 10 | (같음) | `lab01-challenge` | 4/4 | `10-lab01-challenge-after2` |
| 11 | (같음) | `lab01` | 4/4 | `11-lab01-after2` |
| 12 | 예제 1 `reference/lab02-example1-rules.json` | `lab02` | 7/9 (캐시 케이스 · 접수 답 문장) | `12-lab02-rules` |
| 13 | (같음) | `bench` 프린터 | p50 2.45초 | `13-lab02-rules-bench` |
| 14 | 예제 2 `reference/lab02-example2-cache.json` | `lab02` | 9/9 (같은 질문 0.1초) | `14-lab02-cache` |
| 15 | (같음) | `lab01` | 3/4 (접수 답 문장) | `15-lab01-cache` |
| 16 | (같음) | `cache-stats` | entries 2 · hits 4 · misses 8 | `16-cache-stats` |
| 17 | (같음) | 예제 3 캐시 씀 · 안 씀 `bench` | p50 **0.05** · 1.99초 | `17-cache-hit-bench` |
| 18 | (같음, 과제 1 전) | `lab02-challenge` | **1/5** | `18-lab02-challenge-before` |
| 19 | (같음) | `bench` VPN 캐시 없이 | p50 2.22 / p95 2.45초 | `19-bench-before` |
| 20 | 과제 1 [`72-lab02-challenge-fast.json`](72-lab02-challenge-fast.json) | `lab02-challenge --repeat 3` | 15/15 | `20-lab02-challenge` |
| 21 | (같음) | `lab02` | 9/9 | `21-lab02-after-challenge` |
| 22 | (같음) | `lab01` | 4/4 | `22-lab01-after-challenge` |
| 23 | (같음) | `bench` VPN 캐시 없이 | p50 **1.12** / p95 1.32초 | `23-bench-after` |
| 24 | (같음, 과제 2 전) | `lab02-challenge2` | **4/6** | `24-lab02-challenge2-before` |
| 25 | 과제 2 [`72-lab02-challenge2-faqcache.json`](72-lab02-challenge2-faqcache.json) | `lab02-challenge2` | 6/6 (다른 말 VPN 0.1초) | `25-lab02-challenge2` |
| 26 | (같음) | `lab02-challenge` | 5/5 | `26-lab02-challenge-after2` |
| 27 | (같음) | `lab02` | 9/9 | `27-lab02-after2` |
| 28 | Lab 02 예제 2 다시 | `fault faq slow · error` + `call` | 느림 11.5초 · 오류 HTTP 500 | `28-lab03-fault-before` |
| 29 | 예제 1 `reference/lab03-example1-resilient.json` | `lab03` | 4/4 (느림 4.6초 · 오류 0.6초) | `29-lab03` |
| 30 | (같음) | `lab02` | 9/9 | `30-lab02-resilient` |
| 31 | (같음, 예제 2 전) | `llm down` + `call` + `llm up` | HTTP 500 · 1.3초 | `31-llm-down-before` |
| 32 | (같음) | `lab03-llm-down` | **1/4** | `32-lab03-llm-down-before` |
| 33 | 예제 2 `reference/lab03-example2-llm-fallback.json` | `lab03-llm-down` | 4/4 | `33-lab03-llm-down` |
| 34 | (같음) | `lab02` | 9/9 | `34-lab02-llm-fallback` |
| 35 | (같음) | `lab03` | 4/4 | `35-lab03-llm-fallback` |
| 36 | (같음, 예제 3 전) | `lab03-ticket` | **2/4** | `36-lab03-ticket-before` |
| 37 | 예제 3 `reference/lab03-example3-ticket-down.json` | `lab03-ticket` | 4/4 | `37-lab03-ticket` |
| 38 | (같음) | `lab03` | 4/4 | `38-lab03-after-ticket` |
| 39 | (같음) | `lab03-llm-down` | 4/4 | `39-lab03-llm-down-after-ticket` |
| 40 | 과제 1 [`72-lab03-challenge-health.json`](72-lab03-challenge-health.json) | `lab03-challenge` | 4/4 | `40-lab03-challenge` |
| 41 | (헬프데스크는 예제 3, 과제 2 전) | `lab03-challenge2` | **3/4** | `41-lab03-challenge2-before` |
| 42 | 과제 2 [`72-lab03-challenge2-breaker.json`](72-lab03-challenge2-breaker.json) | `lab03-challenge2` | 4/4 (다음 질문 0.1초) | `42-lab03-challenge2` |
| 43 | (같음) | `lab03` | 4/4 | `43-lab03-after-breaker` |
| 44 | — | `doctor` | `ready` | `44-doctor` |
---
## Lab 01 · 재고, 단계별 시간을 남기고, 병목 찾기
### 00 · 예제 1 · 기준을 bench로 재기
실행한 명령:
```bash
uv run tools/flow.py push reference/baseline-helpdesk.json
uv run tools/flow.py call base-helpdesk requests/faq-vpn.json -v
uv run tools/flow.py bench base-helpdesk requests/faq-vpn.json -n 5
uv run tools/flow.py bench base-helpdesk requests/ticket-own.json -n 5
uv run tools/flow.py bench base-helpdesk requests/off-topic.json -n 5
```
출력 (그대로):
```text
$ uv run tools/flow.py call base-helpdesk requests/faq-vpn.json -v
HTTP 200 · 4.4s
→ request POST /webhook/base-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
]
}
$ uv run tools/flow.py bench base-helpdesk requests/faq-vpn.json -n 5
POST http://localhost:5678/webhook/base-helpdesk × 5
✔ #1 · HTTP 200 · 2.98s · route=자동 답변
✔ #2 · HTTP 200 · 2.52s · route=자동 답변
✔ #3 · HTTP 200 · 2.69s · route=자동 답변
✔ #4 · HTTP 200 · 2.84s · route=자동 답변
✔ #5 · HTTP 200 · 2.54s · route=자동 답변
p50 2.69s · p95 2.98s · max 2.98s · failures 0/5
report: runs/bench-base-helpdesk-20261024-174234.json
$ uv run tools/flow.py bench base-helpdesk requests/ticket-own.json -n 5
POST http://localhost:5678/webhook/base-helpdesk × 5
✔ #1 · HTTP 200 · 3.04s · route=자동 답변
✔ #2 · HTTP 200 · 3.14s · route=자동 답변
✔ #3 · HTTP 200 · 2.61s · route=자동 답변
✔ #4 · HTTP 200 · 3.36s · route=자동 답변
✔ #5 · HTTP 200 · 2.68s · route=자동 답변
p50 3.04s · p95 3.36s · max 3.36s · failures 0/5
report: runs/bench-base-helpdesk-20261024-174249.json
$ uv run tools/flow.py bench base-helpdesk requests/off-topic.json -n 5
POST http://localhost:5678/webhook/base-helpdesk × 5
✔ #1 · HTTP 200 · 2.90s · route=자동 답변
✔ #2 · HTTP 200 · 2.55s · route=자동 답변
✔ #3 · HTTP 200 · 2.62s · route=자동 답변
✔ #4 · HTTP 200 · 2.56s · route=자동 답변
✔ #5 · HTTP 200 · 2.44s · route=자동 답변
p50 2.56s · p95 2.90s · max 2.90s · failures 0/5
report: runs/bench-base-helpdesk-20261024-174302.json
```
**쉽게 말하면**
- `call`은 한 번, `bench`는 같은 질문을 5번 불러 시간을 잽니다. 모두 ✔(HTTP 200)라 실패는 없습니다.
- 첫 `call`이 4.4초로 유난히 느린 것은 push로 n8n이 막 다시 켜졌기 때문입니다 (첫 호출, 콜드 스타트). 그래서 기준 숫자는 그 뒤 `bench`의 p50 · p95로 적습니다.
- 질문 세 종류 모두 2.5~3초 안팎입니다. IT와 상관없는 점심 질문도 거의 같은 시간이 걸린다는 것이 눈여겨볼 점입니다 (LLM을 세 번 똑같이 부르기 때문).
- push 직후 첫 호출 4.4초 (10/18 6.5초), 그 뒤 VPN p50 2.69 · p95 2.98초, 접수 3.04 · 3.36초, 점심 2.56 · 2.90초.
### 01 · 기준으로 `lab01` → 1/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab01.json -v --webhook base-helpdesk
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/base-helpdesk · 4 case(s) × 1
✘ VPN FAQ · 단계별 시간 · 3.5s · timings has none of ['answer_ms']
→ request POST /webhook/base-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.",
"sources": [
"FAQ-003"
]
}
✘ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.9s · timings has none of ['total_ms']; steps has none of ['answer']; llm_calls has none of ['1', '2', '3']
→ request POST /webhook/base-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주십시오.",
"sources": [
"FAQ-003"
]
}
✘ 접수 현황 · 2.8s · timings has none of ['answer_ms']
→ request POST /webhook/base-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": []
}
✔ 빈 질문 · 0.0s
→ request POST /webhook/base-helpdesk
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
Passed 1/4
report: runs/lab01-20261024-174311.json
```
**쉽게 말하면**
- 기준 워크플로우는 **아직 아무것도 고치지 않은** 것이라 실패가 정상입니다.
- ✘ `timings has none of ['answer_ms']`: 응답에 단계별 시간 칸 `timings`가 아예 없어서 `answer_ms`를 찾지 못했다는 뜻입니다.
- ✘ `steps has none of ['answer']; llm_calls has none of ['1', '2', '3']`: 지나간 단계(`steps`)와 LLM 호출 수(`llm_calls`)도 없다는 뜻입니다. 예제 2에서 이 칸들을 만듭니다.
- ✔ 빈 질문은 처음부터 HTTP 400으로 거절하도록 되어 있어 통과합니다.
- 기준에는 `timings` · `steps` · `llm_calls`가 없어 빈 질문만 통과.
### 02 · 예제 2 · 단계별 시간 → `lab01` 3/4
codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json` · 이 기록에서 push한 같은 역할의 정답 `reference/lab01-example2-timings.json`):
```text
my-work/helpdesk.json 은 reference/baseline-helpdesk.json 을 복사한 것이야. 웹훅 경로를 my-helpdesk, 이름을 "72 IT helpdesk (my)"로 바꾸고,
답은 그대로 두고 결과에 다음을 더해 줘 (AGENTS.md 3절, 5절의 시각 남기기).
- Has question? 다음에 Start clock, Parse category 뒤에 After classify, Get ticket 뒤에 After data, Answer 뒤에 After answer, Review 뒤에 After review (각각 Date.now()).
- Build result 에서 timings {classify_ms, data_ms, answer_ms, review_ms, total_ms}, steps(지나간 단계), llm_calls(지나간 Agent 수), cached:false.
지나갔는지는 $('노드').isExecuted 로.
기대 결과 cases/lab01.json.
```
실행한 명령:
```bash
uv run tools/flow.py push reference/lab01-example2-timings.json
uv run tools/flow.py test cases/lab01.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ VPN FAQ · 단계별 시간 · 4.3s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 1210,
"data_ms": 29,
"answer_ms": 1119,
"review_ms": 1042,
"total_ms": 3426
}
}
✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.8s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 753,
"data_ms": 19,
"answer_ms": 1058,
"review_ms": 890,
"total_ms": 2731
}
}
✘ 접수 현황 · 3.3s · answer has none of ['처리 중']
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 929,
"data_ms": 21,
"answer_ms": 1118,
"review_ms": 1029,
"total_ms": 3108
}
}
✔ 빈 질문 · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
Passed 3/4
report: runs/lab01-20261024-174333.json
```
**쉽게 말하면**
- `timings` · `steps` · `llm_calls`를 넣은 뒤라 시간 · 구조 케이스는 ✔입니다.
- ✘ `접수 현황 · answer has none of ['처리 중']`: 답에 접수 상태 "처리 중"이라는 글자가 없었다는 뜻입니다. 워크플로우 잘못이 아니라 **검토 LLM이 문장을 다듬다가 "처리 중"을 다른 말로 바꿔 쓴 것**입니다 (LLM 문장은 실행마다 달라짐).
- 그래서 같은 테스트를 다시 돌리면 통과하기도 합니다 (07단계는 4/4). 이 문제는 Lab 02 과제 1에서 검토 LLM을 지우면 사라집니다.
- 접수 케이스는 검토 LLM이 "처리 중"을 바꿔 써서 실패 (정답의 잘못이 아님, 10/18 · 10/23은 4/4).
### 03 · 예제 2 · 한 번 불러 `timings` 보기
실행한 명령:
```bash
uv run tools/flow.py call my-helpdesk requests/faq-vpn.json -v
```
출력 (그대로):
```text
$ uv run tools/flow.py call my-helpdesk requests/faq-vpn.json -v
HTTP 200 · 2.7s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하시고, 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 616,
"data_ms": 18,
"answer_ms": 1124,
"review_ms": 920,
"total_ms": 2698
}
}
```
**쉽게 말하면**
- 응답의 `timings`를 보면 시간이 어디서 쓰였는지 보입니다: 분류 0.6초 + 데이터 0.02초 + 답 1.1초 + 검토 0.9초.
- 데이터 API는 0.02초뿐이고 나머지는 모두 LLM입니다. **느린 곳은 LLM 세 번**이라는 것이 오늘 고칠 방향입니다.
- 분류 616 · 데이터 18 · 답 1124 · 검토 920 · 합 2698ms. LLM 셋이 98.6%.
### 04 · 예제 3 · 질문 종류별로 재기
실행한 명령:
```bash
uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5
uv run tools/flow.py bench my-helpdesk requests/ticket-own.json -n 5
uv run tools/flow.py bench my-helpdesk requests/off-topic.json -n 5
uv run tools/flow.py call my-helpdesk requests/ticket-own.json -v
uv run tools/flow.py call my-helpdesk requests/off-topic.json -v
```
출력 (그대로):
```text
$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5
POST http://localhost:5678/webhook/my-helpdesk × 5
✔ #1 · HTTP 200 · 2.57s · route=자동 답변, llm_calls=3, cached=false
✔ #2 · HTTP 200 · 3.08s · route=자동 답변, llm_calls=3, cached=false
✔ #3 · HTTP 200 · 2.91s · route=자동 답변, llm_calls=3, cached=false
✔ #4 · HTTP 200 · 2.99s · route=자동 답변, llm_calls=3, cached=false
✔ #5 · HTTP 200 · 2.80s · route=자동 답변, llm_calls=3, cached=false
p50 2.91s · p95 3.08s · max 3.08s · failures 0/5
report: runs/bench-my-helpdesk-20261024-174351.json
$ uv run tools/flow.py bench my-helpdesk requests/ticket-own.json -n 5
POST http://localhost:5678/webhook/my-helpdesk × 5
✔ #1 · HTTP 200 · 2.86s · route=자동 답변, llm_calls=3, cached=false
✔ #2 · HTTP 200 · 3.05s · route=자동 답변, llm_calls=3, cached=false
✔ #3 · HTTP 200 · 3.12s · route=자동 답변, llm_calls=3, cached=false
✔ #4 · HTTP 200 · 2.94s · route=자동 답변, llm_calls=3, cached=false
✔ #5 · HTTP 200 · 2.99s · route=자동 답변, llm_calls=3, cached=false
p50 2.99s · p95 3.12s · max 3.12s · failures 0/5
report: runs/bench-my-helpdesk-20261024-174406.json
$ uv run tools/flow.py bench my-helpdesk requests/off-topic.json -n 5
POST http://localhost:5678/webhook/my-helpdesk × 5
✔ #1 · HTTP 200 · 2.38s · route=자동 답변, llm_calls=3, cached=false
✔ #2 · HTTP 200 · 2.36s · route=자동 답변, llm_calls=3, cached=false
✔ #3 · HTTP 200 · 2.48s · route=자동 답변, llm_calls=3, cached=false
✔ #4 · HTTP 200 · 2.49s · route=자동 답변, llm_calls=3, cached=false
✔ #5 · HTTP 200 · 2.84s · route=자동 답변, llm_calls=3, cached=false
p50 2.48s · p95 2.84s · max 2.84s · failures 0/5
report: runs/bench-my-helpdesk-20261024-174419.json
$ uv run tools/flow.py call my-helpdesk requests/ticket-own.json -v
HTTP 200 · 2.8s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 818,
"data_ms": 43,
"answer_ms": 877,
"review_ms": 895,
"total_ms": 2647
}
}
$ uv run tools/flow.py call my-helpdesk requests/off-topic.json -v
HTTP 200 · 2.5s
→ request POST /webhook/my-helpdesk
{
"question": "오늘 점심 메뉴 추천해 주세요",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "other",
"answer": "찾으시는 정보를 확인하지 못했습니다. 점심 메뉴 추천은 담당 직원에게 문의해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 732,
"data_ms": 19,
"answer_ms": 740,
"review_ms": 978,
"total_ms": 2482
}
}
```
**쉽게 말하면**
- 질문 종류마다 5번씩 잽니다. 결과 끝의 `llm_calls=3`은 세 종류 모두 LLM을 세 번 불렀다는 뜻입니다.
- 점심 메뉴처럼 데이터가 없는 질문도 LLM 세 번 → 2.5초입니다. "건너뛸 수 있는 일을 하고 있다"는 증거입니다 (Lab 02에서 줄임).
- p50: VPN 2.91 · 접수 2.99 · 점심 2.48초. 접수 2647ms 중 LLM 2590ms(97.8%), 점심 2482ms 중 2450ms(98.7%).
- 점심 메뉴는 데이터가 없는데도 LLM 세 번 (`category: other`).
### 05 · 과제 1 하기 전 → `lab01-challenge` 0/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab01-challenge.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✘ FAQ · 접수 시간 따로 · 2.7s · timings has none of ['faq_ms']
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 595,
"data_ms": 18,
"answer_ms": 1005,
"review_ms": 1012,
"total_ms": 2643
}
}
✘ 접수 조회 시간 · 2.6s · timings has none of ['ticket_ms']
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 716,
"data_ms": 19,
"answer_ms": 958,
"review_ms": 881,
"total_ms": 2594
}
}
✘ 가장 오래 걸린 단계 · 3.1s · slowest_step has none of ['classify', 'answer', 'review']
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 991,
"data_ms": 18,
"answer_ms": 1024,
"review_ms": 991,
"total_ms": 3037
}
}
✘ 접수 현황도 같은 모양 · 2.8s · answer has none of ['처리 중']; slowest_step has none of ['classify', 'answer', 'review']
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 803,
"data_ms": 36,
"answer_ms": 884,
"review_ms": 1043,
"total_ms": 2781
}
}
Passed 0/4
report: runs/lab01-challenge-20261024-174435.json
```
**쉽게 말하면**
- 과제 1을 하기 **전**이라 0/4가 정상입니다. 실패 줄은 "무엇을 만들어야 하는지" 알려 주는 목록으로 읽습니다.
- ✘ `timings has none of ['faq_ms']` · `['ticket_ms']`: 데이터 조회 시간이 아직 `data_ms` 하나로 합쳐져 있어 FAQ · 접수 칸이 따로 없습니다.
- ✘ `slowest_step has none of [...]`: 가장 느린 단계 이름 칸 `slowest_step`이 아직 없습니다.
- 마지막 케이스의 `answer has none of ['처리 중']`은 02단계와 같은 까닭(검토 LLM이 바꿔 씀)입니다.
- `faq_ms` · `ticket_ms` · `slowest_step`이 없음.
### 06 · 과제 1 · FAQ · 접수 나누기와 `slowest_step` → 4/4
codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 [`72-lab01-challenge-timings.json`](72-lab01-challenge-timings.json)):
```text
my-work/helpdesk.json 의 Search FAQ 와 Get ticket 사이에 After FAQ(Date.now()) 를 더해, timings 의 data_ms 를 faq_ms 와 ticket_ms 로 나눠 줘.
total_ms 를 뺀 timings 중 가장 큰 단계 이름(_ms 뺀 것)을 slowest_step 으로. cases/lab01-challenge.json, cases/lab01.json 둘 다 통과.
```
실행한 명령:
```bash
uv run tools/flow.py push answers/72-lab01-challenge-timings.json
uv run tools/flow.py test cases/lab01-challenge.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ FAQ · 접수 시간 따로 · 4.5s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 1365,
"faq_ms": 16,
"ticket_ms": 14,
"answer_ms": 1196,
"review_ms": 993,
"total_ms": 3614
},
"slowest_step": "classify"
}
✔ 접수 조회 시간 · 2.7s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 774,
"faq_ms": 17,
"ticket_ms": 13,
"answer_ms": 971,
"review_ms": 815,
"total_ms": 2601
},
"slowest_step": "answer"
}
✔ 가장 오래 걸린 단계 · 3.0s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 680,
"faq_ms": 17,
"ticket_ms": 10,
"answer_ms": 1044,
"review_ms": 1007,
"total_ms": 2769
},
"slowest_step": "answer"
}
✔ 접수 현황도 같은 모양 · 3.0s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 723,
"faq_ms": 13,
"ticket_ms": 11,
"answer_ms": 1029,
"review_ms": 1162,
"total_ms": 2951
},
"slowest_step": "review"
}
Passed 4/4
report: runs/lab01-challenge-20261024-174502.json
```
**쉽게 말하면**
- 칸을 나누고 `slowest_step`을 더한 뒤 4/4입니다.
- `slowest_step`은 실행마다 바뀝니다: push 직후 첫 호출은 분류(`classify`)가 가장 느렸고, 그 뒤로는 답 · 검토가 번갈아 1등입니다. 답 LLM과 검토 LLM의 시간이 비슷하기 때문입니다. 그래서 테스트는 셋 중 하나면 통과로 봅니다.
- `slowest_step`: classify 1 (push 직후 첫 호출, classify 1365ms) · answer 2 · review 1. 실행마다 바뀜.
### 07 · 과제 1 뒤 `lab01` → 4/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab01.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ VPN FAQ · 단계별 시간 · 2.9s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 844,
"faq_ms": 16,
"ticket_ms": 9,
"answer_ms": 990,
"review_ms": 927,
"total_ms": 2807
},
"slowest_step": "answer"
}
✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.8s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 715,
"faq_ms": 16,
"ticket_ms": 14,
"answer_ms": 1064,
"review_ms": 988,
"total_ms": 2810
},
"slowest_step": "answer"
}
✔ 접수 현황 · 3.0s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 810,
"faq_ms": 16,
"ticket_ms": 11,
"answer_ms": 1014,
"review_ms": 1058,
"total_ms": 2925
},
"slowest_step": "review"
}
✔ 빈 질문 · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
Passed 4/4
report: runs/lab01-20261024-174511.json
```
**쉽게 말하면**
- 과제 1에서 칸을 더했어도 원래 테스트(`lab01`)가 그대로 통과하는지 확인합니다. 4/4라 **원래 계약을 깨지 않았습니다.**
- 이번에는 검토 LLM이 "처리 중"을 그대로 두어 02단계에서 실패했던 접수 케이스도 통과했습니다.
- 이번에는 접수 답에 "처리 중"이 남아 통과.
### 08 · 과제 2 하기 전 → `lab01-challenge2` 0/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab01-challenge2.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✘ 예산 안 (넉넉한 예산) · 2.9s · over_budget = null (expected false); budget_ms = null (expected 60000); llm_ms has none of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true,
"budget_ms": 60000
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 628,
"faq_ms": 11,
"ticket_ms": 11,
"answer_ms": 1142,
"review_ms": 1028,
"total_ms": 2862
},
"slowest_step": "answer"
}
✘ 예산 넘음 (0.5초 예산) · 2.7s · over_budget = null (expected true); budget_ms = null (expected 500)
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true,
"budget_ms": 500
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 658,
"faq_ms": 14,
"ticket_ms": 11,
"answer_ms": 1013,
"review_ms": 930,
"total_ms": 2640
},
"slowest_step": "answer"
}
✘ 예산을 안 주면 3초 · 3.6s · budget_ms = null (expected 3000); llm_ms has none of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 650,
"faq_ms": 14,
"ticket_ms": 13,
"answer_ms": 1767,
"review_ms": 1061,
"total_ms": 3520
},
"slowest_step": "answer"
}
✘ 접수 현황도 같은 모양 · 2.8s · budget_ms = null (expected 3000); llm_ms has none of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 772,
"faq_ms": 14,
"ticket_ms": 12,
"answer_ms": 895,
"review_ms": 1006,
"total_ms": 2719
},
"slowest_step": "review"
}
Passed 0/4
report: runs/lab01-challenge2-20261024-174523.json
```
**쉽게 말하면**
- 과제 2를 하기 **전**이라 0/4가 정상입니다.
- ✘ `over_budget = null (expected false)` · `budget_ms = null (expected 60000)`: 응답에 그 칸이 없어서 값이 `null`(없음)로 읽혔다는 뜻입니다.
- ✘ `llm_ms has none of ['0', ... '9']`: `llm_ms`에 숫자가 하나도 없다, 곧 칸이 없다는 뜻입니다.
- `budget_ms` · `over_budget` · `llm_ms`가 없음.
### 09 · 과제 2 · 시간 예산 → `lab01-challenge2` 4/4
codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 [`72-lab01-challenge2-budget.json`](72-lab01-challenge2-budget.json)):
```text
my-work/helpdesk.json 의 Build result 에서, 본문 budget_ms (없으면 3000, 숫자로)를 읽어
결과에 llm_ms(classify · answer · review 의 _ms 합, timings 밖에 둔다), budget_ms, over_budget(timings.total_ms > budget_ms)을 더해 줘.
timings, slowest_step 은 그대로. cases/lab01-challenge2.json, cases/lab01-challenge.json, cases/lab01.json 모두 통과.
```
실행한 명령:
```bash
uv run tools/flow.py push answers/72-lab01-challenge2-budget.json
uv run tools/flow.py test cases/lab01-challenge2.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ 예산 안 (넉넉한 예산) · 4.2s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true,
"budget_ms": 60000
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 1208,
"faq_ms": 17,
"ticket_ms": 14,
"answer_ms": 1230,
"review_ms": 857,
"total_ms": 3357
},
"slowest_step": "answer",
"llm_ms": 3295,
"budget_ms": 60000,
"over_budget": false
}
✔ 예산 넘음 (0.5초 예산) · 2.7s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true,
"budget_ms": 500
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 667,
"faq_ms": 15,
"ticket_ms": 11,
"answer_ms": 980,
"review_ms": 916,
"total_ms": 2609
},
"slowest_step": "answer",
"llm_ms": 2563,
"budget_ms": 500,
"over_budget": true
}
✔ 예산을 안 주면 3초 · 3.2s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 761,
"faq_ms": 18,
"ticket_ms": 18,
"answer_ms": 1027,
"review_ms": 1185,
"total_ms": 3023
},
"slowest_step": "review",
"llm_ms": 2973,
"budget_ms": 3000,
"over_budget": true
}
✔ 접수 현황도 같은 모양 · 2.9s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 795,
"faq_ms": 13,
"ticket_ms": 14,
"answer_ms": 1017,
"review_ms": 1046,
"total_ms": 2904
},
"slowest_step": "review",
"llm_ms": 2858,
"budget_ms": 3000,
"over_budget": false
}
Passed 4/4
report: runs/lab01-challenge2-20261024-174548.json
```
**쉽게 말하면**
- 예산 칸을 만든 뒤 4/4입니다.
- 기본 예산 3초(3000ms)에서 VPN이 3023ms라 `over_budget: true`가 나왔습니다. 23ms 차이로 넘은 것이라 실행마다 true · false가 바뀔 수 있고, 테스트는 그래서 기본값 케이스에서 `budget_ms`만 봅니다.
- `llm_ms`를 `timings` 밖에 두었기 때문에 `slowest_step`이 `llm`으로 바뀌지 않고 그대로 단계 이름입니다.
- 예산 60000 → false (3357ms, push 직후), 500 → true, 기본 3000 → VPN 3023ms로 **true**, 접수 2904ms로 false.
- `llm_ms`는 `timings` 밖이라 `slowest_step`은 그대로 단계 이름.
### 10 · 과제 2 뒤 `lab01-challenge` → 4/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab01-challenge.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ FAQ · 접수 시간 따로 · 2.7s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 603,
"faq_ms": 9,
"ticket_ms": 10,
"answer_ms": 969,
"review_ms": 1043,
"total_ms": 2677
},
"slowest_step": "review",
"llm_ms": 2615,
"budget_ms": 3000,
"over_budget": false
}
✔ 접수 조회 시간 · 2.6s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 614,
"faq_ms": 13,
"ticket_ms": 13,
"answer_ms": 1019,
"review_ms": 876,
"total_ms": 2543
},
"slowest_step": "answer",
"llm_ms": 2509,
"budget_ms": 3000,
"over_budget": false
}
✔ 가장 오래 걸린 단계 · 2.9s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 687,
"faq_ms": 17,
"ticket_ms": 10,
"answer_ms": 1146,
"review_ms": 965,
"total_ms": 2838
},
"slowest_step": "answer",
"llm_ms": 2798,
"budget_ms": 3000,
"over_budget": false
}
✔ 접수 현황도 같은 모양 · 2.9s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 767,
"faq_ms": 15,
"ticket_ms": 9,
"answer_ms": 1053,
"review_ms": 1007,
"total_ms": 2865
},
"slowest_step": "answer",
"llm_ms": 2827,
"budget_ms": 3000,
"over_budget": false
}
Passed 4/4
report: runs/lab01-challenge-20261024-174559.json
```
**쉽게 말하면**
- 과제 2를 해도 과제 1에서 만든 칸이 그대로라 4/4입니다. 고칠 때마다 **앞 테스트도 다시** 돌리는 까닭입니다.
- 과제 1의 칸이 그대로.
### 11 · 과제 2 뒤 `lab01` → 4/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab01.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ VPN FAQ · 단계별 시간 · 3.0s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 785,
"faq_ms": 14,
"ticket_ms": 9,
"answer_ms": 1053,
"review_ms": 1031,
"total_ms": 2902
},
"slowest_step": "answer",
"llm_ms": 2869,
"budget_ms": 3000,
"over_budget": false
}
✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.7s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 750,
"faq_ms": 15,
"ticket_ms": 14,
"answer_ms": 984,
"review_ms": 895,
"total_ms": 2672
},
"slowest_step": "answer",
"llm_ms": 2629,
"budget_ms": 3000,
"over_budget": false
}
✔ 접수 현황 · 3.4s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"cached": false,
"steps": [
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"classify_ms": 938,
"faq_ms": 10,
"ticket_ms": 9,
"answer_ms": 981,
"review_ms": 1355,
"total_ms": 3310
},
"slowest_step": "review",
"llm_ms": 3274,
"budget_ms": 3000,
"over_budget": true
}
✔ 빈 질문 · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
Passed 4/4
report: runs/lab01-20261024-174609.json
```
**쉽게 말하면**
- 원래 테스트도 4/4. Lab 01을 마친 워크플로우는 처음 계약을 지키면서 시간 칸 · 예산 칸을 더 가졌습니다.
- 원래 계약 그대로.
## Lab 02 · 규칙 먼저, 캐시, LLM 한 번
### 12 · 예제 1 · 규칙 먼저 → `lab02` 7/9
codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 `reference/lab02-example1-rules.json`):
```text
my-work/helpdesk.json 의 순서를 바꿔 줘: Has question? → Start clock → Search FAQ → Get ticket → After data → Rule classify(Code) → Known?(If).
Rule classify: 질문에 T-1234 같은 번호(/T-?\d{4}/i)가 있으면 ticket, FAQ 결과가 1개 이상이면 faq, 아니면 unknown. {category, by:'rule'}.
Known? true → Answer, false → Classify → Parse category → After classify → Answer. Answer 는 $json.category 로 분류를 읽는다.
Build result: 분류는 Parse category 가 실행됐으면 그것, 아니면 Rule classify. steps 에 rule 또는 classify.
나머지(답, 검토, timings)는 그대로. 기대 결과 cases/lab02.json 앞 네 케이스.
```
실행한 명령:
```bash
uv run tools/flow.py push reference/lab02-example1-rules.json
uv run tools/flow.py test cases/lab02.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 2}
POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1
✔ 예제 1 · 프린터 FAQ는 규칙으로 · 3.8s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.",
"sources": [
"FAQ-004"
],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 41,
"answer_ms": 1766,
"review_ms": 1115,
"total_ms": 2945
}
}
✔ 예제 1 · 접수 번호는 규칙으로 · 2.5s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 32,
"answer_ms": 1041,
"review_ms": 1209,
"total_ms": 2295
}
}
✔ 예제 1 · 없는 접수 번호 · 2.1s
→ request POST /webhook/my-helpdesk
{
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-9999 접수 건은 현재 확인되지 않습니다. 담당 직원이 확인 후 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 23,
"answer_ms": 1209,
"review_ms": 797,
"total_ms": 2040
}
}
✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 2.4s
→ request POST /webhook/my-helpdesk
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "죄송합니다만, 해당 정보를 찾지 못했습니다. 담당 직원이 곧 도움을 드리겠습니다. 감사합니다.",
"sources": [],
"llm_calls": 3,
"cached": false,
"steps": [
"rule",
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 21,
"classify_ms": 582,
"answer_ms": 908,
"review_ms": 877,
"total_ms": 2401
}
}
✔ 예제 2 · 처음 묻는 VPN · 2.3s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 20,
"answer_ms": 1202,
"review_ms": 994,
"total_ms": 2234
}
}
✘ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 1.9s · took 1.9s (expected at most 1s); cached = false (expected true); llm_calls = 2 (expected 0)
→ request POST /webhook/my-helpdesk
{
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 문제가 계속되면 담당 직원에게 문의해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 24,
"answer_ms": 967,
"review_ms": 890,
"total_ms": 1898
}
}
✔ 예제 2 · no_cache면 캐시 안 씀 · 2.3s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 17,
"answer_ms": 1128,
"review_ms": 1048,
"total_ms": 2213
}
}
✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 2.2s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 30,
"answer_ms": 829,
"review_ms": 1300,
"total_ms": 2171
}
}
✘ 예제 2 · 접수 현황은 캐시 안 함 (2) · 2.0s · answer has none of ['처리 중']
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 20,
"answer_ms": 951,
"review_ms": 940,
"total_ms": 1924
}
}
Passed 7/9
report: runs/lab02-20261024-174642.json
```
**쉽게 말하면**
- `before`가 먼저 캐시를 비웁니다 (`cleared: 2`는 지운 개수). 그래서 테스트끼리 서로 영향을 주지 않습니다.
- 예제 1(규칙 먼저) 케이스 넷은 모두 ✔입니다.
- ✘ `같은 질문 · took 1.9s (expected at most 1s); cached = false; llm_calls = 2`: 같은 질문이면 캐시에서 1초 안에 와야 하는데 아직 캐시가 없어 LLM을 두 번 불렀다는 뜻입니다. 캐시는 예제 2에서 만드니 **지금은 실패가 맞습니다.**
- ✘ `접수 현황은 캐시 안 함 (2) · answer has none of ['처리 중']`: 02단계와 같은 까닭(검토 LLM이 "처리 중"을 바꿔 씀)입니다.
- 예제 1 케이스 넷은 모두 통과. 실패 둘: "같은 질문" 캐시 케이스(캐시가 아직 없으니 맞음), 접수 답 문장(검토 LLM).
### 13 · 예제 1 · 규칙으로 정한 질문 재기
실행한 명령:
```bash
uv run tools/flow.py bench my-helpdesk requests/faq-printer.json -n 5
```
출력 (그대로):
```text
$ uv run tools/flow.py bench my-helpdesk requests/faq-printer.json -n 5
POST http://localhost:5678/webhook/my-helpdesk × 5
✔ #1 · HTTP 200 · 2.50s · route=자동 답변, llm_calls=2, cached=false
✔ #2 · HTTP 200 · 2.45s · route=자동 답변, llm_calls=2, cached=false
✔ #3 · HTTP 200 · 2.51s · route=자동 답변, llm_calls=2, cached=false
✔ #4 · HTTP 200 · 2.39s · route=자동 답변, llm_calls=2, cached=false
✔ #5 · HTTP 200 · 2.12s · route=자동 답변, llm_calls=2, cached=false
p50 2.45s · p95 2.51s · max 2.51s · failures 0/5
report: runs/bench-my-helpdesk-20261024-174654.json
```
**쉽게 말하면**
- 프린터 질문은 FAQ 낱말로 규칙이 분류하니 `llm_calls=2`(답 + 검토)입니다. 세 번이던 것이 두 번이 되어 p50 2.45초입니다.
- 프린터 p50 2.45초 (LLM 둘).
### 14 · 예제 2 · 캐시 → `lab02` 9/9
codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 `reference/lab02-example2-cache.json`):
```text
my-work/helpdesk.json 에 캐시를 더해 줘 (AGENTS.md 2절 데이터 API, 3절 캐시 규칙).
Start clock 다음: Cache key(Code: question 을 trim·소문자·연속 공백 하나, use_cache = body.no_cache !== true)
→ Cache lookup(GET http://data-api:8800/cache/<encodeURIComponent(key)>, neverError) → Cached?(value 있고 use_cache)
true → Cached result(route·category·answer·sources + llm_calls 0, cached true, steps ['cache'], timings.total_ms) → Send cached
false → 지금 흐름(Search FAQ ...).
Build result 뒤: Cache it?(category faq 이고 use_cache) true → Store in cache(POST /cache/<key> {value:{route,category,answer,sources}, ttl 600}) → Send result.
접수(ticket)는 저장하지 않는다. 기대 결과 cases/lab02.json 전체, lab01 도 통과.
```
실행한 명령:
```bash
uv run tools/flow.py push reference/lab02-example2-cache.json
uv run tools/flow.py test cases/lab02.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0}
POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1
✔ 예제 1 · 프린터 FAQ는 규칙으로 · 3.9s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.",
"sources": [
"FAQ-004"
],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 76,
"answer_ms": 1837,
"review_ms": 1048,
"total_ms": 2981
}
}
✔ 예제 1 · 접수 번호는 규칙으로 · 2.1s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 32,
"answer_ms": 911,
"review_ms": 1002,
"total_ms": 1957
}
}
✔ 예제 1 · 없는 접수 번호 · 2.0s
→ request POST /webhook/my-helpdesk
{
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-9999 접수 건은 현재 확인되지 않습니다. 담당 직원이 확인 후 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 40,
"answer_ms": 1008,
"review_ms": 870,
"total_ms": 1932
}
}
✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 2.6s
→ request POST /webhook/my-helpdesk
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "죄송합니다만, 해당 정보를 찾지 못했습니다. 담당 직원이 곧 도움을 드리겠습니다. 감사합니다.",
"sources": [],
"llm_calls": 3,
"cached": false,
"steps": [
"rule",
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 32,
"classify_ms": 767,
"answer_ms": 810,
"review_ms": 962,
"total_ms": 2586
}
}
✔ 예제 2 · 처음 묻는 VPN · 2.2s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 58,
"answer_ms": 1140,
"review_ms": 929,
"total_ms": 2143
}
}
✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.1s
→ request POST /webhook/my-helpdesk
{
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 34
}
}
✔ 예제 2 · no_cache면 캐시 안 씀 · 2.3s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 33,
"answer_ms": 1024,
"review_ms": 1125,
"total_ms": 2194
}
}
✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 2.2s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 35,
"answer_ms": 964,
"review_ms": 1161,
"total_ms": 2173
}
}
✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 2.2s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 27,
"answer_ms": 886,
"review_ms": 1239,
"total_ms": 2168
}
}
Passed 9/9
report: runs/lab02-20261024-174725.json
```
**쉽게 말하면**
- 캐시를 넣은 뒤 9/9입니다.
- "같은 질문(띄어쓰기 · 대소문자 다름)"이 0.1초: 질문을 다듬은 글자를 키로 써서 앞 질문의 답을 캐시에서 바로 꺼냈습니다 (`cached: true`, `llm_calls: 0`).
- 접수 현황은 두 번 물어도 캐시하지 않고 매번 새로 조회합니다 (상태가 바뀌기 때문).
- 같은 질문(띄어쓰기 · 대소문자 다름) 0.1초, `cached: true`, `llm_calls: 0`. 이번에는 접수 답에도 "처리 중"이 남음.
### 15 · 캐시 뒤 `lab01` → 3/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab01.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ VPN FAQ · 단계별 시간 · 2.1s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 34,
"answer_ms": 1125,
"review_ms": 852,
"total_ms": 2029
}
}
✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.2s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 30,
"answer_ms": 1182,
"review_ms": 890,
"total_ms": 2114
}
}
✘ 접수 현황 · 2.1s · answer has none of ['처리 중']
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 50,
"answer_ms": 884,
"review_ms": 1046,
"total_ms": 1992
}
}
✔ 빈 질문 · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
Passed 3/4
report: runs/lab01-20261024-174732.json
```
**쉽게 말하면**
- ✘ `접수 현황 · answer has none of ['처리 중']`: 이번에도 **검토 LLM이 "처리 중"을 다른 말로 바꿔 쓴 것**입니다. 캐시와는 상관없습니다 (접수는 캐시하지 않음).
- 나머지 셋은 ✔이라 캐시를 넣어도 원래 계약은 그대로입니다.
- 접수 답 문장 (검토 LLM). 캐시와는 상관없음.
### 16 · 캐시 적중 세기
실행한 명령:
```bash
curl -s localhost:8800/cache-stats
```
출력 (그대로):
```text
$ curl -s localhost:8800/cache-stats; echo
{"entries": 2, "hits": 4, "misses": 8}
```
**쉽게 말하면**
- 데이터 API의 캐시 통계입니다. `entries 2`는 저장된 답 2개, `hits 4`는 캐시에서 찾은 횟수, `misses 8`은 찾았는데 없었던 횟수입니다. 적중률은 4 ÷ (4 + 8) ≈ 33%입니다.
- entries 2 · hits 4 · misses 8.
### 17 · 예제 3 · 캐시 씀 · 안 씀
실행한 명령:
```bash
curl -s -XPOST localhost:8800/cache-clear
uv run tools/flow.py bench my-helpdesk requests/faq-vpn.json -n 5
uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5
curl -s localhost:8800/cache-stats
```
출력 (그대로):
```text
$ curl -s -XPOST localhost:8800/cache-clear; echo
{"cleared": 2}
$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn.json -n 5
POST http://localhost:5678/webhook/my-helpdesk × 5
✔ #1 · HTTP 200 · 2.07s · route=자동 답변, llm_calls=2, cached=false
✔ #2 · HTTP 200 · 0.06s · route=자동 답변, llm_calls=0, cached=true
✔ #3 · HTTP 200 · 0.05s · route=자동 답변, llm_calls=0, cached=true
✔ #4 · HTTP 200 · 0.04s · route=자동 답변, llm_calls=0, cached=true
✔ #5 · HTTP 200 · 0.05s · route=자동 답변, llm_calls=0, cached=true
p50 0.05s · p95 2.07s · max 2.07s · failures 0/5
report: runs/bench-my-helpdesk-20261024-174734.json
$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5
POST http://localhost:5678/webhook/my-helpdesk × 5
✔ #1 · HTTP 200 · 1.99s · route=자동 답변, llm_calls=2, cached=false
✔ #2 · HTTP 200 · 1.98s · route=자동 답변, llm_calls=2, cached=false
✔ #3 · HTTP 200 · 2.21s · route=자동 답변, llm_calls=2, cached=false
✔ #4 · HTTP 200 · 1.97s · route=자동 답변, llm_calls=2, cached=false
✔ #5 · HTTP 200 · 2.24s · route=자동 답변, llm_calls=2, cached=false
p50 1.99s · p95 2.24s · max 2.24s · failures 0/5
report: runs/bench-my-helpdesk-20261024-174745.json
$ curl -s localhost:8800/cache-stats; echo
{"entries": 1, "hits": 9, "misses": 1}
```
**쉽게 말하면**
- 캐시를 비우고 같은 VPN 질문 5번: 첫 번만 캐시에 없어 2.07초(`cached=false`), 나머지 넷은 캐시에서 0.04~0.06초(`cached=true`, `llm_calls=0`).
- 그래서 p50은 0.05초인데 p95 · max는 첫 번의 2.07초입니다. 캐시는 "보통"을 크게 줄이지만 "처음 묻는 질문"은 줄이지 못합니다.
- `no_cache`로 다시 재면 매번 LLM이라 p50 1.99초. 마지막 통계 `hits 9 · misses 1`은 `no_cache` 요청도 캐시를 **조회는** 해서 hits로 세기 때문입니다 (쓰지는 않음).
- 캐시 씀: 첫 번 2.07초, 나머지 0.04~0.06초 (p50 0.05초). 캐시 안 씀: p50 1.99초.
- 그 뒤 hits 9 · misses 1: `no_cache` 요청도 **조회는** 하므로 hits로 셈.
### 18 · 과제 1 하기 전 → `lab02-challenge` 1/5
실행한 명령:
```bash
uv run tools/flow.py test cases/lab02-challenge.json -v
```
출력 (그대로):
```text
✔ before: POST /webhook/my-helpdesk · HTTP 200 · 2.0s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직
POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1
✘ VPN · LLM 한 번 · 2.5초 안 · 2.2s · llm_calls = 2 (expected 1)
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 33,
"answer_ms": 1054,
"review_ms": 1038,
"total_ms": 2141
}
}
✘ 비밀번호 · LLM 한 번 · 2.0s · llm_calls = 2 (expected 1)
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 언제든 문의해 주시기 바랍니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 72,
"answer_ms": 943,
"review_ms": 950,
"total_ms": 1984
}
}
✘ 접수 현황 · LLM 한 번 · 2.3s · llm_calls = 2 (expected 1)
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 26,
"answer_ms": 932,
"review_ms": 1241,
"total_ms": 2213
}
}
✘ 규칙으로 모르면 분류 + 답 두 번 · 2.8s · llm_calls = 3 (expected 2)
→ request POST /webhook/my-helpdesk
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "죄송합니다만, 해당 정보를 찾지 못했습니다. 담당 직원이 곧 도움을 드리겠습니다. 감사합니다.",
"sources": [],
"llm_calls": 3,
"cached": false,
"steps": [
"rule",
"classify",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 27,
"classify_ms": 730,
"answer_ms": 1041,
"review_ms": 926,
"total_ms": 2736
}
}
✔ 빈 질문은 LLM 없이 · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
Passed 1/5
report: runs/lab02-challenge-20261024-174756.json
```
**쉽게 말하면**
- 과제 1을 하기 **전**이라 1/5가 정상입니다. `before`는 워밍업 호출(첫 호출이 느린 것을 미리 치움)입니다.
- ✘ `llm_calls = 2 (expected 1)`: 이 과제의 목표는 LLM 한 번인데 아직 답 + 검토 두 번을 부른다는 뜻입니다.
- ✘ `llm_calls = 3 (expected 2)`: 규칙으로 모르는 질문은 분류 + 답 + 검토 세 번. 목표는 분류 + 답 두 번입니다.
- 검토 LLM을 지우면 모두 하나씩 줄어듭니다 (20단계).
- `llm_calls`가 2 · 3 (답 + 검토). 빈 질문만 통과.
### 19 · 과제 1 하기 전 시간
실행한 명령:
```bash
uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5
```
출력 (그대로):
```text
$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5
POST http://localhost:5678/webhook/my-helpdesk × 5
✔ #1 · HTTP 200 · 2.26s · route=자동 답변, llm_calls=2, cached=false
✔ #2 · HTTP 200 · 2.45s · route=자동 답변, llm_calls=2, cached=false
✔ #3 · HTTP 200 · 2.11s · route=자동 답변, llm_calls=2, cached=false
✔ #4 · HTTP 200 · 2.22s · route=자동 답변, llm_calls=2, cached=false
✔ #5 · HTTP 200 · 2.03s · route=자동 답변, llm_calls=2, cached=false
p50 2.22s · p95 2.45s · max 2.45s · failures 0/5
report: runs/bench-my-helpdesk-20261024-174807.json
```
**쉽게 말하면**
- 고치기 전 시간을 재 둡니다 (캐시 없이 p50 2.22초). 고친 뒤(23단계)와 **같은 날, 같은 요청**으로 비교하려는 것입니다.
- p50 2.22 · p95 2.45초.
### 20 · 과제 1 · 검토를 검사 Code로 → 15/15
codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 [`72-lab02-challenge-fast.json`](72-lab02-challenge-fast.json)):
```text
my-work/helpdesk.json 에서 Review(검토 LLM)와 Review model 을 지우고, 그 자리에 Review check(Code)를 둬:
Answer 의 output 을 trim, 비었으면 empty, 한글이 없으면 not_korean, 400자 넘으면 자르고 too_long 을 review_problems 에.
Answer 지시문에 "합니다체, 3문장 이내"가 이미 있는지 확인. steps 에는 review 대신 check, llm_calls 는 검토를 세지 않는다.
나머지는 그대로. cases/lab02-challenge.json, lab02.json, lab01.json 모두 통과.
```
실행한 명령:
```bash
uv run tools/flow.py push answers/72-lab02-challenge-fast.json
uv run tools/flow.py test cases/lab02-challenge.json -v --repeat 3
```
출력 (그대로):
```text
✔ before: POST /webhook/my-helpdesk · HTTP 200 · 2.4s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리
POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 3
✔ VPN · LLM 한 번 · 2.5초 안 #1 · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 37,
"answer_ms": 986,
"review_ms": 11,
"total_ms": 1046
}
}
✔ VPN · LLM 한 번 · 2.5초 안 #2 · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 28,
"answer_ms": 1057,
"review_ms": 14,
"total_ms": 1107
}
}
✔ VPN · LLM 한 번 · 2.5초 안 #3 · 1.2s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 41,
"answer_ms": 952,
"review_ms": 20,
"total_ms": 1023
}
}
✔ 비밀번호 · LLM 한 번 #1 · 1.0s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 도움이 필요하시면 언제든 문의해 주세요.",
"sources": [
"FAQ-001"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 81,
"answer_ms": 868,
"review_ms": 16,
"total_ms": 974
}
}
✔ 비밀번호 · LLM 한 번 #2 · 0.9s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 32,
"answer_ms": 824,
"review_ms": 15,
"total_ms": 881
}
}
✔ 비밀번호 · LLM 한 번 #3 · 0.9s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 48,
"answer_ms": 811,
"review_ms": 12,
"total_ms": 879
}
}
✔ 접수 현황 · LLM 한 번 #1 · 0.9s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 29,
"answer_ms": 798,
"review_ms": 15,
"total_ms": 848
}
}
✔ 접수 현황 · LLM 한 번 #2 · 1.0s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 28,
"answer_ms": 924,
"review_ms": 12,
"total_ms": 971
}
}
✔ 접수 현황 · LLM 한 번 #3 · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 30,
"answer_ms": 1001,
"review_ms": 45,
"total_ms": 1096
}
}
✔ 규칙으로 모르면 분류 + 답 두 번 #1 · 1.6s
→ request POST /webhook/my-helpdesk
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"classify",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 23,
"classify_ms": 723,
"answer_ms": 838,
"review_ms": 15,
"total_ms": 1606
}
}
✔ 규칙으로 모르면 분류 + 답 두 번 #2 · 1.4s
→ request POST /webhook/my-helpdesk
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"classify",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 27,
"classify_ms": 594,
"answer_ms": 698,
"review_ms": 13,
"total_ms": 1339
}
}
✔ 규칙으로 모르면 분류 + 답 두 번 #3 · 1.6s
→ request POST /webhook/my-helpdesk
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"classify",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 30,
"classify_ms": 746,
"answer_ms": 801,
"review_ms": 15,
"total_ms": 1599
}
}
✔ 빈 질문은 LLM 없이 #1 · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
✔ 빈 질문은 LLM 없이 #2 · 0.1s
→ request POST /webhook/my-helpdesk
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
✔ 빈 질문은 LLM 없이 #3 · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
Passed 15/15
report: runs/lab02-challenge-20261024-174837.json
```
**쉽게 말하면**
- 검토 LLM을 검사 Code로 바꾼 뒤 15/15 (`--repeat 3`이라 5케이스 × 3번).
- VPN · 비밀번호 · 접수가 0.9~1.2초로 2.5초 기대 안에 넉넉히 들어옵니다. LLM이 한 번(`llm_calls: 1`)이 되었기 때문입니다.
- 세 번 반복해도 모두 통과하니 "운 좋게 한 번 통과"가 아닙니다.
- VPN 1.1~1.2초 · 비밀번호 0.9~1.0초 · 접수 0.9~1.1초 (`llm_calls: 1`), 규칙으로 모르는 질문 1.4~1.6초 (2).
### 21 · 과제 1 뒤 `lab02` → 9/9
실행한 명령:
```bash
uv run tools/flow.py test cases/lab02.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1}
POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1
✔ 예제 1 · 프린터 FAQ는 규칙으로 · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 28,
"answer_ms": 999,
"review_ms": 15,
"total_ms": 1047
}
}
✔ 예제 1 · 접수 번호는 규칙으로 · 0.9s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 29,
"answer_ms": 802,
"review_ms": 50,
"total_ms": 899
}
}
✔ 예제 1 · 없는 접수 번호 · 1.0s
→ request POST /webhook/my-helpdesk
{
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 28,
"answer_ms": 901,
"review_ms": 15,
"total_ms": 951
}
}
✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 1.6s
→ request POST /webhook/my-helpdesk
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"classify",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 30,
"classify_ms": 572,
"answer_ms": 898,
"review_ms": 14,
"total_ms": 1522
}
}
✔ 예제 2 · 처음 묻는 VPN · 1.2s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 26,
"answer_ms": 1106,
"review_ms": 11,
"total_ms": 1149
}
}
✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.1s
→ request POST /webhook/my-helpdesk
{
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 21
}
}
✔ 예제 2 · no_cache면 캐시 안 씀 · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 22,
"answer_ms": 1013,
"review_ms": 21,
"total_ms": 1063
}
}
✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 25,
"answer_ms": 989,
"review_ms": 33,
"total_ms": 1069
}
}
✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 0.9s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 24,
"answer_ms": 786,
"review_ms": 17,
"total_ms": 838
}
}
Passed 9/9
report: runs/lab02-20261024-174846.json
```
**쉽게 말하면**
- 검토 LLM이 없어져 접수 답의 "처리 중"이 바뀌지 않고 그대로 나옵니다. 그래서 그동안 가끔 실패하던 접수 케이스도 이제 통과합니다.
- 검토 LLM이 없으니 접수 답의 "처리 중"이 그대로.
### 22 · 과제 1 뒤 `lab01` → 4/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab01.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ VPN FAQ · 단계별 시간 · 1.2s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 32,
"answer_ms": 1122,
"review_ms": 11,
"total_ms": 1173
}
}
✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 1.3s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 28,
"answer_ms": 1219,
"review_ms": 14,
"total_ms": 1268
}
}
✔ 접수 현황 · 1.0s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 28,
"answer_ms": 907,
"review_ms": 15,
"total_ms": 956
}
}
✔ 빈 질문 · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
Passed 4/4
report: runs/lab01-20261024-174849.json
```
**쉽게 말하면**
- 원래 테스트도 4/4. `lab01`은 `steps`에 `answer`가 있는지만 보므로, `review`가 `check`로 바뀌어도 깨지지 않습니다.
- `lab01`은 `steps`에 `answer`만 봄.
### 23 · 과제 1 뒤 시간
실행한 명령:
```bash
uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5
```
출력 (그대로):
```text
$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5
POST http://localhost:5678/webhook/my-helpdesk × 5
✔ #1 · HTTP 200 · 1.32s · route=자동 답변, llm_calls=1, cached=false
✔ #2 · HTTP 200 · 1.14s · route=자동 답변, llm_calls=1, cached=false
✔ #3 · HTTP 200 · 1.11s · route=자동 답변, llm_calls=1, cached=false
✔ #4 · HTTP 200 · 1.12s · route=자동 답변, llm_calls=1, cached=false
✔ #5 · HTTP 200 · 1.05s · route=자동 답변, llm_calls=1, cached=false
p50 1.12s · p95 1.32s · max 1.32s · failures 0/5
report: runs/bench-my-helpdesk-20261024-174855.json
```
**쉽게 말하면**
- 같은 요청으로 다시 재면 p50 2.22 → **1.12초**. LLM 한 번을 빼서 약 1초 줄었습니다 (교안 1단계의 어림과 맞음).
- p50 2.22 → **1.12초**, p95 2.45 → 1.32초.
### 24 · 과제 2 하기 전 → `lab02-challenge2` 4/6
실행한 명령:
```bash
uv run tools/flow.py test cases/lab02-challenge2.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 2}
POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1
✔ 처음 묻는 VPN (캐시 없음) · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 39,
"answer_ms": 947,
"review_ms": 16,
"total_ms": 1009
}
}
✘ 다른 말로 묻는 VPN → FAQ 번호 캐시 · 1.2s · took 1.2s (expected at most 1s); cached = false (expected true); cache_by = null (expected faq); llm_calls = 1 (expected 0)
→ request POST /webhook/my-helpdesk
{
"question": "VPN 연결이 계속 끊어지는데요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 연결이 계속 끊어질 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 27,
"answer_ms": 1067,
"review_ms": 14,
"total_ms": 1117
}
}
✘ 같은 말은 질문 캐시 · 0.1s · cache_by = null (expected question)
→ request POST /webhook/my-helpdesk
{
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 14
}
}
✔ no_cache면 FAQ 번호 캐시도 안 씀 · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 연결이 계속 끊어지는데요",
"user": "최유진",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 연결이 계속 끊어질 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 21,
"answer_ms": 1014,
"review_ms": 9,
"total_ms": 1051
}
}
✔ 다른 FAQ는 따로 (프린터 처음) · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 26,
"answer_ms": 1041,
"review_ms": 17,
"total_ms": 1091
}
}
✔ 접수 현황은 캐시 안 함 · 0.9s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 25,
"answer_ms": 869,
"review_ms": 13,
"total_ms": 913
}
}
Passed 4/6
report: runs/lab02-challenge2-20261024-174901.json
```
**쉽게 말하면**
- 과제 2를 하기 **전**이라 4/6이 정상입니다.
- ✘ `다른 말로 묻는 VPN · cached = false; cache_by = null; llm_calls = 1`: 글자가 다른 질문이라 질문 캐시에 맞지 않아 LLM을 불렀다는 뜻입니다. 같은 FAQ(FAQ-003)로 답하는 질문이니 FAQ 번호 캐시가 있으면 맞았을 것입니다.
- ✘ `같은 말은 질문 캐시 · cache_by = null (expected question)`: 캐시에서는 왔지만(0.1초) **어느 캐시에서 왔는지** 알려 주는 `cache_by` 칸이 아직 없습니다.
- "다른 말 VPN"은 `cached: false`, LLM 1번, 1.2초. "같은 말"은 캐시는 맞지만 `cache_by` 칸이 없음.
### 25 · 과제 2 · FAQ 번호 캐시 → 6/6
codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 [`72-lab02-challenge2-faqcache.json`](72-lab02-challenge2-faqcache.json)):
```text
my-work/helpdesk.json 에 FAQ 번호 캐시를 더해 줘 (AGENTS.md 3절 캐시 키 표).
Rule classify 다음: FAQ cache key(Code: category 가 faq 이고 use_cache 이고 FAQ 결과가 있으면 faq_key = 'faq-' + 첫 FAQ id, 아니면 'none')
→ FAQ cache lookup(GET /cache/<faq_key>, neverError) → FAQ cached?(value 있고 faq_key 가 'none' 아님)
true → FAQ cached result(llm_calls 0, cached true, cache_by 'faq', steps ['faq', 'rule', 'cache']) → Send
false → Back to rule(Code: Rule classify 의 결과를 그대로) → Known?
Store in cache 뒤에 Store by FAQ(POST /cache/<faq_key>, 같은 value, ttl 600) → Result again.
질문 캐시의 Cached result 에는 cache_by 'question', Build result 에는 cache_by null.
기대 결과 cases/lab02-challenge2.json, lab02-challenge.json, lab02.json 도 통과.
```
실행한 명령:
```bash
uv run tools/flow.py push answers/72-lab02-challenge2-faqcache.json
uv run tools/flow.py test cases/lab02-challenge2.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 3}
POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1
✔ 처음 묻는 VPN (캐시 없음) · 2.5s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 71,
"answer_ms": 1556,
"review_ms": 23,
"total_ms": 1665
}
}
✔ 다른 말로 묻는 VPN → FAQ 번호 캐시 · 0.1s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 연결이 계속 끊어지는데요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"cache_by": "faq",
"steps": [
"faq",
"rule",
"cache"
],
"timings": {
"total_ms": 44
}
}
✔ 같은 말은 질문 캐시 · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"cache_by": "question",
"steps": [
"cache"
],
"timings": {
"total_ms": 19
}
}
✔ no_cache면 FAQ 번호 캐시도 안 씀 · 1.2s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 연결이 계속 끊어지는데요",
"user": "최유진",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 연결이 계속 끊어질 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 27,
"answer_ms": 1052,
"review_ms": 20,
"total_ms": 1110
}
}
✔ 다른 FAQ는 따로 (프린터 처음) · 1.2s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 42,
"answer_ms": 1035,
"review_ms": 14,
"total_ms": 1102
}
}
✔ 접수 현황은 캐시 안 함 · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 34,
"answer_ms": 796,
"review_ms": 14,
"total_ms": 852
}
}
Passed 6/6
report: runs/lab02-challenge2-20261024-174920.json
```
**쉽게 말하면**
- FAQ 번호 캐시를 넣은 뒤 6/6.
- "VPN 연결이 계속 끊어지는데요"처럼 다른 말로 물어도 같은 FAQ라 0.1초, `cache_by: faq`, `llm_calls: 0`입니다.
- `no_cache`를 보내면 두 캐시 모두 쓰지 않고(1.2초), 프린터처럼 다른 FAQ는 따로 계산합니다.
- "VPN 연결이 계속 끊어지는데요" → `cache_by: faq`, `llm_calls: 0`, 0.1초. 같은 말 → `cache_by: question`, 0.0초.
### 26 · 과제 2 뒤 `lab02-challenge` → 5/5
실행한 명령:
```bash
uv run tools/flow.py test cases/lab02-challenge.json -v
```
출력 (그대로):
```text
✔ before: POST /webhook/my-helpdesk · HTTP 200 · 1.1s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리
POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1
✔ VPN · LLM 한 번 · 2.5초 안 · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 27,
"answer_ms": 1017,
"review_ms": 16,
"total_ms": 1067
}
}
✔ 비밀번호 · LLM 한 번 · 1.0s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 도움이 필요하시면 언제든 문의해 주세요.",
"sources": [
"FAQ-001"
],
"llm_calls": 1,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 27,
"answer_ms": 840,
"review_ms": 40,
"total_ms": 928
}
}
✔ 접수 현황 · LLM 한 번 · 0.9s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 28,
"answer_ms": 835,
"review_ms": 19,
"total_ms": 891
}
}
✔ 규칙으로 모르면 분류 + 답 두 번 · 1.7s
→ request POST /webhook/my-helpdesk
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"classify",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 27,
"classify_ms": 842,
"answer_ms": 771,
"review_ms": 11,
"total_ms": 1659
}
}
✔ 빈 질문은 LLM 없이 · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
Passed 5/5
report: runs/lab02-challenge-20261024-174926.json
```
**쉽게 말하면**
- 과제 1 테스트도 5/5. 이 테스트의 요청은 모두 `no_cache`라 새 캐시가 끼어들지 않습니다.
- 모두 `no_cache`라 두 번째 캐시와 상관없음.
### 27 · 과제 2 뒤 `lab02` → 9/9
실행한 명령:
```bash
uv run tools/flow.py test cases/lab02.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 4}
POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1
✔ 예제 1 · 프린터 FAQ는 규칙으로 · 1.2s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 33,
"answer_ms": 1111,
"review_ms": 17,
"total_ms": 1168
}
}
✔ 예제 1 · 접수 번호는 규칙으로 · 1.0s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락 바랍니다.",
"sources": [],
"llm_calls": 1,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 27,
"answer_ms": 895,
"review_ms": 16,
"total_ms": 945
}
}
✔ 예제 1 · 없는 접수 번호 · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.",
"sources": [],
"llm_calls": 1,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 25,
"answer_ms": 946,
"review_ms": 17,
"total_ms": 998
}
}
✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 1.7s
→ request POST /webhook/my-helpdesk
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"classify",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 25,
"classify_ms": 623,
"answer_ms": 1026,
"review_ms": 14,
"total_ms": 1699
}
}
✔ 예제 2 · 처음 묻는 VPN · 1.2s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 27,
"answer_ms": 1112,
"review_ms": 23,
"total_ms": 1168
}
}
✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"cache_by": "question",
"steps": [
"cache"
],
"timings": {
"total_ms": 15
}
}
✔ 예제 2 · no_cache면 캐시 안 씀 · 1.2s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 21,
"answer_ms": 1065,
"review_ms": 32,
"total_ms": 1137
}
}
✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 1.0s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 30,
"answer_ms": 936,
"review_ms": 15,
"total_ms": 988
}
}
✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 0.9s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"cache_by": null,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 31,
"answer_ms": 822,
"review_ms": 14,
"total_ms": 873
}
}
Passed 9/9
report: runs/lab02-20261024-174935.json
```
**쉽게 말하면**
- Lab 02 처음 테스트도 9/9. 두 번째 캐시를 더해도 질문 캐시 케이스가 그대로입니다.
- 원래 캐시 케이스 그대로.
## Lab 03 · 멈춰도 답하게
### 28 · 예제 1 · 고치기 전에 고장 겪기
실행한 명령:
```bash
uv run tools/flow.py push reference/lab02-example2-cache.json
uv run tools/flow.py fault faq slow
uv run tools/flow.py call my-helpdesk '{"question": "비밀번호를 잊어버렸어요", "no_cache": true}' -v
uv run tools/flow.py fault faq error
uv run tools/flow.py call my-helpdesk '{"question": "비밀번호를 잊어버렸어요", "no_cache": true}' -v
uv run tools/flow.py fault all ok
```
출력 (그대로):
```text
$ uv run tools/flow.py fault faq slow
✔ fault switch: {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0}
$ uv run tools/flow.py call my-helpdesk '{"question": "비밀번호를 잊어버렸어요", "no_cache": true}' -v
HTTP 200 · 11.5s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 언제든 문의해 주세요.",
"sources": [
"FAQ-001"
],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"review"
],
"timings": {
"data_ms": 8144,
"answer_ms": 1518,
"review_ms": 1013,
"total_ms": 10701
}
}
$ uv run tools/flow.py fault faq error
✔ fault switch: {"faq": "error", "tickets": "ok", "slow_seconds": 8.0}
$ uv run tools/flow.py call my-helpdesk '{"question": "비밀번호를 잊어버렸어요", "no_cache": true}' -v
HTTP 500 · 0.3s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요",
"no_cache": true
}
← response HTTP 500
{
"message": "Error in workflow"
}
$ uv run tools/flow.py fault all ok
✔ fault switch: {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
```
**쉽게 말하면**
- **고치기 전에 고장을 먼저 겪어 보는** 단계라 결과가 나쁜 것이 정상입니다.
- FAQ를 8초 느리게 하면 11.5초를 기다린 뒤에야 평소 답이 옵니다. 기다리는 한도(timeout)가 없어 끝까지 기다렸기 때문입니다.
- FAQ가 오류(500)를 내면 0.3초 만에 **HTTP 500**: 워크플로우가 오류에서 멈춰 사용자는 아무 안내도 받지 못합니다.
- 마지막 `fault all ok`로 고장을 꼭 되돌립니다.
- FAQ 8초 늦음 → **11.5초** 뒤 평소 답 (10/23 13.2초). FAQ 500 → **HTTP 500** 0.3초.
### 29 · 예제 1 · timeout · 재시도 · 담당자 → `lab03` 4/4
codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 `reference/lab03-example1-resilient.json`):
```text
my-work/helpdesk.json 의 Search FAQ 와 Get ticket 에 options.timeout 2000, 노드 설정 retryOnFail true, maxTries 2, waitBetweenTries 500,
onError continueErrorOutput 을 넣어 줘. 두 노드의 오류 출력(main 1번)은 Data down(Set: route 담당자 연결, answer "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", reason data_api_down)
→ Send data down(Respond). Get ticket 은 404 를 받아야 하니 neverError 는 그대로 둔다. 기대 결과 cases/lab03.json, lab02 도 통과.
```
실행한 명령:
```bash
uv run tools/flow.py push reference/lab03-example1-resilient.json
uv run tools/flow.py test cases/lab03.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ 평소 · 3.1s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 605,
"answer_ms": 1634,
"review_ms": 13,
"total_ms": 2270
}
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0}
✔ FAQ가 느릴 때 (8초) · 4.6s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0}
✔ FAQ가 오류일 때 (500) · 0.6s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 4}
✔ before: POST /webhook/my-helpdesk · HTTP 200 · 2.1s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0}
✔ 캐시에 있으면 FAQ가 멈춰도 답 · 0.1s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 21
}
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
Passed 4/4
report: runs/lab03-20261024-175023.json
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
```
**쉽게 말하면**
- timeout · 재시도 · 오류 출력을 넣은 뒤 4/4.
- `before` · `after` 줄은 테스트가 케이스 앞에서 고장을 켜고 뒤에서 끄는 것입니다.
- 느림은 4.6초(2초 기다림 + 0.5초 쉼 + 2초 다시) 뒤, 오류는 0.6초 뒤 `담당자 연결`로 정해진 답을 합니다. 캐시에 있는 질문은 FAQ가 멈춰도 0.1초에 답합니다.
- 느림 4.6초 · 오류 0.6초 → `담당자 연결`, 캐시에 있으면 0.1초.
- 평소 VPN의 `data_ms`가 **605ms** (예제 2 단계 32~76ms): `Get ticket`의 재시도가 `/tickets/NONE` 404에도 걸림 (예제 3에서 고침).
### 30 · 평소 그대로 → `lab02` 9/9
실행한 명령:
```bash
uv run tools/flow.py test cases/lab02.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1}
POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1
✔ 예제 1 · 프린터 FAQ는 규칙으로 · 1.8s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 585,
"answer_ms": 1071,
"review_ms": 20,
"total_ms": 1687
}
}
✔ 예제 1 · 접수 번호는 규칙으로 · 1.2s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 71,
"answer_ms": 1030,
"review_ms": 19,
"total_ms": 1128
}
}
✔ 예제 1 · 없는 접수 번호 · 1.8s
→ request POST /webhook/my-helpdesk
{
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 555,
"answer_ms": 1179,
"review_ms": 24,
"total_ms": 1774
}
}
✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 2.2s
→ request POST /webhook/my-helpdesk
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"classify",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 554,
"classify_ms": 650,
"answer_ms": 927,
"review_ms": 14,
"total_ms": 2152
}
}
✔ 예제 2 · 처음 묻는 VPN · 1.7s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 553,
"answer_ms": 1015,
"review_ms": 28,
"total_ms": 1605
}
}
✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.1s
→ request POST /webhook/my-helpdesk
{
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 19
}
}
✔ 예제 2 · no_cache면 캐시 안 씀 · 1.7s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 551,
"answer_ms": 1081,
"review_ms": 34,
"total_ms": 1685
}
}
✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 1.2s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 31,
"answer_ms": 1062,
"review_ms": 14,
"total_ms": 1114
}
}
✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 47,
"answer_ms": 977,
"review_ms": 15,
"total_ms": 1049
}
}
Passed 9/9
report: runs/lab02-20261024-175036.json
```
**쉽게 말하면**
- 고장 대비를 넣어도 평소 테스트(`lab02`)는 9/9. timeout 2초는 평소(수십 ms)에는 걸리지 않습니다.
- timeout 2초는 평소에는 영향 없음.
### 31 · 예제 2 · 고치기 전에 LLM 멈춤 겪기
실행한 명령:
```bash
uv run tools/flow.py llm down
uv run tools/flow.py call my-helpdesk requests/faq-vpn-nocache.json -v
uv run tools/flow.py llm up
```
출력 (그대로):
```text
$ uv run tools/flow.py llm down
✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up
$ uv run tools/flow.py call my-helpdesk requests/faq-vpn-nocache.json -v
HTTP 500 · 1.3s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 500
{
"message": "Error in workflow"
}
$ uv run tools/flow.py llm up
✔ OpenAI credential: real key from .env restored
```
**쉽게 말하면**
- 고치기 전에 LLM 멈춤을 겪어 봅니다. `llm down`은 OpenAI 키를 일부러 틀리게 바꿉니다.
- 1.3초 만에 **HTTP 500**: LLM 노드가 실패하자 워크플로우가 멈췄습니다. 마지막 `llm up`으로 진짜 키를 되돌립니다.
- LLM 401 → HTTP 500 1.3초.
### 32 · 예제 2 · 고치기 전 → `lab03-llm-down` 1/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab03-llm-down.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 2}
✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✘ VPN · FAQ 원문으로 · 0.9s · status 500 (expected 200); route = null (expected 간단 답변); answer has none of ['인증서']
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 500
{
"message": "Error in workflow"
}
✘ 접수 현황 · 상태 그대로 · 0.4s · status 500 (expected 200); route = null (expected 간단 답변); answer has none of ['처리 중']
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 500
{
"message": "Error in workflow"
}
✘ 모르는 질문 · 담당자 · 0.9s · status 500 (expected 200); route = null (expected 담당자 연결)
→ request POST /webhook/my-helpdesk
{
"question": "오늘 점심 메뉴 추천해 주세요",
"user": "이준호"
}
← response HTTP 500
{
"message": "Error in workflow"
}
✔ 빈 질문 · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
Passed 1/4
report: runs/lab03-llm-down-20261024-175047.json
✔ OpenAI credential: real key from .env restored
```
**쉽게 말하면**
- 고치기 **전**이라 1/4가 정상입니다.
- ✘ `status 500 (expected 200); route = null`: 응답이 HTTP 500이라 `route` 같은 칸이 하나도 없다는 뜻입니다. 기대는 LLM 없이도 200으로 `간단 답변`을 하는 것입니다.
- ✘ `answer has none of ['인증서']` · `['처리 중']`: 답 자체가 없으니 FAQ 원문 · 접수 상태 글자도 없습니다.
- ✔ 빈 질문은 LLM 전에 거절하니 고장과 상관없이 통과합니다.
- 빈 질문만 통과.
### 33 · 예제 2 · LLM 없이 간단 답변 → 4/4
codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 `reference/lab03-example2-llm-fallback.json`):
```text
my-work/helpdesk.json 의 Classify 와 Answer Agent 에 onError continueErrorOutput 을 넣고, 두 노드의 오류 출력을 Simple answer(Code) → Send simple answer 로 이어 줘.
Simple answer: 질문에 접수 번호가 있고 Get ticket 결과에 status 가 있으면 route 간단 답변, "<번호> 접수 건은 현재 '<status>' 상태입니다. <note>".
FAQ 결과가 있으면 route 간단 답변, 첫 FAQ answer + " (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)", sources [그 id].
아니면 route 담당자 연결. 모두 llm_calls 0, reason llm_down. 기대 결과 cases/lab03-llm-down.json.
```
실행한 명령:
```bash
uv run tools/flow.py push reference/lab03-example2-llm-fallback.json
uv run tools/flow.py test cases/lab03-llm-down.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0}
✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ VPN · FAQ 원문으로 · 2.6s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "간단 답변",
"category": "faq",
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다. (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"reason": "llm_down"
}
✔ 접수 현황 · 상태 그대로 · 0.3s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "간단 답변",
"category": "ticket",
"answer": "T-1002 접수 건은 현재 '처리 중' 상태입니다. 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다.",
"sources": [],
"llm_calls": 0,
"reason": "llm_down"
}
✔ 모르는 질문 · 담당자 · 0.8s
→ request POST /webhook/my-helpdesk
{
"question": "오늘 점심 메뉴 추천해 주세요",
"user": "이준호"
}
← response HTTP 200
{
"route": "담당자 연결",
"category": "other",
"answer": "지금은 자동 답변을 할 수 없어 담당자에게 연결합니다.",
"sources": [],
"llm_calls": 0,
"reason": "llm_down"
}
✔ 빈 질문 · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
Passed 4/4
report: runs/lab03-llm-down-20261024-175108.json
✔ OpenAI credential: real key from .env restored
```
**쉽게 말하면**
- LLM 노드에 오류 출력을 달고 간단 답변을 만든 뒤 4/4.
- VPN은 FAQ 원문을 그대로, 접수는 상태를 그대로(0.3초), 모르는 질문은 담당자로 보냅니다. LLM 없이 **데이터로 답할 수 있는 것은 데이터로** 답한 것입니다.
- VPN 2.6초 FAQ 원문 · 접수 0.3초 상태 그대로 · 점심 0.8초 담당자.
### 34 · `llm up` 뒤 `lab02` → 9/9
실행한 명령:
```bash
uv run tools/flow.py test cases/lab02.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0}
POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1
✔ 예제 1 · 프린터 FAQ는 규칙으로 · 1.8s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 568,
"answer_ms": 1078,
"review_ms": 12,
"total_ms": 1679
}
}
✔ 예제 1 · 접수 번호는 규칙으로 · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락 바랍니다.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 38,
"answer_ms": 969,
"review_ms": 15,
"total_ms": 1032
}
}
✔ 예제 1 · 없는 접수 번호 · 1.7s
→ request POST /webhook/my-helpdesk
{
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드리겠습니다.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 546,
"answer_ms": 1011,
"review_ms": 46,
"total_ms": 1632
}
}
✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 2.2s
→ request POST /webhook/my-helpdesk
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 2,
"cached": false,
"steps": [
"rule",
"classify",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 560,
"classify_ms": 881,
"answer_ms": 734,
"review_ms": 23,
"total_ms": 2206
}
}
✔ 예제 2 · 처음 묻는 VPN · 1.9s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 560,
"answer_ms": 1222,
"review_ms": 12,
"total_ms": 1803
}
}
✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.1s
→ request POST /webhook/my-helpdesk
{
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 20
}
}
✔ 예제 2 · no_cache면 캐시 안 씀 · 1.7s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 560,
"answer_ms": 1041,
"review_ms": 17,
"total_ms": 1625
}
}
✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 1.0s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락 바랍니다.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 27,
"answer_ms": 934,
"review_ms": 19,
"total_ms": 986
}
}
✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 1.2s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 31,
"answer_ms": 1016,
"review_ms": 43,
"total_ms": 1110
}
}
Passed 9/9
report: runs/lab02-20261024-175123.json
```
**쉽게 말하면**
- `llm up` 뒤 평소 테스트 9/9. 대안 길을 넣어도 평소에는 원래 길로 갑니다.
- 평소대로.
### 35 · 데이터 고장도 그대로 → `lab03` 4/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab03.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ 평소 · 1.7s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 571,
"answer_ms": 1006,
"review_ms": 16,
"total_ms": 1600
}
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0}
✔ FAQ가 느릴 때 (8초) · 4.6s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0}
✔ FAQ가 오류일 때 (500) · 0.6s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 2}
✔ before: POST /webhook/my-helpdesk · HTTP 200 · 1.9s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0}
✔ 캐시에 있으면 FAQ가 멈춰도 답 · 0.1s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 21
}
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
Passed 4/4
report: runs/lab03-20261024-175132.json
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
```
**쉽게 말하면**
- 데이터 고장 테스트(`lab03`)도 그대로 4/4. 예제 2를 하면서 예제 1의 설정을 망가뜨리지 않았다는 확인입니다.
- 예제 1의 설정이 남아 있음.
### 36 · 예제 3 · 고치기 전 → `lab03-ticket` 2/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab03-ticket.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1}
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ 평소 접수 현황 · 1.0s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 33,
"answer_ms": 915,
"review_ms": 12,
"total_ms": 970
}
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "slow_seconds": 8.0}
✘ 접수 API 500 · 접수 질문은 담당자 · 0.6s · reason = data_api_down (expected tickets_down)
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "slow_seconds": 8.0}
✘ 접수 API 500 · FAQ 질문은 그대로 답 · 0.6s · route = 담당자 연결 (expected 자동 답변); category = null (expected faq); answer has none of ['인증서']
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ 없는 접수 번호는 그대로 (404는 고장이 아님) · 1.6s
→ request POST /webhook/my-helpdesk
{
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 564,
"answer_ms": 978,
"review_ms": 10,
"total_ms": 1558
}
}
Passed 2/4
report: runs/lab03-ticket-20261024-175136.json
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
```
**쉽게 말하면**
- 고치기 **전**이라 2/4가 정상입니다. 이번 고장은 접수 API만 500입니다.
- ✘ `reason = data_api_down (expected tickets_down)`: 담당자 연결은 했지만 까닭을 "데이터 전체 고장"으로 적었습니다. 접수 API만 멈춘 것을 구분하지 못한 것입니다.
- ✘ `FAQ 질문은 그대로 답 · route = 담당자 연결 (expected 자동 답변)`: FAQ는 멀쩡한데 VPN 질문까지 담당자로 갔습니다. 모든 질문이 접수 조회를 지나가고, 오류 출력은 어떤 질문인지 가리지 않고 같은 길로 보내기 때문입니다.
- ✔ 없는 접수 번호(404)는 고장이 아니라 평소 답이 맞습니다.
- 접수 API 500에서 접수 질문은 `reason: data_api_down`, **VPN 질문도 `담당자 연결`** (0.6초). 오류 출력은 질문을 가리지 않음.
### 37 · 예제 3 · 접수 API만 고장 → 4/4
codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 `reference/lab03-example3-ticket-down.json`):
```text
my-work/helpdesk.json 의 Get ticket 응답 설정에 fullResponse 와 neverError 를 켜 줘 (결과가 {statusCode, body}).
Get ticket 보통 출력 다음에 Ticket check(Code): ticket = statusCode 500 이상이면 {} 아니면 body, ticket_status = statusCode,
tickets_down = statusCode 500 이상이고 질문에 접수 번호(/T-?\d{4}/i)가 있을 때만 true.
→ Tickets down?(If) true → Tickets down(Set: route 담당자 연결, category ticket, answer "접수 현황 시스템이 응답하지 않아 담당자에게 연결합니다. 잠시 뒤 다시 물어봐 주세요.", reason tickets_down, llm_calls 0) → Send tickets down
false → 지금 흐름(After data …).
$('Get ticket').first().json 을 읽던 곳은 모두 $('Ticket check').first().json.ticket 으로 바꾼다. 기대 결과 cases/lab03-ticket.json, lab03.json, lab03-llm-down.json 도 통과.
```
실행한 명령:
```bash
uv run tools/flow.py push reference/lab03-example3-ticket-down.json
uv run tools/flow.py test cases/lab03-ticket.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0}
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ 평소 접수 현황 · 2.4s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 102,
"answer_ms": 1458,
"review_ms": 13,
"total_ms": 1594
}
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "slow_seconds": 8.0}
✔ 접수 API 500 · 접수 질문은 담당자 · 0.1s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "담당자 연결",
"category": "ticket",
"answer": "접수 현황 시스템이 응답하지 않아 담당자에게 연결합니다. 잠시 뒤 다시 물어봐 주세요.",
"reason": "tickets_down",
"llm_calls": 0
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "slow_seconds": 8.0}
✔ 접수 API 500 · FAQ 질문은 그대로 답 · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 38,
"answer_ms": 1023,
"review_ms": 12,
"total_ms": 1083
}
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ 없는 접수 번호는 그대로 (404는 고장이 아님) · 1.3s
→ request POST /webhook/my-helpdesk
{
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "ticket",
"answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.",
"sources": [],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 44,
"answer_ms": 1192,
"review_ms": 22,
"total_ms": 1270
}
}
Passed 4/4
report: runs/lab03-ticket-20261024-175154.json
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
```
**쉽게 말하면**
- `fullResponse`로 상태 코드를 보고 나눈 뒤 4/4.
- 접수 질문만 `tickets_down`으로 0.1초에 담당자, VPN 질문은 평소처럼 `자동 답변`. **멈춘 부분만 넘기고 나머지는 평소대로** 한 것입니다.
- 접수 질문 `tickets_down` 0.1초, VPN 질문 `자동 답변` 1.1초 (`data_ms` 38ms), 없는 번호(404)는 평소 답.
### 38 · 예제 3 뒤 `lab03` → 4/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab03.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ 평소 · 1.2s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 61,
"answer_ms": 1037,
"review_ms": 14,
"total_ms": 1120
}
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0}
✔ FAQ가 느릴 때 (8초) · 4.8s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0}
✔ FAQ가 오류일 때 (500) · 0.6s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0}
✔ before: POST /webhook/my-helpdesk · HTTP 200 · 1.3s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0}
✔ 캐시에 있으면 FAQ가 멈춰도 답 · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 17
}
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
Passed 4/4
report: runs/lab03-20261024-175202.json
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
```
**쉽게 말하면**
- 예제 3을 한 뒤에도 FAQ 고장 처리(`lab03`)가 그대로 4/4입니다.
- FAQ 고장 처리 그대로.
### 39 · 예제 3 뒤 `lab03-llm-down` → 4/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab03-llm-down.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1}
✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ VPN · FAQ 원문으로 · 0.7s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "간단 답변",
"category": "faq",
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다. (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"reason": "llm_down"
}
✔ 접수 현황 · 상태 그대로 · 0.5s
→ request POST /webhook/my-helpdesk
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "간단 답변",
"category": "ticket",
"answer": "T-1002 접수 건은 현재 '처리 중' 상태입니다. 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다.",
"sources": [],
"llm_calls": 0,
"reason": "llm_down"
}
✔ 모르는 질문 · 담당자 · 0.3s
→ request POST /webhook/my-helpdesk
{
"question": "오늘 점심 메뉴 추천해 주세요",
"user": "이준호"
}
← response HTTP 200
{
"route": "담당자 연결",
"category": "other",
"answer": "지금은 자동 답변을 할 수 없어 담당자에게 연결합니다.",
"sources": [],
"llm_calls": 0,
"reason": "llm_down"
}
✔ 빈 질문 · 0.0s
→ request POST /webhook/my-helpdesk
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
Passed 4/4
report: runs/lab03-llm-down-20261024-175206.json
✔ OpenAI credential: real key from .env restored
```
**쉽게 말하면**
- LLM 멈춤 테스트도 4/4. 간단 답변이 바뀐 자리(`Ticket check`)에서 접수 건을 읽도록 함께 고쳤기 때문입니다.
- 간단 답변이 `Ticket check`의 접수 건을 읽음.
### 40 · 과제 1 · 상태 점검 → `lab03-challenge` 4/4
codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/health.json` · 이 기록에서 push한 같은 역할의 정답 [`72-lab03-challenge-health.json`](72-lab03-challenge-health.json)):
```text
my-work/health.json 을 새로 만들어 줘 (웹훅 my-health, AGENTS.md 3절).
Start(Code t0) → Check data API(GET http://data-api:8800/health, timeout 2000, onError continueRegularOutput, alwaysOutputData)
→ Check FAQ(GET /faq?q=vpn, 같은 설정) → Check LLM(Agent "Reply with the single word OK.", onError continueRegularOutput, alwaysOutputData;
모델 options timeout 8000, maxRetries 0) → Summarize(Code): data_api = status ok, faq = results 가 배열, llm = output 에 ok.
failed = 거짓인 것 이름. status: 모두 참 ok, data_api 나 llm 중 하나라도 참이면 degraded, 아니면 down. seconds.
기대 결과 cases/lab03-challenge.json.
```
실행한 명령:
```bash
uv run tools/flow.py push answers/72-lab03-challenge-health.json
uv run tools/flow.py test cases/lab03-challenge.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-health · 4 case(s) × 1
✔ 평소 · 2.2s
→ request POST /webhook/my-health
{}
← response HTTP 200
{
"status": "ok",
"checks": {
"data_api": true,
"faq": true,
"llm": true
},
"failed": [],
"seconds": 1.4
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0}
✔ FAQ 오류 · 1.0s
→ request POST /webhook/my-health
{}
← response HTTP 200
{
"status": "degraded",
"checks": {
"data_api": true,
"faq": false,
"llm": true
},
"failed": [
"faq"
],
"seconds": 1
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0}
✔ FAQ 느림 (8초) · 점검은 빨리 끝남 · 3.0s
→ request POST /webhook/my-health
{}
← response HTTP 200
{
"status": "degraded",
"checks": {
"data_api": true,
"faq": false,
"llm": true
},
"failed": [
"faq"
],
"seconds": 2.9
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up
✔ LLM 멈춤 · 0.4s
→ request POST /webhook/my-health
{}
← response HTTP 200
{
"status": "degraded",
"checks": {
"data_api": true,
"faq": true,
"llm": false
},
"failed": [
"llm"
],
"seconds": 0.3
}
✔ OpenAI credential: real key from .env restored
Passed 4/4
report: runs/lab03-challenge-20261024-175230.json
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
```
**쉽게 말하면**
- 상태 점검 `my-health` 4/4.
- 평소에는 `ok`, FAQ 오류 · LLM 멈춤에서는 `degraded`(일부만 됨)를 1초 안팎에 알려 줍니다. FAQ가 8초 느려도 점검은 3.0초에 끝납니다 (점검마다 짧은 timeout).
- 평소 `ok` 2.2초 · FAQ 오류 `degraded` 1.0초 · FAQ 느림 3.0초 · LLM 멈춤 0.4초.
### 41 · 과제 2 하기 전 → `lab03-challenge2` 3/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab03-challenge2.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0}
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ 평소 · 1.3s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 72,
"answer_ms": 1098,
"review_ms": 18,
"total_ms": 1212
}
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0}
✔ FAQ 느림 · 처음 한 번은 기다린 뒤 담당자 · 4.6s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
✘ FAQ 느림 · 다음 질문은 기다리지 않음 (차단기 열림) · 4.6s · took 4.6s (expected at most 1s); reason = data_api_down (expected breaker_open)
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진",
"no_cache": true
}
← response HTTP 200
{
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0}
✔ FAQ가 돌아오고 차단기를 비우면 평소대로 · 1.2s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 43,
"answer_ms": 1092,
"review_ms": 15,
"total_ms": 1161
}
}
Passed 3/4
report: runs/lab03-challenge2-20261024-175242.json
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ after: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0}
```
**쉽게 말하면**
- 과제 2를 하기 **전**이라 3/4가 정상입니다.
- ✘ `다음 질문은 기다리지 않음 · took 4.6s (expected at most 1s); reason = data_api_down (expected breaker_open)`: FAQ가 느리다는 것을 앞 질문에서 이미 알았는데도 다음 사람도 4.6초를 똑같이 기다렸다는 뜻입니다. 차단기가 없어서 매번 FAQ를 다시 부르기 때문입니다.
- FAQ 느림에서 다음 질문도 4.6초 기다린 뒤 `data_api_down`.
### 42 · 과제 2 · 차단기 → 4/4
codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 [`72-lab03-challenge2-breaker.json`](72-lab03-challenge2-breaker.json)):
```text
my-work/helpdesk.json 에 FAQ 차단기를 더해 줘 (AGENTS.md 3절 캐시 키 표).
Cached? false → Breaker check(GET http://data-api:8800/cache/breaker-faq, neverError) → Breaker open?(value 있음)
true → Breaker open(Set: route 담당자 연결, answer "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", reason breaker_open, llm_calls 0) → Send breaker open
false → Search FAQ.
Search FAQ 의 오류 출력 → Open breaker(POST /cache/breaker-faq {value:{open:true, since: Date.now()}, ttl 30}, neverError) → Data down.
닫는 노드는 만들지 않는다 (ttl 30초가 지나면 데이터 API가 지움). 기대 결과 cases/lab03-challenge2.json, lab03.json 도 통과.
```
실행한 명령:
```bash
uv run tools/flow.py push answers/72-lab03-challenge2-breaker.json
uv run tools/flow.py test cases/lab03-challenge2.json -v
```
출력 (그대로):
```text
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0}
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ 평소 · 2.7s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 114,
"answer_ms": 1678,
"review_ms": 14,
"total_ms": 1827
}
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0}
✔ FAQ 느림 · 처음 한 번은 기다린 뒤 담당자 · 4.8s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
✔ FAQ 느림 · 다음 질문은 기다리지 않음 (차단기 열림) · 0.1s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진",
"no_cache": true
}
← response HTTP 200
{
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "breaker_open",
"llm_calls": 0
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1}
✔ FAQ가 돌아오고 차단기를 비우면 평소대로 · 1.1s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 52,
"answer_ms": 1005,
"review_ms": 18,
"total_ms": 1084
}
}
Passed 4/4
report: runs/lab03-challenge2-20261024-175304.json
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ after: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0}
```
**쉽게 말하면**
- 차단기를 넣은 뒤 4/4.
- 처음 한 번은 4.8초 기다린 뒤 담당자(`data_api_down`), 그때 차단기를 엽니다. 다음 질문은 FAQ를 부르지 않고 **0.1초** 만에 담당자(`breaker_open`).
- FAQ가 돌아오고 캐시(차단기 포함)를 비우면 다시 평소 답(1.1초).
- 처음 4.8초 `data_api_down` → 다음 질문 **0.1초** `breaker_open` → FAQ가 돌아오고 캐시를 비우면 1.1초 평소 답.
### 43 · 과제 2 뒤 `lab03` → 4/4
실행한 명령:
```bash
uv run tools/flow.py test cases/lab03.json -v
```
출력 (그대로):
```text
POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1
✔ 평소 · 1.2s
→ request POST /webhook/my-helpdesk
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"cached": false,
"steps": [
"rule",
"faq",
"ticket",
"answer",
"check"
],
"timings": {
"data_ms": 77,
"answer_ms": 1000,
"review_ms": 16,
"total_ms": 1104
}
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0}
✔ FAQ가 느릴 때 (8초) · 4.6s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0}
✔ FAQ가 오류일 때 (500) · 0.1s
→ request POST /webhook/my-helpdesk
{
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
}
← response HTTP 200
{
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "breaker_open",
"llm_calls": 0
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1}
✔ before: POST /webhook/my-helpdesk · HTTP 200 · 1.3s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0}
✔ 캐시에 있으면 FAQ가 멈춰도 답 · 0.1s
→ request POST /webhook/my-helpdesk
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 21
}
}
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
Passed 4/4
report: runs/lab03-20261024-175311.json
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0}
```
**쉽게 말하면**
- 데이터 고장 테스트도 4/4.
- FAQ 오류 케이스가 이번에는 0.1초입니다. 바로 앞 케이스(FAQ 느림)가 연 차단기가 아직 30초 안이라 `breaker_open`으로 바로 넘어갔기 때문입니다. 테스트는 `route`(담당자 연결)만 보므로 통과입니다.
- FAQ 오류 케이스는 앞 케이스(FAQ 느림)가 연 차단기 때문에 **0.1초** `breaker_open` (29단계 예제 1 정답은 0.6초 `data_api_down`). 테스트는 `route`만 봐서 통과.
### 44 · 끝나면 doctor → ready
실행한 명령:
```bash
uv run tools/flow.py doctor
```
출력 (그대로):
```text
$ uv run tools/flow.py doctor
! n8n install folder /Users/mcchae/day65-n8n
→ day 65 SETUP-N8N 5단계 (get.n8n.io 설치). 다른 곳에 설치했다면 Last3Mon/65/n8n/.env 에 N8N_HOME=<폴더>
✔ n8n container day65-n8n-n8n-1
✔ n8n http://localhost:5678
✔ data API http://localhost:8800
✔ fault switch /admin/fault (day 72 data API)
✔ n8n → http://data-api:8800
✔ credential 'OpenAI (.env)' (openAiCourse0001)
✔ node catalog (for check)
✔ CLI coding agent: codex, claude, agy
ready
```
**쉽게 말하면**
- 끝나면 `doctor`로 고장 스위치 · OpenAI 키가 평소대로인지 확인합니다. 마지막 줄 `ready`면 됩니다.
- 첫 줄의 `!`는 강사 Mac에 `~/day65-n8n` 폴더가 없어서 나온 경고입니다. 학생 WSL에서는 ✔입니다.
- 강사 Mac에는 `~/day65-n8n` 폴더가 없어 첫 줄이 `!`입니다 (학생 WSL은 ✔).
## 다시 돌려 보기
```bash
cd ~/aio-01d4-stu/Last3Mon/72
uv run tools/flow.py push answers/72-lab01-challenge2-budget.json
uv run tools/flow.py test cases/lab01-challenge2.json -v # 4/4
uv run tools/flow.py push answers/72-lab02-challenge2-faqcache.json
uv run tools/flow.py test cases/lab02-challenge2.json -v # 6/6
uv run tools/flow.py push reference/lab03-example3-ticket-down.json
uv run tools/flow.py test cases/lab03-ticket.json -v # 4/4
uv run tools/flow.py push answers/72-lab03-challenge2-breaker.json answers/72-lab03-challenge-health.json
uv run tools/flow.py test cases/lab03-challenge2.json -v # 4/4
uv run tools/flow.py test cases/lab03-challenge.json -v # 4/4
uv run tools/flow.py doctor # ready
```
- 과제 정답 여섯 파일은 이 폴더(`Last3Mon/72/answers/`)에 있습니다 (강사 저장소 `Day/72/answers/`와 같은 파일). 예제 정답은 `reference/`.
- 고장 연습 뒤에는 `uv run tools/flow.py fault all ok`, `uv run tools/flow.py llm up`, `curl -s -XPOST localhost:8800/cache-clear`(차단기)로 되돌립니다 (테스트 파일은 `after`로 되돌림).