728x90

들어가기에 앞서....

작년부터 핫해진 랭체인+LLM을 이용한 다양한 기술들이 나오고있습니다.

솔직히 국내에서 랭체인 강의들이 존재하지만(패캠,인프런,노마드코더 그리고 유데미) 대다수 기본적인 랭체인에 대해서 설명을 해주는게 아직까지는 다 인 상황입니다.

저 또한 마찬가지고 아직까지 배우고있으며 배운 내용을 토대로 조금 더 사람들에게 내용을 공유하고싶어서 이 글을 작성하게 되었습니다.


 

기초 프롬프트


프롬프트 엔지니어링은 다양한 어플리케이션 연구 주제에 언어 모델(LMs) 을 효율적으로 사용할 수 있도록 프롬프트를 개발하고 최적화하는 비교적 새로운 분야입니다. 프롬프트 엔지니어링 기술은 대규모 언어모델(LLMs)의 기능과 한계를 더 잘 이해하는데 도움이 됩니다. 

 

현재 많은 연구자분들께서 프롬프트 엔지니어링을 사용해서 질문 답변 및 산술 추론과 같은 일반적이고 복잡한 다양한 작업에서 LLMs의 역량을 향상시키고 있습니다. 

그러한 기술들을 토대로 이 글을 읽으시는 개발자분들은 프롬프트 엔지니어링을 사용하여 LLMs 및 기타 도구와 강력하고 효과적인 프롬프트 기술을 설계하고 고객을 위한 서비스를 만들어나아갑니다.

 

간단하고 작은 프롬프트만으로도 많은 일들을 할 수 있습니다 가령, 특정 보험에 특화 되어있는 프롬프트가 있다면 다른 일반적인 프롬프트에 비해 정보의 양과 퀄리티가 뛰어나 더 정확한 답변을 할수 있을것입니다. 

 

모델에게 전달하는 프롬프트는 지시나 질문과 같은 정보와 맥락, 입력 또는 예시와 같은 다른 세부 사항을 포함할 수 있습니다. 

이러한 구성 요소를 사용하여 모델에게 더욱 절적하게 지시하고 더 나은 결과를 얻을 수 있습니다.

 

간단한 예시

 

Prompt :

하늘은

 

Output : 

푸른색입니다.
하늘은 맑은 날에는 푸른색입니다. 흐린 날엔 하늘은 회색 혹은 하얀색을 띌 수 있습니다.

 

언어 모델은 "하늘은" 이라는 짧은 문장의 맥락을 파악하고 글을 이어 나갑니다. 결과물은 예상하지 못했거나 우리가 달성하려던 작업과는 조금 동 떨어진, 즉 오답을 줄 수 도 있습니다.

 

하지만 사용자는 모델에게 더 자세한 문맥과 구체적으로 원하는 것에 대한 지시를 내리는 일이 중요합니다.

 

다시, 자세한 문맥과 구체적인 지시를 포함한 예시를 다시 보여드리겠습니다.

 

Prompt :

문장을 완성해 줘 :
하늘은

 

 

Output : 

오늘 매우 아름답습니다.

 

답변이 조금 더 깔끔하지 않나요? "하늘은" 위에 문장을 완성해 줘: 라는 구체적인 문장을 붙여서 모델은 좀 더 정확한 답변을 출력했습니다.

 

모델에게 작업을 수행 하도록 지시하기 위해 최적의 프롬프트를 설계하는 이러한 접근 방식을 프롬프트 엔지니어링 이라고 합니다.


프롬프트 형식

위에 간단한 프롬프트를 입력했습니다. 일반적으로 프롬프트는 다음과 같은 형식을 따릅니다.

 

<질문>?

혹은

<지시>

 

또한 질의 응답 형식으로도 작성이 가능하며, 다음과 같이 많은 QA 데이터 세트에서 표준 형식으로 사용합니다.

 

Q : <질문>?
A :

 

위와 같이 프롬프트를 수행하는 방식을, Zero-shot Prompting 이라고 합니다. (shot prompting 은 예제의 수를 나타냅니다.) 즉 0개의 예제의 수 입니다. 

