Skip to content
··6분 읽기

AI 에이전트로 코딩 수렁에서 빠져나온 방법

코딩 실력 제로와 열정으로 챗봇 제작에 뛰어들었지만, v0.1이 CSV 데이터베이스와 원시적 청킹으로 이루어진 재앙이라는 것을 발견했습니다 — AI 에이전트가 저를 구해줄 때까지.

업데이트 (2026): 이 챗봇은 Sydney로 발전했습니다! 여러 번의 반복을 거쳐, Sydney는 이제 /ask/에 있으며 블로그 콘텐츠와 제품에 집중합니다.


지난해 11월 초, 저는 DIY 챗봇 버전 0.1을 출시했습니다. 그때 저는 "v0.1이 코딩 초보자로서 큰 첫 걸음이었지만, 상당한 한계가 있었다"고 썼습니다. 결과적으로 그것은 과소평가였습니다. 전체 과정과 제가 만든 것이 얼마나 투박했는지 제대로 인식하지 못했습니다. 현실은, 첫 시도가 진심 어린 것이었지만, 열정은 있되 제한된 노하우로 급하게 짜맞춘 프로토타입에 가까웠습니다.

이 글은 단순한 후속편이 아닙니다; 그 시점부터 펼쳐진 여정 — 시행착오와 값진 교훈으로 가득한 여정에 대한 깊은 탐구입니다. 단지 투명성을 위해서뿐만 아니라, 비슷한 길을 가는 누군가에게 공감하거나 도움이 될 수 있기를 바라며 이 모험의 세세한 부분까지 공개합니다. (추가 맥락으로, 저는 코딩 경험이 전혀 없는 중년의 광고업 전문가입니다.)

언급했듯이, 챗봇 v0.1을 출시하기 위해 저는 주로 이 짧은 강좌 "Building Systems with the ChatGPT API"와 OpenAI의 두 가지 쿡북: Question answering using embeddings-based search와 How to count tokens with tiktoken의 지침을 따랐습니다.

다음은 제 챗봇 v0.1이 끔찍했던 이유입니다:

  • 청킹(Chunking): 저는 단순히 긴 블로그 글을 토큰 길이에 기반한 더 작은 청크로 분할했습니다, 즉 단순한 정적 문자 청크입니다. 가능한 가장 원시적인 분할 방법이었습니다 :D. 왜 이것이 끔찍한 아이디어인지 알고 싶으시다면, Greg Kamradt의 5단계 텍스트 분할에 대해 여기서 읽어보세요.
  • 임베딩(Embedding): 임베딩에 어려움을 겪었습니다. OpenAI의 임베딩 모델을 사용했지만, API 요청 제한에 계속 걸려 임베딩 과정이 중간에 실패했습니다. 그래서 요청을 배치로 나누고 배치 사이에 타임아웃을 추가해 제한을 피하는 방법을 배웠습니다. 결국 생성된 임베딩을 임시방편 "데이터베이스"로 간단한 .csv 파일에 저장했습니다.
  • 데이터베이스: CSV가 데이터베이스에 최적이 아니라는 것을 알았지만, 더 나은 대안을 구현할 기술이 부족했습니다.
  • 메타데이터: 처음에는 게시 날짜와 포스트 URL 같은 메타데이터를 포함하는 것이 챗봇이 사용자 질문에 정확하게 답하는 데 중요하다는 것을 인식하지 못했습니다. 관련 메타데이터를 포함하기 위해 임베딩과 저장 과정을 반복해야 했습니다.
  • 검색기(Retriever): 다양한 검색기 유형과 알고리즘에 대해 몰랐습니다. 단순히 OpenAI의 관련성 검색을 사용해 하드코딩된 수의 결과를 가져왔습니다.
  • 메모리: 대화를 하려면 챗봇이 사용자가 이전에 말한 것을 기억할 수 있어야 합니다. 그리고 이것이 (당시) gpt-3.5의 제한된 컨텍스트 윈도우 길이에서 청크 크기와 검색할 결과 수 사이의 명확한 트레이드오프가 있는 부분이었습니다.
    • 예를 들어, 청크 크기가 800 토큰이고 검색기가 상위 8개 결과를 반환하면, 6,400 토큰 또는 이전 모델 제한의 50% 이상입니다.
    • 위의 것은 1개의 질문일 뿐이므로 다중 턴 대화에서 메모리가 얼마나 빨리 가득 차는지 상상할 수 있습니다.
    • 이 문제를 해결하는 한 가지 방법은 더 작은 청크 크기를 사용하고 검색기가 더 적은 결과를 반환하게 하는 것이지만, 기본적인 검색기(위)로는 모델이 질문에 답하기 위한 포괄적인 정보를 가지지 못한다는 것을 의미합니다.
  • 어떤 IDE도 사용하지 않았습니다. 모든 코드를 Mac의 TextEdit으로 편집했습니다 :D (제가 초보였다고 말했나요? :P)
  • 계속 나열할 수 있지만 감을 잡으셨을 것입니다.

