728x90

Python 생태계에서 패키지 관리와 가상환경 설정은 필수입니다. 그동안 pipenv, poetry와 같은 도구들이 널리 쓰였지만, 최근엔 극강의 속도와 단순함을 자랑하는 새로운 패키지 매니저 uv가 주목받고 있습니다.

Python 패키지 매니저 비교: pipenv, poetry, uv

기능 / 도구 pipenv poetry uv (by Astral)
가상환경 관리 내장 (자동 생성) 내장 (PEP 582 미지원) 내장 (자동 venv 생성 및 활성화)
설치 속도 느림 보통 매우 빠름 (Rust 기반)
의존성 해결 pipfile.lock 기반 pyproject.toml + poetry.lock 사용 pyproject.toml, uv.lock 지원
lockfile 지원
CLI 편의성 제한적 (pip + virtualenv wrapper) 직관적, 다양한 명령어 지원 심플한 CLI (uv pip, uv venv 등)
커뮤니티 및 안정성 오래된 프로젝트, 유지 보수 적음 성숙한 프로젝트, 인기 많음 신생 도구, 빠르게 성장 중

실사용 후기 (개인 기준)

  • pipenv : 작은 프로젝트에선 문제없지만, 복잡한 의존성에서는 종종 충돌 발생. 속도도 느림.
  • poetry : 문법도 직관적이고, 대규모 프로젝트에 적합. 다만, 설치 속도가 아쉽고 느릴 때가 있음.
  • uv : 설치 속도와 가벼움은 진짜 최고. Rust 기반이라 poetry보다 훨씬 빠르고, pip 대비 의존성 해결도 안정적.

결론: 속도와 간결함이 필요한 경우 uv가 최적. 대규모 프로젝트에서 신뢰성과 커뮤니티를 원한다면 poetry.

💻 macOS에 uv 설치하기 (with Homebrew)

1. 설치 명령어

brew install uv

2. 설치 확인

uv --version

⚙️ uv 기본 사용법

uv는 poetry 와 비슷하게 잠금 파일, 작업 공간 등을 지원하여 프로젝트 종속성과 환경을 관리합니다.

uv init 명령을 사용하여 새로운 Python 프로젝트를 만들 수 있습니다.

uv init uv-test
cd uv-test

uv는 다음 파일을 생성하며, 기본적인 파이썬 프로젝트 구조를 생성하게 됩니다.

.
├── .git
├── .gitignore
├── .python-version
├── main.py
├── pyproject.toml
└── README.md

main.py 파일에는 간단한 "Hello world" 프로그램이 포함되어 있습니다. uv run 명령어로 실행합니다.

uv run main.py

(결과)
Creating virtual environment at: .venv
Hello from uv-test!

uv run 을 실행하면, 프로젝트 내부에 파이썬 가상환경 .venv 가 자동으로 생성되며, 파이썬 프로그램이 실행됩니다.

📦 프로젝트 예시 구조

.
├── .git
├── .gitignore
├── .python-version
├── .venv
├── main.py
├── pyproject.toml
├── README.md
└── uv.lock

pyproject.toml 에는 poetry의 종속성 관리와 같이 프로젝트에 대한 메타데이터가 포함되어 있습니다.
pyproject.toml 을 사용하여 종속성은 물론, 프로젝트 설명이나 라이선스와 같은 세부 정보를 관리할 수 있고, uv adduv remove 명령을 사용하여 프로젝트를 관리할 수 있습니다.

🚀 마무리하며

Python의 패키지 관리 도구는 점점 더 다양해지고 있습니다. uv는 빠른 설치, 심플한 CLI, 강력한 의존성 해결을 통해 새로운 대안으로 부상하고 있습니다.
특히, poetry가 제공하는 워크플로우를 그대로 가져오면서도 훨씬 가볍고 빠르기 때문에, 개발 속도와 효율을 중요하게 여기는 개발자라면 꼭 한번 경험해보길 추천합니다.

Reference

728x90
반응형
728x90

🔍 ElevatedButton이란?

ElevatedButton은 그림자(shadow)가 있는 입체감 있는 버튼으로, Material Design 스타일을 따릅니다.

📙 버튼 스타일 종류 - Material Design Buttons

(코드 샘플)

ElevatedButton(
  onPressed: () {
    print('버튼이 눌렸어요!');
  },
  child: Text('눌러보세요'),
)

(결과)

🧱 기본 구조

ElevatedButton의 생성자는 아래와 같은 구조를 가집니다:

