<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>How to Survive the 21st Century</title>
    <link>https://jangcarru20100919.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 10 Apr 2026 17:33:39 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>라이드T</managingEditor>
    <image>
      <title>How to Survive the 21st Century</title>
      <url>https://tistory1.daumcdn.net/tistory/7198221/attach/0cf627606f7541e1930ea9b826beb2e5</url>
      <link>https://jangcarru20100919.tistory.com</link>
    </image>
    <item>
      <title>[DB 비서 Project] 데이터 전처리 Prefix 적용 전 vs 후</title>
      <link>https://jangcarru20100919.tistory.com/101</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;RAG(Retrieval-Augmented Generation) 시스템의 성능을 결정짓는 것은 결국 데이터의 품질과 임베딩 전략이다. 2026년 4월 6일, PDF 전처리 파이프라인을 고도화하고 메타데이터 Prefix가 검색 품질에 미치는 영향을 심층 분석하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-06 오후 1.50.20.png&quot; data-origin-width=&quot;687&quot; data-origin-height=&quot;778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAcueP/dJMcaiv4fr3/UwOn4a2HNxvehv3ROg2B60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAcueP/dJMcaiv4fr3/UwOn4a2HNxvehv3ROg2B60/img.png&quot; data-alt=&quot;메타데이터 전처리 넣기 전, 시맨틱 청킹으로만 검색 했을때의 군집화.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAcueP/dJMcaiv4fr3/UwOn4a2HNxvehv3ROg2B60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAcueP%2FdJMcaiv4fr3%2FUwOn4a2HNxvehv3ROg2B60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;687&quot; height=&quot;778&quot; data-filename=&quot;스크린샷 2026-04-06 오후 1.50.20.png&quot; data-origin-width=&quot;687&quot; data-origin-height=&quot;778&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;메타데이터 전처리 넣기 전, 시맨틱 청킹으로만 검색 했을때의 군집화.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여러 데이터가 뒤섞여있는 모습을 볼 수 있다. 이 상태에서 다시 실험 시작!!&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size26&quot;&gt;1. 실험 환경 및 데이터셋&lt;/h2&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;실험은 MacBook Air M2(MPS 가속) 환경에서 진행되었으며, jhgan/ko-sroberta-multitask 모델을 사용했다. 대상 데이터는 IT 인사이트 리포트, ESG 보고서, 고고학 자료 등 다양한 도메인이 섞인 PDF 25개로, 총 12,917개의 청크를 베이스라인으로 설정했다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;7&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;상세 내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0,0&quot;&gt;임베딩 모델&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,1,1,0&quot;&gt;ko-sroberta-multitask (768차원)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,2,0,0&quot;&gt;벡터 DB&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,2,1,0&quot;&gt;FAISS (L2 정규화 후 내적 검색)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,3,0,0&quot;&gt;하이브리드 설정&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,3,1,0&quot;&gt;Dense(70%) + BM25(30%)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,4,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,4,0,0&quot;&gt;청킹 전략&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,4,1,0&quot;&gt;시맨틱 청킹 (Similarity Threshold: 0.55)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-path-to-node=&quot;8&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size26&quot;&gt;2. 전처리 고도화: Regex v2와 4종 품질 필터&lt;/h2&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;단순한 텍스트 추출을 넘어, 데이터의 '순도'를 높이기 위해 전처리 파이프라인을 강화했다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size23&quot;&gt;2.1 Regex v2 및 공백 복원(restore_spaces)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;PDF 파싱 시 2단 컬럼 구조에서 발생하는 문장 붙여쓰기 문제를 해결하기 위해 restore_spaces() 로직을 추가했다. 한글과 영문, 숫자 사이의 경계를 탐지하여 공백을 삽입함으로써 시맨틱 청킹의 정확도를 높였다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size23&quot;&gt;2.2 4종 품질 필터 도입&lt;/h3&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;의미 없는 '이상치(Outlier)' 청크를 제거하기 위해 다음과 같은 품질 필터를 적용했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;15&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,0,0&quot;&gt;기호/숫자 밀도&lt;/b&gt;: 특수문자와 숫자가 50% 이상인 청크 제거 (표 파편 제거)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,1,0&quot;&gt;숫자 전용 패턴&lt;/b&gt;: 수치 데이터만 나열된 노이즈 제거&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,2,0&quot;&gt;공백 밀도&lt;/b&gt;: 200자 이상 청크 중 공백이 너무 적은 레이아웃 오류 제거&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,3,0&quot;&gt;알파 비중&lt;/b&gt;: 한글/영문 비중이 20% 미만인 유령 청크 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16&quot;&gt;결과:&lt;/b&gt; 총 12,917개 청크 중 &lt;b data-index-in-node=&quot;19&quot; data-path-to-node=&quot;16&quot;&gt;182개(1.4%)의 노이즈 청크를 성공적으로 필터링&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqyJbS/dJMcagE4Tej/7usKMhlV0XpfypjrHqRIyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqyJbS/dJMcagE4Tej/7usKMhlV0XpfypjrHqRIyk/img.png&quot; style=&quot;width: 46.4027%; margin-right: 10px;&quot; data-widthpercent=&quot;46.95&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2026-04-06 오후 1.51.35.png&quot; data-origin-height=&quot;817&quot; data-origin-width=&quot;1072&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqyJbS/dJMcagE4Tej/7usKMhlV0XpfypjrHqRIyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqyJbS%2FdJMcagE4Tej%2F7usKMhlV0XpfypjrHqRIyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1072&quot; height=&quot;817&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yBzme/dJMcaakw4P6/F2MRwxsWS9qcqbJKcLwrW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yBzme/dJMcaakw4P6/F2MRwxsWS9qcqbJKcLwrW1/img.png&quot; style=&quot;width: 52.4345%;&quot; data-widthpercent=&quot;53.05&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2026-04-06 오후 1.51.12.png&quot; data-origin-height=&quot;837&quot; data-origin-width=&quot;1241&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yBzme/dJMcaakw4P6/F2MRwxsWS9qcqbJKcLwrW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyBzme%2FdJMcaakw4P6%2FF2MRwxsWS9qcqbJKcLwrW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1241&quot; height=&quot;837&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Prefix 적용 된 예쁜 군집화 ver&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-06 오후 2.21.08.png&quot; data-origin-width=&quot;1191&quot; data-origin-height=&quot;839&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0qc0i/dJMcagLQumO/N3DOzJozMfKQAbZTYOXoQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0qc0i/dJMcagLQumO/N3DOzJozMfKQAbZTYOXoQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0qc0i/dJMcagLQumO/N3DOzJozMfKQAbZTYOXoQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0qc0i%2FdJMcagLQumO%2FN3DOzJozMfKQAbZTYOXoQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1191&quot; height=&quot;839&quot; data-filename=&quot;스크린샷 2026-04-06 오후 2.21.08.png&quot; data-origin-width=&quot;1191&quot; data-origin-height=&quot;839&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;군집화가 너무 잘 됐길래 이게 성공인줄 알았다...그치만 나의 오산이었음을...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-06 오후 1.53.49.png&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;936&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tDg3i/dJMcadIhA5c/KXzCkSkawhOasxbDGUQBhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tDg3i/dJMcadIhA5c/KXzCkSkawhOasxbDGUQBhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tDg3i/dJMcadIhA5c/KXzCkSkawhOasxbDGUQBhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtDg3i%2FdJMcadIhA5c%2FKXzCkSkawhOasxbDGUQBhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;936&quot; data-filename=&quot;스크린샷 2026-04-06 오후 1.53.49.png&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;936&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색도 잘 되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLTvQD/dJMcadIhA5b/rXWgAblG5eVyZqAyuHkev0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLTvQD/dJMcadIhA5b/rXWgAblG5eVyZqAyuHkev0/img.png&quot; style=&quot;width: 45.2286%; margin-right: 10px;&quot; data-widthpercent=&quot;45.76&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2026-04-06 오후 1.55.01.png&quot; data-origin-height=&quot;931&quot; data-origin-width=&quot;781&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLTvQD/dJMcadIhA5b/rXWgAblG5eVyZqAyuHkev0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLTvQD%2FdJMcadIhA5b%2FrXWgAblG5eVyZqAyuHkev0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;781&quot; height=&quot;931&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRVffd/dJMcadBtNcz/g6dYpKQhFL6ZZoqHYlISIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRVffd/dJMcadBtNcz/g6dYpKQhFL6ZZoqHYlISIk/img.png&quot; style=&quot;width: 53.6086%;&quot; data-widthpercent=&quot;54.24&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2026-04-06 오후 1.55.21.png&quot; data-origin-height=&quot;879&quot; data-origin-width=&quot;874&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRVffd/dJMcadBtNcz/g6dYpKQhFL6ZZoqHYlISIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRVffd%2FdJMcadBtNcz%2Fg6dYpKQhFL6ZZoqHYlISIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;874&quot; height=&quot;879&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 것도 너무 잘 갖고 오길래 음? 이제 PDF 전처리 끝인가? 했는데 전혀 아니었다 ^^ 재미나이와 대화하면서 교차 검증 해본 결과,&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8&quot;&gt;왜 이게 &quot;괜찮은 게 아닐 수도&quot; 있나요?&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;점수 인플레이션&lt;/b&gt;: 평균 점수가 &lt;span data-index-in-node=&quot;17&quot; data-math=&quot;0.6&quot;&gt;0.6&lt;/span&gt;까지 올라왔다는 건, 검색어와 상관없는 데이터들도 &quot;나도 60% 정도는 정답이야!&quot;라고 주장하고 있다는 뜻입니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;모호한 경계&lt;/b&gt;: 진짜 정답(&lt;span data-index-in-node=&quot;14&quot; data-math=&quot;0.9&quot;&gt;0.9&lt;/span&gt;)과 일반 데이터(&lt;span data-index-in-node=&quot;27&quot; data-math=&quot;0.6&quot;&gt;0.6&lt;/span&gt;)의 차이가 크지 않습니다. AI가 답변을 생성할 때, 살짝만 삐끗해도 관련 없는 &lt;span data-index-in-node=&quot;76&quot; data-math=&quot;0.6&quot;&gt;0.6&lt;/span&gt;점짜리 데이터를 정답으로 착각해 읽어올 위험(Hallucination)이 커집니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 하더라.... 그렇구나....&amp;nbsp;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;10&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size23&quot;&gt;3. 왜 이렇게 변했을까요? (가설)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;가장 의심되는 주범은 &lt;b data-index-in-node=&quot;12&quot; data-path-to-node=&quot;12&quot;&gt;메타데이터 Prefix&lt;/b&gt;입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 검색어에 포함된 단어가 파일명에도 들어있다면, 해당 파일의 &lt;b data-index-in-node=&quot;36&quot; data-path-to-node=&quot;13,0,0&quot;&gt;모든 청크&lt;/b&gt;가 그 파일명 덕분에 점수가 일괄적으로 뻥튀기됩니다.&lt;/li&gt;
