고객 지원 에이전트

고객 지원 에이전트 (Customer support agent)

이 가이드에서는 클로드로 고객 지원 챗봇을 만드는 방법을 단계별로 배울 수 있어요. 제품 질문에 답하고, 주제에서 벗어나지 않으며, 도구 사용(tool use)을 통해 견적을 생성하는 챗봇이죠. 이상적인 상호작용을 정의하고 각 작업으로 나누고, 성공 기준을 세우는 것부터 시작해 강력한 프롬프트와 도구, UI를 갖춘 실제 챗봇까지 구현해 볼 거예요.

출처: 문서

본문

전제 조건 (Prerequisites)

이 가이드를 따르려면 다음이 필요해요:

  • 클로드 API 키(ANTHROPIC_API_KEY 환경 변수로 설정)
  • Python 3.10 이상

필요한 패키지를 설치하세요:

pip install anthropic streamlit python-dotenv

클로드로 구축하기 전에 (Before building with Claude)

고객 지원 채팅에 클로드를 사용할지 결정하기

고객 지원 프로세스의 일부를 자동화하기 위해 클로드 같은 LLM을 써야 한다는 핵심 신호는 다음과 같아요:

클로드는 대량의 유사 질문을 효율적으로 처리하는 데 뛰어나며, 더 복잡한 문제는 인간 에이전트가 처리하도록 풀어 줘요. 클로드는 방대한 지식 베이스에서 정보를 빠르게 검색·처리·결합할 수 있는 반면, 인간 에이전트는 여러 소스를 조사하거나 상담하는 데 시간이 필요할 수 있어요. 클로드는 피로 없이 연중무휴 지원을 제공할 수 있지만, 지속적인 커버리지를 위해 인간 에이전트를 배치하는 것은 비싸고 어려울 수 있어요. 클로드는 추가 인력을 고용하고 훈련시킬 필요 없이 갑작스러운 문의량 증가를 처리할 수 있어요. 클로드에게 브랜드의 톤과 가치를 일관되게 표현하도록 지시할 수 있는 반면, 인간 에이전트는 커뮤니케이션 스타일이 다양할 수 있어요.

다른 LLM 대신 클로드를 선택할 때 고려할 사항:

  • 자연스럽고 미묘한 대화를 중시한다면: 클로드의 정교한 언어 이해는 다른 LLM과의 채팅보다 더 사람처럼 느껴지는 자연스럽고 컨텍스트를 인식하는 대화를 가능하게 해요.
  • 복잡하고 개방적인 질문을 자주 받는다면: 클로드는 사용자 발화의 조합을 광범위하게 프로그래밍할 필요 없이 캔드가 아닌 다양한 주제와 문의를 처리할 수 있어요.
  • 확장 가능한 다국어 지원이 필요하다면: 클로드의 다국어 능력은 각 지원 언어에 별도 챗봇이나 광범위한 번역 과정 없이 200개 이상의 언어로 대화할 수 있게 해요.

이상적인 채팅 상호작용 정의하기

이상적인 고객 상호작용을 개요로 작성해 고객이 클로드와 어떻게, 언제 상호작용하기를 기대하는지 정의하세요. 이 개요는 솔루션의 기술적 요구 사항을 결정하는 데 도움이 돼요.

다음은 자동차 보험 고객 지원을 위한 예시 채팅 상호작용이에요:

  • 고객: 지원 채팅 경험 시작

    • 클로드: 고객을 따뜻하게 맞이하고 대화 시작
  • 고객: 새 전기 자동차 보험 문의

    • 클로드: 전기차 보장에 대한 관련 정보 제공
  • 고객: 전기 자동차 보험의 고유한 요구와 관련된 질문

    • 클로드: 정확하고 유익한 답변 제공 및 출처 링크 제공
  • 고객: 보험이나 자동차와 무관한 주제 질문

    • 클로드: 관련 없는 주제는 논의하지 않는다고 분명히 하고 사용자를 자동차 보험으로 되돌림
  • 고객: 보험 견적에 관심 표현

    • 클로드: 적절한 견적을 결정하기 위한 일련의 질문을 하고, 응답에 적응
    • 클로드: 사용자로부터 수집한 필요한 정보와 함께 견적 생성 API 도구를 사용하라는 요청 전송
    • 클로드: API 도구 사용에서 응답 정보를 받아 자연스러운 응답으로 종합하고 사용자에게 제공된 견적 제시
  • 고객: 후속 질문

    • 클로드: 필요에 따라 후속 질문에 답변
    • 클로드: 보험 절차의 다음 단계로 고객을 안내하고 대화 마무리
자신의 유스 케이스에 대해 실제 예시를 작성할 때, 이 상호작용의 실제 말을 적어보면 원하는 톤, 응답 길이, 세부 수준에 대한 감을 잡을 수 있어 유용할 수 있어요.

상호작용을 고유한 작업으로 나누기

고객 지원 채팅은 질문 답변부터 정보 검색, 요청 처리에 이르는 여러 작업이 하나의 고객 상호작용으로 묶인 집합이에요. 구축을 시작하기 전에 이상적인 고객 상호작용을 클로드가 수행할 수 있게 하려는 모든 작업으로 분해하세요. 이렇게 하면 모든 작업에 대해 클로드를 프롬프트하고 평가할 수 있고, 테스트 케이스를 작성할 때 고려해야 할 상호작용 범위에 대한 좋은 감을 얻을 수 있어요.

고객들은 사용자 요청에 따른 대화의 가능한 전환점을 상호작용 순서도로 시각화하는 것이 도움이 되기도 해요.

예시 보험 상호작용과 관련된 핵심 작업은 다음과 같아요:

  1. 인사와 일반 안내

    • 고객을 따뜻하게 맞이하고 대화 시작
    • 회사와 상호작용에 대한 일반 정보 제공
  2. 제품 정보

    • 전기 자동차 보장에 대한 정보 제공 이를 위해서는 클로드가 필요한 정보를 컨텍스트에 갖고 있어야 하며, RAG 통합이 필요할 수 있음을 의미해요.
    • 고유한 전기 자동차 보험 요구와 관련된 질문에 답변
    • 견적이나 보험 세부 사항에 대한 후속 질문에 답변
    • 적절할 때 출처 링크 제공
  3. 대화 관리

    • 주제 유지(자동차 보험)
    • 주제 밖 질문을 관련 주제로 되돌리기
  4. 견적 생성

    • 견적 자격을 결정하기 위한 적절한 질문
    • 고객 응답에 따라 질문 적응
    • 수집한 정보를 견적 생성 API에 제출
    • 제공된 견적을 고객에게 제시

