Inflearn brand logo image

인프런 커뮤니티 질문&답변

산마로님의 프로필 이미지
산마로

작성한 질문수

모두를 위한 대규모 언어 모델 LLM Part 5 - LangGraph로 나만의 AI 에이전트 만들기

STORM(Synthesis of Topic Outlines through Retrieval and Multi-perspective Question Asking) 아키텍처 구현하기 (1/4)

STORM 관련 ipynb 소스에서

작성

·

66

0

안녕하세요.. 수업노트에 있는 storm colab 파일을 따라 하는데..

import json

from langchain_core.runnables import RunnableConfig


async def gen_answer(
    state: InterviewState,
    config: Optional[RunnableConfig] = None,
    name: str = "Subject_Matter_Expert",
    max_str_len: int = 15000,
):
    swapped_state = swap_roles(state, name)  # Convert all other AI messages
    # 쿼리 생성
    queries = await gen_queries_chain.ainvoke(swapped_state)
    query_results = await search_engine.abatch(
        queries["parsed"].queries, config, return_exceptions=True
    )
    successful_results = [
        res for res in query_results if not isinstance(res, Exception)
    ]
    # url와 콘텐츠 추출
    all_query_results = {
        res["url"]: res["content"] for results in successful_results for res in results
    }
    # We could be more precise about handling max token length if we wanted to here
    dumped = json.dumps(all_query_results)[:max_str_len]
    ai_message: AIMessage = queries["raw"]
    tool_call = queries["raw"].tool_calls[0]
    tool_id = tool_call["id"]
    tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)
    swapped_state["messages"].extend([ai_message, tool_message])
    # Only update the shared state with the final answer to avoid
    # polluting the dialogue history with intermediate messages
    generated = await gen_answer_chain.ainvoke(swapped_state)
    cited_urls = set(generated["parsed"].cited_urls)
    # Save the retrieved information to a the shared state for future reference
    cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}
    formatted_message = AIMessage(name=name, content=generated["parsed"].as_str)
    return {"messages": [formatted_message], "references": cited_references}


이 부분에서 궁금한 것이 생겼습니다. 중간에

tool_call = queries["raw"].tool_calls[0]
    tool_id = tool_call["id"]

 

중간에 tool_calls 관련 정보를 호출하는데..그럴려면

gen_queries_chain

이 체인에 tool_bind된 llm이 사용되어야 하는 것 아닌가요? duckduckgo 관련 search_engine함수를 @tool을 이용해서 tool로 선언한 것 같은데.. 해당 퉁을 llm에 바인딩하는 것을 못보아서.. tool index 부분에서

Cell In[46], line 30, in gen_answer(state, config, name, max_str_len) 28 dumped = json.dumps(all_query_results)[:max_str_len] 29 ai_message: AIMessage = queries["raw"] ---> 30 tool_call = queries["raw"].tool_calls[0] 31 tool_id = tool_call["id"] 32 tool_message = ToolMessage(tool_call_id=tool_id, content=dumped) IndexError: list index out of range

가 발생하는 것 같습니다.

 

어떻게 수정하면 되는지 알려주세요..

답변 2

0

AISchool님의 프로필 이미지
AISchool
지식공유자

안녕하세요~. 반갑습니다.

먼저 수강에 불편을 드려서 죄송합니다.

강의 영상 촬영 시점 이후로 많은 시간이 흐르면서 라이브러리 사용법등에 변화가 생긴것 같습니다.

우선 실습 같은 경우 2025-01-16에 최신 버전에 맞게 정리한 아래 colab 링크로 실습 진행 부탁드립니다.

 

https://inf.run/S2BhG

 

해당 부분로직이 복잡해서 충분히 헷갈리실수도 있으실 것 같아요. 조금 자세하게 풀어서 설명드리면

 

