<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>오늘도, 코드더하기</title>
    <link>https://todaycodeplus.tistory.com/</link>
    <description>Autocrypt라는 회사에서 데이터 엔지니어링을 하고있습니다!</description>
    <language>ko</language>
    <pubDate>Sun, 2 Aug 2026 00:20:09 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>크라우니</managingEditor>
    <image>
      <title>오늘도, 코드더하기</title>
      <url>https://tistory1.daumcdn.net/tistory/4751077/attach/2848c352d2f24c568bc917cae57fd57d</url>
      <link>https://todaycodeplus.tistory.com</link>
    </image>
    <item>
      <title>[PostgreSQL] Vector Search를 위한 인덱스 타입 비교 (HNSW vs IVFFlat)</title>
      <link>https://todaycodeplus.tistory.com/82</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;pg.png&quot; data-origin-width=&quot;221&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0glAm/btsKIThZt0G/fGtInptkyTRRkAgbo1Fge1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0glAm/btsKIThZt0G/fGtInptkyTRRkAgbo1Fge1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0glAm/btsKIThZt0G/fGtInptkyTRRkAgbo1Fge1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0glAm%2FbtsKIThZt0G%2FfGtInptkyTRRkAgbo1Fge1%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;221&quot; height=&quot;228&quot; data-filename=&quot;pg.png&quot; data-origin-width=&quot;221&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 요즘 AI 시대를 맞이하여 벡터 검색(Vector Search)이 정말 핫한데요. PostgreSQL에서도 pgvector 확장을 통해 벡터 검색을 지원하고 있습니다. 오늘은 벡터 검색에 사용되는 두 가지 주요 인덱스 타입인 HNSW와 IVFFlat에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;무엇이 다를까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 두 인덱스의 주요 특징을 간단히 비교해볼까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HNSW (Hierarchical Navigable Small World)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그래프 기반 인덱스&lt;/li&gt;
&lt;li&gt;빠른 검색 속도&lt;/li&gt;
&lt;li&gt;더 많은 메모리 사용&lt;/li&gt;
&lt;li&gt;인덱스 생성이 더 오래 걸림&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IVFFlat (Inverted File Flat)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클러스터 기반 인덱스&lt;/li&gt;
&lt;li&gt;적은 메모리 사용&lt;/li&gt;
&lt;li&gt;빠른 인덱스 생성&lt;/li&gt;
&lt;li&gt;HNSW보다는 조금 느린 검색 속도&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 사용 예제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 예제로 두 인덱스의 사용법을 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- pgvector 확장 설치
CREATE EXTENSION vector;

-- 테스트용 테이블 생성
CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    content TEXT,
    embedding vector(1536)  -- OpenAI의 embedding 차원
);

-- HNSW 인덱스 생성
CREATE INDEX hnsw_idx ON documents 
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- IVFFlat 인덱스 생성
CREATE INDEX ivf_idx ON documents 
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;각 인덱스의 동작 원리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HNSW의 동작 방식&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- HNSW 검색 예제
SELECT content, 
       1 - (embedding &amp;lt;=&amp;gt; '[vector_values]') as similarity
FROM documents
ORDER BY embedding &amp;lt;=&amp;gt; '[vector_values]'
LIMIT 5;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HNSW는 다음과 같이 동작합니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;계층적 그래프 구조 생성&lt;/li&gt;
&lt;li&gt;상위 레이어에서 하위 레이어로 이동하며 검색&lt;/li&gt;
&lt;li&gt;각 노드는 이웃 노드들과 연결&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 파라미터:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;m: 노드당 최대 이웃 수&lt;/li&gt;
&lt;li&gt;ef_construction: 인덱스 생성 시 정확도&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IVFFlat의 동작 방식&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- IVFFlat 검색 예제
SET ivfflat.probes = 10;  -- 검색할 클러스터 수 설정

SELECT content, 
       1 - (embedding &amp;lt;=&amp;gt; '[vector_values]') as similarity
FROM documents
ORDER BY embedding &amp;lt;=&amp;gt; '[vector_values]'
LIMIT 5;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IVFFlat은 다음과 같이 동작합니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터를 여러 클러스터로 분할&lt;/li&gt;
&lt;li&gt;쿼리 벡터와 가장 가까운 클러스터 선택&lt;/li&gt;
&lt;li&gt;선택된 클러스터 내에서 검색&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 파라미터:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;lists: 클러스터 수&lt;/li&gt;
&lt;li&gt;probes: 검색할 클러스터 수&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 벤치마크 결과를 보여드리겠습니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 100만 개의 벡터에 대한 검색 성능 테스트
EXPLAIN ANALYZE
SELECT id, 1 - (embedding &amp;lt;=&amp;gt; '[vector_values]') as similarity
FROM documents
ORDER BY embedding &amp;lt;=&amp;gt; '[vector_values]'
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반적인 성능 특성&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;검색 시간
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HNSW: ~5-10ms&lt;/li&gt;
&lt;li&gt;IVFFlat: ~20-30ms&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;메모리 사용량
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HNSW: 원본 데이터의 약 50-100%&lt;/li&gt;
&lt;li&gt;IVFFlat: 원본 데이터의 약 20-30%&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떤 상황에서 어떤 인덱스를 선택할까?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HNSW 선택 시기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색 속도가 최우선일 때&lt;/li&gt;
&lt;li&gt;메모리 자원이 충분할 때&lt;/li&gt;
&lt;li&gt;높은 정확도가 필요할 때&lt;/li&gt;
&lt;li&gt;실시간 검색이 필요할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IVFFlat 선택 시기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 사용량이 중요할 때&lt;/li&gt;
&lt;li&gt;빠른 인덱스 생성이 필요할 때&lt;/li&gt;
&lt;li&gt;약간의 검색 시간 증가를 감수할 수 있을 때&lt;/li&gt;
&lt;li&gt;대규모 데이터셋을 다룰 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의할 점&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인덱스 생성 시간 고려하기&lt;/li&gt;
&lt;li&gt;메모리 사용량 모니터링하기&lt;/li&gt;
&lt;li&gt;정기적인 VACUUM ANALYZE 실행하기&lt;/li&gt;
&lt;li&gt;적절한 파라미터 튜닝하기&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 실제로 두 인덱스를 모두 적용한뒤 RAG 평가 도구를 사용해서 실제 검증후 사용하고 있습니다. 여러분도 적용과 함깨 RAG를 테스트하여 각 어플리케이션에 맞는 인덱스 선택이 필요하다고 생각합니다!&lt;br /&gt;&lt;br /&gt;다음에는 벡터 검색의 성능 최적화 팁에 대해 다뤄보도록 하겠습니다. 감사합니다!  &lt;/p&gt;</description>
      <category>RDBMS</category>
      <category>AI</category>
      <category>db</category>
      <category>DB성능</category>
      <category>PostgreSQL</category>
      <category>RAG</category>
      <category>vector search</category>
      <author>크라우니</author>
      <guid isPermaLink="true">https://todaycodeplus.tistory.com/82</guid>
      <comments>https://todaycodeplus.tistory.com/82#entry82comment</comments>
      <pubDate>Thu, 14 Nov 2024 21:35:46 +0900</pubDate>
    </item>
    <item>
      <title>[PostgreSQL] 인덱스(Index)의 기초 이해하기</title>
      <link>https://todaycodeplus.tistory.com/81</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;pg.png&quot; data-origin-width=&quot;221&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/44Pf4/btsKIyysEcG/GPYSfuJeZEMGbJTdvjxL6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/44Pf4/btsKIyysEcG/GPYSfuJeZEMGbJTdvjxL6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/44Pf4/btsKIyysEcG/GPYSfuJeZEMGbJTdvjxL6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F44Pf4%2FbtsKIyysEcG%2FGPYSfuJeZEMGbJTdvjxL6k%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;221&quot; height=&quot;228&quot; data-filename=&quot;pg.png&quot; data-origin-width=&quot;221&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 오늘은 PostgreSQL에서 매우 중요한 개념인 인덱스(Index)에 대해 알아보려고 합니다. 데이터베이스 성능 최적화에 있어 인덱스는 정말 중요한 요소인데요, 어떤 경우에 필요하고 어떻게 동작하는지 자세히 살펴보겠습니다.&lt;/p&gt;
&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;h3 data-ke-size=&quot;size23&quot;&gt;인덱스가 없을 때의 문제점&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 인덱스가 없는 상황에서의 검색
SELECT * FROM users WHERE email = 'user@example.com';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 테이블의 모든 row를 확인해야 합니다(Full Table Scan). 데이터가 많아질수록 성능은 급격히 저하됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스의 장점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;검색 속도 향상&lt;/li&gt;
&lt;li&gt;ORDER BY 연산 최적화&lt;/li&gt;
&lt;li&gt;중복 값 제어 (UNIQUE 인덱스의 경우)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 인덱스의 종류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 다양한 종류의 인덱스를 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;B-tree 인덱스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 일반적으로 사용되는 인덱스입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 기본 B-tree 인덱스 생성
CREATE INDEX idx_user_email ON users(email);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Hash 인덱스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동등 비교(=)에만 사용됩니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- Hash 인덱스 생성
CREATE INDEX idx_user_id_hash ON users USING HASH (user_id);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GiST 인덱스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지리 데이터나 기하 데이터에 적합합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- GiST 인덱스 생성 예시
CREATE INDEX idx_location ON cities USING GIST (location);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GIN 인덱스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열이나 전문 검색에 사용됩니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 전문 검색을 위한 GIN 인덱스
CREATE INDEX idx_document_text ON documents USING GIN (to_tsvector('english', content));&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 인덱스 동작 원리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 동작 방식&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 예시 테이블
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255),
    name VARCHAR(100)
);

