12장. 자동화는 내가 틀린 곳까지 반복했다.
주간 보고서를 만들던 봇이 주간 지표를 지어냈다. 문장은 유창했고 숫자에는 망설임이 없었다. 그래서 처음에는 계산이 틀렸다는 사실보다 그럴듯한 보고서가 먼저 눈에 들어왔다. 원자료와 대조하고 나서야 실제로 존재하지 않는 수치가 문장 사이에 들어간 것을 확인했다.
나는 프롬프트를 고쳤다. 숫자를 만들지 말라는 문장을 더 분명하게 넣고, 원자료에 없는 값은 쓰지 말라고 거듭 적었다. 지시는 길어졌지만 문제는 사라지지 않았다. 모델에게 계산과 서술을 함께 맡긴 구조가 그대로였기 때문이다.
주간 지표를 지어낸 봇
이 봇이 하던 일은 복잡해 보이지 않았다. 정해진 자료를 읽고, 변화량을 계산하고, 사람이 읽을 문장으로 정리하는 작업이었다. 계산식도 이미 알고 있었다. 보고서가 필요할 때마다 같은 종류의 숫자를 다시 다뤘다. 나는 이 정도면 프롬프트로 충분히 묶을 수 있다고 봤다.
문제가 난 뒤에도 한동안 문장을 고치는 데 매달렸다. 더 엄격한 금지, 출처 확인 요구, 자신 없는 값의 생략 규칙을 보탰다. 모델은 그 요구를 읽고도 계산 과정에서 빈틈이 생기면 자연스러운 숫자로 메웠다. 잘못을 숨기려는 행동이라기보다 다음 문장을 완성하는 방식에 가까웠다. 보고서를 읽는 사람에게는 의도보다 결과가 중요했다. 존재하지 않는 지표는 여전히 존재하지 않는 지표였다.
오류를 발견하는 데도 모델의 말은 도움이 되지 않았다. 계산을 마친 뒤 스스로 정확하다고 설명할 수 있었고, 어느 자료를 썼는지 그럴듯하게 정리할 수도 있었다. 자기 보고를 근거로 삼으면 잘못된 계산과 잘못된 설명이 서로를 지지했다. 확인하려면 결국 원자료와 계산 과정을 따로 열어야 했다.
여기서 자동화의 단위가 잘못됐다는 사실이 보였다. 나는 보고서 전체를 하나의 작업으로 보고 있었다. 실제로는 원자료를 읽는 일, 숫자를 계산하는 일, 이미 계산된 값을 문장으로 옮기는 일이 달랐다. 세 일을 한 번에 맡기면서 계산의 정답 여부와 문장의 자연스러움을 같은 출력에서 평가했다.
계산을 코드로 옮겼다.
계산을 코드로 옮겼다. 원자료에서 필요한 값을 가져오고 정해진 식을 적용하는 부분은 평범한 프로그램이 맡았다. 모델은 코드가 이미 계산한 값을 문장으로 정리했다. 쓸 수 있는 숫자의 범위가 출력 전에 정해졌고, 계산이 틀리면 문장보다 먼저 코드에서 드러났다.
변화는 프롬프트의 문체가 아니라 책임의 위치에서 생겼다. 이전 구조에서는 모델이 어떤 값을 선택하고 계산하고 설명했다. 바꾼 뒤에는 계산 규칙을 코드가 실행했고, 모델은 그 결과를 읽는 역할만 맡았다. 숫자가 맞아야 한다는 요구가 부탁에서 속성으로 바뀌었다.
이후 주간 보고서를 여덟 번 다시 확인했다. 그 범위에서는 날조는 다시 나타나지 않았다. 여덟 번은 영구적인 보증이 아니다. 다른 자료, 다른 계산, 다른 모델에서도 같은 결과가 난다는 뜻도 아니다. 같은 보고 과정에서 문제를 재현하던 구조를 바꾼 뒤 여덟 번 동안 재발을 보지 못했다는 기록은 남았다.
이 경험에서 얻은 규칙은 단순했다. 정답이 정해진 계산은 모델에게 소망으로 전달하지 말고 실행 가능한 규칙으로 만든다. 모델이 잘하는 서술과 코드가 잘하는 계산을 나누면 검사할 지점도 분명해진다. 당시에는 이 규칙이 자동화 문제를 다루는 데 꽤 넓게 통할 것이라고 생각했다.
그 생각은 절반만 맞았다. 계산으로 닫을 수 있는 부분에서는 효과가 있었다. 어떤 값을 더하고 나눌지 이미 정해졌고 같은 입력에는 같은 결과가 나와야 했다. 디자인 판단처럼 목표 자체가 다투어지는 일까지 같은 방식으로 옮기자 다른 문제가 생겼다. 규칙을 코드로 쓰는 일과 그 규칙이 옳은지를 확인하는 일은 같지 않았다.
수천 개의 단언문이 놓친 결함
뒤이어 만든 자동 검사 도구에는 판단 규칙을 코드로 넣었다. 입력을 읽고 기준을 적용해 결과를 내는 구조였다. 같은 입력에는 같은 판정이 나왔고, 규칙이 발동한 이유도 추적할 수 있었다. 사람의 감각 속에 있던 일부 기준을 반복 실행할 수 있게 됐다는 점에서는 앞선 보고서 자동화와 닮아 있었다.
도구에는 수천 개의 단언문으로 이뤄진 테스트가 있었다. 알려진 입력을 넣고 기대한 결과가 나오는지 확인했다. 테스트가 통과할 때마다 규칙이 코드에 잘 옮겨졌다는 확신이 커졌다. 구현 속도도 빨랐다. 기계가 코드의 많은 부분을 만들었고, 나는 기준을 정하고 결과를 검토했다.
그 도구는 결함 여섯 개를 안고 배포됐다. 테스트가 많다는 사실과 필요한 질문을 모두 던졌다는 사실을 혼동했다. 이미 예상한 정상 경로와 오류는 촘촘히 확인했지만, 시스템의 핵심 주장을 거꾸로 공격하는 입력은 부족했다. 수천 개의 단언문이 같은 이해의 테두리 안에 있었다.
여섯 개 가운데 세 개는 기존 테스트가 통과한 뒤 다른 모델에 핵심 주장을 반증해 보라고 요청하면서 발견됐다. 같은 코드에 대한 설명을 다시 듣거나 테스트를 요약하게 한 것이 아니었다. 도구가 틀렸다고 가정하고 그 가정을 실제 입력으로 만들게 했다. 질문의 방향이 바뀌자 통과 기록 뒤에 숨어 있던 경계가 보였다.
그중 둘은 검사 계층 자체에 있었고, 하나는 규칙이 실행 경로에 연결되지 않은 문제였다. 규칙의 계산이 맞는지 확인하는 일, 계산에 들어갈 입력을 제대로 거르는 일, 그 규칙이 제품에서 실제 호출되는지 확인하는 일이 따로 있었다. 나는 앞의 질문에 많은 테스트를 썼고 뒤의 두 질문에는 충분히 쓰지 않았다.
결함 가운데 하나는 빈 입력을 최고 등급으로 판정했다. 아무것도 들어오지 않았는데 가장 좋은 결과가 나왔다. 비어 있는 값을 먼저 거부해야 했지만, 뒤의 점수 계산은 검사할 항목이 없다는 사실을 감점 없는 상태로 읽었다. 정상 입력의 다양한 조합을 확인한 테스트는 빈 입력이 평가 대상이 될 자격부터 묻지 않았다.
다른 결함은 최악의 값에서 페널티를 없앴다. 나쁜 값일수록 불리해져야 하는 규칙이 경계 끝에서 반대로 작동했다. 중간 구간의 예시는 예상대로 움직였고 테스트도 통과했다. 가장 나쁜 값 하나를 넣었을 때만 계산 경로가 달라졌는데, 규칙의 방향만 보고 끝점까지 밀어 보지 않은 탓이었다.
문서에 있다고 적은 규칙을 건너뛰는 결함도 있었다. 설명에는 검사한다고 쓰여 있었지만 실제 실행 경로에서는 호출되지 않았다. 규칙 자체를 따로 시험하면 맞는 결과를 냈다. 전체 도구 안에서 그 규칙이 실제로 연결되어 있는지는 다른 질문이었다. 부품 테스트가 통과해도 실제 제품에서는 규칙이 실행 경로에서 빠질 수 있었다.
세 결함은 서로 다른 모양이었지만 내가 빠뜨린 일은 비슷했다. 입력이 정상이라는 전제, 값이 경계 안에 있다는 전제, 작성한 규칙은 당연히 연결됐다는 전제를 시험 밖에 두었다. 코드는 그 전제를 의심하지 않았다. 테스트도 내가 의심한 부분만 검사했으니 같은 전제를 공유했다.
여섯 결함 중 일부는 시스템의 주장을 반증해 보라는 적대적 검토에서 드러났다. 이미 만든 테스트를 더 많이 반복한 것이 아니라, 이 도구가 맞다고 믿는 전제를 깨는 입력을 찾았다. 빈 입력, 경계 끝의 값, 문서와 실행의 차이는 모두 정상 작동을 확인하는 질문에서 비켜나 있었다.
생성 비용과 검토 비용
첫 사건에서는 계산을 코드로 옮긴 뒤 문제가 줄었다. 다른 사건에서는 판단을 코드로 옮긴 도구가 결함을 반복했다. 둘은 자동화가 좋거나 나쁘다는 결론으로 묶이지 않는다. 자동화한 대상과 실패한 지점이 달랐다.
보고서의 계산은 정답 조건이 비교적 닫혀 있었다. 원자료와 계산식이 있었고 결과를 다시 계산할 수 있었다. 검사 도구의 일부 규칙도 같은 성격을 가졌다. 어떤 입력을 평가 대상으로 인정할지, 경계에서 어떤 행동을 오류로 볼지, 문서의 약속이 전체 실행에서 지켜지는지는 별도의 판단이 필요했다. 코드는 적힌 규칙을 반복했지만 적히지 않은 질문까지 만들지는 않았다.
기계가 구현을 빨리 만든 덕분에 진입 비용은 낮아졌다. 예전보다 적은 시간과 숙련으로 작동하는 도구를 만들 수 있었지만 검토 비용은 같은 폭으로 줄지 않았다. 코드가 빨리 늘수록 내가 명시하지 않은 가정도 함께 구현됐고, 잘못 적은 규칙은 모든 실행에서 같은 결과를 내는 속성이 됐다.
테스트 수는 안도감을 줬지만 판단의 다양성을 나타내지는 않았다. 같은 이해에서 나온 단언문은 수가 많아도 맹점을 공유한다. 정상 경로와 경계값, 연결 상태, 반증 입력은 서로 다른 질문이어서 어느 하나의 개수를 늘려도 나머지가 채워지지 않았다.
디자인 리뷰에서도 비슷한 착각을 해 왔다. 화면을 오래 보고 의견을 많이 붙였다는 사실이 서로 다른 관점으로 검토했다는 뜻은 아니었다. 모두가 같은 성공 장면을 상정하면 코멘트의 수가 늘어도 같은 실패를 놓친다. 자동 검사 도구에서는 그 한계가 더 선명했다. 코멘트 대신 통과 숫자가 쌓였고, 숫자는 검토가 넓었다는 인상을 줬다.
검토를 넓히려면 예시를 더 많이 만드는 것보다 전제를 나눠야 했다. 아무 입력도 없을 때, 값이 허용 범위의 끝에 닿을 때, 개별 규칙은 맞지만 전체 경로에서 호출되지 않을 때를 각각 물어야 했다. 같은 판정 규칙을 반복 확인하는 일과 시스템이 판정할 자격을 확인하는 일은 다른 작업이었다.
그렇다고 모든 검사에 사람의 승인을 붙이는 것이 답은 아니다. 길이 제한, 필수 필드, 정해진 계산처럼 오류 기준이 닫힌 일은 자동화가 더 일관되게 처리한다. 사람이 매번 다시 보는 편이 오히려 실수와 지연을 늘릴 수 있다. 사람이 남아야 할 자리라는 표현보다 어떤 질문이 아직 코드에 들어가지 않았는지 찾는 편이 정확했다.
첫 사건의 여덟 번도 같은 경계를 가진다. 재발이 없었다는 관찰은 계산 분리가 그 보고 과정에서 작동했다는 근거다. 프롬프트가 항상 실패한다거나 모든 계산을 코드로 옮겨야 한다는 명령은 아니다. 계산 규칙이 자주 바뀌고 탐색 자체가 목적이라면 모델이 제안한 값을 사람이 확인하는 구조가 더 적합할 수도 있다.
다른 사건의 결함 여섯 개도 자동 검사 일반의 실패율을 말해 주지 않는다. 한 도구의 구현과 검토에서 발견된 사건이다. 여기서 가져갈 수 있는 것은 더 좁다. 구현을 생산하는 비용이 낮아져도 정확성을 확인하는 질문까지 자동으로 생기지는 않았다. 특히 도구가 판정자가 될 때는 도구 자신이 주장하는 정상 작동을 공격하는 검토가 필요했다.
자동화에 남길 수 없던 판단
리포팅 봇에서 나는 계산과 서술을 나눴다. 자동 검사 도구에서는 규칙과 규칙의 타당성을 나눠야 했다. 앞의 분리는 코드 경계로 비교적 선명하게 만들 수 있었다. 뒤의 분리는 입력 범위와 실패 조건, 반증 방법을 계속 고쳐야 했다.
자동화할 규칙을 쓰는 순간 이미 선택이 일어난다. 무엇을 오류로 볼지, 빈 입력을 어떻게 다룰지, 어느 경계값을 위험으로 셀지 정한다. 그 선택이 문서와 테스트, 실제 실행에 같은 모습으로 들어가야 한다. 셋 중 하나가 빠지면 도구는 일관되게 다른 일을 할 수 있다.
판단을 코드로 옮기면 사람의 변덕을 줄일 수 있다. 같은 입력에 같은 결과를 내고, 어떤 값 때문에 규칙이 발동했는지 남길 수 있다. 동시에 잘못된 판단도 더 안정적으로 반복한다. 자동화의 신뢰성은 흔들리지 않는 데만 있지 않았다. 무엇을 반복하는지 확인하고 틀렸을 때 규칙을 바꿀 수 있어야 했다.
나는 첫 성공에서 자동화의 경계를 너무 넓게 읽었다. 계산을 분리해 날조가 멈춘 경험은 강했다. 그래서 판단을 실행 가능한 규칙으로 만들면 같은 종류의 통제가 생길 것이라 기대했다. 다른 도구가 보여 준 것은 코드가 판단을 책임져 주지 않는다는 사실이었다. 코드는 내가 적은 판단과 빼먹은 판단을 구분하지 않고 실행했다.
그 뒤로는 도구가 무엇을 맞혔는지만 보지 않았다. 어떤 입력을 평가 대상에서 빼는지, 가장 나쁜 값에서 규칙이 어느 방향으로 움직이는지, 문서의 약속이 실제 호출 경로에 있는지를 먼저 확인했다. 테스트를 더 촘촘히 쓰는 습관보다 테스트가 보호하는 믿음을 밖으로 꺼내는 습관이 필요했다.
도구가 배포된 뒤에야 빈 입력과 끝점, 끊긴 연결을 본 일은 검토의 실패였다. 테스트를 썼다는 사실로 검토가 끝났다고 여긴 내 판단도 그 안에 포함된다. 자동화는 그 착각까지 성실하게 반복했다. 다음 도구에서 바꿔야 할 것은 테스트의 숫자보다 질문을 만드는 방식이었다.