LLM・RAG

LangChainでRAGを構築する手順|v1.4系の2-Step・エージェント方式とPGVectorStore移行

LangChainでRAGを構築する手順|v1.4系の2-Step・エージェント方式とPGVectorStore移行

LangChainでRAG(検索拡張生成)を組む手順を、2026年10月時点の最新版 langchain 1.4.7 で実行し直した結果とともに整理します。0.x系の記事に載っている from langchain.chains import ... は今の版では動かず、公式のRAGチュートリアルもDeep Agents前提のページへ移りました。社内規程を検索して答えるRAGを題材に、文書の分割から回答生成、本番のベクトルストアへの移行までを順に見ていきます。

まとめ

  • パッケージ:本体 langchain 1.4.7 は Python 3.10 以上。埋め込みとLLMは langchain-openai や langchain-ollama などのプロバイダ別パッケージを足します。
  • 移行:langchain.chains は存在しません。旧コードを動かすなら langchain-classic を入れて import 元を langchain_classic.chains に変えます。
  • 日本語の分割:既定の separators には句点が無く、文の途中で切れます。「。」を足し、keep_separator="end" も指定します。
  • 回答生成:必ず検索してから答えるなら dynamic_prompt ミドルウェアで作る2-Step RAG、検索するかどうかをモデルに任せるなら create_agent に検索ツールを渡すエージェント方式です。小型のローカルモデルではエージェント方式が検索を飛ばして誤答したため、2-Stepを選びます。
  • ストリーミング:langchain-core 1.7.0(2026年10月22日)から、stream_events(version="v3") の生イベントで届くツール引数が「ここまでの全体」から「追加分だけ」に変わりました。.tool_calls.get() と .output は変わらないので、生イベントを直接読むコードだけ直します。
  • 本番移行:langchain-postgres の PGVector は非推奨で、後継は PGVectorStore。InMemoryVectorStore 用に書いた関数型の filter は PGVectorStore ではエラーにならずに無視され、権限で絞ったはずの文書まで返ります。

社内文書の検索と回答生成におけるRAGとLangChainの役割分担

RAGは、質問を受けた時点で自社データを検索し、見つかった本文をプロンプトに差し込んでから回答させる方式です。LLMが学習していない社内規程も回答の根拠にできます。規程の改訂時は、検索用チャンクと埋め込みを更新し、旧版を削除することで検索対象へ反映します。LangChainが引き受けるのは、文書の分割、埋め込みモデルとベクトルストアの共通インターフェース、LLM呼び出しとツール実行のループという配線部分です。検索精度そのものは、どの埋め込みモデルとベクトルストアを選び、どう分割するかで決まります。

社内RAGを構築するv1.4系のパッケージ構成とインストール

社内文書RAGの実行環境をそろえるインストールと確認済みバージョン

Python 3.13 の仮想環境に本体一式を入れます。PyPIのlangchainは 1.4.7(2026年10月21日公開)が最新で、要求する Python は 3.10 以上です。

# 本体と分割、0.x系チェーン用の互換パッケージ
pip install langchain langchain-classic langchain-text-splitters

# 埋め込みとLLM(OpenAIなら langchain-openai、ローカルなら langchain-ollama)
pip install langchain-openai langchain-ollama numpy
langchain                1.4.7   (2026-10-21)
langchain-core           1.7.0   (2026-10-22)
langchain-text-splitters 1.1.3   (2026-10-02)
langchain-classic        1.0.8   (2026-06-10)
langchain-openai         1.7.2   (2026-10-14)
langchain-ollama         1.1.0   (2026-04-07)
langgraph                1.2.17  (2026-10-24)

上の版は本文の検証に使った組み合わせです。2026年10月31日時点の PyPIのlangchain-core は 1.7.3(10月29日公開)、langgraph は 1.2.19(10月30日公開)まで進んでいます。1.7.1〜1.7.3 のリリースノートに並ぶのは v3 ストリームのトレース用メタデータの保持やメッセージ結合の修正で、本記事のRAGのコードに関わる変更は見当たりません。1.7.0 で入ったストリーミングの変更は、後述の「ツール引数のストリーミング表示」の章で扱います。

langchain 1.4.7 の必須依存は langchain-core>=1.6.13、langgraph>=1.2.12,<1.3.0、pydantic の3つだけです。埋め込みモデルもLLMのクライアントも入らないので、pip install langchain だけではRAGは動きません。1.4.0(2026年9月3日)では MCP用の langchain.mcp 名前空間が追加されましたが、RAGの書き方は1.3系から変わっていません。

langchain.chains の消失とlangchain-classic

公式の移行ガイドは、レガシーチェーン(LLMChain など)、langchain.retrievers 配下のレトリーバー、indexing API、hub モジュールを langchain-classic へ移したと書いています。1.4.7 で旧来の import を実行すると次のとおりです。

