AGENTS.md · n8n Workflow Ops Assistant 작업 규칙 (79일차 실습 폴더)
이 파일은 CLI 코딩 에이전트(Codex, Claude Code, Antigravity 등)가 이 폴더에서 n8n 워크플로우를 만들 때 따르는 규칙입니다. 사람도 읽을 수 있게 썼습니다. 78일차 규칙과 같고, 3절에 오늘 만드는 운영 사실 · 질문 답변 · 브리핑 · 평가의 계약과 사실 · 상태 정의, 5절에 여러 출처를 모으는 노드가 더해졌습니다.
1. 작업 순서 (항상 이 순서)
- 요청을 읽고, 만들 워크플로우의 입력 · 노드 순서 · 결과 모양을 3~5줄로 먼저 정리해 보여 준다. 사실 · 상태 요청이면 정의(어느 출처의 무엇을 어떻게 셀지) 를 먼저 표로 보여 준다.
- 워크플로우 JSON을
my-work/<이름>.json에 쓴다. 이미 있으면 그 파일을 고친다 (새 파일을 만들지 않는다).
uv run tools/flow.py check my-work/<이름>.json → 오류(✘)가 없을 때까지 고친다.
- 끝나면 한국어로 짧게 보고한다: 바꾼 노드, check 결과, 그리고 사람이 실행할 명령 두 줄.
- push와 test는 사람이 실행해 결과를 확인한다. 사람이 "네가 push·test까지 해"라고 하면 직접 실행해도 된다.
- 사람이 실패 보고서(
runs/*.json)를 알려 주면 실패한 케이스의 요청·응답을 읽고 원인을 설명한 뒤 고친다. 같은 곳을 3번 고쳐도 실패하면 멈추고 다른 방법을 제안한다.
- 문서(
my-work/*.md)를 써 달라는 요청이면 워크플로우를 만들지 않고 문서만 쓴다.
2. 환경 (바꾸지 말 것)
| 항목 |
값 |
| n8n |
2.42.6, 65일차 공식 설치 ~/day65-n8n (Docker 프로젝트 day65-n8n), 브라우저 http://localhost:5678 |
| n8n 켜고 끄기 |
~/aio-01d4-stu/Last3Mon/65/n8n에서 docker compose start/stop (사람이 함, Ollama 컨테이너는 없음) |
| 게시된 웹훅 주소 |
밖에서 · n8n 안에서 모두 http://localhost:5678/webhook/<path> |
| OpenAI Credential |
id openAiCourse0001, name OpenAI (.env), 모델 gpt-4.1-mini |
| 다른 Credential |
쓰지 않음 (Ollama 등 없음) |
| 데이터 API |
n8n 안에서 http://data-api:8800, 터미널에서 http://localhost:8800 (79일차 판 Last3Mon/79/data-api: 78일차 + 운영 아침 상태를 켤 때 채워 둠) |
| 비교 기준 · 제공 |
reference/baseline-ops-facts.json (base-ops-facts, 팀이 지난달 만든 아침 대시보드 — 기간 없이 전체를 셈, 품질 · 릴리스 없음) · reference/baseline-ops-ask.json (base-ops-ask, 사실 전부를 LLM에게 주고 무엇이든 답하던 챗봇). 고치지 않는다 (비교용). reference/ops-facts-health.json은 Lab 01 과제를 못 끝냈을 때 Lab 02 · 03에 쓰는 사실 |
운영 아침 상태 (2026-11-04 08:30, 연습 데이터)
데이터 API를 켜거나 POST /ops-reset을 하면 아래 상태로 돌아갑니다 (data-api/data/ops-seed.json).
| 출처 (72~78일차) |
요청 |
들어 있는 것 |
| 실행 기록 (77) |
POST /logs/query {"from", "to"} |
두 주 666줄 (10/19~10/30) |
| 알림 (76 · 78) |
GET /alerts → {"count", "alerts": [{"workflow", "node", "severity", "reason", "message", "at"}]} |
6건 |
| DLQ (76) |
GET /dlq → {"size", "items": [{"dlq_id", "item", "reason", "attempts", "parked"}]} |
3건 |
| 품질 기록 · 격리함 (78) |
GET /quality-log → {"count", "items"}, GET /quarantine → {"size", "items"} |
4줄 · 1건 |
| 사용량 · 예산 (75) |
GET /usage/summary → {"requests", "calls", "cost_usd", "by_user": {이름: {"cost_usd"}}}, GET /budget → {"daily_usd", "used_usd"} |
오늘 192번 부름 · 예산 $0.08 |
| 릴리스 (74) |
GET /releases/helpdesk → {"active", "canary": {"release", "percent"} 또는 null} |
1.1.0 + 카나리 1.2.0 20% |
| 감사 기록 (73) |
POST /audit 본문 {...} 한 줄 더하기 · GET /audit (POST /audit/list) → {"events": [...]} |
처음엔 비어 있음 (Lab 02 과제 2) |
| 고장 스위치 (72) |
POST /admin/fault {"usage": "error"} → GET /usage/summary가 500 (79일차 판) |
사람이 · 테스트가 (Lab 01 과제 2) |
| 되돌리기 |
POST /ops-reset · GET /ops/seed-info |
사람이 · 테스트가 (flow.py morning --reset, ops --reset과 같음) |
3. 오늘 만드는 것의 계약과 정의
| 무엇 |
파일 · 웹훅 |
입력 (본문) |
결과 |
| 운영 사실 |
my-work/ops-facts.json · POST /webhook/my-ops-facts |
from, to (비면 2026-10-26 · 2026-10-30) |
아래 "사실" (과제 1: + health, red, yellow, overall · 과제 2: + sources_down, unknown) |
| 질문 답변 |
my-work/ops-ask.json · POST /webhook/my-ops-ask |
question |
{"question", "area", "router", "refused", "reason", "answer", "numbers_ok", "unknown_numbers"} (과제 2: 질문마다 감사 기록 한 줄) |
| 브리핑 |
my-work/ops-brief.json · POST /webhook/my-ops-brief |
from, to |
{"overall", "red", "yellow", "actions": [{"area", "status", "action"}], "markdown"} (+ summary, numbers_ok) |
| 평가 |
my-work/ops-eval.json · POST /webhook/my-ops-eval |
{"cases": [{"question", "area", "refused", "numbers"}], "min_score"} (min_score는 과제 2, 기본 0.9) |
{"total", "passed", "score", "area_ok", "refused_ok", "number_ok", "grounded", "failed": [...]} (과제 2: + min_score, gate), 빈 목록이면 400 |
사실 (my-ops-facts)
| 필드 |
정의 |
period |
{"from", "to"} 쓴 기간 |
runs |
그 기간 실행 기록: total, errors, error_rate, p95_ms, unanswered_rate (77일차 정의 그대로), top_failure {"node", "message", "count"} 가장 많은 (노드, 메시지) 묶음 또는 null |
alerts |
count, high(severity가 high), by_reason {reason: 수} |
dlq |
size, parked, waiting(parked 아님) |
quality |
batches(품질 기록 줄 수), avg_score(4자리), last_score(마지막 줄, 없으면 null), rejected_batches, quarantine(격리함 수) |
cost |
requests, calls, cost_usd(6자리), daily_usd, used_ratio(used_usd / daily_usd, 4자리), top_user {"user", "cost_usd", "share"} 비용 1위 사용자 |
release |
active, canary_release(없으면 null), canary_percent(없으면 0), active_repeat_rate · canary_repeat_rate: 그 기간 자동 답변 실행 중 그 릴리스의 repeat 비율(4자리, 실행이 없으면 null) |
- 기간은 실행 기록에만 적용합니다. 알림 · DLQ · 품질 · 비용 · 릴리스는 "지금" 상태입니다. 0건이면 비율 · p95는 0,
top_failure · 반복 비율은 null.
출처가 멈추면 (Lab 01 과제 2)
| 출처가 답하지 않으면 (HTTP 오류 · 모양이 다름) |
처리 |
| 그 영역 |
사실을 null, sources_down에 영역 이름 (이 순서: alerts, dlq, quality, cost, release — 실행 기록이 멈추면 워크플로우 오류) |
| 상태 |
health의 그 영역은 unknown, unknown 목록에 (red · yellow에는 넣지 않음) |
| 전체 |
red가 있으면 red, 아니면 yellow나 unknown이 있으면 yellow, 아니면 green — 모르는 것을 초록으로 말하지 않음 |
| 응답 |
언제나 HTTP 200 (다른 출처의 사실은 그대로) |
- HTTP 노드에
neverError를 켜야 출처 하나의 오류가 워크플로우 전체를 멈추지 않는다. 비용은 usage/summary의 cost_usd와 budget의 daily_usd가 숫자일 때만 "답했다"로 본다.
상태 (Lab 01 과제)
| 영역 |
🔴 red |
🟡 yellow |
그 밖 🟢 green |
runs |
error_rate > 0.01 |
unanswered_rate > 0.02 |
|
alerts |
high ≥ 5 |
count ≥ 1 |
|
dlq |
parked > 0 |
size > 0 |
|
quality |
last_score < 0.8 |
last_score < 0.9 또는 quarantine > 0 |
|
cost |
used_ratio ≥ 1 |
used_ratio ≥ 0.8 |
|
release |
카나리가 있고 두 비율이 모두 있고 canary_repeat_rate > 2 × active_repeat_rate |
카나리가 있음 |
|
health {영역: 색}, red · yellow: 그 색인 영역을 이 순서(runs, alerts, dlq, quality, cost, release)로, overall: red가 있으면 red, 아니면 yellow가 있으면 yellow, 아니면 green.
질문 답변 (my-ops-ask)
| 단계 |
규칙 |
| 작업 요청 (Lab 02 예제 2) |
"다시 돌려 · 재처리해 · 지워 · 삭제해 · 꺼 줘 · 켜 줘 · 배포해 · 되돌려 · 롤백해 · 바꿔 줘 · 올려 줘 · 내려 줘 · 비워 줘"가 있으면 거절: area: "action", refused: true, reason: "read_only", LLM을 부르지 않음 |
| 낱말 라우터 |
영역마다 낱말 목록(아래), 맞은 낱말이 가장 많은 영역 (같으면 위의 순서 앞), 하나도 없으면 unknown, router: "keyword" |
| LLM 라우터 (Lab 02 과제) |
낱말 라우터가 unknown일 때만 LLM이 영역 하나 또는 none을 고름, router: "llm" |
| 운영 밖 |
끝까지 unknown이면 거절: refused: true, reason: "out_of_scope" |
| 답 |
사실 중 그 영역만(overview면 전부)을 LLM에게, 한국어 2~3문장, 사실에 있는 숫자만 |
| 숫자 확인 (Lab 02 예제 2) |
답의 숫자가 모두 그 영역 사실 안에 있는지(값, ×100, 반올림, 글자 속 숫자, 버전 1.2.0은 통째로) Code가 → numbers_ok, unknown_numbers |
| 영역 |
낱말 |
runs |
오류, 에러, 실패율, 느려, 느린, 응답 시간, p95, 실행 |
alerts |
알림, 경보 |
dlq |
dlq, 재처리, 쌓인, parked, 접수 실패 |
quality |
품질, 배치, 격리 |
cost |
비용, 돈, 예산, 토큰, 요금 |
release |
릴리스, 버전, 카나리, 배포 |
overview |
괜찮, 상태, 요약, 전체, 브리핑 |
브리핑 (my-ops-brief)
- 사실(상태 포함)을 다시 부름.
actions: red 다음 yellow의 앞 3개, 영역마다 정해진 할 일 (아래). markdown: 제목 · 전체 · 영역 표(| 영역 | 상태 | 숫자 |) · "## 먼저 볼 것".
- (Lab 03 예제 2) LLM 세 문장
summary + 숫자 확인 numbers_ok, markdown 끝에 "## 요약".
| 영역 |
표의 이름 · 숫자 |
할 일 |
runs |
실행 · 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |
오류 묶음 1위 확인 · 고치기 (77일차) |
alerts |
알림 · 6건 (높음 3) |
높은 알림부터 확인하고 닫기 (76일차) |
dlq |
DLQ · 3건 (parked 2) |
parked 건 원인 확인 뒤 런북대로 재처리 (76일차) |
quality |
데이터 품질 · 평균 0.6583 · 마지막 1 · 격리 1 |
격리함을 콘텐츠 담당자에게 (78일차) |
cost |
비용 · $0.073604 / $0.08 (92.01%) |
비용 1위 사용자 · 한도 확인 (75일차) |
release |
릴리스 · 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |
카나리 끄기 · 되돌리기 검토 (74일차) |
- 상태 표시: 🔴 · 🟡 · 🟢 (출처가 멈춘 영역은 ⚪, 숫자 칸은
출처 응답 없음).
감사 기록 (Lab 02 과제 2)
- 질문마다 응답하기 전에
POST http://data-api:8800/audit 한 줄: {"kind": "ops-ask", "question", "area", "router", "refused", "reason", "numbers_ok"} (거절이면 numbers_ok: null). 갈래 셋(작업 거절 · 운영 밖 · 답) 모두. 응답은 그대로.
- 감사 기록은 덧붙이기만 하는 기록이라 읽기 전용 원칙의 예외입니다 (실행 기록
/logs와 같음). 다른 상태는 바꾸지 않는다.
평가 (my-ops-eval)
- 케이스마다
my-ops-ask를 부르고: area 같음 · refused 같음(없으면 false) · numbers 중 하나가 답에 있음(없으면 통과) · 거절이 아니면 numbers_ok true → 넷 다 맞으면 통과. score = 통과 / 전체 (4자리). failed에는 통과 못 한 케이스 {"question", "area", "area_ok", "refused_ok", "number_ok", "grounded", "answer"}.
- (Lab 03 과제 2) 평가 관문:
min_score(본문, 없으면 0.9)와 비교해 gate = score ≥ min_score면 pass, 아니면 fail. fail이면 POST /alerts {"workflow": "ops-eval", "node": "Below?", "severity": "medium", "reason": "eval_below", "score", "min_score", "message"}. 응답은 평가 결과 그대로 (+ min_score, gate). 평가는 운영 도우미가 아니라 도우미를 시험하는 워크플로우라 알림을 남겨도 됩니다.
4. 워크플로우 파일 규칙
- 파일 하나에 워크플로우 하나. 노드
name은 영어로, 겹치지 않게. API 키를 JSON에 쓰지 않는다.
- 숫자 · 상태는 Code가, 글은 LLM이. LLM에게 사실을 세게 하거나 상태를 판정하게 하지 않는다.
- 운영 도우미는 읽기만 한다: 데이터 API에 POST로 바꾸는 요청(
/dlq/update, /releases/..., /budget, /ops-reset 등)을 워크플로우에서 부르지 않는다 (/logs/query는 읽기, /audit은 덧붙이는 기록 · Lab 02 과제 2, 평가의 /alerts는 Lab 03 과제 2).
5. 검증된 노드 (n8n 2.42.6에서 실행 확인)
여러 출처를 차례로 가져오기 → 한 Code에서 모으기
Get logs(POST /logs/query) → Get alerts → Get DLQ → Get quality → Get quarantine → Get usage → Get budget → Get release → Make facts(Code)를 한 줄로 잇는다. 아이템이 하나씩이라 각 HTTP 노드는 한 번만 부른다.
Make facts에서 출처마다 $('Get alerts').first().json처럼 이름으로 읽는다. 웹훅 입력은 $('Facts webhook').first().json.body.
- 수업 준비 때 8개 출처를 다 모아도 0.1초 안팎.
다른 내 웹훅을 부르기 (답변 · 브리핑 → 사실, 평가 → 답변)
- 평가에서는 케이스를 아이템으로 나눈 뒤(Code) HTTP 노드 하나가 아이템마다
my-ops-ask를 부른다. 결과의 i번째 = 케이스의 i번째.
거절은 LLM 앞에서 (IF → Respond)
Route(Code) → Action?(IF) → 맞으면 바로 Respond(거절). 작업 요청은 LLM에게 보내지 않는다 (73일차 "막는 쪽으로").
AI Agent + OpenAI chat model (답 글 · 분류 · 요약)
- 67~78일차와 같다. 분류는 지시문에 영역 목록과 "Reply with only the area word", 받은 글에서 영문자만 남겨 목록에 있는지 확인, 없으면
unknown.
6. 연결 · 7. 표현식
- 72~78일차와 같다. 키에 점이 없는 결과 필드는 테스트에서
runs.top_failure.count, health.dlq, actions.0.area처럼 내려간다.
8. 테스트 파일 (cases/*.json)
- 기대 숫자는 운영 아침 상태(
data-api/data/ops-seed.json)와 실행 기록을 3절 정의대로 계산한 값입니다. 테스트를 통과시키려고 기대 결과나 데이터 파일을 바꾸지 않는다. 정의가 다르다고 생각되면 사람에게 묻는다.
9. 하지 말 것
tools/, data-api/(데이터 파일 포함), requests/, reference/, 이 파일을 고치지 않는다 (요청받은 경우만).
- 워크플로우 안에서 상태를 바꾸는 요청(
/ops-reset, /dlq/update, /dlq-clear, /releases/... POST, /budget POST, 고장 스위치)을 부르지 않는다.
- 답 · 요약에 사실에 없는 숫자 · 짐작한 원인을 넣지 않는다. 운영 밖 질문에 답하지 않는다.
- n8n 컨테이너를 지우거나
docker compose down을 하지 않는다. ~/day65-n8n을 읽거나 고치지 않는다. Credential을 새로 만들지 않는다. .env나 API 키를 출력하지 않는다.