Главная / Статьи / Практические заметки: Я создал локального ИИ-агента с Ollama — и самая сложная часть

Практические заметки: Я создал локального ИИ-агента с Ollama — и самая сложная часть

Пошаговое руководство по практическим заметкам: я создал локального ИИ-агента с помощью Ollama — и самая сложная часть: контракты, проверки и места для вставки кода для команд, использующих эту модель.

2143 слов

В этом руководстве пошагово описывается путь от сырья до функционирующей системы для статьи «Я создал локального ИИ-агента с Ollama — и сложнее всего было не с моделью». Основное внимание уделяется практическим шагам, четкой проверке результатов и коду, который можно просто добавить в репозиторий без необходимости догадываться о его назначении. На этапе обзора необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед внесением изменений в код. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии системы. Лучше использовать небольшие, тестируемые единицы кода вместо обширных скриптов. При сбое шага причина должна быть связана с конкретной функцией, а не с запутанной цепочкой операций.

Почему вы выбрали Ollama

При работе над этапом «Почему вы выбрали Ollama» сначала запишите условия соглашения: необходимые входные данные, сигнал о успешном выполнении и что происходит при частичной неудаче. Такой список поможет сохранять честность при последующих изменениях кода. Рассматривайте этот этап как соглашение между входными данными и проверенными выходными результатами. Укажите названия файлов, определите критерии успешности и не допускайте молчаливого частичного выполнения задачи. Записывайте идентификатор запроса, идентификатор модели и время задержки при каждом вызове. Без такой отчетности периодические ошибки провайдера будут выглядеть как баги приложения.

ollama pull qwen3
pip install ollama
from ollama import chat
response = chat(
    model="qwen3",
    messages=[
        {"role": "user", "content": "Explain what an overdue invoice is."}
    ],
)print(response.message.content)

Чат-бот отвечает; агент принимает меры

При работе над этапом, на который отвечает чат-бот, сначала запишите условия работы: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает избегать ошибок при последующих изменениях кода. Рядом с функциональными результатами записывайте время выполнения, стоимость токенов или запросов. Отслеживание затрат с самого начала предотвращает неожиданные счета при переходе от демо-среды к общедоступным средам. Всегда фиксируйте идентификатор запроса, идентификатор модели и время задержки при каждом вызове. Без такой записи периодические ошибки поставщика могут показаться багами приложения.

Начните с простых инструментов

На этапе «Начните с ограниченного набора инструментов» сначала запишите условия работы: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Храните конфигурацию отдельно от кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь код. Записывайте ID запроса, ID модели и время задержки при каждом вызове. Без такой отчетности периодические ошибки поставщика могут выглядеть как баги приложения. На этапе «Начните с ограниченного набора инструментов» сначала запишите условия работы: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Воздерживайтесь от использования обширных скриптов в пользу небольших, тестируемых единиц. Если какой-то шаг не сработает, причина должна быть связана с конкретной функцией, а не с запутанной цепочкой операций.

CUSTOMERS = {
    "acme plumbing": {
        "customer_id": "cus_1042",
        "name": "Acme Plumbing",
        "email": "billing@example.com",
    }
}
INVOICES = [
    {
        "invoice_id": "INV-2048",
        "customer_id": "cus_1042",
        "amount": 1850.00,
        "days_overdue": 18,
    }
]
def find_customer(name: str) -> dict:
    customer = CUSTOMERS.get(name.strip().lower())
    return customer or {"error": "customer_not_found"}
def get_overdue_invoices(customer_id: str) -> dict:
    matches = [
        invoice
        for invoice in INVOICES
        if invoice["customer_id"] == customer_id
        and invoice["days_overdue"] > 0
    ]
    return {"invoices": matches, "count": len(matches)}

Предоставьте модели инструменты, а не вымышленный доступ

Этот этап предоставления инструментов модели работает наилучшим образом, если рассматривать его как измеримую основу. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работ. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия создаваемым элементам, определите критерии успеха и не соглашайтесь на молчаливое частичное выполнение задачи. Заблокируйте интерпретатор и файлы зависимостей перед тем, как научить модель работе в цикле. Различия между лаптопом и средой CI являются наиболее распространенной причиной скрытых сбоев в демонстрациях API.

import json
from ollama import chat
def find_customer(name: str) -> dict:
    """Find a customer by business name and return its verified record."""
    customer = CUSTOMERS.get(name.strip().lower())
    return customer or {"error": "customer_not_found"}
