제 1 장

한울연구소의 문서 업무 봇

한울연구소는 가상의 회사다. 앞으로 나오는 문서와 계정, 금액과 시행일도 모두 가상의 값이다.

한울연구소에 갓 들어온 직원이 총무팀에 물었다. “교육비 신청은 어디서 하나요?” 담당자는 하던 일을 멈추고 공유 폴더를 뒤져 규정 문서를 찾아 링크를 보내 줬다. 같은 링크를 다시 찾아 보내는 것이 이달 들어 벌써 열한 번째였다.

링크 하나로 끝나는 질문이라면 열한 번이든 스무 번이든 다시 보내면 그만이었다. 그런데 직원마다 알고 싶어 하는 대목이 달랐다. 어떤 직원은 신청 방법 대신 한도를 물었고 어떤 직원은 작년 규정을 읽고 와서 바뀐 것이 있느냐고 되물었다. 팀장 승인이 먼저인지 결제가 먼저인지 헷갈려 하는 직원도 있었다. 담당자도 어느 조항에 그 답이 있는지까지 외우고 있지는 않아서, 질문을 받을 때마다 규정을 처음부터 다시 펴 봐야 했다. 스무 장 남짓한 규정을 몇 번이고 훑고 있자니 이게 총무 일인지 문서 찾아 주는 일인지 모를 지경이었다.

이런 질문이 몇 달째 줄지 않자 총무팀도 방법을 바꿔 보기로 했다. 결국 담당자가 개발팀을 찾아갔다. 사내 문서를 읽고 직원 질문에 대신 답해 주는 프로그램을 하나 만들어 달라는 요청이었다. 개발팀도 마침 언어 모델을 사내 업무에 써 볼 기회를 찾고 있던 참이라 그 자리에서 해 보겠다고 했다.

개발팀은 그날 오후에 바로 작업에 착수했다. 교육비 규정과 사내 안내문 몇 장을 한 폴더에 모아 두고, 질문이 들어오면 그 문서를 통째로 읽어 질문 뒤에 붙이고, 그대로 언어 모델에 넘기고, 돌아온 문장을 화면에 띄우는 것이 전부였다. 문서라고 해야 몇 장뿐이라 길이에 여유가 있었고, 폴더에 있는 것을 전부 넣으면 그만이었다.

만들어 놓고 총무팀 담당자를 불러 직접 물어보게 했는데, 예상외로 잘 동작했다. 담당자가 “교육비 신청은 어디서 하나요?”라고 묻자 지원 포털에서 신청하라는 답이 돌아왔고, 팀장 승인이 먼저인지 결제가 먼저인지 묻자 승인을 먼저 받으라고 했다. 그동안 직원들에게 받아 온 질문을 이리저리 바꿔 물어도 묻는 의도에 맞게 대답했다.

담당자가 이 정도면 쓰겠다고 해서 개발팀은 그다음 주 회의에서 이 화면을 그대로 보여 줬다. 이달에만 링크를 열한 번 보낸 사람이 직접 쓸 만하다고 한 마당이라 토를 다는 사람은 없었고, 그 자리에서 다음 달부터 전 직원이 쓰기로 했다. 오후 한나절에 만든 것이 이 정도면 나머지 문서를 마저 붙이는 일만 남은 셈이었다.

한 달 뒤 사내 공지가 나갔다. 데모 때와 같은 화면이었지만 묻는 사람은 담당자 한 명에서 전 직원으로 늘어 있었다. 하지만 막상 직원들이 쓰기 시작하자 첫 주부터 바로 문제가 발생했다.

업무 봇

직원들이 봇에 보낸 요청은 크게 세 가지였다.

직원의 요청봇이 하는 일
“교육비 신청은 어디서 하나요?”그 직원이 읽을 수 있는 문서에서 근거를 찾아 답한다
“제 장비 교체 신청은 어떻게 됐나요?”티켓 시스템에서 그 직원의 티켓만 조회한다
“노트북 고장 티켓 올려 주세요”초안을 보여 주고 확인을 받은 뒤 한 번만 등록한다