나의 "죽음의 계곡"

v0.1의 한계를 넘어 개선하고 싶어서, 스킬을 업그레이드하기 위한 퍼즐 조각을 제공해줄 여러 온라인 강좌를 시도했습니다. 하지만 진전은 막다른 골목으로만 이어졌습니다.

벡터 데이터베이스 (Vector Databases: from Embeddings to Applications with Weaviate), 평가적 RAG 방법 (Building and Evaluating Advanced RAG Applications with Llama-Index and Truera), 고급 검색 기법 (Advanced Retrieval for AI with Chroma) 강좌들을 힘들게 수강했습니다. 아무리 노력해도 이론을 제 블로그 데이터를 사용한 실제 적용으로 연결할 수 없었습니다.

강좌가 잘못 설계된 것일까요? 아닙니다 — 부족한 것은 제 자신의 기초 지식이었습니다. 그래도, 실패에 이은 실패는 엄청나게 좌절스러웠고, 의욕을 꺾었습니다. 저는 비유적으로 어떻게 나아가야 할지 모르는 계곡에 갇혀 있었습니다.

시간이 지나며 얻은 작은 성과들

그래도 반복된 실패에서 가치 있는 교훈들이 나왔습니다:

  • TextEdit에서 VS Code로의 전환

  • GitHub Copilot 확장 프로그램 활용

  • 개발 환경으로서 Jupyter Notebook의 가치 인식

마지막 강좌에서 챗봇 구축을 위한 새로운 인기 프레임워크인 LangChain을 언급했습니다. 사실 몇 달 전에 LangChain 튜토리얼("LangChain: Chat with Your Data"와 "Functions, Tools and Agents with LangChain")을 시도했지만 별 성과가 없었습니다. 하지만 이제 힘들게 얻은 지식을 가지고, 문서를 다시 살펴보니 깨달음이 왔습니다. 개념들이 맞아떨어졌고 모듈러 아키텍처가 직관적으로 이해되었습니다.

LangChain의 강력한 기능을 제 열정 프로젝트에 적용하는 것을 구상할 수 있었습니다. 마침내, 앞으로 나아갈 길이 드러났습니다! 한 걸음씩, 데이터 수집, 임베딩, 저장 및 검색을 위한 파이프라인에 적응했습니다.

각 구현에 성공할 때마다 자신감이 커졌습니다. V2가 형태를 갖추기 시작했습니다...

기초 재구축

Langchain을 가이드로 삼아, 챗봇을 처음부터 재구축하기 시작했습니다:

WordPress 내보내기를 JSON으로 수집

수집 매개변수를 조정하는 시행착오 끝에, LangChain의 JSONLoader가 내보낸 글을 올바르게 파싱했습니다. 이제 검증된 입력이 다운스트림 파이프라인에 공급될 수 있었습니다. (이 단계의 코드는 이 공개 Github 저장소의 "DataIngestionAndIndexing.ipynb" 파일에 있습니다.)

