byteforce

CPN 한국어 자습서 · 러닝패스 2 / 4 — Building with the Claude API

API로 Claude에 접근하기

멀티턴 대화

Multi-Turn conversations

요청 하나는 보냈습니다. 이제 그걸 여러 번 이어 붙여 대화로 만듭니다. 핵심은 하나입니다 — API는 아무것도 기억하지 않으므로, 맥락은 전부 여러분의 코드가 들고 있어야 합니다.

전체 내레이션영상 나레이션 한국어 번역 (전체)

Stephen Grider · Anthropic 기술 스태프

지금까지 작성한 코드는 모델과의 아주 단순한 주고받기를 시뮬레이션합니다. 이 대화를 이런 채팅 창 안에서 시각화해볼 수 있습니다. 우리는 "양자 컴퓨팅이 뭐야? 한 문장으로 답해줘" 같은 요청을 보냈고, 아주 단순한 한 문장짜리 답변을 받았습니다.

자연스럽게 어느 시점엔 이 대화를 이어가고 싶어집니다. "문장 하나 더 써줘" 같은 후속 질문을 보내면, 양자 컴퓨팅을 어떤 식으로든 더 확장한 답변이 돌아오기를 기대하게 됩니다.

이렇게 여러 메시지로 이뤄진 대화를 하려면, Anthropic API와 Claude 자체에 대해 반드시 이해하고 넘어가야 할 정말 중요한 점이 하나 있습니다. 바로 Anthropic API와 Claude는 여러분이 보내는 어떤 메시지도 저장하지 않는다는 것입니다. 여러분이 보낸 메시지도, 돌려받은 응답도 어떤 형태로든 저장되지 않습니다.

그래서 컨텍스트나 흐름을 유지하는 여러 메시지짜리 대화를 하고 싶다면, 두 가지를 해야 합니다. 첫째, 코드 안에서 주고받는 모든 메시지의 목록을 직접 관리해야 합니다. 둘째, 후속 요청을 보낼 때마다 그 전체 메시지 목록을 함께 보내야 합니다.

이 개념을 좀 더 자세히 짚어서 무슨 일이 벌어지는지 확실히 해두겠습니다. 먼저 Claude가 메시지를 저장하지 않는다는 걸 증명하는 짧은 예제 코드를 써보겠습니다. 양자 컴퓨팅이 무엇인지 물어본 다음 "문장 하나 더 써줘"라고 묻는 상황을 시뮬레이션해보면 증명할 수 있습니다.

노트북으로 돌아가, 처음에 양자 컴퓨팅이 뭔지 물었던 셀로 갑니다. 여기에 미리 작성해둔 두 번째 요청을 붙여넣겠습니다. 이 두 번째 요청에서는 Claude에게 문장 하나를 더 써달라고 하고, 그 후속 요청의 응답 텍스트를 출력해보겠습니다. 이렇게 하면 첫 질문을 한 뒤 후속 질문을 보내는 상황이 시뮬레이션됩니다. 그러면 읽을 수 있거나 쓸모 있는 텍스트가 전혀 돌아오지 않는 걸 보게 됩니다. 이 셀을 실행해보면 양자 컴퓨팅과는 전혀 상관없는 결과가 돌아옵니다.

왜 양자 컴퓨팅에 관한 내용이 아니라 이런 결과가 나오는지 확실히 하기 위해 다이어그램 몇 개를 살펴보겠습니다. 이 다이어그램은 방금 작성한 질의 코드에서 무슨 일이 일어나는지 보여줍니다. 처음에 Claude에게 요청을 보낼 때는 user 메시지 하나로 "양자 컴퓨팅을 한 문장으로 정의해줘"라고 합니다. 그러면 예상대로 양자 컴퓨팅을 한 문장으로 정의한 응답이 돌아옵니다. 그다음 두 번째 요청에서는 본문에 메시지가 단 하나뿐이고, 그 메시지는 "문장 하나 더 써줘"입니다. 이걸 보내면 Claude는 과거 대화나 이전에 주고받은 메시지에 대한 기억이 전혀 없습니다. 그래서 요청을 최대한 충실히 수행하려 할 뿐이고, 어떤 문장을 쓰긴 하지만 그게 양자 컴퓨팅에 관한 것일 가능성은 거의 없습니다.

이제 이 문제를 어떻게 해결하는지 보여드리겠습니다. 먼저 다시 user 메시지 하나로 초기 요청을 보냅니다. 그런 다음 돌려받은 assistant 메시지를 메시지 목록에 추가(append)합니다. 그 assistant 응답을 왼쪽 목록에 더한다고 상상하면 됩니다. 그리고 대화를 이어가고 싶을 때는 맨 아래에 user 메시지를 추가합니다. 이제 이걸 진짜 대화처럼 읽을 수 있습니다. 양자 컴퓨팅을 정의해달라고 했고, 응답을 받았고, 이제 Claude에게 묻고 싶은 또 다른 질문을 더하는 거죠. 이 경우엔 "문장 하나 더 써줘"입니다.