ElevatedButton({
  required VoidCallback? onPressed,
  VoidCallback? onLongPress,
  ButtonStyle? style,
  FocusNode? focusNode,
  bool autofocus = false,
  Clip clipBehavior = Clip.none,
  required Widget child,
})
  • onPressed: 버튼이 눌렸을 때 호출되는 콜백 (null이면 버튼 비활성화)

  • child: 버튼 내부에 표시될 위젯 (보통은 Text)

  • style: 버튼의 색상, 테두리, 그림자 등을 커스터마이징 가능

🎨 스타일 커스터마이징

style 속성을 사용하면 버튼을 다양하게 꾸밀 수 있습니다.

ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.blue,       // 배경색
    foregroundColor: Colors.white,      // 텍스트 & 아이콘 색
    shadowColor: Colors.black,          // 그림자 색
    elevation: 8,                        // 버튼 입체감
    padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(16),
    ),
    textStyle: TextStyle(
      fontSize: 18,
      fontWeight: FontWeight.bold,
    ),
  ),
  child: Text('스타일 적용된 버튼'),
)

(결과)

728x90
반응형

'Language > Flutter' 카테고리의 다른 글

(Flutter) ListView Widget  (1) 2025.03.31
(Flutter) Scaffold class  (0) 2025.03.25
728x90

Flutter 앱을 개발하면서 가장 자주 사용 되는 UI 요소 중 하나가 바로 스크롤 가능한 리스트, ListView 입니다.

📌 ListView란?

ListView는 Flutter에서 수직 혹은 수평으로 스크롤 가능한 리스트 뷰를 제공하는 위젯 입니다. 빌더 패턴을 활용해 동적으로 아이템을 생성할 수 있습니다.

🧱 ListView의 주요 생성자

1. ListView()

  • 고정된 children 리스트를 직접 전달.
    • 단점: 많은 항목이 있으면 성능 저하 발생 (모든 위젯을 메모리에 생성)
ListView(
  children: [
    Text('Item 1'),
    Text('Item 2'),
    Text('Item 3'),
  ],
)

(결과)

2. ListView.builder()

  • 스크롤할 때마다 위젯을 생성하는 방식으로 성능에 유리.
  • 항목이 많거나 동적으로 생성되는 경우에 필수.
ListView.builder(
  itemCount: 100,
  itemBuilder: (context, index) {
    return ListTile(title: Text('Item $index'));
  },
)

(결과)

3. ListView.separated()

  • 각 항목 사이에 구분선이나 위젯을 넣고 싶을 때 사용.
ListView.separated(
  itemCount: 10,
  itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
  separatorBuilder: (context, index) => Divider(), // 구분선
)

(결과)

4. ListView.custom()

  • 고급 커스터마이징이 필요한 경우 사용.
  • SliverChildDelegate를 통해 직접 동작 방식 정의 가능.

📏 주요 속성

속성 설명
scrollDirection 기본은 Axis.vertical. 수평 스크롤은 Axis.horizontal로 설정
reverse true로 설정 시 리스트의 순서를 반대로
controller 스크롤을 제어하기 위한 ScrollController 연결
shrinkWrap true로 설정하면 남은 공간만 차지 (Nested Scroll 시 유용)
physics 스크롤 동작 제어 (BouncingScrollPhysics, NeverScrollableScrollPhysics 등)

🧠 주의할 점 & 팁

✅ 성능 최적화

  • 많은 항목이 있을 경우에는 반드시 ListView.builder() 사용.
  • shrinkWrap: true는 필요한 경우에만, 일반 상황에선 성능 저하 가능 (예: 다른 스크롤 뷰 안에 ListView 포함할 때).
  • physics: NeverScrollableScrollPhysics() 로 스크롤 비활성화 가능.

✅ 자동 스크롤 위치 유지

  • controller를 사용해 스크롤 위치 제어 가능.
    • 예: ScrollController, jumpTo(), animateTo() 등 활용.
728x90
반응형

'Language > Flutter' 카테고리의 다른 글

(Flutter) ElevatedButton Class  (0) 2025.04.03
(Flutter) Scaffold class  (0) 2025.03.25
728x90

📌 Scaffold 클래스란?

Scaffold는 Flutter에서 화면을 구성할 때 기본적인 뼈대를 제공하는 위젯(Widget)입니다. Material Design 앱의 기본 구조를 쉽게 구성할 수 있도록 도와줍니다.

📦 Scaffold 주요 속성

  • appBar: 상단에 위치하는 앱 바
  • drawer: 왼쪽 슬라이드 메뉴
  • bottomNavigationBar: 하단 내비게이션 바
  • floatingActionButton: 플로팅 액션 버튼
  • body: 화면의 메인 콘텐츠 영역
  • resizeToAvoidBottomInset : 키보드가 뜰 때 body가 자동으로 리사이즈될지 여부
  • backgroundColor : 배경색 지정

