빌릿 챗봇 프로젝트 5


빌릿 챗봇 프로젝트 리뷰 5

agent

챗봇을 구현하기 위해 가장 신경쓴 부분은 제때 필요한 도구가 호출되게 하는 것이었다.
처음엔 간이대출심사만 바인딩하고 나머지는 체인으로 해결할 생각이었다.
하지만 이런 방식은 도구 호출 안정성이 떨어지고, 에러가 발생할 수 있으므로 에이전트를 활용하는 것으로 바꾸었다.

agent 코드

import funcsfortool2
from funcsfortool2 import SimpleScreening, retrieve, fin_retirever
from langchain.agents import AgentExecutor, create_openai_functions_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.tools.render import render_text_description
from langchain_community.chat_message_histories import SQLChatMessageHistory
from sqlalchemy import create_engine
import time
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain.callbacks.base import BaseCallbackHandler


simple = SimpleScreening()
retriever_tool = retrieve()
fin_retriever_tool = fin_retirever()



template2 = """
        당신은 외국인 노동자들을 위한 P2P서비스 빌리잇(Billit)의 ㅈㄴ똑똑한 챗봇 상담사입니다.
        당신은 다음의 도구들을 사용할 수 있습니다.:
        {tools}
        도구의 이름은 {tool_names}입니다.
        History: 저장된 대화내역 입니다. 당신은 이걸 기반으로 대답할 수 있습니다. 자세한 예시는 rule에 있습니다.:{chat_history}
        다음의 rule을 무조건 지키면서 사용자의 질문에 대답하세요.
        rule:
        - Question이 들어오면 [{tool_names}]중 하나의 툴을 선택해 사용하세요
        - 다만 '그것들의 차이가 뭐야?', '내가 물어봤던거 다시 알려줘.'와 같은 상황에선 회원가입을 유도하는 답을 하세요.
        - 질의응답 유형의 Question이 들어오면 local_retriever를 쓰세요.
            - 예시: '자료열람요구권이 뭐야?', '대출 어떻게 받아?', '투자는 어떻게 해?'와 같은 상황에선 local_retriever를 쓰세요.
        - Question의 언어에 맞게 답하세요. 만약 Question이 베트남어면 베트남어로 답하세요. Question이 한국어면 한국어로 답하세요. Question의 언어를 모르면 영어로 답하세요.
        - 대답은 짧고 간단하게 해야합니다.
        - 말 끝에 회원가입을 유도하는 말을 덧붙이세요.
            - 예시 : '자세한 사항을 알고 싶으시면 회원가입을 진행해 주세요.'
"""

class NonLoginAgent():
    def __init__(self, llm):
        self.llm = llm
        self.tools =  [retriever_tool]
        self.prompt = ChatPromptTemplate.from_messages([
            ("system", template2),
            MessagesPlaceholder("chat_history", optional = True),
            ("human", "{input}"),
            MessagesPlaceholder("agent_scratchpad")
        ])
        self.agent = self._get_agent()
    
    def _get_agent(self):
        tool_names = ", ".join([t.name for t in self.tools])
        prompt_fin = self.prompt.partial(tools=render_text_description(self.tools), tool_names=tool_names)
        agent_t = create_openai_functions_agent(llm = self.llm, tools = self.tools, prompt = prompt_fin)
        agent_aex = AgentExecutor(agent = agent_t, tools = self.tools, handle_parsing_errors=True, max_iterations = 15, verbose=False)
        return agent_aex
    
    def answer_to_me(self, query):
        agent = self.agent
        result = agent.invoke({'input' : query})
        return result
    

template = """
        당신은 외국인 노동자들을 위한 P2P서비스 빌리잇(Billit)의 똑똑한 챗봇 상담사입니다.
        당신은 다음의 도구들을 사용할 수 있습니다.:
        {tools}
        도구의 이름은 {tool_names}입니다.
        History: 저장된 대화내역 입니다. 당신은 이걸 기반으로 대답할 수 있습니다. 자세한 예시는 rule에 있습니다.:{chat_history}
        다음의 rule을 무조건 지키면서 사용자의 질문에 대답하세요.
        rule:
        - Question이 들어오면 [{tool_names}]중 하나의 툴을 선택해 사용하세요
        - 다만 '그것들의 차이가 뭐야?', '내가 물어봤던거 다시 알려줘.'와 같은 상황에선 History만 쓰세요.
        - 서비스 이용방법, 대출 및 투자 방법 유형의 Question이 들어오면 local_retriever를 쓰세요.
            - 예시: '대출 받고 싶은데 어떻게 해?', '투자는 어떻게 해?', '이 서비스는 뭐하는 서비스야?', '서비스 이용방법 알려줘'와 같은 상황에선 local_retriever를 쓰세요.
        - 대출심사 유형의 Question이 들어오면 simple_screening을 쓰세요.
            - 예시: '나 대출 가능해?', '나 연봉이 4000인데 대출 가능해?', '나 대출 가능한지 봐줘.'와 같은 상황에서 simple_screening을 쓰세요.
        - 금융 지식 관련 Question에선 financial_vocabulary를 쓰세요
            - 예시 : '자료열람요구권이 뭐야?', '개인신용평가대응권을 못쓰는 상황이 있어?', '인지세가 뭐야?', '인지세는 누가, 얼마나 부담해?'와 같은 질문이 들어오면 financial_vocabulary를 쓰세요.
        - Question의 언어에 맞게 답하세요. 만약 Question이 베트남어면 베트남어로 답하세요. Question이 한국어면 한국어로 답하세요. Question의 언어를 모르면 영어로 답하세요.
"""

