Практическое применение LangChain 1.x: цепочки обработки данных, RAG, инструменты и агенты в локальной среде
Научитесь создавать цепочки обработки данных, методы генерации с улучшением на основе поиска, инструменты и агентные системы RAG с использованием LangChain 1.x в рамках бесплатной локальной установки Ollama без необходимости API-ключей.
Это третья часть серии практических уроков, продолжающая предыдущие работы по созданию RAG и агентов с нуля на чистом Python. Подход здесь намеренно отличается: вместо того чтобы самостоятельно собирать каждую составляющую, вы увидите, как LangChain объединяет всю эту технику в несколько строк кода. Поскольку вы уже сами создавали основные компоненты, у вас есть хорошая возможность понять, что на самом деле делает каждая абстракция, вместо того чтобы рассматривать её как что-то магическое. Это понимание важно — оно определяет разницу между разработчиками, которые эффективно используют LangChain, и теми, кто постоянно сталкивается с его сложностями.
Всё в этом уроке работает локально и бесплатно, с использованием локальной модели через Ollama вместе с локальными эмбеддингами. Не требуются API-ключи, и нет необходимости беспокоиться о лимитах использования.
Примечание о версиях: данный учебник предназначен для LangChain 1.x и протестирован с использованием версий
langchain==1.3.11иlangchain-core==1.4.8. Версия LangChain 1.0 принесла существенные изменения в структуре — текущий API агентов основан на функцииcreate_agent, в то время как более старые компоненты, такие какAgentExecutorиinitialize_agent, были перенесены в отдельный пакетlangchain-classic. Многие учебные материалы, доступные в Интернете, по-прежнему демонстрируют подходы, характерные для версий до 1.0; приведённые здесь импорты соответствуют текущим версиям, и каждый из них был проверен на правильность работы.
Как следовать инструкциям: откройте файл с именем lc.py, запустите каждый блок кода по порядку и выполняйте упражнения «Ваша очередь», когда они появятся. Каждый раз, когда вы увидите фразу «это вы создали», речь идет об реализации из предыдущих уроков этой серии.
Шаг 0 — Что такое LangChain
LangChain лучше всего понимать как совокупность стандартизированных, взаимозаменяемых компонентов для создания приложений на основе больших языковых моделей — таких как обертки для моделей, шаблоны запросов, механизмы поиска информации, хранилища векторов, инструменты и агенты. Все эти компоненты соответствуют общему интерфейсу, что позволяет объединять их между собой и заменять один другим (например, использовать другую модель или хранилище векторов) без необходимости переписывать логику приложения.
Концепция, объединяющая всё воедино, — это Runnable. Каждый компонент предоставляет один и тот же метод .invoke(), и любые два компонента могут быть связаны друг с другом с помощью оператора трубки |. Этот механизм связи называется LCEL — сокращение от LangChain Expression Language. Как только каждая часть вашей системы будет использовать интерфейс Runnable, весь пайплайн RAG или агент можно будет описать всего в нескольких строках.
Откровенный компромисс, который стоит упомянуть сразу: LangChain сокращает количество повторяющегося кода и предоставляет доступ к обширному каталогу готовых интеграций. Взамен он вводит слои абстракции, которые могут затруднить отладку — бывают моменты, когда хочется просто смотреть на обычный цикл, написанный вручную. Настоящим навыком является понимание того, когда такая абстракция оправдывает свою стоимость, и мы снова вернемся к этому компромиссу на шаге 7.
Настройка (бесплатная локальная среда)
pip install langchain langchain-core langchain-ollama langchain-huggingface langchain-text-splitters sentence-transformers
Также потребуется установленный Ollama (он бесплатен и работает локально); после этого следует загрузить модель, способную выполнять вызовы инструментов:
ollama pull llama3.2 # ~2 GB; needs ~8 GB RAM. qwen2.5 also works well.
Если вы предпочитаете обойтись без Ollama, вы всё равно можете запустить разделы chain и RAG, используя локальную модель Hugging Face через библиотеку
langchain-huggingface. Однако разделы agent требуют надёжной обработки вызовов инструментов, что маленькие модели с ограниченными ресурсами CPU обычно делают плохо. Для шагов с 4 по 6 настоятельно рекомендуется использовать Ollama.
Шаг 1 — Основной механизм: цепочка операций с использованием символа трубки |
В предыдущем уроке по RAG вы вручную формировали запрос с помощью f-string, передавали его модели и очищали результат с помощью метода .strip(). LangChain предоставляет возможность использовать точно такую же последовательность операций в виде выражения с трубкой. Добавьте следующий код в файл lc.py:
from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
llm = ChatOllama(model="llama3.2", temperature=0)
prompt = ChatPromptTemplate.from_template(
"Explain {topic} in exactly one sentence."
)
# The chain: prompt -> model -> plain-string parser
chain = prompt | llm | StrOutputParser()
print(chain.invoke({"topic": "retrieval-augmented generation"}))
Чтение данных по трубопроводу слева направо: функция prompt преобразует введённый словарь в сообщение с правильным форматированием, llm превращает это сообщение в ответ модели, а функция StrOutputParser() извлекает обычный текст из объекта ответа.
Вы уже создали это ранее. Данный трубопровод функционально идентичен последовательности f"Explain {topic}...", затем generator(prompt) и в конце [0]["generated_text"].strip() из предыдущего обзора RAG — три ручных шага, теперь представленные в виде трёх компонентов, соединённых трубопроводом. Логика не изменилась; была лишь стандартизирована интерфейсная часть.
Ваша очередь: каждый компонент типа Runnable изначально поддерживает методы .batch() и .stream(). Попробуйте это:
for piece in chain.stream({"topic": "vector embeddings"}):
print(piece, end="", flush=True) # tokens arrive as they're generated
print()
print(chain.batch([{"topic": "agents"}, {"topic": "chunking"}])) # two at once
Обратите внимание, что стриминг и группировка данных появились бесплатно просто потому, что вы использовали стандартный интерфейс Runnable. Этот момент «бесплатности» в миниатюре и является основанием для использования LangChain.
Шаг 2 — RAG по-методу LangChain
Пришло время воссоздать вашу ручную схему RAG с использованием элементов конструкции LangChain. Каждый из этих элементов напрямую соответствует тому, что вы уже писали вручную.
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Same Nimbus knowledge base from the RAG tutorial
DOCUMENTS = [
"Nimbus is a fictional note-taking app. The free plan, Nimbus Lite, allows up to 50 notes and 1 GB of storage.",
"Nimbus Pro costs 8 dollars per month billed annually, or 10 dollars billed monthly. It includes 50 GB of storage and collaboration for up to 5 people.",
"Nimbus stores notes encrypted at rest with AES-256. End-to-end encryption is Pro-only and must be enabled in Settings > Security.",
"Nimbus offers a 30-day refund policy on all paid plans. Refunds reach the original payment method within 5 business days.",
"Nimbus live chat support is staffed for Pro customers, Monday to Friday, 9am-6pm UTC. Free users get email support with a 48-hour response time.",
]
# 1. Split (↔ your chunk_text function)
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=40)
chunks = splitter.create_documents(DOCUMENTS)
# 2. Embed locally (↔ your sentence-transformers model)
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
# 3. Store + index (↔ your numpy array of vectors). No server needed.
vectorstore = InMemoryVectorStore.from_documents(chunks, embeddings)
# 4. Retriever (↔ your retrieve() with cosine top-k)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
for doc in retriever.invoke("How much does Pro cost?"):
print("-", doc.page_content[:70], "...")
↔ вы сами это создали — всю эту систему целиком. RecursiveCharacterTextSplitter выполняет функцию разбиения текста на части, но делает это более тщательно: он разделяет текст по границам абзацев и предложений, а не просто считает слова. HuggingFaceEmbeddings представляет собой обертку вокруг той же модели all-MiniLM-L6-v2, которую вы использовали ранее. InMemoryVectorStore заменяет ваш массив векторов в формате numpy, а его метод .as_retriever() выполняет тот же поиск по косинусному сходству типа top-k, который вы реализовывали вручную. Четыре строки здесь описывают всё, что вы создали на этапах с 2 по 4 ранее.
Далее соедините процесс поиска с генерацией с помощью LCEL:
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
rag_prompt = ChatPromptTemplate.from_template(
"Answer using only the context. If it's not there, say you don't know.\n\n"
"Context:\n{context}\n\nQuestion: {question}\nAnswer:"
)
def format_docs(docs):
return "\n\n".join(d.page_content for d in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| rag_prompt
| llm
| StrOutputParser()
)
print(rag_chain.invoke("How much does Nimbus Pro cost per month?"))
Словарь в начале цепочки обрабатывает два пути параллельно: question просто передаёт входные данные без изменений, тогда как context направляет эти же данные через механизм поиска и форматирует результаты. Оба результата затем поступают в промпт. ↔ вы сами это создали — это по сути ваша старая функция rag_answer(): поиск данных, вставка их в промпт, генерация ответа — всё это сведено к одному выражению.
Ваша очередь: Попробуйте вызвать rag_chain.invoke("Могут ли свободные пользователи пользоваться чатом в реальном времени?"), а затем задайте не связанный с этим вопрос, например rag_chain.invoke("Какая столица Франции?"). Обратите внимание на ответ «Я не знаю» — это тот же самый проверочный вопрос, что и в предыдущем уроке RAG, подчеркивающий одну и ту же мысль: качество поиска определяет качество ответа. После этого вызовите retriever.invoke(...) отдельно, чтобы узнать, что именно было извлечено, если ответ кажется некорректным. Такое разделение — проверка поиска независимо от генерации ответа — является привычкой при отладке, которую вы уже использовали ранее, и LangChain сохраняет её, оставляя эти два шага как отдельные объекты типа Runnable.
Шаг 3 — Инструменты
В учебном руководстве по агентам вы определили инструменты как словарь TOOLS и сами написали парсер на основе регулярных выражений для извлечения названия инструмента и его входных данных из необработанного текстового вывода модели. LangChain полностью устраняет необходимость в таком парсере благодаря встроенному вызову инструментов: вы описываете, что делает инструмент, модель отвечает структурированным вызовом, а LangChain занимается маршрутизацией. Определение инструмента выглядит следующим образом с использованием декоратора @tool:
from langchain_core.tools import tool
import ast, operator, datetime
_OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg}
def _ev(n):
if isinstance(n, ast.Constant): return n.value
if isinstance(n, ast.BinOp): return _OPS[type(n.op)](_ev(n.left), _ev(n.right))
if isinstance(n, ast.UnaryOp): return _OPS[type(n.op)](_ev(n.operand))
raise ValueError("unsupported")
@tool
def calculator(expression: str) -> str:
"""Evaluate a basic arithmetic expression like '8 * 12'."""
return str(_ev(ast.parse(expression, mode="eval").body))
@tool
def get_today(_: str = "") -> str:
"""Return today's date in ISO format."""
return datetime.date.today().isoformat()
Вот то, что стоит рассмотреть подробнее. Вы можете увидеть, что именно сгенерировал LangChain из вашей функции:
print(calculator.name) # 'calculator'
print(calculator.description) # the docstring
print(calculator.args) # {'expression': {'title': 'Expression', 'type': 'string'}}
Эта последняя строка — это подлинный, проверенный результат. LangChain проанализировал ваше указание типа (expression: str) вместе с документацией и на основе этого создал схему — именно эта схема используется моделью для определения того, следует ли вызывать инструмент и как это сделать. ↔ вы сами создали её; раньше вы вручную писали описания инструментов внутри своего SYSTEM_PROMPT и сами анализировали вывод модели. Теперь сама документация становится описанием, а анализ происходит автоматически. Это объясняет, почему документация и указания типов имеют огромное значение — они не просто документация, а определяют, как модель понимает и использует инструмент. Небрежная документация приводит к тому, что модель неправильно вызывает инструмент.
Ваша очередь: Замените документацию калькулятора на что-то бесполезное, например """делает математику""", затем снова проверьте значение .description. На 4-м шаге вы сами увидите, как более слабая документация приводит к худшим решениям модели при выборе инструментов. Описание, которое вы пишете, служит своего рода рулём для управления поведением модели.
Шаг 4 — Агенты в одном вызове
Именно здесь окупаются предыдущие усилия. Агент, созданный вами вручную, требовал цикла, временной памяти, парсера, обработки ошибок, ограничения по количеству шагов и системного промпта, который обучал модель формату ReAct. В LangChain 1.x всё это сводится к одному вызову функции: create_agent.
from langchain.agents import create_agent
agent = create_agent(
model=llm, # your ChatOllama from Step 1
tools=[calculator, get_today], # the @tool functions from Step 3
system_prompt="You are a helpful assistant. Use tools for math and dates.",
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "What is 8 times 12, and what is today's date?"}]}
)
print(result["messages"][-1].content)
Это весь агент, от начала до конца. ↔ вы сами его создали — полностью. Функция create_agent внутри себя выполняет цикл «причина-действие-наблюдение», направляет задачу в соответствующий инструмент, подает полученные данные обратно в модель, проверяет условия остановки и соблюдает лимит шагов — всё то, что вы вручную собрали в функции run_agent. В основе всего лежит LangGraph, именно поэтому поведение алгоритма настолько надежно.
Если вы хотите наблюдать за процессом принятия решений — так же, как это показывал режим verbose=True — передавайте промежуточные шаги в реальном времени, а не просто ждите окончательного ответа:
inputs = {"messages": [{"role": "user", "content": "How much is a year of Nimbus Pro?"}]}
for chunk in agent.stream(inputs, stream_mode="updates"):
print(chunk)
По мере его работы вы увидите каждое решение, принятое моделью, и каждый результат, возвращаемый инструментом; они будут выводиться один за другим. Это тот же самый паттерн «Мысль/Действие/Наблюдение», что и в ручном отслеживании, только теперь информация предоставляется в виде структурированных объектов обновлений, а не в виде сырого текста, который приходилось парсить самостоятельно.
Ваша очередь: Попробуйте задать вопрос, который заставит модель использовать два инструмента подряд — например, попросите её определить дату окончания 30-дневного срока возврата средств, если пробный период начался сегодня. Посмотрите, правильно ли она сначала вызывает функцию get_today, а затем calculator. После этого вернитесь к шагу 7 материала об агентах — все способы сбоев, которые вы там описали (изменение формата вывода, выдуманные названия инструментов, бесконечные циклы), могут проявиться и здесь. Фреймворк не улучшает способность слабой модели к логическим рассуждениям; он просто скрывает внутренние механизмы. Именно понимание этой разницы позволит вам быстрее отлаживать этих агентов, чем тем, кто сразу переходит к использованию фреймворка, не создав модель сначала.
Шаг 5 — Собираем всё вместе: агент, выполняющий поиск информации (агентный RAG)
Здесь сходятся все три предыдущих урока. Возьмите своего поискового агента и представьте его в виде инструмента, затем передайте этот инструмент агенту. Начиная с этого момента сам агент решает, когда необходимо выполнять поиск документов — он может искать несколько раз или сочетать процесс поиска с вычислениями в зависимости от потребностей.
@tool
def search_nimbus_docs(query: str) -> str:
"""Search the Nimbus product documentation for facts about plans, pricing, refunds, security, and support."""
docs = retriever.invoke(query)
return "\n\n".join(d.page_content for d in docs)
smart_agent = create_agent(
model=llm,
tools=[search_nimbus_docs, calculator, get_today],
system_prompt=(
"You answer questions about the Nimbus app. "
"Use search_nimbus_docs for any product facts, and calculator for arithmetic. "
"Base answers only on retrieved facts."
),
)
q = "How much would Nimbus Pro cost a team of 4 for a full year?"
result = smart_agent.invoke({"messages": [{"role": "user", "content": q}]})
print(result["messages"][-1].content)
Чтобы правильно ответить на подобный вопрос, агенту сначала необходимо поискать цену месячной подписки, а только затем выполнить вычисления по формуле 8 * 12 * 4. Это и есть процесс поиска (из первого урока), представленный в виде инструмента (из третьего урока) и используемый агентом (из второго урока) — три отдельные концепции, работающие вместе как единая система. Возможность для агента самостоятельно решать, когда выполнять поиск, значительно гибче, чем использование фиксированной последовательности операций rag_chain из второго шага, и это подход, часто встречающийся в реальных производственных системах.
Ваша очередь: Также транслируйте выполнение этого агента с помощью smart_agent.stream(..., stream_mode="updates") и подтвердите порядок действий — поиск происходит до вычислений. Если ваш локальный модель пытается решать арифметические задачи самостоятельно вместо того, чтобы использовать инструмент калькулятора (что часто бывает у менее мощных моделей), усильте системное указание, добавив что-то вроде "Вы ОбЯЗАНЫ использовать калькулятор на каждом этапе арифметических вычислений." Это тот же метод, который сработал в учебном пособии по агентам.
Шаг 6 — Краткий обзор остальных компонентов
На этом этапе у вас уже есть основная структура. Стоит ознакомиться с несколькими дополнительными элементами LangChain, каждый из которых соответствует чему-то, что вы уже создавали вручную:
- Загрузчики документов (
langchain-community) — позволяют напрямую вставлять PDF-файлы, веб-страницы, страницы Notion и подобные источники в те же объектыDocument, которые уже использует инструмент для разбиения текста. Это заменяет ручную вставку текста в список на реальную, структурированную обработку данных. - Хранилища векторов производственного уровня — позволяют заменить внутреннее хранилище в памяти на Chroma или FAISS (импортируемые с помощью
from langchain_chroma import Chroma) для сохранения векторных представлений на диске и масштабирования за пределы возможностей оперативной памяти. Поскольку оба инструмента предоставляют одинаковый интерфейс.as_retriever(), ничего в последующих этапах работы с цепочкой не требуется менять — именно такая согласованность является основной целью замены.
create_agent становится недостаточной (например, при наличии ветвящихся путей, этапов утверждения с участием человека или сотрудничестве нескольких агентов), используется LangGraph — более низкоуровневый движок графов, на котором построена сама функция create_agent.Шаг 7 — Когда использовать LangChain, а когда нет (честный взгляд)
На этом этапе вы уже дважды создавали похожую систему — один раз с нуля, другой раз с использованием фреймворка — что позволяет вам самостоятельно выполнить этот вызов. Именно в этом и заключалась цель рассмотрения обеих версий.
LangChain оправдывает себя, когда необходимо объединить множество существующих интеграций — несколько загрузчиков документов, несколько хранилищ векторов, более одного поставщика моделей — и вы не хотите вручную реализовывать логику потоковой обработки, группировки данных, повторных попыток и отслеживания для каждой из них. Он также полезен, когда вы планируете часто менять компоненты и нуждаетесь в стабильном интерфейсе для их замены, или когда вы создаете агента и предпочитаете не заниматься самостоятельной реализацией логики принятия решений.
Писать код вручную часто более разумно, когда приложение достаточно маленькое, и изучение абстракций LangChain займёт больше времени, чем просто написание пятидесяти с лишним строк, которые вы уже умеете писать. Это также лучший вариант, когда необходим полный контроль над происходящим — пошаговое прослеживание уровней фреймворка для отладки проблем действительно создаёт трудности, и эта жалоба обоснованна — или когда добавление слоя косвенности скроет логику, которая на самом деле более понятна в чистом Python. Пайплайн RAG и агент, созданный вручную в предыдущих уроках, полностью пригодны для использования в производственных условиях; использование фреймворка никак не делает код, написанный вручную, худшим.
Здесь нет единственно правильного ответа. Причина, по которой вы сначала изучили ручную версию, заключается в том, чтобы выбор данной рамки стал осознанным решением, принятым при полном понимании того, что она заменяет, а не стандартным вариантом, к которому обращаются из-за неясности внутреннего устройства.
Шаг 8 — Куда двигаться дальше
- Если ваша локальная настройка кажется слишком медленной, существуют бесплатные тарифы для хостинга моделей от Groq и Google Gemini. Переключение осуществляется одной строкой кода — замените
ChatOllamaнаChatGroqили используйтеinit_chat_model("gemini-...", model_provider="google_genai")— поскольку всё остальное работает через один и тот же стандартный интерфейс. Для использования любого из вариантов потребуется API-ключ, но их бесплатные тарифы не стоят ничего.
AgentExecutor, initialize_agent или LLMChain, которые с тех пор были изменены или устарели.Модель мышления, которую стоит сохранить
LangChain по сути представляет собой ваши собственные разработанные компоненты, стандартизированные под единый интерфейс — Runnable — и соединённые с помощью символа |. Ничего из того, что в нём есть, не является по-настоящему новой идеей, если вы сами создали эти элементы:
- «Цепочка» — это последовательность от запроса к модели до парсера, которую вы уже написали, просто объединённая вместе.
- «Ретривер» — это ваша логика встраивания данных и косинусного поиска, обёрнутая в общий интерфейс.
- «Инструмент» — это функция, которую вы написали, плюс автоматически сгенерированная схема, позволяющая модели вызывать её напрямую вместо того, чтобы вы сами парсили её текстовый вывод.
- «Агент» с помощью функции
create_agent— это весь ваш цикл «анализировать-действовать-наблюдать», сведённый к одному вызову.
Когда что-то идет не так, вы отлаживаете проблему тем же способом, как всегда: выделяете неисправный компонент. Тестируйте механизм получения данных отдельно, выводите значения параметров инструмента .args или отслеживайте промежуточные шаги агента. Фреймворк лишь изменяет объем кода, который необходимо вводить — он не меняет самих процессов, и вы уже понимаете, что происходит.
Устранение неполадок
- Если возникает ошибка
ImportErrorпри вызове функцийcreate_agentилиlangchain_ollama, скорее всего, у вас установлена версия раньше 1.0 или отсутствует какой-то пакет. Запустите командуpip install -U langchain langchain-ollamaи проверьте, что значениеlangchain.__version__начинается с1..
AgentExecutor или initialize_agent, это старый API. В версии 1.x он был перенесён в langchain-classic; новый код должен использовать вместо этого create_agent.ollama serve или откройте приложение и убедитесь, что ваша модель отображается в списке с помощью команды ollama list.qwen2.5.HuggingFaceEmbeddings кажется медленным при первом использовании, поскольку он загружает модель эмбеддингов (примерно 80 МБ) и сохраняет её во временной памяти. После этого сам процесс поиска данных выполняется быстро.Теперь вы создали систему RAG, агентов и фреймворк, объединяющий их — сначала вручную, затем с помощью LangChain. Вы понимаете ту составляющую, к которой большинство людей обращаются только снаружи. Приятного использования.
Связанные материалы
- Понимание ИИ-агентов: цели, инструменты, память и цикл действий агента — простое для новичков объяснение того, чем ИИ-агенты отличаются от чат-ботов, с описанием основных компонентов, цикла принятия решений, уровней автономии и практических применений в реальном мире.
- Структурные ограничения для ИИ-агентов: внутри пайплайна ResolveFlow — объясняет, как агент на основе LangGraph обеспечивает разделение процессов рассуждения и выполнения с помощью проверок на уровне кода вместо инструкций из промптов, включая ошибку поиска данных, возникшую на этапе работы.