def get_overdue_invoices(customer_id: str) -> dict:
    """Return overdue invoices for a verified customer ID."""
    matches = [
        invoice
        for invoice in INVOICES
        if invoice["customer_id"] == customer_id
        and invoice["days_overdue"] > 0
    ]
    return {"invoices": matches, "count": len(matches)}
TOOLS = {
    "find_customer": find_customer,
    "get_overdue_invoices": get_overdue_invoices,
}

Создайте цикл агента

Этап создания цикла агента работает наилучшим образом, если рассматривать его как измеримую величину. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объёма работ. Записывайте временные показатели, а также стоимость токенов или запросов рядом с функциональными результатами. Отслеживание затрат на раннем этапе предотвращает неожиданные счёты при переходе от демо-среды к общедоступным средам. Закрепите интерпретатор и файлы блокировки зависимостей перед тем, как начинать обучение циклу. Различия в работе между ноутбуком и средой CI являются наиболее распространённой причиной скрытых сбоев в демонстрациях API.

SYSTEM_PROMPT = """
You are an invoice assistant.
Rules:
- Never invent a customer, invoice, email address, balance, or date.
- Use find_customer before requesting invoices.
- Only use customer IDs returned by tools.
- If a tool returns an error or no records, explain that clearly.
- You may draft communication, but you cannot send it.
"""
def run_agent(user_request: str) -> str:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_request},
    ]    for _ in range(6):
        response = chat(
            model="qwen3",
            messages=messages,
            tools=list(TOOLS.values()),
        )        messages.append(response.message)        if not response.message.tool_calls:
            return response.message.content        for call in response.message.tool_calls:
            name = call.function.name
            arguments = call.function.arguments            if name not in TOOLS:
                result = {"error": "tool_not_allowed"}
            else:
                try:
                    result = TOOLS[name](**arguments)
                except (TypeError, ValueError) as error:
                    result = {
                        "error": "invalid_tool_arguments",
                        "detail": str(error),
                    }            messages.append(
                {
                    "role": "tool",
                    "tool_name": name,
                    "content": json.dumps(result),
                }
            )    return "I stopped because the task exceeded the maximum number of steps."

Настоящее решение заключалось не в улучшении запроса

Настоящее решение заключается в том, что этапы работы работают лучше всего, когда их рассматривают как измеримые объекты. Сохраните один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объёма работ. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь код. Закрепите интерпретатор и файл с информацией о зависимостях до того, как начнёте использовать циклы. Различия между ноутбуком и средой CI являются наиболее распространённой причиной скрытых сбоев в демонстрациях API. Настоящее решение заключается в том, что этапы работы работают лучше всего, когда их рассматривают как измеримые объекты. Сохраните один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объёма работ. Предпочитайте небольшие, тестируемые единицы кода вместо обширных скриптов. Когда какой-то шаг сбивается, причина сбоя должна указывать на конкретную функцию, а не на запутанную цепочку операций.

Добавьте структурированный вывод на границе

Для добавления структурированного вывода на данном этапе необходимо заранее определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Рассматривайте этот этап как контракт между входными данными и проверенными результатами. Дайте названия элементам, определите критерии успеха и не допускайте безответственного частичного выполнения задачи. Отделите процесс создания клиента от цикла обработки сообщений, чтобы можно было заменять поставщиков без переписывания машины состояний диалога.

from pydantic import BaseModel, Field
class ReminderReview(BaseModel):
    customer_name: str
    invoice_ids: list[str]
    total_due: float = Field(ge=0)
    draft_subject: str
    draft_body: str
    requires_approval: bool = True
review_response = chat(
    model="qwen3",
    messages=messages,
    format=ReminderReview.model_json_schema(),
)
review = ReminderReview.model_validate_json(
    review_response.message.content
)

Состояние и память — это разные вещи

Для государственных систем память является этапом выполнения процесса; перед изменением кода необходимо определить входные данные, ответственного за данный этап и критерии завершения. Операторы должны иметь возможность перезапустить этап с известной точки контроля, не догадываясь о скрытом состоянии системы. Рядом с функциональными результатами следует записывать время выполнения операций, а также стоимость токенов или запросов. Отображение затрат с самого начала помогает избежать неожиданных расходов при переходе с демо-среды в общедоступные среды. Необходимо разделить процесс создания клиента от цикла обработки сообщений, чтобы можно было заменять поставщиков без переписывания машины состояний диалога.

task_state = {
    "customer_id": "cus_1042",
    "verified_invoice_ids": ["INV-2048"],
    "approved_actions": [],
}