-- 인덱스 생성
CREATE INDEX idx_user_email ON users(email);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스는 다음과 같은 과정으로 동작합니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;인덱스 생성 시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지정된 컬럼의 값들을 정렬&lt;/li&gt;
&lt;li&gt;정렬된 값과 실제 레코드의 위치 정보를 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쿼리 실행 시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스에서 'test@example.com' 값을 검색&lt;/li&gt;
&lt;li&gt;해당 값의 위치 정보를 사용해 실제 레코드 접근&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT * FROM users WHERE email = 'test@example.com';&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 계획으로 확인하기&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'test@example.com';&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Index Scan using idx_user_email on users
  Index Cond: (email = 'test@example.com'::text)
  ...&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의할 점&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인덱스도 공짜가 아닙니다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저장 공간을 차지합니다&lt;/li&gt;
&lt;li&gt;INSERT, UPDATE, DELETE 시 추가 작업이 필요합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모든 컬럼에 인덱스를 생성하는 것은 좋지 않습니다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 변경이 자주 일어나는 컬럼&lt;/li&gt;
&lt;li&gt;선택도(Selectivity)가 낮은 컬럼&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정기적인 관리가 필요합니다&lt;br /&gt;```sql&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-- 인덱스 재구축&lt;br /&gt;REINDEX INDEX idx_user_email;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-- 사용되지 않는 인덱스 확인&lt;br /&gt;SELECT * FROM pg_stat_user_indexes;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;```&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&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;</description>
      <category>RDBMS</category>
      <category>db</category>
      <category>DB성능</category>
      <category>index</category>
      <category>RDBMS</category>
      <category>인덱스</category>
      <author>크라우니</author>
      <guid isPermaLink="true">https://todaycodeplus.tistory.com/81</guid>
      <comments>https://todaycodeplus.tistory.com/81#entry81comment</comments>
      <pubDate>Thu, 14 Nov 2024 21:30:01 +0900</pubDate>
    </item>
    <item>
      <title>[Python] URL 라이브러리 403 에러 해결하기 (urllib/requests)</title>
      <link>https://todaycodeplus.tistory.com/80</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;파이썬.jpeg&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx4vxN/btsKIfeQkTh/1x5LxGNKdbl793n9NXLB11/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx4vxN/btsKIfeQkTh/1x5LxGNKdbl793n9NXLB11/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx4vxN/btsKIfeQkTh/1x5LxGNKdbl793n9NXLB11/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx4vxN%2FbtsKIfeQkTh%2F1x5LxGNKdbl793n9NXLB11%2Fimg.jpg&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;225&quot; height=&quot;225&quot; data-filename=&quot;파이썬.jpeg&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 오늘은 웹 크롤링을 하다 보면 자주 마주치게 되는 403 에러에 대해 이야기해보려고 합니다. 특히 Python의 urllib이나 requests를 사용할 때 자주 발생하는데요, 이 문제를 어떻게 해결할 수 있는지 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;무엇이 문제일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;403 에러가 발생하는 주된 이유는 웹사이트에서 봇이나 자동화된 접근을 차단하기 때문입니다. 특히 다음과 같은 경우에 자주 발생합니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;기본 헤더로만 접근할 때&lt;/li&gt;
&lt;li&gt;접근 패턴이 봇처럼 보일 때&lt;/li&gt;
&lt;li&gt;특정 라이브러리의 시그니처가 감지될 때&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법 살펴보기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 다양한 헤더 시도하기&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import requests

# 여러 가지 헤더를 준비
headers_1 = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
}

headers_2 = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15'
}

try:
    # 첫 번째 헤더로 시도
    response = requests.get('https://example.com', headers=headers_1)
    print(&quot;성공!&quot;)
except:
    # 실패하면 두 번째 헤더로 시도
    response = requests.get('https://example.com', headers=headers_2)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 가짜 헤더 생성하기&lt;/h3&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;import random

# User-Agent 목록
user_agents = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...',
    'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) ...'
]

