HARD-задача: прятки в репозитории
Домашка
Учебная домашка модуля — «агент с одним инструментом» — была милосердна: сценарий из двух шагов. Настоящая задача жёстче: агент должен найти ответ в репозитории за ограниченное число шагов и уложиться в бюджет токенов промпта. Проверяться будет не «нашёл вообще», а «нашёл дёшево». Это первая задача курса, где качество измеряется сразу в двух валютах: шагах и токенах.
Описание задачи
Файл scavenger.py с функцией:
def find_answer(llm, root, question: str,
max_steps: int = 4, max_prompt_tokens: int = 700) -> strОна собирает конвейер из уже написанных модулей: make_tools(root) для инструментов, петлю и сжатие истории под бюджет max_prompt_tokens (на каждый промпт, через budget_history и estimate_tokens). Возвращает ответ агента (DONE) или статус лимита.
Требования:
Ни один промпт, отправленный модели, не должен превышать
max_prompt_tokensтокенов (поestimate_tokens).Число обращений к модели — не больше
max_steps.Петля, инструменты, сжатие — переиспользуются из
loop.py,tools.py,history.py; копипаста кода между модулями — дисквалификация стиля (и моя, и ваша).
Как проверить себя
test_scavenger.py:
from pathlib import Path
from fake_llm import ScriptedLLM
from scavenger import find_answer
from tokens import estimate_tokens
def make_repo(tmp_path):
src = tmp_path / "src"
src.mkdir()
(src / "auth.py").write_text(
"def validate_email(email: str) -> bool:\n"
" return '@' in email and '.' in email.split('@')[-1]\n",
encoding="utf-8",
)
(src / "billing.py").write_text(
"def charge(amount: int) -> str:\n"
" if amount <= 0:\n"
" raise ValueError('amount must be positive')\n"
" return 'ok'\n",
encoding="utf-8",
)
return tmp_path
def test_finds_answer_within_budget(tmp_path):
root = make_repo(tmp_path)
llm = ScriptedLLM([
"ACT: search validate_email",
"DONE: src/auth.py, функция validate_email",
])
answer = find_answer(llm, root, "где валидируется email?", max_prompt_tokens=400)
assert answer == "src/auth.py, функция validate_email"
assert len(llm.prompts) <= 4
for prompt in llm.prompts:
assert estimate_tokens(prompt) <= 400, estimate_tokens(prompt)
def test_survives_huge_observation(tmp_path):
root = make_repo(tmp_path)
(root / "big.txt").write_text("шум " * 2000, encoding="utf-8")
llm = ScriptedLLM([
"ACT: read_file big.txt",
"DONE: нашёл не там, но уложился",
])
answer = find_answer(llm, root, "прочитай большой файл", max_prompt_tokens=300)
assert answer == "нашёл не там, но уложился"
for prompt in llm.prompts:
assert estimate_tokens(prompt) <= 300Второй тест — сердце задачи: наблюдение от big.txt (16000 токенов по оценке) обязано срезаться сжатием истории, чтобы второй промпт остался в бюджете. Если ваш find_answer просто копирует run_agent — тест это поймает.
Критерии готовности
Оба теста зелёные, на любом размере бюджета из тестов.
Код конвейера — композиция трёх модулей, без дублирования их логики.
Коммит:
scavenger.py,test_scavenger.py.
Проверьте на живой модели (необязательно)
Прогоните find_answer(LLMClient(), "путь-к-своему-репо", "где читается конфигурация?") с max_steps=4. Посмотрите в сохранённых промптах (добавьте их запись, как в ScriptedLLM), по какому пути модель шла. Если циклится на list_dir — это не код плохой, это промпт: подсказка «начинай с search по ключевому слову из вопроса» лечит за одну правку.
Петля агента: решить → сделать → посмотреть
5 мин
Домашка: агент с одним инструментом
3 мин
Квиз: Петля агента: решить → сделать → посмотреть
6 мин
Инструменты: руки агента
5 мин
Домашка: набор разведчика
3 мин
Квиз: Инструменты: руки агента
5 мин
Память и контекст: что тащим, что выбрасываем
5 мин
Домашка: сжатие истории
3 мин
Квиз: Память и контекст: что тащим, что выбрасываем
6 мин
Свой цикл против LangChain и LlamaIndex
4 мин
HARD-задача: прятки в репозитории
3 мин
Квиз: Свой цикл против LangChain и LlamaIndex
5 мин
Проект «Разведчик»: агент, который знает ваш репозиторий
3 мин