Локальное решение не всегда означает безопасность

Поскольку For the Local не автоматически настраивает среду выполнения, перед изменением кода необходимо определить входные данные, ответственного за шаг и критерии завершения. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь граф процессов. Разделяйте создание клиента и цикл обработки сообщений, чтобы можно было заменять поставщиков без переписывания машины состояний разговора. Поскольку For the Local не автоматически настраивает среду выполнения, перед изменением кода необходимо определить входные данные, ответственного за шаг и критерии завершения. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Предпочитайте небольшие, тестируемые единицы кода большим скриптам. Когда шаг терпит неудачу, причина должна указывать на конкретную ответственность, а не на целый комплекс факторов.

Система с использованием специально настроенных каналов передачи данных.

Как тестировать агента

При работе над этим этапом сначала запишите условия работы: необходимые входные данные, сигнал о успешном выполнении и последствия частичной неудачи. Такой список поможет избежать ошибок при последующих изменениях кода. Рассматривайте этот этап как договор между входными данными и проверенными результатами. Дайте названия создаваемым объектам, определите критерии успешности и не допускайте молчаливого частичного выполнения задачи. Записывайте идентификатор запроса, идентификатор модели и время задержки при каждом вызове. Без такой отчетности случайные ошибки поставщика будут выглядеть как баги приложения.

Как выглядела рабочая версия

На этапе разработки рабочей версии сначала запишите условия работы: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Рядом с функциональными результатами отметьте время выполнения и стоимость токенов или запросов. Отслеживание затрат с самого начала предотвращает неожиданные счета при переходе от демо-среды к общедоступным средам. Всегда фиксируйте идентификатор запроса, идентификатор модели и время задержки при каждом вызове. Без такой записи периодические ошибки поставщика могут показаться багами приложения.

User request
  → find_customer(name="Acme Plumbing")
  → verified customer_id: cus_1042
  → get_overdue_invoices(customer_id="cus_1042")
  → verified invoice: INV-2048, $1,850, 18 days overdue
  → generate draft
  → wait for human approval

Заключительный урок

При работе над заключительным этапом урока сначала запишите условия взаимодействия: необходимые параметры входных данных, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Храните конфигурацию отдельно от кода приложения. Файлы с настройками окружения, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь код. Записывайте ID запроса, ID модели и время задержки при каждом вызове. Без такой отчетности периодические ошибки поставщика могут выглядеть как баги приложения. При работе над заключительным этапом урока сначала запишите условия взаимодействия: необходимые параметры входных данных, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Воздерживайтесь от использования обширных скриптов в пользу небольших, тестируемых единиц кода. Если какой-то шаг не сработает, ошибка должна указывать на конкретную ответственность, а не на запутанную цепочку операций.

Чек-лист для эксплуатации

Этап операционного чек-листа работает наилучшим образом, когда его рассматривают как измеримую основу. Соберите один идеальный пример выполнения, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объема работ.

Задокументируйте одновременно успешный сценарий работы и сценарий восстановления. Повторные попытки, проверки со стороны оператора и обработка некорректных сообщений являются частью продукта, а не последующими доработками.

Заблокируйте версии интерпретатора и файлы зависимостей до того, как начнете использовать циклы. Различия между ноутбуком и средой CI являются наиболее распространенной причиной незаметных сбоев при демонстрации API.

При следующем шаге, связанном с написанием кода или вызовом инструмента, предпочитайте структурированные выводы с проверкой по схеме вместо свободного текста.

Выполняйте контрольные точки после дорогостоящих операций. Система возобновления работы не должна повторно оплачивать тот же вызов большой языковой модели, когда оператор пытается выполнить следующий этап.

Заблокируйте версии зависимостей и запишите хэш изображения, с использованием которого выполнялась демонстрация. Воспроизводимость важнее коллективных знаний.

Перед внедрением данной стек-технологии необходимо заморозить версии, сгенерировать эталонный отчет для критической части работы и уточнить шаги возврата к предыдущему состоянию. В совместных средах требуются ограничения на скорость работы, проверки принадлежности ресурсов и четко определенный ответственный за обновление секретов. Лучше выбирать надежность, даже если она кажется менее привлекательной, чем красивые одноразовые демонстрации.

Примечание для a5f763eecd03: не храните ключи поставщика в репозитории, установите лимит токенов на одну сессию и сохраняйте отчеты рядом с фикстчерами для оценки, чтобы последующие замены моделей можно было сравнивать.