학습 자료

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.jsondev 스크립트는 index.ts 전용이니, 이번엔 tsx 를 직접 불러 파일명을 지정한다.

npx tsx --env-file=.env broken.ts
터미널 — npx tsx --env-file=.env broken.ts
  • 1번째 호출: 안녕하세요, 민준님! 😊
  • 기억했습니다. 당신의 이름은 **민준**이군요.
  • 다만 한 가지 알려드릴 점은, 저는 매 대화마다 이전 대화의 기록을 유지하지 않는다는 것입니다. 즉, 이 대화가 끝나고 새로운 대화를 시작하면 제가 민준님의 이름을 기억하지 못할 수 있습니다.
  • 현재 이 대화 내에서는 민준님이라고 부르며 도와드릴 수 있습니다. 편하게 말씀해 주세요! 👋
  • 2번째 호출: 안녕하세요! 저는 이전 대화 내용을 기억하지 못합니다.
  • 우리가 처음 만났거나, 새로운 대화를 시작한 것 같습니다. 혹시 이름을 알려주시겠어요?

정확히 예상한 대로다. 두 번째 호출에서 Claude 는 민준이라는 이름을 완전히 잊었다. 재밌는 건 첫 번째 답변에서 Claude 스스로 이걸 미리 경고했다는 점이다. "저는 매 대화마다 이전 대화의 기록을 유지하지 않는다"는 말이 답변 안에 이미 들어있었다.

매 요청은 서버 입장에서 완전히 새롭다
  1. 요청 1messages: [사용자 메시지 1개]
  2. 서버요청 하나만 보고 응답한다. 이전 요청은 모른다
  3. 요청 2messages: [사용자 메시지 1개], 방금 요청과 무관하다
서버가 상태를 안 가지고 있다는 뜻에서 이런 API 를 stateless(무상태) 라 부른다.

세 가지 역할 — system, user, assistant

그럼 "지금까지의 대화 전체"는 어떻게 표현할까. 2강에서 이미 messages 배열에 role: "user" 를 하나 넣어봤다. 사실 역할은 세 가지다.

세 가지 role
system대화 시작 전에 미리 정해두는 지침 (messages 배열 밖, 별도 파라미터)"너는 친절한 한국어 어시스턴트다"
user사람이 입력한 말"내 이름은 민준이야"
assistantClaude 가 이전에 낸 답변"안녕하세요, 민준님!"
system 은 messages 배열 안에 들어가지 않는다. messages.create() 의 별도 system 파라미터다.

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 를 부를 때마다 벌어지는 일을 그려보면 이렇다.

messages 배열이 두 번째 ask() 호출 시점에 쌓인 모습
  • user: "내 이름이 뭐라고 했지?"2턴
  • assistant: "안녕하세요, 민준님! ..."1턴 응답
  • user: "내 이름은 민준이야. 기억해줘."1턴

messages 배열 — 아래가 먼저 들어간 원소

이 세 개를 통째로 다시 보낸 뒤에야 Claude 가 2턴 질문에 답한다.

실행해본다.

npm run dev
터미널 — npx tsx index.ts
  • > 내 이름은 민준이야. 기억해줘.
  • 안녕하세요, 민준님! 만나서 반갑습니다. 도움이 필요하신 것이 있으면 편하게 말씀해주세요.
  • > 내 이름이 뭐라고 했지?
  • 민준님이라고 말씀하셨습니다!

이번엔 기억한다. 코드에서 바뀐 건 딱 하나, 매번 배열 전체를 다시 보냈다는 것뿐이다. Claude 서버 입장에서는 여전히 두 번의 독립된 요청이지만, 두 번째 요청 안에 "지금까지 있었던 일"이 전부 들어있으니 마치 기억하는 것처럼 답할 수 있다.

정리하면

Claude API 는 아무것도 기억하지 않는다. '대화'는 client 가 지금까지의 메시지를 매번 통째로 다시 보내서 만드는 착각이다. system 은 그 대화 내내 지킬 규칙이고, user/assistant 는 오간 말이 쌓이는 기록이다.

다시 짚어보기
  1. 01

    무상태(stateless) API 를 직접 확인했다

    이름을 알려준 뒤 독립된 새 호출로 물어보니 Claude 가 완전히 잊었다.

    npx tsx broken.ts
  2. 02

    세 가지 role 을 구분했다

    system(규칙, 배열 밖) · user(사람 말) · assistant(Claude 답변, 배열 안).

  3. 03

    messages 배열을 누적해 진짜 멀티턴을 만들었다

    매 턴마다 user 메시지를 넣고, 응답을 assistant 로 다시 넣어 다음 요청에 통째로 보낸다.

    npm run dev

다음 강의에서는 응답을 기다리는 방식을 바꿔본다. 지금까지는 Claude 가 답을 다 만들 때까지 화면이 멈춰 있었다. 다음 강의에서 글자가 한 자씩 흘러나오는 스트리밍을 붙여, 체감 속도를 바꿔본다.

system · user · assistant — 역할과 멀티턴 대화 — 디코드랩(DCODELAB)