headers = {
    'User-Agent': random.choice(user_agents),
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3',
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 동적 크롤링 시도하기&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;from selenium import webdriver
from selenium.webdriver.chrome.options import Options

# Selenium 설정
chrome_options = Options()
chrome_options.add_argument('--headless')  # 헤드리스 모드
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')

# 브라우저 실행
driver = webdriver.Chrome(options=chrome_options)
driver.get('https://example.com')

# 페이지 내용 가져오기
content = driver.page_source
driver.quit()&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. HTML 직접 파싱하기 (마지막 수단)&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;from bs4 import BeautifulSoup

# HTML 문자열 (직접 브라우저에서 복사한 HTML)
html_content = &quot;&quot;&quot;
&amp;lt;html&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;div class=&quot;content&quot;&amp;gt;
            원하는 내용
        &amp;lt;/div&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&quot;&quot;&quot;

# BeautifulSoup으로 파싱
soup = BeautifulSoup(html_content, 'html.parser')
content = soup.find('div', class_='content')&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의할 점&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;웹사이트의 robots.txt를 반드시 확인하세요.&lt;/li&gt;
&lt;li&gt;과도한 요청은 피해야 합니다.&lt;/li&gt;
&lt;li&gt;웹사이트의 이용약관을 확인하세요.&lt;/li&gt;
&lt;li&gt;필요한 경우 해당 웹사이트 관리자에게 허가를 받으세요.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;403 에러는 크롤링을 하다 보면 정말 자주 마주치게 되는 문제인데요, 위의 방법들을 순차적으로 시도해보면 대부분의 경우 해결할 수 있습니다. 다만, 웹사이트의 정책과 규칙을 준수하면서 크롤링하는 것이 중요합니다. &lt;br /&gt;&lt;br /&gt;다음에는 또 다른 크롤링 팁으로 찾아뵙도록 하겠습니다. 감사합니다!  &lt;/p&gt;</description>
      <category>Python</category>
      <author>크라우니</author>
      <guid isPermaLink="true">https://todaycodeplus.tistory.com/80</guid>
      <comments>https://todaycodeplus.tistory.com/80#entry80comment</comments>
      <pubDate>Thu, 14 Nov 2024 21:26:06 +0900</pubDate>
    </item>
    <item>
      <title>[Langchain] Passthrough를 활용한 간단한 Chain 구성하기</title>
      <link>https://todaycodeplus.tistory.com/79</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;langchain.jpeg&quot; data-origin-width=&quot;232&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dh8olB/btsKIDl8Ite/Ll6LkYJUV6HJwXeILBKgD1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dh8olB/btsKIDl8Ite/Ll6LkYJUV6HJwXeILBKgD1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dh8olB/btsKIDl8Ite/Ll6LkYJUV6HJwXeILBKgD1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdh8olB%2FbtsKIDl8Ite%2FLl6LkYJUV6HJwXeILBKgD1%2Fimg.jpg&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;232&quot; height=&quot;218&quot; data-filename=&quot;langchain.jpeg&quot; data-origin-width=&quot;232&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 오늘은 Langchain에서 Chain을 구성할 때 자주 사용되는 Passthrough에 대해서 알아보려고 합니다. 특히 LCEL(Langchain Expression Language)에서 많이 활용되는 기능인데요, 이름 그대로 데이터를 '통과'시키는 역할을 하지만 생각보다 활용도가 높답니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;무엇이 좋을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Passthrough는 다음과 같은 장점이 있습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Chain 구성을 단순화할 수 있습니다.&lt;/li&gt;
&lt;li&gt;입력 데이터를 그대로 전달하면서도 추가적인 처리가 가능합니다.&lt;/li&gt;
&lt;li&gt;다른 Chain들과 조합하기 쉽습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드로 살펴보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 예제 코드로 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731586788812&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from langchain.chat_models import ChatOpenAI
from langchain.schema.runnable import RunnablePassthrough
from langchain.prompts import ChatPromptTemplate

# LLM 모델 설정
llm = ChatOpenAI()

# 프롬프트 템플릿 생성
prompt = ChatPromptTemplate.from_template(&quot;&quot;&quot;
다음 주제에 대해 설명해주세요: {topic}
추가 컨텍스트: {context}
&quot;&quot;&quot;)

# Passthrough를 활용한 Chain 구성
chain = (
    {
        &quot;topic&quot;: RunnablePassthrough(),
        &quot;context&quot;: lambda x: f&quot;이 주제는 {x}와 관련이 있습니다.&quot;
    }
    | prompt
    | llm
)

# 실행
response = chain.invoke(&quot;파이썬 비동기 프로그래밍&quot;)
print(response)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;실행 결과&amp;nbsp;&lt;br /&gt;AIMessage(content='파이썬 비동기 프로그래밍은 동시성 프로그래밍을 구현하는 중요한 방법입니다. &lt;/span&gt;&lt;/span&gt;&lt;span&gt;이는 I/O 바운드 작업을 효율적으로 처리할 수 있게 해주며, async/await 키워드를 통해 &lt;/span&gt;&lt;span&gt;직관적인 코드 작성이 가능합니다...')&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의할 점&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Passthrough는 단순히 데이터를 전달하는 것처럼 보이지만, Chain 구성에서 중요한 역할을 합니다.&lt;/li&gt;
&lt;li&gt;데이터 형식을 잘 맞춰줘야 합니다. 특히 딕셔너리 형태로 전달할 때 키값을 잘 확인해야 합니다.&lt;/li&gt;
&lt;li&gt;람다 함수와 함께 사용할 때는 입력값의 타입을 잘 확인해야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Passthrough는 정말 단순한 기능이지만, 이를 잘 활용하면 복잡한 Chain도 깔끔하게 구성할 수 있습니다. 저도 처음에는 단순히 '통과'시키는 용도로만 생각했는데, 실제로 사용해보니 다양한 활용이 가능하더라구요. 특히 LCEL과 함께 사용하면 더욱 강력한 기능을 발휘할 수 있습니다. 여러분도 한번 시도해보시는 건 어떨까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 더 심화된 내용으로 찾아뵙도록 하겠습니다. 감사합니다!  &lt;span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Python</category>
      <category>AI</category>
      <category>langchain</category>
      <category>LLM</category>
      <category>Python</category>
      <author>크라우니</author>
      <guid isPermaLink="true">https://todaycodeplus.tistory.com/79</guid>
      <comments>https://todaycodeplus.tistory.com/79#entry79comment</comments>
      <pubDate>Thu, 14 Nov 2024 21:22:40 +0900</pubDate>
    </item>
    <item>
      <title>[Langchain]  FastAPI에서 LLM Agent를 활용한 Response Streaming</title>
      <link>https://todaycodeplus.tistory.com/78</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-31 오후 11.32.55.png&quot; data-origin-width=&quot;293&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEDdnS/btsKszQvArU/2hoKELckReKm89BsENSSpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEDdnS/btsKszQvArU/2hoKELckReKm89BsENSSpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEDdnS/btsKszQvArU/2hoKELckReKm89BsENSSpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEDdnS%2FbtsKszQvArU%2F2hoKELckReKm89BsENSSpK%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;293&quot; height=&quot;177&quot; data-filename=&quot;스크린샷 2024-10-31 오후 11.32.55.png&quot; data-origin-width=&quot;293&quot; data-origin-height=&quot;177&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&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;&lt;a href=&quot;https://todaycodeplus.tistory.com/70&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://todaycodeplus.tistory.com/70&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1730384406670&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Langchain] Langserve에서 LLM Agent를 활용한 Response Streaming&quot; data-og-description=&quot;기존에 LCEL기반의 LLM pipeline에서 Agent를 활용하는 방식으로 전환을 하고 있었습니다. 그 이유는 아래와 같습니다.1. Retriever를 구조화하여 사용하기 힘들다.2. 복잡한 형태, 크로스 도매인을 가진 &quot; data-og-host=&quot;todaycodeplus.tistory.com&quot; data-og-source-url=&quot;https://todaycodeplus.tistory.com/70&quot; data-og-url=&quot;https://todaycodeplus.tistory.com/70&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dNqJeH/hyXsTund9j/znJRiSJ1ZGd92m3ojw1h0k/img.png?width=378&amp;amp;height=133&amp;amp;face=0_0_378_133,https://scrap.kakaocdn.net/dn/lT1KD/hyXsZahJcS/z4QkxtEqem6ornjXFgjdwk/img.png?width=378&amp;amp;height=133&amp;amp;face=0_0_378_133,https://scrap.kakaocdn.net/dn/FfFhT/hyXpwAPE3s/eMYaPmpQgjz8qMyl5Pt4tK/img.jpg?width=500&amp;amp;height=333&amp;amp;face=0_0_500_333&quot;&gt;&lt;a href=&quot;https://todaycodeplus.tistory.com/70&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://todaycodeplus.tistory.com/70&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dNqJeH/hyXsTund9j/znJRiSJ1ZGd92m3ojw1h0k/img.png?width=378&amp;amp;height=133&amp;amp;face=0_0_378_133,https://scrap.kakaocdn.net/dn/lT1KD/hyXsZahJcS/z4QkxtEqem6ornjXFgjdwk/img.png?width=378&amp;amp;height=133&amp;amp;face=0_0_378_133,https://scrap.kakaocdn.net/dn/FfFhT/hyXpwAPE3s/eMYaPmpQgjz8qMyl5Pt4tK/img.jpg?width=500&amp;amp;height=333&amp;amp;face=0_0_500_333');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Langchain] Langserve에서 LLM Agent를 활용한 Response Streaming&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;기존에 LCEL기반의 LLM pipeline에서 Agent를 활용하는 방식으로 전환을 하고 있었습니다. 그 이유는 아래와 같습니다.1. Retriever를 구조화하여 사용하기 힘들다.2. 복잡한 형태, 크로스 도매인을 가진&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;todaycodeplus.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무엇이 바뀌었을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 기존에는 방식이 다음과 같았습니다.&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Agent가 결과물을 생성하는 과정에서 LLM모델의 Output에서 생성되는 Token을 Callback으로 인식합니다.&lt;/li&gt;
&lt;li&gt;Callback으로 들어온 Token을 Async Queue에 담습니다.&lt;/li&gt;
&lt;li&gt;Async Queue에 담긴 Token들을 비동기로 Generator를 사용해서 Output을 냅니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식에는 좀 문제가 있었는데, 우선 복잡도가.. Agent의 답변을 내리는 것도 아니고 LLM에서 생성하는 모든 Token을 내립니다. 그런데 이제 Async queue를 곁들여서 그래서 Callback을 한번 거쳐 나가다 보니, 좀 복잡합니다. (위에 글을 보시면 복잡하다는 게 확 느껴질 겁니다.)&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;그래서 이것을 단순화시키고자 Callback을 제거하고, Langchain의 Runnable객체로 생성된 Chain이라면 가지고 있는 &lt;span style=&quot;color: #f3c000;&quot;&gt;&lt;b&gt;astream_events&lt;/b&gt;&lt;/span&gt; 메서드를 사용하기로 했습니다. 그리고 방식은 기존과 같이 SSE를 async generator를 사용해서 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 설명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 다른 구현체가 필요 없습니다. 기존에는 Custom Callback 같은 녀석을 만들어줄 필요가 없는 거죠 다만 이미 짜인 데이터를 내려받다 보니, 내려오는 데이터의 구조가 좀 복잡 시럽 다는 것만 빼면요&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1730384843445&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@app.post(&quot;/chat&quot;)
async def stream_agent(input_data: QuestionInput):
    try:
        logger.info(f&quot;Received question: {input_data.question}&quot;)
        logger.info(f&quot;Chat history length: {len(input_data.chat_history)}&quot;)

        question_data = {
            &quot;chat_history&quot;: input_data.chat_history,
            &quot;question&quot;: input_data.question
        }

        async def generate():
            try:
                async for event in agent_pipeline.astream_events(question_data, config={&quot;callbacks&quot;: [langfuse_handler]}, version=&quot;v2&quot;):
                    output_data = event[&quot;data&quot;]
                    if &quot;chunk&quot; in output_data.keys():
                        if isinstance(output_data[&quot;chunk&quot;], AIMessageChunk):
                            yield f'data: {json.dumps({&quot;token&quot;: output_data[&quot;chunk&quot;].__dict__[&quot;content&quot;]})}\n\n'

            except Exception as e:
                yield f&quot;data: {json.dumps({'error': str(e)})}\n\n&quot;

            finally:
                yield &quot;data: [DONE]\n\n&quot;

        logger.info(&quot;run_agent completed&quot;)
        return StreamingResponse(generate(), media_type=&quot;text/event-stream&quot;)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))&lt;/code&gt;&lt;/pre&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;nbsp;&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;b&gt;다만 주의할 점이 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;내려오는 데이터 구조가 복잡합니다. 단순하지 않고 객체 형태로 내려오는 데이터도 있기 때문에 원하는 데이터를 저보다 더 거르고 싶다면 if문의 분기를 피할 수 없을 것 같습니다.&lt;/li&gt;