이 메시지 목록을 Claude에게 보내면, Claude는 대화 전체의 컨텍스트와 히스토리를 갖게 됩니다. 이 질문 흐름에서 우리가 주고받은 이전 메시지를 전부 본 상태죠. 그러면 Claude가 좀 더 합리적인 답변, 이전 답변을 조금 더 확장하는 한 문장짜리 후속 답변을 줄 수 있게 됩니다.

이 전체 흐름을 직접 보기 위해 노트북으로 돌아가, 대화의 전체 컨텍스트를 유지할 수 있는 코드를 작성해보겠습니다. 먼저 대화의 히스토리, 즉 컨텍스트를 유지하는 데 도움이 될 헬퍼 함수 세 개를 만들겠습니다. 이 헬퍼 함수들은 이 과정의 나머지 부분에서 꽤 자주 사용하게 됩니다.

이 셀에서 위쪽에 공간을 좀 두고, 히스토리 유지를 도와줄 첫 번째 헬퍼 함수를 정의합니다. 이름은 add_user_message로 하겠습니다. 메시지 목록과 텍스트를 인자로 받습니다. 그런 다음 user_message 변수를 만드는데, role은 user이고 content는 우리가 넘긴 텍스트가 됩니다. 그리고 이 새 user 메시지를 메시지 목록에 추가합니다.

다음으로 assistant 메시지를 히스토리에 추가하는 데 특화된 두 번째 헬퍼 함수를 추가합니다. 시간을 아끼려고 방금 함수를 복사하겠습니다. 이름을 add_assistant_message로 바꾸고, user라는 단어가 보이는 곳마다 전부 assistant로 바꿔줍니다.

이제 세 번째 헬퍼 함수입니다. 아래에 있는 messages.create 호출을 가져와 이름을 chat으로 바꿉니다. chat을 호출할 때마다 메시지 목록을 넘기는데, 이게 제 메시지 히스토리가 됩니다. 그런 다음 호출 부분을 들여쓰고, messages 인자를 함수로 넘어온 messages로 바꾼 뒤, 이 함수에서 message.content[0].text를 반환합니다.

이렇게 헬퍼 함수 세 개가 완성됐습니다. 다시 말하지만 이 함수들은 과정의 나머지 부분에서 꽤 자주 쓰게 됩니다. 이 함수들 덕분에 시간이 지나도 히스토리나 컨텍스트를 유지하는 대화를 훨씬 쉽게 다룰 수 있습니다. 이제 어떻게 활용하는지 보여드리겠습니다.

아래 다음 셀에 대화 히스토리를 유지하는 과정을 안내해줄 주석 몇 개를 적습니다. 먼저 빈 메시지 목록을 만듭니다. 이 messages 변수가 대화 히스토리 전체를 저장한다고 보면 됩니다. 시간이 지나면서 여기에 여러 user 메시지와 assistant 메시지를 더해갑니다.

다음으로 초기 user 메시지를 추가합니다. add_user_message 함수를 호출해 메시지를 더할 목록을 넘기고, user 텍스트는 "양자 컴퓨팅을 한 문장으로 정의해줘"로 합니다. 제대로 가고 있는지 확인하려고 메시지 목록을 출력하고 셀을 실행합니다. 보면 곧바로 올바른 메시지 구조가 나옵니다. 리스트 안에 role이 user이고 Claude에 넣고 싶은 내용을 담은 딕셔너리가 들어 있습니다.

이제 방금 만든 chat 함수로 Claude를 손쉽게 호출할 수 있습니다. chat을 호출하면서 메시지 목록을 넘기면 어떤 답변이 돌아옵니다. 그 답변을 출력하고 셀을 다시 실행하면 양자 컴퓨팅에 관한 문장을 보게 됩니다. 이제 우리는 Claude에 초기 메시지를 보내고 그 응답으로 assistant 메시지를 돌려받은 상황입니다.

이제 이 답변을 대화 히스토리에 추가해야 합니다. 방금 정의한 add_assistant_message 함수로 추가합니다. add_assistant_message를 호출해 메시지 목록에, 구체적으로는 방금 돌려받은 answer의 내용을 더합니다. 다시 한번 메시지 목록이 제대로인지 확인합니다. messages를 출력하면 user 메시지, 그다음 Claude에서 받은 내용이 담긴 후속 assistant 메시지가 보입니다. 좋습니다.