💡 예제 확인

샘플로 로컬 프로젝트를 생성 하려면:

flutter create --sample=material.Scaffold.1 mysample

( 코드 내용 )

import 'package:flutter/material.dart';

/// Flutter code sample for [Scaffold].

void main() => runApp(const ScaffoldExampleApp());

class ScaffoldExampleApp extends StatelessWidget {
  const ScaffoldExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: ScaffoldExample());
  }
}

class ScaffoldExample extends StatefulWidget {
  const ScaffoldExample({super.key});

  @override
  State<ScaffoldExample> createState() => _ScaffoldExampleState();
}

class _ScaffoldExampleState extends State<ScaffoldExample> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample Code')),
      body: Center(child: Text('You have pressed the button $_count times.')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() => _count++),
        tooltip: 'Increment Counter',
        child: const Icon(Icons.add),
      ),
    );
  }
}

이 예제는 appBarbody로 구성되어 FloatingActionButton이 있는 Scaffold를 보여줍니다. FloatingActionButton 은 클릭 시 카운터를 증가시키도록 콜백 처리 됩니다.

결론

Flutter를 공부하는 입장에서 Scaffold는 매 화면을 구성할 때 출발점이 되는 위젯이니, 완벽하게 이해하고 넘어가는 것이 좋습니다.

Scaffold 없이 CustomScrollView 같은 걸로 직접 구성도 가능하지만, 처음에는 Scaffold를 적극 활용하는 것이 개발 생산성도 좋고 UI 구현도 훨씬 빠릅니다.

Reference

728x90
반응형

'Language > Flutter' 카테고리의 다른 글

(Flutter) ElevatedButton Class  (0) 2025.04.03
(Flutter) ListView Widget  (1) 2025.03.31
728x90

🧩 Flutter UI의 핵심 철학 : 모든 것이 위젯이다!

Flutter를 처음 접하면 가장 먼저 듣게 되는 말이 있습니다.

“ Flutter는 모든 것이 위젯이다! ”

Flutter에서 UI를 구성하는 모든 요소는 위젯(Widget) 입니다.

그 예시를 볼까요?

요소 Flutter에서의 위젯
버튼 ElevatedButton
텍스트 Text
이미지 Image
정렬 Row, Column
레이아웃 여백 Padding, SizedBox
페이지 구조 Scaffold, AppBar

심지어 Padding, Margin, 애니메이션도 전부 위젯입니다!


🧱 위젯의 종류: Stateless vs Stateful

유형 설명 대표 예시
StatelessWidget 상태를 가지지 않음 Text, Icon, Row
StatefulWidget 상태 변화가 있음 TextField, Switch, Scaffold 등
  • StatelessWidget : 값이 변하지 않는 정적인 화면 요소
  • StatefulWidget : 입력에 따라 바뀌는 동적인 화면 요소

화면에 입력창이나 버튼처럼 상호작용이 필요하다면 StatefulWidget이 쓰입니다.

🎯 UI는 어떻게 구성할까?

Flutter는 XML이나 Storyboard 없이, 코드로 UI를 직접 구성합니다.

즉, 위젯들을 조합하고 중첩하여 트리 구조(Tree) 를 만드는 방식입니다.

💡 예제 코드

Scaffold(
  appBar: AppBar(title: Text('Hello')),
  body: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('Welcome!'),
        ElevatedButton(
          onPressed: () {
            print('Clicked!');
          },
          child: Text('Click'),
        ),
      ],
    ),
  ),
);
  • Scaffold는 페이지 전체 레이아웃을 담당
    • Scaffold는 Material Design의 시각적 Layout 구조를 표현함.
  • AppBar, Text, Button 모두 위젯
  • Center, Column은 레이아웃을 배치하는 위젯

이런 식으로 위젯 안에 또 다른 위젯이 들어가는 구조로 UI가 만들어집니다.

728x90
반응형
728x90

1. Cursor AI

Cursor AI는 AI 기반의 코드 편집기로, 개발자가 코드를 보다 빠르고 효율적으로 작성할 수 있도록 도와주는 도구입니다. 기존의 GitHub Copilot과 같은 AI 보조 도구와 유사하며, 강력한 코드 자동 완성 기능과 코드 리뷰 및 리팩토링 기능을 제공합니다.

