---
title: "LangChainでRAGを構築する手順｜v1.4系の2-Step・エージェント方式とPGVectorStore移行"
url: "https://www.issoh.co.jp/tech/details/2332/"
published: 2024-05-31
updated: 2026-10-25
categories: ["LLM・RAG"]
publisher: "株式会社一創"
---

# 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-postgres` の `PGVector` は非推奨で、後継は `PGVectorStore`。`InMemoryVectorStore` 用に書いた関数型の `filter` は `PGVectorStore` ではエラーにならずに無視され、権限で絞ったはずの文書まで返ります。

## RAGとLangChainの役割分担

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

## v1.4系のパッケージ構成とインストール

### インストールと確認済みバージョン

Python 3.13 の仮想環境に本体一式を入れます。[PyPIのlangchain](https://pypi.org/project/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)
```

`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 名前空間が追加](https://github.com/langchain-ai/langchain/releases/tag/langchain%3D%3D1.4.0)されましたが、RAGの書き方は1.3系から変わっていません。

### langchain.chains の消失とlangchain-classic

[公式の移行ガイド](https://docs.langchain.com/oss/python/migrate/langchain-v1)は、レガシーチェーン（`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を実装例で解説](/tech/details/9663/)にまとめています。

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

### 既定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のサイズ設計を日本語で実測](/tech/details/8016/)で詳しく扱っています。

## ベクトルストアへの登録と検索

### InMemoryVectorStoreへの登録と検索確認

最初は `langchain-core` に含まれる `InMemoryVectorStore` で検索が成立するかを見ます。APIキー無しで試せるよう、埋め込みはOllamaのローカルモデルにしました。Ollamaをインストールしてサーバーを起動し、埋め込み用の [bge-m3](https://ollama.com/library/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実装での判断基準](/tech/details/16044/)を参照してください。

## 回答生成の方式｜2-Step RAGとエージェント方式

LangChainの公式ドキュメントの [Retrievalのページ](https://docs.langchain.com/oss/python/deepagents/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](https://ollama.com/library/qwen2.5)（約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](https://github.com/langchain-ai/langchain/blob/master/libs/partners/ollama/langchain%5Follama/chat%5Fmodels.py)に「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別の反復上限を実測で解説](/tech/details/13602/)、検索の要否をモデルに判断させる構成全般は [Agentic RAGとは？従来RAGとの違い・設計パターンとLangGraph実装の落とし穴](/tech/details/8079/)で扱っています。

### 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から始め、検索の空振りが問題になってからエージェント方式へ移る順序が手戻りを減らします。両方を並べて切り替える構成は保守対象が倍になります。

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

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

`InMemoryVectorStore` はプロセスが終われば消えるので、本番では外部のストアへ移します。PostgreSQLを使うなら `langchain-postgres`（0.0.19・2026年10月6日公開）ですが、[READMEには](https://github.com/langchain-ai/langchain-postgres)「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](https://github.com/langchain-ai/langchain-postgres/blob/main/langchain%5Fpostgres/v2/async%5Fvectorstore.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 元を変更 |
| 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](https://docs.langchain.com/oss/python/deepagents/rag) へ転送されます。同じく `/langchain/retrieval` も `/deepagents/retrieval` へ転送されます。転送先は `create_deep_agent` を前提に、スキルに沿った検索、ルーブリックによる根拠の採点（`deepagents>=0.6.5`・ベータ）、ToDoリストに沿った順次検索、チャンクをファイルへ退避してサブエージェントに読ませる方式、の4パターンを紹介しています。チュートリアルの題材は最後の方式です。

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

## 精度が出ないときの切り分け

### 検索結果の単独確認とBM25併用の判断

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

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

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

### 回答の根拠逸脱と検索・生成の評価

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

## 社内RAGで実装前に決めること

### 権限とアクセス制御

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

### 費用と外部委託の判断

費用は、埋め込み（登録時と検索時）、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構築支援](/service/ai/rag/)で承っています。

## よくある質問

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

動きません。`langchain` 1.4.7 の必須依存は `langchain-core`・`langgraph`・`pydantic` だけで、埋め込みモデルとLLMのクライアントも、分割用の `langchain-text-splitters` も入りません。OpenAIなら `langchain-openai`、ローカルモデルなら `langchain-ollama` を追加します。

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

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

### LangChainのRAGはローカルモデルで動かせますか？

動かせます。`langchain-ollama` の `OllamaEmbeddings` と `ChatOllama` に替えるだけで、本記事のコードはそのまま動きます。`ChatOllama` は `tool_choice` を無視するため、本記事のOllama構成では2-Step RAGを推奨します。

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

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

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

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

## 関連記事

- [LangChain v1.0とは｜create\_agent・ミドルウェア・langchain-classicを実装例で解説](/tech/details/9663/)
- [チャンクとトークンの違いとは？チャンク分割の手法とRAGのサイズ設計を日本語で実測【2026年版】](/tech/details/8016/)
- [LangChain BM25Retrieverの使い方｜インストール・日本語対応・ハイブリッド検索](/tech/details/3751/)
- [Agentic RAGとは？従来RAGとの違い・設計パターンとLangGraph実装の落とし穴](/tech/details/8079/)
- [RAGCheckerとは｜RAGを検索と生成に分けて診断するAmazonの評価フレームワーク](/tech/details/4745/)

---

出典: [LangChainでRAGを構築する手順｜v1.4系の2-Step・エージェント方式とPGVectorStore移行](<https://www.issoh.co.jp/tech/details/2332/>)（株式会社一創）
