# Lab 02 · LLM 답을 데이터로 바꾸기 (입력 → LLM → 파싱)
- 관련 강의: [2단계 · 데이터 파싱과 변환 노드](../../Lecture/66/02-STAGE2-PARSE-AND-TRANSFORM.md)
- 소요 시간: 약 55분 (예제 1 15분 · 예제 2 15분 · 과제 25분)
- 준비: 65일차 n8n 실행 (`cd ~/aio-01d4-stu/Last3Mon/65/n8n && docker compose start`), Windows 브라우저에서 http://localhost:5678
- 목표: LLM의 답은 **글자**라는 것을 눈으로 확인하고, Code 노드로 **목록**으로 바꾼다. 그다음 뽑을 정보를 하나 늘려 본다.
```mermaid
flowchart LR
E1["예제 1
LLM 없이
글자 → 목록"] --> E2["예제 2
진짜 LLM이 붙은
1부 실행"] --> T["과제
참석자도 뽑기"]
```
> 노드 이름은 뒤의 표현식에서 그대로 쓰므로 **안내와 똑같이** 지어 주세요. 노드 이름은 노드를 열고 맨 위 제목을 눌러 바꿉니다.
## 예제 1 · LLM 없이 "글자 → 목록" (15분)
LLM을 부르기 전에, LLM이 돌려줄 법한 답을 **직접 적어 넣고** 파싱부터 연습합니다. 돈도 시간도 들지 않고 결과가 항상 같습니다.
```mermaid
flowchart LR
T(["Start
Manual Trigger"]) --> F["Fake LLM answer
Edit Fields"] --> P["Parse actions
Code"]
```
1. 왼쪽 위 **+ (Create) → Workflow**. 이름을 `66 Practice 1 · parse text`로 바꿉니다.
2. **Add first step**에서 `Manual Trigger`(Trigger manually)를 추가하고 이름을 `Start`로 바꿉니다.
3. 오른쪽 `+` → **Edit Fields (Set)**, 이름 `Fake LLM answer`. **Add Field**로 필드 하나를 만듭니다.
| Name | Type | Value (Fixed 모드 그대로) |
| --- | --- | --- |
| `content` | String | 아래 한 줄 |
```text
{"actions": [{"task": "신규 고객 문의 응답 템플릿 정리", "owner": "이준호", "due": "금요일"}, {"task": "회의실 예약 규칙 개선", "owner": "", "due": ""}]}
```
4. 오른쪽 `+` → **Code**, 이름 `Parse actions`. Language `JavaScript`, Mode `Run Once for All Items`. 원래 있던 코드를 지우고 아래를 붙여 넣습니다.
```javascript
// Parse the answer text (JSON written as text) into a clean list of action items.
const raw = String($input.first().json.content ?? '').trim().replace(/^```(json)?\s*|```$/g, '');
let data;
try {
data = JSON.parse(raw);
} catch (error) {
return [{ json: { actions: [], count: 0, error: 'answer is not valid JSON' } }];
}
const items = Array.isArray(data) ? data : (data.actions ?? []);
const actions = items
.filter(item => item && String(item.task ?? '').trim())
.map(item => ({
task: String(item.task).trim(),
owner: String(item.owner || '미정').trim() || '미정',
due: String(item.due || '미정').trim() || '미정',
}));
return [{ json: { actions, count: actions.length } }];
```
5. 아래쪽 **Execute workflow**를 누르고 두 노드의 OUTPUT을 비교합니다. (오른쪽 위 `Table / JSON / Schema` 보기를 바꿔 가며 봅니다.)
| 노드 | 보이는 것 |
| --- | --- |
| Fake LLM answer | `content` **한 칸**에 긴 글자 한 줄 |
| Parse actions | `actions`가 **목록 2개**로 펼쳐짐, `count: 2`, 두 번째 할 일의 `owner`·`due`가 `미정` |
6. **답을 일부러 망가뜨려 봅니다.** `Fake LLM answer`의 값에서 **맨 끝 `}` 하나를 지우고** 다시 실행합니다.
| 결과 | 뜻 |
| --- | --- |
| `{"actions": [], "count": 0, "error": "answer is not valid JSON"}` | 망가진 답이 와도 **같은 모양**으로 끝나고, 이유(`error`)가 남음 |
확인한 뒤 지운 `}`를 다시 넣고 **Save** 합니다.
## 예제 2 · 진짜 LLM이 붙은 1부 실행하기 (15분)
이번에는 같은 파싱 앞에 **Webhook과 LLM**이 붙은 워크플로우를 가져와 실행합니다.
```mermaid
flowchart LR
W(["Meeting webhook"]) --> S["Settings"] --> H{"Has notes?"}
H -->|false| R0["Reject empty
(400)"]
H -->|true| L{{"LLM extract actions"}} --> P["Parse actions"] --> R["Send result"]
```
1. **+ (Create) → Workflow** → 오른쪽 위 **⋯ → Import from File...** → `Last3Mon/66/n8n/workflows/lab02-example2-meeting-part1.json`
- Windows 파일 선택 창 주소창: `\\wsl$\Ubuntu\home\<사용자>\aio-01d4-stu\Last3Mon\66\n8n\workflows`
2. 이름이 `66 Meeting notes (my)`인지 확인하고 **Save** 합니다. (Lab 03에서 이 워크플로우를 이어서 완성합니다.)
3. 노드 세 개를 열어 **어디서 무엇을 하는지** 확인합니다.
| 노드 | 볼 곳 |
| --- | --- |
| `Settings` | `title`, `notes`가 Webhook 입력(`$json.body`)에서 꺼낸 값 |
| `LLM extract actions` | 아래로 내려 **JSON** 칸: ① `schema`(답의 모양) ② `role: 'system'`(지시문) ③ `role: 'user'`(회의록) |
| `Parse actions` | 예제 1과 같은 코드. 첫 줄만 `choices[0].message.content`(LLM 답이 있는 곳)를 읽음 |
4. `Meeting webhook` 노드를 열고 **Listen for test event**를 누른 뒤, WSL 터미널에서 보냅니다.
```bash
cd ~/aio-01d4-stu/Last3Mon/66
curl -s -m 300 -X POST http://localhost:5678/webhook-test/my-meeting-actions \
-H 'Content-Type: application/json' -d @n8n/requests/weekly.json
```
5. 결과와 캔버스의 OUTPUT을 확인합니다. (수업 준비 때 `gpt-4.1-mini`로 실제 실행한 결과)
```text
{"actions":[{"task":"신규 고객 문의 응답 템플릿 정리","owner":"이준호","due":"금요일"},
{"task":"VPN 장애 보고서 작성","owner":"박서연","due":"다음 주 월요일"},
{"task":"서버 점검 일정 공지","owner":"김민지","due":"오늘"},
{"task":"회의실 예약 규칙 개선 담당자 선정","owner":"미정","due":"10월 23일"}],"count":4}
```
| 노드 | 확인할 것 |
| --- | --- |
| LLM extract actions | `choices[0].message.content`가 따옴표로 둘러싸인 **글자** (예제 1의 `Fake LLM answer`와 같은 모양) |
| Parse actions | `actions`가 **목록 4개**, 4번째 `owner`가 `미정` (`due`는 LLM이 "다음 회의는 10월 23일"을 보고 `10월 23일`로 채우는 경우가 많음) |
6. Listen을 다시 누르고 `empty.json` → HTTP **400** `{"error":"notes is required"}`, `no-action.json` → `{"actions":[],"count":0}` 도 확인합니다. (Test URL은 **한 번에 한 요청**만 받습니다.)
## 과제 · 참석자도 뽑기 (25분)
팀장이 요청했습니다.
> "할 일과 함께 **회의 참석자 명단**도 받아 보고 싶어요."
예제 2의 `66 Meeting notes (my)`를 고쳐서, 결과에 `attendees`(참석자 이름 목록)가 들어가게 만드세요.
| 요청 | 기대 결과 |
| --- | --- |
| `weekly.json` | 할 일 4건과 함께 `"attendees":["김민지","이준호","박서연"]` |
| `no-action.json` | `"attendees":[]` (참석자가 적혀 있지 않음) |
| `empty.json` | 그대로 HTTP 400 |
**먼저 생각해 보기 (3분)**: LLM에게 무엇을 더 알려 줘야 하나요? LLM 답이 바뀌면 그다음 노드는 무엇을 더 꺼내야 하나요? 고칠 곳은 **세 군데**입니다. 활동지나 메모장에 세 곳을 먼저 적은 뒤 고칩니다.
힌트 1 · 고칠 세 곳
| 곳 | 노드 | 무엇을 |
| --- | --- | --- |
| ① 답의 모양 | `LLM extract actions`의 `schema` | `properties`에 `attendees`(글자 목록) 추가, `required`에도 추가 |
| ② 지시문 | `LLM extract actions`의 `role: 'system'` 문장 | 참석자 이름도 `attendees`에 넣으라고 한 문장 추가 |
| ③ 꺼내기 | `Parse actions`의 마지막 `return` | `attendees`도 돌려주기 (없으면 `[]`) |
힌트 2 · ① schema에 넣을 한 줄
`actions: { ... } },` 줄 **바로 아래**에 넣습니다.
```text
attendees: { type: 'array', items: { type: 'string' } },
```
그리고 `required: ['actions'],` 를 `required: ['actions', 'attendees'],` 로 바꿉니다.
힌트 3 · ② 지시문과 ③ 코드
② `"... when there is no action item.` 뒤, 닫는 따옴표 `"` **앞**에 붙입니다.
```text
Also list the attendee names in \"attendees\" (use [] when not stated).
```
③ 마지막 `return` 줄을 아래 두 줄로 바꿉니다.
```javascript
const attendees = Array.isArray(data.attendees) ? data.attendees.map(name => String(name).trim()).filter(Boolean) : [];
return [{ json: { actions, count: actions.length, attendees } }];
```
**테스트**: Listen for test event → `weekly.json`, 다시 Listen → `no-action.json`, 다시 Listen → `empty.json`. 세 결과를 위 표와 비교합니다.
> 참석자를 넣었더니 **할 일이 3건**으로 줄었다면 한 번 더 보내 보세요. 수업 준비 때 8번 중 1번 이런 일이 있었습니다. 뽑을 것을 늘리면 원래 뽑던 결과도 흔들릴 수 있으니, 고친 뒤에는 **원래 기대 결과(할 일 4건)도 다시** 확인합니다.
**더 생각해 보기 (빨리 끝난 사람)**
- ①만 고치고 ②를 안 고치면? ③을 안 고치면? 하나씩 되돌려 실행해 보고, 어느 노드의 OUTPUT에서 `attendees`가 사라지는지 찾아보세요.
- 참석자 명단은 LLM이 찾는 일인가요, 규칙이 하는 일인가요?
## 막혔을 때
- `invalid syntax`: JSON 칸에서 `}`와 `}`가 **붙은** 곳이 있는지 봅니다. 표현식 `{{ }}` 안에서 `}}`가 붙으면 표현식이 거기서 끝납니다. `} }`처럼 띄어 씁니다.
- `401`: `LLM extract actions`의 Credential이 `OpenAI (.env)`인지 확인합니다. 없다면 65일차 SETUP 6단계(`register-credentials.sh`)를 다시 실행합니다.
- 고치다가 망가졌다면: 워크플로우를 지우고 예제 2의 파일을 **다시 가져옵니다**.
- Test URL 404: Listen for test event를 다시 누릅니다.
## 확인
- [ ] 예제 1: `Fake LLM answer`(글자)와 `Parse actions`(목록)의 차이를 OUTPUT으로 확인했다. 망가진 답에서 `error`를 봤다.
- [ ] 예제 2: `weekly` 4건, `empty` 400, `no-action` 0건을 확인했다.
- [ ] 과제: `weekly`에서 `attendees` 3명, `no-action`에서 `[]`를 받았다.