>>> from langchain.chains import create_retrieval_chain
ModuleNotFoundError: No module named 'langchain.chains'
>>> from langchain.chains import RetrievalQA
ModuleNotFoundError: No module named 'langchain.chains'
>>> from langchain_classic.chains import create_retrieval_chain   # 通る

既存コードを延命するなら import 元の書き換えで足ります。新しく組むなら、後述の2-Step RAGとエージェント方式はどちらも langchain 本体だけで書けるので、langchain-classic に頼る理由はありません。v1.0の変更点全体は LangChain v1.0とは|create_agent・ミドルウェア・langchain-classicを実装例で解説にまとめています。

社内文書のチャンク分割|日本語で既定設定が文中で切る位置と分割結果

日本語の社内文書に対する既定separatorsと句読点追加の実測

分割には RecursiveCharacterTextSplitter を使います。既定の separators は改行2つ、改行、空白、空文字の順で、句点「。」が入っていません。日本語は単語の間に空白も無いため、1段落が chunk_size を超えると空文字で、つまり文字数ぴったりの位置で切られます。第1条〜第4条を条ごとに改行した560字の規程を、chunk_size=200 で3通りに分けた結果です(langchain-text-splitters 1.1.3)。規程は検証用に作った架空のものです。

from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter

text = (
    "第1条(目的)この規程は、会社が貸与する端末の取り扱いと、業務での外部サービス利用の手続きを定めます。"
    "対象は正社員、契約社員、派遣社員を含む全従業員です。\n"
    "第2条(持ち出し)端末を社外へ持ち出す場合は、持ち出しの3営業日前までに所定の申請書を提出します。"
    "申請は情報システム部の承認を得たうえで有効になり、承認前の持ち出しは認めません。"
    "出張など継続的に持ち出す場合は、期間をまとめて申請できます。ただし期間は最長で3か月とし、更新するときは改めて申請します。"
    "紛失や盗難が起きた場合は、発覚から1時間以内に情報システム部へ電話で連絡し、翌営業日までに報告書を提出します。\n"
    "第3条(外部サービス)生成AIを含む外部サービスに業務データを入力する場合は、事前に利用申請を行い、"
    "情報システム部が安全性を確認したサービスのみ利用できます。確認済みのサービスは社内ポータルの一覧で公開します。"
    "一覧にないサービスを使う必要があるときは、利用目的、入力するデータの種類、利用期間を記載して申請します。"
    "顧客の個人情報と未公開の財務情報は、確認済みのサービスであっても入力してはいけません。\n"
    "第4条(違反)この規程に違反した場合は、就業規則に基づき処分の対象となります。"
    "違反を発見した従業員は、所属長または情報システム部へ速やかに報告します。"
)
docs = [Document(page_content=text, metadata={"source": "kitei.md", "dept": "all"})]

default =RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
ja = RecursiveCharacterTextSplitter(
    chunk_size=200, chunk_overlap=20,
    separators=["\n\n", "\n", "。", "、", " ", ""],
)
ja_end = RecursiveCharacterTextSplitter(
    chunk_size=200, chunk_overlap=20, keep_separator="end",
    separators=["\n\n", "\n", "。", "、", " ", ""],
)
既定       6チャンク [77, 199, 26, 199, 21, 75]  合計597字
句読点追加 6チャンク [77, 149, 56, 199, 1, 75]   合計557字
同+end     5チャンク [77, 150, 55, 200, 75]      合計557字

既定の2番目の末尾: '…発覚から1時間以内に情報システム部へ電話で連絡し、翌営業日までに報告書を'
既定の3番目の全文: '部へ電話で連絡し、翌営業日までに報告書を提出します。'

既定設定では第2条の「紛失時は翌営業日までに報告書を提出」という文が「報告書を」で切れ、残りの26字が独立したチャンクになりました。この分割が検索にどう響くかは、次章でローカルの埋め込みモデルを使って確かめます。

keep_separatorで「。」だけのチャンクが出る問題

句読点を足すと切れ目は文の境界にそろいますが、上の5番目のように長さ1のチャンクが出ます。中身は「。」1文字です。keep_separator の既定値 True は区切り文字を次のチャンクの先頭に付けるため、200字ちょうどの条文の後ろに句点だけが残りました。2番目以降のチャンクが「。紛失や盗難が…」と句点で始まるのも同じ原因です。keep_separator="end" にすると句点が文末に残り、「。」だけのチャンクも消えます。

もう1つの副作用は chunk_overlap が効かなくなる点です。句読点を足した側の合計557字は、原文560字から改行3つを引いた値と一致しており、重複が1字も付いていません。重複は分割単位をまたいで付くため、1文が chunk_overlap の20字より長いと付きません。前後の文脈を持たせたい場合は、重複に頼らず chunk_size を条文1つが収まる長さ(400〜600字程度)に広げる方が確実です。分割の考え方と単位の違いは チャンクとトークンの違いとは?チャンク分割の手法とRAGのサイズ設計を日本語で実測で詳しく扱っています。

