Scriptone Scriptone

PythonにおけるLangChain Nimbleの使用例

Nimble Retriever가 LangChain에 통합되었다고 하여 무엇인지 이해가 안 가서 코드를 작성하여 검증해 보았습니다.

Langchain Nimbleとは

이번에 LangChain에 통합된 Nimble은 웹상의 데이터를 수집하고 콘텐츠 추출에 특화된 AI를 탑재한 Web 스크래핑 플랫폼입니다. 웹 페이지의 HTML 구성은 다양하며, Bot에 의한 크롤링이 방지된 사이트도 있습니다. 구조화된 데이터를 추출하는 데 이 다양성과 Bot의 대책이 어려움을 초래합니다. Nimble은 이러한 대책을 수행하여 웹 페이지에서 콘텐츠를 정확하고 깨끗하게 추출할 수 있으며, 이 기능이 Web API로 제공됩니다. 이번 LangChain으로의 통합으로 LangChain의 일반적인 취급 방식에 따르지만 Nimble의 Web API를 이용할 수 있게 되었으며, LangChain의 langchain_core.documents.base.Document[source] 형식으로 문서를 추출할 수 있게 되었습니다.

사용 방법

Python의 라이브러리로 제공되며, pip 등으로 도입할 수 있습니다.

pip install -U langchain-nimble

라이브러리를 도입한 후 Nimble의 Web 페이지에서 계정을 만드세요. 이메일 주소가 Gmail 등 무료 서비스인 경우 등록이 불가능할 수 있으니 주의하세요. 계정을 만든 후 API를 사용하기 위한 준비가 필요합니다. 로그인 후 왼쪽 사이드 메뉴의 ‘Pipelines’를 클릭한 후 ‘NimbleAPI’를 클릭합니다. Username & Password 아래에 3개의 텍스트 박스가 있지만, 그 중 가장 오른쪽에 있는 ‘Base64 token’이 LangChain Nimble에서 사용할 API 키입니다.

이 API 키를 복사하여 환경 변수 NIMBLE_API_KEY의 값에 복사한 토큰을 붙여넣어 주세요. Windows 등 환경 변수를 설정한 후 재시작하면 좋을 것 같습니다. Mac이나 Linux의 경우 source 명령어 또는 export 등을 사용하여 설정한 환경 변수를 사용할 수 있게 해주세요.

사용 예

GitHub에 샘플 코드를 게시하였습니다. 리포지토리를 클론한 후 uv sync를 실행하면 실행 환경을 바로 만들 수 있을 것입니다. 그 후 uv run main.py -q "{조사하고 싶은 내용을 작성}" -k {참조할 문서 수를 정수로 작성(Optional)}로 CLI를 통해 실행할 수 있습니다.

코드 전체

import pathlib
import logging
import tomllib
from typing_extensions import Any, TypedDict

import fire
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_nimble import NimbleSearchRetriever
from langgraph.graph import START, END, StateGraph
from pydantic import BaseModel, Field


logger = logging.getLogger(__name__)


class SummaryData(BaseModel):
    summary: str = Field(
        ...,
        description="取得したデータを要約したテキストを格納する。",
    )


class State(TypedDict):
    query: str
    k: int
    docs: list[str]
    summary: str
    config: dict[str, Any]


def get_config() -> dict[str, Any]:
    this_dir = pathlib.Path(__file__).parent
    config_file = this_dir / "config.toml"
    with config_file.open("rb") as f:
        return tomllib.load(f)


def retrieve(state: State) -> dict[str, Any]:
    """クエリに基づいてドキュメントを検索し、関連するコンテンツをリスト形式で返す関数。NimbleSearchRetrieverを使用。"""
    retriever = NimbleSearchRetriever(k=state["k"])
    example_docs = retriever.invoke(state["query"])
    doc_list: list[str] = [doc.page_content for doc in example_docs]
    return {"docs": doc_list}


