Slack, GitHub, Jira, Google Docs 등 서비스별로 각기 다른 대화가 진행됩니다. 어디서 무엇을 결정했는지 자신도 모르게 잊어버리는 경우가 자주 있었습니다. AI에게 코드를 작성하게 되면서는, 이제는 AI에게 전달하는 정보에서 중요한 경위가 누락되는 일이 생겼습니다. 잘 안 된다고 느끼는 순간이 여러 번 있었습니다.
그래서 팀의 결정에 대해 '왜 그렇게 결정되었는지', '지금도 유효한지'를 물으면, 질문자가 볼 수 있는 기록만을 근거로 답변하는 에이전트인 'AlterEcho'를 만들었습니다. Cloud Run 위에서 구동하며, Vertex AI의 Gemini를 사용하여 결정 추출, 대체 여부 판정, 답변 생성을 수행합니다. 본 작품은 제5회 Agentic AI Hackathon with Google Cloud에 출품한 결과물입니다.
어떤 문제에 직면했는가
AlterEcho 자체도 코드의 상당 부분을 AI 에이전트에게 작성하게 하여 만들었습니다. PR(Pull Request)은 계속 늘어나지만, 그 PR에 '왜 그렇게 했는지'가 적혀있을 거라고 보장할 수 없습니다. 경위는 Slack 스레드나 Jira 댓글 등에 흩어져 있습니다.
더 큰 문제는 결정이 나중에 뒤집어진다는 것입니다. 겨우 찾아낸 결정이 이미 유효하지 않을 수도 있습니다. 과거 발언을 모아서 돌려주는 검색만으로는, 폐기된 결정도 현재의 결정과 똑같은 모습으로 나타납니다.
처음에는 회의록을 자동으로 요약하는 AI를 생각했습니다. 하지만 요약만으로는 결정 자체가 나중에 덮어쓰여졌다는 사실 자체를 추적할 수 없습니다. 그래서 이 아이디어는 버리고, 결정과 그것을 대체한 결정 간의 연결고리를 갖도록 했습니다.
데모에서는 인증 방식에 대한 이야기로 이 상황을 재현했습니다. 지난달 JWT(JSON Web Token)로 하기로 결정했다가, 지난주 세션으로 되돌렸습니다. 되돌린 이유는 AI 에이전트가 작성한 PR에는 없고, #security 채널의 지적에만 남아 있습니다. 그러자 설계 리뷰에서 '지금 인증은 세션 방식이죠? 지난달 JWT로 했다고 들었는데'라는 질문을 받는 상황입니다.
AlterEcho로 할 수 있는 것들
Slack, GitHub, Google Drive/Docs, Jira 등지에서 가져온 이벤트를 사람・사건・산출물・주제・결정의 그래프로 만들어 BigQuery에 기록합니다. 질문이 들어오면, 이 그래프에서 근거를 찾아 답변합니다.
Slack에서 @AlterEcho 인증 방식은 왜 지금 형태가 되었나요?라고 멘션하면, 질문한 본인에게만 보이는 답변(ephemeral)으로 응답합니다. 웹의 문의 화면 /ask에서도 들을 수 있습니다. 답변에는 질문자가 볼 수 있는 Slack 발언이나 PR 링크를 첨부했습니다. 팀 기록에서 확인할 수 없는 것은 일반론이라고 밝히고, 추측으로 단정하지 않도록 합니다.
결정을 다루는 방식에서는 두 가지를 정했습니다. 첫째, 대화에서 추출한 결정은 처음에는 QUARANTINED(보류) 상태로 두고, 해당 PR이 병합(merge)된 후에야 ACTIVE 상태로 만듭니다. 말만 하고 구현되지 않은 안건을 결정으로 답변하고 싶지 않았기 때문입니다. 둘째, 새로운 결정이 오래된 결정을 대체했다고 판단하면, 오래된 것을 에이전트가 스스로 ARCHIVED(보관) 처리합니다. 기록은 지우지 않고 계보로 남기며, 화면에서는 취소선으로 표시합니다.
데모 영상
폐기된 결정을 스스로 하위 버전으로 낮추기
AlterEcho에서 가장 보여주고 싶었던 부분입니다. 질문을 기다리지 않고, 과거의 결정이 아직 유효한지 에이전트가 재판단합니다. 설계서에는 '과거 자신의 출력을 보존 대상으로 갖는다'고 적었습니다.
구조는 다음과 같습니다:
- Slack 등의 대화에서 Gemini가 결정 후보를 짧은 구조화된 JSON으로 추출합니다. 이 시점에서는
QUARANTINED 상태입니다. - 결정에 대응하는 PR이 병합되면, GitHub Webhook을 받아 ACTIVE로 올립니다. - 올라온 결정과 같은 주제(Entity)에 있는 가장 최근의 ACTIVE한 결정 최대 5개를 골라, 새로운 결정이 그것을 대체할지 Gemini에게 판정하게 합니다. - 대체한다고 판정되면, 새로운 결정에서 오래된 결정으로 supersedes(대체함) 관계를 추가합니다. 오래된 결정은 ARCHIVED됩니다.
이것은 BigQuery의 VIEW가 추가된 이력으로부터 그렇게 유도하기 때문입니다. 사람의 승인은 거치지 않습니다. - Cloud Scheduler가 매일 1회 재판단(re-judgment) 작업을 시작합니다. 판단에 사용하는 것은 추출된 결정 노드와 Entity 간의 연결 정보만이며, 원본 대화 로그를 Gemini에게 다시 보내지는 않습니다.
사람의 승인이 없기 때문에 틀렸을 때 되돌릴 수 없다는 것이 문제입니다. 실제로 평가에서는 오하향(誤降格)이 4건 나왔습니다 (나중에 자세히 쓰겠습니다).
그래서, nodes / edges 테이블에는 UPDATE도 DELETE도 하지 않기로 했습니다. classification, supersedes, revokes, tombstone을 추가하고, 현재 상태는 VIEW로 유도합니다. 하향 조정 취소(undo_supersede) 역시 대상의 supersedes 엣지를 무효화하는 revokes 엣지 한 줄을 추가하는 것만으로 충분합니다. 다음 쿼리부터 이전 결정은 ACTIVE 상태로 돌아오고, 최초 판단도 이력에 남습니다.
승격과 하향 조정은 작업 로그 화면(/work-logs)에 시간/입력/판단/근거 링크와 함께 남깁니다. 에이전트가 조용히 하향 조정하도록 두지 않고, 관계자가 나중에 자신의 화면에서 확인할 수 있도록 하고 싶었기 때문입니다. 그런데 기록 준비를 하던 2026-10-10에, 실제 환경에서는 승격과 하향 조정이 이 작업 로그에 기록되어 있지 않다는 것을 발견했습니다. 그날 안에 수정했습니다.
데모에서는 'JWT로 변경하기'(결정 A)가 '세션으로 복원하기'(결정 B)에 의해 무효화됩니다. B를 구현한 PR을 병합하면, B가 ACTIVE 상태가 되고, A는 ARCHIVED 상태로 떨어집니다. dev 환경에서는 2026-10-10의 병합으로 B가 ACTIVE 상태가 되었습니다. 기록 전에 (2026-10-09와 10-10) 승격과 하향 조정을 추가하여 무효화하는 리셋 스크립트로 상태를 되돌리고, 다시 병합하여 재승격하는 것도 확인했습니다.
볼 수 있는 기록만으로 탐색하게 하기
그래프로 답변하는 시스템에는 특유의 누수 현상이 있습니다. 본문을 가리더라도 'A와 C가 연결되어 있다'거나 'N 건을 비공개 처리했다'는 건수를 통해 보이지 않아야 할 정보가 파악됩니다.
그래서 가져온 결과를 앱에서 나중에 필터링하는 것은 그만두고, 탐색을 시작하기 전에 보이는 범위를 좁히기로 했습니다.
열람자별로 전용 서비스 계정을 준비하고, worker와 web은 서버 측에서 이를 빌려 BigQuery를 읽습니다. BigQuery의 authorized view는 SESSION_USER()로부터 열람자를 식별하여, 그 사람이 볼 수 있는 행만 반환합니다. 엣지는 양 끝 노드가 모두 보일 때만 반환됩니다. 답변 에이전트의 도구에는 열람자를 지정하는 인수가 없습니다. 열람자는 코드 측에서 구속하고 있으므로, 모델이 열람자를 위장할 수는 없습니다.
실제로 효과가 있는지 여부는 2026-09-19에 열람자의 서비스 계정을 실제로 빌려 BigQuery가 반환하는 SESSION_USER()로 확인했습니다.
사토(#dev만) | 타나카(#dev와 #security ) |
|---|---|---|
v_nodes 행 수 | 236 | 286 |
v_edges 행 수 | 86 | 103 |
원본 테이블 nodes 직접 참조 | Forbidden | Forbidden |
ask_why (질문 '인증 방식')로 얻은 본문 | 0건 | 1건 |
앱을 거치지 않고 BigQuery에 직접 쿼리를 날려도, 사토와 타나카에게 반환되는 행 수는 다르며, 원본 테이블은 어느 쪽에서도 읽을 수 없었습니다.
같은 질문에 대한 답변에서도 이 차이가 나타납니다. 2026-10-11 시점의 dev 환경에서의 답변입니다 (모델 출력이라 문구는 매번 조금씩 달라집니다).
사토(#dev만):
JWT 채택 결정은 현재 유효하지 않습니다. 이후 세션 기반 인증으로 복원하는 변경이 있었습니다. 이유는 팀 기록에서는 확인할 수 없습니다.
타나카(#dev와 #security ):
업무 위탁의 교체나 단말기 분실 시에 JWT로는 즉시 효력을 상실할 수 없기 때문에 세션 기반으로 복원했습니다.
단 하나의 예외만 남겨두었습니다. 결정 목록과 계보 화면은 Firestore의 투영(projection)을 읽고, 앱이 조회 범위에 따라 필터링합니다. 투영은 승격/강등 직후와 30분마다 다시 생성됩니다. 직후 재구축에 실패할 경우, 다음 정기 실행까지 최대 30분 동안 화면 상태가 이전 그대로 남아 있습니다. 답변(ask_why)은 투영이 아닌 authorized view를 읽으므로, 만료된 근거가 답변에 섞일 염려가 없습니다.
아키텍처
| 역할 | 사용 기술 |
|---|
| 실행 기반 | Cloud Run의 3개 서비스 (receiver: 서명 검증 및 수신, worker: 동기/추출/대화, web: 조회 UI 및 /ask ) |
| AI | Vertex AI의 Gemini (gemini-3.8-flash, 실패 시 gemini-3.7-flash)로 추출/대체 판정/재판단/답변. 임베딩은 gemini-embedding-001 |
| 에이전트 | Agent Development Kit (ADK)의 LlmAgent. worker와 web에서 구동 |
| 데이터 | BigQuery (추가 전용 이벤트 그래프 및 authorized views), Firestore (조회용 투영, 동기 커서, 중복 제거 상태) |
| 비동기 및 정기 처리 | Pub/Sub, Cloud Scheduler (수집, 투영 재구축, 일일 재판단, 생존 확인의 7개 작업) |
| 인프라 관리 | Terraform. GitHub Actions에서 Workload Identity Federation으로 배포 |
대화에서는 이력을 검색할 필요가 있는지 코드로 판정합니다. 처음에는 탐색 여부까지 모델에 맡겨서 2회 왕복이 걸렸습니다. 응답이 느렸기 때문에 판정을 코드 쪽으로 옮기고, 필요한 경우에만 먼저 ask_why를 1회 실행하여 그 결과를 전달하고 ADK의 에이전트가 1회 왕복으로 답변하도록 했습니다.
오판격 4건은 '강등은 신중하게'라는 방침으로 작업했던 입장에서 무거운 결과였습니다. 판정 기준을 '양립할지 여부 YES/NO'의 이분법적 선택지로 좁히는 축소안도 시도했지만, 오판격은 4건 그대로 줄어들지 않았고 재현율만 떨어졌습니다. 결국 그 안은 채택하지 않고, undo로 되돌릴 수 있다는 점과 작업 로그에서 확인할 수 있는 점을 보완하는 형태로 만들었습니다.
공개 OSS의 PR/Issue 스레드에서는 결정을 한 건도 찾을 수 없었습니다. 원인을 조사해 보니, 애초에 데이터를 가져오는 단계에서 PR/Issue 본문을 저장하지 않았던 것이 문제였습니다. 본문에만 적혀 있는 경위는 추출에 사용될 수 없었던 것입니다. 일반적인 OSS에 사용할 거라면, 우선 데이터 수집 범위부터 재검토할 필요가 있습니다.
응답에는 20~30초가 걸립니다. 왕복(round trip)을 한 번으로 줄였음에도 불구하고, BigQuery의 권한이 부여된 VIEW 탐색(p50 15.4초) 부분이 남아있어 이곳이 다음 병목 지점입니다.
개발 과정
거의 혼자 개발했습니다. 코드 대부분은 AI 에이전트가 작성했습니다. Claude Code를 지휘자로 활용하여 이슈를 분배하게 하고, 구현은 Codex CLI와 Antigravity CLI에 맡겼으며, PR 검토에는 GitHub Copilot을 사용했습니다. 위임받은 결과물을 그대로 믿지 않고, 차이점(diff)을 읽고 실제 기기에서 작동하는 것을 확인한 후에 병합하도록 했습니다.
개발을 시작하고 3주가 지난 시점에서 기록을 되돌아보면, 가장 많은 시간을 할애했던 것은 구현보다 검증이었습니다. 수정 전 코드에서도 통과해 버리는 테스트 케이스가 몇 번 나와서, 위임 지침서에 '기존 구현으로 돌아갔을 때 실패하는 회귀 테스트를 추가한다'고 적도록 했습니다. '운영 환경 설정은 변경하지 않는다'와 같은 지침서의 다른 항목들도 실제 실패 경험에서 추가한 것입니다.
결론
대체 판정에서는 오판격이 4건 나왔고, 공개 OSS에서의 재현율은 0%였습니다. 응답 시간도 20~30초가 걸립니다. 그럼에도 불구하고, 강등을 추가하여 취소할 수 있다는 점과 열람자에게 보이는 범위만으로 탐색하는 것은 실제로 작동시켜 확인해 볼 수 있었습니다. 다음에 손보고 싶은 부분은 대체 판정과 데이터 수집 범위입니다.
논의