이전에 저는 AWSKRUG에서 세션을 한번 진행해본 경험이 있습니다. 그 경험에 대한 짧은 후기는 이 게시글에 정리해두기도 했고요.
단순히 해보고 싶어서 내 경험에 도움이 될 것 같아 요청을 드리고 진행했던 것인데, 감사하게도 선물을 받을 수 있었습니다.
그 중 AWS credit 100$를 받은게 있는데요. 6월에 받고... 8월까지 딱히 쓸 곳이 없어서 방치만 해두고 있었습니다...ㅎ
방치를 해 둔 이유는 그냥 어디에 쓸지 정하지 않았기 때문입니다. 유효기간이 있긴 하니, 그 전까지 등록을 해야할 건데... 이걸 언젠가 써야하긴 하는데, 뭐할때 쓸지 잘 모르겠고.. 프로젝트할 때 쓰자니 몇 달 유지하지도 못할 것 같고... 이래저래 어디에 뭘 할 때 쓸지 고민을 많이 하면서 시간만 하염없이 지나갔습니다.
그렇게 기억속에 크레딧이 있다는걸 짱박아두다가... 까먹어버렸는데요. 이후에 우연히 전철을 타고 가면서 다시 기억에서 끄집어내는 일이 생겼습니다. 바로 우아한 테크에서 온라인 라이브로 진행한 우아한 테크 세미나 라이브였는데요. 주제는 생성 AI로 똑똑하게 일하는 법
이었고, 요즘 핫한 LLM을 회사 내부에서 어떻게 이용하는지를 주제로 진행이 되었습니다.
여러 내용 중 2부의 로컬 LLM
파트에 Pull Request 코드 리뷰하기
를 들었을 때 이걸 제가 진행하는 프로젝트에도 적용해보고 싶어졌고, 아 나 AWS Credit이 있으니 한번 AWS Bedrock을 사용해서 PR 리뷰봇 한번 만들어보자!
라는 생각을 하게 되었습니다.
AWS Bedrock은 AWS에서 지원하는 LLM 서비스입니다. 미리 사전 학습된 언어 모델을 사용할 수 있고, 개발자는 해당 모델로 본인의 목적에 맞게 조정해나가며 사용하기만 하면 되죠.
AWS에서 자체적으로 제공하는 모델중에는 Titan 모델이 있는데요. 해당 모델 이외에도 Claude 등 LLM 서비스를 운영하는 업체 또한 Marketplace에 들어와 있어서 AWS 내에서 여러 LLM 모델을 함께 사용할 수 있습니다.
AWS에서 크레딧을 등록하면 정말 많은 서비스에 사용이 가능합니다. 물론 그 중 AWS Bedrock도 Amazon Bedrock이라는 이름으로 사용 가능한 서비스 목록에 들어와 있습니다.
그래서 저는 AWS에서 사용 가능한 모든 모델에 대해 해당 크레딧을 사용할 수 있는줄 알았습니다.
하지만 실제로는 AWS의 자체 모델만 크레딧으로 사용할 수 있었습니다. (Marketplace에 있는 서비스는 크레딧으로 결제 불가)
30,000원이 청구된 다음에서야 알 수 있었다는 안타까운 사실이 남아있습니다.(후술 예정)
만일 크레딧으로 Bedrock 서비스를 사용하려고 한다면, Marketplace에 있는 모델은 안 될 수도 있다는 사실을 알아두시길 바랍니다. (대부분의 크레딧은 안되지만, 되는 종류도 간혹 있음)
처음에는 Claude를 사용했습니다. (크레딧으로 결제될 줄 알았지..) 하지만 크레딧을 못쓴다는것을 깨닫고, AWS Titan으로 넘어왔습니다.
최근까지 개발을 진행하였고, 자세한 코드는 깃허브 레포에서 확인이 가능합니다.
처음에 정했던 목표는 다음과 같습니다.
diff를 가져오는 방식은 쉽게 진행되었습니다.
여기 깃허브 REST API Document에서 친절하게 알려주고 있거든요.
위 이미지처럼 데이터를 넣고, 헤더에 application/vnd.github.diff
를 넣어서 요청하면 diff에 해당하는 데이터를 반환해줍니다.
이 글을 작성하면서 한 번 더 문서를 봤는데,
https://github.com/octocat/Hello-World/pull/1347.diff
으로도 diff 파일을 가져올 수 있다고 하네요? public 레포에서만 되는건진 잘 모르겠지만, 한 가지 방법이 더 있었던 것 같습니다.
위 이미지는 참고용으로 diff가 어떻게 생겼는지 가져왔습니다. 저걸 파싱해서 페이지에서 보여주고 있겠구나 라는 생각이 듭니다.
def get_pr_diff():
api_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}"
# GitHub API 요청 헤더
headers = {
"Authorization": f"Bearer {github_token}",
"Accept": "application/vnd.github.diff"
}
# API 요청 생성
req = urllib.request.Request(api_url, headers=headers)
try:
# API 요청 실행
with urllib.request.urlopen(req) as response:
# 응답 읽기
response_data = response.read()
# 응답 디코딩 (UTF-8 사용)
response_text = response_data.decode('utf-8')
# 상태 코드 확인
status_code = response.getcode()
print(f"Status Code: {status_code}")
print(f"Response: {response_text[:100]}...") # 처음 100자만 출력
except urllib.error.HTTPError as e:
print(f"HTTP Error: {e.code}")
print(f"Reason: {e.reason}")
except urllib.error.URLError as e:
print(f"URL Error: {e.reason}")
이렇게 코드를 작성해서 Get
요청을 보내면 응답값을 받아올 수 있습니다.
이 코드에서는 urllib
를 사용했는데, requests
가 아닌 urllib
를 사용한 이유는 조금이라도 더 가벼운 것을 써야 Github Action에서 사용했을 때 조금이라도 빠르게 끝나지 않을까?
라는 쓸데없는 걱정에서 비롯된 것입니다. (현재는 인증서때문에 urllib 요청이 실패하는 경우가 있어서 requests를 사용하고 있습니다. 나중에 시간되면 urllib를 좀 분석해봐야겠다는 생각은 하고 있습니다.)
이제 변경된 사항으로 리뷰를 요청합니다. 프롬프트는 적당히 느낌있게 짜봤고... 아래 코드처럼 구성되어있습니다.
def analyze_with_bedrock(diff):
formatted_prompt = f"Human: You're a senior backend engineer. Below there is a code diff please help me do a code review.\n\nFormat:\n- Numbering issues, specify file. Use markdown, headers, code blocks.\n- Suggest improvements/examples.\n- Be constructive.\n\nPR diff:\n\n{diff}\n\nProvide detailed review. Please answer to korean Assistant:"
# botocore 세션 생성
session = botocore.session.get_session()
session.set_credentials(
access_key=access_key,
secret_key=secret_key
)
# Bedrock 런타임 클라이언트 생성
client = session.create_client('bedrock-runtime', region_name=aws_region)
# 요청 본문 생성
request_body = json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": int(max_tokens),
"messages": [
{
"role": "user",
"content": formatted_prompt
}
]
})
try:
# Bedrock 모델 호출
response = client.invoke_model(
modelId=anthropic_model,
contentType='application/json',
accept='application/json',
body=request_body
)
for event in response['body']:
# 바이트 문자열을 일반 문자열로 디코딩
response_str = event.decode('utf-8')
# JSON 파싱
response_json = json.loads(response_str)
# content의 text 추출
content_text = response_json['content'][0]['text']
return content_text
except botocore.exceptions.ClientError as error:
print("An error occurred:", error)
except (KeyError, IndexError) as e:
print("An error occurred:", e)
boto3
가 아닌 botocore
를 사용하였는데, 이것도 조금이라도 무게를 줄이고자... 한 것입니다. (왜 서비스마다 패키지가 없는걸까.. .내가 못찾는걸까...)
session
을 만들어서 로그인을 진행합니다.
이후 bedrock-runtime
이라는 클라이언트를 만들고, 요청 형식에 따라서 invoke_model
을 때려버렸습니다.
요청 형식은 여기 공식 문서를 참고하시면 편합니다. 참 잘 설명되어있는데, 이런 사이트를 찾는 것도 좀 복잡합니다. (간단한 테스트 목적으로 간단하게 구성해봤습니다.)
invoke_model을 때려버리면, 시간이 좀 걸리고... response에 응답을 담아줍니다. 이제 이 response를 utf-8로 변환하고, 응답 형식에 따라서 읽어주면 모든게 리뷰에 대한 응답을 받을 수 있습니다.
이제 목표였던 것의 마지막 단계입니다. Github에서 diff를 가져온 PR에 Comment로 LLM에서 받아온 응답을 남기면 됩니다.
def post_review(comment):
# GitHub API 엔드포인트
api_url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
# GitHub API 요청 헤더
headers = {
"Authorization": f"Bearer {github_token}",
"Accept": "application/vnd.github-commitcomment.raw+json",
"Content-Type": "application/json"
}
data = {
"body": "# [REVIEW_BOT]\n" + comment
}
# JSON 데이터를 인코딩
data = json.dumps(data).encode('utf-8')
# 요청 객체 생성
req = urllib.request.Request(api_url, data=data, headers=headers, method='POST')
try:
# API 요청 보내기
with urllib.request.urlopen(req) as response:
if response.getcode() == 201:
print("Comment posted successfully!")
else:
print(f"Failed to post comment: {response.getcode()}")
print(response.read().decode('utf-8'))
except urllib.error.HTTPError as e:
print(f"HTTP Error: {e.code}")
print(e.read().decode('utf-8'))
except urllib.error.URLError as e:
print(f"URL Error: {e.reason}")
마찬가지로 Document에 존재하는 API를 사용하여 구성하였고, 단순하게 헤더 하나와 리뷰 내용을 합쳐서 Comment로 남기게 되었습니다.
그러면 이제 이렇게 깔끔하게 리뷰를 남겨주는 것을 볼 수 있었습니다. (이렇게 깔끔하게 남겨주는건 모두 모델이 Claude Sonnet 3.5버전이었기 때문입니다...)
이제 이렇게 최소 기능만을 가진 코드 리뷰 봇을 만들어보았습니다. 개선해나갈 부분이 참 많이 보이는 것 같습니다. 좀 더 커스텀할 부분도 많았으면 좋겠고, 모델이 Claude만 되는것도 좀 불만입니다. (AWS 서비스 내에서 동작한다고 해도, 각 모델별로 요청 형식이 다름)
현재도 종종 개선해나아가고 있는데 이 개선에 대해서도 추가적으로 글을 작성할 계획이라 이번에는 여기에서 급하게 마무리를 하겠습니다.
다음 글의 내용은 해당 코드를 Github 마켓플레이스에 등록하고, Git actions에서 py 파일을 사용하는 것에서 등록한 서비스를 사용하도록 바꾸는 과정에 대해서 정리할 것 같네요.
그리고 그 다음에는 크레딧 문제로 Titan 모델을 사용할 수 있도록 수정하는 과정과, 커스텀 옵션 추가과정에 대해서 정리할 것으로 현재 계획중입니다.
모든 코드와 배포는 이 깃허브 레포지토리에서 진행되었으니 참고 부탁드립니다.