성공 기준 세우기

지원 팀과 협력해 측정 가능한 벤치마크와 목표로 성공 기준을 정의하고 상세 평가를 작성하세요.

다음은 정의한 작업을 클로드가 얼마나 성공적으로 수행하는지 평가하는 데 사용할 수 있는 기준과 벤치마크예요:

이 지표는 클로드가 다양한 주제에 걸친 고객 문의를 얼마나 정확하게 이해하는지 평가해요. 대화 샘플을 검토하고 클로드가 고객 의도, 중요한 다음 단계, 성공적인 해결이 무엇인지 등을 올바르게 해석하는지 평가해 측정해요. 이해 정확도 95% 이상을 목표로 하세요. 이는 클로드의 응답이 고객의 특정 질문이나 문제를 얼마나 잘 다루는지 평가해요. 대화 세트를 평가하고 각 응답의 관련성에 점수를 매겨요(확장하려면 LLM 기반 채점 사용). 관련성 점수 90% 이상을 목표로 하세요. 클로드가 컨텍스트로 제공받은 정보를 기반으로 사용자에게 제공한 일반 회사·제품 정보의 정확성을 평가해요. 이 소개 정보에서 100% 정확도를 목표로 하세요. 제공되는 링크나 출처의 빈도와 관련성을 추적해요. 추가 정보가 유익할 수 있는 상호작용의 80%에서 관련 출처를 제공하는 것을 목표로 하세요. 클로드가 예시 구현의 자동차 보험 같은 주제를 얼마나 잘 유지하는지 측정해요. 응답의 95%가 자동차 보험이나 고객의 특정 질문과 직접 관련되도록 목표하세요. 클로드가 정보 콘텐츠를 생성해야 할 때를 결정하고 그 콘텐츠가 얼마나 관련성 있는지 측정해요. 예를 들어 이 구현에서는 클로드가 언제 견적을 생성해야 하는지 이해하고 그 견적이 얼마나 정확한지 측정하게 돼요. 성공적인 고객 상호작용에 필수인 정보이므로 100% 정확도를 목표로 하세요. 이는 클로드가 질문에 인간 개입이 필요한 때를 인식하고 적절히 에스컬레이션하는 능력을 측정해요. 올바르게 에스컬레이션된 대화 비율을 에스컬레이션되었어야 했지만 그렇지 못한 대화와 비교해 추적해요. 에스컬레이션 정확도 95% 이상을 목표로 하세요.

지원에 클로드를 사용하는 것의 비즈니스 영향을 평가하는 데 사용할 수 있는 기준과 벤치마크:

이는 클로드가 대화 전반에 걸쳐 고객 감정을 유지하거나 개선하는 능력을 평가해요. 감정 분석 도구로 각 대화의 시작과 끝에서 감정을 측정해요. 상호작용의 90%에서 감정 유지 또는 개선을 목표로 하세요. 인간 개입 없이 챗봇이 성공적으로 처리한 고객 문의의 비율. 문의 복잡성에 따라 보통 70-80% 디플렉션율을 목표로 해요. 고객이 챗봇 상호작용에 얼마나 만족하는지에 대한 측정. 보통 상호작용 후 설문으로 진행해요. CSAT 점수 5점 만점에 4점 이상을 목표로 하세요. 챗봇이 문의를 해결하는 데 걸리는 평균 시간. 이슈 복잡성에 따라 크게 다르지만, 일반적으로 인간 에이전트보다 낮은 AHT를 목표로 하세요.

클로드를 고객 서비스 에이전트로 구현하는 방법

올바른 클로드 모델 선택하기

모델 선택은 비용, 정확도, 응답 시간 사이의 트레이드오프에 달려 있어요.

고객 지원 채팅의 경우 Claude Opus 5는 긴 다단계 대화에 걸친 깊은 추론이 필요한 가장 복잡한 지원 시나리오를 포함해 지능, 지연 시간, 비용의 균형을 잡기에 잘 맞아요. 하지만 RAG, 도구 사용, 긴 컨텍스트 프롬프트를 포함한 여러 프롬프트가 있는 대화 흐름이라면 지연 시간 최적화를 위해 Claude Haiku 4.5가 더 적합할 수 있어요.

강력한 프롬프트 만들기

고객 지원에 클로드를 사용하려면 클로드가 적절히 응답할 수 있을 만큼 충분한 지시와 컨텍스트를 가지면서도, 광범위한 고객 문의를 처리할 만큼 충분한 유연성을 가져야 해요.

먼저 강력한 프롬프트의 요소를 작성하는 것부터 시작하되, 시스템 프롬프트로 시작해요. config.py라는 파일을 만들고 다음 각 블록을 추가하세요:

IDENTITY = """You are Eva, a friendly and knowledgeable AI assistant for Acme Insurance
Company. Your role is to warmly welcome customers and provide information on
Acme's insurance offerings, which include car insurance and electric car
insurance. You can also help customers get quotes for their insurance needs."""
모든 정보를 시스템 프롬프트에 넣어 지시를 사용자 대화와 분리하고 싶을 수도 있지만, 클로드는 실제로 첫 `User` 턴 안에 프롬프트 콘텐츠의 대부분을 작성할 때 가장 잘 작동해요(유일한 예외는 역할 프롬프팅이에요). 자세한 내용은 [시스템 프롬프트로 클로드에게 역할 부여](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#give-claude-a-role)를 읽어 보세요.

복잡한 프롬프트는 하위 섹션으로 나누어 한 번에 한 부분씩 작성하는 것이 좋아요. 각 작업에 대해 클로드가 그 작업을 잘 수행하는 데 필요한 프롬프트 부분을 정의하는 단계별 과정을 따르면 더 나은 결과를 얻을 수 있어요. 이 자동차 보험 고객 지원 예시에서는 "인사와 일반 안내" 작업부터 시작해 프롬프트의 모든 부분을 조금씩 작성할 거예요. 이렇게 하면 전체 프롬프트의 개별 부분을 더 빠르게 조정할 수 있어 디버깅도 더 쉬워져요.

STATIC_GREETINGS_AND_GENERAL = """
<static_context>
Acme Auto Insurance: Your Trusted Companion on the Road

About:
At Acme Insurance, we understand that your vehicle is more than just a mode of transportation—it's your ticket to life's adventures.
Since 1985, we've been crafting auto insurance policies that give drivers the confidence to explore, commute, and travel with peace of mind.
Whether you're navigating city streets or embarking on cross-country road trips, Acme is there to protect you and your vehicle.
Our innovative auto insurance policies are designed to adapt to your unique needs, covering everything from fender benders to major collisions.
With Acme's award-winning customer service and swift claim resolution, you can focus on the joy of driving while we handle the rest.
We're not just an insurance provider—we're your co-pilot in life's journeys.
Choose Acme Auto Insurance and experience the assurance that comes with superior coverage and genuine care. Because at Acme, we don't just
insure your car—we fuel your adventures on the open road.

Note: We also offer specialized coverage for electric vehicles, ensuring that drivers of all car types can benefit from our protection.

Acme Insurance offers the following products:
- Car insurance
- Electric car insurance
- Two-wheeler insurance

Business hours: Monday-Friday, 9 AM - 5 PM EST
Customer service number: 1-800-123-4567
</static_context>

"""

자동차 보험과 전기 자동차 보험 정보에도 같은 작업을 하세요.

STATIC_CAR_INSURANCE = """
<static_context>
Car Insurance Coverage:
Acme's car insurance policies typically cover:
1. Liability coverage: Pays for bodily injury and property damage you cause to others.
2. Collision coverage: Pays for damage to your car in an accident.
3. Comprehensive coverage: Pays for damage to your car from non-collision incidents.
4. Medical payments coverage: Pays for medical expenses after an accident.
5. Uninsured/underinsured motorist coverage: Protects you if you're hit by a driver with insufficient insurance.

Optional coverages include:
- Rental car reimbursement
- Roadside assistance
- New car replacement
</static_context>

"""

STATIC_ELECTRIC_CAR_INSURANCE = """
<static_context>
Electric Car Insurance:
Our specialized electric car insurance goes beyond traditional auto coverage,
offering tailored protection for your high-tech investment. In addition to standard coverages, we offer:
- Safeguarding expensive battery systems
- Covering home charging equipment
- Adapting to the rapidly evolving world of electric vehicles

Whether you're driving a Rivian, Nissan Leaf, Chevrolet Bolt,
or any other electric vehicle, our knowledgeable agents are ready to craft a policy
that gives you the confidence to enjoy every electron-powered mile.
</static_context>

"""

정적 콘텐츠를 갖췄으니, 클로드의 응답을 안내할 "좋은" 상호작용 샘플을 최소 4-5개 추가하세요. 이 예시들은 이상적인 고객 상호작용을 대표해야 하며 가드레일과 도구 호출 같은 요소를 포함할 수 있어요.

EXAMPLES = """
Here are a few examples of how you can interact with customers:

<example 1>
H: Hi, do you offer commercial insurance for small businesses?

A: Ah! Unfortunately, we don't offer commercial insurance at this time.
However, we do provide a range of personal insurance products including car insurance and electric car insurance.
Would you like to know more about these offerings?
</example 1>

<example 2>
H: Hi, what are your business hours?

A: Our business hours are Monday to Friday, 9 AM to 5 PM EST.
If you have any questions or need assistance, feel free to reach out during those times.
How else can I assist you today?
</example 2>

<example 3>
H: I'm shopping for car insurance. What does Acme offer?

A: Great choice! Acme's car insurance policies typically include:
- Liability coverage: Protects you if you're at fault in an accident
- Collision coverage: Pays for damage to your car in an accident
- Comprehensive coverage: Covers non-collision incidents like theft or vandalism
- Medical payments coverage: Pays for medical expenses after an accident
- Uninsured/underinsured motorist coverage: Protects you from drivers with insufficient insurance

We also offer optional coverages like rental car reimbursement, roadside assistance, and new car replacement.
Would you like more details on any of these coverages, or do you have specific questions about insuring your vehicle?
</example 3>

<example 4>
H: I'd like to hear about your electric car insurance.

A: Certainly! Our electric car insurance is specially designed to meet the unique needs of electric vehicle owners.
In addition to all the standard coverages we offer for regular cars, our electric car policies include:

- Battery coverage: Protection against damage or degradation of your vehicle's expensive battery system
- Tech package: Covers repairs or replacement of advanced electronics and onboard computer systems
- Charging station coverage: Protects both home and portable charging equipment
- Cybersecurity protection: Coverage against hacking or digital threats
- Specialized roadside assistance: Includes flatbed towing to protect the vehicle's undercarriage

We cover a wide range of electric vehicles, including popular models from Tesla, Rivian, Nissan, Chevrolet, and more.
Is there a specific electric vehicle you're looking to insure, or would you like more details on any of these coverages?
</example 4>

<example 5>
H: I'd like to get a quote for my car insurance.

A: Certainly! I'd be happy to help you get a quote for your car insurance.
To provide you with an accurate quote, I'll need to collect some information about your vehicle and the primary driver.
Let's start with the basics:

1. What is the make and model of your vehicle?
2. What year was it manufactured?
3. Approximately how many miles have you driven?
4. What is the age of the primary driver?

Once you provide this information, I'll use our quoting tool to generate a personalized insurance quote for you.
</example 5>

"""

또한 클로드가 고객과 어떻게 상호작용해야 하는지에 대한 금지 사항을 설명하는 중요한 지시를 포함하고 싶을 거예요. 이는 브랜드 가드레일이나 지원 정책에서 가져올 수 있어요.

ADDITIONAL_GUARDRAILS = """Please adhere to the following guardrails:
1. Only provide information about insurance types listed in our offerings.
2. If asked about an insurance type we don't offer, politely state
that we don't provide that service.
3. Do not speculate about future product offerings or company plans.
4. Don't make promises or enter into agreements it's not authorized to make.
You only provide information and guidance.
5. Do not mention any competitor's products or services.
"""

이제 이 모든 섹션을 단일 문자열로 결합해 프롬프트로 사용하세요.

TASK_SPECIFIC_INSTRUCTIONS = " ".join(
    [
        STATIC_GREETINGS_AND_GENERAL,
        STATIC_CAR_INSURANCE,
        STATIC_ELECTRIC_CAR_INSURANCE,
        EXAMPLES,
        ADDITIONAL_GUARDRAILS,
    ]
)

도구 사용으로 동적·에이전트 기능 추가하기

클로드는 클라이언트 측 도구 사용 기능으로 동적으로 조치를 취하고 정보를 검색할 수 있어요. 먼저 프롬프트가 사용해야 할 외부 도구나 API를 나열하세요.

이 예시에서는 견적을 계산하는 도구 하나로 시작해요.

다시 강조하지만, 이 도구는 실제 계산을 수행하지 않고, 지정된 인수로 도구를 사용해야 한다는 신호를 애플리케이션에 보내는 것뿐이에요.

모델 이름, 도구 정의, 스텁 구현을 config.py에 추가하세요:

import time

MODEL = "claude-opus-5-5"

TOOLS = [
    {
        "name": "get_quote",
        "description": "Calculate the insurance quote based on user input. Returned value is per month premium.",
        "input_schema": {
            "type": "object",
            "properties": {
                "make": {"type": "string", "description": "The make of the vehicle."},
                "model": {"type": "string", "description": "The model of the vehicle."},
                "year": {
                    "type": "integer",
                    "description": "The year the vehicle was manufactured.",
                },
                "mileage": {
                    "type": "integer",
                    "description": "The mileage on the vehicle.",
                },
                "driver_age": {
                    "type": "integer",
                    "description": "The age of the primary driver.",
                },
            },
            "required": ["make", "model", "year", "mileage", "driver_age"],
        },
    }
]


def get_quote(make, model, year, mileage, driver_age):
    """Returns the premium per month in USD"""
    # You can call an http endpoint or a database to get the quote.
    # Here, we simulate a delay of 1 seconds and return a fixed quote of 100.
    time.sleep(1)
    return 100

프롬프트 배포하기

프롬프트가 얼마나 잘 작동하는지 테스트 프로덕션 환경에 배포하고 평가를 실행하지 않고는 알기 어려워요. 프롬프트, Anthropic SDK, Streamlit을 사용자 인터페이스로 사용해 작은 애플리케이션을 만드세요.

chatbot.py라는 파일(또는 언어에 해당하는 모듈)에서 Anthropic SDK와의 상호작용을 캡슐화할 ChatBot 클래스를 설정하세요.

클래스에는 두 개의 주요 메서드가 있어야 해요. 하나는 API를 호출해 메시지를 생성하는 것이고, 다른 하나는 들어오는 각 사용자 입력을 처리하는 것이에요.

```python Python # In your chatbot.py, import these from the config.py you wrote above: # from config import IDENTITY, TOOLS, MODEL, get_quote from anthropic import Anthropic from dotenv import load_dotenv

load_dotenv()

class ChatBot: def init(self, session_state): self.anthropic = Anthropic() self.session_state = session_state

  def generate_message(
      self,
      messages,
      max_tokens,
  ):
      try:
          response = self.anthropic.messages.create(
              model=MODEL,
              system=IDENTITY,
              max_tokens=max_tokens,
              messages=messages,
              tools=TOOLS,
          )
          return response
      except Exception as e:
          return {"error": str(e)}

  def process_user_input(self, user_input):
      self.session_state.messages.append({"role": "user", "content": user_input})

      response_message = self.generate_message(
          messages=self.session_state.messages,
          max_tokens=2048,
      )

      if "error" in response_message:
          return f"An error occurred: {response_message['error']}"

      if response_message.content[-1].type == "tool_use":
          tool_use = response_message.content[-1]
          func_name = tool_use.name
          func_params = tool_use.input
          tool_use_id = tool_use.id

          result = self.handle_tool_use(func_name, func_params)
          self.session_state.messages.append(
              {"role": "assistant", "content": response_message.content}
          )
          self.session_state.messages.append(
              {
                  "role": "user",
                  "content": [
                      {
                          "type": "tool_result",
                          "tool_use_id": tool_use_id,
                          "content": f"{result}",
                      }
                  ],
              }
          )

          follow_up_response = self.generate_message(
              messages=self.session_state.messages,
              max_tokens=2048,
          )

          if "error" in follow_up_response:
              return f"An error occurred: {follow_up_response['error']}"

          response_text = next(
              (block.text for block in follow_up_response.content if block.type == "text"),
              None,
          )
          if response_text is None:
              raise Exception("An error occurred: Unexpected response type")
          self.session_state.messages.append(
              {"role": "assistant", "content": response_text}
          )
          return response_text

      text_block = next(
          (block for block in response_message.content if block.type == "text"), None
      )
      if text_block is not None:
          response_text = text_block.text
          self.session_state.messages.append(
              {"role": "assistant", "content": response_text}
          )
          return response_text

      raise Exception("An error occurred: Unexpected response type")

  def handle_tool_use(self, func_name, func_params):
      if func_name == "get_quote":
          premium = get_quote(**func_params)
          return f"Quote generated: ${premium:.2f} per month"

      raise Exception("An unexpected tool was used")

```typescript TypeScript
import Anthropic from "@anthropic-ai/sdk";

class ChatBot {
  // IDENTITY, MODEL, TOOLS, and getQuote mirror the config.py values defined
  // earlier in this guide (shown in Python).
  readonly anthropic = new Anthropic();
  readonly messages: Anthropic.MessageParam[] = [];

  async generateMessage(
    messages: Anthropic.MessageParam[],
    maxTokens: number
  ): Promise<Anthropic.Message> {
    return this.anthropic.messages.create({
      model: MODEL,
      system: IDENTITY,
      max_tokens: maxTokens,
      messages,
      tools: TOOLS
    });
  }

  async processUserInput(userInput: string): Promise<string> {
    this.messages.push({ role: "user", content: userInput });

    const responseMessage = await this.generateMessage(this.messages, 2048);

    const lastBlock = responseMessage.content.at(-1);
    if (lastBlock?.type === "tool_use") {
      const toolResult = this.handleToolUse(lastBlock.name, lastBlock.input);

      this.messages.push({ role: "assistant", content: responseMessage.content });
      this.messages.push({
        role: "user",
        content: [{ type: "tool_result", tool_use_id: lastBlock.id, content: toolResult }]
      });

      const followUpResponse = await this.generateMessage(this.messages, 2048);

      const followUpBlock = followUpResponse.content.find(
        (block): block is Anthropic.TextBlock => block.type === "text"
      );
      if (!followUpBlock) {
        throw new Error("An error occurred: Unexpected response type");
      }
      this.messages.push({ role: "assistant", content: followUpBlock.text });
      return followUpBlock.text;
    }

    const firstBlock = responseMessage.content.find(
      (block): block is Anthropic.TextBlock => block.type === "text"
    );
    if (firstBlock) {
      this.messages.push({ role: "assistant", content: firstBlock.text });
      return firstBlock.text;
    }

    throw new Error("An error occurred: Unexpected response type");
  }

  handleToolUse(toolName: string, toolInput: unknown): string {
    if (toolName === "get_quote") {
      // The SDK types tool_use.input as unknown; narrow it to the get_quote schema.
      if (
        toolInput === null ||
        typeof toolInput !== "object" ||
        !("make" in toolInput) || typeof toolInput.make !== "string" ||
        !("model" in toolInput) || typeof toolInput.model !== "string" ||
        !("year" in toolInput) || typeof toolInput.year !== "number" ||
        !("mileage" in toolInput) || typeof toolInput.mileage !== "number" ||
        !("driver_age" in toolInput) || typeof toolInput.driver_age !== "number"
      ) {
        throw new Error("An error occurred: Unexpected tool input");
      }
      const { make, model: vehicleModel, year, mileage, driver_age: driverAge } = toolInput;

      const premium = getQuote(make, vehicleModel, year, mileage, driverAge);
      return `Quote generated: $${premium.toFixed(2)} per month`;
    }

    throw new Error("An unexpected tool was used");
  }
}
using System.Text.Json;
using Anthropic;
using Anthropic.Models.Messages;

// Config.Model, Config.Identity, Config.Tools, and Config.GetQuote mirror
// the config.py values defined earlier in this guide (shown in Python).
public class ChatBot
{
    private readonly AnthropicClient _anthropic = new();

    public List<MessageParam> Messages { get; } = [];

    public async Task<Message> GenerateMessage(List<MessageParam> messages, long maxTokens) =>
        await _anthropic.Messages.Create(
            new MessageCreateParams
            {
                Model = Config.Model,
                System = Config.Identity,
                MaxTokens = maxTokens,
                Messages = messages,
                Tools = Config.Tools,
            }
        );

    public async Task<string> ProcessUserInput(string userInput)
    {
        Messages.Add(new() { Role = Role.User, Content = userInput });

        var responseMessage = await GenerateMessage(Messages, maxTokens: 2048);

        if (responseMessage.Content[^1].TryPickToolUse(out var toolUse))
        {
            var toolResult = HandleToolUse(toolUse.Name, toolUse.Input);

            Messages.Add(new()
            {
                Role = Role.Assistant,
                Content = responseMessage.Content
                    .Select(contentBlock => new ContentBlockParam(contentBlock.Json))
                    .ToList(),
            });
            Messages.Add(new()
            {
                Role = Role.User,
                Content = new List<ContentBlockParam>
                {
                    new ToolResultBlockParam { ToolUseID = toolUse.ID, Content = toolResult },
                },
            });

            var followUpResponse = await GenerateMessage(Messages, maxTokens: 2048);

            foreach (var block in followUpResponse.Content)
            {
                if (block.TryPickText(out var followUpText))
                {
                    Messages.Add(new() { Role = Role.Assistant, Content = followUpText.Text });
                    return followUpText.Text;
                }
            }

            throw new InvalidOperationException("An error occurred: Unexpected response type");
        }

        foreach (var block in responseMessage.Content)
        {
            if (block.TryPickText(out var textBlock))
            {
                Messages.Add(new() { Role = Role.Assistant, Content = textBlock.Text });
                return textBlock.Text;
            }
        }

        throw new InvalidOperationException("An error occurred: Unexpected response type");
    }

    public string HandleToolUse(string funcName, IReadOnlyDictionary<string, JsonElement> funcParams)
    {
        if (funcName == "get_quote")
        {
            var premium = Config.GetQuote(
                funcParams["make"].GetString()!,
                funcParams["model"].GetString()!,
                funcParams["year"].GetInt64(),
                funcParams["mileage"].GetInt64(),
                funcParams["driver_age"].GetInt64()
            );
            return $"Quote generated: ${premium:F2} per month";
        }

        throw new ArgumentException("An unexpected tool was used");
    }
}
import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/anthropics/anthropic-sdk-go"
)