&lt;li&gt;예를 들어, &quot;삼성전자&quot;를 검색했는데 모든 청크 앞에 [삼성전자 지속가능경영보고서]가 붙어 있다면, 내용이 무엇이든 일단 높은 점수를 받고 시작하는 거죠.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;군집화가 너무 잘 돼서 기뻐했는데 좋은 징조가 전혀 아니었다ㅠㅠ&lt;/p&gt;
&lt;hr data-path-to-node=&quot;17&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;18&quot; data-ke-size=&quot;size26&quot;&gt;3. 핵심 실험: 메타데이터 Prefix의 함정&lt;/h2&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;일단 오늘 가장 중요한 실험은 파일명과 페이지 정보를 본문 앞에 붙이는 것이 검색에 도움이 되는가?였다. 앞서 성능이 저하 된 게 검증 됐기 때문에 메타데이터를 뺀 버전을 다시 확인할 필요가 있었다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;20&quot; data-ke-size=&quot;size23&quot;&gt;Run 1 vs Run 3 비교 분석&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;21&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;21,0,0&quot;&gt;Run 1 (Prefix 포함)&lt;/b&gt;: 모든 청크 앞에 [{파일명} / p.{페이지}]를 삽입.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;21,1,0&quot;&gt;Run 3 (No Prefix)&lt;/b&gt;: 순수 본문만 임베딩.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfWYZu/dJMcaiQmNSM/5we0v7caxvEkyePmadBw20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfWYZu/dJMcaiQmNSM/5we0v7caxvEkyePmadBw20/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1882&quot; data-origin-height=&quot;928&quot; data-filename=&quot;스크린샷 2026-04-06 오후 10.08.16.png&quot; style=&quot;width: 49.4699%; margin-right: 10px;&quot; data-widthpercent=&quot;50.05&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfWYZu/dJMcaiQmNSM/5we0v7caxvEkyePmadBw20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfWYZu%2FdJMcaiQmNSM%2F5we0v7caxvEkyePmadBw20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1882&quot; height=&quot;928&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rsR4y/dJMcadhf4Ek/PW6QX8cPOOgAONXEKXC3E0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rsR4y/dJMcadhf4Ek/PW6QX8cPOOgAONXEKXC3E0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1870&quot; data-origin-height=&quot;924&quot; data-filename=&quot;스크린샷 2026-04-06 오후 10.09.51.png&quot; style=&quot;width: 49.3673%;&quot; data-widthpercent=&quot;49.95&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rsR4y/dJMcadhf4Ek/PW6QX8cPOOgAONXEKXC3E0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrsR4y%2FdJMcadhf4Ek%2FPW6QX8cPOOgAONXEKXC3E0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1870&quot; height=&quot;924&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Prefix 적용 ver&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;22&quot; data-ke-size=&quot;size20&quot;&gt;시각적으로&amp;nbsp;예쁜&amp;nbsp;군집&amp;nbsp;&amp;rarr;&amp;nbsp;Prefix&amp;nbsp;있음이&amp;nbsp;유리&lt;br /&gt;검색&amp;nbsp;변별력,&amp;nbsp;의미&amp;nbsp;기반&amp;nbsp;분리&amp;nbsp;&amp;rarr;&amp;nbsp;Prefix&amp;nbsp;없음이&amp;nbsp;유리&amp;nbsp;(분포도&amp;nbsp;비교에서&amp;nbsp;확인됨)&lt;/h4&gt;
&lt;h3 data-path-to-node=&quot;22&quot; data-ke-size=&quot;size23&quot;&gt;분석 결과: &quot;Prefix는 변별력을 저해한다&quot;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;실험 결과, Prefix를 사용했을 때 오히려 검색의 변별력이 낮아지는 현상이 관찰되었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;24&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;24,0,0&quot;&gt;점수 분포의 왜곡&lt;/b&gt;: Prefix가 포함된 경우 점수 분포가 0.55~0.65 구간에 과도하게 밀집(종 모양)되었다. 반면, Prefix를 제거하자 &lt;b data-index-in-node=&quot;82&quot; data-path-to-node=&quot;24,0,0&quot;&gt;중심이 0.20~0.30으로 이동하며 관련 있는 문서만 0.8점대로 튀어나오는 이상적인 좌편향 분포&lt;/b&gt;를 보였다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-06 오후 10.36.05.png&quot; data-origin-width=&quot;2502&quot; data-origin-height=&quot;1668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsltvn/dJMcacCFMEL/FKAVE2lhTGRHc2kKKmSK8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsltvn/dJMcacCFMEL/FKAVE2lhTGRHc2kKKmSK8K/img.png&quot; data-alt=&quot;Prefix 다시 뺀 ver&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsltvn/dJMcacCFMEL/FKAVE2lhTGRHc2kKKmSK8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdsltvn%2FdJMcacCFMEL%2FFKAVE2lhTGRHc2kKKmSK8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2502&quot; height=&quot;1668&quot; data-filename=&quot;스크린샷 2026-04-06 오후 10.36.05.png&quot; data-origin-width=&quot;2502&quot; data-origin-height=&quot;1668&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Prefix 다시 뺀 ver&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 군집화가 확실할 수록 데이터의 검색 기능이 좋아졌다고 생각했지만 실상은 아니었다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시각적으로 예쁜 군집화와 검색 엔진의 예쁜 군집화의 개념이 아예 달랐음 ㅜ 해당 사진이 그 예이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; Cursor 답변: 지금 목표가 검색 품질 향상이라면 Prefix 없음 유지가 맞습니다. 시각화가 덜 예뻐 보이는 건 오히려 &quot;파일 출처 바이어스가 빠진 것&quot;이라 정상입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇구나.. 내가 잘 못 알고 있던거구나...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnnAlf/dJMcafF9Xbv/zAVU8DkKvpH9hM4Pu2gXsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnnAlf/dJMcafF9Xbv/zAVU8DkKvpH9hM4Pu2gXsK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1880&quot; data-origin-height=&quot;934&quot; data-filename=&quot;스크린샷 2026-04-06 오후 10.36.42.png&quot; style=&quot;width: 49.4449%; margin-right: 10px;&quot; data-widthpercent=&quot;50.03&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnnAlf/dJMcafF9Xbv/zAVU8DkKvpH9hM4Pu2gXsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnnAlf%2FdJMcafF9Xbv%2FzAVU8DkKvpH9hM4Pu2gXsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1880&quot; height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oZwsG/dJMb99Tsza0/gykGqKjkONu7eUYVsY6aD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oZwsG/dJMb99Tsza0/gykGqKjkONu7eUYVsY6aD0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1878&quot; data-origin-height=&quot;934&quot; data-filename=&quot;스크린샷 2026-04-06 오후 10.36.32.png&quot; data-widthpercent=&quot;49.97&quot; style=&quot;width: 49.3923%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oZwsG/dJMb99Tsza0/gykGqKjkONu7eUYVsY6aD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoZwsG%2FdJMb99Tsza0%2FgykGqKjkONu7eUYVsY6aD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1878&quot; height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Prefix를 제거하니 변별력이 좋아진 모습을 볼 수 있다. &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;24&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;24,1,0&quot;&gt;Dense 점수의 하향 평준화&lt;/b&gt;: 파일명이 반복 패턴으로 작용하여, 임베딩 모델이 같은 파일 내의 모든 청크를 비슷하게 인식(Vector 수렴)해 버리는 부작용이 발생했다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;24,2,0&quot;&gt;군집화의 질&lt;/b&gt;: 시각적으로는 Prefix가 있을 때 파일별로 섬처럼 잘 모이는 듯 보였으나, 이는 내용이 아닌 &lt;b data-index-in-node=&quot;61&quot; data-path-to-node=&quot;24,2,0&quot;&gt;'출처'에 의한 인위적 군집&lt;/b&gt;이었다. Prefix를 제거했을 때 비로소 &lt;b data-index-in-node=&quot;100&quot; data-path-to-node=&quot;24,2,0&quot;&gt;'내용 기반'의 진정한 시맨틱 군집화&lt;/b&gt;가 이루어졌다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-path-to-node=&quot;25&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;26&quot; data-ke-size=&quot;size26&quot;&gt;4. 결론 및 향후 계획&lt;/h2&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;오늘의 실험을 통해 과도한 메타데이터 삽입은 본문의 의미 정보를 희석시킨다는 결론에 도달했다. 검색 재현율(Recall)을 위해 파일명을 활용하고 싶다면, 본문에 직접 삽입하기보다는 DB의 메타데이터 필드를 활용하거나 아주 짧은 키워드(Short Domain Prefix)만 사용하는 것이 바람직하다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;28&quot; data-ke-size=&quot;size23&quot;&gt;최종 선택된 파이프라인 설정&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;29&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;29,0,0&quot;&gt;전처리&lt;/b&gt;: Regex v2 + 공백 복원 적용&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;29,1,0&quot;&gt;필터&lt;/b&gt;: 4종 품질 필터 상시 가동&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;29,2,0&quot;&gt;Prefix&lt;/b&gt;: 제거 (--no-prefix)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;29,3,0&quot;&gt;가중치a&lt;/b&gt;: 0.7 (Dense 위주) 이건&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-path-to-node=&quot;30&quot; data-ke-size=&quot;size23&quot;&gt;향후 과제&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;31&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span data-index-in-node=&quot;0&quot; data-math=&quot;\alpha&quot;&gt;a&lt;/span&gt;&amp;nbsp;값을 0.5~0.6으로 조정하여 키워드 매칭(Sparse)의 보완 능력 테스트&lt;/li&gt;
&lt;li&gt;[IT], [고고학]과 같은 초단축 도메인 Prefix 실험 -&amp;gt; 이전이랑 다르게 조금 더 짧은 버전으로 Prefix 를 적용해보자!&lt;/li&gt;
&lt;li&gt;HWP, DOCX 등 파일 형식 확장 및 통합 임베딩 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project</category>
      <category>AI</category>
      <category>PDF전처리</category>
      <category>prefix</category>
      <category>메타데이터</category>
      <category>트러블슈팅</category>
      <category>프로젝트</category>
      <author>라이드T</author>
      <guid isPermaLink="true">https://jangcarru20100919.tistory.com/101</guid>
      <comments>https://jangcarru20100919.tistory.com/101#entry101comment</comments>
      <pubDate>Mon, 6 Apr 2026 12:02:57 +0900</pubDate>
    </item>
    <item>
      <title>[DB 비서 Project] 실전 데이터 임베딩 및 3D 시각화 분석</title>
      <link>https://jangcarru20100919.tistory.com/100</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;본 포스팅에서는 &lt;b&gt;DB비서&amp;nbsp;&lt;/b&gt;파이프라인을 활용하여 25건의 실전 PDF 데이터를 처리하고, 이를 3차원 공간에 시각화하여 분석한 실험 결과를 정리해보려고 한다. 단순한 텍스트 검색을 넘어 데이터의 의미적 구조를 파악하고 검색 효율을 극대화하는 과정을 담았다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;4&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size26&quot;&gt;1. 실험 개요 및 환경&lt;/h2&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;이번 실험의 목적은 AI, SW 정책, ESG, 고고학 등 서로 이질적인 도메인의 한국어 PDF 25건을 대상으로 '추출 - 시맨틱 청킹 - 임베딩 - 하이브리드 검색'으로 이어지는 파이프라인의 실무 적용 가능성을 검증하는 것.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;7&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0,0&quot;&gt;컴퓨팅 자원&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,1,1,0&quot;&gt;MacBook Air M2 (MPS 가속)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,2,0,0&quot;&gt;임베딩 모델&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,2,1,0&quot;&gt;jhgan/ko-sroberta-multitask (768 dim)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,3,0,0&quot;&gt;벡터 저장소&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,3,1,0&quot;&gt;FAISS (L2 Normalization 적용)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,4,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,4,0,0&quot;&gt;데이터 구성&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;7,4,1,0&quot;&gt;AI 브리프, 삼성 지속가능경영 보고서, 고고학 시굴조사 보고서 등 25종&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;특히 도메인이 극명하게 갈리는 고고학 자료와 SW 정책 자료를 함께 인덱싱하여, 모델이 의미적 거리를 얼마나 잘 식별하는지 확인하는 데 초점을 맞췄다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;9&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size26&quot;&gt;2. 5단계 파이프라인 수행 지표&lt;/h2&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;전체 공정은 약 9분 16초가 소요되었으며, 14,000개가 넘는 청크를 처리하는 데 있어 크게 문제가 되지 않았다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;12&quot; data-ke-size=&quot;size23&quot;&gt;단계별 소요 시간 (Total: 554s)&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;13&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,0,0&quot;&gt;텍스트 추출 (Step 2)&lt;/b&gt;: 25개 PDF에서 텍스트를 파싱하는 데 &lt;b data-index-in-node=&quot;39&quot; data-path-to-node=&quot;13,0,0&quot;&gt;105.6초&lt;/b&gt; 소요.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,1,0&quot;&gt;시맨틱 청킹 (Step 3)&lt;/b&gt;: 문맥의 흐름을 분석하여 14,231개의 청크로 분할하는 데 &lt;b data-index-in-node=&quot;50&quot; data-path-to-node=&quot;13,1,0&quot;&gt;311.8초&lt;/b&gt; 소요.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,2,0&quot;&gt;임베딩 및 저장 (Step 4)&lt;/b&gt;: 전 청크를 벡터화하여 FAISS에 저장하는 데 &lt;b data-index-in-node=&quot;45&quot; data-path-to-node=&quot;13,2,0&quot;&gt;130.8초&lt;/b&gt; 소요 (초당 약 108.8개 청크 처리).&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,3,0&quot;&gt;하이브리드 검색 (Step 5)&lt;/b&gt;: 14,231개 데이터 중 최적의 결과 5개를 찾는 데 &lt;b data-index-in-node=&quot;49&quot; data-path-to-node=&quot;13,3,0&quot;&gt;0.99초&lt;/b&gt; 소요.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-path-to-node=&quot;14&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;15&quot; data-ke-size=&quot;size26&quot;&gt;3. 시맨틱 청킹 및 임베딩 분석&lt;/h2&gt;
