2025년 5월 17일 토요일

ReAct 에이전트 프레임웍 내부 코드 구조 개발해보기

ReAct(Reasoning and Acting) 에이전트 구조는 대형 언어 모델(LLM)을 기반으로 도구 호출 기능을 통합한 지능형 에이전트 시스템을 구현하는 설계 방식이다. 사용자의 지시를 추론하고 필요한 경우 외부 도구를 호출하여 목표를 달성하는 능력을 지니며, 최근 AutoGPT, BabyAGI, Manus 등 다양한 오픈소스 프로젝트에서 채택되고 있다. 이 글에서는  ReAct 에이전트 프레임웍 내부 코드 구조를 직접 개발해본다. 아울러, ReAct 에이전트의 문제점들을 살펴보고 솔류션을 생각해본다. 

ReAct 구조 

ReAct 시퀀스 처리 구조는 LLM 기반 에이전트가 사고(Think), 행동(Act), 관찰(Observe), 결론(Final)의 단계로 사용자 질의에 응답하는 체계적 프로세스이다. 이 구조는 복잡한 문제 해결을 위해 LLM이 도구와 상호작용하면서 점진적으로 정답을 유도해내도록 설계되었다.

다음은 해당 코드에서 구현된 ReAct 시퀀스의 흐름이다.

  1. Think
    LLM은 질문에 대한 분석을 수행하며 필요한 경우 어떤 도구를 사용할지를 판단한다. 이 단계는 주로 텍스트 상의 추론 내용으로 표현되며, 도구 실행 없이 논리 전개만 이루어진다.

  2. Act
    필요하다고 판단되면 Act: ToolName[Input] 또는 Act: ToolName(Input) 형식으로 도구 호출을 지시한다. 이 지시문은 정규표현식으로 감지되어 실제 도구 실행이 트리거된다.
  3. Observe
    호출된 도구가 실행되고, 그 결과는 Observe: 접두사를 붙여 기록된다. 이 결과는 이후 컨텍스트에 포함되어 다음 LLM 호출의 입력으로 사용된다.

  4. Final
    충분한 정보가 수집되면 Final Answer: 형식으로 최종 응답을 제공한다. 이는 루프를 종료시키는 조건이기도 하며, 에이전트의 실행 결과로 사용자에게 출력된다.

이러한 시퀀스를 통해 LLM은 단순히 한 번에 답변하는 것이 아니라, 도구를 여러 번 사용하고 그 결과를 반영하며 점진적으로 정확한 응답에 도달한다.

ReAct 에이전트 내부 코드 구현해보기

앞서 언급된 개념을 고려해 ReAct 에이전트의 핵심적인 코드만 구현해 본다. 다음과 같이 관련 라이브러리를 설치한다.

pip install langchain 

다음과 같이 코드를 구현한다. 

import re, os
from langchain.tools import Tool
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain.chat_models import ChatOpenAI
from langchain.schema import HumanMessage
from dotenv import load_dotenv

load_dotenv()

# Calculator function
def calculator(input_str: str) -> str:
    try:
        result = eval(input_str)
        return str(result)
    except Exception as e:
        return f"Error: {e}"

def FireCrawlResults(max_results=3, search_type="web"):
    from langchain_community.tools.fire_crawl import FireCrawlResults
    return FireCrawlResults(max_results=max_results, search_type=search_type) # TBD

# Initialize tools
tools = [
    Tool(
        name="Calculator",
        func=calculator,
        description="Evaluates mathematical expressions. Input should be a valid Python expression."
    ),
    TavilySearchResults(max_results=3, search_type="web"),
    # FireCrawlResults(max_results=3, search_type="web"),
]

# Function to extract tool prototype info
def get_tools_info(tools):
    info_list = []
    for tool in tools:
        # Try to get function signature if possible
        if hasattr(tool, "func"):
            proto = f"{tool.name}(input: str)"
        else:
            proto = f"{tool.name}(input: str)"
        desc = getattr(tool, "description", "No description.")
        info_list.append(f"- {proto}: {desc}")
    return "\n".join(info_list)

# Initialize the language model
llm = ChatOpenAI(temperature=0, model="gpt-4", max_tokens=4000)
answer_validation_llm = ChatOpenAI(temperature=0, model="gpt-4-turbo", max_tokens=4000)

# Prompt template for ReAct
prompt_template = """
Instruction: {instruct}
IMPORTANT: If you do not know the answer, do not use 'Final Answer', just say 'I don't know'.

Tools you can use:
{tools}

Context:
{context}

Query:
{query}

You should follow the ReAct pattern:
- Think: Reason about the question or next step.
- Act: If needed, use a tool in the format Act: <tool>[<input>].
- Observe: Note the result of the action.
- Final: Give the final answer in the format Final Answer: <answer>.
"""

answer_validation_prompt = """
Question: {question}
Answer: {answer}
Is the answer relevant and correct for the question? Reply only with "yes" or "no" and a short reason.
"""

class ReActAgent:
    def __init__(self, llm, tools, prompt_template):
        self.llm = llm
        self.tools = {tool.name: tool for tool in tools}
        self.prompt_template = prompt_template
        self.tools_info = get_tools_info(tools)

    def validate_answer(self, question, answer):
        prompt = answer_validation_prompt.format(question=question, answer=answer)
        messages = [HumanMessage(content=prompt)]
        output = answer_validation_llm(messages)
        response = output.content.strip().lower()
        return response.startswith("yes"), response

    def run(self, input_query: str):
        history = []
        while True:
            prompt = self.prompt_template.format(
                instruct="Answer the question in detail using ReAct reasoning.",
                tools=self.tools_info,
                context="\n".join(history),
                query=input_query
            )
            messages = [HumanMessage(content=prompt)]
            output = self.llm(messages)
            response = output.content.strip()
            history.append(response)

            # Check for Final Answer
            final_match = re.search(r"Final Answer:\s*(.*)", response, re.IGNORECASE)
            if final_match:
                answer = final_match.group(1)
                # is_valid, validation_msg = self.validate_answer(input_query, answer) # TBD. 실제 react 코드 확인.
                # if is_valid:
                return answer

            # Detect action
            action_match = re.search(r"Act:\s*(\w+)[\[\(](.*)[\]\)]", response)
            if action_match:
                action_name = action_match.group(1)
                action_input = action_match.group(2)
                print(f"Action detected: {action_name} with input: {action_input}")

                tool = self.tools.get(action_name)
                if not tool:
                    history.append(f"Observe: Unknown tool: {action_name}")
                    continue
                action_input = action_input.replace("'", "").replace('"', "")  
                result = tool.run(action_input)
                history.append(f"Observe: {result}")
            else:
                # If no action and no final answer, just continue (could be Think step)
                continue

