[NLP] 1-3. Solar Chat API

Paper2Code·2025년 8월 7일

NLP Project

목록 보기
9/12
post-thumbnail
  • Upstage의 Solar LLM을 OpenAI 호환 형태로 제공하는 대화형 API입니다
  • LangChain에서 SolarChat 클래스로 Wrapping되어 있으며, 내부적으로 OpenAI API와 호환됩니다.

01-1

!pip install openai #openai 라이브러리 설치

import pandas as pd
import os
import time
from tqdm import tqdm
from rouge import Rouge # 모델의 성능을 평가하기 위한 라이브러리
from openai import OpenAI # openai==1.2.0

01-2

UPSTAGE_API_KEY = "up_****************" # upstage.ai에서 발급받은 API KEY를 입력해주세요.

client = OpenAI(
	api_key=UPSTAGE_API_KEY,
	base_url="https://api.upstage.ai/v1/solar" #Solar API 엔드포인트
)

엔드포인트란?
Solar Chat API에 요청을 보낼 URL 주소를 뜻함.
https://console.upstage.ai/docs/getting-started

지원 모델 이름설명
upstage/solar-1-mini-chat3B 기반 한국어 최적화 LLM
upstage/solar-10.7b-instruct-v1.010.7B 대규모 모델 (Instruct 기반)
upstage/solar-1-chat(기본 Chat용 모델)

02

# 모델이 만들어내는 말을 실시간으로 글자/단어 단위로 출력하게 되는 구조

stream = client.chat.completions.create(
	model = "solar-1-mini-chat",
	messages = [ 
		{"role": "system", "content": "You are helpful assistant."},
		{"role": "user", "content": "Hello!"}
	], #대화 맥락을 구성하는 부분
	stream=True, 
)

for chunck in stream:
	if chunk.choices[0].delta.content is not None:
		print(chunk.choices[0].delta.content, end="")
  • client.chat.completions.create() : OpenAI 호환 API의 ChatCompletion 요청을 실행하는 함수
  • 이 결과를 stream 변수에 저장.

🔹 messages는 채팅의 대화 맥락을 구성하는 부분입니다.
🔸 "system" 역할: 모델에게 역할이나 성격을 부여 (ex. "요약 전문가", "친절한 조수")
🔸 "user" 역할: 사용자 입력 ("Hello!")

🔹 stream=True
🔸 응답을 한 번에 받는 대신 단어(또는 토큰) 단위로 실시간 스트리밍 받겠다는 설정입니다.
🔸 stream=False로 하면 전체 문장을 한번에 반환합니다.

🔹 for chunk in stream:
🔸stream 객체는 generator로, 각 chunk는 모델이 토큰 단위로 보내오는 응답입니다.
🔸 for chunk in stream:응답이 끝날 때까지 한 줄씩 반복하며 처리합니다.