어떤 예시나 설명이 없이 직접 모델에 응답을 요구합니다. 그렇다면 Chat-GPT 처럼 방대한 양을 가지고 있어서 답을 유추할수 있지만 다른 프롬프트는 확실한 답을 줄 수 없을 수도 있습니다.

 

위에 표준 형식을 고려할 때, 인기 있고 효과적인 프롬프팅 기술 중 하나는 모범 예시를 제공하는 Few-shot Prompting 입니다. 

 

<질문>?
<답변>
<질문>?
<답변>
<질문>?
<답변>
<질문>?
<답변>

 

QA 형식에 따른다면 ....

Q : <질문>?
A : <답변>
Q : <질문>?
A : <답변>
Q : <질문>?
A : <답변>
Q : <질문>?
A :

 

이러한 형식이 정답은 아닙니다. 주어진 업무에 따른 적절한 프롬프트 형식을 사용할 수 있습니다. 

예를 들어, 간단한 분류 작업 혹시 연산등의 T or F 를 나타낼때도 답변을 잘 해 줍니다. 

 

Prompt : 

멋지다! //긍정
나빠! //부정
그 영화는 굉장했어! // 긍정
이 얼마나 맛이 없는가! //

답변은 ???

Output : 

부정

 

Few-shot 프롬프팅은 문맥에 맞는 학습을 할 수 있습니다 그러므로 위에 예시들을 통해 답변을 조금 더 정확하게 유추해서 제공해줍니다.

 

GPT 처럼 방대한 질문을 위한 것이 아닌 특수한(관리자용, 정보가 방대하지 않는 곳) 상황에서 학습을 시켜 쓰이는것도 좋아보입니다.

 

위와 같은 글들을 보다 보니 그럼 프롬프트의 구성 요소들은 무엇이 있을까 생각을 하실수 있습니다.

 

프롬프트 엔지니어링을 접목한 여러 애플리케이션을 반복해서 사용하다 보면 특정 구성 요소들이 존재하는것을 알 수 있습니다. 

 

지시 모델이 수행할 특정 작업 또는 지시
문맥 더 나은 응답을 위해 모델을 조종할 수 있는 외부 정보나 추가 문백
입력 데이터 응답 받고자 하는 입력이나 질문
출력 출력의 유형이나 형식

 

각각 다시 설명 드리겠지만 출력을 설명 드리자면 String 형식의 글도 가능하지만 그래프나 통계 자료등 원하는 방향으로 답을 가져다 줄수도 있습니다.

728x90
728x90

엘라스틱서치, 레디스, 스톰 그리고 스파크의 메트릭 정보를 수집하여서 그라파나에 보여주기 위해 프로메테우스를 사용하고있습니다. 어느날 엘라스틱서치에 오류가 발생하면서 셧다운 된 적이 있습니다. 다행이도 운영중인 서버가 아닌 테스트 서버의 디스크 용량이 꽉차서 운영에는 문제가 되지 않았습니다. 

 

어디서 용량을 많이 잡아 먹는지 보니 프로메테우스에서 대략 80퍼센트의 용량을 먹고 있었습니다.!! 

 

유로든 장소가 손상된 경우 문제결하는 전략은 Prometheus를 종료한 다음 전체 저장소 디렉터리를 제거하는 것입니다. 개별 블록 디렉터리나 WAL 디렉터리제거하여 제를 결할 수도 있습니다. 이는 렉터리당 약 2시간의 데이터실된다는 것을 미합니다. 시 말하지만, Prometheus의 토리지는 구성이 어난 장기 토리지가 아닙니다. 루션은 장된 보존이터 구성을 제공합니다.

주의: 구할 수 상이 발생할 수 있으므로 POSIX와 호환되지 않는 스템은 Prometheus의 컬 저장소에 대해 지원되지 않습니다. NFS 파일 시스템(AWS EFS 포함)은 원되지 않습니다. NFS는 POSIX와 호환될 수 있지만부분의 현은 렇지 않습니다. 정성을 위해 로컬 파일 시스템을 사용하는 것이 좋습니다.

 

 

 

일단 프로메테우스에서 제공하는 정보를 보시면 