agent = ReActAgent(llm=llm, tools=tools, prompt_template=prompt_template)

def test():
    query = "What is the result of 12 * (3 + 4)?"
    result = agent.run(query)
    print(result)

    query = "What is the capital of France?"
    result = agent.run(query)
    print(result)

    query = "In web, What is Taewook kang's paper about Geo BIM using BIM and GIS?"
    result = agent.run(query)
    print(result)

def main():
    print("ReAct Agent is ready to use.")
    print("Available tools:")
    for tool in agent.tools_info.split("\n"):
        print(tool)

    # test()

    print("\nInteractive mode:")
    while True:
        query = input("Enter your query (or 'exit' to quit): ")
        if query.lower() == "exit":
            break
        result = agent.run(query)
        print(result)

if __name__ == "__main__":
    main()


이 코드는 ReAct 시퀀스를 중심으로 구성되어 있다. ReAct란 LLM이 단순한 질의응답을 넘어서서, 생각하고 도구를 사용하고 그 결과를 바탕으로 다시 추론한 뒤 최종적인 답을 내리는 일련의 추론 패턴이다. 이 흐름은 크게 네 단계로 나뉜다. 

첫째, Think 단계에서는 주어진 질문에 대해 어떻게 접근할지를 LLM이 서술적으로 사고한다. 둘째, Act 단계에서는 필요하다고 판단한 도구를 지정된 형식으로 호출한다. 이때 Act: ToolName[Input] 형식을 사용한다. 셋째, Observe 단계에서는 실행된 도구의 결과를 받아 LLM의 다음 추론에 포함될 수 있도록 기록한다. 넷째, Final 단계에서는 Final Answer: 답변 형식으로 최종 결론을 도출한다.

이러한 시퀀스를 실제로 구현하기 위해 코드에서는 특정 정규표현식을 사용하여 각 단계를 감지하고 분기 처리한다. 예를 들어, Act 단계는 Act:라는 키워드를 통해 감지되고, 대응되는 도구 이름과 입력값이 추출된다. 대응되는 도구가 존재하면 해당 함수나 객체의 run 메서드를 호출하여 결과를 얻고, 이를 Observe로 기록하여 다음 프롬프트 생성 시 문맥으로 전달한다. Final Answer가 감지되면 그 값을 최종 출력으로 반환하면서 루프를 종료한다.

코드는 먼저 환경변수를 불러오고, 수학 계산용 calculator 함수와 웹 검색용 Tavily 도구를 정의하고 tools 목록에 등록한다. 각 도구는 Tool 객체로 구성되며, name, func, description 속성을 갖는다. 이 도구들의 인터페이스 정보는 get_tools_info 함수를 통해 LLM 프롬프트에 전달할 수 있는 형식으로 변환된다.

ReActAgent 클래스는 핵심 로직을 담당한다. 생성자에서는 도구를 이름으로 접근 가능하도록 딕셔너리 형태로 구성하며, 프롬프트 템플릿에 포함할 도구 설명을 자동으로 생성한다. run 메서드는 LLM과의 대화를 이끄는 루프이다. 사용자의 입력 쿼리를 바탕으로 Think부터 Final 단계까지의 응답을 순차적으로 생성하고, 도구 호출이 필요한 경우 자동으로 감지하여 실행한다.

LLM은 ChatOpenAI 클래스를 통해 초기화되며, 주 모델과 검증용 모델이 따로 구성된다. 현재 코드는 검증 부분이 주석 처리되어 있어 정답 검증은 수행되지 않지만 validate_answer 메서드는 존재한다. 해당 메서드는 question과 answer를 받아 프롬프트에 삽입하고 gpt-4-turbo 모델로부터 응답을 받아 yes 또는 no 여부와 사유를 반환한다.

main 함수는 인터랙티브한 사용자 입력을 받아 ReActAgent를 호출하는 실행 루틴이다. 사용자가 exit이라고 입력할 때까지 질의를 받고 응답을 출력하는 루프를 구성한다. 이로써 ReActAgent는 반복적인 질의응답 상황에서도 도구를 활용하여 논리적 사고 과정을 수행하고, 외부 정보에 기반한 정답을 도출할 수 있도록 설계되어 있다.

주요 문제 유형

ReAct 기반 에이전트는 구현 및 실전 적용 과정에서 여러 구조적 문제와 비효율이 발생한다. 이러한 문제 사례를 분석하고, 실제 커뮤니티에서 공유된 해결 전략과 함께 개선 방향을 확인해 본다.

추론 오류
에이전트가 잘못된 전제나 문맥에 기반하여 부적절한 도구를 선택하거나 오답을 도출하는 문제가 빈번히 발생한다. 이는 LLM의 한계와 프롬프트 설계 미비, 또는 이전 상태 정보의 왜곡에 기인한다.

반복 및 루프 문제
에이전트가 목표에 도달하지 못하고 동일한 행동을 반복 수행하는 루프에 빠지는 문제가 자주 발생한다. 이는 max_steps 제한이 없거나, 실패 판단 기준이 모호한 경우에 두드러진다.

결과 검증 실패
에이전트가 도출한 결과가 부정확하거나 무의미함에도 불구하고, 이를 최종 결과로 판단하고 종료되는 문제가 있다. 이는 정답 여부를 평가할 수 있는 검증 메커니즘의 부재 혹은 Verifier LLM의 오판 때문이다.

지연 및 비용 문제
ReAct 구조는 추론 Think, 행동 Act, 검증 Verify 단계에서 각각 LLM 호출이 필요하므로 응답 지연이 누적되며, 고성능 모델 사용 시 비용 또한 급증하게 된다.

상태 관리 실패
에이전트가 이전 문맥을 적절히 유지하지 못하거나, 기억을 잘못 참조하여 추론에 실패하는 경우가 있다. 이는 memory overflow, context 길이 초과 등으로 인해 발생한다.

해외 커뮤니티 보고 사례

