20 KiB
Lab 03 · 빠진 입력, 꺼진 Agent, 어려운 테스트
- 관련 강의: 3단계 · 검증과 장애 격리, 확장 고려
- 소요 시간: 약 80분 (예제 1 20분 · 예제 2 20분 · 과제 40분)
- 준비: Lab 02의
my-work/assistant.json(없으면cp reference/lab02-example2-router.json my-work/assistant.json후 push) - 목표: Multi-Agent에서 한 곳의 문제가 전체 답을 망치지 않게 만든다. 테스트로 문제를 드러내고, 고치고, 측정값으로 확장 계획을 쓴다.
flowchart LR
E1["예제 1<br/>빠진 입력"] --> E2["예제 2<br/>Agent가 꺼졌을 때"] --> T["과제<br/>어려운 테스트 · 확장 계획"]
복사해 붙일 요청문은 없습니다. 이번 Lab은 실패를 먼저 보고 요청문을 씁니다.
이 Lab을 읽는 법
예제마다 같은 다섯 걸음으로 갑니다.
flowchart LR
A["① 요청<br/>무엇을 보내나"] --> B["② call<br/>지금 결과"] --> C["③ 원인<br/>Agent를 직접 불러 비교"] --> D["④ 나와야 할 결과<br/>테스트의 기대"] --> E["⑤ 고치고<br/>다시 call · test"]
- 명령은 모두
~/aio-01d4-stu/Last3Mon/68에서 실행합니다. uv run tools/flow.py call <웹훅 경로> <요청>은http://localhost:5678/webhook/<경로>로 POST를 보내고, 첫 줄에HTTP 코드 · 걸린 시간, 그 아래 응답 JSON을 보여 줍니다.<요청>은requests/파일이거나'{"message": "..."}'같은 JSON 글자입니다.- 웹훅 경로: 라우터는
my-assistant(Lab 02에서 만든my-work/assistant.json), Agent 둘은it-agent·meeting-agent(agents/, 준비 때 push). - 아래 "지금 결과"와 "나와야 할 결과"는 수업 준비 때 실제로 돌린 값입니다. LLM 답 문장과 시간은 조금씩 다를 수 있습니다.
예제 1 · 빠진 입력 (20분)
시나리오: 김민지 님이 회의록을 붙이지 않고 "이번 회의록 할 일 정리 부탁해요"만 보냈습니다. 새 시나리오를 만들지 않고 이 요청 하나로 시작합니다.
| 조건 | 값 |
|---|---|
| 고칠 파일 | my-work/assistant.json (Lab 02 예제 2의 라우터, 새 파일을 만들지 않음) |
| 문제 요청 | requests/meeting-no-notes.json |
| 테스트 | cases/lab03.json (이미 있음, 고치지 않음) · cases/lab02.json도 그대로 통과 |
① 보내는 요청 · requests/meeting-no-notes.json
{"message": "이번 회의록 할 일 정리 부탁해요", "user": "김민지"}
회의 업무인데 회의록 원문 notes(그리고 title)가 없습니다. 비교: 회의록이 있는 requests/meeting-only.json에는 "title": "10월 16일 주간 회의", "notes": "참석: …"가 있습니다.
② 지금 결과 (Lab 02 라우터 그대로)
uv run tools/flow.py push my-work/assistant.json
uv run tools/flow.py call my-assistant requests/meeting-no-notes.json
HTTP 500 · 1.0s
{
"message": "Error in workflow"
}
답 대신 오류입니다. 직원은 무엇이 잘못됐는지 알 수 없습니다.
③ 원인 · 회의록 Agent를 라우터 없이 직접 불러 보기
uv run tools/flow.py call meeting-agent requests/meeting-no-notes.json
HTTP 400
{
"error": "notes are required"
}
회의록 Agent는 계약대로 동작했습니다 (notes가 없으면 400, AGENTS.md 3절 · Lab 02 예제 1에서 쓴 my-work/DESIGN.md 2절). 두 결과를 나란히 놓으면 흐름이 보입니다.
라우터 Split tasks → 업무 1개 (type: meeting) → Switch meeting 갈래 → Call meeting agent (notes 없이 부름)
Agent notes 없음 → 400 {"error": "notes are required"} ← 정상
라우터 Call meeting agent가 400을 받고 멈춤 → 워크플로우 전체 실패 → 500 {"message": "Error in workflow"}
n8n Executions에서 이 실행을 열면 Call meeting agent 노드가 빨갛습니다.
④ 나와야 할 결과 · cases/lab03.json 첫 케이스
HTTP 200 · 2.3s
{
"count": 1,
"results": [
{
"index": 1,
"type": "meeting",
"text": "이번 회의록 할 일 정리 부탁해요",
"agent": "meeting-agent",
"route": "입력 필요",
"answer": "회의록 정리를 하려면 회의록 내용(notes)을 함께 보내 주세요."
}
],
"reply": "1. 이번 회의록 할 일 정리 부탁해요\n → [입력 필요] 회의록 정리를 하려면 회의록 내용(notes)을 함께 보내 주세요."
}
| 테스트가 보는 곳 | 값 |
|---|---|
status |
200 |
count |
1 |
results.0.route |
입력 필요 |
reply |
"회의록"이 들어 있음 |
- 응답 모양(
count,results[]의index · type · text · agent · route · answer,reply)은 Lab 02와 같습니다. 칸을 바꾸면lab02가 깨집니다. - 회의록 Agent는 부르지 않습니다 (부르면 또 400).
⑤ 고치고 다시 확인
uv run tools/flow.py push my-work/assistant.json
uv run tools/flow.py call my-assistant requests/meeting-no-notes.json # ④와 같은 모양인가
uv run tools/flow.py test cases/lab03.json # 고치기 전 4/5 → 5/5
uv run tools/flow.py test cases/lab02.json # 6/6 그대로
cases/lab03.json의 다른 케이스 (고치기 전에도 통과, 고친 뒤에도 통과해야 함):
| 케이스 | 요청 | 기대 |
|---|---|---|
| IT 둘 · 둘 다 답 | requests/two-it.json |
count: 2, 두 업무 모두 자동 답변 |
| 복합 요청 · 오류 없음 | requests/composite.json |
count: 3, 어느 결과에도 오류 없음 |
| 기타만 | requests/other-only.json |
results.0.route: 담당자 연결 |
| 빈 요청 | requests/empty-message.json |
HTTP 400 |
▶ 1단계 · 어디서 막을까
두 가지 방법이 있습니다: (가) Agent를 부르고 400이 오면 처리, (나) 부르기 전에 빠진 입력을 보고 Agent를 부르지 않음. ③에서 본 것처럼 400은 Agent가 "이 요청은 처리할 수 없다"고 한 것입니다. 부르기 전에 이미 알 수 있는 일인가요? Lab 01 과제에서 만든 missing이 있다면 어느 쪽이 쉬운가요?
▶ 2단계 · 어느 노드를 고치나
| 노드 | 생각할 것 |
|---|---|
Parse tasks (Code) |
업무마다 빠진 입력(missing)을 코드로 붙일 수 있나요? 회의 업무인데 웹훅 본문의 notes가 비었으면? (LLM에게 묻지 않음) |
Route by type (Switch) |
빠진 입력이 있는 업무가 meeting 갈래로 가지 않게 하려면? 그 업무가 갈 새 갈래가 필요합니다. Switch는 위에서부터 처음 맞는 갈래 하나로만 갑니다 |
| 새 결과 노드 (Set) | Agent를 부르지 않고 route: 입력 필요와 답 문장을 만들어 Collect results(Merge)로. Merge의 입력 수도 하나 늘어납니다 |
▶ 3단계 · 요청문 점검
- ②의
call결과(500)와 ③의 결과(400)를 그대로 붙였다 - 원인 설명을 먼저 요구했다
- ④의
route값과 답 문장을 정했다 - 응답 모양은 그대로,
cases/lab02.json도 계속 통과해야 한다고 적었다
예제 2 · Agent가 꺼졌을 때 (20분)
시나리오: 회의록 Agent가 고장 나 응답하지 않는 날, 박서연 님이 복합 요청을 보냈습니다. 새 시나리오를 만들지 않습니다.
| 조건 | 값 |
|---|---|
| 고칠 파일 | 예제 1을 고친 my-work/assistant.json |
| 문제 요청 | requests/composite.json (회의록 Agent를 off로 꺼 둔 상태) |
| 테스트 | cases/lab03-down.json (이미 있음) · 다시 켠 뒤 cases/lab02.json, cases/lab03.json |
① 보내는 요청 · requests/composite.json
{
"message": "T-1002 처리 상태 알려주고, 첨부한 회의록에서 할 일도 정리해줘. 그리고 프린터 출력이 안 돼요.",
"user": "박서연",
"title": "10월 16일 주간 회의",
"notes": "참석: 김민지, 이준호, 박서연. 1) 신규 고객 문의 응답 템플릿을 이준호가 금요일까지 정리한다. 2) … 다음 회의는 10월 23일."
}
업무 셋(IT · 회의록 · IT), 입력은 모두 있습니다. 회의록 Agent가 켜져 있으면 Lab 02에서 이미 통과한 요청입니다.
② 지금 결과 (회의록 Agent를 끄고)
uv run tools/flow.py off agents/meeting-agent.json
uv run tools/flow.py call my-assistant requests/composite.json
HTTP 500 · 4.3s
{
"message": "Error in workflow"
}
먼저 생각해 보기: 4.3초 동안 IT 업무 둘(T-1002, 프린터)은 이미 답을 받았습니다. 그런데 응답에 T-1002 상태가 있나요? 회의록 Agent 하나가 꺼졌는데 왜 IT 답까지 사라졌을까요?
③ 원인 · 꺼진 Agent를 직접 불러 보기
uv run tools/flow.py call meeting-agent requests/agent-meeting.json
HTTP 404
(웹훅이 게시되어 있지 않다는 n8n의 응답)
라우터 Call IT agent → 200 두 번 (T-1002, 프린터 답을 받음)
Call meeting agent → 404 (꺼진 Agent) → 노드가 멈춤
→ 워크플로우 전체 실패 → 500. 받아 둔 IT 답까지 버림
cases/lab03-down.json을 돌리면 "회의록만"도 500, "IT만"은 회의록 Agent를 부르지 않으니 200입니다 (고치기 전 1/3).
④ 나와야 할 결과 · cases/lab03-down.json 첫 케이스
HTTP 200 · 4.6s
{
"count": 3,
"results": [
{"index": 1, "type": "it", "text": "T-1002 처리 상태 알려줘", "agent": "it-agent",
"route": "자동 답변", "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, VPN 인증서 발급 승인 대기 중입니다. …"},
{"index": 2, "type": "meeting", "text": "첨부한 회의록에서 할 일 정리", "agent": "meeting-agent",
"route": "담당자 연결", "answer": "담당 Agent가 응답하지 않아 담당자에게 전달했습니다.", "actions": []},
{"index": 3, "type": "it", "text": "프린터 출력이 안 돼요", "agent": "it-agent",
"route": "자동 답변", "answer": "프린터 전원과 네트워크 연결을 확인하고, …"}
],
"reply": "1. T-1002 처리 상태 알려줘\n → [자동 답변] …\n2. 첨부한 회의록에서 할 일 정리\n → [담당자 연결] 담당 Agent가 응답하지 않아 담당자에게 전달했습니다.\n3. 프린터 출력이 안 돼요\n → [자동 답변] …"
}
| 케이스 (회의록 Agent 꺼짐) | 요청 | 테스트가 보는 곳 |
|---|---|---|
| 복합 요청 | requests/composite.json |
HTTP 200, count: 3, results.0.route: 자동 답변 + 답에 "처리 중", results.1.route: 담당자 연결 + 답에 "응답하지 않아" |
| 회의록만 | requests/meeting-only.json |
HTTP 200, results.0.route: 담당자 연결 |
| IT만 | requests/it-only.json |
HTTP 200, results.0.route: 자동 답변 |
고장 난 업무 하나만 담당자 연결, 나머지는 그대로입니다.
⑤ 고치고 다시 확인 (그리고 꼭 다시 켜기)
uv run tools/flow.py push my-work/assistant.json
uv run tools/flow.py call my-assistant requests/composite.json # ④와 같은 모양인가
uv run tools/flow.py test cases/lab03-down.json # 고치기 전 1/3 → 3/3
uv run tools/flow.py push agents/meeting-agent.json # 회의록 Agent 다시 켜기
uv run tools/flow.py test cases/lab02.json # 6/6
uv run tools/flow.py test cases/lab03.json --repeat 3 # 예제 1이 그대로인지
▶ 1단계 · 오류를 "결과"로 바꾸기
Agent를 부르는 노드가 404 · 500을 받으면 n8n은 기본으로 워크플로우를 멈춥니다. 멈추지 않고 그 응답을 다음 노드로 넘기는 설정이 있습니다 (AGENTS.md 5절 HTTP Request). 넘겨받은 404 응답에는 route도 answer도 없습니다.
▶ 2단계 · 어느 노드를 고치나
| 노드 | 생각할 것 |
|---|---|
Call IT agent, Call meeting agent (HTTP Request) |
위 1단계의 설정. 회의록만이 아니라 둘 다 (IT Agent도 고장 날 수 있음) |
IT result, Meeting result (Set) |
응답에 route가 있으면 그대로, 없으면 무엇으로? answer가 없으면 어떤 문장으로? (④의 값) |
정상 응답에는 route가 있고 404 응답에는 없다는 차이를 이용합니다.
▶ 3단계 · 얼마나 기다릴까
꺼진 Agent는 바로 404가 오지만, 느린 Agent는 응답이 안 올 수도 있습니다. 직원이 얼마나 기다려도 될지 정하고 요청문에 적습니다. (느린 Agent는 오늘 테스트로 만들지 않습니다. 값을 정해 넣는 것까지만 합니다.)
과제 · 어려운 테스트 쓰고 고치기, 확장 계획 (40분)
팀장이 요청했습니다.
"다음 주부터 전 직원이 씁니다. 지금 테스트는 우리가 고친 것만 확인하잖아요. 실제로 들어올 만한 어려운 요청으로 테스트를 더 만들고, 통과시키세요. 그리고 사람이 몰리면 어떻게 할지 숫자로 설명해 주세요."
cases/my-cases.json에 어려운 케이스 5개 이상을 직접 씁니다 (아래 예 둘 + 내가 찾은 셋 이상).- 지금 라우터로
call·test해서 실패를 보고, 에이전트에게 고치게 합니다 (테스트의 기대 결과는 바꾸지 않음). my-work/DESIGN.md5·6절을 채웁니다. 6절의 시간은 내runs/보고서의 값으로 씁니다.
케이스 파일 모양 · cases/my-cases.json
{
"webhook": "my-assistant",
"cases": [
{"name": "user 없이 IT 질문", "body": {"message": "VPN이 계속 끊겨요"},
"expect": {"equals": {"results.0.route": "입력 필요"}}},
{"name": "공백만 보냄", "body": {"message": " ", "user": "이준호"},
"expect": {"status": 400}}
]
}
request(파일) 대신body로 직접 씁니다. 형식은 AGENTS.md 8절,cases/lab03.json을 열어 따라 해도 됩니다.- 기대 결과는 업무 규칙 한 줄에서 나옵니다: "누가 물었는지 모르면 Agent를 부르지 않고
입력 필요", "빈 요청은 공백만 있어도 400".
예 A · user 없이 IT 질문
uv run tools/flow.py call my-assistant '{"message": "VPN이 계속 끊겨요"}'
| 결과 | |
|---|---|
| 지금 (예제 2까지 고친 판) ✘ | HTTP 200 · 2.3s, "route": "담당자 연결", "answer": "담당 Agent가 응답하지 않아 담당자에게 전달했습니다." |
| 나와야 할 결과 ✔ | HTTP 200, "route": "입력 필요", "answer": "누가 요청했는지(user)를 함께 보내 주세요." |
먼저 생각해 보기: IT Agent는 정말 "응답하지 않았"나요? IT Agent를 직접 불러 봅니다.
uv run tools/flow.py call it-agent '{"question": "VPN이 계속 끊겨요"}' # 계약: question · user 둘 다 필수
IT Agent는 계약대로 400을 돌려줍니다. 예제 2에서 만든 "응답이 없으면 담당자 연결"이 이 400까지 장애로 처리해 틀린 말("응답하지 않아")을 합니다. 테스트가 없었다면 몰랐을 구멍입니다.
예 B · 공백만 보냄
uv run tools/flow.py call my-assistant '{"message": " ", "user": "이준호"}'
| 결과 | |
|---|---|
| 지금 ✘ | HTTP 200 · 0.7s, 응답 본문이 비어 있음 |
| 나와야 할 결과 ✔ | HTTP 400, {"error": "message is required"} (requests/empty-message.json과 같음) |
먼저 생각해 보기: 공백 세 칸은 "비어 있다"인가요? 0.7초는 무엇을 하느라 걸린 시간일까요 (빈 요청 requests/empty-message.json은 0.1초도 안 걸림)?
고치고 다시 확인
uv run tools/flow.py push my-work/assistant.json
uv run tools/flow.py test cases/my-cases.json --repeat 3
uv run tools/flow.py test cases/lab03.json
uv run tools/flow.py test cases/lab02.json
▶ 1단계 · 어디서 어려운 요청이 나오나 (나머지 셋 이상)
다섯 곳에서 찾아봅니다: ① 입력 칸이 비거나 이상함 ② 업무 수가 많거나 섞임 ③ 67일차에 지킨 규칙(남의 접수 건)이 라우터를 거쳐도 지켜지나 ④ 붙어 온 자료와 요청이 안 맞음 ⑤ 다른 언어 · 다른 표기. 각 케이스의 기대 결과를 업무 규칙 한 줄로 먼저 적고 테스트로 옮깁니다. 기대가 맞는지 모르겠으면 그 Agent를 직접 call 해서 계약(AGENTS.md 3절)과 비교합니다.
▶ 2단계 · 예 A · B는 어느 노드를 고치나
| 예 | 생각할 것 |
|---|---|
| A | 예제 1에서 missing을 보고 갈래를 나눈 것은 meeting뿐이었나요? it 업무도 같은 검사를 거치게 하려면 Switch의 어느 조건을? 답 문장은 무엇이 빠졌는지에 따라 달라야 합니다 (notes면 회의록, user면 요청한 사람) |
| B | 웹훅 바로 뒤 Has message?가 무엇을 "비어 있음"으로 보나요? 앞뒤 공백을 지운 뒤 보게 하면 LLM도 부르지 않습니다 |
▶ 3단계 · 좋은 케이스의 조건
- 기대 결과가 하나로 정해진다 (사람마다 다르게 판단하면 테스트가 아님)
- 통과해도 의미가 있다: 지금 통과하는 케이스도 "고칠 때 깨지지 않게" 남겨 둔다
not_contains로 나오면 안 되는 것(남의 이름, 오류)을 확인한다- 테스트가 실패하면 먼저 "테스트가 틀렸나, 워크플로우가 틀렸나"를 계약 표로 판단한다
▶ 4단계 · 확장 계획 생각하기
보고서에서 IT 하나, 회의록 하나, 복합 요청의 시간을 찾습니다. 복합 요청 시간은 각 업무 시간의 합에 가까운가요, 가장 긴 것에 가까운가요? 두 가지를 같이 생각합니다: ① 단독 요청 시간에는 나누기 LLM(약 1초) 이 이미 들어 있다 ② 같은 Agent로 가는 업무들은 HTTP 노드 하나가 동시에 보내고, 다른 Agent 갈래는 차례로 실행된다. 21명이 동시에 보내면 n8n 하나가 몇 개를 동시에 처리해야 하나요? 3단계 교안 5절의 수직 · 수평 확장 표를 보고 내 측정값에 맞는 첫 조치를 고릅니다.
한눈에
| 단계 | 요청 | 지금 | 원인 (직접 call로 확인) | 나와야 할 결과 |
|---|---|---|---|---|
| 예제 1 | 회의록 없는 회의 | 500 | 회의록 Agent의 400에 라우터가 멈춤 | 200, 입력 필요 |
| 예제 2 | 회의록 Agent 꺼짐 + 복합 | 500, IT 답도 사라짐 | 404에 라우터가 멈춤 | 200, 회의록만 담당자 연결 |
| 과제 예 A | user 없는 IT | 200 담당자 연결 (틀린 말) |
IT Agent의 400을 장애로 처리 | 200, 입력 필요 (user) |
| 과제 예 B | 공백만 | 빈 200 | 공백도 "비어 있지 않음" | 400 |
막혔을 때
| 증상 | 할 일 |
|---|---|
call meeting-agent가 처음부터 404 |
회의록 Agent가 꺼져 있음: uv run tools/flow.py push agents/meeting-agent.json |
off 후 다른 테스트도 이상함 |
위와 같이 다시 켭니다 |
고쳤는데 lab02가 깨짐 |
결과 모양(계약)이 바뀌었는지 확인: results.N의 필드 이름 |
| 계속 안 됨 | cp reference/lab03-example2-isolated.json my-work/assistant.json 후 push로 과제를 이어 갑니다 |
확인
- 예제 1:
call결과가 500 → ④ 모양,cases/lab03.json5/5 (빠진 입력은 Agent를 부르지 않고입력 필요). - 예제 2: 회의록 Agent가 꺼져도
lab03-down3/3, 다시 켠 뒤lab02·lab03통과. - 과제:
cases/my-cases.json5개 이상(예 A · B 포함) 3번 반복 통과,DESIGN.md5 · 6절에 측정값. - 테스트 보고서를
my-work/TEST-REPORT.md로 채웠다.