Files

17 KiB
Raw Permalink Blame History

AGENTS.md · n8n Workflow Ops Assistant 작업 규칙 (79일차 실습 폴더)

이 파일은 CLI 코딩 에이전트(Codex, Claude Code, Antigravity 등)가 이 폴더에서 n8n 워크플로우를 만들 때 따르는 규칙입니다. 사람도 읽을 수 있게 썼습니다. 78일차 규칙과 같고, 3절에 오늘 만드는 운영 사실 · 질문 답변 · 브리핑 · 평가의 계약과 사실 · 상태 정의, 5절에 여러 출처를 모으는 노드가 더해졌습니다.

1. 작업 순서 (항상 이 순서)

  1. 요청을 읽고, 만들 워크플로우의 입력 · 노드 순서 · 결과 모양을 3~5줄로 먼저 정리해 보여 준다. 사실 · 상태 요청이면 정의(어느 출처의 무엇을 어떻게 셀지) 를 먼저 표로 보여 준다.
  2. 워크플로우 JSON을 my-work/<이름>.json에 쓴다. 이미 있으면 그 파일을 고친다 (새 파일을 만들지 않는다).
  3. uv run tools/flow.py check my-work/<이름>.json → 오류(✘)가 없을 때까지 고친다.
  4. 끝나면 한국어로 짧게 보고한다: 바꾼 노드, check 결과, 그리고 사람이 실행할 명령 두 줄.
    uv run tools/flow.py push my-work/<이름>.json
    uv run tools/flow.py test cases/<요청에 나온 테스트 파일>.json
    
  5. push와 test는 사람이 실행해 결과를 확인한다. 사람이 "네가 push·test까지 해"라고 하면 직접 실행해도 된다.
  6. 사람이 실패 보고서(runs/*.json)를 알려 주면 실패한 케이스의 요청·응답을 읽고 원인을 설명한 뒤 고친다. 같은 곳을 3번 고쳐도 실패하면 멈추고 다른 방법을 제안한다.
  7. 문서(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": "79 Ops facts (my)", "nodes": [ ... ], "connections": { ... }, "settings": { "executionOrder": "v1" }}
  • 파일 하나에 워크플로우 하나. 노드 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에서 모으기

{"name": "Get alerts", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [440, 300],
 "parameters": {"url": "http://data-api:8800/alerts", "options": {}}}
  • 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초 안팎.

다른 내 웹훅을 부르기 (답변 · 브리핑 → 사실, 평가 → 답변)

{"name": "Get facts", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [880, 300],
 "parameters": {"method": "POST", "url": "http://localhost:5678/webhook/my-ops-facts", "sendBody": true, "specifyBody": "json",
   "jsonBody": "={{ JSON.stringify({ from: $('Ask webhook').first().json.body?.from ?? '', to: $('Ask webhook').first().json.body?.to ?? '' }) }}", "options": {"timeout": 30000}}}
  • 평가에서는 케이스를 아이템으로 나눈 뒤(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)

{"webhook": "my-ops-facts", "before": [{"url": "http://localhost:8800/ops-reset", "body": {}}], "cases": [
  {"name": "지난주 실행", "body": {}, "expect": {"equals": {"runs.total": 357, "runs.error_rate": 0.0588}}},
  {"name": "DLQ #3 처리 뒤", "before": [{"url": "http://localhost:8800/dlq/update", "body": {"dlq_id": 3, "remove": true}}], "body": {}, "expect": {"equals": {"dlq.size": 2}}}
]}
  • 기대 숫자는 운영 아침 상태(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 키를 출력하지 않는다.