자동 텍스트 분할

제 순진한 토큰 길이 청킹은 LangChain의 SentenceTransformers로 대체되었으며, 고급 NLP를 사용해 의미 단위를 분할합니다. 더 이상 중간에 잘린 문장이 없습니다! 설정은 메모리가 제한된 모델에 적합한 크기의 청크를 유지했습니다.

from langchain.text_splitter import SentenceTransformersTokenTextSplitter
# Define the token splitter with specific configurations
token_splitter = SentenceTransformersTokenTextSplitter(
    chunk_overlap=0,  # Overlap between chunks
    tokens_per_chunk=256  # Number of tokens per chunk
)
# Split the documents into chunks based on tokens
all_splits = token_splitter.split_documents(documents)
print(f"Total document splits: \{len(all_splits)\}")

임베딩 및 인덱스 생성

LangChain의 OpenAI 임베딩 래퍼를 사용하면서 과거 OpenAI API 제한과의 씨름은 사라졌습니다. 두 줄의 코드로 번들되어, 임베딩이 분할된 텍스트에서 핵심 특징을 깔끔하게 추출했습니다.

벡터 저장소로는 WeaviateChroma 대신 FAISS(Facebook AI Similarity Search)를 선택했습니다. 업계에서 검증된 FAISS가 제 필요에 맞는 기능과 복잡성의 올바른 균형을 갖추고 있었습니다. CPU 버전이 블로그 청크를 빠르게 인덱싱하여 컴팩트한 검색 가능한 데이터베이스를 출력했습니다. 더 이상 배치 처리나 OpenAI API 요청 제한에 대해 걱정할 필요가 없습니다.

Langchain은 여러 벡터 저장소를 지원하므로 여기서 확인할 수 있습니다.

# Initialize embeddings and FAISS vector store
embeddings = OpenAIEmbeddings()
db = FAISS.from_documents(all_splits, embeddings)

# Save the vector store locally
db.save_local("path/to/save/faiss_index")  # Placeholder for save path/ index name

두 줄의 코드! 그게 전부입니다.

또한 콘텐츠가 적지 않은 경우 (저는 400개 이상의 블로그 글이 있습니다), 임베딩 직후에 바로 검색기를 사용하려 하면 안 됩니다. 인덱스가 완료/안정화되는 데 약간의 시간이 필요하기 때문입니다.

검색기 평가

이 검색기 설정은 FAISS를 벡터 저장소로 사용하여 단 1줄의 코드로 완료됩니다 :D.

retriever = db.as_retriever(search_type="mmr")

인덱스와 검색기가 준비되면서, 지능형 챗봇에 공급할 데이터 파이프라인이 준비되었습니다!

대화형 에이전트 설계

LangChain의 에이전트 프레임워크를 사용하여 이 챗봇을 구축하기로 했습니다. 이 시점에서 과한가요? 네 맞습니다. 하지만 시간이 지남에 따라 이 챗봇을 발전시키고 더 많은 "도구", 즉 기능을 제공하고 싶다는 희망이 있습니다. Langchain은 에이전트를 설정하고 사용할 도구를 제공하는 것을 매우 쉽게 만듭니다.

embeddings = OpenAIEmbeddings()
db = FAISS.load_local("path/to/your/faiss_index_file", embeddings)  # Replace the path with your actual FAISS index file path
retriever = db.as_retriever(search_type="mmr")
tool = create_retriever_tool(
    retriever,
    "search_your_blog",  # Replace "search_your_blog" with a descriptive name for your tool
    "Your tool description here"  # Provide a brief description of what your tool does
)

tools = [tool]
prompt_template = ChatPromptTemplate.from_messages([
    # Customize the prompt template according to your chatbot's persona and requirements
])