&lt;h3 data-path-to-node=&quot;16&quot; data-ke-size=&quot;size23&quot;&gt;의미 기반의 데이터 분할 (Semantic Chunking)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;고정 길이 분할 대신 문장 간 유사도를 측정하는 시맨틱 청킹을 적용했다. 유사도 임계값은 &lt;b data-index-in-node=&quot;50&quot; data-path-to-node=&quot;17&quot;&gt;0.55&lt;/b&gt;로 설정하여 문맥이 변하는 지점을 정교하게 포착했다. 그 결과 총 &lt;b data-index-in-node=&quot;91&quot; data-path-to-node=&quot;17&quot;&gt;14,231개&lt;/b&gt;의 청크가 생성되었으며, 평균 길이는 약 113자였다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;18&quot; data-ke-size=&quot;size23&quot;&gt;임베딩 품질 검증&lt;/h3&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;임베딩 단계에서는 L2 정규화를 적용하여 모든 벡터를 단위 구(Unit Sphere)에 투영했다. 정규화 전 평균 L2 노름(Norm) 값이 10.34로 고르게 분포하여, 모델이 데이터의 특징을 안정적으로 추출하고 있음을 확인했다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;20&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;21&quot; data-ke-size=&quot;size26&quot;&gt;4. 하이브리드 검색 및 성능 검증&lt;/h2&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;검색 엔진의 변별력을 높이기 위해 Dense(의미) 70%와 Sparse(BM25 키워드) 30%를 결합한 하이브리드 공식을 사용했다&lt;/p&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;저번 더미데이터로 테스트 했을땐 8:2로 가중치를 두었었는데, 아무래도 데이터 용량이 늘어나면 키워드도 중요해지니 가중치를 조금 더 늘려보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-04 오후 11.44.43.png&quot; data-origin-width=&quot;3420&quot; data-origin-height=&quot;2214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FYCKw/dJMcahYe5PU/MOdiGBT5eyCKeq5bCehFS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FYCKw/dJMcahYe5PU/MOdiGBT5eyCKeq5bCehFS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FYCKw/dJMcahYe5PU/MOdiGBT5eyCKeq5bCehFS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFYCKw%2FdJMcahYe5PU%2FMOdiGBT5eyCKeq5bCehFS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3420&quot; height=&quot;2214&quot; data-filename=&quot;스크린샷 2026-04-04 오후 11.44.43.png&quot; data-origin-width=&quot;3420&quot; data-origin-height=&quot;2214&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우측은 실제 데이터의 하천 부분을 보고 왼쪽 테스트 html 파일에 검색해본 것이다. 일단 상위 1번에 잘 올라온 것 확인.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 안에 들어가서 제대로 저 페이지를 갖고 왔는지 확인해보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두근 두근&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-04 오후 11.45.08.png&quot; data-origin-width=&quot;3420&quot; data-origin-height=&quot;2214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PKlno/dJMcabwWCTS/P1TrW8Cxofc7Owro8gqMJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PKlno/dJMcabwWCTS/P1TrW8Cxofc7Owro8gqMJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PKlno/dJMcabwWCTS/P1TrW8Cxofc7Owro8gqMJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPKlno%2FdJMcabwWCTS%2FP1TrW8Cxofc7Owro8gqMJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3420&quot; height=&quot;2214&quot; data-filename=&quot;스크린샷 2026-04-04 오후 11.45.08.png&quot; data-origin-width=&quot;3420&quot; data-origin-height=&quot;2214&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 갖고 온다! 페이지 수도 맞았고, 해당 파일의 원본도 같이 띄워주는 로직을 추가하니 확인하기가 훨씬 편해졌다 쿄쿄&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;26&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;23&quot; data-ke-size=&quot;size23&quot;&gt;&quot;클라우드 조달&quot; 검색 사례 분석&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;처음 임베딩후 질의어 &quot;클라우드 조달&quot; 입력 후, 상위 5개 결과 중 4개가 관련 주제를 다룬 '인사이트리포트 2017-006' 문서에서 도출되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-path-to-node=&quot;25&quot;&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;25,0,0&quot; data-index-in-node=&quot;0&quot;&gt;Rank 1&lt;/b&gt;: 최종 점수&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-path-to-node=&quot;25,0,0&quot; data-index-in-node=&quot;14&quot;&gt;0.9496&lt;/b&gt;으로 압도적 1위 기록. 의미 점수(70%)와 키워드 점수(30%)가 조화롭게 작용했다.&lt;/li&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;25,1,0&quot; data-index-in-node=&quot;0&quot;&gt;특이사항&lt;/b&gt;: 키워드가 부족하더라도 문맥적 유사도가 높은 청크들이 상위권에 배치되어, 단순 키워드 검색의 한계를 보완했다.&lt;/li&gt;
&lt;li&gt;이건 터미널을 닫아서 캡쳐를 못 했다ㅠㅡㅜ 까비&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-path-to-node=&quot;27&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-path-to-node=&quot;27&quot; data-ke-size=&quot;size26&quot;&gt;5. 3D 시각화 및 군집 분석 (XAI)&lt;/h2&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;가장 흥미로운 지점은 PCA 및 UMAP을 활용한 3차원 공간 분석이다. 14,000개의 지식 파편들이 벡터 공간에서 어떻게 군집을 이루는지 시각적으로 입증했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-05 오전 12.06.21.png&quot; data-origin-width=&quot;2590&quot; data-origin-height=&quot;1890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biLQr6/dJMcagyfs2u/HZQg57ZAQe3idKxxMJIW4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biLQr6/dJMcagyfs2u/HZQg57ZAQe3idKxxMJIW4K/img.png&quot; data-alt=&quot;흰 테두리 동그라미는 이상치 데이터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biLQr6/dJMcagyfs2u/HZQg57ZAQe3idKxxMJIW4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiLQr6%2FdJMcagyfs2u%2FHZQg57ZAQe3idKxxMJIW4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2590&quot; height=&quot;1890&quot; data-filename=&quot;스크린샷 2026-04-05 오전 12.06.21.png&quot; data-origin-width=&quot;2590&quot; data-origin-height=&quot;1890&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;흰 테두리 동그라미는 이상치 데이터&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;29&quot; data-ke-size=&quot;size23&quot;&gt;데이터 지도의 특징&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;30&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;30,0,0&quot;&gt;시맨틱 허브(Central Hub)&lt;/b&gt;: SW 정책, IT 일반론 등 보편적인 서술이 담긴 청크들이 중앙에 거대한 덩어리를 형성했다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;30,1,0&quot;&gt;주제별 섬(Islands)&lt;/b&gt;: 고고학 관련 보고서들은 중앙 덩어리에서 완전히 분리되어 독자적인 섬을 형성했다. 이는 모델이 도메인 간의 차이를 명확히 인지하고 있다는 증거다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;30,2,0&quot;&gt;이상치(Outliers)&lt;/b&gt;: 전체 데이터 중 상위 5%에 해당하는 이상치들을 흰색 테두리로 강조했다. 분석 결과, 주로 페이지 번호, 표 데이터, 혹은 매우 특수한 전문 용어가 포함된 청크들로 확인되었다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-path-to-node=&quot;31&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;32&quot; data-ke-size=&quot;size26&quot;&gt;6. 결론 및 향후 과제&lt;/h2&gt;
&lt;p data-path-to-node=&quot;33&quot; data-ke-size=&quot;size16&quot;&gt;이번 실험을 통해 실전 규모의 데이터를 1초 이내에 검색하고 시각화할 수 있는 안정적인 파이프라인을 구축했다. 특히 3D 지도를 통해 데이터의 누락이나 편중을 직관적으로 확인할 수 있었다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;34&quot; data-ke-size=&quot;size23&quot;&gt;향후 개선 방향&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;35&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;35,0,0&quot;&gt;노이즈 필터링&lt;/b&gt;: 80자 미만의 짧은 청크(약 7.5%)가 이상치의 주범으로 파악되어, 이를 제거하는 전처리 로직을 강화할 예정이다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;35,1,0&quot;&gt;정규표현식 도입&lt;/b&gt;: 페이지 번호, 발행처 등 반복되는 머리말/꼬리말을 자동으로 정제하는 정규화 과정을 추가한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;35,2,0&quot;&gt;가중치 최적화&lt;/b&gt;: 현재 0.7:0.3인 하이브리드 비율을 데이터 특성에 맞춰 미세 조정(Fine-tuning)할 계획이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;데이터가 단순히 저장되는 것이 아니라, 스스로 군집을 이루고 의미를 드러내는 과정은 언제나 흥미롭다. 다음 실험에서는 전처리 강화 후 변화된 데이터 지도를 공유하고자 한다. 그나저나 왜 잘 될까?,,, 잘 돼도 불안하다... 내일은 pdf 파일 형식 말고 다른 파일 형식으로 실험해봐야징&lt;/p&gt;
&lt;p data-path-to-node=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;7&quot; data-ke-size=&quot;size26&quot;&gt;&amp;lt;보완할 점 (Technical Debt)&amp;gt;&lt;/h2&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;가장 시급한 것은 '데이터의 노이즈'를 걷어내는 작업이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;초단기 청크(&amp;lt; 80자) 처리&lt;/b&gt;: 현재 1,065건(7.5%)의 짧은 청크가 존재한다. 이들은 의미가 파편화되어 있어 검색 시 &quot;엉뚱한 정답&quot;을 내놓을 확률이 높다. 인덱싱 전에 무조건 필터링하거나, 앞뒤 청크와 병합하는 로직이 필요해보임. (양방향 청크를 도입해보자)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;헤더/푸터(Header/Footer) 오염&lt;/b&gt;: PCA 지도 중앙에 뭉친 덩어리 중 상당수는 &quot;페이지 번호&quot;, &quot;발행처&quot; 등일 것같다. 이는 검색 엔진이 모든 페이지가 정답 후보라고 착각하게 만들 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,2,0&quot;&gt;편중 현상&lt;/b&gt;: 특정 문서(나이테 보고서 등)가 전체 청크의 13%를 차지하고 있다. 특정 도메인의 목소리가 너무 크면 검색 결과가 왜곡될 수 있으므로, 문서당 최대 청크 수(Max Chunks per Doc)를 제한하는 &lt;b data-index-in-node=&quot;124&quot; data-path-to-node=&quot;9,2,0&quot;&gt;샘플링 캡&lt;/b&gt; 도입을 고려해 볼것.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;10&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size26&quot;&gt;7. 이미지 안의 텍스트 처리 전략&lt;/h2&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;티스토리에 올릴 때 이미지 속 텍스트는 이미지 설명(Caption)과 본문 텍스트로 반드시 분리해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,0,0&quot;&gt;이미지 텍스트 추출&lt;/b&gt;: 이미지에 있는 핵심 수치(예: 소요 시간 996.7ms, 청크 수 14,231개 등)는 이미지로만 두지 말고 &lt;b data-index-in-node=&quot;73&quot; data-path-to-node=&quot;13,0,0&quot;&gt;본문에 텍스트로 다시 한 번 적자.&lt;/b&gt; (검색 엔진 최적화 및 접근성 향상)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,1,0&quot;&gt;이미지 설명 추가&lt;/b&gt;: 예를 들어 score_analysis_top5.png 아래에는 다음과 같은 설명을 덧붙이자.&lt;/li&gt;
&lt;li data-path-to-node=&quot;13,1,1&quot;&gt;&quot;상위 5개 결과 분석 결과, Dense 점수(의미)가 평균 70% 이상 기여하고 있으며, 키워드(BM25)가 부족한 경우에도 맥락을 통해 정답을 찾아내고 있음을 확인했습니다.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-path-to-node=&quot;15&quot; data-ke-size=&quot;size26&quot;&gt;8. 앞으로 해야 할 점 (Next Steps)&lt;/h2&gt;
&lt;h3 data-path-to-node=&quot;17&quot; data-ke-size=&quot;size23&quot;&gt;① 전처리 파이프라인 고도화 (Cleaning)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;정규표현식을 활용해 PDF 추출물에서 불필요한 공백, 특수문자, 페이지 번호를 지우는 &lt;b data-index-in-node=&quot;48&quot; data-path-to-node=&quot;18&quot;&gt;Clean-Embed-Index&lt;/b&gt; 루틴을 완성.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;19&quot; data-ke-size=&quot;size23&quot;&gt;② 하이브리드 검색의 'Sweet Spot' 찾기&lt;/h3&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;현재 a = &lt;span data-index-in-node=&quot;3&quot; data-math=&quot;\alpha=0.7&quot;&gt;0.7&lt;/span&gt;이 좋지만, 도메인(IT vs 고고학)에 따라 최적의 비율이 다를 수 있다.&lt;/p&gt;
&lt;div data-path-to-node=&quot;21&quot;&gt;
&lt;div data-math=&quot;\text{Final Score} = \alpha \cdot \text{Dense} + (1-\alpha) \cdot \text{Sparse}&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-05 오전 12.41.34.png&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blwU3m/dJMcaakwfeY/gLckfVZTFkrxfaM947bhKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blwU3m/dJMcaakwfeY/gLckfVZTFkrxfaM947bhKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blwU3m/dJMcaakwfeY/gLckfVZTFkrxfaM947bhKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblwU3m%2FdJMcaakwfeY%2FgLckfVZTFkrxfaM947bhKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;782&quot; height=&quot;106&quot; data-filename=&quot;스크린샷 2026-04-05 오전 12.41.34.png&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;위 공식에서 a를 0.5부터 0.9까지 0.1 단위로 테스트하여 &lt;b data-index-in-node=&quot;42&quot; data-path-to-node=&quot;22&quot;&gt;가장 '정답 같은' 결과가 나오는 지점&lt;/b&gt;을 해야함. (최적의 파라미터 찾기)&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;23&quot; data-ke-size=&quot;size23&quot;&gt;③ 시각화 도구의 상설화&lt;/h3&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;현재는 viz-3d 페이지가 수동으로 들어가야 하지만, 웹 메인 화면 상단에 &lt;b data-index-in-node=&quot;43&quot; data-path-to-node=&quot;24&quot;&gt;'실시간 데이터 분포 모니터링'&lt;/b&gt; 탭으로 고정해보기???&lt;/p&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project</category>
      <category>AI</category>
      <category>db비서</category>
      <category>finalproject</category>
      <category>문서</category>
      <author>라이드T</author>
      <guid isPermaLink="true">https://jangcarru20100919.tistory.com/100</guid>
      <comments>https://jangcarru20100919.tistory.com/100#entry100comment</comments>
      <pubDate>Sun, 5 Apr 2026 00:26:27 +0900</pubDate>
    </item>
    <item>
      <title>[DB비서 Project] 나만의 AI DB 비서, 그 첫 번째 기록 (feat. RAG &amp;amp; 하이브리드 검색)</title>
      <link>https://jangcarru20100919.tistory.com/97</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;KDT 과정의 마무리인 파이널프로젝트 주제가 확정 되었다. 나만의 DB 비서.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 팀은 파편화된 개인 데이터를 통합 학습하여, 사용자가 원하는 정답과 그 근거를 즉시 제시하는 &lt;b data-index-in-node=&quot;104&quot; data-path-to-node=&quot;3&quot;&gt;'지능형 DB 비서'&lt;/b&gt; 구축을 목표로 삼았다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4&quot;&gt;1. 프로젝트의 목표: &quot;개인 비정형 데이터의 자산화&quot;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;현대인은 매일 방대한 양의 PDF, 엑셀, 메모 등을 생성한다. 그러나 정작 중요한 정보를 찾기 위해서는 폴더를 뒤지거나 기억에 의존해야 하는 비효율이 발생한다. 우리는 사용자가 업로드한 모든 비정형 데이터를 AI가 이해하고, 자연어 질의에 대해 정확한 출처와 함께 답변하는 폐쇄형 개인 DB 시스템을 지향한다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6&quot;&gt;2. 핵심 기술: 하이브리드 검색(Hybrid Search) 엔진&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;일단 내가 맡은 부분은 텍스트 파일의 검색 기능이다. 사용자가 음성이나 텍스트로 해당 파일의 내용을 검색할 경우 그 내용을 찾아주고 이메일로 요약 및 해당 파일 전체를 전송해주는 기능이다. 하지만 단순한 키워드 매칭이나 벡터 검색만으로는 실전 서비스의 정확도를 담보할 수 없었다. 이에 따라 의미 기반의 &lt;b data-index-in-node=&quot;60&quot; data-path-to-node=&quot;7&quot;&gt;Dense Embedding&lt;/b&gt;과 키워드 중심의 &lt;b data-index-in-node=&quot;85&quot; data-path-to-node=&quot;7&quot;&gt;BM25&lt;/b&gt;를 결합한 하이브리드 방식으로 실험해보고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,0&quot;&gt;Dense Embedding (80%)&lt;/b&gt;: ko-sroberta-multitask 모델을 사용하여 문장의 맥락과 의도를 파악한다. 이는 &quot;봄에 진행한 기획안&quot;과 같은 추상적인 질문에 대응하는 핵심 동력이다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,0&quot;&gt;Sparse BM25 (20%)&lt;/b&gt;: 특정 제품명이나 보안 코드(예: SEC-BO-206) 등 고유 명사를 정확히 포착하기 위해 키워드 가중치를 부여했다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,2,0&quot;&gt;최적화&lt;/b&gt;: L2 정규화(Normalization)를 거친 벡터를 FAISS IndexFlatIP 인덱스에 저장하여 검색 속도와 정확도를 동시에 확보했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9&quot;&gt;3. 설명 가능한 AI(XAI)의 구현&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;나의 DB비서는 검색 결과의 투명성을 높이기 위해 &lt;b data-index-in-node=&quot;29&quot; data-path-to-node=&quot;10&quot;&gt;Score Breakdown&lt;/b&gt; 기능을 도입했다. 사용자에게 단순히 결과만 보여주는 것이 아니라, 해당 문서가 선정된 수학적 근거를 시각적으로 제시한다.&lt;/p&gt;
