Кодинг-агент на Swift: устройство, цикл агента, управление контекстом | AiManual
AiManual Logo Ai / Manual.
18 Июн 2026 Гайд

Пишем кодинг-агента на Swift: внутреннее устройство, цикл агента и управление контекстом

Глубокий разбор создания AI-агента для кодинга на Swift: REPL-цикл, Agent Loop, управление контекстом, инструменты и типичные ошибки. Код и архитектура.

Реклама
partv1

Почему большинство агентов на Swift — мертворождённые?

Попытки написать своего кодинг-агента на Swift обычно заканчиваются одним из двух исходов: либо вы упираетесь в стены управления контекстом, либо ваш агент впадает в бесконечный цикл, переписывая один и тот же файл. Я перебрал десяток реализаций — от простых скриптов до полноценных обвязок вроде тех, что используют в Claude Code на Mac M3. И сегодня разберу, как выглядит внутреннее устройство минимально жизнеспособного агента на Swift.

Большинство туториалов учат просто дёргать API LLM и выводить результат. Но проблема не в вызове — проблема в цикле агента и управлении контекстом. Агент, который не умеет поддерживать сессию дольше одного запроса, бесполезен. Тот, кто не контролирует длину истории, быстро вылетает за лимит токенов. Я покажу, как строить REPL-цикл и Agent Loop, управлять инструментами и не сойти с ума от утечки контекста.

REPL-цикл: сердце интерактивного агента

REPL (Read-Eval-Print Loop) — классическая архитектура для диалогового агента. Пользователь вводит запрос, агент читает, обрабатывает LLM, выполняет инструменты, выводит результат — и снова ждёт ввода. Звучит просто, но дьявол в деталях.

// Минимальный REPL-цикл агента
import Foundation

actor Agent {
    let llm: LLMProvider
    var tools: [String: Tool]
    var history: [Message] = []
    let maxTokens = 8_000

    init(llm: LLMProvider, tools: [Tool]) {
        self.llm = llm
        self.tools = Dictionary(uniqueKeysWithValues: tools.map { ($0.name, $0) })
    }

    func start() async {
        print("🤖 Agent ready. Type 'exit' to quit.")
        while true {
            print("> ", terminator: "")
            guard let input = readLine(), input != "exit" else { break }
            history.append(.user(input))
            await process()
        }
    }

    private func process() async {
        // 1. Обрезаем контекст до maxTokens
        trimHistory()
        // 2. Запрашиваем LLM
        let response = await llm.generate(history: history, tools: Array(tools.keys))
        // 3. Парсим ответ — ожидаем JSON с action
        guard let action = parseAction(from: response) else {
            print("Failed to parse action")
            return
        }
        // 4. Выполняем инструмент
        if action.type == "final" {
            print(action.output)
            history.append(.assistant(action.output))
        } else if let tool = tools[action.toolName] {
            let result = await tool.call(with: action.arguments)
            history.append(.observation(result))
            // 5. Повторяем цикл — агент может делать несколько шагов
            await process()
        }
    }
}

Ключевая особенность: агент рекурсивно вызывает process(), пока не получит финальный ответ. Это и есть Agent Loop. Без такого цикла агент сможет выполнить только одно действие. В реальных задачах ему нужно прочитать файл, подумать, записать изменения, проверить — и только потом ответить.

⚠️ Важно: рекурсивный цикл надо ограничивать по глубине и времени, иначе уйдёте в бесконечность. Добавляйте счётчик итераций.

Инструменты: как агент взаимодействует с кодом

Агент без инструментов — это просто болтун. Чтобы он мог редактировать файлы, запускать тесты или выполнять команды shell, нужно определить протокол и реализовать конкретные инструменты.

protocol Tool {
    var name: String { get }
    var description: String { get }
    var parameters: [String: ParameterInfo] { get }
    func call(with arguments: [String: String]) async -> String
}

struct ReadFileTool: Tool {
    let name = "read_file"
    let description = "Read a file from the project"
    let parameters = ["path": .string(description: "Absolute or relative path")]

    func call(with arguments: [String: String]) async -> String {
        guard let path = arguments["path"] else { return "Error: missing path" }
        do {
            return try String(contentsOfFile: path, encoding: .utf8)
        } catch {
            return "Error: \(error.localizedDescription)"
        }
    }
}