// ChatBot wraps the Anthropic client and the conversation history. The
// identity, model, tools, and getQuote values it uses mirror the config.py
// definitions earlier in this guide (shown in Python).
type ChatBot struct {
	client   anthropic.Client
	messages []anthropic.MessageParam
}

func NewChatBot() *ChatBot {
	return &ChatBot{client: anthropic.NewClient()}
}

func (bot *ChatBot) GenerateMessage(ctx context.Context, messages []anthropic.MessageParam, maxTokens int64) (*anthropic.Message, error) {
	return bot.client.Messages.New(ctx, anthropic.MessageNewParams{
		Model:     model,
		System:    []anthropic.TextBlockParam{{Text: identity}},
		MaxTokens: maxTokens,
		Messages:  messages,
		Tools:     tools,
	})
}

func (bot *ChatBot) ProcessUserInput(ctx context.Context, userInput string) (string, error) {
	bot.messages = append(bot.messages, anthropic.NewUserMessage(anthropic.NewTextBlock(userInput)))

	response, err := bot.GenerateMessage(ctx, bot.messages, 2048)
	if err != nil {
		return "", err
	}

	lastBlock := response.Content[len(response.Content)-1]
	if toolUse, ok := lastBlock.AsAny().(anthropic.ToolUseBlock); ok {
		result, err := bot.HandleToolUse(toolUse.Name, toolUse.Input)
		if err != nil {
			return "", err
		}

		bot.messages = append(bot.messages,
			response.ToParam(),
			anthropic.NewUserMessage(anthropic.NewToolResultBlock(toolUse.ID, result, false)),
		)

		followUp, err := bot.GenerateMessage(ctx, bot.messages, 2048)
		if err != nil {
			return "", err
		}

		for _, block := range followUp.Content {
			if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
				bot.messages = append(bot.messages, anthropic.NewAssistantMessage(anthropic.NewTextBlock(textBlock.Text)))
				return textBlock.Text, nil
			}
		}
		return "", fmt.Errorf("an error occurred: unexpected response type")
	}

	for _, block := range response.Content {
		if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
			bot.messages = append(bot.messages, anthropic.NewAssistantMessage(anthropic.NewTextBlock(textBlock.Text)))
			return textBlock.Text, nil
		}
	}

	return "", fmt.Errorf("an error occurred: unexpected response type")
}