&lt;div data-path-to-node=&quot;11&quot;&gt;
&lt;div data-math=&quot;FinalScore = (\alpha \cdot Dense_{\mathrm{norm}}) + ((1-\alpha) \cdot \mathrm{BM}25_{\mathrm{norm}})&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-04 오후 3.16.21.png&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FuRnb/dJMcafMVrcy/y3CkreaKCzYrgZutOzolFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FuRnb/dJMcafMVrcy/y3CkreaKCzYrgZutOzolFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FuRnb/dJMcafMVrcy/y3CkreaKCzYrgZutOzolFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFuRnb%2FdJMcafMVrcy%2Fy3CkreaKCzYrgZutOzolFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;936&quot; height=&quot;114&quot; data-filename=&quot;스크린샷 2026-04-04 오후 3.16.21.png&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;위 식을 바탕으로 각 결과 카드에 의미 점수와 키워드 점수의 기여도를 그래프로 표기하여 시스템에 대한 사용자의 신뢰도를 높였다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13&quot;&gt;4. 실험적 발견: 성능 절벽(Performance Cliff)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;개발 과정에서 데이터 규모에 따른 성능 변화를 관찰하기 위해 100개부터 10,000개(약 50,000개 청크)까지 스트레스 테스트를 진행했다. 실험 결과, 데이터 밀도가 일정 수준을 넘어서면 유사 벡터 간의 간섭으로 인해 정확도가 급감하는 &lt;b data-index-in-node=&quot;135&quot; data-path-to-node=&quot;14&quot;&gt;'성능 절벽'&lt;/b&gt; 현상을 데이터로 확인했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;그리고 이 결과를 팀원들과 공유하니, 팀장님께서 데이터의 양을 차례로 늘리는 방식이 아닌 한 번에 훅 늘려서 유사도가 떨어진 것 같다는 의견을 주셨다. 생각해보니, 내가 100개 -&amp;gt; 500개 -&amp;gt; 10,000개로 급작스럽게 늘리긴 했었다! (왜 이 생각을 못 했지? 500개까지는 잘 찾아오길래 갑자기 수를 확 늘린게 화근이 됐었다....) 특히 실제 데이터가 아닌 더미데이터로 실험을 해본 거라 이제 실제 문서용으로 파라미터를 찾는 실험을 해볼 생각이다.&amp;nbsp; 이를 통해 향후 대규모 데이터 환경에서는 &lt;b data-index-in-node=&quot;181&quot; data-path-to-node=&quot;14&quot;&gt;리랭커(Reranker)&lt;/b&gt; 도입과 시맨틱 청킹(Semantic Chunking)이 필수적이라는 기술적 통찰을 얻었다. 하지만 아직 실제 데이터셋으로 실험을 해보진 않아서, 데이터양이 많아질 경우 해당 방법을 도입할 생각.&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-04 오후 3.18.04.png&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqvAAw/dJMcabqdrTO/BfJLE9iRASNDPK8dGcn9IK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqvAAw/dJMcabqdrTO/BfJLE9iRASNDPK8dGcn9IK/img.png&quot; data-alt=&quot;데이터 10,000개 부터 아무것도 못 갖고 오는 현상&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqvAAw/dJMcabqdrTO/BfJLE9iRASNDPK8dGcn9IK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqvAAw%2FdJMcabqdrTO%2FBfJLE9iRASNDPK8dGcn9IK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;902&quot; height=&quot;898&quot; data-filename=&quot;스크린샷 2026-04-04 오후 3.18.04.png&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데이터 10,000개 부터 아무것도 못 갖고 오는 현상&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-04 오후 3.18.11.png&quot; data-origin-width=&quot;2960&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfhzaU/dJMcafMVrhK/YFYn7NHVJqzfSWySW4ixCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfhzaU/dJMcafMVrhK/YFYn7NHVJqzfSWySW4ixCk/img.png&quot; data-alt=&quot;ㅋㅋ처참함&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfhzaU/dJMcafMVrhK/YFYn7NHVJqzfSWySW4ixCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdfhzaU%2FdJMcafMVrhK%2FYFYn7NHVJqzfSWySW4ixCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2960&quot; height=&quot;680&quot; data-filename=&quot;스크린샷 2026-04-04 오후 3.18.11.png&quot; data-origin-width=&quot;2960&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ㅋㅋ처참함&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;15&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15&quot;&gt;5. 향후 과제: 실전 데이터(Real-world Data) 대응&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;현재 정제된 텍스트 기반의 실험을 마치고, 실제 업무 환경에서 쓰이는 복잡한 파일 형식들을 정복할 준비를 마쳤다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;17&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다단 구조와 표가 포함된 &lt;b data-index-in-node=&quot;14&quot; data-path-to-node=&quot;17,0,0&quot;&gt;고난도 PDF&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;국내 행정 문서의 표준인 &lt;b data-index-in-node=&quot;14&quot; data-path-to-node=&quot;17,1,0&quot;&gt;HWP&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;반정형 데이터의 핵심인 &lt;b data-index-in-node=&quot;13&quot; data-path-to-node=&quot;17,2,0&quot;&gt;XLSX(Excel) -&amp;gt; 일단 보류. PDF 먼저 잘 찾아 오는지 실험 할 것임&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&quot;가장 난도 높은 데이터를 먼저 해결해야 범용적인 성능을 보장할 수 있다&quot;는 전략 하에, 공공기관 보도자료 및 기업 사업보고서 등 실제 파일을 활용한 극한의 테스트를 이어갈 계획이다. 벌써부터 트러블슈팅이 넘쳐난다. 후..&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://file:///Users/jangjuyeon/FP_Chainers/modaflow_viz/modaflow_report.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http://file:///Users/jangjuyeon/FP_Chainers/modaflow_viz/modaflow_report.html&lt;/a&gt;&lt;/p&gt;</description>
      <category>Project</category>
      <category>AI</category>
      <category>KDT파이널프로젝트</category>
      <category>나의DB비서</category>
      <category>아이디어라도 확정돼서 다행이다</category>
      <category>화이팅...</category>
      <author>라이드T</author>
      <guid isPermaLink="true">https://jangcarru20100919.tistory.com/97</guid>
      <comments>https://jangcarru20100919.tistory.com/97#entry97comment</comments>
      <pubDate>Thu, 2 Apr 2026 17:15:00 +0900</pubDate>
    </item>
    <item>
      <title>Multi-Agent</title>
      <link>https://jangcarru20100919.tistory.com/99</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 AI 에이전트가 하나의 팀처럼 협력하여 복잡한 문제를 해결하는 &lt;b data-index-in-node=&quot;51&quot; data-path-to-node=&quot;3&quot;&gt;Multi-Agent&lt;/b&gt; 시스템에 대해 정리한다. 각 분야의 전문가 AI를 만들고, 이들을 지휘하는 팀장을 세우는 것이 핵심이다.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size26&quot;&gt;1. Multi-Agent 시스템이란?&lt;/h2&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;혼자서 모든 일을 다 잘하는 AI를 만들기보다는, 특정 분야에 특화된 에이전트들을 모아 협력하게 만드는 것이 더 효율적이다. 이를 위해 대화 내용을 공유하는 &lt;b data-index-in-node=&quot;88&quot; data-path-to-node=&quot;5&quot;&gt;State&lt;/b&gt;(상태)와 다음 순서를 결정하는 &lt;b data-index-in-node=&quot;111&quot; data-path-to-node=&quot;5&quot;&gt;Supervisor&lt;/b&gt;(팀장)의 역할이 중요하다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;6&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;7&quot; data-ke-size=&quot;size26&quot;&gt;2. 코드 한 줄 리뷰 및 주요 문법 해설&lt;/h2&gt;
&lt;h3 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size23&quot;&gt;2.1 팀의 공동 대화방 설정 (AgentState)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;에이전트들이 정보를 공유할 수 있는 데이터 구조를 정의한다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiVo5GInNGTAxUAAAAAHQAAAAAQhxc&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;class AgentState(TypedDict):
    # 공동 대화창인 messages와 다음 순서를 정하는 next 칸을 만든다.
    messages: Annotated[Sequence[BaseMessage], operator.add]
    next: str
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;11&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,0,0&quot;&gt;코드 리뷰&lt;/b&gt;: TypedDict를 사용하여 딕셔너리에 들어갈 데이터의 형식을 미리 정한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,0&quot;&gt;Python 문법 설명&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;11,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,1,0,0&quot;&gt;Annotated &amp;amp; operator.add&lt;/b&gt;: 보통 변수에 새 값을 넣으면 기존 값은 사라진다. 하지만 여기서 operator.add를 사용하면 새로운 메시지가 생성될 때마다 기존 대화 리스트 뒤에 차곡차곡 쌓이게(append) 된다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,1,1,0&quot;&gt;Sequence&lt;/b&gt;: 리스트나 튜플처럼 순서가 있는 데이터 묶음을 뜻한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;12&quot; data-ke-size=&quot;size23&quot;&gt;2.2 전문가 에이전트 생성 함수&lt;/h3&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;각 팀원에게 역할과 도구를 부여한다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiVo5GInNGTAxUAAAAAHQAAAAAQiBc&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def create_agent(llm, tools, system_prompt):
    prompt = ChatPromptTemplate.from_messages([
        (&quot;system&quot;, system_prompt), # &quot;너는 최고의 프로그래머야&quot; 같은 역할을 부여한다.
        MessagesPlaceholder(variable_name=&quot;messages&quot;), # 이전 대화 내용을 기억하게 한다.
    ])
    return prompt | llm.bind_tools(tools)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;15&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,0,0&quot;&gt;코드 리뷰&lt;/b&gt;: create_agent 함수는 팀원 에이전트를 찍어내는 공장 역할을 한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,1,0&quot;&gt;Python 문법 설명&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;15,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,1,1,0,0&quot;&gt;Pipe 연산자 (|)&lt;/b&gt;: LangChain에서 사용되는 특수 문법으로, 앞 단계의 결과물(Prompt)을 뒷 단계(LLM)로 전달하는 연결 고리다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;16&quot; data-ke-size=&quot;size23&quot;&gt;2.3 팀의 지휘자, Supervisor 노드&lt;/h3&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;팀원들의 대화를 듣고 다음 차례를 지목한다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiVo5GInNGTAxUAAAAAHQAAAAAQiRc&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;members = [&quot;Researcher&quot;, &quot;Coder&quot;]
options = [&quot;FINISH&quot;] + members

class routeResponse(TypedDict):
    next: Literal[*options] # 다음 순서는 무조건 Researcher, Coder, FINISH 중 하나여야 한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;19&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,0,0&quot;&gt;코드 리뷰&lt;/b&gt;: Supervisor는 업무가 끝났는지(FINISH), 아니면 다른 팀원이 더 일해야 하는지 결정한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,1,0&quot;&gt;Python 문법 설명&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;19,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,1,1,0,0&quot;&gt;Literal&lt;/b&gt;: 값의 종류를 엄격하게 제한한다. AI가 팀원 이름이 아닌 엉뚱한 대답을 하지 못하도록 방지한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,1,1,1,0&quot;&gt;Unpacking (*options)&lt;/b&gt;: 리스트 안에 있는 요소들을 하나씩 풀어서 넣어주는 기법이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;20&quot; data-ke-size=&quot;size23&quot;&gt;2.4 에이전트 연결 및 흐름 제어 (Nodes &amp;amp; Edges)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;에이전트들을 마디(&lt;b data-index-in-node=&quot;10&quot; data-path-to-node=&quot;21&quot;&gt;Node&lt;/b&gt;)로 등록하고 연결 고리(&lt;b data-index-in-node=&quot;28&quot; data-path-to-node=&quot;21&quot;&gt;Edge&lt;/b&gt;)를 만든다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiVo5GInNGTAxUAAAAAHQAAAAAQihc&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;workflow = StateGraph(AgentState)
workflow.add_node(&quot;Researcher&quot;, researcher_node)
workflow.add_node(&quot;Coder&quot;, coder_node)
workflow.add_node(&quot;supervisor&quot;, supervisor_node)

# 모든 팀원은 일을 마치면 팀장(supervisor)에게 보고한다.
for member in members:
    workflow.add_edge(member, &quot;supervisor&quot;)

