root layout

패스트캠퍼스

  1. 강의 질문
  2. AI TECH

Part2-ch2 Agentic RAG예시코드 실행 안되는 문제와 해결방법

2025.11.30 18:26 수정

현재 예시로 제공된 코드가 실행이 안되서 관련 자료 로그 남깁니다.


실행시 아래와 같은 에러 발생

1) if hasattr(last_message, "tool_calls") and last_message.tool_calls: > Cannot access attribute "tool_calls" for class

2) result = search_app.invoke( > ValueError


GPT5.1답변처럼 변경해주니 실행이 됩니다. 이게 맞는 해결책인지 궁금하네요 아님 버전을 올리면 또 달라질지


참고로 langchain/langgraph 패키지 2025.11.30기준 최신버전으로 테스트해봐도 마찬가지임.

```

"langgraph>=1.0.4",

"langchain>=1.1.0",

"langchain-core>=1.1.0",

"langchain-openai>=1.1.0",

"langchain-anthropic>=1.2.0",

"langchain-google-genai>=3.2.0",

"langchain-community>=0.4",

"langgraph-cli[inmem]>=0.4.0",

"langchain-mcp-adapters",

```



pyproject.toml

```toml

[project]

# Only needed so uv/pip recognizes it as a project

name = "fc-multiagent"

version = "0.0.0"

description = "A collection of tutorial notebooks"

requires-python = ">=3.11,<3.15"

# Dependencies you need in the notebooks

dependencies = [

"langgraph>=1.0.3",

"langchain>=1.0.8",

"langchain-core>=1.1.0",

"langchain-openai>=1.0.3",

"langchain-anthropic>=1.1.0",

"langchain-google-genai>=3.1.0",

"langchain-community>=0.4",

"langgraph-cli[inmem]>=0.4.0",

"langchain-mcp-adapters",

"rich>=14.2.0",

"icecream>=2.1.8",

"debugpy>=1.8.17",

"nbconvert>=7.16.6",

"langchain-text-splitters>=1.0.0",

"langchain-tavily>=0.2.13",

"langchain-qdrant>=1.1.0",

"python-dotenv>=1.2.1",

"arxiv>=2.3.1",

"jupyterlab>=4.5.0",

]

[project.optional-dependencies]

# Extra tools for development

dev = [

"nbdime>=4.0.2",

"ruff>=0.14.6",

"mypy>=1.18.1"

]

[tool.ruff]

# What to check

lint.select = [

"E", # pycodestyle

"F", # pyflakes

"I", # isort

"D", # pydocstyle

"D401", # docstring first line imperative

"UP", # pyupgrade

]

# Rules to ignore

lint.ignore = [

"T201", # allow print()

"UP006", # keep using typing.List etc.

"UP007", # allow Union[X, Y]

"UP035", # ignore Self type suggestion

"D417", # don't require param docs

"E501", # ignore line length

"D101", # ignore docstring for these labs

"D103", # ignore docstring for these labs

]

[tool.ruff.lint.per-file-ignores]

"tests/*" = ["D", "UP"]

[tool.ruff.lint.pydocstyle]

convention = "google"

```




"""ToolRuntime과 Command를 활용한 Tool 작성"""

from typing import TypedDict, Annotated

from langgraph.graph import StateGraph, START, END

from langgraph.graph.message import add_messages

from langgraph.prebuilt import ToolNode

from langchain.tools import tool, ToolRuntime

from langgraph.types import Command

from langchain.messages import HumanMessage, AIMessage, ToolMessage, AnyMessage

from langchain.chat_models import init_chat_model


class SearchState(TypedDict):

   """검색 에이전트 State"""


   messages: Annotated[list[AnyMessage], add_messages]

   search_count: int  # 검색 횟수 추적

   user_name: str  # 사용자 이름





@tool

def search_knowledge_base(

   query: str,

   runtime: ToolRuntime,  # 핵심: ToolRuntime 자동 DI 주입

) -> Command:

   """

   지식 베이스에서 검색하며 State를 업데이트합니다.



   Args:

       query: 검색어

       runtime: LangGraph runtime (자동 주입)



   Returns:

       Command 객체 (State 업데이트 포함)

   """

   # 1. 현재 State 접근

   current_state = runtime.state

   search_count = current_state.get("search_count", 0)

   user_name = current_state.get("user_name", "사용자")


   print(f"\n[Tool] search_knowledge_base 실행")

   print(f"  - 사용자: {user_name}")

   print(f"  - 현재까지 검색 횟수: {search_count}")

   print(f"  - 검색어: {query}")


   # 2. 실제 검색 수행 (여기서는 모의 데이터)

   mock_results = {

       "langgraph": "LangGraph는 LLM 기반 애플리케이션을 위한 상태 기반 워크플로우 프레임워크입니다.",

       "tool": "Tool은 LLM이 외부 기능을 호출할 수 있게 하는 인터페이스입니다.",

   }


   result = mock_results.get(

       query.lower(), f"'{query}'에 대한 검색 결과를 찾을 수 없습니다."

   )



   # 3. Command로 State 업데이트

   # return Command(

   #     update={

   #         "search_count": search_count + 1,  # 검색 횟수 증가

   #     },

   #     # tool 결과는 자동으로 messages에 추가됨

   # )

   # Tool의 반환값(여기서는 Command)은 자동으로 ToolMessage로 변환되어 messages에 추가됩니다

   return Command(

       update={

           "search_count": search_count + 1,

           "messages": [ToolMessage(content=result, tool_call_id=runtime.tool_call_id)],

       }

   )


@tool

def reset_search_count(runtime: ToolRuntime) -> Command:

   """

   검색 횟수를 초기화합니다.

   Args:

       runtime: LangGraph runtime

   """

   current_count = runtime.state.get("search_count", 0)

   print(f"\n[Tool] 검색 카운터 초기화 (현재: {current_count} → 0)")


   # return Command(update={"search_count": 0})

   return Command(

       update={

           "search_count": 0,

           "messages": [

               ToolMessage(

                   content=f"검색 횟수를 {current_count}에서 0으로 초기화했습니다.",

                   tool_call_id=runtime.tool_call_id,

               )

           ],

       }

   )


# Tool 리스트

search_tools = [search_knowledge_base, reset_search_count]


llm = init_chat_model("openai:gpt-4.1-mini", temperature=0)


# LLM 설정

llm_with_tools = llm.bind_tools(search_tools)


def agent_node(state: SearchState) -> dict:

   """LLM Agent Node"""

   response = llm_with_tools.invoke(state["messages"])

   return {"messages": [response]}


# def should_continue(state: SearchState) -> str:

#     """Tool 호출 여부 확인"""

#     last_message = state["messages"][-1]

#     if hasattr(last_message, "tool_calls") and last_message.tool_calls:

#         return "tools"

#     return "end"



def should_continue(state: SearchState) -> str:

   last = state["messages"][-1]



   # v1 표준 방식 – content_blocks 검사

   for block in getattr(last, "content_blocks", []):

       if block.get("type") == "tool_call":

           return "tools"

   return "end"


# 그래프 생성

search_graph = StateGraph(SearchState)

search_graph.add_node("agent", agent_node)

search_graph.add_node("tools", ToolNode(search_tools))



search_graph.add_edge(START, "agent")

search_graph.add_conditional_edges(

   "agent", should_continue, {"tools": "tools", "end": END}

)

search_graph.add_edge("tools", "agent")

search_app = search_graph.compile()


# 테스트

print("\n" + "=" * 60)

print("ToolRuntime 데모 시작")

print("=" * 60)



result = search_app.invoke(

   {

       "messages": [HumanMessage(content="LangGraph에 대해 검색해줘")],

       "search_count": 0,

       "user_name": "홍길동",

   }

)



print("\n=== 최종 State ===")

print(f"총 검색 횟수: {result['search_count']}")

print(f"사용자: {result['user_name']}")

print(f"메시지 수: {len(result['messages'])}")

result['messages']





GPT참고 답변과 정리

강의 원본 예시코드 문제점과 해결책

1) 오류 1 — last_message.tool_calls 접근 불가

