LangChain スマートカスタマーサービスボット

この記事では、これまで学んだ知識を統合して、完全なスマートカスタマーサービスボットを構築します。ナレッジベースの検索、注文の処理、必要に応じてオペレーターへの転送ができます。


要件分析

機能実装方法
ナレッジベース Q&ARAG 検索 + モデル回答
注文照会@tool ツール関数
会話メモリSqliteSaver Checkpointer
センシティブな内容のフィルタリング@before_model Middleware
オペレーター転送HITL interrupt() / Command(resume=...)

環境構築

実際にコードを書く前に、実行環境を準備しましょう。以下の手順に従えば、数分で準備できます。

ステップ1:Python バージョンの確認

Python 3.10 以上を使用することをお勧めします。ターミナルを開いて、次のように入力します:

python --version

バージョンが 3.10 より低い場合は、先に Python をアップグレードしてから、後続の手順に進むことをお勧めします。

ステップ2:仮想環境の作成とアクティブ化(推奨)

システムの Python 環境を汚さないように、このプロジェクト専用の仮想環境を別途作成することをお勧めします:

# 创建虚拟环境
python -m venv venv

# 激活虚拟环境(Windows)
venv\Scripts\activate

# 激活虚拟环境(macOS / Linux)
source venv/bin/activate

アクティブ化に成功すると、ターミナルのプロンプトの先頭に(venv)という文字が表示されます。

ステップ3:依存パッケージのインストール

このプロジェクトでは合計6つのサードパーティライブラリを使用し、それぞれ異なる機能を担当します。問題が発生したときに、どのライブラリが正しくインストールされていないかを特定しやすいように、以下の順序で1つずつインストールすることをお勧めします。

インストールコマンド用途
pip install langchainLangChain コアライブラリ。create_agent、@tool などの基本機能を提供
pip install langchain-deepseekinit_chat_model が "deepseek:" プレフィックスを認識し、DeepSeek モデルを呼び出せるようにする
pip install langchain-openaiOpenAIEmbeddings を提供し、ナレッジベースのテキストをベクトルに変換するために使用します
pip install langchain-chroma chromadbローカルベクトルデータベース。ナレッジベースのベクトルの保存と検索に使用
pip install langgraph-checkpoint-sqliteSqliteSaver は、会話メモリを SQLite ファイルに永続化します
pip install python-dotenv.env ファイルから API キーなどの環境変数を読み取る

1つのコマンドでまとめてインストールすることもできます:

pip install langchain langchain-deepseek langchain-openai langchain-chroma chromadb langgraph-checkpoint-sqlite python-dotenv

ステップ4:APIキーの設定

プロジェクトのルートディレクトリに、名前が.envというファイルを作成します(ファイル名はドットで始まり、拡張子はありません)。次の内容を入力してください:

# DeepSeek 官网申请:https://platform.deepseek.com
DEEPSEEK_API_KEY=sk-你的deepseek密钥

# OpenAI 官网申请:https://platform.openai.com
# 这里只用来调用 embedding 接口,不涉及 Chat 模型
OPENAI_API_KEY=sk-你的openai密钥

OpenAI のキーがない場合は、Alibaba Cloud Bailian(DashScope)を使用できます。.envファイルのコードは次のとおりです:

# DeepSeek 官网申请:https://platform.deepseek.com
DEEPSEEK_API_KEY=sk-你的deepseek密钥

# 阿里云百炼控制台申请:https://bailian.console.aliyun.com
# 这里用来调用通义千问的 Embedding 服务,给知识库文本做向量化
DASHSCOPE_API_KEY=sk-你的百炼密钥

.env ファイルには秘密鍵が保存されています。Git リポジトリにコミットしたり、他人と共有したりしないでください。プロジェクト内に.gitignoreファイルを作成し、1行追加してください:.env誤ってコミットするのを防ぐためです。

ステップ5:インストールの検証

新しいcheck_install.pyファイルを作成し、次のスクリプトを実行して、依存関係とキーが正しく設定されているか確認します:

サンプル

# ファイルパス: check_install.py
import os
from dotenv import load_dotenv

load_dotenv()

# 依存パッケージが正常にインポートできるか確認
import langchain
import langchain_deepseek
import langchain_openai
import langchain_chroma
import chromadb
import langgraph

print(f"langchain バージョン: {langchain.__version__}")

