오늘은 LangChain과 임베딩 모델, 벡터 데이터베이스(Pinecone) 같은 AI 파이프라인 도구들에 대해 실습해보았다.
랭체인(LangChain)은 대규모 언어 모델(LLM)을 기반으로 한 애플리케이션 개발을 위한 오픈 소스 프레임워크이다.
llm = ChatOpenAI(model='gpt-4o-mini')
template = PromptTemplate.from_template("{city}의 랜드마크는 어디인가요?")
# LCEL 파이프라인 구성
chain_lcel = template | llm
chain_lcel.invoke({'city' : '파리'})
결과 :
AIMessage(content='파리에는 많은 유명한 랜드마크가 있습니다. 그 중 몇 가지를 소개하자면:\n\n1. 에펠탑 (Eiffel Tower) - 파리의 상징적인 건축물로, 1889년에 완공되었습니다. 전망대에서 파리 전경을 감상할 수 있습니다.\n\n2. 루브르 박물관 (Louvre Museum) - 세계에서 가장 큰 미술관으로, 모나리자, 비너스 드 미로 등의 유명한 작품들이 소장되어 있습니다.\n … 하며 쭉 나오며, 직관적으로 쭉 연결해주는 파이프라인이다.
prompt1 = PromptTemplate.from_template('{city}의 랜드마크를 1개만 알려주세요. 설명은 필요없고 이름만 1개 알려주세요.')
prompt2 = PromptTemplate.from_template('{landmark}에 {transport}로 가는 방법이 무엇인가요.')
chain1 = LLMChain(llm=llm, prompt=prompt1, output_key='landmark', verbose=True)
chain2 = LLMChain(llm=llm, prompt=prompt2)
seq_chain = SequentialChain(chains=[chain1, chain2], input_variables=['city', 'transport'])
seq_chain.invoke({'city' : 'seoul', 'transport' : '지하철'} )
결과 :

이처럼 나오고 단계별로 작업을 묶어서 실행한다.
이후에는 임베딩 모델(openai, upstage)도 실습해보았다.
texts = ['king', 'queen', 'slave', '왕']
openai_embeddings = {txt : get_openai_embedding(txt) for txt in texts}
cosine_similarity(openai_embeddings['queen'], openai_embeddings['king'])
결과 : np.float64(0.590601530239691)
upstage_embeddings = {txt : get_upstage_embedding(txt) for txt in texts}
cosine_similarity(upstage_embeddings['queen'], upstage_embeddings['king'])
결과 : np.float64(0.6445486818426877)
OpenAI와 Upstage 임베딩 모델을 사용해 단어들을 벡터로 변환하고, 코사인 유사도를 계산해 단어 간 의미적 유사성을 확인해보았다. 'queen'과 'king'간의 유사도는 upstage 모델이 더 높게 나왔다.
Vector Database는 벡터 형태로 데이터를 저장하고 검색하는 데이터베이스이다. 벡터 데이터는 일반적으로 고차원 공간에 위치하며, Vector Database는 이러한 데이터를 효율적으로 저장하고 검색할 수 있다. 그 중 오늘은 Pinecone을 사용해보았다.
pinecone_client = Pinecone(api_key=PINECONE_API_KEY)
pinecone_client.create_index(
name='embedding-3d',
dimension=3,
metric='cosine',
spec=ServerlessSpec(
cloud='aws',
region='us-east-1'
)
)
index = pinecone_client.Index('embedding-3d')
index.upsert( # update + insert 함수
vectors = [{'id' : 'vec1' , 'values' : [1.0, 1.5, 2.0] , 'metadata' : {'genre' : 'drama'}},
# 'values'와 'metadata'를 바꿔가며 6번 반복 (vec6까지) ...
],
namespace='embedding-3d-ns1'
)
index라는 저장소를 새로 만들고, Pinecone 벡터 데이터베이스에 3차원 벡터들과 메타데이터(장르)를 저장해보았다. 그 결과 Pinecone 브라우저에 가보면

embedding-3d의 인덱스가 새로 생겼고, 그 안에

내가 넣어준 데이터가 6개가 들어가있는 것을 볼 수 있다.
오늘 수업에서는 LangChain, 임베딩 모델, 벡터 데이터베이스까지 한 번에 다뤄보면서 AI 파이프라인이 어떻게 연결되는지 큰 그림을 볼 수 있었다. 단순히 LLM에 프롬프트를 던지는 것이 아니라, LCEL과 SequentialChain으로 여러 작업을 단계별로 묶어내는 과정이 새로웠던 것 같다. 오늘 배운 내용을 통해 단순한 모델 사용을 넘어서, 검색·임베딩·LLM을 결합한 더 실용적인 서비스같은 것들을 프로젝트에서 직접 만들어보고 싶다는 생각이 들기도 했다.
어제에 이어 오늘도 임베딩하는 실습을 진행하였다. 위키 데이터셋을 불러와 벡터DB 인덱스를 구성하는 과정이다. 인덱스의 이름은 wiki이고 1536차원, 모델은 text-embedding-3-small을 사용하였다.
splitter = RecursiveCharacterTextSplitter(
chunk_size=400,
chunk_overlap=20,
length_function=len,
separators=["\n\n", "\n", " "] # 자르는 순서
)
splitter로 텍스트를 자르고, 문서에 넣어 적재하였다. 그 결과
index.describe_index_stats()
결과 :
{'dimension': 1536,
'index_fullness': 0.0,
'metric': 'cosine',
'namespaces': {'wiki-nss1`': {'vector_count': 1200}},
'total_vector_count': 1200,
'vector_type': 'dense'}
로 1200개의 데이터가 적재된 것을 볼 수 있다.
이후에는 질의 임베딩도 실습해보았다.
question = ["벨기에는 어디에 있나요?"]
vector_store = PineconeVectorStore(index=index, embedding=embedding, text_key='full_text')
docs = vector_store.similarity_search(query=question[0], k=5, namespace='wiki-nss1`')
docs
결과 : [Document(id='eba1897c-7877-4b38-bda3-33d7dac5962a', metadata={'chunk_id': 11.0, 'title': '엔트로피', 'url': 'https://ko.wikipedia.org/wiki/%EC%97%94%ED%8A%B8%EB%A1%9C%ED%94%BC', 'wiki_id': '158'}, page_content='엔트로피(, )는 열역학적 계의 유용하지 않은 (일로 변환할 수 없는) 에너지의 흐름을 설명할 때 이용되는 상태 함수다. 통계역학적으로, 주어진 거시적 상태에 대응하는 미시적 상태의 수의 로그로 생각할 수 있다. ...
이런식으로 엔트로피에 대한 정의가 설명된다. 왜 이런 엉뚱한 결과가 나왔는지는 좀 더 고민해봐야 할 것이고, 단순히 벡터DB에 문서를 넣고 검색하는 것만으로는 원하는 답변을 얻기 어렵다는 걸 체감했다.
이 다음으로는 데이터 전처리 실습도 해보았다. 소득세법 데이터를 docx로 저장하고, 로딩하는데 chunk_size=1500, chunk_overlap=200 으로 분할하였다. 모델은 text-embedding-3-large을 적용하였고, Chroma를 사용해 데이터를 적재하였다. 먼저 질의를 해보면 아래처럼 높은 답변 5개가 나온다.

이를 llm에 적용시켜 프롬프트를 주었더니 이처럼 답변의 내용을 볼 수 있으며, 질문에 대한 답을 생성해냈다.

다른 사람들과 결과가 조금 다르게 나오기도 하였는데, 기존 데이터에서 이미지 표를 텍스트로 변환하는 과정이 각자 다르기도 하고, 모델의 답변이 항상 같지 않기 때문에 그런 것 같다.
Pinecone에 적재하고 질의를 던졌을 때 기대한 답변이 아닌 전혀 다른 결과가 나온 것을 보면서, 단순히 데이터를 넣고 검색하는 것만으로는 원하는 수준의 성능이 나오지 않는다는 점을 배울 수 있었다. 특히 텍스트를 어떻게 잘라서(chunking) 저장하느냐가 결과 품질에 크게 영향을 준다는 것을 확인했다. 앞으로는 전처리와 검색 전략을 더 깊이 고민해봐야겠다는 생각이 들었다.
오늘은 Kaggle 와인데이터를 활용해 RAG 실습을 진행했다. 가장 먼저 recommend_dishes_chain랭체인 프롬프트와 Describe_dish_flavor_chain랭체인 프롬프트를 작성하였다.
recommend_dishes_chain 와인 추천 랭체인 프롬프트 def recommend_dishes_chain(query):
chat_template = ChatPromptTemplate.from_messages(
[
('system', SYSTEM_PROMPT),
('human', [{'type':'text', 'text':query['text']},
{'type':'image_url', 'image_url':{'url':query['image_url']}}])
]
)
chain = chat_template | llm | output_parser
return chain
Describe_dish_flavor_chain 요리 맛 요약 랭체인 프롬프트def describe_dish_flavor(query):
prompt = ChatPromptTemplate.from_messages([
("system", """
Persona:
As a flavor analysis system, I am equipped with a deep understanding of food ingredients, and ...
"""),
("user", "이 요리의 이름과 맛을 한 문장으로 요약해주세요.")
])
template = []
if query["image_url"]:
template += [{"image_url": image_url} for image_url in query["image_url"]]
prompt += HumanMessagePromptTemplate.from_template(template)
output_parser = StrOutputParser()
chain = prompt | llm | output_parser
return chain
연결해둔 랭체인 프롬프트들을 작성 후, CSVLoader로 데이터를 LangChain Document 객체로 변환하고 와인 데이터를 OpenAI Embeddings (text-embedding-3-large)모델로 임베딩을 하였다. 어떤 분은 허깅페이스에서 새로운 모델을 찾아 임베딩해보고, 데이터베이스에 적재할 때 어떤 분은 Pinecone에 적재하기도 했지만, 나는 크로마DB에 적재하였다. 데이터가 커서 적재하는 게 오래 걸렸지만 크로마에 적재하는 경우가 가장 빨랐다.
이후 질의를 던져보기도 했다.


마지막으로 chain 연결을 한 후 음식 사진을 보여주어, invoke()함수로 원하는 답변을 얻을 수 있다. 음식 사진을 보고, 음식의 맛을 요약 후 어울리는 와인을 추천해주는 RAG 실습이 마무리되었다.

수업 막바지에는 Fast API에 대한 설명과 연습을 해보는 것으로 마무리 되었다.
FastAPI 프레임워크는 모던 Python 기능(type hints 등)을 완전히 활용하고, 기본적으로 비동기 코드를 지원하여 높은 성능을 제공한다
app = FastAPI()
@app.get('/')
def root():
return {'message' : '안녕~! fast api...~~'}
if __name__ == "__main__":
uvicorn.run('exam1:app', host='127.0.0.1', port=8002, reload=True)
파이썬 파일을 실행하면 웹 브라우저에서 아래와 같이 뜨는 것을 확인할 수 있다.

오늘은 RAG 파이프라인을 직접 구현해보면서 이론으로만 알던 개념이 실제 코드로 어떻게 연결되는지 체험할 수 있었다. 단순히 임베딩과 검색만 하는 것이 아니라, 음식 이미지를 보고 맛을 요약한 뒤 어울리는 와인을 추천해주는 체인을 연결하면서 AI가 실제 서비스로 확장될 수 있음을 실감했다. 또한 여러 개의 체인을 작성해 단계별로 연결하는 과정에서, LangChain의 장점이 단순한 API 호출을 넘어서 “복잡한 AI 파이프라인”을 설계할 수 있게 해준다는 것을 느낄 수 있었다.
FastAPI 기본 사용법을 배우고, 간단한 웹 API부터 DB, HuggingFace 모델 연동까지 실습했다.
음식 항목(Item)을 등록하고, 수정하고, 일부만 업데이트하는 데이터를 SQLite에 저장까지 CRUD 실습을 하였다.
@app.get("/items")
async def read_items(db=Depends(get_db)):
items = db.query(ItemModel).all()
return items
@app.post('/items', status_code=201)
async def create_item(item: Item, db=Depends(get_db)):
db_item = ItemModel(**item.model_dump())
db.add(db_item)
db.commit()
return item
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, db=Depends(get_db)):
db_item = db.query(ItemModel).filter(ItemModel.id == item_id).first()
if not db_item:
raise HTTPException(status_code=404, detail="Item not found")
for key, value in item.model_dump().items():
setattr(db_item, key, value)
db.commit()
db.refresh(db_item)
return {"item_id": item_id, **item.model_dump()}
@app.delete("/items/{item_id}")
async def delete_item(item_id: int, db: Session = Depends(get_db)):
# DB에서 해당 아이템 찾기
db_item = db.query(ItemModel).filter(ItemModel.id == item_id).first()
if not db_item:
raise HTTPException(status_code=404, detail="Item not found")
# 삭제
db.delete(db_item)
db.commit()
return {"message": f"Item {item_id} has been deleted successfully"}
SQLite 데이터베이스에 적재하고, 수정하고, 보고, 삭제까지 바로 반영되어서 쉽게 볼 수 있었다.

수업 마지막에는 FastAPI와 허깅페이스를 활용하여 감정 분석 모델을 불러와 직접 원하는 문장을 입력해보고, 긍정/부정과 점수를 예측해보았다.

입력란에 I like Friday를 입력했더니 그 결과 아래처럼 나왔다.

FastAPI를 이용해 웹 API를 만들고 DB와 연결하는 과정을 실습하면서, 실제 서비스 개발의 기본기를 체험할 수 있었다. 단순히 API를 호출하는 것이 아니라, SQLite에 데이터를 저장하고 CRUD 작업이 바로 반영되는 것을 확인하니 백엔드 로직이 어떻게 동작하는지 더 명확히 이해할 수 있었던 것 같다. 마지막에 HuggingFace 모델을 FastAPI에 연동해 감정 분석을 실습했을 때는, 지금까지 배운 AI와 웹 기술이 실제로 연결되는 모습을 직접 볼 수 있어서 흥미로웠다.
항상 잘 보고있습니다