
생성 AI를 누가 사용해도 같은 결과로 — 러닝 코치를 워크플로로 고정하기
요약
사용자의 프롬프트 숙련도에 상관없이 일관된 결과를 얻기 위해 AI 워크플로를 설계하는 방법을 다룹니다. 러닝 코치 앱 'RunAdvisory' 사례를 통해 핵심 로직은 고정하고 AI는 보조적으로 활용하는 구조를 설명합니다.
핵심 포인트
- 프롬프트 숙련도에 의존하지 않는 일관된 AI 워크플로 설계의 중요성
- 핵심 비즈니스 로직은 AI가 아닌 결정론적 코드로 고정하여 신뢰성 확보
- 데이터(Strava)를 통해 변수를 추정하고 AI를 통해 자연스러운 결과 도출
- 환각(Hallucination) 문제를 방지하기 위한 구조적 접근법
- AI에게 부탁하면 대부분의 일은 편리하게 해낼 수 있다.
- 하지만 같은 것을 부탁하더라도, 사용하는 사람에 따라 돌아오는 결과는 달라진다.
- 누가 해도 좋은 결과가 나오도록 정돈하는 것 —— 그 방법을 러닝 코치를 만드는 과정에서 실제로 시도해 본 기록.
러닝 코치 앱을 만든다면, 지금은 이렇게 만드는 것이 빠르다. ChatGPT에게 "당신은 러닝 코치입니다. 오늘 할 연습을 짜주세요"라고 부탁하고, 자신의 연습 기록을 건네준다. 그럴듯한 메뉴가 돌아오고, 잘 활용할 줄 아는 사람에게는 그것으로 충분하다.
다만, 제대로 활용하려면 은근히 손이 많이 간다. 달릴 때마다 결과를 다시 붙여넣어야 한다. 돌아온 내용이 맞는지, 그럴싸한 오류인 환각 (Hallucination)은 아닌지 의심하며 별도로 확인해야 한다. 프롬프트 (Prompt) 작성법이 미흡하면 답도 흔들린다.
이는 최근 업무에서도 자주 보이는 형태다. AI에 익숙한 사람은 무심결에 정확하게 지시하여 좋은 결과를 이끌어낸다. 하지만 익숙하지 않은 사람은 지시가 모호하거나, 돌아온 내용을 확인할 여력이 없어 똑같이 사용할 수 없다. 같은 도구라도 실력에 따라 결과가 달라진다.
그래서 최근 자주 하는 방식은, 누가 사용해도 같은 지점에 도달할 수 있도록 워크플로 (Workflow)를 짜두는 것이다. 프롬프트의 숙련도나 확인하는 인내심에 결과를 맡기지 않는다. 생성 AI (Generative AI)의 장점은 살리되, 어긋나면 곤란한 부분은 이쪽에서 고정해 둔다.
이번 러닝 코치도 그 형태로 만들어 보았다. RunAdvisory라는 iOS 앱이다.
토대로 삼은 것은 다니엘스의 이론이다. 『다니엘스의 러닝 포뮬러 (Daniels' Running Formula)』 —— 러닝의 연습 강도를 체계적으로 결정하기 위해 개정판이 거듭되며 읽혀온 책으로, 그 중심에는 VDOT이라는 개념이 있다. 대략 말하자면 "현재의 달리기 능력을 하나의 숫자로 나타낸 것"이다. 이 숫자만 결정되면, 천천히 달리는 이지 런 (Easy Run)은 이 페이스, 빠르게 끊어 달리는 인터벌 (Interval)은 이 페이스와 같이 연습마다 적절한 속도가 줄줄이 나온다. 다만, 그 VDOT을 구하려면 본래 5000m나 레이스를 전력으로 달려 측정할 필요가 있다.
RunAdvisory가 하는 일은 단순하다. 평소 사용하는 연습 기록 앱 (Strava)의 데이터에서 현재의 달리기 능력을 읽어 들여, 이론에 따라 오늘 할 연습을 하나 반환한다. 그 전력 질주는 필요 없다. 달릴 때마다 최신 기록으로 다시 측정하기 때문에, 달리기 능력이 변하면 숫자도 그만큼 움직인다.
"오늘 밤은 5:00 그룹에 들어가. 이번 주에 인터벌이 쌓였으니 하나 빼고 보존해. 끝나면 조깅 2km 추가해."
클럽은 페이스별 "그룹"으로 나뉘어 달린다. 오늘 밤 어느 그룹에 들어갈지, 이번 주의 부하를 보고 한 세트를 뺄지, 마무리로 가볍게 달릴지 —— 오늘의 할 일이 이 한마디에 담겨 있다. 표도 플랜도 이론도 보여주지 않는다. 건네주는 것은 오늘 할 일 하나뿐이다. 만든 것은 온전히 나 자신을 위해서였다. 전력 질주는 가급적 피하고 싶고, VDOT을 다시 측정하는 것은 지속하기 어렵다. 이론은 알고 있어도 연습으로 이어지지 않는다 —— 나 자신이 바로 그 직전에서 멈춰 있었다.
핵심은 이 "오늘 할 일"을 AI에게 쓰게 해서 내보내는 것이 아니라는 점이다. 다니엘스의 이론을 검증할 수 있는 결정적인 로직 (Logic)으로 고정하여 보유한다. 페이스 처방은 책의 식과 표 그대로다. 따라서 누가 사용하더라도 같은 입력이라면 같은 답이 돌아오고, 왜 그 숫자인지도 나중에 추적할 수 있다. 책 밖으로 나온 점은 단 하나 —— 다니엘스는 "현재의 달리기 능력을 알고 있다"는 전제에서 시작하므로, 그 VDOT을 평소 기록으로부터 추정하는 부분만 직접 구성했다.
여기서부터는 그 "누가 사용해도 벗어나지 않는 것"을 어떻게 구성했는지에 대한 이야기다. 이론을 어떻게 고정했는지, 고정한 것을 어떻게 확인했는지, 그리고 AI를 어디에 남겨두었는지에 대해 말하겠다.
서적에는 VDOT별로 "이지 런은 이 페이스, 인터벌은 이 페이스"라는 빠른 참조표가 실려 있다. 구현 시에는 그 표를 찾아 보간하는 것이 아니라, 표의 근거가 된 식 —— 다니엘스의 표를 뒷받침하는 Gilbert의 산소 비용 식 (Oxygen Cost Equation) —— 을 그대로 해석식으로 두었다.
oxygenCost(v) = -4.60 + 0.182258·v + 0.000104·v² // v = m/min (Gilbert 산소 비용 식)
percent(t) = 0.8 + 0.1894393·e^(-0.012778·t)
+ 0.2989558·e^(-0.1932605·t) // t = 분 (%VO2max의 저하 곡선)
...
표를 참조하는 방법도 있다. 여기서 식으로 만든 이유는 모든 페이스를 동일한 하나의 원리에서 도출하고 싶었기 때문이다. 나중에 "왜 이 숫자인가"를 원전까지 추적할 수 있다.
5000m를 20:00로 달리는 사람이라면, 이 식을 통해 VDOT는 49.8로 나온다. E / M / T / I / R——이지(Easy), 마라톤(Marathon), 역치(Threshold), 인터벌(Interval), 리피티션(Repetition), 즉 느린 쪽에서 빠른 쪽으로 나열된 5가지 페이스는, VDOT에 계수(0.72~1.05)를 곱한 값을 동일한 식으로 다시 속도로 풀어내어 구한다. 인쇄판의 표와도 오차 없이 일치한다.
토대가 이 정도로 맞으면, 역으로 '맞지 않는 부분'이 선명하게 보인다.
식도 처방도 책에 적힌 대로 구현했다. 핵심은 제대로 작동한다. 빠져 있는 것은 그 주변의 세부 사항이다. 원전에 적혀 있음에도 아직 코드에 완전히 반영되지 않은 조건들이 남는다. 이 '적혀 있는데 빠져 있는 것'을 찾아내는 것이, 지루하지만 효과적인 작업이다.
그래서 변경의 분기점마다 여러 AI를 점검 요원으로 세워, 구현 내용을 원전과 대조한다. 관점은 6가지로 나누었다——VDOT의 과학 / 각 페이스의 도출 / 강도 판정 / 부하와 시기 배분 / 메뉴 대조 / 누락 비평. 각각이 담당 코드를 읽고 원전의 식, 수치, 장(Chapter)과 대조해 나간다.
단 한 번의 감사로 원전과의 누락 및 미비 사항이 22건 발견되었다. 중대한 것이 5건, 중간 정도가 10건, 가벼운 것이 7건이었다.
수정 순서는 코드의 깔끔함으로 결정하지 않는다. 그 누락으로 인해 달리는 사람의 몸에 어떤 일이 일어나는가로 결정한다. 가장 우선순위가 높았던 것은 더위 보정이 들어가 있지 않았던 점이었다. 한여름 밤의 클럽 훈련에서도 선선한 날과 같은 페이스를 내버린다. 그 페이스는 너무 빨라서 도달할 수 없고, 억지로 따라가려다가는 부상으로 이어진다. 따라서 이론적인 큰 누락보다, 몸에 직접적인 영향을 주는 누락을 먼저 수정한다.
누락을 찾아내는 것까지가 점검 요원인 AI의 일이다. 사람이 맡는 것은 그 다음 단계——발견된 누락에 방금 본 것과 같은 우선순위를 부여하는 일이다. 기계가 빠짐없이 찾아내고, 사람이 신체적인 감각으로 순서를 정한다.
내 기록으로 앱을 테스트하던 중, 표시된 이지(Easy) 페이스의 범위가 걸렸다. 체감상으로는 6:30/km 정도가 기분 좋다. 그런데 나오는 범위가 어딘가 맞지 않았다. "이거, 정말 맞는 거야?"
당시 구현은 이지 대역을 원전 제4장에 실린 숫자로 산출하고 있었다. 맞아야 정상이었다. 하지만 원전을 다시 읽어보니 착오가 있었다. 제4장의 숫자는 '이지란 이런 존(Zone)이다'라는 넓은 정의였고, 실제로 처방에 사용하는 대역은 따로 있었다. 표 5.2에 나와 있는 조금 더 좁은 대역이다. 정의의 대역과 처방의 대역. 비슷해 보이지만 별개의 것이었다.
처방에 사용하는 대역으로 수정하자 체감상의 6:30과도 자연스럽게 맞아떨어졌다. "맞는 거야?"라는 소박한 의구심이 구현상의 착오를 하나 찾아낸 격이었다.
// DRF Table 5.2 printed E prescription band (about 63-73% VO2max),
// narrower than the chapter 4 zone definition (59-74%).
easyPaceRangeSlowFraction = 0.63
...
원전의 실측값과 Gilbert 식의 검산으로 근거를 확인한 뒤 수정한다. 이론서를 상대할 때는 '어디에 무엇이 적혀 있는가'뿐만 아니라, '어떤 기술을 어떤 계산에 사용해도 되는가'까지 맞춰야 한다. 이 부분을 혼동하면, 올바르게 작성했다고 믿는 구현이 하나씩 조용히 어긋나게 된다.
더 큰 미비 사항은 주력을 단 하나의 숫자로 측정하고 있었다는 점이었다.
초기에는 모든 실적 중에서 가장 높은 VDOT를 하나 선택하고, 그것으로부터 E / M / T / I / R은 물론 마라톤 예측치까지 도출했다. 이것 자체는 Daniels의 입문적인 지침대로이며, 책에도 "거리마다 VDOT는 차이가 나지만, 가장 높은 값을 모든 페이스에 사용해도 좋다"라고 되어 있다.
다만 읽어나가다 보면, Daniels 자신의 사용법은 조금 더 세밀하다. 목표 레이스에 가까운 거리의 실적에서 VDOT를 가져오고, 스피드 위주의 페이스는 짧은 거리의 속도에 맞춘다. 속도의 근거와 지구력의 근거를 하나의 숫자에 묶어두지 않는다. 실제 러너들도 단거리 능력이 좋은 사람과 장거리 능력이 강한 사람으로 나뉜다.
단 하나의 최고 VDOT로만 밀어붙이면, 약한 부분을 과도하게 처방하게 된다. 1500m는 빠르지만 하프 마라톤은 평범한 사람에게, 마라톤 페이스나 T 페이스를 30~40초/km 더 빠르게 몇 달 동안 계속 적용하는 것——이는 부상으로 가는 지름길이다.
그래서 Daniels의 방식에 맞춰, 근거를 지속 시간 기준으로 두 계통으로 나누었다. 나누는 방식의 세밀함은 책에 지정되어 있지 않으므로, 우선은 두 가지로 나누었다.
- 15분보다 짧은, 전력에 가까운 달리기 → 인터벌 / 리피티션 (I / R)의 기준으로
- 15분보다 긴 달리기 → 이지 / 마라톤 / 임계 (E / M / T)와, 마라톤 예측의 기준으로
「전력에 가까운 달리기」라고 해도, 측정 테스트를 부과하는 것은 아니다. 평소 연습에 섞여 있는 짧고 빠른 구간을, 그대로 속도의 증거로 사용한다.
속도의 증거와 지구력의 증거를, 별도로 사용한다. 한쪽의 증거가 결여되었을 때는, 보수적인 쪽으로 치우쳐 채운다. 지구력에서 스피드를 보충할 때는 VDOT를 2.0, 스피드에서 지구력을 보충할 때는 5.0 뺀다——결여된 쪽일수록, 안전한 쪽으로 크게 낮춘다. 이 2계통으로 나누는 방식도 할인 수치도, 책에 실려 있는 것이 아니다. 다니엘(Daniels)의 방향에 따라, 이쪽에서 결정한 설계다.
이 누락된 부분을 재점검에서 발견한 것이 오전, 2계통으로 나누는 구현을 내놓은 것이 같은 날 낮이었다. 감사(Audit)에서 「달리는 사람에게 어떤 일이 일어나는가」가 언어화되어 있으면, 수정은 빠르다.
①도 ②도, 원전에 구현을 맞추는 이야기였다. 세 번째는 성격이 다르다. 이론은 그대로 옳고, 어떻게 전달할지만을 결정한 이야기다.
이지(Easy) 페이스에는, 다니엘 스스로가 「폭」을 인정하고 있다. 「대화할 수 있는 속도라면, 이 범위 중 어디라도 좋다」. 이론상으로는, 그것으로 충분하다.
하지만, 메뉴 작성이 익숙하지 않은 사람에게 「이 폭으로 달려」라고 폭 그대로 전달하면, 대개 망설인다. 「그래서, 결국 얼마로 달려야 해?". 폭은, 친절한 듯하면서도, 실행 직전에 사람을 멈추게 한다.
그래서, 오늘 밤의 메뉴로서 전달할 때는, 한 점으로 결정한다. 범위의 중간값을 내놓는다. 이지는 빠른 쪽으로 치우친다고 해서 효과가 늘어나는 것도 아니기에, 중간값이라면 벗어나지 않고, 자신 없는 사람이라도 발을 내디딜 수 있다. 「폭이 있다」라는 이론은 그대로 지표 화면에 남겨두면서, 실행의 장면에서는 망설임을 없앤다. 이론을 버린 것이 아니다. 이론을 따른 상태에서, 사용하는 사람이 고민하지 않아도 되는 형태로 만든 것이다.
이론을 굳히고, 원전과 대조하여 확인했다. 마지막은, AI를 어디에 남길 것인가다.
이 앱이 내리는 판단은, 이론으로부터 결정적으로 결정된다. 같은 입력이라면, 같은 답. 테스트로 고정할 수 있고, 왜 그 숫자인지를 나중에 추적할 수 있다.
그 위에, 또 하나 맡고 싶은 고민이 있다. 러닝 클럽에 가면, 그날의 메뉴가 게시되어, 「자신의 달리기 능력에 맞는 조에 들어가라」는 말을 듣는다. 달리기에 익숙하다면 선택할 수 있다. 하지만 막 시작했을 때는, 어느 조가 자신에게 맞는지, 가늠조차 할 수 없다. 이 부분을, 앱이 대신하게 하고 싶다.
다만, 그 조 편성(Grouping)과 메뉴는, 자연어로 배포된다.
「토요일, 5조 변화주. 7:30 조는 500m + 700m를, 2시간까지」
변화주(Fartlek)는, 빠른 구간과 연결 구간을 교대로, 쉬지 않고 계속해서 달리는 메뉴다. 클럽에 따라, 메뉴를 쓰는 방식도 구성하는 방식도 다양하며, 이 한 문장도 그중 하나에 불과하다. 여기서 하고 싶은 것은, 2단계의 읽기다. 우선, 이것은 「어떤 강도의 연습인가」. 다음으로, 그 강도라면 「자신은 어느 조에 들어가는 것이 타당한가」. 전반부의 읽기만을, 자신의 서버(VPS)에서 동작하는 언어 모델(LLM)에 넘긴다. 사용한 도구는 Codex CLI다. 후반부, 그 강도와 달리기 능력을 대조하여 조를 결정하는 것은, 지금까지 굳혀온 결정적인 로직——엔진(Engine) 쪽이다. 이해는 모델, 판단은 엔진이다.
읽기에서 효과적인 것은, 평균으로 뭉뚱그리지 않는 것이다. 변화주를 하나의 평균 페이스로 만들면, 빠른 구간도 연결 구간도 섞여서, 그저 느린 조깅처럼 보여버린다. 그래서 뭉뚱그리지 않고, 구간별로 나누어 다룬다. 이 7:30 조라면, 빠른 구간은 마라톤 페이스, 연결 구간은 이지, 라고 구간별로 풀 수 있다. 쉬지 않고 계속하는 변화주를, 형태만 보고 「인터벌」이라고 단정 짓지도 않는다. 반대로, 1000m를 몇 회, 라는 진짜 인터벌이라면, 그 강도(I)를 떨어뜨리지 않고 남긴다. 빠른 곳을, 느린 곳에서 희석하지 않는다. 이 구분선을, 프롬프트(Prompt)로 LLM에게 전달해 둔다.
「변화주」, 「여유를 가짐」, 「장거리 주」, 「2시간 주」 등, 쉬지 않고 계속 달리는 메뉴는,
질주/연결이 교대로 나타나더라도 인터벌(I)로 분류하지 마세요. 연속주로서 표현하고,
components로 500m/700m 등의 구조를 남겨주세요.
...
모델의 출력에는, 읽어낸 강도나 구조에 더해, 어느 정도 확실한지에 대한 「확신도(Confidence)」도 붙는다. 다만, 그 출력은 형태가 흔들려서, 지시해도 「E~M」과 같은 중간 표기를 써오기도 한다. 그래서, 출력을 확인하는 공정에서, 관대하게 버릴 부분과, 엄격하게 거부할 부분을 나눈다.
- 합성된 강도(intensity)처럼 일부 구간만 이상한 경우에는, 그 구간만 제외하고 진행한다. 단 하나의 결함 때문에 분석 전체를 버리지 않는다.
- 선택된 그룹이 없거나 신뢰도(confidence)가 깨지는 등 핵심적인 결함이 있는 경우에는 통째로 반려한다. 핵심이 없다면 통과시키지 않는다.
"어디까지 허용하고, 어디서부터 거부할 것인가". 이 선을 긋는 작업이 생성 AI를 실운용(production)에 올릴 때의 핵심이다.
중계(relay) 역시 공개 포트(open port)를 열지 않고 처리했다. 앱으로부터의 분석 요청은 데이터베이스에 한 줄을 쌓는 것뿐이다. VPS의 워커(worker)가 외부 연결을 통해 이를 가져가고, 결과를 다시 써넣는다. VPS 측은 수신 포트를 일절 열지 않는다. 이해(understanding)는 외부 모델에 맡기더라도, 경로는 수중에 둔다.
판단을 결정적인 엔진에 맡긴 이유는, 누가 사용하더라도 같은 답에 도달하게 하고, 그 근거를 나중에 원전(source)까지 추적할 수 있도록 하기 위해서다. AI에 맡긴 것은 언어의 판독뿐이다. 주행 능력의 추정이나 처방된 페이스도 그 사람의 데이터 범위를 벗어나지 않으며, 학습(training)에도 사용하지 않는다.
직접 해보며 알게 된 것은, "AI를 사용하는 곳을 최소한으로 좁힌다"는 것이 말처럼 자동으로 결정되지 않는다는 사실이다. 처리 흐름을 한 번 전부 분해하여, 기존 방식대로 견고하게 설계해야 할 부분과 AI에게 맡겨도 좋은 부분을 하나씩 분리해야 한다. 그 판단이 가능해져야 비로소 AI는 최소한의 영역에 머물 수 있다. 게다가, 설계를 견고히 한 쪽도 방심할 수 없다. 원전과 대조하는 리뷰(review) 관점을 미리 정해두지 않으면, 올바르게 작성했다고 믿는 부분에 조용히 구멍이 뚫린다.
생성 AI에게 "당신은 다니엘입니다"라고 부탁하면 즉시 작동한다. 이번에 했던 작업은 그 편리함을 누가 사용하더라도 동일하게 효과를 발휘하는 형태로 재구성하는 작업이었을지도 모른다. 러닝에 국한된 이야기라면 아마 아닐 것이다.
- RunAdvisory (App Store에서 공개 중·무료)
- 소개 페이지: https://run-advisory-web.vercel.app
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기