근거를 찾지 못하면 봇은 모른다고 답한다. 사용자가 읽을 수 없는 문서는 봇에 넘어가기 전에 빠진다.

첫 데모

개발팀은 담당자가 물었을 때 봇이 무엇을 보고 답한 것인지부터 짚어 봤다. 모델이 실제로 받은 입력은 이런 모양이었다.

[교육비 안내]
직원 교육비 한도는 분기당 20만 원이다.
구매 전에 팀장의 승인을 받고 지원 포털에서 신청한다.

[질문]
교육비 신청은 어디서 하나요?

이렇게 문서와 질문을 하나로 묶어 모델에 넘기는 입력을 프롬프트라고 부른다. 모델은 “지원 포털에서 신청하되 구매 전에 팀장 승인을 받으세요”라고 답했다. 정확했다.

그런데 이 모델이 한울연구소의 내부 정책을 원래부터 알고서 대답한 것은 아니다. 교육비 규정은 어느 모델의 학습 자료에도 들어 있지 않다. 한울연구소가 가상의 회사라서 그런 것만은 아니고, 실제 회사라도 사내 규정을 인터넷에 공개하지는 않는다. 모델은 단지 방금 프롬프트에 붙여 준 두 문장을 읽고 답했을 뿐이다.

확인하는 방법은 간단하다. 문서를 빼고 질문만 보내 보면 된다. 같은 모델에 같은 질문인데도 20만 원이라는 숫자는 나올 수 없다. 그 숫자가 모델 안에 없기 때문이다. 모델은 그대로 두고 넣어 주는 것만 바꿔도 답이 달라진다.

모델이 학습에서 얻어 온 것은 한국어로 문장을 쓰고 앞뒤 관계를 따지는 능력이다. 지금 교육비 한도가 얼마인지는 거기 없어서, 요청이 올 때마다 개발팀이 문서를 찾아 넣어 줘야 한다. 그러니 개발팀이 손댈 곳은 모델이 아니라 모델에게 넘기는 입력 쪽이다.

첫 주의 실패

공지가 나간 뒤로 질문이 한꺼번에 들어왔다. 담당자 한 명이 묻던 데모 때와 달리 직원들은 저마다 다른 것을 물었는데, 봇은 무엇을 묻든 매끄러운 한국어로 답했다. 그래서 개발팀도 처음에는 잘 돌아가는 줄 알았다. 네 군데가 걸리고 나서야 아니라는 것을 알았다. 고작 나흘 사이에 일어난 일이다.

① 개정 전 금액으로 답했다. 9월 2일, 봇이 교육비 한도를 20만 원이라고 답했다. 8월에 규정이 개정돼 30만 원이 됐는데 데모에 넣어 둔 파일이 개정 전 것이었다. 파일만 갈아 끼우면 될 것 같지만 여기에는 문제가 하나 더 있다. 문서에는 등록된 날과 시행되는 날이 따로 있어서 가장 최근에 올라온 파일이 꼭 지금 유효한 파일은 아니다.

최신 문서와 유효한 문서v1 · 한도 20만 원시행 전v2 · 한도 30만 원1/1 · v1 시행8/20 · v2 등록9/1 · v2 시행8/25 질문9/2 질문8/25 「지금 한도는?」 → v1 · 20만 원 (최신 문서는 v2, 유효한 문서는 v1)9/2 「지금 한도는?」 → v2 · 30만 원9/2 「개정 전 한도는?」 → v1 · 20만 원 (질문에 시점 조건이 있다)

그래서 개발팀은 본문에 네 가지를 더해 doc_id, revision, effective_from, audience, body 다섯 필드로 문서를 저장한다. 어느 문서의 몇 번째 개정인지, 언제부터 유효한지, 어느 부서가 읽을 수 있는지를 본문과 함께 적어 둔다. 그래야 서버가 질문 시점에 맞는 리비전을 고른다.

