# Lab 03 · 브리핑과 평가 · 먼저 볼 것을 정하고, 도우미를 시험하기 - 관련 강의: [3단계 · 브리핑과 평가](../../Lecture/79/03-STAGE3-OPS-BRIEF.md) - 소요 시간: 50분 (6교시 한 시간) · 예제 1 14분 · 예제 2 8분 · 예제 3 6분 · 과제 1 12분 · 과제 2 10분 - 준비: Lab 01의 `my-ops-facts`(과제까지) · Lab 02의 `my-ops-ask`(과제까지)가 게시돼 있음 - 목표: 사실 · 상태로 아침 **브리핑**(영역 표 · 먼저 볼 것 셋)을 규칙으로 만들고, LLM 세 문장 요약을 붙여 숫자를 확인하고, 사람이 일을 하나씩 처리할 때 브리핑이 어떻게 바뀌는지 재 보고, **골든 질문**으로 도우미를 평가하고, 평가 점수가 기준 아래면 알리는 **평가 관문**을 만든다. ```mermaid flowchart LR E1["예제 1
브리핑"] --> E2["예제 2
LLM 요약"] --> E3["예제 3
아침이 바뀌면"] --> T1["과제 1
골든 질문 평가"] --> T2["과제 2
평가 관문"] ``` > 복사해 붙일 요청문은 없습니다. 막히면 `▶` 를 하나씩 펼칩니다. 연습이 끝나면 `uv run tools/flow.py morning --reset`. ## 예제 1 · 브리핑 (14분) ### 이 실습의 의도 **한 줄로:** 운영 사실(상태 포함)을 다시 불러 **아침 브리핑**(`my-ops-brief`)을 만듭니다: 전체 · 영역 표(🔴 · 🟡 · 🟢와 숫자) · "먼저 볼 것" 셋과 정해진 할 일. LLM은 쓰지 않습니다. > **생활 속 예 · 인수인계지 한 장.** 아침 인수인계는 환자마다 차트를 다 읽지 않고 한 장으로 끝냅니다: 병동 전체 상태, 환자별 신호등, 그리고 "오늘 먼저 볼 환자 셋 · 할 일". 할 일은 그때그때 지어내지 않고 병원의 지침(런북)에 적힌 대로 씁니다. 예제 1은 그 한 장을 만드는 실습입니다. **상황.** 운영 담당자는 08:30에 숫자 서른 개가 아니라 **무엇부터 볼지**를 알고 싶습니다. 사실과 상태는 Lab 01에서 정했습니다. 브리핑은 그것을 정해진 모양으로 줄 세우기만 합니다. | 칸 (AGENTS.md 3절 "브리핑") | 정의 | | --- | --- | | `actions` | `red` 다음 `yellow`의 앞 **3개**, 영역마다 정해진 할 일 (예: DLQ → "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)") | | `markdown` | 제목 · 전체 · `\| 영역 \| 상태 \| 숫자 \|` 표 · "## 먼저 볼 것" | | 상태 | 사실의 `health` 그대로 (다시 판정하지 않음) | **순서.** 테스트 먼저(404) → 요청문 → push → 4/4 → `call`로 브리핑 읽기. **끝나면 알게 되는 것.** 브리핑은 **판정이 아니라 배열**이라는 것 (판정은 사실 한 곳에서). 할 일을 규칙으로 쓰는 까닭 (77일차 주간 보고의 "다음 할 일"과 같음). **적을 곳.** `my-work/OPS.md` 7절. ```bash uv run tools/flow.py test cases/lab03.json -v # 고치기 전 (my-ops-brief가 아직 없음) ``` ### 이해하기 ```mermaid flowchart LR W(["from · to"]) --> F["Get facts
my-ops-facts (상태 포함)"] --> B["Make brief (Code)
표 · 먼저 볼 것 셋 · 할 일"] --> O(["overall · red · yellow · actions · markdown"]) ``` **판정은 한 곳에.** 브리핑이 상태를 다시 판정하면(예: 비용 80%를 브리핑에서 따로 셈), Lab 01의 상태와 어긋나는 날이 옵니다. 브리핑은 `health` · `red` · `yellow`를 **그대로** 읽어 줄 세웁니다. **할 일은 런북에서.** 영역마다 할 일 한 줄은 이미 정해져 있습니다 (72~78일차에 만든 것들). LLM에게 "무엇을 하면 좋을까"를 물으면 그럴듯하지만 날마다 다르고, 런북에 없는 일을 권할 수 있습니다. **테스트 케이스.** | 케이스 | 기대 | | --- | --- | | 전체 · 먼저 볼 것 셋 | 🔴, 빨강 runs · dlq · release → 먼저 볼 것도 그 순서 | | 표에 숫자 | `\| DLQ \| 🔴 \| 3건 (parked 2) \|` | | 할 일은 규칙에서 | "카나리 끄기 · 되돌리기 검토 (74일차)", DLQ 할 일에 parked | | DLQ를 비운 뒤 | 테스트가 DLQ를 모두 비움(`dlq-clear`) → 빨강 runs · release, 셋째로 alerts | **기대 결과.** 고치기 전 0/4 (404), 고친 뒤 4/4.
▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대 요청문에 꼭 들어갈 것: **① 상태의 출처 ② 먼저 볼 것의 수 ③ 마크다운 제목 ④ LLM**. 아래 빈칸 4개를 채웁니다. ```text my-work/ops-brief.json 을 새로 만들어 줘. 웹훅 POST my-ops-brief, 이름 "79 Ops brief (my)". Get facts: HTTP POST http://localhost:5678/webhook/my-ops-facts (본문 from · to 그대로). 상태도 사실에서 ____① 쓴다 (다시 판정하지 않음). Make brief(Code): actions = red 다음 yellow 의 앞 ____②개, 영역마다 AGENTS.md 3절 표의 할 일 글자 그대로. markdown: 제목 · 전체 · | 영역 | 상태 | 숫자 | 표 (🔴 · 🟡 · 🟢, 숫자 칸은 3절 표의 꼴) · "## ____③". LLM 은 ____④. 결과 {overall, red, yellow, actions, markdown}. 기대 결과 cases/lab03.json. ``` 빈칸 힌트: - ① 판정은 어디서 한 번만 하나요? - ② AGENTS.md 3절 "브리핑" 첫 줄. - ③ 테스트 · 예제 2가 찾는 절 제목. - ④ 예제 1에서 LLM을 쓰나요?
```bash uv run tools/flow.py push my-work/ops-brief.json uv run tools/flow.py test cases/lab03.json -v uv run tools/flow.py call my-ops-brief '{}' ``` ### 실행 결과 · 브리핑 수업 준비 때(11/01) 강사 노트북에서 정답 워크플로우로 실제로 나온 출력입니다. 브리핑은 규칙으로 만들어 누구에게나 같습니다. #### 고치기 전 · `my-ops-brief`가 없을 때 실행한 명령: ```bash uv run tools/flow.py test cases/lab03.json -v ``` 출력 (결과 줄만): ```text ✘ 전체 · 먼저 볼 것 셋 · 0.0s · status 404 (expected 200); overall = null (expected red); red = null (expected ["runs", "dlq", "release"]); … ✘ 표에 숫자 · 0.0s · status 404 (expected 200); markdown has none of ['| DLQ | 🔴 | 3건 (parked 2) |'] ✘ 할 일은 규칙에서 · 0.0s · status 404 (expected 200); markdown has none of ['카나리 끄기 · 되돌리기 검토 (74일차)']; actions.1.action has none of ['parked'] ✘ DLQ를 비운 뒤 · 0.0s · status 404 (expected 200); red = null (expected ["runs", "release"]); actions.2.area = null (expected alerts) Passed 0/4 ``` **왜 이렇게 나왔나 (고치기 전):** 웹훅이 아직 없어 404입니다. 0/4는 예상한 결과. #### 고친 뒤 실행한 명령: ```bash uv run tools/flow.py push my-work/ops-brief.json uv run tools/flow.py test cases/lab03.json -v uv run tools/flow.py call my-ops-brief '{}' ``` 출력 (결과 줄과 `call`의 `markdown`): ```text ✔ 전체 · 먼저 볼 것 셋 · 1.1s ✔ 표에 숫자 · 0.2s ✔ 할 일은 규칙에서 · 0.1s ✔ DLQ를 비운 뒤 · 0.1s Passed 4/4 # 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태) 전체: 🔴 red | 영역 | 상태 | 숫자 | | --- | --- | --- | | 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% | | 알림 | 🟡 | 6건 (높음 3) | | DLQ | 🔴 | 3건 (parked 2) | | 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 | | 비용 | 🟡 | $0.073604 / $0.08 (92.01%) | | 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) | ## 먼저 볼 것 1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차) 2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차) 3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차) ``` **고친 뒤 이렇게 나온 까닭** | 칸 | 어디서 | 어떻게 | | --- | --- | --- | | 전체 · 상태 | 사실의 `overall` · `health` | 그대로 (다시 판정하지 않음) | | 표의 숫자 | 사실 | AGENTS.md 3절의 꼴 (비율은 ×100 · 두 자리) | | 먼저 볼 것 | `red` 다음 `yellow`의 앞 3개 | 빨강이 셋이라 셋 다 빨강 | | 할 일 | 영역마다 정해진 한 줄 | 72~78일차 런북 | - **DLQ를 비운 뒤:** 테스트가 DLQ를 모두 비우면(`dlq-clear`) 빨강은 runs · release, 셋째 자리에 노랑의 첫째인 alerts가 들어옵니다. 브리핑을 고치지 않아도 사실이 바뀌면 순서가 바뀝니다 (예제 3). - **0.1~1초:** 첫 호출의 1.1초는 n8n이 막 다시 켜진 뒤라서입니다. LLM은 없습니다. ## 예제 2 · LLM 요약 (8분) ### 이 실습의 의도 **한 줄로:** 브리핑 끝에 LLM이 쓴 **세 문장 요약**(전체 · 가장 급한 곳 · 먼저 할 일)을 붙이고, 요약의 숫자를 Code가 확인합니다. 표 · 먼저 볼 것은 Code가 만든 그대로입니다. > **생활 속 예 · 수간호사의 세 마디.** 인수인계지 한 장을 다 읽을 시간이 없는 과장님께 수간호사가 세 마디로 말합니다: "병동은 위험 단계, 가장 급한 건 3호실 감염, 먼저 격리부터." 그 세 마디에 인수인계지에 없는 숫자가 들어가면 안 됩니다. 예제 2는 세 마디(요약)를 붙이고 숫자를 대조하는 실습입니다. | 노드 | 하는 일 | | --- | --- | | `Summarize` (AI Agent + `gpt-4.1-mini`, temperature 0) | 사실 · `actions`만 받아 한국어 세 문장 | | `Check summary` (Code) | 요약의 숫자가 사실 안에 있나 → `numbers_ok` (Lab 02 예제 2와 같은 방법) | | markdown | 끝에 "## 요약" + 요약 글 (표 · 먼저 볼 것은 그대로) | **끝나면 알게 되는 것.** LLM이 하는 일(말로 줄이기)과 하지 않는 일(판정 · 배열 · 숫자)의 경계. 요약은 실행마다 글이 달라도 숫자 확인으로 묶어 둘 수 있다는 것. **적을 곳.** `my-work/OPS.md` 7절 아래 "LLM 요약이 한 것 · 하지 않은 것". ```bash uv run tools/flow.py test cases/lab03-llm.json -v # 고치기 전 (예제 1의 브리핑) ``` ### 이해하기 **글은 LLM, 숫자는 Code.** 77일차 Lab 02 과제 2 · 오늘 Lab 02 예제 2와 같은 원칙입니다. 요약 글은 실행마다 조금씩 다릅니다 (그래서 테스트는 글 전체가 아니라 `numbers_ok`와 "급한 곳의 낱말 하나"를 봅니다). **기대 결과.** 고치기 전 0/2 (`summary` · `## 요약` 없음), 고친 뒤 2/2, `lab03` 4/4 그대로. 브리핑이 0.1초 → 1~3.5초 (LLM 한 번).
▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대 요청문에 꼭 들어갈 것: **① 몇 문장 ② 확인 칸 ③ 마크다운 절 ④ 표는 누가**. 아래 빈칸 4개를 채웁니다. ```text my-work/ops-brief.json 에 LLM 요약을 더해 줘. Summarize(AI Agent + gpt-4.1-mini, temperature 0): 사실 JSON 과 actions 만 주고 한국어 ____①문장 — 전체 상태 · 가장 급한 곳 · 먼저 할 일. 사실에 없는 숫자 · 원인은 쓰지 않는다. Check summary(Code): 요약의 숫자가 사실 안에 있는지 Lab 02 예제 2 와 같은 방법으로 보고 ____②. markdown 끝에 "## ____③" 과 요약 글. 표 · 먼저 볼 것은 Code 가 만든 그대로 (LLM 이 ____④). 기대 결과 cases/lab03-llm.json, cases/lab03.json 도. ``` 빈칸 힌트: - ① 수간호사의 몇 마디? - ② Lab 02 예제 2와 같은 칸 이름. - ③ 테스트가 찾는 절 제목. - ④ 표와 먼저 볼 것은 LLM이 손대나요?
```bash uv run tools/flow.py push my-work/ops-brief.json uv run tools/flow.py test cases/lab03-llm.json -v uv run tools/flow.py test cases/lab03.json ``` ### 실행 결과 · LLM 요약 수업 준비 때(11/01) 강사 노트북에서 실제로 나온 출력입니다. 요약 글은 실행마다 조금씩 다릅니다. #### 고치기 전 · 예제 1의 브리핑 실행한 명령: ```bash uv run tools/flow.py test cases/lab03-llm.json -v ``` 출력 (결과 줄만): ```text ✘ 요약 세 문장 · 숫자 확인 · 0.1s · numbers_ok = null (expected true); markdown has none of ['## 요약'] ✘ 요약에 급한 곳 · 0.1s · summary has none of ['DLQ', '릴리스', '카나리', '실행', '오류'] Passed 0/2 ``` **왜 이렇게 나왔나 (고치기 전):** 예제 1의 브리핑에는 `summary` · `numbers_ok` · "## 요약"이 없습니다. 0/2는 예상한 결과. #### 고친 뒤 실행한 명령: ```bash uv run tools/flow.py push my-work/ops-brief.json uv run tools/flow.py test cases/lab03-llm.json -v uv run tools/flow.py test cases/lab03.json ``` 출력 (결과 줄과 두 번의 요약): ```text ✔ 요약 세 문장 · 숫자 확인 · 3.5s ✔ 요약에 급한 곳 · 1.1s Passed 2/2 요약 1: "전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며 DLQ 크기는 3입니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생하는 1000ms 타임아웃 오류로 16건이 보고되었습니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다." 요약 2: "전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며, 응답하지 않은 비율은 4.76%입니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다." (lab03) Passed 4/4 ``` **고친 뒤 이렇게 나온 까닭** | | 예제 1 | 예제 2 | | --- | --- | --- | | 표 · 먼저 볼 것 | Code | Code (그대로) | | 요약 | 없음 | LLM 세 문장, `numbers_ok: true` | | 시간 | 0.1초 | 1~3.5초 | - **같은 브리핑, 다른 요약 글:** 두 번 부른 요약이 다릅니다 (둘째 문장 · 숫자 고름). 둘 다 숫자 확인을 통과했고, 둘 다 "가장 급한 것 = 실행(FAQ timeout)"이라고 했습니다. - **요약은 먼저 볼 것 셋 중 하나만 말했습니다.** DLQ parked · 카나리는 세 문장에 들어가지 못했습니다. 그래서 표 · 먼저 볼 것은 LLM에게 맡기지 않고 Code가 만든 그대로 둡니다 — 요약은 **덧붙이는 말**이지 브리핑 자체가 아닙니다. - `lab03` 4/4 그대로 (표 · 순서는 바뀌지 않음). ## 예제 3 · 아침이 바뀌면 (6분, 에이전트 없이) ### 이 실습의 의도 **한 줄로:** 사람이 일을 하나씩 처리할 때(parked 둘 처리 → 카나리 끄기 → 예산 올리기) 같은 브리핑을 다시 불러, **빨강 · 먼저 볼 것이 어떻게 바뀌는지** 봅니다. 워크플로우는 고치지 않습니다. > **생활 속 예 · 오전 회진 뒤의 인수인계지.** 아침에 "먼저 볼 환자 셋"을 받은 간호사가 한 명씩 처치하면, 점심 인수인계지의 순서가 바뀝니다. 처치가 끝난 환자는 빨강에서 노랑으로, 넷째였던 환자가 셋째로 올라옵니다. 예제 3은 처치할 때마다 인수인계지를 다시 뽑아 보는 실습입니다. **상황.** 운영 담당자가 브리핑대로 일합니다. 도우미는 읽기만 하므로, "처리"는 테스트가 사람 대신 합니다(케이스의 `before`: `/dlq/update` · `/releases/helpdesk` · `/budget`). 케이스는 **이어서** 갑니다 (앞 케이스의 처리가 남음). `cases/lab03-after.json`을 예제 2의 브리핑에 돌립니다. | 사람이 한 일 (차례로) | 기대 | | --- | --- | | (처음) | 빨강 runs · dlq · release | | parked 둘(#1 · #2) 처리 | DLQ 🟡 `1건 (parked 0)`, 빨강 runs · release, 먼저 볼 것 셋째에 alerts | | 카나리 끄기 | 릴리스 🟢 `1.1.0`, 빨강 runs 하나, 먼저 볼 것 runs · alerts · dlq | | 예산 올리기 (0.08 → 0.2달러) | 비용 🟢, 노랑 alerts · dlq · quality | | 남은 빨강 | 실행 하나 — 고칠 사람이 있는 일 (77일차) | **끝나면 알게 되는 것.** 브리핑은 한 번 보고 끝나는 보고서가 아니라 **할 일 목록**이라는 것. 그리고 "카나리 끄기 · 예산 올리기"는 빨강을 지우지만 **원인을 고친 것은 아닐 수 있다**는 것. **적을 곳.** `my-work/OPS.md` 8절 표. ```bash uv run tools/flow.py test cases/lab03-after.json -v uv run tools/flow.py morning # 처리 뒤의 아침 (끝나면 morning --reset) ``` ### 이해하기 **빨강을 지우는 일 · 고치는 일.** | 사람이 한 일 | 빨강이 사라진 까닭 | 고친 것인가 | | --- | --- | --- | | parked 둘 처리 (재처리 · 닫기) | parked 0 | 그렇다 — 일을 끝냄 (DLQ의 대기 1건은 남음) | | 카나리 끄기 | 카나리가 없으니 비교할 것이 없음 | 위험을 멈춤 — 1.2.0의 다시 묻는 문제는 그대로 (고쳐서 다시 내보내야) | | 예산 올리기 | 비율 0.9201 → 0.368 | 한도를 바꿈 — 쓰는 양은 그대로 (75일차: 1위 사용자 확인이 먼저) | | (남은 실행) | — | FAQ timeout을 고쳐야 (77일차 Lab 03) | 브리핑의 숫자는 정의대로 맞지만, 숫자가 초록이 된 **까닭**을 함께 말해야 합니다. 77일차 SLO에서 "기준을 낮춰 지킴으로 만들지 않는다"와 같은 이야기입니다. **기대 결과.** 5/5. 끝나면 `morning --reset`으로 아침을 되돌립니다. ### 실행 결과 · 아침이 바뀌면 수업 준비 때(11/01) 강사 노트북에서 실제로 나온 출력입니다 (예제 2의 브리핑, 고치지 않음). 케이스가 이어서 가므로 앞 케이스의 처리가 남습니다. 실행한 명령: ```bash uv run tools/flow.py test cases/lab03-after.json -v ``` 출력 (결과 줄과 케이스마다 `red` · `yellow` · 먼저 볼 것): ```text ✔ 아침 그대로 · 1.4s ✔ parked 둘을 처리하면 → DLQ는 노랑, 먼저 볼 것에 알림 · 1.0s ✔ 카나리를 끄면 → 릴리스는 초록 · 1.1s ✔ 예산을 올리면 → 비용도 초록 · 1.1s ✔ 남은 빨강은 실행 하나 (사람이 고칠 일) · 1.2s Passed 5/5 아침 그대로 red [runs, dlq, release] · yellow [alerts, quality, cost] → 실행 · DLQ · 릴리스 parked 둘 처리 red [runs, release] · yellow [alerts, dlq, quality, cost] → 실행 · 릴리스 · 알림 (표: | DLQ | 🟡 | 1건 (parked 0) |) 카나리 끔 red [runs] · yellow [alerts, dlq, quality, cost] → 실행 · 알림 · DLQ (표: | 릴리스 | 🟢 | 1.1.0 |) 예산 올림 red [runs] · yellow [alerts, dlq, quality] → 실행 · 알림 · DLQ 남은 빨강 overall red · 먼저 볼 것 1번 실행 ``` **왜 이렇게 나왔나 (사람이 한 일마다)** | 사람이 한 일 | 바뀐 것 | 고친 것인가 | | --- | --- | --- | | parked #1 · #2 처리 | DLQ 🔴 → 🟡 (대기 1건 남음), 알림이 먼저 볼 것에 | 그렇다 — 일을 끝냄 | | 카나리 끔 | 릴리스 🔴 → 🟢 | 위험을 멈춤 — 1.2.0의 다시 묻는 문제(8건 중 5건)는 그대로 | | 예산 0.08 → 0.2 | 비용 🟡 → 🟢 (0.9201 → 0.368) | 한도를 바꿈 — 쓴 돈은 그대로 | | (아무도 안 함) | 실행 🔴 그대로 | FAQ timeout을 고쳐야 (77일차 Lab 03) | - 브리핑은 고치지 않았습니다. 사실이 바뀌니 표 · 순서 · 할 일이 따라 바뀌었습니다 — 브리핑은 **할 일 목록**입니다. - 빨강이 지워진 까닭이 셋 다 다릅니다. 회의에서 "릴리스는 초록입니다"라고만 말하면 1.2.0이 고쳐진 줄 압니다. 숫자와 함께 **무엇을 했는지**를 말합니다. - 요약 글(LLM)은 다섯 번 모두 "가장 급한 것은 FAQ timeout"이라고 했습니다 — 남은 빨강과 같습니다. - 끝나면 `morning --reset`으로 아침을 되돌립니다 (다음 과제의 평가가 처음 상태를 씀). ## 과제 1 · 골든 질문 평가 (12분) ### 이 실습의 의도 **한 줄로:** 정답을 아는 질문 여덟 개(`requests/ops-golden.json`)로 도우미를 **시험**하는 평가 워크플로우(`my-ops-eval`)를 만듭니다: 영역 · 거절 · 숫자 · 근거가 모두 맞아야 통과, 점수와 틀린 케이스. > **생활 속 예 · 신규 간호사 시험 문제.** 새 간호사가 병동에 서기 전에 정답이 정해진 상황 문제를 풉니다: "이 환자에게 진통제 더 줘도 되나요?" — 정답은 "처방 확인". 문제를 바꾸지 않고 해마다 같은 문제로 보면, 교육이 나아졌는지 숫자로 압니다. 과제 1은 도우미의 시험지를 만드는 실습입니다. **상황.** 도우미를 팀에 열기 전에, 그리고 고칠 때마다 "전보다 나빠지지 않았나"를 확인해야 합니다 (72 · 74일차 회귀 · 배포 관문). 질문 여덟 개와 기대(영역 · 거절 · 답에 있어야 할 숫자)가 정답지입니다. | 정의 (AGENTS.md 3절 "평가") | | | --- | --- | | 케이스마다 | `my-ops-ask`를 부름 | | 통과 | `area` 같음 · `refused` 같음 · `numbers` 중 하나가 답에 · 거절이 아니면 `numbers_ok` — **넷 다** | | 결과 | `total` · `passed` · `score`(4자리) · 항목별 맞은 수 · `failed`(무엇이 틀렸나 · 답) | | 빈 목록 | 400 | **끝나면 알게 되는 것.** LLM 도우미를 **숫자로 시험**하는 법 (LLM 평가 · 골든 셋). 시험지도 틀릴 수 있다는 것 — 그래서 "틀린 기대를 잡아내는지"도 시험합니다. **적을 곳.** `my-work/OPS.md` 9절. ```bash uv run tools/flow.py test cases/lab03-challenge.json -v # 고치기 전 (my-ops-eval이 아직 없음) ``` ### 이해하기 ```mermaid flowchart LR W(["cases"]) --> E{"비었나?"} -->|예| X(["400"]) E -->|아니오| S["Split cases
아이템마다"] --> A["Ask
HTTP 하나가 아이템마다
my-ops-ask"] --> C["Score (Code)
넷 다 맞으면 통과"] --> O(["total · passed · score · failed"]) ``` **넷 다 맞아야.** 영역만 맞고 숫자가 틀리면, 숫자는 맞는데 근거(`numbers_ok`)가 없으면, 통과가 아닙니다. 그리고 거절해야 할 질문(작업 요청)은 **거절하면 통과**입니다 — 평가는 "답을 잘하나"만이 아니라 "하면 안 되는 것을 안 하나"도 봅니다. **시험지를 시험한다.** 둘째 케이스는 기대를 일부러 틀리게(`알림` 질문의 영역을 `cost`로) 적었습니다. 평가가 그것을 "실패"로 잡아내야 평가를 믿을 수 있습니다. **기대 결과.** 고치기 전 0/3 (404), 고친 뒤 3/3 (골든 8개 중 7~8개 — 점수 0.875~1을 허용).
▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대 요청문에 꼭 들어갈 것: **① 빈 목록 ② 통과 조건 ③ 반올림 ④ 틀린 케이스에 남길 것**. 아래 빈칸 4개를 채웁니다. ```text my-work/ops-eval.json 을 새로 만들어 줘. 웹훅 POST my-ops-eval, 본문 {cases: [{question, area, refused, numbers}]}, 이름 "79 Ops eval (my)". cases 가 비었으면 HTTP ____①. Split cases(Code)로 케이스를 아이템으로 나누고, HTTP 노드 하나가 아이템마다 my-ops-ask 를 부른다. Score(Code): 케이스마다 area 같음 · refused 같음 · numbers 중 하나가 답에 있음 · 거절이 아니면 numbers_ok — ____② 맞으면 통과. score = 통과 / 전체 (소수 ____③자리), failed 에는 통과 못 한 케이스와 어느 것이 틀렸나 (area_ok · refused_ok · number_ok · grounded · ____④). 기대 결과 cases/lab03-challenge.json. ``` 빈칸 힌트: - ① 테스트의 셋째 케이스. - ② 위 "넷 다 맞아야". - ③ 0.875처럼. - ④ 사람이 "왜 틀렸나"를 보려면 무엇이 있어야 하나요?
```bash uv run tools/flow.py push my-work/ops-eval.json uv run tools/flow.py test cases/lab03-challenge.json -v ``` ### 실행 결과 · 골든 질문 평가 수업 준비 때(11/01) 강사 노트북에서 실제로 나온 출력입니다. 평가는 `my-ops-ask`를 질문마다 불러 LLM을 씁니다. #### 고치기 전 · `my-ops-eval`이 없을 때 실행한 명령: ```bash uv run tools/flow.py test cases/lab03-challenge.json -v ``` 출력 (결과 줄만): ```text ✘ 골든 질문 8개 · 0.0s · status 404 (expected 200); total = null (expected 8); area_ok = null (expected 8); refused_ok = null (expected 8); score = null (expected 0.875..1) ✘ 틀린 기대는 잡아냄 · 0.0s · status 404 (expected 200); total = null (expected 1); passed = null (expected 0); failed.0.area = null (expected alerts); failed.0.area_ok = null (expected false) ✘ 빈 목록 → 400 · 0.0s · status 404 (expected 400) Passed 0/3 ``` **왜 이렇게 나왔나 (고치기 전):** 웹훅이 없어 404. 0/3은 예상한 결과. #### 고친 뒤 실행한 명령: ```bash uv run tools/flow.py push my-work/ops-eval.json uv run tools/flow.py test cases/lab03-challenge.json -v ``` 출력 (결과 줄과 응답): ```text ✔ 골든 질문 8개 · 3.9s ✔ 틀린 기대는 잡아냄 · 1.0s ✔ 빈 목록 → 400 · 0.0s Passed 3/3 골든 8 {"total": 8, "passed": 8, "score": 1, "area_ok": 8, "refused_ok": 8, "number_ok": 8, "grounded": 8, "failed": []} 틀린 기대 {"total": 1, "passed": 0, "score": 0, "area_ok": 0, "refused_ok": 1, "number_ok": 1, "grounded": 1, "failed": [{"question": "알림이 몇 건 와 있어?", "area": "alerts", "area_ok": false, …, "answer": "알림은 총 6건이 있습니다. 이 중 3건은 높은 심각도 알림입니다."}]} 빈 목록 HTTP 400 {"error": "cases must be a non-empty list"} ``` **고친 뒤 이렇게 나온 까닭** | | 결과 | 뜻 | | --- | --- | --- | | 골든 8 | **8/8 · score 1** | 영역 · 거절(작업 요청 하나 포함) · 숫자 · 근거가 모두 맞음 | | 틀린 기대 | 0/1, `area_ok: false`만 틀림 | 도우미는 `alerts`로 맞게 답했고, 시험지의 기대(`cost`)가 틀렸다는 것을 평가가 잡아냄 | | 빈 목록 | 400 | "0개 중 0개 통과 = 100%"로 속지 않음 | - **3.9초:** 질문 여덟 개를 차례로 `my-ops-ask`에 보냈습니다 (질문마다 LLM 한두 번). 평가는 도우미보다 느리고 비쌉니다 — 고칠 때마다 돌리되, 운영 중 매 질문마다 돌리지는 않습니다. - **8/8이 "완벽하다"는 뜻은 아닙니다.** Lab 02 과제 1의 "parked 2 · 대기 1을 거꾸로 말한 답"은 숫자가 맞아 이 평가를 통과합니다. 골든 질문을 늘릴 때는 **틀리기 쉬운 질문**(뜻을 뒤집기 쉬운 것)을 더합니다 — `my-work/golden.json`. - 테스트는 점수 0.875~1을 허용합니다 (LLM 답이 한 번 흔들려도 통과하게). 이번에는 1이었습니다. ## 과제 2 · 평가 관문 (10분) ### 이 실습의 의도 **한 줄로:** 평가 점수를 기준(`min_score`, 기본 0.9)과 비교해 `gate`(`pass` · `fail`)를 내고, 기준 아래면 알림을 남깁니다. 도우미를 고칠 때마다 이 관문을 지나게 합니다. > **생활 속 예 · 시험 점수의 합격선.** 신규 간호사 시험에 합격선이 없으면 "몇 점이면 병동에 서도 되나"를 그때그때 정합니다. 합격선을 정해 두고, 떨어지면 교육 담당에게 바로 알립니다. 과제 2는 도우미 시험의 합격선과 알림을 만드는 실습입니다. | 정의 (AGENTS.md 3절 "평가") | | | --- | --- | | `min_score` | 본문, 없으면 0.9 | | `gate` | `score ≥ min_score`면 `pass`, 아니면 `fail` (같으면 통과) | | `fail`이면 | `POST /alerts {workflow: "ops-eval", reason: "eval_below", score, min_score, message}` | | 응답 | 평가 결과 그대로 + `min_score` · `gate` | **끝나면 알게 되는 것.** LLM 도우미에도 74일차와 같은 **배포 관문**을 두는 법. 그리고 평가가 남긴 기록(알림)이 **다음 평가의 답을 바꿀 수 있다**는 것. **적을 곳.** `my-work/OPS.md` 9절 아래 "평가가 남긴 알림이 다음 평가에 준 영향". ```bash uv run tools/flow.py test cases/lab03-challenge2.json -v # 고치기 전 (과제 1의 평가) ``` ### 이해하기 ```mermaid flowchart LR S["Score
+ min_score · gate"] --> B{"fail?"} B -->|예| A["Alert eval
eval_below"] --> R["Result again"] B -->|아니오| R --> O(["평가 결과 그대로"]) ``` **평가는 도우미가 아니다.** 운영 도우미는 읽기만 하지만, 평가는 도우미를 **시험하는** 워크플로우라 알림을 남겨도 됩니다 (AGENTS.md 4절). 알림은 곁가지 — 응답은 `Result again`이 평가 결과를 다시 꺼냅니다 (78일차와 같음). **평가가 남긴 기록이 다음 평가를 바꾼다.** 수업 준비 때 "실패 → 알림" 케이스 **뒤에** "알림이 몇 건?" 케이스를 두었더니, 도우미가 "7건"이라고 답해 "6"을 기대한 평가가 틀렸습니다. 도우미는 맞았습니다 — 평가의 알림이 하나 늘었으니까요. 그래서 케이스 순서를 바꿨습니다. 평가도 **처음 상태**에서, 평가가 바꾸는 것까지 생각해 돌려야 합니다 (78일차 `--repeat`와 같은 이야기). **테스트 케이스.** | 케이스 | 기대 | | --- | --- | | 기준을 안 주면 0.9 | 알림 질문 하나 → `score` 1 · `pass` · `min_score` 0.9 | | 골든 8 · 기준 0.8 | `pass` · `min_score` 0.8 (점수 0.875~1) | | 틀린 기대 · 기준 0.9 | `score` 0 · `fail` | | 실패하면 알림 한 건 | `/alerts/list {reason: eval_below}` → 1건 · `ops-eval` · `score` 0 | **기대 결과.** 고치기 전 0/4, 고친 뒤 4/4, `lab03-challenge` 3/3 그대로.
▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대 요청문에 꼭 들어갈 것: **① 기본 기준 ② 경계 ③ 알림 이유 ④ 응답**. 아래 빈칸 4개를 채웁니다. ```text my-work/ops-eval.json 에 평가 관문을 더해 줘. 본문 min_score (없으면 ____①) 와 비교해 gate = score 가 min_score ____②이면 "pass", 아니면 "fail". 결과에 min_score · gate. fail 이면 POST http://data-api:8800/alerts {workflow: "ops-eval", severity: "medium", reason: "____③", score, min_score, message}. 알림을 보내도 응답은 평가 결과 ____④. 앞의 칸은 그대로. 기대 결과 cases/lab03-challenge2.json, cases/lab03-challenge.json 도. ``` 빈칸 힌트: - ① AGENTS.md 3절 "평가" 마지막 줄. - ② 0.9와 0.9는 통과인가요? (78일차 게이트) - ③ 테스트가 찾는 알림 이유. - ④ 알림 노드 뒤에서 무엇을 응답하나요?
```bash uv run tools/flow.py push my-work/ops-eval.json uv run tools/flow.py test cases/lab03-challenge2.json -v uv run tools/flow.py test cases/lab03-challenge.json uv run tools/flow.py morning --reset ``` ### 실행 결과 · 평가 관문 수업 준비 때(11/01) 강사 노트북에서 실제로 나온 출력입니다. #### 고치기 전 · 과제 1의 평가 실행한 명령: ```bash uv run tools/flow.py test cases/lab03-challenge2.json -v ``` 출력 (결과 줄만): ```text ✘ 기준을 안 주면 0.9 · 1.2s · min_score = null (expected 0.9); gate = null (expected pass) ✘ 골든 8 · 기준 0.8 → 통과 · 2.7s · gate = null (expected pass); min_score = null (expected 0.8) ✘ 틀린 기대 · 기준 0.9 → 실패 · 1.2s · gate = null (expected fail) ✘ 실패하면 알림 한 건 · 0.0s · count = 0 (expected 1); alerts.0.workflow = null (expected ops-eval); alerts.0.score = null (expected 0) Passed 0/4 ``` **왜 이렇게 나왔나 (고치기 전):** 평가 자체는 되지만 `min_score` · `gate` 칸이 없고 알림도 남기지 않습니다. 0/4는 예상한 결과. #### 고친 뒤 실행한 명령: ```bash uv run tools/flow.py push my-work/ops-eval.json uv run tools/flow.py test cases/lab03-challenge2.json -v uv run tools/flow.py test cases/lab03-challenge.json uv run tools/flow.py morning --reset ``` 출력 (결과 줄과 응답 · 알림): ```text ✔ 기준을 안 주면 0.9 · 3.3s ✔ 골든 8 · 기준 0.8 → 통과 · 3.4s ✔ 틀린 기대 · 기준 0.9 → 실패 · 1.2s ✔ 실패하면 알림 한 건 · 0.0s Passed 4/4 골든 8 {"total": 8, "passed": 8, "score": 1, …, "min_score": 0.8, "gate": "pass"} 틀린 기대 {"total": 1, "passed": 0, "score": 0, …, "min_score": 0.9, "gate": "fail"} 알림 {"count": 1, "alerts": [{"workflow": "ops-eval", "node": "Below?", "severity": "medium", "reason": "eval_below", "score": 0, "min_score": 0.9, "message": "ops assistant eval 0 < 0.9", …}]} (lab03-challenge) Passed 3/3 ``` **고친 뒤 이렇게 나온 까닭** | 평가 | score | 기준 | gate | 알림 | | --- | --- | --- | --- | --- | | 알림 질문 하나 (기준 없음) | 1 | 0.9 (기본) | pass | 없음 | | 골든 8 | 1 | 0.8 | pass | 없음 | | 틀린 기대 하나 | 0 | 0.9 | **fail** | **`eval_below` 1건** | - **응답은 평가 결과 그대로**에 `min_score` · `gate` 두 칸만 더해졌습니다 (`lab03-challenge` 3/3 그대로). - **수업 준비 때 겪은 일 — 평가가 남긴 알림이 다음 평가를 바꿨다.** 처음에는 "기준을 안 주면 0.9" 케이스를 **맨 뒤**에 두었습니다. 그러자 도우미가 "알림은 총 **7건**"이라고 답해 "6"을 기대한 평가가 0점이 됐습니다. 도우미가 틀린 것이 아닙니다 — 바로 앞 케이스의 실패가 `eval_below` 알림을 하나 남겨 알림이 7건이 됐습니다 (숫자 확인도 통과: 7은 사실에 있음). 그래서 그 케이스를 맨 앞으로 옮겼습니다. - 평가 · 관문을 CI처럼 돌릴 때는 **처음 상태에서**, 그리고 평가 자신이 바꾸는 것(알림 · 감사 기록)까지 생각해 순서를 정합니다. 78일차 `--repeat`의 `exists_id`와 같은 종류의 함정입니다. ## 막혔을 때 | 증상 | 할 일 | | --- | --- | | 브리핑의 상태가 사실과 다름 | 브리핑이 다시 판정하지 말고 `health`를 그대로 | | 먼저 볼 것이 넷 이상 · 순서가 다름 | `red` 다음 `yellow`, 앞 3개 | | 표의 숫자 꼴이 다름 | AGENTS.md 3절 표의 꼴 그대로 (`3건 (parked 2)`) | | 요약에 없는 숫자 | 요약에 줄 사실을 줄이고, `Check summary`로 | | `## 요약`이 표보다 앞 | markdown 끝에 덧붙임 | | 예제 3에서 숫자가 안 바뀜 | 케이스가 이어서 가는지 (앞 케이스의 처리가 남음) | | 평가가 늘 0점 | `Ask`가 아이템마다 부르는지, i번째 답 = i번째 케이스 | | 거절 질문이 실패로 | 거절이면 `grounded`는 true (`numbers_ok`를 보지 않음) | | 알림 질문의 숫자가 6이 아님 | 앞 평가가 알림을 남김 → `morning --reset` | | 계속 안 됨 | `cp reference/lab03-example2-brief.json my-work/ops-brief.json` 후 push (예제 2까지) | ## 확인 - [ ] 예제 1: `cases/lab03.json` 4/4 (고치기 전 0/4), `OPS.md` 7절. - [ ] 예제 2: `cases/lab03-llm.json` 2/2, `lab03` 4/4. - [ ] 예제 3: `cases/lab03-after.json` 5/5, `OPS.md` 8절. - [ ] 과제 1: `cases/lab03-challenge.json` 3/3, `OPS.md` 9절. - [ ] 과제 2: `cases/lab03-challenge2.json` 4/4, `lab03-challenge`도 통과. - [ ] `my-work/PROMPTS.md`, 끝나면 `uv run tools/flow.py morning --reset`.