Cursor AI의 주요 기능

  • AI 기반 코드 자동 완성: 코드의 문맥을 이해하고 적절한 제안을 제공

  • 코드 리뷰 및 리팩토링: 기존 코드의 품질을 높이도록 도와줌

  • 내장형 터미널 및 IDE 기능: VS Code와 유사한 환경에서 동작

  • 대화형 AI 개발 지원: ChatGPT를 활용한 코드 설명 및 개선 제안

2. MacBook에 Cursor AI 설치하기

2.1. Homebrew를 사용한 설치 (권장)

Homebrew를 사용하면 간편하게 Cursor AI를 설치할 수 있습니다.

brew install --cask cursor

설치가 완료되면 cursor 명령어를 실행하여 정상적으로 동작하는지 확인합니다.

cursor --version

2.2. 공식 웹사이트에서 설치

Homebrew를 사용하지 않는다면 Cursor AI 공식 웹사이트에서 설치 파일을 다운로드할 수 있습니다.

  1. 웹사이트 접속 후 Mac용 설치 파일 다운로드

  2. .dmg 파일을 실행하여 응용 프로그램 폴더에 드래그

  3. 실행 후 초기 설정 진행

Cursor AI 활용하기

Cursor AI를 실행하고, 주로 작성하는 언어는 영어보다는 한국어를 사용이 많을 것임으로 한국어를 설정합니다.
터미널에서 Cursor AI를 사용하고자 하는 경우 Installed "cursor" 선택하여 설치합니다.

VS Code를 사용중이라면 USE Extensions 버튼을 클릭 합니다. 버튼을 클릭하면, VS Code에 설치된 Extension을 그대로 Cursor AI에 적용하게 됩니다.

데이터 보호 설정은 Privacy Mode로 하고, Continue 를 클릭합니다.

Curosor AI 계정이 있으면 그대로 로그인을 진행하고, 계정이 없는 경우 회원가입을 합니다.

소스코드를 관리하는 루트 디렉토리를 Open project 로 해당 폴더를 오픈하면, 하위 디렉토리 모두 불러와서 작업을 할 수 있습니다.

Cursor AI를 활용하여 개발 속도를 향상시키고, 코드 품질을 개선하는 데 도움이 되길 바랍니다!

728x90
반응형
728x90

Time Travel

Time Travel 기능은 대화형 에이전트 개발 시 과거의 대화 상태로 되돌아가거나 대화 경로를 재탐색할 수 있는 도구로 에이전트의 의사 결정 과정을 분석하고, 다양한 시나리오를 테스트하며, 대화 흐름을 최적화할 수 있습니다.

Time Travel 기능의 주요 이점:

대화 흐름 분석: 과거의 대화 상태로 돌아가 에이전트의 반응과 의사 결정 과정을 상세히 검토할 수 있습니다.

시나리오 테스트: 다양한 대화 경로를 탐색하여 에이전트의 반응을 테스트하고 개선할 수 있습니다.

버그 수정 및 최적화: 특정 시점으로 되돌아가 문제를 재현하고 수정하여 에이전트의 성능을 향상시킬 수 있습니다.

Implementation

이전 코드는 질의가 발생하면, human_assistance 툴(Tool)에서 interrupt가 발생되도록 되어 있으므로, 해당 툴을 거치지 않도록 툴 등록을 해제 합니다.

# tools.py


# 도구 목록
tools = [TavilySearchResults(max_results=2)]

1. 그래프(Graph) 실행

get_state_history() 메서드를 사용하여 특정 스레드의 상태 이력을 가져올 수 있습니다. 스냅샷을 통해 원하는 시점으로 되돌아가거나 대화 경로를 재 탐색할 수 있습니다.

다음은 두개의 질의를 전달 한 후, 특정 영역의 state를 다시 실행할 수 있도록 지정합니다. (여기서는 6번째 tool 호출 결과 내역)

# agent.py

# 그래프 실행
if __name__ == "__main__":

    config = {"configurable": {"thread_id": "USER_06"}}
    user_input = {"type": "user", "content": ("My Name is K. I'm learning LangGraph."
                  "Could you do some research on it for me?")}

    for event in graph.stream({"messages": [user_input]}, config, stream_mode="values"):
        event["messages"][-1].pretty_print()


    user_input = {"type": "user", "content": ("I would like to build chatbot agents using LangGraph.")}

    for event in graph.stream({"messages": [user_input]}, config, stream_mode="values"):
        event["messages"][-1].pretty_print()


    to_replay = None
    for state in graph.get_state_history(config):
        print("Num Messages: ", len(state.values["messages"]), "Next: ", state.next)
        print("-" * 80)
        if len(state.values["messages"]) == 6:
            # We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state.
            to_replay = state

    print(to_replay.next)
    print(to_replay.config)