Reddit, GitHub, Hacker News 등 해외 커뮤니티에서는 위 문제들이 반복적으로 보고되고 있다. 주요 사례는 다음과 같다

r/React 에서는 Langchain의 늦은 React 에이전트 성능에 대한 문제가 제기된 적이 있다. 이런 사례는 수도 없이 많다.

r/AutoGPT에서는 도구 호출 반복, final answer 오류 등 루프와 검증 문제를 다룬 글들이 다수 존재한다.

r/MachineLearning에서는 hallucination 문제, tool selection 오류에 대한 토론이 있다
GitHub Issues의 OpenDevin, Voyager 등의 리포지터리에서도 루프, 상태 관리 문제, 도구 실행 실패가 자주 보고되고 있다

이는 ReAct 구조의 본질적인 설계 한계와 관련되며, 이를 보완하기 위한 다양한 시도들이 커뮤니티에서 논의되고 있다

해결 방안

Verifier LLM의 도입
추론 결과에 대해 별도의 고성능 LLM을 사용하여 정답 여부를 판단하도록 구성하는 방식이다 예를 들어 GPT 35 기반 ReAct 에이전트를 GPT 4 기반 Verifier로 보완하는 사례가 있다

Confidence 기반 조건 실행
LLM이 응답에 대한 신뢰도 confidence score를 함께 출력하도록 하고, 일정 기준 미만일 경우에만 검증자 호출 또는 재시도를 수행하는 방식이다

행동 다양성 유도 및 페널티 적용
같은 도구를 반복 호출하지 않도록 행동 선택 시 다양성을 보장하거나 페널티 기반 scoring 방식을 적용한다

결과 품질 기반 Soft Fallback 적용
결과가 일정 기준 미달일 경우 완료 실패 메시지를 출력하거나, 대체 응답을 제공하는 방식으로 흐름을 마무리한다

도메인 기반 grounding 기법
도메인 특화 지식을 embedding하거나 retriever를 통해 grounding context를 제공함으로써 hallucination을 감소시키는 방식이다

LLM 처리 지연 보완을 위한 전략

ReAct 구조에서는 LLM을 여러 번 호출하는 구조로 인해 응답 시간이 증가하는 문제가 발생한다. 이를 해결하기 위한 보완 전략은 다음과 같다

첫째, 동일한 LLM을 사용하는 것이 아니라 추론 단계는 경량 LLM을 사용하고 검증 단계는 고성능 LLM을 사용하는 방식으로 역할 분리를 통해 효율을 높이는 방안이 있다

둘째, LLM의 confidence score를 이용하여 신뢰도가 충분히 높은 경우에는 검증 단계를 생략함으로써 전체 호출 횟수를 줄일 수 있다

셋째, 여러 행동 중 하나를 선택할 때 prefetch 또는 batch decoding 방식을 사용하여 예측을 병렬적으로 수행하면 전체 응답 지연을 줄일 수 있다

넷째, 반복되는 도구 호출에 대해서는 캐싱 메커니즘을 도입하거나 결과를 저장하여 재활용함으로써 불필요한 호출을 방지할 수 있다

다섯째, 검증자 역할을 하는 LLM을 완전한 LLM이 아니라 룰 기반 평가기 또는 소형 모델로 대체하여 빠르게 판단하게 하는 방식도 고려할 수 있다

코드 기반 구현 예시 요약

ReAct 기반 구조는 보통 다음 흐름으로 구성된다

  1. 사용자 입력 수신
  2. LLM을 통해 행동 계획 수립 Think
  3. 해당 도구 실행 Act
  4. 결과 수신 후 Verifier를 통해 정답 여부 판단 Verify
  5. 정답 시 종료, 실패 시 재시도 또는 fallback 처리

이러한 구조는 Python 코드로 구현할 수 있으며, 각 함수는 다음과 같은 역할을 수행한다

  1. think goal 행동 계획 생성
  2. act action 도구 실행 및 결과 수신
  3. verify result, goal 결과 검증
  4. run goal 전체 흐름 통제 및 반복 로직 포함

결론

ReAct 기반 에이전트는 고도의 자율성을 지닌 시스템이나, 구조적으로 여러 한계가 존재한다. 특히 결과 검증 실패, 반복 루프, hallucination, 비용 증가 문제는 실제 운영 및 서비스화에 있어 큰 장애 요인이 된다. 이를 해결하기 위해서는 검증자 LLM 도입, confidence 기반 흐름 제어, 도구 호출의 다양성 보장, grounding 및 fallback 전략이 함께 설계될 수 있다.


부록: 보고서 형식 AI 에이전트 패턴

ReAct 에이전트는 Reasoning and Acting의 약어로, LLM이 툴과 상호작용하며 문제를 해결하는 패턴이다. 이 구조는 인간처럼 사고하고, 필요한 정보를 외부 도구에서 검색하거나 계산을 수행한 뒤, 최종적으로 응답을 생성한다. 이로 인해 복잡한 질의나 멀티스텝 추론이 필요한 경우에 적합한 패턴으로 여겨진다. 그러나 이러한 장점에도 불구하고, LLM이 여러 차례 호출되고 툴도 반복적으로 작동하므로 응답 시간이 느려지고, API 사용량 증가로 인해 비용이 상승하는 단점이 존재한다.

한편 단순히 보고서를 생성하는 용도로 LLM을 사용하는 경우라면, 복잡한 Reasoning이나 여러 번의 외부 툴 호출은 필요하지 않다. 이때는 ReAct 에이전트를 사용하는 것이 오히려 비효율적이며, 불필요한 구조적 복잡성과 지연을 유발한다. 따라서 보고서 자동 생성이라는 목적에 최적화된 에이전트 구조는 단일 호출 기반 구조가 적합하다.

이 구조는 Single-Shot LLM Invocation 패턴으로 명명된다. 이 방식은 LLM을 한 번만 호출하여 입력된 요구 사항에 맞는 보고서를 즉시 생성한다. 별도의 도구 호출이나 intermediate step 없이 하나의 잘 설계된 프롬프트를 중심으로 전체 작업이 이루어진다. 이 패턴은 속도가 빠르고, 처리 흐름이 단순하며, LLM API 호출 횟수를 최소화하므로 비용이 낮다. 특히 단일 보고서, 요약, 이메일 초안, 블로그 콘텐츠와 같은 목적에 적합하며, 사용자의 입력 내용에 따라 문서 포맷이나 톤을 조정하는 것만으로 충분한 성능을 발휘한다.

