diff --git a/03-Agent/05-LangGraph-Structured-Output.ipynb b/03-Agent/05-LangGraph-Structured-Output.ipynb index b69e03b..4370803 100644 --- a/03-Agent/05-LangGraph-Structured-Output.ipynb +++ b/03-Agent/05-LangGraph-Structured-Output.ipynb @@ -3,12 +3,36 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# 구조화된 출력 (Structured Output)\n\n구조화된 출력을 사용하면 에이전트가 특정하고 예측 가능한 형식으로 데이터를 반환할 수 있습니다. 자연어 응답을 구문 분석하는 대신 애플리케이션에서 직접 사용할 수 있는 JSON 객체, Pydantic 모델 또는 데이터클래스 형태로 구조화된 데이터를 얻을 수 있습니다.\n\n**구조화된 출력 전략:**\n\n| 전략 | 설명 |\n|:---|:---|\n| **ProviderStrategy** | OpenAI, Grok 등 네이티브 구조화된 출력 지원 모델용 |\n| **ToolStrategy** | 도구 호출을 통한 구조화된 출력 (대부분의 모델 지원) |\n| **자동 선택** | 스키마 타입만 전달 시 모델에 따라 최적 전략 자동 선택 |\n\nLangChain의 `create_agent`는 `response_format` 매개변수로 구조화된 출력을 설정하며, 결과는 에이전트 상태의 `structured_response` 키에 반환됩니다.\n\n> 참고 문서: [LangChain Structured Output](https://docs.langchain.com/oss/python/langchain/structured_output.md)" + "source": [ + "# 구조화된 출력 (Structured Output)\n", + "\n", + "구조화된 출력을 사용하면 에이전트가 특정하고 예측 가능한 형식으로 데이터를 반환할 수 있습니다. 자연어 응답을 구문 분석하는 대신 애플리케이션에서 직접 사용할 수 있는 JSON 객체, Pydantic 모델 또는 데이터클래스 형태로 구조화된 데이터를 얻을 수 있습니다.\n", + "\n", + "**구조화된 출력 전략:**\n", + "\n", + "| 전략 | 설명 |\n", + "|:---|:---|\n", + "| **ProviderStrategy** | OpenAI, Anthropic, Grok 등 네이티브 구조화된 출력 지원 모델용 |\n", + "| **ToolStrategy** | 도구 호출을 통한 구조화된 출력 (대부분의 모델 지원) |\n", + "| **자동 선택** | 스키마 타입만 전달 시 모델에 따라 최적 전략 자동 선택 |\n", + "\n", + "LangChain의 `create_agent`는 `response_format` 매개변수로 구조화된 출력을 설정하며, 결과는 에이전트 상태의 `structured_response` 키에 반환됩니다.\n", + "\n", + "> 📖 **참고 문서**: [LangChain Structured Output](https://docs.langchain.com/oss/python/langchain/structured-output)" + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## 환경 설정\n\n구조화된 출력 튜토리얼을 시작하기 전에 필요한 환경을 설정합니다. `dotenv`를 사용하여 API 키를 로드합니다.\n\n아래 코드는 환경 변수를 로드합니다." + "source": [ + "## 환경 설정\n", + "\n", + "구조화된 출력 튜토리얼을 시작하기 전에 필요한 환경을 설정합니다. `dotenv`를 사용하여 API 키를 로드하고, `langchain_teddynote`의 로깅 기능을 활성화하여 LangSmith에서 실행 추적을 확인할 수 있도록 합니다.\n", + "\n", + "LangSmith 추적을 활성화하면 에이전트의 구조화된 출력 생성 과정을 시각적으로 디버깅할 수 있어, 스키마 검증 오류나 재시도 과정을 파악하는 데 도움이 됩니다.\n", + "\n", + "아래 코드는 환경 변수를 로드하고 LangSmith 프로젝트를 설정합니다." + ] }, { "cell_type": "code", @@ -16,40 +40,81 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "LangSmith 추적을 시작합니다.\n", + "[프로젝트명]\n", + "LangGraph-V1-Tutorial\n" + ] } ], "source": [ "from dotenv import load_dotenv\n", + "from langchain_teddynote import logging\n", + "\n", + "# 환경 변수 로드\n", + "load_dotenv(override=True)\n", "\n", - "load_dotenv(override=True)" + "# LangSmith 추적 설정\n", + "logging.langsmith(\"LangGraph-V1-Tutorial\")" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "---\n\n## Response Format\n\n에이전트가 구조화된 데이터를 반환하는 방법을 `response_format` 매개변수로 제어합니다:\n\n| 설정 | 설명 |\n|:---|:---|\n| **ToolStrategy[T]** | 도구 호출을 통한 구조화된 출력 |\n| **ProviderStrategy[T]** | 제공자 네이티브 구조화된 출력 사용 |\n| **type[T]** | 스키마 타입 직접 전달 - 모델에 따라 최적 전략 자동 선택 |\n| **None** | 구조화된 출력 없음 |\n\n스키마 타입이 직접 제공되면 LangChain이 자동으로 최적의 전략을 선택합니다:\n- 네이티브 구조화된 출력 지원 모델(OpenAI, Grok)에는 `ProviderStrategy`\n- 다른 모든 모델에는 `ToolStrategy`\n\n구조화된 응답은 에이전트의 최종 상태의 `structured_response` 키에 반환됩니다." + "source": [ + "---\n", + "\n", + "## Response Format\n", + "\n", + "에이전트가 구조화된 데이터를 반환하는 방법을 `response_format` 매개변수로 제어합니다:\n", + "\n", + "| 설정 | 설명 |\n", + "|:---|:---|\n", + "| **ToolStrategy[T]** | 도구 호출을 통한 구조화된 출력 |\n", + "| **ProviderStrategy[T]** | 제공자 네이티브 구조화된 출력 사용 |\n", + "| **type[T]** | 스키마 타입 직접 전달 - 모델에 따라 최적 전략 자동 선택 |\n", + "| **None** | 구조화된 출력 없음 |\n", + "\n", + "스키마 타입이 직접 제공되면 LangChain이 자동으로 최적의 전략을 선택합니다:\n", + "- 네이티브 구조화된 출력 지원 모델(OpenAI, Anthropic, Grok)에는 `ProviderStrategy`\n", + "- 다른 모든 모델에는 `ToolStrategy`\n", + "\n", + "구조화된 응답은 에이전트의 최종 상태의 `structured_response` 키에 반환됩니다." + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "---\n\n## Provider Strategy\n\n일부 모델 제공자(현재 OpenAI 및 Grok만 해당)는 API를 통해 구조화된 출력을 네이티브로 지원합니다. 이 방법은 사용 가능한 경우 가장 신뢰할 수 있는 방법입니다.\n\n스키마 타입을 `create_agent.response_format`에 직접 전달하면 LangChain이 지원 모델에 대해 자동으로 `ProviderStrategy`를 사용합니다." + "source": [ + "---\n", + "\n", + "## Provider Strategy\n", + "\n", + "일부 모델 제공자(OpenAI, Anthropic, Grok 등)는 API를 통해 구조화된 출력을 네이티브로 지원합니다. 이 방법은 모델이 JSON 스키마를 직접 이해하고 준수하도록 강제하므로, 사용 가능한 경우 가장 신뢰할 수 있는 방법입니다.\n", + "\n", + "스키마 타입을 `create_agent`의 `response_format`에 직접 전달하면, LangChain이 지원 모델에 대해 자동으로 `ProviderStrategy`를 사용합니다. 지원되지 않는 모델의 경우 자동으로 `ToolStrategy`로 폴백됩니다.\n", + "\n", + "> 📖 **참고 문서**: [LangChain Models](https://docs.langchain.com/oss/python/langchain/models.md)" + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "### Pydantic 모델\n\nPydantic 모델을 사용하면 필드에 대한 상세한 설명과 검증 규칙을 정의할 수 있습니다. `Field`의 `description`은 모델이 각 필드의 용도를 이해하는 데 도움이 됩니다.\n\n아래 코드는 Pydantic 모델로 연락처 정보를 추출하는 예시입니다." + "source": [ + "### Pydantic 모델\n", + "\n", + "Pydantic 모델을 사용하면 필드에 대한 상세한 설명과 검증 규칙을 정의할 수 있습니다. `Field`의 `description`은 모델이 각 필드의 용도를 이해하는 데 도움이 되며, 이를 통해 더 정확한 구조화된 출력을 생성할 수 있습니다.\n", + "\n", + "Pydantic은 타입 검증, 기본값 설정, 범위 제한(`ge`, `le`) 등 풍부한 검증 기능을 제공하므로, 복잡한 스키마 정의에 가장 적합합니다.\n", + "\n", + "아래 코드는 Pydantic 모델로 연락처 정보를 추출하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -62,22 +127,30 @@ ], "source": [ "from pydantic import BaseModel, Field\n", + "from langchain.chat_models import init_chat_model\n", "from langchain.agents import create_agent\n", - "from langchain_openai import ChatOpenAI\n", "\n", "\n", "class ContactInfo(BaseModel):\n", - " \"\"\"Contact information for a person.\"\"\"\n", + " \"\"\"사람의 연락처 정보를 나타내는 스키마\n", + "\n", + " 이름, 이메일, 전화번호를 구조화된 형태로 추출합니다.\n", + " \"\"\"\n", "\n", - " name: str = Field(description=\"The name of the person\")\n", - " email: str = Field(description=\"The email address of the person\")\n", - " phone: str = Field(description=\"The phone number of the person\")\n", + " name: str = Field(description=\"The name of the person\") # 이름\n", + " email: str = Field(description=\"The email address of the person\") # 이메일 주소\n", + " phone: str = Field(description=\"The phone number of the person\") # 전화번호\n", "\n", "\n", - "# 모델 및 에이전트 생성\n", - "model = ChatOpenAI(model=\"gpt-4.1-mini\")\n", + "# 모델 초기화\n", + "# OpenAI 키 사용 시 gpt-4.1-mini, gpt-5.2 등으로 변경하세요.\n", + "model = init_chat_model(\"claude-sonnet-4-5\")\n", + "\n", + "# 에이전트 생성 - response_format에 스키마 타입 전달 시 ProviderStrategy 자동 선택\n", "agent = create_agent(\n", - " model=model, tools=[], response_format=ContactInfo # ProviderStrategy 자동 선택\n", + " model=model,\n", + " tools=[],\n", + " response_format=ContactInfo,\n", ")\n", "\n", "# 에이전트 실행\n", @@ -93,18 +166,25 @@ ")\n", "\n", "# 구조화된 응답 확인\n", - "print(result[\"structured_response\"])\n", - "# ContactInfo(name='John Doe', email='john@example.com', phone='(555) 123-4567')" + "print(result[\"structured_response\"])" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "### 데이터클래스\n\nPython의 `@dataclass` 데코레이터를 사용하여 스키마를 정의할 수도 있습니다. 필드 설명은 주석으로 추가합니다.\n\n아래 코드는 데이터클래스로 연락처 정보를 추출하는 예시입니다." + "source": [ + "### 데이터클래스\n", + "\n", + "Python의 `@dataclass` 데코레이터를 사용하여 스키마를 정의할 수도 있습니다. 데이터클래스는 Pydantic보다 가볍고 Python 표준 라이브러리의 일부이므로 추가 의존성 없이 사용할 수 있습니다.\n", + "\n", + "필드 설명은 주석(comment)으로 추가하며, 모델이 이를 참고하여 각 필드에 적절한 값을 채웁니다.\n", + "\n", + "아래 코드는 데이터클래스로 연락처 정보를 추출하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -122,17 +202,21 @@ "\n", "@dataclass\n", "class ContactInfo:\n", - " \"\"\"Contact information for a person.\"\"\"\n", + " \"\"\"사람의 연락처 정보를 나타내는 스키마\"\"\"\n", "\n", - " name: str # The name of the person\n", - " email: str # The email address of the person\n", - " phone: str # The phone number of the person\n", + " name: str # 이름 (The name of the person)\n", + " email: str # 이메일 주소 (The email address of the person)\n", + " phone: str # 전화번호 (The phone number of the person)\n", "\n", "\n", + "# 에이전트 생성 - 위에서 초기화한 model 재사용\n", "agent = create_agent(\n", - " model=model, tools=[], response_format=ContactInfo # ProviderStrategy 자동 선택\n", + " model=model,\n", + " tools=[],\n", + " response_format=ContactInfo, # ProviderStrategy 자동 선택\n", ")\n", "\n", + "# 에이전트 실행\n", "result = agent.invoke(\n", " {\n", " \"messages\": [\n", @@ -144,18 +228,26 @@ " }\n", ")\n", "\n", - "print(result[\"structured_response\"])\n", - "# ContactInfo(name='Jane Smith', email='jane@example.com', phone='(555) 987-6543')" + "# 구조화된 응답 확인\n", + "print(result[\"structured_response\"])" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "### TypedDict\n\n`TypedDict`를 사용하면 딕셔너리 형태로 구조화된 출력을 받을 수 있습니다. 반환값이 딕셔너리이므로 JSON 직렬화가 용이합니다.\n\n아래 코드는 TypedDict로 연락처 정보를 추출하는 예시입니다." + "source": [ + "### TypedDict\n", + "\n", + "`TypedDict`를 사용하면 딕셔너리 형태로 구조화된 출력을 받을 수 있습니다. 반환값이 Python 딕셔너리이므로 JSON 직렬화가 용이하고, API 응답이나 데이터베이스 저장에 바로 활용할 수 있습니다.\n", + "\n", + "Pydantic이나 데이터클래스와 달리 인스턴스가 아닌 딕셔너리로 반환되므로, 후처리가 간단한 경우에 적합합니다.\n", + "\n", + "아래 코드는 TypedDict로 연락처 정보를 추출하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -172,17 +264,21 @@ "\n", "\n", "class ContactInfo(TypedDict):\n", - " \"\"\"Contact information for a person.\"\"\"\n", + " \"\"\"사람의 연락처 정보를 나타내는 스키마\"\"\"\n", "\n", - " name: str # The name of the person\n", - " email: str # The email address of the person\n", - " phone: str # The phone number of the person\n", + " name: str # 이름 (The name of the person)\n", + " email: str # 이메일 주소 (The email address of the person)\n", + " phone: str # 전화번호 (The phone number of the person)\n", "\n", "\n", + "# 에이전트 생성\n", "agent = create_agent(\n", - " model=model, tools=[], response_format=ContactInfo # ProviderStrategy 자동 선택\n", + " model=model,\n", + " tools=[],\n", + " response_format=ContactInfo, # ProviderStrategy 자동 선택\n", ")\n", "\n", + "# 에이전트 실행\n", "result = agent.invoke(\n", " {\n", " \"messages\": [\n", @@ -194,30 +290,48 @@ " }\n", ")\n", "\n", - "print(result[\"structured_response\"])\n", - "# {'name': 'Teddy Lee', 'email': 'teddy@example.com', 'phone': '(555) 111-2222'}" + "# 구조화된 응답 확인 - 딕셔너리 형태로 반환됨\n", + "print(result[\"structured_response\"])" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "---\n\n## Tool Calling Strategy\n\n네이티브 구조화된 출력을 지원하지 않는 모델의 경우 LangChain은 도구 호출을 사용하여 동일한 결과를 달성합니다. `ToolStrategy`는 도구 호출을 지원하는 대부분의 최신 모델에서 작동합니다.\n\n`ToolStrategy`를 명시적으로 사용하면 지원 여부와 관계없이 항상 도구 호출 방식을 사용합니다." + "source": [ + "---\n", + "\n", + "## Tool Calling Strategy\n", + "\n", + "네이티브 구조화된 출력을 지원하지 않는 모델의 경우, LangChain은 도구 호출(Tool Calling)을 사용하여 동일한 결과를 달성합니다. `ToolStrategy`는 도구 호출을 지원하는 대부분의 최신 모델에서 작동하므로 범용성이 높습니다.\n", + "\n", + "`ToolStrategy`를 명시적으로 사용하면 네이티브 지원 여부와 관계없이 항상 도구 호출 방식을 사용합니다. 이는 일관된 동작이 필요하거나 특정 모델에서 네이티브 방식에 문제가 있을 때 유용합니다.\n", + "\n", + "> 📖 **참고 문서**: [LangChain Tools](https://docs.langchain.com/oss/python/langchain/tools)" + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "### Pydantic 모델\n\n`ToolStrategy`를 사용할 때도 Pydantic 모델의 필드 설명과 검증 규칙이 동일하게 적용됩니다. `Literal` 타입으로 허용값을 제한하고, `ge`, `le` 등의 검증자로 범위를 지정할 수 있습니다.\n\n아래 코드는 ToolStrategy로 제품 리뷰를 분석하는 예시입니다." + "source": [ + "### Pydantic 모델\n", + "\n", + "`ToolStrategy`를 사용할 때도 Pydantic 모델의 필드 설명과 검증 규칙이 동일하게 적용됩니다. `Literal` 타입으로 허용값을 제한하고, `ge`(greater than or equal), `le`(less than or equal) 등의 검증자로 범위를 지정할 수 있습니다.\n", + "\n", + "스키마 검증에 실패하면 에이전트가 자동으로 재시도하여 올바른 형식의 출력을 생성하도록 유도합니다.\n", + "\n", + "아래 코드는 ToolStrategy로 제품 리뷰를 분석하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "rating=5 sentiment='positive' key_points=['fast shipping', 'expensive']\n" + "rating=5 sentiment='positive' key_points=['fast shipping', 'expensive', 'great product']\n" ] } ], @@ -229,19 +343,27 @@ "\n", "\n", "class ProductReview(BaseModel):\n", - " \"\"\"Analysis of a product review.\"\"\"\n", + " \"\"\"제품 리뷰 분석 결과를 나타내는 스키마\"\"\"\n", "\n", - " rating: int | None = Field(description=\"The rating of the product\", ge=1, le=5)\n", + " rating: int | None = Field(\n", + " description=\"The rating of the product\", ge=1, le=5\n", + " ) # 평점 (1-5)\n", " sentiment: Literal[\"positive\", \"negative\"] = Field(\n", " description=\"The sentiment of the review\"\n", - " )\n", + " ) # 감정 분석 결과\n", " key_points: list[str] = Field(\n", " description=\"The key points of the review. Lowercase, 1-3 words each.\"\n", - " )\n", + " ) # 핵심 포인트\n", "\n", "\n", - "agent = create_agent(model=model, tools=[], response_format=ToolStrategy(ProductReview))\n", + "# 에이전트 생성 - ToolStrategy 명시적 사용\n", + "agent = create_agent(\n", + " model=model,\n", + " tools=[],\n", + " response_format=ToolStrategy(ProductReview),\n", + ")\n", "\n", + "# 에이전트 실행\n", "result = agent.invoke(\n", " {\n", " \"messages\": [\n", @@ -253,14 +375,22 @@ " }\n", ")\n", "\n", - "print(result[\"structured_response\"])\n", - "# ProductReview(rating=5, sentiment='positive', key_points=['fast shipping', 'expensive'])" + "# 구조화된 응답 확인\n", + "print(result[\"structured_response\"])" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "### Union 타입\n\n`Union` 타입을 사용하여 여러 스키마 옵션을 제공할 수 있습니다. 모델은 입력 컨텍스트에 따라 가장 적절한 스키마를 자동으로 선택합니다.\n\n아래 코드는 리뷰와 불만을 구분하여 분석하는 예시입니다." + "source": [ + "### Union 타입\n", + "\n", + "`Union` 타입을 사용하여 여러 스키마 옵션을 제공할 수 있습니다. 모델은 입력 컨텍스트에 따라 가장 적절한 스키마를 자동으로 선택하므로, 다양한 유형의 입력을 하나의 에이전트로 처리할 수 있습니다.\n", + "\n", + "예를 들어, 고객 피드백을 처리할 때 긍정적인 리뷰와 불만 사항을 서로 다른 스키마로 분류하여 각각에 맞는 후속 처리를 할 수 있습니다.\n", + "\n", + "아래 코드는 리뷰와 불만을 구분하여 분석하는 예시입니다." + ] }, { "cell_type": "code", @@ -271,7 +401,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Review: rating=5 sentiment='positive' key_points=['great product', 'fast shipping', 'expensive']\n", + "Review: rating=5 sentiment='positive' key_points=['fast shipping', 'expensive', 'great product']\n", "Complaint: issue_type='shipping' severity='high' description='Package arrived damaged and contents were broken'\n" ] } @@ -284,29 +414,40 @@ "\n", "\n", "class ProductReview(BaseModel):\n", - " \"\"\"Analysis of a product review.\"\"\"\n", - "\n", - " rating: int | None = Field(description=\"The rating of the product\", ge=1, le=5)\n", + " \"\"\"제품 리뷰 분석 결과를 나타내는 스키마\n", + " \n", + " 긍정/부정 감정 분석과 핵심 포인트를 추출합니다.\n", + " \"\"\"\n", + "\n", + " rating: int | None = Field(\n", + " description=\"The rating of the product\", ge=1, le=5\n", + " ) # 평점 (1-5)\n", " sentiment: Literal[\"positive\", \"negative\"] = Field(\n", " description=\"The sentiment of the review\"\n", - " )\n", + " ) # 감정 분석 결과\n", " key_points: list[str] = Field(\n", " description=\"The key points of the review. Lowercase, 1-3 words each.\"\n", - " )\n", + " ) # 핵심 포인트\n", "\n", "\n", "class CustomerComplaint(BaseModel):\n", - " \"\"\"A customer complaint about a product or service.\"\"\"\n", + " \"\"\"고객 불만 정보를 나타내는 스키마\n", + " \n", + " 문제 유형, 심각도, 설명을 구조화된 형태로 추출합니다.\n", + " \"\"\"\n", "\n", " issue_type: Literal[\"product\", \"service\", \"shipping\", \"billing\"] = Field(\n", " description=\"The type of issue\"\n", - " )\n", + " ) # 문제 유형\n", " severity: Literal[\"low\", \"medium\", \"high\"] = Field(\n", " description=\"The severity of the complaint\"\n", - " )\n", - " description: str = Field(description=\"Brief description of the complaint\")\n", + " ) # 심각도\n", + " description: str = Field(\n", + " description=\"Brief description of the complaint\"\n", + " ) # 불만 내용\n", "\n", "\n", + "# 에이전트 생성 - Union 타입으로 여러 스키마 지원\n", "agent = create_agent(\n", " model=model,\n", " tools=[],\n", @@ -343,7 +484,15 @@ { "cell_type": "markdown", "metadata": {}, - "source": "### 커스텀 도구 메시지 콘텐츠\n\n`tool_message_content` 매개변수를 사용하면 구조화된 출력이 생성될 때 대화 기록에 나타나는 메시지를 커스터마이징할 수 있습니다. 이는 후속 대화에서 컨텍스트를 제공하는 데 유용합니다.\n\n아래 코드는 커스텀 도구 메시지를 설정하는 예시입니다." + "source": [ + "### 커스텀 도구 메시지 콘텐츠\n", + "\n", + "`tool_message_content` 매개변수를 사용하면 구조화된 출력이 생성될 때 대화 기록에 나타나는 메시지를 커스터마이징할 수 있습니다. 이 메시지는 후속 대화에서 컨텍스트를 제공하는 데 유용하며, 사용자에게 처리 상태를 알려주는 역할도 합니다.\n", + "\n", + "기본적으로 도구 호출 결과는 JSON 형태로 메시지에 기록되지만, 커스텀 메시지를 설정하면 사람이 읽기 쉬운 형태로 표시됩니다. 이는 챗봇이나 대화형 인터페이스에서 사용자 경험을 개선하는 데 도움이 됩니다.\n", + "\n", + "아래 코드는 회의 메모에서 액션 아이템을 추출하고, 커스텀 도구 메시지를 설정하는 예시입니다." + ] }, { "cell_type": "code", @@ -354,7 +503,10 @@ "name": "stdout", "output_type": "stream", "text": [ - "task='Update the project timeline' assignee='Sarah' priority='high'\n" + "=== Structured Response ===\n", + "task='Update the project timeline' assignee='Sarah' priority='high'\n", + "=== Tool Message Content ===\n", + "Action item captured and added to meeting notes!\n" ] } ], @@ -366,13 +518,19 @@ "\n", "\n", "class MeetingAction(BaseModel):\n", - " \"\"\"Action items extracted from a meeting transcript.\"\"\"\n", + " \"\"\"회의에서 추출된 액션 아이템을 나타내는 스키마\n", + " \n", + " 담당자, 작업 내용, 우선순위를 구조화합니다.\n", + " \"\"\"\n", "\n", - " task: str = Field(description=\"The specific task to be completed\")\n", - " assignee: str = Field(description=\"Person responsible for the task\")\n", - " priority: Literal[\"low\", \"medium\", \"high\"] = Field(description=\"Priority level\")\n", + " task: str = Field(description=\"The specific task to be completed\") # 작업 내용\n", + " assignee: str = Field(description=\"Person responsible for the task\") # 담당자\n", + " priority: Literal[\"low\", \"medium\", \"high\"] = Field(\n", + " description=\"Priority level\"\n", + " ) # 우선순위\n", "\n", "\n", + "# 에이전트 생성 - 커스텀 도구 메시지 설정\n", "agent = create_agent(\n", " model=model,\n", " tools=[],\n", @@ -382,6 +540,7 @@ " ),\n", ")\n", "\n", + "# 에이전트 실행\n", "result = agent.invoke(\n", " {\n", " \"messages\": [\n", @@ -393,19 +552,44 @@ " }\n", ")\n", "\n", + "# 구조화된 응답 출력\n", + "print(\"=== Structured Response ===\")\n", "print(result[\"structured_response\"])\n", - "# MeetingAction(task='Update the project timeline', assignee='Sarah', priority='high')" + "# MeetingAction(task='Update the project timeline', assignee='Sarah', priority='high')\n", + "\n", + "# 도구 메시지 콘텐츠 출력\n", + "print(\"=== Tool Message Content ===\")\n", + "print(result[\"messages\"][-1].content)\n", + "# Action item captured and added to meeting notes!" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "---\n\n## 오류 처리\n\n모델은 도구 호출을 통해 구조화된 출력을 생성할 때 스키마와 일치하지 않는 값을 반환할 수 있습니다. LangChain은 이러한 오류를 자동으로 처리하는 지능형 재시도 메커니즘을 제공합니다.\n\n`handle_errors` 매개변수로 오류 처리 방법을 제어할 수 있으며, 기본값은 `True`입니다." + "source": [ + "---\n", + "\n", + "## 오류 처리\n", + "\n", + "모델은 도구 호출을 통해 구조화된 출력을 생성할 때 스키마와 일치하지 않는 값을 반환할 수 있습니다. 예를 들어, 1-5 범위로 지정된 rating에 10을 반환하거나, 필수 필드를 누락하는 경우가 있습니다. LangChain은 이러한 오류를 자동으로 감지하고 처리하는 지능형 재시도 메커니즘을 제공합니다.\n", + "\n", + "`handle_errors` 매개변수로 오류 처리 방법을 세밀하게 제어할 수 있으며, 기본값은 `True`로 모든 검증 오류를 자동으로 처리합니다. 오류가 발생하면 에이전트는 모델에게 오류 내용을 피드백하고 재시도를 요청하여, 최종적으로 스키마에 맞는 출력을 얻도록 합니다.\n", + "\n", + "이 기능은 프로덕션 환경에서 안정적인 구조화된 출력을 보장하는 데 필수적입니다." + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "### 스키마 검증 오류\n\n구조화된 출력이 예상 스키마와 일치하지 않으면 에이전트는 오류 피드백을 제공하고 모델에게 재시도를 요청합니다. 예를 들어, rating이 1-5 범위인데 10이 입력되면 자동으로 수정을 시도합니다.\n\n아래 코드는 스키마 검증 오류 처리 예시입니다." + "source": [ + "### 스키마 검증 오류\n", + "\n", + "구조화된 출력이 예상 스키마와 일치하지 않으면 에이전트는 오류 피드백을 제공하고 모델에게 재시도를 요청합니다. 예를 들어, rating이 1-5 범위인데 사용자가 \"10/10\"이라고 입력하면, 모델이 이를 10으로 파싱하려다 검증 오류가 발생하고 자동으로 5로 수정됩니다.\n", + "\n", + "이 재시도 메커니즘 덕분에 실제 사용자 입력이 예상 범위를 벗어나더라도 안정적으로 처리할 수 있습니다. LangSmith에서 실행 추적을 확인하면 재시도 과정을 시각적으로 볼 수 있습니다.\n", + "\n", + "아래 코드는 스키마 검증 오류가 발생했을 때 자동으로 수정되는 예시입니다." + ] }, { "cell_type": "code", @@ -427,10 +611,18 @@ "\n", "\n", "class ProductRating(BaseModel):\n", - " rating: int | None = Field(description=\"Rating from 1-5\", ge=1, le=5)\n", - " comment: str = Field(description=\"Review comment\")\n", + " \"\"\"제품 평점 정보를 나타내는 스키마\n", + " \n", + " 평점은 1-5 범위로 제한되며, 리뷰 코멘트를 포함합니다.\n", + " \"\"\"\n", + "\n", + " rating: int | None = Field(\n", + " description=\"Rating from 1-5\", ge=1, le=5\n", + " ) # 평점 (1-5 범위)\n", + " comment: str = Field(description=\"Review comment\") # 리뷰 코멘트\n", "\n", "\n", + "# 에이전트 생성 - 기본값 handle_errors=True로 자동 재시도 활성화\n", "agent = create_agent(\n", " model=model,\n", " tools=[],\n", @@ -438,10 +630,12 @@ " system_prompt=\"You are a helpful assistant that parses product reviews. Do not make any field or value up.\",\n", ")\n", "\n", + "# 10/10 입력 - 범위를 벗어나므로 자동으로 5로 수정됨\n", "result = agent.invoke(\n", " {\"messages\": [{\"role\": \"user\", \"content\": \"Parse this: Amazing product, 10/10!\"}]}\n", ")\n", "\n", + "# 구조화된 응답 확인\n", "print(result[\"structured_response\"])\n", "# ProductRating(rating=5, comment='Amazing product')\n", "# 모델이 자동으로 수정하여 10을 5로 변경" @@ -450,26 +644,50 @@ { "cell_type": "markdown", "metadata": {}, - "source": "### 오류 처리 전략\n\n`handle_errors` 매개변수를 사용하여 오류 처리 방법을 커스터마이징할 수 있습니다.\n\n| 설정 | 설명 |\n|:---|:---|\n| `True` | 모든 오류를 자동으로 처리하고 재시도 (기본값) |\n| `False` | 오류 발생 시 예외 발생 |\n| 문자열 | 커스텀 오류 메시지로 재시도 |\n| 예외 클래스 | 특정 예외만 처리 |\n| 콜러블 | 커스텀 오류 핸들러 함수 사용 |" + "source": [ + "### 오류 처리 전략\n", + "\n", + "`handle_errors` 매개변수를 사용하여 오류 처리 방법을 다양하게 커스터마이징할 수 있습니다. 요구사항에 따라 자동 재시도, 예외 발생, 커스텀 핸들러 등을 선택할 수 있습니다.\n", + "\n", + "| 설정 | 설명 |\n", + "|:---|:---|\n", + "| `True` | 모든 오류를 자동으로 처리하고 재시도 (기본값) |\n", + "| `False` | 오류 발생 시 예외 발생 |\n", + "| 문자열 | 커스텀 오류 메시지로 재시도 |\n", + "| 예외 클래스 | 특정 예외만 처리 |\n", + "| 콜러블 | 커스텀 오류 핸들러 함수 사용 |\n", + "\n", + "아래에서 각 전략의 사용 예시를 살펴봅니다." + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "### 커스텀 오류 메시지\n\n문자열을 전달하면 해당 메시지로 모델에게 재시도를 요청합니다.\n\n아래 코드는 커스텀 오류 메시지 설정 예시입니다." + "source": [ + "### 커스텀 오류 메시지\n", + "\n", + "문자열을 `handle_errors`에 전달하면 해당 메시지로 모델에게 재시도를 요청합니다. 기본 오류 메시지 대신 더 구체적인 안내를 제공할 수 있으므로, 모델이 오류를 더 잘 이해하고 올바른 형식으로 수정할 수 있습니다.\n", + "\n", + "특히 복잡한 스키마나 특정 도메인의 규칙이 있는 경우, 커스텀 메시지로 명확한 지시를 제공하면 재시도 성공률이 높아집니다.\n", + "\n", + "아래 코드는 커스텀 오류 메시지를 설정하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "from langchain.agents.structured_output import ToolStrategy\n", "\n", + "# 커스텀 오류 메시지로 에이전트 생성\n", "agent = create_agent(\n", " model=model,\n", " tools=[],\n", " response_format=ToolStrategy(\n", " schema=ProductRating,\n", + " # 오류 발생 시 이 메시지로 재시도 요청\n", " handle_errors=\"Please provide a valid rating between 1-5 and include a comment.\",\n", " ),\n", ")" @@ -478,20 +696,29 @@ { "cell_type": "markdown", "metadata": {}, - "source": "### 특정 예외만 처리\n\n예외 클래스를 전달하면 해당 예외만 처리하고 다른 예외는 그대로 발생합니다.\n\n아래 코드는 특정 예외만 처리하는 예시입니다." + "source": [ + "### 특정 예외만 처리\n", + "\n", + "예외 클래스를 `handle_errors`에 전달하면 해당 예외만 처리하고 다른 예외는 그대로 발생합니다. 이는 예상된 검증 오류만 재시도하고, 예상치 못한 시스템 오류는 즉시 감지하고 싶을 때 유용합니다.\n", + "\n", + "예를 들어, `ValueError`만 처리하면 범위 검증 오류는 재시도하지만, 네트워크 오류나 타입 불일치 같은 다른 오류는 예외로 발생시킵니다.\n", + "\n", + "아래 코드는 특정 예외만 처리하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ + "# ValueError만 처리하는 에이전트 생성\n", "agent = create_agent(\n", " model=model,\n", " tools=[],\n", " response_format=ToolStrategy(\n", " schema=ProductRating,\n", - " handle_errors=ValueError, # ValueError만 재시도, 다른 예외는 발생\n", + " handle_errors=ValueError, # ValueError만 재시도, 다른 예외는 그대로 발생\n", " ),\n", ")" ] @@ -499,20 +726,30 @@ { "cell_type": "markdown", "metadata": {}, - "source": "### 여러 예외 유형 처리\n\n튜플로 여러 예외 클래스를 전달하면 해당 예외들을 모두 처리합니다.\n\n아래 코드는 여러 예외 유형을 처리하는 예시입니다." + "source": [ + "### 여러 예외 유형 처리\n", + "\n", + "튜플로 여러 예외 클래스를 `handle_errors`에 전달하면 해당 예외들을 모두 처리합니다. 이는 여러 종류의 검증 오류를 한 번에 처리하고 싶을 때 유용합니다.\n", + "\n", + "일반적으로 `ValueError`와 `TypeError`를 함께 처리하면 대부분의 스키마 검증 오류를 커버할 수 있습니다.\n", + "\n", + "아래 코드는 여러 예외 유형을 처리하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ + "# 여러 예외 유형을 처리하는 에이전트 생성\n", "agent = create_agent(\n", " model=model,\n", " tools=[],\n", " response_format=ToolStrategy(\n", " schema=ProductRating,\n", - " handle_errors=(ValueError, TypeError), # ValueError 및 TypeError 재시도\n", + " # ValueError 및 TypeError 모두 재시도\n", + " handle_errors=(ValueError, TypeError),\n", " ),\n", ")" ] @@ -520,11 +757,19 @@ { "cell_type": "markdown", "metadata": {}, - "source": "### 커스텀 오류 핸들러 함수\n\n함수를 전달하면 오류 발생 시 해당 함수가 호출되어 커스텀 오류 메시지를 생성합니다.\n\n아래 코드는 커스텀 오류 핸들러 함수 예시입니다." + "source": [ + "### 커스텀 오류 핸들러 함수\n", + "\n", + "함수(콜러블)를 `handle_errors`에 전달하면 오류 발생 시 해당 함수가 호출되어 커스텀 오류 메시지를 생성합니다. 이 방법은 오류 유형에 따라 다른 메시지를 반환하거나, 오류 정보를 로깅하는 등 복잡한 처리가 필요할 때 유용합니다.\n", + "\n", + "LangChain은 `StructuredOutputValidationError`(스키마 검증 실패)와 `MultipleStructuredOutputsError`(여러 출력 반환) 등의 구체적인 예외 클래스를 제공합니다. 이를 활용하면 오류 유형별로 세밀한 처리가 가능합니다.\n", + "\n", + "아래 코드는 커스텀 오류 핸들러 함수를 사용하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -535,14 +780,26 @@ "\n", "\n", "def custom_error_handler(error: Exception) -> str:\n", + " \"\"\"오류 유형에 따른 커스텀 오류 메시지 생성\n", + " \n", + " Args:\n", + " error: 발생한 예외 객체\n", + " \n", + " Returns:\n", + " 모델에게 전달할 오류 메시지\n", + " \"\"\"\n", " if isinstance(error, StructuredOutputValidationError):\n", + " # 스키마 검증 오류\n", " return \"There was an issue with the format. Try again.\"\n", " elif isinstance(error, MultipleStructuredOutputsError):\n", + " # 여러 구조화된 출력이 반환된 경우\n", " return \"Multiple structured outputs were returned. Pick the most relevant one.\"\n", " else:\n", + " # 기타 오류\n", " return f\"Error: {str(error)}\"\n", "\n", "\n", + "# 커스텀 오류 핸들러로 에이전트 생성\n", "agent = create_agent(\n", " model=model,\n", " tools=[],\n", @@ -555,19 +812,28 @@ { "cell_type": "markdown", "metadata": {}, - "source": "### 오류 처리 비활성화\n\n`handle_errors=False`를 설정하면 오류 발생 시 예외가 그대로 발생합니다.\n\n아래 코드는 오류 처리 비활성화 예시입니다." + "source": [ + "### 오류 처리 비활성화\n", + "\n", + "`handle_errors=False`를 설정하면 오류 발생 시 예외가 그대로 발생합니다. 이는 개발 중에 오류를 디버깅하거나, 커스텀 오류 처리 로직을 직접 구현하고 싶을 때 유용합니다.\n", + "\n", + "프로덕션 환경에서는 일반적으로 `handle_errors=True`(기본값)를 사용하여 안정적인 동작을 보장하지만, 개발 및 테스트 환경에서는 비활성화하여 문제를 빠르게 파악할 수 있습니다.\n", + "\n", + "아래 코드는 오류 처리를 비활성화하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ + "# 오류 처리 비활성화 - 모든 오류가 예외로 발생\n", "agent = create_agent(\n", " model=model,\n", " tools=[],\n", " response_format=ToolStrategy(\n", - " schema=ProductRating, handle_errors=False # 모든 오류 발생\n", + " schema=ProductRating, handle_errors=False # 오류 발생 시 예외 발생\n", " ),\n", ")" ] @@ -575,11 +841,21 @@ { "cell_type": "markdown", "metadata": {}, - "source": "---\n\n## 종합 예제\n\n`Union` 타입과 오류 처리를 결합한 실용적인 예제입니다. 책과 영화 추천을 모두 처리하고, 오류 발생 시 자동으로 재시도합니다.\n\n아래 코드는 여러 추천 유형을 처리하는 에이전트 예시입니다." + "source": [ + "---\n", + "\n", + "## 종합 예제\n", + "\n", + "지금까지 학습한 `Union` 타입과 오류 처리를 결합한 실용적인 예제입니다. 하나의 에이전트가 책과 영화 추천을 모두 처리하며, 입력 내용에 따라 적절한 스키마를 자동으로 선택합니다.\n", + "\n", + "`handle_errors=True`로 설정하여 오류 발생 시 자동으로 재시도하므로, 프로덕션 환경에서도 안정적으로 동작합니다. 이 패턴은 다양한 유형의 입력을 처리해야 하는 챗봇이나 AI 어시스턴트에 적합합니다.\n", + "\n", + "아래 코드는 책과 영화 추천을 처리하는 에이전트 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -587,10 +863,10 @@ "output_type": "stream", "text": [ "Book recommendation:\n", - "title='Dune' author='Frank Herbert' genre='fiction' rating=5 summary='Set in a distant future amidst a huge interstellar empire, Dune tells the story of Paul Atreides, a young noble who becomes embroiled in a complex struggle over a desert planet and its valuable resource, spice. The novel explores themes of politics, religion, and ecology.'\n", + "title='Dune' author='Frank Herbert' genre='science' rating=5 summary='Set in the distant future, Dune follows Paul Atreides as his family takes control of the desert planet Arrakis, the only source of the valuable spice melange. The story weaves together themes of politics, religion, ecology, and destiny as Paul navigates betrayal, finds his place among the native Fremen, and potentially fulfills an ancient prophecy. This epic tale explores power, survival, and human potential in a richly imagined universe.'\n", "\n", "Movie recommendation:\n", - "title='The 40-Year-Old Virgin' director='Judd Apatow' year=2005 genre='comedy' rating=4\n" + "title='Superbad' director='Greg Mottola' year=2007 genre='comedy' rating=4\n" ] } ], @@ -601,32 +877,37 @@ "from langchain.agents.structured_output import ToolStrategy\n", "\n", "\n", - "# 여러 응답 유형 정의\n", "class BookRecommendation(BaseModel):\n", - " \"\"\"Book recommendation with details.\"\"\"\n", + " \"\"\"책 추천 정보를 나타내는 스키마\n", + " \n", + " 제목, 저자, 장르, 평점, 요약을 구조화합니다.\n", + " \"\"\"\n", "\n", - " title: str = Field(description=\"Book title\")\n", - " author: str = Field(description=\"Author name\")\n", + " title: str = Field(description=\"Book title\") # 책 제목\n", + " author: str = Field(description=\"Author name\") # 저자명\n", " genre: Literal[\"fiction\", \"non-fiction\", \"science\", \"history\", \"biography\"] = Field(\n", " description=\"Book genre\"\n", - " )\n", - " rating: int = Field(description=\"Rating from 1-5\", ge=1, le=5)\n", - " summary: str = Field(description=\"Brief summary of the book\")\n", + " ) # 장르\n", + " rating: int = Field(description=\"Rating from 1-5\", ge=1, le=5) # 평점 (1-5)\n", + " summary: str = Field(description=\"Brief summary of the book\") # 책 요약\n", "\n", "\n", "class MovieRecommendation(BaseModel):\n", - " \"\"\"Movie recommendation with details.\"\"\"\n", - "\n", - " title: str = Field(description=\"Movie title\")\n", - " director: str = Field(description=\"Director name\")\n", - " year: int = Field(description=\"Release year\")\n", + " \"\"\"영화 추천 정보를 나타내는 스키마\n", + " \n", + " 제목, 감독, 개봉년도, 장르, 평점을 구조화합니다.\n", + " \"\"\"\n", + "\n", + " title: str = Field(description=\"Movie title\") # 영화 제목\n", + " director: str = Field(description=\"Director name\") # 감독명\n", + " year: int = Field(description=\"Release year\") # 개봉년도\n", " genre: Literal[\"action\", \"comedy\", \"drama\", \"horror\", \"sci-fi\"] = Field(\n", " description=\"Movie genre\"\n", - " )\n", - " rating: int = Field(description=\"Rating from 1-5\", ge=1, le=5)\n", + " ) # 장르\n", + " rating: int = Field(description=\"Rating from 1-5\", ge=1, le=5) # 평점 (1-5)\n", "\n", "\n", - "# 에이전트 생성\n", + "# 에이전트 생성 - Union 타입으로 여러 추천 유형 지원\n", "agent = create_agent(\n", " model=model,\n", " tools=[],\n", @@ -636,14 +917,14 @@ " system_prompt=\"You are a helpful entertainment recommendation assistant.\",\n", ")\n", "\n", - "# 책 추천\n", + "# 책 추천 요청\n", "result1 = agent.invoke(\n", " {\"messages\": [{\"role\": \"user\", \"content\": \"Recommend a good science fiction book\"}]}\n", ")\n", "print(\"Book recommendation:\")\n", "print(result1[\"structured_response\"])\n", "\n", - "# 영화 추천\n", + "# 영화 추천 요청\n", "result2 = agent.invoke(\n", " {\n", " \"messages\": [\n", @@ -657,15 +938,42 @@ }, { "cell_type": "markdown", - "source": "---\n\n## 정리\n\n이 튜토리얼에서는 LangGraph 에이전트의 구조화된 출력 기능을 학습했습니다.\n\n**핵심 개념 요약:**\n\n| 개념 | 설명 |\n|:---|:---|\n| **ProviderStrategy** | OpenAI, Grok 등 네이티브 지원 모델용 (가장 신뢰성 높음) |\n| **ToolStrategy** | 도구 호출을 통한 구조화된 출력 (대부분의 모델 지원) |\n| **Union 타입** | 여러 스키마 중 자동 선택 |\n| **handle_errors** | 오류 발생 시 자동 재시도 및 수정 |\n\n**스키마 정의 방법:**\n- Pydantic 모델: 가장 풍부한 검증 기능 제공\n- 데이터클래스: 간단한 스키마 정의\n- TypedDict: 딕셔너리 형태로 반환\n\n**실전 팁:**\n- 필드에 명확한 description 제공\n- `Literal` 타입으로 허용값 제한\n- `ge`, `le` 등 검증자로 범위 지정\n- Union 타입으로 다양한 응답 유형 처리", - "metadata": {} + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 정리\n", + "\n", + "이 튜토리얼에서는 LangGraph 에이전트의 구조화된 출력 기능을 학습했습니다. 구조화된 출력을 사용하면 에이전트의 응답을 예측 가능한 형식으로 받아 애플리케이션에서 쉽게 처리할 수 있습니다.\n", + "\n", + "**핵심 개념 요약:**\n", + "\n", + "| 개념 | 설명 |\n", + "|:---|:---|\n", + "| **ProviderStrategy** | OpenAI, Anthropic, Grok 등 네이티브 지원 모델용 (가장 신뢰성 높음) |\n", + "| **ToolStrategy** | 도구 호출을 통한 구조화된 출력 (대부분의 모델 지원) |\n", + "| **Union 타입** | 여러 스키마 중 자동 선택 |\n", + "| **handle_errors** | 오류 발생 시 자동 재시도 및 수정 |\n", + "\n", + "**스키마 정의 방법:**\n", + "- **Pydantic 모델**: 가장 풍부한 검증 기능 제공 (권장)\n", + "- **데이터클래스**: 간단한 스키마 정의, 추가 의존성 없음\n", + "- **TypedDict**: 딕셔너리 형태로 반환, JSON 직렬화 용이\n", + "\n", + "**실전 팁:**\n", + "- 필드에 명확한 `description` 제공\n", + "- `Literal` 타입으로 허용값 제한\n", + "- `ge`, `le` 등 검증자로 범위 지정\n", + "- `Union` 타입으로 다양한 응답 유형 처리\n", + "- 프로덕션에서는 `handle_errors=True` 사용 (기본값)" + ] } ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "langgraph-v1-tutorial", "language": "python", - "name": "python3" + "name": "langgraph-v1-tutorial" }, "language_info": { "codemirror_mode": { @@ -677,9 +985,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.11" + "version": "3.11.9" } }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +}