Домашка: агент с одним инструментом
Домашка
Сегодня в devbuddy появляется главное — loop.py, петля агента. Миниатюрная, с текстовым протоколом из урока, но настоящая: она сама решает, когда позвать инструмент, читает наблюдения и останавливается. Проверять будете сценариями ScriptedLLM — как в бою, только без сети и счета за токены.
Описание задачи
Файл loop.py.
Основная функциональность
build_prompt(task: str, history: list[str]) -> str— промпт одного шага: строкаЗадача: ..., затем (если история не пуста) блокНаблюдения:со строками- ..., в конце инструкция отвечать строго одной строкойACT: <инструмент> <аргумент>илиDONE: <ответ>.run_agent(llm, tools: dict, task: str, max_steps: int = 5) -> str:до
max_stepsраз спрашивает модель;DONE: <ответ>— вернуть<ответ>(без префикса, без краевых пробелов);ACT: <имя> <аргумент>— вызвать инструмент изtools; результат добавить в историю как<имя>(<аргумент>) -> <результат>;неизвестное имя — добавить в историю
ошибка: неизвестный инструмент <имя>и продолжать;строка без знакомого префикса —
ValueErrorс текстом ответа;лимит исчерпан — вернуть
ЛИМИТ ШАГОВ (.)
Дополнительные условия
Никаких обращений к сети; модель — любой объект с
ask(prompt) -> str.tools— словарьимя -> функция(аргумент_строкой) -> строка.
Как проверить себя
test_loop.py, запуск python -m pytest test_loop.py -v (класс ScriptedLLM положите в fake_llm.py рядом с FakeLLM):
import pytest
from fake_llm import ScriptedLLM
from loop import build_prompt, run_agent
def test_done_after_one_action():
llm = ScriptedLLM(["ACT: read_file secret.txt", "DONE: кекс"])
tools = {"read_file": lambda path: "пароль: кекс"}
assert run_agent(llm, tools, "найди пароль") == "кекс"
assert llm.replies == []
def test_observation_fed_back():
llm = ScriptedLLM(["ACT: read_file secret.txt", "DONE: кекс"])
tools = {"read_file": lambda path: "пароль: кекс"}
run_agent(llm, tools, "найди пароль")
assert "read_file(secret.txt) -> пароль: кекс" in llm.prompts[1]
def test_unknown_tool_is_observation():
llm = ScriptedLLM(["ACT: rm all", "DONE: ничего"])
result = run_agent(llm, {}, "уничтожь всё")
assert result == "ничего"
assert "неизвестный инструмент rm" in llm.prompts[1]
def test_step_limit():
llm = ScriptedLLM(["ACT: read_file a", "ACT: read_file b", "ACT: read_file c"])
tools = {"read_file": lambda p: "пусто"}
assert run_agent(llm, tools, "задача", max_steps=2) == "ЛИМИТ ШАГОВ (2)"
assert len(llm.prompts) == 2
def test_garbage_raises():
llm = ScriptedLLM(["мне кажется, тут нужно подумать..."])
with pytest.raises(ValueError):
run_agent(llm, {}, "задача")
def test_first_prompt_has_task_and_no_history():
llm = ScriptedLLM(["DONE: ок"])
run_agent(llm, {}, "найди пароль")
assert "Задача: найди пароль" in llm.prompts[0]
assert "Наблюдения:" not in llm.prompts[0]Пример работы программы
>>> from loop import run_agent
>>> from fake_llm import ScriptedLLM
>>> llm = ScriptedLLM(["ACT: read_file todo.txt", "DONE: купить кекс"])
>>> tools = {"read_file": lambda p: "купить кекс"}
>>> run_agent(llm, tools, "что в списке дел?")
'купить кекс'Критерии готовности
Шесть тестов зелёные.
Петля не делает ни одного
ask()сверх нужного (проверяетсяllm.prompts).Коммит:
loop.py,fake_llm.py(с обоими классами),test_loop.py.
Петля агента: решить → сделать → посмотреть
5 мин
Домашка: агент с одним инструментом
3 мин
Квиз: Петля агента: решить → сделать → посмотреть
6 мин
Инструменты: руки агента
5 мин
Домашка: набор разведчика
3 мин
Квиз: Инструменты: руки агента
5 мин
Память и контекст: что тащим, что выбрасываем
5 мин
Домашка: сжатие истории
3 мин
Квиз: Память и контекст: что тащим, что выбрасываем
6 мин
Свой цикл против LangChain и LlamaIndex
4 мин
HARD-задача: прятки в репозитории
3 мин
Квиз: Свой цикл против LangChain и LlamaIndex
5 мин
Проект «Разведчик»: агент, который знает ваш репозиторий
3 мин