День 4: Tool Calling и Structured Outputs
Цель: Научить агентов использовать инструменты и получать структурированные ответы
Время изучения: 2-3 часа
Код: src/day4_tools_structured.py
Тесты: tests/test_day4_tools_structured.py
🤖 Связь с агентами
Вчера: Создали систему Planner → Executor → Critic
Сегодня: Даём агентам инструменты — они становятся по-настоящему полезными!
Что мы строим?
Зачем это нужно?
Реальный сценарий: Исследовательский агент
Tools = способность агента взаимодействовать с внешним миром!
Часть 1: Что такое Tools?
Определение
Tool (инструмент) — это функция, которую агент может вызвать для выполнения действия.
def search_tool(query: str) -> str:
"""Поиск информации в интернете."""
# В реальности: вызов API поиска
return f"Search results for: {query}"
def calculator_tool(expression: str) -> float:
"""Вычисление матем��тического выражения."""
# В реальности: безопасное вычисление
return eval(expression)
def read_file_tool(filepath: str) -> str:
"""Чтение содержимого файла."""
with open(filepath, 'r') as f:
return f.read()
Как агент выбирает tool?
Часть 2: Создание Tools
Простые инструменты
from typing import Dict, Any
# Tool 1: Поиск
def search_internet(query: str) -> str:
"""
Поиск информации в интернете.
Args:
query: Поисковый запрос
Returns:
Результаты поиска
"""
# Эмуляция поиска
results = {
"langgraph": "LangGraph is a library for building stateful, multi-actor applications with LLMs.",
"python": "Python is a high-level programming language.",
"ai": "Artificial Intelligence is the simulation of human intelligence."
}
for key in results:
if key in query.lower():
return results[key]
return f"No results found for: {query}"
# Tool 2: Калькулятор
def calculator(expression: str) -> str:
"""
Вычисление математического выражения.
Args:
expression: Математическое выражение (например, "2 + 2")
Returns:
Результат вычисления
"""
try:
# В реальности: использовать безопасный парсер
result = eval(expression)
return f"Result: {result}"
except Exception as e:
return f"Error: {str(e)}"
# Tool 3: Работа с файлами
def read_file(filepath: str) -> str:
"""
Чтение содержимого файла.
Args:
filepath: Путь к файлу
Returns:
Содержимое файла
"""
try:
with open(filepath, 'r') as f:
content = f.read()
return f"File content ({len(content)} chars): {content[:100]}..."
except FileNotFoundError:
return f"Error: File not found: {filepath}"
except Exception as e:
return f"Error: {str(e)}"
Реестр инструментов
# Словарь досту��ных инструментов
TOOLS = {
"search": search_internet,
"calculator": calculator,
"read_file": read_file
}
def execute_tool(tool_name: str, **kwargs) -> str:
"""
Выполнение инструмента по имени.
Args:
tool_name: Имя инструмента
**kwargs: Аргументы для инструмента
Returns:
Результат выполнения
"""
if tool_name not in TOOLS:
return f"Error: Unknown tool '{tool_name}'"
tool = TOOLS[tool_name]
try:
result = tool(**kwargs)
return result
except Exception as e:
return f"Error executing {tool_name}: {str(e)}"
Часть 3: Structured Outputs
Зачем нужны?
Проблема: LLM возвращает текст, а нам нужна структура
# ❌ Неструктурированный ответ
response = "I will search for LangGraph and then calculate the sum"
# ✅ Структурированный ответ
response = {
"steps": [
{"action": "search", "query": "LangGraph"},
{"action": "calculator", "expression": "10 + 20"}
]
}
Pydantic для валидации
from pydantic import BaseModel, Field
from typing import List, Literal
class ToolCall(BaseModel):
"""Вызов инструмента."""
tool_name: Literal["search", "calculator", "read_file"]
arguments: Dict[str, Any]
reasoning: str = Field(description="Почему выбран этот инструмент")
class Plan(BaseModel):
"""Структурированный план."""
task: str
steps: List[str]
estimated_time: int = Field(description="Оценка времени в минутах")
class ExecutionResult(BaseModel):
"""Результат выполнения."""
success: bool
result: str
tool_used: str
execution_time: float
class Critique(BaseModel):
"""Оценка критика."""
quality_score: int = Field(ge=1, le=10, description="Оценка качества от 1 до 10")
feedback: str
should_retry: bool
suggestions: List[str]
Часть 4: Executor с Tools
from typing import TypedDict, List, Optional
class AgentState(TypedDict, total=False):
task: str
plan: List[str]
current_step: int
step_result: str
tools_used: List[str] # 🆕 История использованных tools
critique: str
status: str
iteration: int
max_iterations: int
final_result: str
def executor_agent_with_tools(state: AgentState) -> AgentState:
"""
Executor Agent с поддержкой tools.
В реальной системе здесь LLM выбирает tool и вызывает его.
"""
updated = dict(state)
plan = state.get("plan", [])
current_step = state.get("current_step", 0)
if current_step >= len(plan):
updated["status"] = "done"
updated["final_result"] = state.get("step_result", "Task completed")
return updated
step = plan[current_step]
# Простая эмуляция выбора tool
# В реальности: LLM анализирует шаг и выбирает tool
tool_name = None
tool_args = {}
if "search" in step.lower() or "find" in step.lower():
tool_name = "search"
tool_args = {"query": step}
elif "calculate" in step.lower() or "compute" in step.lower():
tool_name = "calculator"
tool_args = {"expression": "10 + 20"} # Упрощение
elif "read" in step.lower() or "file" in step.lower():
tool_name = "read_file"
tool_args = {"filepath": "example.txt"}
# Выполняем tool
if tool_name:
result = execute_tool(tool_name, **tool_args)
tools_used = updated.get("tools_used", [])
tools_used.append(tool_name)
updated["tools_used"] = tools_used
print(f" 🔧 Used tool: {tool_name}")
else:
result = f"Completed: {step}"
updated["step_result"] = result
updated["status"] = "critiquing"
updated["iteration"] = updated.get("iteration", 0) + 1
print(f"\n⚙️ Executor: Executing step {current_step + 1}/{len(plan)}")
print(f" Step: {step}")
print(f" Result: {result}")
return updated
Часть 5: Planner со Structured Output
def planner_agent_structured(state: AgentState) -> AgentState:
"""
Planner Agent с структурированным выводом.
В реальной системе здесь LLM возвращает Plan (Pydantic модель).
"""
updated = dict(state)
task = state.get("task", "")
# Эмуляция структурированного планирования
# В реальности: LLM возвращает Plan модель
plan_obj = Plan(
task=task,
steps=[
f"Search for information about '{task}'",
f"Calculate statistics for '{task}'",
f"Summarize findings for '{task}'"
],
estimated_time=15
)
updated["plan"] = plan_obj.steps
updated["current_step"] = 0
updated["status"] = "executing"
updated["iteration"] = updated.get("iteration", 0) + 1
print(f"\n📋 Planner: Created structured plan")
print(f" Task: {plan_obj.task}")
print(f" Steps: {len(plan_obj.steps)}")
print(f" Estimated time: {plan_obj.estimated_time} minutes")
for i, step in enumerate(plan_obj.steps, 1):
print(f" {i}. {step}")
return updated
Полный пример
from langgraph.graph import StateGraph, END
def create_agent_with_tools():
"""Создаёт граф с поддержкой tools."""
graph = StateGraph(AgentState)
# Используем версии с tools
graph.add_node("planner", planner_agent_structured)
graph.add_node("executor", executor_agent_with_tools)
graph.add_node("critic", critic_agent)
graph.set_entry_point("planner")
graph.add_edge("planner", "executor")
graph.add_edge("executor", "critic")
graph.add_conditional_edges(
"critic",
should_continue,
{
"execute": "executor",
"critique": "critic",
"end": END
}
)
return graph.compile()
def main():
"""Пример использования агентов с tools."""
workflow = create_agent_with_tools()
initial_state: AgentState = {
"task": "Research LangGraph and calculate usage statistics",
"iteration": 0,
"max_iterations": 10,
"status": "planning",
"tools_used": []
}
print("="*60)
print("🚀 Agents with Tools")
print("="*60)
result = workflow.invoke(initial_state)
print("\n" + "="*60)
print("📊 Final Result")
print("="*60)
print(f"Task: {result.get('task')}")
print(f"Tools used: {result.get('tools_used', [])}")
print(f"Iterations: {result.get('iteration')}")
print(f"Status: {result.get('status')}")
print(f"Final result: {result.get('final_result')}")
Интеграция с реальным LLM
С LangChain
from langchain_openai import ChatOpenAI
from langchain.tools import Tool
# Определяем tools для LangChain
tools = [
Tool(
name="search",
func=search_internet,
description="Search for information on the internet"
),
Tool(
name="calculator",
func=calculator,
description="Calculate mathematical expressions"
)
]
# LLM с tools
llm = ChatOpenAI(model="gpt-4")
llm_with_tools = llm.bind_tools(tools)
def executor_with_real_llm(state: AgentState) -> AgentState:
"""Executor с реальным LLM."""
step = plan[current_step]
# LLM выбирает tool
response = llm_with_tools.invoke(f"Execute this step: {step}")
# Если LLM вызвал tool
if response.tool_calls:
tool_call = response.tool_calls[0]
tool_name = tool_call["name"]
tool_args = tool_call["args"]
# Выполняем tool
result = execute_tool(tool_name, **tool_args)
else:
result = response.content
# ... обновляем state
Ключевые выводы
✅ Tools — функции, которые агенты могут вызывать
✅ Реестр tools — словарь доступных инструментов
✅ Structured Outputs — Pydantic модели для валидации
✅ LLM выбирает tool — на основе анализа задачи
✅ История tools — отслеживание использованных инструментов
🤖 Связь с агентами
Сегодня вы научились:
- ✅ Создавать инструменты для агентов
- ✅ Агенты выбирают и используют tools
- ✅ Структурировать ответы агентов (Pydantic)
- ✅ Интегрировать с реальным LLM
Следующий шаг: Добавим память (Memory) — агенты будут помнить контекст и историю!
Следующий шаг: День 5
В следующей статье мы изучим:
- Memory — агенты помнят контекст
- Checkpointing — сохранение состояния
- Финальный пайплайн — всё вместе
👉 День 5: Memory и финальный пайплайн (скоро)
Ресурсы
Прогресс: 4/10 дней ✅