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 найкраще розуміти як сукупність стандартизованих, взаємозамінних компонентів для створення додатків на основі LLM — таких як обгортки моделей, шаблони запитів, засоби пошуку, сховища векторів, інструменти та агенти. Усі ці елементи відповідають спільному інтерфейсу, що дозволяє поєднувати їх між собою та замінювати один на інший (застосовувати іншу модель, змінювати сховища векторів) без необхідності переписувати логіку додатку.
Концепція, яка об’єднує все разом, — це 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 — три ручні кроки, які тепер представлені у вигляді трьох Runnables, з’єднаних через потік. Логіка не змінилася; лише інтерфейс було стандартизовано.
Ваша черга: кожен 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 зберігає її, залишаючи ці два кроки як окремі елементи для виконання.
Крок 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. Ви розумієте той шар, до якого більшість людей звертаються лише ззовні. Насолоджуйтесь його використанням.
Пов’язана література
- Розуміння AI-агентів: цілі, інструменти, пам’ять та цикл агента — просте пояснення для початківців про те, чим AI-агенти відрізняються від чат-ботів, з описом основних компонентів, циклу прийняття рішень, рівнів автономії та прикладів використання у реальному світі.
- Структурні механізми контролю для AI-агентів: як це працює у пайплайні ResolveFlow — пояснює, як агент на основі LangGraph забезпечує розділення процесів міркувань та виконання за допомогою перевірок на рівні коду замість інструкцій у запитах, включаючи помилку отримання даних, яка виникла під час роботи.