&lt;li&gt;에러 처리가 좀 복잡합니다. 어떻게 에러를 처리할 것인가에 대해서 고민을 더 해봐야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 이런 단점을 상쇄할 만큼 확실히 코드 가독성 측면에서도, queue를 한 번 더 거치지 않아도 돼서 성능면에서도 더 뛰어나기 때문에 훨씬 나아졌습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 얼마나 기본기가 없었는지 깨달은 내용입니다. 전반적인 비동기 처리에 대한 지식이나, python generator의 구동방식 Langchain을 활용하는 방법까지 단순히 땜빵식이 아니라 더 깊게, 더 치열하게 생각하고 고민해야지만 한다는 생각을 더 하게 됩니다.&lt;/p&gt;</description>
      <category>Python</category>
      <category>Agent</category>
      <category>AI</category>
      <category>ai chat</category>
      <category>langchain</category>
      <category>streaming</category>
      <author>크라우니</author>
      <guid isPermaLink="true">https://todaycodeplus.tistory.com/78</guid>
      <comments>https://todaycodeplus.tistory.com/78#entry78comment</comments>
      <pubDate>Thu, 31 Oct 2024 23:33:50 +0900</pubDate>
    </item>
    <item>
      <title>[후기] SPH - Google Maps 모빌리티 비즈니스 웨비나 발표</title>
      <link>https://todaycodeplus.tistory.com/77</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;발표는 어떻게 하게 됐을까?&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-31 오후 11.14.46.png&quot; data-origin-width=&quot;271&quot; data-origin-height=&quot;155&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ycr65/btsKrQepOtN/1kvZLBSiJDrtz25N5Sv7i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ycr65/btsKrQepOtN/1kvZLBSiJDrtz25N5Sv7i0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ycr65/btsKrQepOtN/1kvZLBSiJDrtz25N5Sv7i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fycr65%2FbtsKrQepOtN%2F1kvZLBSiJDrtz25N5Sv7i0%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;271&quot; height=&quot;155&quot; data-filename=&quot;스크린샷 2024-10-31 오후 11.14.46.png&quot; data-origin-width=&quot;271&quot; data-origin-height=&quot;155&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 세션의 발표는 비즈니스 세션으로 업계의 동향이나, 우리 회사가 집중하고 있는 영역에 대해서 설명하는 세션이었습니다. 3월부터는 개발 부서에서 팀을 이전하여 사업전략에서 데이터 업무를 이어나가고 있었는데, 마침 동료분으로부터 해당 발표를 소개받았습니다. 원래는 다른 분이 예정돼 있었으나, 갑작스러운 출장 일정으로 인하여 제게 인계 됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비즈니스 세션인데 부담되지 않았을까?&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;발표 준비&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느정도 발표의 가이드라인이 주어졌는데요, 모빌리티 사업의 동향을 우리 회사의 관점에서 해석하는 파트와 -&amp;gt; 그래서 우리는 지금 어떤 일을 하고 어떤 미래를 그리고 있는지 -&amp;gt; 그래서 현재 어떤 제품으로 시장에 접근하고 있는지 순서로 발표를 구성했습니다. 그러다 보니 자연스럽게 큰 주제로 연결됐고 마침 다행히도 회사의 제품들이 이런 큰 주제들과 연관이 있어 자료를 쭉 작성하게 됐습니다.&amp;nbsp;&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;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-31 오후 11.05.16.png&quot; data-origin-width=&quot;969&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crwt2J/btsKrTCbOeR/Zl2QKoUArbMvRdjBArxYfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crwt2J/btsKrTCbOeR/Zl2QKoUArbMvRdjBArxYfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crwt2J/btsKrTCbOeR/Zl2QKoUArbMvRdjBArxYfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcrwt2J%2FbtsKrTCbOeR%2FZl2QKoUArbMvRdjBArxYfk%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;418&quot; height=&quot;235&quot; data-filename=&quot;스크린샷 2024-10-31 오후 11.05.16.png&quot; data-origin-width=&quot;969&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-31 오후 11.05.34.png&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6cVrF/btsKrvhozPP/SefCnkSBuKGuwvIW9li9wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6cVrF/btsKrvhozPP/SefCnkSBuKGuwvIW9li9wK/img.png&quot; data-alt=&quot;발표 자료 내용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6cVrF/btsKrvhozPP/SefCnkSBuKGuwvIW9li9wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6cVrF%2FbtsKrvhozPP%2FSefCnkSBuKGuwvIW9li9wK%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;392&quot; height=&quot;218&quot; data-filename=&quot;스크린샷 2024-10-31 오후 11.05.34.png&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;530&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;전반적으로 메시지를 강조하고, 20분 정도의 발표시간을 고려하여 어느 정도 읽을 수 있는 글을 첨부하여 각 슬라이드에 의미를 함축적으로 담았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;발표 당일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발표 당일 오전에 리허설을 통해서 웨비나 환경에 대한 적응을 할 수 있도록 안내해 주셔서 오전에 리허설 통해서 최종 시간 점검 및 음성 영상 점검 모두 잘 마쳤었고, 리허설을 할 당시까지만 해도 어느 정도의 사람이 들어올지 전혀 알 수 없었습니다. 발표를 시작하니 60명 정도가 참석하여 내용을 들어주셨더라고요&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;발표 자체는 발표 준비를 열심히 했다고 생각했고, 웨비나 특성상 발표 자료의 발표자 슬라이드를 통해서 어느 정도의 발표 가닥을 미리 적어두었어서 발표 자체는 크게 어려움 없이 진행했습니다. 그것보다.. 소개해주신 SPH담당자님이 엄청 거창하게 귀한 손님으로 포장해 주셔서 그게 조금 부담을 유발하지 않았나 (ㅋㅋ).&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;발표 후기&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;발표 영상 및 기사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;유튜브&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtu.be/JCcphCgYijc?si=ouyJ6LvVtjZcBX0V&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtu.be/JCcphCgYijc?si=ouyJ6LvVtjZcBX0V&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-31 오후 11.15.13.png&quot; data-origin-width=&quot;267&quot; data-origin-height=&quot;147&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rNOfO/btsKr8lxQU6/9Lrk96KLe2Q1HIWP7pEhq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rNOfO/btsKr8lxQU6/9Lrk96KLe2Q1HIWP7pEhq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rNOfO/btsKr8lxQU6/9Lrk96KLe2Q1HIWP7pEhq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrNOfO%2FbtsKr8lxQU6%2F9Lrk96KLe2Q1HIWP7pEhq0%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;267&quot; height=&quot;147&quot; data-filename=&quot;스크린샷 2024-10-31 오후 11.15.13.png&quot; data-origin-width=&quot;267&quot; data-origin-height=&quot;147&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;매일신문 기사&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://naver.me/xa5I4q2g&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://naver.me/xa5I4q2g&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1730384038081&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;SPH, &amp;lsquo;구글 지도를 이용한 모빌리티 비즈니스 전략&amp;rsquo; 웨비나 성료&quot; data-og-description=&quot;구글맵스 플랫폼 프리미어 파트너사 에스피에이치(SPH, 대표: 소광진)가 지난 15일 &amp;lsquo;구글 지도를 이용한 비즈니스 전략&amp;rsquo; 웨비나를 성료했다고 밝혔다. 3회차를 맞은 이번 웨비나는 국내외 모빌&quot; data-og-host=&quot;n.news.naver.com&quot; data-og-source-url=&quot;https://naver.me/xa5I4q2g&quot; data-og-url=&quot;https://n.news.naver.com/article/009/0005386471&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cqG4sG/hyXsZg3lLY/hYFgUNbipldLZCZzZ25kT1/img.png?width=700&amp;amp;height=392&amp;amp;face=0_0_700_392,https://scrap.kakaocdn.net/dn/mQLO6/hyXs076q68/PLrgIAqdbtgCsN1KxtckaK/img.png?width=700&amp;amp;height=392&amp;amp;face=0_0_700_392&quot;&gt;&lt;a href=&quot;https://naver.me/xa5I4q2g&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://naver.me/xa5I4q2g&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cqG4sG/hyXsZg3lLY/hYFgUNbipldLZCZzZ25kT1/img.png?width=700&amp;amp;height=392&amp;amp;face=0_0_700_392,https://scrap.kakaocdn.net/dn/mQLO6/hyXs076q68/PLrgIAqdbtgCsN1KxtckaK/img.png?width=700&amp;amp;height=392&amp;amp;face=0_0_700_392');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;SPH, &amp;lsquo;구글 지도를 이용한 모빌리티 비즈니스 전략&amp;rsquo; 웨비나 성료&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;구글맵스 플랫폼 프리미어 파트너사 에스피에이치(SPH, 대표: 소광진)가 지난 15일 &amp;lsquo;구글 지도를 이용한 비즈니스 전략&amp;rsquo; 웨비나를 성료했다고 밝혔다. 3회차를 맞은 이번 웨비나는 국내외 모빌&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;n.news.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발자로서 일상</category>
      <category>모빌리티</category>
      <category>비즈니스</category>
      <category>사업</category>
      <category>웨비나</category>
      <category>전략</category>
      <author>크라우니</author>
      <guid isPermaLink="true">https://todaycodeplus.tistory.com/77</guid>
      <comments>https://todaycodeplus.tistory.com/77#entry77comment</comments>
      <pubDate>Thu, 31 Oct 2024 23:16:28 +0900</pubDate>
    </item>
    <item>
      <title>[후기] 우아콘 이그나이트 세션 발표 참여</title>
      <link>https://todaycodeplus.tistory.com/76</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떻게 신청하게 됐을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 LLM을 활용한 전기차 정보 제공 서비스를 만들고 있는데, 동료 PM으로부터 메시지를 받았습니다. 우아콘을 10월 30일 날 진행하니 신청해 보라는 것이었고, 내부 내용을 살펴보니 이번에는 참여형 세션이라고 해서 외부 인물도 발표를 할 수 있는 세션이 있다는 것을 알게 됐습니다. 그래서 고민 한.. 0.1 그람쯤 하고, 설마 되겠어라는 생각으로 작성한 참가 신청을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참여 확정 안내&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-31 오후 10.37.11.png&quot; data-origin-width=&quot;508&quot; data-origin-height=&quot;397&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mNuVW/btsKq2fCrzj/R8a0uab3o1M6FeUM4PWdHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mNuVW/btsKq2fCrzj/R8a0uab3o1M6FeUM4PWdHk/img.png&quot; data-alt=&quot;참여 확정 메일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mNuVW/btsKq2fCrzj/R8a0uab3o1M6FeUM4PWdHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmNuVW%2FbtsKq2fCrzj%2FR8a0uab3o1M6FeUM4PWdHk%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;508&quot; height=&quot;397&quot; data-filename=&quot;스크린샷 2024-10-31 오후 10.37.11.png&quot; data-origin-width=&quot;508&quot; data-origin-height=&quot;397&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;참여 확정 메일&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-29 오후 11.02.58.png&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;1014&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjWINr/btsKrdgXDSF/uFWy6SmozDXy6VM3ysV2I1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjWINr/btsKrdgXDSF/uFWy6SmozDXy6VM3ysV2I1/img.png&quot; data-alt=&quot;우아콘 페이지 업데이트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjWINr/btsKrdgXDSF/uFWy6SmozDXy6VM3ysV2I1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjWINr%2FbtsKrdgXDSF%2FuFWy6SmozDXy6VM3ysV2I1%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;538&quot; height=&quot;447&quot; data-filename=&quot;스크린샷 2024-10-29 오후 11.02.58.png&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;1014&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;와.. 정말 돼버렸네, 저는 이 메일을 처음 받았을 때 약간 인지부조화가 와서, 멍~ 했었는데요.. 내가 어찌 됐든 콘퍼런스 참가자로 발표도 하고 뭔가.. 이상한 기분이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자료 준비 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 발표를 할까 정말 고민이 많이 됐습니다. 발표 시간은 각자 5분 정도로 매우 짧았기 때문에, 어떤 주제를 선택할까 그리고 어떻게 풀어나갈까 고민이 참 많이 됐습니다. 우선 신청을 하면서 앱에 LLM을 탑재하면서 겪었던 문제점을 해결한 과정을 공유하고 싶다고 신청했기 때문에 방향을 &quot;문제 해결을 한 과정&quot;으로 좁히고, 최근에 Token 비용을 줄이고, 성능 개선을 했던 사례를 소개하기로 결심했습니다.&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;그렇게 선정된 제목은 바로.. &quot;더 나은 AI를 위하여! 문서 압축부터 성능 개선까지&quot; 제목에서 알 수 있듯 LLM에 주는 문서를 압축하여 성능을 개선했던 후기를 작성했습니다.&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;우선 회사 템플릿을 찾고.. 간단하고 빠른 발표이니만큼 글을 많이 채우기보다 아이코닉한 형태의 PPT를 만들고 싶었고, 그래서 아래와 같은 PPT가 탄생했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-31 오후 10.41.45.png&quot; data-origin-width=&quot;715&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPefXb/btsKrBIxfqF/7NFgQEUbqZz6kDymXuUcA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPefXb/btsKrBIxfqF/7NFgQEUbqZz6kDymXuUcA0/img.png&quot; data-alt=&quot;발표 문서 일부&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPefXb/btsKrBIxfqF/7NFgQEUbqZz6kDymXuUcA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPefXb%2FbtsKrBIxfqF%2F7NFgQEUbqZz6kDymXuUcA0%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;441&quot; height=&quot;505&quot; data-filename=&quot;스크린샷 2024-10-31 오후 10.41.45.png&quot; data-origin-width=&quot;715&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;발표 문서 일부&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;발표 당일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소에 긴장이랑 거리가 먼 사람이라고 항상 생각하는데, 나이를 먹어서인지(나이 탓 좀 하고 싶었습니다.) 심장이 뛰고 그런 상태가 됐습니다. 같이 발표를 하는 분들이 모두 재미있는 주제를 들고 오셔서 더 긴장됐던 것 같습니다. 뭔가 제 내용은 업무적인 심각한 내용 같아 보였거든요. 저는 3번째 차례로 발표하게 됐고, 모쪼록 발표는 잘 마쳤습니다! 발표는 말하고자 하는 내용은 모두 잘 전달했다고 생각하는데, 긴장을 했는지 말을 엄청 빨리 한 것 같아요.. 그래서 그런가 장표가 2장 남았는데 1분 20초나 남았더라고요.&amp;nbsp;&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;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발표를 하는 것은 여유와의 싸움인 것 같습니다. 이번 발표를 하면서 아쉬웠던 점이 여유롭게 재미있게 발표하지 못한 것입니다. 생각을 해보니, 발표자가 어느 정도 여유로워야 청중에게 들리기도 훨씬 좋을 것 같았습니다. 앞으로 또 발표할 기회가 온다면 조금 더 여유롭게 발표를 즐기며 해볼 수 있는 기회가 또 있겠.. 죠?&amp;nbsp;&lt;/p&gt;</description>
      <category>개발자로서 일상</category>
      <category>AI</category>
      <category>GenAI</category>
      <category>RAG</category>
      <category>발표</category>
      <category>우아콘</category>
      <category>컨퍼런스</category>
      <author>크라우니</author>
      <guid isPermaLink="true">https://todaycodeplus.tistory.com/76</guid>
      <comments>https://todaycodeplus.tistory.com/76#entry76comment</comments>
      <pubDate>Thu, 31 Oct 2024 22:55:29 +0900</pubDate>
    </item>
    <item>
      <title>[LLM] Semantic Search를 위한 Langchain Custom Retriever 구현하기</title>
      <link>https://todaycodeplus.tistory.com/75</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요 오늘은 Langchain에서 Custom Retriever를 구현하는 방법을 소개해볼까 합니다.&amp;nbsp;&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;1. 아 뭔가 Retriever에서 데이터가 출력되기 전에 ReRank를 사용하고 나온다면 어떨가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 어? 뭐야 이건 Retriever가 없네? Langchain_community 라이브러리를 뒤져봐도 내가 원하는 Retriever가 없는경우 (제가 못찾았을 수 있습니다.. 머쓱..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 데이터가 Retriever에서 필터를 가지거나, Similarity Search가 되기전 뭔가 처리를 했으면 좋겠다거나..&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;이런 이유로 기존에 Langchain에서 제공되던 Retriever를 수정하여 사용 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Base Retriever에서 중요한 매서드들&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[API 문서]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://api.python.langchain.com/en/latest/retrievers/langchain_core.retrievers.BaseRetriever.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://api.python.langchain.com/en/latest/retrievers/langchain_core.retrievers.BaseRetriever.html&lt;/a&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;위 문서를 보면 기본적으로 Base Retriever가 어떤 구조를 가지고 있는지 알 수 있습니다. 이중에서 우리가 중요하게 생각해야하는 매서드는 아래와 같습니다.&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;1. invoke 매서드와 ainvoke 매서드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 매서드는 기본적으로 as_retriever를 사용하게 되어 Retriever의 형태를 갖추면 사용할 수 있습니다. 이 매서드는 여기뿐 아니라 langchain에서 는 Chain이 생성되고도 사용합니다. 주로 어떤 입력값을 받아 LLM또는 Retriever의 값을 Return 해줍니다.&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;2. get_relevant_documents 매서드와 aget_relevant_documents 매서드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 매서드들은 invoke가 실행될 때 실제로 이 매서드가 실행 됩니다. 이 매서드는 인자 값을 살펴 보면 주로 Query, tag, **kwargs 등을 받습니다. 이 값들은 검색에 필요한 쿼리와 Search option을 제공하기 위함입니다.&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;사실 이 두개만 알면 어느정도 소화가 가능합니다. ((왜냐하면 오늘은 조회를 목적으로한 Custom Retriever를 소개할 예정입니다. 만약 문서 추가까지 필요한 상황이라면, add_document 매서드 까지 손봐야합니다.))&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;h4 data-ke-size=&quot;size20&quot;&gt;2. Custom Retriever 구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Class로 구성되며 위에서 언급한 BaseRetriever를 상속받아 Class가 구성됩니다. 그래서 기본적인 골조를 모두 가져온 상태에서 내가 필요한 값을 init에 받고, 실제로 조회가 이루어지는 get_relevant_documents를 수정하여 사용가능한 형태로 가공합니다.&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;이번 예제는 이전시간에 다루었던 OpenSearch의 Lexical Search를 위한 데이터 검색 Class를 만들겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725545750428&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class OpenSearchBM25Retriever(BaseRetriever):
    &quot;&quot;&quot;OpenSearch retriever that uses BM25.&quot;&quot;&quot;

    client: Any
    &quot;&quot;&quot;OpenSearch client.&quot;&quot;&quot;
    index_name: str
    &quot;&quot;&quot;Name of the index to use in OpenSearch.&quot;&quot;&quot;
    search_size: int
    &quot;&quot;&quot;Search data size to use in OpenSearch.&quot;&quot;&quot;

    def _get_relevant_documents(
        self, query: str, *, run_manager: CallbackManagerForRetrieverRun
    ) -&amp;gt; List[Document]:
        query_dict = {&quot;query&quot;: {&quot;match&quot;: {&quot;text&quot;: query}}, &quot;size&quot;: self.search_size}
        res = self.client.search(index=self.index_name, body=query_dict)

        docs = []
        for r in res[&quot;hits&quot;][&quot;hits&quot;]:
            docs.append(Document(page_content=r[&quot;_source&quot;][&quot;text&quot;], metadata=r[&quot;_source&quot;][&quot;metadata&quot;]))
        return docs&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 매우 간결하죠? 왜냐하면 1번에서 언급한 내용처럼 add_document에 대한 내용이 전부 빠져있기 때문입니다. 단순히 조회를 위한&amp;nbsp; Custom은 간단합니다. 만약 Client를 생성하는 방법을 알고 싶다면 이전글을 참고하세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://todaycodeplus.tistory.com/74&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://todaycodeplus.tistory.com/74&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1725545828733&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[LLM] OpenSearch - VectorStore로 써보기&quot; data-og-description=&quot;안녕하세요 오늘은 Vector Store로 많이 쓰이는 OpenSearch를 가져와 봤습니다. Elastic Search를 Fork해서 사용하는만큼 Elastic Search와 유사한점도 그리고 다른점도 있습니다. 하지만 오늘은 이 차이점을 설&quot; data-og-host=&quot;todaycodeplus.tistory.com&quot; data-og-source-url=&quot;https://todaycodeplus.tistory.com/74&quot; data-og-url=&quot;https://todaycodeplus.tistory.com/74&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cE0kEu/hyWZaREv3k/wl3REFMxD8kw0cBqSxKfy0/img.jpg?width=800&amp;amp;height=457&amp;amp;face=0_0_800_457,https://scrap.kakaocdn.net/dn/GVY4M/hyWZchB7ng/Ket8eTxXUk12eAwkVR5zQk/img.jpg?width=800&amp;amp;height=457&amp;amp;face=0_0_800_457,https://scrap.kakaocdn.net/dn/l6uwx/hyWZiWqfWX/zkNukeTpzgWUbeqzcGRxNK/img.png?width=2243&amp;amp;height=793&amp;amp;face=0_0_2243_793&quot;&gt;&lt;a href=&quot;https://todaycodeplus.tistory.com/74&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://todaycodeplus.tistory.com/74&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cE0kEu/hyWZaREv3k/wl3REFMxD8kw0cBqSxKfy0/img.jpg?width=800&amp;amp;height=457&amp;amp;face=0_0_800_457,https://scrap.kakaocdn.net/dn/GVY4M/hyWZchB7ng/Ket8eTxXUk12eAwkVR5zQk/img.jpg?width=800&amp;amp;height=457&amp;amp;face=0_0_800_457,https://scrap.kakaocdn.net/dn/l6uwx/hyWZiWqfWX/zkNukeTpzgWUbeqzcGRxNK/img.png?width=2243&amp;amp;height=793&amp;amp;face=0_0_2243_793');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[LLM] OpenSearch - VectorStore로 써보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요 오늘은 Vector Store로 많이 쓰이는 OpenSearch를 가져와 봤습니다. Elastic Search를 Fork해서 사용하는만큼 Elastic Search와 유사한점도 그리고 다른점도 있습니다. 하지만 오늘은 이 차이점을 설&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;todaycodeplus.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;nbsp;&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;1. BaseRetriever를 상속받아 Class OpenSearchBM25Retriever를 열어줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. init인자로 (client, index_name, search_size)를 받아줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 각각 OpenSearch Client, 조회할 index 이름, Score로 정렬된 값에서 상위 몇개를 가져올 것인지 결정하는 값입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. _get_relevant_documents 매서드를 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 내부에 들어있는 로직은 이전글에서 BM25를 조회할 때 썻던 로직을 그대로 가져다 썼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. &lt;b&gt;&lt;u&gt;이렇게 구성하고 Retriever를 생성하면 BaseRetriver가 가지고 있던 invoke를 사용할 수 있고 이 매서드가 내부에서 사용하는 _get_relevant_documents 매서드는 우리가 작성한 매서드를 사용하게 되어 원하는 로직대로 동작하게 됩니다.&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 마치며&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다 너무 간단해서 놀라시진 않았는가 싶습니다.문서를 꼼꼼히 읽어보시면 금방 만드실 수 있는 부분이고 Langchain을 더 잘 사용하는데 도움이 됐으면 좋겠습니다.&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;다음글은 RAG의 LLM기반 성능평가 진행을 해보려고 합니다. 이미 진행하고 있는데, 아직 모듈화가 좀 덜 되어서 공개가좀 어려워서 모듈화를 좀더 마치고, 바로 사용가능한 형태로 해보려고 합니다. Git에 업로드하여 누구나 사용할 수 있도록 Open 하면 좋을것 같아서요&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;</description>
      <category>DL</category>
      <category>AI</category>
      <category>LLM</category>
      <category>lnagchain</category>
      <category>OpenSearch</category>
      <category>RAG</category>
      <category>Retriever</category>
      <category>vectorstore</category>
      <author>크라우니</author>
      <guid isPermaLink="true">https://todaycodeplus.tistory.com/75</guid>
      <comments>https://todaycodeplus.tistory.com/75#entry75comment</comments>
      <pubDate>Thu, 5 Sep 2024 23:27:14 +0900</pubDate>
    </item>
    <item>
      <title>[LLM] OpenSearch - VectorStore로 써보기</title>
      <link>https://todaycodeplus.tistory.com/74</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요 오늘은 Vector Store로 많이 쓰이는 OpenSearch를 가져와 봤습니다. Elastic Search를 Fork해서 사용하는만큼 Elastic Search와 유사한점도 그리고 다른점도 있습니다. 하지만 오늘은 이 차이점을 설명하는 자리는 아니라 이만 줄이고, 이제 Vector Store로서 장/단점을 그리고 간단할 설치와 Langchain에서 사용까지 알아볼까 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. OpenSearch의 장점과 단점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 첫번째 장점&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그건 바로 최근 아주 관심을 많이 받고 있는 RAG중에서도 Full Text Search (Lexical Search)를 Simillerity Search(Semantic Search)와 같이 쓸 수 있단 점 입니다. 성능이 좋은 다른 DB도 많지만. 기본적으로 이 둘을 지원하면서 다른 여러가지 방법의 Sementic Search를 손쉽게 &quot;무료로&quot; 지원하는 DB는 많지 않습니다.&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;그건 바로 익숙함 인데요, 이 부분은 공감이 잘 안되실 수 있겠지만.. 데이터 엔지니어로 일하고 있는 제 입장에서는 ES는 아주 소중한 로그 저장소중 하나입니다. 그래서 자주 써보았기 때문에 아주 손쉽게 잘 사용할 수 있다. Index나 검색 방법 Client를 사용한 데이터 조회까지 불편함 없이 사용이 가능합니다.&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;운영이 쉽지 않다. 입니다. 왜냐하면 OpenSearch는 일단 성능을 유지하기 위한 비용이 많이 들어가고, 이 도구를 유지하기 위해서는 어딘가에 Build 하여 사용해야 하는데, 이런 버전관리며.. Storage 관리며.. 여러가지 관리가 쉽지 않습니다. 단.. 이건 AWS의 OpenSearch SaaS버전을 사용하게 되면 많이 줄어듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 두번째 단점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리와 별개로 Vector Search로만 쓰기는 너무 아깝다.. 성능을 더 높혀서 로그 저장과 같이 쓸거냐? 그러면... 로그가 쏟아져 들어오고 조회해가는 그 Resource사용과 Vector Search를 같이 사용하며 안정성에 문제가 될 수도 있고.. 여러가지 이슈로 병합하여 사용하기 쉽지 않은데... 그렇다고 단일로 쓰기는 좀 아까운?? (물론 RAG에 집어넣어야 할 문서가 엄청 많다면 얘기가 다릅니다.) 이건 어디까지나 개인적 단점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. OpenSearch Test용 Install&amp;nbsp;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenSearch를 빌드하여 운영 용도로 사용하고 싶으신 분은 이 글을 참고만 해주세요, 이 글은 단순 Test용 OpenSearch 구현을 목적으로 합니다.&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;우선 저는 Docker-compose를 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 가장 중요하게 보셔야할 부분은 OPENSEARCH_INITIAL_ADMIN_PASSWORD입니다. 이 값에 여러분이 설정하고자 하는 초기 비밀번호를 넣어주시면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID : admin&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PASSWD : 여러분이 넣은 passwd로 접속 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1725452005237&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3'