Инструменты нужно регистрировать в агенте и передавать их описания LLM. Обычно в system prompt добавляют список инструментов с JSON-схемой. LLM выбирает, какой вызвать, и возвращает JSON с именем и аргументами. Этот парсинг — самое хрупкое место. Используйте Codable и жёсткую валидацию, иначе агент сломается на первой опечатке.

В статье QuillCode: разбор архитектуры своего кодинг-агента авторы как раз жалуются на то, что LLM часто генерирует некорректный JSON. Решение — использовать constrained decoding или хотя бы давать в prompt пример успешного разбора.

Управление контекстом: главный враг и союзник

Без контроля контекста агент сначала теряет из виду задачу, а потом просто перестаёт генерировать связные ответы. Лимиты моделей (Claude 3.5 Sonnet — 200K токенов, GPT-4 — 128K) кажутся огромными, но каждое наблюдение и результат работы инструмента занимают место. Через 5-10 шагов история может перевалить за 50K токенов.

Вот три техники, которые я использую в продакшене:

  1. Sliding window — храним только последние N сообщений (например, 20). Всё остальное отбрасываем или суммаризируем.
  2. Суммаризация — каждые K шагов просим LLM сжать историю в short-term memory. Эту технику детально разобрали в статье «Как не сжечь токены».
  3. Тримминг по токенам — перед отправкой запроса считаем токены и удаляем самые старые сообщения, пока не уложимся в лимит.
func trimHistory() {
    var totalTokens = history.reduce(0) { $0 + $1.tokenCount }
    while totalTokens > maxTokens, history.count > 1 {
        let removed = history.removeFirst()
        totalTokens -= removed.tokenCount
    }
}

❌ Ошибка: удалять только user-сообщения — ломает диалоговую структуру. Лучше удалять пары (user + assistant) или суммаризировать.

Более продвинутый подход — контекст-инжиниринг, о котором мы говорили в статье «Контекст-инжиниринг для coding-агентов». Там разобрано, как структурировать сессии, чтобы агент не «забывал» цель.

Типичные ошибки при сборке агента

ОшибкаПоследствиеРешение
Бесконечный циклРасход токенов, зависаниеMax итераций (например, 20)
Некорректный JSON от LLMПоломка пайплайнаПовторный запрос с исправлением
Переполнение контекстаПотеря понимания, вылетSliding window + суммаризация
Инъекции командУязвимости безопасностиСтрогий sandbox, белый список команд

Особенно опасен agentic loop, когда агент сам решает, что делать дальше. В архитектуре мультиагентных систем используют специальный harness, который контролирует жизненный цикл. Для одноагентной версии достаточно простого счётчика.

Собираем всё вместе: минимальный рабочий агент

Ниже полный код агента, который читает файл, пишет в файл и выполняет shell-команды. В production стоит добавить асинхронную отмену, логирование и более умный парсинг. Но для старта — сойдёт.

@main
struct CodingAgentApp {
    static func main() async {
        let llm = OpenAIProvider(apiKey: ProcessInfo.processInfo.environment["OPENAI_API_KEY"]!)
        let tools: [Tool] = [ReadFileTool(), WriteFileTool(), ShellTool()]
        let agent = Agent(llm: llm, tools: tools)
        await agent.start()
    }
}

Этот код запускает REPL-цикл. Агент может читать исходники, править их и запускать тесты. Разумеется, без контролируемого доступа к shell это опасно. Рекомендую запускать в Docker-контейнере или хотя бы ограничить права.

Важный момент: в Xcode 26.3 Apple уже встроила поддержку агентов (статья «Агентное программирование в Xcode 26.3»), но наша самописная обвязка даёт полный контроль — можно кастомизировать промпты, управлять контекстом и выбирать любую модель.

Неочевидный совет напоследок

Не пытайтесь сделать агента универсальным. Агент, который пытается делать всё — от рефакторинга Swift до деплоя в Kubernetes — будет жрать токены и тупить на каждом шагу. Заточите его под конкретную задачу: например, только для работы с SwiftUI-вьюхами или только для автоматического написания Unit-тестов. Тогда и контекст меньше, и поведение предсказуемее. Взгляните на подход GitHub Copilot Custom Agents — они как раз показывают, что узкая специализация побеждает универсальность.

И помните: агент — это не волшебная палочка. Это программа, которая принимает решения на основе LLM. А значит, ошибки неизбежны. Главное — сделать так, чтобы они не стоили вам бешеных счетов за API.

Подписаться на канал