func (bot *ChatBot) HandleToolUse(toolName string, toolInput json.RawMessage) (string, error) {
	if toolName != "get_quote" {
		return "", fmt.Errorf("an unexpected tool was used: %s", toolName)
	}

	var input struct {
		Make      string `json:"make"`
		Model     string `json:"model"`
		Year      int    `json:"year"`
		Mileage   int    `json:"mileage"`
		DriverAge int    `json:"driver_age"`
	}
	if err := json.Unmarshal(toolInput, &input); err != nil {
		return "", err
	}
	premium := getQuote(input.Make, input.Model, input.Year, input.Mileage, input.DriverAge)
	return fmt.Sprintf("Quote generated: $%.2f per month", premium), nil
}

import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.core.JsonValue;
import com.anthropic.models.messages.ContentBlock;
import com.anthropic.models.messages.ContentBlockParam;
import com.anthropic.models.messages.Message;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.MessageParam;
import com.anthropic.models.messages.ToolResultBlockParam;
import com.anthropic.models.messages.ToolUseBlock;

// IDENTITY, MODEL, TOOLS, and getQuote mirror the config.py values defined
// earlier in this guide (shown in Python).
class ChatBot {
    final AnthropicClient anthropic;
    final List<MessageParam> messages;

    ChatBot() {
        // Reads the API key from the ANTHROPIC_API_KEY environment variable
        this.anthropic = AnthropicOkHttpClient.fromEnv();
        this.messages = new ArrayList<>();
    }

