219 KiB
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.1
1.2초 · 비밀번호 0.91.0초 · 접수 0.91.1초 (1.6초 (2).llm_calls: 1), 규칙으로 모르는 질문 1.4
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/NONE404에도 걸림 (예제 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_down0.1초, VPN 질문자동 답변1.1초 (data_ms38ms), 없는 번호(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-health4/4. -
평소에는
ok, FAQ 오류 · LLM 멈춤에서는degraded(일부만 됨)를 1초 안팎에 알려 줍니다. FAQ가 8초 느려도 점검은 3.0초에 끝납니다 (점검마다 짧은 timeout). -
평소
ok2.2초 · FAQ 오류degraded1.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로 되돌림).