社内文書のベクトルストアへの登録と検索結果を確認する手順と実測

InMemoryVectorStoreへの社内文書の登録と検索確認

最初は langchain-core に含まれる InMemoryVectorStore で検索が成立するかを見ます。APIキー無しで試せるよう、埋め込みはOllamaのローカルモデルにしました。Ollamaをインストールしてサーバーを起動し、埋め込み用の bge-m3(約1.2GB・1024次元・多言語対応)と、次章で使う生成用の qwen2.5:1.5b を ollama pull bge-m3・ollama pull qwen2.5:1.5b で取得しておきます。以下のコードはOllamaの既定の接続先を使います。OpenAIを使う場合は1行目の import と emb を OpenAIEmbeddings(model="text-embedding-3-large") に替えるだけです。

from langchain_ollama import OllamaEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore

emb = OllamaEmbeddings(model="bge-m3")
vector_store = InMemoryVectorStore(emb)
ids = vector_store.add_documents(ja_end.split_documents(docs))

retriever = vector_store.as_retriever(search_kwargs={"k": 2})
for d, score in vector_store.similarity_search_with_score("端末を紛失したら報告書はいつまでに出しますか", k=2):
    print(round(score, 3), d.page_content[:20])

as_retriever の戻りは VectorStoreRetriever で、search_type は "similarity"(既定)・"mmr"・"similarity_score_threshold" の3種です。ただし InMemoryVectorStore で閾値方式を選ぶと、関連度スコア関数が未実装のため invoke の時点で NotImplementedError になります。インストール手順にnumpyを入れているのは、InMemoryVectorStore の類似度計算がnumpyを使うのに、langchain-core 1.7.0 の依存にも langchain-openai・langchain-ollama の依存にも入っていないためです。未導入だと検索の時点で ImportError: cosine_similarity requires numpy to be installed. になり、埋め込みを使わず配線だけ確かめる DeterministicFakeEmbedding では登録の時点で NameError: name 'np' is not defined になります。

日本語の社内文書でチャンク分割の違いが検索順位に出た実測と比較

前章の「既定」と「句読点追加+end」の2通りでベクトルストアを作り、同じ3つの質問で上位2件を取りました。数値はコサイン類似度です。

質問 既定の1位 句読点+endの1位
持ち出しの承認者 第2条 0.652 第2条 0.682
紛失時の報告書の期限 第2条(文が途中で切れた側) 0.685 紛失の文 0.733
生成AIへの個人情報入力 第3条 0.666 第3条 0.667

差が出たのは2問目です。既定の分割では、期限の文が「報告書を」で途切れた第2条のチャンクが1位に来て、「翌営業日までに報告書を提出します」を含む26字の断片は2位(0.670)でした。k=1 で運用していれば、「翌営業日までに報告書を」で文が終わっていない断片だけがLLMに渡り、何をするのかを最後まで読めません。句読点で切った側は紛失時の手順の文が1位になり、3問目でも2位との差が0.666対0.594から0.667対0.379へ開きました。埋め込みモデルの選び方は 埋め込みモデルとは?仕組みと日本語モデルの選び方・RAG実装での判断基準を参照してください。

社内文書の回答生成の方式|2-Step RAGとエージェント方式

LangChainの公式ドキュメントの Retrievalのページは、RAGを3つに分けています。検索を必ず先に実行する2-Step RAG、検索するかどうかと検索語をエージェントが決めるAgentic RAG、質問の書き換えや回答の検証を挟むHybrid RAGです。公式の比較表では、2-Stepは制御しやすく応答が速い代わりに柔軟性が低く、用途の例はFAQや文書ボットとされています。

dynamic_promptで作る社内文書向けの2-Step RAG

0.x系で create_retrieval_chain が担っていた「検索してから1回だけ生成する」構成は、v1では create_agent にツールを渡さず、@dynamic_prompt ミドルウェアで検索結果をシステムプロンプトへ差し込む形で書けます。langchain-classic は要りません。

from langchain.agents import create_agent
from langchain.agents.middleware import dynamic_prompt, ModelRequest
from langchain_ollama import ChatOllama

@dynamic_prompt
def prompt_with_context(request: ModelRequest) -> str:
    question = request.state["messages"][-1].text
    hits = vector_store.similarity_search(question, k=2)
    context = "\n\n".join(f"[{d.metadata['source']}] {d.page_content}" for d in hits)
    return f"次の資料だけを根拠に日本語で答えてください。\n\n{context}"

