내가 프롬프트 엔지니어링을 담당하고있는 FINGOO프로젝트에서는 Chat GPT 3.5모델을 사용하여 자칫하면 어려울 수 있는 우리의 서비스를 최대한 쉽게 풀어가고자 하고 있다. 우리의 서비스에서 이 모델 API를 활용할 때의 질문 기준을 4가지로 정의하였다.
~을 예측해줘, ~을 설명해줘, ~을 분석해줘, ~을 추천해줘. 해당 질문 4가지의 케이스로 들어올 것을 판단하고, 우리의 서비스에서 이를 어떻게 분류하고, 어떻게 적용할 것인가에 대해 굉장히 많은 실험과 이야기를 하였으며 현재 어느정도 성능이 나오는 서비스가 되었다. 이를 조금 더 자세하게 풀어보고 나중에 내가 또 해당 API를 사용할 때 이를 참고해서 보려고 한다.
우리가 생각한 GPT 로직은 다음과 같다.
사용자가 질문 → GPT가 질문을 이해, 케이스 구분 → 해당 케이스의 프롬프트 제공 → 사용자에게 답변 전달
해당 순서로 진행이 될 것이라 예상하고, GPT의 Function Calling 을 활용하여 이를 대처하고자 하였다.
그리고 수많은 실험을 거쳐서 현재 우리가 사용하는 로직은 다음과 같다.
사용자가 질문 -> GPT가 질문을 이해하여 케이스 구분 -> 각 케이스별 instruction 제공 -> 각 케이스에 따른 indicator-board 로직 처리 + 해당로직에서 사용한 값과 instruction을 가져와 GPT에 전달 -> 출력필드에 대한 instruction을 활용하여 GPT가 답변 제공
내가 맡은 역할은 크게 3가지의 역할을 하였다.
이때 대부분의 파일구조와 기능이 어느정도 구현된 파일을 가지고 이를 프롬프트만 수정하면 되는 역할이어서 내가 미리 마련해둔 프롬프트를 각 구조에 맞게 적용만 시키면 되는 상황이었다.
일단 Function calling할 때 사용할 때 프롬프트 먼저 확인해보기
{
type: 'function',
function: {
name: 'get_instructions',
description: '사용자 질문에 대한 적절한 instruction을 얻는다.',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
enum: ['predict', 'analyze', 'explain', 'recommend'],
description: `""사용자가 하는 질문의 타입은 다음 중 하나가 될 수 있다:
- predict: 경제 지표 예측과 해석을 요청하는 질문.""
- analyze: 시장 상황이나 지수와 같은 종합적인 경제 상황에 대한 분석을 요청하는 질문(예시: IT시장 상황을 분석해줘)
- explain: 특정 단일 지표에 대한 설명이나 분석을 요청하는 질문 (예시: 월마트의 현재 상황을 분석해줘, WMT분석해줘)
- recommend: 유망한 경제 지표를 추천받는 질문 (예시: 현재 은행관련 괜찮은 주식이 있을까?)
`,
},
},
required: ['query'],
},
},
}
사용자가 질문을 하게 되면 먼저 get_instruction을 먼저 호출할 수 있게 질문에 대한 instruction을 얻는다라고 함수에 대한 정의를 실시하였다. 그리고 해당 프롬프트에서 각각의 케이스를 구분하며 해당 케이스가 어떠한 역할을 하는 것인지를 구분하는 프롬프트를 구성하였다. 원래는 질문의 다양성을 고려하여 zero shot prompting 으로 진행하려 하였으나 analyze와 explain의 성격이 어느정도 비슷한 점을 고려하고, S&P500지수, 나스닥 지수와 같은 지수가 GPT에서는 단일 지표로 이해할 수 있다는 생각이 들어 해당 few shot prompting을 사용하기로 생각했고, 최소한의 예시를 들어 질문을 판별 할 수 있도록 하였다.
구분하면서 나온 query를 통해 각 케이스에 맞는 instruction을 제공하게 되고 해당 Instruction은 다음과 같다.
const instructions = [
{
type: 'predict',
instruction: `
경제 지표에 대한 예측을 수행한 후 결과에 대한 해석을 제공합니다.
지시사항:
가이드라인:
- 예측 결과 값을 기반으로 GPT가 알고 있는 지식을 활용하여 해석을 제공 합니다.
- 예측 결과 값이 부정확할 수 있음을 설명해야 합니다.
`,
},
{
type: 'analyze',
instruction: `
시장 상황이나 지수를 분석하는데 필요한 지표를 가져온 후 지표를 바탕으로 분석을 제공합니다.
지시사항:
`,
},
{
type: 'explain',
instruction: `
사용자가 질문한 지표에 대한 개요를 지피티가 알고있는 정보로 사용자에게 설명합니다.
지시사항:
`,
},
{
type: 'recommend',
instruction: `
사용자에게 현재 상황에 맞는 경제 지표를 추천합니다.
지시사항:
`,
},
];
export default function useInstruction() {
const getInstruction = ({ query }: { query: string }) => {
const instruction = instructions.find((instruction) => instruction.type === query);
return JSON.stringify({
instruction: instruction?.instruction,
});
};
return {
getInstruction,
};
}
각 케이스별로 instruction을 Type별로 구분하고, 우리가 받은 query와 type이 동일할 때 해당 instruction을 전달한다. 각 instruction별로 해당 질문에서 가지는 케이스가 로직을 처리하는데 필요한 함수를 호출할 때 어떤 인자를 전달하며 그 인자에 대한 포맷, 불가능한 인자 등을 서술하여 우리가 사용하는 로직에 있어서 올바르게 처리할 수 있도록 하였다.
우리 로직에서 가장 중요하게 처리해야하는 부분은 아무래도 올바른 심볼값을 전달하는 것이다. 우리의 API에서는 FRED API를 사용하고있고, 해당 API에서 받는 심볼값을 우리가 전달해주어야지 올바른 호출이 된다. 여기서 생긴 문제가 있었다. 예를 들어 S&P500지수 예측해줘 라는 질문에서 GPT가 전달해주어야하는 심볼값은 SPX이다. 하지만 가끔씩 SP500이딴 심볼을 내뱉기도하고 전혀 다른 나스닥지수의 심볼을 뱉는 등의 문제가 발생하였다. 지시사항에 S&P 500 지수 심볼: SPY 이런식으로 일단 추가하여 우리가 엣지 케이스를 발견할 때 프롬프트를 수정하는 방식으로 일단 진행하였고, 이는 모델의 문제인지 우리가 잘못 내용을 전달하고 있는지를 실험후에 수정하기로 하였다.
각각의 instruction마다 로직을 처리하는 내용이 다르기에 그 중 하나인 recommend부분을 설명해보고자 한다.
const analyzeEconomicHandler = async (symbols: string[]) => {
// 1. 심볼로 아이디 가져오기
const indicators = (
await Promise.all(
symbols.map(async (symbol) => {
const indicator = await getIndicatorBySymbol(symbol);
if (!indicator) return;
return createIndicator(indicator);
}),
)
).filter((indicator) => indicator !== undefined) as Indicator[];
// 2. 메타데이터 만들기
const metadataId = await createIndicatorBoardMetadata('Fingoo가 분석한 지표들');
// 3. 메타데이터에 지표 추가
await addIndicatorsToMetadata(metadataId, indicators);
await revalidateIndicatorBoardMetadataList();
// 4. 값 가져오기
const indicatorsValue = await Promise.all(
indicators.map(async (indicator) => {
return await getIndicatorValue(indicator);
}),
);
// 5. 메타데이터 선택
displayIndicatorBoardMetadata(metadataId);
// 6. GPT에 분석 요청
return `
분석한 지표들: ${JSON.stringify(symbols)}
분석 지표들 값: ${JSON.stringify(indicatorsValue.map((indicatorValue) => indicatorValue.values))}
- 반드시 분석한 지표에 대한 이름과 심볼명을 밝혀야합니다.
- 분석한 지표들을 바탕으로 종합적인 시장 추세를 설명해야합니다.
`;
};
각 주석에 명시된대로 우리는 해당 로직을 처리하였다. 이 로직이 처리되기 전 우리가 목표로하는 지표 여기서는 분석 지표가 될 것이고 이에 대한 심볼값을 제공해준다. 이때 우리 API를 활용하여 해당 심볼의 아이디를 가져오고 이를 가지고 메타데이터에 지표를 추가하며 해당 값을 가져오고 이를 다시 GPT에 넘겨주는 형식의 로직이다. 이런 방식을 선택하여 값을 출력하기 이전 사용자가 대기하는 시간동안 메타데이터에 값을 보여주어 먼저 차트를 확인하여 UX적 효과를 높이는 효과가 있다. 또한 가장 중요한 이유로 instruction을 받아오는 것 부터 출력까지 각 단계를 구체화시켜서 어느 한 부분에 문제가 있을 경우 이를 파악하기 좋아 리팩토링에 유리해진다. 아무튼 이러한 처리과정을 거친 후 출력을 하는 태스크로 넘아가게된다.
{
type: 'function',
function: {
name: 'speak_to_user',
// description: '도구 응답 지시사항을 준수하여 사용자의 질문에 대답합니다.',
description:
'properties의 message description에 명시된 출력 필드를 반드시 구분하고, 출력필드의 내용 그대로 사용자의 질문에 대답합니다. ',
parameters: {
type: 'object',
properties: {
message: {
type: 'string',
description: `사용자에게 전달할 메세지 입니다. 사용자의 질문 타입에 따라 출력 필드를 구분하여 메시지를 제공합니다. 출력필드 내의 내용을 반드시 따라야합니다. 반드시 한국어로 답변해야합니다.
- anaylize 출력 필드:
연관 지표 설명: 시장이나 지수 분석에 사용한 지표(심볼) 이름과 해당 연관 지표를 선택한 이유 설명 (예시: 코스피지수에서 높은 점유를 가진 삼성전자와 sk하이닉스를 참고하였습니다.)
연관 지표 추세 분석: 각각의 연관지표 6개월 동안의 대략적인 추세 설명 (예시: 6개월간의 시장 지표 AAPL: 상승 추세, TSLA: 하락 추세)
시장 및 지수에 대한 분석: 연관 지표 추세에 따른 종합적인 분석 설명 (연관 지표들의 추세를 통해 해당 시장은 점진적으로 성장한다고 볼 수 있다.)
추가 참고 지표 추천: 추가적으로 살펴보면 좋을 것 같은 연관 지표가 있다면 해당 지표의 이름(심볼명)
`,
},
},
required: ['message'],
},
},
},
해당 instruction에 맞는 각각의 출력필드를 제공하였고, 여기서 하나의 출력필드에 여러 소제목을 달아 각각의 영역을 구분하여 출력하도록 function_calling을 다음처럼 사용하였다. 이때 우리가 사용한 값들을 활용하는데 이렇게 될 경우 우리가 가지고 있는 예측지표를 통해 나온 값을 가지고 지피티가 단순 평가를 하게 되기도하고, 각각의 분석, 설명, 추천이라는 테스크에 대해 지피티가 현재의 값을 가지고 더욱 적극적으로 이에 대한 해석을 제공하게 되어 답변의 자유도가 높아지는 효과를 가질 수 있다.
일단 이러한 방식을 통해 프롬프트를 조정하였고, 현재 여러 엣지케이스를 발견하고 이를 처리하는 과정 중에 있다. 단순히 프롬프트 한 부분에 모든 내용을 다 때려박는 것이 개발과정에서 편할지는 몰라도 나중에 리팩토링하는 과정, 지피티 값에 오류가 발생하여 이를 다듬어야하는 프롬프트 엔지니어링 부분에서는 function calling을 적극적으로 활용하여 프롬프트 엔지니어링을 진행하는 것이 더욱 효과적일 것이다.