services:
  opensearch-node1:
    image: opensearchproject/opensearch:latest
    container_name: opensearch-node1
    environment:
      - cluster.name=opensearch-cluster
      - node.name=opensearch-node1
      - discovery.seed_hosts=opensearch-node1,opensearch-node2
      - cluster.initial_master_nodes=opensearch-node1,opensearch-node2
      - bootstrap.memory_lock=true # along with the memlock settings below, disables swapping
      - &quot;OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m&quot; # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
      - &quot;OPENSEARCH_INITIAL_ADMIN_PASSWORD=YourTestPasswd123!&quot;
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536 # maximum number of open files for the OpenSearch user, set to at least 65536 on modern systems
        hard: 65536
    volumes:
      - opensearch-data1:/usr/share/opensearch/data
    ports:
      - 9200:9200
      - 9600:9600 # required for Performance Analyzer
    networks:
      - opensearch-net
  opensearch-node2:
    image: opensearchproject/opensearch:latest
    container_name: opensearch-node2
    environment:
      - cluster.name=opensearch-cluster
      - node.name=opensearch-node2
      - discovery.seed_hosts=opensearch-node1,opensearch-node2
      - cluster.initial_master_nodes=opensearch-node1,opensearch-node2
      - bootstrap.memory_lock=true
      - &quot;OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m&quot;
      - &quot;OPENSEARCH_INITIAL_ADMIN_PASSWORD=YourTestPasswd123!&quot;
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - opensearch-data2:/usr/share/opensearch/data
    networks:
      - opensearch-net
  opensearch-dashboards:
    image: opensearchproject/opensearch-dashboards:latest
    container_name: opensearch-dashboards
    ports:
      - 5601:5601
    expose:
      - &quot;5601&quot;
    environment:
      OPENSEARCH_HOSTS: '[&quot;https://opensearch-node1:9200&quot;,&quot;https://opensearch-node2:9200&quot;]'
    networks:
      - opensearch-net

