Організуйте агентів-дослідників та програмістів за допомогою LangGraph
Створіть додаток LangGraph з кількома агентами за допомогою promptів типу soul, моделей Ollama, пошуку Tavily, інструментів передачі завдань та чекпоїнтів Postgres для організації робочих процесів під керуванням оркестратора.
Створення одного ШІ-агента є простим у порівнянні з координацією кількох фахівців, які знають свої обов’язки та коли передавати роботу іншим. Робочий процес LangGraph може призначити Оркеструвальника відповідальним за маршрутизацію, тоді як Дослідник та Програміст виконують конкретні завдання. Кожен агент зосереджується на певній функції — пошуку інформації, реалізації чи координації — тож граф співпрацює для отримання однієї відповіді, замість того щоб змушувати одну модель виконувати всі кроки.
Розробка
Початок нового проекту
Якщо у вас є Python, створіть основу проекту LangGraph:
Langgraph new
Ця команда створює шаблон, який можна використовувати знову. Додайте залежності для виконання за допомогою uv:
uv add "ddgs>=9.16.0" "deepagents>=0.7.13" "langchain>=1.4.0" "langchain-community>=0.4.2" "langchain-ollama>=1.1.0" "langchain-openrouter>=0.2.8" "langgraph>=1.0.0" "langgraph-checkpoint-postgres>=3.1.2" "psycopg[binary,pool]>=3.3.5" "python-dotenv>=1.0.1"
Налаштуйте файл .env для відстеження дій LangSmith, URI до Postgres як контрольної точки у продакшені та ключ Tavily для пошуку в Інтернеті:
# To separate your traces from other application
LANGSMITH_PROJECT=langgraph-agent
# Add API keys for connecting to LLM providers, data sources, and other integrations here
LANGSMITH_API_KEY=lsv2_xxx
POSTGRES_URI=postgresql://xxx:xxx@localhost:5432/postgres?sslmode=disable
# Tavily Search API - Get your key at https://tavily.com
TAVILY_API_KEY=tvly-xxx
LangSmith розділяє сліди за проектами, Postgres зберігає повідомлення у стані контролю для стабільної роботи, а Tavily підтримує інструмент пошуку дослідника.
Агент SOUL
Кожному агенту потрібен системний запит (“душа”), збережений у форматі Markdown. Впорядкуйте їх у директорії src/agent:
src/agent/
---------- souls/
----------------- orchestrator/
----------------- researcher/
----------------- coder/
Створіть файли SOUL.md для оркестратора, дослідника та програміста. Приклад матеріалу запиту:
# Orchestrator
**Role:** Intelligent Coordinator - strategic entry point for all user queries
**Mission:** Understand user intent, assess query clarity, and prepare optimal handoff to specialist agents
You are Orchestrator, the strategic coordinator of this multi-agent system.
## Core Responsibilities
### 1. Query Understanding & Validation
- **Parse user intent** — what is the user really asking for?
- **Assess clarity** — is the request clear and actionable?
- **Identify ambiguity** — are there missing details or unclear requirements?
### 2. User Engagement
- **Acknowledge requests** — brief confirmation you understand the query
- **Ask clarifying questions** — if requirements unclear or ambiguous
- **Set expectations** — let user know what specialist will handle this
### 3. Context Preparation
- **Enrich query** — add relevant context from conversation history
- **Frame problem** — structure request for optimal specialist handling
- **Maintain continuity** — ensure conversation state flows correctly
## Decision Logic
### When to Ask Clarifying Questions
- Ambiguous requirements ("make it better" - better how?)
- Multiple possible interpretations (research vs implementation)
- Missing critical details (which file? which feature?)
- Vague scope ("fix the app" - what's broken?)
### When to Route Directly
- Clear, specific requests
- Obvious intent (research vs coding)
- Sufficient context provided
- Standard queries
## What You DON'T Do
- **Don't answer technical questions** — delegate to researcher
- **Don't write code** — delegate to coder
- **Don't implement solutions** — specialists execute
- **Don't do deep analysis** — specialists research
## Response Style
- **Brief and strategic** — acknowledge, clarify if needed, route
- **User-focused** — ensure user understands what happens next
- **Context-aware** — reference conversation history when relevant
## Example Responses
**Clear query:**
"Understood - you want to learn how bubble sort works in C++. Routing to researcher to explain the algorithm logic and implementation approach."
**Ambiguous query:**
"I see you want to improve authentication. To route this correctly, can you clarify: are you looking for an explanation of how our current auth works (research), or do you want to implement specific improvements (coding)?"
**Follow-up query:**
"Got it - continuing from our previous discussion about the login system, you want to fix the 500 error. Routing to coder to debug and implement the fix."
# Researcher
**Role:** Researcher
**Mission:** Investigates bounded current-reality questions using current and original sources. Produces concise, inspectably cited briefings with uncertainty, counterevidence, and unresolved gaps, then returns them to @orchestrator for quality review. Does not write to the wiki.
You are Researcher, a persistent named agent (profile `researcher`) on this machine.
You keep your own memory, skills, and conversation history across sessions.
## Research protocol
- Research bounded questions about current real-world conditions using recent evidence and the most direct or original sources available.
- Provide inspectable citations and distinguish evidence, inference, uncertainty, counterevidence, and unresolved gaps.
- Save each completed briefing as a Markdown file under `/workspace/article/`, using a clear descriptive filename.
- Include the research question, date, summary, findings, source links, uncertainty, counterevidence, and unresolved gaps.
- Return the briefing and its file path to @orchestrator for review.
- Do not hand work directly to @librarian unless the user explicitly requests it.
- Treat the Markdown file as a working research artifact-not private memory or canonical Wiki knowledge.
- Write only wiki inside /workspace/article/
# Coder
## Objective
Executes coding tasks as instructed by the orchestrator. Transforms orchestrator directives into clean, correct, and well-documented Python code. Does not operate independently — only implements what the orchestrator specifies.
## Scope
* Writes new code (scripts, functions, algorithms, data transformations) as directed by orchestrator.
* Debugs, refactors, and optimizes existing code per orchestrator instructions.
* Explains technical decisions when orchestrator requests it.
* **Awaits orchestrator instructions** — does not self-initiate work or interpret requirements independently.
## Out of Scope
* Does not make product, business, strategic, or architectural decisions — strictly implements orchestrator directives.
* Does not interpret user requirements directly — only follows orchestrator's parsed and assigned tasks.
* Does not fabricate library behavior, APIs, or results it hasn't verified — if uncertain, reports back to orchestrator.
* Does not modify scope or requirements; if orchestrator's instruction is ambiguous or infeasible, asks orchestrator for clarification.
## Standards
* **Correctness first:** code should run as intended and handle reasonable edge cases (empty inputs, invalid types, boundary values).
* **Readability:** clear naming, consistent style (PEP 8), and logical structure over cleverness.
* **Documentation:** every non-trivial function includes a docstring (purpose, parameters, return value); complex logic includes inline comments explaining *why*, not just *what*.
* **Minimal footprint:** no unnecessary dependencies, no dead code, no speculative features beyond what was asked.
* **Testability:** where relevant, includes example usage or simple test cases to demonstrate the code works.
## Output Format
When completing an orchestrator-assigned task, report back with:
1. **Task completion status** — confirmation of what was implemented per orchestrator's instruction (1–2 sentences).
2. **Code** — complete, runnable, and documented.
3. **Issues or blockers** — any technical obstacles, ambiguities, or clarifications needed from orchestrator (only if applicable).
**Important:** Wait for orchestrator directives. Do not proceed with tasks until orchestrator assigns them.
Ці файли кодують роль, завдання та обмеження функціонування, щоб кожен вузол завантажував стабільний персонаж замість того, щоб зберігати довгі рядки у Python.
Модель Ollama
У цьому посібнику використовується хмарна модель Gemma 4, розміщена на серверах Ollama, тому немає потреби у великому локальному завантаженні. Інші постачальники можуть використовуватися для експериментів.
ollama pull gemma4:cloud
Визначте невеликий обгорток для моделі у директорії src/agent:
class ModelAgent:
"""A simple agent that uses the Ollama model for chat."""
def __init__(self, model_name: str = "ollama:gemma4:31b-cloud"):
self.model_name = model_name
def load_model(self):
if self.model_name.startswith("ollama:"):
new_model_name = self.model_name.split("ollama:")[1]
print(f"Loading Ollama model: {new_model_name}")
# Auto-detect Ollama base URL
# Priority: OLLAMA_BASE_URL env var > auto-detect Docker > localhost
base_url = os.getenv("OLLAMA_BASE_URL")
if not base_url:
# Auto-detect: check if running in Docker
if os.path.exists("/.dockerenv") or os.path.exists("/run/.containerenv"):
# Running in container - use host.docker.internal
base_url = "http://host.docker.internal:11434"
print("Detected Docker environment, using host.docker.internal")
else:
# Running locally - use localhost
base_url = "http://localhost:11434"
print("Detected local environment, using localhost")
return ChatOllama(
model=new_model_name,
base_url=base_url
)
else:
raise ValueError("Unsupported model name")
Обгортка не включає деталі постачальника до графу: вузли викликають один клас та передають назву моделі, яка їм потрібна.
Глобальна змінна
Спочатку ініціалізуйте спільні значення за замовчуванням, перш ніж оголошувати вузли чи інструменти:
DEFAULT_MODEL = "ollama:gemma4:31b-cloud"
base_dir = os.path.dirname(os.path.abspath(__file__))
client = SandboxClient()
ls_sandbox = client.create_sandbox()
backend = LangSmithSandbox(sandbox=ls_sandbox)
Інструмент Tavily
Досліднику потрібен інтернет-пошук. Tavily налаштований як інструмент, створюваний за потреби, який читає TAVILY_API_KEY:
# Lazy initialization for search tool - requires TAVILY_API_KEY env var
_search_tool = None
def get_search_tool():
global _search_tool
if _search_tool is None:
_search_tool = TavilySearchResults(
max_results=5,
search_depth="advanced",
include_answer=True,
include_raw_content=False,
)
return _search_tool
Передача обов’язків
Передача обов’язків — це інструмент, який повертає команду графу та передає керування іншому агенту. Звичайні інструменти виконують роботу; передача обов’язків змінює того, хто виконуватиме наступний крок. Вона має значення для:
- Координації кількох агентів
- Агентів із різними наборами інструментів
- Оркестратора, який делегує завдання замість того, щоб виконувати їх сам
Визначте механізми передачі обов’язків для дослідників та програмістів:
# Agent wrap tool
@tool
def handoff_to_researcher_agent(task: str, runtime: ToolRuntime) -> Command:
"""Delegate internet research and reference tasks to the researcher agent.
Provide a complete, self-contained description in the task parameter,
as the agent lacks access to prior conversation history.
"""
return Command(
goto="researcher",
update={"messages": [ToolMessage(
content=f"Handed off to researcher with task: {task}",
tool_call_id=runtime.tool_call_id,
)]},
graph=Command.PARENT,
)
@tool
def handoff_to_coder_agent(task: str, runtime: ToolRuntime) -> Command:
"""Delegate coding and filesystem tasks to the coder agent.
Provide a complete, self-contained description in the task parameter,
as the agent lacks access to prior conversation history.
"""
return Command(
goto="coder",
update={"messages": [ToolMessage(
content=f"Handed off to coder with task: {task}",
tool_call_id=runtime.tool_call_id,
)]},
graph=Command.PARENT,
)
Вузли агентів
Впровадіть три вузли — Оркестратор, Дослідник та Кодувальник — часто з різними назвами моделей, щоб можна було налаштовувати витрати та точність для кожної ролі:
async def orchestrator_agent(state: RouterState, runtime: Runtime[Context]) -> Dict[str, Any]:
file_path = os.path.join(base_dir, "souls", "orchestrator", "SOUL.md")
soul = await read_md_file(file_path)
# Use context model or default (context can be None)
model_name = (runtime.context or {}).get("orchestrator_model", DEFAULT_MODEL)
model = ModelAgent(model_name=model_name).load_model()
agent = create_agent(
model=model,
tools=[
handoff_to_researcher_agent,
handoff_to_coder_agent,
],
system_prompt=soul,
)
result = await agent.ainvoke({"messages": state["messages"]})
return {"messages": result["messages"]}
async def researcher_agent(state: RouterState, runtime: Runtime[Context]) -> Dict[str, Any]:
file_path = os.path.join(base_dir, "souls", "researcher", "SOUL.md")
soul = await read_md_file(file_path)
# Use context model or default (context can be None)
model_name = (runtime.context or {}).get("researcher_model", DEFAULT_MODEL)
model = ModelAgent(model_name=model_name).load_model()
agent = create_agent(
model=model,
tools=[get_search_tool()],
system_prompt=soul,
)
last_msg = state["messages"][-1]
human_msg = HumanMessage(content=last_msg.content)
# Invoke with conversation context - agent will see full message history
result = await agent.ainvoke({"messages": [human_msg]})
result["messages"] = [
m for m in result["messages"] if not isinstance(m, HumanMessage)
]
# Return results with source tracking
return {
"messages": result["messages"],
"results": [{"source": "researcher", "result": str(result["messages"][-1].content)}]
}
async def coder_agent(state: RouterState, runtime: Runtime[Context]) -> Dict[str, Any]:
file_path = os.path.join(base_dir, "souls", "coder", "SOUL.md")
soul = await read_md_file(file_path)
# Use context model or default (context can be None)
model_name = (runtime.context or {}).get("coder_model", DEFAULT_MODEL)
model = ModelAgent(model_name=model_name).load_model()
agent = create_deep_agent(
model=model,
tools=[],
system_prompt=soul,
backend=backend
)
last_msg = state["messages"][-1]
human_msg = HumanMessage(content=last_msg.content)
# Invoke with conversation context - agent will see full message history
result = await agent.ainvoke({"messages": [human_msg]})
result["messages"] = [
m for m in result["messages"] if not isinstance(m, HumanMessage)
]
# Return results with source tracking
return {
"messages": result["messages"],
"results": [{"source": "coder", "result": str(result["messages"][-1].content)}]
}
Шлях Кодувальника приймає параметр backend, щоб виконання коду могло відбуватися в ізольованому середовищі, окремо від хоста. У посібнику Deep Agents backends LangChain описано кілька варіантів backend, зокрема композитні маршрутизатори: https://docs.langchain.com/oss/python/deepagents/backends#compositebackend-router
Логіка графа
Під’єднайте підготовлені елементи до графа стану:
builder = StateGraph(RouterState, context_schema=Context)
builder.add_node("orchestrator", orchestrator_agent)
builder.add_node("researcher", researcher_agent)
builder.add_node("coder", coder_agent)
builder.add_edge(START, "orchestrator")
# LangGraph API provides persistence automatically
# - langgraph dev: in-memory checkpointer
# - production deploy: PostgreSQL checkpointer (uses POSTGRES_URI from .env)
graph = builder.compile()
Під час локальної розробки можна використовувати механізм checkpointer в пам’яті від LangGraph. Під час розгортання в продакшені передається URI до Postgres, щоб чекпоїнти зберігалися після перезапуску процесу.
Тестування
Існує два режими запуску, які охоплюють весь життєвий цикл: langgraph dev для локальних ітерацій та langgraph up для роботи у режимі продакшену.
Корисним тестом є присвоєння назви та запитання про те, чи пам’ятається вона пізніше — це підтверджує збереження контексту розмови. Питання, пов’язані з дослідженнями, мають спрямовуватися до дослідника та використовувати Tavily. Запити на програмування, наприклад реалізацію алгоритму quicksort у C++, мають передаватися програмісту. Якщо всі три шляхи функціонують успішно, граф готовий до додавання нових агентів, інструментів чи навичок у майбутньому розширенні.
Підсумок
Гнучкість LangGraph є основною перевагою для проектування багатоагентних систем. Ніщо не змушує використовувати фіксовану кількість агентів чи певну топологію. Граф може починатися з одного вузла та розвиватися, додаючи спеціалізовані агенти, умовне маршрутизування, паралельні гілки, цикли, паузи з участю людини чи вкладені підграфи у міру уточнення вимог.