# キーが設定されているか確認
assert os.getenv("DEEPSEEK_API_KEY"), "DEEPSEEK_API_KEY が検出されませんでした。.env ファイルを確認してください"
# assert os.getenv("OPENAI_API_KEY"), "OPENAI_API_KEY が検出されませんでした。.env ファイルを確認してください"
assert os.getenv("DASHSCOPE_API_KEY"), "DASHSCOPE_API_KEY が検出されませんでした。.env ファイルを確認してください"

print("環境設定が成功しました~カスタマーサービスボットの作成を始められます!")

実行:

python check_install.py

キーが正しく設定されている場合、出力は次のようになります:

langchain 版本: 1.3.0
环境配置成功~可以开始写客服机器人了!

エラーが発生した場合は、通常、パッケージがインストールされていないか、.env 内のキーが正しく入力されていないことが原因です。エラーメッセージに応じて、対応する手順に戻って確認してください。


コード全文

環境構築が完了したら(依存パッケージのインストール、 .env の設定は前の節「環境構築」を参照)、完全なカスタマーサービスボットのコードを作成できます:

サンプル

# ファイルパス: customer_service_bot.py
# 依存パッケージのインストール、.env の設定は前の節「環境構築」を参照
from dotenv import load_dotenv
load_dotenv()

import os
import sqlite3
from typing import Annotated
from langchain.tools import tool
from langchain.agents import create_agent
from langchain.agents.middleware import before_model, after_model
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage, AIMessage
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.types import interrupt, Command


# ========== 1. ナレッジベースの準備 ==========

knowledge_base = [
    "サンプルは2013年に設立され、中国国内でトップクラスの無料プログラミング学習プラットフォームです。",
    "このプラットフォームは、Python、Java、HTML、CSS、JavaScript などを含む 300 以上のチュートリアルを提供しています。",
    "Python3 基礎チュートリアルは全30章で、累計学習者数は500万人を超えています。コースは完全に無料です。",
    "VIP 会員費は月額 ¥99、年額 ¥799 で、ビデオコースと1対1の質疑応答サービスが含まれます。",
    "返金ポリシー:購入後7日以内かつ3レッスン以内であれば全額返金できます。",
    "プラットフォームはオンラインプログラミング環境をサポートしており、ソフトウェアをインストールせずにコードを記述・実行できます。",
    "カスタマーサービスの受付時間:月曜日から金曜日 9:00〜18:00、土日 10:00〜16:00。",
]

# アリババクラウド百炼(DashScope)の通義千問(Qwen)Embedding サービスを使用
# 百炼の Embedding API は OpenAI API 仕様と互換性があるため、langchain-openai の
# OpenAIEmbeddings を直接使い、base_url を百炼の互換エンドポイントに指定するだけでよいです。
# langchain-community / dashscope をインストールする必要はありません(このパッケージはメンテナンスを停止しています)。
# text-embedding-v4 は現在推奨されている汎用ベクトルモデルで、デフォルトで 1024 次元のベクトルを出力します。
#
# 欠かせない重要なパラメータが2つあります:
# - check_embedding_ctx_length=False:OpenAIEmbeddings はデフォルトで tiktoken を使用します
テキストを事前に token ID 配列にエンコードしてから送信します(OpenAI 公式 API はこの形式を認識します)。
ただし、百炼の互換 API は元の文字列のみを受け付けるため、このオプションをオフにしないと、
"contents is neither str nor list of str" というエラーが発生します。
# - chunk_size=10:百炼 Embedding API は1回のリクエストで最大10件のテキストしか受け付けません
# OpenAIEmbeddings はデフォルトで一度に1000件をまとめて送信するため、ナレッジベースが少し大きいと上限を超えてエラーになります。
embeddings = OpenAIEmbeddings(
    model="text-embedding-v4",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    check_embedding_ctx_length=False,
    chunk_size=10,
)
chunks = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=30
                                         ).create_documents(knowledge_base)
vector_store = Chroma.from_documents(chunks, embeddings)
retriever = vector_store.as_retriever(search_kwargs={"k": 3})


# ========== 2. ツールの定義 ==========

@tool
def search_kb(query: str) -> str:
    """サンプルのナレッジベースを検索し、プラットフォーム、コース、ポリシーなどの公式情報を取得します。

    Args:
query: 検索する質問またはキーワード
    """

    docs = retriever.invoke(query)
    if not docs:
        return "関連情報が見つかりませんでした。オペレーターへの転送をお勧めします。"
    return "\n".join(f"- {doc.page_content}" for doc in docs)


# 模擬注文データベース
orders_db = {
    "ORD-2024-001": {"user": "小明", "item": "VIP 年間会員",
                      "amount": 799, "status": "完了", "date": "2024-01-15"},
    "ORD-2024-002": {"user": "小明", "item": "Python 実践コース",
                      "amount": 199, "status": "配送中", "date": "2024-03-20"},
}