② 없는 조항을 지어냈다. 직원이 “육아휴직 중에도 교육비가 나오나요?”라고 묻자 봇이 나온다고 답하면서 지급 조건 세 줄을 덧붙였다. 복직 후 6개월 근무, 부서장 확인, 분할 지급 불가처럼 그럴듯한 조건이었는데 등록된 문서에는 그런 조항이 없었다. 모델은 모를 때도 모른다고 하지 않고 그럴듯한 문장을 만들어 낸다. 이것을 환각(hallucination)이라고 부른다. 없는 조항을 지어낸 것도 있는 숫자를 잘못 옮긴 것도 화면에서는 똑같이 매끄러운 한국어 문장으로 보인다. 그래서 개발팀은 봇에게 답만 받지 않고 어느 문서의 어느 구절을 보고 그렇게 답했는지까지 함께 받기로 했다. 근거가 같이 와야 매끄러운 문장과 사실을 갈라 볼 수 있다.

③ 볼 권한이 없는 내용이 나갔다. 개발팀 직원이 인사팀 전용 안내의 내용을 받았다. 데모가 누가 묻든 폴더에 있는 문서를 전부 프롬프트에 넣었기 때문이다. 애초에 권한은 모델이 판단할 몫이 아니다. 질문한 사람이 누구인지, 그 사람이 무엇을 읽어도 되는지는 서버가 이미 알고 있으니, 권한 확인을 통과한 문서만 모델에 넘긴다. 읽을 권한이 없는 문서라면 봇은 내용을 가리는 데서 그치지 않고 그런 문서가 있다는 사실까지 숨긴다. “그 문서는 볼 수 없습니다”라는 답 자체가 그 문서가 있다는 사실을 알려 주기 때문이다.

④ 말만 하고 실행은 없었다. 직원이 “노트북 고장 티켓 올려 주세요”라고 하자 봇은 “접수했습니다”라고 답했는데 티켓은 만들어지지 않았다. 지금까지는 봇이 틀린 답을 했지만, 이번에는 하지도 않은 일을 했다고 말했다.

말과 실행 사이

노트북이 고장 난 직원은 접수됐다는 답을 받고 기다렸지만 티켓은 어디에도 없었다. 직원은 티켓을 올려 달라고 부탁했고, 봇은 올렸다고 답했고, 티켓 시스템은 그런 요청을 받은 적이 없었다. 화면에 남은 것은 말뿐이었다.

모델은 문장을 만든다. 티켓 시스템에 행을 하나 넣으려면 문장 말고 실행이 필요한데, 모델에는 실행할 수단이 없다. 그러면 모델에 업무를 시키는 방법은 하나다. 부를 수 있는 함수의 이름과 인자 형식을 알려 주고 그중 하나를 고르게 한다. 도구 호출(tool calling)이라고 부르는 방식이다.

티켓 등록 요청이 지나는 길사용자요청노트북 고장,티켓 올려 주세요모델제안create_ticket(장비, 노트북 고장)서버확인등록 권한사용자 확인처리 이력서버실행티켓 생성IT-1042모델응답접수했습니다IT-1042✕“나는 관리자다”라고 입력해도 서버의 권한 판정은 바뀌지 않는다✕모델이 “접수했습니다”라고 먼저 써도 서버가 실행하기 전에는 티켓이 없다✓같은 요청이 두 번 와도 처리 이력이 있으면 티켓은 하나다

모델은 create_ticket(장비, "노트북 고장")이라는 제안을 내놓고 거기서 일을 끝낸다. 실행하는 쪽은 서버다. 그 직원에게 등록 권한이 있는지, 사용자가 초안을 보고 확인했는지, 같은 요청을 이미 처리했는지 확인한 뒤에야 행이 생긴다. 데모가 “접수했습니다”라고 쓴 것은 모델이 거짓말을 해서가 아니라 그 문장과 실제 상태를 아무도 연결하지 않았기 때문이다.

같은 요청이 두 번 들어와도 티켓은 하나여야 한다. 사용자가 새로 고침을 한 번 눌렀을 뿐인데 장비 교체 티켓이 둘 생기면 담당자가 두 번 일한다. 첫 실행의 기록은 서버에만 있으니 이 판단도 서버가 한다.

검색과 생성