async def gen_answer(
    state: InterviewState,
    config: Optional[RunnableConfig] = None,
    name: str = "Subject_Matter_Expert",
    max_str_len: int = 15000,
):
    swapped_state = swap_roles(state, name)  # Convert all other AI messages
    # 쿼리 생성
    queries = await gen_queries_chain.ainvoke(swapped_state)
    query_results = await search_engine.abatch(
        queries["parsed"].queries, config, return_exceptions=True
    )
    successful_results = [
        res for res in query_results if not isinstance(res, Exception)
    ]
    # url와 콘텐츠 추출
    all_query_results = {
        res["url"]: res["content"] for results in successful_results for res in results
    }
    # We could be more precise about handling max token length if we wanted to here
    dumped = json.dumps(all_query_results)[:max_str_len]
    ai_message: AIMessage = queries["raw"]
    tool_call = queries["raw"].tool_calls[0]
    tool_id = tool_call["id"]
    tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)
    swapped_state["messages"].extend([ai_message, tool_message])
    # Only update the shared state with the final answer to avoid
    # polluting the dialogue history with intermediate messages
    generated = await gen_answer_chain.ainvoke(swapped_state)
    cited_urls = set(generated["parsed"].cited_urls)
    # Save the retrieved information to a the shared state for future reference
    cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}
    formatted_message = AIMessage(name=name, content=generated["parsed"].as_str)
    return {"messages": [formatted_message], "references": cited_references}

최신 버전에 맞게 변경하기 전(2025-01-16 수정본 이전)에 코드를 보시면 이렇게 tool_call과 tool_call_id를 가져오는 부분이 있는데 여기서 tool_call은 duckduckgosearch tool을 의미하는게 아니라 structured output을 호출하는 것을 tool 호출로 간주하는 것입니다. (duckduckgo search tool 호출 결과는 query_results로 가져옵니다.)

 

    tool_call = queries["raw"].tool_calls[0]
    tool_id = tool_call["id"]
    tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)

gen_queries_chain을 보시면 아래와 같이

)
gen_queries_chain = gen_queries_prompt | ChatOpenAI(
    model="gpt-4o-mini"
).with_structured_output(Queries, include_raw=True)

Queries라는 구조를 강제하는 structured output이 binding되어 있습니다.

 

하지만 structured output 호출이후 structured output의 호출 결과는 사용하지 않고 dumped라는 변수에 저장된 duckduckgo searchtool 호출결과를 아래와 같이 ToolMessage에 강제 지정해버립니다.

그럼 왜 structured output을 호출해서 parsing하려고 하느냐?라고 한다면 tool_call의 id 값을 가져와서 ToolMessage를 지정할때 id값에 넣어주려고 한것 같습니다.

    tool_call = queries["raw"].tool_calls[0]
    tool_id = tool_call["id"]
    tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)

하지만 최신버전에서는 structured output를 호출했을때 이 결과를 tool_calls로 리턴해주지 않기 때문에 에러가 발생한 것입니다.

 

2025-01-16에 제가 수정한 코드를 보시면 structured output을 호출해서 tool_id를 가져오는 로직을 삭제하는 형태로 우회해서 처리한 모습을 확인해 보실수 있습니다.

 

기존 코드에서도 structured output의 호출 결과는 실제로 사용하지는 않고 오직 tool_id만을 얻는 용도로 사용했었기 때문에(어차피 tool message 선언쪽에 실제 값은 duckduckgo search 결과를 가져온 query_results를 사용하고 로직상 tool_id가 크게 의미 있는 값은 아닙니다.) 사실 어떻게 보면 불필요하게 복잡한 형태로 구현되어 있었습니다.

 

그럼 왜 저렇게 복잡하게 tool_call_id를 얻어오려고 한것인가요?라고 한다면 추측하면 예전 버전에서는 ToolMessage를 선언할때 tool_call_id가 필수값으로 지정되어있었어야 했었던것이 아닐까 싶습니다.

 

좋은 하루되세요.

감사합니다.

0

안녕하세요, 인프런 AI 인턴입니다. 산마로님!

질문 주신 내용은 LangChain을 사용하여 여러 개의 AI 모델과 도구를 연결하고 활용하는 데 관련된 부분입니다. 주어진 코드에서는 여러 도구와 체인을 사용하여 검색 요청을 생성하고 그 결과를 처리하는 과정이 포함되어 있습니다.

