지시를 구조로 쓴다
Article
Part 2가 끝났다. 지금까지는 방향이 한쪽이었다. 대화를 어떻게 이어가고, 어떻게 잘라낼지, 말하는 건 늘 우리였고 Claude 는 받아서 답할 뿐이었다.
Part 3부터는 반대다. Claude 가 어떤 모양으로 답하게 만들지를 우리가 설계한다. 오늘 만들 건 메모 정리 비서다. 두서없이 적은 메모를 던지면, 할 일 목록으로 뽑아준다. 이 비서를 11강까지 계속 다듬어서 Course A 의 결과물인 똑똑한 CLI 어시스턴트로 완성한다.
일단 시켜본다
my-ai-assistant 폴더에 naive.ts 를 만든다. 시스템 프롬프트는 딱 한 문장이다.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
async function organize(note: string) {
const response = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 300,
system: "메모를 할 일 목록으로 정리해줘.",
messages: [{ role: "user", content: note }],
});
const text = response.content[0].type === "text" ? response.content[0].text : "";
console.log(`\n--- 입력 ---\n${note}`);
console.log(`--- 출력 ---\n${text}`);
}
await organize(
"내일까지 보고서 초안 써야 하고, 다음 주 화요일에 팀장님이랑 미팅 있음. 시간 날 때 도서관 책 반납도 해야 함. 제일 급한 건 보고서.",
);
await organize(
"치과 예약 잡기, 이번 주 안에. 프로젝트 문서 리뷰 오늘 중으로 끝내야 함. 엄마 생신 선물 사기, 다음 달 초까지.",
);
실행한다.
npx tsx --env-file=.env naive.ts
- --- 입력 ---
- 내일까지 보고서 초안 써야 하고, 다음 주 화요일에 팀장님이랑 미팅 있음. 시간 날 때 도서관 책 반납도 해야 함. 제일 급한 건 보고서.
- --- 출력 ---
- # 📋 내일까지 해야 할 일 목록
- ## 🔴 긴급
- - [ ] **보고서 초안 작성** (내일까지 마감)
- ## 🟡 중요
- - [ ] **다음 주 화요일 팀장님 미팅 준비**
- ## 🟢 보통
- - [ ] **도서관 책 반납** (시간 날 때)
- 💡 팁: 보고서를 먼저 끝내고, 미팅 준비하면서...
- --- 입력 ---
- 치과 예약 잡기, 이번 주 안에. 프로젝트 문서 리뷰 오늘 중으로 끝내야 함. 엄마 생신 선물 사기, 다음 달 초까지.
- --- 출력 ---
- # 📝 할 일 목록
- ## 🔴 긴급 (오늘)
- - [ ] 프로젝트 문서 리뷰 완료
- ## 🟡 이번 주
- - [ ] 치과 예약 잡기
- ## 🟢 다음 달 초까지
- - [ ] 엄마 생신 선물 사기
- 💡 팁: 프로젝트 문서는 오늘 중에 마치고...
얼핏 보면 둘 다 그럴싸하다. 마크다운 헤더에 이모지까지, 보기엔 예쁘다. 그런데 나란히 놓고 보면 문제가 보인다.
- 첫 번째는 긴급 · 중요 · 보통이라는 우선순위 기준으로 묶었다
- 두 번째는 오늘 · 이번 주 · 다음 달 초까지라는 시간 기준으로 묶었다
- 둘 다 끝에 "팁"이라는 잡담을 덧붙였고, 길이도 매번 다르다
같은 지시를 줬는데 분류 기준 자체가 달랐다. 사람이 읽기엔 둘 다 괜찮지만, 이 출력을 파싱해서 할 일 앱에 넣으려는 프로그램 입장에서는 재앙이다. 헤더가 몇 개인지, 이모지가 어떤 규칙인지 예측할 수가 없다.
지시를 구조로 나눈다
해결책은 간단하다. 무엇을 할지, 어떤 모양으로 답할지를 문장이 아니라 구조로 못 박는다. 태그로 구역을 나누면 된다.
- system: "정리해줘"무엇을, 어떻게 할지 전부 모델의 판단에 맡긴다
- <role> <task> <output_format>역할 · 작업 · 출력 형식을 구역으로 나눠 못 박는다
index.ts 를 새로 만든다. naive.ts 와 구조는 똑같고, system 프롬프트만 바꾼다.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const SYSTEM_PROMPT = `<role>
너는 메모 정리 비서다. 사용자가 두서없이 적은 메모에서 할 일을 뽑아 목록으로 만든다.
</role>
<task>
입력 메모에서 "해야 할 일"을 모두 찾아낸다. 마감이 있으면 그대로 살리고, 없으면 "미정"으로 표시한다.
</task>
<output_format>
- 마크다운, 이모지, 헤더, 굵은 글씨를 쓰지 않는다. 순수 텍스트 줄만 출력한다.
- 할 일 한 줄은 반드시 이 형식이다: "- [마감기한] 할 일 내용"
- 마감이 급한 순서로 정렬한다. 마감이 없는 항목은 맨 뒤에 둔다.
- 목록 앞뒤로 인사말, 설명, 팁, 요약을 절대 덧붙이지 않는다. 목록 줄만 출력한다.
</output_format>`;
async function organize(note: string) {
const response = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 300,
system: SYSTEM_PROMPT,
messages: [{ role: "user", content: note }],
});
const text = response.content[0].type === "text" ? response.content[0].text : "";
console.log(`\n--- 입력 ---\n${note}`);
console.log(`--- 출력 ---\n${text}`);
}
await organize(
"내일까지 보고서 초안 써야 하고, 다음 주 화요일에 팀장님이랑 미팅 있음. 시간 날 때 도서관 책 반납도 해야 함. 제일 급한 건 보고서.",
);
await organize(
"치과 예약 잡기, 이번 주 안에. 프로젝트 문서 리뷰 오늘 중으로 끝내야 함. 엄마 생신 선물 사기, 다음 달 초까지.",
);
세 구역이 하는 일은 명확하다.
<role>: 이 비서가 누구인지. 정체성이 명확할수록 딴 길로 안 샌다<task>: 무엇을 찾아야 하는지. "할 일"의 기준(마감 유무)까지 못 박았다<output_format>: 결과가 어떤 모양이어야 하는지. 하지 말아야 할 것까지 명시했다
<role> <task> <output_format> 은 Claude 가 특별 취급하는 예약어가 아니다. 그냥 텍스트를 감싼 태그일 뿐이다. 그런데도 효과가 있는 이유는, 사람이 봐도 구역이 나뉘어 있으면 헷갈리지 않듯 모델도 그렇기 때문이다.
다시 실행하면
같은 두 메모를 그대로 넣고 돌린다.
npx tsx --env-file=.env index.ts
- --- 입력 ---
- 내일까지 보고서 초안 써야 하고, 다음 주 화요일에 팀장님이랑 미팅 있음. 시간 날 때 도서관 책 반납도 해야 함. 제일 급한 건 보고서.
- --- 출력 ---
- - [내일] 보고서 초안 작성
- - [다음 주 화요일] 팀장님과 미팅
- - [미정] 도서관 책 반납
- --- 입력 ---
- 치과 예약 잡기, 이번 주 안에. 프로젝트 문서 리뷰 오늘 중으로 끝내야 함. 엄마 생신 선물 사기, 다음 달 초까지.
- --- 출력 ---
- - [오늘] 프로젝트 문서 리뷰
- - [이번 주] 치과 예약 잡기
- - [다음 달 초] 엄마 생신 선물 사기
두 메모 모두 똑같은 형식으로 나왔다. - [마감기한] 내용 한 줄씩, 마감이 급한 순서, 팁도 이모지도 없다. 분류 기준이 왔다 갔다 하던 문제도 사라졌는데, 애초에 "우선순위로 나눌지 시간으로 나눌지"를 모델이 고민할 필요가 없어졌기 때문이다. <output_format> 이 그 선택지를 아예 없애버렸다.
system: "정리해줘"
naive.ts
- 메모마다 분류 기준이 달랐다
- 마크다운 · 이모지가 섞여 파싱하기 어렵다
- 매번 팁 문장이 따라붙는다
<role> <task> <output_format>
index.ts
- 두 메모 모두 같은 형식으로 나왔다
- 순수 텍스트, 정해진 줄 형식만 나온다
- 군더더기 문장이 없다
정리하면
지시가 한 문장이면 모델이 매번 다른 방식으로 빈틈을 채운다. 역할 · 작업 · 출력 형식을 태그로 나눠 명시하면, 그 빈틈 자체가 줄어들어 출력이 일관돼진다.
- 01
한 문장 지시의 문제를 실제로 확인했다
같은 지시로 두 메모를 정리시켰더니, 분류 기준과 형식이 매번 달랐다.
- 02
<role> <task> <output_format> 로 지시를 구조화했다
무엇을 할지, 어떤 모양으로 답할지를 태그로 나눠 명시했다.
npx tsx --env-file=.env index.ts - 03
두 메모 모두 같은 형식으로 정리됐다
분류 기준이 흔들리던 문제, 팁이 따라붙던 문제가 함께 사라졌다.
그런데 구조화된 지시로도 못 잡는 규칙이 있다. 다음 강의에서 할 일 내용을 짧게 압축하라는 규칙을 하나 추가해본다. 문장으로 아무리 강조해도 모델이 잘 안 지키는 규칙이다. 그걸 **예시(few-shot)**로 어떻게 잡는지 확인한다.