단일 호출 구조는 별도의 외부 툴이나 체인을 필요로 하지 않기 때문에 LangChain이나 ReAct 구조 없이도 Python 코드 수준에서 간단히 구현이 가능하다. 예를 들어, 사용자가 요구사항을 입력하면 그에 맞는 템플릿 기반 프롬프트를 작성하고, 이를 LLM에 전달해 한 번에 결과를 반환받는 방식이다. 이러한 프롬프트에 보고서의 형식, 어조, 길이 등을 명시하면, 결과물의 일관성과 품질도 충분히 확보할 수 있다.

이 외에도 필요에 따라 Plan-and-Write 패턴을 고려할 수 있다. 이 패턴은 먼저 전체 문서의 개요를 작성한 뒤, 각 항목에 따라 내용을 생성하는 방식이다. 좀 더 명확한 구성과 섹션 구분이 필요할 때 유용하다. 통계 수치나 외부 데이터를 포함해야 할 경우에는 Toolformer와 같이 선택적으로 툴을 호출하는 패턴을 도입할 수 있다. 마지막으로 문서가 일정한 형식을 유지해야 하거나 후처리를 자동화해야 할 때는 Structured Prompting 패턴을 사용할 수 있으며, JSON 또는 Markdown 형식으로 문서 구조를 고정하여 일관성을 높인다.

결론적으로, 보고서 자동 생성처럼 복잡한 추론이나 외부 툴 연동이 불필요한 작업에는 ReAct와 같은 다단계 에이전트 구조는 적합하지 않다. 대신 단일 LLM 호출로 작업을 완료하는 Single-Shot 방식이 가장 효율적이며, 프롬프트 최적화만으로 높은 품질의 결과를 얻을 수 있다. 

부록: AI 에이전트 오케스트레이션

사용자 질문에 따라 적절한 에이전트 패턴을 선택하기 위해서는 입력의 목적과 복잡성을 빠르게 분류할 수 있는 에이전트 오케스트레이션 메커니즘이 필요하다. 다음 의사코드는 솔류션을 보여준다. 

def classify_input(user_input):
    if "보고서" in user_input or "작성해" in user_input:
        return "simple_generation"
    if "json" in user_input or "표로" in user_input:
        return "structured_output"
    if "비교해" in user_input or "어떤게 더 나아" in user_input:
        return "tool_assisted"
    if "계산" in user_input or any(op in user_input for op in "+-*/"):
        return "tool_assisted"
    return "multi_step_reasoning"

def classify_input(user_input):
    result = rule_based_classifier(user_input)
    if result == "unknown":
        return llm_classifier(user_input)
    return result

여기서는 classify_input 함수가 사용되며, 이 함수는 사용자의 입력 문장을 분석하여 어떤 처리 패턴이 적합한지를 판단하는 역할을 수행한다. 하지만 이 함수 자체가 느리거나 LLM 호출을 과도하게 유발하면 전체 시스템의 응답성이 떨어지므로, 이를 빠르게 실행할 수 있는 설계가 중요하다.

classify_input을 빠르게 실행하기 위한 방법은 크게 세 가지로 구분된다. 첫 번째는 경량 LLM을 사용하는 방식이다. 이 방법은 GPT-3.5 Turbo, Claude Instant, DistilBERT 등의 속도가 빠른 경량 언어 모델을 활용하여 입력을 분류하는 방식이다. 프롬프트는 미리 정의된 범주 중에서 입력이 어떤 태스크 유형에 해당하는지를 선택하게 하며, 이에 따라 적절한 패턴으로 연결된다. 이 방식은 유연하고 적응력이 높지만, 여전히 LLM 호출이기 때문에 응답 속도에 영향을 줄 수 있다. 그럼에도 불구하고 ReAct 등 복잡한 구조보다는 훨씬 빠르고 저비용이다.

두 번째 방식은 규칙 기반 분류 방식이다. 이는 키워드 기반으로 입력 문장을 빠르게 분류하는 방법으로, 예를 들어 ‘보고서’, ‘작성해’ 등의 단어가 포함되어 있으면 단순 생성 태스크로 간주하고, ‘비교’, ‘계산’, ‘표로’ 등의 단어가 있으면 툴 기반 또는 구조화된 출력을 요구하는 태스크로 분류하는 것이다. 이 방식은 매우 빠르며, 수 마이크로초 이내에 실행이 가능하고, 비용도 발생하지 않는다. 그러나 복잡한 문장이나 여러 의미가 섞인 문장에 대해서는 정확하게 분류하지 못할 위험이 있다.

세 번째는 하이브리드 방식이다. 이 방식은 앞서 언급한 규칙 기반 분류기를 우선 사용하고, 그 결과가 불확실하거나 unknown일 경우에만 경량 LLM을 호출하여 보완 분류를 수행한다. 이 구조는 속도와 정확도 사이의 균형을 잡기 위해 매우 현실적인 대안이며, 실제로 OpenAI API 기반 에이전트나 LangChain의 라우팅 모듈에서도 유사한 방식이 채택된다. 구현은 상대적으로 복잡할 수 있으나, 전체 시스템의 응답성과 품질을 함께 유지할 수 있는 방법이다.

결론적으로 classify_input 함수를 빠르게 실행하기 위해서는 입력의 유형과 빈도에 따라 최적화 전략을 선택해야 한다. 단순한 텍스트 입력이 반복되는 시스템에서는 규칙 기반 분류기로 충분하며, 복합 입력이 자주 등장하는 환경에서는 하이브리드 분류기를 설계하는 것이 이상적이다. 

2025년 4월 17일 목요일

OpenAI 코딩 멀티 에이전트 Codex 오픈소스 도구 사용법

이 글은 OpenAI가 개발한 코딩 멀티 에이전트 Codex 사용법을 간략히 소개한다. 

현재 시점에서 14시간 전에 OpenAI o3, o4, codex 가 공개되었다. 모두 멀티 AI 에이전트 기능을 충실히 구현한 영상을 데모가 업로드되었고, 특히, 자동화 코딩을 지원하는 codex(코덱스) 가 로컬 컴퓨터에서 실행 가능한 형태로 공개된 점이 인상적이었다. 
OpenAI o3, o4, codex 공개 영상