def summarize(state: State) -> dict[str, Any]:
    """ドキュメントのリストを要約する関数。ChatPromptTemplateとChatOpenAIを使用して要約を生成。"""
    prompt = ChatPromptTemplate.from_template(
        template=state["config"]["summarize"]["prompt"]
    )
    llm = ChatOpenAI(model_name=state["config"]["summarize"]["model"])
    chain = prompt | llm.with_structured_output(SummaryData)
    context = "\n\n".join(doc for doc in state["docs"])
    logger.debug(context)
    res: SummaryData = chain.invoke({"context": context})
    return {"summary": res.summary}


def proc(q: str, k: int = 5):
    """指定されたクエリとk値で処理を実行し、要約を取得する関数。kが正の整数であることを検証。"""
    if not isinstance(k, int) or k < 1:
        raise ValueError("kは1以上の整数でなければなりません。")
    # graphを作る
    graph_builder = StateGraph(State)
    # Nodeを追加する
    graph_builder.add_node("retrieve", retrieve)
    graph_builder.add_node("summarize", summarize)
    # Edgeを追加する
    graph_builder.add_edge(START, "retrieve")
    graph_builder.add_edge("retrieve", "summarize")
    graph_builder.add_edge("summarize", END)
    # Compile
    app = graph_builder.compile()
    # appを実行する
    state: State = {
        "query": q,
        "k": k,
        "docs": [],
        "summary": "",
        "config": get_config(),
    }
    res = app.invoke(state)
    # 結果を出力する
    logger.info(res["summary"])


def main():
    fire.Fire(proc)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    main()

解説

간단한 LangChain, LangGraph를 사용한 처리 흐름을 구성하고 있으며, Langchain Nimble로 문서를 추출하고, OpenAI의 gpt-4.1-nano로 수집한 문서를 모순 없이 요약하는 지시를 하고 있습니다. 모델과 프롬프트는 외부의 config.toml에 다음과 같이 정의하고 있습니다.

[summarize]
model = "gpt-4.1-nano"
prompt = """
コンテキストを20センテンス以内にかつ矛盾生じないようにして要約してください。

Context:
{context}
"""

OpenAI의 모델을 사용하고 있지만 Nimble과 마찬가지로 환경 변수를 정의해야 하며, OPENAI_API_KEY에 API 키를 설정하여 실행해야 하니 주의하세요. Nimble의 처리는 간단하며, NimbleSearchRetriever에 문서를 얼마나 수집할지 양의 정수 값으로 설정합니다. 그 후 retriever로 Langchain의 익숙한 invoke 메서드를 사용하여 찾고 싶은 내용을 문자열로 제공하면 Nimble 측에서 스크래핑을 수행하고, Langchain의 Document 목록을 반환합니다.

def retrieve(state: State) -> dict[str, Any]:
    """クエリに基づいてドキュメントを検索し、関連するコンテンツをリスト形式で返す関数。NimbleSearchRetrieverを使用。"""
    retriever = NimbleSearchRetriever(k=state["k"])
    example_docs = retriever.invoke(state["query"])
    doc_list: list[str] = [doc.page_content for doc in example_docs]
    return {"docs": doc_list}

그 후, 수집한 문서를 요약하거나, 문서 기반의 정보만으로 답변(RAG), 관련 정보를 동적으로 검색 쿼리를 만들어 추가 검색하는 등 발전적인 처리로 연결할 수 있습니다. LangChain만으로도 요약이나 RAG는 충분히 가능하지만, 동적 검색이나 반복 처리의 판단이 포함된 복잡한 워크플로를 구축하는 경우 LangGraph도 함께 사용하면 좋을 것입니다.

まとめ

Nimble을 사용하면 검색과 스크래핑을 높은 정확도로 수행할 수 있으며, 검색 관련 오류 발생률을 낮출 수 있습니다. 또한 쿼리를 통해 문서를 추출하는 절차는 간단하므로, RAG나 DeepResearch의 자작 등 개발에서의 놀이 폭이 넓어질 것입니다. LangChain의 사용 감각에 맞게 쉽게 Nimble을 시험해 볼 수 있으니 꼭 시도해 보시기 바랍니다!

관련 글