    Message generateMessage(List<MessageParam> messages, long maxTokens) {
        return anthropic.messages().create(MessageCreateParams.builder()
                .model(MODEL)
                .system(IDENTITY)
                .maxTokens(maxTokens)
                .messages(messages)
                .tools(TOOLS)
                .build());
    }

    String processUserInput(String userInput) {
        messages.add(MessageParam.builder()
                .role(MessageParam.Role.USER)
                .content(userInput)
                .build());

        Message responseMessage = generateMessage(messages, 2048);

        List<ContentBlock> content = responseMessage.content();
        ContentBlock lastBlock = content.getLast();
        if (lastBlock.isToolUse()) {
            ToolUseBlock toolUse = lastBlock.asToolUse();
            Map<String, JsonValue> toolInput =
                    (Map<String, JsonValue>) toolUse._input().asObject().orElseThrow();
            String result = handleToolUse(toolUse.name(), toolInput);

            messages.add(MessageParam.builder()
                    .role(MessageParam.Role.ASSISTANT)
                    .contentOfBlockParams(content.stream().map(ContentBlock::toParam).toList())
                    .build());
            messages.add(MessageParam.builder()
                    .role(MessageParam.Role.USER)
                    .contentOfBlockParams(List.of(ContentBlockParam.ofToolResult(
                            ToolResultBlockParam.builder()
                                    .toolUseId(toolUse.id())
                                    .content(result)
                                    .build())))
                    .build());

            Message followUpResponse = generateMessage(messages, 2048);

            ContentBlock followUpBlock = followUpResponse.content().stream()
                    .filter(ContentBlock::isText)
                    .findFirst()
                    .orElseThrow(() -> new IllegalStateException("An error occurred: Unexpected response type"));
            String responseText = followUpBlock.asText().text();
            messages.add(MessageParam.builder()
                    .role(MessageParam.Role.ASSISTANT)
                    .content(responseText)
                    .build());
            return responseText;
        } else {
            ContentBlock textBlock = content.stream()
                    .filter(ContentBlock::isText)
                    .findFirst()
                    .orElseThrow(() -> new IllegalStateException("An error occurred: Unexpected response type"));
            String responseText = textBlock.asText().text();
            messages.add(MessageParam.builder()
                    .role(MessageParam.Role.ASSISTANT)
                    .content(responseText)
                    .build());
            return responseText;
        }
    }

    String handleToolUse(String funcName, Map<String, JsonValue> funcParams) {
        return switch (funcName) {
            case "get_quote" -> {
                double premium = getQuote(
                        funcParams.get("make").asStringOrThrow(),
                        funcParams.get("model").asStringOrThrow(),
                        ((Number) funcParams.get("year").asNumber().orElseThrow()).longValue(),
                        ((Number) funcParams.get("mileage").asNumber().orElseThrow()).longValue(),
                        ((Number) funcParams.get("driver_age").asNumber().orElseThrow()).longValue());
                yield "Quote generated: $%.2f per month".formatted(premium);
            }
            default -> throw new IllegalArgumentException("An unexpected tool was used");
        };
    }
}
use Anthropic\Client;
use Anthropic\Messages\Message;
use Anthropic\Messages\MessageParam;
use Anthropic\Messages\TextBlock;
use Anthropic\Messages\ToolResultBlockParam;
use Anthropic\Messages\ToolUseBlock;

class ChatBot
{
    // MODEL, IDENTITY, TOOLS, and get_quote() mirror the config.py values
    // defined earlier in this guide (shown in Python).

    /** @var list<MessageParam> */
    public private(set) array $messages = [];

    public function __construct(
        private readonly Client $anthropic = new Client(),
    ) {}

    /**
     * @param list<MessageParam> $messages
     */
    public function generateMessage(array $messages, int $maxTokens): Message
    {
        return $this->anthropic->messages->create(
            model: MODEL,
            system: IDENTITY,
            maxTokens: $maxTokens,
            messages: $messages,
            tools: TOOLS,
        );
    }

