Агенты¶
Agent объявляет модель, системный промпт, инструменты и память. Цикл
использования инструментов, диспетчеризацию, учёт токенов и трейсинг берёт на
себя фреймворк.
from mlango.agents import Agent, BufferMemory, tool
@tool
def search_docs(query: str, limit: int = 5) -> list[str]:
"""Поиск по документации продукта.
Args:
query: Что искать.
limit: Максимум результатов.
"""
return retrieve(query, limit)
class Support(Agent):
"""Отвечает на вопросы о продукте по документации."""
class Meta:
model = "claude-opus-5"
system = "Ты инженер поддержки. Ссылайся на использованные разделы."
tools = [search_docs]
memory = BufferMemory(k=20)
max_steps = 8
effort = "high"
result = Support().run("Как перевыпустить API-ключ?")
result.output
result.tools_used
result.usage.total_tokens
result.trace_uuid
python manage.py agent support.Support # интерактивно
python manage.py agent support.Support "как мне..." # один вопрос
python manage.py agent support.Support "..." --show-steps
Опции Meta¶
| Опция | По умолчанию | Назначение |
|---|---|---|
model |
DEFAULT_AGENT_MODEL |
Идентификатор модели |
system |
"" |
Системный промпт |
tools |
[] |
Функции с @tool или экземпляры Tool |
provider |
DEFAULT_PROVIDER |
Ключ из настройки PROVIDERS |
memory |
NullMemory() |
Бэкенд памяти диалога |
max_steps |
AGENT_MAX_STEPS |
Жёсткий предел цикла |
max_tokens |
4096 |
Предел вывода на один вызов модели |
thinking |
"adaptive" |
Режим размышления; None убирает параметр |
effort |
не задан | low, medium, high, xhigh, max |
tracing |
TRACING |
Записывать спаны для этого агента |
Собрать промпт из полей самого агента — переопределите get_system():
class Support(Agent):
tone = fields.ChoiceField(["formal", "friendly"], default="friendly")
def get_system(self) -> str:
return f"Ты инженер поддержки. Пиши в тоне: {self.tone}."
Инструменты¶
Декоратор @tool читает аннотации типов и докстроку и строит JSON-схему, которая
нужна модели, — поэтому инструмент описан ровно в одном месте.
@tool
def set_status(ticket: str, status: Literal["open", "closed"]) -> str:
"""Изменить статус тикета.
Args:
ticket: Идентификатор тикета, должен начинаться на T-.
status: Новый статус.
"""
if not ticket.startswith("T-"):
raise ToolError("Идентификаторы тикетов начинаются на T-.")
return f"{ticket} -> {status}"
| Аннотация | Схема |
|---|---|
str, int, float, bool |
string, integer, number, boolean |
Literal["a", "b"] |
string с enum |
list[int] |
array из integer |
dict[str, int] |
object |
X \| None |
X, и не обязательный |
Подкласс Enum |
string с enum его значений |
| Параметр со значением по умолчанию | не обязательный, default записан |
Записи Args: в стиле Google становятся описаниями свойств, включая перенесённые
на несколько строк.
Ошибки — для модели, а не для вас¶
Инструмент, бросивший исключение, не роняет агента. Исключение становится результатом с ошибкой, который модель может прочитать и исправиться:
raise ToolError("Идентификаторы тикетов начинаются на T-.") # ваше сообщение
raise ValueError("boom") # станет "ValueError: boom"
Это намеренно: необработанное исключение посреди цикла теряет весь трейс, а модель обычно способна исправиться, если ей сказать, что пошло не так.
Строгий режим¶
Гарантирует, что аргументы точно соответствуют схеме.
Память¶
| Бэкенд | Хранит | Переживает перезапуск |
|---|---|---|
NullMemory() |
ничего — каждый ран с нуля (по умолчанию) | — |
BufferMemory(k=20) |
последние k сообщений, в процессе |
нет |
WindowMemory(k=20, keep_first=1) |
якорный ход плюс последние k |
нет |
MetastoreMemory(max_turns=20) |
восстанавливает из записанных трейсов | да |
MetastoreMemory не хранит ничего дополнительно: раз каждый вызов и так
трассируется, диалог восстанавливается из этих записей. Один источник истины,
поэтому просмотр трейсов в админке и память агента не могут разойтись.
Память ключуется по session_id:
agent.run("Меня зовут Ада", session_id="user-42")
agent.run("Как меня зовут?", session_id="user-42")
Провайдеры¶
| Провайдер | Примечания |
|---|---|
anthropic |
Claude. Нужен pip install "mlango[anthropic]" и учётные данные |
echo |
Детерминированный, офлайн. Без учётных данных и без затрат |
Провайдер echo — причина, по которой набор тестов фреймворка бесплатно
работает в CI, и по которой свежий проект работает до того, как у вас появится
API-ключ. Он следует простым правилам — use <tool> {json} вызывает инструмент —
чего достаточно, чтобы прогнать полный многошаговый цикл.
Переключение в настройках:
Параметры сэмплирования
Текущие модели Claude отклоняют temperature, top_p и top_k. Провайдер
отбрасывает их с предупреждением, а не даёт запросу упасть; глубина
управляется через effort.
Добавить провайдера — это один класс:
from mlango.agents.providers import Completion, Provider, ToolCall, Usage
class VLLMProvider(Provider):
name = "vllm"
requires = ("openai",)
def complete(self, *, model, messages, system="", tools=None,
max_tokens=4096, thinking=None, effort=None, **kw) -> Completion:
response = call_your_server(...)
return Completion(
text=response.text,
tool_calls=[ToolCall(id=c.id, name=c.name, arguments=c.args)
for c in response.calls],
stop_reason=Completion.TOOL_USE if response.calls else Completion.END_TURN,
usage=Usage(input_tokens=response.prompt_tokens,
output_tokens=response.completion_tokens),
)
Провайдер делает ровно одну вещь: превращает запрос в один ответ. Цикл, память и трейсинг остаются за фреймворком, поэтому смена провайдера никогда не меняет поведение агента.
Стриминг¶
run() возвращается только когда цикл закончен. Многошаговый агент может думать
минуту, а пустой экран в течение минуты читается как «сломалось» — поэтому
stream() отдаёт события по мере их появления:
from mlango.agents import Finished, TextChunk, ToolCalled
for event in Support().stream("Как перевыпустить API-ключ?"):
if isinstance(event, TextChunk):
print(event.text, end="", flush=True)
elif isinstance(event, ToolCalled):
print(f"\n[вызываю {event.name}]")
elif isinstance(event, Finished):
print(f"\n[{event.usage['total_tokens']} токенов, трейс {event.trace[:8]}]")
| Событие | Когда |
|---|---|
Started |
Один раз, перед первым вызовом модели |
Thinking |
Перед каждым вызовом модели, чтобы UI показывал прогресс во время ожидания |
TextChunk |
Текст ассистента |
ToolCalled |
Модель попросила инструмент, он вот-вот запустится |
ToolFinished |
Инструмент вернул результат или упал |
StepFinished |
Один проход цикла завершён, с учётом токенов |
Finished |
Последнее. Несёт тот же AgentRun, что возвращает run() |
Failed |
Цикл бросил исключение; оно последует |
stream() и run() используют один цикл, поэтому они не могут разойтись в
том, что сделал агент. У каждого события есть .kind (стабильное snake_case-имя)
и .describe(), возвращающий JSON-безопасный объект.
По HTTP¶
urlpatterns = [
path("chat/", Support.as_endpoint()),
path("chat/stream/", Support.as_stream_endpoint()),
]
Стриминговый эндпоинт говорит на text/event-stream, который браузеры понимают
нативно:
const response = await fetch("/api/chat/stream/", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({message: "Как перевыпустить API-ключ?"}),
});
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
while (true) {
const {value, done} = await reader.read();
if (done) break;
// строки "event: <kind>" и "data: <json>"
}
Трейсинг¶
Каждый вызов модели и каждый вызов инструмента становится упорядоченным спаном одного трейса, поэтому на вопрос «почему он это сказал?» можно ответить потом, из админки.
python manage.py traces list
python manage.py traces list --agent support.Support
python manage.py traces show a1b2c3d4 -v 2
from mlango.agents import get_trace, recent_traces
trace = get_trace("a1b2c3d4")
[(s.kind, s.name, s.duration_s) for s in trace.spans]
Трейсинг работает по принципу best effort: сбой метастора ухудшает наблюдаемость,
но никогда не ломает агента. Выключить для агента — Meta.tracing = False,
глобально — TRACING = False.
Цикл¶
На каждом шаге, пока модель не перестанет просить инструменты или пока не
исчерпан max_steps:
- Вызвать провайдера с сообщениями, системным промптом и схемами инструментов
- Записать спан
llmс расходом токенов - При
refusalостановиться и сообщить об этом - При
pause_turnпереотправить без изменений, чтобы серверный инструмент продолжил - Выполнить каждый запрошенный инструмент, каждый в своём спане
tool - Дописать все результаты инструментов одним пользовательским сообщением
- Повторить
Возврат результатов одним сообщением важен: разбиение учит модель перестать батчить свои вызовы.
Сигналы¶
from mlango.core.signals import agent_finished, agent_started, agent_step, tool_called
@receiver(tool_called)
def audit(sender, agent, tool, arguments, **kwargs):
log.info("%s вызвал %s с %s", sender._meta.label, tool.name, arguments)
Развёртывание¶
curl -X POST http://127.0.0.1:8000/api/chat/ \
-H 'Content-Type: application/json' \
-d '{"message": "Как перевыпустить API-ключ?", "session_id": "user-42"}'
Безопасность¶
Инструмент выполняется с правами вашего процесса. Тот, что запускает shell, пишет файлы или вызывает внутренний API, даёт модели такую же дотяжку. Валидируйте входы и ставьте всё разрушительное за явное подтверждение на вашей стороне, а не рассчитывайте на осторожность модели.