🔹 if chunk.choice[0].delta.content is not None: 🔸 각 chunk에는 choices리스트가 있으며, 그 안에 **응답된 텍스트 조각**이delta.content`에 저장됩니다.
🔸 이 줄은 응답이 비어 있지 않을 경우만 출력을 하겠다는 조건

🔹 print(chunk.choices[0].delta.content, end="")
🔸 응답된 텍스트 조각을 화면에 출력
🔸end=""는 줄바꿈 없이 이어서 출력하겠다는 뜻

📦 02-1. 먼저, 모델이 스트리밍 응답을 줄 때 생기는 구조를 보자

스트리밍으로 모델이 보내는 각 조각(chunk)은 대략 이렇게 생겼어요:

{
  "choices": [
    {
      "delta": {
        "content": "안"
      },
      "finish_reason": null
    }
  ]
}
🧩 02-2. 구성 설명:
  • chunk: 전체 응답 조각 하나
  • chunk.choices: 모델의 응답 리스트 (보통 1개만 존재함)
  • chunk.choices[0]: 첫 번째 응답
  • chunk.choices[0].delta: 새로 생성된 텍스트 조각 (delta는 "차이" 또는 "새로 생성된 부분"이라는 의미)
  • chunk.choices[0].delta.content: 이번에 새로 나온 단어 혹은 글자 (예: "안")
chunk
└── choices (list)
    └── [0]
        └── delta
            └── content → 새로 생성된 텍스트 조각 ("안", "녕", ...)
🧠 02-3. 왜 delta인가?

스트리밍에서는 전체 응답이 아닌 "조각(delta)"만 주기 때문에,
한 번에 하나씩 이전 응답 대비 "새로 생성된" 부분만 포함해서 보내는 겁니다.

03

# 모델 성능에 대한 평가 지표를 정의합니다.본 대회에서는 ROUGE 점수를 통해 모델의 성능을 평가합니다.
rouge = Rouge() 
def compute_metrics(pred, gold):
	results = rouge.get_scores(pred, gold, avg=True) 
	#pred(예측요약)과 gold(정답요약)을 비교하여 rouge 전수 계산 
	result = {key: value["f"] for key, value in results.items()}
	#ROUGE-1/2/L 중 F1-score만 뽑아 딕셔너리로 정리
	return result #예:{'rouge-1': 0.53, 'rouge-2': 0.39, 'rouge-l': 0.50}형태 리턴

📌 참고사항

  1. 형태소 기반 ROUGE 사용 아님
    • 이 함수는 단어 단위 기준이며, 대회에서는 형태소 단위 평가 기준을 쓰니 미묘한 차이 있음
    • 형태소 단위 ROUGE를 사용하려면 KoNLPy + custom tokenizer 또는 rouge_score (HuggingFace용)를 조합해서 직접 구현해야 함
  2. 문장 여러 개 평가 시
   pred_list = ["요약문1", "요약문2", ...] 
   gold_list = ["정답1", "정답2", ...] 
   rouge.get_scores(pred_list, gold_list, avg=True)```
##### ✅ 수정/보완 포인트 (선택 사항)

