Desktop-приложения без API — классическая головная боль инженера по автоматизации. Legacy ERP, CAD-программы, банк-клиенты, 1С в режиме thick client. Selenium их не видит, REST API нет. Мы решаем эту задачу через AI-агента, который либо использует Computer Use (скриншот + управление мышью/клавиатурой), либо обращается к accessibility API (pywinauto, UI Automation) — более надёжный вариант для Windows-приложений. В этой статье разберём, как построить такого агента, какие подводные камни и сколько это экономит.
Почему AI-агент, а не RPA?
Классические RPA-системы (UiPath, Blue Prism) требуют жёсткой разметки интерфейса и ломаются при любом обновлении. AI-агент на базе LLM понимает UI на уровне семантики: ему не нужен заранее заданный селектор — он анализирует дерево элементов и принимает решение о действии. Это снижает стоимость поддержки на 40–60% на дистанции. Мы опираемся на стандарты Windows UI Automation API и документацию pywinauto.
Как мы строим такого агента?
В основе лежит связка pywinauto + Anthropic API. pywinauto обращается к Windows UI Automation API — та же технология, что использует экранный диктор. Элементы находятся по accessibility-атрибутам (AutomationId, Name, ControlType), а не по пикселям. Значительно надёжнее скриншотов.
from anthropic import Anthropic import pywinauto from pywinauto.application import Application from pywinauto.findwindows import ElementNotFoundError import json import subprocess import time client = Anthropic() class DesktopAppAgent: """AI-агент для автоматизации Windows desktop-приложений""" def __init__(self, app_path: str = None, app_title: str = None): self.app_path = app_path self.app_title = app_title self.app = None self.main_window = None def launch_or_connect(self): """Запускает приложение или подключается к запущенному""" try: if self.app_title: self.app = Application(backend="uia").connect(title_re=f".*{self.app_title}.*") elif self.app_path: self.app = Application(backend="uia").start(self.app_path) time.sleep(2) # ждём инициализации except pywinauto.findwindows.ElementNotFoundError: if self.app_path: self.app = Application(backend="uia").start(self.app_path) time.sleep(2) self.main_window = self.app.top_window() def get_ui_tree(self, max_depth: int = 4) -> dict: """Получает дерево UI-элементов""" def extract_element(element, depth=0): if depth > max_depth: return None try: info = { "name": element.window_text()[:100] if element.window_text() else "", "control_type": element.element_info.control_type, "automation_id": element.element_info.automation_id or "", "enabled": element.is_enabled(), "visible": element.is_visible(), "rect": str(element.rectangle()), } children = [] for child in element.children(): child_info = extract_element(child, depth + 1) if child_info and (child_info["visible"] or child_info["enabled"]): children.append(child_info) if children: info["children"] = children[:20] # не более 20 дочерних return info except Exception: return None return extract_element(self.main_window) def find_and_interact(self, instruction: str) -> str: """LLM определяет какой элемент нужен и что с ним делать""" ui_tree = self.get_ui_tree() response = client.messages.create( model="claude-sonnet-4-5", max_tokens=512, messages=[{ "role": "user", "content": f"""Проанализируй дерево UI и верни JSON с действием: {{ "action": "click|type|select|get_value|find", "automation_id": "ID элемента если есть", "name": "имя элемента", "control_type": "тип элемента", "value": "значение для type/select" }} Инструкция: {instruction} UI-дерево: {json.dumps(ui_tree, ensure_ascii=False)[:4000]} Только JSON.""" }], ) try: text = response.content[0].text action = json.loads(text[text.find("{"):text.rfind("}") + 1]) return self._execute_ui_action(action) except Exception as e: return f"Ошибка парсинга: {e}" def _execute_ui_action(self, action: dict) -> str: """Выполняет действие с UI-элементом""" try: # Ищем элемент по automation_id или имени element = None if action.get("automation_id"): element = self.main_window.child_window( auto_id=action["automation_id"] ) elif action.get("name"): element = self.main_window.child_window( title=action["name"], control_type=action.get("control_type"), ) if not element: return "Элемент не найден" act = action.get("action", "click") if act == "click": element.click_input() return f"Кликнул на {action.get('name', action.get('automation_id'))}" elif act == "type": element.set_edit_text(action.get("value", "")) return f"Ввёл: {action.get('value', '')}" elif act == "select": element.select(action.get("value", "")) return f"Выбрал: {action.get('value', '')}" elif act == "get_value": return element.window_text() or element.get_value() except ElementNotFoundError: return f"Элемент не найден: {action}" except Exception as e: return f"Ошибка: {type(e).__name__}: {e}" return "Действие выполнено" class DesktopWorkflowAgent: """Высокоуровневый агент для выполнения задач в desktop-приложении""" TOOLS = [ { "name": "get_ui_state", "description": "Получает текущее состояние UI-дерева приложения", "input_schema": {"type": "object", "properties": {}}, }, { "name": "interact_with_element", "description": "Взаимодействует с UI-элементом (клик, ввод текста, выбор)", "input_schema": { "type": "object", "properties": { "automation_id": {"type": "string"}, "action": {"type": "string", "enum": ["click", "type", "select", "get_value"]}, "value": {"type": "string"}, }, "required": ["action"], }, }, { "name": "wait", "description": "Ждёт изменения состояния приложения", "input_schema": { "type": "object", "properties": { "seconds": {"type": "number", "default": 1.0}, "wait_for_element": {"type": "string"}, }, }, }, { "name": "keyboard_shortcut", "description": "Нажимает комбинацию клавиш (Ctrl+S, Alt+F4, etc.)", "input_schema": { "type": "object", "properties": { "shortcut": {"type": "string", "description": "Например: Ctrl+S, Alt+Tab, F2"}, }, "required": ["shortcut"], }, }, ] def __init__(self, desktop_agent: DesktopAppAgent): self.agent = desktop_agent async def run(self, task: str) -> dict: messages = [{"role": "user", "content": task}] steps = 0 while steps < 40: response = client.messages.create( model="claude-sonnet-4-5", max_tokens=1024, system="Ты — агент автоматизации desktop-приложения. Используй инструменты последовательно.", tools=self.TOOLS, messages=messages, ) tool_results = [] done = False for block in response.content: if hasattr(block, "text") and block.text: if "выполнено" in block.text.lower() or "завершено" in block.text.lower(): done = True elif block.type == "tool_use": result = "" inp = block.input if block.name == "get_ui_state": result = json.dumps(self.agent.get_ui_tree(max_depth=3), ensure_ascii=False)[:3000] elif block.name == "interact_with_element": result = self.agent._execute_ui_action(inp) elif block.name == "wait": time.sleep(inp.get("seconds", 1.0)) result = "Waited" elif block.name == "keyboard_shortcut": import pyautogui keys = inp["shortcut"].replace("+", " ").split() pyautogui.hotkey(*[k.lower() for k in keys]) result = f"Pressed {inp['shortcut']}" tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": result, }) if done or response.stop_reason == "end_turn": return {"success": True, "steps": steps} messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": tool_results}) steps += 1 return {"success": False, "steps": steps} Какой подход выбрать: pywinauto vs Computer Use?
| Критерий | pywinauto + UI Automation | Computer Use (скриншот) |
|---|---|---|
| Надёжность | Высокая — работа с элементами по атрибутам | Средняя — зависит от разрешения экрана и контраста |
| Скорость | Быстро — без рендеринга изображений | Медленно — требуется генерация скриншота и его анализ |
| Поддержка изменений UI | Частичная — при обновлении automation_id может измениться | Высокая — не зависит от атрибутов |
| Поддерживаемые приложения | Windows (WPF, WinForms, 1C, SAP) | Любые с графическим интерфейсом |
| Доступ к сложным элементам (таблицы, гриды) | Есть через UI Automation pattern | Ограничен (только видимая область) |
Мы используем оба подхода: pywinauto как основной, Computer Use как fallback. Если приложение имеет доступную модель UI — предпочитаем pywinauto, иначе Computer Use.
Когда нужен Computer Use как fallback?
Computer Use выручает, когда приложение использует кастомную графику или не предоставляет accessibility-дерево. Например, старые CAD-системы или визуализаторы данных. В таких случаях агент делает скриншот области и передаёт его LLM для анализа. Однако это медленнее и менее надёжно: latency p99 выше, и модель может ошибиться в определении координат. Поэтому применяем Computer Use только там, где pywinauto бессилен.
Практический кейс: автоматизация 1С:Бухгалтерия из нашей практики
Задача: ежемесячное формирование 40 отчётов в 1С для 12 юрлиц. Процесс занимал 3 рабочих дня двух бухгалтеров.
Подход: pywinauto для 1С desktop-клиента (версия 8.3). UI Automation работает с 1С через COM-объекты и accessibility API.
Результаты:
- 40 отчётов × 12 юрлиц: 3 рабочих дня → 4 часа (ночной запуск)
- Ошибки ручного ввода: снизились до 0
- Сложность: 1С периодически меняет automation_id при обновлениях — пришлось добавить fallback-поиск по имени элемента
Ключевой момент при работе с 1С: приложение использует кастомный движок, не все элементы видны через стандартный UI Automation. Часть автоматизации реализована через COM-интерфейс 1С напрямую. Наш опыт показывает, что гибридная схема (pywinauto + COM) даёт лучший результат.
Что входит в работу?
- Анализ UI-дерева приложения — выявление доступных элементов, составление карты автоматизации.
- Разработка агента для конкретного workflow — реализация последовательности действий с fallback-логикой.
- Интеграция с LLM — выбор модели (Claude или GPT), настройка промптов для устойчивой генерации JSON-команд.
- Батчевая обработка + мониторинг — запуск по расписанию, отправка алертов об ошибках.
- Документация — описание архитектуры, инструкция по запуску, поддержка.
Ориентировочные сроки
| Этап | Длительность |
|---|---|
| Анализ UI-дерева + базовая автоматизация | 3–5 дней |
| Конкретный workflow (форма → обработка → результат) | 1–2 недели |
| 1С/SAP специфика (COM + pywinauto) | +1 неделя |
| Батчевая обработка + мониторинг | +1 неделя |
Мы гарантируем стабильность агента на протяжении всего срока эксплуатации — в случае обновления приложения корректируем селекторы и промпты. Работаем с desktop-автоматизацией более 5 лет, выполнено свыше 20 проектов для банков, ретейла и логистики.
Получите консультацию по вашему сценарию — оценим проект за 1–2 дня. Свяжитесь с нами, мы подскажем оптимальное решение под ключ.







