Agent = LLM + 도구(API) + 계획 능력. 질문에 "답"하는 것이 아니라 목표를 "달성"합니다.
AWS Bedrock에서 Agent를 직접 설계하고 GitHub PR 자동 리뷰 파이프라인을 구축하는 과정을 처음부터 끝까지 다룹니다. 단순히 질문에 답하는 LLM을 넘어, 스스로 판단하고 외부 API를 호출하며 Slack 알림까지 보내는 에이전트를 만들 수 있습니다. 이 글을 읽고 나면 Agent가 어떻게 동작하는지, 왜 단순 LLM 호출과 다른지, 실무에서 어떻게 써야 하는지 감이 잡힐 것입니다.
🧭 AI Agent란 무엇인가
일반 LLM과 Agent의 결정적 차이
생성형 AI를 처음 접하면 대부분 이런 경험을 합니다. "오늘 서울 날씨 알려줘"라고 물으면 LLM은 "저는 실시간 정보에 접근할 수 없습니다"라고 정중하게 거절합니다. 학습 데이터 안에만 갇혀 있기 때문입니다.
Agent는 다릅니다. 같은 질문을 받으면 "날씨 API를 호출해야겠다"고 스스로 판단하고, API를 실제로 호출하고, 결과를 받아 "서울은 맑음, 22도입니다"라고 답합니다. 이 차이가 전부입니다. 말만 하는 시스템과, 행동까지 하는 시스템의 차이.
Agent의 핵심 구성 요소를 팀에 비유하면 이렇습니다.
구성 요소 역할 팀 비유
| Planner (LLM) | 전체 계획 수립, 어떤 도구를 쓸지 결정 | 팀장 (판단과 지시) |
| Tool Invoker | API 호출, 코드 실행 등 실제 도구 작동 | 실행자 (손과 발) |
| Memory | 과거 대화 내용 및 작업 맥락 유지 | 메모장 (단기/장기 기억) |
| Knowledge Base | 학습되지 않은 외부 전문 지식 참조 | 사전/백과사전 (외부 지식) |
Agent의 내부 동작 흐름
사용자가 "PR #123 리뷰하고, 결과를 Slack에 알려줘"라고 요청하면 Agent 내부에서 다음이 일어납니다.
1단계 — Planner (LLM) 추론: "사용자가 PR 리뷰와 Slack 알림을 요청했다. 먼저 PR 내용을 가져오고, 리뷰한 후, 결과를 Slack으로 전송해야 한다."
2단계 — Tool Invoker 실행:
- Step 1: get_github_pr(owner, repo, pr_number=123) → 코드 가져오기
- Step 2: review_code(code) → 리뷰 결과 생성
- Step 3: send_slack(message=리뷰결과) → Slack 전송
3단계 — 응답 생성: "PR #123 리뷰를 완료하고 Slack으로 결과를 전송했습니다."
이 일련의 과정을 ReAct(Reasoning + Acting) 패턴이라고 부릅니다. 생각(Reasoning)과 행동(Acting)을 번갈아 가며 목표를 향해 나아가는 방식으로, 현재 대부분의 상용 Agent 시스템이 이 패턴을 기반으로 동작합니다.
🏗️ Amazon Bedrock Agent 아키텍처
우리가 만들 것: Chandler Agent
이번 글에서 구현할 에이전트는 GitHub PR을 자동으로 리뷰하는 Chandler 입니다. 전체 아키텍처는 이렇습니다.
Chandler Agent
├── Instructions (지침)
│ └── "너는 코드 리뷰 전문가야. 스타일, 버그, 보안을 검사해."
├── Knowledge Base (지식)
│ ├── PEP8 스타일 가이드
│ ├── OWASP 보안 규칙
│ └── Google Style Guide
└── Action Group (도구)
├── get_github_pr ← PR 가져오기
├── post_pr_comment ← 댓글 등록
└── send_slack_message ← Slack 알림
Bedrock Agent의 핵심 구성 요소
Bedrock에서 Agent를 구성할 때 알아야 할 5가지 개념이 있습니다.
- Instructions — Agent의 역할, 행동 규칙, 출력 형식을 정의합니다. Agent의 "업무 매뉴얼"입니다. 구체적일수록 좋습니다.
- Knowledge Base — RAG(Retrieval Augmented Generation)를 통해 참조할 외부 문서 저장소입니다. Agent에 연결하면 질문에 답할 때 자동으로 KB를 검색합니다. 코드 스타일 가이드, 보안 문서 같은 전문 지식을 여기에 넣어두면 됩니다.
- Action Group — 외부 시스템과 통신하기 위한 도구 모음입니다. Lambda 함수와 OpenAPI 스키마로 구성됩니다. Agent의 "손과 발"이 되는 부분입니다.
- Model — Agent의 두뇌 역할을 하는 LLM입니다. 이 글에서는 Claude Sonnet 4.6(global.anthropic.claude-sonnet-4-6)을 사용합니다.
- Alias — 배포 버전을 관리하는 식별자입니다. dev, prod처럼 환경을 분리할 수 있습니다. 실무에서는 반드시 사용하는 것이 좋습니다.
Agent를 만들고 사용하는 흐름은 이렇습니다.
# 1. Agent 생성
agent = create_agent(name, instruction, model, role_arn)
# 2. (선택) Knowledge Base 연결
associate_agent_knowledge_base(agent_id, kb_id)
# 3. Prepare (배포 준비 — 설정 컴파일)
prepare_agent(agent_id)
# 4. Alias 생성 (호출용)
create_agent_alias(agent_id, alias_name)
🛠️ 기본 Agent 생성 — Boto3로 처음부터 만들기
Agent 생성 코드
import boto3
# Bedrock Agent 클라이언트 초기화
bedrock_agent = boto3.client('bedrock-agent', region_name='ap-northeast-2')
bedrock_agent_runtime = boto3.client('bedrock-agent-runtime', region_name='ap-northeast-2')
agent_name = "Chandler-Reviewer"
agent_resource_role_arn = "arn:aws:iam::YOUR_ACCOUNT_ID:role/AmazonBedrockAgentServiceRole"
# Instructions 정의
instruction_content = """당신은 시니어 개발자이자 코드 리뷰 전문가입니다.
## 역할
- Python, JavaScript, Java 코드 리뷰를 수행합니다.
- 버그, 보안 취약점, 스타일 위반을 찾습니다.
## 행동 규칙
1. 코드를 받으면 반드시 Knowledge Base를 참고하여 검사합니다.
2. 발견된 문제는 심각도(높음/중간/낮음)와 함께 보고합니다.
3. 수정 제안은 구체적인 코드 예시를 포함합니다.
## 출력 형식
🔴 높은 심각도
[라인번호] 문제: 설명
수정 제안: 코드 예시
🟡 중간 심각도
...
🟢 낮은 심각도
...
## 제약사항
- 모르는 내용은 "정보 부족"이라고 답변합니다.
- 보안 취약점은 반드시 보고합니다."""
foundation_model = "global.anthropic.claude-sonnet-4-6"
# Agent 생성 또는 업데이트
agent_id = None
try:
# 기존 Agent 확인
list_agents_response = bedrock_agent.list_agents(maxResults=100)
for agent_summary in list_agents_response['agentSummaries']:
if agent_summary['agentName'] == agent_name:
agent_id = agent_summary['agentId']
print(f"기존 Agent 발견: {agent_id}")
break
if agent_id:
# 기존 Agent 업데이트
bedrock_agent.update_agent(
agentId=agent_id,
agentName=agent_name,
agentResourceRoleArn=agent_resource_role_arn,
instruction=instruction_content,
foundationModel=foundation_model,
description="코드 리뷰 및 보안 분석 에이전트"
)
print(f"✅ Agent 업데이트 완료: {agent_id}")
else:
# 새 Agent 생성
response = bedrock_agent.create_agent(
agentName=agent_name,
agentResourceRoleArn=agent_resource_role_arn,
instruction=instruction_content,
foundationModel=foundation_model,
description="코드 리뷰 및 보안 분석 에이전트"
)
agent_id = response['agent']['agentId']
print(f"✅ Agent 생성 완료: {agent_id}")
except Exception as e:
print(f"오류 발생: {e}")
if "AccessDeniedException" in str(e):
print("HINT: IAM 역할에 bedrock:InvokeModel 권한이 있는지 확인하세요.")
좋은 Instructions를 작성하는 법
Instructions는 Agent의 업무 매뉴얼입니다. 모호하게 쓰면 Agent도 모호하게 행동합니다.
항목 나쁜 예 좋은 예
| 역할 명확화 | "코드 리뷰해줘" | "당신은 10년차 시니어 개발자입니다" |
| 구체적 작업 | "버그 찾아줘" | "논리적 오류, 예외 처리 누락, 무한 루프를 찾아주세요" |
| 출력 형식 | (형식 없음) | "Markdown 테이블로 정리해주세요" |
| 참고 자료 | (없음) | "Knowledge Base의 PEP8 문서를 먼저 검색하세요" |
| 제약 조건 | (없음) | "모르면 '모르겠습니다'라고 답변" |
Instructions 분량도 중요합니다. 단순 요약기나 번역기라면 200~500 토큰으로도 충분하지만, 코드 리뷰처럼 여러 판단이 필요한 에이전트라면 500~1500 토큰 수준의 상세한 지침이 더 잘 동작합니다.
Knowledge Base 연결
3장에서 미리 구축해둔 Knowledge Base(코드 스타일 가이드, 보안 문서 등)를 Agent에 연결합니다.
# 이미 생성된 Knowledge Base ID
kb_id = "YOUR_KNOWLEDGE_BASE_ID"
association = bedrock_agent.associate_agent_knowledge_base(
agentId=agent_id,
agentVersion="DRAFT",
knowledgeBaseId=kb_id,
description="코드 스타일 및 보안 규칙",
knowledgeBaseState="ENABLED"
)
print(f"✅ Knowledge Base 연결 완료: {kb_id}")
KB를 연결하면 Agent가 질문에 답할 때 자동으로 관련 문서를 검색해 활용합니다. Instructions에 KB 활용 지침을 명시해두면 더욱 일관성 있게 동작합니다.
## Knowledge Base 활용 방법
- 코드 스타일 검사 시 반드시 Knowledge Base의 PEP8 문서를 참고하세요.
- 보안 취약점 탐지 시 Knowledge Base의 OWASP 문서를 참고하세요.
- 검색 결과가 없으면 "관련 문서를 찾을 수 없습니다"라고 알려주세요.
Agent Prepare와 Alias
설정을 마쳤으면 Prepare를 실행해야 합니다. Prepare는 Instructions, KB, Action Group 설정을 실제 배포 가능한 상태로 컴파일하는 과정입니다.
import time
# Prepare 실행
prepare_response = bedrock_agent.prepare_agent(agentId=agent_id)
print(f"Prepare 요청 완료. 상태: {prepare_response['agentStatus']}")
# Prepare 완료 대기 (최대 60초)
for i in range(60):
agent_info = bedrock_agent.get_agent(agentId=agent_id)
status = agent_info['agent']['agentStatus']
if status == 'PREPARED':
print(f"✅ PREPARED 완료 (소요: {i+1}초)")
break
elif status == 'FAILED':
print(f"❌ Prepare 실패: {agent_info['agent']['failureReasons']}")
break
time.sleep(1)
# Alias 생성 (호출용)
alias_response = bedrock_agent.create_agent_alias(
agentId=agent_id,
agentAliasName="dev",
description="개발 환경용 Alias"
)
alias_id = alias_response['agentAlias']['agentAliasId']
print(f"✅ Alias 생성 완료: {alias_id}")
Alias가 필요한 이유는 세 가지입니다. 개발(dev)과 운영(prod) 환경을 물리적으로 분리해 코드 수정이 실 서비스에 바로 영향을 미치지 않도록 방지할 수 있고, 문제 발생 시 이전 버전을 가리키는 Alias로 즉시 교체해 롤백할 수 있습니다. 두 버전을 각각 다른 Alias로 운영해 A/B 테스트도 할 수 있습니다.
Agent 호출
def invoke_agent(prompt, session_id="test-session"):
"""Agent를 호출하고 스트리밍 응답을 하나의 문자열로 반환"""
response = bedrock_agent_runtime.invoke_agent(
agentId=agent_id,
agentAliasId=alias_id,
sessionId=session_id, # 사용자별 고유 세션 ID
inputText=prompt,
enableTrace=False
)
# 스트리밍 응답 처리
full_response = ""
for event in response['completion']:
if 'chunk' in event:
chunk_bytes = event['chunk']['bytes']
full_response += chunk_bytes.decode('utf-8')
return full_response
# 코드 리뷰 요청 테스트
test_code = """
def divide(a, b):
return a / b
def get_user(id):
query = f"SELECT * FROM users WHERE id = {id}"
return execute(query)
"""
prompt = f"""
다음 코드를 리뷰해주세요:
```python
{test_code}
버그, 보안 취약점, 스타일 문제를 찾아주세요. """
response = invoke_agent(prompt, session_id="code-review-session") print("Agent 응답:") print(response)
`sessionId`는 대화 컨텍스트를 유지하는 핵심입니다. 동일한 세션 ID로 여러 번 호출하면 이전 대화를 기억합니다. "이 함수 봐줘" 이후 "방금 본 코드에서 버그 수정해줘" 같은 멀티턴 대화가 가능한 이유입니다. 기본 세션 만료 시간은 마지막 대화로부터 1시간입니다.
### Trace로 Agent의 추론 과정 들여다보기
`enableTrace=True`를 설정하면 Agent가 어떻게 생각하는지 볼 수 있습니다.
```python
def inspect_agent_thinking(prompt):
response = bedrock_agent_runtime.invoke_agent(
agentId=agent_id,
agentAliasId=alias_id,
sessionId="trace-demo",
inputText=prompt,
enableTrace=True # Trace 활성화
)
for event in response['completion']:
if 'trace' in event:
trace_info = event['trace']['trace']
# 전처리 단계 — 사용자 입력 해석
if 'preProcessingTrace' in trace_info:
pre = trace_info['preProcessingTrace']
print(f"[전처리] {pre.get('rationale', 'N/A')}")
# 계획 수립 단계 — Agent의 생각
if 'orchestrationTrace' in trace_info:
orch = trace_info['orchestrationTrace']
print(f"\n[계획 수립] {orch.get('rationale', 'N/A')}")
elif 'chunk' in event:
print(f"\n[최종 응답]:")
print(event['chunk']['bytes'].decode('utf-8'))
Trace를 통해 Agent가 문제를 어떻게 이해했는지, KB에서 무엇을 검색했는지, 어떤 도구를 호출하려고 하는지, 최종 응답을 어떻게 생성했는지 전 과정을 확인할 수 있습니다. 디버깅할 때 필수입니다.
🔧 Tool Use — Agent에 손발을 달아주다
Tool Use(Function Calling)란
단순 LLM과 Agent의 가장 큰 차이는 Tool Use입니다. Tool Use가 없으면 Agent는 말만 합니다. Tool Use가 있으면 행동합니다.
❌ Tool Use 없을 때:
사용자: "내 GitHub PR 가져와줘"
Agent: "죄송합니다. 저는 GitHub에 접근할 수 없어요."
✅ Tool Use 있을 때:
사용자: "내 GitHub PR 가져와줘"
Agent: (내부 추론) "get_github_pr 도구를 써야겠다!"
Agent: Lambda 호출 → GitHub API → PR 정보 반환
Agent: "PR #123: Fix login bug (작성자: devkim, 상태: open, 변경 파일: 3개)"
Action Group의 구조
Bedrock Agent에서 Tool은 Action Group 단위로 관리됩니다. Action Group은 세 가지 요소로 구성됩니다.
1. OpenAPI Schema (도구 설명서) Agent가 이 문서를 읽고 "언제, 어떻게" 도구를 사용할지 결정합니다. description 필드가 핵심입니다. 구체적으로 쓸수록 Agent가 정확한 상황에서 도구를 선택합니다.
2. Lambda Function (실제 실행 코드) GitHub API 호출, 데이터 처리, 결과 반환 등 실제 로직이 담깁니다.
3. IAM Role (권한) Agent가 Lambda를 호출할 수 있도록 허용하는 권한 설정입니다.
첫 번째 Tool: GitHub PR 가져오기
Tool 스펙:
항목 내용
| Tool 이름 | get_github_pr |
| 설명 | GitHub Pull Request의 상세 정보를 가져옵니다. |
| 파라미터 | owner, repo, pr_number |
| 반환값 | PR 제목, 설명, 변경 파일 목록 등 |
Agent가 이 Tool을 사용하는 흐름:
사용자: "my-org/my-repo의 PR #123 가져와줘"
Agent 내부 추론:
1. "사용자가 PR 정보를 요청했네"
2. "get_github_pr 도구를 사용해야겠다"
3. "파라미터: owner='my-org', repo='my-repo', pr_number=123"
4. → Lambda 호출!
Lambda: GitHub API 호출 → PR 정보 가져오기 → 결과 반환
Agent: "PR #123: Fix login bug (작성자: devkim, 상태: open, 변경 파일: 3개)"
Lambda 함수 구현:
import json
import os
import logging
from github import Github, GithubException
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def handler(event, context):
logger.info(f"Received event: {json.dumps(event)}")
try:
# Bedrock Agent가 전달한 파라미터 추출
parameters = {p["name"]: p["value"] for p in event.get("parameters", [])}
owner = parameters.get("owner")
repo_name = parameters.get("repo")
pr_number = parameters.get("pr_number")
# 필수 파라미터 검증
if not all([owner, repo_name, pr_number]):
raise ValueError("Missing required parameters: owner, repo, pr_number")
# GitHub API 호출
g = Github(os.environ['GITHUB_TOKEN'])
repo = g.get_repo(f"{owner}/{repo_name}")
pr = repo.get_pull(int(pr_number))
# 응답 생성
response_body = {
"title": pr.title,
"body": pr.body or "",
"state": pr.state,
"author": pr.user.login,
"created_at": pr.created_at.isoformat(),
"updated_at": pr.updated_at.isoformat(),
"changed_files": pr.changed_files,
"additions": pr.additions,
"deletions": pr.deletions,
"diff_url": pr.diff_url
}
# Bedrock Agent가 이해하는 형식으로 반환
return {
"messageVersion": "1.0",
"response": {
"actionGroup": event["actionGroup"],
"apiPath": event["apiPath"],
"httpMethod": event["httpMethod"],
"httpStatusCode": 200,
"responseBody": {
"application/json": {"body": response_body}
}
}
}
except GithubException as e:
logger.error(f"GitHub API error: {e}")
return build_error_response(event, f"GitHub API error: {e.data.get('message', str(e))}", 404)
except Exception as e:
logger.error(f"Unexpected error: {e}")
return build_error_response(event, str(e), 500)
def build_error_response(event, error_msg, status_code):
return {
"messageVersion": "1.0",
"response": {
"actionGroup": event.get("actionGroup"),
"apiPath": event.get("apiPath"),
"httpMethod": event.get("httpMethod"),
"httpStatusCode": status_code,
"responseBody": {
"application/json": {"body": {"error": error_msg}}
}
}
}
Lambda 응답은 반드시 messageVersion, response 구조를 따라야 합니다. 이 형식이 맞지 않으면 Agent가 결과를 이해하지 못합니다.
OpenAPI Schema 작성:
{
"openapi": "3.0.0",
"info": {
"title": "GitHub PR Tools",
"version": "1.0.0"
},
"paths": {
"/pr": {
"get": {
"operationId": "get_github_pr",
"summary": "Get GitHub Pull Request details",
"description": "Retrieves detailed information about a specific GitHub Pull Request. Use this when the user asks about PR details, wants to review a PR, or needs to analyze code changes in a pull request.",
"parameters": [
{
"name": "owner",
"in": "query",
"required": true,
"schema": {"type": "string"},
"description": "The GitHub repository owner/organization name (e.g., 'aws-samples', 'facebook')"
},
{
"name": "repo",
"in": "query",
"required": true,
"schema": {"type": "string"},
"description": "The GitHub repository name"
},
{
"name": "pr_number",
"in": "query",
"required": true,
"schema": {"type": "integer"},
"description": "The Pull Request number to retrieve"
}
],
"responses": {
"200": {
"description": "PR details retrieved successfully",
"content": {"application/json": {"schema": {"type": "object"}}}
}
}
}
}
}
}
description 필드는 Agent가 읽는 "사용 설명서"입니다. "Use this when the user asks about PR details"처럼 언제 써야 하는지 명확히 적어주면 Agent가 올바른 상황에서 도구를 선택합니다.
Action Group 생성:
import json
def create_action_group(agent_id, lambda_arn, api_schema):
response = bedrock_agent.create_agent_action_group(
agentId=agent_id,
agentVersion="DRAFT",
actionGroupName="GitHubPRTools",
actionGroupExecutor={
"lambda": lambda_arn # Lambda 함수 ARN
},
apiSchema={
"payload": json.dumps(api_schema) # OpenAPI 스키마
},
actionGroupState="ENABLED",
description="Tools for fetching GitHub Pull Request information"
)
action_group_id = response['agentActionGroup']['actionGroupId']
print(f"✅ Action Group 생성 완료: {action_group_id}")
return action_group_id
⚠️ Action Group 이름에는 하이픈(-)을 사용하지 않는 것이 좋습니다. GitHubPRTools처럼 카멜케이스나 밑줄을 사용하세요. 또한 Action Group을 추가하거나 변경하면 반드시 Agent를 다시 Prepare해야 합니다. 그리고 Lambda 함수에 Bedrock Agent가 호출할 수 있도록 리소스 기반 정책도 추가해야 합니다.
lambda_client = boto3.client('lambda')
lambda_client.add_permission(
FunctionName=lambda_function_name,
StatementId='AllowBedrockAgentInvoke',
Action='lambda:InvokeFunction',
Principal='bedrock.amazonaws.com',
SourceArn=f'arn:aws:bedrock:{region}:{account_id}:agent/{agent_id}'
)
이 단계를 빠뜨리면 Agent가 Lambda를 호출할 수 없습니다.
Token 보안 관리
GitHub Personal Access Token은 절대로 코드에 직접 입력하지 않는 것이 좋습니다. AWS Secrets Manager나 Lambda 환경 변수에 저장하는 방법을 권장합니다. Colab에서 실습할 때는 Colab Secrets를 활용하세요.
from google.colab import userdata
GITHUB_TOKEN = userdata.get('GITHUB_TOKEN')
🚀 Tool 확장 — PR 댓글, Slack 알림까지
이제 Agent가 정보를 가져오는 것을 넘어, 실제로 뭔가를 "하는" 단계입니다. PR에 댓글을 남기고, Slack으로 알림을 보내는 Tool을 추가합니다.
두 번째 Tool: PR에 댓글 남기기
Tool 스펙:
항목 내용
| Tool 이름 | post_pr_comment |
| 설명 | GitHub Pull Request에 댓글을 추가합니다. |
| 파라미터 | owner, repo, pr_number, comment |
| Lambda 역할 | GitHub API (POST /issues/{pr_number}/comments) 호출 |
# Lambda 함수: post_pr_comment
def handler(event, context):
try:
parameters = {p["name"]: p["value"] for p in event.get("parameters", [])}
owner = parameters.get("owner")
repo_name = parameters.get("repo")
pr_number = int(parameters.get("pr_number"))
comment = parameters.get("comment")
if not all([owner, repo_name, pr_number, comment]):
raise ValueError("Missing required parameters")
# GitHub API 호출 — 댓글 등록
g = Github(os.environ['GITHUB_TOKEN'])
repo = g.get_repo(f"{owner}/{repo_name}")
pr = repo.get_pull(pr_number)
pr.create_issue_comment(comment)
response_body = {
"success": True,
"message": f"Comment added to PR #{pr_number}",
"pr_url": pr.html_url
}
return build_success_response(event, response_body)
except GithubException as e:
return build_error_response(event, str(e), 400)
except Exception as e:
return build_error_response(event, str(e), 500)
GitHub API를 직접 호출할 때는 PyGithub 라이브러리와 requests 라이브러리 두 가지 선택지가 있습니다.
방식 장점
| PyGithub | 코드가 직관적이고 간결, 속도 제한 처리 자동화 |
| 직접 requests | 외부 라이브러리 없이 가볍게 처리, Lambda 패키지 크기 절감 |
PyGithub는 약 2MB, requests는 약 0.5MB입니다. Lambda 제한(250MB)을 감안하면 둘 다 사용 가능하지만, 의존성을 줄이고 싶다면 requests를 선택할 수 있습니다.
OpenAPI Schema에 새 경로를 추가합니다.
"/pr/comment": {
"post": {
"operationId": "post_pr_comment",
"summary": "Add a comment to a Pull Request",
"description": "Adds a comment to a specific GitHub Pull Request. Use this when the user wants to leave feedback, approval, or questions on a PR.",
"parameters": [
{"name": "owner", "in": "query", "required": true, "schema": {"type": "string"}, "description": "Repository owner/organization"},
{"name": "repo", "in": "query", "required": true, "schema": {"type": "string"}, "description": "Repository name"},
{"name": "pr_number", "in": "query", "required": true, "schema": {"type": "integer"}, "description": "Pull Request number"},
{"name": "comment", "in": "query", "required": true, "schema": {"type": "string"}, "description": "The comment text to post"}
],
"responses": {"200": {"description": "Comment added successfully"}}
}
}
세 번째 Tool: Slack 알림 보내기
Tool 스펙:
항목 내용
| Tool 이름 | send_slack_message |
| 설명 | Slack 채널로 메시지를 전송합니다. |
| 파라미터 | channel, message |
| Lambda 역할 | Slack Incoming Webhook URL로 POST 요청 발송 |
Slack 연동 방법은 두 가지입니다.
방식 특징
| Incoming Webhook | 설정이 간단, 특정 채널에 고정 |
| Chat.postMessage API | 유연성이 높음, OAuth 토큰 필요, 채널을 동적으로 지정 가능 |
처음 시작하는 경우에는 Incoming Webhook이 간단하고 좋습니다.
# Lambda 함수: send_slack_message
import requests
def handler(event, context):
try:
params = {p["name"]: p["value"] for p in event.get("parameters", [])}
channel = params.get("channel")
message = params.get("message")
if not channel or not message:
raise ValueError("Missing channel or message")
webhook_url = os.environ['SLACK_WEBHOOK_URL']
# Slack 메시지 전송
payload = {"text": message}
if channel:
payload["channel"] = channel
response = requests.post(webhook_url, json=payload)
response.raise_for_status()
result = {"success": True, "message": "Slack message sent"}
return build_success_response(event, result)
except Exception as e:
return build_error_response(event, str(e), 500)
⚠️ Slack Webhook URL은 비밀번호와 같습니다. 절대 코드에 직접 입력하지 말고, Lambda 환경 변수나 AWS Secrets Manager에 저장하세요.
세 가지 Tool을 하나의 Action Group으로 통합
# 최종 통합 API 스키마 — 세 가지 Tool 모두 포함
final_api_schema = {
"openapi": "3.0.0",
"info": {"title": "GitHub PR Tools", "version": "1.0.0"},
"paths": {
"/pr": {"get": {...}}, # get_github_pr
"/pr/comment": {"post": {...}}, # post_pr_comment
"/slack/send": {"post": {...}} # send_slack_message
}
}
# Action Group 업데이트
bedrock_agent.update_agent_action_group(
agentId=agent_id,
agentVersion="DRAFT",
actionGroupId=action_group_id,
actionGroupName="GitHubPRTools",
actionGroupExecutor={"lambda": lambda_arn},
apiSchema={"payload": json.dumps(final_api_schema)},
actionGroupState="ENABLED"
)
# 변경 후 반드시 Prepare 재실행
bedrock_agent.prepare_agent(agentId=agent_id)
print("✅ Agent Prepare 완료 — 세 가지 Tool 사용 가능")
복합 시나리오 테스트: 한 번의 요청으로 세 가지 작업 자동 실행
def test_multi_tool_scenario(agent_id, alias_id):
prompt = """
다음 작업을 순서대로 수행해주세요:
1. my-org/my-repo의 PR #123 정보를 가져오고
2. 코드 리뷰 결과를 '코드가 깔끔합니다! 수고하셨어요' 댓글로 남겨주세요
3. 완료되면 #code-review 채널에 'PR #123 리뷰 완료' 메시지를 보내주세요
"""
response = bedrock_agent_runtime.invoke_agent(
agentId=agent_id,
agentAliasId=alias_id,
sessionId="multi-tool-session",
inputText=prompt,
enableTrace=True
)
for event in response['completion']:
if 'chunk' in event:
print(event['chunk']['bytes'].decode('utf-8'))
elif 'trace' in event:
trace = event['trace']['trace']
if 'orchestrationTrace' in trace:
orch = trace['orchestrationTrace']
rationale = orch.get('rationale', '')
if rationale:
print(f"\n💭 Agent 생각: {rationale[:200]}...")
Agent는 이 요청을 받아 스스로 3단계로 계획을 세웁니다.
- get_github_pr 실행 → PR 정보 획득
- 코드를 내부적으로 분석한 뒤 post_pr_comment 실행
- send_slack_message 실행
최종 응답: "모든 작업이 완료되었습니다."
순차 호출 vs 병렬 호출
여러 Tool을 조합할 때 두 가지 전략이 있습니다.
순차 호출(Sequential): 앞선 도구의 결과가 뒤따르는 도구의 입력이 되는 경우. PR 내용을 먼저 가져온 뒤, 그 내용을 분석해서 댓글을 다는 것이 대표적입니다.
병렬 호출(Parallel): 서로 독립적인 도구를 동시에 호출하는 경우. 리뷰가 끝난 시점에 GitHub 댓글 등록과 Slack 알림을 동시에 실행하면 전체 지연 시간을 줄일 수 있습니다. Bedrock Agent는 기본적으로 순차 호출하지만, 프롬프팅을 통해 병렬 호출도 유도할 수 있습니다.
Tool 호출 횟수 제한: max_iterations
Agent가 무한 루프에 빠지거나 불필요한 비용이 발생하는 것을 막기 위한 설정입니다.
bedrock_agent.update_agent(
agentId=agent_id,
...
maxIterations=5 # 최대 5번의 Tool 호출 허용
)
사용 사례 추천 max_iterations
| 단순 정보 조회 (1단계) | 1 ~ 2 |
| 2~3단계 워크플로우 | 5 |
| 복잡한 다단계 자동화 | 10 ~ 15 |
권장 공식: 예상 Tool 호출 수 + 2 (여유분). max_iterations에 도달하면 Agent는 더 이상 Tool을 호출하지 않고 현재까지의 결과를 바탕으로 응답을 생성합니다.
Tool 결과의 자연어 변환
Lambda가 반환한 원시 JSON을 Agent가 자동으로 자연어로 재구성합니다. Instructions에 포맷 지침을 추가하면 더 일관성 있는 응답을 얻을 수 있습니다.
## Tool 결과 처리 규칙
- get_github_pr 결과: "PR #{number}: {title} (작성자: {author}, 상태: {state})"
- post_pr_comment 결과: "✅ 댓글이 성공적으로 등록되었습니다."
- send_slack_message 결과: "📨 Slack 알림이 전송되었습니다."
Lambda 반환 (Raw) Agent가 재구성 (자연어)
| {"title":"Fix bug", "state":"open"} | "PR 제목은 'Fix bug'이고, 현재 상태는 열려있습니다." |
⚠️ 주의사항
1. Action Group 변경 후 반드시 Prepare를 재실행하세요. Instructions, Action Group, Knowledge Base 설정을 바꾸면 반드시 prepare_agent()를 다시 실행해야 합니다. Prepare를 빠뜨리면 이전 설정 그대로 동작합니다.
2. GitHub Token, Slack Webhook URL을 코드에 하드코딩하지 마세요. API 키나 Webhook URL을 코드에 직접 입력하면 Git에 올라가는 순간 유출될 수 있습니다. Lambda 환경 변수나 AWS Secrets Manager에 저장하는 것이 좋습니다.
3. Lambda 응답 형식을 정확히 지키세요. Lambda가 Bedrock Agent에게 응답할 때는 messageVersion, response, responseBody 구조를 반드시 따라야 합니다. 형식이 맞지 않으면 Agent가 결과를 이해하지 못합니다.
4. Action Group 이름에 하이픈(-)을 사용하지 마세요. GitHub-PR-Tools 대신 GitHubPRTools나 GitHub_PR_Tools를 사용하세요.
5. Bedrock Agent IAM 역할과 Lambda 리소스 기반 정책을 모두 설정하세요. Agent가 Lambda를 호출하려면 두 가지 권한이 모두 필요합니다. Lambda 리소스 기반 정책에 Bedrock Agent를 Principal로 추가하는 단계를 빠뜨리기 쉬우니 주의가 필요합니다.
💡 직접 구현하면서 느낀 것들
Bedrock Agent를 직접 구현해보면 몇 가지 인상적인 지점이 있습니다.
Instructions의 품질이 전부에 가깝습니다. 같은 도구를 연결해도 Instructions를 어떻게 쓰느냐에 따라 Agent 동작이 완전히 달라집니다. "코드 리뷰해줘"와 "10년차 시니어 개발자로서 논리적 오류, 예외 처리 누락, SQL Injection 취약점을 심각도와 함께 보고해줘"는 결과가 다릅니다. Instructions 작성이 사실상 엔지니어링의 핵심입니다.
OpenAPI Schema의 description이 도구 선택 정확도를 결정합니다. Agent는 사용자의 자연어 요청과 Schema의 description을 비교해서 어떤 도구를 쓸지 결정합니다. "언제 이 도구를 써야 하는지" 명확히 적어둘수록 오발동이 줄어듭니다.
Trace는 디버깅의 핵심입니다. Agent가 예상과 다르게 동작할 때 enableTrace=True로 추론 과정을 보면 원인을 빠르게 파악할 수 있습니다. "왜 이 도구를 선택했지?", "왜 이 파라미터를 넣었지?"를 직접 확인할 수 있어서 실제로 많이 씁니다.
Agent와 직접 LLM 호출의 경계를 명확히 해두는 것이 좋습니다. 단순 Q&A나 예산/성능 최적화가 우선이라면 직접 호출이 낫습니다. RAG가 필요하거나, 외부 API를 호출해야 하거나, 멀티턴 복잡한 작업이라면 Agent가 적합합니다.
앞으로의 방향: Multi-Agent Collaboration
2025년 3월, Amazon Bedrock에서 Multi-Agent Collaboration이 정식 출시되었습니다. 단일 에이전트로 감당하기 어려운 복잡한 워크플로우를 여러 전문화된 에이전트가 협력해서 처리하는 방식입니다.
Supervisor 에이전트가 전체 요청을 분석하고, 하위 전문 에이전트들에게 작업을 위임하는 구조입니다. 예를 들어 금융 분석 시스템이라면 데이터 수집 에이전트, 트렌드 분석 에이전트, 투자 추천 에이전트가 각자의 역할을 맡아 병렬로 처리하고 결과를 통합하는 방식입니다. 단일 에이전트 대비 복잡한 다단계 작업에서 정확도와 처리 속도가 개선된다고 알려져 있습니다.
또한 Amazon Bedrock AgentCore라는 플랫폼도 출시되어, 에이전트의 메모리(에피소딕 메모리 포함), 보안(Identity, Policy), 관찰 가능성(Observability) 등 프로덕션 운영에 필요한 기능들이 점점 정교해지고 있습니다. Agent를 단순히 만드는 것을 넘어 안전하게 운영하는 방향으로 생태계가 빠르게 발전하고 있습니다.
📚 참고 자료
'Concepts > Cloud Infra' 카테고리의 다른 글
| Amazon Bedrock으로 AI 코드 리뷰어 만들기: LLM 호출부터 RAG까지 완전 정복 (0) | 2026.05.24 |
|---|---|
| AWS Bedrock RAG로 코드 스타일 & 보안 검사 자동화 구현하기 (0) | 2026.05.22 |
| AWS 기초개념 : 가상화&EC2 (0) | 2026.05.19 |
| AWS 구성 및 IAM (0) | 2026.05.18 |
| 클라우드 컴퓨팅 (0) | 2026.05.13 |