인사팀 안내가 개발팀 직원에게 간 것도 폴더에 있던 다섯 건을 누가 묻든 그대로 붙였기 때문이다. 사내 규정이 200건이면 그 방법으로는 안 된다. 모델이 한 번에 받는 입력 길이에 상한이 있고, 길이를 채울수록 비용과 응답 시간도 함께 오르기 때문이다.

그래서 질문마다 필요한 문서만 골라 넣는다. 200건 중 이 질문에 관련된 두세 건을 찾고, 그 사람이 읽을 수 있는 것만 남기고, 질문 시점에 유효한 리비전으로 추린 다음 모델에 넘긴다. 찾아서 넣고 그 자료로 답하게 하는 구조를 RAG(retrieval-augmented generation)라고 부른다. 대신 검색이 들어오면서 확인할 것이 하나 늘었다. 모델이 잘 답했는지를 보기 전에, 답에 필요한 문서가 애초에 후보로 올라왔는지를 먼저 확인해야 한다.

데모와 서비스의 차이

개발팀이 네 가지 문제를 하나씩 짚어 보니 데모에 없던 단계를 앞뒤로 붙여야 했다.

데모의 네 단계와 서비스의 일곱 단계데모데모에 없던 결정질문문서 전부붙이기모델화면서비스사용자 질문인증된 사용자요청 검사누가 물었나허용 범위문서 검색유효한 판읽을 권한모델답변 초안도구 제안도구 실행사용자 확인한 번만응답 검증근거 확인도구 결과사용자 응답상태 + 근거

데모는 네 단계뿐이다. 질문을 받고, 문서를 전부 붙이고, 모델을 호출하고, 나온 문장을 보여 준다. 서비스는 같은 모델을 쓰면서 앞뒤로 단계를 더 둔다. 앞에는 누가 물었고 무엇을 읽을 수 있는지 정하는 단계가, 뒤에는 모델이 돌려준 것을 검사하고 실행하는 단계가 온다. 이 책에서는 대부분 모델이 아니라 앞의 권한 확인과 뒤의 결과 검사를 만든다.

답할 수 없을 때

“제 장비 교체 신청은 어떻게 됐나요?”를 붙이면서 개발팀은 한 가지를 정해야 했다. 티켓 시스템이 응답하지 않을 때 봇이 무엇이라고 답할 것인가. 데모는 어떤 질문에도 문장 하나로 답한다. 서비스는 답할 수 없는 경우를 넷으로 나누고 각각 다르게 처리한다.

다섯 가지 답변 상태이 사용자에게 허용된 요청인가아니오forbidden접근이 제한됐다고만 알린다예질문에 대상과 조건이 다 있나아니오needs_clarification빠진 정보를 되묻는다예조회·등록 도구가 정상 동작했나아니오unavailable실패했다고 알리고 재시도 가능 여부를 말한다예허용된 문서·결과로 답을 확인했나아니오insufficient_evidence확인할 근거가 없다고 알린다예answered답변과 근거를 보여 준다

조회 도구가 시간 초과로 실패했으면 “신청 내역이 없다”가 아니라 “지금은 확인할 수 없다”다. 실패했는데 없다고 답하면 장애가 정상 응답으로 집계된다. 이 다섯 상태는 이 책의 서비스가 정한 값이다. 모델 API가 돌려주는 상태 코드에는 이런 구분이 없다. API 호출이 정상으로 끝나도 근거가 없으면 insufficient_evidence다. 장비 교체를 물은 직원에게 “없다”와 “지금은 확인할 수 없다”는 전혀 다른 말이다. 앞을 들으면 다시 신청해야 하고 뒤를 들으면 기다리면 된다.

틀린 답의 원인

직원이 현재 교육비 한도를 물었는데 봇은 20만 원이라고 답했다. 이제 서비스에는 단계가 여럿이라, 화면에 보이는 오답은 하나지만 원인은 검색·문맥·모델 셋 중 하나다.

같은 오답, 세 가지 원인틀린 곳검색 결과모델 입력답변① 검색v1v2✕v120만 원② 문맥v1v2v1v2✕20만 원③ 모델v2v220만 원✕화면에서는 셋이 같은 오답이다