저장

Prometheus에는 로컬 온디스크 시계열 데이터베이스가 포함되어 있지만 선택적으로 원격 스토리지 시스템과도 통합됩니다.

로컬 저장소

Prometheus의 로컬 시계열 데이터베이스는 로컬 스토리지에 매우 효율적인 맞춤형 형식으로 데이터를 저장합니다.

 

온디스크 레이아웃

취된? 샘플은 2시간 단위로 그룹화됩니다. 각 2시간 블록은 해당 시간창에 한 모든 계열 샘플이 포함된 Chunk 하위 디렉터리, 타데이터 파일 및 인덱스 파일(지표 이름 및 레이블을 청크 디렉터리의 시계열에 색인화함)이 함된 디렉터리로 구성됩니다. 크 디렉터리의 샘플은 기본적으로 각각 대 512MB 의 하나 이상의 세그먼트 일로 그룹화됩니다. 

 

API를 통해 리즈가 제되면 제 기록은 도의 삭제 표시 파일에 저장됩니다(청크 세그먼트에서 데이터를 즉시 삭제하는 대신).

어오는 플의 재 블록은모리에 보관되며전히 유지되지 습니다. Prometheus 버가 다시 작될 때 재생할 수 있는 WAL(미리 쓰기 로그)을 해 충돌로부터 보호됩니다. 미리 쓰기 그 파일은 wal128MB 그먼트로 디렉터리에 장됩니다. 이러한 파일에는 아직 압축되지 않은 시 데이터가 포함되어 있습니다. 따라서 일반 록 파일보다 훨씬 더 큽니다. Prometheus는 소 3개의 미리 쓰기그 파일을 보관합니다. 래픽이 많은 서버는 소 2시간의 시 데이터를 보관하기 위해 3개 이상의 WAL 파일을 보관할 수 있습니다.

Prometheus 버의 이터 렉터리는 다음과 같습니다.

시간 및 크기 보존 정책이 모두 지정된 경우 먼저 트리거되는 것이 사용됩니다.

만료된 블록 정리는 백그라운드에서 수행됩니다. 만료된 블록을 제거하는 데 최대 2시간이 걸릴 수 있습니다. 블록은 제거되기 전에 완전히 만료되어야 합니다.

./data
├── 01BKGV7JBM69T2G1BGBGM6KB12
│   └── meta.json
├── 01BKGTZQ1SYQJTR4PB43C8PD98
│   ├── chunks
│   │   └── 000001
│   ├── tombstones
│   ├── index
│   └── meta.json
├── 01BKGTZQ1HHWHV8FBJXW1Y3W0K
│   └── meta.json
├── 01BKGV7JC0RY8A6MACW02A2PJD
│   ├── chunks
│   │   └── 000001
│   ├── tombstones
│   ├── index
│   └── meta.json
├── chunks_head
│   └── 000001
└── wal
    ├── 000000002
    └── checkpoint.00000001
        └── 00000000
728x90
728x90

데이터 크기가 2.4GB로 제한인 2.3GB를 초과 했을때 나타났던 오류 구문입니다. 

일단 방법중 하나인 제한을 증가 시킵니다. 

즉 더 큰 데이터 크기를 수용하기 위해 제한을 늘려야가능합니다 다만 이 접근 방식을 사용하면 메모리 사용량이 증가하고 잠재적인 성능 문제가 발생할 수 있습니다.

elasticsearch.yml 에서 변경이 가능합니다. 

indicies.breakter.total.limit : "3gb"

혹은 쿼리 최적화를 통해 검색 데이터 양을 줄이면됩니다. 추천: 데이터의 양을 제한하려면 필터, 집계 또는 기타 쿼리 최적화를 사용하는 것이 좋습니다. 

 

동시 요청 수 를 줄이거나 페이징 및 스크롤링 컨테스트를 동시에 열어두지 않는지 확인 하여야 합니다. 

 

아마 엘라스틱서치를 사용하시는 분들은 모니터링 툴을 사용하실꺼라고 생각합니다. 모니터링 기능을 사용하여서 클러스터를 정기적으로 모니터링 하고 리소스 사용량과 쿼리 패턴 및 인덱싱 속도를 분석해야합니다. 

 