volumes:
  opensearch-data1:
  opensearch-data2:

networks:
  opensearch-net:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Dashboard 접속&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행된 Docker의 DashBoard의 주소 (아마도 localhost:5601)로 접속해보시면 다음과 같은 화면이 나오고 접속이 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-09-04 오후 9.16.15.png&quot; data-origin-width=&quot;1363&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rg8n8/btsJqZKlKYo/9QavfFQfbFmyeKq57knrm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rg8n8/btsJqZKlKYo/9QavfFQfbFmyeKq57knrm0/img.png&quot; data-alt=&quot;auth page&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rg8n8/btsJqZKlKYo/9QavfFQfbFmyeKq57knrm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frg8n8%2FbtsJqZKlKYo%2F9QavfFQfbFmyeKq57knrm0%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;1363&quot; height=&quot;840&quot; data-filename=&quot;스크린샷 2024-09-04 오후 9.16.15.png&quot; data-origin-width=&quot;1363&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;auth page&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-09-04 오후 9.17.19.png&quot; data-origin-width=&quot;381&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIxNDB/btsJrporQgT/SjyHqGBY1XkMsTTGnNChaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIxNDB/btsJrporQgT/SjyHqGBY1XkMsTTGnNChaK/img.png&quot; data-alt=&quot;index management&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIxNDB/btsJrporQgT/SjyHqGBY1XkMsTTGnNChaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIxNDB%2FbtsJrporQgT%2FSjyHqGBY1XkMsTTGnNChaK%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;381&quot; height=&quot;376&quot; data-filename=&quot;스크린샷 2024-09-04 오후 9.17.19.png&quot; data-origin-width=&quot;381&quot; data-origin-height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;index management&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 매뉴에 들어가서 index Management를 찾아주시고 들어갑니다. 그리고 다시 왼쪽의 매뉴를 보면 Index가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-09-04 오후 9.18.06.png&quot; data-origin-width=&quot;373&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBP7zd/btsJqS5EbFj/JbJWpEi6pqKTDoJZn1otj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBP7zd/btsJqS5EbFj/JbJWpEi6pqKTDoJZn1otj1/img.png&quot; data-alt=&quot;index menu&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBP7zd/btsJqS5EbFj/JbJWpEi6pqKTDoJZn1otj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBP7zd%2FbtsJqS5EbFj%2FJbJWpEi6pqKTDoJZn1otj1%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;373&quot; height=&quot;394&quot; data-filename=&quot;스크린샷 2024-09-04 오후 9.18.06.png&quot; data-origin-width=&quot;373&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;index menu&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 다음과 같은 index목록이 나타나고, 우측 상단에 create index를 눌러서 인덱스를 생성해 줍니다. 저는 이미 생성 했고, 제가 개발하고 있는 evpedia의 이름을 가진 index가 보이네요 ㅎㅎ&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-09-04 오후 9.18.53.png&quot; data-origin-width=&quot;2243&quot; data-origin-height=&quot;793&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHXPTi/btsJqasnBUi/6GGA9k0H9SJjQ12luYXG8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHXPTi/btsJqasnBUi/6GGA9k0H9SJjQ12luYXG8k/img.png&quot; data-alt=&quot;index list&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHXPTi/btsJqasnBUi/6GGA9k0H9SJjQ12luYXG8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHXPTi%2FbtsJqasnBUi%2F6GGA9k0H9SJjQ12luYXG8k%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;2243&quot; height=&quot;793&quot; data-filename=&quot;스크린샷 2024-09-04 오후 9.18.53.png&quot; data-origin-width=&quot;2243&quot; data-origin-height=&quot;793&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;index list&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Langchain으로 Document 전송하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Langchain을 사용할것이고 &lt;b&gt;opensearch-py&lt;/b&gt; 라이브러리가 필요합니다. (Document Split 과정은 생략하겠습니다.)&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;주의해야할 점이 있는데.. 이건 Langchain의 OpenSearchVectorSearch 클래스의 큰 문제? 라고 생각하긴 하는데, 사실 기본적인 Vector저장을 위한 index를 만들기가 쉽지 않습니다. 그래서 아까 만들어둔 index를 그냥 우선 class에 기입하시구요..&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;해당 객체의 create_index를 통해서 index를 만들고 만들어진 index이름으로 다시 객채를 생성하거나 값을 변경하는것을 추천드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 index가 vector search를 위한 값들로 생성이 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1725452658570&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from langchain.vectorstores import OpenSearchVectorSearch
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings
from opensearchpy import OpenSearch, RequestsHttpConnection
from langchain_community.utilities.redis import get_client
from opensearchpy import OpenSearch
import pandas as pd
import time
from datetime import datetime