문제의 원인

tool_calls에 접근하려 하는데 IndexError가 발생하는 점으로 보아, tool_calls 리스트가 비어 있어서 문제가 발생하는 것으로 판단됩니다. 이 문제는 gen_queries_chain에서 tool_bind된 모델이나 도구가 제대로 설정되지 않았거나 올바르지 않은 방법으로 사용되고 있기 때문일 수 있습니다.

해결 방안

  1. Tool 바인딩 확인:

    • gen_queries_chain에서 어떤 LLM(대규모 언어 모델)이나 도구가 사용되고 있는지를 명확히 확인해야 합니다. tool_calls는 해당 체인에서 사용된 도구 호출 정보를 담고 있어야 하므로, 도구와 적절히 연결되어 있지 않다면 IndexError가 발생할 수 있습니다. @tool 데코레이터가 제대로 적용되어 있는지 확인하세요.
  2. 체인 로직 및 초기화 점검:

    • gen_queries_chain 초기화 시에 필요로 하는 모든 요소가 적절히 처리되어 있는지 확인하세요. 여기에는 입력 형식, 필요한 도구의 준비 및 바인딩 등이 포함됩니다.
  3. 디버깅:

    • 코드 실행 중간에 중간 결과를 print해 보거나 디버그를 사용하여 queries["raw"]의 내용물을 파악해 보세요. 올바르게 구성되었는지 확인하는 것이 중요합니다.
  4. 매뉴얼 수정:

    • LangChain 도큐멘테이션이나 관련 지식공유자들의 예제 코드를 참고하여, 어떻게 도구가 바인딩되고 어떻게 호출되는지에 대한 정확한 구문을 확인하세요.

유사한 해결 방안

  • storm 아키텍쳐 구현하기: 유사한 방식으로 AI 모델을 체인과 도구로 연계하려고 할 때 발생할 수 있는 오류와 해결 과정에 대해 다룹니다.

문제가 해결되길 바라며, 여전히 문제 해결이 어렵거나 추가 정보가 필요한 경우, 관련 강의의 지식공유자에게 문의하시거나 추가 정보를 제공해 주시면 더 도움을 드리도록 하겠습니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

산마로님의 프로필 이미지
산마로
질문자

Tool binding을 어디서 해야 하는지를 알려주셔야 하는 것 아닌가요? 어디서 하는지를 제가 알면, 제가 왜 이 강의를 돈을 주면서 수강할까요?

산마로님의 프로필 이미지
산마로
질문자

혹시나 해선..

tools = [search_engine]
bindtool_fastllm = fast_llm.bind_tools(tools=tools)
gen_queries_chain_withbindtool = gen_queries_prompt|bindtool_fastllm.with_structured_output(Queries, include_raw=True)

와 같이 퉁바인딩을 시도하고 바인딩된 llm을 이용해서 chain 을 구성한 후,

queries = await gen_queries_chain_withbindtool.ainvoke(swapped_state)

를 실행했는데.. 반환된 queris의 raw 값에는 역시 tool_calls라는 것이 없습니다. 제가 접근한 방법이 잘못된 것 같은데.. 무엇이 잘못되었는지를 짚어주시면 좋겠습니다. 단순히 fast_llm 대신 tools가 바인딩된 fast_llm 으로 사용했는데, 이럽 접근이 잘못된 것인가요?

 

 

 

 

AISchool님의 프로필 이미지
AISchool
지식공유자

안녕하세요~. 반갑습니다.

먼저 수강에 불편을 드려서 죄송합니다.

강의 영상 촬영 시점 이후로 많은 시간이 흐르면서 라이브러리 사용법등에 변화가 생긴것 같습니다.

우선 실습 같은 경우 2025-01-16에 최신 버전에 맞게 정리한 아래 colab 링크로 실습 진행 부탁드립니다.

 

https://inf.run/S2BhG

 

해당 부분로직이 복잡해서 충분히 헷갈리실수도 있으실 것 같아요. 조금 자세하게 풀어서 설명드리면

 