llm = ChatOllama(model="qwen2.5:1.5b", temperature=0)
agent = create_agent(llm, tools=[], middleware=[prompt_with_context])
result = agent.invoke({"messages": [{"role": "user", "content": "端末を紛失したら報告書はいつまでに出しますか"}]})
print(result["messages"][-1].text)
端末を紛失したら、情報システム部へ電話で連絡し、翌営業日までに報告書を提出します。

生成モデルは qwen2.5:1.5b(約1GB)です。メッセージ列は質問と回答の2件だけで、モデル呼び出しは1回でした。システムプロンプトに「資料に無いことは『資料にありません』と答える」と加えて規程に無い社員食堂の営業時間を尋ねると、「資料に記載されていません。」と返り、推測で営業時間を作ることはありませんでした。出典を表示したい場合は、hits の metadata["source"] を回答と並べて返します。

create_agentに社内文書の検索ツールを渡すエージェント方式

検索を関数(ツール)としてモデルに渡すと、検索するかどうか、何を検索語にするかをモデルが判断します。質問を分けて2回検索したり、空振りしたら語を変えて引き直したりできるのが利点です。

from langchain.tools import tool
from langchain.agents.middleware import ToolCallLimitMiddleware

@tool
def search_kitei(query: str) -> str:
    """社内規程を検索して該当する条文を返す。"""
    hits = vector_store.similarity_search(query, k=2)
    return "\n\n".join(f"[{d.metadata['source']}] {d.page_content}" for d in hits)

agent = create_agent(
    llm,
    tools=[search_kitei],
    system_prompt="社内規程の質問には必ずsearch_kiteiで検索し、検索結果だけを根拠に日本語で答える。",
    middleware=[ToolCallLimitMiddleware(tool_name="search_kitei", run_limit=3, exit_behavior="end")],
)

同じ qwen2.5:1.5b でこのエージェントに2問目を2回投げたところ、2回とも検索ツールを1度も呼ばず、「報告書は1週間以内に提出する必要があります」という趣旨の回答を返しました(言い回しは回ごとに少し変わります)。規程の正解は翌営業日で、根拠の無い誤答です。システムプロンプトで「必ず検索」と指示しても、小型モデルはツールを呼ばないことがあります。エージェント方式を採るなら、ツール呼び出しが安定するモデルで、正解の分かっている質問を流して検索が実行されているかを先に確かめてください。

社内文書の根拠を取得するための検索の強制とツール呼び出し回数の上限

検索ツールが1つで、過去の会話履歴を引き継がない単発実行に限り、最初のモデル呼び出しでツールの使用を強制するには、@wrap_model_call ミドルウェアで tool_choice を上書きします。

from langchain.agents.middleware import wrap_model_call
from langchain_core.messages import ToolMessage

@wrap_model_call
def force_first_search(request, handler):
    searched = any(isinstance(m, ToolMessage) for m in request.state["messages"])
    if not searched:
        request = request.override(tool_choice="search_kitei")
    return handler(request)

agent = create_agent(
    llm,
    tools=[search_kitei],
    system_prompt="社内規程の質問には検索結果だけを根拠に日本語で答える。",
    middleware=[
        force_first_search,
        ToolCallLimitMiddleware(tool_name="search_kitei", run_limit=3, exit_behavior="end"),
    ],
)

呼び出しを記録するテスト用モデルで確かめると、tool_choice は1回目が "search_kitei"、検索後の2回目は None で渡りました。ChatOpenAI の bind_tools は tool_choice を受け付けます。一方 ChatOllama は ソースのdocstringに「Ollamaが対応していないため現在は無視される」と明記されており、上の qwen2.5:1.5b に付けても結果は変わりませんでした。今回のChatOllamaとqwen2.5:1.5bの構成で検索を必須にするなら、2-Step RAGを選ぶべきです。

逆に検索が止まらない場合に備えるのが ToolCallLimitMiddleware です。run_limit=2 にして検索を6回要求し続けるテスト用モデルで流すと、3回目以降のツール結果は「Tool call limit exceeded. Do not call ‘search_kitei’ again.」に置き換わりました。exit_behavior が既定の "continue" だとモデル呼び出しは続き、"end" ならその時点で打ち切り、"error" なら ToolCallLimitExceededError を送出します。止まる条件の設計は エージェントループとは?止まる条件とSDK別の反復上限を実測で解説、検索の要否をモデルに判断させる構成全般は Agentic RAGとは?従来RAGとの違い・設計パターンとLangGraph実装の落とし穴で扱っています。

create_retrieval_chainの継続利用と入出力仕様

0.x系のコードを書き換えずに動かす場合は、langchain-classic 1.0.8 の create_retrieval_chain がそのまま使えます。invoke({"input": ...}) の戻り値は answer・context・input の3キーで、chat_history など入力に足したキーはそのまま戻り値にも残ります。検索結果は context キーに入るため、プロンプトに記述する変数は {context} と {input} です。プロンプトだけを {docs} に変えると ValueError: Prompt must accept context as an input variable. になり、create_stuff_documents_chain に document_variable_name="docs" を指定しても、create_retrieval_chain と組み合わせると実行時に KeyError: 'docs' で止まります。

