Files

219 KiB
Raw Permalink Blame History

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가 보여 주는 것

✔ 다른 말로 묻는 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)와 고장을 먼저 겪는 단계는 프롬프트가 없습니다.

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 응답에 따라 흔들립니다. 실패하면 먼저 같은 테스트를 한 번 더 돌려 봅니다.

시나리오 한눈에

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 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 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 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 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 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 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로 재기

실행한 명령:

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

출력 (그대로):

$ 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

실행한 명령:

uv run tools/flow.py test cases/lab01.json -v --webhook base-helpdesk

출력 (그대로):

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):

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.

실행한 명령:

uv run tools/flow.py push reference/lab01-example2-timings.json
uv run tools/flow.py test cases/lab01.json -v

출력 (그대로):

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 보기

실행한 명령:

uv run tools/flow.py call my-helpdesk requests/faq-vpn.json -v

출력 (그대로):

$ 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 · 질문 종류별로 재기

실행한 명령:

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

출력 (그대로):

$ 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

실행한 명령:

uv run tools/flow.py test cases/lab01-challenge.json -v

출력 (그대로):

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):

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 둘 다 통과.

실행한 명령:

uv run tools/flow.py push answers/72-lab01-challenge-timings.json
uv run tools/flow.py test cases/lab01-challenge.json -v

출력 (그대로):

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

실행한 명령:

uv run tools/flow.py test cases/lab01.json -v

출력 (그대로):

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

실행한 명령:

uv run tools/flow.py test cases/lab01-challenge2.json -v

출력 (그대로):

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):

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 모두 통과.

실행한 명령:

uv run tools/flow.py push answers/72-lab01-challenge2-budget.json
uv run tools/flow.py test cases/lab01-challenge2.json -v

출력 (그대로):

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

실행한 명령:

uv run tools/flow.py test cases/lab01-challenge.json -v

출력 (그대로):

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

실행한 명령:

uv run tools/flow.py test cases/lab01.json -v

출력 (그대로):

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):

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 앞 네 케이스.

실행한 명령:

uv run tools/flow.py push reference/lab02-example1-rules.json
uv run tools/flow.py test cases/lab02.json -v

출력 (그대로):

✔ 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 · 규칙으로 정한 질문 재기

실행한 명령:

uv run tools/flow.py bench my-helpdesk requests/faq-printer.json -n 5

출력 (그대로):

$ 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):

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 도 통과.

실행한 명령:

uv run tools/flow.py push reference/lab02-example2-cache.json
uv run tools/flow.py test cases/lab02.json -v

출력 (그대로):

✔ 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

실행한 명령:

uv run tools/flow.py test cases/lab01.json -v

출력 (그대로):

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 · 캐시 적중 세기

실행한 명령:

curl -s localhost:8800/cache-stats

출력 (그대로):

$ 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 · 캐시 씀 · 안 씀

실행한 명령:

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

출력 (그대로):

$ 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

실행한 명령:

uv run tools/flow.py test cases/lab02-challenge.json -v

출력 (그대로):

✔ 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 하기 전 시간

실행한 명령:

uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5

출력 (그대로):

$ 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):

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 모두 통과.

실행한 명령:

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

출력 (그대로):

✔ 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.11.2초 · 비밀번호 0.91.0초 · 접수 0.91.1초 (llm_calls: 1), 규칙으로 모르는 질문 1.41.6초 (2).

21 · 과제 1 뒤 lab02 → 9/9

실행한 명령:

uv run tools/flow.py test cases/lab02.json -v

출력 (그대로):

✔ 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

실행한 명령:

uv run tools/flow.py test cases/lab01.json -v

출력 (그대로):

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 뒤 시간

실행한 명령:

uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5

출력 (그대로):

$ 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

실행한 명령:

uv run tools/flow.py test cases/lab02-challenge2.json -v

출력 (그대로):

✔ 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):

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 도 통과.

실행한 명령:

uv run tools/flow.py push answers/72-lab02-challenge2-faqcache.json
uv run tools/flow.py test cases/lab02-challenge2.json -v

출력 (그대로):

✔ 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

실행한 명령:

uv run tools/flow.py test cases/lab02-challenge.json -v

출력 (그대로):

✔ 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

실행한 명령:

uv run tools/flow.py test cases/lab02.json -v

출력 (그대로):

✔ 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 · 고치기 전에 고장 겪기

실행한 명령:

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

출력 (그대로):

$ 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):

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 도 통과.

실행한 명령:

uv run tools/flow.py push reference/lab03-example1-resilient.json
uv run tools/flow.py test cases/lab03.json -v

출력 (그대로):

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

실행한 명령:

uv run tools/flow.py test cases/lab02.json -v

출력 (그대로):

✔ 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 멈춤 겪기

실행한 명령:

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

출력 (그대로):

$ 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

실행한 명령:

uv run tools/flow.py test cases/lab03-llm-down.json -v

출력 (그대로):

✔ 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):

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.

실행한 명령:

uv run tools/flow.py push reference/lab03-example2-llm-fallback.json
uv run tools/flow.py test cases/lab03-llm-down.json -v

출력 (그대로):

✔ 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

실행한 명령:

uv run tools/flow.py test cases/lab02.json -v

출력 (그대로):

✔ 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

실행한 명령:

uv run tools/flow.py test cases/lab03.json -v

출력 (그대로):

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

실행한 명령:

uv run tools/flow.py test cases/lab03-ticket.json -v

출력 (그대로):

✔ 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):

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 도 통과.

실행한 명령:

uv run tools/flow.py push reference/lab03-example3-ticket-down.json
uv run tools/flow.py test cases/lab03-ticket.json -v

출력 (그대로):

✔ 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

실행한 명령:

uv run tools/flow.py test cases/lab03.json -v

출력 (그대로):

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

실행한 명령:

uv run tools/flow.py test cases/lab03-llm-down.json -v

출력 (그대로):

✔ 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):

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.

실행한 명령:

uv run tools/flow.py push answers/72-lab03-challenge-health.json
uv run tools/flow.py test cases/lab03-challenge.json -v

출력 (그대로):

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

실행한 명령:

uv run tools/flow.py test cases/lab03-challenge2.json -v

출력 (그대로):

✔ 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):

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 도 통과.

실행한 명령:

uv run tools/flow.py push answers/72-lab03-challenge2-breaker.json
uv run tools/flow.py test cases/lab03-challenge2.json -v

출력 (그대로):

✔ 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

실행한 명령:

uv run tools/flow.py test cases/lab03.json -v

출력 (그대로):

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

실행한 명령:

uv run tools/flow.py doctor

출력 (그대로):

$ 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은 ✔).

다시 돌려 보기

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로 되돌림).