async def gen_answer(
    state: InterviewState,
    config: Optional[RunnableConfig] = None,
    name: str = "Subject_Matter_Expert",
    max_str_len: int = 15000,
):
    swapped_state = swap_roles(state, name)  # Convert all other AI messages
    # 쿼리 생성
    queries = await gen_queries_chain.ainvoke(swapped_state)
    query_results = await search_engine.abatch(
        queries["parsed"].queries, config, return_exceptions=True
    )
    successful_results = [
        res for res in query_results if not isinstance(res, Exception)
    ]
    # url와 콘텐츠 추출
    all_query_results = {
        res["url"]: res["content"] for results in successful_results for res in results
    }
    # We could be more precise about handling max token length if we wanted to here
    dumped = json.dumps(all_query_results)[:max_str_len]
    ai_message: AIMessage = queries["raw"]
    tool_call = queries["raw"].tool_calls[0]
    tool_id = tool_call["id"]
    tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)
    swapped_state["messages"].extend([ai_message, tool_message])
    # Only update the shared state with the final answer to avoid
    # polluting the dialogue history with intermediate messages
    generated = await gen_answer_chain.ainvoke(swapped_state)
    cited_urls = set(generated["parsed"].cited_urls)
    # Save the retrieved information to a the shared state for future reference
    cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}
    formatted_message = AIMessage(name=name, content=generated["parsed"].as_str)
    return {"messages": [formatted_message], "references": cited_references}

최신 버전에 맞게 변경하기 전(2025-01-16 수정본 이전)에 코드를 보시면 이렇게 tool_call과 tool_call_id를 가져오는 부분이 있는데 여기서 tool_call은 duckduckgosearch tool을 의미하는게 아니라 structured output을 호출하는 것을 tool 호출로 간주하는 것입니다. (duckduckgo search tool 호출 결과는 query_results로 가져옵니다.)

 

    tool_call = queries["raw"].tool_calls[0]
    tool_id = tool_call["id"]
    tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)

gen_queries_chain을 보시면 아래와 같이

)
gen_queries_chain = gen_queries_prompt | ChatOpenAI(
    model="gpt-4o-mini"
).with_structured_output(Queries, include_raw=True)

Queries라는 구조를 강제하는 structured output이 binding되어 있습니다.

 

하지만 structured output 호출이후 structured output의 호출 결과는 사용하지 않고 dumped라는 변수에 저장된 duckduckgo searchtool 호출결과를 아래와 같이 ToolMessage에 강제 지정해버립니다.

그럼 왜 structured output을 호출해서 parsing하려고 하느냐?라고 한다면 tool_call의 id 값을 가져와서 ToolMessage를 지정할때 id값에 넣어주려고 한것 같습니다.

    tool_call = queries["raw"].tool_calls[0]
    tool_id = tool_call["id"]
    tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)

하지만 최신버전에서는 structured output를 호출했을때 이 결과를 tool_calls로 리턴해주지 않기 때문에 에러가 발생한 것입니다.

 

2025-01-16에 제가 수정한 코드를 보시면 structured output을 호출해서 tool_id를 가져오는 로직을 삭제하는 형태로 우회해서 처리한 모습을 확인해 보실수 있습니다.

 

기존 코드에서도 structured output의 호출 결과는 실제로 사용하지는 않고 오직 tool_id만을 얻는 용도로 사용했었기 때문에(어차피 tool message 선언쪽에 실제 값은 duckduckgo search 결과를 가져온 query_results를 사용하고 로직상 tool_id가 크게 의미 있는 값은 아닙니다.) 사실 어떻게 보면 불필요하게 복잡한 형태로 구현되어 있었습니다.

 

그럼 왜 저렇게 복잡하게 tool_call_id를 얻어오려고 한것인가요?라고 한다면 추측하면 예전 버전에서는 ToolMessage를 선언할때 tool_call_id가 필수값으로 지정되어있었어야 했었던것이 아닐까 싶습니다.

 

좋은 하루되세요.

감사합니다.

산마로님의 프로필 이미지
산마로

작성한 질문수

질문하기