観点 2-Step(dynamic_prompt) エージェント方式 create_retrieval_chain
パッケージ langchain langchain langchain-classic
検索回数 必ず1回 モデルが判断(0回もある) 必ず1回
LLM呼び出し 1回 検索の実行ラウンド数などで変動 1回(掲載構成)
小型ローカルモデル 向く 検索を飛ばす例あり 向く
向く用途 規程Q&A・FAQ 調査・多段の質問 既存コードの延命

要件が固まっていない段階では2-Stepから始め、検索の空振りが問題になってからエージェント方式へ移る順序が手戻りを減らします。両方を並べて切り替える構成は保守対象が倍になります。

ツール引数のストリーミング表示とlangchain-core 1.7.0の変更点

社内向けのチャット画面に組み込むと、回答を1文字ずつ表示したくなります。エージェント方式では、モデルが検索ツールに渡す引数(検索語)も少しずつ届くので、「『紛失 報告書』で検索中」のような途中経過も出せる。この途中の引数の届き方が、2026年10月22日公開の langchain-core 1.7.0のリリースノートで変更されました。langchain 1.4.7 の依存は langchain-core>=1.6.13 なので、新しく環境を作れば1.7系が入ります。ツール呼び出しそのものの仕組みは LangGraphのTool Calling実装|ToolNode・create_agent・エラー処理とループ上限で解説しています。

stream_events v3の生イベントでtool-call-deltaへ変わった点

変更の対象は、チャットモデルの stream_events(version="v3") が返す ChatModelStream です。v3 はベータ扱いで、呼び出すと LangChainBetaWarning が出ます。生のイベントを反復すると、message-start、content-block-start、content-block-delta、content-block-finish、message-finish の順に辞書が届く仕組みです。ツールの引数は content-block-delta の delta に入ります。

