system · user · assistant — 역할과 멀티턴 대화
Article
Part 1이 끝났다. 세팅하고, 첫 호출을 하고, 비용까지 셌다. 그런데 지금까지 우리가 부른 건 전부 "한 번 묻고 끝나는" 호출이었다. Claude 와 진짜 대화를 하려면, 내가 방금 한 말을 Claude 가 다음 질문에서도 기억해야 한다.
그런데 정말 그럴까? 코드로 먼저 확인해본다.
이름을 알려주고, 다시 물어본다
my-ai-assistant 폴더에 broken.ts 를 만든다. 별개의 두 번 호출로, 이름을 알려준 다음 그 이름을 다시 물어본다.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const first = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: "내 이름은 민준이야. 기억해줘." }],
});
console.log("1번째 호출:", first.content[0].text);
const second = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: "내 이름이 뭐라고 했지?" }],
});
console.log("\n2번째 호출:", second.content[0].text);
package.json 의 dev 스크립트는 index.ts 전용이니, 이번엔 tsx 를 직접 불러 파일명을 지정한다.
npx tsx --env-file=.env broken.ts
- 1번째 호출: 안녕하세요, 민준님! 😊
- 기억했습니다. 당신의 이름은 **민준**이군요.
- 다만 한 가지 알려드릴 점은, 저는 매 대화마다 이전 대화의 기록을 유지하지 않는다는 것입니다. 즉, 이 대화가 끝나고 새로운 대화를 시작하면 제가 민준님의 이름을 기억하지 못할 수 있습니다.
- 현재 이 대화 내에서는 민준님이라고 부르며 도와드릴 수 있습니다. 편하게 말씀해 주세요! 👋
- 2번째 호출: 안녕하세요! 저는 이전 대화 내용을 기억하지 못합니다.
- 우리가 처음 만났거나, 새로운 대화를 시작한 것 같습니다. 혹시 이름을 알려주시겠어요?
정확히 예상한 대로다. 두 번째 호출에서 Claude 는 민준이라는 이름을 완전히 잊었다. 재밌는 건 첫 번째 답변에서 Claude 스스로 이걸 미리 경고했다는 점이다. "저는 매 대화마다 이전 대화의 기록을 유지하지 않는다"는 말이 답변 안에 이미 들어있었다.
- 요청 1messages: [사용자 메시지 1개]
- 서버요청 하나만 보고 응답한다. 이전 요청은 모른다
- 요청 2messages: [사용자 메시지 1개], 방금 요청과 무관하다
세 가지 역할 — system, user, assistant
그럼 "지금까지의 대화 전체"는 어떻게 표현할까. 2강에서 이미 messages 배열에 role: "user" 를 하나 넣어봤다. 사실 역할은 세 가지다.
| system | 대화 시작 전에 미리 정해두는 지침 (messages 배열 밖, 별도 파라미터) | "너는 친절한 한국어 어시스턴트다" |
| user | 사람이 입력한 말 | "내 이름은 민준이야" |
| assistant | Claude 가 이전에 낸 답변 | "안녕하세요, 민준님!" |
system 이 특별한 이유는 위치다. user/assistant 는 대화가 오가며 계속 쌓이는 배열 원소지만, system 은 대화 내용이 아니라 "이 대화 내내 지킬 규칙"이다. 그래서 매번 다시 쓰지 않고 한 번만 정의해서 매 요청에 그대로 딸려 보낸다.
진짜 멀티턴을 만든다
이제 index.ts 를 고쳐, messages 배열을 함수 밖에 두고 대화가 이어질 때마다 누적시킨다. 사용자 메시지를 넣기 전에, Claude 의 답변도 role: "assistant" 로 배열에 밀어 넣는 게 핵심이다.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const messages: Anthropic.MessageParam[] = [];
async function ask(userText: string) {
messages.push({ role: "user", content: userText });
const response = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 1024,
system: "너는 친절한 한국어 어시스턴트다. 답은 항상 한두 문장으로 짧게 한다.",
messages,
});
const text = response.content[0].text;
messages.push({ role: "assistant", content: text });
console.log(`\n> ${userText}`);
console.log(text);
}
await ask("내 이름은 민준이야. 기억해줘.");
await ask("내 이름이 뭐라고 했지?");
ask 를 부를 때마다 벌어지는 일을 그려보면 이렇다.
- user: "내 이름이 뭐라고 했지?"2턴
- assistant: "안녕하세요, 민준님! ..."1턴 응답
- user: "내 이름은 민준이야. 기억해줘."1턴
messages 배열 — 아래가 먼저 들어간 원소
실행해본다.
npm run dev
- > 내 이름은 민준이야. 기억해줘.
- 안녕하세요, 민준님! 만나서 반갑습니다. 도움이 필요하신 것이 있으면 편하게 말씀해주세요.
- > 내 이름이 뭐라고 했지?
- 민준님이라고 말씀하셨습니다!
이번엔 기억한다. 코드에서 바뀐 건 딱 하나, 매번 배열 전체를 다시 보냈다는 것뿐이다. Claude 서버 입장에서는 여전히 두 번의 독립된 요청이지만, 두 번째 요청 안에 "지금까지 있었던 일"이 전부 들어있으니 마치 기억하는 것처럼 답할 수 있다.
정리하면
Claude API 는 아무것도 기억하지 않는다. '대화'는 client 가 지금까지의 메시지를 매번 통째로 다시 보내서 만드는 착각이다. system 은 그 대화 내내 지킬 규칙이고, user/assistant 는 오간 말이 쌓이는 기록이다.
- 01
무상태(stateless) API 를 직접 확인했다
이름을 알려준 뒤 독립된 새 호출로 물어보니 Claude 가 완전히 잊었다.
npx tsx broken.ts - 02
세 가지 role 을 구분했다
system(규칙, 배열 밖) · user(사람 말) · assistant(Claude 답변, 배열 안).
- 03
messages 배열을 누적해 진짜 멀티턴을 만들었다
매 턴마다 user 메시지를 넣고, 응답을 assistant 로 다시 넣어 다음 요청에 통째로 보낸다.
npm run dev
다음 강의에서는 응답을 기다리는 방식을 바꿔본다. 지금까지는 Claude 가 답을 다 만들 때까지 화면이 멈춰 있었다. 다음 강의에서 글자가 한 자씩 흘러나오는 스트리밍을 붙여, 체감 속도를 바꿔본다.