# embedding Function 생성
embeddings = OpenAIEmbeddings(
    api_key=OPENAI_API_KEY, 
    model=&quot;text-embedding-3-large&quot;,
    dimensions=1536,
    chunk_size=5000
)

# OpenSearch 연결 설정
host = 'localhost'  # OpenSearch 엔드포인트
port = 9200
auth = ('admin', 'YourOpensearchPasswd12!')  # 기본 인증 정보

# BM25 사용을 위한 Client 생성
client = OpenSearch(
    hosts = [{'host': host, 'port': port}],
    http_auth = auth,
    use_ssl = True,
    verify_certs = False,
    ssl_show_warn = False,
    connection_class = RequestsHttpConnection
)

# OpenSearchVectorSearch 초기화
vector_store = OpenSearchVectorSearch(
    index_name=&quot;test_evpedia&quot;,
    embedding_function=embeddings,
    opensearch_url=&quot;https://localhost:9200&quot;,
    http_auth=auth,
    use_ssl=True,
    verify_certs=False,
    ssl_show_warn=False
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 저는 기존에 사용하던 PgVector로 부터 데이터를 parquet으로 내려받아 놓은게 있는데요, 이 부분은 각자의 데이터를 가져와주시면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1725452922245&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def parse_datetime(value):
    metadata = value.copy()
    metadata[&quot;create_date&quot;] = metadata[&quot;create_date&quot;][:10]

    return metadata

migration_df = pd.read_parquet(&quot;./migration_data.parquet&quot;, engine='pyarrow')
migration_df[&quot;cmetadata&quot;] = migration_df[&quot;cmetadata&quot;].apply(lambda x: parse_datetime(x))
print(f&quot;Total load Data set: {len(migration_df)}&quot;)

docs = []
for _, data in migration_df.iterrows():
    docs.append(Document(page_content=data[&quot;document&quot;], metadata=data[&quot;cmetadata&quot;]))

print(docs[:5])
print(len(docs))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 보셔서 아시겠지만. add_document 함수를 사용하기 위해서, docs 리스트 안에 Document 객체로 말아 넣고 있습니다.&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;pre id=&quot;code_1725452991611&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for i in range(200):
    input_docs = docs[i*100:(i+1)*100]
    print(f&quot;문서 개수 {len(input_docs)}&quot;)
    if len(input_docs) == 0:
        break

    # 데이터 임베딩 및 인덱싱
    vector_store.add_documents(input_docs)
    
    print(f&quot;Data embedded and indexed successfully Count: {i+1*100}.&quot;)

    time.sleep(10)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 분할하여 전송하는 이유는 제 GPT 레벨이 낮아서.. 한번에 많은 양의 데이터가 인코딩이 안됩니다. 그래서 100개씩 잘라서 넣어주고 있습니다.&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;이렇게 하면 데이터가 정상적으로 index안에 들어가있는 것은 dashboard 또는 조회를 통해서 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. Sementic Search&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 간단합니다. 만들어둔 OpenSearchVectorSearch 객체에 as_retriever 매서드를 사용해 줍시다.&lt;/p&gt;
&lt;pre id=&quot;code_1725453140731&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;retriever = vector_store.as_retriever()
retriever.invoke(&quot;아이오닉 5를 구매할 때 알아야할 중요한 5가지 사항을 알려줘&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어? 그럼 BM25는 어떻게 쓰냐구요? 아쉽게도 OpenSearch는 BM25 Retriever를 직접 제공하지 않습니다. 여러분이 만들어 쓰셔야합니다.&amp;nbsp; 거기에 사용되는 아주 중요한 부분은 바로 요고 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725453246524&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;query_dict = {&quot;query&quot;: {&quot;match&quot;: {&quot;text&quot;: query}}, &quot;size&quot;: search_size}
res = client.search(index=self.index_name, body=query_dict)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 만들어둔 Client에 직접 조회해서 가져와야합니다..&amp;nbsp;&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;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 이정도로 마치고 다음번에는 Retriever를 직접 구현하는 세션을 가져볼까 합니다. 이런식으로 BM25를 따로 쓸 수 없잖아요?&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;nbsp;&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;저는 지금 Multi Query Retriever와 ParentDocumentRetriever를 쓰고있는데요, Langchain이 v0.2에 들어오면서 LLM기반의 ReRaker를 지원하기 시작했고, 이것을 모두 하나의 Retriever의 get_relevant_documents에 섞어서 쓰고 있습니다. 조회 한번에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2개의 Retrieve와 ReRank까지 가능하죠&amp;nbsp;&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;</description>
      <category>DL</category>
      <category>AI</category>
      <category>DL</category>
      <category>GenAI</category>
      <category>langchain</category>
      <category>LLM</category>
      <category>OpenSearch</category>
      <category>RAG</category>
      <category>semanticsearch</category>
      <category>vector</category>
      <category>vectorstore</category>
      <author>크라우니</author>
      <guid isPermaLink="true">https://todaycodeplus.tistory.com/74</guid>
      <comments>https://todaycodeplus.tistory.com/74#entry74comment</comments>
      <pubDate>Wed, 4 Sep 2024 21:39:17 +0900</pubDate>
    </item>
    <item>
      <title>[LLM] 프롬프트를 활용하며 느낀점 &amp;amp; 효과가 있던 방법 2편</title>
      <link>https://todaycodeplus.tistory.com/73</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난번 소개드린 1편에 이어서 2편을 적어보려 합니다.&lt;br /&gt;&lt;a href=&quot;https://todaycodeplus.tistory.com/72&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://todaycodeplus.tistory.com/72&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1724850102524&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[LLM] 프롬프트를 활용하며 느낀점 &amp;amp; 효과가 있던 방법 1편&quot; data-og-description=&quot;오늘은 프롬프트를 활용하며 느낀점을 한번 정리해보려고 합니다.&amp;nbsp;본격적으로 프롬프트를 활용해서 서비스를 개발하기 시작한것은 약 3개월 전 입니다. 그 당시 저는 프롬프트라곤 코드 작성&quot; data-og-host=&quot;todaycodeplus.tistory.com&quot; data-og-source-url=&quot;https://todaycodeplus.tistory.com/72&quot; data-og-url=&quot;https://todaycodeplus.tistory.com/72&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ZWnDt/hyWVSDptXL/T7QuzmLzTVGpn3y2vLPhoK/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cc32Y8/hyWVRxJeGA/hC7khoIhT242oHWCHGl711/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cMN7Wo/hyWVRYNUdQ/pD89evhRKhVkFKSLd8VI0k/img.jpg?width=500&amp;amp;height=333&amp;amp;face=0_0_500_333&quot;&gt;&lt;a href=&quot;https://todaycodeplus.tistory.com/72&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://todaycodeplus.tistory.com/72&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ZWnDt/hyWVSDptXL/T7QuzmLzTVGpn3y2vLPhoK/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cc32Y8/hyWVRxJeGA/hC7khoIhT242oHWCHGl711/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cMN7Wo/hyWVRYNUdQ/pD89evhRKhVkFKSLd8VI0k/img.jpg?width=500&amp;amp;height=333&amp;amp;face=0_0_500_333');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[LLM] 프롬프트를 활용하며 느낀점 &amp;amp; 효과가 있던 방법 1편&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;오늘은 프롬프트를 활용하며 느낀점을 한번 정리해보려고 합니다.&amp;nbsp;본격적으로 프롬프트를 활용해서 서비스를 개발하기 시작한것은 약 3개월 전 입니다. 그 당시 저는 프롬프트라곤 코드 작성&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;todaycodeplus.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 구조화된 출력 (Structured output)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 지금은 크게 의미가 없을 수 있습니다. 왜냐하면 OpenAI에서 Structured Output을 지원하는 API를 제공하기 시작했기 때문이죠&lt;br /&gt;&lt;a href=&quot;https://platform.openai.com/docs/guides/structured-outputs&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://platform.openai.com/docs/guides/structured-outputs&lt;/a&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;우선 첫번째로 할 것은 바로 Few-Shot Eng입니다. 프롬프트에 여러 예시를 통해 질문의 답변을 제어할 수 있는데요, 이것을 통해서 input과 output에 대한 예시를 주어 LLM의 답변 방향을 결정 할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1724850639517&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. Identify the language of the input question.
2. Determine if the question is requesting a translation.
3. The response must be in JSON format as shown in the examples.
4. If a translation is requested but no target language is specified, default to Korean.

Examples 1:
    Input: &quot;안녕 반가워&quot;
    Output:
    {{ 
    &quot;language&quot;: &quot;Korean&quot;,
    &quot;translation_request&quot;: false,
    &quot;translate_to&quot;: null 
    }}
Examples 2:
    Input: &quot;hello hi&quot;
    Output:
    {{ 
    &quot;language&quot;: &quot;English&quot;,
    &quot;translation_request&quot;: false,
    &quot;translate_to&quot;: null 
    }}
    
[your response here]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한가지 방법이 더 있는데요 구조화된 출력을 하기 위해서 답변부의 형식을 변경하는것 입니다. 위에서 보여드린 예제의 Your Response Here 부분을 수정하는 것 입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1724850717525&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Example 1
{{ 
        &quot;language&quot;: &quot;[language]&quot;,
        &quot;translation_request&quot;: [true/false],
        &quot;translate_to&quot;: &quot;[target language]&quot;  // If translation is not requested, use null. If no target language is specified, default to Korean.
}}

# Example 2
{{ 
        &quot;TEST_KEY&quot;: &quot;[Your Response Here]&quot;
}}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 JSON형식으로 작성하고 각각의 Dict Key값에 어떤것이 들어가야하는지 정의할 수 있습니다. 아니면 단일 답변인 경우에는 JSON구조의 Key를 적어주시고 Value에&amp;nbsp; Your Response Here을 적어 여기에 답을 하라는 표현을 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 데이터 추출을 위한 프롬프트 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 데이터 추출을 하기위해서 LLM을 사용하기도 합니다. 예를 들어서 뉴스 기사에서 언급된 상표명만 추출 한다던가, 아니면 유저가 작성한 코맨트에서 언급된 상품만을 추출한다던가 하는 일등이 있죠, 이런 간단한 작업은 아주 성능이 낮은 모델 &quot;ex) GPT-4o-mini&quot; 에서도 아주 잘 추출을 하곤 합니다. 하지만 &quot;더 잘&quot; 추출하기 위해서는 프롬프트 엔지니어링이 필요합니다.&amp;nbsp;&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;저는 이런 추출형 프롬프트의 구조를 4단계로 나눕니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;제공 정보&lt;br /&gt;이 부분에는 내가 제공할 정보 예를 들었던 기사, 코맨트 등등을 입력합니다.&lt;/li&gt;
&lt;li&gt;추출 규칙&lt;br /&gt;내가 추출할 내용에서 뽑을 방법, 내용을 정의 합니다.&amp;nbsp;&lt;br /&gt;예를 들어서 [&quot;추출 할 대상&quot;, &quot;추출 된 항목에 대한 규칙&quot;, &quot;추출 된 항목을 구조화하는 RE식&quot; ...] 같은 것들이 들어가게 됩니다.&lt;/li&gt;
&lt;li&gt;답변 규칙&lt;br /&gt;이제 구조화되어 추출된 데이터를 출력 할 수 있도록 구조화 해야합니다. (여기서 구조화는 Value값의 구조화가 아닌 출력 format을 의미합니다.) 구조화를 위한 규칙을 정합니다. 구조화에 대한 내용은 1번을 참고해 주세요&lt;/li&gt;
&lt;li&gt;출력 시작&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 구성하면 다음과 같은 프롬프트를 만들 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1724851880366&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;MILK_NAME_EXTRACT_PROMPT = PromptTemplate(
        input_variables=[&quot;news_main&quot;],
        template=dedent(&quot;&quot;&quot;
        당신은 정해진 규칙에 맞게 주어진 정보 속에서 데이터를 추출하는 AI데이터 추출기 입니다. 주어진 news_main 값에서 우유의 제조사(OEM)와 우유의 이름을 추출해야 합니다.
        1. 제공 정보
            {news_main}
        2. 추출 규칙
            - 추출 할 OEM이름 및 제조사 이름을 모두 영문(Eng)으로 작성해야 합니다. (ex. 서울우유 무균 흰우유 -&amp;gt; seoulmilk/sterile_white_milk)
            - 제조사(OEM)과 우유이름은 '/'로 구분해야 합니다.(ex. 서울우유 무균 흰우유 -&amp;gt; seoulmilk/sterile_white_milk)
            - 모든 문자는 소문자로 작성되어야 합니다. (ex. 서울우유 무균 흰우유 -&amp;gt; seoulmilk/sterile_white_milk)
            - 제조사(OEM) 및 우유이름에 들어가는 특수문자는 모두 제거합니다. (매일우유@무균@흰우유/ -&amp;gt; mailmilk/sterile_white_milk)
            - 작성된 이름의 띄어쓰기는 모두 '_'로 대체 합니다.

        3. 답변 규칙
            - 답변은 반드시 JSON구조로 이루어져야 합니다.
            - JSON 구조는 다음과 같습니다.
                {{&quot;name&quot; : [&quot;milk_name1&quot;, &quot;milk_name2:, ...]}}
            - JSON을 출력할 때 markdown 형식을 사용하지 않습니다. 
        
        지금부터 추출을 시작합니다.
        [your response here]
        &quot;&quot;&quot;)
    )&lt;/code&gt;&lt;/pre&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;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘도 두개정도의 팁을 던지고 갑니다. 모두 프롬프트 작성하며 신나게 LLM과 놀기를 바랍니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;그래서 제가 만든&amp;nbsp; Prompt가 들어간 서비스는??&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://play.google.com/store/apps/details?id=com.autocrypt.evpedia&amp;amp;pcampaignid=web_share&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://play.google.com/store/apps/details?id=com.autocrypt.evpedia&amp;amp;pcampaignid=web_share&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1724851991684&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;EVPEDIA - AI 전기차 딜러 - Google Play 앱&quot; data-og-description=&quot;나만을 위한 전기차 AI 딜러. 최신 전기차 뉴스, 충전소 위치, 구매 상담 등을 손쉽게 탐색해보세요.&quot; data-og-host=&quot;play.google.com&quot; data-og-source-url=&quot;https://play.google.com/store/apps/details?id=com.autocrypt.evpedia&amp;amp;pcampaignid=web_share&quot; data-og-url=&quot;https://play.google.com/store/apps/details?id=com.autocrypt.evpedia&amp;amp;hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3z4ZH/hyWVVz6Zqt/GYzG5PtJ8ApIbwm506qXvK/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/bsAHIw/hyWVXSdZLc/wgiUhKil75fCgZEkuCu9O1/img.png?width=600&amp;amp;height=300&amp;amp;face=0_0_600_300,https://scrap.kakaocdn.net/dn/NbxeH/hyWVZoYquk/lO9AD3aeK8S0SHxfK7i9I0/img.png?width=222&amp;amp;height=296&amp;amp;face=0_0_222_296&quot;&gt;&lt;a href=&quot;https://play.google.com/store/apps/details?id=com.autocrypt.evpedia&amp;amp;pcampaignid=web_share&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://play.google.com/store/apps/details?id=com.autocrypt.evpedia&amp;amp;pcampaignid=web_share&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3z4ZH/hyWVVz6Zqt/GYzG5PtJ8ApIbwm506qXvK/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/bsAHIw/hyWVXSdZLc/wgiUhKil75fCgZEkuCu9O1/img.png?width=600&amp;amp;height=300&amp;amp;face=0_0_600_300,https://scrap.kakaocdn.net/dn/NbxeH/hyWVZoYquk/lO9AD3aeK8S0SHxfK7i9I0/img.png?width=222&amp;amp;height=296&amp;amp;face=0_0_222_296');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;EVPEDIA - AI 전기차 딜러 - Google Play 앱&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;나만을 위한 전기차 AI 딜러. 최신 전기차 뉴스, 충전소 위치, 구매 상담 등을 손쉽게 탐색해보세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;play.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;짜잔 드디어 출시가 됐습니다. 모두 한번씩 사용해 보세요!!&lt;/p&gt;</description>
      <category>DL</category>
      <category>AI</category>
      <category>chatbot</category>
      <category>DeepLearning</category>
      <category>DL</category>
      <category>GenAI</category>
      <category>langchain</category>
      <category>LLM</category>
      <category>prompt</category>
      <category>챗봇</category>
      <category>프롬프트 엔지니어</category>
      <author>크라우니</author>
      <guid isPermaLink="true">https://todaycodeplus.tistory.com/73</guid>
      <comments>https://todaycodeplus.tistory.com/73#entry73comment</comments>
      <pubDate>Wed, 28 Aug 2024 22:36:11 +0900</pubDate>
    </item>
  </channel>
</rss>