배경 및 목적
LLM 또는 RAG시스템으로 쿼리를 해보면 내 의도와는 다른 결과가 나오기도 한다. 도대체 생성형AI안쪽에서는 무슨일이 일어나고 있는가를 알고 싶다. print()문을 쓰거나 vs code의 breakpoint를 쓰는 방법이 있으나 그건 디버깅할때 쓰는게 맞다. 생성형AI의 품질문제는 과거 요청된 쿼리까지 모두 기록으로 남아있어야 한다. 바로 이런걸 해주는것 중의 하나가 langsmith이다.
LlamaIndex로 만들어진 어플리케이션에서도 LangChain의 패밀리중의 하나인 LangSmith를 써보자는게 이번의 목표이다.
참고 자료
라마인덱스와 랭스미스를 직접 결합하면 될줄알았는데, 나의 어프로치는 실패했다. 인터넷에 사례를 찾다가 아래와 같은 아티클을 발견해서 따라했다. 해당 글은 목표가 langsmith의 도입은 아 니었으나 langchain을 쓰기만 하면 langsmith도입은 그냥 될것 같아서 시도해 보기로 한다
활용 툴
설치한 패키지들
langchain
langchain_community #덕덕고등 외부 서비스를 langchain에 통합하는 역할의 패키지
langchain_openai
llama_index
llama-index-llms-langchain #라마인덱스에서 랭체인을 쓰기 위한 어뎁터 역할의 패키지
duckduckgo-search
실행 과정
관련 환경변수들을 세팅하는데 중요한것은 LANGCHAIN_TRACING_V2 이다. 이곳에 true로 기록해줘야 langsmith가 동작한다. 나머지는 보면 해석되는 항목들이다.
os.environ['OPENAI_API_KEY'] = "sk-xxxx"
os.environ['LANGCHAIN_TRACING_V2'] = "true"
os.environ['LANGCHAIN_API_KEY'] = "lsv2_xxxx" #langsmith
os.environ['LANGCHAIN_PROJECT'] = 'langchain+llamaindex agentic rag'
언제나 그렇듯, llm모델과 rag를 위한 임베딩 모델을 만든다.
# LLM
llm = ChatOpenAI(model_name="gpt-4-1106-preview", temperature=0, streaming=True)
# Embedding Model
embed_model = OpenAIEmbedding(
model="text-embedding-3-small", embed_batch_size=100
)
언제나 그렇듯, RAG검색 대상이 되는 PDF를 읽어온다. 그걸 가지고 index객체를 만든다
# load data
lyft_docs = SimpleDirectoryReader(
input_files=["./data/10k/lyft_2021.pdf"]
).load_data()
uber_docs = SimpleDirectoryReader(
input_files=["./data/10k/uber_2021.pdf"]
).load_data()
# build index
lyft_index = VectorStoreIndex.from_documents(lyft_docs)
uber_index = VectorStoreIndex.from_documents(uber_docs)
인덱스 객체에서 QueryEngine을 만든다. 이건 RAG검색용이다. 보통은 retriever라고 변수명을 쓰기도 한
lyft_engine = lyft_index.as_query_engine(similarity_top_k=3)
uber_engine = uber_index.as_query_engine(similarity_top_k=3)
Query_Engine을 tool로 만든다. Agentic 모델에서 말하는 Tool인것 같은데, 정확하게 어떤 역할인지는 확실하지 않다.
query_engine_tools = [
QueryEngineTool(
query_engine=lyft_engine,
metadata=ToolMetadata(
name="lyft_10k",
description=(
"Provides information about Lyft financials for year 2021. "
"Use a detailed plain text question as input to the tool."
),
),
),
QueryEngineTool(
query_engine=uber_engine,
metadata=ToolMetadata(
name="uber_10k",
description=(
"Provides information about Uber financials for year 2021. "
"Use a detailed plain text question as input to the tool."
),
),
),
]
한줄이지만, 중요하다. 라마인덱스용 tool에서 랭체인용 tool로 바꾼다
llamaindex_to_langchain_converted_tools = [t.to_langchain_tool() for t in query_engine_tools]
이제 프롬프트 작성이다. 기쁘다. 마지막문장에 한국어로 대답해달라고 추가해본다.
system_context = "You are a stock market expert.\
You will answer questions about Uber and Lyft companies as in the persona of a veteran stock market investor.\
Ensure all answers are provided in Korean."
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
system_context,
),
("placeholder", "{chat_history}"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
]
)
# Construct the Tools agent
agent = create_tool_calling_agent(llm, tools, prompt,)
agent
AgentExecutor생성. 준비는 이것으로 the end.
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, return_intermediate_steps=True, handle_parsing_errors=True, max_iterations=10)
agent_executor
그럼 질문을 해봅시다. 결과는 아래와 같이 소소해 보입니만,
정확한 대답을 하느냐 못하느냐 또는 Agentic App이 어느 툴을 써서 동작했느냐, 그리고 얼마나 시간이 걸렸느냐 와 같은게 궁금하다면, LangSmith를 봐야 합니다.
몇번을 쿼리하건 데이터가 계속쌓이겠죠. RAG에 해답이 없는 질문이 시간이 많이 걸리는게 보입니다.
쿼리 하나를 처리하는데 3단계가 걸렸네요. 더 자세하게 보이는법도 있으리라 생각됩니다. 두개의 외부 툴을 선언했기 때문에 그 두개가 보이고 그중에 어떤 툴이 동작해는지가 표시되고 있습니다.
한글로 물어봐도 영어로 바뀌어서 동작하네요.
결과 및 인사이트
하려고 하는 기능의 특성에 따라 여러 llm모델들을 조합할수 있다는 것을 알았다. 그리고 내부 동작을 langsmith같은 trace툴로 볼수 있다는 것도 알았다.
그런데 문제는 아직 내가 익숙해지려면 구만리정도 남았다는거다.
숨겨진 버그
이 코드의 원본에는 두개의 retriver외에 외부 검 색엔진(덕덕고)도 있다. RAG에 해당하는 답이 없을경우에 동작해야 하는데, 에러를 일으키며 중지되어 버리고 있다. 해결책은 발견하지 못했다.
AgentExecutor.invoke() 또는 run() 메소드 실행시 Too many arguments 에러 메세지 출력되는 문제로 tool chain제거
ToolException: Too many arguments to single-input tool DuckDuckGoSearch.
Consider using StructuredTool instead. Args: [['Lyft revenue growth 2022'], {'tags': ['finance', 'stock market', 'Lyft'], 'callbacks': []}]