1.6系までは delta の種類が "block-delta" で、fields.args には「ここまでに届いた引数の全体」(スナップショット)が入っていました。1.7.0 からは種類が "tool-call-delta" になり、args には前回からの追加分(スライス)だけが入ります。変更のプルリクエスト(#41257)によると、呼び出しの id・name・プロバイダ側の index は content-block-start に1度だけ載り、後から届いた id や name は content-block-finish に載ります。検索語 {"query": "紛失 報告書 期限"} が3回に分かれて届く場合、delta は次の形です。

# langchain-core 1.6系まで(block-delta:ここまでの全体)
{"type": "block-delta", "fields": {"type": "tool_call_chunk", "args": "{\"query\": "}}
{"type": "block-delta", "fields": {"type": "tool_call_chunk", "args": "{\"query\": \"紛失 報告書"}}
{"type": "block-delta", "fields": {"type": "tool_call_chunk", "args": "{\"query\": \"紛失 報告書 期限\"}"}}

# langchain-core 1.7.0以降(tool-call-delta:追加分だけ)
{"type": "tool-call-delta", "args": "{\"query\": "}
{"type": "tool-call-delta", "args": "\"紛失 報告書"}
{"type": "tool-call-delta", "args": " 期限\"}"}

この形は、1.7.0 のwheelに含まれる chat_model_stream.py の処理(block-delta は保持中の引数を置き換え、tool-call-delta は連結する)から組み立てたもので、実際のモデルの出力をそのまま写したものではありません。分割の位置はモデルとプロバイダで変わります。1.7.0 の ChatModelStream は両方の形を受け付け、古い block-delta が来た場合も保持中の引数との差分だけを .tool_calls へ流します。

影響を受けるコードと受けないコード、langchain-core 1.7未満への固定

リリースノートは「.tool_calls.get() と .output は不変」と明記しています。生イベントを読まず、用意された取り出し口だけを使うコードは書き換え不要です。

llm_with_tools = llm.bind_tools([search_kitei])
stream = llm_with_tools.stream_events(
    [{"role": "user", "content": "端末を紛失したら報告書はいつまでに出しますか"}],
    version="v3",
)
for chunk in stream.tool_calls:      # 1.6.13以降は引数の追加分(ToolCallChunk)
    print(chunk["name"], chunk["args"])
print(stream.tool_calls.get())       # 完成したツール呼び出しの一覧(1.7.0でも不変)
print(stream.output.tool_calls)      # 組み立て済みのAIMessage(1.7.0でも不変)
  • 書き換えが要る:生イベントを反復し、block-delta の args を「ここまでの全体」として画面に出しているコード。1.7.0 では block-delta が来なくなり、表示が止まります。
  • 変わらない:.tool_calls.get() と .output。完成した引数を受け取るので、版による差はありません。
  • 1.6.13から既に差分:.tool_calls の反復。リリースノートは、1.6.13 以降は各呼び出しの新しい引数片を返すと書いています。
  • 対象外:従来の stream()・astream() が返す AIMessageChunk の tool_call_chunks。リリースノートとプルリクエストが扱うのは v3 のイベントです。

生イベントを直接読むなら、content-block-start で name を控え、tool-call-delta の args を呼び出しの index ごとに連結します。

names, args_by_index = {}, {}
for ev in llm_with_tools.stream_events(messages, version="v3"):
    if ev["event"] == "content-block-start" and ev["content"]["type"] == "tool_call_chunk":
        names[ev["index"]] = ev["content"].get("name")
    elif ev["event"] == "content-block-delta" and ev["delta"]["type"] == "tool-call-delta":
        i = ev["index"]
        args_by_index[i] = args_by_index.get(i, "") + ev["delta"]["args"]
        print(names.get(i), args_by_index[i])   # ここまでの検索語を表示

すぐに直せない場合の回避策として、リリースノートは langchain-core<1.7 への固定を挙げています。langchain 1.4.7 は langchain-core>=1.6.13 を要求するため、指定は pip install "langchain-core>=1.6.13,<1.7" の範囲になります。ただし固定は一時しのぎです。v3 はベータで形がまた変わり得るため、画面表示に要るのが完成した検索語だけなら .tool_calls.get() に寄せる方が、次の変更に強い書き方といえます。ストリーミングとコールバックをAPIキー無しで試す方法は FakeLLMとは|LangChainのテスト用ダミーLLMの使い方とStreaming・Callbacksの落とし穴が参考になります。

PGVectorStoreへの移行|filterの型の違いで権限フィルタが外れる

PGVectorの非推奨化とPGVectorStoreへの移行

InMemoryVectorStore はプロセスが終われば消えるので、本番では外部のストアへ移します。PostgreSQLを使うなら langchain-postgres(0.0.19・2026年10月6日公開)ですが、READMEには「v0.0.14以降 PGVector は非推奨で、PGVectorStore へ移行する」と書かれています。新規に使うストアは PGVectorStore です。PostgreSQL 18.6 にpgvector 0.8.7を入れたローカル環境で、次のコードが動きました。

from langchain_postgres import PGEngine, PGVectorStore, Column

engine = PGEngine.from_connection_string(url="postgresql+psycopg://postgres@localhost:5432/ragdb")
engine.init_vectorstore_table(
    table_name="kitei",
    vector_size=1024,                      # bge-m3 の次元数
    metadata_columns=[Column("dept", "TEXT"), Column("source", "TEXT")],
)
store = PGVectorStore.create_sync(
    engine=engine, table_name="kitei",
    embedding_service=emb, metadata_columns=["dept", "source"],
)
# ここからは権限フィルタ検証用の別データを使う
from langchain_core.documents import Document

docs = [
    Document(page_content="端末の社外持ち出しは情報システム部への事前申請が必要です。", metadata={"source": "sec.md", "dept": "all"}),
    Document(page_content="有給休暇は取得予定日の3営業日前までに申請します。", metadata={"source": "kinmu.md", "dept": "all"}),
    Document(page_content="給与テーブルは人事部のみ閲覧可。", metadata={"source": "hr.md", "dept": "hr"}),
]
store.add_documents(docs)

metadata_columns に挙げたキーは独立した列になり、それ以外は langchain_metadata というJSON列に入ります。権限で絞りたい項目(部門・ロールなど)は列として定義しておくと、SQL側でもインデックスを張れます。InMemoryVectorStore では使えなかった similarity_score_threshold も、PGVectorStore ではエラーにならずに動きました。

PGVectorStoreで関数型filterが無視される条件

メモリで検証してから本番のストアへ差し替えるとき、書き換えが要るのが filter です。InMemoryVectorStore の filter は「Document を受けて真偽を返す関数」で、辞書を渡すと TypeError: 'dict' object is not callable になります。PGVectorStore は逆に辞書を受け取ります。問題は、関数を渡したときの PGVectorStore の振る舞いです。

# 人事部の文書(dept="hr")を含む3件を登録済み
store.similarity_search("閲覧", k=3, filter=lambda d: d.metadata["dept"] == "all")
# → 3件(hr の文書も返る。エラーにならない)

store.similarity_search("閲覧", k=3, filter={"dept": "all"})
# → 2件(sec.md, kinmu.md)

store.similarity_search("閲覧", k=3, filter={"dept": {"$in": ["hr"]}})
# → 1件(hr.md)

関数を渡すと絞り込みが丸ごと外れ、人事部限定の文書まで返りました。0.0.19 の async_vectorstore.py は if filter and isinstance(filter, dict): のときだけ条件句を組み立てるため、辞書以外の値は無言で捨てられます。メモリ上の検証では権限フィルタが効いていたのに、本番へ差し替えた瞬間に効かなくなる、という事故がテストをすり抜けて起こり得ます。移行時は filter を辞書へ書き換え、権限外の文書が返らないことをテストに入れてください。存在しない列名({"nosuch": "a"})を指定した場合は0件が返り、こちらもエラーにはなりません。

旧版コードやストア移行時のModuleNotFoundErrorの原因と対処

0.x系の記事を写したときや、ベクトルストアを差し替えたときに出るエラーを、1.4.7 の環境で再現した結果です。

  • No module named 'langchain.chains':v1で本体から削除された。langchain-classic を入れ、import 元を langchain_classic.chains に変える。
  • No module named 'langchain_classic':互換パッケージが未導入。pip install langchain-classic で入る。
  • No module named 'langchain_community':langchain 1.x の依存に無い。pip install langchain-community で追加する。
  • No module named 'pgvector':community版 PGVector の依存に無い。langchain-postgres へ移行する。
  • No module named 'psycopg':同じくドライバが未導入。langchain-postgres へ移行すれば入る。
  • ImportError: cosine_similarity requires numpy:InMemoryVectorStore がnumpyを使う。pip install numpy で解消する。
  • NameError: name 'np' is not defined:DeterministicFakeEmbedding がnumpyを使う。こちらも pip install numpy で解消する。

langchain-community(最新0.4.2・2026年5月22日公開)の PGVector は内部で pgvector.sqlalchemy を import しますが、依存に pgvector も psycopg も入っていません。langchain-postgres は両方を依存に持つので、pip install langchain-postgres でそろいます。

公式チュートリアルの現在地|RAGページはDeep Agentsへ移動

「LangChain RAG チュートリアル」で検索すると上位に出る docs.langchain.com/oss/python/langchain/rag は、308(恒久的リダイレクト)で /oss/python/deepagents/rag へ転送されます。同じく /langchain/retrieval も転送先は /deepagents/retrieval です。転送先は create_deep_agent を前提に、スキルに沿った検索、ルーブリックによる根拠の採点(deepagents>=0.6.5・ベータ)、ToDoリストに沿った順次検索、チャンクをファイルへ退避してサブエージェントに読ませる方式、の4パターンを紹介しています。チュートリアルの題材は最後の方式です。

deepagents の最新は 0.7.32(2026年10月23日公開)で、Python 3.11 以上が必要です。依存に langchain>=1.4.7 に加えて langchain-anthropic と langchain-google-genai が入るため、本体だけの環境より依存が増えます。素の埋め込みとベクトルストアの手順は knowledge-baseのページに残っています。単一の検索と回答で足りる社内Q&Aなら、その手順に本記事の2-Step RAGを接ぐのが最短です。LangChainとLangGraphの関係は LangChainとLangGraphの違いと使い分け|v1.0以降の関係をコードで確認で確認できます。

社内文書RAGで精度が出ないときの検索結果と回答生成の切り分け

社内文書RAGの検索結果の単独確認と完全一致検索のBM25併用の判断

回答が的外れなときは、LLMを挟まずに検索結果だけを出力します。

for i, d in enumerate(retriever.invoke("端末を紛失したら報告書はいつまでに出しますか"), 1):
    print(i, d.metadata["source"], d.page_content[:40])

正解を含むチャンクが上位に無ければ原因は検索側で、プロンプトを直しても改善しません。前述の分割設定を見直し、型番や条項番号のように完全一致で引きたい語が多いなら全文検索を併用します。BM25の追加は LangChain BM25Retrieverの使い方|インストール・日本語対応・ハイブリッド検索、複数の検索結果の統合は RRF(Reciprocal Rank Fusion)とは?計算式とk=60の意味・製品別設定・RAG-Fusionでの使い方が該当します。

社内文書RAGの回答の根拠逸脱を切り分ける検索・生成それぞれの評価

検索は当たっているのに回答がずれるなら生成側です。チャンクが文の途中で切れている、渡すチャンクが多すぎる、プロンプトが資料外の推測を許している、のどれかが多い原因です。エージェント方式なら、メッセージ列に ToolMessage があるか(検索が呼ばれたか)を先に見ます。正解の分かっている質問を20〜30件用意しておけば、変更のたびに効果を比較することが可能です。検索と生成を分けて測る枠組みは RAGCheckerとは|RAGを検索と生成に分けて診断するAmazonの評価フレームワークにあります。

社内RAGで実装前に決める文書のアクセス権限と費用・外部委託の分担

人事・法務の社内文書を検索対象に含める場合の権限設計とアクセス制御

社内文書RAGで最初に問題になるのは精度より権限です。共有ドライブを丸ごと1つのベクトルストアに入れると、人事評価や給与のファイルまで誰でも検索で引けます。対策は、メタデータに部門やロールを持たせてフィルタで絞るか、権限区分ごとにコレクション(テーブル)を分けるかです。フィルタ方式はインデックスが1つで済む反面、前章のように実装の取り違えがそのまま漏洩になります。人事・法務の文書を含むなら、コレクション分離に加え、認証済みユーザーの権限に基づく接続先の選択と、PostgreSQLのロール・テーブル権限を設計してください。分離しただけでは他のコレクションへのアクセスを防げません。

社内文書RAGの入力トークンと応答時間に基づく費用と外部委託の判断

費用は、埋め込み(登録時と検索時)、LLMの入出力トークン、ベクトルストアの稼働費で決まります。単価は各社が改定するので、見積もりでは提供元の料金ページを確認してください。継続して効くのは質問1件あたりのLLM入力トークンで、渡すチャンク数 k と chunk_size の積は取得文書の最大文字数の目安です。入力トークン数にはシステムプロンプト、質問、会話履歴も含まれるため、使用モデルのトークナイザーで測ります。Ollamaのローカルモデルで組めばAPI費用はかかりませんが、CPUのみ(Intel Core i9-9880H)で qwen2.5:1.5b に報告書の期限を答えさせた2-Step RAGは、1回の計測で7.9秒でした。CPUや入出力の長さで変わるので、本番の応答時間は自社の環境で測ってください。外部に頼む場合は、権限設計と評価用の質問セットを自社で持ち、実装と運用の型作りを任せる分担にすると、納品物の良否を自社で判断できます。設計から運用までの相談は RAG構築支援で承っています。

よくある質問

pip install langchain だけでRAGは動きますか?

動きません。langchain 1.4.7 の必須依存は langchain-core・langgraph・pydantic だけで、埋め込みモデルとLLMのクライアントも、分割用の langchain-text-splitters も入りません。OpenAIなら langchain-openai、ローカルモデルなら langchain-ollama を追加します。ローカルの場合も OllamaEmbeddings と ChatOllama に替えるだけで本記事のコードは動きますが、ChatOllama は tool_choice を無視するため、検索を必須にするなら2-Step RAGを選びます。

LangChainのRAGエージェントとRAGチェーンはどちらを選びますか?

根拠が必須の規程Q&AやFAQなら、検索を必ず1回実行する2-Step RAG(dynamic_prompt)を選びます。質問が曖昧で検索語の言い換えや複数回の検索が要る用途なら、create_agent に検索ツールを渡すエージェント方式です。後者を選ぶ場合は ToolCallLimitMiddleware で検索回数に上限を付けます。

langchain-core 1.7.0に上げるとストリーミング表示のコードは壊れますか?

stream_events(version="v3") の生イベントを反復し、block-delta に入る「ここまでの引数全体」を表示しているコードだけが影響を受けます。1.7.0 からツール引数は tool-call-delta の追加分として届くため、呼び出しごとに連結して読みます。.tool_calls.get() と .output は変わらず、従来の stream() の tool_call_chunks も対象外です。すぐに直せない場合は langchain-core を1.7未満(1.6.13以上)に固定します。

LangChainを使わずにRAGは作れますか?

作れます。埋め込みAPIを呼び、ベクトルDBで検索し、取得した本文をプロンプトに連結してLLMへ渡せば同じ処理です。接続先を1つに固定でき、エージェント的な処理も要らないなら、自前実装の方が依存は減ります。LlamaIndexとの比較は LlamaIndexとは?RAG構築の仕組み・LangChainとの違いと採用判断を実装者向けに解説を参照してください。

LangGraphやDeep Agentsへ移行する必要はありますか?

単一の検索と回答で足りるなら不要です。create_agent は内部でLangGraphを使うため、グラフを書かなくてもツール実行ループは動きます。LangGraphを直接扱うのは条件分岐や人の承認待ちを自分で設計する段階、deepagents はサブエージェントへの分担が要る規模になってからです。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

ほか 6 件の記事からもリンクされています。

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.06 テックブログ ドコモ不正アクセス:最大29.5万件の流出項目と見舞金、My docomo停止の経過と偽連絡の見分け方
  2. 2026.11.03 テックブログ ネットマイル不正アクセスで最大549万人:対象確認と退会会員データを消す実装
  3. 2026.11.06 テックブログ あいおいニッセイ同和の不正アクセス:44.4万件の漏えい項目と窓口、止まった手続きの代わり
  4. 2026.11.07 コラム サイバーセキュリティ緊急対策パッケージとは?4本柱の施策と期限、企業と委託先が今やること
  5. 2026.11.06 テックブログ アイリスオーヤマ情報漏洩:アイリスプラザ770万人分の対象項目と本物の連絡の見分け方

RELATED POSTS 関連記事

目次