llm = ChatOpenAI(model_name="gpt-3.5-turbo-1106", temperature=0)
llm_with_tools = llm.bind_tools(tools)
agent = (
    \{
        "input": lambda x: x["input"],
        "agent_scratchpad": lambda x: format_to_openai_tool_messages(x["intermediate_steps"]),
        "chat_history": lambda x: x["chat_history"],
    \}
    | prompt_template
    | llm_with_tools
    | OpenAIToolsAgentOutputParser()
)

agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

에이전트의 최종 전체 Python 코드

마지막으로, 챗봇 v2를 사용해 보고 싶으시다면, 여기에 있습니다.

챗봇이 Chandler에 대해 아무것도 모르는 것이 이상한가요?

추신: 챗봇이 저에 대해 아무것도 모른다고 알려주신 분들께 감사드립니다. 맞습니다! "소개" 페이지를 내보내는 것을 잊고 "게시된 글"만 내보냈기 때문입니다. 이것을 두 번째로 잊은 것이라 평가 질문 목록에 저에 대한 기본 질문을 포함하겠습니다. 교훈을 얻었습니다!

건설적인 피드백을 공유해주신 모든 분들께 감사드립니다. 계속 보내주세요. 그리고 네, 챗봇이 시작이 매우 느린 것을 알고 있으므로 그것도 작업 중입니다. :| (제가 초보라고 말했나요? :P)

빠른 업데이트

챗봇이 저에 대해 아무것도 모르는 문제는 이제 수정되었습니다. 제가 한 것과 배운 것은 다음과 같습니다:

  • 위의 방법대로 Wordpress에서 "About me" 페이지를 .XML로 내보냈습니다.
  • 위와 같이 FAISS를 사용하여 텍스트 분할 및 임베딩 생성을 수행했습니다. 검색기를 테스트하기 위해 다른 이름으로 벡터 저장소를 로컬에 저장했습니다
# save the vector store to local machine
db.save_local("faiss_index_about")
# set up the retriever to test the new vector store about Chandler
from langchain.retrievers.multi_query import MultiQueryRetriever
llm = ChatOpenAI(temperature=0)
retriever_from_llm = MultiQueryRetriever.from_llm(
    retriever=db.as_retriever(), llm=llm
)
# test retriever
question = "Who is Chandler Nguyen?"
results = retriever_from_llm.get_relevant_documents(query=question, top_k=8)
for doc in results:
    print(f"Content: {doc.page_content}, Metadata: {doc.metadata}")

  • 결과적으로 두 개의 FAISS 벡터 저장소를 병합하는 과정은 놀랍도록 간단합니다. 여기 문서를 참조하세요.
# Try to merge two FAISS vector stores into 1
# load the vector store from local machine
from langchain_community.vectorstores import FAISS
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
db_1 = FAISS.load_local("faiss_index_about", embeddings)
db_2 = FAISS.load_local("faiss_index", embeddings)
db_2.merge_from(db_1)
# save the vector store to local machine
db_2.save_local("faiss_index_v2")
# test the new vector store to confirm correct retrieved documents
retriever = db_2.as_retriever(search_type="mmr")
results = retriever.get_relevant_documents("Who is Chandler Nguyen?")
for doc in results:
    print(f"Content: {doc.page_content}, Metadata: {doc.metadata}")
  • 그 후로는 위와 거의 같은 과정이며, 새로운 벡터 저장소를 사용합니다

2월 14일 업데이트: 챗봇 v2.10 공개

이 챗봇 배포 2주 후, 향상된 속도, 확장성, 단순성으로 사용자 경험을 한 단계 높인 버전 2.10을 소개했습니다. 여기에서 자세히 읽어보실 수 있습니다.

3월 25일 업데이트: 프론트엔드 업그레이드에서 Docker 고난과 돌파구까지

여기에서 자세히 읽어보실 수 있습니다.

계속 읽기

나의 여정
연결
언어
환경설정