(결과)


# ~~ 생략 ~~

Num Messages:  8 Next:  ()
--------------------------------------------------------------------------------
Num Messages:  7 Next:  ('chatbot',)
--------------------------------------------------------------------------------
Num Messages:  6 Next:  ('tools',)
--------------------------------------------------------------------------------
Num Messages:  5 Next:  ('chatbot',)
--------------------------------------------------------------------------------
Num Messages:  4 Next:  ('__start__',)
--------------------------------------------------------------------------------
Num Messages:  4 Next:  ()
--------------------------------------------------------------------------------
Num Messages:  3 Next:  ('chatbot',)
--------------------------------------------------------------------------------
Num Messages:  2 Next:  ('tools',)
--------------------------------------------------------------------------------
Num Messages:  1 Next:  ('chatbot',)
--------------------------------------------------------------------------------
Num Messages:  0 Next:  ('__start__',)
--------------------------------------------------------------------------------

{'configurable': {'thread_id': 'USER_06', 'checkpoint_ns': '', 'checkpoint_id': '1efe9bc1-fa9d-66aa-8006-85f90316985e'}}

메시지의 내용은 생략하고, 이후 결과들을 확인하면 6번째 호출은 도구를 호출 한 결과로써 to_replay.config 에는 checkpoint_id가 존재 하고 해당 id를 통해서 LangGraph의 체크포인터가 해당 시점의 state를 다시 가져올 수 있습니다.

checkpoint_id로 langsmith에서 확인할 수 있습니다.

Reference

728x90
반응형
728x90

Customizing State

LangGraph는 모델과 툴의 상태를 State로 정의하고, 이를 통해 상태의 변화를 추적합니다.
State의 변화 관리를 통해서 프로젝트의 요구 사항에 맞춰 상태를 효율적으로 관리하고 추적할 수 있게 해줍니다. 이를 통해 다양한 노드와 툴(Tool)이 상호작용하는 과정에서 발생할 수 있는 문제를 예방하고, 더 나은 사용자 경험을 제공할 수 있습니다.

Implementation

여기서는 이전 Agent의 내용을 바탕으로 챗봇이 검색 도구를 사용하여 특정 정보를 찾고 검토를 위해 사람에게 전달하는 시나리오를 구현합니다.

1. State에 키 추가

특정 정보를 찾아 State의 수정하기 위해서, 필요한 정보를 용이하게 접근할 수 있도록 state에 key 값을 추가합니다. 여기서는 namebirthday 키 값을 추가합니다.

# state.py


from typing import TypedDict, Annotated, Sequence
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage

class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], add_messages]
    name: str
    birthday: str

2. State 업데이트

LangGraph에서 Human-in-the-loop 상호작용을 위한 human_assistance 도구를 다음과 같이 개선합니다.

✅ Call ID 기반의 State 업데이트

  • Call ID를 활용하여 업데이트할 정확한 State를 찾도록 수정
  • 각 실행 인스턴스를 구분하고 올바른 State 업데이트 보장

✅ 응답 정확도 판별 및 반환 값 변경

  • y로 시작하는 응답("y")이면 결과를 반환, 아닌 경우에는 수정된 답변으로 반환
  • 응답에서 "correct" 키가 없을 경우 기본값 빈 문자열 ("")을 반환

InjectedToolCallId 사용하여 tool_call_id 관리

  • LangChain 내부에서 tool_call_id를 자동 관리하도록 변경
  • 각 툴의 실행 결과가 섞이지 않도록 분리하여 후속 작업의 혼선 방지

Command를 활용한 State 업데이트

  • State 변경 사항을 명확하게 추적 가능하도록 Command 사용

# tools.py

# 도구 정의
@tool
def human_assistance(name: str, birthday: str, tool_call_id: Annotated[str, InjectedToolCallId]) -> str:
    """Request assistance from a human."""
    human_response = interrupt({
        "question": "Is this correct?",
        "name": name,
        "birthday": birthday,
    })

    if human_response.get("correct", "").lower().startswith("y"):
        verified_name = name
        verified_birthday = birthday
        response = "Correct"
    else:
        verified_name = human_response.get("name", name)
        verified_birthday = human_response.get("birthday", birthday)
        response = f"Made a correction: {human_response}"

    state_update = {
        "name": verified_name,
        "birthday": verified_birthday,
        "messages": [ToolMessage(response, tool_call_id=tool_call_id)]  ,
    }

    return Command(update=state_update) # 상태 업데이트 명령어 반환

3. 그래프(Graph) 실행