@tool
def query_order(order_id: str) -> str:
    """注文番号に基づいて注文のステータスと詳細を照会します。

    Args:
order_id: 注文番号(例: ORD-2024-001)
    """

    order = orders_db.get(order_id.upper())
    if not order:
        return f"注文 {order_id} が見つかりません。注文番号が正しいか確認してください。"
    return (f"注文 {order_id}:{order['item']} | "
            f"金額 ¥{order['amount']} | "
            f"ステータス {order['status']} | "
            f"日付 {order['date']}")


@tool
def transfer_to_human(reason: str) -> str:
    """ユーザーをオペレーターに転送します。

    Args:
reason: 転送理由
    """

    approval = interrupt({
        "action": "transfer_to_human",
        "reason": reason,
        "message": f"ユーザーがオペレーターへの転送を要求しています。理由:{reason}。転送しますか?"
    })
    if approval.get("confirmed"):
        return (f"オペレーターに転送しました。予想待ち時間は {approval.get('wait_time', 3)} 分です。"
                fチケット番号:TK-{approval.get('ticket_id', 'N/A')})
    return お取り次ぎはキャンセルされました。引き続き対応させていただきます。


# ========== 3. Middleware の定義 ==========

@before_model
def content_guard(state, runtime):
    """ユーザー入力の不適切な内容をフィルタリングする"""
    last_msg = state["messages"][-1] if state.get("messages") else None
    if not last_msg:
        return None
    content = str(getattr(last_msg, 'content', ''))
    blocked = [黄X, X博, 違法]
    for word in blocked:
        if word in content:
            return {
                "jump_to": "end",
                "messages": [HumanMessage(content=申し訳ありませんが、このリクエストは処理できません。)]
            }
    return None


@after_model
def auto_signature(state, runtime):
    """カスタマーサポートの署名を自動的に追加する"""
    msgs = state.get("messages", [])
    if not msgs:
        return None
    last = msgs[-1]
    if last.type == "ai" and last.content and not (
        hasattr(last, 'tool_calls') and last.tool_calls
    ):
        # ポイント:last.id を再利用して、add_messages reducer がそのメッセージをその場で置き換えるようにする。
        # 履歴に新しいメッセージとして追加するのではなく(そうしないと履歴がどんどん肥大化し、
        # 毎ターン「署名なし版」と「署名付き版」が1件ずつ増えてしまう)
        return {"messages": [AIMessage(
            id=last.id,
            content=last.content
            + "\n\n---\nサンプル カスタマーセンター | 営業時間 9:00-18:00"
        )]}
    return None


# ========== 4. Agent の作成 ==========

# SqliteSaver.from_conn_string() はコンテキストマネージャーを返すため、「使い終わったら閉じる」だけの
# 使い捨てスクリプトにのみ適している。カスタマーサービスボットは複数の chat() 呼び出し間で同じデータベース接続を維持する必要があるため、
# ここでは自分で接続を確立してから SqliteSaver コンストラクタに渡している。
# check_same_thread=False は、Web フレームワークが通常、同じ接続をスレッド間で呼び出すためである。
conn = sqlite3.connect("customer_service.db", check_same_thread=False)
checkpointer = SqliteSaver(conn)

model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)

agent = create_agent(
    model=model,
    tools=[search_kb, query_order, transfer_to_human],
    middleware=[content_guard, auto_signature],
    checkpointer=checkpointer,
    system_prompt="""あなたはサンプル のスマートカスタマーサービス「小菜」です。

## あなたの役割
1. すべてのユーザーを熱意を持って対応し、「您」という敬称で呼ぶ
2. プラットフォーム情報、コース内容、ポリシーなどの問題については、search_kb を使って検索する
3. 注文照会については、query_order ツールを使用する
4. 解決できない問題に遭遇した場合は、transfer_to_human を使用して人工オペレーターに転送する

## 行動規範
- 回答は簡潔に、毎回 2〜3 文で
- 分からない場合はナレッジベースを検索し、見つからない場合は正直に伝える
- 友好的で親しみやすい口調を保つ"""
,
)


# ========== 5. 対話インターフェース ==========