```python
def compute_metrics(pred, gold):
    results = rouge.get_scores(pred, gold, avg=True)
    return {
        "rouge-1-f": results["rouge-1"]["f"],
        "rouge-2-f": results["rouge-2"]["f"],
        "rouge-l-f": results["rouge-l"]["f"],
    }

이렇게 명시적으로 바꾸면 추후 CSV 저장이나 정렬 시 더 명확하게 활용할 수 있습니다.

04

# Dialogue를 입력으로 받아, Solar Chat API에 보낼 Prompt를 생성하는 함수를 정의합니다.

def build_prompt(dialogue):
	system_prompt = "You are an expert in the field of dialogue summarization. Please summarize the following dialogue."
	# system role은 모델에게 역할과 태도를 정의하는 부분.
	user_prompt = f"Dialogue:\n{dialogue}\n\nSummary:\n"
	#실제 요약할 대화 내용이 들어감. 
	return [
		{
			"role": "system",
			"content": system_prompt
		},
		{
			"role": "user",
			"content": user_prompt
		}
	]

🔹 user role에는 실제 요약할 대화 내용이 들어갑니다.
- "Dialogue:\n{dialogue}"로 대화가 어떤 것인지 보여주고,
- \n\nSummary:\n으로 요약문을 작성하라고 유도합니다.
- 이 구조는 모델이 "Summary:" 뒤에 이어서 요약을 자연스럽게 생성하도록 합니다.

05

def summarization(dialogue): #대화 내용(dialogue)을 받아 요약을 수행하는 함수 정의
    summary = client.chat.completions.create( #client는 미리 설정된 openai 인스턴스
        model="solar-1-mini-chat",
        messages=build_prompt(dialogue), #이전에 정의한 함수로 만들어진 프롬프트 메세지 사용
    ) #openai 호환 방식의 Solar API를 호출

    return summary.choices[0].message.content
	#최종적으로 요약 결과 텍스트만 반환

✅ 보완 아이디어 (선택)

보완 포인트설명
🔁 retry logicAPI 오류 대비 재시도 로직 (try-except)
⏱ 응답 시간 측정inference latency 측정 시 time.time() 사용
📄 로그 저장출력 로그 또는 실패 대화 저장
🚫 content null 처리choices[0].message.content가 None일 수 있으니 get() 처리 고려

✅ 개선 버전 예시 (예외 처리 포함)

def summarization(dialogue):
    try:
        response = client.chat.completions.create(
            model="solar-1-mini-chat",
            messages=build_prompt(dialogue),
        )
        return response.choices[0].message.content.strip()
    except Exception as e:
        print(f"[Error] summarization failed: {e}")
        return "요약 실패"

✚ 추가 파라미터 설정 (선택사항)

1) temperature=0.2
  • 출력의 다양성을 조절하는 파라미터입니다.
  • 값이 낮을수록 → 더 결정적(deterministic) 결과가 나옵니다.
    • 0.0이면 항상 거의 같은 출력 (가장 확률 높은 토큰만 선택)
    • 1.0이면 창의적이고 다양성이 높은 결과
  • 0.2는 거의 일관되고 안정적인 요약 결과를 생성하게 합니다.
2)top_p=0.3
  • nucleus sampling (확률 질량 누적) 방식입니다.
  • 상위 p 확률에 해당하는 토큰들 중에서 샘플링
  • top_p=0.3이면 → 전체 확률 분포의 상위 30%에 해당하는 토큰 중에서 선택
  • 이는 temperature와 함께 모델의 창의성/일관성 trade-off를 세밀하게 조절하는 데 사용됩니다.

🔁 temperaturetop_p는 일반적으로 하나만 조절하는 게 좋지만, 둘 다 설정해도 동작은 합니다.
(다만 지나치게 조이면 모호하거나 짧은 응답이 나올 수도 있으니 튜닝 필요)

✅ 요약: 목적에 맞는 파라미터 조합

목적추천 값
최대한 일관된 요약 (대회 제출용)temperature=0.2, top_p=0.3
약간의 다양성temperature=0.7, top_p=0.9
다양한 버전 생성 (augmentation)temperature=1.0, top_p=1.0

06

# Train data 중 처음 3개의 대화를 요약합니다.

def test_on_train_data(num_samples=3):
	for idx, row in train_df[:num_samples].iterrows():
		dialogue = row['dialogue'] #현재 대화 내용 추출
		summary = summarization(dialogue) #정의한 함수통해 solar chat api로 요약 생성
		print(f"Dialogue:\n{dialogue}\n")
		print(f"Pred Summary: {summary}\n")
		print(f"Gold Summary: {row['summary']}\n")
		print("=="*50)
  • .itterows() : 각 행을 (index, row Series) 형태로 반복함.
개선 포인트설명
⏱ 속도 측정요약 시간 time.time()으로 측정
🧪 평가 지표 출력compute_metrics(pred, gold) 함수로 ROUGE 점수 함께 출력
📄 결과 저장.csv 또는 .jsonl 파일로 저장하여 나중에 비교
💻 tqdm progress bar10개 이상 처리할 경우 진행률 표시
def test_on_train_data(num_samples=3):
    for idx, row in train_df[:num_samples].iterrows():
        dialogue = row['dialogue']
        gold = row['summary']
        pred = summarization(dialogue)
        scores = compute_metrics(pred, gold)

        print(f"Dialogue:\n{dialogue}\n")
        print(f"Pred Summary: {pred}\n")
        print(f"Gold Summary: {gold}\n")
        print(f"ROUGE Scores: {scores}\n")
        print("=="*50)

07

# Validation data의 대화를 요약하고, 점수를 측정합니다.

def validate(num_samples=-1):
	# num_samples가 양수일 경우 해당 개수만큼, 음수이면 전체 validation 세트를 사용
	val_samples = val_df[:num_samples] if num_samples > 0 else val_df
	
	scores = [] #개별 예측에 대한 점수를 저장할 리스트
	
	for idx, row in tqdm(val_samples.iterrows(), total=len(val_samples)):
		dialogue = row['dialogue']
		summary = summarization(dialogue)
		results = compute_metrics(summary, row['summary'])
		# 모델의 요약 결과와 정답 요약 비교하여 ROUGE F1 계산
		avg_score = sum(results.values()) / len(results)
		# ROUGE-1/2/L의 평균값을 하나의 점수로 계산
		scores.append(avg_score)
	
	val_avg_score = sum(scores) / len(scores)
	
	print(f"Validation Average Score: {val_avg_score}")
	

"val_df의 전체 샘플에 대해 모델이 생성한 요약문과 정답 요약문 사이의 ROUGE-F1 평균 점수" 출력

Rouge-F1 점수 범위 의미

dialogue summarization은 자유도가 높고 정답이 다양하기 때문에, 일반적인 문서 요약보다 점수가 낮게 나오는 게 일반적입니다.

점수 범위의미
0.05 이하모델이 거의 엉뚱한 요약을 함 (거의 무작위 수준)
0.1~0.2아주 기본적인 의미는 담고 있지만 핵심 정보는 많이 놓침
0.3~0.5핵심 단어가 많이 일치. 꽤 괜찮은 요약
0.6 이상거의 정답과 유사한 고성능 요약 모델 수준

💬 지금 점수는?

Validation Average Score: 0.097 ≒ 9.7%
  • 아주 낮은 점수
  • 정답 요약과의 유사성이 거의 없다는 뜻
  • 원인 분석이 필요

💡 낮은 점수가 나올 수 있는 주요 원인

가능성설명
🔹 모델이 대화를 제대로 이해 못함prompt가 너무 짧거나 일반적일 수 있음
🔹 프롬프트가 영어인데 대화가 한글모델 혼란 유발 (한국어 모델이면 system role도 한글로!)
🔹 Solar 모델 성능 한계solar-1-mini-chat은 3B 모델로 작은 편
🔹 gold summary와 표현 방식 차이정답은 "요약적", 모델은 "해석적/묘사적" 생성
🔹 형태소 단위 ROUGE 미사용compute_metrics()가 단어 기준이면 실제 평가보다 점수 낮게 나올 수 있음

💡 해결 방향 제안

시도할 것설명
✅ 프롬프트 개선system prompt와 user prompt를 모두 한국어로
✅ 모델 교체solar-10.7b-instruct로 전환 (가능하다면)
✅ Few-shot 학습 유도예시 요약문 추가로 성능 향상 유도
✅ 평가 방식 검토rouge_score 또는 형태소 기반 tokenizer 적용

08

""" test 데이터셋에 대해 Solar 모델로 요약을 생성하고,  
	rate limit(속도 제한)을 고려하여 안전하게 결과 파일로 저장하는 inference 함수"""
def inference():
	test_df = pd.read_csv(os.path.join(DATA_PATH, 'test.csv'))
	
	summary = [] #요약 결과를 저장할 리스트summary 초기화
	start_time = time.time() #1분 간 처리 개수 측정을 위한 시간 측정 시작
	for idx, row in tqdm(test_df.iterrows(), total=len(test_df)):
	# test 데이터셋을 한 줄씩 순회하며 tqdm으로 진행률 시각화
		dialogue = row['dialogue']
		summary.append(summarization(dialogue)) #현재 대화문 요약 후, 리스트에 추가
		
		# Rate limit 방지를 위해 1분 동안 최대 100개의 요청을 보내도록 합니다.
		if (idx + 1) % 100 == 0: 
			end_time = time.time()
			elapsed_time = end_time - start_time
			#100개 요청마다 경과 시간 측정
		if elapsed_time < 60:
			wait_time = 60 - elapsed_time + 5
			print(f"Elapsed time: {elapsed_time:.2f} sec")
			print(f"Waiting for {wait_time} sec")
			time.sleep(wait_time)
			# 만약 100개를 1분보다 빨리 처리했다면 -> 남은 시간 + 여유 5초만큼 대기
		start_time = time.time()
		# 다음 100개 구간을 위해 시간 초기화
	output = pd.DataFrame(
		{
			"fname": test_df['fname'],
			"summary" : summary,
		}
	)
	# test_df에서 fname열과 생성된 summary리스트를 합쳐 새 DataFrame 생성
	if not os.path.exists(RESULT_PATH):
		os.makedirs(RESULT_PATH) #결과 저장 폴더가 없으면 생성
	output.to_csv(os.path.join(RESULT_PATH, "output_solar.csv"), index=False)
	#최종 요약 결과를 output_solar.csv로 저장
	return output

09

# Few-shot prompt를 생성하기 위해, train data의 일부를 사용합니다.

few_shot_samples = train_df.sample(1) #train_df에서의 임의의 1개 샘플 무작위로 뽑음
#뽑은 row에서 dialogue,summary 컬럼 각각 추출

sample_dialogue1 = few_shot_samples.iloc[0]['dialogue']
sample_summary1 = few_shot_samples.iloc[0]['summary']
# 이 데이터를 프롬프트 앞부분에 few-shot 예시로 사용할 수 있게 됩니다.
print(f"Sample Dialogue1:\n{sample_dialogue1}\n")
print(f"Sample Summary1: {sample_summary1}\n")

10-1

Few-shot Prompting 기반 프롬프트 생성 함수

# Prompt를 생성하는 함수를 수정합니다.

def build_prompt(dialogue):
	system_prompt = "You are a expert in the field of dialogue summarization, summarize the given dialogue in a concise manner. Follow the user's instruction carefully and provide a summary that is relevant to the dialogue." 
	
	user_prompt = (
		"Following the instructions below, summarize the given document.\n"
		"Instructions:\n"
		"1. Read the provided sample dialogue and corresponding summary.\n"
		"2. Read the dialogue carefully.\n"
		"3. Following the sample's style of summary, provide a concise summary of the given dialogue.\n\n"
		"Sample Dialogue:\n"
		f"{sample_dialogue1}\n\n"
		"Sample Summary:\n"
		f"{sample_summary1}\n\n"
		"Dialogue:\n"
		f"{dialogue}\n\n"
		"Summary:\n"
	)
	
	return [
		{
		"role": "system",
		"content": system_prompt
		},
		{
		"role": "user",
		"content": user_prompt
		}
	]


# few
system_prompt
  • 모델에게 역할 부여: “너는 대화 요약 전문가야.”
  • 요약 스타일: “간결하게 요약하라.”
  • 입력에 충실히 따르라는 명시적 가이드도 포함
user_prompt
  • Few-shot example새로운 대화를 함께 제공
  • 지시문(Instructions:)이 포함되어 있어서 모델이 학습하기에 좋음
  • "Following the sample's style..."를 통해 output style consistency를 유도함

10-2

# Few-shot sample을 다른 방식으로 사용하여 prompt를 생성합니다.

def build_prompt(dialogue):
	system_prompt = "You are a expert in the field of dialogue summarization, summarize the given dialogue in a concise manner. Follow the user's instruction carefully and provide a summary that is relevant to the dialogue."
	  
	few_shot_user_prompt_1 = (
		"Following the instructions below, summarize the given document.\n"
		"Instructions:\n"
		"1. Read the provided sample dialogue and corresponding summary.\n"
		"2. Read the dialogue carefully.\n"
		"3. Following the sample's style of summary, provide a concise summary of the given dialogue. Be sure that the summary is simple but captures the essence of the dialogue.\n\n"
		"Dialogue:\n"
		f"{sample_dialogue1}\n\n"
		"Summary:\n"
	)
	
	few_shot_assistant_prompt_1 = sample_summary1
	
	user_prompt = (
		"Dialogue:\n"
		f"{dialogue}\n\n"
		"Summary:\n"
	)
	
	return [
		{"role": "system", "content": system_prompt},
		{"role": "user", "content": few_shot_user_prompt_1},
		{"role": "assistant", "content": few_shot_assistant_prompt_1},
		{"role": "user", "content": user_prompt},
	]
항목방식 1: 한 덩어리 user prompt방식 2: user-assistant role 분리
구조system + user(prompt 전체)system + user + assistant + user
샘플 예시 방식샘플 대화 & 요약을 텍스트로 삽입샘플 대화 → 모델의 응답을 실제 assistant role로 삽입
모델 관점새로운 대화를 처음 보는 입력처럼 처리이전 대화 흐름을 학습처럼 모방
대표 활용GPT-style instruction tuningChat-based fine-tuning (SFT), LangChain 등
성능 효과명시적 task 지시 효과 有실질적 imitation/few-shot 효과 강함

🎯 방식 1: 한 덩어리 prompt (명시적 지시)

return [
  {"role": "system", "content": "..."},
  {"role": "user", "content": """
      Sample Dialogue: ...
      Sample Summary: ...
      Dialogue: ...
      Summary:
  """}
]
✅ 특징
  • 모델은 "이 프롬프트 전체를 새로운 입력으로 간주"함
  • 예시도, 문제도, 모두 한 메시지 내에서 학습
  • 기본적으로 GPT의 instruction tuning 구조와 유사
✅ 장점
  • 설계 간단, 길이 제어 쉬움
  • 샘플 여러 개 넣기 편함
  • gpt-3.5-turbo 같은 모델에 최적화
⚠️ 한계
  • 일부 모델에서는 sample summary를 단순한 "문자열"로 취급
  • few-shot imitation 효과가 덜함

🧠 방식 2: role 분리 (chat-style few-shot)

return [
  {"role": "system", "content": "..."},
  {"role": "user", "content": "Dialogue: 샘플 대화\nSummary:"},
  {"role": "assistant", "content": "샘플 요약"},
  {"role": "user", "content": "Dialogue: 실제 대화\nSummary:"},
]
✅ 특징
  • 모델이 이전 대화 context를 이해하고 답변을 이어감
  • 마치 "대화를 통해 task를 학습"하는 구조
  • SFT(supervised fine-tuning) 데이터 포맷과 매우 유사
✅ 장점
  • 모델이 assistant 역할을 모방하도록 훈련된 경우, 성능 극대화
  • 실제 solar, gpt, llama류 모델들이 이 포맷에서 매우 잘 작동함
  • 샘플과 실제 문제를 분리해 context로 학습 유도
⚠️ 한계
  • 길이 제한에 걸릴 수 있음 (샘플이 많아질수록)
  • multi-shot 넣을 때 role 순서를 신경 써야 함

✅ 언제 어떤 방식이 좋은가?

상황추천 방식
명시적으로 "이렇게 요약해"라고 지시하고 싶다방식 1 (한 덩어리 user prompt)
Solar / GPT 등 chat 기반 모델을 모방시키고 싶다방식 2 (user + assistant role 분리) ✅
샘플이 매우 짧다방식 1
샘플과 입력 길이가 길다방식 1 (token overflow 방지)
모델이 SFT 스타일로 학습된 경우방식 2 ✅

🚀 실전 적용 팁

  • Solar 같은 모델에는 방식 2가 거의 항상 더 잘 작동합니다.
    • Upstage가 SFT + RLHF 구조로 학습시켰기 때문이에요.
  • 두 방식 다 시험해보되, validation 성능을 기준으로 선택하는 것이 안전합니다.
  • 2개 이상의 샘플을 사용할 땐, 방식 2에서 user → assistant 쌍을 반복하면 됩니다.
profile
As I Imagine | 이론 정리 사이트 Tistory 링크 참조 ↓ 홈 아이콘 클릭)

0개의 댓글