<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>one_two_three.log</title>
        <link>https://velog.io/</link>
        <description>하나씩 뚝딱뚝딱</description>
        <lastBuildDate>Fri, 06 Mar 2026 10:26:28 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>one_two_three.log</title>
            <url>https://velog.velcdn.com/images/one_two_three/profile/437160ab-9c7c-4857-b787-339a96840dce/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. one_two_three.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/one_two_three" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[LangGraph] 로컬여행가이드 에이전트 코드 살펴보기]]></title>
            <link>https://velog.io/@one_two_three/LangGraph-%EB%A1%9C%EC%BB%AC%EC%97%AC%ED%96%89%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%BD%94%EB%93%9C-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@one_two_three/LangGraph-%EB%A1%9C%EC%BB%AC%EC%97%AC%ED%96%89%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%BD%94%EB%93%9C-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Fri, 06 Mar 2026 10:26:28 GMT</pubDate>
            <description><![CDATA[<p>이전 게시물에서 여행 유형을 기반으로 사용자의 여행을 더 정밀하게 분석할 수 있는 추가 기능에 대해 소개했다.</p>
<p>에이전트가 사용자의 여행 유형을 어떻게 대화에 반영하고, 사용자의 여행 선호도를 어떻게 분석하는지 코드를 통해 살펴보자.</p>
<p>(직접 코드로 보는게 이해가 더 빠를수도 있으나 짧게 설명을 진행합니다)</p>
<blockquote>
<p><a href="https://github.com/devHaneul/TBTI-chatbot/tree/main">여행 가이드 에이전트 전체 코드</a></p>
</blockquote>
<hr>
<br>

<h2 id="langgraph-에이전트-작동-흐름">LangGraph 에이전트 작동 흐름</h2>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/8d261177-f778-4658-b2ec-623abfebc94c/image.png" alt=""></p>
<p><a href="https://velog.io/@one_two_three/LangGraph-%EB%A1%9C%EC%BB%AC%EC%97%AC%ED%96%89-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%97%AC%ED%96%89-%EC%9C%A0%ED%98%95-%EA%B8%B0%EB%B0%98-%EB%8B%B5%EB%B3%80-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0">이전 게시물</a>에서 이미 작동 흐름에 대해  설명하였다.</p>
<p>코드의 이해를 위해 <strong>LangGraph</strong>에 대해 조금 설명하자면, LangGraph는 그래프 기반의 워크 플로우로 노드(Node)와 엣지(Edge), 상태(State)를 활용하여 위와 같은 흐름으로 구조화 할 수 있다. </p>
<p>보라색 네모칸은 LangGraph에서 <strong>노드</strong>를 의미하며 각 노드는 하나의 작업을 수행하는 함수로 <strong>상태(State)</strong>를 매개변수로 받아 업데이트된 상태(State)를 반환한다. 에이전트의 상태(State)는 여러 노드가 작업을 수행하며 값을 공유하고 업데이트하는 저장소이다. 이렇게 구현된 노드를 <strong>엣지(Edge)</strong>를 통해 연결하는데 여러 조건 분기도 가능하다. </p>
<blockquote>
<p>자세한 LangGraph의 설명은 공식 문서를 살펴보자🙂
<a href="https://docs.langchain.com/oss/python/langgraph/overview">LangGraph 공식 문서</a></p>
</blockquote>
<p><br><br></p>
<h2 id="1-메인-코드">1. 메인 코드</h2>
<pre><code class="language-python">
# 사용자별 에이전트 생성 함수
def create_user_agent(userId: Optional[str], model: LanguageModelLike, tools: Union[ToolExecutor, Sequence[BaseTool], ToolNode]) -&gt; CompiledGraph:
    # 사용자별 mcheckpointer 가져오기
    user_checkpointer = get_user_checkpointer(userId)

    # 에이전트 생성
    return create_my_agent(
        model=model,
        tools=tools,
        checkpointer=user_checkpointer
    )

class QuestionRequest(BaseModel):
    userMessage: str
    userId: Optional[str] = None
    tbtiType: Optional[str] = None


class AiResponse(BaseModel):
    typeNum: Optional[int] = None
    answer: str
    place: Optional[List[Dict]] = None


app = FastAPI()

# 호출할 함수 리스트 가져오기
tools = tools_of_travel[&quot;list_of_func&quot;] + tools_of_type[&quot;list_of_func&quot;]


# 이전 state 값 저장
previous_state = {
    &quot;messages&quot; : None,
    &quot;previous_result&quot; : None,
    &quot;final_response&quot; : None,
    &quot;tbti_of_user&quot; : None,
    &quot;filtering&quot; : {}
}

db = database

@app.post(&quot;/ask-ai/&quot;, response_model=AiResponse)
async def ask_ai(request: QuestionRequest):
    global previous_state
    userId = request.userId or &quot;false&quot;
    userMessage = request.userMessage
    tbtiType = request.tbtiType

    try:
        db.reconnect()

        # 사용자 ID를 기반으로 에이전트 생성
        user_agent = create_user_agent(userId, llm, tools)

         # TBTI 유형 저장
        previous_state[&quot;tbti_of_user&quot;] = tbtiType

        # 사용자 ID를 포함한 설정 구성
        config = {&quot;configurable&quot;: {&quot;thread_id&quot;: userId, &quot;user_id&quot;: userId}}

        system_prompt = &quot;&quot;&quot;
        - You are a tour guide called &#39;TBTI&#39;. Ask the user a short and clear question.
        - Only up to five locations will be notified.
        - Don&#39;t ask a question what type of trip the user wants.
        - Don&#39;t ask specifically what kind of trip users want.
        &quot;&quot;&quot;

        messages_list = [(&quot;system&quot;, f&quot;{system_prompt}&quot;)] 
        messages_list.append((&quot;human&quot;, f&quot;{userMessage}&quot;))
        previous_state[&quot;messages&quot;] = messages_list

        # 에이전트 실행
        response = user_agent.invoke(previous_state, config)
        previous_state = response

        # JSON 직렬화 시 SecretStr 값 처리
        return response[&#39;final_response&#39;]

    except Exception as e:
        print(&quot;에러 발생: &quot;, e)
        raise HTTPException(status_code=500, detail=&quot;AI 처리 중 오류 발생&quot;)
    finally:
        db.unconnect()

if __name__ == &quot;__main__&quot;:
    import uvicorn
    uvicorn.run(app, host=&quot;0.0.0.0&quot;, port=8001)
</code></pre>
<p>FastAPI를 통해 사용자의 질문을 받고 프론트단으로 답변을 전달한다.</p>
<ul>
<li>전달받는값: 사용자 질문, 사용자 ID, 사용자의 여행 유형(TBTI)</li>
<li>응답값: typeNum, AI 챗봇 응답, 여행지 추천 장소</li>
</ul>
<p>주요한 부분은 <code>create_user_agent</code>에서 사용자만의 에이전트를 생성하고, 사용자의 질문 및 시스템 프롬프트를 <strong>에이전트 상태(State)</strong>에 저장하여 에이전트를 실행시킨다.</p>
<p><em>previous_state</em> 딕셔너리에 에이전트의 이전 상태값을 저장하는 이유는 사용자의 여행 유형이나 이전 대화 기록을 통해 사용자와의 대화를 자연스럽게 연결하기 위함이다.</p>
<p>이제 에이전트의 구현 코드를 살펴보자</p>
<p><br><br><br></p>
<h2 id="2-에이전트-구현-코드">2. 에이전트 구현 코드</h2>
<pre><code class="language-python">def create_my_agent(
    model: LanguageModelLike,
    tools: Union[ToolExecutor, Sequence[BaseTool], ToolNode],
    checkpointer: Optional[BaseCheckpointSaver] = None
) -&gt; CompiledGraph:

    # LLM 시스템 프롬프트 리스트 로드 - 작동되는 함수에 따라 시스템 프롬프트 내용 다름
    configuration_for_answers = system_informations_of_functions

    # 도구 호출 가능한 모델 로드
    model_with_tools = model.bind_tools(tools)

    def save_user_info(state: AgentState):
        return {&#39;tbti_of_user&#39;: state[&quot;tbti_of_user&quot;]}

                                ...... 더보기</code></pre>
<p><code>create_my_agent</code> 는 에이전트 생성 함수로 LLM, LLM 외부 도구(Tools), 채팅 메모리를 매개변수로 받아 CompiledGraph를 리턴한다. 이 함수에는  <strong>노드(Node)</strong>로 작동되는 작업들이 구현되어 있으며 노드와 엣지로 작업 흐름 또한 구현한다.</p>
<p>에이전트의 상태 및 작업 함수들을 하나씩 살펴보자.</p>
<h3 id="2-1-에이전트-상태state">2-1. 에이전트 상태(State)</h3>
<pre><code class="language-python"># 노드에 전달되는 state
class AgentState(TypedDict):
    messages : Annotated[list, add_messages] 
    previous_result : Optional[str]                   
    final_response : Optional[dict]                  
    tbti_of_user : Optional[str]             
    filtering : Optional[dict]              </code></pre>
<p><strong>LangGraph</strong>에서 에이전트의 상태(State)를 TypedDict을 사용하여 다음과 같이 정의할 수 있고, 모든 노드에서 이 값들을 공유하고 수정할 수 있다. 여행 가이드의 경우로 다섯가지의 요소만 저장을 했다.</p>
<ul>
<li><em>messages</em> : 대화 기록 저장</li>
<li><em>previous_result</em>  : 이전 단계에서의 생성된 AI 답변</li>
<li><em>final_response</em> : 사용자에게 전달될 최종 답변</li>
<li>tbti_of_user :  사용자의 여행 유형 TBTI</li>
<li><em>filtering</em> : 사용자의 여행 특성 (데이터베이스 검색 필터 조건)
이 값은 사용자의 여행 유형에 따라 추가되는 필터로 사용자에게 맞는 여행지를 검색할 때 사용되는 정보이다.</li>
</ul>
<p><br><br></p>
<h3 id="2-2-노드node-작업-구현">2-2. 노드(Node) 작업 구현</h3>
<p>노드의 흐름대로 노드에 어떤 작업이 이뤄지는지 설명하고자 한다.
<br></p>
<h4 id="1--사용자의-여행-유형-저장">1.  사용자의 여행 유형 저장</h4>
<pre><code class="language-python">    # 첫 노드
    def save_user_info(state: AgentState):
        return {&#39;tbti_of_user&#39;: state[&quot;tbti_of_user&quot;]}

    # 검색 필터를 생성해야 하는지 파악
    def should_create_filter(state: AgentState):
        filtering = state[&#39;filtering&#39;]
        tbti = state[&#39;tbti_of_user&#39;]

        if len(filtering) &gt; 0:
            return &quot;start talking&quot;
        elif len(filtering) == 0 and tbti != None:
            return &quot;create filter&quot;
        else:
            return &quot;start talking&quot;
</code></pre>
<p>사용자의 여행 유형을 서버에서 받게 되면 유형을 에이전트 상태(State)에 저장하고, 여행 유형을 에이전트가 처음 받게 되면 검색 필터가 존재하지 않는다. </p>
<p>따라서 <code>should_create_filter</code>를 통해 <strong>검색 필터가 없는지 파악</strong>하고, 없다면 <em>create filter</em> 문자열을 리턴한다. 리턴된 값을 통해 검색 필터를 생성하는 작업 함수로 이동할 수 있다. 이 함수는 <strong>조건부 엣지(Conditional Edge)</strong>를 만들기 위해 사용되는데 에이전트 작동 흐름을 다시 보면 첫 노드 <strong>start-node</strong>에서 두 갈래로 나누어진 조건부 엣지를 확인할 수 있다.</p>
<p><br><br></p>
<hr>
<h4 id="2--검색-필터-생성-및-시스템-프롬프트-추가">2.  검색 필터 생성 및 시스템 프롬프트 추가</h4>
<pre><code class="language-python"># 새로운 검색 필터 생성 노드
    def generate_new_filter(state: AgentState):
        messages = state[&quot;messages&quot;]
        filtering = {}
        system_prompt = [&#39;Ask a question in Korean one by one.&#39;]

        # 사용자 여행 유형 가져오기
        tbti = state[&#39;tbti_of_user&#39;]
        try:
            if tbti not in [&#39;AIEU&#39;, &#39;AIEP&#39;, &#39;AIFU&#39;, &#39;AIFP&#39;, &#39;ASEU&#39;, &#39;ASEP&#39;, &#39;ASFU&#39;, &#39;ASFP&#39;, &#39;CIEU&#39;, &#39;CIEP&#39;, &#39;CIFU&#39;, &#39;CIFP&#39;, &#39;CSEU&#39;, &#39;CSEP&#39;, &#39;CSFU&#39;, &#39;CSFP&#39;] and tbti == None:
                return {&quot;filtering&quot;: filtering, &quot;messages&quot;: messages}

            else:
                # 여행 유형에 따른 검색 필터 및 추가 질문 준비
                tbti = list(tbti)
                for one_type in tbti:
                    match one_type:
                        case &#39;A&#39;:
                            filtering[&quot;mood&quot;] = &quot;(mood == 0)&quot;
                        case &#39;C&#39;:
                            filtering[&quot;mood&quot;] = &quot;(mood == 1)&quot;
                        case &#39;I&#39;:
                            system_prompt.append(tools_of_type[&#39;check_companion_animal&#39;][&quot;added_system_message&quot;])
                        case &#39;P&#39;:
                            pass
                        case &#39;S&#39;:
                            system_prompt.append(tools_of_type[&#39;check_child&#39;][&quot;added_system_message&quot;])
                            system_prompt.append(tools_of_type[&#39;check_companion_animal&#39;][&quot;added_system_message&quot;])
                        case &#39;E&#39;:
                            system_prompt.append(tools_of_type[&#39;check_distance&#39;][&quot;added_system_message&quot;])
                        case &#39;F&#39;:
                            filtering[&quot;parking&quot;] = &quot;(parking == true)&quot;
                        case &#39;U&#39;:
                            filtering[&quot;reservation&quot;] = &quot;(reservation == true)&quot;

        except Exception as e:
            print(e, &quot; 올바른 TBTI 유형을 전달하세요.&quot;)

        added_system_msg = &#39; &#39;.join(system_prompt)
        messages = [
            (&quot;system&quot;, f&quot;{added_system_msg}&quot;)
        ]

        return {&quot;filtering&quot;: filtering, &quot;messages&quot;: messages}
</code></pre>
<p>위의 작업은 사용자의 여행 유형에 따른 검색 필터를 에이전트 상태(State)에 저장한다. 특정 유형에 경우, 추가적인 여행 특성을 구체적으로 파악하기 위해 시스템 프롬프트를 추가하여 LLM이 사용자의 여행에 대해 추가적인 질문을 하도록 유도한다.
<br>
만약 사용자의 여행 유형이 <code>APSU</code> 인 경우 👆</p>
<ul>
<li>검색 필터 filtering:  mood == 0, reservation == True 저장</li>
<li><code>S</code> 유형에 속하기 때문에 LLM의 시스템 프롬프트에는 <pre><code>&quot;Ask first if users travel with their children.&quot;
&quot;Ask first if users are going to travel with their pets.&quot;</code></pre></li>
</ul>
<p>위의 두 문장이 추가되어 사용자 여행에 대해 구체적으로 알기 위한 LLM의 추가 질문이 이어질 수 있다.</p>
<p><br><br></p>
<hr>
<h4 id="3-ai-모델-로드-및-답변-생성">3. AI 모델 로드 및 답변 생성</h4>
<pre><code class="language-python"> # 사용할 AI 모델 로드 및 AI 답변 처리
    def talk_to_model(state: AgentState):
        response = model_with_tools.invoke(state[&#39;messages&#39;])
        last_response = response.content.replace(&#39;\&quot;&#39;, &#39;\&#39;&#39;)
        last_response = f&#39;{{\&quot;answer\&quot;: \&quot;{last_response}\&quot;, \&quot;place\&quot;: null}}&#39;

        # AI 답변을 json 형식의 문자열로 만들어 previous_result에 저장 / 답변 history에 저장 
        return {&quot;previous_result&quot; : last_response , &quot;messages&quot; : [response]}

     # 도구를 작동 시킬 지 파악
    def should_continue(state: AgentState):
        messages = state[&quot;messages&quot;]
        last_message = messages[-1]

        # 함수 호출이 없으면 바로 사용자에게 리턴
        if not last_message.tool_calls:
            return &quot;pass&quot;
        # 있으면 워크플로우 지속
        else:
            return &quot;work&quot;
</code></pre>
<p>상태(State)에 사용자의 여행 유형과 검색 필터가 모두 저장되고 나면  사용자의 질문에 대한 LLM 답변을 생성하는 작업으로 진행된다.</p>
<p>LLM 답변을 생성하여 JSON 형식의 문자열로 만들고 리턴하며 상태 속 <em>previous_result</em> 요소에 답변을 저장한다.</p>
<p>사용자에 질문을 LLM이 받게 되면 단순한 답변을 바로 생성해도 되는지, 추가 작업을 위해 외부 도구 호출이 필요한지 파악한다. 외부 도구 호출이 필요할 경우, <code>should_continue</code> 함수에서 work 문자열을 리턴하여 도구(Tools) 노드로 이동시킨다. 호출 가능한 외부 도구는 아래 경로로 확인할 수 있다.</p>
<blockquote>
<p><a href="https://github.com/rlsid/TBTI-chatbot/tree/main/callable_tools">여행 가이드 챗봇 외부 도구</a></p>
</blockquote>
<br>

<hr>
<h4 id="4-필터링-추가-기능">4. 필터링 추가 기능</h4>
<pre><code class="language-python"># 검색 필터를 추가할 지, 검색 결과를 통한 답변을 생성할 지 파악
    def should_make_answer(state: AgentState):
        name_of_tools = tools_of_type.keys()
        tool_messages = state[&quot;messages&quot;][-1]

        if tool_messages.name in name_of_tools:
            return &quot;add type&quot;
        else:
            return &quot;make answer&quot;

    # 여행 취향을 파악하기 위한 추가 질문 답변 처리
    def process_type_result(state: AgentState):
        filtering = state[&quot;filtering&quot;]     
        messages = state[&quot;messages&quot;]

        ai_message = filter_messages(messages, include_types=[AIMessage])[-1]
        tool_call_ids = [item[&#39;id&#39;] for item in ai_message.tool_calls]

        # 이전 실행된 tool 결과 가져오기
        tool_messages =  filter_messages(messages, include_types=[ToolMessage], include_ids=tool_call_ids)
        for tool in tool_messages:
            content = tool.content
            split_result = content.split(&#39;,&#39;)
            if split_result[1] != &#39;null&#39;:
                filtering[split_result[0]] = split_result[1]
            else:
                if split_result[0] in filtering:
                    del filtering[split_result[0]]

        print(&#39;filteirng: &#39;, filtering)
        return {&quot;filtering&quot;: filtering}</code></pre>
<p>여행 가이드 챗봇이 가진 외부 도구에는 여행지 추천, 여행 계획 수립 도구가 있지만, 사용자의 여행 특성을 추가하는 도구도 존재한다. </p>
<br>

<p>만약 👆</p>
<ul>
<li><p>사용자 질문: 나 강아지랑 여행가고 싶어 </p>
<pre><code class="language-python"># 반려동물과 함께 여행하는지 아닌지에 대한 답변에 작동되어 답변 저장
@tool
def check_companion_animal(response: bool) -&gt; str:
&quot;&quot;&quot;check whether the users are going to travel with their pets or not and save their answers to the parameters.

  Args:
    response: if the user is going to travel with a pet, put &#39;true&#39;. if not, put &#39;false&#39;.
&quot;&quot;&quot;

if response == True:
  filter_string = &#39;animal,(animal == true)&#39;
else:
  filter_string = &#39;animal,null&#39;
return filter_string</code></pre>
<p>위의 도구가 실행되고, 위의 도구는 <code>animal,(animal == true)</code> 문자열을 리턴한다. 위 값은 <code>process_type_result</code> 함수에서 후처리 되어 에이전트 상태(State)의_ filtering_에 (animal == true) 값이 추가되도록 한다.</p>
</li>
</ul>
<p>결과적으로, 사용자가 어떠한 여행을 원하는지 구체적인 정보를 이러한 방식으로 알아낼 수 있고, 사용자의 여행 특성이 변화한다면 즉각적으로 수정하여 LLM이 반영할 수 있도록 한다.</p>
<br>

<h4 id="❓필터링filtering-값은-언제-쓰이는가">❓필터링(filtering) 값은 언제 쓰이는가?</h4>
<pre><code class="language-python">@tool
def recommand_travel_destination(question : str, location : str, area : str, filtering: Annotated[dict, InjectedState(&#39;filtering&#39;)]) -&gt; str:
    &quot;&quot;&quot;
    recommand the various places that user wants to know or to travel
    It only works when user wants to know the various places.
    It doesn&#39;t work when user told to plan the trip and when user told to reserve the place.

    Args:
        question: input the user&#39;s questions.
        location: input the area of Korea to travel, e.g. 서울 or 부산 or 대구 or 강원도
        area: Enter only the following words to indicate where the place in the user&#39;s question belongs to the following Korean administrative districts. e.g. 강원특별자치도 
              - 한국 행정 구역 : 서울특별시, 부산광역시, 인천광역시, 대구광역시, 대전광역시, 광주광역시, 울산광역시, 세종특별자치시, 경기도, 충청북도, 충청남도, 전라남도, 경상북도, 경상남도, 강원특별자치도, 전북특별자치도, 제주특별자치도
    &quot;&quot;&quot;
    milvus = database

    # 사용자 질문 벡터화
    vector  =  embedding(question)

    # 필터링 생성 후 테이블 검색 진행
    milvus_filter = None
    area_filter = f&quot;area_name == &#39;{area}&#39;&quot;

    if bool(filtering):
        area_filter = area_filter + &#39; &amp;&amp; &#39;
        another = &#39; &amp;&amp; &#39;.join(filtering.values())
        milvus_filter = area_filter + another
    else:
        milvus_filter = area_filter

    results_localCreator, results_nowLocal = milvus.search_all_tables(embedding=vector, filtering=milvus_filter, top_k=5)

    # 쿼리 결과 합치기
    total_results = milvus.get_formatted_results(results_localCreator, results_nowLocal)

    return f&quot;user question: {question} \n\nreference: \n{total_results}&quot;</code></pre>
<p>여행지 추천 함수가 작동 시 위의 함수의 파라미터로 가져와,** milvus 데이터 베이스** 필터 조건으로 쓰여 사용자에게 맞는 장소만 가져오는 역할을 하게됨</p>
<p><br><br></p>
<hr>
<h4 id="5-도구-호출-후-ai-답변-생성-및-전달">5. 도구 호출 후, AI 답변 생성 및 전달</h4>
<pre><code class="language-python"># 도구 작동 후 함수 결과 LLM에게 최종 전달 후 답변 생성
    def respond_after_calling_tools(state: AgentState):
        # 작동된 마지막 도구 메시지 가져오기
        messages = state[&quot;messages&quot;]
        last_tool_message = messages[-1]

        # 작동된 함수 이름 가져오기
        name_of_functions_called = last_tool_message.name

        # 함수 리턴값 가져오기 = 검색 결과 가져오기
        reference = last_tool_message.content

        # 작동된 도구에 맞는 시스템 프롬프트 가져오기
        system_prompt = configuration_for_answers[name_of_functions_called][&#39;system_prompt&#39;]
        response_format = configuration_for_answers[name_of_functions_called][&#39;response_format&#39;]

        # 결과 참고해서 LLM 답변 생성
        messages = [
            {&quot;role&quot;:&quot;system&quot;, &quot;content&quot;: f&quot;{system_prompt}&quot;},
            {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;:f&quot;{reference}&quot;}
        ]

        llm_response = chat_completion_request(
            messages=messages,
            response_format=response_format
        ).choices[0].message.content   

        return {&quot;previous_result&quot;: llm_response}

     def post_processing_of_answer(state: AgentState):
        ai_answer = state[&quot;previous_result&quot;]
        escaped_response = escape_json_strings(ai_answer)
        return {&quot;final_response&quot; : escaped_response}</code></pre>
<p>일반적인 여행지 추천과 같은 외부 도구가 실행된 후,
실행 결과를 LLM이 참고하여 답변을 생성할 수 있도록 한다. 여러가지 외부 도구들이 존재할 것이고, 작동된 도구에 맞는 시스템 프롬프트, LLM의 답변 형식을 불러와 LLM을 실행한다. LLM의 답변은 에이전트 상태의 _previous_result_에 저장한다.</p>
<p><code>post_processing_of_answer</code> 노드에서는 json 문자열을 딕셔너리 자료형으로 변환하는 작업을 수행하고, 최종 답변으로 에이전트 상태(State)에 저장한다.</p>
<br>

<hr>
<h4 id="6-에이전트-노드-생성-및-흐름-구현">6. 에이전트 노드 생성 및 흐름 구현</h4>
<pre><code class="language-python">  # 새로운 그래프 정의
    workflow = StateGraph(AgentState)

    # 각 노드 생성
    workflow.add_node(&quot;start-node&quot;, save_user_info)
    workflow.add_node(&quot;generate-filter&quot;, generate_new_filter)
    workflow.add_node(&quot;talk-to-human&quot;, talk_to_model)
    workflow.add_node(&quot;tools&quot;, ToolNode(tools))
    workflow.add_node(&quot;add-filter&quot;, process_type_result)
    workflow.add_node(&quot;respond&quot;, respond_after_calling_tools)
    workflow.add_node(&quot;json-processing&quot;, post_processing_of_answer)

    # 그래프 진입 포인트 설정
    workflow.set_entry_point(&quot;start-node&quot;)

    workflow.add_conditional_edges(
        &quot;start-node&quot;,
        should_create_filter,
        {
            &quot;start talking&quot;: &quot;talk-to-human&quot;,
            &quot;create filter&quot;: &quot;generate-filter&quot;,
        },
    )

    workflow.add_edge(&quot;generate-filter&quot;, &quot;talk-to-human&quot;)

    workflow.add_conditional_edges(
        &quot;talk-to-human&quot;,
        should_continue,
        { 
            &quot;work&quot;: &quot;tools&quot;,
            &quot;pass&quot;: &quot;json-processing&quot;
        }
    )

    workflow.add_conditional_edges(
        &quot;tools&quot;,
        should_make_answer,
        {
            &quot;add type&quot;: &quot;add-filter&quot;,
            &quot;make answer&quot;: &quot;respond&quot;
        }
    )

    workflow.add_edge(&quot;add-filter&quot;, &quot;talk-to-human&quot;)
    workflow.add_edge(&quot;respond&quot;, &quot;json-processing&quot;)
    workflow.add_edge(&quot;json-processing&quot;, END)

    graph = workflow.compile(
        checkpointer=checkpointer
    )
</code></pre>
<p>위의 모든 작업 함수들로 노드를 생성하고, 그래프의 진입 포인트, 엣지를 통하여 그래프의 작업 흐름을 생성한다.</p>
<p>compile 메서드로 생성된 graph가 바로 에이전트가 된다. Main 코드에서 생성된 에이전트는 사용자의 상호작용하며 기록된 대화 내용을 통해 사용자의 여행 유형을 답변에 반영하며, 사용자의 여행 계획이나 취향이 변화였을 때 즉각적으로 수정할 수 있었다.</p>
<p><br><br></p>
<hr>
<h4 id="7-느낀-점-😶">7. 느낀 점 😶</h4>
<p>조금 지난 코드인데 이 프로그램을 개발할 당시, 어떤 구조로 어떻게 개발해야 할지 까마득했던 기억이 난다. 현재, AI 에이전트의 라이브러리나 다양한 도구가 많이 개발된 것을 보아 새로운 문서와 기능들을 공부하여 더욱 보완된 에이전트를 개발하고 싶다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[LangGraph] 로컬여행 가이드 에이전트 - 여행 유형 기반 답변 생성하기]]></title>
            <link>https://velog.io/@one_two_three/LangGraph-%EB%A1%9C%EC%BB%AC%EC%97%AC%ED%96%89-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%97%AC%ED%96%89-%EC%9C%A0%ED%98%95-%EA%B8%B0%EB%B0%98-%EB%8B%B5%EB%B3%80-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@one_two_three/LangGraph-%EB%A1%9C%EC%BB%AC%EC%97%AC%ED%96%89-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%97%AC%ED%96%89-%EC%9C%A0%ED%98%95-%EA%B8%B0%EB%B0%98-%EB%8B%B5%EB%B3%80-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 26 Nov 2025 00:16:08 GMT</pubDate>
            <description><![CDATA[<p>기능을 발전시켜두고 기록은 까먹은 채... 지금이라도 복습 겸 기록을 시작한다.</p>
<hr>
<h2 id="새로운-기능">새로운 기능</h2>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/a3483835-2e03-49f7-a549-858b9e819f55/image.png" alt=""></p>
<p><a href="https://velog.io/@one_two_three/%ED%95%9C%EA%B5%AD-%EB%A1%9C%EC%BB%AC-%EC%97%AC%ED%96%89%EA%B0%80%EC%9D%B4%EB%93%9C%EB%B4%87-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-LangChain-LangGraph-%ED%99%9C%EC%9A%A9">이전 게시물</a>에서 보완해야 하는 부분에 사용자의 여행 취향을 반영할 수 있는 효율적인 방법이 필요하다고 얘기했었다. 대화를 시작하기 전 사용자의 여행 유형을 파악하고 그 유형을 기반으로 대화를 진행하면 사용자에게 더 잘 맞는 답변을 만들수 있지 않을까?</p>
<p>그래서 일명 TBTI (Travel MBTI)를 만들고 이 설문 조사를 바탕으로 사용자의  유형을 정한 후 가이드 봇은 이에 따라 달라지는 추가적인 질문을 통해 사용자의 여행 선호도를 분석한다. </p>
</br>
<br>

<h2 id="여행-유형-분석-퀴즈">여행 유형 분석 퀴즈</h2>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/c02ceac6-7215-44f7-bf7a-e6e22a9267d1/image.png" alt=""></p>
<p>팀원들과 TBTI 테스트를 만들어 사용자의 여행 부캐를 찾을 수 있도록 하였다.</p>
<p>질문은 총 4가지로 구성된다.</p>
<br>


<h3 id="1-어떤-분위기의-여행-선호하는지">1. 어떤 분위기의 여행 선호하는지?</h3>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/76abb1d9-25f6-454e-86cc-7d8f7da9cc7e/image.png" alt=""></p>
<br>




<h3 id="2-혼자-여행을-선호하는지-">2. 혼자 여행을 선호하는지 ?</h3>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/eddf0bae-e2e4-42d5-bfbd-8e95c11c5099/image.png" alt=""></p>
<br>

<h3 id="3-여행-시-대중교통-vs-자동차-">3. 여행 시, 대중교통 vs 자동차 ?</h3>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/e4b8a79e-cdf0-44f2-a2ef-c87165c5482d/image.png" alt=""></p>
<br>

<h3 id="4-즉흥적인-여행-vs-계획적인-여행-">4. 즉흥적인 여행 vs 계획적인 여행 ?</h3>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/d088cdcf-8047-4fc4-a3cb-022df7506b89/image.png" alt=""></p>
<br>

<h3 id="tbti-결과">TBTI 결과</h3>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/d8aff678-3107-4bc2-8961-8e4eac4a312b/image.png" alt=""></p>
<p>설문 조사가 끝나면 유형에 따른 귀여운 동물로 개인의 TBTI 캐릭터를 설명과 함께 얻게 된다.</p>
<hr>
<br>


<h2 id="사용자-유형-기반-채팅-시작">사용자 유형 기반 채팅 시작</h2>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/68d688df-498c-4314-93aa-0a45a5b8e3bf/image.png" alt=""></p>
<p>그림과 같이 채팅방 상단에 사용자의 유형을 볼 수 있으며 사용자와 채팅을 할 때, 가이드 봇은 유형에 따라 다른 질문들로 사용자의 선호 여행을 분석할 수 있도록 한다.</p>
<hr>
<p><br><br></p>
<h2 id="달라진-가이드-봇-작동-흐름">달라진 가이드 봇 작동 흐름</h2>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/8d261177-f778-4658-b2ec-623abfebc94c/image.png" alt=""></p>
<p>위의 흐름은 LangGraph의 기능을 통해 자동 생성된 그림이다. 전보다 살짝은 더 복잡해졌다. 위의 보라색 네모칸은 LangGraph에서 노드를 의미하고, 프로세스 진행 시 모든 노드에 공유하는 정보를 저장할 수 있다. 노드의 주요 정보는 사용자의 <strong>여행 유형(TBTI)</strong>, <strong>필터(filter)</strong> </p>
<p><strong>필터(filter)</strong>는 쉽게 말하면 사용자의 여행 특성으로 여행 장소 데이터베이스 검색 시 검색 조건으로 이용된다.</p>
<p>노드 별로 진행 흐름을 설명해본다.</p>
<ol>
<li><em>start-node</em> : 사용자의 TBTI(여행 유형 특성)을 전달 받아 이 정보를 프로세스에 저장한다.
사용자의 필터 정보가 없을 경우, 필터를 생성하기 위해 <em>generate-filter</em> 노드로 이동한다. 그 외의 경우는 <em>talk-to-human</em> 노드로 이동하여 AI와 대화가 바로 진행 된다.</li>
<li><em>talk-to-human</em> : 사용자의 질문에 대한 답변을 생성한다. 사용자의 대화 중 상세한 답변 생성을 데이터베이스 검색 등과 같은 추가 작업이 필요한 경우, 함수를 추가 호출한다. 함수가 호출될 경우, <em>tools</em> 노드로 이동한다.</li>
<li><em>add-filter</em> : 대화 중 사용자의 여행 특성을 추가하는 역할을 한다. 예를 들어, 사용자가 대화 중 갑자기 아이와 여행하기로 여행 계획이 변경되었다고 말한다면, 아이와 함께 여행하기에 좋은 장소를 검색하기 위해 검색 조건을 필터 변수에 추가한다.</li>
<li><em>respond</em> : 함수 호출 결과들을 바탕으로 답변을 생성한다.</li>
<li><em>json-processing</em> : 사용자에게 답변을 보내기 위해 올바른 json 형태의 문자열을 딕셔너리 자료형으로 변환하는 단순 처리 과정이다. AI 답변을 정해진 형식으로 후처리 후 사용자에게 전달 된다.</li>
</ol>
</br>
</br>

<blockquote>
<p>메인 코드는 다음과 같고 다음 게시물에서  프로그램에 대한 자세할 설명이 이어진다. <br>
<a href="https://github.com/rlsid/TBTI-chatbot/blob/main/agent_executor.py">LangGraph Agent 메인 코드</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[OpenAI의 구조화된 출력(Structured Outputs)을 사용해보자]]></title>
            <link>https://velog.io/@one_two_three/OpenAI%EC%9D%98-%EA%B5%AC%EC%A1%B0%ED%99%94%EB%90%9C-%EC%B6%9C%EB%A0%A5Structrued-Outputs%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@one_two_three/OpenAI%EC%9D%98-%EA%B5%AC%EC%A1%B0%ED%99%94%EB%90%9C-%EC%B6%9C%EB%A0%A5Structrued-Outputs%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Wed, 18 Dec 2024 10:49:11 GMT</pubDate>
            <description><![CDATA[<p>이 게시물을 읽기 전 JSON에 대해 잘 모르겠다면 이전 게시물을 참고하길 바란다.</p>
<blockquote>
<p><a href="https://velog.io/@one_two_three/JSON-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC%EC%99%80-%EC%9E%90%EC%A3%BC-%EC%9D%BC%EC%96%B4%EB%82%98%EB%8A%94-JSONDecodeError%EC%97%90-%EB%8C%80%ED%95%B4">JSON 개념 정리와 자주 일어나는 JSONDecodeError에 대해</a></p>
</blockquote>
<hr>
<h2 id="구조화된-출력structrued-outputs">구조화된 출력(Structrued Outputs)?</h2>
<p>우리가 사용하는 OpenAI 모델은 웹 서버와의 통신을 용이하게 하기 위해 구조화된 출력(Structured Outputs)을 사용해야 했다.</p>
<p>구조화된 출력(Structured Outputs)은 LLM의 답변을 지정된 JSON 형식에 맞게 생성시켜주는 기능이다. 아래의 형태처럼 일반적인 긴 줄글의 답변이 아닌 유효한 JSON 형식의 답변으로 모델이 생성할 수 있다.</p>
<pre><code class="language-python">{
  &quot;steps&quot;: [
    {
      &quot;explanation&quot;: &quot;Subtract 31 from both sides to isolate the term with x.&quot;,
      &quot;output&quot;: &quot;8x + 31 - 31 = 2 - 31&quot;
    },
    {
      &quot;explanation&quot;: &quot;This simplifies to 8x = -29.&quot;,
      &quot;output&quot;: &quot;8x = -29&quot;
    },
    {
      &quot;explanation&quot;: &quot;Divide both sides by 8 to solve for x.&quot;,
      &quot;output&quot;: &quot;x = -29 / 8&quot;
    }
  ],
  &quot;final_answer&quot;: &quot;x = -29 / 8&quot;
}</code></pre>
<br>

<p>OpenAI API에서 구조화된 출력은 <code>gpt-4o</code>로 시작되는 모델에서만 사용이 가능하다.</p>
<ul>
<li>o1-2024-12-17 이후 모델</li>
<li>gpt-4o-mini-2024-07-18 이후 모델</li>
<li>gpt-4o-2024-08-06 이후 모델</li>
</ul>
<p>원래는 JSON 모드를 통해 답변을 출력하다가 더 정확하고 유효한 JSON 답변을 출력하기 위해서 구조화된 출력을 사용하기 시작했다.구조화된 출력 기능은 개발자가 제공한 스키마와 일치하도록 제한하고, 복잡한 스키마를 더 잘 이해하도록 모델을 훈련시킨 결과이다.</p>
<p>OpenAI가 구조화된 출력을 평가한 결과 gpt-4o-2024-08-06 모델에서 100%의 신뢰성을 가진 출력을 얻을 수 있었다고 한다.
<img src="https://velog.velcdn.com/images/one_two_three/post/ac61c85b-8891-4742-a89a-e6335119717b/image.png" alt=""></p>
<p>OpenAI의 Structured Outputs에 대해서는 아래의 문서를 참고하면 더 구체적인 개념과 사용법을 알 수 있다. </p>
<blockquote>
<ul>
<li><a href="https://openai.com/index/introducing-structured-outputs-in-the-api/">Introducing Structured Outputs in the API</a></li>
</ul>
</blockquote>
<ul>
<li><a href="https://platform.openai.com/docs/guides/structured-outputs">Structured Outputs</a></li>
</ul>
<br>

<hr>
<h2 id="구조화된-출력-사용-예시">구조화된 출력 사용 예시</h2>
<p>구조화된 출력을 사용하는 경우는 두가지 인데 하나는 함수 호출을 사용할 때와 JSON 스키마 응답 형식을 사용할 때이다. 나는 함수 호출은 다른 방식을 이용했고, 모델이 함수의 작동 결과를 참고하여 답변을 생성할 때 JSON 형식으로 생성하도록 사용하였다.</p>
<h3 id="1-출력-구조-정의">1. 출력 구조 정의</h3>
<p>먼저 모델이 출력할 때 따라야 하는 JSON 스키마로서 데이터 구조를 정의해줘야 한다. 정의하는 방법은 SDK를 사용하여 Pydantic을 사용할 수 있지만 나는 사용자 질문에 따라 답변하는 JSON 값이 미묘하게 다르기 때문에 더 정확한 출력 유도를 위해서 JSON 스키마 여러개를 직접 정의하였다.</p>
<pre><code class="language-python">myFormat = {
    &quot;type&quot;: &quot;json_schema&quot;,
    &quot;json_schema&quot; : {
        &quot;name&quot; : &quot;A_unique_answer&quot;,
        &quot;schema&quot; : {
            &quot;type&quot;: &quot;object&quot;,
            &quot;properties&quot;: {
                &quot;response&quot; : {
                    &quot;type&quot;: &quot;string&quot;,
                    &quot;description&quot; : &quot;put a short sentence that gives information about the places the user like&quot;
                },
                &quot;additionalInfo&quot;: {
                    &quot;type&quot;: &quot;array&quot;,
                    &quot;items&quot;: {
                        &quot;type&quot;: &quot;object&quot;,
                        &quot;properties&quot; : {
                            &quot;name&quot; : {
                                &quot;type&quot;: &quot;string&quot;,
                                &quot;description&quot; : &quot;The name of the place&quot;
                            },
                            &quot;explanation&quot; : {
                                &quot;type&quot;: &quot;string&quot;,
                                &quot;description&quot; : &quot;A brief description of the place.&quot;
                            }
                        },
                        &quot;additionalProperties&quot;: False,
                        &quot;required&quot; : [&quot;place_name&quot;, &quot;explanation&quot;]
                    }
                }
            },
            &quot;additionalProperties&quot;: False,
            &quot;required&quot; : [&quot;response&quot;, &quot;additionalInfo&quot;] 
        },
        &quot;strict&quot; : True
    }</code></pre>
<p>구조화된 출력을 사용하기 위해서는 모든 필드를 <code>required</code>로 지정해야 하고 <code>strict</code>는 <strong>True</strong>로 설정하여 제공된 스키마에 출력을 일치하도록 맞춘다. JSON 스키마에 정의되지 않은 추가적인 키/값 생성을 막기 위해서 <code>additionalProperties</code>를 <strong>False</strong>로 설정하였다. </p>
<p>이렇게 스키마를 정의하고 <strong>myFormat</strong> 변수에 저장하였다.</p>
<br>

<h3 id="2-호출-api에-출력-구조-제공">2. 호출 API에 출력 구조 제공</h3>
<pre><code class="language-python">messages = [
    {&quot;role&quot;:&quot;system&quot;, &quot;content&quot;: &quot;당신은 사용자의 취향을 파악하여 사용자가 좋아할 만한 장소를 추천해주는 봇입니다.&quot;},
    {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;:f&quot;{reference}&quot;}
]

response = client.chat.completions.create(
        model=model,
        messages=messages,
        response_format=myFormat
)</code></pre>
<p>위에서 정의한 스키마를 OpenAPI를 호출할 때 <code>response_format</code> 파라미터에 넣어준다. 이렇게 넣어주면 내가 지정한 JSON 형식으로 답변을 생성한다. Pydantic 객체로 출력 구조를 정의했으면 <code>client.chat.completions.parse()</code> 메소드를 이용해 JSON 응답을 내가 만든 객체로 변환해 가져올 수 있다.</p>
<br>

<h3 id="3-생성된-출력-확인">3. 생성된 출력 확인</h3>
<pre><code class="language-python">answer = response.choices[0].message.content
print(answer)

출력)
&#39;{
    &quot;response&quot; : &quot;사용자가 좋아할 만한 장소를 알려드립니다.&quot;,
    &quot;additionalInfo&quot; :   
        [
            {
                 &quot;name&quot; : &quot;하늘하늘 카페&quot;,
                &quot;explanation&quot; : &quot;따뜻한 무드의 카페로 가족과 함께 방문하기 좋은 곳입니다.&quot;
            },
            {
                 &quot;name&quot; : &quot;바다의 울림&quot;,
                &quot;explanation&quot; : &quot;사용자는 귀여운 물품을 좋아하기 때문에 바다와 관련된 물품을 파는 곳에서 다양한 굿즈를 얻을 수 있습니다.&quot;
            },
            ...
        ]
 }&#39;</code></pre>
<p>위의 방법대로 답변을 확인해 봤을 때, 지정한 JSON 형식의 문자열로 답변이 잘 생성된 것을 알 수 있었다. </p>
<br>

<pre><code class="language-python">def change_to_dict(answer):
    try:
        response_dict = json.loads(answer, strict=False)
        return response_dict
    except Exception as e:
        print(f&quot;Error escaping JSON strings: {e}&quot;)
        return response</code></pre>
<p>생성한 JSON 문자열을 <code>json.loads()</code> 함수를 이용해 파이썬 딕셔너리로 변환할 수 있다. 이때, 유효하지 않은 JSON 형식의 데이터일 경우 오류가 발생하는데 Structured Outputs로 생성한 답변은 오류가 발생하지 않았다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JSON 개념 정리와 자주 일어나는 JSONDecodeError에 대해]]></title>
            <link>https://velog.io/@one_two_three/JSON-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC%EC%99%80-%EC%9E%90%EC%A3%BC-%EC%9D%BC%EC%96%B4%EB%82%98%EB%8A%94-JSONDecodeError%EC%97%90-%EB%8C%80%ED%95%B4</link>
            <guid>https://velog.io/@one_two_three/JSON-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC%EC%99%80-%EC%9E%90%EC%A3%BC-%EC%9D%BC%EC%96%B4%EB%82%98%EB%8A%94-JSONDecodeError%EC%97%90-%EB%8C%80%ED%95%B4</guid>
            <pubDate>Tue, 17 Dec 2024 09:55:59 GMT</pubDate>
            <description><![CDATA[<p>OpenAI의 구조화된 출력(Structured Outputs)와 웹 통신으로 JSON 데이터를 다루면서 JSON에 대해 공부하게 되었다. 이번 게시물에서는 JSON의 개념 정리를 짧게 해보고 자주 일어나는 오류에 대해 살펴 보자 😶</p>
<hr>
<h2 id="json-개념-정리">JSON 개념 정리</h2>
<h3 id="json은">JSON은?</h3>
<ul>
<li>데이터를 교환하기 위해 경량화된 형식</li>
<li>인간이 읽고 쓰기에 용이하고 기계가 파싱하고 생성하기도 간편한 형식</li>
<li>웹 어플리케이션 데이터 교환에 많이 사용</li>
</ul>
<br>

<h3 id="기본-json-문법">기본 JSON 문법</h3>
<pre><code class="language-python">{
    &quot;first_name&quot; : &quot;Katie&quot;,
    &quot;last_name&quot; : &quot;Rodgers&quot;
}</code></pre>
<ul>
<li>키-값의 쌍으로 데이터가 쓰인다.</li>
<li>여러 쌍의 키-값의 모음은 객체라고 한다. 객체는 중괄호 안에 있다.</li>
</ul>
<br>

<h3 id="직렬화serialization">직렬화(serialization)</h3>
<pre><code class="language-python">import json

data = {&quot;response&quot; : &quot;hi&quot;, &quot;info&quot;: None}

json_object = json.dumps(data)

print(type(json_object))
print(json_object)

# 출력
# &lt;class &#39;str&#39;&gt;
# {&quot;response&quot;: &quot;hi&quot;, &quot;info&quot;: null}</code></pre>
<p>파이썬에서<code>json.dumps()</code> 함수를 이용해 파이썬 객체를 JSON 형식의 문자열로 변환할 수 있고 이를 <strong>직렬화(serialization)</strong>라고 한다. 변환된 데이터는 파일에 저장하거나 네트워크를 통해 전송할 수 있다. </p>
<p>파이썬의 <strong>None</strong> 값은 JSON으로 변환되면 <strong>null</strong>로 바뀌고 JSON 모듈은 항상 <strong>바이트 객체</strong>가 아닌 <strong>문자열 객체</strong>를 생성한다는 것을 참고하길 바란다.</p>
<p><code>json.dump()</code> 함수는 파이썬 객체를 JSON 형식 스트림(파일류 객체)로 직렬화 한다는 점에서 차이가 있다. </p>
<br>

<h3 id="역직렬화deserialization">역직렬화(deserialization)</h3>
<pre><code class="language-python">import json

string = &#39;{&quot;response&quot; : &quot;hi&quot;, &quot;info&quot;: null}&#39;

json_object = json.loads(string)

print(type(json_object))
print(json_object)

#출력
#&lt;class &#39;dict&#39;&gt;
#{&#39;response&#39;: &#39;hi&#39;, &#39;info&#39;: None}</code></pre>
<p><code>json.loads()</code> 함수를 통해 JSON 형식의 문자열이나 바이트 배열 객체를 파이썬 객체로 변환할 수 있다. 이를 <strong>역직렬화(deserialization)</strong>라고 한다. </p>
<p>위의 경우는 JSON 형식의 문자열이 파이썬 객체인 딕셔너리로 변환된 모습이다. 역직렬화 되는 데이터가 유효한 JSON 문자열이 아닌 경우 <strong>JSONDecodeError</strong>를 발생시키는 데 몇가지 오류는 아래에서 살펴 본다.</p>
<p><code>json.load()</code>는 JSON 형식을 포함하는 텍스트나 바이너리 파일을 파이썬 객체로 역직렬화 하는 함수이다.</p>
<p><br><br></p>
<h2 id="jsondecodeerror-변환할-때-조심해야-하는-것">JSONDecodeError: 변환할 때 조심해야 하는 것</h2>
<p><strong>1. JSON의 키-값은 모두 이중 따옴표(”)로 둘러 쌓여 있어야 함</strong></p>
<pre><code class="language-python">import json

# 옳게된 경우: &#39;{&quot;response&quot; : &quot;hi&quot;, &quot;info&quot;: null}&#39;
string = &quot;{&#39;response&#39; : &#39;hi&#39;, &#39;info&#39;: null}&quot;

# json.loads() 부분 오류 발생!!
json_object = json.loads(string)</code></pre>
<ul>
<li>발생한 오류: <em>json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)</em></li>
</ul>
<br>

<p>*<em>2. JSON 값 안에서 이중 따옴표를 사용할 수 없음 - 그냥 따옴표는 사용 *</em>가능</p>
<pre><code class="language-python">import json

# 옳게된 경우: &#39;{&quot;response&quot; : &quot;안녕 너 \&#39;김치\&#39; 먹어 봤어? &quot;, &quot;info&quot;: null}&#39;
string = &#39;{&quot;response&quot; : &quot;안녕 너 &quot;김치&quot; 먹어 봤어? &quot;, &quot;info&quot;: null}&#39;

# json.loads() 부분 오류 발생!!
json_object = json.loads(string)</code></pre>
<ul>
<li>발생한 오류: <em>json.decoder.JSONDecodeError: Expecting &#39;,&#39; delimiter: line 1 column 22 (char 21)</em></li>
</ul>
<br>

<p><strong>3. JSON 값 내에 제어 문자 포함될 수 없음</strong></p>
<pre><code class="language-python">import json

string = &#39;{&quot;response&quot; : &quot;안녕 너 \&#39;김치\&#39; 먹어 봤어? \n&quot;, &quot;info&quot;: null}&#39;

# json.loads() 부분 오류 발생!!
json_object = json.loads(string)
print(json_object)</code></pre>
<p>0-31 범위의 문자 코드인 <code>\t</code> <code>\n</code> <code>\0</code> <code>\r</code> 같은 제어 문자가 문자열 내부에 허용될 수 없다. </p>
<ul>
<li><p>발생한 오류: <em>json.decoder.JSONDecodeError: Invalid control character at: line 1 column 33 (char 32)</em></p>
</li>
<li><p><strong>해결 방법</strong>
<code>json.loads()</code> 함수에 strict 파라미터를 False로 설정하면 JSON 문자열 내에 제어 문자가 허용된다.</p>
<pre><code class="language-python">  json_object = json.loads(string, strict=False)
  print(json_object)

  # 출력
  # {&#39;response&#39;: &quot;안녕 너 &#39;김치&#39; 먹어 봤어? \n&quot;, &#39;info&#39;: None}</code></pre>
</li>
</ul>
<blockquote>
<p><a href="https://docs.python.org/3.13/library/json.html#basic-usage">파이썬 공식 문서</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[LangGraph] 여행 가이드봇 구현하기 ]]></title>
            <link>https://velog.io/@one_two_three/LangGraph-%EC%97%AC%ED%96%89-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%B4%87-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@one_two_three/LangGraph-%EC%97%AC%ED%96%89-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%B4%87-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 08 Nov 2024 11:44:06 GMT</pubDate>
            <description><![CDATA[<p>이전 게시물에서 나만의 agent로 여행 지원 챗봇의 흐름을 설명하였다.
그 흐름에 맞춰 구현한 코드 내용에 대해 구체적으로 기록해 보려고 한다.</p>
<p>( langGraph 처음 사용했기 때문에 잘못된 부분이 있을 수 있습니다 🥲)</p>
<blockquote>
<p><a href="https://github.com/devHaneul/TBTI-chatbot">여행 가이드봇 전체 코드</a></p>
</blockquote>
<hr>
<h2 id="여행-가이드봇-작동-흐름">여행 가이드봇 작동 흐름</h2>
<p>흐름을 아래에서 다시 확인해보자
<img src="https://velog.velcdn.com/images/one_two_three/post/bdbc2558-7017-48de-8460-ad6bfa75a6f2/image.png" alt="">
현재 흐름은 단순하다. </p>
<ol>
<li><p>언어 모델이 로드되어 있는 <code>agent</code>  노드에 사용자 메시지 전달</p>
</li>
<li><p>사용자 메시지가 호출할 수 있는 함수와 관련이 있는지 판단 후 다음 단계 진입, 관련이 있는 경우 AI가 <strong>tool_calls</strong> 변수를 가진 메시지 생성</p>
</li>
<li><p>호출 가능한 함수가 존재한다면, <code>tools</code> 노드로 진입 후 작동, 이때 질문과 관련된 정보 검색도 호출된 함수 안에서 실행
호출 가능한 함수가 없다면, AI의 일반적인 답변을 생성한 후 다음 노드로 전달</p>
</li>
<li><p>호출 결과는 <code>respond</code> 노드로 전달되어 호출된 함수에 따라 다른 지시사항과 함께 적용되어 답변 생성 (답변은 JSON 형태로 생성)</p>
</li>
<li><p>생성된 답변이 지정된 JSON 형식으로 잘 생성되고, 올바른 JSON인지 확인하는 단계를 <code>json-processing</code> 노드에서 진행 후 답변을 사용자에게 전달한다.</p>
</li>
</ol>
<p><br><br><br></p>
<h2 id="1-main-진행-코드">1. Main 진행 코드</h2>
<pre><code class="language-python">import os
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional, List, Dict
from langgraph.checkpoint.memory import MemorySaver

from openAI_api import llm
from access_milvusDB import database
from available_functions import callable_tools
from agent_executor import create_my_agent

# langsmith, langchain 환경 설정
os.environ[&quot;LANGCHAIN_TRACING_V2&quot;] = &quot;true&quot;
os.environ[&quot;LANGCHAIN_API_KEY&quot;] = &quot;api key&quot;
os.environ[&quot;LANGCHAIN_PROJECT&quot;] = &quot;test name&quot;


class QuestionRequest(BaseModel):
    question: str


class AiResponse(BaseModel):
    answer: str
    place: Optional[List[Dict]] = None


app = FastAPI()

# 대화 기록 메모리 생성
memory = MemorySaver()

# 호출할 함수 리스트 가져오기
tools = callable_tools

# 에이전트 생성 
agent = create_my_agent(
    model=llm,
    tools=tools,
    checkpointer=memory
)

config = {&quot;configurable&quot;: {&quot;thread_id&quot;: &quot;test-thread1&quot;}}    

db = database

@app.post(&quot;/ask-ai/&quot;, response_model=AiResponse)
async def ask_ai(request: QuestionRequest):
    question = request.question  # JSON에서 question 필드 추출

    try:
        db.reconnect()

        system_prompt = &quot;&quot;&quot;
        - You are a tour guide called &#39;TBTI&#39;. Ask the user a short and clear question.
        - Just ask once what kind of trip the user wants.
          ex. Is there anything you want when you travel?
        - Only up to five locations will be notified.
        &quot;&quot;&quot;

        messages_list = [(&quot;system&quot;, f&quot;{system_prompt}&quot;)] 
        messages_list.append((&quot;human&quot;, f&quot;{question}&quot;))

        # 에이전트 실행
        response = agent.invoke({&quot;messages&quot;: messages_list}, config)[&#39;final_response&#39;]
        #print(response)

        return response

    except Exception as e:
        print(&quot;에러 발생: &quot;, e)
        raise HTTPException(status_code=500, detail=&quot;AI 처리 중 오류 발생&quot;)
    finally:
        db.unconnect()

if __name__ == &quot;__main__&quot;:
    import uvicorn
    uvicorn.run(app, host=&quot;0.0.0.0&quot;, port=8991)</code></pre>
<p>사용자의 질문은 fastAPI 이용해서 받아오며 나중에 답변도 이를 통해 전달한다.
위의 코드에서 중요한 부분은 에이전트 생성과 실행 부분이다.</p>
<p>에이전트는 <code>create_my_agent</code> 함수를 통해 생성하는데 이는 LangGraph의 <code>create_react_agent</code> 코드와 아래의 문서를 참고해 구현하였다.</p>
<p>에이전트를 생성하면 <code>invoke()</code>에 사용자 질문과 메모리 설정을 담아 실행시킨다.</p>
<blockquote>
<ul>
<li><a href="https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.chat_agent_executor.create_react_agent">LangGraph Prebuit Components</a></li>
</ul>
</blockquote>
<ul>
<li><a href="https://langchain-ai.github.io/langgraph/how-tos/react-agent-structured-output/">How to return structured output with a ReAct style agent</a></li>
</ul>
<p><br><br><br></p>
<h2 id="2-agent-구현-코드">2. agent 구현 코드</h2>
<p>전체 코드는 다음과 같다. 
<code>create_my_agent</code> 안에 흐름의 각 단계를 의미하는 함수들이 정의되어 있고 마지막 부분엔 이 함수들을 이용한 노드와 노드를 연결하는 edges를 구성하여 흐름을 만들었다.</p>
<pre><code class="language-python">import json
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from langchain_core.tools import BaseTool
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.graph.graph import CompiledGraph
from langgraph.prebuilt.tool_executor import ToolExecutor
from langgraph.graph.message import add_messages
from langchain_core.language_models import LanguageModelLike

from criteria_of_answers import system_informations_of_functions
from openAI_api import chat_completion_request

from typing import (
    Optional, 
    TypedDict,
    Annotated, 
    Union, 
    Sequence
) 


def escape_json_strings(response):
    try:
        # JSON 문자열을 딕셔너리 자료형으로 변환
        response_dict = json.loads(response)
        return response_dict
    except Exception as e:
        print(f&quot;Error escaping JSON strings: {e}&quot;)
        return response


# 노드에 전달되는 state
class AgentState(TypedDict):
    previous_result : str # 이전 단계에서의 결과값 저장
    final_response : dict # 사용자에게 전달되는 최종 메시지
    messages : Annotated[list, add_messages]  # 대화 history 전달


def create_my_agent(
    model: LanguageModelLike,
    tools: Union[ToolExecutor, Sequence[BaseTool], ToolNode],
    checkpointer: Optional[BaseCheckpointSaver] = None
) -&gt; CompiledGraph:

    # LLM 시스템 프롬프트 리스트 로드 - 작동되는 함수에 따라 시스템 프롬프트 내용 다름
    configuration_for_answers = system_informations_of_functions

    # 함수 호출 도구 사용할 수 있는 모델 생성
    model_with_tools = model.bind_tools(tools)

    # 사용할 AI 모델 로드 및 AI 답변 처리
    def call_model(state: AgentState):
        response = model_with_tools.invoke(state[&#39;messages&#39;])
        last_response = response.content.strip(&quot;&lt;&gt;() &quot;).replace(&#39;\&quot;&#39;, &#39;\&#39;&#39;)
        last_response = f&#39;{{\&quot;answer\&quot;: \&quot;{last_response}\&quot;, \&quot;place\&quot;: null}}&#39;

        # AI 답변을 json 형식의 문자열로 만들어 previous_result에 저장 / 답변 history에 저장 
        return {&quot;previous_result&quot; : last_response , &quot;messages&quot; : [response]}

    # 도구 작동 후 함수 결과 LLM에게 최종 전달 후 답변 생성
    def respond_after_calling_tools(state: AgentState):
        # 작동된 마지막 도구 메시지 가져오기
        messages = state[&quot;messages&quot;]
        last_tool_message = messages[-1]

        # 작동된 함수 이름 가져오기
        name_of_functions_called = last_tool_message.name
        print(name_of_functions_called)

        # 함수 리턴값 가져오기 = 검색 결과 가져오기
        reference = last_tool_message.content

        # 작동된 도구에 맞는 시스템 프롬프트 가져오기
        system_prompt = configuration_for_answers[name_of_functions_called][&#39;system_prompt&#39;]
        response_format = configuration_for_answers[name_of_functions_called][&#39;response_format&#39;]

        # 결과 참고해서 LLM 답변 생성
        messages = [
            {&quot;role&quot;:&quot;system&quot;, &quot;content&quot;: f&quot;{system_prompt}&quot;},
            {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;:f&quot;{reference}&quot;}
        ]

        llm_response = chat_completion_request(
            messages=messages,
            response_format=response_format
        ).choices[0].message.content   

        return {&quot;previous_result&quot;: llm_response}

    # Define the function that determines whether to continue or not
    def should_continue(state: AgentState):
        messages = state[&quot;messages&quot;]
        last_message = messages[-1]

        # 함수 호출이 없으면 바로 사용자에게 리턴
        if not last_message.tool_calls:
            return &quot;pass&quot;
        # 있으면 워크플로우 지속
        else:
            return &quot;work&quot;

    def post_processing_of_answer(state: AgentState):
        ai_answer = state[&quot;previous_result&quot;]
        escaped_response = escape_json_strings(ai_answer)
        return {&quot;final_response&quot; : escaped_response}


    # 새로운 그래프 정의
    workflow = StateGraph(AgentState)

    # agent, tools 노드 생성
    workflow.add_node(&quot;agent&quot;, call_model)
    workflow.add_node(&quot;tools&quot;, ToolNode(tools))
    workflow.add_node(&quot;respond&quot;, respond_after_calling_tools)
    workflow.add_node(&quot;json-processing&quot;, post_processing_of_answer)

    # Set the entrypoint as `agent`
    # This means that this node is the first one called
    workflow.set_entry_point(&quot;agent&quot;)

    # We now add a conditional edge
    workflow.add_conditional_edges(
        &quot;agent&quot;,
        should_continue,
        {
            &quot;work&quot;: &quot;tools&quot;,
            &quot;pass&quot;: &quot;json-processing&quot;,
        },
    )

    workflow.add_edge(&quot;tools&quot;, &quot;respond&quot;)
    workflow.add_edge(&quot;respond&quot;, &quot;json-processing&quot;)
    workflow.add_edge(&quot;json-processing&quot;, END)

    graph = workflow.compile(
        checkpointer=checkpointer
    )

    return graph</code></pre>
<p>하나씩 뽑아서 구체적으로 내용을 확인해 보겠다.</p>
<br>

<h3 id="2-1-state-설정">2-1. State 설정</h3>
<pre><code class="language-python"># 노드에 전달되는 state
class AgentState(TypedDict):
    previous_result : str # 이전 단계에서의 결과값 저장
    final_response : dict # 사용자에게 전달되는 최종 메시지
    messages : Annotated[list, add_messages]  # 대화 history 전달</code></pre>
<p>각 노드들이 공유할 수 있는 데이터 구조라고 볼 수 있다. 노드에서 위에 설정된 변수들에 접근하여 이를 사용하거나 저장할 수 있다.</p>
<p>나는 주석에 달린 것처럼 <strong>이전 단계에서의 결과값</strong>, <strong>마지막 전달되는 최종 메시지</strong>, <strong>대화 기록</strong>을 저장하여 사용하였다.</p>
<p><br><br></p>
<h3 id="2-2-실행할-함수작업-정의">2-2. 실행할 함수(작업) 정의</h3>
<pre><code class="language-python">-------------------------------- 작업 1 ---------------------------------

   # 함수 호출 도구 사용할 수 있는 모델 생성
    model_with_tools = model.bind_tools(tools)

    # 사용할 AI 모델 로드 및 AI 답변 처리
    def call_model(state: AgentState):
        response = model_with_tools.invoke(state[&#39;messages&#39;])
        last_response = response.content.strip(&quot;&lt;&gt;() &quot;).replace(&#39;\&quot;&#39;, &#39;\&#39;&#39;)
        last_response = f&#39;{{\&quot;answer\&quot;: \&quot;{last_response}\&quot;, \&quot;place\&quot;: null}}&#39;

        return {&quot;previous_result&quot; : last_response , &quot;messages&quot; : [response]}
</code></pre>
<p><code>bind_tools</code> 함수를 이용해 호출 가능한 도구 정보들을 모델이 담고 이를 사용할 것이다. 작업 내용을 짧게 정리해보겠다.</p>
<ul>
<li>call_model : 
도구를 호출할 수 있는 모델을 가져와 사용자의 메시지를 모델에 전달해 답변을 생성한다. 답변의 내용만을 추출해 previous_results 변수에 딕셔너리 형태의 문자열로 저장하고 messages 변수에는 대화 기록을 저장한다.</li>
</ul>
<br>





<pre><code class="language-python">-------------------------------- 작업 2 ---------------------------------

    # 도구 작동 후 함수 결과 LLM에게 최종 전달 후 답변 생성
    def respond_after_calling_tools(state: AgentState):
        # 작동된 마지막 도구 메시지 가져오기
        messages = state[&quot;messages&quot;]
        last_tool_message = messages[-1]

        # 작동된 함수 이름 가져오기
        name_of_functions_called = last_tool_message.name
        print(name_of_functions_called)

        # 함수 리턴값 가져오기 = 검색 결과 가져오기
        reference = last_tool_message.content

        # 작동된 도구에 맞는 시스템 프롬프트 가져오기
        system_prompt = configuration_for_answers[name_of_functions_called][&#39;system_prompt&#39;]
        response_format = configuration_for_answers[name_of_functions_called][&#39;response_format&#39;]

        # 결과 참고해서 LLM 답변 생성
        messages = [
            {&quot;role&quot;:&quot;system&quot;, &quot;content&quot;: f&quot;{system_prompt}&quot;},
            {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;:f&quot;{reference}&quot;}
        ]

        llm_response = chat_completion_request(
            messages=messages,
            response_format=response_format
        ).choices[0].message.content   

        return {&quot;previous_result&quot;: llm_response}
</code></pre>
<ul>
<li>respond_after_calling_tools : 
호출된 도구의 작동 결과와 지정 프롬프트를 이용해 답변을 생성하는 작업이다. 작동 결과를 대화 기록에서 추출하고 작동된 도구 이름에 따라 각각 다른 시스템 프롬프트 내용을 가져온다.</li>
</ul>
<p>이 함수에선 내부에 도구 호출 가능한 모델이 아닌 <strong>일반 LLM을 사용</strong>하여 답변을 생성하는데 <strong>아래의 json 스키마 같이 지정된 json 형태로 답변</strong>을 생성하도록 했다. 답변 json 형태도 호출되는 함수에 따라 다르다.</p>
<p>생성한 답변은 마지막에 previous_result 변수에 저장해 이전 단계 결과값으로 저장한다.</p>
<pre><code>  response_format_1 = {
      &quot;type&quot;: &quot;json_schema&quot;,
      &quot;json_schema&quot; : {
          &quot;name&quot; : &quot;A_general_answer&quot;,
          &quot;schema&quot; : {
              &quot;type&quot;: &quot;object&quot;,
              &quot;properties&quot; : {
                  &quot;answer&quot;: {
                      &quot;type&quot;: &quot;string&quot;,
                      &quot;description&quot; : &quot;put your answer in the value.&quot;
                  },
                  &quot;place&quot;: {
                      &quot;type&quot;: [&quot;null&quot;, &quot;object&quot;],
                      &quot;description&quot;: &quot;This will be null.&quot;
                  }
              },
              &quot;required&quot; : [&quot;answer&quot;, &quot;place&quot;],
              &quot;additionalProperties&quot;: False
          },
          &quot;strict&quot; : True
      }
  }</code></pre><br>

<pre><code class="language-python">
-------------------------------- 작업 3,4 -------------------------------

    def should_continue(state: AgentState):
        messages = state[&quot;messages&quot;]
        last_message = messages[-1]

        # 함수 호출이 없으면 바로 사용자에게 리턴
        if not last_message.tool_calls:
            return &quot;pass&quot;
        # 있으면 워크플로우 지속
        else:
            return &quot;work&quot;

    def post_processing_of_answer(state: AgentState):
        ai_answer = state[&quot;previous_result&quot;]
        escaped_response = escape_json_strings(ai_answer)
        return {&quot;final_response&quot; : escaped_response}</code></pre>
<ul>
<li><p>should_continue : 
사용자의 질문이 도구 호출이 가능한 지 판단하는 작업
함수 호출이 필요하면 <strong>work</strong> 과정으로 진행, 없으면 <strong>pass</strong> 과정으로 진행</p>
</li>
<li><p>post_processing_of_answer : 
이전 단계에서 저장된 LLM의 답변을 다시 가져와 <strong>JSON 형식이 아닌 문자열</strong>이 있는지 검토하고 이를 <strong>파이썬 딕셔너리</strong>의 형태로 변환해 저장한다.</p>
</li>
</ul>
<p><br><br></p>
<h3 id="2-3-그래프-생성">2-3. 그래프 생성</h3>
<pre><code class="language-python">    # 새로운 그래프 정의
    workflow = StateGraph(AgentState)

    # agent, tools 노드 생성
    workflow.add_node(&quot;agent&quot;, call_model)
    workflow.add_node(&quot;tools&quot;, ToolNode(tools))
    workflow.add_node(&quot;respond&quot;, respond_after_calling_tools)
    workflow.add_node(&quot;json-processing&quot;, post_processing_of_answer)

    # Set the entrypoint as `agent`
    # This means that this node is the first one called
    workflow.set_entry_point(&quot;agent&quot;)

    # We now add a conditional edge
    workflow.add_conditional_edges(
        &quot;agent&quot;,
        should_continue,
        {
            &quot;work&quot;: &quot;tools&quot;,
            &quot;pass&quot;: &quot;json-processing&quot;,
        },
    )

    workflow.add_edge(&quot;tools&quot;, &quot;respond&quot;)
    workflow.add_edge(&quot;respond&quot;, &quot;json-processing&quot;)
    workflow.add_edge(&quot;json-processing&quot;, END)

    graph = workflow.compile(
        checkpointer=checkpointer
    )

    return graph</code></pre>
<p>이제 그래프를 정의해 에이전트 흐름을 구현해 본다.</p>
<p><code>StateGraph</code> 객체를 통해 &#39;상태&#39;를 가지는 챗봇의 구조를 정의했다고 할 수 있다. 그 다음으로 위에서 정의한 작업 내용들을 노드로 구성하고, 호출 가능한 도구들도 <code>ToolNode</code>로 정의하여 하나의 노드로 만든다.</p>
<p>노드 생성 후에는 시작 노드를 설정하고 <code>Edge</code>를 통해 노드들을 연결한다. 이는 노드들이 통신하는 지점을 만드는 것이며 <code>Edge</code>는 진행 로직을 만들고 라우팅 되거나 중지되는 부분도 만들 수 있다.</p>
<p>마지막에 구현한 흐름을 <code>compile()</code>을 통해  실행 가능한 에이전트를 생성한다.</p>
<p><br><br><br></p>
<h2 id="3-호출-도구-정의-코드">3. 호출 도구 정의 코드</h2>
<pre><code class="language-python">import json
from langchain_core.tools import tool
from access_milvusDB import database
from openAI_api import embedding

@tool
def recommand_travel_destination(question : str, location : str, area : str) -&gt; str:
    &quot;&quot;&quot;
    recommand the various places that user wants to know or to travel
    It only works when user wants to know the various places.
    It doesn&#39;t work when user told to plan the trip and when user told to reserve the place.

    Args:
        question: Identify the travel the user want and input the questions.
        location: input the area of Korea to travel, e.g. 서울 or 부산 or 대구 or 강원도
        area: Enter only the following words to indicate where the place in the user&#39;s question belongs to the following Korean administrative districts. e.g. 강원특별자치도 
              - 한국 행정 구역 : 서울특별시, 부산광역시, 인천광역시, 대구광역시, 대전광역시, 광주광역시, 울산광역시, 세종특별자치시, 경기도, 충청북도, 충청남도, 전라남도, 경상북도, 경상남도, 강원특별자치도, 전북특별자치도, 제주특별자치도
    &quot;&quot;&quot;

    milvus = database

    # 사용자 질문 벡터화
    vector  =  embedding(question)

    # 필터링 생성 후 테이블 검색 진행
    filtering = f&quot;area_name == &#39;{area}&#39;&quot; 
    results_localCreator, results_nowLocal = milvus.search_all_tables(embedding=vector, filtering=filtering)

    # 쿼리 결과 합치기
    total_results = milvus.get_formatted_results(results_localCreator, results_nowLocal)

    return f&quot;user question: {question} \n\nreference: \n{total_results}&quot;


@tool
def search_specific_place(question : str, place_name : str = None) -&gt; json:
    &quot;&quot;&quot;
    give the information of the specific places mentioned by the user.
    It works when a user question contains a name of specific place.
    It doesn&#39;t work when you recommand a place.

    Args:
        question: input the user&#39;s question as it is
        place_name: The name of the particular place that user wants to know or to reserve
    &quot;&quot;&quot;

    # 특정 장소 검색 과정 진행....

    return f&quot;user question: {question} \n\nreference: \n{total_results}&quot;



callable_tools = [recommand_travel_destination, create_travel_plan, search_specific_place]</code></pre>
<p>여행지 추천, 여행 계획 생성, 특정 장소 검색 기능을 도구들로 정의하여 모델이 사용자 질문의 의도를 파악해 그에 맞는 기능을 사용할 수 있도록 한다. 위의 코드는 중간 생략해서 전체 코드는 아니다.</p>
<p>호출할 도구는 위와 같이 정의하면 되는데 <code>@tool</code> 어노테이션을 달아 호출할 도구라는 것을 표시하고, 함수 내부의 주석으로 어떤 기능이며 언제 작동하고, 입력될 파라미터 값의 설명을 포함하여 LLM이 도구를 이해하고 사용할 수 있도록 만든다.</p>
<p>호출할 도구들은 정의 후 <code>callable_tools</code>안에 넣어 리스트로 저장한다.</p>
<p><br><br></p>
<h2 id="코드-진행-예시">코드 진행 예시</h2>
<p>사용자 질문에서 LLM의 답변까지 어떻게 진행되는 건지 예시를 들어 정리해보도록 하겠다.</p>
<p>🙎 사용자: <strong>강릉으로 여행 갈 건데 갈만한 곳 알려줘.</strong></p>
<br>

<p><strong>1. Main 작동</strong>
    - <a href="http://0.0.0.0:8991/ask-ai">http://0.0.0.0:8991/ask-ai</a> 경로로 사용자 질문 post 요청
    - 전달 받은 JSON 에서 사용자 질문을 추출 후 생성한 에이전트에 전달</p>
<br>

<p><strong>2. 에이전트 작동</strong></p>
<ul>
<li><p><em>agent node</em> : 
에이전트는 전달 받은 질문이 여행지 추천 함수인 <code>recommand_travel_destination</code>와 관련있다고 판단</p>
</li>
<li><p><em>tools node</em> : 
여행지 추천 함수 호출하여 작동, 강릉의 여행지를 검색하여 사용자에게 맞는 여행지 리스트를 질문과 함께 리턴</p>
</li>
<li><p><em>respond node</em> : 
함수 리턴 결과와 여행지 추천 시 답변 생성 기준을 가져와 LLM 답변 생성</p>
</li>
<li><p><em>json-processing</em> : 
LLM이 생성한 JSON 형태의 문자열이 올바른 JSON 형식인지 확인 후 딕셔너리로 변환
딕셔너리 객체를 최종 전달 메시지로 저장 후 사용자에게 전달</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[왜 LangGraph를 사용했는가? ]]></title>
            <link>https://velog.io/@one_two_three/%EC%99%9C-LangGraph%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%96%88%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@one_two_three/%EC%99%9C-LangGraph%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%96%88%EB%8A%94%EA%B0%80</guid>
            <pubDate>Fri, 08 Nov 2024 07:49:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://langchain-ai.github.io/langgraph/">LangGraph 공식 문서</a></p>
</blockquote>
<p>저번 게시물에서 프로그램에 대해 간단하게 소개하며 개발하면서 까다로웠던 점들을 이야기했다. 다시 한번 그 부분을 살펴보며 LangChain과 LangGraph 라이브러리로 어떻게 구현할 예정인지 살펴보겠다.</p>
<p>( 아직 개발 능력이 부족해 잘못 판단한 부분이 있을 수 있습니다 🥲)</p>
<br>

<hr>
<h2 id="개발하면서-까다로웠던-부분">개발하면서 까다로웠던 부분</h2>
<br>

<p>*<em>프로그램에 사용되는 LLM은.. *</em></p>
<p><strong>1. 이전 대화 내용을 기억할 수 있어야 한다.</strong>
자연어 처리 모델를 이용해 대화를 하다 보면 대화 내용이 쌓이게 되는데, 앞선 대화 속에 사용자의 요구사항을 모델이 기억하고 알맞은 답변을 하기 위해서는 대화 내용을 기억할 수 있어야 한다.</p>
<p>👉 LangChain과 LangGraph의 memory를 사용할 수 있다.</p>
<br>

<p><strong>2. 도구를 호출할 수 있어야 한다.</strong>
사용자의 다양한 요구에 맞는 능동적인 답변을 생성하기 위해서 사용자의 질문과 관련된 함수를 작동시킬 수 있도록 구성하였다. 따라서 위에서 설명한 여행지 추천, 여행 계획 생성, 특정 장소 검색 기능은 각각 다른 함수들로 구성되어 있고, 사용자 질문의 의도를 파악한 후 모델이 관련 함수를 호출한 후 답변을 생성한다.</p>
<pre><code>EX. 사용자 질문: 강릉에서 아이와 함께 갈 수 있는 장소 알려줄래?
    -&gt; 모델은 여행지 추천 함수를 작동 후 작동 결과를 다음 단계로 전달</code></pre><p>👉 LangChain의 <code>AgentExecutor</code>를 통해 가능
👉 LangGraph의 사전 구축된 <code>create_react_agent</code> 를 통해 가능</p>
<br>

<p><strong>3. 항상 JSON 형식으로 답변을 생성해야 한다.</strong>
LLM의 답변을 JSON 형식으로 만들어 웹 서버로 전달해야 하며 FastAPI를 사용한다. 웹 서버에서는 JSON을 받아 필요한 값을 추출해 질문에 가장 효과적인 방식으로 답변을 재구성한다. JSON의 key는 <code>answer</code>와 <code>place</code>가 존재한다.</p>
<p>👉 LangChain <code>with_structured_output()</code> 메서드를 이용한 모델의 답변을 구조적 데이터로 반환 가능
👉 JSON 스키마 설정을 출력 형태로 전달해 구조적 출력이 가능한 모델이 있음 (EX. OpenAI)</p>
<br>

<p><strong>4. 상황에 따라 생성해야 하는 JSON 값이 다르다.</strong>
간단하게 예를 들어 설명하면,</p>
<ol>
<li>여행지 추천의 경우
장소의 그림과 정보를 전달해 카드 형식으로 사용자에게 보여줘야 하기 때문에 다음과 같이 구성된다. <code>answer</code> 값으로 짧은 추천의 말과, <code>place</code>에는 장소의 정보가 전달된다.<pre><code> {
     &quot;answer&quot; : &quot;AI의 짧은 추천의 말&quot;,
     &quot;place&quot; :   
         [
             {
                  &quot;place_name&quot; : &quot;장소 이름&quot;,
                 &quot;description&quot; : &quot;장소 설명&quot;,
                 &quot;redirection_url&quot; : &quot;장소의 상세페이지 이동 경로&quot;
             },
             ...
         ]
 }</code></pre></li>
<li>여행 계획 생성의 경우
사용자에게 전달되는 답변은 긴 문자열이기 때문에
여행 계획의 긴 내용이 <code>answer</code> 키의 값으로 들어가고, <code>place</code> 값은 <strong>null</strong>로 된다.<pre><code> {
     &quot;answer&quot; : &quot;1박 2일의 긴 여행 계획 내용&quot;,
     &quot;place&quot; : null
 }</code></pre></li>
</ol>
<p>👉 Pydantic Class , TypedDict, Json-Schema 등의 방식으로 원하는 형태의 답변 출력을 의도할 수 있음</p>
<p><br><br></p>
<h2 id="어떻게-구현해볼까">어떻게 구현해볼까?</h2>
<h3 id="1-langchain과-langgraph의-사전-구축-agent-사용">1. LangChain과 LangGraph의 사전 구축 Agent 사용</h3>
<pre><code class="language-python">import getpass
import os

from typing import Optional
from langgraph.prebuilt import create_react_agent
from langchain_core.pydantic_v1 import BaseModel, Field
from available_functions import callable_tools


os.environ[&quot;OPENAI_API_KEY&quot;] = getpass.getpass()

from langchain_openai import ChatOpenAI

# Pydantic
class Joke(BaseModel):
    &quot;&quot;&quot;Joke to tell user.&quot;&quot;&quot;

    setup: str = Field(description=&quot;The setup of the joke&quot;)
    punchline: str = Field(description=&quot;The punchline to the joke&quot;)
    rating: Optional[int] = Field(
        default=None, description=&quot;How funny the joke is, from 1 to 10&quot;
    )

llm = ChatOpenAI(model=&quot;gpt-4o-mini&quot;)
structured_llm = llm.with_structured_output(Joke)

# 호출할 함수 리스트 가져오기
tools_of_list = callable_tools

agent = create_react_agent(
    model=structured_llm,
    tools=tools_of_list
)

response = agent.invoke({&quot;messages&quot;: [(&quot;human&quot;, &quot;안녕?&quot;)]})
print(response[&quot;messages&quot;])</code></pre>
<p>간단하게 코드를 짜서 가능성만 확인해본다.
<code>create_react_agent</code> 메서드는 Langgraph의 사전 구축 Agent로 이를 통해 쉽게 도구를 호출하고 작동시킬 수 있다. 또한 LLM의 답변이 JSON 형식으로 나오도록 <code>with_structured_output()</code>를 사용하고 싶었다.</p>
<p>하지만 이 코드는 오류가 발생해 작동될 수 없다.
구조적 출력이 가능한 모델을 <code>create_react_agent</code>의 model 파라미터로 전달할 수 없기 때문이다. 오류 메시지는 다음과 같다.
<br></p>
<p>** 무슨 오류인가?**</p>
<pre><code>AttributeError: &#39;RunnableSequence&#39; object has no attribute &#39;bind_tools </code></pre><p><code>Create_react_agent</code>는 내부적으로 모델의 <code>bind_tools</code> 메서드를 호출한다.
<code>with_structured_output()</code>의 리턴값은 RunnableSequence 객체이기 때문에 이 객체는 <code>bind_tools</code> 메서드를 작동시킬 수 없기 때문에 일어나는 오류이다.</p>
<p><strong>LangChain의 기존 <code>AgentExecutor</code>를 사용해도 같은 문제가 일어나게 되었다.</strong></p>
<p><code>create_react_agent</code>의 결과를 이용한 후처리 단계를 chain에 달면 되지만 우리 프로그램의 복잡한 로직을 구현하기에 한계가 있다고 생각했다.</p>
<p><br><br><br></p>
<h3 id="2-langgraph를-통해-나만의-agent를-만들어보자">2. LangGraph를 통해 나만의 Agent를 만들어보자</h3>
<p>LangChain의 AgentExecutor와 사전 구축되어 제공되는 agent에 한계를 느낄 때쯤, 구조적 출력이 가능한 agent 생성 글을 보게 되었다.</p>
<blockquote>
<p><a href="https://langchain-ai.github.io/langgraph/how-tos/react-agent-structured-output/">How to return structured output with a ReAct style agent</a></p>
</blockquote>
<p>LangChain은 보통 도구 호출 후 결과가 LLM에게 다시 반환되어 이 결과를 참고하여 답변을 생성한다. 이 과정이 일정하게 묶여있어 도구 호출 결과에 따라 다른 후처리 과정을 구현하기가 어렵다고 생각되었다.</p>
<p>LangGraph를 사용하면 도구 호출 결과에 따라 다른 과정을 구현하고 검토하기 편리하며, 에이전트의 흐름을 직접 지정하고 제어할 수 있다는 점에서 유연하다고 판단했다.</p>
<p>또한 좋은 점은 프로그램 각 단계의 상태를 저장하고 확인할 수 있기 때문에 데이터의 일관성과 올바른 실행 순서를 보장할 수 있다는 점이다.</p>
<p><br><br><br></p>
<h2 id="나만의-agent는">나만의 Agent는?</h2>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/bdbc2558-7017-48de-8460-ad6bfa75a6f2/image.png" alt="">
현재 흐름은 단순하다. </p>
<ol>
<li><p>언어 모델이 로드되어 있는 <code>agent</code>  노드에 사용자 메시지 전달</p>
</li>
<li><p>사용자 메시지가 호출할 수 있는 함수와 관련이 있는지 판단 후 다음 단계 진입, 관련이 있는 경우 AI가 <strong>tool_calls</strong> 변수를 가진 메시지 생성</p>
</li>
<li><p>호출 가능한 함수가 존재한다면, <code>tools</code> 노드로 진입 후 작동, 이때 질문과 관련된 정보 검색도 호출된 함수 안에서 실행
호출 가능한 함수가 없다면, AI의 일반적인 답변을 생성한 후 다음 노드로 전달</p>
</li>
<li><p>호출 결과는 <code>respond</code> 노드로 전달되어 호출된 함수에 따라 다른 지시사항과 함께 적용되어 답변 생성 (답변은 JSON 형태로 생성)</p>
</li>
<li><p>생성된 답변이 지정된 JSON 형식으로 잘 생성되고, 올바른 JSON인지 확인하는 단계를 <code>json-processing</code> 노드에서 진행 후 답변을 사용자에게 전달한다.</p>
</li>
</ol>
<p>이제 이러한 흐름의 자세한 코드 내용은 다음 게시물에서 이어진다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[LangChain][LangGraph] 한국 로컬 여행가이드 Chatbot 개발 프로젝트]]></title>
            <link>https://velog.io/@one_two_three/%ED%95%9C%EA%B5%AD-%EB%A1%9C%EC%BB%AC-%EC%97%AC%ED%96%89%EA%B0%80%EC%9D%B4%EB%93%9C%EB%B4%87-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-LangChain-LangGraph-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@one_two_three/%ED%95%9C%EA%B5%AD-%EB%A1%9C%EC%BB%AC-%EC%97%AC%ED%96%89%EA%B0%80%EC%9D%B4%EB%93%9C%EB%B4%87-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-LangChain-LangGraph-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Wed, 06 Nov 2024 14:53:45 GMT</pubDate>
            <description><![CDATA[<p>공모전에서 챗봇을 개발하는 파트를 맡아 한국 로컬 위주의 여행 지원 봇을 구축해 보았다. 아직 완성본은 아니기에 현재 완료된 부분과 앞으로 새롭게 추가할 기능, 변경된 부분들을 차분히 기록해보고자 한다.</p>
<p>이 챗봇은 로컬 여행 관련 웹 메인 페이지에 위치해 Java Spring 웹서버와 FastAPI를 활용해 통신한다.</p>
<hr>
<h2 id="구현-프로그램-내용">구현 프로그램 내용</h2>
<p>로컬 여행 챗봇으로 사용자에게  필요한 정보를 친숙한 대화 형식의 방식으로 지원해준다. 이 챗봇이 가진 기능은 크게 세가지이다.</p>
<h3 id="프로그램-기능">프로그램 기능</h3>
<ol>
<li><p><strong>여행지 추천</strong>
사용자가 여행하고자 하는 지역을 파악한 후 사용자 질문과 관련된 장소를 최대 5곳 추천해준다. 아래의 그림 처럼 장소명, 설명, 장소의 그림과 함께 사용자에게 장소를 추천한다.
<img src="https://velog.velcdn.com/images/one_two_three/post/a57c9fe1-fb08-4356-a742-d5272bf9cf6d/image.png" alt=""></p>
</li>
<li><p><strong>여행 계획 생성</strong>
여행 지역, 여행 기간을 파악한 후 사용자가 원하는 장소들로 구성된 계획을 생성해준다.      </p>
<ul>
<li>여행 계획 생성 조건</li>
</ul>
<ol>
<li>하루에 방문할 여행지의 카테고리는 모두 달라야 한다.</li>
<li>마지막 날에 숙박 업소는 추천하지 않는다.</li>
<li>여행 계획에 포함된 각 장소들의 거리가 10km 이내가 되도록 구성한다.</li>
<li>추천해주는 각 장소는 장소의 이름, 설명, 위치를 제공한다.</li>
</ol>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/b88e4873-102a-4ad0-90ce-352423dfd67c/image.png" alt=""></p>
<ol start="3">
<li><strong>특정 장소 정보 제공</strong>
사용자가 특정 장소의 정보를 원할 시, 특정 여행지 이름을 파악하여 관련 장소의 카드를 전달한다. 이 카드를 누르면 장소의 상세정보를 보여주는 페이지로 이동할 수 있다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/4ea515b4-9374-4120-a1f2-1cd7b70eb444/image.png" alt=""></p>
<p><br><br></p>
<h3 id="프로그램-시스템-구성">프로그램 시스템 구성</h3>
<ul>
<li>활용 라이브러리: LangChain, LangGraph</li>
<li>주요 적용 기술: RAG (Retrieval-Argumented Generation)</li>
<li>사용된 모델: GPT-4o-mini (OpenAI), BGE-M3-Korean (Transformers)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/1e2fa5a8-c901-4d2d-9cf7-378ac7609c1a/image.png" alt=""></p>
<p><strong>시스템 흐름 설명</strong></p>
<ul>
<li><p>챗봇이 작동되면 사용자의 질문이 LangGraph Agent에 전달된다. 질문의 의도를 파악하여 질문과 관련된 외부 도구(= 함수)를 호출할 수 있다.</p>
</li>
<li><p>이 도구가 호출될 경우, 사용자 질문과 관련된 함수가 작동되며 질문을 벡터화 시킨 후 관련된 자료를 검색한다. ( RAG 로직이 여기에 이 부분에서 쓰인다.)</p>
</li>
<li><p>호출된 함수에 지정된 지시사항과 검색 참고 자료를 이용해 자연어 처리 모델이 답변을 생성하고, 알맞은 JSON 형식으로 답변을 후처리 후 사용자에게 전달
<br><br></p>
</li>
</ul>
<h3 id="프로그램-특장점">프로그램 특장점</h3>
<ol>
<li><p><strong>로컬 크리에이터 기반의 특수 데이터 제공</strong>
 로컬 크리에이터는 지역의 자산을 활용해 사업을 하고 계신 분들을 의미하며, 정부에서 지역 활성화 프로젝트로 이들을 지원하기도 한다. 마케팅 역량이 부족한 로컬 크리에이터 위주로 장소를 소개하여 일반 여행자가 알아내기 힘든 장소들 위주로 정보를 제공한다.</p>
</li>
<li><p><strong>여행 가이드봇 제공</strong></p>
<p> 챗봇을 통해 정보 접근 편의성을 증대시키며 개인의 관심사와 취향을 실시간으로 반영해 여행 정보를 제공 받을 수 있다.
 설문 형태의 데이터 군집화 결과가 아닌 사용자와 소통하는 간단한 방식으로 장소를 추천한다.</p>
</li>
<li><p><strong>장소의 구체적인 정보 체크 가능</strong></p>
<p>강원도 로컬 크리에이터 위주로 설문조사를 진행하여 장소의 예약 가능 여부, 반려동물 동반 가능, 분위기, 휠체어 접근 가능 여부 등 디테일한 정보들을 제공함.</p>
<p>다른 여행 사이트에서 제공하지 않는 정보들을 제공하며 챗봇과의 대화와 메인페이지의 장소 검색 기능을 통해 이러한 정보를 손쉽게 얻을 수 있다.</p>
</li>
</ol>
<p><br><br></p>
<hr>
<h2 id="개발하면서-까다로웠던-부분">개발하면서 까다로웠던 부분</h2>
<p>왜 LangGraph 라이브러리를 사용했는지에 대한 답변도 포함한다.</p>
<br>

<p>*<em>프로그램에 사용되는 LLM은.. *</em></p>
<p><strong>1. 이전 대화 내용을 기억할 수 있어야 한다.</strong>
자연어 처리 모델를 이용해 대화를 하다 보면 대화 내용이 쌓이게 되는데, 앞선 대화 속에 사용자의 요구사항을 모델이 기억하고 알맞은 답변을 하기 위해서는 대화 내용을 기억할 수 있어야 한다.
<br></p>
<p><strong>2. 도구를 호출할 수 있어야 한다.</strong>
사용자의 다양한 요구에 맞는 능동적인 답변을 생성하기 위해서 사용자의 질문과 관련된 함수를 작동시킬 수 있도록 구성하였다. 따라서 위에서 설명한 여행지 추천, 여행 계획 생성, 특정 장소 검색 기능은 각각 다른 함수들로 구성되어 있고, 사용자 질문의 의도를 파악한 후 모델이 관련 함수를 호출한 후 답변을 생성한다.</p>
<pre><code>EX. 사용자 질문: 강릉에서 아이와 함께 갈 수 있는 장소 알려줄래?
    -&gt; 모델은 여행지 추천 함수를 작동 후 작동 결과를 다음 단계로 전달</code></pre><p><strong>3. 항상 JSON 형식으로 답변을 생성해야 한다.</strong>
LLM의 답변을 JSON 형식으로 만들어 웹 서버로 전달해야 하며 FastAPI를 사용한다. 웹 서버에서는 JSON을 받아 필요한 값을 추출해 질문에 가장 효과적인 방식으로 답변을 재구성한다. JSON의 key는 <code>answer</code>와 <code>place</code>가 존재한다.
<br></p>
<p><strong>4. 상황에 따라 생성해야 하는 JSON 값이 다르다.</strong>
간단하게 예를 들어 설명하면,</p>
<ol>
<li>여행지 추천의 경우
장소의 그림과 정보를 전달해 카드 형식으로 사용자에게 보여줘야 하기 때문에 다음과 같이 구성된다. <code>answer</code> 값으로 짧은 추천의 말과, <code>place</code>에는 장소의 정보가 전달된다.<pre><code> {
     &quot;answer&quot; : &quot;AI의 짧은 추천의 말&quot;,
     &quot;place&quot; :   
         [
             {
                  &quot;place_name&quot; : &quot;장소 이름&quot;,
                 &quot;description&quot; : &quot;장소 설명&quot;,
                 &quot;redirection_url&quot; : &quot;장소의 상세페이지 이동 경로&quot;
             },
             ...
         ]
 }</code></pre></li>
<li>여행 계획 생성의 경우
사용자에게 전달되는 답변은 긴 문자열이기 때문에
여행 계획의 긴 내용이 <code>answer</code> 키의 값으로 들어가고, <code>place</code> 값은 <strong>null</strong>로 된다.<pre><code> {
     &quot;answer&quot; : &quot;1박 2일의 긴 여행 계획 내용&quot;,
     &quot;place&quot; : null
 }</code></pre></li>
</ol>
<p><br><br></p>
<h2 id="보완해야-하는-부분">보완해야 하는 부분</h2>
<ol>
<li><p>웹 메인 페이지에서 여러 사용자들이 챗봇을 각각 이용할 수 있어야 한다.
현재 병렬적으로 작동하지 않아 한 사용자가 답변을 받은 후 다른 사용자가 이용할 수 있다. </p>
</li>
<li><p>사용자의 여행 취향을 파악할 수 있는 조금 더 효율적인 방법이 필요하다. 
사용자와의 대화를 통해 취향에 맞는 장소를 대부분 알려주지만 안정성과 속도 측면에서 좋은 편은 아니라고 판단된다. (가끔 취향과 맞지 않는 장소를 추천해줄 때도 있다) 
대화를 시작하기 전 사용자의 여행 유형을 전달하여 이를 기반으로 답변을 생성하는 방식으로 재구성해 보고 싶다.</p>
</li>
<li><p>Self-RAG / Corrective-RAG 방식을 적용해 답변의 검색 정확도와 완성도를 높이는 방법으로 구성하고 싶다.</p>
</li>
</ol>
<br>
위의 보완점들을 조금씩 해결해가면서 수정한 내용들도 이어서 블로그에 기록할 예정이다. 수정하면서 시스템 구성과 흐름은 조금 달라질 수도 있을 것 같다. 

<p>다음 게시물은 왜 LangGraph 라이브러리를 사용했는지에 대한 구체적인 내용과 구현 내용을 적어볼 것이다 😉</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RAG는 무엇인가? RAG를 간단하게 구현해보자 ]]></title>
            <link>https://velog.io/@one_two_three/RAG%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@one_two_three/RAG%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Thu, 22 Aug 2024 12:17:38 GMT</pubDate>
            <description><![CDATA[<p>공모전에서 챗봇을 개발하는 부분을 맡아 처음에는 LLM(Large Language Models)을 파인 튜닝(fine-tuning) 통해 우리 프로그램 만의 AI를 만들려고 했다. 파인 튜닝은 사전 학습 모델에 새로운 추가 데이터를 학습 시켜 모델을 최적화하는 방법이다. </p>
<p>하지만, 학습 시킬 수 있는 데이터의 개수가 부족하기 때문인지 정확도가 좋지 않았고 학습하는 시간도 너무 오래 걸리기 때문에 시간 낭비인듯한 느낌이 들었다. 
또한, 프롬프트 엔지니어링(prompt-engineering) 만으로 우리가 원하는 AI 기능을 개발할 수 없었기 때문에 RAG를 도입하기로 결정했다.</p>
<br>

<p>만약 나처럼 챗봇을 처음 개발해야 한다면 아래의 단계대로 챗봇 구현 가능성에 대해 파악하면 좋겠다.</p>
<ol>
<li><p><strong>프롬프트 엔지니어링(prompt-engineering)을 시도해 본다.</strong>
프롬프트 엔지니어링은 언어 모델에게 원하는 결과를 얻기 위해서 언어 모델에게 전달될 텍스트들을 효율적으로 설계하는 것이라고 볼 수 있다.
OpenAI 공식 문서에서 프롬프트 엔지니어링을 위한 전략들과 예시들을 확인해 볼 수 있다.</p>
<blockquote>
<p><a href="https://platform.openai.com/docs/guides/prompt-engineering">Prompt Engineering</a></p>
</blockquote>
</li>
<li><p>프롬프트 엔지니어링으로 해결되지 않는다면 <strong>RAG(Retrieval-Augmented Generation)를 도입한다.</strong>
RAG가 무엇인지 아래에서 자세하게 살펴보겠다.</p>
</li>
<li><p>RAG를 사용해도 해결되지 않을 때 <strong>파인 튜닝</strong>을 이용해본다.
파인 튜닝으로 데이터 학습의 효과를 나타내고 싶다면 학습 데이터의 개수가 최소 10만건이 존재해야 한다.</p>
</li>
</ol>
<hr>
<br>

<h2 id="rag란">RAG란?</h2>
<p>RAG(Retrieval-Augmented Generation)는 LLM의 단점을 개선시키기 위해 학습 데이터 소스 이외에 신뢰할 수 있는 외부 지식 베이스를 참조하도록 하는 기술이다.</p>
<p>간단하게 예를 든다면, 대규모 언어 모델이 시험을 치룰 때 자신이 알고 있는 지식으로만 시험을 보는 것이 아닌 참고할 수 있는 오픈북 하나를 가지고 시험을 볼 수 있는 것이다.</p>
<p>참고할 수 없는 오픈북이 없을 때 LLM이 발생시킬 수 있는 문제점들이 존재한다.</p>
<ul>
<li>자신이 잘 모르는 부분이라면 허위 정보를 제공한다.</li>
<li>구체적이고 최신의 정보가 아닌 오래되었거나 일반적인 정보를 제공한다.</li>
<li>신뢰할 수 없는 출처의 정보를 제공한다.</li>
<li>대화의 맥락을 잘 이해하지 못한다.</li>
</ul>
<p>RAG를 이용한다면 이러한 문제점들을 개선 시킬 수 있다. 
실제 프로젝트에 RAG를 도입하면서 모델의 답변 정확도와 신뢰성이 향상되었기 때문에 매우 효율적인 방법으로 자연어 처리 어플리케이션을 구현할 수 있었고, 모델을 학습시키는데에 큰 비용과 시간이 들지 않았다.</p>
<p><br><br></p>
<h2 id="rag-작동-방식">RAG 작동 방식</h2>
<p>RAG는 아래와 같은 방식으로 동작하기 때문에 RAG를 구현하기 위해선 <strong>벡터 데이터베이스</strong>, <strong>텍스트 임베딩 모델</strong>, <strong>LLM</strong> 이 세가지 구성 요소가 꼭 필요하다.</p>
<ol>
<li>사용자가 질문을 입력한다.</li>
<li>사용자의 질문을 임베딩 모델이 벡터로 변환하여 질문과 관련된 정보를 벡터 데이터 베이스에서 찾는다. 
이 벡터 데이터베이스에선 모델이 참고할 수 있는 외부 지식(웹 문서, 기업 내부 문서) 등이 담겨져 있다.</li>
<li>검색된 정보는 LLM의 입력 데이터가 되고, 사용자의 질문과 검색된 데이터를 기반으로 LLM이 답변을 생성한다.</li>
<li>생성된 답변을 사용자에게 전달한다.</li>
</ol>
<p>내가 선택한 세가지 구성요소는 아래와 같다. DB 생성 과정은 <a href="https://velog.io/@one_two_three/Milvus-DB-%EC%BB%AC%EB%A0%89%EC%85%98-%EC%83%9D%EC%84%B1-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%85%EB%A0%A5%ED%95%B4%EB%B3%B4%EA%B8%B0">이전 게시물</a>을 참고 하길 바란다.</p>
<ul>
<li>벡터 DB : <strong>Milvus</strong> </li>
<li>텍스트 임베딩 모델: <strong>text-embedding-3-small</strong> (OpenAI)</li>
<li>LLM : <strong>gpt-4o</strong> (OpenAI)</li>
</ul>
<hr>
<br>

<h2 id="rag-구현">RAG 구현</h2>
<p>이제 작동 방식을 알아보았으니 이와 같은 방식으로 RAG를 간단하게 구현해보고자 한다. 구현 테스트는 colab에서 진행 했으며 LLM 챗봇에게 어떤 특정한 장소를 추천 받는 상황으로 가정한다.
<br></p>
<h3 id="1-필요-라이브러리-및-환경-준비">1. 필요 라이브러리 및 환경 준비</h3>
<p>필요한 라이브러리를 임포트 하고 LLM이 참고할 데이터가 저장된 벡터 DB 테이블을 로드시켜 준비한다.</p>
<pre><code class="language-python">import pandas as pd
import numpy as np
import time
import os

from pymilvus import MilvusClient
from pymilvus import FieldSchema, DataType
from pymilvus import FieldSchema, CollectionSchema
from openai import OpenAI
from google.colab import userdata

#openai api 쓰기 위한 환경변수 설정
EMBEDDINGS_KEY = userdata.get(&#39;EMBEDDINGS_KEY&#39;)
os.environ[&quot;OPENAI_API_KEY&quot;] = EMBEDDINGS_KEY

openAI_api = OpenAI(
    # This is the default and can be omitted
    api_key=os.environ.get(&quot;OPENAI_API_KEY&quot;),
)


# 벡터 데이터베이스 연결
url = userdata.get(&quot;URL&quot;)
client = MilvusClient(url)


# 컬렉션(=테이블)이름 리스트
collection_list = [&#39;myStartup_travel_sites&#39;, &#39;nowlocal_travel_sites&#39;, &#39;nature_travel_sites&#39;]

# 컬렉션 로드하기
for collection_name in collection_list:
    client.load_collection(
        collection_name=collection_name,
      )
    # 로드된 상태인지 확인
      res = client.get_load_state(
        collection_name=collection_name
      )
     print(res)
</code></pre>
<br>

<p>출력 결과는 아래와 같다.</p>
<pre><code>DEBUG:pymilvus.milvus_client.milvus_client:Created new connection using: 117e983a7c4
{&#39;state&#39;: &lt;LoadState: Loaded&gt;}
{&#39;state&#39;: &lt;LoadState: Loaded&gt;}
{&#39;state&#39;: &lt;LoadState: Loaded&gt;}</code></pre><p><br><br></p>
<h3 id="2--사용자-질문-입력-후-벡터화">2.  사용자 질문 입력 후 벡터화</h3>
<pre><code class="language-python"># 질문 벡터화 함수
def embed_question(question):
    response = openAI_api.embeddings.create(
        input=question,
        model=&quot;text-embedding-3-small&quot;,
        dimensions=768
    )
    embedding = response.data[0].embedding
    return embedding


# 사용자 질문 저장
example_question = &quot;서울 카페 추천해줘&quot;

# 질문 임베딩
embedding = embed_question(example_question)</code></pre>
<p>사용자는 챗봇에게 <strong>&quot;서울 카페를 추천해줘&quot;</strong> 라고 질문한다.
embedding 변수에 사용자 질문이 768 차원의 벡터로 변환되어 저장된다.</p>
<p><br><br></p>
<h3 id="3-벡터-db-검색">3. 벡터 DB 검색</h3>
<pre><code class="language-python"># 단일 테이블 검색 함수
def search_table(table_name, embedding, top_k):
    search_params = {&quot;metric_type&quot;: &quot;IP&quot;, &quot;params&quot;: {}}
    results = client.search(
        collection_name=table_name,
        data=[embedding],
        anns_field=&quot;embedding&quot;,
        search_params=search_params,
        output_fields=[&quot;id&quot;, &quot;text&quot;],
        limit=top_k
    )
    return results

# 여러 테이블 검색 함수
def search_all_tables(embedding):
    results_myCreator = search_table(&#39;myStartup_travel_sites&#39;, embedding, top_k=6)
    results_nowLocal = search_table(&#39;nowlocal_travel_sites&#39;, embedding, top_k=5)
    results_nature = search_table(&#39;nature_travel_sites&#39;, embedding, top_k=4)

    return results_myCreator, results_nowLocal, results_nature


# 테이블 검색
results_myCreator, results_nowLocal, results_nature = search_all_tables(embedding)</code></pre>
<p>테이블 세개에 각각 쿼리를 날려 사용자 질문에 기반한 관련 정보를 가져온다. 각각의 쿼리 결과로 데이터의 기본 키 <strong>id와 text 문서를 리턴</strong>한다.
벡터 DB 검색 시 유사도 <strong>metric 유형은 IP</strong>이며 데이터 베이스에 존재한 벡터와 사용자 질문 벡터의 거리를 측정하여 가까운 거리의 데이터들을 반환한다.</p>
<p>Milvus 벡터 검색 함수들을 구체적으로 알고 싶다면 <a href="https://milvus.io/docs/single-vector-search.md">single vector search</a> 공식 문서를 참고해주길 바란다.</p>
<p><br><br></p>
<h3 id="4-검색-결과-재구성">4. 검색 결과 재구성</h3>
<pre><code class="language-python"># 쿼리 검색 결과 하나의 문자열로 수정
def format_results(results_myCreator, results_nowLocal, results_nature):

    list_of_results = [results_myCreator[0], results_nowLocal[0], results_nature[0]]
    formatted_results = &quot;&quot;

    for result in list_of_results:
      length = len(result)
      for num in range(length):
        formatted_results += result[num][&#39;entity&#39;][&#39;text&#39;] + &quot;\n&quot;

    return formatted_results


# 쿼리 결과 하나의 문자열로 묶기
formatted_results = format_results(results_myCreator, results_nowLocal, results_nature)
formatted_results</code></pre>
<p>쿼리 결과로 얻은 데이터를 하나의 텍스트로 구성하여 묶는다. 아래와 같은 형식으로 쿼리 결과가 하나의 텍스트로 만들어 진다.</p>
<pre><code>장소명: 구욱희씨 서울숲 본점
카테고리: 카페/디저트
장소 키워드: 디저트 카페, 카페
위치: 서울 성동구 성수동1가 685-271 1, 2층

장소명: 새서울
카테고리: 주류
장소 키워드: 추억여행, 바
위치: 서울 종로구 돈화문로11길 28-5

장소명: 서울앵무새
카테고리: 카페/디저트
장소 키워드: 디저트, 카페, 앵무새, 볼거리
위치: 서울 성동구 성수동1가 685-213 B1~2F

장소명: 성수동대림창고갤러리
카테고리: 카페/디저트
장소 키워드: 카페, 디저트
위치: 서울 성동구 성수동2가 322-32

....</code></pre><p><br><br></p>
<h3 id="5-llm에-참고-자료-전달-및-답변-생성">5. LLM에 참고 자료 전달 및 답변 생성</h3>
<pre><code class="language-python"># LLM 답변 생성 함수
def sendLLM(question, results):
    response = openAI_api.chat.completions.create(
        model=&quot;gpt-4o&quot;,
        messages=[
            {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;- 당신은 여행을 계획하는데 도움을 주는 챗봇 TBTI입니다. \n-당신의 역할은 사용자의 질문에 reference를 바탕으로 답변하는 것 입니다. \n- 만약 사용자의 질문이 reference와 관련이 없다면, {제가 가지고 있는 정보로는 답변할 수 없습니다.}라고만 반드시 말하세요.&quot;},
            {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;:f&quot;사용자 질문: {question} \n reference: {results}&quot; }
        ],
    )
    return response.choices[0].message.content


#LLM에 전달
LLM_response = sendLLM(example_question, formatted_results)
LLM_response</code></pre>
<p>LLM에 참고 자료를 전달해 답변을 생성했다. 출력 결과를 확인해보면 참고 자료의 정보만을 이용해 답변을 생성한 것을 볼 수 있다.</p>
<br>

<p><strong>[ 출력 결과 ]</strong></p>
<pre><code>서울에서 추천할 만한 카페는 아래와 같습니다:

1. **구욱희씨 서울숲 본점**
   - 위치: 서울 성동구 성수동1가 685-271 1, 2층
   - 키워드: 디저트 카페, 카페

2. **서울앵무새**
   - 위치: 서울 성동구 성수동1가 685-213 B1~2F
   - 키워드: 디저트, 카페, 앵무새, 볼거리

3. **성수동대림창고갤러리**
   - 위치: 서울 성동구 성수동2가 322-32
   - 키워드: 카페, 디저트

4. **LCDC SEOUL**
   - 위치: 서울 성동구 성수동2가 275-28
   - 키워드: LCDC,카페, bar, 베이커리, 문화공간

이 카페들은 성수동 일대에 위치하고 있어 접근성도 좋고 다양한 경험을 할 수 있습니다.</code></pre><br>
전달된 자료를 잘 참고해 대답시키기 위해선 **LLM 프롬프트 설계**도 중요하다.
참고 자료 이외의 거짓 데이터를 꾸며 대답하는 것을 막기 위해 아래의 형식으로 프롬프트를 구성하였다.

<ul>
<li><p><strong>System</strong> :  </p>
<ul>
<li>당신은 여행을 계획하는데 도움을 주는 챗봇입니다. </li>
<li>당신의 역할은 사용자의 질문에 reference를 바탕으로 답변하는 것 입니다. </li>
<li>만약 사용자의 질문이 reference와 관련이 없다면, 
{제가 가지고 있는 정보로는 답변할 수 없습니다.} 라고만 반드시 말하세요.</li>
</ul>
</li>
<li><p>User: </p>
<ul>
<li>사용자 질문: {사용자 질문} 
reference: {참고 자료}</li>
</ul>
</li>
</ul>
<br>

<p>이렇게 간단하게 RAG를 구현하여 LLM의 환각 증세를 줄이고 원하는 답변 결과를 얻을 수 있었다. 만약 챗봇의 말투를 다른 방식으로 변화 시키고 싶다면 사용 언어 모델의 파라미터나 파인 튜닝 기법으로 변화 시킬 수 있을 것이다.</p>
<p>더 유연한 챗봇 개발을 위해 langchain 프레임 워크를 활용하여 간편하고 빠른 어플리케이션을 구현해볼 수 있다.</p>
<p><br><br></p>
<blockquote>
<p>[ 참고 자료 ]
<a href="https://aws.amazon.com/ko/what-is/retrieval-augmented-generation/">https://aws.amazon.com/ko/what-is/retrieval-augmented-generation/</a>
<a href="https://www.ncloud-forums.com/topic/277/">https://www.ncloud-forums.com/topic/277/</a>
<a href="https://modulabs.co.kr/blog/retrieval-augmented-generation/">https://modulabs.co.kr/blog/retrieval-augmented-generation/</a>
<a href="https://milvus.io/docs/build-rag-with-milvus.md">https://milvus.io/docs/build-rag-with-milvus.md</a>
<a href="https://velog.io/@pearl1058/Fine-tuning-%ED%8C%81%EB%93%A4AWSKRUG-%ED%9B%84%EA%B8%B0">https://velog.io/@pearl1058/Fine-tuning-%ED%8C%81%EB%93%A4AWSKRUG-%ED%9B%84%EA%B8%B0</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Milvus DB 컬렉션에 데이터 입력해보기]]></title>
            <link>https://velog.io/@one_two_three/Milvus-DB-%EC%BB%AC%EB%A0%89%EC%85%98%EC%97%90-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%85%EB%A0%A5%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@one_two_three/Milvus-DB-%EC%BB%AC%EB%A0%89%EC%85%98%EC%97%90-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%85%EB%A0%A5%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 30 Jul 2024 16:17:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Rag 시스템을 만들기 위한 테스트 과정에서의 기록입니다.</p>
</blockquote>
<p>이전 게시물에서 Milvus Lite를 이용해 간단하게 데이터베이스를 구축하고 컬렉션을 생성했다. 이번에는 생성한 컬렉션에 데이터를 입력해보는 과정을 알아보겠다.</p>
<p>Milvus Lite가 무엇인지 알고싶다면 이전 게시물이나 공식 문서를 참고해주길 바란다.</p>
<blockquote>
<p><a href="https://milvus.io/docs/milvus_lite.md">Run Milvus Lite</a></p>
</blockquote>
<p></br><br></p>
<h2 id="1-db-연결-및-컬렉션-로드">1. DB 연결 및 컬렉션 로드</h2>
<pre><code class="language-python"># Milvus Lite DB 연결
from pymilvus import MilvusClient

client = MilvusClient(&quot;milvus_demo.db&quot;)

출력) 
DEBUG:pymilvus.milvus_client.milvus_client:Created new connection using: 117e983a7c4f4e0eb80454971bbde4a2</code></pre>
<p>Milvus Lite DB 데이터베이스 파일명을 이용해 데이터베이스에 연결한다.
</br><br></p>
<pre><code class="language-python"># 만들어진 컬렉션 확인
client.list_collections()

출력)
[&#39;myStartup_travel_sites&#39;, &#39;nature_travel_sites&#39;, &#39;nowlocal_travel_sites&#39;]</code></pre>
<p>만들어진 컬렉션 리스트를 확인해서 컬렉션  이름을 확인한다.</p>
<p></br><br></p>
<pre><code class="language-python"># 컬렉션 이름과 가져올 데이터 파일명 저장
data_list = {
    # key   : 컬렉션 이름 
    # value : 가져올 데이터 파일이름
    &#39;myStartup_travel_sites&#39; : &#39;data_myStartup_time.xlsx&#39;,
    &#39;nowlocal_travel_sites&#39; : &#39;data_nowlocal_time.xlsx&#39;,
    &#39;nature_travel_sites&#39; : &#39;data_natural_attractions.xlsx&#39;
}


# 컬렉션 로드하기
for collection_name in data_list.keys():
    client.load_collection(
      collection_name=collection_name,
  )

</code></pre>
<p><code>MilvusClient.load_collection()</code> 전달인자에 컬렉션 이름을 넣고 컬렉션을 로드한다. 로드 이후에 각 컬렉션에 저장될 데이터를 가져오기 위하여 컬렉션 이름과 데이터 파일명을 저장해두었다.</p>
<p></br><br></p>
<pre><code class="language-python"># 컬렉션 로드 된지 확인
for collection_name in data_list.keys():
  res = client.get_load_state(
    collection_name=collection_name
  )
  print(res)

출력)
{&#39;state&#39;: &lt;LoadState: Loaded&gt;}
{&#39;state&#39;: &lt;LoadState: Loaded&gt;}
{&#39;state&#39;: &lt;LoadState: Loaded&gt;}</code></pre>
<p>마지막으로 컬렉션이 로드 되었는지 <code>MilvusClient.get_load_state()</code>를 이용해 확인한다. </p>
<p></br><br></br><br></p>
<h2 id="2-데이터-생성-후-입력">2. 데이터 생성 후 입력</h2>
<h3 id="--생성할-입력-데이터-예시">- 생성할 입력 데이터 예시</h3>
<pre><code># 입력 데이터 예시
data=[
    {&quot;text&quot;: &#39;장소명: 경복궁\n카테고리: 자연명소 ... &#39;, &quot;embedding&quot;: [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]},
    {&quot;text&quot;: &#39;장소명: 남산타워\n카테고리: 자연명소 ... &#39;, &quot;embedding&quot;: [0.19886812562848388, 0.06023560599112088, 0.6976963061752597, 0.2614474506242501, 0.838729485096104]},
    {&quot;text&quot;: &#39;장소명: 남산타워\n카테고리: 자연명소 ... &#39;, &quot;embedding&quot;: [0.43742130801983836, -0.5597502546264526, 0.6457887650909682, 0.7894058910881185, 0.20785793220625592]} 
]</code></pre><p>먼저 입력 데이터의 예시를 보도록 하겠다. 
Milvus DB에서는 입력할 데이터를 딕셔너리 목록으로 구성하고, 각 딕셔너리는 엔티티를 의미한다. </p>
<p>딕셔너리 구성을 보면 <strong>text</strong>와 <strong>embedding</strong> 키만 존재하는 데, <strong>id</strong>는 스키마 생성할 때 <strong>automatic id</strong>로 설정했기 때문에 데이터베이스가 알아서 id를 지정하고 데이터에 추가해준다. 
( automatic id 가 아니라면 id도 딕셔너리에 포함하여 설정해줘야 한다. )</p>
</br>

<h4 id="-필드값-설명-">[ 필드값 설명 ]</h4>
<ul>
<li>text : 장소의 정보(장소명, 위치, 카테고리, 장소키워드)가 담긴 문서<pre><code>  text 예시)
  장소명: 오일차
  카테고리: 카페/키워드
  장소 키워드: 추억여행, 디저트
  위치: 서울 성동구 성수동1가 680-219 1층 ...</code></pre></li>
<li>embedding : 위의 문서를 벡터화 시킨 데이터</li>
</ul>
<p></br><br></p>
<hr>
<h3 id="--데이터-생성-과정">- 데이터 생성 과정</h3>
<br>

<pre><code class="language-python">import os

#openai api 쓰기 위한 환경변수 설정
os.environ[&quot;OPENAI_API_KEY&quot;] = EMBEDDINGS_KEY

openAI_api = OpenAI(
    # This is the default and can be omitted
    api_key=os.environ.get(&quot;OPENAI_API_KEY&quot;),
)

# 컬렉션 이름과 가져올 데이터 파일명 저장
data_list = {
    # key   : 컬렉션 이름 
    # value : 가져올 데이터 파일이름
    &#39;myStartup_travel_sites&#39; : &#39;data_myStartup_time.xlsx&#39;,
    &#39;nowlocal_travel_sites&#39; : &#39;data_nowlocal_time.xlsx&#39;,
    &#39;nature_travel_sites&#39; : &#39;data_natural_attractions.xlsx&#39;
}

# 임베딩 함수
def embed_string(string):
    response = openAI_api.embeddings.create(
        input=string,
        model=&quot;text-embedding-3-small&quot;,
        dimensions=768
    )
    embedding = response.data[0].embedding
    return embedding

# 문서 생성 함수
def make_dataset(row):
    string_data = f&quot;&quot;&quot;장소명: {row[&#39;상호명&#39;]}
카테고리: {row[&#39;카테고리&#39;]}
장소 키워드: {row[&#39;키워드&#39;]}
위치: {row[&#39;주소2&#39;]}
&quot;&quot;&quot;

    vector = embed_string(string_data)

    return {&#39;text&#39;: string_data, &#39;embedding&#39;:vector}


for collection_name, file_name in data_list.items():
  # 엑셀 파일 읽어오기
  original_df = pd.read_excel(file_name, engine=&#39;openpyxl&#39;)

  # 필요없는 열 버리기
  columns_to_drop = [&#39;설명&#39;,&#39;평점&#39;,&#39;운영정보&#39;]
  original_df = original_df.drop(columns=columns_to_drop)

  # 원본 파일 복사
  copy_df = original_df.copy()
  #copy_df = copy_df.head(2)

  dataset = copy_df.apply(make_dataset, axis=1).tolist()

  client.insert(
      collection_name=collection_name,
      data=dataset
  )</code></pre>
<p>위의 코드는 데이터 생성 후 입력까지의 전체 코드이며 임베딩 함수는 openAI의 <strong>text-embedding-3-small</strong> 모델을 이용했다.</p>
</br>

<ol>
<li><p>입력할 데이터들을 엑셀 파일에서 읽어온 후 필요없는 열을 없앤다.
아래와 같은 형태로 데이터가 남게 된다.
<img src="https://velog.velcdn.com/images/one_two_three/post/55d11be1-eadd-4841-a7c8-f3eafd7b4225/image.png" alt=""></p>
</li>
<li><p>각 행을 기준으로 데이터 프레임을 <code>make_dataset</code> 함수에 적용한다.</p>
</li>
<li><p><code>make_dataset</code> 함수에서 데이터 프레임 각 행의 값들을 이용해 문자열을 만들어 <strong>text</strong> 키에 저장한다. 그 후에, 만든 문자열을 벡터화 시켜 <strong>embedding</strong> 키에 값을 집어 넣는다.</p>
</li>
<li><p><code>make_dataset</code> 함수에서 리턴된 딕셔너리 데이터를 모두 리스트에 포함시켜 입력 데이터를 완성한다.</p>
</li>
<li><p><code>MilvusClient.insert()</code> 이 함수에 <strong>컬렉션 이름</strong>과 <strong>데이터셋</strong>을 지정하여 입력을 수행한다.</p>
</li>
</ol>
<p><br><br><br></p>
<h2 id="3-입력된-데이터-개수-확인">3. 입력된 데이터 개수 확인</h2>
<pre><code class="language-py"># 컬렉션 삽입 데이터 개수 확인
for collection_name, file_name in data_list.items():

  print(f&#39;-- {collection_name} --&#39;)
  res = client.query(
      collection_name=collection_name,
      filter=&quot;&quot;,
      output_fields=[&quot;count(*)&quot;]
  )
  print(f&#39;삽입 데이터 개수: {res}&#39;)
  print()



출력)
-- myStartup_travel_sites --
삽입 데이터 개수: data: [&quot;{&#39;count(*)&#39;: 244}&quot;] , extra_info: {&#39;cost&#39;: 0}

-- nowlocal_travel_sites --
삽입 데이터 개수: data: [&quot;{&#39;count(*)&#39;: 154}&quot;] , extra_info: {&#39;cost&#39;: 0}

-- nature_travel_sites --
삽입 데이터 개수: data: [&quot;{&#39;count(*)&#39;: 68}&quot;] , extra_info: {&#39;cost&#39;: 0}</code></pre>
<p><code>MilvusClient.query()</code> 를 이용해 다양한 매개변수 설정으로 입력된 데이터 정보를 확인해 볼 수 있다. 입력하고자 한 데이터의 개수와 입력된 데이터 개수가 같다는 것을 확인해 데이터가 올바르게 잘 들어간 것을 볼 수 있었다.</p>
<pre><code>query(
    collection_name: str,
    filter: str,
    output_fields: Optional[List[str]] = None,
    timeout: Optional[float] = None,
    partition_names: Optional[List[str]] = None,
    **kwargs,
) -&gt; List[dict]</code></pre></br>

<p>Milvus 데이터 검색의 자세한 정보는 공식 문서를 참고해주길 바란다.</p>
<blockquote>
<p><a href="https://milvus.io/api-reference/pymilvus/v2.4.x/MilvusClient/Vector/query.md">Query</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Milvus DB 컬렉션 생성하기]]></title>
            <link>https://velog.io/@one_two_three/Milvus-DB-%EC%BB%AC%EB%A0%89%EC%85%98-%EC%83%9D%EC%84%B1-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%85%EB%A0%A5%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@one_two_three/Milvus-DB-%EC%BB%AC%EB%A0%89%EC%85%98-%EC%83%9D%EC%84%B1-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%85%EB%A0%A5%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 28 Jul 2024 16:15:17 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Rag 시스템을 만들기 위한 테스트 과정에서의 기록입니다.</p>
</blockquote>
<p>Milvus Lite를 이용해 간단하게 데이터베이스를 구축해보고 테이블 생성을 진행해본다.</p>
<p><br><br></p>
<hr>
<h2 id="milvus-lite란">Milvus Lite란?</h2>
<p><strong>벡터 임베딩</strong>과 <strong>유사도 검색 기능</strong>을 갖춘 오픈 소스 데이터베이스 <strong>Milvus의 경량 버전</strong>이다. 
<strong>Milvus Lite</strong>는 Milvus의 핵심 벡터 검색 기능을 제공하고, 대부분의 기능을 포함한다. 따라서, 백만개 미만의 벡터에 대한 빠른 데모를 실행하거나 프로토타입을 빌드하는데 좋다.</p>
<p>Milvus Lite 구동의 자세한 내용은 아래 공식 문서를 참고하시길 바란다.</p>
<blockquote>
<p><a href="https://milvus.io/docs/milvus_lite.md">Run Milvus Lite</a></p>
</blockquote>
</br>

<h2 id="입력-데이터-속성">입력 데이터 속성</h2>
<ol>
<li>장소 ID (기본 키)</li>
<li>text : 장소의 정보(장소명, 위치, 카테고리, 장소키워드)가 담긴 문서<pre><code> text 예시)
 장소명: 오일차
 카테고리: 카페/키워드
 장소 키워드: 추억여행, 디저트
 위치: 서울 성동구 성수동1가 680-219 1층</code></pre></li>
<li>embedding vector : 위의 문서를 벡터화 시킨 데이터</br>

</li>
</ol>
<hr>
<h2 id="1-milvus-설치-및-모듈-임포트">1. Milvus 설치 및 모듈 임포트</h2>
<pre><code class="language-python"># pymilvus 설치
!pip install -U pymilvus

# 모듈 임포트
import pandas as pd
import numpy as np
import time
import os

from openai import OpenAI
from pymilvus import MilvusClient
from pymilvus import FieldSchema, DataType
from pymilvus import FieldSchema, CollectionSchema</code></pre>
<ul>
<li>구글 코랩에서 Milvus의 Python SDK 라이브러리인 pymilvus를 통해 milvus를 사용하였다.</li>
<li>Milvus Lite는 pymilvus와 함께 패키징 되어있기 때문에 설치를 진행해줘야 한다.</li>
</ul>
<p></br></br></p>
<h2 id="2-벡터-데이터베이스-설정">2. 벡터 데이터베이스 설정</h2>
<pre><code class="language-python">client = MilvusClient(&quot;milvus_demo.db&quot;)

출력) 
DEBUG:pymilvus.milvus_client.milvus_client:Created new connection using: 117e983a7c4f4e0eb</code></pre>
<ul>
<li><code>MilvusClient</code> 클래스를 통해 로컬 Milvus Lite 데이터베이스 생성을 해준다.</li>
<li>*&quot;Milvus_demo.db&quot;** 라는 이름으로 모든 데이터를 저장할 데이터베이스 파일이 생성된다.</li>
</ul>
<p></br></br></p>
<h2 id="3-컬렉션-생성">3. 컬렉션 생성</h2>
<pre><code class="language-python"># 인덱스, 벡터 차원, 유사도 메트릭 설정
INDEX_TYPE = &quot;FLAT&quot;
DIMENSION = 768
METRIC_TYPE = &quot;IP&quot;

# 컬렉션 생성하는 클래스
class MakeCollections:
  def __init__(self, client, index_type, metric_type, dimension):
    self.client = client
    self.index_type = index_type
    self.metric_type = metric_type
    self.dimension = dimension


  # 스키마 생성
  def create_schema(self):
    fields = [
      FieldSchema(name=&quot;id&quot;, dtype=DataType.INT64, is_primary=True, auto_id=True),
      FieldSchema(name=&quot;text&quot;, dtype=DataType.VARCHAR, max_length=500, description=&quot;elements of travel sites&quot;),
      FieldSchema(name=&quot;embedding&quot;, dtype=DataType.FLOAT_VECTOR, dim=self.dimension, description=&quot;vector&quot;)
    ]
    schema = CollectionSchema(fields=fields, auto_id=True, description=&quot;travel sites&quot;)
    return schema

  # 인덱스 생성
  def create_index(self):
    index_params = self.client.prepare_index_params()

    index_params.add_index(
      field_name=&quot;embedding&quot;,
      index_type=self.index_type,
      metric_type=self.metric_type
    )
    return index_params

  # 컬렉션 생성
  def create_collection(self, collection_name):
    self.client.create_collection(
      collection_name=collection_name,
      schema=self.create_schema(),
      index_params=self.create_index()
    )

    time.sleep(2)

    res = self.client.get_load_state(
      collection_name=collection_name
    )
    print(res)

    return self.client



collection = MakeCollections(client, INDEX_TYPE, METRIC_TYPE, DIMENSION)
kstartup_collection = collection.create_collection(&quot;myStartup_travel_sites&quot;)
nowlocal_collection = collection.create_collection(&quot;nowlocal_travel_sites&quot;)
nature_collection = collection.create_collection(&quot;nature_travel_sites&quot;)</code></pre>
<ul>
<li>아래는 컬렉션을 생성하는 전체 코드이며 스키마와 인덱스 정보가 같은 3개의 컬렉션을 생성하였다. </li>
<li>컬렉션은 기존 데이터베이스에서 테이블과 같은 의미이고 컬렉션을 만들 때 저장되는 <strong>벡터의 차원</strong>, <strong>인덱스 유형</strong>, <strong>유사도 메트릭 (similarity metric) 유형</strong>을 지정할 수 있다.</li>
<li>컬렉션을 생성하기 전 스키마와 인덱스 매개변수를 설정하고 지정한 설정 정보를 이용하여 컬렉션을 생성한다.</li>
</ul>
</br>
클래스 속 함수를 자세히 살펴보겠다.
</br></br></br>

<h3 id="3-1-스키마-생성-메서드">3-1. 스키마 생성 메서드</h3>
<pre><code class="language-python">  # 스키마 생성
  def create_schema(self):
    fields = [
      FieldSchema(name=&quot;id&quot;, dtype=DataType.INT64, is_primary=True, auto_id=True),
      FieldSchema(name=&quot;text&quot;, dtype=DataType.VARCHAR, max_length=500, description=&quot;elements of travel sites&quot;),
      FieldSchema(name=&quot;embedding&quot;, dtype=DataType.FLOAT_VECTOR, dim=self.dimension, description=&quot;vector&quot;)
    ]
    schema = CollectionSchema(fields=fields, auto_id=True, description=&quot;travel sites&quot;)
    return schema</code></pre>
<ul>
<li>생성 필드: <code>id</code>, <code>text</code>, <code>embedding</code></li>
<li><strong>automatic id</strong>를 사용하여 id값이 자동으로 지정되고 증가한다.</li>
<li><strong>동적 필드</strong>는 사용하지 않으며 데이터 입력 시 지정되지 않은 새로운 필드의 추가되는 것을 막는다.</li>
</ul>
<p>스키마는 컬렉션의 구조를 정의하며 데이터 속성과 유형, 기본 키, 동적 필드 사용여부 등 지정할 수 있다. 스키마를 생성하는 방식은 다양하기 때문에 자세한 내용은 아래의 문서를 참고하길 바란다.</p>
<blockquote>
<p><a href="https://milvus.io/docs/schema.md">Manage Schema</a></p>
</blockquote>
<p></br></br></p>
<h3 id="3-2-인덱스-파라미터-설정-메서드">3-2. 인덱스 파라미터 설정 메서드</h3>
<pre><code class="language-python">  # 인덱스 생성
  def create_index(self):
    index_params = self.client.prepare_index_params()

    index_params.add_index(
      field_name=&quot;embedding&quot;,
      index_type=self.index_type,
      metric_type=self.metric_type
    )
    return index_params</code></pre>
<ul>
<li>인덱스 선정 필드: <code>embedding</code><ul>
<li>index type : <strong>FLAT</strong>
컬렉션의 데이터셋의 규모가 크지않으며 검색 결과의 높은 정확도를  위해 지정</li>
<li>metric type : <strong>IP</strong>
정규화 되지않은 벡터 데이터이고 IP는 주로 자연어 처리 분야에서 텍스트 유사성 검색에 자주 사용됨</li>
</ul>
</li>
</ul>
<p>인덱스와 메트릭 유형을 지정하여 효율적으로 데이터를 정렬하고 필요한 데이터를 빠르게 검색할 수 있도록 한다. </p>
<p>Milvus에서는 FLAT, IVF_FLAT, IVF_SQ8, IVF_PQ 등 다양한 인덱스 유형을 제공하며 구체적인 인덱스 알고리즘과 메트릭에 대해선 다른 블로그나 공식 문서를 참고하길 바란다.</p>
<blockquote>
<p><a href="https://milvus.io/docs/index.md?tab=floating">In-memory Index</a>
<a href="https://milvus.io/docs/metric.md?tab=floating">Similarity Metrics</a></p>
</blockquote>
<p></br></br></p>
<h3 id="3-3-컬렉션-생성-메서드">3-3. 컬렉션 생성 메서드</h3>
<pre><code class="language-python">  # 컬렉션 생성
  def create_collection(self, collection_name):
    self.client.create_collection(
      collection_name=collection_name,
      schema=self.create_schema(),
      index_params=self.create_index()
    )

    time.sleep(2)

    res = self.client.get_load_state(
      collection_name=collection_name
    )
    print(res)

    return self.client</code></pre>
<ul>
<li><code>MilvusClient.create_collection()</code> 함수를 통해 생성할 컬렉션 이름을 지정하고 위에 단계에서 만든 스키마와 인덱스 파라미터를 전달해준다.</li>
<li><code>MilvusClient.get_load_state()</code> 함수를 통해 컬렉션이 생성 후 로드되었는지 확인할 수 있다.</li>
<li>위의 함수 호출을 통한 컬렉션 생성 후 출력값 예시</li>
</ul>
<pre><code>출력)

DEBUG:pymilvus.milvus_client.milvus_client:Successfully created collection: nowlocal_travel_sites
DEBUG:pymilvus.milvus_client.milvus_client:Successfully created an index on collection: nowlocal_travel_sites
{&#39;state&#39;: &lt;LoadState: Loaded&gt;}
</code></pre><p></br><br><br></p>
<h2 id="4-생성한-컬렉션-정보-확인">4. 생성한 컬렉션 정보 확인</h2>
<h3 id="4-1-구체적인-정보-확인하기">4-1. 구체적인 정보 확인하기</h3>
<p><code>MilvusClient.describte_collection()</code>에 컬렉션 이름을 지정하면 해당 컬렉션의 필드 정보 및 설정 등 구체적인 정보를 확인할 수 있다.</p>
<pre><code># 만든 컬렉션 확인
res = client.describe_collection(
    collection_name=&quot;nowlocal_travel_sites&quot;
)
res


출력)
{&#39;collection_name&#39;: &#39;nowlocal_travel_sites&#39;,
 &#39;auto_id&#39;: True,
 &#39;num_shards&#39;: 0,
 &#39;description&#39;: &#39;travel sites&#39;,
 &#39;fields&#39;: [{&#39;field_id&#39;: 100,
   &#39;name&#39;: &#39;id&#39;,
   &#39;description&#39;: &#39;&#39;,
   &#39;type&#39;: &lt;DataType.INT64: 5&gt;,
   &#39;params&#39;: {},
   &#39;auto_id&#39;: True,
   &#39;is_primary&#39;: True},
  {&#39;field_id&#39;: 101,
   &#39;name&#39;: &#39;text&#39;,
   &#39;description&#39;: &#39;elements of travel sites&#39;,
   &#39;type&#39;: &lt;DataType.VARCHAR: 21&gt;,
   &#39;params&#39;: {&#39;max_length&#39;: 500}},
  {&#39;field_id&#39;: 102,
   &#39;name&#39;: &#39;embedding&#39;,
   &#39;description&#39;: &#39;vector&#39;,
   &#39;type&#39;: &lt;DataType.FLOAT_VECTOR: 101&gt;,
   &#39;params&#39;: {&#39;dim&#39;: 768}}],
 &#39;aliases&#39;: [],
 &#39;collection_id&#39;: 0,
 &#39;consistency_level&#39;: 0,
 &#39;properties&#39;: {},
 &#39;num_partitions&#39;: 0,
 &#39;enable_dynamic_field&#39;: False}
</code></pre><p></br></br></p>
<h3 id="4-2-생성된-컬렉션-리스트-확인하기">4-2. 생성된 컬렉션 리스트 확인하기</h3>
<p><code>MilvusClient.list_collections()</code>를 통해 만들어진 컬렉션들을 확인할 수 있다.</p>
<pre><code>#만들어진 컬렉션 리스트 확인
client.list_collections()

출력)
[&#39;myStartup_travel_sites&#39;, &#39;nature_travel_sites&#39;, &#39;nowlocal_travel_sites&#39;]</code></pre><br>


<p>이제 다음 게시물에서 생성된 컬렉션에 맞는 데이터를 입력해 보는 과정을 진행해 보겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MIMIC-IV Data] 약물에 포함된 영양 성분 조사 ]]></title>
            <link>https://velog.io/@one_two_three/%EC%95%BD%EB%AC%BC%EC%97%90-%ED%8F%AC%ED%95%A8%EB%90%9C-%EC%98%81%EC%96%91-%EC%84%B1%EB%B6%84-%EC%A1%B0%EC%82%AC</link>
            <guid>https://velog.io/@one_two_three/%EC%95%BD%EB%AC%BC%EC%97%90-%ED%8F%AC%ED%95%A8%EB%90%9C-%EC%98%81%EC%96%91-%EC%84%B1%EB%B6%84-%EC%A1%B0%EC%82%AC</guid>
            <pubDate>Sat, 20 Jul 2024 16:11:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>교내 데이터베이스 수업에서 진행한 프로젝트를 기록한 게시물 입니다.</p>
</blockquote>
<p>외부 데이터를 활용하여 약물의 성분을 검색하고 해당 성분에 대한 효과를 조사했다. 이 부분은 팀원들이 맡았던 부분으로 조사 과정과 결과를 설명하고자 간략하게 기록한다.</p>
<p></br></br></p>
<h2 id="약물의-ndc-코드-조회">약물의 ndc 코드 조회</h2>
<p>약물에 포함된 구체적인 성분을 알아내기 위해 미국 식품의약국(FDA)가 배정한 국가 약물 코드(NDC)를 조회한다. 이후 FDA api를 이용하여 약물의 성분을 구체적으로 조사할 수 있다.
활용 테이블 :  hops 모듈의 prescriptions </p>
<blockquote>
<p><a href="https://mimic.mit.edu/docs/iv/modules/hosp/prescriptions/">prescriptions 테이블 공식 문서</a></p>
</blockquote>
<pre><code class="language-python">&lt; 팀원 코드 첨부 &gt;

import pymysql
import pandas as pd
import requests

# 데이터베이스 접속 정보 설정
connection = pymysql.connect(
    host=&#39;호스트&#39;,
    user=&#39;유저이름&#39;,
    password=&#39;비밀번호&#39;,
    db=&#39;데이터베이스&#39;
)

try:
    with connection.cursor() as cursor:
        # sql_mode 설정 변경
        sql = &quot;SET sql_mode = &#39;&#39;;&quot;
        cursor.execute(sql)

        # 약물을 그룹화하고 각 약물의 NDC 코드를 조회하는 쿼리
        query = &quot;&quot;&quot;
        SELECT
            p.drug,
            p.ndc
        FROM
            mimic4_fri_7.hosp_prescriptions p
        GROUP BY
            p.drug, p.ndc

        &quot;&quot;&quot;

        cursor.execute(query)
        result = cursor.fetchall()
        df = pd.DataFrame(result, columns=[&#39;drug&#39;, &#39;ndc&#39;])</code></pre>
<ul>
<li>prescriptions 테이블 내의 약물과 NDC를 그룹화하고 각 약물의 NDC 코드를 조회한다.</li>
</ul>
<p></br></br></p>
<h2 id="fda-drug-api-활용한-약물-성분-검색">FDA Drug API 활용한 약물 성분 검색</h2>
<p>openFDA는 <strong>공공 FDA 데이터</strong>를 제공하는 API로, <strong>openFDA drug NDC Directory</strong>를 이용해 NDC 코드가 포함된 약품 정보들을 검색할 수 있다.
위의 단계에서 알아낸 각 약물의 NDC 코드를 이용하여 약물 성분 정보를 검색했다.
<img src="https://velog.velcdn.com/images/one_two_three/post/da68a5cb-c446-41da-838d-2e68eba08ef0/image.png" alt=""></p>
<p>Drug API는 웹 브라우저를 통하여 호출할 수 있으며 자세한 내용은 공식 문서를 확인하길 바란다.</p>
<blockquote>
<p><a href="https://open.fda.gov/apis/drug/ndc/how-to-use-the-endpoint/">https://open.fda.gov/apis/drug/ndc/how-to-use-the-endpoint/</a></p>
</blockquote>
<p></br></br></p>
<h2 id="검색-결과">검색 결과</h2>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/f06b77e4-bc2e-4959-9014-759231b7aa91/image.png" alt=""></p>
<p>자주 쓰이는 10가지 약물에 대한 성분을 조사해본 결과,
<strong>INSULIN LISPRO, SODIUM CHLORIDE, POTASSIUM CHLORIDE</strong> 등이 있었다.</p>
<p>아래의 4가지 성분들은 영양 성분 자체가 약물로 사용되며 각각의 성분이 특정 질환에 대해 어떤 치료효과와 예방효과를 가지고 있는지 연구했다.</p>
<ol>
<li>SODIUM CHLORIDE</li>
<li>POTASSIUM CHLORIDE</li>
<li>DEXTROSE MONOHYDRATE</li>
<li>MAGNESIUM SULFATE HEPTAHYDRATE</li>
</ol>
<p></br></br></p>
<h2 id="약물-성분-효과-연구">약물 성분 효과 연구</h2>
<h4 id="1-sodium-chloride-염화-나트륨">1. SODIUM CHLORIDE (염화 나트륨)</h4>
<ul>
<li><p>치료 효과 
전해질 균형 유지 
; 급성 및 만성 류마티스 심장질환, 허혈 심장질환, 폐성 심장병, 
 뇌혈관 질환 등에 도움됨</p>
</li>
<li><p>예방 효과
효과 없음 
; 상기 표시된 질환에서 예방 효과 없음</p>
<p>단, 고혈압성 질환에는 염화 나트륨 사용 제한이 권장됨</p>
</li>
</ul>
<h4 id="2-potassium-chloride-염화-칼륨">2. POTASSIUM CHLORIDE (염화 칼륨)</h4>
<ul>
<li><p>치료 효과
저칼륨혈증 예방 및 치료
; 급성 류마티스열, 만성 류마티스 심장질환, 고혈압성 질환, 
 허혈 심장질환, 뇌혈관 질환 등에 도움됨</p>
</li>
<li><p>예방 효과
혈압 관리
; 고혈압성 질환 예방에 도움됨
심혈관 건강
; 허혈심장질환, 뇌혈관질환 예방에 일정 부분 도움됨</p>
</li>
</ul>
<h4 id="3-dextrose-monohydrate-포도당-일수화물">3. DEXTROSE MONOHYDRATE (포도당 일수화물)</h4>
<ul>
<li>치료 효과
에너지 공급원
; 모든 질환에서 에너지 공급원으로 작용</li>
</ul>
<ul>
<li>예방 효과
효과 없음 
; 대부분의 질환에서 예방 효과 없음</li>
</ul>
<h4 id="4-magnesium-sulfate-heptahydrate-황산-마그네슘-7수화물">4. MAGNESIUM SULFATE HEPTAHYDRATE (황산 마그네슘 7수화물)</h4>
<ul>
<li><p>치료 효과
항염증 및 혈압 조절
; 급성 류마티스열, 만성 류마티스심장질환, 고혈압성 질환,
 허혈심장질환, 뇌혈관질환 등에서 도움됨</p>
</li>
<li><p>예방 효과
심혈관 건강 유지
; 만성 류마티스심장질환, 고혈압성 질환, 허혈심장질환, 
뇌혈관질환 등에서 예방 효과</p>
</li>
</ul>
<p></br></br></p>
<h2 id="연구-결론">연구 결론</h2>
<ul>
<li>특정 영양성분들이 질환 관리와 예방에 다양한 방식으로 기여할 수 있다는 점을 확인할 수 있다.</li>
<li>각 질병에 따라 다르게 작용하는 효과를 고려하여, 약물의 적절한 사용과 보충이 중요함을 알아볼 수 있었다.</li>
</ul>
<p></br></br></p>
<h3 id="프로젝트-후기">프로젝트 후기..</h3>
<ul>
<li>조사하는 과정을 수월하게 하기 위해선 분석을 통해 도달할 수 있는 목표치를  구체적으로 정하는 것이 좋다고 느꼈다.</li>
<li>MIMIC-IV 데이터와 데이터베이스 활용의 이해도를 높일 수 있었다.</li>
<li>다양한 데이터 분석 알고리즘과 데이터 시각화 방법을 공부해보고 싶다는 생각이 들었다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS EC2 인스턴스에서 Milvus DB 구동하기]]></title>
            <link>https://velog.io/@one_two_three/AWS-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4%EC%97%90%EC%84%9C-Milvus-DB-%EA%B5%AC%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@one_two_three/AWS-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4%EC%97%90%EC%84%9C-Milvus-DB-%EA%B5%AC%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 20 Jul 2024 13:07:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>aws와 docker 사용법에 아직 익숙치 않지만 경험을 기록하기 위한 글입니다 😶</p>
</blockquote>
</br>
프로젝트에서 RAG 기술을 사용하기 위해 Vector DB를 구축해야 한다.
선정한 DB는 Milvus이며, 오픈 소스에다가 클라우드 서비스까지 지원하며 성능도 좋다고 하기에 사용해보기로 결정했다.

<p>직접 조금 사용해보고 느낀 바는 DB 쿼리의 기능이 다양하며, docker와 kubernetes를 활용한 어플리케이션 관리와 배포의 효율성이 매우 좋다고 느꼈다.</p>
<p>Milvus의 자세한 내용은 공식 문서를 참고해보길 바라며, 
AWS cloud에 Milvus의 설치는 어떻게 이루어지고 접속하는지 바로 알아보겠다.</p>
<blockquote>
<p><a href="https://milvus.io/docs/overview.md">What is Milvus</a></p>
</blockquote>
<p></br></br></p>
<p>나는 ec2 인스턴스에 Milvus DB를 설치하고 public 서버 ip와 지정 port를 통해 DB에 외부 접속하는 방식으로 진행 했다.
그 이유는 프로젝트를 진행을 위해 클라우드로 데이터베이스를  공유해야 했고, 리눅스 서버에서의 Milvus DB의 설치는 매우 간단했기 때문이었다.
Milvus의 DB 설치 가이드도 모두 리눅스 명령어로 제공되고 있다.</p>
</br>

<h2 id="ec2-인스턴스-연결">EC2 인스턴스 연결</h2>
<p>aws ec2 인스턴스 유형은 <strong>t2.micro</strong>이고 OS image는 <strong>Amazon Linux 2023 AMI</strong>로 선택해 생성했다. 그 다음, 생성한 인스턴스 연결을 진행한다.</p>
<p>나는 Amazon Ec2 콘솔을 사용하여 연결했다.
<img src="https://velog.velcdn.com/images/one_two_three/post/aa6fd7e6-0591-48bd-9538-8149cc130d05/image.png" alt=""></p>
<p>위의 화면에서 하단의 연결 버튼을 누르면</p>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/3a3b52a0-6efc-4bca-8e94-4b747d91c198/image.png" alt=""></p>
<p>이렇게 브라우저 기반 클라이언트를 사용하여 Linux 인스턴스에 연결 할 수 있다. 이 방법 이외에도 ssh 키를 이용해서 인스턴스에 연결하는 방법이 있다. </p>
<p>다른 연결 방법은 공식 가이드 문서나 다른 블로그를 참고해주길 바란다.</p>
<blockquote>
<p><a href="https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/ec2-instance-connect-methods.html">https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/ec2-instance-connect-methods.html</a></p>
</blockquote>
<p></br></br></p>
<h2 id="docker-설치">Docker 설치</h2>
<p>docker를 통해 Milvus를 설치하고 이용할 것이기 때문에 먼저 docker를 설치해줘야 한다.</p>
<ol>
<li><p>일단 인스턴스에 접속 한 후 최신 버전으로 모든 패키지를 업데이트 한다.</p>
<pre><code> $ sudo yum update -y</code></pre></li>
<li><p>그 다음 Docker를 설치한다.</p>
<pre><code> $ sudo yum install docker -y</code></pre></li>
</ol>
<ol start="3">
<li><p>설치 후 Docker를 실행해본다.</p>
<pre><code> $ sudo service docker start</code></pre></li>
<li><p>현재 사용자를 docker 그룹에 추가한다.
Docker 그룹에 사용자를 추가하면 Docker 명령어를 실행할 때 마다 &#39;sudo&#39;를 사용할 필요가 없다.</p>
<pre><code> $ sudo usermod -aG docker ec2-user</code></pre></li>
<li><p>인스턴스 재접속 후 Docker 명령어를 실행해본다.</p>
<pre><code> $docker run hello-world</code></pre></li>
</ol>
<p>이제 docker에 Milvus를 설치할 수 있는 사전 작업이 완료되었다.</p>
<p></br></br></p>
<h2 id="milvus-설치">Milvus 설치</h2>
<p>Milvus는 docker container로서 구동되기 위해 설치 스크립트를 제공한다. 스크립트에는 아래의 내용들이 있다.
<img src="https://velog.velcdn.com/images/one_two_three/post/671a20c5-56d7-45d1-ae79-e94a3413b8b2/image.png" alt=""></p>
<blockquote>
<p><a href="https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh">https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh</a></p>
</blockquote>
</br>

<ol>
<li><p>아래의 명령어를 입력한다.</p>
<pre><code> # Download the installation script
 curl -sfL https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh -o standalone_embed.sh</code></pre><p> 위의 설치 스크립트를 다운받고 현재 위치한 디렉토리의 내용을 보면
 세개의 파일이 다운받아져 있다.</p>
<pre><code>$ ls
embedEtcd.yaml standalone_embed.sh user.yaml</code></pre></br>
</li>
<li><p>이제 docker 컨테이너를 실행시킨다.</p>
</li>
</ol>
<pre><code>    # Start the Docker container
    $ bash standalone_embed.sh start</code></pre><p>아래의 그림 처럼 컨테이너가 생성되고 실행된다.
<img src="https://velog.velcdn.com/images/one_two_three/post/9157e44f-c432-4e5f-8867-02c0a088b006/image.png" alt=""></p>
</br>

<ol start="3">
<li>실행 되고있는 컨테이너 정보를 확인한다.</li>
</ol>
<pre><code>    $ docker ps</code></pre><ul>
<li>정보를 확인해보면 milvus 이름의 docker 컨테이너가 <strong>19530 port</strong>로 실행되고 있는 것을 알 수 있다.</li>
<li>기본 Milvus의 구성을 바꾸기 위해서는 <strong>user.yaml</strong> 파일에 설정을 추가한 후 서비스를 재시작하면 된다.</li>
<li>Milvus의 데이터 볼륨은 현재 폴더의 <strong>volumes/milvus</strong>에 매핑된다.</li>
</ul>
<p></br></br></p>
<h3 id="컨테이너-중지-및-삭제">컨테이너 중지 및 삭제</h3>
<pre><code># Stop Milvus
$ bash standalone_embed.sh stop

# Delete Milvus data
$ bash standalone_embed.sh delete</code></pre><p>이제 컨테이너의 실행까지 확인 했기 때문에, AWS 인스턴스 서버의 Milvus DB를 외부 접속 하기 위해서 추가적인 설정을 진행한다.</p>
<p></br></br></p>
<h2 id="aws-인바운드-규칙-추가">aws 인바운드 규칙 추가</h2>
<p>aws 인스턴스에 접속하는 트래픽을 제어하는 인바인드 규칙을 추가하여 정해진 경로로만 접속 할 수 있도록 한다.
해당 인스턴스의 보안 그룹을 선택하여 인바운드 규칙 편집을 클릭하여 추가한다.</p>
<h4 id="예시">예시</h4>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/a4d51bf7-e7e9-4c2c-a9d4-848700d92270/image.png" alt=""></p>
<ul>
<li>Type : 사용자 지정 TCP</li>
<li>프로토콜: TCP</li>
<li>포트 범위: 19530</li>
<li>Source: 0.0.0.0/0 (모든 IP 주소 허용, 원래 보안을 위해선 특정 ip로만 접속 할 수 있도록 해야함)</li>
</ul>
<p>기본적으로 Milvus 컨테이너는 port 19530으로 설정되기 때문에 인스턴스의 port도 19530으로 지정했다.</p>
<p></br></br></p>
<h2 id="docker-포트포워딩">Docker 포트포워딩</h2>
<p>aws 인스턴스에 위의 19530 port로 접속했을 시에 내부 도커 컨테이너와 접속하기 위해선 <strong>포트 포워딩</strong>이 필요하다. 호스트의 포트를 컨테이너 포트와 연결하여 밖에서 온 통신을 컨테이너 포트로 전달하는 것이다.  </p>
<p>먼저 기존의 Milvus 컨테이너를 멈춘다.</p>
<pre><code>$ bash standalone_embed.sh stop</code></pre><p> 아래의 명령어 처럼 컨테이너를 생성하고 실행하면 되는데 standalone_embed.sh에 이 코드가 존재하기 때문에 </p>
<pre><code>$ docker run -d -p 19530:19530 milvusdb/milvus:태그명
</code></pre><p>그냥 쉘 스크립트를 지운 후, 다시 한번 실행해주었다.</p>
<pre><code>$ bash standalone_embed.sh delete
$ bash standalone_embed.sh start</code></pre><p></br></br></p>
<h2 id="milvus-서버-접속">Milvus 서버 접속</h2>
<p>포트 포워딩 후 Milvus 컨테이너를 실행하여 DB를 시작해두고 외부에서 접속해본다.</p>
<p>아래의 코드를 실행했을 때 연결된 정보를 가져다주면 접속에 성공한 것이다.</p>
<pre><code class="language-python">
from pymilvus import connections

conn = connections.connect(host=&quot;aws public ip&quot;, port=19530)
connections.get_connection_addr(alias=&quot;default&quot;)

# Output
# {&#39;address&#39;: &#39;aws public ip 주소:19530&#39;, &#39;user&#39;: &#39;&#39;}
</code></pre>
<blockquote>
<p>참조글
<a href="https://milvus.io/docs/install_standalone-docker.md">https://milvus.io/docs/install_standalone-docker.md</a>
<a href="https://chati.tistory.com/124">https://chati.tistory.com/124</a>
<a href="https://jinjinyang.tistory.com/46">https://jinjinyang.tistory.com/46</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MIMIC-IV Data] 최다 처방 약물 검색 및 질환 분류하기]]></title>
            <link>https://velog.io/@one_two_three/%EC%B5%9C%EB%8B%A4-%EC%B2%98%EB%B0%A9-%EC%95%BD%EB%AC%BC-%EA%B2%80%EC%83%89-%EB%B0%8F-%EC%95%BD%EB%AC%BC%EC%97%90-%EB%94%B0%EB%A5%B8-%EC%A7%88%ED%99%98-%EA%B2%80%EC%83%89</link>
            <guid>https://velog.io/@one_two_three/%EC%B5%9C%EB%8B%A4-%EC%B2%98%EB%B0%A9-%EC%95%BD%EB%AC%BC-%EA%B2%80%EC%83%89-%EB%B0%8F-%EC%95%BD%EB%AC%BC%EC%97%90-%EB%94%B0%EB%A5%B8-%EC%A7%88%ED%99%98-%EA%B2%80%EC%83%89</guid>
            <pubDate>Tue, 16 Jul 2024 16:27:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>교내 데이터베이스 수업에서 진행한 프로젝트를 기록한 게시물 입니다.</p>
</blockquote>
<p>지난 게시물에서 MIMIC-IV 데이터와 선정한 연구 주제에 대해서 설명했다.
연구의 순서대로 최다 처방 약물 10가지를 추출하고 해당 약물들이 처방되는 질환에는 어떠한 것이 있는지 알아보자.</p>
<p><br><br></p>
<h2 id="처방된-약물-검색">처방된 약물 검색</h2>
<p>MIMIC-IV 데이터셋에서 hosp 모듈의 prescriptions 테이블을 활용했다.
prescriptions 테이블은 처방된 약물에 대한 정보를 제공하고 약물 이름 및 국가 약물 코드(= ndc), 처방약 복용량 및 투여 경로 등이 포함된다.</p>
<blockquote>
<p>prscriptions 구체적 설명 문서
<a href="https://mimic.mit.edu/docs/iv/modules/hosp/prescriptions/">https://mimic.mit.edu/docs/iv/modules/hosp/prescriptions/</a></p>
</blockquote>
<pre><code class="language-python">&lt;팀원 코드 첨부&gt;

import pymysql
import pandas as pd

# 데이터베이스 접속 정보 설정
connection = pymysql.connect(
    host=&#39;호스트&#39;,
    user=&#39;유저이름&#39;,
    password=&#39;비밀번호&#39;,
    db=&#39;데이터베이스이름&#39;
)

try:
    with connection.cursor() as cursor:
        # SQL 쿼리 작성
        query = &quot;&quot;&quot;
        SELECT drug, COUNT(*) as count
        FROM mimic4_fri_7.hosp_prescriptions
        GROUP BY drug
        ORDER BY count DESC;
        &quot;&quot;&quot;

        # 쿼리 실행 및 결과 가져오기
        cursor.execute(query)
        result = cursor.fetchall()

        # 결과를 데이터프레임으로 변환
        df = pd.DataFrame(result, columns=[&#39;drug&#39;, &#39;count&#39;])

        # 결과 출력
        print(df)

        # 엑셀 파일로 저장
        df.to_excel(&#39;all_prescribed_drugs.xlsx&#39;, index=False)

except Exception as e:
    print(f&quot;An error occurred: {e}&quot;)
    connection.rollback()  # 오류 발생 시 롤백

finally:
    # 데이터베이스 연결 닫기
    if connection:
        connection.close()
</code></pre>
<ol>
<li>SQL 쿼리문을 보면 prescriptions 테이블에서 약물 처방 횟수를 계산한다.</li>
<li>각 약물을 기준으로 그룹화하여 처방 횟수를 정리했다.</li>
</ol>
<p><br><br></p>
<h3 id="검색-결과">검색 결과</h3>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/0000c390-f79b-496f-a9ba-d2f6b3aceea1/image.png" alt="">
결과를 살펴보니 자주 쓰이는 약물 10가지에는 
<strong>Insulin, 0.9% Sodium Chloride, Sodium Chloride 0.9% Flush</strong> 등이 존재하는 것을 확인할 수 있었다.</p>
<p><br><br><br></p>
<h2 id="약물이-처방된-질환-검색">약물이 처방된 질환 검색</h2>
<p>이제 앞서 조사한 약물 10가지가 어떤 질환에 주로 처방되는지 알아보고 이를 질병코드별로 분류하겠다.
이 부분에서는 앞 단계에서 사용했던 prescriptions 테이블과 hosp 모듈의
diagnoses_icd 테이블을 활용했다. diagnoses_icd 테이블은 치료기간 동안 환자의 모든 진단 기록들이 들어 있기 때문에 질병 코드를 얻을 수 있다.</p>
<blockquote>
<p>diagnoses_icd 구체적 설명 문서
<a href="https://mimic.mit.edu/docs/iv/modules/hosp/diagnoses_icd/">https://mimic.mit.edu/docs/iv/modules/hosp/diagnoses_icd/</a></p>
</blockquote>
<pre><code class="language-python">
# 데이터베이스 접속 정보 설정
connection = pymysql.connect(
    host=&#39;호스트&#39;,
    user=&#39;유저이름&#39;,
    password=&#39;비밀번호&#39;,
    db=&#39;데이터베이스이름&#39;
)

try:
    with connection.cursor() as cursor:
        # SQL 쿼리 작성
        query = &quot;&quot;&quot;
            WITH drugs_of_patients AS(
                SELECT subject_id, GROUP_CONCAT(drug) AS drugs
                FROM hosp_prescriptions
                GROUP BY subject_id
            )
            SELECT icd_code, drugs
            FROM hosp_diagnoses_icd A JOIN drugs_of_patients B ON A.subject_id = B.subject_id
            WHERE icd_version = 10
            GROUP BY icd_code;
        &quot;&quot;&quot;

        # 쿼리 실행 및 결과 가져오기
        cursor.execute(query)
        result = cursor.fetchall()

        # 결과를 데이터프레임으로 변환
        df = pd.DataFrame(result, columns=[&#39;icd_code&#39;, &#39;drugs&#39;])
</code></pre>
<ol>
<li>먼저 prescriptions 테이블에서 환자별로 처방된 약품의 목록을 만들어 서브 테이블을 생성한다.</li>
<li>서브 테이블과 diagnoses_icd 테이블을 동등 조인하여 각 환자들의 질병 코드에 따른 약품 목록을 추출한다.
질병 코드에는 ICD-9와 ICD-10이 있는데 ICD-10 질병 코드만 추출하였다.</li>
</ol>
<p><br><br>
ICD-10 버전의 질병 코드는 첫번째 문자인 <strong>알파벳</strong>을 통해 질병을 분류한다.</p>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/3cae8c74-b22a-48b9-bbe6-695a52922d1a/image.png" alt=""></p>
<blockquote>
<p>출처: KOICD 질병분류정보센터 <a href="https://www.koicd.kr/kcd/kcd.do">https://www.koicd.kr/kcd/kcd.do</a></p>
</blockquote>
<p><br><br></p>
<h3 id="검색-결과-1">검색 결과</h3>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/20e2c18c-dd0a-4bee-8735-730b7d31aefd/image.png" alt="">
각 환자들의 질병 코드에 따라 처방된 약품 목록을 확인할 수 있다.</p>
<p><br><br></p>
<h2 id="질병-코드-분류">질병 코드 분류</h2>
<p>위의 코드와 이어지는 나머지 코드이다.</p>
<pre><code class="language-python">        top_10_drugs = [&#39;Insulin&#39;, &#39;0.9% Sodium Chloride&#39;, &#39;Potassium Chloride&#39;, &#39;Sodium Chloride 0.9%  Flush&#39;, &#39;Furosemide&#39;, &#39;5% Dextrose&#39;, &#39;Magnesium Sulfate&#39;, &#39;Metoprolol Tartrate&#39;, &#39;Acetaminophen&#39;, &#39;HYDROmorphone&#39;]


        for drugs_name in top_10_drugs:
            print(&quot;약물 이름: &quot;, drugs_name)

            #drugs_name이 들어간 행들만 추출
            df_drugs = df[df[&#39;drugs&#39;].str.contains(drugs_name)]

            #icd_code(ICD-10)의 첫글자 알파벳을 뽑아 &#39;code_category&#39;열에 새로 저장
            df_drugs = df_drugs.copy()
            df_drugs.loc[:, &#39;code_category&#39;] = df_drugs[&#39;icd_code&#39;].str[0]

            # &#39;Z&#39;인 행 삭제
            df_drugs = df_drugs[df_drugs[&#39;code_category&#39;] != &#39;Z&#39;]

            # 상위 3개의 icd_code 종류 출력
            icd_code = df_drugs[&#39;code_category&#39;].value_counts().head(3)
            icd_code = pd.DataFrame(icd_code).reset_index()
            icd_code.columns = [&#39;top_3_icdCode&#39;, &#39;count&#39;]
            icd_code.index += 1
            print(icd_code)
            print()



except Exception as e:
    print(f&quot;An error occurred: {e}&quot;)
    connection.rollback()  # 오류 발생 시 롤백

finally:
    # 데이터베이스 연결 닫기
    if connection:
        connection.close()</code></pre>
<ol>
<li>반복문을 통해 Insulin, 0.9% Sodium Chloride, Potassium, Chloride 등 자주 쓰이는 약물이 포함된 행들을 각각 추출하여 해당 약물들이 처방된 질병코드들을 알아낸다.</li>
<li>질병코드를 분류하기 위해 icd_code의 첫번째 알파벳을 분리해 &#39;code_category&#39; 새로운 열에 저장한다.</li>
<li>첫번째 알파벳이 &#39;Z&#39;인 행은 삭제한다. Z로 시작하는 코드는 질환이 아닌 진단이나 치료에 관한 내용이라 제외했다.</li>
<li>각 약물들이 가장 많이 처방된 경우의 질병 코드를 상위 3개만 출력한다.</li>
</ol>
<p><br><br></p>
<h3 id="출력-결과">출력 결과</h3>
<pre><code>약물 이름:  Insulin
  top_3_icdCode  count
1             I     73
2             R     57
3             E     47

약물 이름:  0.9% Sodium Chloride
  top_3_icdCode  count
1             I     97
2             R     67
3             K     58

약물 이름:  Potassium Chloride
  top_3_icdCode  count
1             I     91
2             R     60
3             K     58

약물 이름:  Sodium Chloride 0.9%  Flush
  top_3_icdCode  count
1             I     97
2             R     67
3             K     58

약물 이름:  Furosemide
  top_3_icdCode  count
1             I     78
2             R     60
3             K     53

약물 이름:  5% Dextrose
  top_3_icdCode  count
1             I     92
2             R     64
3             K     56

약물 이름:  Magnesium Sulfate
  top_3_icdCode  count
1             I     98
2             R     67
3             K     58

약물 이름:  Metoprolol Tartrate
  top_3_icdCode  count
1             I     80
2             R     54
3             K     38

약물 이름:  Acetaminophen
  top_3_icdCode  count
1             I     91
2             R     64
3             K     56

약물 이름:  HYDROmorphone
  top_3_icdCode  count
1             I     63
2             R     53
3             E     48</code></pre><p>출력 결과를 확인해 봤을 때, 공통적으로 알파벳 <strong>&#39;I&#39;로 시작하는 질병 코드</strong>가 가장 많이 존재한다는 것을 확인할 수 있었다.</p>
<br>

<h3 id="분석-결과-1">분석 결과 1</h3>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/6f9b36f6-a122-4cc2-9949-2ac0e7d82cf3/image.png" alt="">
질병 코드가 I로 시작하는 경우의 질환은 
<strong>순환계통 질환(Diseases of the circulatory system, I00-I99)</strong>이다. </p>
<p>따라서 자주 쓰이는 약물 10가지는 심장 질환, 혈관 질환, 고혈압 및 저혈압 질환에 주로 처방된다는 것을 알 수 있다.</p>
<p><br><br></p>
<h4 id="추가적-조사">추가적 조사</h4>
<p>순환계통 질환 중 어떤 질환일 때 위의 약물 10가지가 많이 처방되는 건지 구체적으로 궁금해서 더 알아보았다.</p>
<pre><code class="language-python">import numpy as np

def classification_of_ICode(x):
    ranges = [
        (0, 2),
        (5, 9),
        (10, 15),
        (20, 25),
        (26, 28),
        (30, 52),
        (60, 69),
        (70, 79),
        (80, 89),
        (95, 99)
    ]

    for i, (start, end) in enumerate(ranges):
        if start &lt;= x &lt;= end:
            count_code[i] += 1

top_10_drugs = [&#39;Insulin&#39;, &#39;0.9% Sodium Chloride&#39;, &#39;Potassium Chloride&#39;, &#39;Sodium Chloride 0.9%  Flush&#39;, &#39;Furosemide&#39;, &#39;5% Dextrose&#39;, &#39;Magnesium Sulfate&#39;, &#39;Metoprolol Tartrate&#39;, &#39;Acetaminophen&#39;, &#39;HYDROmorphone&#39;]

for drugs_name in top_10_drugs:
    print(drugs_name)

    #drugs_name이 들어간 행들만 추출
    df_drugs = df[df[&#39;drugs&#39;].str.contains(drugs_name)]

    #icd_code(ICD-10)의 첫글자 알파벳을 뽑아 &#39;code_category&#39;열에 새로 저장
    df_drugs = df_drugs.copy()
    df_drugs.loc[:, &#39;code_category&#39;] = df_drugs[&#39;icd_code&#39;].str[0]

    #icd_code가 I로 시작하는 행들만 추출
    df_drugs = df_drugs[df_drugs[&#39;code_category&#39;] == &#39;I&#39;]

    # &#39;Z&#39;인 행 삭제
    df_drugs = df_drugs[df_drugs[&#39;code_category&#39;] != &#39;Z&#39;]
    #print(df_drugs)

    #icd_code 코드 숫자 추출
    I_code_df = df_drugs.copy()
    I_code_df[&#39;icd_code&#39;] = I_code_df[&#39;icd_code&#39;].str[1:3]
    I_code = I_code_df[&#39;icd_code&#39;].astype(int)

    count_code = np.zeros(10, dtype=int)

    I_code.apply(classification_of_ICode)

    print(f&#39;순환 계통 질환 10분류 개수: {count_code}&#39;)
    print(f&#39;가장 많이 처방받는 질환 : {count_code.argmax() + 1}분류 질환&#39;)
    print()</code></pre>
<ol>
<li>각 약물들이 처방된 경우의 질병 코드 중에서 icd_code가 알파벳 &#39;I&#39;로 시작하는 행들만 추출한다.</li>
<li>앞서 추출한 행들의 데이터 프레임에서 I로 시작하는 질병 코드의 숫자 부분을 추출한다. 즉, 질병 코드가 I654라면 65인 부분만 뽑아내어 기본 코드를 알아낸다. 
* I654 질병 코드는 I65.4로 표현할 수 있는데, 소수점 부분은 일반적인 질환 범주에서 더 구체적인 진단 세부사항을 나타낸다.</li>
<li>기본 코드를 추출하여 순환 계통 질환 분류 범위에 따라 데이터프레임 각 행의 코드를 분류해 개수를 센다.</li>
</ol>
<p><br><br></p>
<h3 id="분석-결과-2">분석 결과 2</h3>
<pre><code>Insulin
순환 계통 질환 10분류 개수: [ 0  3  4  9  3 26  9  4  7  8]
가장 많이 처방받는 질환 : 6분류 질환

0.9% Sodium Chloride
순환 계통 질환 10분류 개수: [ 0  4  6  9  4 34 13  6 11 10]
가장 많이 처방받는 질환 : 6분류 질환

Potassium Chloride
순환 계통 질환 10분류 개수: [ 0  3  4  9  4 30 13  7 11 10]
가장 많이 처방받는 질환 : 6분류 질환

Sodium Chloride 0.9%  Flush
순환 계통 질환 10분류 개수: [ 0  4  6  9  4 34 13  6 11 10]
가장 많이 처방받는 질환 : 6분류 질환

Furosemide
순환 계통 질환 10분류 개수: [ 0  3  5  8  3 29  9  6  8  7]
가장 많이 처방받는 질환 : 6분류 질환

5% Dextrose
순환 계통 질환 10분류 개수: [ 0  4  6  9  4 33  9  7 11  9]
가장 많이 처방받는 질환 : 6분류 질환

Magnesium Sulfate
순환 계통 질환 10분류 개수: [ 0  4  6  9  4 34 13  7 11 10]
가장 많이 처방받는 질환 : 6분류 질환

Metoprolol Tartrate
순환 계통 질환 10분류 개수: [ 0  4  6  9  2 29 11  7  7  5]
가장 많이 처방받는 질환 : 6분류 질환

Acetaminophen
순환 계통 질환 10분류 개수: [ 0  4  6  9  2 32 13  6  9 10]
가장 많이 처방받는 질환 : 6분류 질환

HYDROmorphone
순환 계통 질환 10분류 개수: [ 0  3  6  6  2 28  3  4  8  3]
가장 많이 처방받는 질환 : 6분류 질환</code></pre><p>위의 결과를 보면 각 약물들은 순환계통의 질환에서 6번째 범위에 속하는 <strong>기타 형태의 심장병</strong>으로 급성 심장막염, 비류마티스성 승모판 장애, 심근병증 등의 질병에 가장 많이 처방된 것을 알 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MIMIC-IV 데이터를 활용한 약물 분석]]></title>
            <link>https://velog.io/@one_two_three/MIMIC-IV-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%95%BD%EB%AC%BC-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@one_two_three/MIMIC-IV-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%95%BD%EB%AC%BC-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Mon, 15 Jul 2024 17:03:03 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>교내 데이터베이스 수업에서 진행한 프로젝트를 기록한 게시물 입니다.</p>
</blockquote>
<p></br></br></p>
<h2 id="mimic-iv-데이터는-무엇인가">MIMIC-IV 데이터는 무엇인가?</h2>
<p><strong>MIMIC-IV</strong>(Medical Information Mart for Intensive Care IV)는 미국 메사추세츠주 보스턴에 있는 Israel Deaconess Medical Center에 입원한 환자들의 실제 병원 체류 기록을 포함하는 관계형 데이터베이스이다. 2008년부터 2019년까지의 기간 동안 수집된 자료를 포함하고 환자의 생체 신호, 투여된 약물, 입원 및 퇴원 등 포괄적인 정보가 포함되어 있다. 따라서 총 7.2GB의 매우 큰 데이터셋으로 몇가지 과정만 거치면 무료로 사용할 수 있다.</p>
<p> <img src="https://velog.velcdn.com/images/one_two_three/post/e2783066-a3d3-4d74-951b-891b7add5d8c/image.png" alt=""></p>
<p>  <a href="https://physionet.org/content/mimiciv/2.2/">PhysioNet</a>에 들어가면, 데이터셋에 대한 설명을 볼 수 있고 제일 아래로 내려가면 *<em>Files *</em>라는 부분이 나온다. 이 부분의 설명대로 여러 조건을 맞추게 된다면 제한된 정보에 접근할 수 있다.</p>
<br>
자세한 과정은 다른 문서나 블로그를 참고하길 바란다

<blockquote>
<ul>
<li><a href="https://baeseongsu.github.io/posts/mimiciii/">https://baeseongsu.github.io/posts/mimiciii/</a></li>
</ul>
</blockquote>
<ul>
<li><a href="https://www.notion.so/laplacian/2-2-ad9d27f8044a46a4af2a9bf4754511cd">https://www.notion.so/laplacian/2-2-ad9d27f8044a46a4af2a9bf4754511cd</a></li>
</ul>
<p></br></br></br></p>
<h2 id="선정-주제">선정 주제</h2>
<p>데이터베이스에 필요한 MIMIC-IV 데이터들을 입력한 후에 이를  활용한 데이터 분석을 진행하였다.
우리팀은  <strong>최다 처방약물 10가지의 영양소 및 효능을 분석</strong>하는 것을 주제로 삼았다. </p>
<h3 id="연구-목적">연구 목적</h3>
<ol>
<li>약물의 영양 성분이 대상 질환에 어떤 효용성을 갖는지 조사</li>
<li>영양 성분을 미리 섭취하는 것으로 대상 질환을 예방할 수 있는지 연구</li>
</ol>
<br>

<h3 id="연구-순서">연구 순서</h3>
<ol>
<li>처방된 약물 리스트 검색 - 최다 처방 약물 10가지를 추출</li>
<li>약물에 따른 질환 검색 및 분류 - 앞서 추출한 약물이 처방되는 질환을 질병 코드별로 분류</li>
<li>약물에 포함된 영양 성분 조사 - 외부 데이터 활용 약물 성분 검색 및 해당 성분에 대한 효과 조사</li>
</ol>
<p>연구할 내용에 대해서는 팀원 모두가 기획했고, 나는 두번째 단계를 맡아 이 부분을 구체적으로 설명할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] Scanner와 BufferedReader의 차이점, StringTokenizer와 split의 차이점]]></title>
            <link>https://velog.io/@one_two_three/%EB%B0%B1%EC%A4%80-Scanner%EC%99%80-BufferedReader%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90-StringTokenizer%EC%99%80-split%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@one_two_three/%EB%B0%B1%EC%A4%80-Scanner%EC%99%80-BufferedReader%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90-StringTokenizer%EC%99%80-split%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Mon, 27 May 2024 01:47:20 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>백준 알고리즘을 풀며 알게된 점을 메모한 게시물입니다.</p>
</blockquote>
<p>처음 백준을 풀기 시작해보니 코드 처리 속도가 중요하다는 것을 알게 되었다.
오늘은 Scanner와 BufferedReader 차이점과 StringTokenizer와 split의 차이점에 대해 메모한다.
문제는 두 수의 입력을 받고 두 수의 차이값을 출력하는 간단한 문제이다.</p>
<hr>
<p></br></br></p>
<h3 id="문제">문제</h3>
<p>백준 문제 1001번
: 두 정수 A와 B를 입력받은 다음, A-B를 출력하는 프로그램을 작성하시오.</p>
<p><strong>입력</strong>
: 첫째 줄에 A와 B가 주어진다. (0 &lt; A, B &lt; 10)</p>
<p><strong>출력</strong>
: 첫째 줄에 A-B를 출력한다.</p>
<p></br></br></p>
<h3 id="scanner-사용했을-때">Scanner 사용했을 때</h3>
<pre><code class="language-java">import java.util.*; 

public class Main{
    public static void main(String args[]){
        Scanner sc = new Scanner(System.in);
        int a,b;
        a = sc.nextInt();
        b = sc.nextInt();
        System.out.println(a-b);
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/44640617-c56c-4476-9596-b7251e3fb2b3/image.png" alt=""></p>
<p>Scanner 클래스를 이용해서 두 수를 <code>nextInt()</code>로 간단하게 읽어왔고, 속도는 <strong>212ms</strong> 정도 걸렸다.</p>
<p></br></br></p>
<h3 id="bufferedreader-사용했을-때">BufferedReader 사용했을 때</h3>
<pre><code class="language-java">import java.io.*;

public class Main{
    public static void main(String args[]) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] s = br.readLine().split(&quot; &quot;);
        int a = Integer.parseInt(s[0]);
        int b = Integer.parseInt(s[1]);

        System.out.println(a-b);
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/d58db9ba-1709-4c51-bc5c-4166b491c97f/image.png" alt=""></p>
<p>BufferedReader를 이용해 입력한 값을 읽어왔고, 입력한 값을 분리하는 과정을 거쳤다. 이 경우 처리속도가 <strong>120ms</strong> 정도이다.</p>
<p>왜 이런 속도차이가 나는 것일까?</p>
<p></br></br></p>
<h3 id="scanner와-bufferedreader의-차이점">Scanner와 BufferedReader의 차이점</h3>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/d7e24d01-779c-4861-8e8b-a01cd55a6367/image.png" alt=""></p>
<ol>
<li><p><strong>데이터 가공</strong></p>
<ul>
<li>Scanner는 값을 정수, 문자열, 불리안 등 다양하게 읽어들일 수 있어 간편하다.</li>
<li>BufferedReader는 입력 받은 데이터가 텍스트이기 때문에 문자나 문자열로 읽어들인 후 값을 변환해야 한다.</li>
</ul>
</li>
<li><p><strong>버퍼 사용</strong></p>
<ul>
<li><p>Scanner는 키보드의 입력이 키를 누르는 즉시 전송한다.</p>
</li>
<li><p>BufferedReader는 8192 byte 크기의 버퍼에 입력 받은 값을 담아두었다가 한번에 전송한다.</p>
<p>👉 데이터를 하나하나 전달하는 것보다 한번에 모아 데이터를 전송하는 것이 성능적인 측면에서 더 효율적이다. 따라서 버퍼의 사용이 더 빠른 처리 속도를 낼 수 있다.</p>
</li>
</ul>
</li>
</ol>
<p></br></br></br></p>
<p><strong>BufferedReader 사용할 때 주의점</strong></p>
<ol>
<li>java.io 패키지를 import 해준다.</li>
<li><code>throws IOException</code>으로 예외처리를 해준거나 <code>try, catch문</code> 으로 예외처리를 해줘야한다.</li>
<li>여러 데이터를 입력 받을 때는 <code>split()</code> 함수를 사용해주거나 <code>StringTokenizer</code> 클래스를 이용한다.
위의 경우는 <code>split ()</code> 을 이용하여 문자열을 구분해주었다.</li>
</ol>
<p></br></br></br></p>
<p><strong>StringTokenizer 클래스 사용해서 문자열을 구분할 때</strong></p>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main{
    public static void main(String args[]) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        int a = Integer.parseInt(st.nextToken());
        int b = Integer.parseInt(st.nextToken());

        System.out.println(a-b);
    }
}</code></pre>
<p>StringTokenizer 클래스는 java.util 패키지에 있고, <code>split ()</code> 처럼 문자열을 분리해준다. 분리된 문자열을 <code>nextToken()</code>으로 가져올 수 있다. 분리된 모든 문자열을 확인하기 위해서는 <code>hasMoreTokens()</code> 을 이용하여 반복문을 돌면서 확인하는 방법이 있다.</p>
<p>StringTokenizer를 생성하는 방법 3가지도 알아보자.</p>
<ol>
<li><p>StringTokenizer st = new StringTokenizer(문자열);
👉 띄어쓰기를 기준으로 문자열을 분리해준다.</p>
</li>
<li><p>StringTokenizer st = new StringTokenizer(문자열, <strong>구분자</strong>);
👉 명시한 구분자로 문자열을 분리해준다.</p>
</li>
<li><p>StringTokenizer st = new StringTokenizer(문자열, 구분자, <strong>true/false</strong>);
👉 세번째 매개변수의 true/false는 구분자를 토큰에 포함할지 여부를 결정한다. true면 구분자를 토큰에 포함되고, false면 포함되지 않는다.</p>
</li>
</ol>
<p></br></br></p>
<h3 id="split과-stringtokenizer의-차이점">Split과 StringTokenizer의 차이점</h3>
<ul>
<li>StringTokenizer는 빈문자열을 토큰으로 인식하지 않지만, Split은 빈문자열을 토큰으로 인식한다.</li>
<li>StringTokenizer는 결과값이 문자열이라면 Split은 결과값이 문자열 배열이다.</li>
<li>StringTokenizer는 문자 또는 문자열로 입력값을 구분하고, split은 정규 표현식으로 구분한다. </li>
</ul>
<blockquote>
<p>참고 게시물</p>
</blockquote>
<ul>
<li><a href="https://dev-coco.tistory.com/94">https://dev-coco.tistory.com/94</a></li>
<li><a href="https://lasbe.tistory.com/48">https://lasbe.tistory.com/48</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[.gitignore 파일이 필요한 이유]]></title>
            <link>https://velog.io/@one_two_three/.gitignore-%ED%8C%8C%EC%9D%BC%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@one_two_three/.gitignore-%ED%8C%8C%EC%9D%BC%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Mon, 20 May 2024 15:21:33 GMT</pubDate>
            <description><![CDATA[<p>깃허브에 파일들을 올리다 보니까 Unity의 <code>Logs</code>, <code>UserSettings</code> 파일들은 깃허브에 굳이 업데이트 하지 않아도 될거 같다는 생각이 들었고, 이를 어떻게 해결할 수 있을까 알아보았다.</p>
<p>gitignore 파일을 이용해서 git에 올릴 필요 없는 파일들을 지정해 git에서 제외 시켜보자</p>
<hr>
<h2 id="gitignore-파일-생성">.gitignore 파일 생성</h2>
<p>이미 존재했던 프로젝트에서 .gitignore을 생성해보겠다.</p>
<ol>
<li><strong>Add file</strong>을 누르고 <strong>Create new file</strong>을 눌러준다.
<img src="https://velog.velcdn.com/images/one_two_three/post/b50e6c7d-248c-4856-99ab-85e62c744fd4/image.png" alt=""></li>
</ol>
<p></br></br></p>
<ol start="2">
<li>프로젝트 명 옆에 /(슬래시) 하고 <code>Name your file</code> 칸이 있다.
이 칸에 <code>.gitignore</code> 라고 입력한다.
<img src="https://velog.velcdn.com/images/one_two_three/post/dd158559-7f0d-421d-9d7d-91e250073acf/image.png" alt=""></li>
</ol>
<p></br></br></p>
<ol start="3">
<li><code>.gitignore</code> 라고 입력하면 .gitignore 파일의 template을 선택할 수 있다. 여기에서 나는 Unity를 선택했다.
<img src="https://velog.velcdn.com/images/one_two_three/post/7f4a1026-c6c1-46d9-989d-7935d000b008/image.png" alt=""></li>
</ol>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/137598b1-46eb-4227-a48e-770d824aac6c/image.png" alt="">
자동적으로 Unity에 대한 ignore 파일들을 설정해주었다.
적혀있는 내용 이외에 제외하고 싶은 파일이 있다면 .gitignore 파일에서 패턴에 맞게 추가 작성해주면 된다.</p>
<p></br></br></br></br></p>
<h2 id="gitignore-파일-적용">.gitignore 파일 적용</h2>
<p>.gitigore 파일을 생성 후 push 했지만 이 파일이 작동하지 않을 수 있다. 중요한 조건이 있다.</p>
<ul>
<li>.gitignore 파일은 프로젝트 최상단에 위치해야 한다.</li>
<li>Unity 프로젝트의 경우 Assets 폴더가 존재하는 곳에 .gitignore 파일이 존재해야 한다.</li>
</ul>
<p>이 조건에 잘 해당하지만, 또 작동하지 않는 경우는 
.gitignore 파일을 <strong>git 프로젝트를 생성</strong>할 때가 아닌 <strong>나중에 추가</strong>했을때 그럴 수 있다.</p>
<p>이때는 git의 캐시를 지워줘야 한다.</p>
<pre><code>git rm -r --cached .
git add. 
git commit -m &quot;커밋메세지&quot;
git push origin {브랜치명}</code></pre><p><img src="https://velog.velcdn.com/images/one_two_three/post/00cf7839-83ce-4ba9-8086-157e2b038228/image.png" alt=""></p>
<p>push 후 확인해 봤을 때 불필요한 파일들이 Git에서 제외된 것을 확인할 수 있었고, 작업하면서 프로젝트에 불필요한 파일이 다시 생성되어도 Git에 업데이트 되지 않았다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[구글 생성형AI Gemma에 대해 알아보고 한번 사용해보자]]></title>
            <link>https://velog.io/@one_two_three/%EA%B5%AC%EA%B8%80-%EC%83%9D%EC%84%B1%ED%98%95AI-Gemma%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@one_two_three/%EA%B5%AC%EA%B8%80-%EC%83%9D%EC%84%B1%ED%98%95AI-Gemma%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 16 May 2024 14:26:43 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에서 LLM을 사용해야 하고, 특정 데이터셋을 사용하여 추가적인 학습도 진행해야 한다. 
따라서 프로젝트에 맞는 생성형 AI 오픈 모델을 선정해야 하는데 구글이 새롭게 발표한 생성형 AI Gemma에 대해 알아보기로 했다.</p>
<p></br></br></p>
<hr>
<h2 id="gemma는-어떤-모델인가">Gemma는 어떤 모델인가</h2>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/e53de543-e7d2-48b1-8db8-c1cad1b8de40/image.png" alt=""></p>
<p>젬마(Gemma)는 최근 구글이 발표한 새로운 인공지능 모델로 대규모 언어 모델이다. 
Google이 작년에 발표했던 제미나이(Gemini)모델의 핵심 기술과 연구를 기반으로 제작된 최점단 경량 오픈 모델로, 모델은 매개변수가 20억개인 <strong>젬마 2B</strong>와 70억개인 <strong>젬마 7B</strong>이 존재한다. 
<img src="https://velog.velcdn.com/images/one_two_three/post/0a1745c9-465b-4bed-8d8f-41c42052a1eb/image.png" alt="">
젬마(Gemma)의 가장 큰 장점은 다른 오픈 LLM보다 주요 벤치마크에서 더 뛰어난 성능을 보여주고 있으며, 노트북, 데스크탑, 모바일 및 클라우드를 포함한 범용적인 디바이스에서 실행될 수 있어 폭넓게 이용 가능하다.</p>
<p>또한, 코랩(Colab), 캐글 노트북(Kaggle notebooks), 허깅 페이스(Hugging Face) 등 다양한 환경에서 젬마를 이용할 수 있다. 이러한 다양한 환경에서 쉽게 이용해 볼 수 있도록 가이드가 모두 존재하고, 새로운 데이터들로 파인 튜닝된 모델도 쉽게 구할 수 있다.</p>
<p>더 자세한 내용은 구글의 문서를 참고하길 바란다.</p>
<blockquote>
<ul>
<li><a href="https://blog.google/intl/ko-kr/products/explore-get-answers/-gemma-open-models-kr/">구글 코리아 블로그</a></li>
</ul>
</blockquote>
<ul>
<li><a href="https://ai.google.dev/gemma/docs?hl=ko&amp;_gl=1*1uaui5*_up*MQ..*_ga*MjExMzkzMjk1OS4xNzE1ODUzMTQz*_ga_P1DBVKWT6V*MTcxNTg1MzE0My4xLjAuMTcxNTg1MzM0OS4wLjAuMTExODA4NTY1MA..">Gemma 모델 개요</a></li>
<li><a href="https://ai.google.dev/gemma?hl=ko">구글 클라우드 젬마 웹사이트</a></li>
</ul>
<p></br></br></p>
<h2 id="gemma를-사용해보자">Gemma를 사용해보자</h2>
<p><a href="https://ai.google.dev/gemma/docs/get_started?hl=ko&amp;_gl=1*1jd7f39*_up*MQ..*_ga*ODgyNjcyMTkuMTcxNTg2NjYxOQ..*_ga_P1DBVKWT6V*MTcxNTg2NjYxOC4xLjAuMTcxNTg2NjYxOC4wLjAuNzY2NjE4MjE3">구글 젬마(Gemma) 가이드</a>를 보면서 Gemma를 사용해 보았다.
KerasNLP는 TensorFlow, Jax, PyTorch에서 기본적으로 작동하는 자연어 처리 라이브러리다. 위의 가이드에서는 KerasNLP를 사용하여 Gemma를 사용해본다.</p>
<h3 id="1-gemma-setup">1. Gemma Setup</h3>
<p>Gemma를 사용하기 전에 Kaggle을 통해 모델에 대한 엑세스를 요청해야 한다.
<a href="https://www.kaggle.com/">Kaggle</a>에 로그인 후 Gemma 모델 카드에서 Request Acess를 눌러 접근 요청을 한다.
<img src="https://velog.velcdn.com/images/one_two_three/post/6a962f80-43da-440f-bbe5-df75fbd835e9/image.png" alt=""></p>
<p></br></br></br></br></p>
<p>엑세스 요청 후 Gemma를 사용하기 위해 <strong>Kaggle 사용자 이름</strong>과 <strong>Kaggle API Key</strong>가 있어야 한다. 
<em>Kaggle 사용자 계정 👉 Settings</em> 로 이동한 후 아래의 API 메뉴에서 <strong>Create New Token</strong>을 누르게 되면, 사용자 인증 정보가 담긴 <strong>Kaggle.json</strong> 파일이 다운로드 된다.</p>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/ab3ae804-a0bf-47b9-8d85-2306ab8e851b/image.png" alt="">
Kaggle.json 파일은 다음과 같은 내용이 존재한다.</p>
<pre><code>{&quot;username&quot;:&quot;your_username&quot;,&quot;key&quot;:&quot;012345678abcdef012345678abcdef1a&quot;}</code></pre><p></br></br></br></br>
코랩의 런타임 유형도 설정해줘야 한다. 
코랩 상단메뉴에  런타임 👉 런타임 유형 변경을 선택하고, 젬마 2B를 사용하기 위해 <strong>T4 GPU *<em>하드웨어 가속기를 선택한다.
젬마 7B를 사용하기 위해선 *</em>A100 GPU</strong>같은 유료 프리미엄 GPU를 설정해줘야 한다.
<img src="https://velog.velcdn.com/images/one_two_three/post/e939db1a-3128-47bf-a2c2-abbdda030ba9/image.png" alt=""></p>
<p></br></br></br></br></p>
<h3 id="2-환경-변수-설정">2. 환경 변수 설정</h3>
<p>Gemma 설정을 완료하고 colab 환경의 환경변수를 설정한다.</p>
<pre><code>import os
from google.colab import userdata

# Note: `userdata.get` is a Colab API. If you&#39;re not using Colab, set the env
# vars as appropriate for your system.
os.environ[&quot;KAGGLE_USERNAME&quot;] = userdata.get(&#39;KAGGLE_USERNAME&#39;)
os.environ[&quot;KAGGLE_KEY&quot;] = userdata.get(&#39;KAGGLE_KEY&#39;)</code></pre><p>위의 코드를 보면 <code>userdata.get()</code> 함수를 이용해 환경 변수를 설정하는 것을 볼 수 있다. 이는 코랩에서 비밀키로 지정한 값을 가져오는 역할을 한다.</p>
<p>코랩 보안 비밀 칸에서 Kaggle.json 파일의 정보들을 입력해 <strong>Kaggle 사용자 이름</strong>과 <strong>Kaggle API Key</strong>를 추가할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/one_two_three/post/89db4c57-94bb-4b92-a250-6d0a81650ce0/image.png" alt=""></p>
<p>위의 그림을 보면 <strong>KAGGLE_USERNAME</strong> 이름으로 username 사용자 이름을 저장하고, <strong>KAGGLE_KEY</strong> 이름으로 API Key를 저장한 것을 볼 수 있다.</p>
<p></br></br></br></br></p>
<h3 id="3-필요-라이브러리-설치">3. 필요 라이브러리 설치</h3>
<p>Keras와 KerasNLP를 설치한다.</p>
<pre><code># Install Keras 3 last. See https://keras.io/getting_started/ for more details.
pip install -q -U keras-nlp
pip install -q -U keras&gt;=3</code></pre><p><img src="https://velog.velcdn.com/images/one_two_three/post/335d06fb-7b49-4e3c-a19a-cefa27151e28/image.png" alt="">
이런 식으로 설치되는 것이 보이고 밑에 빨간 오류 메시지는 무시하고 지나갔다.(keras 3.3.3을 사용하기 때문에 무시해도 된다...)</p>
<p></br></br></p>
<h3 id="4-백엔드-설정">4. 백엔드 설정</h3>
<p>Keras 3는 새로운 대규모 모델 학습과 배포기능을 제공하며 tensorflow, PyTorch, Jax 중에 하나를 설정하여 사용할 수 있다.
가장 적합한 프레임워크를 선택하고 상황에 따라 프레임워크를 전환할 수 있다.</p>
<pre><code>import os

os.environ[&quot;KERAS_BACKEND&quot;] = &quot;jax&quot;  # Or &quot;tensorflow&quot; or &quot;torch&quot;.
os.environ[&quot;XLA_PYTHON_CLIENT_MEM_FRACTION&quot;] = &quot;0.9&quot;</code></pre><p></br></br></p>
<h3 id="5-패키지와-모델-가져오기">5. 패키지와 모델 가져오기</h3>
<p>kerasNLP은 다양한 모델 아키텍처들을 제공하고 있고, from_preset( ) 메서드를 통해서 모델을 불러올 수 있다.</p>
<pre><code>import keras
import keras_nlp

gemma_lm = keras_nlp.models.GemmaCausalLM.from_preset(&quot;gemma_1.1_instruct_2b_en&quot;)</code></pre><p><img src="https://velog.velcdn.com/images/one_two_three/post/f7870b57-4b89-4f9e-a8d7-b110dd7697d2/image.png" alt="">
위의 목록 처럼 다양한 Gemma 모델 중 하나를 지정하면 모델을 가져올 수 있고, 처음에는 가이드에 따라 gemma_2b_en 모델을 사용했다.
gemma_2b_en 모델은 한국어에 대한 이해력이 좋지 않고, 만족할 만한 답변을 얻지 못해서 gemma_1.1_instruct_2b_en 모델을 가져와 사용해 보았다.</p>
<p>gemma의 다른 버전 모델로 바꿔 사용하기 위해서는 코랩의 런타임 연결을 해제 및 삭제해야 한다. gemma 모델의 용량이 크기 때문에 코랩 리소스를 초기화 시킨 후 다른 모델을 받아 올 수 있다.
<img src="https://velog.velcdn.com/images/one_two_three/post/da8c9224-6ac6-4d88-889f-fb3270ed585c/image.png" alt=""></p>
<p></br></br>
<code>summary() 메서드</code>를 통해서 모델의 자세한 정보를 확인할 수 있다.</p>
<pre><code>gemma_lm.summary()</code></pre><p><img src="https://velog.velcdn.com/images/one_two_three/post/bd43c181-0de3-4f87-91e5-84ab973f8977/image.png" alt=""></p>
<p></br></br>
</br></br></p>
<h3 id="6-텍스트-생성하기">6. 텍스트 생성하기</h3>
<p>모델에는 프롬프트를 기반으로 텍스트를 생성하는 <strong>Generate 메서드</strong>가 있다.
<strong>Generate 메서드</strong> 파라미터로 질문을 전달할 수 있고, 선택사항인 max_length 인수는 모델이 생성할 수 있는 텍스트의 최대길이를 지정할 수 있다. </p>
<pre><code>gemma_lm.generate(&quot;What is the meaning of life?&quot;, max_length=64)</code></pre><hr>
<pre><code>결과)

What is the meaning of life?

The question is one of the most important questions in the world.

It’s the question that has been asked by philosophers, theologians, and scientists for centuries.

And it’s the question that has been asked by people who are looking for answers to their own lives</code></pre><p></br></br>
한국 여행지를 추천해달라는 질문을 해보았다.
gemma_1.1_instruct_2b_en 모델은 한국어 지원이 잘 되었고, 나름 구체적인 답변을 얻을 수 있었다.</p>
<pre><code>gemma_lm.generate(&quot;한국 여행지 좀 추천해줄래?&quot;, max_length=500)</code></pre><hr>
<pre><code>결과)

한국 여행지 추천 좀 해줄래?

**목표:** 한국 여행지 추천

**요건:**

* 관광지와 자연 경관이 풍부한 지역
* 문화적 가치가 높은 지역
* 다양한 활동과 경험 제공
* 편리한 이동 환경

**추천 지역:**

**1. 경기도:**

* 경주: 경기도의 전통 문화와 전통 음식
* 평택: 한국의 최고의 수산물 중 하나인 평택 생선의 가치
* 부산: 한국의 대표적인 해안 도시

**2. 충청도:**

* 천안: 천안의 천연 풍경과 역사적 가치
* 홍천: 한국의 최고의 농경지 중 하나인 홍천 농경지
* 제주도: 한국의 제주도는 자연 풍경과 문화적 가치가 풍부한 지역

**3. 전라도:**

* 전주: 전주는 전통 문화와 전통 음식의 가치
* 강릉: 강릉은 한국의 최고의 수산물 중 하나인 강릉 생선의 가치
* 울산: 울산은 한국의 대표적인 산악 지역


**추천 활동:**

* 관광지 방문
* 자연 경관 관찰
* 수산물 구매
* 전통 음식 경험
* 활동과 경험 제공

**추천 경험:**

* 한국의 전통 무용수 경험
* 한국의 전통 음식 경험
* 한국의 수산물 구매 경험
* 한국의 자연 경관 관찰 경험</code></pre><p></br></br>
질문을 묶어서 한꺼번에 물어볼 수 있고, 그에 대한 답변도 한번에 받을 수 있다.</p>
<pre><code>gemma_lm.generate(
    [&quot;What is the meaning of life?&quot;,
     &quot;How does the brain work?&quot;],
    max_length=64)</code></pre><hr>
<pre><code>[&#39;What is the meaning of life?


The question is one of the most important questions in the world.

It’s the question that has been asked by philosophers, theologians, and scientists for centuries.

And it’s the question that has been asked by people who are looking for answers to their own lives&#39;,

&#39;How does the brain work?

The brain is the most complex organ in the human body. It is responsible for controlling all of the body’s functions, including breathing, heart rate, digestion, and more. The brain is also responsible for thinking, feeling, and making decisions.

The brain is made up&#39;]</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[통신할 때, 유니티 측에서 수신 속도를 조절해보자]]></title>
            <link>https://velog.io/@one_two_three/%ED%86%B5%EC%8B%A0%ED%95%A0-%EB%95%8C-%EC%9C%A0%EB%8B%88%ED%8B%B0-%EC%B8%A1%EC%97%90%EC%84%9C-%EC%88%98%EC%8B%A0-%EC%86%8D%EB%8F%84%EB%A5%BC-%EC%A1%B0%EC%A0%88%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@one_two_three/%ED%86%B5%EC%8B%A0%ED%95%A0-%EB%95%8C-%EC%9C%A0%EB%8B%88%ED%8B%B0-%EC%B8%A1%EC%97%90%EC%84%9C-%EC%88%98%EC%8B%A0-%EC%86%8D%EB%8F%84%EB%A5%BC-%EC%A1%B0%EC%A0%88%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Mon, 06 May 2024 15:00:13 GMT</pubDate>
            <description><![CDATA[<p>지난 게시물에서 파이썬과 유니티의 소켓 통신을 구현하여, mediaPipe로 측정한 관절 위치들을 유니티 측으로 전달할 수 있었다.
따라서 전달받은 데이터를 아바타에 실시간 적용하여 사용자의 동작을 아바타가 트래킹 할 수 있는지 확인해보았다.
</br></br></br></p>
<h3 id="발생한-문제">발생한 문제</h3>
<p>유니티에서 수신한 데이터를 아바타에 적용했을 때, 전달받은 데이터대로 사용자의 동작을 따라하지만 렉이 걸린듯이 움직였다. 
또한, 아바타에 지정해둔 초기 자세로 자꾸 돌아가는 듯한 느낌이 들었다.</p>
<p>아바타의 초기 자세👇
<img src="https://velog.velcdn.com/images/one_two_three/post/4d5accd9-ebf9-44d1-9423-db7290bc15df/image.png" alt=""></p>
<p></br></br></p>
<h3 id="문제-예측">문제 예측</h3>
<p>아바타의 동작에 렉이 걸린다는 점과, 초기 자세로 동작이 자꾸 바뀐다는 점에서</p>
<ol>
<li>송신한 데이터들이 분실된다.</li>
<li>송신하는 속도보다 수신하는 속도가 빨라 읽어들일 데이터가 부족하다.</li>
</ol>
<h3 id="문제-해결">문제 해결</h3>
<p>데이터를 읽어들이는 속도를 조절해보기 위해서 코루틴(Coroutine)을 사용했다.</p>
<p><strong>코루틴이란?</strong>
코루틴을 사용하면 작업을 다수의 프레임에 분산할 수 있다. 코루틴은 실행이 일시 정지되거나 중단된 부분에서 다음 프레임을 계속해서 처리할 수 있는 메서드이다.</p>
<blockquote>
<p>유니티 코루틴 개념 참고
<a href="https://docs.unity3d.com/kr/2021.3/Manual/Coroutines.html">https://docs.unity3d.com/kr/2021.3/Manual/Coroutines.html</a></p>
</blockquote>
<p></br></br></p>
<h3 id="코드-수정">코드 수정</h3>
<pre><code class="language-unity">public class FinalTest : MonoBehaviour
{
    public static Socket sock;
    string serverIP = &quot;123.45.67.89&quot;;
    int port = 9999;
    bool socketConnect = false;
    string startPoint = &quot;start&quot;; 

    public Animator anim;
    StoreJointData storeData;
    MoveAvatar moveAvatar;

    bool poseUpdateCheck = true;

    private void Start()
    {
        anim = GetComponent&lt;Animator&gt;();

        storeData = new StoreJointData(anim);
        moveAvatar = new MoveAvatar();

        try
        {
            // TCP 소켓 객체 생성
            sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            // 서버에 연결하기
            IPEndPoint serverEP = new IPEndPoint(IPAddress.Parse(serverIP), port);
            sock.Connect(serverEP);

            socketConnect = true;
            Debug.Log(&quot;Connect Success&quot;);

            // &quot;Start&quot; 문자열 전송
            byte[] buff = Encoding.UTF8.GetBytes(startPoint);
            sock.Send(buff);



        }
        catch (Exception e)
        {
            Debug.Log(&quot;오류: &quot; + e);
        }

        StartCoroutine(PoseUpdate());
    }

    private IEnumerator PoseUpdate()
    {
        while (poseUpdateCheck)
        {
            Vector3[] realJoint = new Vector3[13];
            string[] textLine;
            string[] splitXYZ;

            try
            {
                if (socketConnect)
                {
                    // 전송 받을 바이트 수 먼저 받아오기
                    byte[] buffer = new byte[4];
                    int byteCount = sock.Receive(buffer);
                    Array.Reverse(buffer);
                    int dataByteCount = BitConverter.ToInt32(buffer, 0);

                    // 받으려고 하는 데이터의 수만큼 byte 배열 선언 후 데이터 저장
                    byte[] receivedBuffer = new byte[dataByteCount];
                    byteCount = sock.Receive(receivedBuffer);

                    string msg = Encoding.UTF8.GetString(receivedBuffer, 0, byteCount);

                    textLine = msg.Split(&quot;\n&quot;);

                    for (int i = 0; i &lt; textLine.Length - 1; i++)
                    {

                        string line = textLine[i];

                        splitXYZ = line.Split(&#39; &#39;);

                        if (splitXYZ.All(x =&gt; x is string))
                        {
                            realJoint[i].x = float.Parse(splitXYZ[0]);
                            realJoint[i].y = float.Parse(splitXYZ[1]);
                            realJoint[i].z = float.Parse(splitXYZ[2]);
                        }
                        else
                        {
                            realJoint[i].x = 0f;
                            realJoint[i].y = 0f;
                            realJoint[i].z = 0f;
                        }

                    }
                }
            }
            catch (Exception e)
            {
                Debug.Log(&quot;오류 : &quot; + e);
                socketConnect = false;
                sock.Close();
            }

            // 파일에서 받은 데이터 저장
            storeData.SetTrackJointData(realJoint);

            // 아바타의 팔다리, 몸통 관절데이터 저장
            storeData.Store();

            //움직이기 위한 관절 데이터 전달
            moveAvatar.SetRequiredData(storeData.limbsJointData, storeData.torsoJointData);

            //아바타 팔다리, 몸통 움직이는 함수
            moveAvatar.MoveLimbs();
            moveAvatar.MoveTorso();

            //파이썬 서버와의 통신 속도를 맞추기 위한 딜레이
            yield return new WaitForSeconds(0.048f);

            // 데이터 청소
            storeData.ClearAllData();
            moveAvatar.ClearAllData();
        }

    }

}</code></pre>
<p><strong>핵심 코드</strong></p>
<ol>
<li>Start( ) 함수를 보면 통신을 연결하고 <code>StartCoroutine(PoseUpdate());</code> 함수를 사용하여 코루틴을 시작한다.</li>
<li><code>PoseUpdate()</code> 함수는 Update( ) 함수가 하던 작업을 똑같이 포함하고 있고, 업데이트 되는 프레임의 속도를 조절한다. <pre><code>   // 파이썬 서버와의 통신 속도를 맞추기 위한 딜레이
   yield return new WaitForSeconds(0.048f);</code></pre></li>
</ol>
<p>이 yield 문은 실행이 일시 정지되고 다음 프레임에서 다시 시작되는 부분이다.
<code>WaitForSeconds()</code>를 사용하여 0.048초간 일시정지 하게되어 다음 프레임이 시작되기 전 시간을 약간 지연시켰다.
(통신 상태에 따라 일시정지가 필요한 시간은 다를 수 있음...)</p>
<p></br></br></br>
Update()를 코루틴 함수로 바꾸어 데이터를 수신하는 속도를 약간 늦추었다. 
이렇게 수정한 결과, 아주 약간의 속도차이가 있지만 아바타가 사용자의 동작을 매우 매끄럽게 잘 따라하는 것을 확인할 수 있었다.</p>
<p>사용자의 동작을 따라하는 짧은 영상...
<img src="https://velog.velcdn.com/images/one_two_three/post/de58548a-a5c8-4ec7-9912-e7d2ea13b59f/image.gif" alt=""></p>
<p></br></br></br></p>
<h3 id="아쉬운점">아쉬운점</h3>
<ol>
<li>이제와서 코드를 다시 확인해보니 코드가 효율적이지 못하고, 메모리의 낭비가 있다고 생각한다. </li>
<li>아바타의 관절을 13개만 사용하여, 아바타가 부자연스럽게 움직이고, 동작의 정확성 또한 떨어진다고 생각한다.<h3 id="의의">의의</h3>
</li>
<li>아바타의 관절 위치값을 이용해 아바타의 새로운 동작을 구현할 수 있다.</li>
<li>실시간으로 사용자의 동작을 트래킹하여 UI상으로 사용자의 동작을 확인해 볼 수 있다.</li>
<li>실시간 통신을 직접 구현해보고, 흐름 제어의 중요성을 깨닫게 되었다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unity와 Python의 TCP 소켓 통신을 구현해보자]]></title>
            <link>https://velog.io/@one_two_three/Unity%EC%99%80-Python%EC%9D%98-TCP-%EC%86%8C%EC%BC%93-%ED%86%B5%EC%8B%A0%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@one_two_three/Unity%EC%99%80-Python%EC%9D%98-TCP-%EC%86%8C%EC%BC%93-%ED%86%B5%EC%8B%A0%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 02 May 2024 16:38:02 GMT</pubDate>
            <description><![CDATA[<p>저번 게시물에서 관절의 위치를 이용해 아바타의 움직임을 구현해 보았다.
이번에는 mediaPipe 모듈로 실시간 감지한 관절의 위치를 유니티에 전송해서 데이터를 잘 받아올 수 있는지 확인해 보겠다.</p>
<p></br></br></p>
<h3 id="파이썬-tcp-소켓-통신-코드">파이썬 TCP 소켓 통신 코드</h3>
<pre><code class="language-python">import mediapipe as mp
import socket
import time
import cv2


mpPose = mp.solutions.pose
pose = mpPose.Pose()

# 0은 웹캡
cap = cv2.VideoCapture(0)
# 이전 프레임 처리 시간 저장 변수 초기화
pTime = 0
# 출력하고 싶은 관절들의 인덱스
desired_indices = [0, 11, 12, 13, 14, 15, 16, 23, 24, 25, 26, 27, 28]

# TCP 서버 생성
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((&#39;localhost&#39;, 포트번호))
server_socket.listen(1)  # 최대 1개의 연결을 기다림

print(&quot;서버가 연결을 대기 중입니다.&quot;)

while True:

    # 클라이언트 연결 대기
    try:
        client_socket, addr = server_socket.accept()
        print(f&quot;{addr}에서 연결이 수락되었습니다.&quot;)

        # 문자열 대기
        start_message = client_socket.recv(5).decode()
        if start_message == &#39;start&#39;:
            start_message = &#39;&#39;


        # 실시간을 위해 무한 루프 돌려 줍니다
        while True:

            # 바이트 배열 초기화
            coordinates_bytes = bytearray()

            # 성공여부, 이미지
            success, img = cap.read()

            # 이미지 크기 축소
            new_width = 480  # 가로 크기
            new_height = 640  # 세로 크기
            img_small = cv2.resize(img, (new_width, new_height))

            #rgb로 변경 (mediapipe는 rgb 이미지를 사용)
            imgRGB = cv2.cvtColor(img_small, cv2.COLOR_BGR2RGB)
            results = pose.process(imgRGB)
            # print(&quot;시작&quot;)

            if results.pose_world_landmarks:

                for desired_idx in desired_indices:
                    landmark = results.pose_world_landmarks.landmark[desired_idx]
                    x = landmark.x
                    y = landmark.y
                    z = landmark.z

                    # 선택한 관절 지점의 정보 출력
                    print(f&quot;{x:.5f} {y:.5f} {z:.5f}&quot;.format(x,y,z))

                    # 좌표값을 바이트 배열에 추가
                    coordinates_bytes += f&quot;{x:.5f} {y:.5f} {z:.5f}\n&quot;.format(x, y, z).encode()

                coordinates_bytes = coordinates_bytes.rstrip(b&#39;\0&#39;)

                # 바이트 배열로 변환된 좌표값을 Unity 서버로 전송
                coordinates_length = len(coordinates_bytes)
                length_bytes = coordinates_length.to_bytes(4, byteorder=&#39;big&#39;) 

                # 길이 정보 전송

                client_socket.sendall(length_bytes)

                # 바이트 배열로 변환된 좌표값을 Unity 서버로 전송
                client_socket.sendall(coordinates_bytes)



            cTime = time.time()
            fps = 1 / (cTime - pTime)
            pTime = cTime

            # 이미지에 프레임 속도 표시
            # cv2.putText(img, str(int(fps)), (70, 50), cv2.FONT_HERSHEY_PLAIN, 3, (255, 0, 0), 3)
            cv2.imshow(&quot;image&quot;, img)

            if cv2.waitKey(10) &amp; 0xFF == ord(&#39;q&#39;):  # &#39;q&#39; 키를 누르면 루프 종료
                break

    except Exception as e:
        print(f&quot;연결 수락 중 오류 발생: {e}&quot;)


# 연결 종료
client_socket.close()
server_socket.close()
cap.release()
cv2.destroyAllWindows()</code></pre>
<p></br></br></p>
<h3 id="유니티-소켓-통신-코드">유니티 소켓 통신 코드</h3>
<pre><code class="language-C#">public class FinalTest : MonoBehaviour
{
    public static Socket sock;
    string serverIP = &quot;123.45.67.89&quot;;
    int port = 9999;
    bool socketConnect = false;
    string startPoint = &quot;start&quot;; 

    public Animator anim;
    StoreJointData storeData;
    MoveAvatar moveAvatar;

    Vector3[] realJoint = new Vector3[13];
    string[] textLine;
    string[] splitXYZ;

    private void Start()
    {
        anim = GetComponent&lt;Animator&gt;();

        storeData = new StoreJointData(anim);
        moveAvatar = new MoveAvatar();

        try
        {    
            // TCP 소켓 객체 생성
            sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            // 서버에 연결하기
            IPEndPoint serverEP = new IPEndPoint(IPAddress.Parse(serverIP), port);
            sock.Connect(serverEP);

            socketConnect = true;
            Debug.Log(&quot;Connect Success&quot;);

            // 서버에 &quot;Start&quot; 문자열 전송
            byte[] buff = Encoding.UTF8.GetBytes(startPoint);
            sock.Send(buff);

        }
        catch (Exception e)
        {
            Debug.Log(&quot;오류: &quot; + e);
        }
    }

    private void Update()
    {   

        try {

            if (socketConnect)
            {    
                // 전송 받을 바이트 수 먼저 받아오기
                byte[] buffer = new byte[4];
                int byteCount = sock.Receive(buffer);
                Array.Reverse(buffer);
                int dataByteCount = BitConverter.ToInt32(buffer, 0);

                // 받으려고 하는 데이터의 수만큼 byte 배열 선언 후 데이터 저장
                byte[] receivedBuffer = new byte[dataByteCount]; 
                byteCount = sock.Receive(receivedBuffer);

                string msg = Encoding.UTF8.GetString(receivedBuffer, 0, byteCount);

                textLine = msg.Split(&quot;\n&quot;);

                for(int i = 0; i &lt; textLine.Length - 1; i++)
                {

                    string line = textLine[i];

                    splitXYZ = line.Split(&#39; &#39;);

                    if (splitXYZ.All(x =&gt; x is string))
                    {
                        realJoint[i].x = float.Parse(splitXYZ[0]);
                        realJoint[i].y = float.Parse(splitXYZ[1]);
                        realJoint[i].z = float.Parse(splitXYZ[2]);
                    }
                    else
                    {
                        realJoint[i].x = 0f;
                        realJoint[i].y = 0f;
                        realJoint[i].z = 0f;
                    }

                }
            }
        }
        catch(Exception e) 
        { 
            Debug.Log(&quot;오류 : &quot; + e);
            socketConnect = false;
            sock.Close();
        }

    }

}
</code></pre>
<p></br></br></p>
<h3 id="통신하면서-겪은-오류-1">통신하면서 겪은 오류 1</h3>
<p>13개의 관절 좌표값을 올바르게 전달 받지 못했다.</p>
<pre><code>-0.02801599 1.530401 0.01000143
0.1409548 1.397012 0.02304516
-0.1969829 1.397008 0.01986888
0.37999 1.397016 0.02304507
-0.4362946 1.397007 0.01986932
0.6318882 1.39702 0.02304519
-0.688216 1.397006 0.01986977
0.0739904 0.884761 0.002631317
-0.1300061 0.884761 0.002718598
0.06866307 0.4908463 0.009188599
-0.124666 0.4908559 0.007917952
0.07400247 0.1169901 0.0270884
-0.1299936 0.1169791 0.02709265</code></pre><p>통신할 때 13개의 관절 x y z 좌표값이 위와 같은 형태로 전달 되어야 배열에 값이 잘 저장될 수 있다. </p>
<p></br></br></p>
<pre><code>-0.02801599 1.530401 0.0100
0.1409548 1.397012 0.02304516345
-0.1969829 1.397008 0.019868883
0.37999 1.397016 0.023045073535
-0.4362946 1.397007 0.0198
0.6318882 1.39702 0.02304519
-0.6 1.397006 0.01986977
0.0739904 0.884761 0.002631317
-0.1300061 0.884761 0.002718598
0.06866307 0.4908463 </code></pre><p>이처럼 x, y, z값 중에 z값이 없거나, 13개의 관절 위치값을 받아오지 못하는 일이 발생했다.</p>
</br>

<p>*<em>원인 : *</em>
mediaPipe 모듈로 관절의 위치가 매우 미세하게 측정되어 3차원 좌표값의 길이가 매 프레임마다 달라졌기 때문에 일정한 데이터의 길이로 데이터를 전송 받기 어려웠다.</p>
<p>*<em>해결 : *</em>
전송 받을 바이트 수를 먼저 받아온 후, 그 바이트 수만큼의 배열을 생성해 데이터를 저장했다.</p>
<pre><code class="language-c#">  // 전송 받을 바이트 수 먼저 받아오기
  byte[] buffer = new byte[4];
  int byteCount = sock.Receive(buffer);
  Array.Reverse(buffer);
  int dataByteCount = BitConverter.ToInt32(buffer, 0);

  // 받으려고 하는 데이터의 수만큼 byte 배열 선언 후 데이터 저장
  byte[] receivedBuffer = new byte[dataByteCount]; 
  byteCount = sock.Receive(receivedBuffer);</code></pre>
<p>그런데 이 방식은 크기가 다른 배열을 매 프레임마다 새로 생성하기 때문에 힙 메모리를 낭비할 수 있는 문제가 있다. 
코드를 수정한다면, 파이썬 서버에서 관절 데이터를 보내기 전에 일정한 길이의 데이터만 보내고, 유니티에서는 배열을 하나만 생성해서 생성한 메모리를 재사용 할 수 있도록 하는 것이 좋을 듯 하다.</p>
<p></br></br></br></p>
<h3 id="통신하면서-겪은-오류-2">통신하면서 겪은 오류 2</h3>
<p><strong>원인</strong> : 바이트의 순서가 바뀌어 올바른 값을 얻지 못했다.</p>
<p>파이썬 측에서 보내려고 하는 바이트 수를 먼저 보낸다.</p>
<pre><code class="language-python"># 바이트 배열로 변환된 좌표값을 Unity 서버로 전송
coordinates_length = len(coordinates_bytes)
length_bytes = coordinates_length.to_bytes(4, byteorder=&#39;big&#39;) 

# 길이 정보 전송
client_socket.sendall(length_bytes)

# 바이트 배열로 변환된 좌표값을 Unity 서버로 전송
client_socket.sendall(coordinates_bytes)           </code></pre>
<p><code>to_bytes()</code>로 전송하려고 하는 바이트의 길이를 <strong>빅 엔디안</strong>(big endian)으로 저장해서 보냈지만, 유니티 측에서는 <strong>리틀 엔디안</strong>(little endian)으로 받게 되어 올바른 값을 확인하기 힘들었다. </p>
<p><strong>해결</strong> :</p>
<pre><code class="language-c#">// 전송 받을 바이트 수 먼저 받아오기
byte[] buffer = new byte[4];
int byteCount = sock.Receive(buffer);
Array.Reverse(buffer);</code></pre>
<p>유니티에서 전송받은 데이터를 <code>Array.Reverse()</code> 로 이용해 받은 바이트를 역순으로 바꾼다. 바꾼 값을 확인해 봤을 때 파이썬에서 전송했던 값을 올바르게 받을 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[통신하기 전 Unity 아바타 관절 움직이는 코드 최종 수정]]></title>
            <link>https://velog.io/@one_two_three/%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0-%EC%A0%84-Unity-%EC%95%84%EB%B0%94%ED%83%80-%EA%B4%80%EC%A0%88-%EC%9B%80%EC%A7%81%EC%9D%B4%EB%8A%94-%EC%BD%94%EB%93%9C-%EC%B5%9C%EC%A2%85-%EC%88%98%EC%A0%95</link>
            <guid>https://velog.io/@one_two_three/%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0-%EC%A0%84-Unity-%EC%95%84%EB%B0%94%ED%83%80-%EA%B4%80%EC%A0%88-%EC%9B%80%EC%A7%81%EC%9D%B4%EB%8A%94-%EC%BD%94%EB%93%9C-%EC%B5%9C%EC%A2%85-%EC%88%98%EC%A0%95</guid>
            <pubDate>Thu, 02 May 2024 16:37:37 GMT</pubDate>
            <description><![CDATA[<p>통신을 시작하기 전 mediaPipe 모듈을 실행해보았다. 웹캠(webcam)을 키고 몇 초간 어떤 동작을 수행하면서, 필요한 13개의 관절 위치값을 텍스트 파일에 기록했다.
<img src="https://velog.velcdn.com/images/one_two_three/post/e306e51b-c2e9-4628-b709-9773c9c96648/image.png" alt=""></p>
<p>이 텍스트 파일의 값들을 아바타에 적용시켜서 아바타가 동작을 잘 수행하는지 최종적으로 점검 했다.</p>
<blockquote>
<p>아바타 관절을 움직이는 코드는 저번 게시물 참고🙂
<a href="https://velog.io/@one_two_three/%EC%95%84%EB%B0%94%ED%83%80%EC%9D%98-%EA%B4%80%EC%A0%88%EC%9D%84-%EC%9B%80%EC%A7%81%EC%97%AC%EC%84%9C-%EB%8F%99%EC%9E%91%EC%9D%84-%EC%88%98%ED%96%89%ED%95%98%EB%8A%94-%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90">저번 게시물</a></p>
</blockquote>
<p></br></br></br></p>
<h3 id="코드-실행-후-문제점">코드 실행 후 문제점</h3>
<p>저번 게시물에서 텍스트 파일에 기록되어있는 관절값을 가져와 아바타에 적용한 후 아바타가 잘 움직이는 것을 확인했다. 
<strong>그런데</strong>, mediaPipe 모듈로 측정한 데이터를 아바타에 적용했을 때 아바타의 동작이 잘 수행되지 않고, 아바타의 몸도 이상해졌다(보기 흉함..)</p>
</br>

<h3 id="문제-예측">문제 예측</h3>
<ol>
<li>텍스트 파일에 관절 순서가 뒤바뀌어 기록되었기 때문에 아바타의 관절 위치가 잘못되었나?
👉 확인해보니 아바타의 관절 순서에 맞게 잘 기록되어있음</li>
<li>아바타의 관절을 움직이는 코드에 문제가 있나?
👉 확인해보니 관절 위치를 수정해야 하는 부분이 있었다!</li>
</ol>
<p></br></br></br></p>
<h3 id="문제-해결">문제 해결</h3>
<p>mediaPipe의 <strong>Pose Landmark model</strong>은 두가지의 Output을 리턴한다.
Output 하나는 <code>Landmarks</code> 이고, 다른 하나는 <code>WorldLandmarks</code> 이다.
나는     <code>WorldLandmarks</code> 결과를 사용해 관절의 위치를 텍스트 파일에 기록했다.
<img src="https://velog.velcdn.com/images/one_two_three/post/fd122784-760c-48a5-98c8-47837d9d1741/image.png" alt="">
<code>WorldLandmarks</code>는 관절의 x, y, z값이 모두 엉덩이 사이의 중심(24번과 23번의 중심값)을 원점으로 하는 <strong>상대좌표</strong>이다.
<img src="https://velog.velcdn.com/images/one_two_three/post/dfa4890f-f6e9-4e7a-b708-9728ce9aa5ec/image.png" alt=""></p>
<p>따라서 mediaPipe 모듈로 측정한 관절의 좌표값을 이용해 아바타를 움직이기 위해선 코드의 수정이 필요하다.</p>
<p><br><br>
다른 클래스의 코드는 그대로 두고 StoreJointData.cs의 MakeVirtualData 함수를 수정한다.</p>
<pre><code class="language-c#">    // 가상의 관절 만들기, 관절 위치 재조정
    public void MakeVirtualData()
    {
        // 가상의 목 관절의 위치 구하기
        virtualNeck = (trackJoint[1] + trackJoint[2]) / 2.0f;
        virtualNeck.y += 0.05f;

        // 가상의 힙 관절의 위치 구하기
        virtualHips = (trackJoint[7] + trackJoint[8]) / 2.0f;
        virtualHips.y += 0.95f;

        //가상의 UpperChest 관절 위치 구하기
        virtualUpperChest = (trackJoint[1] + trackJoint[2]) / 2.0f;
        virtualUpperChest.y -= 0.1f;

        virtualNeck += virtualHips;
        virtualUpperChest += virtualHips;

        for (int i = 0; i &lt; 13; i++)
        {
            trackJoint[i].y *= -1f; // 트래킹한 조인트 값의 y좌표가 땅과 반대로 되어있음
            trackJoint[i] += virtualHips; // pose_world_landmarks는 엉덩이 중간 포인트를 기준으로 상대좌표이므로 Hips의 위치를 더해 절대 좌표를 구해준다.
        }

        anim.GetBoneTransform(HumanBodyBones.Hips).position = virtualHips;
    }</code></pre>
<ul>
<li><p><code>virtualHips(엉덩이 사이의 중간 지점)</code>의 위치는 절대 좌표로 잡아두고, 다른 관절들은 <code>virtualHips</code> 위치를 더해 3차원 <strong>절대 좌표</strong>로 바꾸어준다.</p>
</li>
<li><p>mediaPipe의 모델이 사용하는 3차원 좌표계와 유니티에서 사용하는 좌표계의 차이가 있어 y좌표값이 반대로 되어 있음을 발견했다.</p>
<pre><code>trackJoint[i].y *= -1f;</code></pre><p>트래킹한 관절의 y좌표 값에 -1을 곱해 방향을 바꾸어 아바타의 몸이 반대로 뒤집어 지지 않도록 코드를 수정한다.</p>
</li>
</ul>
<blockquote>
</blockquote>
<p><a href="https://github.com/devHaneul/rotate-unity-avatar-joint/blob/main/Assets/Avartar/AvatarScripts/StoreJointData.cs">전체코드</a></p>
<p></br></br></br></p>
<p>코드를 수정한 후 다시 데이터를 적용했을 때 아바타가 잘 움직인다😁 
이제 노트북 웹캠을 통해 실시간 관절 데이터를 유니티 서버로 전달하고, 실시간으로 아바타를 움직여 볼 것이다.</p>
]]></description>
        </item>
    </channel>
</rss>