Интеллектуальный бот службы поддержки клиентов LangChain

В этой статье объединяются знания, полученные ранее, чтобы создать полноценного интеллектуального бота службы поддержки клиентов. Он умеет искать по базе знаний, обрабатывать заказы и при необходимости переводить на живого оператора.


Анализ требований

ХарактеристикаРеализация
Вопросы и ответы по базе знанийRAG-поиск + ответ модели
Запрос по заказуФункция инструмента @tool
Память диалогаЧекпоинтер SqliteSaver
Фильтрация чувствительного контентаПромежуточное ПО @before_model
Перевод на живого оператора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)text.

Шаг 3: Установка зависимостей

В этом проекте используется всего 6 сторонних библиотек, каждая отвечает за разные функции. Рекомендуется устанавливать их по одной в следующем порядке, чтобы было проще определить, какая библиотека установлена неправильно, если возникнет проблема:

Команда установкиНазначение
pip install langchainОсновная библиотека LangChain, предоставляющая базовые возможности, такие как create_agent и @tool
pip install langchain-deepseekПозволяет init_chat_model распознавать префикс "deepseek:" и вызывать модель DeepSeek
pip install langchain-openaiПредоставляет OpenAIEmbeddings для преобразования текста базы знаний в векторы
pip install langchain-chroma chromadbЛокальная векторная база данных для хранения и извлечения векторов базы знаний
pip install langgraph-checkpoint-sqliteSqliteSaver сохраняет память диалога в файл SQLite
pip install python-dotenvСчитывает переменные среды, такие как ключи API, из файла .env

Вы также можете установить их все одной командой:

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 Bailian..envКод для файла .env выглядит следующим образом:

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

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

Файл .env хранит приватные ключи. Обязательно не добавляйте его в Git-репозиторий и не делитесь им с другими. Вы можете создать новый.gitignoreфайл в проекте и добавить строку.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 = [
    "EXAMPLE — ведущая бесплатная платформа для изучения программирования в Китае, основанная в 2013 году.",
    "Платформа предлагает 300+ руководств, охватывающих Python, Java, HTML, CSS, JavaScript и другие.",
    "Базовый учебник по Python3 состоит из 30 глав, общее число учащихся превысило 5 миллионов. Курс полностью бесплатный.",
    "VIP-членство стоит ¥99/месяц или ¥799/год, включая видеокурсы и поддержку в формате вопросов и ответов один на один.",
    "Политика возврата: полный возврат доступен в течение 7 дней после покупки и в пределах 3 уроков.",
    "Платформа поддерживает онлайн-среду программирования, позволяя писать и запускать код без установки какого-либо программного обеспечения.",
    "Часы работы службы поддержки: с понедельника по пятницу 9:00-18:00, в выходные 10:00-16:00.",
]

# Использование сервиса Embedding Tongyi Qianwen от Alibaba Cloud Bailian (DashScope)
# API Embedding от Bailian совместим со спецификацией OpenAI API, поэтому мы напрямую используем langchain-openai
# OpenAIEmbeddings и укажите base_url на совместимый эндпоинт Bailian,
# нет необходимости устанавливать langchain-community / dashscope (этот пакет больше не поддерживается).
# text-embedding-v4 в настоящее время является рекомендуемой универсальной векторной моделью, по умолчанию выдающей 1024-мерные векторы.
#
# Требуются два ключевых параметра:
# - check_embedding_ctx_length=False: OpenAIEmbeddings по умолчанию использует tiktoken
# чтобы предварительно закодировать текст в массив token id перед отправкой (официальный OpenAI API принимает этот формат),
# но совместимый API Bailian принимает только необработанные строки. Если не отключить эту опцию, будет выброшена
# ошибка "contents is neither str nor list of str".
# - chunk_size=10: API Embedding от Bailian принимает не более 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:
    """Поиск в EXAMPLE базе знаний официальной информации о платформе, курсах, политиках и т. д.

    Аргументы:
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:
    """Запросить статус и детали заказа по номеру заказа.

    Аргументы:
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:
    """Перевести пользователя к оператору-человеку.

    Аргументы:
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 = ["Huang X", "X Bo", "незаконный"]
    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 заменял сообщение на месте,
        # а не добавлять его как новое сообщение в историю (иначе история будет продолжать расти,
        # при каждом раунде добавляя одну "неподписанную версию" и одну "подписанную версию")
        return {"messages": [AIMessage(
            id=last.id,
            content=last.content
            + "\n\n---\n"Центр обслуживания клиентов EXAMPLE | Часы работы: 9:00-18:00"
        )]}
    return None


# ========== 4. Создать агента ==========

# SqliteSaver.from_conn_string() возвращает менеджер контекста, подходящий только для "закрытия после использования"
# одноразовые скрипты. Боту службы поддержки необходимо сохранять одно и то же соединение с базой данных между несколькими вызовами chat(),
# поэтому здесь мы сами устанавливаем соединение и передаём его в конструктор SqliteSaver.
# check_same_thread=False нужен потому, что веб-фреймворки используют одно и то же соединение в разных потоках.
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=""Вы — "Сяо Цай", интеллектуальный агент службы поддержки клиентов EXAMPLE.

## Ваш ИИ
Приветливо встречайте каждого пользователя и обращайтесь к ним на «вы» (вежливая форма «вы»)
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}}

    # Запуск агента
    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.
    """

    config = {"configurable": {"thread_id": thread_id}}
    result = agent.invoke(
        Команда(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, "Я хочу подать жалобу, пожалуйста, переведите меня на живого оператора."))       # Вызвать прерывание, дождаться одобрения
    print(resume_transfer(user_id, confirmed=True,
                           wait_time=5, ticket_id="8823"))  # Возобновить после подтверждения бэкенда

    conn.close()

Результаты запуска:

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

---
Example 客服中心 | 工作时间 9:00-18:00

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

---
Example 客服中心 | 工作时间 9:00-18:00

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

---
Example 客服中心 | 工作时间 9:00-18:00

---
Example 客服中心 | 工作时间 9:00-18:00

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

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

---
Example 客服中心 | 工作时间 9:00-18:00

---
Example 客服中心 | 工作时间 9:00-18:00

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

---
Example 客服中心 | 工作时间 9:00-18:00

---
Example 客服中心 | 工作时间 9:00-18:00

Сводка проекта

Этот бот службы поддержки интегрирует следующие возможности LangChain:

ХарактеристикаИспользование в проекте
RAG-поискинструмент search_kb + векторное хранилище Chroma
Вызов инструментовquery_order、transfer_to_human
CheckpointerSqliteSaver сохраняет диалоги, обеспечивая многоходовую память
Промежуточное ПОbefore_model фильтрация содержимого + after_model добавление подписи (повторно использовать id сообщения для замены на месте)
HITLinterrupt() приостанавливает выполнение + Command(resume=...) возобновляет после одобрения, обеспечивая полный цикл передачи человеку
Другие расширения