상태(State) 업데이트 결과를 실행하여 확인합니다.
최근에 화두가 되었던, DeepSeek에 대한 출시일정을 확인하는 프롬프트(Prompt)를 구성해서 확인 해보겠습니다.


# agent.py

# 그래프 실행
if __name__ == "__main__":

    config = {"configurable": {"thread_id": "USER_03"}}
    user_input = {"type": "user", "content": "Can you look up when DeepSeek was released? \
When you have the answer, use the human_assistance tool for review."}

    for event in graph.stream({"messages": [user_input]}, config, stream_mode="values"):
        event["messages"][-1].pretty_print()

    human_command = Command(
        resume={
            "name": "DeepSeek-R1",
            "birthday": "Jan 20, 2025"
        }
    )

    for event in graph.stream(human_command, config, stream_mode="values"):
        event["messages"][-1].pretty_print()

(결과)

================================ Human Message =================================

Can you look up when DeepSeek was released? When you have the answer, use the human_assistance tool for review.
================================== Ai Message ==================================
Tool Calls:
  tavily_search_results_json (call_JK559rFs2TUTnOBXzq397glS)
 Call ID: call_JK559rFs2TUTnOBXzq397glS
  Args:
    query: DeepSeek release date
================================= Tool Message =================================
Name: tavily_search_results_json