class LoginAgent(NonLoginAgent):
    def __init__(self, llm, db_path, user_id):
        super().__init__(llm)
        self.model = llm
        self.prompt = ChatPromptTemplate.from_messages([
            ("system", template),
            MessagesPlaceholder("chat_history", optional = True),
            ("human", "{input}"),
            MessagesPlaceholder("agent_scratchpad")
        ])
        self.db_path = db_path
        self.engine = create_engine(self.db_path)
        self.user_id = user_id
        self.session_id = self._get_conversation_id()
        self.tools = [simple, retriever_tool, fin_retriever_tool]
        self.memory = SQLChatMessageHistory(
            table_name = self.user_id,
            session_id = self.session_id,
            connection = self.engine
        )
        self.agent = self._get_agent()
        self.agent_hist = self._agent_history()
    
    def _get_conversation_id(self):
        user_id = self.user_id
        now = time
        time1 = str(now.localtime().tm_year)
        time2 = str(now.localtime().tm_mon)
        time3 = str(now.localtime().tm_mday)
        conversation_id = user_id + time1 + time2 + time3
        return conversation_id

    
    def load_memory(self):
        return SQLChatMessageHistory(
            table_name = 'history',
            session_id = self._get_conversation_id(),
            connection = self.engine
        )
    

    def _agent_history(self):
        agent = self.agent
        history_agent = RunnableWithMessageHistory(
            agent,
            self.load_memory,
            input_messages_key = 'input',
            history_messages_key = 'chat_history'
        )
        return history_agent
    
    def answer_to_me(self, query):
        agent = self.agent_hist
        memory = self.memory
        result = agent.invoke({'input' : query}, config = {'configurable' : {'session_id' : self.session_id}})
        print("result :", result)
        memory.add_user_message(query)
        memory.add_ai_message(result['output'])
        return result
  • 에이전트는 크게 두 가지로 나뉘어져 있다.
    • NonLoginAgent 모듈은 로그인하지 않은 사용자에게 아주 기초적인 내용만(사용메뉴얼, 금융어휘) 아주 간단하게 알려준다
    • LoninAgent는 로그인한 사용자에게 간이대출심사, 대화내역 저장 및 대화내역 참고해 대답의 기능을 한다.
  • LoginAgent는 NonLoginAgent를 상속받아 구현
    • NonLoginAgent에 들어가는 기능은 LoginAgent에도 똑같이 들어가므로 코드 중복을 방지할 수 있다.
    • NonLoginAgent를 만들 때 LoginAgent를 상속받아 만들면, 간이대출심사 등 로그인 하지 않은 사용자에게 제공하면 안되는 기능이 제공될 수 있다.

프롬프트

프롬프트에서는 tool_names를 알려줘 어떤 도구들이 있는지 알려준다.
그리고 어떤 상황에서 어떤 도구를 선택해야 하는지를 알려준다.

  • 이 과정에서 예시와 함께 알려줘 정확도를 높인다.

또한 질문 언어에 맞게 대답하라는 규칙을 추가했다.

  • 외국인 노동자 대상이기 때문에 다국어 지원이 돼야하기 때문.

simple_screening을 쓸 땐 사용자 입력에 따라 파라미터가 입력됨

  • 때문에 여기서 바로 실제의 대출심사로는 안넘어감.
  • 만약 그럴 경우 정확한 대출 심사에 차질이 생긴다.

메모리

sql을 사용해 대화내역을 저장한다.

  • 이 경우 랭체인의 SQLChatMessageHistory 클래스를 사용한다.
  • 대화구분을 위한 session_id는 유저 식별 번호(id 혹은 핸드폰번호)에 접속 날짜(연월일)를 붙인 값이다.
    • 이렇게 하면 모델이 대화 내용을 참고해 대답할 때 시간을 줄일 수 있게된다.

또한 에이전트가 대화내용을 참고해 대답할 수 있게 하기 위해 RunnableWithMessageHistory를 써서 에이전트가 대화내용을 참고할 수 있게 한다.
메모리를 참고해 대답하는 경우는 프롬프트에 예시와 함께 작성했다.

질문

answer_to_me 함수를 통해 사용자의 질문을 입력받고, 그에 맞는 대답을 반환 받는다.
또한 대답이 생성되면 사용자의 질문과 LLM의 대답을 각각 저장한다.

  • LLM의 대답을 저장할 땐 output부분만 저장한다.
  • 기타 정보도 같이 저장하면 비용이 증가하고, LLM이 대화내역을 참고할 땐 output부분만 보면 되기 때문이다.

agent