코덱스 코드 및 도구는 Github에 공개되었다.
사용환경
우분투 등 리눅스 계열 혹은 윈도우즈 파워셀에서 실행해야 동작한다.  
오픈AI Key를 미리 터미널에서 설정한다.
export OPENAI_API_KEY="your-api-key-here"

파워셀에서는 다음 명령으로 설정한다. 
set OPENAI_API_KEY="your-api-key-here"

다만, 리눅스가 기본이라 윈도우즈에서는 코덱스 수행 중에 몇몇 에러가 발생할 수 있다.

코덱스 설치
터미널에서 다음을 실행해 설치 및 환경 설정을 한다. 
git clone https://github.com/openai/codex.git
cd codex
npm install -g @openai/codex

설치 시 에러가 발생하면, super user 권한으로 설치한다.
sudo npm install -g @openai/codex

설치 시 npm error code SELF_SIGNED_CERT_IN_CHAIN 에러 발생하면 다음 명령 실행 후 패키지 재설치한다.
npm config set strict-ssl false -g

코덱스 실행
터미널에서 코덱스를 실행한다. 
codex 

프롬프트를 직접 입력해도 된다.  
codex "explain this codebase to me"

만약 처음 코엑스를 실행하면, 다음과 같이 OpenAI platform 로그인이 요청된다. 

이때는 다음과 같이 로그인(Overview - OpenAI API)하고 인증한다.
로그인에 성공하면 다음과 같이 출력된다. 

앱을 개발해보자. 
codex --approval-mode full-auto "create the fanciest todo-list app"

그럼 todo list app을 codex가 개발하는 것을 확인할 수 있다. 이외에 다음과 같은 프롬프트를 입력해보자. 
"create the ascii rendering web viewer using webcam"
"create 3D tetris using three.js"


마무리
점차 AI 에이전트가 우리가 사용하는 개인 기기(노트북, 스마트폰 등)에 침투하고 있다. MCP는 그 시작인 것이고, Codex는 AI OS 위치로 맵핑하고자 한다. 올해 더 큰 경쟁과 발전이 있으리라 생각된다. 아직 codex는 실행이 불완전하지만, 

레퍼런스

2025년 4월 12일 토요일

Gemini 기반 MCP 서버 및 클라이언트 개발해 보기

이 글은 Gemini 기반 MCP 서버 및 클라이언트를 개발하는 방법을 간략히 보여준다.

MCP 기반 멀티 AI 에이전트 아키텍처 개념도

MCP의 개념과 상세한 동작 방식은 다음 글을 참고한다.

개요
MCP는 클라이언트-서버 구조를 따른다. 클라이언트는 서버의 MCP 도구를 사용하는 AI 앱이나 LLM을 의미한다. 서버는 MCP 도구를 공급하고, API, 데이터소스 인터페이스를 제공한다. 

MCP를 통해 LLM이 해결하지 못하는 작업은 외부 시스템과 연결해 서비스 받을 수 있다.

MCP서버는 파일 시스템 조작, 웹 검색, 데이터베이스 조작, 버전 관리 등 다양한 도구를 제공할 수 있다. 

제미니 LLM 기반 MCP 구조
다음은 제미니 LLM 기반 MCP 구조 예시를 보여준다. 이 예는 비행기 예약 유스케이스를 구현한다.
구조의 각 번호는 시퀀스 시나리오를 보여준다. 이 내용은 다음과 같다.
  1. MCP 호스트가 사용자 명령 입력. 예) 내일 인천에서 애틀란타 가는 비행편 찾기
    1. 클라이언트 스크립트가 입력을 처리(CLIENT.PY)
  2. 클라이언트가 MCP 서버 프로세스 시작(MCP-FLIGHT-SEARCH). STDIO 통신 채널 연결 및 관련 도구 검색
  3. 클라이언트가 사용자 명령에 대한 함수 호출 방법을 수신함
  4. 클라리언트가 함수 호출 방법에 대한 정확한 함수 호출 형식을 GEMINI에서 획득. 함수 호출 형식에 부합하는 적절한 MCP 도구를 서버에 호출. 서버의 도구 함수 호출 결과를 리턴
  5. MCP 서버가 구글 항공편 검색을 위한 SerpAPI를 호출. 구글 항공편 데이터 질의.
  6. 구글 항공편 정보 리턴
  7. 서버에서 클라이언트로 해당 정보 리턴
  8. 클라이언트가 호스로 해당 정보 전달
개발 환경
개발을 위한 최소한의 환경은 파이썬 3.8+이다. 이외 다음을 준비한다.

다음 종속성을 터미널에서 설치한다. google-genai는 google 생성AI 라이브러리이며, mcp는 MCP 서버 통신을 위한 파이썬 SDK이다. 
pip install google-genai mcp

환경변수를 설정한다. 
export GEMINI_API_KEY="your-google-api-key"
export SERP_API_KEY="your-serpapi-key"

항공편 검색 MCP 서버 설치
MCP 프로토콜 공개 이후로 많은 MCP 서버가 개발되었다. 우리는 항공편 검색 MCP 서버 오픈소스인 mcp-flgiht-search 를 사용한다. 다음을 설치한다.
pip install mcp-flight-search

코딩해보기
다음과 같이 client.py를 코딩한다. 
import os, sys, time, asyncio
from google import genai
from google.genai import types
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from dotenv import load_dotenv

load_dotenv()

gemini_api_key = os.getenv("GEMINI_API_KEY")
serp_api_key = os.getenv("SERP_API_KEY")

client = genai.Client(api_key=gemini_api_key)

server_params = StdioServerParameters(
    command="mcp-flight-search",
    args=["--connection_type", "stdio"],
    env={"SERP_API_KEY": serp_api_key},
)

