Створення дослідницького агента ReAct у LangGraph: мозок, руки, маршрутизатор
Дізнайтеся, як реалізувати цикл ReAct „розуміння-дія-спостереження“ як підграф LangGraph із примусовим відображенням, обмеженнями на ітерації та паралельними методами збору та аналізу даних.
Одне запитання до LLM не може дослідити тему, про яку воно нічого не знає. Корисний агент для досліджень мусить шукати інформацію, читати отримані результати, визначати, що все ще бракує, та продовжувати пошуки, поки не матиме достатньо даних для відповіді. Паттерн ReAct надає цій поведінці чіткої структури, а LangGraph дозволяє представити її у вигляді невеликої, чітко визначеної графи замість заплутаних циклів while. До кінця цього огляду ви зрозумієте кожну вершину функціонального підграфа для досліджень за принципом ReAct, дізнаєтесь, де він може зазнати невдачі, та отримаєте список оновлень, які забезпечують його безпечну роботу в продакшені.
Цей дизайн ґрунтується на компоненті дослідника з відкритого проекту під назвою deep-research-agent. На високому рівні один запуск виглядає так:
- Від користувача надходить запит на дослідження.
- LLM, який виступає у ролі „мозку“, аналізує запит та обирає інструмент для використання.
Що насправді є патерном ReAct
ReAct — це скорочення від „Reason and Act“. Цей підхід походить з наукової статті „ReAct: Synergizing Reasoning and Acting in Language Models“ (вперше опублікованої у 2022 році та представленої на ICLR 2023). Основне спостереження полягає у тому, що мовні моделі працюють краще, коли поєднують два типи дій: роздуми щодо наступних кроків та використання інструментів для отримання реальної інформації. Кожен з цих етапів сам по собі є недостатнім. Модель, яка лише роздумує, буде впевнено вигадувати факти, оскільки немає нічого, що б підтверджувало її твердження. Модель, яка лише діє, буде механічно використовувати інструменти, не аналізуючи отриману інформацію.
Керівництво з архітектури Google Cloud описує цю схему як цикл з кроками у природній мові, який триває до досягнення умови завершення. На практиці він складається з трьох повторюваних фаз:
- Міркування. Модель аналізує всю зібрану до цього моменту інформацію та вирішує, чи вже була надана відповідь на запит, чи що робити далі.
- Дія. Виходячи з цих міркувань, вона або запускає інструмент для збору додаткових даних, або пише остаточну відповідь, що завершує цикл.
- Спостереження. Результат роботи інструменту повертається та зберігається в розмові. Оскільки попередні спостереження залишаються видимими, модель може будувати свої висновки на їх основі, замість того щоб повторювати пошуки чи забувати контекст.
Це схоже на те, як досвідчений інженер досліджує незнайому проблему: шукає інформацію, роздумує над нею, шукає наступні дані, і лише потім робить висновок. Документація Anthropic щодо використання інструментів описує той самий механізм з боку API: модель надсилає запит на використання інструменту, ваше додаток його виконує та повертає результат, і цей процес повторюється. Цикл залишається однаковим незалежно від того, чи є моделлю Claude, GPT чи Gemini.
Це важлива ідея, яку потрібно пам’ятати: ReAct — це шаблон, а не функція бібліотеки. LangGraph випадково надає зручний спосіб його реалізації за допомогою StateGraph, але ви можете реалізувати той самий цикл з будь-якою моделлю та будь-яким кодом оркестрації. Проект deep-research-agent упаковує його як підграф LangGraph, що робить його комбінованим елементом: більша система може викликати його як єдину одиницю. Якщо ви хочете повторно ознайомитися з концепцією перед тим, як приступити до коду, перегляньте наш огляд того, як штучні інтелектуальні агенти поєднують міркування з діями у реальному світі.
Три компоненти та стан, який вони ділять
Цикл дослідника складається з трьох невеликих функцій, кожна з яких виконує певну завдання. „Мозок“ (llm_call) читає поточну розмову та відповідає або простим текстом, або запитами на використання інструментів. „Руки“ (tool_node) виконують будь-які запитані дії з інструментами та повертають їхні результати. „Маршрутизатор“ (should_continue) вирішує, чи потрібен наступний етап. Розділення цих обов’язків дозволяє проводити модульні тести для кожної з функцій, незалежно змінювати модель чи інструменти, а також краще розуміти цикл, читаючи три короткі функції.
Визначення стану дослідника
Усе, що проходить через граф LangGraph, знаходиться у об’єкті стану, оголошеному як TypedDict. Об’єкт стану дослідника має п’ять полів. researcher_messages зберігає поточну розмову; він обгорнутий у Annotated разом із редюсером add_messages, який наказує LangGraph об’єднувати нові повідомлення з існуючим списком замість того, щоб перезаписувати його. tool_call_iterations підраховує кількість циклів у ланцюзі операцій, research_topic фіксує те, що досліджує агент, compressed_research отримує кінцевий огляд, а raw_notes збирає нотатки за допомогою редюсера operator.add, тож списки, повернуті різними вузлами, об’єднуються.
# Define the state that flows through the entire ReAct loop
from typing import Annotated, Sequence, List, TypedDict
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage
import operator
class ResearcherState(TypedDict):
# The message history accumulates as the loop runs
researcher_messages: Annotated[Sequence[BaseMessage], add_messages]
# Tracks how many tool call iterations have occurred
tool_call_iterations: int
# The topic this researcher is investigating
research_topic: str
# The final compressed output after the loop ends
compressed_research: str
# Raw notes collected during research
raw_notes: Annotated[List[str], operator.add]
Редюсери є тим, що забезпечує роботу циклу. Кожного разу, коли „мозок“ відправляє команди або руки повертають результати, вузол передає лише нові повідомлення, а редюсер додає їх. Без функції add_messages кожен вузол замінював би історію, і модель втратила б усе, чого навчилася під час попередніх ітерацій.
У проекті також визначається більш вузька схема виведення даних. Вона контролює, які поля виходять з підграфа, коли його викликає граф-батько.
# Output schema controls what the parent graph sees
class ResearcherOutputState(TypedDict):
compressed_research: str
raw_notes: Annotated[List[str], operator.add]
researcher_messages: Annotated[Sequence[BaseMessage], add_messages]
Розділення внутрішнього стану від стану, який виводиться, — це хороша практика у LangGraph. Такі записи, як tool_call_iterations, мають значення лише всередині циклу, тому граф-батько ніколи їх не бачить. Граф-батько отримує стиснуті результати досліджень, необроблені нотатки та повідомлення, що дозволяє зберегти інтерфейс між графами простим та зрозумілим.
Чому саме TypedDict, а не звичайний словник? Він точно задокументовує, які дані проходять через граф, і дозволяє інструментам перевірки типів виявляти неправильно написані ключі. Функція-редюсер add_messages додає додаткову логіку: вона додає нові повідомлення, а коли надходяче повідомлення має ID вже наявного у списку, вона замінює це повідомлення замість того, щоб дублювати його, тож порядок та ідентичність залишаються послідовними.
„Мозок“: llm_call
„Мозок“ — це те місце, де відбувається міркування. Він створює запит на основі системного повідомлення та всієї історії повідомлень та запитує модель, що робити далі. Зауважте, що в джерелі цей фрагмент позначений як JavaScript; насправді це Python.
# The "brain" of the researcher: analyzes current state and decides next action
from langchain_core.messages import SystemMessage
def llm_call(state: ResearcherState):
# Invoke the LLM with the system prompt and full conversation history
return {
"researcher_messages": [
model_with_tools.invoke(
[SystemMessage(content=research_agent_prompt.format(date=get_today_str()))]
+ state["researcher_messages"]
)
]
}
Тут відбуваються три речі. По-перше, з research_agent_prompt створюється SystemMessage, до якого через get_today_str() додається сьогоднішня дата, щоб модель могла оцінити актуальність інформації. По-друге, цей системний повідомлення додається до всього, що знаходиться в state["researcher_messages"], щоб модель завжди бачила повний контекст. По-третє, model_with_tools.invoke() надсилає все це до моделі, яка має підключені інструменти. Відповідь може бути простим текстом, що свідчить про завершення дослідження, або одним чи кількома запитами на використання інструментів, що свідчить про необхідність додаткової інформації. Функція повертає відповідь, загорнуту у список під ключем researcher_messages, а редюсер додає її до списку.
Підказка системи створюється заново при кожному запиті, а не зберігається у стані. Це дозволяє підтримувати чистоту історії та гарантує, що інструкції завжди будуть на першому місці, навіть після багатьох ітерацій.
model_with_tools створюється під час налаштування. Замість імпорту класу від постачальника, такого як ChatOpenAI, проект використовує функцію init_chat_model() з бібліотеки LangChain, яка приймає рядок імені моделі з префіксом „provider“.
# Initialize the model using LangChain's provider-agnostic helper
from langchain.chat_models import init_chat_model
# The project uses different models for different tasks
model = init_chat_model(model="openai:gpt-4o")
# Bind the research tools so the model knows what actions are available
model_with_tools = model.bind_tools([tavily_search, think_tool])
Перевагою є префікс постачальника: перехід від "openai:gpt-4o" до моделі Anthropic (рядка у форматі "anthropic:claude-sonnet-4-20250514") є зміною конфігурації, а не зміною імпорту. Перевірте поточний список моделей вашого постачальника, адже ідентифікатори змінюються з часом. Функція .bind_tools() потім інформує модель про доступні дії. Приєднано два інструменти: tavily_search, який виконує пошук у Інтернеті через API Tavily, та think_tool — інструмент для рефлексії, про який йтиметься далі.
Інструменти: tool_node
Ці інструменти отримують запити від інструментів з останнього повідомлення „мозку“ та виконують їх. Цей фрагмент коду також написаний на Python, незважаючи на позначку JavaScript.
# The "hands" of the researcher: executes all tool calls from the brain
from langchain_core.messages import ToolMessage
def tool_node(state: ResearcherState):
# Get the tool calls from the last message (the brain's output)
tool_calls = state["researcher_messages"][-1].tool_calls
observations = []
# Execute each tool call and collect raw results
for tool_call in tool_calls:
tool = tools_by_name[tool_call["name"]]
observations.append(tool.invoke(tool_call["args"]))
# Convert raw results into properly formatted ToolMessage objects
tool_outputs = [
ToolMessage(
content=str(observation),
name=tool_call["name"],
tool_call_id=tool_call["id"]
)
for observation, tool_call in zip(observations, tool_calls)
]
return {"researcher_messages": tool_outputs}
Функція читає tool_calls з найновішого повідомлення, шукає кожен запитаний інструмент за назвою у словнику tools_by_name та викликає його з аргументами, наданими моделлю. Саме друга частина є вирішальною для коректності: кожен необроблений результат обгортається у об’єкт ToolMessage, який містить три поля. content зберігає результат у вигляді рядка, name фіксує, який інструмент його створив, а tool_call_id пов’язує результат із конкретним запитом, який його викликав. API моделей вимагають наявності цього ID; результат інструменту, який не можна прив’язати до запиту, відхиляється, а запит без відповідного результату залишає розмову у недійсному стані.
Мапування tools_by_name використовується, але ніколи не визначається у зошиті проекту. Її потрібно створити самостійно, наприклад у вигляді {"tavily_search": tavily_search, "think_tool": think_tool}, або за допомогою конструкції dictionary comprehension над списком інструментів, щоб назви завжди відповідали тому, що було прив’язано.
Якщо „мозок“ просить tavily_search знайти „найновіші дослідження в галузі ШІ“, „руки“ виконують запит та повертають повідомлення у такому форматі:
ToolMessage(content="Search results for 'latest AI research': ...", name="tavily_search", tool_call_id="call_abc123")
Оскільки виклики інструментів виконуються послідовно у звичайному циклі for, виконання кількох пошуків займає стільки ж часу, скільки й усі вони разом. Це прийнятно для демонстрації; пізніше ми розглянемо способи зробити цей елемент більш надійним.
Роутер: should_continue
Роутер — це найменша складова, яка керує всім циклом. Він аналізує останнє повідомлення та обирає наступний вузол.
# The "router": determines whether to loop again or finish
from typing import Literal
def should_continue(state: ResearcherState) -> Literal["tool_node", "compress_research"]:
# Check the last message in the conversation
messages = state["researcher_messages"]
last_message = messages[-1]
# If the brain requested tool calls, continue the loop
if last_message.tool_calls:
return "tool_node"
# If no tool calls, the brain is done researching
return "compress_research"
Якщо останнє повідомлення від „мозку“ містить tool_calls, роутер повертає "tool_node", і цикл продовжується. Якщо „мозок“ створив лише текст, роутер повертає "compress_research", що призводить до завершення циклу та переходу до кроку узагальнення. Рішення приймається виключно на основі результату моделі; сам роутер не має думки щодо того, чи є дослідження достатньо якісним.
Literal["tool_node", "compress_research"] — це анотація, яка повертається функцією та вказує LangGraph, які напрямки є можливими. LangGraph використовує її для визначення гілок маршрутизатора (наприклад, під час створення графа або коли не надано явного мапування), тож це більше, ніж просто документація. Однак це не заважає функції повертати під час виконання якийсь інший рядок; це проявиться як помилка під час обрання відповідної гілки.
Чому маршрутизувати до кроку стиснення замість того, щоб одразу закінчити? Тому що цей підграф призначений для виклику агентом-керувальником. Керувальнику потрібна стисла, структурована відповідь, а не довгий опис пошуків, роздумів та даних інструментів. Стиснення даних всередині підграфа допомагає зберегти невеликий обсяг контексту у самого керувальника.
Підключення циклу за допомогою StateGraph
Після створення трьох функцій наступним кроком є їх з’єднання. Замість того, щоб вручну писати алгоритм керування, ви оголошуєте вузли та ребра, а LangGraph запускає граф. Виконання починається в «мозку», проходить через маршрутизатор та або потрапляє до рук (після чого завжди повертається до «мозку»), або виходить через compress_research.
Складання та компіляція графа
Цей фрагмент також написаний на Python, незважаючи на позначку JavaScript.
# Build the ReAct loop as a LangGraph StateGraph
from langgraph.graph import StateGraph, START, END
# Initialize the graph with both input state and output schema
agent_builder = StateGraph(ResearcherState, output_schema=ResearcherOutputState)
# Add the three nodes to the graph
agent_builder.add_node("llm_call", llm_call) # The brain
agent_builder.add_node("tool_node", tool_node) # The hands
agent_builder.add_node("compress_research", compress_research) # The exit point
# Wire the entry point: execution starts at the brain
agent_builder.add_edge(START, "llm_call")
# Wire the router: after the brain thinks, decide what to do next
agent_builder.add_conditional_edges(
"llm_call",
should_continue,
{
"tool_node": "tool_node",
"compress_research": "compress_research",
},
)
# Wire the loop: after the hands act, always go back to the brain
agent_builder.add_edge("tool_node", "llm_call")
# Wire the exit: after compression, end the graph
agent_builder.add_edge("compress_research", END)
# Compile the graph into a runnable agent
researcher_agent = agent_builder.compile()
Якщо читати зверху вниз:
StateGraph(ResearcherState, output_schema=ResearcherOutputState)створює граф із повним внутрішнім станом та обмеженою схемою вихідних даних, яку будуть бачити батьківські графи.- Реєструються три вузли:
llm_call,tool_nodeтаcompress_research.
add_edge(START, „llm_call“) робить мозок вхідною точкою.add_conditional_edges під’єднує маршрутизатор до виходу мозку, використовуючи словник, який прив’язує кожне можливе значення повернення до відповідного вузла.tool_node назад до llm_call закриває цикл.add_edge(„compress_research“, END) завершує граф, як тільки підсумок буде записаний..compile() перетворює це оголошення на об’єкт, який можна запустити. Його назва — researcher_agent, а не просто agent, тому що в повному проекті це підграф, який ініціюється керуючим елементом.
Мапування, яке передається до функції add_conditional_edges, заслуговує на увагу. Його ключі "tool_node" та "compress_research" повинні точно збігатися з тим, що повертає should_continue, а їхні значення — це справжні назви вузлів. Під час компіляції перевіряється наявність мапованих пунктів призначення, тому помилка в назві вузла призводить до збою на ранньому етапі, а не посеред виконання. З іншого боку, маршрутизатор, який повертає значення, відсутнє у мапі, призводить до збою лише тоді, коли виконується відповідна гілка, тому необхідно протестувати маршрутизатор у обох гілках. Проте це набагато простіше перевірити, ніж еквівалентний ручно написаний цикл while, у якому неправильна гілка просто буде діяти некоректно.
Візуалізація циклу
Компільована графа створює такий потік:
START
│
▼
llm_call (Brain reasons about the query)
│
├── has tool_calls? ──► tool_node (Hands execute tools)
│ │
│ └──► llm_call (Back to brain)
│
└── no tool_calls? ──► compress_research (Summarize and exit)
Це класичний цикл ReAct. Мозок та руки можуть чергуватися стільки разів, скільки модель продовжує запитувати інструменти. Кожен прохід додає спостереження до стану, тож кожне нове рішення приймається на основі більшої кількості інформації, ніж попереднє.
Додавання перевірок стану
Приклад, наведений раніше, — це автономний агент у оперативній пам’яті. Для процесів, які тривають довго, на етапі компіляції додається механізм перевірки стану, щоб стан графа зберігався після кожного кроку.
# Production: add checkpointing for fault tolerance
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver()
agent = agent_builder.compile(checkpointer=checkpointer)
MemorySaver зберігає точки контролю в оперативній пам’яті процесу, що ідеально підходить для розробки та тестування, але зникає після перезапуску. Для продакшну LangGraph пропонує постійні зберігачі точок контролю, такі як PostgresSaver та SqliteSaver, які дозволяють продовжити перерваний запуск з останнього збереженого кроку та фіксувати кожну зміну стану. Один практичний момент: як тільки у графа є зберігач точок контролю, кожен виклик invoke потребує у своїй конфігурації ідентифікатора потоку (наприклад {"configurable": {"thread_id": "..."}}), щоб LangGraph знав, яку збережену розмову завантажити та оновити.
Зміцнення ланцюга обробки: рефлексія, бюджети та паралелізм
Простий цикл ReAct має два способи збою, які проявляються швидко. Він може продовжувати циклюватися без досягнення стабільного стану, витрачаючи токени та кредити API на пошук за пошуком. Або ж він може швидко пройти кілька пошуків, не справді аналізуючи їх, що призводить до повернення поверхневих результатів. Проєкт вирішує обидві проблеми, а потім розширює цикл за допомогою трьох додатків.
Примусове роздумування за допомогою think_tool
Найцікавішим додатком є інструмент, який зовсім не виконує жодних зовнішніх дій. Його єдиною метою є змусити модель зупинитися та написати свої думки.
# A tool that forces the agent to pause and reflect
from langchain_core.tools import tool
@tool(parse_docstring=True)
def think_tool(reflection: str) -> str:
"""Tool for strategic reflection on research progress and decision-making.
Use this tool after each search to analyze results and plan next steps
systematically. This creates a deliberate pause in the research workflow
for quality decision-making.
Args:
reflection: Your detailed reflection on research progress, findings,
gaps, and next steps.
Returns:
Confirmation that reflection was recorded for decision-making.
"""
return f"Reflection recorded: {reflection}"
think_tool отримує рядок з інформацією про reflection та повертає його з попереджувальним префіксом. Довгий документаційний текст не є лише декорацією: за наявності параметра parse_docstring=True LangChain витягує опис інструменту та опис аргументів з цього тексту, і саме цей текст читає модель під час вибору інструменту. Цей ефект досягається завдяки системному запиту, який просить агента викликати think_tool після кожного пошуку. Це змушує модель описати те, що вона щойно дізналася, що все ще бракує та що вона планує робити далі.
Без цього кроку агенти схильні до послідовних пошуків, не синтезуючи нічого між ними. Інженерна команда Anthropic зробила відповідне спостереження: агенти, які можуть переглядати та коригувати власні результати, є більш надійними, оскільки вони виявляють помилки до того, як вони посилюються, та можуть скоригувати свої дії, коли відхиляються від цілі. think_tool додає невеликий механізм самоперевірки до кожної ітерації. Це дешево, адже коштує лише кількості токенів самого роздуму плюс один додатковий цикл обробки, і це допомагає моделі залишатися зосередженою на своїй меті.
Припустимо, що після пошуку агент робить висновок, що знайшов три підходи до індексації RAG, але все ще не має критеріїв для їх порівняння. Інструмент просто повторює цей висновок із своїм префіксом підтвердження:
Reflection recorded: The search results show three approaches to RAG indexing. I still need to find benchmarks comparing them.
Цей рядок зберігається як ToolMessage, тож під час наступної ітерації система знову читає свій власний план. Чому використовується інструмент замість простого запиту до моделі „думати крок за кроком“? Виклик інструменту є окремою, видимою подією у логах; він записується в історію повідомлень у передбачуваному форматі, і запит може вимагати його використання у певний момент циклу.
Контроль бюджету в системному запиті
Друге доповнення обмежує кількість пошуків, які виконує агент:
Budget rules embedded in the system prompt:
- Simple queries: 2 to 3 search calls maximum
- Complex queries: up to 5 search calls maximum
- Always stop after 5 calls if sources are not found
Ці обмеження знаходяться у системному запиті, а не в коді. Моделі повідомляють, скільки пошуків достатньо для простого чи складного запиту та коли слід зупинитися. Це прагматичний вибір: без лічильників чи додаткової логіки графа, лише інструкції, яким модель має довіряти.
Цей компроміс є реальним. У поясненнях Google Cloud зазначається, що ітеративний підхід спричиняє затримку порівняно з одним запитом, а результати сильно залежать від якості моделі. Бюджет пошуку безпосередньо обмежує цю затримку, визначаючи кількість можливих ітерацій.
Проте обмеження, засновані на запитах, є більш гнучкими. Модель може неправильно оцінити складність або просто проігнорувати інструкцію. У структурі вже є поле tool_call_iterations, тому легко додати жорстке обмеження, яке буде дотримуватися роутер. Надійна конфігурація використовує обидва підходи: запити формують нормальну поведінку, а код гарантує верхню межу. Наша стаття „Обмежені агентні цикли для використання інструментів LLM“ розглядає цю саму ідею у TypeScript.
Сканування та збір даних із наглядачем
Третій підхід розглядає весь цикл ReAct як повторно використовуваного працівника. Агент-керівник розділяє складне запитання на підзапитання та паралельно запускає окремий підграф дослідника для кожного з них.
Supervisor receives: "Compare the economic impact of AI on healthcare vs. education"
Supervisor creates two parallel research tasks:
├── ReAct Agent 1: Research AI impact on healthcare
└── ReAct Agent 2: Research AI impact on education
Both agents run their ReAct loops independently.
Results are gathered and synthesized by the supervisor.
Це шаблон розсіювання та збирання: роботу розподіляють між незалежними працівниками, а потім збирають та об’єднують їхні результати. Кожен дослідник має власний стан, інструменти та бюджет, тому довга історія пошуків за одним підзапитанням ніколи не впливає на контекст іншого. Керівник бачить лише стиснуті результати.
Сам керівник є невеликим графом. Замість фіксованого циклу for над підзапитаннями він використовує тип повернення Command від LangGraph, який дозволяє вузлу оновити стан та вказати наступний вузол за один крок:
Supervisor sub-graph nodes:
├── supervisor (LLM decides what to do next)
├── supervisor_tools (executes supervisor-level tools like ConductResearch)
├── red_team (attacks draft logic to find flaws)
└── context_pruner (clears raw notes to manage context size)
The supervisor_tools node uses Command to route dynamically:
- If research is needed → spawns researcher sub-graphs via ConductResearch tool
- If critique is needed → routes to red_team node
- If context is bloated → routes to context_pruner node
- If research is complete → routes to END
Для керівника дослідник є чорною скринькою. Він викликає інструмент ConductResearch, а цей інструмент запускає скомпільований підграф дослідника, який незалежно виконує повний цикл ReAct. Навколо нього розташовані інші спеціалізовані вузли: вузол red_team, який атакує логіку проекту для пошуку слабких місць, та вузол context_pruner, який видаляє необроблені нотатки, коли обсяг контексту стає занадто великим.
Використання Command замість статичних кроків надає керівникові гнучкості під час виконання. Після кожного кроку він може вирішити запустити більше дослідників, надіслати проект на перевірку, скоротити обсяг контексту чи завершити роботу, залежно від поточного стану. Недолік полягає у тому, що логіка маршрутизації потрапляє до коду вузлів, тож форма графа стає менш очевидною лише з огляду на оголошення кроків; тому важливішими стають належне логування та відстеження.
Відстеження повного виконання
Щоб побачити, як елементи працюють разом, задайте агенту досить складне запитання. Джерело позначає цей виклик як звичайний текст; насправді це Python.
# Run the agent with a research question
result = agent.invoke({
"researcher_messages": [
HumanMessage(content="What are the main approaches to reducing hallucination in RAG systems?")
]
})
Якщо ви будете виконувати цей код самостійно, потрібно внести два невеликі корективи. Граф, скомпільований раніше, називається researcher_agent, тож використовуйте саме цю назву (або версію agent, збережену раніше). А якщо ви використовуєте збережену версію, передайте thread_id у налаштуваннях, як описано вище.
Приклад сліду виконання виглядає ось так. Це журнал подій, а не код на Python.
--- Iteration 1 ---
[Brain] Reasoning: I need to search for approaches to reducing RAG hallucination.
[Brain] Tool call: search_tool(query="reducing hallucination in RAG systems approaches")
[Hands] Executing search_tool...
[Hands] Results: Found 5 relevant articles about RAG hallucination reduction.
[Router] Last message has tool_calls? No (think_tool was called)
--- Iteration 2 ---
[Brain] Tool call: think_tool(reflection="The search results mention three main
approaches: better chunking strategies, re-ranking retrieved documents, and
adding citation verification. I should search for specific implementations.")
[Hands] Executing think_tool...
[Router] tool_calls present -> route to tool_node
--- Iteration 3 ---
[Brain] Tool call: search_tool(query="citation verification RAG pipeline implementation")
[Hands] Executing search_tool...
[Router] tool_calls present -> route to tool_node
--- Iteration 4 ---
[Brain] Tool call: think_tool(reflection="I now have solid coverage of the three
main approaches with implementation details. I have enough information to
provide a comprehensive answer.")
[Hands] Executing think_tool...
[Router] tool_calls present -> route to tool_node
--- Iteration 5 ---
[Brain] No tool calls. Generating final response.
[Router] No tool_calls -> route to compress_research
[Compress] Summarizing all findings into structured output.
Що показує цей слід:
- Агент здійснив два пошуки та два виклики для рефлексії, що цілком в межах ліміту, встановленого для запиту.
- Кожен етап рефлексії узагальнював отриману інформацію та підготовлював наступний пошук, що саме і має забезпечувати правило forced-reflection.
tool_node, коли існували виклики інструментів, а до compress_research — коли їх не було.Читайте цей запис як схематичний опис, а не як буквальний вихід програми. У ньому пошуковий інструмент позначений як search_tool, хоча насправді це tavily_search; крім того, у першій ітерації роутер зазначає, що викликів інструментів не було, хоча фактично була зроблена заявка на пошук, що мало б спрямувати обчислення до tool_node. Головне, що варто запам’ятати, — це чергування пошуку та аналізу, поки модель не відповідає простим текстом.
Впровадження цього циклу у продакшн
Підграф дослідника є міцною основою, але п’ять оновлень роблять його значно надійнішим у реальних сценаріях використання:
- Забезпечте дотримання бюджету в коді. Збільшуйте значення
tool_call_iterationsпісля кожного проходження та нехайshould_continueспрямовує виконання до функціїcompress_research, як тільки досягнеться максимум, незалежно від запиту моделі. Це є захисним механізмом у рамках „м’яких“ обмежень запиту. - Надсилайте інформацію про прогрес користувачам. Функція
.astream_events()у LangGraph генерує події щодо змін стану вузлів та викликів інструментів, тож інтерфейс може відображати повідомлення на кшталт „Шукаю...“ чи „Аналізую результати...“ під час виконання циклу замість індикатора завантаження.
try/except та поверніть об’єкт ToolMessage, який описує помилку. Таким чином система бачитиме невдачу та зможе спробувати ще раз із іншими параметрами або змінити підхід. Документація Anthropic рекомендує саме таким чином повідомляти моделі про помилки інструменту.PostgresSaver для завдань, які вимагають багатьох ітерацій, щоб перервана робота могла продовжитися з того місця, де вона зупинилась, а кожен крок міркування та виклик інструменту залишалися прозорими для перевірки.Основні висновки
- ReAct — це цикл мислення, дії та спостереження; він не залежить від конкретної платформи, а LangGraph просто робить цей цикл очевидним та доступним для перевірки.
- Три вузли спеціалізованого призначення (мозок, руки, маршрутизатор) разом із станом, підтримуваним за допомогою редуктора, достатні для функціонального дослідницького агента.
- Завжди пов’язуйте результат кожного інструменту з його
tool_call_id, а також розділяйте внутрішній стан від того, що відображає підграф. - Інструмент рефлексії без дій є недорогим та видимим способом змусити відбутися синтез між пошуками.
- Бюджети запитів впливають на поведінку, але лише обмеження на рівні коду гарантує завершення роботи.
Пов’язана література
- Створення AI-агента з нуля: шаблони, ReAct та LangGraph — Дізнайтеся про основні концепції AI-агентів — планування, використання інструментів, рефлексія та шаблон ReAct — а також про те, як LangChain та LangGraph допомагають у ручному створенні такого агента.
- Вибір фреймворку для Python-AI-агента у 2026 році: практичне порівняння — Порівнює п’ять фреймворків для Python-AI-агентів за способом роботи з помилками та складністю, допомагаючи читачам підібрати відповідний інструмент для своєї робочої процедури.
- Маршрутизація, Fan-Out, ReAct, критика та схвалення: п’ять шаблонів LangGraph — Дізнайтеся про п’ять шаблонів робочих процесів у LangGraph, від маршрутизаторів та циклів ReAct до елементів оцінки та людського схвалення, а також про правила, необхідні для їх використання у продакшені.
- Як працює InMemorySaver у LangGraph: як функціонують чекпоїнти, записи та блоки даних — Детально розгляньте механізми зберігання, запису та обробки даних у InMemorySaver LangGraph та простежте, як один невеликий запуск графа перетворюється на три взаємопов’язані чекпоїнти.
- Specialist Agents, a Keyword Router and interrupt(): A LangGraph Coach — Розробка асистента LangGraph із двома спеціалістами, детерміністичним маршрутизатором, спільним станом, який зберігається під час передачі обов’язків, та системою перевірки від людини, заснованою на механізмах interrupt та Command.
- From a One-Node Chatbot to an MCP-Backed Agent in LangGraph — Створення додатку LangGraph шар за шаром: стан та функції зміни стану, краї мережі, цикли використання інструментів, потоки з можливістю перевірки, три режими потокової обробки даних та інструменти, які надаються через MCP.