От чата к агенту
Попросите программу «посмотреть» pyproject.toml — и без доступа
к диску она уверенно опишет, что там обычно бывает. Файл она
не открывала: это обычный чат. В этом уроке дадим ей один настоящий
инструмент — чтение файла — и увидим разницу.
read, реально читает файл с диска и отвечает
уже по фактам, а не по догадкам.
Слова, которые встретятся
- Чатбот — модель только пишет текст. Файлы, терминал, поиск ей недоступны.
- Агент — та же модель, но у неё есть инструменты (tools): функции, которые вы написали на Python и которые она может вызвать по ходу ответа.
- Tool / инструмент — обычная функция с описанием. Модель решает «когда вызвать» по docstring; вы решаете «что сделать» в теле функции.
- Harness — «обвязка» вокруг модели: инструменты, правила безопасности, лимиты. Весь курс как раз про то, как её собрать.
-
Контекст — всё, что модель «помнит» в этом разговоре
(ваши сообщения, ответы, результаты инструментов). Место не бесконечное:
поэтому мы ограничиваем, сколько строк возвращает
read.
Быстрый путь
- Создайте проект и поставьте библиотеку
pydantic-ai. - Напишите
main.pyс агентом без инструментов — посмотрите на «догадки». - Добавьте инструмент
read: нумерация строк и потолок в 500 строк. - Снова спросите про файл — теперь ответ должен опираться на реальное содержимое.
Подготовка проекта
Команда uv run подхватывает только то, что записано в проекте.
Без шага ниже получите
ModuleNotFoundError: No module named 'pydantic_ai'.
Terminal
mkdir teensycode && cd teensycode
uv init --name teensycode
uv add pydantic-ai
В магазине пакетов имя с дефисом: pydantic-ai.
В коде — подчёркивание: from pydantic_ai import Agent.
Модель DeepSeek подключается строкой
deepseek:deepseek-chat
(подробнее в
документации Pydantic AI).
Ключ API возьмите на platform.deepseek.com и положите в окружение:
export DEEPSEEK_API_KEY="sk-..."
uv run python -c "import pydantic_ai; print(pydantic_ai.__version__)".
Должна появиться версия. Если снова ошибка импорта — вы не в папке
с pyproject.toml или забыли uv add.
Упражнение 1.1
Сначала минимальный агент, потом один инструмент.
- Импорты:
Agentи (для инструмента)RunContext. - Модель:
deepseek:deepseek-chat. -
Инструмент
read: путь к файлу, по желанию с какой строки начать (offset) и сколько строк взять (limit). - Не больше 500 строк за раз; в ответе инструмента у каждой строки номер.
Шаг 1. Чатбот (без инструментов)
Пока инструментов нет — модель может только говорить.
Напишите такой main.py:
main.py
import asyncio
import sys
from pathlib import Path
from pydantic_ai import Agent
cwd = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path.cwd()
prompt = " ".join(sys.argv[2:]) if len(sys.argv) > 2 else "Привет!"
agent = Agent(
"deepseek:deepseek-chat",
instructions=(
f"Ты coding-агент.\nРабочая директория: {cwd}\n"
"У тебя нет инструментов и нет доступа к файловой системе. "
"Не выдумывай вызовы tools, XML, DSML или shell-команды. "
"Отвечай только обычным текстом — можно честно сказать, "
"что не видишь файлы, или предположить типичную структуру."
),
)
async def main() -> None:
result = await agent.run(prompt)
print(result.output)
if __name__ == "__main__":
asyncio.run(main())
Terminal
uv run python main.py . "Какие файлы в этом проекте?"
Хороший исход — обычный текст: «не вижу файлы» или
вымышленная структура. Плохой — «запуск» ls или странная
разметка в ответе. Модель всё ещё только болтает: это чатбот.
result.output. Усильте instructions (как выше)
или спросите мягче: «Что обычно лежит в pyproject.toml?». Когда
появится настоящий @agent.tool, библиотека передаст модели
список реальных инструментов, и вызовы пойдут через ваш Python-код.
Шаг 2. Один инструмент меняет всё
Замените main.py целиком (не дописывайте
кусок к версии без tools). Нужны оба импорта:
Agent и RunContext.
RunContext — «контекст запуска»: через него инструмент
получает то, что вы передали в deps= (у нас — корневая
папка проекта). Без deps=cwd функция не знает, откуда читать.
main.py
import asyncio
import sys
from pathlib import Path
from pydantic_ai import Agent, RunContext
MAX_LINES = 500
cwd = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path.cwd()
prompt = " ".join(sys.argv[2:]) if len(sys.argv) > 2 else "Привет!"
agent = Agent(
"deepseek:deepseek-chat",
deps_type=Path,
instructions=(
f"Ты coding-агент.\nРабочая директория: {cwd}\n"
"Используй инструмент read, чтобы открывать файлы. "
"Не выдумывай содержимое и не имитируй tool-calls текстом."
),
)
@agent.tool
def read(
ctx: RunContext[Path],
path: str,
offset: int | None = None,
limit: int | None = None,
) -> str:
"""Прочитать файл проекта. Возвращает пронумерованные строки.
WHEN TO USE: просмотр содержимого, конфигов, исходников.
WHEN NOT TO USE: поиск по нескольким файлам (используй grep).
"""
abs_path = (ctx.deps / path).resolve()
if not str(abs_path).startswith(str(ctx.deps.resolve())):
return "Ошибка: путь вне рабочей директории."
if not abs_path.is_file():
return f"Ошибка: файл не найден: {path}"
lines = abs_path.read_text(encoding="utf-8").splitlines()
start = (offset or 1) - 1
lines = lines[start:]
if limit is not None:
lines = lines[:limit]
truncated = len(lines) > MAX_LINES
if truncated:
lines = lines[:MAX_LINES]
# Явная нумерация: каждая строка вида "12: содержимое"
numbered_lines: list[str] = []
first_line_no = offset or 1
for i, line in enumerate(lines):
numbered_lines.append(f"{first_line_no + i}: {line}")
body = "\n".join(numbered_lines)
if truncated:
body += f"\n... (обрезано на {MAX_LINES} строках)"
# Печатаем результат инструмента в консоль — иначе в ответе модели
# будет только пересказ без номеров строк.
print("--- tool read output (пронумерованные строки) ---")
print(body)
print("--- конец tool output ---")
return body
async def main() -> None:
result = await agent.run(prompt, deps=cwd)
print("=== ответ модели ===")
print(result.output)
if __name__ == "__main__":
asyncio.run(main())
Agent. Нужно:
from pydantic_ai import Agent, RunContext.
Проверьте
Terminal
uv run python main.py . "Прочитай pyproject.toml"
Ожидаемый вид консоли (сначала инструмент, потом модель):
--- tool read output (пронумерованные строки) ---
1: [project]
2: name = "ai-loop-agent"
3: version = "0.1.0"
...
--- конец tool output ---
=== ответ модели ===
Вот содержимое файла pyproject.toml: ...
Если видите только пересказ без блока tool read output —
в read нет print(body) из листинга.
Номера в ответе модели не обязательны: они в выводе инструмента.
Пока нет поиска и терминала, держитесь фраз вида «Прочитай вот этот файл».
Пункт про «путь относительно рабочей директории»: первый аргумент
(. или полный путь к проекту) — это корень. Модель
передаёт относительное имя вроде pyproject.toml, а код
склеивает его с корнем. Запрос вроде «прочитай /etc/passwd»
должен вернуть ошибку «путь вне рабочей директории», а не секреты системы.
Готово, если:
- Есть
pyproject.toml, пакет ставится черезuv add pydantic-ai - Без инструментов — обычный текстовый ответ, без реальных вызовов функций
- С
read— модель вызывает инструмент и опирается на содержимое файла - В консоли есть блок с строками
1: …,2: …; в коде —offset,limitи потолок 500 - Пути считаются от корня проекта; выход «наверх» или в
/etcблокируется
seq 1 1000 > /tmp/big.txt, попросите агента прочитать его
(скопируйте файл в проект или укажите доступный путь). Уберите
MAX_LINES и сравните. Что сломается раньше на длинной задаче —
здравый смысл ответа или лимит контекста?