색인 설정 조정이 필요합니다. - 검색 작업과 관련된 색인 설정을 컴토하고 샤드 수, 복제본, 새로고침 간견등의 설정을 조정하는것이 좋습니다. 

늘 실제 적용하기 이전에 테스트를 충분히 하시고 여러가지를 검토 후 변경하시기를 권장합니다. 

728x90
728x90

멀티 쓰레드 이용해서 자바 달력 코드 변경 하는 구문입니다. 

예시 ) 50개의 스레드 를 이용해서 for 문을 돌릴때 50개의 순서대로 시간이 상승하는 코드입니다. 

50개가 같은 시간으로 값을 보내는게 아닌 한개의 스레드마다 대략 26개의 데이터를 00:00:00 개의 넣고 차례대로 1초씩 상승해서 데이터를 넣습니다. 

	private static final String FORMAT_STRING = "yyyy-MM-dd HH:mm:ss";
	private static final AtomicInteger sharedCounter = new AtomicInteger(0);
	private static volatile String startDateString = "2021-10-01 00:00:00";
    
    //시간이 다 지나면 날짜가 하루 올라가는 코드 구문
public static String generateDate(int len) { 
        try {
            String FORMAT_STRING = "yyyy-MM-dd HH:mm:ss";
            FastDateFormat format = FastDateFormat.getInstance(FORMAT_STRING);

            Date startDate = format.parse(startDateString); 

            Calendar cal = Calendar.getInstance();
            cal.setTime(startDate);

            for (int day = 1; day <= 1; day++) {
                for (int i = 1; i <= len; i++) {
                    cal.add(Calendar.MILLISECOND, 28);
                    System.out.println(i + " : " + format.format(cal));
                }
                cal.setTime(startDate);
                cal.add(Calendar.DATE, 1); 
                startDateString = format.format(cal);
            }
            return startDateString;

        } catch (Exception e) {
            e.printStackTrace();
            return null; // or handle the exception as appropriate for your use case
        }
    }
    
    @Override
	public void run() {
	
		int total = 0;
		int lengSize = 100;
		long started = System.currentTimeMillis();
		try {
			JSONObject event = new JSONObject();
			
			for (int i = 0; i < lengSize; i++) {
				int currentValue = getNextValueFromSharedCounter();
				String formattedDate = getFormattedDate(currentValue);
				Map<String, Object> result = test(formattedDate, lengSize);
				for (String key : result.keySet()) {
					event.put(key, result.get(key));
				}
				submitServletDataSync("http://192.168.0.12:8081/drpc/" + svc, event.toString());
				total++;

			}
		
		} catch (ParseException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		long now1 = System.currentTimeMillis();
		System.out.println("TPS " + lengSize / ((now1 - started)/ 1000.0));
	}
    
    
 private static int getNextValueFromSharedCounter() {
		return sharedCounter.getAndIncrement();
	}
    
private static String getFormattedDate(int counter) throws ParseException {
		Calendar cal = Calendar.getInstance();
		String FORMAT_STRING = "yyyy-MM-dd HH:mm:ss";
		FastDateFormat format = FastDateFormat.getInstance(FORMAT_STRING);
        Date startDate = format.parse(startDateString);
        cal.setTime(startDate);
		// Use the counter to modify the date
        cal.setTimeInMillis(cal.getTimeInMillis() + counter * 28);
		return format.format(cal.getTime());

		// Your existing test and submitServletDataSync methods...
	}
    
    
  	public static void main(String[] args) throws Exception {
		Runnable r = new FdsTestSource();
		ExecutorService excutor = Executors.newFixedThreadPool(20);
		int n = 2;
		for (int i = 0; i < n; i++) {
			excutor.submit(r);
			Thread.sleep(10);
		}
		excutor.shutdown();
		excutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
	}
728x90
728x90

추석 연휴 동안 서버 3대중 한대가 죽어서 2대로 테스트 데이터를 받고있었습니다. 

오늘 회사 서버를 살려서 연결 해 보니, 새로 생성된 데이터의 샤드가 제대로 분배가 되지 않는것을 파악했습니다.

 

그 해결책으로 밑에 구문을 보시면 될거같습니다.

 

 

장애 대응으로 한참 샤드 복구와 샤드 할당 작업 등이 수행 중인 바쁜 상황에서 새 인덱스가 생성 될 때를 조심해야 한다. 

엘라스틱서치에 새 샤드가 할당될 때 엘라스틱서치는 해당 노드에 총 몇개의 샤드가 있는지 체크한 뒤 적은 수의 샤드를 들고 있는 노드에 새 샤드를 할당한다. 

 

문제는 장애 복구 작업 중 방금 재시작된 노드는 샤드 복구가 제대로 끝난 상태가 아니라는 것이다. 이 노드는 겉으로 보기에는 적은 샤드를 들고 있는 것으로 판정된다. 이때 새 인덱스가 생성되면 이 인덱스의 모든 샤드를 한 노드가 몰아서 할당받는다. 그렇게 해야 전체적인 샤드 수의 균형이 맞다고 엘라스틱서치가 판단하기 때문이다. 

여기에 데이터까지 들어오면 원래는 수십 대의 노드가 분산해서 색인해야 할 데이터를 한 노드가 전부 받아서 처리하다가 다시 죽는 문제가 발생한다. 

cluster.routing.allocation.balance.shard 설정은 샤드 할당의 균형을 맞추는 경향성을 지정한다. 이 값이 낮을수록 균형을 신경 쓰지 않는다. 기본값은 0.45다. 장애 복구 작업 도중 새 인덱스가 생성될 것으로 예상되면 이 값을 임시로 0으로 낮춘다.

 

# 가중치: 노드마다 샤드 개수 밸런싱 < 인덱스마다 샤드 개수 밸런싱
cluster.routing.allocation.balance.shard: 0
cluster.routing.allocation.balance.index: 1
cluster.routing.allocation.balance.threshold: 1.0

쿼리
PUT _cluster/settings
{
  "transient": {
    "cluster.routing.allocation.balance.shard": 0.0,
    "cluster.routing.allocation.balance.index": 1.0
  }
}

공식 문서 :  https://www.elastic.co/guide/en/elasticsearch/reference/7.2/shards-allocation.html

 

Cluster level shard allocation | Elasticsearch Guide [7.2] | Elastic

Regardless of the result of the balancing algorithm, rebalancing might not be allowed due to forced awareness or allocation filtering.

www.elastic.co

비상 상황 처리가 끝나면 이 값을 다시 원래대로 돌려야한다. 계속 0으로 놓고 운영을 하면 특정 노드에 특정 샤드의 배분이 우선된다. 균형을 고려하지 않고 순서대로 단순하게 샤드를 배정하기 때문에 시간이 지날수록 어떤 노드는 특정한 샤드를 많이 할당받고, 어떤 노드는 샤드를 잘 할당 받지 않는 현상이 심화된다. 

 

당장 큰 문제는 생기지 않겠지만 인덱스당 샤드의 수 대비 노드의 수가 크면 클수록, 인덱스의 수가 많을수록, 인덱스마다 데이터의 성질이 다를수록 쏠림 경항이 심해지며 점점 문제의 소지가 된다.

 

이미 특정 노드에 샤드가 몰린 경우 수동으로 reroute API를 호출해서 다른 노드에 샤드를 옮길 수 있다. 다만 이 reroute 작업이 수행되는 동안은 클러스터 부하가 커지기 때문에 전후 상황을 봐서 수행해야 한다. reroute API 는 다음과 같이 호출한다. commands에  하나 이상의 명령을 지정할 수도 있다. 

POST _cluster/rerout
{
	"commands":[
    {
      "move":{
        "index": "test-index-5",
        "shard" : 0,
        "from_node": "es02",
        "to_node": "es01"
      }
    }
  ]
}

 

직업 REST API를 호출하는 방법 외에도 cerebro 같은 관리 도구를 사용하면 UI상에서 샤드 분포를 눈으로 확인하며 쉽고 편리하게 샤드를 옮길 수 있다.

728x90

+ Recent posts