AWS 기술 블로그
A1Mobilsoft의 운영 문의 자동화 여정 (2): 세 에이전트 구현과 사람 vs AI 검증
본 블로그 시리즈는 A1Mobilsoft, Megazone, AWS 가 공동 작성 하였으며, 두 편으로 이루어져 있습니다.
각 편의 구성은 다음과 같습니다.
- 1편 (0~3장): 문제 정의와 3-에이전트 아키텍처 설계
- 2편 (4~7장, 현재 글): 세 에이전트 구현 우여곡절과 사람 vs AI 검증
2편: Bedrock AgentCore·Strands 기반 구현 디테일, 프롬프트 캐시, 그리고 14건 병렬 비교 결과
1편에서는 32개월간 846건이 쌓인 슬랙 #운영개발문의 채널의 문제 정의와, Knowledge·Code·DB 3-에이전트 아키텍처 설계를 다뤘습니다. 이번 2편에서는 A1Mobilsoft·메가존클라우드(MegazoneCloud)·AWS가 AWS DevCraft 2.0을 통해 함께 구현하며 겪은 우여곡절과, 실제 사람 vs AI 병렬 비교 검증 결과를 서술합니다.
4. 우여곡절: 에이전트별 구현 디테일
4.1 Knowledge Agent: ‘비슷한데 다른 문제’ 잡기
4.1.1 처음엔 그냥 벡터 검색이었다, 그리고 바로 깨졌다
처음 버전은 단순했습니다. Bedrock Knowledge Base에 Slack 스레드 원문을 넣고, retrieve 도구로 Top 5를 받아 Supervisor에 그대로 넘기는 방식이었죠. 그런데 두 가지 오답 패턴이 유난히 자주 나왔습니다.
케이스 1 — 키워드 동거 오답
| 항목 | 내용 |
|---|---|
| 현재 문의 | “셀프클레임 신청이 안 넘어가요” |
| Knowledge Base 1순위 (벡터 score 0.82) | “셀프클레임 취소가 안 돼요” (작년 케이스) |
| 문제 | 키워드(셀프클레임/안 됨)는 90% 일치하지만 흐름은 정반대(신청 vs 취소). 그대로 인용하면 완전히 틀린 답. |
케이스 2 — 도메인은 같은데 화면이 다르다
| 항목 | 내용 |
|---|---|
| 현재 문의 | “차량 사진 업로드가 안 돼요” (폼 모바일, consign_photo) |
| Knowledge Base 1순위 | “사진 업로드가 안 돼요” (렌카 ReviewPhoto) |
| 문제 | 같은 도메인에 같은 단어지만, 다른 레포·다른 코드. 그대로 인용하면 잘못된 파일 경로가 답변에 박힘. |
벡터 검색이 풀지 못하는 건 결국 ‘의미적으로 같은 문제인가?’라는 판단이고, 이건 LLM이 훨씬 잘합니다. 그래서 검색 단계에 LLM 두 패스를 더 끼워 넣은 것이 지금의 구조입니다.
4.1.2 Retrieve → LLM Rerank → Diagnosis 3단계 구성
[Stage 1] Retrieve : Strands retrieve 도구 → Bedrock Knowledge Base에서 Top 5 + score
(numberOfResults=5, score≥0.4, enableMetadata=true)
[Stage 2] Rerank : Haiku가 query_type 분류(오류/절차/데이터/요청)
+ 각 후보를 0~10점 재평가 → 5점 미만 컷
[Stage 3] Diagnosis : Haiku가 "현재 문의와 정말 같은 문제인가?" 엄격 판단
출력: { match: bool, confidence: 0.0~1.0,
recommendation: str, matched_cases: [int] }
설계 의도는 분명합니다.
- Stage 1은 의도적으로 느슨하게 잡습니다. MIN_SCORE=0.4, MAX_RESULTS=5. 여기서 너무 좁히면 Stage 2·3가 평가할 후보 자체가 사라집니다. 처음에는 0.6 이상으로 잡았다가 ‘0.55짜리 정답 사례를 일찍 잘라버린’ 사고가 잦아 0.4까지 내렸습니다. 거르는 일은 LLM 단계에 위임한다는 분담 원칙입니다.
- Stage 2는 산출물이 둘입니다. Rerank 프롬프트는 점수만이 아니라 현재 문의의 유형(오류/절차/데이터/요청)도 함께 분류합니다. 덕분에 절차 류 문의에 ‘오류’ 사례가 1순위로 와도 자동으로 걸러집니다.
- Stage 3가 마지막 게이트입니다. Knowledge Base 답변을 Supervisor가 그대로 채택할지, Code Agent로 넘길지가 여기서 갈립니다.
4.1.3 Diagnosis 출력 스키마 + confidence 임계값 튜닝 히스토리
{
"match": true | false,
"confidence": 0.0 ~ 1.0,
"recommendation": "해결법 또는 안내 메시지",
"matched_cases": [일치하는 사례 인덱스 번호]
}
| 시도 | confidence 임계값 | 결과 |
|---|---|---|
| v1 | confidence ≥ 0.7 단일 컷 | 0.65~0.75 구간 false positive 다수 발생 |
| v2 | confidence ≥ 0.8 단일 컷 | false positive 감소, false negative 증가 |
| v3 (현재) | match=true AND confidence ≥ 0.75 | 운영 채널 30건 백테스트 + 합성 50건 측정 결과, false positive 사실상 0, false negative도 v2 대비 개선 |
핵심 설계는 match boolean 단일 게이트입니다. 초기 버전에서는 confidence ≥ 0.7만으로 답변 채택 여부를 결정했는데, 0.65~0.75 구간이 가장 위험했습니다. ‘비슷한데 다른 문제’가 자주 그 점수대에 떴고, 그게 그대로 오답으로 새어 나갔습니다. 임계값을 0.75 → 0.8 → 0.85로 올려보면 false positive는 줄지만, 이번엔 정답까지 같이 잘리는 false negative가 폭증했습니다. 그래서 match boolean을 별도의 1차 게이트로 떼어내고, confidence는 우선순위와 UI 표시용으로만 한정했습니다.
부정 조건을 명시적으로 나열한 것도 핵심이었습니다. Diagnosis 프롬프트에 false 트리거 4개를 박아 둔 것이, ‘긍정만 잘 맞히면 된다’는 모델의 경향을 누르는 데 가장 효과적이었습니다.
match: false (아래 중 하나라도 해당되면 false)
- 같은 키워드를 사용하지만 문의 목적이 다름
- 대상/환경이 다름
- 과거 사례는 시스템 장애/버그인데 현재 문의는 업무 절차/방법 질문
- 과거 사례의 해결책이 현재 문의에 적용 불가능
4.1.4 match=false 케이스 예시
앞서 본 케이스 1을 그대로 다시 봅니다.
[현재 문의] 셀프클레임 신청이 안 넘어가요
[Knowledge Base 1순위 (벡터 score 0.82)] 셀프클레임 취소가 안 돼요
[Diagnosis 출력]
{
"match": false,
"confidence": 0.4,
"recommendation": "유사 사례는 있으나 동일 문제로 판단하기 어렵습니다",
"matched_cases": []
}
Diagnosis가 ‘신청 흐름과 취소 흐름은 반대 방향이며 해결 절차도 다르다’고 명시하며 걸러냅니다. 그래서 Supervisor는 Knowledge Base 답변 채택을 포기하고, 정상적으로 Code Agent 호출 단계로 넘어갑니다. 벡터 score만 봤다면 그대로 오답이 나갔을 케이스가 막힌 셈입니다.
4.1.5 왜 모두 Haiku인가: Haiku로 내리며 겪은 실제 이슈
세 단계 모두 Claude Haiku 4.5(global.anthropic.claude-haiku-4-5-20251001-v1:0)를 씁니다. 이유는 비용과 지연, 양쪽입니다.
- Rerank/Diagnosis는 의미 비교에 가까운 작업이라, Sonnet과의 차이가 우리 도메인에서는 유의미하게 작았습니다(백테스트 30건에서 동일 판정 비율 95% 이상).
- Knowledge Base 검색의 1차 게이트라는 역할상, 빠른 응답이 더 중요합니다. Haiku는 같은 프롬프트 기준 Sonnet보다 평균 1.5~2배 빠릅니다.
다만 Haiku로 내리면서 부딪힌 실제 이슈가 둘 있었습니다.
이슈 1. JSON 파싱 실패
Haiku가 코드블록을 닫지 않거나 trailing comma를 흘리는 빈도가 Sonnet보다 높았습니다. 그래서 파서 단에 코드블록을 자동으로 떼어내는 로직을 더했습니다.
_CODE_BLOCK_RE = re.compile(r"```(?:json)?\s*\n?(.*?)\n?\s*```", re.DOTALL)
def _parse_llm_json(message: dict | str) -> dict | list:
text = _extract_message_text(message).strip()
match = _CODE_BLOCK_RE.search(text)
if match:
text = match.group(1).strip()
return json.loads(text)
이슈 2. Instruction following 약화
‘반드시 위 5개 후보 중에서만 고르고, 새로 만들지 마세요’ 같은 부정형 지시가 종종 무시됐습니다. Haiku가 ‘더 적절한’ 케이스를 임의로 지어내는 사고였죠. 그래서 프롬프트를 고쳐 두 가지를 강제했습니다.
- “출력은 다음 schema를 정확히 따르세요”라는 지시에 명시적 1-shot example을 항상 붙였습니다.
- 부정형 지시를 나열형 + “…의 경우 false 반환” 형태로 바꿨습니다.
이 두 패치 뒤로 JSON 파싱 실패율은 측정 기간 0건, instruction 위반 케이스는 합성 50건 셋 기준 1건 이하로 안정화됐습니다.
4.1.6 정리: Knowledge Agent 핵심 결정 다섯 가지
- 요약하지 말고 원문을 보존한다. 검색은 원문이, 해석은 Agent가 맡는다.
- 이미지를 텍스트의 일부로 멀티모달 caption화해 같은 문서에 통합한다(Knowledge Base 증분 업데이트 루프에서 자세히).
- Retrieve → Rerank → Diagnosis 3단계로 ‘비슷한데 다른 문제’를 LLM이 판정한다.
- match boolean + confidence 두 축으로 1차 컷과 우선순위를 분리한다.
- 모두 Haiku로 가되, JSON 파서를 보강하고 1-shot을 강제해 비용과 정확도를 함께 챙긴다.
4.2 Code Agent: 사내 레포를 실제로 읽게 만들기
Code Agent는 Knowledge Agent가 답하지 못하는 문의를 떠맡으려고 만든 에이전트입니다. 즉 ‘Knowledge Base에 기억이 없는’ 질문, 이번 주에 들어간 코드 변경이 원인일지 모르는 버그, 아직 사례가 쌓이지 않은 신규 기능에 대한 질문 같은 것들입니다.
이 에이전트가 하는 일은 한 줄로 요약됩니다. 사내 레포의 실제 코드를 읽어, 문의의 원인 가설을 세우고 답을 만든다. 단순해 보이지만, 정작 ‘코드를 실제로 읽게 만드는’ 부분에서 가장 많이 헤맸습니다.
4.2.1 GitHub REST API → git clone → S3 캐싱까지
1차 시도: GitHub REST API
처음에 잡은 건 가장 표준적인 접근이었습니다.
GET /search/code로 키워드 검색GET /repos/{owner}/{repo}/contents/{path}로 파일 조회GET /repos/{owner}/{repo}/pulls로 PR 조회
깔끔해 보였지만, 며칠 돌려보니 네 가지 벽에 차례로 부딪혔습니다.
- Code Search API 분당 10회 Rate Limit. ReAct 루프 안에서 Code Agent는 검색·파일 조회·이력 추적을 섞어 가며 1~2초 단위로 도구를 호출합니다. 검색 도구만으로도 분당 10회 한도는 단일 문의를 처리하는 도중에 닿을 수 있는 수치입니다.
- 384KB 이상 파일 검색 불가. 우리 백엔드에는 수백 KB짜리 legacy 로직 파일이 있는데, 정작 검색이 필요한 파일에서 검색이 안 됩니다.
- 기본 브랜치만 검색 가능. feature 브랜치에서 배포 전 검증 중인 문의는 분석 자체가 안 됩니다.
- 네이티브 Git 명령을 흉내 내기 어려움. ‘이 코드가 언제, 어떤 PR에서, 누구에 의해 바뀌었는가’를 추적하려면 git log·git blame·git diff 같은 조합형 명령이 필요한데, REST API로 한 번에 흉내 내려니 호출 수와 로직이 너무 비싸졌습니다.
그래서 REST API는 포기했습니다. 로컬 파일 시스템에서 grep, file_read, git 네이티브 명령을 그대로 쓰는 방향으로 틀었습니다.
2차 시도: 매 세션 git clone
자연스러운 다음 옵션은 세션마다 git clone --depth 1이었습니다. 이것도 금세 막혔습니다.
- 응답 시간. 큰 레포는 shallow clone도 10~30초가 걸립니다. 세션마다 반복되면 체감 응답 시간이 늘어집니다.
- PAT 런타임 노출. Personal Access Token을 microVM 런타임이 들고 있어야 합니다. AgentCore Runtime은 세션마다 microVM이 격리되지만, 토큰이 런타임 메모리에 올라가 있다는 사실 자체가 보안 관점에서는 부담입니다.
- Abuse detection 위험. 같은 IP 대역에서 단시간에 같은 레포를 반복 clone하면 GitHub의 abuse detection에 걸릴 수 있습니다.
여기서 멈추고 다시 생각했습니다. 중요한 건 ‘매번 가져오기’가 아니라, ‘코드의 최신 스냅샷을 빠르게 쓸 수 있는 곳에 미리 두기’였습니다.
3차: GitHub Actions(OIDC) → S3 tar 캐싱
정착한 구조는 이렇습니다.
[push 시점에 한 번만]
push → GitHub Actions (OIDC, 키리스 인증) → tar czf repo.tar.gz . → aws s3 cp s3://repo-cache/
[세션 시작마다]
ensure_repo() → aws s3 cp s3://repo-cache/repo.tar.gz . → tar xzf → /mnt/workspace/<repo>
핵심 결정은 둘이었습니다.
- GitHub Actions OIDC. Actions에서 임시 AWS 자격증명을 받아 Amazon S3에 업로드합니다. Actions에 AWS 액세스 키를 영구 저장하지 않고, microVM 런타임에는 GitHub 자격증명이 아예 없습니다(S3 read IAM만 가집니다).
- fetch-depth: 0. Actions가 checkout할 때 전체 git history를 가져와 tar에 담습니다. 덕분에 microVM 안에서 git log·git blame·git diff가 모두 됩니다.
결과는 이렇습니다. 세션당 준비는 수 초(S3는 같은 리전, AWS 백본 전송), 런타임에 GitHub 자격증명 없음, 모든 Git 네이티브 명령 사용 가능, Rate Limit·파일 크기·브랜치 제한 없음.
블로그 입장에서 이 흐름의 진짜 의미는 ‘REST API → clone → S3 tar’라는 순차적 실패담이 아닙니다. 매 단계에서 무엇을 포기하고 무엇을 얻을지 명확히 저울질한 결과라는 점입니다.
한 줄 교훈: 서버리스·세션 격리 환경에서 ‘매번 외부에서 받아오기’는 비싼 기본값이다. 언제 한 번 받아 어디에 둘지부터 정해야 한다.
4.2.2 분석 플로우와 도구 구성
Code Agent가 한 문의를 받았을 때 도구를 쓰는 일반적인 순서는 이렇습니다.
ensure_repo(repo)— S3 캐시에서 워크스페이스로 압축 해제 (이미 있으면 skip)- 심볼·파일 좁히기 — 우선 Serena의
get_symbols_overview/find_symbol로 LSP 기반 심볼 검색. 비코드 파일이거나 grep이 더 빠르면 shell grep으로 fallback - 본문 정밀 읽기 —
find_symbol(include_body=True)로 함수/클래스 단위 본문을 가져옴. 파싱 불가 파일은 file_read - (필요 시) shell로
git log --oneline -20 -- {파일}또는 git blame — 변경 이력 추적 - 결과를 종합해 답변
도구 스택은 이중 모드입니다.
| 모드 | 도구 | 강점 | 사용 케이스 |
|---|---|---|---|
| Primary — Serena MCP | find_symbol, get_symbols_overview, find_referencing_symbols, search_for_pattern 등 | LSP 기반 심볼 단위 검색 — 함수/클래스를 정확히 찾아 본문만 가져옴(file_read보다 토큰 효율적) | 소스 코드 (.ts, .tsx, .js, .vue 등) |
| Fallback — shell + file_read | Strands 공식 shell, file_read | grep, git, 비코드 파일 조회 | 설정 파일·로그·LSP 미지원 파일, Serena MCP 비가용 시 |
Serena가 쓸 수 있으면 자동으로 Primary 모드로 들어가고, 시작 시 SerenaUnavailableError가 나면 즉시 Fallback으로 전환합니다. 직접 구현한 건 ensure_repo 하나뿐이고, 나머지는 Strands 공식 도구와 Serena MCP 도구를 그대로 씁니다.
import os
import re
import subprocess
from strands import tool
WORKSPACE = "/mnt/workspace"
REPO_CACHE_BUCKET = "repo-cache"
# 경로 순회·쉘 인젝션 방지 — alphanumeric, hyphen, underscore만 허용
REPO_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")
@tool
def ensure_repo(repo: str) -> str:
"""S3에 캐싱된 레포 tar.gz를 다운로드·압축 해제한다.
이미 워크스페이스에 있으면 재사용한다."""
if not REPO_NAME_PATTERN.match(repo):
raise ValueError(f"invalid repo name: {repo}")
repo_dir = f"{WORKSPACE}/{repo}"
if os.path.isdir(repo_dir):
return repo_dir # 동일 세션에서 재호출되면 skip
tar_path = f"{WORKSPACE}/{repo}.tar.gz"
subprocess.run(
["aws", "s3", "cp",
f"s3://{REPO_CACHE_BUCKET}/{repo}.tar.gz",
tar_path],
check=True,
)
os.makedirs(repo_dir, exist_ok=True)
subprocess.run(["tar", "xzf", tar_path, "-C", repo_dir], check=True)
os.remove(tar_path)
return repo_dir
설계상 명시적으로 챙긴 건 둘입니다.
- 레포 이름 정규식 검증. Supervisor가 LLM 출력으로 repo 파라미터를 채우기 때문에, 경로 순회(
../)나 쉘 인젝션 가능성을 입구에서 막습니다. - 세션 캐시. 같은 세션 안에서 같은 레포로 ensure_repo가 여러 번 불려도, 두 번째부터는 디렉터리만 돌려주고 끝냅니다.
4.2.3 Hook 3중 방어벽: 같은 도구 반복 호출을 어떻게 잡았나 ⭐
도구와 캐시는 잡혔지만, 다른 문제가 남았습니다. Code Agent가 같은 grep, file_read를 반복 호출하는 토큰 폭주였죠.
전형적인 패턴은 이렇습니다.
1. grep -rln "OrderService" /mnt/workspace/<repo>/src
2. file_read OrderService.java
3. grep -rln "createOrder" /mnt/workspace/<repo>/src
4. grep -rln "OrderService" /mnt/workspace/<repo>/src ← 같은 명령 다시
5. file_read OrderService.java ← 같은 파일 다시
...
ReAct 루프 안에서 LLM이 ‘이전 턴에 뭘 봤는지’를 잊고 같은 탐색을 되풀이합니다. 토큰은 쌓이고 응답 시간은 길어집니다. 근본 원인을 세 갈래로 쪼개 봤습니다.
- 프롬프트 충돌. ‘단계별로 충분히 탐색하라’와 ‘충분히 봤으면 즉시 결론’이 같은 시스템 프롬프트에 같이 들어 있어, LLM에게 너무 넓은 재량이 주어졌습니다. 도구 호출 횟수가 동일 문의에 대해 9~15회 사이에서 비결정적으로 흔들렸습니다.
- 컨텍스트 누적. 루프가 깊어질수록 attention이 희석돼 ‘이미 본 것’에 대한 인식이 흐려집니다.
- sampling 비결정성. temperature를 명시하지 않아 루프 이탈 시점이 비결정적이었습니다.
해결은 방어벽 세 개를 겹치는 쪽으로 갔습니다. 어느 한 층만으로는 부족했기 때문입니다.
방어벽 1. 프롬프트: 판단 트리 + 체크포인트
시스템 프롬프트를 ‘규칙 나열식’에서 ‘페르소나 + 판단 트리’로 다시 설계했습니다. 단계를 진행하기 전마다 ‘지금까지 본 것으로 답할 수 있는가?’를 명시적으로 체크하도록 강제했죠. 이 변경 하나만으로도 단순 조회 시나리오에서 31회 / 265K 토큰 → 3회 / 14K 토큰으로 줄었고, 더 복잡한 미구현 기능 분석 시나리오에서도 30회 / 514K 토큰 → 15회 / 208K 토큰으로 줄었습니다.
한 줄 교훈: 규칙을 길게 줄수록 LLM은 더 길게 탐색한다. 페르소나 한 줄이 더 강한 통제를 낳는다.
방어벽 2. 모델: temperature 0.3
판단 시점의 비결정성을 줄이려고 temperature를 명시적으로 낮췄습니다. 같은 문의에 대해 도구 호출 횟수의 분포가 눈에 띄게 좁아졌습니다.
방어벽 3. Hook: 루프 상한 강제 종료
프롬프트와 sampling을 다 잡아도 루프는 가끔 폭주합니다. 마지막 안전망으로 AgentCore Hook을 걸어, 도구 호출 총 횟수가 20회를 넘어가면 그 자리에서 즉시 종료하고 Supervisor에 ‘Code Agent가 결론을 내지 못함’ 신호를 돌려보냅니다. 20회는 일반적인 처리 범위(9~15회)에 충분한 여유를 두면서, 그 이상은 거의 예외 없이 루프 폭주라는 운영 관찰을 근거로 정한 임계값입니다. 사용자에게 틀린 답을 길게 만들어 주느니, 빠르게 에스컬레이션 신호를 주는 게 낫다는 판단이었습니다.
방어벽 1 적용 전후를, 테스트 시나리오 3건으로 측정한 값입니다.
| 테스트 시나리오 | 적용 전 | 적용 후 |
|---|---|---|
| 단순 조회 (로그인 입력 항목) | 31회 / 265K 토큰 | 3회 / 14K 토큰 |
| 미구현 기능 분석 (배차 대기 탭) | 30회 / 514K 토큰 | 15회 / 208K 토큰 |
| 버그 시나리오 (차량회전률) | — | 11회 / 92K 토큰 |
방어벽 2·3은 평균값을 줄였다기보다 분포를 안정시킨 쪽에 가깝습니다. 방어벽 1을 적용한 뒤에도 같은 문의가 9~15회 사이에서 흔들렸는데, temperature 0.3으로 분포를 좁히고 20회 hard cap으로 폭주를 막았습니다. 전체 운영 평균(처리 건수·응답 시간 추이 등)은 6장 사람 vs AI 비교에서 다룹니다.
위 표는 fallback 모드(shell + file_read)에 방어벽 1만 적용한 시점의 값입니다. 이후 4.2.2에서 설명한 Serena MCP를 Primary로 들이고 Bedrock 프롬프트 캐싱을 켜면서 도구 호출 수가 한 번 더 줄어, 4.2.4에서 보일 같은 차량회전률 케이스의 3회 호출 / 24K 입력 토큰까지 안정화됐습니다. 즉 방어벽 1(프롬프트) → Serena(LSP 도구) → 캐시(토큰) → 방어벽 2·3(분포·상한)의 누적 효과입니다.
이 절의 한 줄 교훈은 이렇습니다. 프롬프트만으로도, 모델 파라미터만으로도, Hook만으로도 부족했다. 셋을 겹쳐야 비결정성이 잡혔다.
4.2.4 실제 분석 사례 1건: 차량회전률 100% 초과 버그
운영 환경과 동일한 구성(Serena 모드 + 프롬프트 캐싱 + 방어벽 1·2·3)으로, 검증용 강제 버그 시나리오 한 건을 Code Agent에 직접 던져 분석 흐름을 따라가 봤습니다.
문의: “홈 대시보드에서 차량회전률이 100%가 넘는 수치로 나옵니다. 비정상적인 것 같은데 확인 부탁드립니다.”
Code Agent의 분석 흐름
| # | 도구 | 호출 내용 | 결과 |
|---|---|---|---|
| 1 | shell (fallback) | grep -rlI “차량회전률” . | 후보 파일 2건 — domainComponents/…/ConnectionRequest.tsx, pages/home/CarStatusCard.js |
| 2 | get_symbols_overview (Serena) | 디렉토리 path를 인자로 호출 | 에러 — Expected a file path, but got a directory path |
| 3 | find_symbol (Serena) | name_path_pattern=’CarStatusCard’, relative_path=’pages/home/CarStatusCard.js’, include_body=True | CarStatusCard 클래스 본문 반환 → getRotatePercentage()에서 total / using 발견 |
흥미로운 점은 2단계에서 에러가 난 뒤의 자가 정정입니다. Serena 심볼 검색 도구는 디렉토리가 아닌 파일 경로를 요구하는데, Code Agent가 처음엔 디렉토리를 넣어 실패했습니다. 그 응답을 받자마자 1단계 grep 결과에서 얻은 정확한 파일 경로로 인자를 바꿔, 3단계에서 성공했죠. Hook 방어벽은 이 에러를 따로 처벌하지 않고 통과시켰고(정확히는 호출 카운터에는 들어가지만 hard cap에는 한참 못 미칩니다), 그 덕에 에이전트가 스스로 회복하는 모양이 됐습니다.
도출한 답변
파일: pages/home/CarStatusCard.js
함수: getRotatePercentage() (line 12~14)
문제 코드:
return `${((total / using) * 100).toFixed(2)}%`;
올바른 코드:
return `${((using / total) * 100).toFixed(2)}%`;
원인: 차량회전률 계산 시 분자(using_car)와 분모(total_car)가 뒤바뀌어,
운행 중 차량 < 전체 차량인 정상 상황에서 항상 100% 초과로 계산됨.
정답(getRotatePercentage 라인 12~14의 비율 반전)을 정확히 짚었습니다. 사람 담당자가 받았다면 ‘홈 대시보드 어딘가의 비율 계산 함수’부터 더듬어야 했을 문의입니다. 실제 운영에서 Code Agent까지 내려가는 비중은 크지 않지만(6장 기준 14건 중 1건, 7%), 한 번 내려가면 사람이 한 시간 넘게 더듬을 일을 수십 초에 끝낸다는 게 이 에이전트의 값어치입니다.
계측치
| 항목 | 값 | 비고 |
|---|---|---|
| tool 호출 수 | 3회 | shell 1 + Serena 2 (1 에러 포함) |
| 입력 토큰 | 23,877 | 신규 63 + 캐시 R 15,359 + 캐시 W 8,455 |
| 출력 토큰 | 831 | — |
| End-to-end 시간 | 약 18.5초 (cold) | S3 다운로드·Serena 초기화 포함, 분석 본체는 약 6초 |
| 정답 도달 | ✅ | 파일·함수·라인·수정 방법까지 일치 |
변동성 검증 (3회 반복)
같은 문의를 3회 반복 실행했더니, 3회 모두 동일한 도구 시퀀스(shell → get_symbols_overview → find_symbol)와 동일한 호출 수(3회)로 수렴했습니다. 초기 fallback 모드의 9~15회 변동과 비교하면, Serena Primary + 캐시 + 방어벽이 누적된 지금 시스템은 같은 문의에 대해 사실상 결정적으로 동작합니다. 4.2.3의 ‘분포 안정화’ 주장이 실측치로 확인된 셈입니다.
Cold vs Warm: 시간은 어디에 쓰이는가
/mnt/workspace/<repo> 캐시를 지운 cold 실행(18.5초)과 캐시가 살아 있는 warm 실행(18.4초)을 비교하면 차이가 약 0.1초에 불과합니다. 즉 4.2.1에서 들인 S3 tar 캐시는 다운로드 시간 자체가 거의 영향을 주지 않을 만큼 빠릅니다. End-to-end 18.5초의 대부분은 Serena MCP 초기화, 즉 LSP 부팅과 gitignore 로딩, TypeScript 프로젝트 인덱싱에 쓰입니다. AgentCore Runtime이 매 세션 새 microVM을 띄우는 구조라 이 초기화 비용은 세션당 고정으로 발생하므로, 응답 시간을 더 줄이려면 Serena 초기화 단계의 캐싱·재사용 전략이 다음 최적화 후보가 됩니다.
4.3 DB Agent: MCP 게이트웨이를 거치는 이유
Knowledge Agent와 Code Agent로 답이 나오지 않는 영역
운영개발문의 채널의 문의는 크게 셋으로 나뉩니다. 과거에 비슷한 사례가 있는 반복성 문의, 코드 동작을 확인해야 하는 문의, 그리고 실제 데이터 상태를 확인해야만 답할 수 있는 문의입니다. 앞의 둘은 Knowledge Agent와 Code Agent가 맡지만, 마지막은 어느 쪽으로도 답이 나오지 않습니다.
대표적인 패턴은 이렇습니다. “연번 12345번 보험계약, 앱에서 안 떠요.” “오늘 정산이 어제보다 30% 적게 들어왔어요.” “예약 상태는 분명히 완료인데 화면에선 진행 중으로 보입니다.”
코드를 분석하면 ‘어떤 조건일 때 안 보이는지’는 알 수 있습니다. 그러나 그 레코드가 실제로 그 조건에 해당하는지는 코드만으로 판정할 수 없습니다. 정산 수치가 줄었다는 문의도 마찬가지입니다. 로직 변경이 없었다면 답은 데이터에 있고, 그 답은 운영 DB를 직접 조회해야만 나옵니다.
이런 문의를 자동으로 처리하려고 Supervisor에 세 번째 서브 에이전트인 ask_db_agent를 더했습니다. 그리고 Code Agent와 양방향으로 교차 호출할 수 있게 묶었습니다.
ask_db_agent 응답에 needs_code_check=true → ask_code_agent 추가 호출
ask_code_agent 결과에 "데이터 확인 필요" → ask_db_agent 추가 호출
사내 표준 게이트웨이를 따르는 MCP 기반 설계
DB Agent를 처음 설계할 때 가장 먼저 부딪힌 결정은 ‘RDS에 어떻게 닿을 것인가’였습니다. 가장 쉬운 길은 에이전트 코드가 RDS에 직접 붙는 것이지만, 이 방식은 운영 시스템에서 받아들이기 어려웠습니다.
- 자격증명 격리가 무너집니다. RDS 비밀번호를 AgentCore Runtime이 직접 들고 있어야 하고, 누출되면 피해 범위가 큽니다.
- 권한·감사 정책이 분산됩니다. 사내에는 이미 다른 팀이 운영 중인 rds-mcp-server가 표준 게이트웨이로 자리 잡아 가고 있었습니다. 우회하면 SELECT 정책·쿼리 로그·접근 통제가 두 갈래로 쪼개집니다.
- 사고가 났을 때 원인 추적이 어렵습니다. 누가 어떤 쿼리를 언제 실행했는지를 한곳에서 보기 어려워집니다.
그래서 DB Agent는 운영 환경에서 반드시 rds-mcp-server를 거쳐 RDS에 접근하도록 설계했습니다. AgentCore Runtime은 RDS 자격증명을 갖지 않으며, MCP 서버의 SELECT 전용·LIMIT 강제 정책을 그대로 따릅니다. 통신은 Streamable HTTP MCP 프로토콜로 이뤄집니다.
참고로 로컬 개발 환경에서는 MCP 서버를 거치지 않고 pymysql로 RDS에 직결하는 보조 경로도 함께 운영합니다. AgentCore 배포 없이 빠르게 디버깅하기 위함이며,
RDS_MCP_URL환경변수 유무로 분기됩니다.
안전 장치
LLM이 운영 DB에 접근하는 것은 그 자체로 위험합니다. DB Agent는 여러 겹의 가드로 위험을 분산시킵니다.
- SELECT 전용 강제 (MCP 서버 레벨). rds-mcp-server는 SELECT 외의 모든 쿼리를 거부합니다. AgentCore Runtime이 어떤 쿼리를 보내든, LLM이 실수하든, 프롬프트 인젝션이 들어오든 서버 측에서 차단됩니다. 자격증명을 가진 쪽이 정책을 강제한다는 원칙입니다.
- LIMIT 자동 주입. 쿼리에 LIMIT이 없으면 강제로 LIMIT 100이 주입됩니다. 거대 테이블 풀스캔으로 운영 DB에 부하를 주는 시나리오를 막습니다.
- 시스템 프롬프트의 명시 원칙. 도구·서버 레벨 차단이 1차 방어선이고, 프롬프트가 2차 방어선입니다. “SELECT 쿼리만 사용한다(데이터 수정 절대 금지)”로 LLM이 애초에 위험한 쿼리를 시도하지 않게 유도합니다.
- 도구 호출 5회 상한. 탐색이 길어지면 강제 종료합니다. 쿼리 폭주를 시간으로 막는, 단순하지만 효과적인 장치입니다.
- 카탈로그 사전 주입. LLM이 네이밍을 유추하면 잘못된 쿼리와 루프를 부릅니다. 이를 막으려고 테이블 리스트를 시스템 프롬프트에 미리 주입합니다. 안전이자 비용 최적화입니다.
- 자격증명의 단방향 격리. 운영 환경에서 RDS 자격증명은 rds-mcp-server만 보유합니다. AgentCore Runtime은 비밀번호를 모르며, MCP 서버를 통해야만 RDS에 닿습니다. 자격증명 누출의 폭이 좁습니다.
- 실행 SQL 추적 로깅. 에이전트가 실제로 실행한 모든 SQL은 응답 JSON의
executed_sqls필드에 담겨 돌아옵니다. MCP 서버 측에도 별도 쿼리 로그가 남아, 클라이언트·서버 양쪽에서 같은 사실을 검증할 수 있습니다.
5. 세 에이전트의 공통 기반
5.1 Prompt Cache: ReAct 루프의 토큰 통제
왜 캐시가 필요했는가
ReAct 루프는 사이클이 늘수록 system prompt와 tool definition이 매번 다시 전송되는 구조입니다. 사이클 5회면 같은 system prompt가 다섯 번 쌓여, 입력 토큰의 70~80%가 ‘어제와 똑같은’ 내용이라는 게 우리가 측정한 결과였습니다.
특히 Supervisor와 Code Agent의 system prompt는 깁니다. Supervisor는 ① 페르소나 ② 도구 사용 가이드 ③ CASE 1/2-A/2-B 응답 템플릿 ④ 16개 레포 가이드 ⑤ DB → Code 교차 호출 시나리오까지 담아 평균 4,000~6,000 토큰, Code Agent는 Serena 도구 계층과 코드 분석 원칙을 합쳐 약 2,500 토큰입니다. 이게 매 ReAct 사이클마다 다시 전송됩니다.
캐시 포인트 설계
Bedrock의 prompt caching을 들이면서 캐시 경계를 이렇게 잡았습니다.
[캐시 영역 — 매 호출 동일, prompt cache로 재사용]
① 시스템 프롬프트 (페르소나, 도구 사용 규칙, 응답 형식)
② 도구 정의 블록 (Strands @tool schema)
③ 레포 가이드 (project-keywords.json → 마크다운 고정 컨텍스트)
── 캐시 경계 ──
[비캐시 영역 — 매 호출 가변]
· 사용자 문의 본문
· 도구 호출 결과 (이전 ReAct 턴)
순서가 핵심입니다. 고정 컨텍스트를 먼저, 가변 컨텍스트를 뒤로 밀어야 캐시 prefix가 매번 일치합니다.
효과: 차량회전률 케이스의 실측치
| 토큰 구성 | 수치 | 비율 |
|---|---|---|
| 신규 입력 토큰 | 63 | 0.3% |
| 캐시 Read (이전 호출 prefix 재사용) | 15,359 | 64% |
| 캐시 Write (이번 호출에서 새로 캐싱) | 8,455 | 36% |
| 입력 토큰 총합 | 23,877 | 100% |
같은 분석을 캐시 없이 했다면 약 38K 토큰을 매번 정가 그대로 청구받았을 것입니다. Bedrock의 캐시 Read는 input 정가의 약 1/10 단가로 청구되므로, 단일 호출 기준 입력 비용이 50% 이상 절감됩니다. 운영 채널의 하루 처리 건수를 생각하면 누적 효과는 훨씬 큽니다.
get_token_report() 함수에서 캐시 사용/미사용 가정 비용을 항상 함께 출력하게 만들어, 운영 중에도 ‘캐시가 실제로 일하고 있는지’를 매 호출마다 확인할 수 있게 했습니다(앞서 못박은 기준 4의 구체 구현체입니다).
5.2 피드백 기반 Knowledge Base 증분 업데이트 루프
답변 완료 → Knowledge Base 자동 반영
Slack 답변 블록 하단의
/
버튼이 Knowledge Base 반영의 입구입니다. 모델 자체가 학습되는 건 아니고, 데이터 소스가 시간에 따라 누적되는 RAG 루프입니다. 벡터 인덱스는 Amazon OpenSearch Serverless에 저장됩니다.