이제 마지막 단계입니다. user 메시지를 하나 더 추가하고 대화 히스토리 전체를 다시 Claude에 보냅니다. add_user_message로 메시지 목록에 더하고, 후속 요청은 "문장 하나 더 써줘"로 합니다. 그런 다음 업데이트된 메시지 목록으로 chat을 다시 호출해 answer에 할당하고 출력합니다. 실행해서 결과를 봅시다. 잠깐 멈춘 뒤, 분명히 양자 컴퓨팅에 관한 후속 메시지가 돌아옵니다. 대화 히스토리 전체를 제대로 유지한 것으로 보입니다.

이걸로 꽤 잘 됐습니다. 이제 과정의 나머지 부분에서 계속 사용할 재사용 가능한 헬퍼 함수 세 개를 갖게 됐습니다.

이 장에서 배우는 것What you'll learn

약 9분
1

API와 Claude는 메시지를 저장하지 않는다 — 매 요청은 독립적

2

대화를 이어가는 두 가지 — 메시지 목록 직접 관리 + 매번 전체 전송

3

같은 후속 질문도 히스토리 유무에 따라 답이 갈린다

4

헬퍼 셋 — add_user_message · add_assistant_message · chat

5

빈 목록에서 시작해 한 바퀴 도는 대화 루프 완성

왜 대화가 저절로 안 이어지나

Why conversations don't continue on their own

지금까지 만든 코드는 한 번의 단순한 교환을 흉내 냅니다. 질문 하나를 보내고 답 하나를 받는 것이죠.

단순한 한 번의 교환 · [전송]을 눌러 보세요

자연스럽게 이 대화를 이어가고 싶어집니다. "문장 하나 더 써줘" 같은 후속 질문을 보내, 앞 답을 더 확장한 응답을 기대하게 됩니다. 그런데 여기서 막힙니다.

대화가 저절로 이어지지 않는 이유
  • Anthropic API와 Claude는 여러분이 보낸 메시지도, 돌려준 응답도 저장하지 않습니다.
  • 그래서 매 요청은 완전히 독립적입니다 — 직전에 무슨 말을 했는지 Claude는 알지 못합니다.
  • 대화를 이어가려면 두 가지가 필요합니다. ① 코드에서 메시지 목록을 직접 관리하고, ② 후속 요청마다 그 전체 목록을 함께 보냅니다.

정말 그런지 직접 확인해 봅니다. 앞서 양자 컴퓨팅을 물어본 뒤, 직전 대화는 빼고 후속 질문만 따로 보내 봅니다.

히스토리 없는 후속 요청 · 노트북 셀
# (첫 셀에서 "양자 컴퓨팅이 뭐야?"를 이미 물어봤다고 합시다.)

# 두 번째 요청 — 후속 질문만 담고, 직전 대화는 함께 보내지 않습니다
message = client.messages.create(
    model=model,
    max_tokens=1000,
    messages=[
        {"role": "user", "content": "Write another sentence"}
    ]
)

message.content[0].text
그 응답 · 노트북 셀
# 양자 컴퓨팅과는 전혀 무관한 문장이 돌아옵니다
'The vibrant sunset painted the sky with hues of orange and purple, casting a warm glow over the peaceful countryside.'

양자 컴퓨팅과는 아무 상관 없는 문장이 돌아옵니다. Claude에는 직전 질문의 기억이 없으니, "문장 하나 더"라는 요청만 받아 임의의 문장을 만들어낸 것입니다.

메시지를 쌓아 맥락 유지하기

Maintaining context

해결책은 단순합니다. 주고받은 메시지를 목록에 차곡차곡 쌓고, 후속 요청 때마다 그 목록 전체를 보내는 것입니다.

흐름은 이렇습니다. 먼저 user 메시지 하나로 요청을 보냅니다. 돌아온 assistant 응답을 목록에 추가합니다. 이어서 후속 user 메시지를 또 추가합니다. 그리고 이 늘어난 목록 전체를 다시 보냅니다 — 그러면 Claude가 앞선 맥락을 전부 보고 답할 수 있습니다.

대화 흐름 · 단계를 눌러 메시지가 쌓이는 걸 보세요
Messages In Request
userDefine quantum computing in one sentence
assistantQuantum computing is a type of computation…
userWrite another sentence
assistantLeveraging the bizarre properties of quantum physics…
Claude

헬퍼 함수 세 개

Three helper functions

매번 손으로 메시지 딕셔너리를 만들어 목록에 넣는 건 번거롭습니다. 그래서 작은 헬퍼 함수 세 개를 만들어 둡니다. 이 과정 내내 계속 쓰게 됩니다.

헬퍼 함수 세 개 · 노트북 셀
def add_user_message(messages, text):
    user_message = {"role": "user", "content": text}
    messages.append(user_message)


def add_assistant_message(messages, text):
    assistant_message = {"role": "assistant", "content": text}
    messages.append(assistant_message)


def chat(messages):
    message = client.messages.create(
        model=model,
        max_tokens=1000,
        messages=messages,
    )
    return message.content[0].text