[{"url": "https://en.wikipedia.org/wiki/DeepSeek", "content": "It would not be used for stock trading and would be
separate from High-Flyer's financial business.[4] In May 2023, the company was launched as DeepSeek.[2] DeepSeek's 
development is funded by High-Flyer.[3] After releasing DeepSeek-V2 in May 2024 which offered strong performance for 
a low price, DeepSeek became known as the catalyst for China's AI model price war. On 29 November 2023, DeepSeek 
launched DeepSeek LLM (large language model) which scaled up to 67B parameters. In December 2024, DeepSeek-V3 was 
launched. Benchmark tests showed it outperformed Llama 3.1 and Qwen 2.5 while matching GPT-4o and Claude 3.5 Sonnet.
[10][11] DeepSeek's optimization on limited resources highlighted potential limits of US sanctions on China's AI 
development.[12] \"Meet DeepSeek Chat, China's latest ChatGPT rival with a 67B model\". \"Chinese start-up DeepSeek's
new AI model outperforms Meta, OpenAI products\"."}, {"url": "https://www.techtarget.com/whatis/feature/DeepSeek-
explained-Everything-you-need-to-know", "content": "DeepSeek, a Chinese AI firm, is disrupting the industry with its 
low-cost, open source large language models, challenging U.S. tech giants. What is DeepSeek? DeepSeek focuses on 
developing open source LLMs. The company's first model was released in November 2023. DeepSeek Since the company was 
created in 2023, DeepSeek has released a series of generative AI models. The low-cost development threatens 
the business model of U.S. tech companies that have invested billions in AI. In contrast with OpenAI, which is 
proprietary technology, DeepSeek is open source and free, challenging the revenue model of U.S. companies charging 
monthly fees for AI services. Being based in China, DeepSeek challenges U.S. technological dominance in AI."}]
================================== Ai Message ==================================
Tool Calls:
  human_assistance (call_IBFdno2pWzb8gSFgFKfO7ySw)
 Call ID: call_IBFdno2pWzb8gSFgFKfO7ySw
  Args:
    name: DeepSeek
    birthday: November 2023
================================== Ai Message ==================================
Tool Calls:
  human_assistance (call_IBFdno2pWzb8gSFgFKfO7ySw)
 Call ID: call_IBFdno2pWzb8gSFgFKfO7ySw
  Args:
    name: DeepSeek
    birthday: November 2023
================================= Tool Message =================================
Name: human_assistance

Made a correction: {'name': 'DeepSeek-R1', 'birthday': 'Jan 20, 2025'}
================================== Ai Message ==================================

DeepSeek was launched in May 2023, with its first model released in November 2023. However, there was a correction 
indicating that the specific model "DeepSeek-R1" will be released on January 20, 2025.

결과를 확인하면, 검색 툴(tavily)을 통해 얻은 birthday는 November 2023 으로 human_assistance 툴에 interrupt 가 발생하였고, 리뷰를 통해 정정된 메시지로 전달 된 것을 확인 할 수 있다.
출시일을 birthday에 매핑되어 출력되어 의아하게 생각할 수 있는데, 이는 흔히 birthday생일 뜻으로만 인식할 수 있는데 사전을 검색해보면 다른 뜻도 있음을 확인 할 수 있습니다.(부족한 영어실력이..발목을..😭😭)

3. 수동으로 State 업데이트 하기

graph.update_state를 활용하여 수동으로 thread의 키값으로 기반으로 state를 업데이트 할 수 있습니다.
기존 name 필드에 적용되어 있는 값을 DeepSeek-R1 Model로 이름을 변경하겠습니다.


# agent.py

# 그래프 실행
if __name__ == "__main__":

    print("================================= Graph State =================================")
    snapshot = graph.get_state(config)
    result = {k: v for k, v in snapshot.values.items() if k in ("name", "birthday")}
    print(result)

    print("============================ Manually Updating State ==========================")
    update_state = graph.update_state(config, {"name": "DeepSeek-R1 Model"})
    print(update_state)    
    snapshot = graph.get_state(config)

    result = {k: v for k, v in snapshot.values.items() if k in ("name", "birthday")}
    print(result)

(결과)

================================= Graph State =================================
{'name': 'DeepSeek-R1', 'birthday': 'Jan 20, 2025'}
============================ Manually Updating State ==========================
{'configurable': {'thread_id': 'USER_03', 'checkpoint_ns': '', 'checkpoint_id': '1efe8419-d950-6098-8006-3d30fc5fee6b'}}
{'name': 'DeepSeek-R1 Model', 'birthday': 'Jan 20, 2025'}

일반적으로 interrupt 기능을 사용하는 것이 권장 되며, 이를 통해 Human-in-the-loop 에서 필요한 정보만 전달하고, 불필요한 상태 변경을 방지할 수 있습니다.

4. LangGraph Studio에서 확인하기

LangGraph Studio에서 보다 편리하게 테스트 해볼 수 있습니다.

동일 질문의 질의하면, 검색 툴을 통해서 결과 반환된 후 Interrupt가 발생하고, NameBirthday를 입력할 수 있도록 커서가 이동 하였습니다.

업데이트 할 메시지를 입력하고, Resume을 클릭하며 내용의 변경사항과 최종 chatbot이 반환하는 메시지를 확인할 수 있습니다.

Reference

728x90
반응형
728x90

AI 에이전트는 항상 예상대로 동작하지 않을 수 있으며, 일부 작업은 사람의 승인이 필요할 수도 있습니다. 이때, LangGraph의 Human-in-the-loop(HITL) 기능을 사용하면, 에이전트가 특정 작업을 완료하는 데 있어 사람의 입력을 받을 수 있게 되어 신뢰성과 제어를 향상시킬 수 있습니다.

Human-in-the-loop(HITL) 기능 개요

Human-in-the-loop(HITL)은 에이전트가 특정 작업을 수행하는 중간에 사용자의 승인을 받거나, 작업을 일시적으로 멈추고 사람의 피드백을 기다릴 수 있는 기능입니다. 이 기능을 통해 AI 시스템은 자동화된 작업 외에도 사람이 개입해야 하는 작업을 처리할 수 있으며, 결과적으로 더 높은 정확도와 안정성을 제공합니다.

LangGraph에서는 interrupt() 함수로 이러한 흐름을 제어할 수 있습니다. 이 함수는 작업 흐름을 일시적으로 멈추고, 사용자가 작업을 확인하거나 추가적인 입력을 제공할 수 있도록 합니다. 이후, 사용자가 입력을 제공하면, 작업이 다시 진행됩니다.

활용 시나리오

Human-in-the-loop(HITL) 기능은 여러 상황에서 유용하게 활용될 수 있으며, 이를 통해 자동화의 신뢰성을 높이고, 중요한 결정을 사람이 개입하여 더 정확하게 처리할 수 있습니다.

서비스 승인 및 배포: 자동화된 시스템이 서비스를 배포하기 전에, 관리자에게 승인을 요청하여 실수로 잘못된 배포가 이루어지지 않도록 합니다.

위험 관리: 중요한 결정을 내리기 전, 시스템이 자동으로 결정을 내리는 대신 사람의 의견을 반영하여 리스크를 최소화합니다.

고객 서비스: AI 챗봇이 고객과의 대화 중 민감한 정보를 처리할 때, 사람이 개입하여 확인 후 진행하는 방식으로 사용될 수 있습니다.

Implementation

LangGraph에서 Human-in-the-loop 기능을 활용하려면, 기존의 작업 흐름에서 특정 부분에 interrupt() 를 추가하면 됩니다. 이를 통해 에이전트가 중요한 결정을 내리기 전에 사람의 입력을 받을 수 있게 됩니다.

1. Human Assistant Tool 만들기

챗봇에서 사람의 개입을 할 수 있는 Tool을 정의하고, 도구목록에 human_assitance 함수를 추가합니다.

# tools.py

from langchain_community.tools.tavily_search import TavilySearchResults
from dotenv import load_dotenv
from langchain_core.tools import tool
from langgraph.types import interrupt

load_dotenv()

# 도구 정의
@tool
def human_assistance(query: str) -> str:
    """Request assistance from a human."""
    human_response = interrupt({"query": query})

    return human_response["data"]


# 도구 목록
tools = [TavilySearchResults(max_results=2), human_assistance]

2. 그래프 실행

#agent.py

# 그래프 실행
if __name__ == "__main__":

    config = {"configurable": {"thread_id": "USER_02"}}
    user_input = {"type": "user", "content": "AI 에이전트를 개발하기 위해 전문적인 가이드를 받고 싶다."}

    for event in graph.stream({"messages": [user_input]}, config, stream_mode="values"):
        event["messages"][-1].pretty_print()


    print("================================= Graph State =================================")
    snapshot = graph.get_state(config)
    print(snapshot.next)

    # "AI 에이전트를 개발하려면 안정적이고 확장이 용이한 LangGraph를 확인해보시기를 권장합니다."
    human_response = (        
        "If you want to develop AI agents, we recommend checking out LangGraph, which is reliable and scalable."
    )

    human_command = Command(resume={"data": human_response})

    # human_command = None

    events = graph.stream(human_command, config, stream_mode="values")
    for event in events:
        if "messages" in event:
            event["messages"][-1].pretty_print()

(결과)

================================ Human Message =================================

AI 에이전트를 개발하기 위해 전문적인 가이드를 받고 싶다.
================================== Ai Message ==================================
Tool Calls:
  human_assistance (call_Xyzp8czUPulqvGa9WsWT0qKf)
 Call ID: call_Xyzp8czUPulqvGa9WsWT0qKf
  Args:
    query: AI 에이전트 개발에 대한 전문적인 가이드 요청
================================= Graph State =================================
('tools',)
================================== Ai Message ==================================
Tool Calls:
  human_assistance (call_Xyzp8czUPulqvGa9WsWT0qKf)
 Call ID: call_Xyzp8czUPulqvGa9WsWT0qKf
  Args:
    query: AI 에이전트 개발에 대한 전문적인 가이드 요청
================================= Tool Message =================================
Name: human_assistance

If you want to develop AI agents, we recommend checking out LangGraph, which is reliable and scalable.
================================== Ai Message ==================================

AI 에이전트를 개발하기 위해 LangGraph를 추천합니다. 이 플랫폼은 신뢰할 수 있고 확장성이 뛰어나며, AI 에이전트를 구축하는 데 필요한 다양한 도구와 리소스를 제공합니다. LangGraph에 대한 자세한 정보와 사용 방법을 살펴보시면 좋겠습니다. 추가적인 도움이 필요하시면 말씀해 주세요!
  • 결과를 확인하면, 사용자 질의(HumanMessage)가 발생한 후 human_assistance Tool에서 interrupt가 발생하여 그래프의 상태가 Tools에 머무른 것을 확인할 수 있습니다.
  • 이후 그래프의 재개(Resume)를 하기 위해 Command의 인자값으로 resume 으로 전달하여 발생된 interrupt를 통과하여 그래프가 재개되었음을 확인할 수 있습니다.
  • 최종적으로, 챗봇은 human_assistance의 Messsage를 통해서 최종적으로 결과를 전달하는 구조가 됨을 확인 할 수 있습니다.

Python의 내장 input()함수와 비슷하게, interrupt도구 내부에서 호출하면 실행이 일시 중지됩니다.
진행 상황은 체크포인터 선택에 따라 지속됩니다 .

4. LangSmith에서 추적 해보기

① Tool 호출단계에서 human_assitance에서 interrupt 가 발생한 부분을 확인할 수 있습니다.
② 그래프의 재개를 위해 Resume의 인자로 Data 키에 message가 들어간 부분을 확인할 수 있습니다.

5. LangGraph Studio에서 확인해보기

LagnGraph Studio에서 테스트를 수행해보면, 사용자 질의가 전달되면 Interrupt가 발생하여 답변이 멈춘상태를 확인할 수 있습니다.

Continue를 클릭하면 그래프 재개를 위한 Resume 박스가 만들어지고, 관련 메시지를 넣을 수 있습니다.

결과를 확인하면, human_assistance의 Tool에서 전달 메세지를 토대로 결과가 전달 되었음을 알 수 있습니다.

Reference

728x90
반응형

+ Recent posts