-
Evaluations를 사용한 점진적 개선 방식으로 프롬프트 향상하기
프롬프트 엔지니어링을 안내하고 앱에 적합한 모델을 선택하는 비교 평가 기법을 알아보세요. 성능 기준치를 설정하고, 평가 전략을 확장하며, 결과를 JSON 형식으로 변환하여 다른 도구와 통합하는 방법을 살펴보세요. 다양한 프롬프팅 전략을 적용해야 하는 경우와 최상의 결과를 얻기 위해 프롬프트를 반복적으로 향상해 나가는 방법을 알아보세요.
챕터
- 0:00 - Introduction
- 2:42 - BookTracker's tagging problem
- 5:27 - Analyzing the evaluation results
- 8:26 - Drift between judge and human
- 9:37 - Measuring drift with Cohen's kappa
- 12:26 - Building a judge alignment evaluation
- 15:16 - Analyzing alignment failures
- 17:16 - Comparative evaluation: control vs experimental
- 19:12 - Refining the scoring dimensions
- 21:23 - Adding few-shot examples to the judge
- 23:38 - Going beyond prompts: adding a tool
- 27:17 - Next steps
리소스
-
비디오 검색…
안녕하세요! 저는 Marcus이며 Evaluations 프레임워크 팀의 매니저입니다 Evaluations를 사용하여 AI 기반 기능을 개선하는 방법을 소개하게 되어 기쁩니다 아마 알고 계시겠지만 앱에 AI를 활용하면 사용자에게 새로운 수준의 개인화 경험을 제공할 수 있습니다 이 기술은 앱에 깊이를 더해 줄 수 있는데 기존 소프트웨어로는 불가능했던 수준입니다 하지만 AI 기반 기능이 모든 상황에서 예상대로 동작하는지 파악하는 것은 어려운 과제이기도 합니다 이를 돕기 위해 Evaluations 프레임워크를 출시하여 자신 있게 출시할 수 있도록 필요한 도구를 제공합니다 자신 있게 출시하려면 프레임워크만으로는 충분하지 않습니다 Evaluations 프레임워크는 힐 클라이밍도 지원합니다 힐 클라이밍은 반복적으로 개선하는 프로세스로 평가 점수를 가이드로 삼아 기능의 품질을 높여 나갑니다 힐 클라이밍은 개발 단계에서 시작합니다 즉, 기존 기능과 비교하여 측정하려는 변경 사항을 적용합니다 모든 변경 사항이 완료되면 평가를 실행해야 합니다 그리고 결과가 기대치를 충족했는지 확인합니다 그런 다음 결과를 분석하여 기능을 더욱 개선할 수 있는 방법을 파악합니다 힐 클라이밍 프로세스를 활용하면 체계적으로 기능을 개선할 수 있는 좋은 방법이지만 효과적인 힐 클라이밍은 루프를 따르는 것 이상이 필요합니다 약간의… 과학적 사고도 필요합니다! 이 영상에서는 힐 클라이밍 루프를 따라 프롬프트를 개선하는 방법을 단계별로 안내해 드리겠습니다 과학적 사고를 함께 적용하면서 진행하겠습니다 다음으로, 비교 Evaluations를 수행하여 힐 클라이밍 프로세스를 더욱 쉽게 만드는 방법을 안내해 드리겠습니다 마지막으로, 프롬프트 변경을 넘어 AI 기반 기능의 다른 측면도 개선해 보겠습니다 더 진행하기 전에 이 영상은 기존 평가를 통해 힐 클라이밍하는 프로세스를 다룹니다 즉, 이미 평가 파이프라인의 기반을 작성했다는 의미로 이를 통해 AI 기반 기능의 강점과 약점을 전체적으로 이해할 수 있습니다 방법을 잘 모르신다면 다른 영상을 확인해 보세요 "Meet the Evaluations framework"입니다 훌륭한 평가 파이프라인을 구축하는 데 필요한 모든 내용을 다루고 있습니다 이제 시작해 보겠습니다 "Meet the Evaluations framework" 영상에서 Book Tracker를 소개해 드렸습니다 혹시 잊으셨다면, Book Tracker는 독자들이 책을 목록화하고 리뷰할 수 있는 앱입니다 최근 고전 문학을 많이 읽었는데 카탈로그에 책들을 추가해 두었습니다 사실 방금 "Treasure Island"를 다 읽었습니다!
정말 많은 생각을 하게 만드는 책으로 충성과 배신 사이의 긴장감을 다루고 있습니다 Book Tracker의 새로운 기능 중 하나는 태깅 서비스입니다 독자의 리뷰를 기반으로 모델이 태그를 생성합니다 이 리뷰에 생성된 태그는 책의 일반적인 주제를 다루고 있지만 뭔가 부족한 것 같습니다 "tense"나 "morally grey" 같은 태그도 있을 것으로 기대했는데 이야기의 주제를 잘 나타내는 태그입니다 "Little Women"에 생성된 태그도 비슷한 문제가 있었습니다 poignant 같은 태그는 독자가 느낀 감정에 더 가깝고 책의 내용과는 거리가 있습니다 책 리뷰의 감정 표현은 좋지만 태그 목록에는 포함되지 않는 것이 좋습니다 또한 quiet-steadiness 같은 태그는 리뷰에서 직접 가져온 표현으로 나중에 라이브러리를 검색할 때 유용한 태그가 되기 어렵습니다 Book Tracker의 태그 생성기가 기대만큼 좋지 않은 것 같습니다 다행히도 동료들이 이 기능을 작성할 때 Evaluation을 작성해 두었고 태그를 기준에 따라 측정합니다 Book Tracker 도서 태그에 대한 Evaluation이 여기 있습니다 태그의 품질을 어떻게 판단하는지 특히 관심이 있어서 스크롤을 내려 살펴보겠습니다 앱의 정성적 측면은 score dimensions 유형으로 캡처됩니다 관련성은 태그가 책의 줄거리에 관한 정보를 얼마나 잘 나타내는지를 추적합니다 책의 줄거리 주제 또는 기타 관련 정보를 얼마나 잘 반영하는지 측정합니다 유용성은 태그가 검색어로서 얼마나 좋은지를 측정합니다
ModelJudgeEvaluator는 score dimensions를 사용하여 프롬프트와 함께 각 태그 세트에 대한 점수를 생성합니다 두 권의 책을 Evaluation에 추가할 계획이며 반환된 태그를 검토하겠습니다 또한 내 평가와 모델 심사위원의 평가를 비교해 볼 좋은 기회가 될 것입니다 모델 심사위원의 평점과 비교해 보겠습니다 AI 기반 기능의 일부를 개선하고 싶다는 생각이 힐 클라이밍 프로세스의 시작입니다 루프를 시작하려면 개발 단계에서 시작합니다 여기서 기능과 Evaluation에 필요한 모든 변경 사항을 적용합니다 이 경우 "Treasure Island" 리뷰를 Evaluation 데이터셋에 추가하겠습니다 "Little Women"도 동일하게 추가했습니다 이 두 항목이 데이터셋에 추가되었으니 이제 Evaluation을 실행하여 모델이 생성하는 태그를 확인하고 심사위원이 어떻게 점수를 매기는지 보겠습니다 하지만 그 전에 실행한 Evaluation이 기대를 충족했는지 먼저 확인해야 합니다 참고로 Swift Testing의 expect 매크로를 사용하여 기대치를 정의할 수 있습니다 이렇게 하면 테스트 통과 여부로 기대치가 충족되었는지 확인할 수 있습니다
이 경우, Evaluation은 모든 기대치를 충족했습니다 하지만 태그가 기대만큼 좋지 않다는 것을 알기 때문에 더 자세히 조사해야 합니다 이제 분석 단계로 넘어갑니다 Xcode의 새로운 evaluations 리포트는 마지막 Evaluation 실행에 대한 심층 정보를 제공합니다 자세한 내용을 보려면 BookTaggingEvaluation 실행을 클릭하면 됩니다 Evaluation 세부 정보 보기가 표시됩니다 상단에는 집계 지표 차트가 있습니다 그 아래에는 결과 테이블이 있습니다
이제 모델의 응답과 이전에 생성한 예상 태그 목록을 비교해 보겠습니다 Assistant Editor를 열어서 확인할 수 있습니다 이제 자세한 정보를 볼 수 있습니다 데이터셋의 각 항목에 대해 생성된 태그 정보입니다 모델이 생성한 것과 제가 기대했던 것의 차이에 집중하겠습니다 이 테이블에서 자세히 검토할 수 있습니다 용어 모음 자체는 나쁘지 않지만 여러 핵심 세부 사항이 빠져 있습니다 사용자가 검색하고 싶을 수 있는 이야기의 세부 사항들입니다 그래서 저는 이 태그들에 관련성 점수 4점을 줬을 것이고 유용성은 2점을 줬을 것입니다 모델 심사위원도 태그에 관련성 점수 4점을 줬는데 이는 좋습니다 하지만 유용성에도 4점을 줬는데 이는 맞지 않습니다 "Little Women" 리뷰의 태그에 대해서도 같은 생각인지 확인해야겠습니다 태그에 제가 기대했던 유용한 정보가 모두 포함되어 있지 않습니다 심사위원의 점수에 대해서도 마찬가지로 동의하지 않습니다 다시 말씀드리지만 관련성은 4점이 맞고 유용성은 2점이어야 합니다 이 분석을 통해 모델 심사위원과 제가 태그를 평가하는 방식에 차이가 있다는 것이 명확해졌습니다 모델과 인간 사이의 이 불일치를 드리프트라고 합니다 이는 모든 개발자가 직면하는 문제입니다 지능형 기능을 평가하려는 모든 개발자가 겪는 문제입니다 이유를 설명하겠습니다 10개의 샘플로 구성된 평가가 있다고 가정해 봅시다 모델 심사위원과 사람에게 각 샘플을 평가해 달라고 요청합니다 그러면 모델과 사람이 1에서 4점 척도로 평가하고 마지막에 점수를 평균하여 집계를 구성합니다 모델과 인간이 평가에서 자주 의견이 다르다면 평균 점수가 서로 달라집니다 이것이 드리프트라는 이름의 유래입니다 데이터셋이 계속해서 커질수록 드리프트는 점점 더 커집니다 그 시점에서는 기능이 제대로 평가되고 있는지 알기 어려워집니다 이를 해결하려면 심사위원을 전문가의 의견에 맞게 조정할 수 있습니다 드리프트가 문제라는 것을 알았으니 모델 심사위원이 전문가 평가에서 얼마나 벗어났는지 파악하는 방법이 필요합니다 이를 달성하는 한 가지 방법은 전문가의 평가를 나란히 놓고 두 평가가 일치하는 곳을 표시하는 것입니다 이를 사용하여 비율을 계산할 수 있습니다 이 비율을 정확도라고 합니다 점수 척도의 모든 값이 동등하게 나타날 가능성이 있다면 일치도를 측정하는 좋은 방법입니다 하지만 데이터셋에는 점수의 분포가 고르지 않을 가능성이 더 높습니다 즉, 점수 분포가 고르지 않을 수 있습니다 생각해 보세요, 데이터셋에는 보통 고품질 출력의 예시가 포함됩니다 따라서 인간 평가자가 데이터셋의 항목에 더 높은 점수를 줄 가능성이 높습니다 그러면 모델이 더 작은 데이터셋을 높은 점수로 판단할 때 두 평가가 일치하는 것처럼 보일 수 있습니다 하지만 더 다양한 점수를 가진 더 큰 데이터셋에 적용할 때 높게 점수를 주려는 경향이 여전히 드리프트를 초래합니다 따라서 정확도에 대한 대안이 필요합니다 데이터셋의 가중치 특성과 모델이 정답을 맞출 가능성을 모두 고려하는 방법입니다 다행히 해결책이 있습니다!
코헨의 카파 계수는 수학적 공식으로 1960년 통계학자이자 심리학자인 Jacob Cohen이 대중화했습니다 코헨의 카파는 일치도를 측정합니다 즉, 두 평가자가 얼마나 자주 동의하는지입니다 이를 위해 평가자들이 동의한 비율을 알아야 합니다 이것이 바로 정확도입니다 앞서 말한 정확도 지표가 계산하는 것이 바로 이것입니다 하지만 이제 새로운 값을 계산해야 합니다 우연의 일치, 즉 한 평가자가 운이 좋아서 우연히 일치할 가능성을 나타냅니다 이 운은 특정 답변이 나타날 가능성에 따라 가중치가 부여됩니다 그렇다면 어떻게 계산하는지 알아보겠습니다 일치도를 계산하려면 정확도 점수에서 시작합니다 정확도 점수에서 두 평가자가 무작위로 동의할 가능성을 뺍니다 마지막으로, 그 차이를 무작위 동의의 역수로 나눕니다 즉, 두 평가자가 의도적으로 동의할 가능성입니다 그 결과가 일치도입니다 코헨의 카파는 일치도를 측정하는 강력한 방법으로 모델 심사위원과 전문가 의견 사이의 일치도를 측정합니다 이를 사용하여 나와 모델 심사위원 사이의 일치도 점수를 힐 클라이밍할 수 있습니다 이제 힐 클라이밍 루프의 처음으로 돌아가서 개발 단계에서 시작합니다 이를 위해 평가를 설정하여 내 평가와 심사위원을 비교하고 일치도 점수를 생성하겠습니다 이를 위해 평가를 작성해야 하는데 네 가지 구성 요소로 이루어져 있습니다 첫 번째는 데이터셋입니다 다음은 평가의 대상입니다 그런 다음 평가자를 정의해야 합니다 마지막으로 결과를 집계해야 합니다 데이터셋부터 시작해 보겠습니다 이 평가가 제대로 작동하려면 모델 심사위원과 저 모두 동일한 데이터셋을 평가해야 합니다 이 경우 모델 심사위원은 태그를 검토하므로 심사위원과 제가 검토할 공통 태그 세트를 생성해야 합니다 심사위원과 함께 검토할 태그입니다 딱 맞는 데이터셋이 있습니다 이전 평가에 리뷰와 태그 컬렉션이 포함되어 있습니다 이 평가를 테스트에서 실행했기 때문에 Xcode가 첨부 파일을 생성했습니다 생성된 모든 평가 데이터가 포함되어 있습니다 해당 첨부 파일을 가져와 요약과 태그 쌍을 추출할 수 있습니다 요약과 태그 쌍을 추출했으니 이제 내 평가를 추가해야 합니다 그런 다음 이 파일의 내용을 평가의 입력으로 전달할 수 있습니다 다음으로 평가의 대상을 캡처해야 합니다 일반적으로 subject 메서드는 API 호출을 위한 것이지만 기능과 관련된 API 호출입니다 생성된 모델 응답이 데이터셋의 일부이기 때문에 이미 생성된 태그를 단순히 반환할 수 있습니다 이제 평가자를 정의해야 합니다 짐작하셨겠지만 평가자는 책 태그 평가에서와 동일한 모델 심사위원 평가자입니다 도서 태그 평가에서와 동일합니다 여기서 심사위원이 평가를 제공합니다 마지막으로 결과를 집계해야 합니다 여기서 내 평가와 심사위원의 평가를 비교합니다 이를 위해 코헨의 카파를 계산해야 합니다 사용자 정의 집계 메서드로 계산할 수 있습니다 코헨의 카파 외에도 평균도 계산하겠습니다 그리고 각 점수 차원의 표준 편차도 계산합니다 심사위원 점수가 오르는지 내려가는지 파악하는 데 도움이 됩니다 이제 평가와 함께 테스트를 설정할 수 있습니다 이 테스트에서 기대치를 설정했습니다 내 평가와 심사위원의 평가가 일치도 점수 0.6을 생성해야 한다는 것입니다 통계학자에 따르면 이 숫자를 선택한 이유는 일치도 점수 0.6이 의미 있는 수준의 동의를 나타내기 때문입니다 이제 평가를 실행하고 일치도의 기준선을 파악할 시간입니다 그런 다음 평가가 기대치를 충족했는지 확인합니다 테스트가 실패한 것 같습니다 기대치가 충족되지 않았다는 의미입니다 따라서 다시 결과를 자세히 분석할 시간입니다 일치도 점수가 기대치와 일치하지 않는다는 것을 알았습니다 더 많은 정보를 얻기 위해 평가 리포트로 이동할 수 있습니다 예상대로 유용성과 관련성 모두에 대한 점수가 꽤 낮습니다 모델 심사위원과 제가 일치하지 않는다는 의미입니다 이제 더 많은 정보를 얻고 싶습니다 데이터셋의 각 샘플이 어떻게 수행되었는지에 대한 정보입니다 이를 위해 assistant를 열어야 합니다 결과를 자세히 살펴보겠습니다 결과를 스캔하다가 "Frankenstein" 리뷰가 눈에 띄었습니다 내 태그 평가와 심사위원의 평가 사이에 꽤 큰 차이가 있습니다 심사위원의 평가입니다 심사위원은 self-help 같은 태그가 self-improvement와 함께 이야기와 관련이 있다고 생각하는 것 같습니다 psychological도 괜찮은 검색어이지만 사용자가 실제로 검색할 가능성은 낮은 용어입니다 그런 다음 데이터셋의 다른 항목들을 살펴보기 시작했습니다 비슷한 문제가 있는 항목들을 살펴보다가 "The Ramakien" 리뷰를 발견했습니다 심사위원과 저는 이 용어 모음이 도움이 되고 책의 내용과 관련이 있다는 데 동의합니다 우리가 동의하지 않는 것은 유용성입니다 visual-dimension과 quaint-dignity 같은 용어는 너무 구체적입니다 그렇다면 여기서 문제가 무엇일까요?
모델이 자체적으로 충분한 지식을 갖추고 있지 않아서 좋은 태그와 나쁜 태그를 구별하지 못하는 것 같습니다 심사위원 프롬프트가 충분한 맥락을 제공하지 않기 때문일 것입니다 이를 위해 새로운 프롬프트를 개발해야 합니다 그러면 현재 프롬프트의 일치도 점수를 새 프롬프트의 점수와 비교할 수 있습니다 다행히 Xcode 27에서는 두 평가의 결과를 서로 비교할 수 있게 되었습니다 비교할 때 과학적 사고가 큰 도움이 됩니다 과학 실험에는 두 그룹이 있습니다 기준선을 나타내는 대조군과 실험군입니다 실험군은 비교하려는 변경 사항을 나타냅니다 두 버전의 지침을 같은 방식으로 생각할 수 있습니다 대조군은 기본 프롬프트로 표현되고 실험군은 새롭게 변경된 프롬프트로 표현됩니다 이제 실험적 프롬프트로 평가의 두 번째 버전을 만들어야 합니다 실험적 프롬프트와 함께입니다 기준선으로는 이전과 동일한 모델 심사위원 프롬프트로 동일한 평가를 사용합니다 실험적 프롬프트로는 더 자세한 설명을 작성했습니다 태그 세트를 어떻게 판단해야 하는지에 대한 더 자세한 설명입니다 앱에 대한 맥락을 심사위원에게 제공하는 것으로 시작합니다 판단하려는 내용에 대한 맥락입니다 그런 다음 좋은 태그의 예를 제공합니다 나쁜 태그를 식별하는 방법도 제공합니다 두 프롬프트를 작성했으니 두 평가를 테스트 스위트에 추가할 수 있습니다 두 평가를 모두 실행합니다 이제 스위트를 실행하고 결과를 비교해 보겠습니다 평가가 완료되었으니 평가 리포트로 돌아갈 수 있습니다 관련성에 대한 일치도 점수가 개선된 것 같습니다 유용성에 대한 일치도 점수는 상당히 떨어졌습니다 이런 트레이드오프의 균형을 맞추는 것은 까다롭습니다 어떻게 진행할지 신중하게 생각해야 합니다 하지만 심층 분석 전에 통과 여부를 확인해야 합니다 테스트에서 확인했듯이 통과하지 못했습니다 더 생각해 본 후 이 프롬프트 변경을 유지하기로 했습니다 다음 반복 라운드에서는 유용성 점수 개선에 집중하겠습니다 따라서 결과를 검토하는 가장 효과적인 방법은 두 심사위원의 유용성 점수를 서로 비교하는 것입니다 이를 위해 평가 리포트의 새 비교 보기를 사용할 수 있습니다 평가 리포트에서 비교 버튼을 열고 기준선 평가를 열 수 있습니다 여기서 두 프롬프트의 점수를 나란히 검토할 수 있습니다 즉시 눈에 띈 것 중 하나는 유용성 점수 사이의 차이였습니다 "Picture of Dorian Gray" 리뷰에서의 유용성 점수 차이입니다 모델이 유용성에 대해 너무 가혹하게 판단하는 것 같습니다 실험적 평가의 유용성 열이 내 추측을 뒷받침하는 것 같습니다 모든 점수가 3 또는 2라는 것을 알아챘는데 이는 너무 가혹합니다 여기서 도움이 될 수 있는 것은 각 점수 차원을 어떻게 평가할지에 대해 더 구체적으로 명시하는 것입니다 이를 위해 실험적 평가에 몇 가지 변경 사항을 적용해야 합니다 실험적 평가를 변경하기 전에 새 프롬프트를 적용했습니다 실험적 평가에서 가져온 새 프롬프트를 기준선에 적용했습니다 이렇게 하면 변수가 하나만 달라집니다 즉, 점수 차원의 변경 사항만 다릅니다 관련성에 대해서는 장르 태그의 필요성을 강조하는 약간 더 긴 설명을 제공했습니다 유용성에 대한 설명도 있습니다 지나치게 구체적인 태그에 대해 더 비판적으로 보도록 강조합니다 다시 한 번 평가가 실행되기를 기다리겠습니다 두 점수 모두 기준선에 비해 크게 향상되었습니다 이 구체적인 점수 차원들이 훨씬 더 도움이 될 것 같습니다 하지만 아직 일치도 목표에 완전히 도달하지 못했습니다 이제 또 다른 비교를 해야 합니다 어디서 개선이 가능한지 확인해야 합니다 더 자세히 분석하기 위해 실험적 평가로 돌아갔습니다 결과를 자세히 검토하고 싶어서 assistant 보기를 열겠습니다 결과를 살펴보다가 "Moby Dick" 리뷰에 도달했습니다 관련성 점수가 일치하기 시작했습니다 하지만 유용성 점수는 아직 개선이 필요합니다 일부 결과는 유망해 보이지만 다른 것들은 여전히 많이 벗어나 있습니다 "Frankenstein" 리뷰는 여전히 심사위원에게 어려움을 주고 있습니다 심사위원에게 지금 필요한 것은 제가 판단하는 방식의 예시입니다 내 기준에 따라 판단하는 패턴을 제공해야 합니다 힐 클라이밍의 또 다른 라운드가 필요합니다 이미 기준선 평가에 새로운 점수 차원을 추가했습니다 이제 주요 심사위원 프롬프트를 수정했습니다 목표에 대한 더 자세한 정보를 제공하기 위해 태그 생성 기능의 목표에 대한 자세한 정보입니다 모델이 문제 영역에 집중할 수 있도록 돕습니다 그런 다음 여러 예시를 작성했습니다 모델이 검토 지침으로 사용할 예시들입니다 모델에게 몇 가지 예시만 제공하도록 했습니다 더 긴 목록을 제공하면 일치도 점수를 과적합시킬 위험이 있습니다 그러면 심사위원이 실제로 저와 일치하는지 파악하기 어려워집니다 이제 공정한 비교이므로 평가를 실행하고 결과를 확인해야 합니다 드디어 점수가 기대값을 넘었습니다! 드디어 통과했으며 루프에서 나올 수 있습니다! 이제 모델 심사위원이 평가를 제공할 때 자신 있게 말할 수 있습니다 내 기준에 따라 태그가 좋은지 나쁜지 자신 있게 말할 수 있습니다 이제 심사위원을 활용하여 Book Tracker의 Book Tagging Service를 평가할 수 있습니다 지금까지 프롬프트에 대해 힐 클라이밍하는 방법을 살펴봤습니다 점진적으로 더 나아지게 만드는 방법입니다 이제 프롬프트 이외의 방법으로 기능을 개선하는 방법을 보여드리겠습니다 태그를 생성하기 위해 Book Tracker는 온디바이스 모델을 사용합니다 독자들은 책을 카탈로그할 때 다양한 장소에 있을 수 있기 때문에 온디바이스 모델을 사용하면 어디에 있든 태그를 생성할 수 있습니다 모델에게 태그를 생성하는 책에 대한 더 많은 맥락을 제공하고 싶습니다 태그 생성 대상 책에 대한 맥락입니다 추가 맥락이 모델이 더 관련성 높은 태그를 생성하는 데 도움이 될 것이라고 생각합니다 유용한 태그를 생성합니다 더 좋은 점은 Book Tracker에는 이미 필요한 데이터가 있다는 것입니다 리뷰를 작성할 때 저자 이름을 저장하기 때문입니다 그리고 리뷰를 작성할 때 책 제목도 저장합니다 따라서 태그 생성기를 돕기 위해 책에 대한 추가 정보를 가져오는 도구를 만들었습니다 사용 가능한 경우 책 제목과 저자를 제공합니다 이 도구를 추가하는 것은 힐 클라이밍의 한 형태입니다 증분적 변경을 통해 기능의 품질을 향상시키려고 하기 때문입니다 점진적인 변경을 통해 품질을 향상시킵니다 이 평가에서는 도서 태깅 Evaluation을 사용하겠습니다 이제 개선된 모델 심사위원이 있습니다 하지만 도구 없이 기능의 품질과 도구 있는 기능의 품질을 비교하는 방법이 필요합니다 이를 위해 Book Tagging Service에 변경 사항을 만들어야 합니다 BookTaggingService는 이제 도구 목록을 입력으로 받습니다 기본값을 빈 배열로 설정했습니다 따라서 기존 평가는 변경이 필요 없습니다 하지만 이제 새 평가를 작성해야 합니다 도구 있는 서비스와 도구 없는 서비스를 비교하기 위해서입니다 새로 작성한 평가입니다 다른 평가와 완전히 동일합니다 유일한 차이점은 이제 tools 배열에 새 검색 도구를 전달한다는 것입니다 따라서 평가의 두 인스턴스를 정의하기만 하면 됩니다 하나는 도구 없이 하나는 도구와 함께입니다 이제 평가를 실행하고 출시 준비가 되었는지 확인해 보겠습니다 도구를 사용하는 서비스가 모든 기대치를 충족했습니다 결과가 좋아 보입니다 하지만 Book Tracker의 데이터셋에는 책과 리뷰 쌍이 13개밖에 없습니다 이는 사용자가 태깅을 위해 제출할 다양한 책과 리뷰를 다루지 못합니다 사용자가 제출할 수 있는 다양한 리뷰를 다루지 못합니다 또한 도구를 사용한 서비스 평가 결과를 살펴보다가 도구를 사용한 서비스입니다 도구를 사용한 서비스가 더 잘 수행되고 있지만 도구가 모든 곳에서 필요한 곳에서 호출되지 않는 것 같습니다 도구가 올바른 상황에서 호출되었는지 여부를 확인하는 방법이 필요합니다 다행히 Evaluations 프레임워크가 두 문제 모두 해결하는 데 도움이 됩니다 도구 사용 평가와 관련된 API에 대해 더 알아보려면 포괄적인 데이터셋 생성에 대해서도요 "Create robust evaluations for agentic apps" 영상을 확인하세요 거기서 tool call 평가자에 대해 배울 수 있습니다 Sample Generator API 사용 방법과 앱이 접할 수 있는 다양한 사용 사례를 테스트하는 방법도 알 수 있습니다 마치기 전에 오늘 다룬 내용을 요약하겠습니다 힐 클라이밍은 한 번에 하나의 변경 사항에 집중할 때 가장 효과적입니다 이를 위해 루프의 모든 반복을 과학 실험처럼 다루세요 변경 사항을 격리할 수 있다면 각 부분이 전체 품질에 어떻게 기여하는지 이해하는 데 도움이 됩니다 각 부분이 개별적으로 작동하는 방식을 알면 나중에 버그나 원치 않는 패턴을 해결하기 위해 어디를 변경해야 하는지도 더 잘 파악할 수 있습니다
두 번째로, 이 프로세스에는 시간이 걸립니다 모든 변경 사항이 긍정적인 결과를 가져오지는 않습니다 하지만 실패한 실험도 성공한 실험만큼 많은 것을 알려줍니다 세 번째로, 좋은 실험에는 창의성이 필요합니다 지능형 기능에는 변경할 수 있는 것들이 매우 많습니다 기능에서 변경할 수 있는 것으로는 지침 도구 그리고 응답 생성에 사용하는 모델이나 모델들이 있습니다 평가 측면에서는 데이터셋을 변경할 수 있습니다 집계 방법 심지어 평가자 자체도 변경할 수 있습니다 모든 것이 가능합니다 힐 클라이밍 방법을 생각할 때 이 모든 것들을 고려하세요 마지막으로, 드리프트에 주의하세요 평가자를 평가하는 것이 다소 메타적으로 느껴질 수 있지만 잘 조정된 모델 평가자는 장기적으로 시간을 절약해 줍니다 모델은 인간보다 훨씬 빠르게 평가를 생성할 수 있습니다 따라서 일치 상태를 유지하면 데이터셋이 점점 더 많은 사용 사례를 커버하도록 성장할수록 유용한 신호를 얻을 수 있습니다 오늘 다룬 내용에 대해 더 알고 싶다면 사용한 Book Tracker 앱을 검토하거나 모델 심사위원 맞춤 조정을 위한 평가도 확인해 보세요 개발자 문서 웹사이트에서 새로운 모든 API에 대한 포괄적인 설명을 볼 수 있습니다 힐 클라이밍을 통해 평가 점수를 개선하는 방법을 배우는 데 시간을 내주셔서 감사합니다 여러분의 노력은 사용자에게 고품질 경험을 제공하면서 보답받을 것입니다 시청해 주셔서 감사합니다 즐거운 힐 클라이밍 되세요!
-
-
3:54 - The BookTaggingEvaluation
// MARK: - Evaluation struct BookTaggingEvaluation: Evaluation { func subject(from sample: ModelSample<BookTags>) async throws -> ModelSubject<BookTags> { let result = try await BookTaggingService.generateTags(for: sample.promptDescription) return ModelSubject(value: result) } // MARK: - Dataset var dataset = ArrayLoader(samples: Book.sampleBooks.map { book in ModelSample(prompt: book.review, expected: BookTags(tags: book.tags)) } ) // MARK: - Evaluators & Metrics var tagCount = Metric("Tag Count") let hasGenreTag = Metric("Has Genre Tag") let noDuplicates = Metric("No Duplicates") let relevance = ScoreDimension( "Relevance", description: """ Whether each tag describes a quality, theme, or tone of the book itself rather than incidental details or the reader's personal reactions. """, scale: .numeric([ 4: "Every tag describes the book itself", 3: "Most tags describe the book, one picks up a reader reaction or minor detail", 2: "Most tags are surface details or personal reactions, not book descriptors", 1: "Tags don't meaningfully describe the book" ]) ) let usefulness = ScoreDimension( "Usefulness", description: """ Whether tags are at the right granularity for browsing — broad enough that multiple books could share the tag, specific enough to help filter. """, scale: .numeric([ 4: "Every tag could group multiple books while still narrowing a search", 3: "Most tags are at the right level, one is either too broad or too narrow", 2: "Most tags are too broad to filter or too narrow to group", 1: "Tags would not help with browsing" ]) ) var evaluators: Evaluators { // 1. Tag count is within the required 3–8 range Evaluator { _, subject in let count = subject.value.tags.count if (count >= 3 && count <= 8) { return tagCount.passing(rationale: "\(count) tags") } return tagCount.failing(rationale: "Got \(count) tags, expected 3–8") } // 2. At least one tag identifies the genre or literary form Evaluator { _, subject in let tags = subject.value.tags.map { $0.lowercased() } let knownGenres = await BookTaggingService.knownGenres for tag in tags { if knownGenres.contains(tag) { return hasGenreTag.passing(rationale: "Matched \(tag)") } } return hasGenreTag.failing() } // 3. No duplicate tags Evaluator { _, subject in let uniqueCount = Set(subject.value.tags.map { $0.lowercased() }).count if (subject.value.tags.count - uniqueCount) > 0 { return noDuplicates.failing(rationale: "Found \(subject.value.tags.count - uniqueCount) duplicates") } return noDuplicates.passing() } // 4. Overall tag quality — groundedness, coverage, specificity ModelJudgeEvaluator( judge: .default, dimensions: [relevance, usefulness], prompt: ModelJudgePrompt( instructions: """ You are evaluating automatically generated tags for Shelf, a personal book tracking app. Users write a short summary of their reading experience, and the app generates tags to make their library browsable. A good tag describes the book itself — its genre, themes, tone, or setting. A bad tag picks up incidental details or the reader's personal reactions that don't describe the book. """, evaluationTarget: { output in output.tags.joined(separator: ", ") }, reference: { input, _ in ["Expected Tags": input.expected?.tags.joined(separator: ", ") ?? ""] } ) ) } // MARK: - Analysis func aggregateMetrics(using aggregator: inout MetricsAggregator) { aggregator.group("Heuristics") { group in group.computeMean(of: tagCount) group.computeMean(of: hasGenreTag) group.computeMean(of: noDuplicates) } aggregator.group("Quality") { group in group.computeMean(of: relevance.metric) group.computeMean(of: usefulness.metric) } } } -
4:05 - Refined Relevance & Usefulness score dimensions
let relevance = ScoreDimension( "Relevance", description: """ Whether each tag describes the book itself — its genre, themes, tone, or setting — rather than the reader's reactions, meta- commentary about the review, or facts about the author. A book can be "suspenseful" (a property of the text); a reader is "exhausted" (a reaction). Mis-labeling the genre is a serious failure. """, scale: .numeric([ 4: "Every tag describes the book itself", 3: "Most tags describe the book, one picks up a reader reaction or minor detail", 2: "Most tags are surface details or personal reactions, not book descriptors", 1: "Tags don't meaningfully describe the book" ]) ) let usefulness = ScoreDimension( "Usefulness", description: """ Whether tags work as library shelf labels — broad enough that several books could plausibly share the tag, specific enough to meaningfully narrow a search. Standard genre and theme tags work; made-up phrases, character names, hyper-specific descriptors, and overly generic words like "interesting" don't. """, scale: .numeric([ 4: "Every tag could group multiple books while still narrowing a search", 3: "Most tags are at the right level, one is either too broad or too narrow", 2: "Most tags are too broad to filter or too narrow to group", 1: "Tags would not help with browsing" ]) ) -
11:56 - The alignment dataset, extracted to JSON
// Model judge alignment dataset [ { "input": "I have read this book more times than I can count…", "response": "[\"literary-fiction\", \"historical-fiction\", \"family-drama\", \"romantic-drama\", \"character-driven\", \"emotional-intensity\", \"multigenerational-narrative\", \"penned-by-a-woman\"]" } // ... add your expert ratings to each entry ] -
12:31 - The judge alignment evaluation: dataset, subject, evaluator
// Model judge alignment evaluation struct BookTagJudgmentCalibration: Evaluation { // MARK: Dataset — load the extracted summary/tag pairs static let samples: [ModelSample<BookTagJudgmentValue>] = { guard let url = Bundle(for: BundleToken.self).url( forResource: "BookTaggingEvaluation-extracted", withExtension: "json"), let data = try? Data(contentsOf: url) else { return [] } // Build ModelSample array (adding expert ratings) // ... }() var dataset: some Loader { ArrayLoader(samples: Self.samples) } // MARK: Capture Subject — tags are already generated, so just return them func subject(from sample: ModelSample<BookTagJudgmentValue>) async throws -> ModelSubject<BookTagJudgmentValue> { ModelSubject(value: sample.expected ?? BookTagJudgmentValue( tags: [], expertRelevanceScore: 0, expertUsefulnessScore: 0)) } // MARK: Evaluators — the same model judge as the book-tags evaluation var evaluators: Evaluators { ModelJudgeEvaluator( judge: .default, dimensions: [relevance, usefulness], prompt: ModelJudgePrompt( instructions: "You are evaluating automatically generated tags for Book Tracker…", evaluationTarget: { output in output.tags.joined(separator: ", ") }, reference: { input, _ in ["Expected Tags": input.expected?.tags.joined(separator: ", ") ?? ""] } ) ) } } -
13:00 - Cohen's kappa aggregation
func aggregateMetrics(using aggregator: inout MetricsAggregator) { let expertRelevance = Self.samples.map { Double($0.expected?.expertRelevanceScore ?? 0) } let expertUsefulness = Self.samples.map { Double($0.expected?.expertUsefulnessScore ?? 0) } aggregator.group("Relevance") { group in group.computeMean(of: relevance.metric) group.computeStandardDeviation(of: relevance.metric) group.custom(of: relevance.metric, label: "Relevance Alignment Score") { judge in cohensKappa(ratings1: expertRelevance, ratings2: judge) ?? 0 } } aggregator.group("Usefulness") { group in group.computeMean(of: usefulness.metric) group.computeStandardDeviation(of: usefulness.metric) group.custom(of: usefulness.metric, label: "Usefulness Alignment Score") { judge in cohensKappa(ratings1: expertUsefulness, ratings2: judge) ?? 0 } } } -
13:24 - The judge calibration test
// Model judge alignment tests @Suite("Book Tag Judge Calibration") struct BookTagJudgmentCalibrationTests { static let evaluation = BookTagJudgmentCalibration() @Test("Judge Calibration", .evaluates(evaluation)) func evaluateJudgeCalibration() async throws { let result = EvaluationContext.current.result let usefulnessMetric = BookTagJudgmentCalibrationTests.evaluation.usefulness.metric let relevanceMetric = BookTagJudgmentCalibrationTests.evaluation.relevance.metric #expect(result.aggregateValue(.custom(label: "Relevance: Judge vs Expert")) > 0.6) #expect(result.aggregateValue(.custom(label: "Usefulness: Judge vs Expert")) > 0.6) } } -
16:33 - The experimental judge prompt
// Experimental evaluation struct BookTagJudgmentCalibrationExperimental: Evaluation { var evaluators: Evaluators { ModelJudgeEvaluator( judge: .default, dimensions: [relevance, usefulness], prompt: ModelJudgePrompt( instructions: """ You are an experienced reader and librarian evaluating tags automatically generated for Book Tracker... Score the tag set on two independent dimensions: Relevance and Usefulness. ## What a good tag looks like - Genre/form, theme/subject, tone/atmosphere, setting/era ## Common failure modes - Reader reactions, meta-commentary, author facts, genre contradictions """, // ← full prompt is ~40 lines; abbreviated here evaluationTarget: { output in output.tags.joined(separator: ", ") }, reference: { input, _ in ["Book Review": input.promptDescription, "Tags Generated for the Review": input.expected?.tags.joined(separator: ", ") ?? ""] } ) ) } } -
20:12 - Few-shot worked examples in the judge prompt
struct ExperimentalBookTagJudgmentCalibration: Evaluation { var evaluators: Evaluators { ModelJudgeEvaluator( judge: SystemLanguageModel(), dimensions: [relevance, usefulness], prompt: ModelJudgePrompt( instructions: """ You are calibrating with an expert librarian who scores automatically generated tags for Book Tracker... Your goal is to match how the librarian scores. Use the worked examples to calibrate. ## Worked examples ### Example A — clean fit (Pride and Prejudice) Tags: romance, historical-fiction, love, redemption, passion Librarian: Relevance 4, Usefulness 4 ### Example E — flat genre contradiction (Frankenstein) Tags: horror, science-fiction, ... self-help, self-improvement Librarian: Relevance 2, Usefulness 3 ... (6 examples A–F; keep the set small to avoid overfitting) """, // ← full prompt is ~60 lines; abbreviated here evaluationTarget: { output in output.tags.joined(separator: ", ") }, reference: { input, _ in ["Book Review": input.promptDescription, "Tags Generated for the Review": input.expected?.tags.joined(separator: ", ") ?? ""] } ) ) } } 9. The BookLookupTool — slides 166–167 -
22:03 - The BookLookupTool
// Book Information Lookup Tool struct BookLookupTool: Tool { let name = "lookupBook" let description = "Looks up the title and author of a book given distinguishing details — such as character names, settings, quoted lines, or notable plot points — extracted from a reader's review." @Generable struct Arguments { @Guide(description: "Distinguishing details from the review that identify the book, such as character names, settings, quoted lines, or notable plot points.") var details: String } @Generable struct Output { @Guide(description: "The title of the identified book, or an empty string if no match was found.") var title: String @Guide(description: "The author of the identified book, or an empty string if no match was found.") var author: String } func call(arguments: Arguments) async throws -> Output { let needles = arguments.details .lowercased() .split(whereSeparator: { !$0.isLetter && !$0.isNumber }) .map(String.init) .filter { $0.count >= 4 } let best = Book.sampleBooks .map { book -> (book: Book, score: Int) in let review = book.review.lowercased() let score = needles.reduce(0) { partial, needle in partial + (review.contains(needle) ? 1 : 0) } return (book, score) } .max(by: { $0.score < $1.score }) guard let match = best, match.score > 0 else { return Output(title: "", author: "") } return Output(title: match.book.title, author: match.book.author) } } -
22:36 - BookTaggingService with a tools parameter
// Book Tagging Service struct BookTaggingService { static func generateTags(for review: String, tools: [any Tool] = []) async throws -> BookTags { let prompt = tagsPrompt(review: review) let session = LanguageModelSession( model: SystemLanguageModel(guardrails: .permissiveContentTransformations), tools: tools, instructions: instructions ) let response = try await session.respond(to: prompt, generating: BookTags.self) return response.content } } -
22:57 - Evaluation with the lookup tool
// Evaluation of tags with tool struct BookTaggingWithLookupEvaluation: Evaluation { func subject(from sample: ModelSample<BookTags>) async throws -> ModelSubject<BookTags> { let result = try await BookTaggingService.generateTags( for: sample.promptDescription, tools: [BookLookupTool()] ) return ModelSubject(value: result) } // ... same dataset, evaluators, and aggregation as BookTaggingEvaluation } -
23:09 - Compare with/without the tool in one suite
@Suite("Book Tag Evaluations") struct BookTagEvaluationTests { static let evaluation = BookTaggingEvaluation() static let lookupEvaluation = BookTaggingWithLookupEvaluation() @Test("Book Tag Evaluations", .evaluates(evaluation, info: evaluationInfo)) func evaluateBookTagging() async throws { let result = EvaluationContext.current.result let rangeMetric = BookTagEvaluationTests.evaluation.tagCount let dupeMetric = BookTagEvaluationTests.evaluation.noDuplicates #expect(result.aggregateValue(.mean(of: rangeMetric)) >= 0.8) #expect(result.aggregateValue(.mean(of: dupeMetric)) == 1) } @Test("Book Tag Evaluations (with BookLookupTool)", .evaluates(lookupEvaluation, info: lookupEvaluationInfo)) func evaluateBookTaggingWithLookup() async throws { let result = EvaluationContext.current.result let rangeMetric = BookTagEvaluationTests.lookupEvaluation.tagCount let dupeMetric = BookTagEvaluationTests.lookupEvaluation.noDuplicates #expect(result.aggregateValue(.mean(of: rangeMetric)) >= 0.8) #expect(result.aggregateValue(.mean(of: dupeMetric)) == 1) } }
-
-
- 0:00 - Introduction
Hill-climbing — iteratively improving an intelligence feature using evaluation scores as a guide (develop, run, analyze) — framed around bringing scientific thinking to that loop. Assumes you've already built an evaluation pipeline (see "Meet the Evaluations framework").
- 2:42 - BookTracker's tagging problem
Revisits BookTracker, whose tag generator produces tags that miss key themes or reflect the reader's feelings rather than the book. The existing evaluation judges tag quality via score dimensions (Relevance, Usefulness) and a ModelJudgeEvaluator.
- 5:27 - Analyzing the evaluation results
Adds two reviews to the dataset, runs the evaluation (Swift Testing #expect), and uses the Xcode evaluation report and assistant editor to compare generated tags against expected ones, revealing the human and model judge disagree on usefulness.
- 8:26 - Drift between judge and human
That disagreement is drift, the divergence between a model judge's ratings and an expert's. As the dataset grows, drift widens, making it hard to trust the evaluation, so the judge must be aligned to expert opinion.
- 9:37 - Measuring drift with Cohen's kappa
Accuracy alone misleads on unevenly-distributed scores (a high-scoring judge looks aligned by luck). Cohen's kappa coefficient measures true alignment by subtracting the chance of random agreement from accuracy and normalizing, a robust drift metric.
- 12:26 - Building a judge alignment evaluation
Builds an evaluation comparing the presenter's ratings to the judge's over a shared dataset: extract summary/tag pairs from the prior run's attachment, add human ratings, reuse the same ModelJudgeEvaluator as subject, and aggregate Cohen's kappa (plus mean and standard deviation), targeting an alignment of 0.6.
- 15:16 - Analyzing alignment failures
The alignment test fails. Drilling into the report (for example Frankenstein, The Ramakien) shows the judge rating overly-specific or off-theme tags too highly, the judge's prompt lacks the context to tell a good tag from a bad one.
- 17:16 - Comparative evaluation: control vs experimental
Xcode 27 can compare two evaluations like a controlled experiment: a baseline (control) prompt versus an experimental prompt that adds app context plus examples of good and bad tags. Running both shows relevance improved while usefulness dropped, a tradeoff to weigh.
- 19:12 - Refining the scoring dimensions
Keeping the prompt change, the side-by-side comparison view reveals the judge grading usefulness too harshly. Applying the new prompt to the baseline to isolate one variable, the ScoreDimension descriptions are sharpened (emphasizing genre tags; being critical of overly-specific ones), improving both scores.
- 21:23 - Adding few-shot examples to the judge
Still short of the goal, the judge prompt is grounded with the feature's purpose and a few worked examples of how the presenter rates, deliberately few to avoid overfitting the alignment score. Scores finally exceed expectations, so the judge is trusted and the loop exits.
- 23:38 - Going beyond prompts: adding a tool
Hill-climbing isn't only prompts: to give the on-device tag model more context, a BookLookupTool supplies the title and author. BookTaggingService gains a tools parameter (defaulting empty), and a second evaluation compares the feature with versus without the tool, the tool version scores better, though the small 13-sample dataset and unobserved tool calls point to "Create robust evaluations for agentic apps."
- 27:17 - Next steps
Think like a scientist (one change at a time), invest the time (failed experiments still inform), be creative (instructions, tools, models, datasets, aggregations, and evaluators are all fair game), and watch for drift. Download the Book Tracker sample and review the documentation.