async def run():
    async with stdio_client(server_params) as (read, write): # 항공 예약 검색 도구 등록
        async with ClientSession(read, write) as session:
            prompt = f"Find Flights from Atlanta to Las Vegas 2025-08-15" # 사용자 질의 명령
            await session.initialize()

            mcp_tools = await session.list_tools() # 도구 리스트 획득
            tools = [
                types.Tool(
                    function_declarations=[
                        {
                            "name": tool.name,
                            "description": tool.description,
                            "parameters": {
                                k: v
                                for k, v in tool.inputSchema.items()
                                if k not in ["additionalProperties", "$schema"]
                            },
                        }
                    ] # 해당 도구 함수 선언 생성
                )
                for tool in mcp_tools.tools
            ]

            response = client.models.generate_content(
                model="gemini-2.5-pro-exp-03-25",
                contents=prompt,
                config=types.GenerateContentConfig(
                    temperature=0,
                    tools=tools,
                ), # LLM 모델에 프롬프트 전달.
            )

            if response.candidates[0].content.parts[0].function_call:
                function_call = response.candidates[0].content.parts[0].function_call # 함수호출정보

                result = await session.call_tool(
                    function_call.name, arguments=dict(function_call.args)
                ) # 도구 함수 호출

                print("--- Formatted Result ---") # Add header for clarity
                try:
                    flight_data = json.loads(result.content[0].text)
                    print(json.dumps(flight_data, indent=2))
                except json.JSONDecodeError:
                    print("MCP server returned non-JSON response:")
                    print(result.content[0].text)
                except (IndexError, AttributeError):
                     print("Unexpected result structure from MCP server:")
                     print(result)
            else:
                print("No function call was generated by the model.")
                if response.text:
                     print("Model response:")
                     print(response.text)

asyncio.run(run()) # 클라이언트 실행

실행한다. 그럼 프롬프트에 대해 LLM이 적절한 도구와 파라메터를 확인해 함수 호출 정보를 생성한다. 이를 call_tool로 호출한 결과가 표시된다 

레퍼런스


2025년 4월 4일 금요일

Gradio HTML Javascript 렌더링 방법

이 글은 Gradio HTML Javascript 렌더링 방법을 간략히 정리한다. 가끔, HTML, javascript(자바스크립트)를 Gradio 페이지에 표시하고 싶을 때가 있다. 예를 들어, Gradio 기존 컴포넌트에서 지원되지 않는 3D 그래픽을 렌더링해야 할 때가 있다.
Gradio 3차원 모델 렌더링 예시

하지만, Gradio는 HTML만 지원하고, javascript는 다양한 이유로 사용자가 직접 Gradio에서 실행하는 것을 허용하지 않는다. 이 경우, iframe 을 사용한다.
Gradio에서 three.js 코드 실행 예시

다음은 Gradio에서 3D graphic 렌더링을 위해 three.js를 사용하는 예를 보여준다.
import os, json, requests, gradio as gr
from gradio import Interface, File, Files, Button, Label, Markdown

class 3D_viewer_component:
    def __init__(self, gr):
        self.html_code = """
<iframe srcdoc="
<!DOCTYPE html>
<html>
  <head>
    <style>
      body, html { margin: 0; overflow: hidden; height: 100%; }
      canvas { display: block; }
    </style>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js'></script>
  </head>
  <body>
    <script>
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(WIDTH, HEIGHT);
renderer.setClearColor(0x111111, 1);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(70, WIDTH / HEIGHT, 0.1, 10000);
camera.position.z = 30;
camera.position.y = 10;
scene.add(camera);

const boxGeometry = new THREE.BoxGeometry(10, 10, 10);
const basicMaterial = new THREE.MeshBasicMaterial({ color: 0x0095dd });
const cube = new THREE.Mesh(boxGeometry, basicMaterial);
cube.position.x = -25;
cube.rotation.set(0.4, 0.2, 0);
scene.add(cube);

const torusGeometry = new THREE.TorusGeometry(7, 1, 16, 32);
const phongMaterial = new THREE.MeshPhongMaterial({ color: 0xff9500 });
const torus = new THREE.Mesh(torusGeometry, phongMaterial);
torus.rotation.set(0.5, 0.5, 0);
scene.add(torus);

const light = new THREE.PointLight(0xffffff);
light.position.set(-10, 15, 50);
scene.add(light);

let t = 0;
function render() {
  t += 0.01;
  requestAnimationFrame(render);
  cube.rotation.y += 0.01;
  torus.scale.y = Math.abs(Math.sin(t));
  renderer.render(scene, camera);
}
render();
    </script>
  </body>
</html>
" width="100%" height="100%" style="border:none;"></iframe>
        """

        self.component = gr.HTML(self.html_code)

with gr.Blocks(title='3D viewer') as interface:
gr.Markdown("# 3D viewer")
with gr.Row(equal_height=True):
3D_viewer_component(gr)
3D_viewer_component(gr)

interface.launch(share=True)

참고로, Gradio에서 공식적으로 추천하는 방법은 다음과 같이 커스텀 컴포넌트를 개발한 것이다.
커스텀 컴포넌트 생성 명령 예시
명령 실행 순서(일부)

위 그림과 같이 컴포넌트 생성하면, 다음처럼 프로젝트 파일이 생성되고, 이를 수정해 개발하는 방식이다.
- backend/ <- The python code for your custom component
- frontend/ <- The javascript code for your custom component
- demo/     <- A sample app using your component. Modify this!
- pyproject.toml <- Used to build the package and specify package metadata.
Gradio 커스텀 컴포넌트 예시(Gradio Custom Components Gallery)

이외 3차원 모델 파일 뷰어는 Model3D 컴포넌트를 사용할 수도 있다.
Model3D 예시

레퍼런스

2025년 3월 29일 토요일

딥시크(deep seek) 오픈소스 코드 및 구조 분석하기

앞서 AI에이전트 오픈 미노스를 분석해 보았는 데, 내친김에 그 동안 말많았던 딥시크(deep seek)를 분석해 보기로 한다. 개인적으로 언론의 기사를 잘 믿지는 않는다. 홍보성 퍼나른 기사가 많기도 하고, 특히 테크분야에서도 약장사분들? 많아, 어디까지 진실인지 아닌지 모르기 때문이다. 정말 대단한지, 아니면 지재권 완전 무시하고 기술 탈취?해 싼 제품 잘 만드는 중국 방식으로 개발된 것인지 알려진 자료에 기반해 팩트 확인해 보고자 한다.
딥시크 (theconversation.com, 2025)

참고로, 마누스 오픈소스에 관심 있다면 다음 링크를 참고한다. 

딥시크 분석 준비
다음 링크를 방문해 코드를 클론한다.
터미널에서 다음 명령을 실행한다. 
git clone https://github.com/deepseek-ai/DeepSeek-V3.git

