72일차 정답 추가 (실습 정답본 · answers · VIBE-A · tbench)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mcchaeandClaude Opus 5.5 committed 2026-10-26 20:15:51 +09:00
1 parent baf72561f7
commit d88c5122d5
105 files changed
+24165

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
# Lab 01 · 재고, 단계별 시간을 남기고, 병목 찾기
> **강사용 정답본** · 학생 저장소에 올리지 않습니다. 학생용 문제는 [LAB-01-Q-MEASURE-BOTTLENECK.md](LAB-01-Q-MEASURE-BOTTLENECK.md).
> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 정답 워크플로우를 2026-10-18에 1회씩 실행한 값입니다 (강사 Mac, gpt-4.1-mini).
> 10/24에 실습을 5개로 늘리며 정답을 학생 순서대로 다시 돌렸습니다 (45단계). 명령 · `-v` 출력 전부와 codex 프롬프트: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md).
## 예제 1 · 기준
| 요청 | 결과 |
| --- | --- |
| `base-helpdesk` push 직후 첫 호출 (`faq-vpn`) | 6.5초 |
| `bench base-helpdesk requests/faq-vpn.json -n 5` | p50 2.61초 · p95 2.83초 · 실패 0 |
| `call base-helpdesk` `ticket-own` · `off-topic` | 3.3초 · 3.1초 |
- `off-topic`(점심 메뉴)도 LLM을 세 번 부르고 `route: 자동 답변`으로 "찾지 못했습니다"를 답합니다. 3단계 대체 경로와 2단계 규칙에서 다시 이야기합니다.
## 예제 2 · 요청문 예
```text
my-work/helpdesk.json 은 reference/baseline-helpdesk.json 을 복사한 것이야. 웹훅 경로를 my-helpdesk, 이름을 "72 IT helpdesk (my)"로 바꾸고,
답은 그대로 두고 결과에 다음을 더해 줘 (AGENTS.md 3절, 5절의 시각 남기기).
- Has question? 다음에 Start clock, Parse category 뒤에 After classify, Get ticket 뒤에 After data, Answer 뒤에 After answer, Review 뒤에 After review (각각 Date.now()).
- Build result 에서 timings {classify_ms, data_ms, answer_ms, review_ms, total_ms}, steps(지나간 단계), llm_calls(지나간 Agent 수), cached:false.
지나갔는지는 $('노드').isExecuted 로.
기대 결과 cases/lab01.json.
```
**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`)
| 빈칸 | 답 | 왜 |
| --- | --- | --- |
| ① | `After data` | 다른 시각 노드처럼 "After + 끝난 단계". 데이터(FAQ · 접수 조회)가 끝난 뒤 |
| ② | `Date.now()` | 지금 시각을 ms 숫자로 (AGENTS.md 5절 `Start clock` 예) |
| ③ | `data_ms` | `timings` 칸 이름 규칙 `<단계>_ms` (AGENTS.md 3절) |
| ④ | `$('노드').isExecuted` | 노드가 실행됐으면 true (AGENTS.md 5절) |
정답: [reference/lab01-example2-timings.json](reference/lab01-example2-timings.json)
| 수업 준비 때 결과 | |
| --- | --- |
| `cases/lab01.json` | 4/4 (기준 `--webhook base-helpdesk`는 1/4) |
| `faq-vpn`의 `timings` | classify 524 · data 21 · answer 1017 · review 919 · total 2500 ms (다른 실행: 565 · 23 · 1063 · 879 · 2546) |
- LLM 세 번이 전체의 약 99%. 데이터 API 두 번은 20ms대.
- 10/24 다시 돌린 결과: `lab01` **3/4** — 접수 케이스에서 검토 LLM이 "처리 중"을 "승인 대기 중"으로 바꿔 써 실패 (정답의 잘못이 아님). VPN `timings` 616 · 18 · 1124 · 920 · 2698ms.
## 예제 3 · 결과 (에이전트 없이, 10/24)
예제 2 정답(`reference/lab01-example2-timings.json`) 그대로, 캐시 없음.
| 질문 종류 | p50 | p95 | classify | data | answer | review | total | LLM 몫 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| FAQ (VPN) | 2.91초 | 3.08초 | 616 | 18 | 1124 | 920 | 2698 | 98.6% |
| 접수 번호 (T-1002) | 2.99초 | 3.12초 | 818 | 43 | 877 | 895 | 2647 | 97.8% |
| IT 밖 (점심 메뉴) | 2.48초 | 2.84초 | 732 | 19 | 740 | 978 | 2482 | 98.7% |
(p50 · p95는 `bench -n 5`, 단계 칸은 `call -v` 한 번의 ms. FAQ 줄의 단계 칸은 예제 2 `call` 값)
- 학생이 쓸 한 줄 예: "세 종류 모두 LLM이 98%. 점심 메뉴는 데이터가 없는데도 LLM 세 번 → 규칙으로 걸러 낼 후보."
- 같은 실행에서 `lab01` 접수 케이스가 한 번 실패했습니다 (검토 LLM이 "처리 중"을 "승인 대기 중"으로 바꿈). Lab 02 과제 1의 이야기 거리.
## 과제 1 · 요청문 예
```text
my-work/helpdesk.json 의 Search FAQ 와 Get ticket 사이에 After FAQ(Date.now()) 를 더해, timings 의 data_ms 를 faq_ms 와 ticket_ms 로 나눠 줘.
total_ms 를 뺀 timings 중 가장 큰 단계 이름(_ms 뺀 것)을 slowest_step 으로. cases/lab01-challenge.json, cases/lab01.json 둘 다 통과.
```
**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`)
| 빈칸 | 답 | 왜 |
| --- | --- | --- |
| ① | `After FAQ` | FAQ 검색이 끝난 뒤의 시각 노드 |
| ② | `faq_ms` | `cases/lab01-challenge.json`의 `expect` 칸 이름 |
| ③ | `ticket_ms` | ②와 짝. `data_ms`를 FAQ와 접수 조회로 나눔 |
| ④ | 가장 큰 | "가장 느린" = `_ms` 값이 가장 큰 칸 |
정답: [answers/72-lab01-challenge-timings.json](answers/72-lab01-challenge-timings.json)
| 수업 준비 때 결과 | |
| --- | --- |
| `cases/lab01-challenge.json` | 4/4 (예제 2 상태 0/4) |
| `cases/lab01.json` | 4/4 |
| `faq-vpn` | classify 565 · faq 14 · ticket 9 · answer 1063 · review 879 · total 2546 ms, `slowest_step: answer` |
10/24 다시 돌린 결과: `lab01-challenge` 4/4, `slowest_step`은 네 번 중 answer 2 · review 1 · classify 1 (push 직후 첫 호출에서 classify 1365ms).
## 과제 2 · 요청문 예
```text
my-work/helpdesk.json 의 Build result 에서, 본문 budget_ms (없으면 3000, 숫자로)를 읽어
결과에 llm_ms(classify · answer · review 의 _ms 합, timings 밖에 둔다), budget_ms, over_budget(timings.total_ms > budget_ms)을 더해 줘.
timings, slowest_step 은 그대로. cases/lab01-challenge2.json, cases/lab01-challenge.json, cases/lab01.json 모두 통과.
```
**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`)
| 빈칸 | 답 | 왜 |
| --- | --- | --- |
| ① | `budget_ms` | 본문 입력 칸 = 결과 칸 이름 (AGENTS.md 3절 "입력") |
| ② | `3000` | 기본 예산 3초 (조건 표 · AGENTS.md 3절) |
| ③ | 밖 | `timings` 안에 두면 `slowest_step`이 `llm` 합계를 고르게 됨 |
| ④ | `timings.total_ms` | 요청 하나 전체 시간 |
정답: [answers/72-lab01-challenge2-budget.json](answers/72-lab01-challenge2-budget.json) (과제 1 정답의 `Build result`에 세 줄)
| 10/24 결과 | |
| --- | --- |
| 고치기 전 (과제 1 정답) | `lab01-challenge2` 0/4 (칸이 없음) |
| 고친 뒤 | `lab01-challenge2` 4/4 · `lab01-challenge` 4/4 · `lab01` 4/4 |
| 예산 60000 | `total_ms` 3357 (push 직후) · `llm_ms` 3295 · `over_budget: false` |
| 예산 500 | 2609 · 2563 · `true` |
| 기본 3000 (VPN) | 3023 · 2973 · **`true`** (23ms 넘음) |
| 기본 3000 (접수) | 2904 · 2858 · `false` |
- 기본 예산에서 `over_budget`이 실행마다 바뀌는 것이 정상입니다 (VPN이 3초 안팎). 풀이 때 "그래서 2단계에서 줄인다"로 잇습니다.
- 처음 만든 케이스 파일은 접수 케이스에 답 "처리 중"을 기대했는데, 검토 LLM이 문장을 바꿔 실패했습니다 (10/24). 모양을 보는 테스트이므로 `category: ticket`으로 바꿨습니다.
+143
View File
@@ -0,0 +1,143 @@
# Lab 02 · 규칙 먼저, 캐시, LLM 한 번
> **강사용 정답본** · 학생 저장소에 올리지 않습니다. 학생용 문제는 [LAB-02-Q-OPTIMIZE-FLOW.md](LAB-02-Q-OPTIMIZE-FLOW.md).
> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 정답 워크플로우를 2026-10-18에 1회씩 실행한 값입니다 (강사 Mac, gpt-4.1-mini, bench는 n=5).
> 10/24에 실습을 5개로 늘리며 정답을 학생 순서대로 다시 돌렸습니다 (45단계). 명령 · `-v` 출력 전부와 codex 프롬프트: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md). 10/24 점수: 예제 1 `lab02` 7/9 (캐시 케이스 · 접수 답에서 검토 LLM이 "처리 중"을 바꿈), 나머지는 모두 기대대로.
## 예제 1 · 요청문 예
```text
my-work/helpdesk.json 의 순서를 바꿔 줘: Has question? → Start clock → Search FAQ → Get ticket → After data → Rule classify(Code) → Known?(If).
Rule classify: 질문에 T-1234 같은 번호(/T-?\d{4}/i)가 있으면 ticket, FAQ 결과가 1개 이상이면 faq, 아니면 unknown. {category, by:'rule'}.
Known? true → Answer, false → Classify → Parse category → After classify → Answer. Answer 는 $json.category 로 분류를 읽는다.
Build result: 분류는 Parse category 가 실행됐으면 그것, 아니면 Rule classify. steps 에 rule 또는 classify.
나머지(답, 검토, timings)는 그대로. 기대 결과 cases/lab02.json 앞 네 케이스.
```
**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`)
| 빈칸 | 답 | 왜 |
| --- | --- | --- |
| ① | `/T-?\d{4}/i` | 하이픈 있어도 없어도, 대소문자 무시, 숫자 네 자리 (`T-1002` · `t1002`) |
| ② | 1개 이상 | `/faq`의 `results`가 비어 있지 않으면 FAQ로 답할 수 있음 |
| ③ | `Parse category` | LLM 분류 길에서 분류 결과를 만드는 마지막 노드 |
| ④ | `rule` | 규칙 길을 지났다는 `steps` 이름 (AGENTS.md 3절) |
정답: [reference/lab02-example1-rules.json](reference/lab02-example1-rules.json)
| 수업 준비 때 결과 | |
| --- | --- |
| `cases/lab02.json` | 7/9 (예제 2의 "같은 질문" 캐시 케이스와, 답 문장이 기대 표현과 달랐던 "없는 접수 번호" 실패. 10/23~24에는 "없는 접수 번호"가 통과해 8/9) |
| `cases/lab01.json` | 4/4 |
| 규칙 질문 시간 | 접수 1.9~2.3초 (예제 2 timings 상태에서 2.4~2.8초) |
- 처음 쓴 "없는 접수 번호" 기대(`찾지 못`)가 틀렸습니다: 검토 LLM이 "확인되지 않습니다"로 바꿔 말함. 기대를 "없다는 여러 표현 + 상태를 지어내지 않음(`not_contains` 처리 중 등)"으로 고쳤습니다 (71일차 "모른다의 여러 모양"과 같음).
## 예제 2 · 요청문 예
```text
my-work/helpdesk.json 에 캐시를 더해 줘 (AGENTS.md 2절 데이터 API, 3절 캐시 규칙).
Start clock 다음: Cache key(Code: question 을 trim·소문자·연속 공백 하나, use_cache = body.no_cache !== true)
→ Cache lookup(GET http://data-api:8800/cache/<encodeURIComponent(key)>, neverError) → Cached?(value 있고 use_cache)
true → Cached result(route·category·answer·sources + llm_calls 0, cached true, steps ['cache'], timings.total_ms) → Send cached
false → 지금 흐름(Search FAQ ...).
Build result 뒤: Cache it?(category faq 이고 use_cache) true → Store in cache(POST /cache/<key> {value:{route,category,answer,sources}, ttl 600}) → Send result.
접수(ticket)는 저장하지 않는다. 기대 결과 cases/lab02.json 전체, lab01 도 통과.
```
**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`)
| 빈칸 | 답 | 왜 |
| --- | --- | --- |
| ① | trim · 소문자 · 연속 공백 하나 | 띄어쓰기 · 대소문자만 다른 질문을 같은 키로 (AGENTS.md 3절) |
| ② | `body.no_cache !== true` | 본문 `no_cache`가 true가 아닐 때만 캐시 사용 |
| ③ | `neverError` | 캐시에 없을 때의 404에서 멈추지 않게 (AGENTS.md 5절) |
| ④ | 접수(ticket) | 상태가 바뀌는 답은 캐시하지 않음 (AGENTS.md 3절 굵은 줄) |
정답: [reference/lab02-example2-cache.json](reference/lab02-example2-cache.json)
| 수업 준비 때 결과 | |
| --- | --- |
| `cases/lab02.json` | 9/9 · 같은 질문(띄어쓰기·대소문자 다름) 0.1초, `llm_calls: 0` |
| `cases/lab01.json` · `lab03.json` | 4/4 · 2/4 (3단계 전) |
| 캐시 없이 (`faq-vpn-nocache`) bench | p50 2.01초 · p95 2.23초 |
- 처음에는 "규칙으로 모르는 질문"(회의실 화면 공유)이 LLM 분류로 `faq`가 되어 **캐시에 들어갔고**, 다음 테스트에서 0.1초로 나와 `llm_calls` 기대가 깨졌습니다. 그 케이스에 `no_cache: true`를 넣었습니다 → 수업 이야기 거리: "캐시는 테스트도 바꾼다".
## 예제 3 · 결과 (에이전트 없이, 10/24)
예제 2 정답(`reference/lab02-example2-cache.json`), 캐시를 비운 뒤.
| 재기 | 첫 호출 | p50 | max |
| --- | --- | --- | --- |
| 캐시 씀 (`faq-vpn` × 5) | 2.07초 (LLM 2번) | **0.05초** | 2.07초 |
| 캐시 안 씀 (`faq-vpn-nocache` × 5) | 1.99초 | 1.99초 | 2.24초 |
- `/cache-stats` 뒤: `entries 1 · hits 9 · misses 1`. `no_cache` 요청도 캐시를 **조회는** 하므로 hits로 셉니다 (쓰지는 않음). 학생이 "적중 9번?"이라고 하면 이 이야기를 합니다.
- 적중률 어림 (0.05초 · 2.0초): 10% → 1.81초, 30% → 1.42초, 60% → 0.83초.
## 과제 1 · 요청문 예
```text
my-work/helpdesk.json 에서 Review(검토 LLM)와 Review model 을 지우고, 그 자리에 Review check(Code)를 둬:
Answer 의 output 을 trim, 비었으면 empty, 한글이 없으면 not_korean, 400자 넘으면 자르고 too_long 을 review_problems 에.
Answer 지시문에 "합니다체, 3문장 이내"가 이미 있는지 확인. steps 에는 review 대신 check, llm_calls 는 검토를 세지 않는다.
나머지는 그대로. cases/lab02-challenge.json, lab02.json, lab01.json 모두 통과.
```
**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`)
| 빈칸 | 답 | 왜 |
| --- | --- | --- |
| ① | `400` | 답 길이 상한 (교안 2단계 6절 검사 표) |
| ② | 합니다체, 3문장 이내 | 검토 LLM이 하던 말투 · 길이 다듬기를 지시문으로 |
| ③ | `check` | 검토 대신 코드 검사를 지났다는 단계 이름 (다른 짧은 이름도 됨) |
| ④ | 검토를 세지 않는다 | 지운 Review는 빼고 셈: 규칙 길 1, LLM 분류 길 2 |
정답: [answers/72-lab02-challenge-fast.json](answers/72-lab02-challenge-fast.json)
| 수업 준비 때 결과 | |
| --- | --- |
| `cases/lab02-challenge.json` | 5/5 (VPN 1.2초 · 비밀번호 1.1초 · 접수 1.3초) · 캐시 단계 상태 1/5 |
| `cases/lab02.json` · `lab01.json` | 9/9 · 4/4 |
| 캐시 없이 bench | p50 2.01 → **1.15초** · p95 2.23 → **1.16초** |
- 처음 쓴 `lab01` · `lab02` 기대에 `llm_calls: 3`, `steps`에 `review` 같은 **구조**가 들어 있어, 과제로 정당하게 빨라진 워크플로우가 실패했습니다. 품질 테스트는 **답의 내용**만 보게 고쳤습니다 (구조 기대는 그 단계의 테스트에만) → 이야기 거리: "최적화하면 깨지는 테스트는 무엇을 재고 있었나".
10/24 다시 돌린 결과: 고치기 전 `lab02-challenge` 1/5, bench p50 2.22 · p95 2.45초 → 고친 뒤 15/15 (`--repeat 3`, 0.9~1.6초), `lab02` 9/9, `lab01` 4/4, bench p50 **1.12** · p95 1.32초.
## 과제 2 · 요청문 예
```text
my-work/helpdesk.json 에 FAQ 번호 캐시를 더해 줘 (AGENTS.md 3절 캐시 키 표).
Rule classify 다음: FAQ cache key(Code: category 가 faq 이고 use_cache 이고 FAQ 결과가 있으면 faq_key = 'faq-' + 첫 FAQ id, 아니면 'none')
→ FAQ cache lookup(GET /cache/<faq_key>, neverError) → FAQ cached?(value 있고 faq_key 가 'none' 아님)
true → FAQ cached result(llm_calls 0, cached true, cache_by 'faq', steps ['faq', 'rule', 'cache']) → Send
false → Back to rule(Code: Rule classify 의 결과를 그대로) → Known?
Store in cache 뒤에 Store by FAQ(POST /cache/<faq_key>, 같은 value, ttl 600) → Result again.
질문 캐시의 Cached result 에는 cache_by 'question', Build result 에는 cache_by null.
기대 결과 cases/lab02-challenge2.json, lab02-challenge.json, lab02.json 도 통과.
```
**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`)
| 빈칸 | 답 | 왜 |
| --- | --- | --- |
| ① | `faq-` | 캐시 키 표 둘째 줄: `faq-<FAQ id>` |
| ② | `faq` | FAQ 번호 키로 찾았을 때 (AGENTS.md 3절 Lab 02 과제 2) |
| ③ | `600` | 질문 캐시와 같은 ttl (초) |
| ④ | `question` | ②와 짝. 질문 키로 찾았을 때 |
정답: [answers/72-lab02-challenge2-faqcache.json](answers/72-lab02-challenge2-faqcache.json) (과제 1 정답에 노드 7개: FAQ cache key · FAQ cache lookup · FAQ cached? · FAQ cached result · Send FAQ cached · Back to rule · Store by FAQ)
| 10/24 결과 (`cases/lab02-challenge2.json`) | 고치기 전 (과제 1 정답) | 고친 뒤 |
| --- | --- | --- |
| 처음 묻는 VPN | ✔ 1.1초 | ✔ 2.5초 (push 직후) |
| 다른 말 VPN | ✘ `cached: false`, LLM 1번, 1.2초 | ✔ `cache_by: faq`, LLM 0번, **0.1초** |
| 같은 말 | ✘ `cache_by` 칸 없음 (캐시는 맞음, 0.1초) | ✔ `cache_by: question`, 0.0초 |
| `no_cache` | ✔ | ✔ 1.2초 |
| 프린터 처음 · 접수 현황 | ✔ | ✔ |
| 합 | 4/6 | 6/6, `lab02-challenge` 5/5, `lab02` 9/9 |
- `Back to rule`이 없으면 `Known?`이 캐시 응답을 읽어 분류가 사라집니다. 학생이 가장 많이 막힐 곳으로 예상합니다 (수업 준비 때 예상).
- 풀이 때 "잃는 것": FAQ-003에 묶인 질문은 모두 처음 질문에 맞춘 답을 받습니다.
+164
View File
@@ -0,0 +1,164 @@
# Lab 03 · 멈춰도 답하게
> **강사용 정답본** · 학생 저장소에 올리지 않습니다. 학생용 문제는 [LAB-03-Q-STABILITY.md](LAB-03-Q-STABILITY.md).
> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 정답 워크플로우를 2026-10-18에 1회씩 실행한 값입니다 (강사 Mac, gpt-4.1-mini).
> 10/24에 실습을 5개로 늘리며 정답을 학생 순서대로 다시 돌렸습니다 (45단계). 명령 · `-v` 출력 전부와 codex 프롬프트: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md).
## 예제 1 · 고치기 전 (캐시 단계 워크플로우)
| 상황 | 결과 |
| --- | --- |
| FAQ 느림 (8초) | **9.0초** 기다린 뒤 `자동 답변` (사용자는 9초를 기다림) |
| FAQ 오류 (500) | 0.1초 만에 **HTTP 500** `Error in workflow` |
| 캐시에 있는 질문 + FAQ 오류 | 0.1초, `cached: true` (캐시 확인이 FAQ 앞에 있어서 이미 됨) |
| `cases/lab03.json` | 2/4 |
## 예제 1 · 요청문 예
```text
my-work/helpdesk.json 의 Search FAQ 와 Get ticket 에 options.timeout 2000, 노드 설정 retryOnFail true, maxTries 2, waitBetweenTries 500,
onError continueErrorOutput 을 넣어 줘. 두 노드의 오류 출력(main 1번)은 Data down(Set: route 담당자 연결, answer "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", reason data_api_down)
→ Send data down(Respond). Get ticket 은 404 를 받아야 하니 neverError 는 그대로 둔다. 기대 결과 cases/lab03.json, lab02 도 통과.
```
**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`)
| 빈칸 | 답 | 왜 |
| --- | --- | --- |
| ① | `2000` | 2초를 ms로 |
| ② | `2` | 처음 + 다시 한 번. 최악 2 × 2000 + 500 = 4.5초로 느림 케이스 안 |
| ③ | `continueErrorOutput` | 실패하면 두 번째(오류) 출력으로 (AGENTS.md 5절) |
| ④ | `neverError` | 404(없는 번호)를 보통 결과로 받기 |
정답: [reference/lab03-example1-resilient.json](reference/lab03-example1-resilient.json)
| 수업 준비 때 결과 | |
| --- | --- |
| `cases/lab03.json` | 4/4 · FAQ 느림 **4.6초** `담당자 연결` · FAQ 오류 **0.6초** `담당자 연결` · 캐시 0.1초 |
- **바로잡음 (10/24):** 수업 준비 때 여기에 "`neverError` 때문에 접수 API 500이어도 실패로 보지 않는다"고 적었는데, 시험 워크플로우로 재 보니 이 n8n(2.42.6)에서 `neverError`는 **4xx만** 보통 결과로 받고 500은 재시도 뒤 오류 출력으로 갑니다 ([AGENTS.md](AGENTS.md) 5절 표). 대신 404도 재시도를 해서, 이 정답에서 VPN 질문의 `data_ms`가 605ms입니다 (예제 2 단계 32~76ms). 둘 다 예제 3에서 다룹니다.
## 예제 2 · 요청문 예
```text
my-work/helpdesk.json 의 Classify 와 Answer Agent 에 onError continueErrorOutput 을 넣고, 두 노드의 오류 출력을 Simple answer(Code) → Send simple answer 로 이어 줘.
Simple answer: 질문에 접수 번호가 있고 Get ticket 결과에 status 가 있으면 route 간단 답변, "<번호> 접수 건은 현재 '<status>' 상태입니다. <note>".
FAQ 결과가 있으면 route 간단 답변, 첫 FAQ answer + " (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)", sources [그 id].
아니면 route 담당자 연결. 모두 llm_calls 0, reason llm_down. 기대 결과 cases/lab03-llm-down.json.
```
**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`)
| 빈칸 | 답 | 왜 |
| --- | --- | --- |
| ① | `간단 답변` | LLM 없이 데이터 그대로 답하는 `route` (두 곳 같음) |
| ② | 자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다. | 평소 답이 아니라는 안내 (문장은 학생이 정해도 됨) |
| ③ | `담당자 연결` | 데이터로도 답할 수 없을 때 사람에게 |
| ④ | `llm_down` | AGENTS.md 3절 Lab 03 `reason` 중 LLM이 멈춘 경우 |
정답: [reference/lab03-example2-llm-fallback.json](reference/lab03-example2-llm-fallback.json)
| 수업 준비 때 결과 | |
| --- | --- |
| 고치기 전 (`lab03-llm-down`) | 1/4 · 세 질문 모두 **HTTP 500** (빈 질문 400만 통과) |
| 고친 뒤 | 4/4 · VPN 2.4초 `간단 답변` · 접수 0.3초 `간단 답변` · 점심 1.1초 `담당자 연결` |
| `llm up` 뒤 `lab03` | 4/4 |
- `flow.py llm down`은 `Last3Mon/65/n8n/register-credentials.sh`를 틀린 키로 실행합니다. n8n 재시작 없이 바로 적용되고, `llm up`도 바로 돌아왔습니다.
## 예제 3 · 요청문 예
```text
my-work/helpdesk.json 의 Get ticket 응답 설정에 fullResponse 와 neverError 를 켜 줘 (결과가 {statusCode, body}).
Get ticket 보통 출력 다음에 Ticket check(Code): ticket = statusCode 500 이상이면 {} 아니면 body, ticket_status = statusCode,
tickets_down = statusCode 500 이상이고 질문에 접수 번호(/T-?\d{4}/i)가 있을 때만 true.
→ Tickets down?(If) true → Tickets down(Set: route 담당자 연결, category ticket, answer "접수 현황 시스템이 응답하지 않아 담당자에게 연결합니다. 잠시 뒤 다시 물어봐 주세요.", reason tickets_down, llm_calls 0) → Send tickets down
false → 지금 흐름(After data …).
$('Get ticket').first().json 을 읽던 곳은 모두 $('Ticket check').first().json.ticket 으로 바꾼다. 기대 결과 cases/lab03-ticket.json, lab03.json, lab03-llm-down.json 도 통과.
```
**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`)
| 빈칸 | 답 | 왜 |
| --- | --- | --- |
| ① | `fullResponse` | `{statusCode, body}`를 통째로 받기 (AGENTS.md 5절 표 셋째 줄) |
| ② | `500 이상` | 서버 고장 범위. 404는 고장이 아님 (두 곳 같음) |
| ③ | `tickets_down` | AGENTS.md 3절 `reason` 중 접수 API만 멈춘 경우 |
| ④ | `$('Ticket check').first().json.ticket` | 이제 접수 건은 `Ticket check`의 `ticket` 칸에 |
정답: [reference/lab03-example3-ticket-down.json](reference/lab03-example3-ticket-down.json) (예제 2 정답에 `fullResponse` + 노드 4개: Ticket check · Tickets down? · Tickets down · Send tickets down)
| 10/24 결과 (`cases/lab03-ticket.json`) | 고치기 전 (예제 2 정답) | 고친 뒤 |
| --- | --- | --- |
| 평소 접수 현황 | ✔ | ✔ 2.4초 |
| 접수 API 500 · 접수 질문 | ✘ `reason: data_api_down` (0.6초) | ✔ `tickets_down`, 0.1초 |
| 접수 API 500 · VPN 질문 | ✘ **`담당자 연결`** (0.6초) | ✔ `자동 답변`, 1.1초, "인증서" |
| 없는 번호 (404) | ✔ | ✔ 1.3초 |
| 합 | 2/4 | 4/4, `lab03` 4/4, `lab03-llm-down` 4/4 |
- 고친 뒤 VPN 질문의 `data_ms` 38ms (예제 1 정답 605ms). 404 재시도가 사라졌기 때문입니다.
- 풀이 때 짚을 것: "오류 출력은 어느 질문인지 모른다" → 실패를 보통 결과로 받아 워크플로우가 판단.
## 과제 1 · 요청문 예
```text
my-work/health.json 을 새로 만들어 줘 (웹훅 my-health, AGENTS.md 3절).
Start(Code t0) → Check data API(GET http://data-api:8800/health, timeout 2000, onError continueRegularOutput, alwaysOutputData)
→ Check FAQ(GET /faq?q=vpn, 같은 설정) → Check LLM(Agent "Reply with the single word OK.", onError continueRegularOutput, alwaysOutputData;
모델 options timeout 8000, maxRetries 0) → Summarize(Code): data_api = status ok, faq = results 가 배열, llm = output 에 ok.
failed = 거짓인 것 이름. status: 모두 참 ok, data_api 나 llm 중 하나라도 참이면 degraded, 아니면 down. seconds.
기대 결과 cases/lab03-challenge.json.
```
**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`)
| 빈칸 | 답 | 왜 |
| --- | --- | --- |
| ① | `continueRegularOutput` | 실패해도 **보통** 출력으로 다음 점검에 (두 곳 같음) |
| ② | `status` 가 `ok` | `/health`가 평소 돌려주는 `{"status":"ok"}` |
| ③ | `output` 에 `ok` | "OK."처럼 대문자 · 마침표가 붙어도 참이 되게 소문자로 비교 |
| ④ | `data_api` 나 `llm` 중 하나라도 참이면 `degraded` | 일부는 된다 = 둘 중 하나는 살아 있음 (학생 기준을 적어도 됨) |
정답: [answers/72-lab03-challenge-health.json](answers/72-lab03-challenge-health.json)
| 수업 준비 때 결과 (`cases/lab03-challenge.json` 4/4) | |
| --- | --- |
| 평소 | `ok`, 2.2초 |
| FAQ 오류 | `degraded`, `checks.faq: false`, 0.8초 |
| FAQ 느림 (8초) | `degraded`, 2.8초 (timeout 2초에서 끊고 계속) |
| LLM 멈춤 | `degraded`, `checks.llm: false`, 0.6초 |
10/24 다시 돌린 결과: `lab03-challenge` 4/4 · 평소 `ok` 2.2초 · FAQ 오류 `degraded` 1.0초 · FAQ 느림 3.0초 · LLM 멈춤 0.4초.
## 과제 2 · 요청문 예
```text
my-work/helpdesk.json 에 FAQ 차단기를 더해 줘 (AGENTS.md 3절 캐시 키 표).
Cached? false → Breaker check(GET http://data-api:8800/cache/breaker-faq, neverError) → Breaker open?(value 있음)
true → Breaker open(Set: route 담당자 연결, answer "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", reason breaker_open, llm_calls 0) → Send breaker open
false → Search FAQ.
Search FAQ 의 오류 출력 → Open breaker(POST /cache/breaker-faq {value:{open:true, since: Date.now()}, ttl 30}, neverError) → Data down.
닫는 노드는 만들지 않는다 (ttl 30초가 지나면 데이터 API가 지움). 기대 결과 cases/lab03-challenge2.json, lab03.json 도 통과.
```
**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`)
| 빈칸 | 답 | 왜 |
| --- | --- | --- |
| ① | `breaker-faq` | 캐시 키 표 마지막 줄 (두 곳 같음) |
| ② | `breaker_open` | AGENTS.md 3절 `reason` 중 차단기가 열린 경우 |
| ③ | `30` | 초. 조건 표 · 캐시 키 표의 ttl |
| ④ | `Data down` | 이번 사용자에게는 예제 1의 정해진 답 |
정답: [answers/72-lab03-challenge2-breaker.json](answers/72-lab03-challenge2-breaker.json) (예제 3 정답에 노드 5개: Breaker check · Breaker open? · Breaker open · Send breaker open · Open breaker)
| 10/24 결과 (`cases/lab03-challenge2.json`) | 고치기 전 (예제 3 정답) | 고친 뒤 |
| --- | --- | --- |
| 평소 | ✔ 1.3초 | ✔ 2.7초 (push 직후) |
| FAQ 느림 · 처음 | ✔ 4.6초 `data_api_down` | ✔ 4.8초 `data_api_down` |
| FAQ 느림 · 다음 질문 | ✘ **4.6초** `data_api_down` | ✔ **0.1초** `breaker_open` |
| 되살아남 (캐시 비움) | ✔ 1.2초 | ✔ 1.1초 |
| 합 | 3/4 | 4/4, `lab03` 4/4 |
- 손으로 30초 닫힘을 보여 줄 때: `fault faq slow` → `call` 둘 → `curl -s localhost:8800/cache/breaker-faq` → 30초 뒤 `call` (다시 4초대, 다시 열림) → `fault all ok`.
- 이야기 거리: 차단기 상태를 워크플로우 밖(캐시)에 둔 까닭 — 웹훅 실행은 요청마다 새로 시작.
+197
View File
@@ -0,0 +1,197 @@
# 7교시 바이브 코딩 · `tbench` 만들기
> **정답본** · 수업이 끝난 뒤(8교시 뒤) 학생 저장소에 올립니다. 그 전에는 올리지 않습니다. 요청서는 [VIBE-REQ-TBENCH.md](VIBE-REQ-TBENCH.md), 문제는 [VIBE-Q-TBENCH.md](VIBE-Q-TBENCH.md).
> 68일차부터 바이브 코딩 프로그램은 **수업 준비 때 미리 만들어 보지 않습니다.** 아래 TODO · SPEC · IMP-PLAN · 요청문과 확인 기준은 예시이고, 걸린 시간은 8교시 강사 바이브 코딩에서 재어 기록합니다.
> **10/26 추가:** 강사 요청으로 R1~R14를 모두 넣은 본을 [tbench/](tbench/)에 직접 만들어 봤습니다 (7절). 요청문으로 CLI 에이전트에 시킨 것이 아니라 Claude Code가 SPEC · IMP-PLAN을 보고 쓴 것이라, 8교시 걸린 시간 표와는 따로 봅니다.
## 1. `Doc/TODO.md` 예
```markdown
# tbench TODO
## 만들고 싶은 것
어느 폴더에서나 `tbench`로 웹훅을 여러 번 불러, 보통 · 느린 쪽 시간과 실패를 보여 주고,
바꾸기 전 · 후를 같은 조건으로 비교하는 터미널 도구. 나중에는 기준을 주면 통과 · 실패만 알려 줌.
## 요청의 말 → 할 일
| # | 요청의 말 (짧게) | 할 일 (내 말) | 꼭 · 나중 |
| --- | --- | --- | --- |
| ① | 여러 번 · 몇 번 실패 · 보통 · 느린 쪽 | N번 부르고 한 번마다 상태 · 시간, 끝에 p50 · p95 · max · 실패 | 꼭 |
| ② | 기다리기 싫음 · 모양 | 오는 대로 표에 한 줄씩, 스파크라인 | 꼭 |
| ③ | 숫자로 비교 · 다른 질문 비교 망신 | 결과 저장, 두 결과 비교, 조건이 다르면 경고 | 꼭 |
| ④ | 한꺼번에 | 동시에 k개 | 나중 |
| ⑤ | 만들어 둔 요청 고르기 | requests 폴더에서 고르기 (turl은 형식 확인 뒤) | 나중 |
| ⑥ | 첫 번만 느림 | 첫 호출 표시, 빼고도 p50, 미리 불러 두기 | 꼭 |
| ⑦ | 왜 실패했는지 | 연결 · 시간 초과 · 서버 오류를 따로 셈 | 꼭 |
| ⑧ | 단계별 시간 평균 | 응답의 timings를 모아 단계 평균 | 나중 |
| ⑨ | 화면 없이 통과 · 실패 | 기준을 넘으면 실패, 종료 코드 | 나중 |
| ⑩ | 줄줄이 기록 | 한 번마다 한 줄 파일 | 나중 |
| ⑪ | 설정 저장 · 이름으로 | 설정 파일, 이름으로 실행 | 나중 |
| ⑫ | 잰 것 목록 | 저장한 결과 목록 | 나중 |
| 끝 | 키가 찍히면 안 됨 | 저장 · 기록에서 키 가리기 | 꼭 |
| 끝 | 진짜 헬프데스크 두드리지 않기 | 시험은 가짜 서버로 | 꼭 |
| 첫 문단 | 어느 폴더에서나 | uv tool install | 꼭 |
## 요청에 없지만 걱정되는 것
- 시간을 어디서부터 어디까지 재나, 끝없이 기다리면?
- p95를 어떻게 계산하나 (사람마다 다르면 비교가 안 됨)
- 저장 폴더 · 설정 파일을 어디에 (어느 폴더에서 실행해도 같은 곳)
## 오늘 50분 안에 못 할 것 같은 것
④ ⑤ ⑧ ⑨ ⑩ ⑪ ⑫ — IMP-PLAN의 다음 단계로
```
## 2. `Doc/SPEC.md` 예 (요구 표는 VIBE-Q와 같음, 확인 기준을 구체적으로)
| 요구 | 확인 기준 |
| --- | --- |
| R1 | 가짜 서버(지연 0.1초, 3번째마다 500)로 n=6 → 실패 2, 시간 6개. `stats.py` 테스트: 1~10초 → p50 5 · p95 10 (nearest-rank, 자리 = 올림(p × n)) |
| R2 | worker로 부름, 결과마다 표 한 줄 · 스파크라인 · 요약 갱신 (가짜 서버 지연 0.5초 × 6에서 3초 동안 한 줄씩 늘어남) |
| R3 | 저장 JSON: `{url, method, body_sha256, n, concurrency, warmup, started, seconds:[...], statuses:[...], errors:[...]}`. compare에서 url · body_sha256 · n이 다르면 `WARNING: different ...` 줄 |
| R4 | 가짜 서버 지연 0.3초 × 6번: `--concurrency 1` 약 1.8초, `3` 약 0.6초 안팎 |
| R5 | `requests/*.json` 목록을 TUI `OptionList`로, 고르면 그 본문으로 실행 |
| R6 | 가짜 서버 첫 응답만 2초 → 1번 줄 note `first call`, "p50 excl. first" 따로. `--warmup 1` → 시간 목록 n개(warmup 제외) |
| R7 | 없는 포트 → `connect` 1, 지연 3초 + `--timeout 1` → `timeout` 1, 500 → `http_5xx` 1, 404 → `http_4xx` 1 |
| R8 | `--header "Authorization: Bearer sk-test"` → 저장 · 기록 파일 `grep -c sk-test` 0, 대신 `"Authorization": "***"` |
| R9 | 응답 JSON의 `timings`(숫자 칸만)를 칸마다 평균. 내 헬프데스크 → `classify_ms … total_ms` 평균 한 줄. `timings`가 없으면 표 없음 |
| R10 | `--max-p95 2.5 --max-failures 0`: 넘으면 마지막 줄 `FAIL p95 3.1 > 2.5`, 종료 코드 1. 아니면 `PASS`, 0 |
| R11 | `--log runs.jsonl`: 줄마다 `{"t", "i", "status", "seconds", "error_kind"}`, 줄 수 = N(+ warmup은 `"warmup": true`) |
| R12 | `~/.tbench/profiles.toml`, `tbench profile save/list`, `tbench run <이름>`: 다른 폴더에서도 같은 결과 파일 조건 |
| R13 | 기본 저장 폴더 `~/.tbench/results/`, `tbench history` → 최신이 위, 날짜 · URL · N · p50 · p95 · 실패 |
| R14 | `uv tool install --editable .` 뒤 `cd /tmp && tbench --help` 성공, `uv tool list`에 `tbench` |
숨은 요구 · 범위 밖은 VIBE-Q 표와 같습니다.
## 3. `Doc/IMP-PLAN.md` 예
```markdown
# tbench IMP-PLAN
## 엔진 1단계 · 부르고 재기 (R1 · R6 · R7 · R8) ← 7교시
- [ ] src/tbench/runner.py: call_once(url, method, body, headers, timeout) → {i, status, seconds, error_kind}
- [ ] src/tbench/stats.py: percentile(nearest-rank), summarize(results) → p50 · p95 · max · failures · by_kind · p50_excl_first
- [ ] tests/conftest.py: 가짜 서버 (지연 · N번째마다 500 · 첫 응답만 느림 · 404)
- [ ] cli.py: tbench <url> --body-file --n --method --timeout --header --warmup --plain
- 확인: uv run pytest -q · uv run tbench <url> -n 5 --plain
## TUI 1단계 · 오는 대로 보이기 (R2) ← 7교시
- [ ] app.py: Sparkline · 요약 Static · DataTable, worker 가 runner 를 돌리며 결과마다 call_from_thread 로 갱신
- 확인: uv run tbench <url> -n 20 (가짜 서버 지연 0.5초)
## 설치 (R14) ← 7교시 끝
- [ ] pyproject [project.scripts] tbench = "tbench.cli:main", uv tool install --editable .
## 엔진 2단계 · 남기고 비교 (R3 · R13 · R11)
- [ ] save(result, path=None) 기본 ~/.tbench/results/<시각>.json, compare(a, b), history()
- [ ] --log runs.jsonl
## 엔진 3단계 · 동시 · 기준 · 응답 분석 · 설정 (R4 · R10 · R9 · R12)
## TUI 2단계 · 고르기 · 비교 · 기록 화면 (R5 · R3 · R13)
```
## 4. 단계별 요청문 예
### 엔진 1단계 · 부르고 재기
```text
Doc/SPEC.md 와 Doc/IMP-PLAN.md 를 읽고 "엔진 1단계"만 만들어 줘. uv 프로젝트(src/tbench), 명령 이름 tbench, 코드와 화면 글자는 영어.
tbench <url> [--body-file f.json] [-n 10] [--method POST] [--timeout 30] [--header "K: V" ...] [--warmup 0] --plain
- 한 번마다 "#i status seconds note", 끝에 p50 · p95 · max · failures(종류별: connect · timeout · http_5xx · http_4xx), p50 excl. first.
시간은 보내기 직전 ~ 본문을 다 받을 때까지. 실패도 한 번으로 셈. 1번 줄 note 는 "first call". warmup 은 재지 않고 버림.
- 백분위: 정렬 후 nearest-rank (ceil(p*n) 번째). stats.py 로 분리하고 pytest 로 1..10 → p50 5, p95 10.
- 엔진은 결과를 하나씩 돌려주는 제너레이터로 (나중에 TUI가 받음). 화면 코드는 아직 쓰지 않는다.
- 테스트는 pytest 안에서 http.server 로 가짜 서버(지연, N번째마다 500, 첫 응답만 느림, 404)를 띄워서. 진짜 n8n 을 부르지 않는다.
- Authorization 등 키 머리글은 출력 · 저장에서 "***".
끝나면 IMP-PLAN 의 엔진 1단계 체크를 표시하고 확인 명령을 알려 줘.
```
### TUI 1단계 · 오는 대로 보이기
```text
IMP-PLAN 의 "TUI 1단계"만 해 줘. tbench <url> ... (--plain 없이) 는 Textual 앱:
위 Sparkline(시간 목록), 요약 줄(완료/전체, 실패, p50, p95, max, p50 excl. first), 아래 DataTable(#, status, seconds, note).
엔진 제너레이터를 worker(thread)에서 돌리고 결과마다 call_from_thread 로 표 · 스파크라인 · 요약을 갱신. 엔진 코드는 바꾸지 않는다.
키: q 끝. 확인: 가짜 서버 지연 0.5초 × 6 에서 한 줄씩 늘어나는지.
```
### 설치
```text
pyproject.toml 에 [project.scripts] tbench = "tbench.cli:main" 이 있는지 보고, 없으면 넣어 줘.
그리고 내가 실행할 명령만 알려 줘: uv tool install --editable . , uv tool list, cd /tmp && tbench --help
```
### 엔진 2단계 · 남기고 비교
```text
IMP-PLAN 의 "엔진 2단계"만: --save [path] (없으면 ~/.tbench/results/<YYYYMMDD-HHMMSS>.json) 에
{url, method, body_sha256, n, concurrency, warmup, started, seconds, statuses, errors} 저장 (키 머리글은 "***").
tbench compare a.json b.json: p50 · p95 · max · failures 와 변화(%) 표, url · body_sha256 · n 이 다르면 WARNING 줄.
tbench history: ~/.tbench/results 의 파일을 최신부터 날짜 · url · n · p50 · p95 · failures 표. --log runs.jsonl: 한 번마다 한 줄.
테스트: 다른 본문 두 결과 → WARNING, 저장 파일에 sk- 없음.
```
### 엔진 3단계 · 동시 · 기준 · 응답 분석 · 설정
```text
IMP-PLAN 의 "엔진 3단계"만: --concurrency k (스레드 풀, 결과는 끝난 순서로 내보내되 번호 유지),
--max-p95 초 · --max-failures 수 → 마지막 줄 PASS/FAIL, 종료 코드 0/1,
응답 JSON 에 timings 가 있으면 숫자 칸마다 평균 (요약 아래 한 줄),
~/.tbench/profiles.toml: tbench profile save <name> <url> [옵션], tbench profile list, tbench run <name> [-n ...].
가짜 서버 테스트: 지연 0.3초 × 6 에서 k=3 이 k=1 보다 빠름, 기준을 넘으면 종료 코드 1.
```
## 5. 7교시에 볼 것
| 볼 것 | 막혔을 때 한마디 |
| --- | --- |
| TODO · SPEC 없이 에이전트부터 | "요청서로 Doc/TODO.md부터. 확인 방법이 없으면 됐는지 모릅니다" |
| IMP-PLAN이 한 단계짜리 | "엔진과 화면을 나누고, 엔진도 1 · 2 · 3으로. 단계마다 commit" |
| 첫 요청에 14개를 다 넣음 | "IMP-PLAN의 엔진 1단계만" |
| 다 끝난 뒤에야 화면이 나옴 | "worker로 부르고 결과마다 표에 더하게" |
| p95가 max와 늘 같음 | "n이 작으면 그럴 수 있어요. 계산 방법을 테스트로 고정" |
| 테스트가 진짜 n8n을 부름 | "pytest 안에서 가짜 서버" |
| 비교가 다른 요청끼리 | "저장할 때 url · 본문 해시를 같이" |
| `tbench`를 못 찾음 | `uv tool update-shell` 뒤 새 터미널, `uv tool list` |
| 설치한 `tbench`가 예전 코드 | `--editable`로 다시, 또는 `uv tool install --force .` |
### 자주 빠뜨리는 것 (수업 준비 때 예상)
| 빠뜨린 것 | 생기는 일 |
| --- | --- |
| 백분위 계산 방법 | 사람마다 · 실행마다 p95가 달라 비교가 흔들림 |
| 실패도 한 번으로 셈 · 실패 종류 | 연결이 안 되면 프로그램이 멈추거나, 실패가 시간 목록에서 빠져 p95가 좋아 보임 |
| 저장 파일에 조건(url · 본문 해시 · N) | 다른 질문으로 잰 것끼리 비교 (요청서의 "망신") |
| 엔진과 화면 나누기 | `--plain` · `compare` · 종료 코드를 화면 없이 못 씀, 테스트가 어려움 |
| 가짜 서버 | 테스트할 때마다 진짜 헬프데스크 · OpenAI 호출 |
| `[project.scripts]` | `uv tool install`은 되는데 `tbench` 명령이 없음 |
| 저장 · 설정 위치를 집 폴더에 | 다른 폴더에서 실행하면 history · profile이 비어 보임 |
## 6. 강사 바이브 코딩 (8교시, 15분 · 시간 기록)
| 할 것 | 걸린 시간 | 메모 |
| --- | --- | --- |
| 엔진 1단계 + TUI 1단계 + 설치를 위 요청문으로 | (8교시에 잼) | |
| R10 `--max-p95` 종료 코드 (74일차 배포 전 검사의 씨앗) | (8교시에 잼) | |
## 7. 수업 준비 때 직접 만들어 본 본 (10/26, `tbench/`)
| 확인 | 결과 |
| --- | --- |
| `uv run pytest -q` (가짜 서버) | **12 passed** (18.8초): 백분위 · 실패 2/6 · 첫 호출 · warmup · 실패 종류 4가지 · 키 가리기 · compare WARNING · history · log · 동시 · 종료 코드 · timings · profile · TUI 한 줄씩 · `--pick` |
| `uv tool install --editable .` → `cd /tmp && tbench --help` | 됨, `uv tool list`에 `tbench v0.1.0` |
| 진짜 헬프데스크 `my-helpdesk` + `requests/faq-vpn-nocache.json` `-n 5 --plain --save --max-p95 4` | 3.947 · 1.579 · 1.532 · 1.824 · 1.429초, 1번 줄 `first call`. **p50 1.579 · p95 3.947 · max 3.947 · p50 excl. first 1.532**, `PASS` |
| 같은 때 `flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5` | p50 1.38 · p95 1.83 · max 1.83초 (바로 앞 tbench가 데워 둠) |
(강사 Mac의 n8n에 올라가 있던 `my-helpdesk`는 규칙 분류 · 코드 검사가 들어간 판이라 `llm_calls` 1, `timings`에 `classify_ms`가 없음.)
만들며 알게 된 것 (학생이 막힐 만한 곳):
| 알게 된 것 | 정한 방법 |
| --- | --- |
| n이 작으면 p95 = max (위 실측: 5번이면 p95는 5번째 = 첫 호출 3.9초) | nearest-rank 그대로 두고, 첫 호출 제외 p50과 `--warmup`으로 따로 봄 (⑥) |
| `profile save`에 키를 넣으면 "남기는 파일에 키가 찍히면 안 돼요"와 부딪힘 | 키 머리글은 `$MY_KEY`처럼 환경 변수로만 저장, 글자 키는 `***`로 바꾸고 안내 (숨은 요구) |
| 응답마다 `timings` 칸이 다름 (규칙 길은 `classify_ms` 없음) | 칸마다 그 칸이 있는 응답끼리 평균 |
| 동시(`--concurrency`)면 결과가 끝난 순서로 옴 | 번호(`i`)는 보낸 순서로 유지, 저장은 번호순 |
| 본문 해시를 JSON을 다시 쓴 글자로 내면 같은 파일도 해시가 달라질 수 있음 | 파일 글자 그대로 보내고 그 글자로 해시 |
| TUI 테스트 | Textual `run_test()` + 가짜 서버 지연 0.5초: 1.3초 뒤 1~5줄, 끝나면 6줄 |
+47
View File
@@ -0,0 +1,47 @@
$ 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
@@ -0,0 +1,95 @@
{
"cases": "lab01.json",
"webhook": "base-helpdesk",
"passed": 1,
"total": 4,
"results": [
{
"case": "VPN FAQ · 단계별 시간",
"webhook": "base-helpdesk",
"run": 1,
"passed": false,
"problems": [
"timings has none of ['answer_ms']"
],
"seconds": 3.5,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.",
"sources": [
"FAQ-003"
]
}
},
{
"case": "VPN FAQ · 전체 시간 · LLM 호출 수",
"webhook": "base-helpdesk",
"run": 1,
"passed": false,
"problems": [
"timings has none of ['total_ms']",
"steps has none of ['answer']",
"llm_calls has none of ['1', '2', '3']"
],
"seconds": 2.9,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주십시오.",
"sources": [
"FAQ-003"
]
}
},
{
"case": "접수 현황",
"webhook": "base-helpdesk",
"run": 1,
"passed": false,
"problems": [
"timings has none of ['answer_ms']"
],
"seconds": 2.8,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "자동 답변",
"category": "ticket",
"answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.",
"sources": []
}
},
{
"case": "빈 질문",
"webhook": "base-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
}
]
}
+58
View File
@@ -0,0 +1,58 @@
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
+137
View File
@@ -0,0 +1,137 @@
{
"cases": "lab01.json",
"webhook": "my-helpdesk",
"passed": 3,
"total": 4,
"results": [
{
"case": "VPN FAQ · 단계별 시간",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 4.3,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "VPN FAQ · 전체 시간 · LLM 호출 수",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.8,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "접수 현황",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"answer has none of ['처리 중']"
],
"seconds": 3.3,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "빈 질문",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
}
]
}
+106
View File
@@ -0,0 +1,106 @@
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
+33
View File
@@ -0,0 +1,33 @@
$ 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
}
}
+92
View File
@@ -0,0 +1,92 @@
$ 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
}
}
@@ -0,0 +1,166 @@
{
"cases": "lab01-challenge.json",
"webhook": "my-helpdesk",
"passed": 0,
"total": 4,
"results": [
{
"case": "FAQ · 접수 시간 따로",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"timings has none of ['faq_ms']"
],
"seconds": 2.7,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "접수 조회 시간",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"timings has none of ['ticket_ms']"
],
"seconds": 2.6,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "가장 오래 걸린 단계",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"slowest_step has none of ['classify', 'answer', 'review']"
],
"seconds": 3.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "접수 현황도 같은 모양",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"answer has none of ['처리 중']",
"slowest_step has none of ['classify', 'answer', 'review']"
],
"seconds": 2.8,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
}
]
}
@@ -0,0 +1,128 @@
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
+165
View File
@@ -0,0 +1,165 @@
{
"cases": "lab01-challenge.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 4,
"results": [
{
"case": "FAQ · 접수 시간 따로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 4.5,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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"
}
},
{
"case": "접수 조회 시간",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.7,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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"
}
},
{
"case": "가장 오래 걸린 단계",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.0,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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"
}
},
{
"case": "접수 현황도 같은 모양",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.0,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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"
}
}
]
}
+136
View File
@@ -0,0 +1,136 @@
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
@@ -0,0 +1,141 @@
{
"cases": "lab01.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 4,
"results": [
{
"case": "VPN FAQ · 단계별 시간",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.9,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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"
}
},
{
"case": "VPN FAQ · 전체 시간 · LLM 호출 수",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.8,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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"
}
},
{
"case": "접수 현황",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.0,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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"
}
},
{
"case": "빈 질문",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
}
]
}
@@ -0,0 +1,112 @@
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
@@ -0,0 +1,180 @@
{
"cases": "lab01-challenge2.json",
"webhook": "my-helpdesk",
"passed": 0,
"total": 4,
"results": [
{
"case": "예산 안 (넉넉한 예산)",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"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']"
],
"seconds": 2.9,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true,
"budget_ms": 60000
},
"response": {
"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"
}
},
{
"case": "예산 넘음 (0.5초 예산)",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"over_budget = null (expected true)",
"budget_ms = null (expected 500)"
],
"seconds": 2.7,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true,
"budget_ms": 500
},
"response": {
"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"
}
},
{
"case": "예산을 안 주면 3초",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"budget_ms = null (expected 3000)",
"llm_ms has none of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']"
],
"seconds": 3.6,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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"
}
},
{
"case": "접수 현황도 같은 모양",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"budget_ms = null (expected 3000)",
"llm_ms has none of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']"
],
"seconds": 2.8,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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"
}
}
]
}
@@ -0,0 +1,138 @@
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
@@ -0,0 +1,179 @@
{
"cases": "lab01-challenge2.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 4,
"results": [
{
"case": "예산 안 (넉넉한 예산)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 4.2,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true,
"budget_ms": 60000
},
"response": {
"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
}
},
{
"case": "예산 넘음 (0.5초 예산)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.7,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true,
"budget_ms": 500
},
"response": {
"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
}
},
{
"case": "예산을 안 주면 3초",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.2,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
},
{
"case": "접수 현황도 같은 모양",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.9,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
]
}
+150
View File
@@ -0,0 +1,150 @@
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
@@ -0,0 +1,177 @@
{
"cases": "lab01-challenge.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 4,
"results": [
{
"case": "FAQ · 접수 시간 따로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.7,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
},
{
"case": "접수 조회 시간",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.6,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
},
{
"case": "가장 오래 걸린 단계",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.9,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
},
{
"case": "접수 현황도 같은 모양",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.9,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
]
}
@@ -0,0 +1,148 @@
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
+150
View File
@@ -0,0 +1,150 @@
{
"cases": "lab01.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 4,
"results": [
{
"case": "VPN FAQ · 단계별 시간",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.0,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
},
{
"case": "VPN FAQ · 전체 시간 · LLM 호출 수",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.7,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
},
{
"case": "접수 현황",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.4,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
},
{
"case": "빈 질문",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
}
]
}
+121
View File
@@ -0,0 +1,121 @@
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
+332
View File
@@ -0,0 +1,332 @@
{
"cases": "lab02.json",
"webhook": "my-helpdesk",
"passed": 7,
"total": 9,
"results": [
{
"case": "예제 1 · 프린터 FAQ는 규칙으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.8,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 접수 번호는 규칙으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.5,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 없는 접수 번호",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.1,
"status": 200,
"request": {
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 규칙으로 모르면 LLM이 분류",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.4,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 처음 묻는 VPN",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.3,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 같은 질문 (띄어쓰기·대소문자 다름)",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"took 1.9s (expected at most 1s)",
"cached = false (expected true)",
"llm_calls = 2 (expected 0)"
],
"seconds": 1.9,
"status": 200,
"request": {
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
},
"response": {
"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
}
}
},
{
"case": "예제 2 · no_cache면 캐시 안 씀",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.3,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 접수 현황은 캐시 안 함 (1)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.2,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 접수 현황은 캐시 안 함 (2)",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"answer has none of ['처리 중']"
],
"seconds": 2.0,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
}
]
}
+268
View File
@@ -0,0 +1,268 @@
✔ 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
@@ -0,0 +1,10 @@
$ 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
+319
View File
@@ -0,0 +1,319 @@
{
"cases": "lab02.json",
"webhook": "my-helpdesk",
"passed": 9,
"total": 9,
"results": [
{
"case": "예제 1 · 프린터 FAQ는 규칙으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.9,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 접수 번호는 규칙으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.1,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 없는 접수 번호",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.0,
"status": 200,
"request": {
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 규칙으로 모르면 LLM이 분류",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.6,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 처음 묻는 VPN",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.2,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 같은 질문 (띄어쓰기·대소문자 다름)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 34
}
}
},
{
"case": "예제 2 · no_cache면 캐시 안 씀",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.3,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 접수 현황은 캐시 안 함 (1)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.2,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 접수 현황은 캐시 안 함 (2)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.2,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
}
]
}
+261
View File
@@ -0,0 +1,261 @@
✔ 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
+134
View File
@@ -0,0 +1,134 @@
{
"cases": "lab01.json",
"webhook": "my-helpdesk",
"passed": 3,
"total": 4,
"results": [
{
"case": "VPN FAQ · 단계별 시간",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "VPN FAQ · 전체 시간 · LLM 호출 수",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.2,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "접수 현황",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"answer has none of ['처리 중']"
],
"seconds": 2.1,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "빈 질문",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
}
]
}
+103
View File
@@ -0,0 +1,103 @@
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
+3
View File
@@ -0,0 +1,3 @@
$ curl -s localhost:8800/cache-stats; echo
{"entries": 2, "hits": 4, "misses": 8}
@@ -0,0 +1,26 @@
$ 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}
@@ -0,0 +1,177 @@
{
"cases": "lab02-challenge.json",
"webhook": "my-helpdesk",
"passed": 1,
"total": 5,
"results": [
{
"case": "VPN · LLM 한 번 · 2.5초 안",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"llm_calls = 2 (expected 1)"
],
"seconds": 2.2,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "비밀번호 · LLM 한 번",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"llm_calls = 2 (expected 1)"
],
"seconds": 2.0,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "접수 현황 · LLM 한 번",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"llm_calls = 2 (expected 1)"
],
"seconds": 2.3,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "규칙으로 모르면 분류 + 답 두 번",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"llm_calls = 3 (expected 2)"
],
"seconds": 2.8,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "빈 질문은 LLM 없이",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
}
]
}
@@ -0,0 +1,135 @@
✔ 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
+10
View File
@@ -0,0 +1,10 @@
$ 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
+491
View File
@@ -0,0 +1,491 @@
{
"cases": "lab02-challenge.json",
"webhook": "my-helpdesk",
"passed": 15,
"total": 15,
"results": [
{
"case": "VPN · LLM 한 번 · 2.5초 안",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "VPN · LLM 한 번 · 2.5초 안",
"webhook": "my-helpdesk",
"run": 2,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "VPN · LLM 한 번 · 2.5초 안",
"webhook": "my-helpdesk",
"run": 3,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "비밀번호 · LLM 한 번",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.0,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "비밀번호 · LLM 한 번",
"webhook": "my-helpdesk",
"run": 2,
"passed": true,
"problems": [],
"seconds": 0.9,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "비밀번호 · LLM 한 번",
"webhook": "my-helpdesk",
"run": 3,
"passed": true,
"problems": [],
"seconds": 0.9,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "접수 현황 · LLM 한 번",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.9,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "접수 현황 · LLM 한 번",
"webhook": "my-helpdesk",
"run": 2,
"passed": true,
"problems": [],
"seconds": 1.0,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "접수 현황 · LLM 한 번",
"webhook": "my-helpdesk",
"run": 3,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "규칙으로 모르면 분류 + 답 두 번",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.6,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "규칙으로 모르면 분류 + 답 두 번",
"webhook": "my-helpdesk",
"run": 2,
"passed": true,
"problems": [],
"seconds": 1.4,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "규칙으로 모르면 분류 + 답 두 번",
"webhook": "my-helpdesk",
"run": 3,
"passed": true,
"problems": [],
"seconds": 1.6,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "빈 질문은 LLM 없이",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
},
{
"case": "빈 질문은 LLM 없이",
"webhook": "my-helpdesk",
"run": 2,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
},
{
"case": "빈 질문은 LLM 없이",
"webhook": "my-helpdesk",
"run": 3,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
}
]
}
+397
View File
@@ -0,0 +1,397 @@
✔ 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
@@ -0,0 +1,319 @@
{
"cases": "lab02.json",
"webhook": "my-helpdesk",
"passed": 9,
"total": 9,
"results": [
{
"case": "예제 1 · 프린터 FAQ는 규칙으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 접수 번호는 규칙으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.9,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 없는 접수 번호",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.0,
"status": 200,
"request": {
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 규칙으로 모르면 LLM이 분류",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.6,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 처음 묻는 VPN",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 같은 질문 (띄어쓰기·대소문자 다름)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 21
}
}
},
{
"case": "예제 2 · no_cache면 캐시 안 씀",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 접수 현황은 캐시 안 함 (1)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 접수 현황은 캐시 안 함 (2)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.9,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
}
]
}
@@ -0,0 +1,261 @@
✔ 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
@@ -0,0 +1,132 @@
{
"cases": "lab01.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 4,
"results": [
{
"case": "VPN FAQ · 단계별 시간",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "VPN FAQ · 전체 시간 · LLM 호출 수",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.3,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "접수 현황",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.0,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "빈 질문",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
}
]
}
@@ -0,0 +1,103 @@
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
+10
View File
@@ -0,0 +1,10 @@
$ 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
@@ -0,0 +1,223 @@
{
"cases": "lab02-challenge2.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 6,
"results": [
{
"case": "처음 묻는 VPN (캐시 없음)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"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
}
}
},
{
"case": "다른 말로 묻는 VPN → FAQ 번호 캐시",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"took 1.2s (expected at most 1s)",
"cached = false (expected true)",
"cache_by = null (expected faq)",
"llm_calls = 1 (expected 0)"
],
"seconds": 1.2,
"status": 200,
"request": {
"question": "VPN 연결이 계속 끊어지는데요",
"user": "최유진"
},
"response": {
"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
}
}
},
{
"case": "같은 말은 질문 캐시",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"cache_by = null (expected question)"
],
"seconds": 0.1,
"status": 200,
"request": {
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 14
}
}
},
{
"case": "no_cache면 FAQ 번호 캐시도 안 씀",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "VPN 연결이 계속 끊어지는데요",
"user": "최유진",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "다른 FAQ는 따로 (프린터 처음)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"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
}
}
},
{
"case": "접수 현황은 캐시 안 함",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.9,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
}
]
}
@@ -0,0 +1,176 @@
✔ 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
@@ -0,0 +1,217 @@
{
"cases": "lab02-challenge2.json",
"webhook": "my-helpdesk",
"passed": 6,
"total": 6,
"results": [
{
"case": "처음 묻는 VPN (캐시 없음)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.5,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"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
}
}
},
{
"case": "다른 말로 묻는 VPN → FAQ 번호 캐시",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"question": "VPN 연결이 계속 끊어지는데요",
"user": "최유진"
},
"response": {
"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
}
}
},
{
"case": "같은 말은 질문 캐시",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 200,
"request": {
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"cache_by": "question",
"steps": [
"cache"
],
"timings": {
"total_ms": 19
}
}
},
{
"case": "no_cache면 FAQ 번호 캐시도 안 씀",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "VPN 연결이 계속 끊어지는데요",
"user": "최유진",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "다른 FAQ는 따로 (프린터 처음)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"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
}
}
},
{
"case": "접수 현황은 캐시 안 함",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
}
]
}
+177
View File
@@ -0,0 +1,177 @@
✔ 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
@@ -0,0 +1,173 @@
{
"cases": "lab02-challenge.json",
"webhook": "my-helpdesk",
"passed": 5,
"total": 5,
"results": [
{
"case": "VPN · LLM 한 번 · 2.5초 안",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "비밀번호 · LLM 한 번",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.0,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "접수 현황 · LLM 한 번",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.9,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "규칙으로 모르면 분류 + 답 두 번",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.7,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "빈 질문은 LLM 없이",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
}
]
}
@@ -0,0 +1,139 @@
✔ 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
+328
View File
@@ -0,0 +1,328 @@
{
"cases": "lab02.json",
"webhook": "my-helpdesk",
"passed": 9,
"total": 9,
"results": [
{
"case": "예제 1 · 프린터 FAQ는 규칙으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 접수 번호는 규칙으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.0,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 없는 접수 번호",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 규칙으로 모르면 LLM이 분류",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.7,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 처음 묻는 VPN",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 같은 질문 (띄어쓰기·대소문자 다름)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 200,
"request": {
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"cache_by": "question",
"steps": [
"cache"
],
"timings": {
"total_ms": 15
}
}
},
{
"case": "예제 2 · no_cache면 캐시 안 씀",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 접수 현황은 캐시 안 함 (1)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.0,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 접수 현황은 캐시 안 함 (2)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.9,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
}
]
}
+270
View File
@@ -0,0 +1,270 @@
✔ 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
@@ -0,0 +1,53 @@
$ 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}
+112
View File
@@ -0,0 +1,112 @@
{
"cases": "lab03.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 4,
"results": [
{
"case": "평소",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "FAQ가 느릴 때 (8초)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 4.6,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
},
"response": {
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
},
{
"case": "FAQ가 오류일 때 (500)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.6,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
},
"response": {
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
},
{
"case": "캐시에 있으면 FAQ가 멈춰도 답",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 21
}
}
}
]
}
+92
View File
@@ -0,0 +1,92 @@
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}
+319
View File
@@ -0,0 +1,319 @@
{
"cases": "lab02.json",
"webhook": "my-helpdesk",
"passed": 9,
"total": 9,
"results": [
{
"case": "예제 1 · 프린터 FAQ는 규칙으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.8,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 접수 번호는 규칙으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 없는 접수 번호",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.8,
"status": 200,
"request": {
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 규칙으로 모르면 LLM이 분류",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.2,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 처음 묻는 VPN",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.7,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 같은 질문 (띄어쓰기·대소문자 다름)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 19
}
}
},
{
"case": "예제 2 · no_cache면 캐시 안 씀",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.7,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 접수 현황은 캐시 안 함 (1)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 접수 현황은 캐시 안 함 (2)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
}
]
}
+261
View File
@@ -0,0 +1,261 @@
✔ 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
@@ -0,0 +1,19 @@
$ 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
@@ -0,0 +1,84 @@
{
"cases": "lab03-llm-down.json",
"webhook": "my-helpdesk",
"passed": 1,
"total": 4,
"results": [
{
"case": "VPN · FAQ 원문으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"status 500 (expected 200)",
"route = null (expected 간단 답변)",
"answer has none of ['인증서']"
],
"seconds": 0.9,
"status": 500,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"message": "Error in workflow"
}
},
{
"case": "접수 현황 · 상태 그대로",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"status 500 (expected 200)",
"route = null (expected 간단 답변)",
"answer has none of ['처리 중']"
],
"seconds": 0.4,
"status": 500,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"message": "Error in workflow"
}
},
{
"case": "모르는 질문 · 담당자",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"status 500 (expected 200)",
"route = null (expected 담당자 연결)"
],
"seconds": 0.9,
"status": 500,
"request": {
"question": "오늘 점심 메뉴 추천해 주세요",
"user": "이준호"
},
"response": {
"message": "Error in workflow"
}
},
{
"case": "빈 질문",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
}
]
}
@@ -0,0 +1,47 @@
✔ 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
@@ -0,0 +1,90 @@
{
"cases": "lab03-llm-down.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 4,
"results": [
{
"case": "VPN · FAQ 원문으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.6,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"route": "간단 답변",
"category": "faq",
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다. (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"reason": "llm_down"
}
},
{
"case": "접수 현황 · 상태 그대로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.3,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "간단 답변",
"category": "ticket",
"answer": "T-1002 접수 건은 현재 '처리 중' 상태입니다. 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다.",
"sources": [],
"llm_calls": 0,
"reason": "llm_down"
}
},
{
"case": "모르는 질문 · 담당자",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.8,
"status": 200,
"request": {
"question": "오늘 점심 메뉴 추천해 주세요",
"user": "이준호"
},
"response": {
"route": "담당자 연결",
"category": "other",
"answer": "지금은 자동 답변을 할 수 없어 담당자에게 연결합니다.",
"sources": [],
"llm_calls": 0,
"reason": "llm_down"
}
},
{
"case": "빈 질문",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
}
]
}
+64
View File
@@ -0,0 +1,64 @@
✔ 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
@@ -0,0 +1,319 @@
{
"cases": "lab02.json",
"webhook": "my-helpdesk",
"passed": 9,
"total": 9,
"results": [
{
"case": "예제 1 · 프린터 FAQ는 규칙으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.8,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 접수 번호는 규칙으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 없는 접수 번호",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.7,
"status": 200,
"request": {
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 1 · 규칙으로 모르면 LLM이 분류",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.2,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 처음 묻는 VPN",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.9,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 같은 질문 (띄어쓰기·대소문자 다름)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"category": "faq",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 20
}
}
},
{
"case": "예제 2 · no_cache면 캐시 안 씀",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.7,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 접수 현황은 캐시 안 함 (1)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.0,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "예제 2 · 접수 현황은 캐시 안 함 (2)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
}
]
}
@@ -0,0 +1,261 @@
✔ 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
@@ -0,0 +1,112 @@
{
"cases": "lab03.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 4,
"results": [
{
"case": "평소",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.7,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "FAQ가 느릴 때 (8초)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 4.6,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
},
"response": {
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
},
{
"case": "FAQ가 오류일 때 (500)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.6,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
},
"response": {
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
},
{
"case": "캐시에 있으면 FAQ가 멈춰도 답",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 21
}
}
}
]
}
@@ -0,0 +1,92 @@
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}
@@ -0,0 +1,119 @@
{
"cases": "lab03-ticket.json",
"webhook": "my-helpdesk",
"passed": 2,
"total": 4,
"results": [
{
"case": "평소 접수 현황",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.0,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "접수 API 500 · 접수 질문은 담당자",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"reason = data_api_down (expected tickets_down)"
],
"seconds": 0.6,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
},
{
"case": "접수 API 500 · FAQ 질문은 그대로 답",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"route = 담당자 연결 (expected 자동 답변)",
"category = null (expected faq)",
"answer has none of ['인증서']"
],
"seconds": 0.6,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
},
{
"case": "없는 접수 번호는 그대로 (404는 고장이 아님)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.6,
"status": 200,
"request": {
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
},
"response": {
"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
}
}
}
]
}
@@ -0,0 +1,90 @@
✔ 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}
+133
View File
@@ -0,0 +1,133 @@
{
"cases": "lab03-ticket.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 4,
"results": [
{
"case": "평소 접수 현황",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.4,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"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
}
}
},
{
"case": "접수 API 500 · 접수 질문은 담당자",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "담당자 연결",
"category": "ticket",
"answer": "접수 현황 시스템이 응답하지 않아 담당자에게 연결합니다. 잠시 뒤 다시 물어봐 주세요.",
"reason": "tickets_down",
"llm_calls": 0
}
},
{
"case": "접수 API 500 · FAQ 질문은 그대로 답",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "없는 접수 번호는 그대로 (404는 고장이 아님)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.3,
"status": 200,
"request": {
"question": "T-9999 접수 건은 어떻게 됐나요?",
"user": "박서연"
},
"response": {
"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
}
}
}
]
}
+110
View File
@@ -0,0 +1,110 @@
✔ 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}
@@ -0,0 +1,112 @@
{
"cases": "lab03.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 4,
"results": [
{
"case": "평소",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "FAQ가 느릴 때 (8초)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 4.8,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
},
"response": {
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
},
{
"case": "FAQ가 오류일 때 (500)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.6,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
},
"response": {
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
},
{
"case": "캐시에 있으면 FAQ가 멈춰도 답",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 17
}
}
}
]
}
@@ -0,0 +1,92 @@
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}
@@ -0,0 +1,90 @@
{
"cases": "lab03-llm-down.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 4,
"results": [
{
"case": "VPN · FAQ 원문으로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.7,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"route": "간단 답변",
"category": "faq",
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다. (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"reason": "llm_down"
}
},
{
"case": "접수 현황 · 상태 그대로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.5,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "간단 답변",
"category": "ticket",
"answer": "T-1002 접수 건은 현재 '처리 중' 상태입니다. 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다.",
"sources": [],
"llm_calls": 0,
"reason": "llm_down"
}
},
{
"case": "모르는 질문 · 담당자",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.3,
"status": 200,
"request": {
"question": "오늘 점심 메뉴 추천해 주세요",
"user": "이준호"
},
"response": {
"route": "담당자 연결",
"category": "other",
"answer": "지금은 자동 답변을 할 수 없어 담당자에게 연결합니다.",
"sources": [],
"llm_calls": 0,
"reason": "llm_down"
}
},
{
"case": "빈 질문",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
}
]
}
@@ -0,0 +1,64 @@
✔ 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
@@ -0,0 +1,94 @@
{
"cases": "lab03-challenge.json",
"webhook": "my-health",
"passed": 4,
"total": 4,
"results": [
{
"case": "평소",
"webhook": "my-health",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.2,
"status": 200,
"request": {},
"response": {
"status": "ok",
"checks": {
"data_api": true,
"faq": true,
"llm": true
},
"failed": [],
"seconds": 1.4
}
},
{
"case": "FAQ 오류",
"webhook": "my-health",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.0,
"status": 200,
"request": {},
"response": {
"status": "degraded",
"checks": {
"data_api": true,
"faq": false,
"llm": true
},
"failed": [
"faq"
],
"seconds": 1
}
},
{
"case": "FAQ 느림 (8초) · 점검은 빨리 끝남",
"webhook": "my-health",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.0,
"status": 200,
"request": {},
"response": {
"status": "degraded",
"checks": {
"data_api": true,
"faq": false,
"llm": true
},
"failed": [
"faq"
],
"seconds": 2.9
}
},
{
"case": "LLM 멈춤",
"webhook": "my-health",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.4,
"status": 200,
"request": {},
"response": {
"status": "degraded",
"checks": {
"data_api": true,
"faq": true,
"llm": false
},
"failed": [
"llm"
],
"seconds": 0.3
}
}
]
}
@@ -0,0 +1,72 @@
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}
@@ -0,0 +1,123 @@
{
"cases": "lab03-challenge2.json",
"webhook": "my-helpdesk",
"passed": 3,
"total": 4,
"results": [
{
"case": "평소",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.3,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "FAQ 느림 · 처음 한 번은 기다린 뒤 담당자",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 4.6,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
},
"response": {
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
},
{
"case": "FAQ 느림 · 다음 질문은 기다리지 않음 (차단기 열림)",
"webhook": "my-helpdesk",
"run": 1,
"passed": false,
"problems": [
"took 4.6s (expected at most 1s)",
"reason = data_api_down (expected breaker_open)"
],
"seconds": 4.6,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진",
"no_cache": true
},
"response": {
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
},
{
"case": "FAQ가 돌아오고 차단기를 비우면 평소대로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진",
"no_cache": true
},
"response": {
"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
}
}
}
]
}
@@ -0,0 +1,97 @@
✔ 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}
@@ -0,0 +1,121 @@
{
"cases": "lab03-challenge2.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 4,
"results": [
{
"case": "평소",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.7,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "FAQ 느림 · 처음 한 번은 기다린 뒤 담당자",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 4.8,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
},
"response": {
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
},
{
"case": "FAQ 느림 · 다음 질문은 기다리지 않음 (차단기 열림)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진",
"no_cache": true
},
"response": {
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "breaker_open",
"llm_calls": 0
}
},
{
"case": "FAQ가 돌아오고 차단기를 비우면 평소대로",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진",
"no_cache": true
},
"response": {
"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
}
}
}
]
}
@@ -0,0 +1,98 @@
✔ 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}
@@ -0,0 +1,113 @@
{
"cases": "lab03.json",
"webhook": "my-helpdesk",
"passed": 4,
"total": 4,
"results": [
{
"case": "평소",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호",
"no_cache": true
},
"response": {
"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
}
}
},
{
"case": "FAQ가 느릴 때 (8초)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 4.6,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
},
"response": {
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "data_api_down"
}
},
{
"case": "FAQ가 오류일 때 (500)",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요",
"user": "한지훈",
"no_cache": true
},
"response": {
"route": "담당자 연결",
"answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.",
"reason": "breaker_open",
"llm_calls": 0
}
},
{
"case": "캐시에 있으면 FAQ가 멈춰도 답",
"webhook": "my-helpdesk",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"category": "faq",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 0,
"cached": true,
"steps": [
"cache"
],
"timings": {
"total_ms": 21
}
}
}
]
}
@@ -0,0 +1,93 @@
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}
+13
View File
@@ -0,0 +1,13 @@
$ 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
@@ -0,0 +1,586 @@
{
"name": "72 IT helpdesk + step timings (answer)",
"nodes": [
{
"name": "Question webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"parameters": {
"httpMethod": "POST",
"path": "my-helpdesk",
"responseMode": "responseNode",
"options": {}
},
"webhookId": "de518e93-ed19-541f-a6fc-687a4efaf1b2",
"position": [
0,
300
],
"id": "f7a423a6-a6a2-57a3-954f-e8540a1ce022"
},
{
"name": "Has question?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.body.question }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"position": [
220,
300
],
"id": "6dce76b0-2dd0-5181-99be-b0b894c87c8b"
},
{
"name": "Reject empty",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "{ \"error\": \"question is required\" }",
"options": {
"responseCode": 400
}
},
"position": [
440,
560
],
"id": "75371917-2483-583e-9135-5c663a18b08b"
},
{
"name": "Start clock",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { t: Date.now() } }];"
},
"position": [
440,
300
],
"id": "34377f95-81c5-5aa3-9e2c-6b63a8e5ae54"
},
{
"name": "Classify",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"parameters": {
"promptType": "define",
"text": "={{ $('Question webhook').first().json.body.question }}",
"options": {
"systemMessage": "You sort one company IT help-desk question. Categories: faq (how to fix or use something: password, account lock, VPN, printer, mail box, software install), ticket (asks about the status of an IT request ticket like T-1002), other (anything else). Return only JSON, no code fence: {\"category\": \"faq\"}",
"maxIterations": 1
}
},
"position": [
660,
300
],
"id": "7a93b236-dbff-52c5-9397-61087bc0688d"
},
{
"name": "Classify model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.2,
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4.1-mini"
},
"options": {
"temperature": 0
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
660,
520
],
"id": "357e7611-4597-5ba3-a6ba-9a4d4d63cda7"
},
{
"name": "Parse category",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const raw = String($json.output ?? '').trim().replace(/^```(json)?\\s*|```$/g, '');\nlet category = 'other';\ntry { category = JSON.parse(raw).category ?? 'other'; } catch (error) { category = 'other'; }\nif (!['faq', 'ticket', 'other'].includes(category)) category = 'other';\nreturn [{ json: { category, by: 'llm' } }];"
},
"position": [
880,
300
],
"id": "9327634c-6764-5403-b7bc-3e666e7ae3d6"
},
{
"name": "After classify",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
1100,
300
],
"id": "8f3da4f6-ce34-5505-b543-e5d68d990a96"
},
{
"name": "Search FAQ",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/faq",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "q",
"value": "={{ $('Question webhook').first().json.body.question }}"
}
]
}
},
"position": [
1320,
300
],
"id": "103c6254-01ea-5e80-81bb-7e3b9ea7beb5"
},
{
"name": "After FAQ",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
1540,
300
],
"id": "d03181b0-715d-5fae-98a0-92c52a2b3b3c"
},
{
"name": "Get ticket",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}",
"options": {
"response": {
"response": {
"neverError": true
}
}
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "requester",
"value": "={{ $('Question webhook').first().json.body.user ?? '' }}"
}
]
}
},
"position": [
1760,
300
],
"id": "f20b5c78-0224-5d55-8d77-0a9e98061a6b"
},
{
"name": "After data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
1980,
300
],
"id": "cca89aef-ef17-5b5f-a3c3-a53e0d762b43"
},
{
"name": "Answer",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"parameters": {
"promptType": "define",
"text": "=질문: {{ $('Question webhook').first().json.body.question }}\n분류: {{ $('Parse category').first().json.category }}\nFAQ: {{ JSON.stringify($('Search FAQ').first().json.results ?? []) }}\n접수 건: {{ JSON.stringify($('Get ticket').first().json) }}",
"options": {
"systemMessage": "You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \"찾지 못했습니다\" and that a staff member will help.",
"maxIterations": 1
}
},
"position": [
2200,
300
],
"id": "d7ec1c62-265f-5d16-b67a-c5922305bb8a"
},
{
"name": "Answer model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.2,
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4.1-mini"
},
"options": {
"temperature": 0
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
2200,
520
],
"id": "5c7f8b81-cd4c-59a2-9899-9bdb4813f7ab"
},
{
"name": "After answer",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
2420,
300
],
"id": "43dab328-4901-5fd6-8238-6e0f836cc31d"
},
{
"name": "Review",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"parameters": {
"promptType": "define",
"text": "=초안: {{ $('Answer').first().json.output }}",
"options": {
"systemMessage": "You review a Korean IT help-desk answer before it is sent. Keep the facts exactly. Make it polite (합니다체), remove anything not supported by the data, keep at most 3 sentences. Return only the final answer text.",
"maxIterations": 1
}
},
"position": [
2640,
300
],
"id": "a4583946-f582-5ad8-963b-b866b74e9ba4"
},
{
"name": "Review model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.2,
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4.1-mini"
},
"options": {
"temperature": 0
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
2640,
520
],
"id": "3be9acc9-4b3d-5a41-b6a4-00fa1b04d777"
},
{
"name": "After review",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
2860,
300
],
"id": "d07d9992-7e5e-5aab-b925-9387021bbaff"
},
{
"name": "Build result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const ran = (name) => { try { return $(name).isExecuted; } catch (error) { return false; } };\nconst body = $('Question webhook').first().json.body;\nconst category = $('Parse category').first().json.category;\nconst answer = String($('Review').first().json.output ?? '').trim();\nconst results = $('Search FAQ').first().json.results ?? [];\nconst sources = category === 'faq' ? results.map((r) => r.id) : [];\nconst steps = [];\nif (ran('Rule classify')) steps.push('rule');\nif (ran('Classify')) steps.push('classify');\nsteps.push('faq', 'ticket', 'answer');\nsteps.push('review');\nconst llm_calls = (ran('Classify') ? 1 : 0) + 1 + 1;\nconst result = { route: '자동 답변', category, answer, sources };\nconst t = (name) => (ran(name) ? $(name).first().json.t : null);\nconst t0 = t('Start clock');\nconst marks = [['classify', t('After classify')], ['faq', t('After FAQ')], ['ticket', t('After data')], ['answer', t('After answer')], ['review', t('After review')]];\nconst timings = {};\nlet last = t0;\nfor (const [label, mark] of marks) {\n if (mark === null) continue;\n timings[`${label}_ms`] = mark - last;\n last = mark;\n}\ntimings.total_ms = Date.now() - t0;\nObject.assign(result, { llm_calls, cached: false, steps, timings });\nconst parts = Object.entries(timings).filter(([key]) => key !== 'total_ms');\nresult.slowest_step = parts.sort((a, b) => b[1] - a[1])[0][0].replace(/_ms$/, '');\nreturn [{ json: result }];"
},
"position": [
3080,
300
],
"id": "079e44f0-da3e-57dd-be6a-87025b6a320f"
},
{
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"position": [
3300,
300
],
"id": "8ac7b99c-d9fa-5a1e-a168-33eb7ab5e2a3"
}
],
"connections": {
"Question webhook": {
"main": [
[
{
"node": "Has question?",
"type": "main",
"index": 0
}
]
]
},
"Has question?": {
"main": [
[
{
"node": "Start clock",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject empty",
"type": "main",
"index": 0
}
]
]
},
"Start clock": {
"main": [
[
{
"node": "Classify",
"type": "main",
"index": 0
}
]
]
},
"Classify model": {
"ai_languageModel": [
[
{
"node": "Classify",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Classify": {
"main": [
[
{
"node": "Parse category",
"type": "main",
"index": 0
}
]
]
},
"Parse category": {
"main": [
[
{
"node": "After classify",
"type": "main",
"index": 0
}
]
]
},
"After classify": {
"main": [
[
{
"node": "Search FAQ",
"type": "main",
"index": 0
}
]
]
},
"Search FAQ": {
"main": [
[
{
"node": "After FAQ",
"type": "main",
"index": 0
}
]
]
},
"After FAQ": {
"main": [
[
{
"node": "Get ticket",
"type": "main",
"index": 0
}
]
]
},
"Get ticket": {
"main": [
[
{
"node": "After data",
"type": "main",
"index": 0
}
]
]
},
"After data": {
"main": [
[
{
"node": "Answer",
"type": "main",
"index": 0
}
]
]
},
"Answer model": {
"ai_languageModel": [
[
{
"node": "Answer",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Answer": {
"main": [
[
{
"node": "After answer",
"type": "main",
"index": 0
}
]
]
},
"After answer": {
"main": [
[
{
"node": "Review",
"type": "main",
"index": 0
}
]
]
},
"Review model": {
"ai_languageModel": [
[
{
"node": "Review",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Review": {
"main": [
[
{
"node": "After review",
"type": "main",
"index": 0
}
]
]
},
"After review": {
"main": [
[
{
"node": "Build result",
"type": "main",
"index": 0
}
]
]
},
"Build result": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,586 @@
{
"name": "72 IT helpdesk + time budget (answer)",
"nodes": [
{
"name": "Question webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"parameters": {
"httpMethod": "POST",
"path": "my-helpdesk",
"responseMode": "responseNode",
"options": {}
},
"webhookId": "de518e93-ed19-541f-a6fc-687a4efaf1b2",
"position": [
0,
300
],
"id": "f7a423a6-a6a2-57a3-954f-e8540a1ce022"
},
{
"name": "Has question?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.body.question }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"position": [
220,
300
],
"id": "6dce76b0-2dd0-5181-99be-b0b894c87c8b"
},
{
"name": "Reject empty",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "{ \"error\": \"question is required\" }",
"options": {
"responseCode": 400
}
},
"position": [
440,
560
],
"id": "75371917-2483-583e-9135-5c663a18b08b"
},
{
"name": "Start clock",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { t: Date.now() } }];"
},
"position": [
440,
300
],
"id": "34377f95-81c5-5aa3-9e2c-6b63a8e5ae54"
},
{
"name": "Classify",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"parameters": {
"promptType": "define",
"text": "={{ $('Question webhook').first().json.body.question }}",
"options": {
"systemMessage": "You sort one company IT help-desk question. Categories: faq (how to fix or use something: password, account lock, VPN, printer, mail box, software install), ticket (asks about the status of an IT request ticket like T-1002), other (anything else). Return only JSON, no code fence: {\"category\": \"faq\"}",
"maxIterations": 1
}
},
"position": [
660,
300
],
"id": "7a93b236-dbff-52c5-9397-61087bc0688d"
},
{
"name": "Classify model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.2,
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4.1-mini"
},
"options": {
"temperature": 0
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
660,
520
],
"id": "357e7611-4597-5ba3-a6ba-9a4d4d63cda7"
},
{
"name": "Parse category",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const raw = String($json.output ?? '').trim().replace(/^```(json)?\\s*|```$/g, '');\nlet category = 'other';\ntry { category = JSON.parse(raw).category ?? 'other'; } catch (error) { category = 'other'; }\nif (!['faq', 'ticket', 'other'].includes(category)) category = 'other';\nreturn [{ json: { category, by: 'llm' } }];"
},
"position": [
880,
300
],
"id": "9327634c-6764-5403-b7bc-3e666e7ae3d6"
},
{
"name": "After classify",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
1100,
300
],
"id": "8f3da4f6-ce34-5505-b543-e5d68d990a96"
},
{
"name": "Search FAQ",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/faq",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "q",
"value": "={{ $('Question webhook').first().json.body.question }}"
}
]
}
},
"position": [
1320,
300
],
"id": "103c6254-01ea-5e80-81bb-7e3b9ea7beb5"
},
{
"name": "After FAQ",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
1540,
300
],
"id": "d03181b0-715d-5fae-98a0-92c52a2b3b3c"
},
{
"name": "Get ticket",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}",
"options": {
"response": {
"response": {
"neverError": true
}
}
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "requester",
"value": "={{ $('Question webhook').first().json.body.user ?? '' }}"
}
]
}
},
"position": [
1760,
300
],
"id": "f20b5c78-0224-5d55-8d77-0a9e98061a6b"
},
{
"name": "After data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
1980,
300
],
"id": "cca89aef-ef17-5b5f-a3c3-a53e0d762b43"
},
{
"name": "Answer",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"parameters": {
"promptType": "define",
"text": "=질문: {{ $('Question webhook').first().json.body.question }}\n분류: {{ $('Parse category').first().json.category }}\nFAQ: {{ JSON.stringify($('Search FAQ').first().json.results ?? []) }}\n접수 건: {{ JSON.stringify($('Get ticket').first().json) }}",
"options": {
"systemMessage": "You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \"찾지 못했습니다\" and that a staff member will help.",
"maxIterations": 1
}
},
"position": [
2200,
300
],
"id": "d7ec1c62-265f-5d16-b67a-c5922305bb8a"
},
{
"name": "Answer model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.2,
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4.1-mini"
},
"options": {
"temperature": 0
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
2200,
520
],
"id": "5c7f8b81-cd4c-59a2-9899-9bdb4813f7ab"
},
{
"name": "After answer",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
2420,
300
],
"id": "43dab328-4901-5fd6-8238-6e0f836cc31d"
},
{
"name": "Review",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"parameters": {
"promptType": "define",
"text": "=초안: {{ $('Answer').first().json.output }}",
"options": {
"systemMessage": "You review a Korean IT help-desk answer before it is sent. Keep the facts exactly. Make it polite (합니다체), remove anything not supported by the data, keep at most 3 sentences. Return only the final answer text.",
"maxIterations": 1
}
},
"position": [
2640,
300
],
"id": "a4583946-f582-5ad8-963b-b866b74e9ba4"
},
{
"name": "Review model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.2,
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4.1-mini"
},
"options": {
"temperature": 0
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
2640,
520
],
"id": "3be9acc9-4b3d-5a41-b6a4-00fa1b04d777"
},
{
"name": "After review",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
2860,
300
],
"id": "d07d9992-7e5e-5aab-b925-9387021bbaff"
},
{
"name": "Build result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const ran = (name) => { try { return $(name).isExecuted; } catch (error) { return false; } };\nconst body = $('Question webhook').first().json.body;\nconst category = $('Parse category').first().json.category;\nconst answer = String($('Review').first().json.output ?? '').trim();\nconst results = $('Search FAQ').first().json.results ?? [];\nconst sources = category === 'faq' ? results.map((r) => r.id) : [];\nconst steps = [];\nif (ran('Rule classify')) steps.push('rule');\nif (ran('Classify')) steps.push('classify');\nsteps.push('faq', 'ticket', 'answer');\nsteps.push('review');\nconst llm_calls = (ran('Classify') ? 1 : 0) + 1 + 1;\nconst result = { route: '자동 답변', category, answer, sources };\nconst t = (name) => (ran(name) ? $(name).first().json.t : null);\nconst t0 = t('Start clock');\nconst marks = [['classify', t('After classify')], ['faq', t('After FAQ')], ['ticket', t('After data')], ['answer', t('After answer')], ['review', t('After review')]];\nconst timings = {};\nlet last = t0;\nfor (const [label, mark] of marks) {\n if (mark === null) continue;\n timings[`${label}_ms`] = mark - last;\n last = mark;\n}\ntimings.total_ms = Date.now() - t0;\nObject.assign(result, { llm_calls, cached: false, steps, timings });\nconst parts = Object.entries(timings).filter(([key]) => key !== 'total_ms');\nresult.slowest_step = parts.sort((a, b) => b[1] - a[1])[0][0].replace(/_ms$/, '');\nconst budget_ms = Number(body.budget_ms ?? 3000);\nresult.llm_ms = ['classify', 'answer', 'review'].reduce((sum, label) => sum + (timings[`${label}_ms`] ?? 0), 0);\nresult.budget_ms = budget_ms;\nresult.over_budget = timings.total_ms > budget_ms;\nreturn [{ json: result }];"
},
"position": [
3080,
300
],
"id": "079e44f0-da3e-57dd-be6a-87025b6a320f"
},
{
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"position": [
3300,
300
],
"id": "8ac7b99c-d9fa-5a1e-a168-33eb7ab5e2a3"
}
],
"connections": {
"Question webhook": {
"main": [
[
{
"node": "Has question?",
"type": "main",
"index": 0
}
]
]
},
"Has question?": {
"main": [
[
{
"node": "Start clock",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject empty",
"type": "main",
"index": 0
}
]
]
},
"Start clock": {
"main": [
[
{
"node": "Classify",
"type": "main",
"index": 0
}
]
]
},
"Classify model": {
"ai_languageModel": [
[
{
"node": "Classify",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Classify": {
"main": [
[
{
"node": "Parse category",
"type": "main",
"index": 0
}
]
]
},
"Parse category": {
"main": [
[
{
"node": "After classify",
"type": "main",
"index": 0
}
]
]
},
"After classify": {
"main": [
[
{
"node": "Search FAQ",
"type": "main",
"index": 0
}
]
]
},
"Search FAQ": {
"main": [
[
{
"node": "After FAQ",
"type": "main",
"index": 0
}
]
]
},
"After FAQ": {
"main": [
[
{
"node": "Get ticket",
"type": "main",
"index": 0
}
]
]
},
"Get ticket": {
"main": [
[
{
"node": "After data",
"type": "main",
"index": 0
}
]
]
},
"After data": {
"main": [
[
{
"node": "Answer",
"type": "main",
"index": 0
}
]
]
},
"Answer model": {
"ai_languageModel": [
[
{
"node": "Answer",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Answer": {
"main": [
[
{
"node": "After answer",
"type": "main",
"index": 0
}
]
]
},
"After answer": {
"main": [
[
{
"node": "Review",
"type": "main",
"index": 0
}
]
]
},
"Review model": {
"ai_languageModel": [
[
{
"node": "Review",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Review": {
"main": [
[
{
"node": "After review",
"type": "main",
"index": 0
}
]
]
},
"After review": {
"main": [
[
{
"node": "Build result",
"type": "main",
"index": 0
}
]
]
},
"Build result": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,852 @@
{
"name": "72 IT helpdesk + one LLM call (answer)",
"nodes": [
{
"name": "Question webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"parameters": {
"httpMethod": "POST",
"path": "my-helpdesk",
"responseMode": "responseNode",
"options": {}
},
"webhookId": "de518e93-ed19-541f-a6fc-687a4efaf1b2",
"position": [
0,
300
],
"id": "4a893fec-ca3a-5349-a7b1-a8de0ac4350c"
},
{
"name": "Has question?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.body.question }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"position": [
220,
300
],
"id": "da287ac6-5b2c-5d11-a358-2c0d575d61ce"
},
{
"name": "Reject empty",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "{ \"error\": \"question is required\" }",
"options": {
"responseCode": 400
}
},
"position": [
440,
560
],
"id": "049a20b6-3c29-5159-a0e6-f5617cf9d8d9"
},
{
"name": "Start clock",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { t: Date.now() } }];"
},
"position": [
440,
300
],
"id": "3b52fb72-af9e-54d9-8928-a1f87966b69f"
},
{
"name": "Cache key",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const body = $('Question webhook').first().json.body;\nconst key = String(body.question ?? '').trim().toLowerCase().replace(/\\s+/g, ' ');\nreturn [{ json: { ...$json, key, use_cache: body.no_cache !== true } }];"
},
"position": [
660,
300
],
"id": "1aed5faa-1a89-578c-8093-61fe2e9a2555"
},
{
"name": "Cache lookup",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "=http://data-api:8800/cache/{{ encodeURIComponent($json.key) }}",
"options": {
"response": {
"response": {
"neverError": true
}
}
}
},
"position": [
880,
300
],
"id": "33b97251-cad7-50a2-805b-ecc7d70001de"
},
{
"name": "Cached?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.value !== undefined && $('Cache key').first().json.use_cache }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"position": [
1100,
300
],
"id": "7ad20324-27b3-5f30-9637-a4fa4d977a53"
},
{
"name": "Cached result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const value = $json.value;\nconst t0 = $('Start clock').first().json.t;\nreturn [{ json: { route: value.route, category: value.category, answer: value.answer, sources: value.sources ?? [],\n llm_calls: 0, cached: true, steps: ['cache'], timings: { total_ms: t0 ? Date.now() - t0 : 0 } } }];"
},
"position": [
1320,
40
],
"id": "b102773a-d6b3-5fd8-9ada-e250f648cc44"
},
{
"name": "Send cached",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"position": [
1540,
40
],
"id": "ff6ce5c6-903d-53f6-96f7-a17baabcc1b6"
},
{
"name": "Search FAQ",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/faq",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "q",
"value": "={{ $('Question webhook').first().json.body.question }}"
}
]
}
},
"position": [
1320,
300
],
"id": "0e3e780d-eaa1-5c13-a60a-f3112a296820"
},
{
"name": "Get ticket",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}",
"options": {
"response": {
"response": {
"neverError": true
}
}
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "requester",
"value": "={{ $('Question webhook').first().json.body.user ?? '' }}"
}
]
}
},
"position": [
1540,
300
],
"id": "f345aacb-db55-5226-bf79-822fd2f814ca"
},
{
"name": "After data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
1760,
300
],
"id": "4c8c6887-4a50-589b-abdf-c8741f1cf589"
},
{
"name": "Rule classify",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const question = String($('Question webhook').first().json.body.question ?? '');\nconst results = $('Search FAQ').first().json.results ?? [];\nlet category = 'unknown';\nif (/T-?\\d{4}/i.test(question)) category = 'ticket';\nelse if (results.length > 0) category = 'faq';\nreturn [{ json: { category, by: 'rule' } }];"
},
"position": [
1980,
300
],
"id": "b01cd9fc-72fc-5ace-860b-6d607bf1e570"
},
{
"name": "Known?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.category }}",
"rightValue": "unknown",
"operator": {
"type": "string",
"operation": "notEquals"
}
}
],
"combinator": "and"
},
"options": {}
},
"position": [
2200,
300
],
"id": "2d154b76-eb12-5038-a639-c5608c6bf788"
},
{
"name": "Classify",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"parameters": {
"promptType": "define",
"text": "={{ $('Question webhook').first().json.body.question }}",
"options": {
"systemMessage": "You sort one company IT help-desk question. Categories: faq (how to fix or use something: password, account lock, VPN, printer, mail box, software install), ticket (asks about the status of an IT request ticket like T-1002), other (anything else). Return only JSON, no code fence: {\"category\": \"faq\"}",
"maxIterations": 1
}
},
"position": [
2420,
300
],
"id": "8ba05958-c916-52e4-95ef-0f38f92246c3"
},
{
"name": "Classify model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.2,
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4.1-mini"
},
"options": {
"temperature": 0
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
2420,
520
],
"id": "14555336-1d5b-5860-98cc-cc7febd5fdab"
},
{
"name": "Parse category",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const raw = String($json.output ?? '').trim().replace(/^```(json)?\\s*|```$/g, '');\nlet category = 'other';\ntry { category = JSON.parse(raw).category ?? 'other'; } catch (error) { category = 'other'; }\nif (!['faq', 'ticket', 'other'].includes(category)) category = 'other';\nreturn [{ json: { category, by: 'llm' } }];"
},
"position": [
2640,
300
],
"id": "53522d0c-94f5-5e97-84ca-dfc99cbea797"
},
{
"name": "After classify",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
2860,
300
],
"id": "ed776d70-2fb9-5b97-86c8-aa43796e8cb3"
},
{
"name": "Answer",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"parameters": {
"promptType": "define",
"text": "=질문: {{ $('Question webhook').first().json.body.question }}\n분류: {{ $json.category }}\nFAQ: {{ JSON.stringify($('Search FAQ').first().json.results ?? []) }}\n접수 건: {{ JSON.stringify($('Get ticket').first().json) }}",
"options": {
"systemMessage": "You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \"찾지 못했습니다\" and that a staff member will help.",
"maxIterations": 1
}
},
"position": [
3080,
300
],
"id": "3cd227a5-1bb5-50e6-9402-69206c7ca546"
},
{
"name": "Answer model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.2,
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4.1-mini"
},
"options": {
"temperature": 0
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
3080,
520
],
"id": "f3dad63e-7b9c-5d29-b79e-077754d581b7"
},
{
"name": "After answer",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
3300,
300
],
"id": "0432c08a-5b0d-5d75-8e06-eab20db15ab1"
},
{
"name": "Review check",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "let answer = String($('Answer').first().json.output ?? '').trim();\nconst problems = [];\nif (!answer) problems.push('empty');\nif (!/[가-힣]/.test(answer)) problems.push('not_korean');\nif (answer.length > 400) { answer = answer.slice(0, 400); problems.push('too_long'); }\nreturn [{ json: { ...$json, answer, review_problems: problems } }];"
},
"position": [
3520,
300
],
"id": "c2d5b0e4-e7e1-5972-8b49-d2aa51694714"
},
{
"name": "After review",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
3740,
300
],
"id": "f031c87a-b44b-55fa-a122-05d93f51fa6e"
},
{
"name": "Build result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const ran = (name) => { try { return $(name).isExecuted; } catch (error) { return false; } };\nconst body = $('Question webhook').first().json.body;\nconst category = $('Parse category').isExecuted ? $('Parse category').first().json.category : $('Rule classify').first().json.category;\nconst answer = $('Review check').first().json.answer;\nconst results = $('Search FAQ').first().json.results ?? [];\nconst sources = category === 'faq' ? results.map((r) => r.id) : [];\nconst steps = [];\nif (ran('Rule classify')) steps.push('rule');\nif (ran('Classify')) steps.push('classify');\nsteps.push('faq', 'ticket', 'answer');\nsteps.push('check');\nconst llm_calls = (ran('Classify') ? 1 : 0) + 1 + 0;\nconst result = { route: '자동 답변', category, answer, sources };\nconst t = (name) => (ran(name) ? $(name).first().json.t : null);\nconst t0 = t('Start clock');\nconst marks = [['data', t('After data')], ['classify', t('After classify')], ['answer', t('After answer')], ['review', t('After review')]];\nconst timings = {};\nlet last = t0;\nfor (const [label, mark] of marks) {\n if (mark === null) continue;\n timings[`${label}_ms`] = mark - last;\n last = mark;\n}\ntimings.total_ms = Date.now() - t0;\nObject.assign(result, { llm_calls, cached: false, steps, timings });\nreturn [{ json: result }];"
},
"position": [
3960,
300
],
"id": "fc92aa99-281c-5e7a-96fb-6868624c4f34"
},
{
"name": "Cache it?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.category === 'faq' && $('Cache key').first().json.use_cache }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"position": [
4180,
300
],
"id": "f14260a7-8d0f-5916-a219-c233bc484860"
},
{
"name": "Store in cache",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "=http://data-api:8800/cache/{{ encodeURIComponent($('Cache key').first().json.key) }}",
"options": {
"response": {
"response": {
"neverError": true
}
}
},
"method": "POST",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ value: { route: $json.route, category: $json.category, answer: $json.answer, sources: $json.sources }, ttl: 600 }) }}"
},
"position": [
4400,
180
],
"id": "8c73289d-f4b4-50c2-a71e-229d368e7b39"
},
{
"name": "Result again",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: $('Build result').first().json }];"
},
"position": [
4620,
180
],
"id": "4033230f-4e3a-515a-a980-ed987bc3372e"
},
{
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"position": [
4840,
300
],
"id": "43408f1e-819c-54ef-bb7b-7039f738e6f1"
}
],
"connections": {
"Question webhook": {
"main": [
[
{
"node": "Has question?",
"type": "main",
"index": 0
}
]
]
},
"Has question?": {
"main": [
[
{
"node": "Start clock",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject empty",
"type": "main",
"index": 0
}
]
]
},
"Start clock": {
"main": [
[
{
"node": "Cache key",
"type": "main",
"index": 0
}
]
]
},
"Cache key": {
"main": [
[
{
"node": "Cache lookup",
"type": "main",
"index": 0
}
]
]
},
"Cache lookup": {
"main": [
[
{
"node": "Cached?",
"type": "main",
"index": 0
}
]
]
},
"Cached?": {
"main": [
[
{
"node": "Cached result",
"type": "main",
"index": 0
}
],
[
{
"node": "Search FAQ",
"type": "main",
"index": 0
}
]
]
},
"Cached result": {
"main": [
[
{
"node": "Send cached",
"type": "main",
"index": 0
}
]
]
},
"Search FAQ": {
"main": [
[
{
"node": "Get ticket",
"type": "main",
"index": 0
}
]
]
},
"Get ticket": {
"main": [
[
{
"node": "After data",
"type": "main",
"index": 0
}
]
]
},
"After data": {
"main": [
[
{
"node": "Rule classify",
"type": "main",
"index": 0
}
]
]
},
"Rule classify": {
"main": [
[
{
"node": "Known?",
"type": "main",
"index": 0
}
]
]
},
"Known?": {
"main": [
[
{
"node": "Answer",
"type": "main",
"index": 0
}
],
[
{
"node": "Classify",
"type": "main",
"index": 0
}
]
]
},
"Classify model": {
"ai_languageModel": [
[
{
"node": "Classify",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Classify": {
"main": [
[
{
"node": "Parse category",
"type": "main",
"index": 0
}
]
]
},
"Parse category": {
"main": [
[
{
"node": "After classify",
"type": "main",
"index": 0
}
]
]
},
"After classify": {
"main": [
[
{
"node": "Answer",
"type": "main",
"index": 0
}
]
]
},
"Answer model": {
"ai_languageModel": [
[
{
"node": "Answer",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Answer": {
"main": [
[
{
"node": "After answer",
"type": "main",
"index": 0
}
]
]
},
"After answer": {
"main": [
[
{
"node": "Review check",
"type": "main",
"index": 0
}
]
]
},
"Review check": {
"main": [
[
{
"node": "After review",
"type": "main",
"index": 0
}
]
]
},
"After review": {
"main": [
[
{
"node": "Build result",
"type": "main",
"index": 0
}
]
]
},
"Build result": {
"main": [
[
{
"node": "Cache it?",
"type": "main",
"index": 0
}
]
]
},
"Cache it?": {
"main": [
[
{
"node": "Store in cache",
"type": "main",
"index": 0
}
],
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
},
"Store in cache": {
"main": [
[
{
"node": "Result again",
"type": "main",
"index": 0
}
]
]
},
"Result again": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,238 @@
{
"name": "72 Health check (answer)",
"nodes": [
{
"name": "Health webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"parameters": {
"httpMethod": "POST",
"path": "my-health",
"responseMode": "responseNode",
"options": {}
},
"webhookId": "bdf5dd1e-edb4-503e-b800-c8653ff5fbb4",
"position": [
0,
300
],
"id": "cd5b6b9d-06a3-57ed-91ab-0ab44b344fde"
},
{
"name": "Start",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { t0: Date.now() } }];"
},
"position": [
220,
300
],
"id": "0445c332-e249-5cce-915d-fca703b570b8"
},
{
"name": "Check data API",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/health",
"options": {
"timeout": 2000
}
},
"onError": "continueRegularOutput",
"alwaysOutputData": true,
"position": [
440,
300
],
"id": "8ce80344-ec8e-5bd5-9a2e-839d49d9f1e9"
},
{
"name": "Check FAQ",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/faq",
"options": {
"timeout": 2000
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "q",
"value": "vpn"
}
]
}
},
"onError": "continueRegularOutput",
"alwaysOutputData": true,
"position": [
660,
300
],
"id": "a935db0a-fc45-5a01-aa52-405740f82e9f"
},
{
"name": "Check LLM",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"parameters": {
"promptType": "define",
"text": "Reply with the single word OK.",
"options": {
"systemMessage": "You are a health check. Reply with OK only.",
"maxIterations": 1
}
},
"onError": "continueRegularOutput",
"alwaysOutputData": true,
"position": [
880,
300
],
"id": "96a58520-65ae-548a-ac2f-0b41d004933f"
},
{
"name": "Health model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.2,
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4.1-mini"
},
"options": {
"temperature": 0,
"timeout": 8000,
"maxRetries": 0
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
880,
520
],
"id": "5aa10812-b13e-5bae-831e-7ccd24e72e62"
},
{
"name": "Summarize",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const data_api = $('Check data API').first().json.status === 'ok';\nconst faq = Array.isArray($('Check FAQ').first().json.results);\nconst llm = /ok/i.test(String($('Check LLM').first().json.output ?? ''));\nconst checks = { data_api, faq, llm };\nconst failed = Object.entries(checks).filter(([, good]) => !good).map(([name]) => name);\nconst status = failed.length === 0 ? 'ok' : (data_api || llm ? 'degraded' : 'down');\nreturn [{ json: { status, checks, failed, seconds: Math.round((Date.now() - $('Start').first().json.t0) / 100) / 10 } }];"
},
"position": [
1100,
300
],
"id": "54ee87ba-881d-5ad1-a032-1262876fdd48"
},
{
"name": "Send status",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"position": [
1320,
300
],
"id": "2340a4f9-d3c9-5717-ad49-b782bcd9f808"
}
],
"connections": {
"Health model": {
"ai_languageModel": [
[
{
"node": "Check LLM",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Health webhook": {
"main": [
[
{
"node": "Start",
"type": "main",
"index": 0
}
]
]
},
"Start": {
"main": [
[
{
"node": "Check data API",
"type": "main",
"index": 0
}
]
]
},
"Check data API": {
"main": [
[
{
"node": "Check FAQ",
"type": "main",
"index": 0
}
]
]
},
"Check FAQ": {
"main": [
[
{
"node": "Check LLM",
"type": "main",
"index": 0
}
]
]
},
"Check LLM": {
"main": [
[
{
"node": "Summarize",
"type": "main",
"index": 0
}
]
]
},
"Summarize": {
"main": [
[
{
"node": "Send status",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+4
View File
@@ -0,0 +1,4 @@
.venv/
__pycache__/
*.egg-info/
.pytest_cache/
+16
View File
@@ -0,0 +1,16 @@
# tbench IMP-PLAN (강사가 직접 만든 기록)
## 엔진 1단계 · 부르고 재기 (R1 · R6 · R7 · R8)
- [x] runner.py: call_once → {status, seconds, error_kind, timings}, run() 제너레이터
- [x] stats.py: nearest-rank (1~10 → p50 5 · p95 10), 실패는 세고 시간 목록에서 뺌, p50 excl. first
- [x] tests/conftest.py 가짜 서버, --plain · --warmup · --header (가리기)
## TUI 1단계 (R2)
- [x] app.py: Sparkline · 요약 · DataTable, worker(thread) + call_from_thread. 테스트: 지연 0.5초 × 6, 1.3초 뒤 1~5줄 → 끝에 6줄
## 설치 (R14)
- [x] [project.scripts] tbench = "tbench.cli:main", uv tool install --editable ., /tmp 에서 tbench --help
## 엔진 2단계 (R3 · R13 · R11)
- [x] --save (기본 ~/.tbench/results/), compare (url · method · body_sha256 · n · concurrency · warmup 다르면 WARNING), history (최신 위), --log
## 엔진 3단계 (R4 · R10 · R9 · R12)
- [x] --concurrency (0.3초 × 6: k=1 1.8초대, k=3 1초 안), --max-p95 · --max-failures 종료 코드 0 · 1, timings 평균, profile save/list · run
## TUI 2단계 (R5)
- [x] --pick DIR: OptionList, Enter 로 그 본문 실행
+38
View File
@@ -0,0 +1,38 @@
# tbench · 강사가 수업 준비 때 직접 만든 본 (2026-10-26)
> **정답 자료** · 학생 저장소에는 수업 뒤 [VIBE-A-TBENCH.md](../VIBE-A-TBENCH.md)와 함께 올립니다.
> 이 본은 강사가 CLI 에이전트에 단계 요청문을 보내 만든 것이 아니라, Claude Code가 VIBE-A의 SPEC · IMP-PLAN을 보고 직접 썼습니다. 학생이 만든 것과 파일 구성이 달라도 됩니다. 요구 R1~R14의 **확인 방법**이 같으면 됩니다.
## 설치 · 실행
```bash
cd ~/aio-01d4-stu/Last3Mon/72/tbench
uv sync && uv run pytest -q # 가짜 서버 테스트 12개 (진짜 헬프데스크를 부르지 않음)
uv tool install --editable . # 어느 폴더에서나 tbench (R14)
cd /tmp && tbench --help
```
| 하고 싶은 것 | 명령 |
| --- | --- |
| 화면(TUI)으로 재기 | `tbench http://localhost:5678/webhook/my-helpdesk --body-file requests/faq-vpn-nocache.json -n 20` |
| 글자로 · 저장 · 기준 | `tbench <url> --body-file x.json -n 10 --plain --save --max-p95 2.5 --max-failures 0` |
| 요청 고르기 | `tbench <url> --pick requests` |
| 동시에 · 미리 부르기 | `--concurrency 3` · `--warmup 1` |
| 키 머리글 | `--header 'Authorization: Bearer $MY_KEY'` (저장 · 기록에는 `***`) |
| 비교 · 목록 | `tbench compare a.json b.json` · `tbench history` |
| 설정 저장 · 부르기 | `tbench profile save vpn <url> --body-file requests/faq-vpn-nocache.json -n 10` · `tbench profile list` · `tbench run vpn --plain` |
| 한 번마다 한 줄 | `--log runs.jsonl` |
저장 위치는 `~/.tbench/` (`results/`, `profiles.toml`). 테스트는 `TBENCH_HOME`을 임시 폴더로 바꿔 씁니다.
## 파일
| 파일 | 하는 일 |
| --- | --- |
| `src/tbench/runner.py` | 엔진: 한 번 부르기(시간 · 상태 · 실패 종류 · `timings`), `run()` 제너레이터 (warmup · 동시) |
| `src/tbench/stats.py` | nearest-rank 백분위, 요약, 기준 검사(PASS · FAIL) |
| `src/tbench/store.py` | 저장(조건 · 본문 해시 · 키 가리기), compare, history, 기록 줄 |
| `src/tbench/profiles.py` | `~/.tbench/profiles.toml` |
| `src/tbench/cli.py` | 명령줄, `--plain` 출력, 종료 코드 |
| `src/tbench/app.py` | Textual 화면 (worker + `call_from_thread`), `--pick` 고르기 |
| `tests/conftest.py` | pytest 안의 가짜 서버 (지연 · N번째마다 500 · 첫 응답만 느림 · 상태 코드 · `timings`) |
+22
View File
@@ -0,0 +1,22 @@
[project]
name = "tbench"
version = "0.1.0"
description = "Call a webhook many times and show how long each call took"
requires-python = ">=3.11"
dependencies = ["httpx>=0.27", "textual>=0.80"]
[project.scripts]
tbench = "tbench.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/tbench"]
[dependency-groups]
dev = ["pytest>=8", "pytest-asyncio>=0.23"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
@@ -0,0 +1 @@
"""tbench: call a webhook many times and show how long each call took."""
+75
View File
@@ -0,0 +1,75 @@
"""Textual screen: sparkline and summary on top, one table row per call as it arrives."""
from pathlib import Path
from textual import work
from textual.app import App, ComposeResult
from textual.widgets import DataTable, Footer, Header, OptionList, Sparkline, Static
from textual.widgets.option_list import Option
from .runner import read_body, run
from .stats import fmt, summarize, summary_lines
class BenchApp(App):
TITLE = "tbench"
BINDINGS = [("q", "quit", "Quit")]
CSS = """
Sparkline { height: 4; margin: 0 1; }
#summary { height: auto; margin: 0 1; }
OptionList { height: 1fr; }
"""
def __init__(self, settings, headers, pick=None):
super().__init__()
self.settings, self.headers, self.pick = settings, headers, pick
self.results, self.body = [], None
def compose(self) -> ComposeResult:
yield Header()
if self.pick:
files = sorted(Path(self.pick).glob("*.json"))
yield OptionList(*[Option(f.name, id=str(f)) for f in files], id="pick")
yield Sparkline([], id="spark")
yield Static("starting...", id="summary")
yield DataTable(id="calls")
yield Footer()
def on_mount(self):
table = self.query_one("#calls", DataTable)
table.add_columns("#", "status", "seconds", "note")
if self.pick:
self.sub_title = f"choose a body from {self.pick}"
self.query_one("#summary", Static).update("Enter: run with this body")
else:
self.start(self.settings.get("body_file"))
def on_option_list_option_selected(self, event):
event.option_list.remove()
self.settings["body_file"] = event.option.id
self.start(event.option.id)
def start(self, body_file):
s = self.settings
self.body = read_body(body_file)
self.sub_title = f"{s['method']} {s['url']} body={Path(body_file).name if body_file else '-'}"
self.measure()
@work(thread=True, exclusive=True)
def measure(self):
s = self.settings
for r in run(s["url"], self.body, s["n"], s["method"], self.headers, s["timeout"], s["warmup"], s["concurrency"]):
self.call_from_thread(self.add_result, r)
self.call_from_thread(self.finish)
def add_result(self, r):
self.results.append(r)
label = f"w{r['i']}" if r["warmup"] else str(r["i"])
status = str(r["status"]) if r["status"] is not None else r["error_kind"]
note = " ".join(x for x in (r["note"], r["error_kind"] if r["status"] else "") if x)
self.query_one("#calls", DataTable).add_row(label, status, fmt(r["seconds"]), note)
measured = [x for x in self.results if not x["warmup"]]
self.query_one("#spark", Sparkline).data = [x["seconds"] for x in measured if x["error_kind"] is None]
self.query_one("#summary", Static).update("\n".join(summary_lines(summarize(measured, self.settings["n"]))))
def finish(self):
self.sub_title += " (done, q to quit)"
+166
View File
@@ -0,0 +1,166 @@
"""Command line: `tbench <url> ...` (TUI, or --plain), compare, history, profile, run."""
import argparse
import os
import sys
from datetime import datetime
from pathlib import Path
from . import profiles, store
from .runner import read_body, run
from .stats import check, fmt, summarize, summary_lines
COMMANDS = ("compare", "history", "profile", "run")
def parse_headers(items):
headers = {}
for item in items or []:
if ":" not in item:
raise SystemExit(f"bad --header {item!r}, use 'Name: value'")
k, v = item.split(":", 1)
headers[k.strip()] = os.path.expandvars(v.strip())
return headers
def add_run_options(p, url=True):
if url:
p.add_argument("url")
p.add_argument("--body-file", help="JSON file to send as the body")
p.add_argument("-n", type=int, default=None, help="measured calls (default 10)")
p.add_argument("--method", default=None, help="HTTP method (default POST)")
p.add_argument("--timeout", type=float, default=None, help="seconds before a call counts as timeout (default 30)")
p.add_argument("--header", action="append", default=[], help="'Name: value', may repeat; $VARS are expanded")
p.add_argument("--warmup", type=int, default=None, help="calls to make and throw away first")
p.add_argument("--concurrency", type=int, default=None, help="calls in flight at once (default 1)")
p.add_argument("--plain", action="store_true", help="print lines instead of the TUI")
p.add_argument("--save", nargs="?", const="", default=None, help="save result JSON (default ~/.tbench/results/)")
p.add_argument("--log", help="append one JSON line per call to this file")
p.add_argument("--max-p95", type=float, default=None, help="FAIL (exit 1) if p95 seconds is above this")
p.add_argument("--max-failures", type=int, default=None, help="FAIL (exit 1) if failures are above this")
p.add_argument("--pick", metavar="DIR", help="choose the body from *.json in DIR (TUI)")
def settings_from(args, base=None):
s = dict(base or {})
for key in ("url", "body_file", "n", "method", "timeout", "warmup", "concurrency", "max_p95", "max_failures"):
value = getattr(args, key, None)
if value is not None:
s[key] = value
if getattr(args, "header", None):
s["headers"] = args.header
s.setdefault("n", 10)
s.setdefault("method", "POST")
s.setdefault("timeout", 30.0)
s.setdefault("warmup", 0)
s.setdefault("concurrency", 1)
return s
def execute(s, plain, save_to, log_path, pick=None):
headers = parse_headers(s.get("headers"))
started = datetime.now().astimezone().isoformat(timespec="seconds")
if not plain:
from .app import BenchApp
app = BenchApp(s, headers, pick=pick)
app.run()
results, body = app.results, app.body
if not results:
return 0
else:
body = read_body(s.get("body_file"))
results = []
log = open(log_path, "a", encoding="utf-8") if log_path else None
print(f"{s['method']} {s['url']} n={s['n']} concurrency={s['concurrency']} warmup={s['warmup']}")
try:
for r in run(s["url"], body, s["n"], s["method"], headers, s["timeout"], s["warmup"], s["concurrency"]):
results.append(r)
if log:
store.log_line(log, r)
label = f"w{r['i']}" if r["warmup"] else f"#{r['i']}"
status = r["status"] if r["status"] is not None else r["error_kind"]
note = " ".join(x for x in (r["note"], r["error_kind"] if r["status"] else "") if x)
print(f"{label:>4} {status!s:>8} {fmt(r['seconds']):>8}s {note}".rstrip())
finally:
if log:
log.close()
if log_path and not plain:
with open(log_path, "a", encoding="utf-8") as log:
for r in results:
store.log_line(log, r)
measured = [r for r in results if not r["warmup"]]
summary = summarize(measured, s["n"])
for line in summary_lines(summary):
print(line)
if save_to is not None:
record = store.build_record(s["url"], s["method"], body, headers, s["n"], s["concurrency"],
s["warmup"], started, results)
print(f"saved {store.save(record, save_to or None)}")
if s.get("max_p95") is not None or s.get("max_failures") is not None:
passed, line = check(summary, s.get("max_p95"), s.get("max_failures"))
print(line)
return 0 if passed else 1
return 0
def main(argv=None):
argv = sys.argv[1:] if argv is None else argv
if argv and argv[0] in COMMANDS:
return sys.exit(sub_main(argv))
p = argparse.ArgumentParser(prog="tbench", description="Call a URL many times and show how long each call took. "
"Also: tbench compare A B | history | profile save/list | run NAME")
add_run_options(p)
args = p.parse_args(argv)
sys.exit(execute(settings_from(args), args.plain, args.save, args.log, args.pick))
def sub_main(argv):
p = argparse.ArgumentParser(prog="tbench")
sub = p.add_subparsers(dest="cmd", required=True)
c = sub.add_parser("compare", help="compare two saved results")
c.add_argument("a")
c.add_argument("b")
sub.add_parser("history", help="list saved results, newest first")
prof = sub.add_parser("profile", help="save or list named settings")
psub = prof.add_subparsers(dest="action", required=True)
ps = psub.add_parser("save")
ps.add_argument("name")
add_run_options(ps)
psub.add_parser("list")
r = sub.add_parser("run", help="run a saved profile")
r.add_argument("name")
add_run_options(r, url=False)
args = p.parse_args(argv)
if args.cmd == "compare":
for line in store.compare(store.load(args.a), store.load(args.b)):
print(line)
return 0
if args.cmd == "history":
rows = store.history()
print(f"{'started':19} {'n':>3} {'p50':>7} {'p95':>7} {'fail':>4} url")
for started, url, n, p50, p95, failures, _ in rows:
print(f"{started:19} {n:>3} {p50:>7} {p95:>7} {failures:>4} {url}")
return 0
if args.cmd == "profile" and args.action == "list":
for name, values in profiles.load_all().items():
print(f"{name:12} {values.get('url')} body={values.get('body_file', '-')} n={values.get('n', 10)}")
return 0
if args.cmd == "profile":
s = {k: v for k, v in vars(args).items() if k in profiles.FIELDS and v is not None}
if s.get("body_file"):
s["body_file"] = str(Path(s["body_file"]).resolve())
safe = []
for h in args.header:
name, _, value = h.partition(":")
if store.mask_headers({name: 1})[name] == "***" and "$" not in value:
print(f"note: {name.strip()} is kept as *** — pass it as an env var, e.g. 'Authorization: Bearer $MY_KEY'")
value = " ***"
safe.append(f"{name}:{value}")
s["headers"] = safe
print(f"saved {args.name} to {profiles.save(args.name, s)}")
return 0
base = profiles.load_all().get(args.name)
if base is None:
print(f"no profile {args.name!r} in {profiles.path()}")
return 2
return execute(settings_from(args, base), args.plain, args.save, args.log, args.pick)
+37
View File
@@ -0,0 +1,37 @@
"""Named settings in ~/.tbench/profiles.toml, so `tbench run <name>` works from any folder."""
import tomllib
from .store import home
FIELDS = ("url", "body_file", "n", "method", "timeout", "warmup", "concurrency", "headers", "max_p95", "max_failures")
def path():
return home() / "profiles.toml"
def load_all():
p = path()
return tomllib.loads(p.read_text(encoding="utf-8")) if p.exists() else {}
def _value(v):
if isinstance(v, str):
return '"' + v.replace("\\", "\\\\").replace('"', '\\"') + '"'
if isinstance(v, list):
return "[" + ", ".join(_value(x) for x in v) + "]"
return str(v).lower() if isinstance(v, bool) else str(v)
def save(name, settings):
profiles = load_all()
profiles[name] = {k: v for k, v in settings.items() if k in FIELDS and v not in (None, [])}
out = []
for pname, values in profiles.items():
out.append(f'["{pname}"]')
out += [f"{k} = {_value(v)}" for k, v in values.items()]
out.append("")
p = path()
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text("\n".join(out), encoding="utf-8")
return p
+67
View File
@@ -0,0 +1,67 @@
"""Call a URL many times. run() yields one result dict per call, as it finishes."""
import json
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
import httpx
def call_once(client, url, method, body, headers, timeout):
"""One call, timed from just before sending until the whole body is read."""
start = time.perf_counter()
status, error_kind, timings = None, None, None
try:
response = client.request(method, url, content=body, headers=headers, timeout=timeout)
response.read()
status = response.status_code
if status >= 500:
error_kind = "http_5xx"
elif status >= 400:
error_kind = "http_4xx"
else:
try:
data = response.json()
if isinstance(data, dict) and isinstance(data.get("timings"), dict):
timings = data["timings"]
except ValueError:
pass
except httpx.TimeoutException:
error_kind = "timeout"
except httpx.TransportError:
error_kind = "connect"
return {"status": status, "seconds": round(time.perf_counter() - start, 4),
"error_kind": error_kind, "timings": timings}
def run(url, body=None, n=10, method="POST", headers=None, timeout=30.0, warmup=0, concurrency=1):
"""Yield warmup calls (warmup=True) first, then n measured calls numbered 1..n."""
headers = dict(headers or {})
if body is not None and not any(k.lower() == "content-type" for k in headers):
headers["Content-Type"] = "application/json"
content = body.encode() if isinstance(body, str) else body
with httpx.Client() as client:
for w in range(warmup):
r = call_once(client, url, method, content, headers, timeout)
yield {"i": w + 1, "warmup": True, "note": "warmup", **r}
first_note = "first call" if warmup == 0 else ""
if concurrency <= 1:
for i in range(1, n + 1):
r = call_once(client, url, method, content, headers, timeout)
yield {"i": i, "warmup": False, "note": first_note if i == 1 else "", **r}
return
with ThreadPoolExecutor(max_workers=concurrency) as pool:
futures = {pool.submit(call_once, client, url, method, content, headers, timeout): i
for i in range(1, n + 1)}
for future in as_completed(futures):
i = futures[future]
yield {"i": i, "warmup": False, "note": first_note if i == 1 else "", **future.result()}
def read_body(path):
"""Read a JSON body file; keep the exact text so its hash matches what was sent."""
if path is None:
return None
with open(path, encoding="utf-8") as f:
text = f.read()
json.loads(text)
return text
+69
View File
@@ -0,0 +1,69 @@
"""Percentiles and summaries. Nearest-rank: sort, take the ceil(p * n)-th value."""
import math
KINDS = ("connect", "timeout", "http_5xx", "http_4xx")
def percentile(values, p):
if not values:
return None
ordered = sorted(values)
rank = max(1, math.ceil(p * len(ordered) - 1e-9))
return ordered[rank - 1]
def average_timings(results):
"""Average every numeric field of response `timings` over successful calls."""
sums, counts = {}, {}
for r in results:
for key, value in (r.get("timings") or {}).items():
if isinstance(value, (int, float)) and not isinstance(value, bool):
sums[key] = sums.get(key, 0) + value
counts[key] = counts.get(key, 0) + 1
return {k: round(sums[k] / counts[k], 1) for k in sums}
def summarize(results, total=None):
"""results: measured calls (no warmup). Failures are counted, not timed."""
ok = [r for r in results if r["error_kind"] is None]
times = [r["seconds"] for r in ok]
rest = [r["seconds"] for r in ok if r.get("note") != "first call"]
by_kind = {k: sum(1 for r in results if r["error_kind"] == k) for k in KINDS}
return {
"done": len(results),
"total": total if total is not None else len(results),
"ok": len(ok),
"failures": len(results) - len(ok),
"by_kind": by_kind,
"p50": percentile(times, 0.5),
"p95": percentile(times, 0.95),
"max": max(times) if times else None,
"p50_excl_first": percentile(rest, 0.5) if len(rest) < len(times) else None,
"timings": average_timings(ok),
}
def fmt(seconds):
return "-" if seconds is None else f"{seconds:.3f}"
def summary_lines(s):
kinds = " ".join(f"{k}={v}" for k, v in s["by_kind"].items() if v)
lines = [
f"done {s['done']}/{s['total']} failures {s['failures']}" + (f" ({kinds})" if kinds else ""),
f"p50 {fmt(s['p50'])}s p95 {fmt(s['p95'])}s max {fmt(s['max'])}s"
+ (f" p50 excl. first {fmt(s['p50_excl_first'])}s" if s["p50_excl_first"] is not None else ""),
]
if s["timings"]:
lines.append("timings avg ms " + " ".join(f"{k} {v:g}" for k, v in s["timings"].items()))
return lines
def check(s, max_p95=None, max_failures=None):
"""Return (passed, line) for the given limits."""
problems = []
if max_p95 is not None and (s["p95"] is None or s["p95"] > max_p95):
problems.append(f"p95 {fmt(s['p95'])} > {max_p95}")
if max_failures is not None and s["failures"] > max_failures:
problems.append(f"failures {s['failures']} > {max_failures}")
return (not problems, "FAIL " + ", ".join(problems) if problems else "PASS")
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more