# 팀장은 다음 목적지를 결정한다.
workflow.add_conditional_edges(&quot;supervisor&quot;, lambda x: x[&quot;next&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;23&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;23,0,0&quot;&gt;코드 리뷰&lt;/b&gt;: 일꾼 노드들은 업무 후 항상 팀장에게 돌아간다. 팀장은 자신의 상태 값(next)에 따라 다음 길을 안내한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;23,1,0&quot;&gt;Python 문법 설명&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;23,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;23,1,1,0,0&quot;&gt;lambda x: x[&quot;next&quot;]&lt;/b&gt;: 이름 없는 일회용 함수다. 복잡한 함수 정의 없이 &quot;입력값 x에서 next라는 키의 값만 뽑아서 경로로 써라&quot;는 명령을 한 줄로 표현한 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;24&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;25&quot; data-ke-size=&quot;size26&quot;&gt;3. 요약&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;26&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;26,0,0&quot;&gt;AgentState&lt;/b&gt;: 팀원들이 같이 쓰는 &lt;b data-index-in-node=&quot;23&quot; data-path-to-node=&quot;26,0,0&quot;&gt;공용 게시판&lt;/b&gt;이다. 누가 무슨 말을 했는지 다 적혀 있다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;26,1,0&quot;&gt;Researcher &amp;amp; Coder&lt;/b&gt;: 게시판 내용을 보고 자기 전문 분야에 맞춰 답변을 적는 &lt;b data-index-in-node=&quot;51&quot; data-path-to-node=&quot;26,1,0&quot;&gt;전문가&lt;/b&gt;들이다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;26,2,0&quot;&gt;Supervisor&lt;/b&gt;: 게시판을 실시간으로 확인하는 &lt;b data-index-in-node=&quot;28&quot; data-path-to-node=&quot;26,2,0&quot;&gt;팀장&lt;/b&gt;이다. &quot;이제 코더가 일할 차례네!&quot;, &quot;다 끝났으니 퇴근하자!&quot;라고 결정한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;26,3,0&quot;&gt;Nodes &amp;amp; Edges&lt;/b&gt;: 전문가들은 각자의 방(&lt;b data-index-in-node=&quot;27&quot; data-path-to-node=&quot;26,3,0&quot;&gt;Node&lt;/b&gt;)에 있고, 방 사이에는 팀장실로 가는 복도(&lt;b data-index-in-node=&quot;56&quot; data-path-to-node=&quot;26,3,0&quot;&gt;Edge&lt;/b&gt;)가 있다. 팀장실 문은 팀장의 결정에 따라 다음 방으로 연결되는 마법의 문(Conditional Edge)이다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>AI</category>
      <category>Multiagent</category>
      <category>multiagent요약</category>
      <author>라이드T</author>
      <guid isPermaLink="true">https://jangcarru20100919.tistory.com/99</guid>
      <comments>https://jangcarru20100919.tistory.com/99#entry99comment</comments>
      <pubDate>Thu, 2 Apr 2026 12:00:39 +0900</pubDate>
    </item>
    <item>
      <title>Text-to-SQL RAG</title>
      <link>https://jangcarru20100919.tistory.com/98</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 인공지능이 사람의 말을 듣고 직접 데이터베이스 언어인 &lt;b data-index-in-node=&quot;40&quot; data-path-to-node=&quot;3&quot;&gt;SQL&lt;/b&gt;로 번역해 정답을 찾아내는 &lt;b data-index-in-node=&quot;58&quot; data-path-to-node=&quot;3&quot;&gt;Text-to-SQL RAG&lt;/b&gt; 시스템에 대해 정리한다. 단순히 정보를 찾는 수준을 넘어, 스스로 판단하고 행동하는 &lt;b data-index-in-node=&quot;121&quot; data-path-to-node=&quot;3&quot;&gt;Agent&lt;/b&gt;를 구축하는 과정이다.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size26&quot;&gt;1. 기본 개념 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;5&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;SQLite&lt;/b&gt;: 서버 없이 파일 하나에 모든 데이터를 담는 가벼운 &lt;b data-index-in-node=&quot;36&quot; data-path-to-node=&quot;5,0,0&quot;&gt;Relational Database&lt;/b&gt;다. 실습에서는 음악 데이터가 담긴 'Chinook.db'를 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;RAG (Retrieval-Augmented Generation)&lt;/b&gt;: AI가 모르는 내용을 외부 도서관(DB)에서 찾아보고 대답하는 기술이다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,2,0&quot;&gt;Agent&lt;/b&gt;: 주어진 도구(&lt;b data-index-in-node=&quot;14&quot; data-path-to-node=&quot;5,2,0&quot;&gt;Tools&lt;/b&gt;)를 사용해 스스로 계획을 세우고 실행하는 지능형 시스템이다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,3,0&quot;&gt;LangGraph&lt;/b&gt;: AI의 사고 흐름을 지도처럼 그려주는 도구다. &lt;b data-index-in-node=&quot;37&quot; data-path-to-node=&quot;5,3,0&quot;&gt;Node&lt;/b&gt;(마디)와 &lt;b data-index-in-node=&quot;47&quot; data-path-to-node=&quot;5,3,0&quot;&gt;Edge&lt;/b&gt;(길)로 구성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;6&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;7&quot; data-ke-size=&quot;size26&quot;&gt;2. 코드 한 줄 리뷰 및 파이썬 문법 해설&lt;/h2&gt;
&lt;h3 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size23&quot;&gt;2.1 데이터베이스 내려받기 및 연결&lt;/h3&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwi60Pqns8mTAxUAAAAAHQAAAAAQ1hM&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;&lt;span&gt;Python&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;import requests # 인터넷 주소로 데이터를 요청하는 라이브러리다.

url = &quot;https://storage.googleapis.com/.../Chinook.db&quot;
response = requests.get(url) # 해당 주소의 데이터를 가져온다.

if response.status_code == 200: # 상태 코드가 200이면 '성공'이라는 뜻이다.
    with open(&quot;Chinook.db&quot;, &quot;wb&quot;) as file: # 'wb'는 Write Binary, 즉 이진 데이터를 쓰겠다는 의미다.
        file.write(response.content)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,0&quot;&gt;문법 설명&lt;/b&gt;: with open(...) as file은 파일을 열고 나서 작업이 끝나면 자동으로 닫아주는 안전한 방식이다. 텍스트가 아닌 DB 파일이므로 wb 모드를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size23&quot;&gt;2.2 도구 상자(Toolkit)에서 필요한 도구 꺼내기&lt;/h3&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwi60Pqns8mTAxUAAAAAHQAAAAAQ1xM&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;list_tables_tool = next(tool for tool in tools if tool.name == &quot;sql_db_list_tables&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,0,0&quot;&gt;문법 설명 (List Comprehension &amp;amp; next)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;tool for tool in tools if ... 부분은 &lt;b data-index-in-node=&quot;34&quot; data-path-to-node=&quot;13,0,1,0,0&quot;&gt;List Comprehension&lt;/b&gt;의 변형으로, 리스트 안의 모든 도구를 하나씩 검사하는 짧은 반복문이다.&lt;/li&gt;
&lt;li&gt;next()는 조건에 맞는 첫 번째 결과물이 나오자마자 바로 가져오는 함수다. 효율성을 위해 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,1,0&quot;&gt;사용 이유&lt;/b&gt;: 도구 상자에서 &quot;테이블 목록을 보여줘&quot;라는 특정 기능만 골라내기 위함이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size23&quot;&gt;2.3 SQL 코드 청소하기 (Regex)&lt;/h3&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwi60Pqns8mTAxUAAAAAHQAAAAAQ2BM&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;import re # 정규표현식(Regular Expression) 도구다.

def _sanitize_sql(text: str) -&amp;gt; str:
    # 텍스트 안에서 ```sql ... ``` 로 감싸진 부분만 찾는다.
    m = re.search(r&quot;
http://googleusercontent.com/immersive_entry_chip/0
* **문법 설명**: `state[&quot;messages&quot;][-1]`에서 `-1`은 리스트의 **가장 마지막 요소**를 뜻한다.
* **사용 이유**: AI가 짠 쿼리가 항상 정답일 수는 없다. 실행 결과를 보고 에러가 나면 스스로 수정하게 만드는 **Feedback Loop**를 구축한 것이다.

---

## 3. 요약 및 정리

이 프로젝트는 다음과 같은 단계로 움직인다.
1.  **Preparation**: DB 파일을 준비하고 AI와 연결한다.
2.  **Tooling**: AI에게 DB를 뒤져볼 수 있는 특수 장비 세트를 준다.
3.  **Workflow**: 질문을 받으면 도구를 골라 쿼리를 짜고, 에러가 나면 수정하고, 성공하면 답을 하는 흐름을 만든다.

**결론**: 이 기술을 사용하면 데이터베이스를 모르는 일반 사용자도 AI와 대화하며 복잡한 데이터를 자유자재로 추출할 수 있게 된다. 이것이 바로 지능형 에이전트의 핵심이다.



--- 
**오늘의 학습 포인트**
* `next()`와 **List Comprehension**으로 데이터를 빠르게 필터링한다.
* **Regex**로 AI의 답변에서 필요한 코드만 추출한다.
* **LangGraph**의 갈림길 설계를 통해 스스로 오류를 고치는 에이전트를 만든다.

---eof&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>딥러닝</category>
      <category>Rag</category>
      <category>text-to-sql</category>
      <author>라이드T</author>
      <guid isPermaLink="true">https://jangcarru20100919.tistory.com/98</guid>
      <comments>https://jangcarru20100919.tistory.com/98#entry98comment</comments>
      <pubDate>Tue, 31 Mar 2026 09:27:39 +0900</pubDate>
    </item>
    <item>
      <title>[Meat-A-Eye 배포] 마지막 트러블 슈팅</title>
      <link>https://jangcarru20100919.tistory.com/96</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 PC라는 온실 속에서는 완벽하게 돌아가던 기능들이 우벤투 서버라는 환경에 놓이는 순간 하나둘씩 삐걱거리기 시작했다. 특히 페이지 새로고침 시 발생하는 403 Forbidden 에러와 환경 의존성 문제로 인한 OCR 인식 불능 현상은 배포 단계에서 반드시 해결해야 할 마지막 혈투였다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;3&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size26&quot;&gt;1. 새로고침 403 Forbidden: SPA와 서버 라우팅의 불협화음&lt;/h2&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;배포 후 가장 당혹스러운 현상은 특정 페이지(예: /mypage)에서 새로고침을 누르는 순간 브라우저에 '403 Forbidden' 혹은 '404 Not Found'가 뜨는 것이다. 이는 React와 같은 SPA(Single Page Application)의 라우팅 방식과 웹 서버의 동작 방식이 다르기 때문에 발생한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;원인:&lt;/b&gt; 사용자가 새로고침을 누르면 브라우저는 서버에 /mypage라는 실제 경로를 요청한다. 하지만 서버의 루트 폴더에는 index.html 파일만 있을 뿐 /mypage라는 물리적 폴더는 존재하지 않는다. 서버 입장에서는 존재하지 않거나 권한이 없는 경로로 판단하여 에러를 반환하는 것이다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;해결 방법: Nginx Fallback 설정&lt;/b&gt; 웹 서버가 어떤 경로로 요청을 받더라도 일단 index.html을 먼저 보여주도록 설정하여, 이후의 라우팅 권한을 클라이언트(React Router 등)에게 넘겨주어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjjsrr67L6TAxUAAAAAHQAAAAAQ8yM&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# Nginx 설정 파일 (/etc/nginx/sites-available/default)
location / {
    root   /var/www/html;
    index  index.html;
    # 요청한 파일($uri)이나 폴더($uri/)가 없으면 무조건 index.html로 보낸다
    try_files $uri $uri/ /index.html;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-path-to-node=&quot;8&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size26&quot;&gt;2. OCR 인식 불가능: 환경 전이(Environment Migration)의 실패&lt;/h2&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;로컬 환경에서 기가 막히게 글자를 읽어내던 OCR 기능이 서버 배포 후 기능이 멈췄다. API는 호출되지만 결과값은 빈 문자열이거나 에러 로그만 가득했다. 이는 단순히 코드의 오류가 아니라 서버의 &lt;b data-index-in-node=&quot;110&quot; data-path-to-node=&quot;10&quot;&gt;시스템 의존성&lt;/b&gt; 문제였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;11&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,0,0&quot;&gt;원인:&lt;/b&gt; OCR 엔진(Tesseract 등)은 Python 라이브러리(pytesseract)만 설치한다고 작동하지 않는다. 서버 OS 레벨에서 엔진 자체가 설치되어 있어야 하며, 한글 인식에 필요한 학습 데이터(kor.traineddata)가 누락되었거나 경로 설정이 잘못된 경우가 대부분이다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,0&quot;&gt;해결 방법: 서버 환경 최적화 및 종속성 해결&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;11,1,1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,1,0,0&quot;&gt;시스템 패키지 설치:&lt;/b&gt; 서버에 직접 접속하여 Tesseract 엔진과 언어 팩을 설치했다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,1,1,0&quot;&gt;경로 명시:&lt;/b&gt; 코드 내에서 엔진의 실행 경로를 명확히 지정하여 서버 환경에서의 혼선을 방지했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjjsrr67L6TAxUAAAAAHQAAAAAQ9CM&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# Ubuntu 서버에서의 해결 과정
sudo apt-get update
sudo apt-get install tesseract-ocr
sudo apt-get install tesseract-ocr-kor # 한글 팩 설치 필수
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjjsrr67L6TAxUAAAAAHQAAAAAQ9SM&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# Python 코드 내 해결책
import pytesseract

# 서버 환경에 설치된 tesseract 실행 파일 경로를 직접 지정
pytesseract.pytesseract.tesseract_cmd = r'/usr/bin/tesseract'

# 이미지 인식 시 한글(kor)과 영어(eng)를 동시에 사용하도록 설정
text = pytesseract.image_to_string(image, lang='kor+eng')
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-path-to-node=&quot;14&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;15&quot; data-ke-size=&quot;size26&quot;&gt;3. 프로젝트를 마치며&lt;/h2&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;로컬과 배포 서버는 엄연히 다른 세상이다. 이번 프로젝트를 통해 배포 단계에서 직면하는 문제들을 해결하며 얻은 교훈은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;17&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,0,0&quot;&gt;환경의 일관성:&lt;/b&gt; 프로젝트 시작할때부터 Docker를 도입했다면 시스템 패키지 설치 문제를 더 깔끔하게 해결했을 것이다. 다음 프로젝트에서는 컨테이너 기반 배포를 우선 고려할 것이다...ㅠ&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,1,0&quot;&gt;서버 설정의 이해:&lt;/b&gt; 배포는 단순히 코드를 올리는 행위가 아니라, Nginx와 같은 웹 서버와 애플리케이션의 라우팅 방식을 조율하는 과정임을 깨달았다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,2,0&quot;&gt;로그 기반 대응:&lt;/b&gt; 서버 환경에서는 에러 메시지가 화면에 직접 뜨지 않으므로, 상세한 로깅(Logging) 환경을 구축하는 것이 트러블슈팅의 핵심임을 다시 한번 확인했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;이것으로 외부 API와의 혈투부터 배포 환경의 복병들까지, 모든 개발 일지를 마친다. 서버 비용 때문에 배포 된 거 확인하고 바로 끊었지만, 여전히 이 프로젝트는 나의 가장 자랑스러운 결과물이다 ㅎ&lt;/p&gt;</description>
      <category>Project</category>
      <category>aws배포</category>
      <category>나의우당탕탕배포기</category>
      <category>배포끝</category>
      <category>코린이</category>
      <author>라이드T</author>
      <guid isPermaLink="true">https://jangcarru20100919.tistory.com/96</guid>
      <comments>https://jangcarru20100919.tistory.com/96#entry96comment</comments>
      <pubDate>Mon, 30 Mar 2026 16:14:34 +0900</pubDate>
    </item>
    <item>
      <title>[Meat-A-Eye 배포] API -&amp;gt; SSL 인증 오류</title>
      <link>https://jangcarru20100919.tistory.com/95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발 과정에서 가장 당혹스러운 순간은 로컬 환경에서는 완벽하게 작동하던 기능이 배포 후 서버에서 침묵할 때다..나도 알고싶지 않았다.. 특히 KAMIS(농수산유통정보)와 공공데이터 포털의 영양정보 API를 연동하며 겪은 '외부 API와의 혈투'는 네트워크 보안과 데이터 규격의 중요성을 뼈저리게 느끼게 해주었다. 그 트러블슈팅 기록을 정리한다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;3&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size26&quot;&gt;1. SSL 핸드셰이크 실패: 보안 프로토콜의 세대 차이&lt;/h2&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;가장 먼저 맞닥뜨린 난관은 &lt;b data-index-in-node=&quot;15&quot; data-path-to-node=&quot;5&quot;&gt;SSL(Secure Sockets Layer) 인증&lt;/b&gt; 오류였다. 최신 우분투(Ubuntu)(이제부터 내 인스턴스는 지우고 팀원 서버에서 같이 스터디하며 배포했다.) 서버는 강화된 보안 가이드라인에 따라 최신 TLS 프로토콜을 요구하지만, 일부 오래된 공공기관 API 서버는 여전히 낮은 버전의 보안 규격을 사용하고 있었다. 우리 프로젝트에는 실시간 API가 서로 연동되어있었는데 그중 시세를 나타내는 KAMIS API가 오래되어 규격이 맞지 않아 프론트에 띄워지지 않았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;현상:&lt;/b&gt; requests.exceptions.SSLError 발생. 서버 간의 보안 연결(Handshake) 단계에서 규격 불일치로 연결이 차단됨.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;해결: verify=False와 경고 제어&lt;/b&gt;&lt;/li&gt;
&lt;li data-path-to-node=&quot;6,1,0&quot;&gt;임시방편으로 Python requests 라이브러리의 인증서 검증 기능을 비활성화하여 연결을 강제했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjjsrr67L6TAxUAAAAAHQAAAAAQ3Rs&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import requests
import urllib3

# 인증서 검증 미실시에 따른 경고 메시지 억제
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# verify=False 설정을 통해 보안 검증 우회
response = requests.get(url, verify=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-path-to-node=&quot;8&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-path-to-node=&quot;8,0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0&quot;&gt;참고:&lt;/b&gt; verify=False는 데이터 통신 보안을 취약하게 만들 수 있으므로, 실제 운영 환경에서는 서버의 OpenSSL 설정을 조정하거나 해당 기관에 보안 업데이트를 요청하는 것이 근본적인 해결책이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-path-to-node=&quot;9&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size26&quot;&gt;2. 500 Internal Error: 인코딩(Key)의 함정&lt;/h2&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;공공데이터 포털 API 사용 시 가장 빈번하게 발생하는 오류는 500 Internal Server Error다. 가이드에 적힌 대로 인증키를 입력했음에도 서버가 응답하지 않는 이유는 대부분 &lt;b data-index-in-node=&quot;106&quot; data-path-to-node=&quot;11&quot;&gt;키 인코딩 방식&lt;/b&gt;에 있었다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;12&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12&quot;&gt;중복 인코딩 문제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;공공데이터 포털은 '인코딩된 키'와 '디코딩된 키'를 동시에 제공한다. Python의 requests 라이브러리는 URL 파라미터를 전달할 때 내부적으로 자동 인코딩을 수행한다. 이때 이미 인코딩된 키를 인자로 넘기면 % 기호가 %25로 변하는 &lt;b data-index-in-node=&quot;137&quot; data-path-to-node=&quot;13&quot;&gt;중복 인코딩&lt;/b&gt;이 발생하여 인증에 실패하게 된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;14&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;문제 유형&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;주요 원인&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;대응 방안&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0,0&quot;&gt;Invalid Service Key&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,1,1,0&quot;&gt;인증키 오타 또는 인코딩 중복&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,1,2,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,2,0&quot;&gt;Decoded Key&lt;/b&gt; 사용 생활화&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,2,0,0&quot;&gt;500 Error&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,2,1,0&quot;&gt;API 서버 내부 마비 또는 필수 파라미터 누락&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,2,2,0&quot;&gt;예외 처리 및 재시도(Retry) 로직 구현&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,3,0,0&quot;&gt;Limited Request&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,3,1,0&quot;&gt;일일 호출 트래픽 초과&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,3,2,0&quot;&gt;API 활용 신청 페이지에서 트래픽 증설 신청&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-path-to-node=&quot;15&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;16&quot; data-ke-size=&quot;size26&quot;&gt;3. DB Fallback: 외부의 불안정성에 대비하는 안전장치&lt;/h2&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;외부 API는 본질적으로 우리가 통제할 수 없는 영역이다. API 서버가 점검 중이거나 응답 속도가 비정상적으로 느려질 경우, 우리 서비스 전체의 가용성이 떨어지는 치명적인 문제가 발생한다. 이를 방지하기 위해 &lt;b data-index-in-node=&quot;118&quot; data-path-to-node=&quot;17&quot;&gt;DB Fallback&lt;/b&gt; 전략을 도입했다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;18&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;18&quot;&gt;안전장치 작동 원리&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;19&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,0,0&quot;&gt;Primary:&lt;/b&gt; 실시간 외부 API 호출을 시도한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,1,0&quot;&gt;Exception:&lt;/b&gt; 타임아웃이나 API 에러가 발생할 경우 이를 즉시 캐치한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,2,0&quot;&gt;Fallback:&lt;/b&gt; 우리 DB에 미리 저장(Caching)해둔 과거 데이터나 기본 규격 정보를 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-path-to-node=&quot;20&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-path-to-node=&quot;20,0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20,0&quot;&gt;교훈:&lt;/b&gt; 외부 데이터는 언제든 끊길 수 있다는 전제하에 설계해야 한다. 사용자에게 에러 메시지를 보여주는 것보다, 조금 오래된 데이터일지라도 안정적인 UI를 제공하는 것이 사용자 경험(UX) 측면에서 훨씬 유리하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;외부 API 연동은 단순한 데이터 수신을 넘어, &lt;b data-index-in-node=&quot;27&quot; data-path-to-node=&quot;23&quot;&gt;서로 다른 환경을 가진 두 시스템 사이의 접점을 조율하는 과정&lt;/b&gt;이었다. SSL 버전 차이부터 인코딩 이슈, 그리고 서버 불안정성까지 해결하며 시스템의 견고함을 한 단계 높일 수 있었다. 이제 슬슬 배포 마무리 하고 싶은데 왜 끝이 안나지..? 처음엔 목표가 일주일안에 끝내는 것이었는데, 아무래도 각자 다른 프로젝트를 진행하면서 하루에 1시간 ~1시간 30분씩 진행을 하다보니 진도가 조금 느려졌다. 곧 끝나니 조금만 더 힘내자!&amp;nbsp;&lt;/p&gt;</description>
      <category>Project</category>
      <category>AI</category>
      <category>meataeye배포</category>
      <category>끝이안나네</category>
      <category>프로젝트 배포</category>
      <author>라이드T</author>
      <guid isPermaLink="true">https://jangcarru20100919.tistory.com/95</guid>
      <comments>https://jangcarru20100919.tistory.com/95#entry95comment</comments>
      <pubDate>Sun, 29 Mar 2026 14:52:14 +0900</pubDate>
    </item>
    <item>
      <title>[Meat-A-Eye 배포] GPU 다이어트</title>
      <link>https://jangcarru20100919.tistory.com/94</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;버지니아 리전에 말뚝을 박았다고 끝이 아니었다. 이제는 로컬에서 GPU(RTX)의 힘으로 쌩쌩 돌아가던 무거운 모델들을 CPU 2개뿐인 척박한 서버 환경에 맞춰 '다이어트' 시켜야 하는 더 큰 산이 기다리고 있었다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size23&quot;&gt;1. GPU에서 CPU로: requirements.txt 다이어트&lt;/h3&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;로컬에서는 당연하게 썼던 torch+cu118 같은 GPU 전용 라이브러리들이 서버에서는 독이 됐다. 수 GB에 달하는 용량은 서버를 숨 막히게 했고, GPU가 없는 환경이라 에러를 뱉어냈다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;7&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,0,0&quot;&gt;핵심 조치:&lt;/b&gt; requirements.txt에서 GPU 관련 태그를 싹 걷어내고 &lt;b data-index-in-node=&quot;44&quot; data-path-to-node=&quot;7,0,0&quot;&gt;CPU 전용 버전&lt;/b&gt;으로 교체했다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0&quot;&gt;코드 수정:&lt;/b&gt; .to('cuda')라고 박혀 있던 코드들을 device = torch.device(&quot;cuda&quot; if torch.cuda.is_available() else &quot;cpu&quot;)로 수정해 어떤 환경에서도 유연하게 돌아가게 만들었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size23&quot;&gt;2. &quot;No space left on device&quot;: 용량 부족과의 전쟁&lt;/h3&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;도커(Docker) 빌드를 돌리자마자 터진 에러. 기본 8GB였던 EBS 용량으로는 거대한 딥러닝 이미지들을 감당할 수 없었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,0&quot;&gt;해결:&lt;/b&gt; 인스턴스를 멈추고 EBS 볼륨을 20GB 이상으로 확장했다. 단순히 늘리는 게 끝이 아니라 리눅스 내부에서 파일 시스템까지 확장해 줘야 비로소 서버가 가동 됐다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size23&quot;&gt;3. DB 리전 이사: 서울은 버리고 버지니아로&lt;/h3&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;리전이 바뀌었으니 데이터베이스(RDS)도 새로 파야 했다. ㅠㅠ 하 서울에 있던 기존 DB는 미련 없이 삭제(물론 백업은 확인했다!)하고 버지니아에 MariaDB 인스턴스를 새로 구축했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,0,0&quot;&gt;삭제의 결단:&lt;/b&gt; 팀원과 상의 끝에 데이터 꼬임을 방지하기 위해 '싹 밀고 다시 시작'하기로 했다. 그래도 db 생성을 다시 하는 게 낫지.. 도커 컨테이너 빌드를 다시 할 생각 하면 끔찍함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size23&quot;&gt;4. 트러블슈팅: RDS '퍼블릭 액세스' 락(Lock) 해제&lt;/h3&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;DB를 만들 때 가장 당황했던 순간. AWS RDS 설정에서 EC2 컴퓨팅 리소스에 연결을 선택하니, 아래에 있는 &lt;b data-index-in-node=&quot;70&quot; data-path-to-node=&quot;15&quot;&gt;'퍼블릭 액세스: 예'&lt;/b&gt; 버튼이 회색으로 변하며 죽어버렸다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;16&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,0,0&quot;&gt;문제:&lt;/b&gt; AWS는 보안을 위해 &quot;EC2랑 연결할 거면 외부(내 노트북)에서는 접속 못 하게 막을게!&quot;라고 고집을 부린 것.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,1,0&quot;&gt;해결:&lt;/b&gt; 결국 'EC2 연결 안 함'으로 두고 '퍼블릭 액세스: 예'를 활성화해 생성했다. 그다음 &lt;b data-index-in-node=&quot;54&quot; data-path-to-node=&quot;16,1,0&quot;&gt;보안 그룹(Security Group)&lt;/b&gt; 설정에 들어가서 내 노트북 IP와 EC2 서버의 보안 그룹 ID를 수동으로 하나하나 등록해 줬다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,2,0&quot;&gt;결과:&lt;/b&gt; 이제 내 노트북(HeidiSQL)에서도, 서버(API)에서도 이 DB로 자유롭게 대화할 수 있는 통로가 뚫렸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;환경 최적화와 DB 구축은 화려하진 않지만 서비스의 '심장'과 '혈관'을 만드는 작업이었다. 이제 서버는 고정 IP(탄력적 IP)까지 부여받아 당당히 버지니아의 한 자리를 차지했다. 하지만 기쁨도 잠시, 우리를 기다리는 건 &lt;b data-index-in-node=&quot;23&quot; data-path-to-node=&quot;19&quot;&gt;오래된 공공데이터 API(KAMIS)와의 SSL 혈투&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project</category>
      <category>DevOps</category>
      <category>meataeye배포</category>
      <category>배포트러블슈팅</category>
      <author>라이드T</author>
      <guid isPermaLink="true">https://jangcarru20100919.tistory.com/94</guid>
      <comments>https://jangcarru20100919.tistory.com/94#entry94comment</comments>
      <pubDate>Sat, 28 Mar 2026 13:33:09 +0900</pubDate>
    </item>
    <item>
      <title>벡터(Vector)의 종류</title>
      <link>https://jangcarru20100919.tistory.com/93</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;인공지능이 텍스트나 이미지를 이해하기 위해서는 데이터를 숫자의 나열인 벡터(Vector)로 변환해야 한다. 이를 벡터화(Vectorization)라고 하며, 변환 방식에 따라 크게 세 가지 종류로 나뉜다. 각 벡터의 특징을 이해하면 서비스의 목적에 맞는 최적의 알고리즘을 선택할 수 있다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size23&quot;&gt;1. 밀집 벡터 (Dense Vector)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;밀집 벡터는 데이터의 의미를 수천 개의 실수(예: 0.12, -0.54)로 표현하는 방식이다. 거의 모든 차원에 숫자가 채워져 있어 '밀집'이라는 이름이 붙었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;특징:&lt;/b&gt; 단어의 표면적인 철자보다는 문맥과 '의미적 유사도'를 파악하는 데 특화되어 있다. 예를 들어 &quot;강아지&quot;와 &quot;댕댕이&quot;라는 단어가 서로 달라도, 인공지능은 두 단어의 벡터 주소를 가깝게 배치하여 같은 의미임을 이해한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;장점:&lt;/b&gt; 유연한 검색이 가능하며, 질문자의 의도를 파악하는 능력이 뛰어나다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,2,0&quot;&gt;단점:&lt;/b&gt; 계산량이 많아 검색 속도가 상대적으로 느릴 수 있고, 정확한 고유 명사(모델명, 품번 등)를 찾는 데는 약할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;7&quot; data-ke-size=&quot;size23&quot;&gt;2. 희소 벡터 (Sparse Vector)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;희소 벡터는 전체 단어 사전 중 특정 단어의 존재 여부를 0과 1(또는 빈도수)로 표현하는 방식이다. 대부분의 값이 0으로 채워지기 때문에 '희소'하다고 표현한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;특징:&lt;/b&gt; 의미보다는 '키워드의 일치 여부'에 집중한다. 전통적인 검색 엔진에서 주로 사용하던 방식이다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;장점:&lt;/b&gt; 정확한 키워드 매칭이 필요한 경우(예: 제품 번호 검색, 특정 법령 조항 검색) 매우 강력한 성능을 발휘한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,2,0&quot;&gt;단점:&lt;/b&gt; &quot;신발&quot;을 검색했을 때 &quot;구두&quot;나 &quot;운동화&quot;처럼 의미는 비슷하지만 철자가 다른 단어는 찾아내지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size23&quot;&gt;3. 양자화 벡터 (Quantized Vector)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;양자화 벡터는 밀집 벡터의 정밀한 숫자들을 더 작은 단위(예: 정수나 이진수)로 압축한 형태다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,0&quot;&gt;특징:&lt;/b&gt; 데이터의 용량을 획기적으로 줄여 메모리 효율을 높인다. 32비트 실수를 8비트나 1비트로 변환하여 저장 공간을 아낀다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,0&quot;&gt;장점:&lt;/b&gt; 검색 속도가 압도적으로 빨라지며, 수억 개의 대규모 데이터를 다룰 때 필수적이다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,2,0&quot;&gt;단점:&lt;/b&gt; 숫자를 단순화하는 과정에서 정보의 손실이 발생하여 검색 정확도가 소폭 하락할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;13&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size23&quot;&gt;요약 및 활용 (Hybrid Search)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;최근에는 밀집 벡터와 희소 벡터의 장점을 결합한 &lt;b data-index-in-node=&quot;27&quot; data-path-to-node=&quot;15&quot;&gt;하이브리드 검색(Hybrid Search)&lt;/b&gt; 방식이 널리 쓰인다. 희소 벡터로 정확한 키워드를 걸러내고, 밀집 벡터로 문맥상의 의미를 보완하여 정확도를 극대화하는 방식이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;16&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;밀집 벡터 (Dense)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;희소 벡터 (Sparse)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;양자화 벡터 (Quantized)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,1,0,0&quot;&gt;핵심 키워드&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,1,1,0&quot;&gt;문맥, 의미 유사성&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,1,2,0&quot;&gt;키워드 일치, 빈도&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,1,3,0&quot;&gt;압축, 속도 최적화&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,2,0,0&quot;&gt;주요 용도&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,2,1,0&quot;&gt;챗봇, 추천 시스템&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,2,2,0&quot;&gt;상품 검색, 기술 문서&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,2,3,0&quot;&gt;대규모 실시간 검색&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,3,0,0&quot;&gt;데이터 형태&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,3,1,0&quot;&gt;0이 거의 없는 실수값&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,3,2,0&quot;&gt;대부분 0인 정수값&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,3,3,0&quot;&gt;단순화된 정수/이진값&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;결론적으로, 인공지능 서비스의 성격에 따라 적절한 벡터 타입을 선택하거나 혼합하여 사용하는 것이 검색 품질을 결정하는 핵심 요소가 된다.&lt;/p&gt;</description>
      <category>VectorDB</category>
      <category>벡터종류</category>
      <category>양자화벡터</category>
      <author>라이드T</author>
      <guid isPermaLink="true">https://jangcarru20100919.tistory.com/93</guid>
      <comments>https://jangcarru20100919.tistory.com/93#entry93comment</comments>
      <pubDate>Fri, 27 Mar 2026 16:11:06 +0900</pubDate>
    </item>
    <item>
      <title>Vector DB</title>
      <link>https://jangcarru20100919.tistory.com/92</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 벡터 데이터베이스란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 데이터베이스는 텍스트, 이미지, 오디오 같은 데이터를 고차원 벡터(숫자 배열)로 변환해 저장하고, 이 벡터들 간의 유사도를 빠르게 검색할 수 있도록 최적화된 데이터베이스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 데이터베이스(MySQL 등)와의 차이를 비교하면 이렇다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;gcode&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;일반 DB (MySQL):
&quot;강아지&quot; 검색 &amp;rarr; &quot;강아지&quot;라는 단어가 정확히 있는 것만 찾음

벡터 DB (ChromaDB):
&quot;강아지&quot; 검색 &amp;rarr; &quot;강아지&quot;, &quot;개&quot;, &quot;반려동물&quot;, &quot;puppy&quot; 모두 비슷한 것으로 찾음
(의미가 비슷하면 찾아낸다)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 의미적 유사성(semantic similarity) 기반 검색이라고 한다. 유사도를 계산하는 수학적 방법으로는 코사인 유사도, 내적(dot product), 유클리드 거리 등을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 추천 시스템, 검색 엔진, RAG(검색 증강 생성) 등에 활용된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. ChromaDB란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChromaDB는 대표적인 오픈소스 벡터 데이터베이스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징은 다음과 같다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;- 파이썬 기반으로 사용이 간편하다
- LangChain과 잘 통합된다
- 벡터 인덱싱 + 메타데이터 저장을 함께 지원한다
- 단순 유사도 검색뿐 아니라 조건 필터링도 가능하다
- 무료 오픈소스이며 로컬 환경부터 클라우드까지 확장 가능하다&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 청크(Chunk)란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청크는 긴 문서를 작은 조각으로 나눈 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 나누냐면, LLM(GPT 같은 AI)은 한 번에 처리할 수 있는 글자 수(토큰 수)에 한계가 있기 때문이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;PDF 문서 (29페이지, 수만 자)
    &amp;darr; 청킹
[조각1: 1~1000자] [조각2: 800~1800자] [조각3: ...]
    &amp;darr;
각 조각을 임베딩 벡터로 변환해서 벡터DB에 저장
    &amp;darr;
질문이 들어오면 &amp;rarr; 관련 조각만 꺼내서 &amp;rarr; LLM에게 전달&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 파트 1 &amp;mdash; PDF 로드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 1 &amp;mdash; PDF 불러오기&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;from langchain_community.document_loaders import PyPDFLoader

file_path = &quot;/content/AI브리프_3월_260303.pdf&quot;
loader = PyPDFLoader(file_path)
pages = loader.load()

print(f'페이지 수: {len(pages)}')
print(pages[0].metadata)
print(pages[0].page_content[:300])&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PyPDFLoader란?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PDF 파일을 파이썬으로 읽어오는 도구다. .load()를 호출하면 페이지별로 Document 객체 리스트를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Document 객체 구조&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;pages[0].metadata    # 문서 정보 (페이지 번호, 파일명, 생성일 등)
pages[0].page_content  # 실제 텍스트 내용&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬 기초 &amp;mdash; f-string&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;hsp&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;print(f'페이지 수: {len(pages)}')
# f&quot;&quot; 안에서 {} 안의 코드가 실행된 결과가 들어간다
# len(pages) &amp;rarr; 리스트의 길이 (페이지 수)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 파트 2 &amp;mdash; 청킹 방법 3가지&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법 1 &amp;mdash; 일반 Chunking (크기 기준)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1000자씩 잘라내는 방식이다. 문단을 무시하고 자르기 때문에 문장이 중간에 잘릴 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,      # 1000자 정도씩 자름
    chunk_overlap=200,    # 이전 청크 끝 200자를 다음 청크에도 포함
    add_start_index=True  # 원문 위치 정보 추가 (하이라이팅, 출처 표시용)
)

docs = text_splitter.split_documents(pages)
print(f'총 {len(docs)}개 만큼의 문서로 청킹되었습니다.')&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chunk_size = 1000이란?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;chunk_size=1000
# 하나의 청크(조각)를 최대 1000자로 제한&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chunk_overlap = 200이란?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;autohotkey&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;chunk_overlap=200
# 이전 청크의 마지막 200자를 다음 청크의 앞부분에 다시 포함시킨다
```

왜 겹치게 하냐면, 문장이 청크 경계에서 잘릴 때 의미가 끊기는 것을 막기 위해서다.
```
청크1: &quot;앤트로픽은 자사의 안전 정책을 발표했다. 이 정책은...&quot;
청크2: &quot;이 정책은 AI 안전 등급 체계를 도입하며...&quot;  &amp;larr; 앞 200자가 겹침&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겹치는 부분이 없으면 청크 경계에서 맥락이 끊겨서 검색 품질이 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;add_start_index = True란?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;add_start_index=True
# 각 청크가 원문의 몇 번째 글자에서 시작했는지 기록해둔다
# 나중에 출처를 표시하거나 원문을 하이라이팅할 때 유용하다&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 확인&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;for doc in docs[:3]:
    print(doc.metadata)       # {'page': 1, 'start_index': 0, ...}
    print(doc.page_content[:500])  # 실제 청크 내용
    print(&quot;-&quot; * 100)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬 기초 &amp;mdash; 슬라이싱&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;docs[:3]  # 리스트의 처음 3개만 가져옴
doc.page_content[:500]  # 문자열의 처음 500자만 가져옴
```

---

### 방법 2 &amp;mdash; SemanticChunker (의미 기준)

단순히 글자 수로 자르는 게 아니라 문장의 의미를 보고 자르는 방식이다.

동작 방식은 이렇다.
```
1. 텍스트를 문장 단위로 나눔
2. 각 문장을 임베딩(벡터)으로 변환
3. 문장 간 유사도 계산
4. 유사도가 낮아지는 지점(주제가 바뀌는 지점)에서 청크를 끊음
5. 의미가 이어지면 계속 같은 청크로 묶음&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;erlang&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings

# OpenAIEmbeddings(): 내부적으로 임베딩 API를 호출. 토큰 수 기준으로 과금
semantic_splitter = SemanticChunker(OpenAIEmbeddings())
semantic_docs = semantic_splitter.split_documents(pages)
print(f'총 {len(semantic_docs)}개 만큼의 문서로 청킹되었습니다.')
```

OpenAIEmbeddings()란?

OpenAI의 임베딩 API를 호출하는 도구다. 텍스트를 숫자 벡터로 변환해준다. 호출할 때마다 비용이 발생한다(토큰 기준 과금).

SemanticChunker의 단점
```
- 청크 크기가 불균형 (어떤 건 100자, 어떤 건 2000자)
- 속도가 느림 (임베딩 + 유사도 계산이 필요하기 때문)
- 비용 증가 (OpenAI 임베딩을 사용하면)
- 완벽하지 않음 (애매한 문장은 잘못 묶일 수 있음)
- 튜닝 난이도 있음 (threshold 잘못 설정하면 너무 잘게 쪼개지거나 너무 크게 묶임)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 사용 방식 1 &amp;mdash; Hybrid 방식&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;# 1차: 의미 기반으로 청킹
semantic_docs = semantic_splitter.split_documents(pages)

# 2차: 크기 제한으로 다시 청킹 (너무 큰 청크 방지)
splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200
)
final_docs = splitter.split_documents(semantic_docs)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 사용 방식 2 &amp;mdash; threshold 조정&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;nix&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;semantic_splitter = SemanticChunker(
    OpenAIEmbeddings(),
    breakpoint_threshold_type='percentile',  # 상위 몇 %를 기준으로 끊을지
    breakpoint_threshold_amount=95           # 상위 95% 차이 나는 곳에서 끊음
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;threshold(임계값)를 높이면 덜 자주 끊고, 낮추면 더 자주 끊는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 파트 3 &amp;mdash; 벡터 리트리버&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;벡터 리트리버란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 리트리버는 질문을 벡터로 변환한 뒤, 벡터DB에 저장된 청크들과 유사도를 계산해서 가장 관련 있는 결과를 찾아주는 검색기다. 의미를 이해하는 검색 엔진이라고 보면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 2 &amp;mdash; ChromaDB에 문서 저장&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;nix&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

# large: 고품질(정확도 높음), 벡터 차원: 3072
# small: 벡터 차원: 1536
embeddings = OpenAIEmbeddings(model='text-embedding-3-large')

vectorstore = Chroma.from_documents(
    documents=docs,           # 저장할 청크 리스트
    embedding=embeddings,     # 임베딩 함수
    persist_directory=&quot;./chroma_db&quot;  # 저장 경로 (파일로 저장)
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chroma.from_documents()의 매개변수 설명&lt;/p&gt;
&lt;div&gt;매개변수설명
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;documents&lt;/td&gt;
&lt;td&gt;벡터 저장소에 추가할 청크(Document) 리스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;embedding&lt;/td&gt;
&lt;td&gt;텍스트를 벡터로 변환하는 임베딩 함수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;persist_directory&lt;/td&gt;
&lt;td&gt;벡터DB를 파일로 저장할 폴더 경로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;collection_name&lt;/td&gt;
&lt;td&gt;생성할 컬렉션 이름 (기본값 있음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ids&lt;/td&gt;
&lt;td&gt;문서 ID 리스트 (기본값은 자동 생성)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;persist_directory란?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;vala&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;persist_directory=&quot;./chroma_db&quot;
# 벡터DB를 메모리가 아니라 파일로 저장한다
# 프로그램을 껐다 켜도 다시 불러올 수 있다
# 지정하지 않으면 메모리에만 저장되어 프로그램 종료 시 사라진다&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 3 &amp;mdash; 유사도 검색&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;query = &quot;개인 정보 보호와 관련하여 앤트로픽의 규제 사항을 우선 순위 높은 것부터 5가지를 정리하여 나열하시오.&quot;
results = vectorstore.similarity_search(query, k=3)
print(results)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;similarity_search(query, k=3)이란?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;similarity_search(
    query,  # 검색할 질문 텍스트
    k=3     # 가장 유사한 상위 k개 결과를 반환
)
```

동작 흐름
```
1. query 텍스트를 임베딩 벡터로 변환
2. DB에 저장된 모든 청크 벡터들과 유사도 비교
3. 유사도 높은 상위 3개 청크를 반환&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 4 &amp;mdash; retriever 객체로 변환&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;autohotkey&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;# vectorstore를 'retriever' 객체 형태로 변환해서 사용하는 표준 RAG 방식
# similarity_search 보다 더 표준화된 방식
vector_retriever = vectorstore.as_retriever(search_kwargs={'k': 3})
vector_result = vector_retriever.invoke(query)
```

as_retriever()란?

vectorstore를 LangChain의 표준 검색기(Retriever) 인터페이스로 변환하는 것이다.

similarity_search()와의 차이
```
similarity_search()  &amp;rarr; 직접 호출하는 방식, 간단하지만 비표준
as_retriever()       &amp;rarr; 표준 RAG 방식, 다른 LangChain 컴포넌트와 쉽게 연결됨
```

---

## 7. 파트 4 &amp;mdash; BM25 리트리버

### BM25 리트리버란?

BM25는 전통적인 키워드 기반 검색 방식이다. 임베딩 없이 텍스트 자체의 단어 빈도로 검색한다. 엘라스틱서치(ElasticSearch) 기술에도 들어가 있다.
```
벡터 리트리버:   &quot;강아지&quot; &amp;rarr; 임베딩 &amp;rarr; 의미적으로 비슷한 것 검색
BM25 리트리버:   &quot;강아지&quot; &amp;rarr; 단어 빈도 계산 &amp;rarr; &quot;강아지&quot;가 많이 나온 것 검색
```

BM25가 계산하는 것들
```
- 단어 빈도(Term Frequency): 이 단어가 문서에 몇 번 나왔나?
- 역문서 빈도(Inverse Document Frequency): 이 단어가 드문 단어인가?
- 문서 길이: 짧은 문서에 같은 빈도로 나오면 더 관련 있다고 봄&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 5 &amp;mdash; BM25 리트리버&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;from langchain_community.retrievers import BM25Retriever

bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 3  # 상위 3개 반환
bm25_result = bm25_retriever.invoke(query)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬 기초 &amp;mdash; 속성 직접 설정&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;bm25_retriever.k = 3
# 객체의 속성(k)에 직접 값을 설정하는 방식
# as_retriever(search_kwargs={'k': 3})와 동일한 역할
```

BM25의 장단점
```
장점:
- 임베딩 API 호출 없음 &amp;rarr; 비용 없음
- 빠름

단점:
- 의미 이해 불가
- &quot;개&quot;로 검색하면 &quot;강아지&quot;는 못 찾음
- 정확한 키워드가 있어야 잘 작동함
```

---

## 8. 파트 5 &amp;mdash; 앙상블 리트리버

### 앙상블 리트리버란?

앙상블 리트리버는 여러 종류의 리트리버를 조합해서 더 정확하고 풍부한 검색 결과를 제공하는 방법이다.
```
BM25 리트리버 (키워드 일치 강점)
    +
벡터 리트리버 (의미 이해 강점)
    =
앙상블 리트리버 (둘의 장점을 합침)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 6 &amp;mdash; 앙상블 리트리버&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;from langchain_classic.retrievers import EnsembleRetriever

ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.6, 0.4]  # 키워드(0.6), 의미(0.4)
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;weights=[0.6, 0.4]란?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;vala&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;weights=[0.6, 0.4]
# BM25 결과에 60% 가중치, 벡터 결과에 40% 가중치를 준다
# 두 리트리버의 결과를 이 비율로 섞어서 최종 순위를 결정한다
# 상황에 따라 조정 가능&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 가중치를 BM25에 더 주는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 노트북에서는 키워드 검색이 의미 검색보다 이 문서에 더 적합하다고 판단해서 BM25에 0.6을 준 것이다. 상황에 따라 반대로 설정할 수도 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 파트 6 &amp;mdash; RAG 그래프 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름을 LangGraph로 연결하는 부분이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 7 &amp;mdash; State 정의&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;from langgraph.graph import StateGraph, MessagesState, START, END

class State(MessagesState):
    context: str&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MessagesState란?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;# LangGraph에서 제공하는 기본 State 클래스
# 내부에 이미 messages 필드가 Annotated[list, add_messages]로 정의돼 있다
# 직접 class State(TypedDict): messages: Annotated[list, add_messages] 를 쓰는 것과 동일

class State(MessagesState):
    context: str  # MessagesState에 context 필드를 추가한 것&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 8 &amp;mdash; retriever 노드&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;pf&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;def retriever(state: State):
    print(&quot;----------RETRIEVER----------&quot;)
    query = state[&quot;messages&quot;][0].content  # 첫 번째 메시지 (사용자 질문)
    ensemble_result = ensemble_retriever.invoke(query)

    content = ensemble_result[0].page_content  # 가장 유사한 청크의 내용
    print(&quot;[CONTEXT]\n&quot;, content)
    return {&quot;context&quot;: content}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;state[&quot;messages&quot;][0].content란?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;markdown&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;state[&quot;messages&quot;][0]  # 메시지 리스트의 첫 번째 메시지 (사용자 질문)
.content              # 그 메시지의 텍스트 내용&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return {&quot;context&quot;: content}란?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;autoit&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;# State에 &quot;context&quot; 값을 업데이트한다
# 다음 노드(answer)에서 state[&quot;context&quot;]로 이 값에 접근할 수 있다&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 9 &amp;mdash; answer 노드&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;python&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model='gpt-5.4-2026-03-05', temperature=0)
prompt = ChatPromptTemplate.from_messages(
    [
        (
           &quot;system&quot;,
           &quot;&quot;&quot;당신은 검색된 문서를 바탕으로 질문에 답하는 도우미입니다.
           반드시 한국어로 답변하세요.
           모르는 내용은 억지로 추측하지 말고 모른다고 답하세요.
           [검색 문맥]
           {context}
           &quot;&quot;&quot;
        ), (&quot;human&quot;, &quot;{question}&quot;)
    ]
)

def answer(state: State):
    print('---------- ANSWER ---------')
    query = state['messages'][-1].content  # 마지막 메시지 (사용자 질문)
    context = state['context']             # retriever 노드에서 가져온 검색 결과
    chain = prompt | llm
    response = chain.invoke(
        {
            &quot;context&quot;: context,
            &quot;question&quot;: query
        }
    )
    return {'messages': [response]}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;temperature=0이란?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;vala&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;temperature=0
# AI의 창의성/랜덤성을 0으로 설정 &amp;rarr; 항상 가장 확실한 답변만 한다
# 0에 가까울수록 일관된 답, 1에 가까울수록 다양한 답
# RAG처럼 정확한 정보 전달이 중요한 경우 temperature=0을 사용&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;{context}와 {question}이란?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;prompt = ChatPromptTemplate.from_messages([
    (&quot;system&quot;, &quot;...[검색 문맥]\n{context}&quot;),  # 나중에 실제 검색 결과가 채워짐
    (&quot;human&quot;, &quot;{question}&quot;)                   # 나중에 실제 질문이 채워짐
])

chain.invoke({
    &quot;context&quot;: context,   # {context} 자리에 들어갈 실제 값
    &quot;question&quot;: query     # {question} 자리에 들어갈 실제 값
})&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 10 &amp;mdash; 그래프 연결&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;graph_builder.add_node('retriever', retriever)
graph_builder.add_node('answer', answer)

graph_builder.add_edge(START, 'retriever')
graph_builder.add_edge('retriever', 'answer')
graph_builder.add_edge('answer', END)

graph = graph_builder.compile()
```

흐름
```
START &amp;rarr; retriever 노드 &amp;rarr; answer 노드 &amp;rarr; END
          (문서 검색)      (LLM 답변 생성)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 11 &amp;mdash; 그래프 실행&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;from langchain_core.messages import HumanMessage

response = graph.invoke(
    {
        &quot;messages&quot;: [HumanMessage(content='레스토랑 치폴레는 대화형 AI를 활용했더니 어떻게 됐어?')]
    }
)

for msg in response['messages']:
    msg.pretty_print()
```

실제 실행 흐름
```
1. 사용자 질문: &quot;레스토랑 치폴레는 대화형 AI를 활용했더니 어떻게 됐어?&quot;
       &amp;darr;
2. retriever 노드:
   - 질문을 벡터로 변환
   - 앙상블 리트리버로 관련 청크 검색
   - 가장 관련 있는 청크를 context로 저장
       &amp;darr;
3. answer 노드:
   - context + 질문을 프롬프트에 채워 넣음
   - LLM에게 전달 &amp;rarr; &quot;치폴레는 대화형 AI로 채용 속도를 75% 높였다&quot;는 답변 생성
       &amp;darr;
4. 최종 답변 출력
```

---

## 10. 전체 흐름 최종 요약
```
PDF 문서 로드 (PyPDFLoader)
    &amp;darr;
청킹 (RecursiveCharacterTextSplitter 또는 SemanticChunker)
    &amp;darr;
임베딩 변환 (OpenAIEmbeddings)
    &amp;darr;
벡터DB 저장 (ChromaDB)
    &amp;darr;
리트리버 생성
  - 벡터 리트리버 (의미 기반)
  - BM25 리트리버 (키워드 기반)
  - 앙상블 리트리버 (둘을 조합)
    &amp;darr;
LangGraph RAG 구성
  retriever 노드 &amp;rarr; answer 노드
    &amp;darr;
사용자 질문 &amp;rarr; 관련 문서 검색 &amp;rarr; LLM 답변 생성&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 핵심 용어 정리&lt;/h2&gt;
&lt;div&gt;용어설명
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;벡터 데이터베이스&lt;/td&gt;
&lt;td&gt;임베딩 벡터로 데이터를 저장하고 의미 기반으로 검색하는 DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChromaDB&lt;/td&gt;
&lt;td&gt;대표적인 오픈소스 벡터 데이터베이스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;청크(Chunk)&lt;/td&gt;
&lt;td&gt;긴 문서를 작은 조각으로 나눈 것&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;chunk_size&lt;/td&gt;
&lt;td&gt;한 청크의 최대 글자 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;chunk_overlap&lt;/td&gt;
&lt;td&gt;청크 간 겹치는 글자 수 (맥락 유지용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RecursiveCharacterTextSplitter&lt;/td&gt;
&lt;td&gt;글자 수 기준으로 자르는 청커&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SemanticChunker&lt;/td&gt;
&lt;td&gt;의미 유사도 기준으로 자르는 청커&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;임베딩(Embedding)&lt;/td&gt;
&lt;td&gt;텍스트를 숫자 벡터로 변환하는 것&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;similarity_search&lt;/td&gt;
&lt;td&gt;유사도 기반으로 청크를 검색하는 메서드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;as_retriever&lt;/td&gt;
&lt;td&gt;vectorstore를 표준 검색기 인터페이스로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BM25&lt;/td&gt;
&lt;td&gt;키워드 빈도 기반의 전통적 검색 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;앙상블 리트리버&lt;/td&gt;
&lt;td&gt;여러 리트리버를 가중치로 조합한 검색기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;persist_directory&lt;/td&gt;
&lt;td&gt;벡터DB를 파일로 저장하는 경로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;temperature&lt;/td&gt;
&lt;td&gt;LLM의 창의성/랜덤성 설정값 (0=일관성, 1=다양성)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MessagesState&lt;/td&gt;
&lt;td&gt;LangGraph에서 messages 필드가 내장된 기본 State 클래스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAG&lt;/td&gt;
&lt;td&gt;검색 증강 생성 &amp;mdash; 문서 검색 결과를 LLM에게 제공해 정확도를 높이는 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;</description>
      <category>딥러닝</category>
      <category>chunk</category>
      <category>embedding</category>
      <category>Rag</category>
      <category>VectorDB</category>
      <author>라이드T</author>
      <guid isPermaLink="true">https://jangcarru20100919.tistory.com/92</guid>
      <comments>https://jangcarru20100919.tistory.com/92#entry92comment</comments>
      <pubDate>Thu, 26 Mar 2026 09:13:01 +0900</pubDate>
    </item>
  </channel>
</rss>