폴더 및 파일 구조 분석 
vscode를 띄워 보니 폴더 구조는 다음과 같다. 
  - workflows: 워크플로우 관련 폴더
  - inference: 하위 폴더
    - configs: 설정 파일 폴더
    - convert.py: Python 파일, 4KB
    - fp8_cast_b16f.py: Python 파일, 5KB
    - generate.py: Python 파일, 8KB
    - kernel.py: Python 파일, 9KB
    - model.py: Python 파일, 8KB
    - requirements.txt: 텍스트 파일, 1KB


공개된 소스 코드를 보니 굳이 실행을 위한 패키지 설치는 할 필요 없을 것 같다. 일단, 오픈소스가 아니다. 단순히, inference 추론 코드만 공개되어 있다(어그로). 오픈소스라면, 최소한, train 학습 코드와 기본 데이터셋 정도는 공개되어야 한다(언론이 왜 오픈소스라 난리였는지 사실 이해가 안되는...). 

코드는 대부분 트랜스포머 구조를 그대로 사용한다. 트랜스포머스는 구글에서 이미 2017년 개발 공개된 LLM 모델 학습 메커니즘이다. 딥시크에서 사용한 상세한 기술을 알고 싶다면, 다음 링크를 참고하길 바란다.

소스 코드 분석
공개된 코드는 주로 추론을 위해 딥시크 측에서 공개한 학습된 가중치 모델 파일을 로딩해 사용하기 위한 model.py, 양자화로 성능 가속을 위한 fp8_cast_bf16.py, 질의 프롬프트에 대한 모델 출력을 생성하는 generate.py 정도가 분석할 필요가 있어 보인다. 

model.py 분석
일단, 사용하는 라이브러리 대부분이 미국 빅테크 기업 혹은 연구소에서 개발된 것들을 임포트에 사용하고 있다. 이 분야에서 모르면 간첩인 pytorch, 심지어 triton은 NVIDIA GPU 가속 최적화를 위해 사용하는 라이브러리를 직접 사용한다(NVIDIA 종속적). 

모델의 전체 구조는 트랜스포머를 그대로 따르며, 이미 오픈소스 공개된 코드에 나오는 키워드도 다음처럼 그대로 사용하고 있다.

model.forward를 확인해보자. 일반적인 트랜스포머스 forward 루틴으로 보여진다. 
    def forward(self, tokens: torch.Tensor, start_pos: int = 0):
        seqlen = tokens.size(1)
        h = self.embed(tokens)  # 1) 입력 임베딩
        freqs_cis = self.freqs_cis[start_pos:start_pos+seqlen]  # 2) # 위치 인코딩
        mask = None
        if seqlen > 1:              # 3) 마스킹
            mask = torch.full((seqlen, seqlen), float("-inf"), device=tokens.device).triu_(1)
        for layer in self.layers:   # 4) 트랜스포머스 레이어 실행 계산
            h = layer(h, start_pos, freqs_cis, mask)
        h = self.norm(h)[:, -1]   # 5) 출력 정규화
        logits = self.head(h)     # 6) 로짓 계산
        if world_size > 1:
            all_logits = [torch.empty_like(logits) for _ in range(world_size)]
            dist.all_gather(all_logits, logits)
            logits = torch.cat(all_logits, dim=-1)
        return logits

이 함수는 입력 토큰을 받아 로짓(예측값)을 계산하는 역할을 한다. 예측값은 미리 준비된 사전에서 예측된 단어를 선택할 때 역할을 한다(입력 토큰에 대한 다음 토큰 예측 생성과정). 주요 동작은 다음과 같다.
  1. embed: 입력 임베딩 처리: 입력으로 제공된 토큰 텐서를 임베딩 레이어를 통해 변환하여 초기 입력 표현을 생성한다. 
  2. freqs_cis: 로터리 임베딩 계산:  주파수 정보를 담고 있는 로터리 임베딩 텐서를 시퀀스 길이에 맞게 선택하여, 위치 정보를 모델에 제공한다. 위치임베딩이란 기법도 이미 트랜스포머스 논문(Google, 2017)에 구현된 것이다.
  3. mask: 시퀀스 길이가 1보다 클 경우, 미래 정보가 영향을 미치지 않도록 상삼각형 형태의 마스크를 생성한다. 마스크는 모델이 언어 생성 시 현재 시점 이전의 정보만을 활용하게 한다.
  4. Transformer 레이어 통과: 모델 내부의 여러 Transformer 레이어를 입력 데이터가 순차적으로 통과하며, 각 레이어에서 입력 표현이 갱신된다. 
  5. norm: 출력 정규화 및 최종 표현 추출. 마지막 Transformer 레이어의 출력을 정규화하고, 시퀀스의 마지막 토큰에 해당하는 표현을 추출한다. 
  6. logits: 로짓 계산. 추출된 최종 표현을 출력 레이어(헤드)에 전달하여 로짓, 즉 예측값을 계산한다. 이는 각 토큰에 대한 다음 단어 또는 출력값의 확률 분포를 나타낸다.
결과적으로, 이 메서드는 입력 토큰 시퀀스를 기반으로 각 토큰에 대한 예측값을 반환하며, 이는 주로 언어 모델링 및 자연어 처리 작업에 활용된다.

모델의 전체 구조는 다음과 같다. 대부분 파이토치 기반 트랜스포머스 코드(이미 많이 공개된 코드 조각)를 사용한다(딥시크 처음 언론 보도가 실제로 얼마나 차이가 있는 지 확인할 수 있음).
딥시크-V3 모델 구조(UML)

다만, 실행 속도 등 최적화를 위해 병렬처리, torch.einsum 함수를 이용해 GPU 연산을 직접 이용해 트랜스포머 어텐션 모델 QKV 코사인 유사도 계산하는 등의 노력을 하고 있다(이 또한 이미 알려진 것).
토큰 시퀀스 임베딩 벡터 간 유사도 스코어 계산 및 학습하는 부분(일부. 트랜스포머스 모델의 전형적인 루틴임. 여기서 bshd는 batch, source seqnce, heads, feature demension 의 약자로 입력 텐서의 모양-차원을 정의함)

kernel.py 분석
이 모듈은 주로 성능과 관련된 양자화를 다룬다. triton 라이브러리를 사용해 주어진 텐서를 양자화하여, 32비트 실수 연산을 8비트 실수 연산으로 처리할 수 있게 한다. 8비트 텐서 실수 연산을 위한 함수도 같이 구현되어 있다. 예를 들어, 다음 fp8_gemm 함수는 8비트 양자화된 a, b 텐서를 입력받아 행렬곱한 후 c를 리턴한다.