● 원인
  • LangChain v1에서는 message.tool_calls 필드가 삭제되었다.

  • 모든 tool call 정보는 content_blocks 안에 들어간다.

● 해결

should_continue에서 content_blocks 기반으로 tool call을 탐지해야 한다.


def should_continue(state: SearchState) -> str:

    msg = state["messages"][-1]

    for block in getattr(msg, "content_blocks", []):

        if block.get("type") == "tool_call":

            return "tools"

    return "end"

2) 오류 2 — ToolNode: “ToolMessage가 Command.update에 없다”

● 원인

LangGraph v1은 모든 tool 호출에 대해 Command(update={"messages": [ToolMessage(...)]}) 형식을 반드시 요구한다.


즉, Tool이 반환한 결과를 ToolMessage로 직접 State에 넣어야만 한다.


구버전처럼 “Tool 반환하면 자동 message append”는 없어졌다.


● 해결

모든 tool에서 Command.update에 ToolMessage를 직접 넣는다.


from langchain.messages import ToolMessage


@tool

def search_knowledge_base(query: str, runtime: ToolRuntime) -> Command:

    result = ...

    return Command(

        update={

            "search_count": runtime.state["search_count"] + 1,

            "messages": [

                ToolMessage(

                    content=result,

                    tool_call_id=runtime.tool_call_id

                )

            ]

        }

    )



reset tool도 동일하게 ToolMessage 포함.

🔥 한 문장 요약

LangChain v1에서는 tool_calls 속성을 직접 읽으면 안 되고, 모든 Tool은 반드시 Command.update에 ToolMessage를 명시적으로 넣어야 한다.


이 두 가지를 수정하면 example3는 완전히 정상 동작한다.



ps, 본 게시판 글쓰기시 code block에 대한 copy/paste가 안먹히네요.하 이 게시판은 markdown code syntax highlighting도 안되고 좀 답답하네요..



답변 

연관 질문

커뮤니티 질문보기