    public function processUserInput(string $userInput): string
    {
        $this->messages[] = MessageParam::with(role: 'user', content: $userInput);

        $responseMessage = $this->generateMessage($this->messages, maxTokens: 2048);

        $content = $responseMessage->content;
        $lastBlock = array_last($content);

        if ($lastBlock instanceof ToolUseBlock) {
            $toolResult = $this->handleToolUse($lastBlock->name, $lastBlock->input);

            $this->messages[] = MessageParam::with(role: 'assistant', content: $content);
            $this->messages[] = MessageParam::with(
                role: 'user',
                content: [
                    ToolResultBlockParam::with(toolUseID: $lastBlock->id, content: $toolResult),
                ],
            );

            $followUpResponse = $this->generateMessage($this->messages, maxTokens: 2048);

            $firstBlock = array_find(
                $followUpResponse->content,
                static fn ($block): bool => $block instanceof TextBlock,
            );
            if (!$firstBlock instanceof TextBlock) {
                throw new RuntimeException('An error occurred: Unexpected response type');
            }

            $this->messages[] = MessageParam::with(role: 'assistant', content: $firstBlock->text);

            return $firstBlock->text;
        }

        $firstBlock = array_find($content, static fn ($block): bool => $block instanceof TextBlock);
        if ($firstBlock instanceof TextBlock) {
            $this->messages[] = MessageParam::with(role: 'assistant', content: $firstBlock->text);

            return $firstBlock->text;
        }

        throw new RuntimeException('An error occurred: Unexpected response type');
    }

    /**
     * @param array<string, mixed> $funcParams
     */
    private function handleToolUse(string $funcName, array $funcParams): string
    {
        if ($funcName === 'get_quote') {
            $premium = get_quote(...$funcParams);

            return sprintf('Quote generated: $%.2f per month', $premium);
        }

        throw new RuntimeException('An unexpected tool was used');
    }
}
# IDENTITY, MODEL, TOOLS, and get_quote mirror the config.py values defined
# earlier in this guide (shown in Python).
require "anthropic"

class ChatBot
  attr_reader :messages

  def initialize
    @anthropic = Anthropic::Client.new
    @messages = []
  end

  def generate_message(messages, max_tokens)
    @anthropic.messages.create(
      model: MODEL,
      system_: IDENTITY,
      max_tokens:,
      messages:,
      tools: TOOLS
    )
  end

  def process_user_input(user_input)
    @messages << {role: "user", content: user_input}

    response_message = generate_message(@messages, 2048)

    case response_message.content
    in [*, Anthropic::ToolUseBlock => tool_use]
      result = handle_tool_use(tool_use.name, tool_use.input)
      @messages << {role: "assistant", content: response_message.content}
      @messages << {
        role: "user",
        content: [{type: "tool_result", tool_use_id: tool_use.id, content: result}]
      }

      follow_up_response = generate_message(@messages, 2048)

      case follow_up_response.content
      in [*, Anthropic::TextBlock => text_block, *]
        @messages << {role: "assistant", content: text_block.text}
        text_block.text
      else
        raise "An error occurred: Unexpected response type"
      end
    in [*, Anthropic::TextBlock => text_block, *]
      @messages << {role: "assistant", content: text_block.text}
      text_block.text
    else
      raise "An error occurred: Unexpected response type"
    end
  end

  def handle_tool_use(tool_name, tool_input)
    raise "An unexpected tool was used" unless tool_name == "get_quote"

    premium = get_quote(**tool_input)
    format("Quote generated: $%.2f per month", premium)
  end
end

사용자 인터페이스 만들기

메인 메서드로 Streamlit을 사용해 이 코드를 배포해 보세요. 이 main() 함수는 Streamlit 기반 채팅 인터페이스를 설정해요. Streamlit은 Python 프레임워크이므로 이 부분은 Python으로만 보여주며, 위의 ChatBot 클래스가 어떤 언어로도 이식할 수 있는 부분이에요.

app.py 파일에서 이렇게 하세요:

import streamlit as st
from chatbot import ChatBot
from config import TASK_SPECIFIC_INSTRUCTIONS


def main():
    st.title("Chat with Eva, Acme Insurance Company's Assistant🤖")

    if "messages" not in st.session_state:
        st.session_state.messages = [
            {"role": "user", "content": TASK_SPECIFIC_INSTRUCTIONS},
            {"role": "assistant", "content": "Understood"},
        ]

    chatbot = ChatBot(st.session_state)

    # Display user and assistant messages skipping the first two
    for message in st.session_state.messages[2:]:
        # ignore tool use blocks
        if isinstance(message["content"], str):
            with st.chat_message(message["role"]):
                st.markdown(message["content"])

    if user_msg := st.chat_input("Type your message here..."):
        st.chat_message("user").markdown(user_msg)

        with st.chat_message("assistant"):
            with st.spinner("Eva is thinking..."):
                response_placeholder = st.empty()
                full_response = chatbot.process_user_input(user_msg)
                response_placeholder.markdown(full_response)


if __name__ == "__main__":
    main()

프로그램을 다음으로 실행하세요:

streamlit run app.py

프롬프트 평가하기

프롬프트는 프로덕션 준비가 되려면 종종 테스트와 최적화가 필요해요. 솔루션의 준비 상태를 판단하려면 정량적·정성적 방법을 결합한 체계적인 과정으로 챗봇 성능을 평가하세요. 정의한 성공 기준을 기반으로 강력한 경험적 평가를 만들면 프롬프트를 최적화할 수 있어요.

성능 개선

복잡한 시나리오에서는 표준 프롬프트 엔지니어링 기법가드레일 구현 전략을 넘어 성능을 개선하기 위한 추가 전략을 고려하는 것이 도움이 될 수 있어요. 몇 가지 흔한 시나리오:

RAG로 긴 컨텍스트 지연 시간 줄이기

많은 양의 정적·동적 컨텍스트를 다룰 때 모든 정보를 프롬프트에 포함하면 높은 비용, 느린 응답 시간, 컨텍스트 창 한도 도달로 이어질 수 있어요. 이 시나리오에서는 RAG(검색 증강 생성) 기법을 구현하면 성능과 효율을 개선할 수 있어요.

Voyage 같은 임베딩 모델을 사용해 정보를 벡터 표현으로 변환하면 더 확장 가능하고 반응적인 시스템을 만들 수 있어요. 이 접근 방식은 모든 가능한 컨텍스트를 매 프롬프트에 포함하는 대신 현재 쿼리에 따라 관련 정보를 동적으로 검색하게 해요.

지원 유스 케이스에 RAG를 구현하면 광범위한 컨텍스트 요구가 있는 시스템에서 정확도를 높이고 응답 시간과 API 비용을 줄이는 것으로 나타났어요. 작동 예시는 RAG 레시피를 참고하세요.

도구 사용으로 실시간 데이터 통합

계좌 잔액이나 정책 세부 사항 같은 실시간 정보가 필요한 쿼리를 다룰 때는 임베딩 기반 RAG 접근만으로는 충분하지 않아요. 대신 도구 사용이 챗봇이 정확한 실시간 응답을 제공하는 능력을 향상시킬 수 있어요. 예를 들어 도구 사용으로 고객 정보를 조회하고, 주문 세부 사항을 검색하며, 고객을 대신해 주문을 취소할 수 있어요.

도구 사용: 고객 서비스 에이전트 레시피에 설명된 이 접근 방식은 실제 데이터를 클로드의 응답에 통합하고 더 개인화되고 효율적인 고객 경험을 제공하게 해요.

입력·출력 가드레일 강화

챗봇을 배포할 때, 특히 고객 서비스 시나리오에서 오용, 범위 밖 질문, 부적절한 응답과 관련된 위험을 방지하는 것이 중요해요. 클로드가 본래 그런 시나리오에 탄력적이지만, 챗봇 가드레일을 강화하기 위한 추가 단계는 다음과 같아요:

  • 환각 줄이기: 사실 확인 메커니즘과 인용(citations)을 구현해 응답을 제공된 정보에 근거하게 하세요.
  • 정보 교차 확인: 에이전트의 응답이 회사 정책과 알려진 사실과 일치하는지 검증하세요.
  • 계약적 약속 회피: 에이전트가 권한이 없는 약속을 하거나 계약을 맺지 않도록 하세요.
  • 제일브레이크 완화: 무해성 화면과 입력 검증 같은 방법으로 사용자가 모델 취약점을 악용해 부적절한 콘텐츠를 생성하는 것을 방지하세요.
  • 경쟁사 언급 회피: 브랜드 집중을 유지하고 어떤 경쟁사 제품·서비스도 언급하지 않도록 경쟁사 언급 필터를 구현하세요.
  • 출력 일관성 높이기: 길고 복잡한 상호작용 중에도 클로드가 스타일을 바꾸거나 캐릭터에서 벗어나지 않도록 하세요.
  • 개인 식별 정보(PII) 제거: 명시적으로 요구되고 승인되지 않는 한 응답에서 PII를 제거하세요.

스트리밍으로 인지된 응답 시간 줄이기

잠재적으로 긴 응답을 다룰 때 스트리밍을 구현하면 사용자 참여와 만족도를 높일 수 있어요. 이 시나리오에서 사용자는 전체 응답이 생성될 때까지 기다리는 대신 점진적으로 답을 받아요.

스트리밍 구현 방법:

  1. Anthropic Streaming API를 사용해 스트리밍 응답을 지원하세요.
  2. 프론트엔드가 들어오는 텍스트 청크를 처리하도록 설정하세요.
  3. 도착하는 각 청크를 표시해 실시간 타이핑을 시뮬레이션하세요.
  4. 전체 응답을 저장하는 메커니즘을 구현해 사용자가 나갔다 돌아와도 볼 수 있게 하세요.

어떤 경우에는 스트리밍이 기본 지연 시간이 더 높은 더 고급 모델의 사용을 가능하게 해요. 점진적 표시가 더 긴 처리 시간의 영향을 완화하기 때문이에요.

챗봇 확장

챗봇의 복잡성이 커짐에 따라 애플리케이션 아키텍처도 그에 맞춰 진화할 수 있어요. 아키텍처에 추가 계층을 넣기 전에 다음의 덜 망라적인 옵션을 고려하세요:

  • 프롬프트를 최대한 활용하고 프롬프트 엔지니어링으로 최적화하고 있는지 확인하세요. 프롬프트 엔지니어링 가이드를 사용해 가장 효과적인 프롬프트를 작성하세요.
  • 프롬프트에 추가 도구를 추가하고(프롬프트 체인 포함 가능) 필요한 기능을 달성할 수 있는지 확인하세요.

챗봇이 엄청나게 다양한 작업을 처리한다면 초기 고객 쿼리를 라우팅할 별도의 의도 분류기를 추가하는 것을 고려할 수 있어요. 기존 애플리케이션의 경우, 분류기를 통해 고객 쿼리를 라우팅한 다음 각각 자체 도구 세트와 시스템 프롬프트를 가진 전문화된 대화로 보내는 결정 트리를 만드는 것이 포함돼요. 이 방법은 지연 시간을 늘릴 수 있는 클로드에 대한 추가 호출이 필요하다는 점을 유의하세요.

지원 워크플로우에 클로드 통합하기

이 예시들은 Streamlit 환경 안에서 호출 가능한 Python 함수에 초점을 맞췄지만, 실시간 지원 챗봇에 클로드를 배포하려면 API 서비스가 필요해요.

이렇게 접근할 수 있어요:

  1. API 래퍼 만들기: 분류 함수 주위에 간단한 API 래퍼를 개발하세요. 예를 들어 Flask API나 Fast API를 사용해 코드를 HTTP 서비스로 감쌀 수 있어요. HTTP 서비스는 사용자 입력을 받아 어시스턴트 응답을 통째로 반환할 수 있어요. 따라서 서비스는 다음과 같은 특성을 가질 수 있어요:

    • Server-Sent Events(SSE): SSE는 서버에서 클라이언트로 응답의 실시간 스트리밍을 가능하게 해요. LLM과 작업할 때 매끄럽고 인터랙티브한 경험을 제공해요.
    • 캐싱: 캐싱 구현은 응답 시간을 개선하고 불필요한 API 호출을 줄일 수 있어요.
    • 컨텍스트 유지: 사용자가 나갔다 돌아왔을 때 컨텍스트를 유지하는 것은 대화의 연속성에 중요해요.
  2. 웹 인터페이스 만들기: Claude 기반 에이전트와 상호작용하기 위한 사용자 친화적인 웹 UI를 구현하세요.

다음 단계 (Next steps)

클로드에게 당신의 API에 대한 접근을 부여해 고객을 대신해 조치를 취하게 하세요. 정의한 성공 기준에 대해 지원 에이전트를 측정할 평가를 구축하세요. 응답을 스트리밍해 고객이 생성될 때 답을 보게 하세요. 더 나은 작업 성능을 위해 시스템 프롬프트와 예시를 다듬으세요.

더 알아보기 (Learn more)