def chat(thread_id: str, message: str) -> str:
    """ユーザーメッセージを処理して返信を返す"""
    config = {"configurable": {"thread_id": thread_id}}

    # Agent を実行
    result = agent.invoke(
        {"messages": [HumanMessage(content=message)]},
        config=config,
    )

    # 転送が必要かどうかを確認する(HITL)
    state = agent.get_state(config)
    if state.tasks and state.tasks[0].interrupts:
        interrupt_info = state.tasks[0].interrupts[0].value
        return f"[承認が必要] {interrupt_info.get('message', '')}"

    return result["messages"][-1].content


def resume_transfer(thread_id: str, confirmed: bool,
                     wait_time: int = 3, ticket_id: str = "0001") -> str:
    """人工オペレーターがバックグラウンドで承認した後、interrupt() によって中断された転送フローを再開します。

transfer_to_human ツール内で待機している approval データに対応します。
    """

    config = {"configurable": {"thread_id": thread_id}}
    result = agent.invoke(
        Command(resume={
            "confirmed": confirmed,
            "wait_time": wait_time,
            "ticket_id": ticket_id,
        }),
        config=config,
    )
    return result["messages"][-1].content


# ========== 6. テスト ==========

if __name__ == "__main__":
    user_id = "user_xiaoming"

    print("=== テスト 1:ナレッジベース検索 ===")
    print(chat(user_id, "Python3 チュートリアルは何章ありますか?"))
    print()

    print("=== テスト 2:注文照会 ===")
    print(chat(user_id, "私の注文 ORD-2024-001 のステータスは何ですか?"))
    print()

    print("=== テスト 3:VIP 相談 ===")
    print(chat(user_id, "VIP 会員はいくらですか?"))
    print()

    print("=== テスト 4:メモリのテスト ===")
    print(chat(user_id, "さっき私はどんな質問をしましたか?"))
    print()

    print("=== テスト 5:人工オペレーターへの転送(HITL) ===")
    print(chat(user_id, "苦情があります。人工オペレーターに転送してください"))       # interrupt をトリガーし、承認を待つ
    print(resume_transfer(user_id, confirmed=True,
                           wait_time=5, ticket_id="8823"))  # バックグラウンドで確認後、再開する

    conn.close()

実行結果:

=== 测试 1:知识库查询 ===
您好!サンプル的 **Python3 基础教程共 30 章**,完全免费,累计学习人次已超 500 万哦!如需了解其他教程,也可以随时问我~

---
サンプル 客服中心 | 工作时间 9:00-18:00

=== 测试 2:订单查询 ===
您好!您查询的订单 **ORD-2024-001** 状态为 **已完成**。订单内容是 **VIP 年费会员**,金额 **¥799**,下单日期为 **2024-01-15**。请问还有什么可以帮您的吗?

---
サンプル 客服中心 | 工作时间 9:00-18:00

=== 测试 3:VIP 咨询 ===
您好!VIP 会员有 **¥99/月** 和 **¥799/年** 两种套餐,包含视频课程和一对一答疑服务哦~请问您需要办理哪种呢?

---
サンプル 客服中心 | 工作时间 9:00-18:00

---
サンプル 客服中心 | 工作时间 9:00-18:00

=== 测试 4:测试记忆 ===
您好!您刚才问了以下三个问题:
1. **Python3 教程有多少章?** —— 共 30 章,完全免费
2. **我的订单 ORD-2024-001 状态是什么?** —— 状态为"已完成"
3. **VIP 会员多少钱?** —— ¥99/月 或 ¥799/年

还有什么需要我帮忙的吗?

---
サンプル 客服中心 | 工作时间 9:00-18:00

---
サンプル 客服中心 | 工作时间 9:00-18:00

=== 测试 5:人工转接(HITL) ===
[需要审批] 用户请求转接人工客服,原因:用户要求投诉,需要转接人工客服处理。是否转接?
您好!已经为您转接人工客服,预计等待约 5 分钟。您的工单号是 **TK-8823**,请稍候,客服会尽快为您处理~

---
サンプル 客服中心 | 工作时间 9:00-18:00

---
サンプル 客服中心 | 工作时间 9:00-18:00

プロジェクトのまとめ

このカスタマーサービスボットは、以下の LangChain 機能を統合しています:

機能プロジェクト内での使用
RAG 検索search_kb ツール + Chroma ベクトルストア
ツール呼び出しquery_order、transfer_to_human
CheckpointerSqliteSaver による対話の永続化で、マルチターンのメモリを実現
Middlewarebefore_model によるコンテンツフィルタリング + after_model による署名追加(メッセージ id を再利用してその場で置き換え)
HITLinterrupt() による実行の一時停止 + Command(resume=...) による承認後の再開で、完全な人工オペレーター転送のクローズドループを実現
その他の拡張