검색 결과에 v2가 없었으면 색인이나 검색을 고친다. 검색은 v2를 찾았는데 길이 제한에 걸려 모델 입력에서 빠졌으면 문맥 구성을 고친다. v2만 넘겼는데도 20만 원이라고 답했다면 그때 프롬프트와 모델을 본다. 그 전에 기대한 답부터 다시 본다. 물어본 사람이 생각한 ‘최신 정책’이 시행일과 어긋나면 모델이 아니라 기대한 답을 고친다.

단계마다 무엇이 들어가고 나왔는지 남겨야 원인을 구별할 수 있다. 요청 식별자, 문서 리비전, 모델 설정, 선택한 문서, 처리 상태, 단계별 시간부터 기록한다. 데모에는 이 가운데 하나도 없었다. 그래서 9월 2일에는 개발팀이 폴더의 파일을 직접 열어 보고서야 왜 20만 원이 나왔는지 알았다.

요구사항으로 옮기기

첫 주가 끝나자 개발팀은 봇을 다시 시범 운영으로 돌렸다. 링크를 열한 번 보내던 담당자만 그대로 쓰기로 하고, 나머지 직원은 준비가 끝난 뒤에 다시 쓰게 하기로 했다. 그러고 나서 개발팀은 어디가 잘못됐고 그 자리를 무엇으로 메워야 하는지를 일곱 줄로 적었다.

식별자요구사항
R1허용된 문서에 있는 정보로 답하고 근거를 함께 보여 준다
R2질문 시점에 유효한 문서 리비전을 쓴다
R3사용자가 읽을 수 없는 정보는 답변에도 도구 결과에도 넣지 않는다
R4정보가 부족하면 되묻거나 답변 불가로 처리한다
R5변경 업무는 필요한 인자와 사용자의 확인을 갖춘 뒤 실행한다
R6조회 실패와 결과 없음을 구분한다
R7같은 변경 요청을 다시 보내도 결과가 중복되지 않는다

개발팀은 요구사항을 하나같이 “무엇을 하면 안 되는가”가 아니라 “어떤 상황에서 무엇을 해야 하는가”로 썼다. 그래야 요구사항마다 봇이 그 조건을 놓치는 상황을 만들어 확인할 수 있다. 개정 직후에 들어온 질문은 R2에서 걸리고, 같은 등록 요청을 두 번 보내면 R7에서 걸린다. 그런 상황을 스무 개 골라 첫 점검 목록 Q01–Q20으로 삼는다.

되돌린 뒤로 봇을 쓰는 사람은 담당자 한 명뿐이었다. 직원들은 다시 총무팀으로 찾아왔고, 담당자는 그 질문을 받아 봇에게 대신 물었다. 교육비 한도를 묻자 분기당 30만 원이라는 답이 바로 나왔고, 담당자는 규정을 한 번 더 펴 확인한 다음에야 직원에게 전했다. 이달 들어 열한 번이던 링크는 그 주에도 몇 번 더 늘었다. 다만 그 답을 찾는 데 걸리는 시간은 전보다 짧아졌다.

그 주 회의에서 담당자는 쓸 만하다고 했다. 대신 전 직원이 다시 쓰게 되는 날이 언제냐고 물었다. 개발팀은 아직 아무 말도 할 수 없었다. 무엇을 고쳐야 하는지는 이미 적어 뒀지만, 그 한 줄을 코드로 옮기는 데 며칠이 걸리는지는 해 보기 전에 모를 일이었다.

데모는 오후 한나절이면 만들 수 있었다. 폴더에 있는 문서를 전부 붙여 모델에 넘기면 그만이었기 때문이다. 하지만 이제는 문서마다 리비전과 시행일을 적어 넣어야 하고, 질문이 올 때마다 그 사람이 읽어도 되는 문서인지 가려야 하고, 모델이 돌려준 문장에 근거가 붙었는지 검사해야 하고, 접수했다는 말이 실제 티켓과 맞는지 확인해야 한다. 이런 과정이 없으면 아직 전 직원이 자유롭게 쓸 만한 업무 봇이라고 말할 수 없다.

문제와 모델의 경계1 / 21