각 함수가 하는 일은 다음과 같습니다.

헬퍼 함수하는 일
add_user_message전달받은 텍스트를 {"role": "user", "content": text} 형태로 만들어 메시지 목록 끝에 추가합니다.
add_assistant_message같은 일을 하되 role"assistant"입니다. Claude가 돌려준 답을 목록에 넣을 때 씁니다.
chat현재 메시지 목록 전체create()로 보내고, 응답에서 content[0].text만 꺼내 돌려줍니다.
왜 함수로?

대화가 길어질수록 메시지를 추가하고 보내는 일이 반복됩니다. 이 셋만 있으면 "메시지 추가 → chat 호출 → 답을 다시 추가"를 짧게 되풀이할 수 있습니다.

전부 합치기

Putting it all together

이제 세 함수로 전체 대화 흐름을 짜 봅니다. 빈 목록에서 시작해 질문을 넣고, 답을 받고, 그 답을 목록에 넣고, 후속 질문을 더한 뒤, 늘어난 목록으로 다시 호출합니다.

전부 합치기 · 노트북 셀
# 시작 메시지 목록 만들기
messages = []

# 초기 user 질문 추가 — "Define quantum computing in one sentence"
add_user_message(messages, "Define quantum computing in one sentence")

# 메시지 목록을 chat에 넘겨 답변 받기
answer = chat(messages)

# 받은 답변을 assistant 메시지로 목록에 추가
add_assistant_message(messages, answer)

# user의 후속 질문 추가
add_user_message(messages, "Write another sentence")

# 업데이트된 목록으로 chat을 다시 호출해 최종 답변 받기
answer = chat(messages)
answer

실행하면 잠깐 멈춘 뒤 답이 돌아옵니다. 같은 후속 질문인데도 히스토리를 함께 보냈을 때와 아닐 때 결과가 어떻게 갈리는지 직접 눌러 비교해 보세요.

같은 후속 질문, 히스토리만 다르게 · 버튼을 눌러 비교하세요
후속 요청은 두 경우 모두 똑같습니다 — "Write another sentence"
맥락을 들고 있으면 달라진다
  • 이제 "Write another sentence"라는 똑같은 후속 질문이 양자 컴퓨팅을 이어 확장하는 답으로 돌아옵니다.
  • 달라진 건 질문이 아니라, 그 앞에 쌓인 메시지 목록 전체를 함께 보냈다는 점뿐입니다.
  • 방금 만든 세 함수는 이 과정의 나머지 부분에서 계속 재사용합니다 — 대화를 다루는 기본 도구입니다.

스스로 점검

Quick check

Q1Anthropic API와 Claude는 여러분이 보낸 메시지를 어떻게 다룰까요?

Q2대화를 이어가려면 후속 요청에 무엇을 보내야 할까요?

Q3Claude가 돌려준 답을 목록에 추가할 때 그 메시지의 role은?


LAB · 실습 콘솔MESSAGES ARRAY

메시지 배열 조립기

메시지 배열에 턴을 쌓아 보고, 전송될 요청 본문이 어떻게 자라는지 확인합니다.

index.js
// 메시지 배열 조립기 — Messages API는 대화를 저장하지 않는다.
// 매 요청마다 지금까지의 user·assistant 메시지를 배열로 함께 보낸다.

function addUserMessage(messages, text) {
  messages.push({ role: "user", content: text });
  return messages;
}

function addAssistantMessage(messages, text) {
  messages.push({ role: "assistant", content: text });
  return messages;
}

// user ↔ assistant 교대 규칙을 점검한다.
function checkAlternation(messages) {
  const problems = [];
  if (messages.length > 0 && messages[0].role !== "user") {
    problems.push("첫 메시지는 user 여야 합니다.");
  }
  for (let i = 1; i < messages.length; i++) {
    if (messages[i].role === messages[i - 1].role) {
      problems.push((i + 1) + "번째가 앞과 같은 role(" + messages[i].role + ")입니다.");
    }
  }
  return problems;
}

// ── 여기서부터 직접 고쳐 보세요 ──
const messages = [];
addUserMessage(messages, "부산 2박 3일 코스 짜줘");
addAssistantMessage(messages, "첫날은 해운대와 달맞이길을 추천합니다.");
addUserMessage(messages, "둘째 날은 비 예보가 있어. 실내 위주로 바꿔줘");

const problems = checkAlternation(messages);
console.log("메시지 " + messages.length + "개 · 교대 규칙 " +
  (problems.length ? "위반 " + problems.length + "건" : "정상"));
problems.forEach(function (p) { console.log("  · " + p); });

console.log("이번 요청으로 전송될 본문:");
console.log(JSON.stringify(
  { model: "claude-sonnet-4-6", max_tokens: 1024, messages: messages },
  null, 2
));