convert.py 분석
이 모듈은 모델 파일 포맷을 주어진 옵션에 맞게 단순히 체크포인트 파일로 변환하는 역할을 한다. 복잡한 내용은 별로 없어 상세 설명은 생략한다.

configs 파일 분석
이 폴더 내 모델의 구조를 정의하는 주요 변수가 정의되어 있다. 예를 들어, config_16B.json 파일은 다음과 같이 정의된다. 
의미는 다음과 같다. 
1. 모델 구조  
   - vocab_size: 어휘 크기 (102,400).  
   - dim: 임베딩 차원 (2048).  
   - inter_dim: FFN의 확장 차원 (10,944).  
2. MoE 관련  
   - n_routed_experts: 총 Expert 수 (64).  
   - n_activated_experts: 활성화 Expert 수 (6).  
   - moe_inter_dim: MoE Expert의 내부 FFN 차원 (1,408).  
3. Attention 관련  
   - n_layers: Transformer 레이어 수 (27).  
   - n_heads: Attention Head 수 (16).  
   - kv_lora_rank: 키/값 벡터의 LoRA 랭크 (512).  
   - qk_nope_head_dim: NOPE 기반 헤드 차원 (128).  
   - qk_rope_head_dim: RoPE 기반 헤드 차원 (64).  
4. 기타  
   - mscale: 모델 안정성을 위한 스케일 값 (0.707).  

분석해 본 결과, 사실 LLM에서 모델 구조, 추론 구현 부분 및 양자화 모듈만 대부분 공개되어 있다는 것을 알 수 있다. 

최소한 데이터셋 모듈이라도 공개되어야 어떤 식으로 데이터를 학습했는지 확인할 수 있지만, 이런 중요 모듈은 공개되어 있지 않아, 딥시크-V3는 오픈소스를 공개했다고 말하기 어렵다(가중치와 모델 모듈만 오픈. 이건 reddit에서도 까이고 있는데, 그냥 오픈웨이트 open weights 모델 코드임).

더 파보기
오픈소스라기에는 좀 실망이라, 무언가 더 없는 지 공개된 자료들을 파보기로 한다. 일단, 딥시크 개발사 github에서 최근 주목받고 있는 프로젝트만 다음처럼 정렬해본다.

이 중 체크한 부분이 먼가 있는 듯 하여, 들어가 확인해 본다. 
MIT 라이센스라 표시만 되어 있지, 코드가 없음
딥시크 기반 코더는 평가, 데모 코드만 있고, 파인튠은 학습 데이터셋 제공 없음 
평가 코드만 있음(MIT 라이센스 표시만. 오픈소스? 무슨 의미가?)
딥시크-V3 학습모델(가중치파일) 공개된 부부(허깅페이스)

더 파보았지만, 딥시크에서 주장하는 것은 오픈소스가 아닌 오픈웨이트 모델에 더 가까워보인다. 세계적인 홍보와 언론의 관심에 비해 무늬만 MIT라이센스 오픈소스가 아닌지 의문이다.

마무리
좀 시간을 내어 분석한 후, 확인 사살한 것은 다음과 같다. 
  1. 언론에서 말하는 것과는 상당한 차이가 있는 딥시크 기술 오픈소스였다. 대부분 이미 개발된 오픈소스를 가져다 쓴 것으로 보인다. 앞에 언급한 몇몇 성능 최적화 부분은 좋은 접근인 것이나, 메타(페북)의 라마(Llama)가 공개한 기술에 비하면 비교할 만한 것이 아니다.
  2. 중국은 확실히 홍보(x10배)에 천재적인 능력(약팔이)이 있다(진심으로). 
  3. 중국이 잘하는 선진국(미국) 기술 가져와 자국것으로 포장해 저가로 파는 기술은 세계 최고다. 
의문점은 한국 언론에서 다음과 같이 패닉성 기사를 쓸 때, 왜 남이 말한 것 받아 만 쓰고 팩트 확인하지 않았냐는 것인데... 좀 생각해보니 국내 딥시크 기사는 관련 컨텐츠를 해외에서 퍼온 검증도 안된 글을 기사로 정리한 것으로 이해된다. 사실, 첨단 기술을 팩트 체크할 리소스가 있는 언론이 많지는 않다고 생각한다. 그럼에도 파급격있는 채널은 뉴스를 전할때 항상 팩트 확인하려는 노력이 필요하다.
패닉성 딥시크 언론 보도(연합, 2025.1.27, 뉴스튜데이, 2025.3.28)

자극적 기사들로 얼마 전 정부 국회는 패닉되고, 급하게 만든 인공지능 진흥전략이 판을 치게 된 트리거 역할을 했다. 여론은 인공지능분야도 우리가 중국에 뒤쳐졌다는 것을 확인하는 계기는 되었다. 긍정적 효과도 있었다고 생각되나, 부작용도 있다. 예를 들어,불필요한 일들이 벌어지고(갑작스런 GPU전수조사? 등), 갑작스런 대규모 GPU 센터 개발 계획이 발표되고, 이로 인해 세금이 비합리적으로 계획 투입되고, .. 이상한 방향으로 국가 첨단기술 연구개발 전략이 설정되고... 대규모 세금이 투입되지 않을 까라는 생각을 들게 만든다.

이 글에서 딥시크가 오픈소스 맞는지(거짓), 정말 중국 독자 기술로 개발했는지(거짓), 자국 GPU 사용해 개발했는 지(거짓. 기껏 추론 부분만 Google TPU같이 NPU 사용했을 가능성), 기술적 개선이 있었는지(성능 최적화 부분은 약간 인정), 정말 공개한 것은 무엇인지(모델만. 오픈웨이트) 등의 질문을 확인해 보았다. 

이 상황이면, GPT 학습 데이터를 증류해(카피해) 모델을 학습했다는 것이 더 신빙성 있어 보인다(가성비있게 실리콘밸리 테크 기술을 카피해 싸게 소프트웨어를 개발했다는 쪽이 더 맞는 듯. 물론 이것도 아무나 할 수 있는 건 아니다). 좀 더 시간이 있으면, 허깅페이스에 공개된 내용을 분석할 계획이다.
레퍼런스