Lambda 전처리에서 하는 일
AWS Lambda(kb-data-preprocessor)는 네 가지를 합니다.
| 단계 | 동작 |
|---|---|
| 1. 원문 보존 | 부모 메시지 + 답글 + permalink를 그대로 이어 붙임. 요약·재구성하지 않음 — 요약본은 벡터 검색을 망치고 환각을 Knowledge Base에 박제하기 때문 |
| 2. 노이즈 제거 | Slack 멘션 토큰(<@U…>, <!subteam^…>) 정규식 제거, 연속 공백·줄바꿈 정리 |
| 3. 이미지 멀티모달 캡션 | 첨부 이미지 최대 3장(≤5MB) → Bedrock Claude Haiku로 “에러 메시지, UI 상태, 문제 상황”을 1~2문장 캡션화 → 원문 뒤 [첨부 이미지] … 블록으로 추가 |
| 4. 자동 Knowledge Base Sync | start_ingestion_job 직접 호출. 스케줄러 없이 데이터가 들어오는 즉시 동기화 |
이미지 멀티모달 캡션이 들어가면서, 텍스트만으로는 잡히지 않던 ‘결제 안 돼요’ 류 모호 문의의 검색 정확도가 눈에 띄게 올라갔습니다. 이미지가 별도 인덱스가 아니라, 같은 Knowledge Base 문서 안의 한 섹션으로 들어가기 때문입니다.
Knowledge Base에 넣는 기준 / 거르는 기준
| 넣는다 | 거른다 |
|---|---|
| 부모 메시지 + 답글 텍스트가 모두 존재 (valid_threads 필터 통과) | 텍스트가 비어 있는 스레드 (Lambda가 자동 스킵) |
| Supervisor가 운영 문의로 정상 처리한 케이스 | Supervisor가 처음부터 ‘운영 문의가 아님’으로 무시한 케이스 (애초에 답변 없음 → |
| 같은 thread_ts의 재처리는 idempotent 덮어쓰기로 최신 상태 갱신 | (해당 없음 — 중복 인덱싱 위험 없음) |
idempotent가 만들어내는 운영 편의
thread_ts를 파일명으로 쓰는 idempotent 설계 덕분에:
- 같은 스레드가 여러 번
를 받아도 파일명이 thread-{ts}로 고정이라 그냥 덮어쓰기입니다. - 답변 뒤 추가 댓글이 달려도
upload_resolved_thread를 한 번 더 호출하면 최신 상태로 Knowledge Base가 갱신됩니다. - 잘못 들어간 케이스는 raw S3 객체만 지우고 재처리를 트리거하면 Knowledge Base에서 깔끔히 빠집니다.
운영 단순성은 idempotent 한 줄에서 옵니다.
Knowledge Base Sync 운영 이슈
- 반영 지연: Ingestion job 시작부터 인덱스 반영까지 평균 30초~2분. 비동기라 진행 중에도 기존 인덱스로 정상 검색이 되므로 답변 누락은 없습니다.
- 벡터 임베딩 비용: 매 ingestion마다 Bedrock embedding 호출이 발생합니다. 같은 스레드를 재처리하면 비용이 누적되므로, thread_ts 파일명 기반 idempotent 덮어쓰기(같은 스레드는 늘 같은 키로 저장)가 비용 통제에 직접 기여합니다.
- 실패 처리: ingestion job이 실패하면 Lambda는 별도 retry 없이 종료합니다. 대신 운영 채널에 알림이 가도록 Amazon CloudWatch Logs Insights에 ERROR 키워드 알람을 걸어 두었습니다.
6. 사람 vs AI 병렬 비교 데이터
같은 운영 문의에 사람과 AI가 각각 어떻게 답하는지 1:1로 맞대어 본 결과를 공유합니다. 사내 운영팀의 실제 문의 옆에 AI 답변을 그대로 붙여 비교한 데이터입니다.
6.1 검증 방식
운영 채널은 평소대로 사람이 응대하고, 같은 문의를 가상의 운영문의 채널에 다시 올려 AI 에이전트가 응대하도록 했습니다. 가상 채널은 에이전트 개발자 외에는 접근하지 않으므로, 실제 사용자 응대에는 영향을 주지 않습니다.
| 항목 | 내용 |
|---|---|
| 사람 응답 출처 | #운영개발문의 — 실제 운영 채널, 평소대로 운영팀이 응대 |
| AI 응답 출처 | 가상 운영문의 채널 — 동일 문의를 다시 올려 AI 에이전트가 응대 |
| 비교 단위 | 두 채널에서 제목·식별자(IMS연번·접수번호 등)가 모두 일치하는 문의만 채택 |
| 노출 제한 | 식별자·사용자 ID 등 PII는 마스킹 처리 |
| 평가 | 공개된 4축 루브릭(원인 일치도·데이터 정확성·권고 실용성·누락)으로 14건의 사람/AI 답변 쌍을 5점 척도로 재채점. 단일 평가자 기준이라 ±0.3 수준의 주관 편차를 전제 |
6.2 기간과 표본
- 기간: 2026-04-01 ~ 2026-05-20 (약 7주)
- 제목 매칭 후보: 21건
- 식별자 검증 통과 (유효 비교): 14건
- 제외 7건: 식별자가 없어 1:1 매칭이 불가능한 4건(일반 공지 1건 + 식별자 미기재 운영 문의 3건), 그리고 제목은 같지만 실제로는 다른 건을 가리키는 인스턴스 3건
식별자가 적히지 않은 문의(로그인 불가·실시간 현황 등)도 정상적인 운영 문의입니다. 다만 사람과 AI 두 채널의 답변이 같은 사건을 가리키는지 확증할 수 없어, 정확도 비교에서만 제외했습니다.
6.3 정량 비교
정확도 평가는 사람의 최종 답변을 정답으로 가정하고, AI 답변이 네 가지 축(원인 일치도·데이터 정확성·권고 실용성·누락)에서 얼마나 부합하는지 5점 척도로 매긴 값입니다.
| 지표 | 사람 대응 | AI 에이전트 대응 |
|---|---|---|
| 1차 응답까지 (중앙값) | 10.0분 (평균 19.2분) | 2초 (평균 11초) |
| 원인·해결 제시까지 (중앙값)¹ | 42분 | 31초 (평균 39초) |
| 답변 정확도 (사람 답변을 정답으로 가정)² | 정답 기준 | 3.55 / 5 (71%) |
¹ 사람은 ‘원인 파악’이 끝난 시점이 로그에 따로 남지 않아, 스레드 종결(resolution) 시각으로 측정했습니다. 평균은 일·주 단위로 늘어진 소수 스레드 때문에 23시간까지 튀므로 중앙값을 대표값으로 봅니다. AI는 최종 분석 답변이 게시된 시각 기준입니다. ² 본문 6.1에 공개한 4축 루브릭으로 14건을 재채점한 값입니다(축별 평균: 데이터 정확성 3.93 · 권고 실용성 3.71 · 원인 일치도 3.36 · 누락 3.21). 단일 평가자 채점이라 ±0.3 수준의 주관 편차가 있습니다.
이 14건은 인위적으로 추린 표본이 아니라, AI를 병렬로 가동한 시점부터 #운영개발문의 채널에 실제로 들어온 문의를 그대로 모은 데이터입니다. 1.2장의 채널 통계와 같은 출처이고, 사람 수치는 그중 제목·식별자가 검증된 14건 기준입니다. 그래서 1.2장의 평균 2.6시간·중앙값 16.7분(32개월·846건 전체)과 여기 수치는 모집단이 다릅니다. 같은 채널을 보지만 기간과 표본 크기가 다른 별도 측정이므로, 두 값을 직접 빼서 비교하지는 말아 주세요.
5점 척도에서 최저 등급(완전히 헛짚은 답변)은 0건이었습니다(케이스 평균 최저 2.50/5). 다만 데이터는 정확히 짚었어도 도메인 룰을 위배한 권고를 한 경우가 1건 있었습니다(6.6, 권고 축 1점). ‘AI가 모든 답을 정답으로 줬다’가 아니라, ‘잘못된 방향이라도 빠르게 검토할 수 있는 형태로 줬다’가 더 정확한 표현입니다.
6.4 에이전트별 기여도
AI 응답 14건이 어느 단계에서 결론을 냈는지 분류했습니다.
| 분류 | 비율 | 설명 |
|---|---|---|
| Knowledge 단독 해결 | 36% (5/14) | KB의 과거 유사 사례 매칭만으로 완결 (DB·코드 조회 없음) |
| DB 조회까지 | 50% (7/14) | KB 매칭 부족 → DB 직접 조회로 레코드 상태를 확인하고 결론 |
| Code 분석까지 | 7% (1/14) | 레포 정적 분석으로 코드 결함(파일·라인·수정안)을 특정 |
| 추가 정보 요청 (불완전) | 7% (1/14) | 유력 원인 후보는 제시했으나 확정을 위해 추가 정보 요청 |
단계 귀속 기준: Knowledge 단독 해결 = 답변에 DB 값·코드 분석이 전혀 없음 / DB 조회까지 = DB 컬럼·상태값을 본문에 인용 / Code 분석까지 = 실제 파일·라인 수준의 결함 제시. “코드 레벨을 확인하겠다”고 서술만 하고 실제로는 DB 데이터로 결론낸 건은 DB로 분류했습니다. 정밀한 tool-call 귀속은 에이전트 실행 로그가 있어야 확정됩니다.
덧붙이면, AI 응답 14건은 모두 KB를 조회했고 그중 13건(93%)이 실제 과거 사례를 인용했으며, 7건(50%)이 DB 실제 값을 답변 본문에 명시했습니다. 사람 응답에서 같은 형태의 인용이 드러난 비율은 0%였지만, 이는 정보를 안 봤다는 뜻이 아닙니다. 사람도 관리자 페이지나 쿼리로 DB를 똑같이 조회합니다. 단지 그 과정을 답변에 옮겨 적지 않을 뿐이죠. 차이는 정보 접근이 아니라, 근거를 드러내는 방식과 사후 감사 가능성에 있습니다.
결과적으로 두 답변은 얼마나 같았나
6.3의 정확도 점수는 4축 평균이라 ‘전반적으로 얼마나 잘했는지’를 보여줍니다. 그와 별개로, 최종 결론 자체가 일치했는지를 binary로 분류해 봤습니다. 표현 차이는 무시하고 결론만 비교한 결과입니다.
| 결과 패턴 | 비율 | 의미 |
|---|---|---|
| 같은 결론에 도달 (AI가 빠름) | 64% (9/14) | 원인·해결 방향이 일치. 평균 도달 시간만 차이. → 6.5의 사례가 여기 속합니다. |
| 사람이 더 깊은 진단 | 29% (4/14) | 비즈니스 룰·UX·시스템 변천사 등 KB에 없는 맥락이 결정적. → 6.6의 사례가 여기 속합니다. |
| 진단은 유사하나 사람이 추가 검증 지적 | 7% (1/14) | ‘변경 가능?’ 같은 결정 요청에 AI는 즉답, 사람은 영향 범위부터 확인. |
결론 일치를 기준으로 보면, AI 답변이 사람과 같은 결론에 도달한 비율은 64%(9/14)입니다. 나머지 36%에서는 사람의 답변이 더 풍부했거나, AI 답변에 위험한 권고가 섞여 있었습니다.
6.5 AI가 이긴 케이스: “교체계약서가 안 보여요”
사용자 문의(요지): 자체건 계약서를 모바일에서 작성하려는데 대기 차량 목록이 비어 있음.
사람 응답은 19개 메시지가 오간 끝에, 16번째 메시지에서 원인이 짚였습니다.
“자체건일 때는 ‘A업체 인증 사업자 차량’으로 필터링하지 않아야 하는데, 앱과 웹 모두 자체건에서도 그 필터를 켜고 차량 목록을 요청하고 있었습니다.”
AI 응답은 접수 신호 3초, 원인·임시조치 제시까지 65초. 같은 결론이었습니다.
“해당 계약서가 A업체 인증 청구 사업자로 작성된 경우, 모바일에서는 A업체 인증 사업자 소속 차량만 목록에 표시되는 필터링 로직이 자체건에도 적용되어 정상 차량이 노출되지 않습니다. 과거 유사 사례 KB#A1xxx와 동일 패턴입니다.”
같은 결론에 도달하기까지 4일과 65초. AI 답변은 여기에 임시 우회 절차(청구 사업자를 임시 변경 후 작성)와 근본 수정 방향(필터링 로직 재검토)까지 덧붙였습니다. 이 건은 14건 재채점에서 최고점(4.75/5)을 받았습니다.
6.6 AI가 못 푼 케이스: “B업체 접수번호 수정 부탁드려요”
사용자 문의(요지): IMS연번 29***86, B업체 렌카콜 건의 접수번호를 26*******로 수정 요청.
AI 응답은 DB Agent로 receipt_string = NULL을 확인한 뒤, UPDATE 쿼리 형태까지 제시했습니다.
사람 응답은 다른 차원의 답이었습니다.
“B업체 전산콜은 접수번호 변경이 불가합니다. B업체와 전산 연동되어 있어 요청의 접수번호로만 주고받을 수 있게 되어 있어, 별도 신규 접수로 처리하셔야 합니다.”
기술적으로는 UPDATE가 가능하지만, 외부 시스템(B업체 전산)과의 정합성 때문에 해서는 안 되는 작업입니다. 이런 비즈니스 룰은 KB에 적혀 있지 않으면 AI가 알 길이 없습니다. 이 건은 14건 재채점에서 최저점(2.50/5)을 받았습니다. 데이터 정확성은 만점(receipt_string = NULL을 정확히 확인)이었지만, 권고 실용성이 1점이었기 때문입니다.
이번 분석에서 AI가 무너진 영역은 비슷한 결을 가집니다.
| 약점 | 본질 |
|---|---|
| 비즈니스 룰 무지 | KB에 없는 도메인 제약을 모름 |
| 코드 흐름 추적 한계 | 데이터 이상은 보지만, 그 데이터가 만들어진 호출 경로를 역추적하기 어려움 |
| UX 미묘 버그 미발견 | 토글 전환 같은 사용 경험 기반 버그는 KB·DB로 잡히지 않음 |
| 시스템 변천사 부재 | ‘예전엔 없던 기능이 지금은 가능해졌다’ 같은 타임라인 맥락이 없음 |
6.7 사람 vs AI 병렬 비교에서 확인한 것
14건을 나란히 놓으니 강점과 한계가 또렷하게 갈렸습니다.
AI가 이긴 쪽은 속도와 근거였습니다. 9건에서 사람과 같은 결론에 분이 아니라 초 단위로 도달했고, KB 사례와 DB 값을 본문에 남겨 판단 과정을 되짚을 수 있게 했습니다.
AI가 진 쪽은 한 곳을 가리켰습니다. KB·DB·코드 어디에도 적혀 있지 않은 것(비즈니스 룰, 보이지 않는 맥락, 레거시 정책) 앞에서는 어김없이 멈췄습니다.
이 비교를 통해, 비즈니스 룰을 독립 항목으로 정리해 KB에 등록하고 진단에 활용할 수 있도록 개선해야 한다는 점을 확인했습니다.
7. 마치며: DevCraft 경험과 함께
같은 문제를 겪는 조직에 한 문장으로 전하고 싶다면 이렇습니다. ‘검색이 풀지 못하는 건 결국 같은 문제인지를 판단하는 일이고, 그 판단을 LLM에게 명시적으로 맡기는 것이 시작이다.’
본문에서 확인한 한계는 그대로 다음 계획이 됩니다.
- 비즈니스 룰의 KB화. 6.6에서 본 것처럼, 코드·DB 어디에도 적혀 있지 않은 도메인 제약(예: 외부 전산 연동 건은 접수번호 변경 불가)이 AI의 최대 약점이었습니다. 비즈니스 룰을 독립 문서로 정리해 Knowledge Base에 등록하고, Diagnosis 단계에서 우선 참조하도록 개선할 계획입니다.
- Serena 초기화 캐싱. Code Agent end-to-end 18.5초의 대부분이 세션마다 반복되는 LSP 부팅·프로젝트 인덱싱 비용이었습니다. 초기화 산출물의 캐싱·재사용으로 응답 시간을 더 줄이는 것이 다음 최적화 과제입니다.
- 적용 채널 확대. 현재 폼(IMSform)·렌카 두 서비스 채널에 우선 도입된 시스템을, 운영 데이터가 쌓이는 대로 나머지 서비스 채널로 넓혀갈 예정입니다.
그리고 AWS DevCraft 2.0은 이 시스템을 ‘운영하면서 자라는’ 구조로 만들게 해 준 결정적인 기회였습니다. 단발성 데모로 끝내지 않고, Knowledge Base 증분 축적 루프와 per-agent 토큰 메트릭까지 한 사이클로 묶는 일을 2주라는 짧은 기간 안에 끝낼 수 있었습니다.
“문의가 들어오는 채널 자체를 다시 디자인할 수 있다는 걸, 직접 만들어보고서야 알았습니다.” — A1Mobilsoft 개발팀
본 글은 고객이 AWS·파트너사와 함께 자신만의 AI Agent를 개발하도록 지원하는 프로그램인 AWS DevCraft 2.0에서, A1Mobilsoft와 MegazoneCloud가 함께 진행한 작업의 기록입니다. Amazon Bedrock AgentCore Runtime, Strands Agents SDK, Amazon Bedrock Knowledge Base, Amazon CloudWatch, AWS Lambda, Amazon S3를 사용했습니다.
참고 자료
- Amazon Bedrock AgentCore 개발자 가이드
- Amazon Bedrock Knowledge Bases 문서
- Amazon Bedrock Prompt Caching
- Strands Agents SDK
- Amazon OpenSearch Serverless
- 1편: A1Mobilsoft의 수 시간 걸리던 운영 문의를 몇 분으로 ① — 문제 정의와 3-에이전트 아키텍처 설계 (게시 URL 삽입)
비용 안내
이 시스템은 Amazon Bedrock(Claude 모델 추론·Knowledge Base 임베딩), Amazon OpenSearch Serverless, Amazon Bedrock AgentCore Runtime, AWS Lambda, Amazon S3 등 유료 서비스를 사용합니다. 실제 비용은 처리하는 문의 건수와 토큰 사용량에 따라 달라지며, 프롬프트 캐싱으로 입력 토큰 비용을 크게 절감할 수 있습니다. 자세한 내용은 각 서비스의 요금 페이지를 참고하세요.
면책 조항: 본문의 문의 예시, 코드, 식별자(IMS연번·예약 ID·접수번호 등)와 데이터는 외부에 공개 가능한 수준으로 비식별·재구성한 것으로, 실제 고객 데이터 및 운영 환경과 무관합니다.