Читать книгу LangChain и OpenSCAD: Практическая реализация AI-агента - - Страница 2
Глава 2. Основы построения AI‑метапрограмм
ОглавлениеВведение в создание AI‑агента в OpenSCAD через LangChain
OpenSCAD ― это скриптовый языкъ моделирования твердых тел, широко используемый в техническом проектировании и 3D‑печати. Нативное API OpenSCAD ориентировано на декларативное описание геометрии, но для взаимодействия с внешними данными, динамического выбора параметров и генерации сложных моделей часто требуется более гибкое управление. Именно здесь вступает в игру LangChain, позволяющая построить так называемый AI‑агент, который может автоматически генерировать код OpenSCAD, учитывая запросы пользователя, ограничения проектирования и результаты предыдущих шагов.
В этом разaking мы рассмотрим пошаговый процесс написания такого агента, опираясь на возможности библиотеки LangChain, а также покажем практико‑ориентированный формат кода, который можно сразу использовать в своих проектах.
1. Подготовка окружения
Для начала необходимо установить необходимые зависимости. На Python‑окружении рекомендуется создать виртуальное окружение и установить пакеты langchain‑core, langchain‑openai (или другой провайдер LLM‑модели), а также библиотеку python‑openSCAD, которая обеспечивает возможность генерировать и отправлять команды OpenSCAD из кода Python. Кроме того, потребуется доступ к LLM‑модели, например, GPT‑4 или аналог.
Команды установки выглядят так:
```
python -m venv venv
source venv/bin/activate # Linux/macOS
venv\Scripts\activate # Windows
pip install langchain-core langchain-openai python-openSCAD
```
После установки необходимо задать переменные окружения для доступа к LLM: OPENAI_API_KEY и другие параметры, связанные с выбранным провайдером.
2. Общая архитектура агента
AI‑агент в контексте LangChain представляет собой цепочку (pipeline) of LLM‑вызовов, правил и вспомогательных компонентов. Для OpenSCAD‑сценария типичная структура выглядит так:
‑ Входные данные – запрос пользователя, описывающий требуемую модель (например, «создайçõesлучайную спираль с радиусом от 5 до 15 мм и пяти estivesseтами»).
‑ Преобразователь запроса (prompt template) – переводит естественный язык в промпт, пригодный LLM.
‑ LLM‑вызов – запрашивает у модели генерацию кода OpenSCAD.
‑ Пост‑обработка – фильтрует полученный код: проверка синтаксиса, корректировка параметров, приведение к формату, совместимому с OpenSCAD.
‑ Вывод – передача готового кода в OpenSCAD или сохранение в файл.
LangChain предоставляет готовые классы для построения таких цепочек: PromptTemplate, LLMChain, SequentialChain и др. В нашем случае наиболее подходящим вариантом будет использование LLMChain с пользовательским обработчиком вывода (output parser), который преобразует строку кода в объект, который можно непосредственно выполнить.
3. Создание шаблона промпта
Промпт должен четко формулировать требования к коду: указать типы геометрических примитивов, их параметры, отношения между элементами, а также ограничения по размеру модели. Пример шаблона:
```
Ты – эксперт по OpenSCAD. Сгенерируй только корректный код OpenSCAD, соответствующий следующему описанию:
{user_request}
Обязательно включи модуль main() и заверши код оператором render()!\nКод:
```
Здесь {user_request} будет заменяться на реальный запрос пользователя. Подобный шаблон гарантирует, что модель будет выводить именно код, а не объяснительные тексты.
4. Настройка LLMChain
С помощью LLMChain мы свяжем промпт, модель и обработчик. Пример реализации:
```
from langchain import LLMChain
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4", temperature=0.2, max_tokens=1500)
prompt = PromptTemplate(
input_variables=["user_request"],
template=(
"Ты – эксперт по OpenSCAD. Сгенерируй только корректный код OpenSCAD, "
"соответствующий описанию:\n{user_request}\n"
"Обязательно включи модуль main() и заверши код оператором render()!\n"
"Код:"
)
)
chain = LLMChain(
llm=llm,
prompt=prompt,
output_parser=OpenSCADParser()
)
```
В данном примере `OpenSCADParser` – пользовательский класс, реализующий парсинг полученного текста в валидный набор команд. Он будет подробно рассмотрен ниже.
5. Реализация парсера кода OpenSCAD
Получаемый от LLM текст может содержать лишние префиксы, пояснительные строки и даже ошибки синтаксиса. Чтобы гарантировать корректную работу, нужен парсер, который:
‑ Отрезает все строки до первого появления слова `module` или `//=== START CODE ===//`.
‑ Удаляет комментарии в конце строк.
‑ Проверяет наличие обязательных блоков `module main()` и `render()`; при их отсутствии добавляет.
‑ Возвращает чистый код в виде строки.
Пример реализации простого парсера:
```
import re
class OpenSCADParser:
def parse(self, text: str) -> str:
# Удаляем всё до первого признака начала кода
match = re.search(r"(?s)(module\s+\w+\|?\s*\(.*?\)|//=== START CODE ===//)", text)
if not match:
raise ValueError("Не найден маркер начала кода")
start_idx = match.start(1) if match.lastgroup == "module" else match.start("START CODE")
code = text[start_idx:].strip()
# Удаляем комментарии в конце строк
lines = code.splitlines()
processed = []
for line in lines:
if "#" in line:
line = line[: line.index("#")].rstrip()
processed.append(line)
code = "\n".join(processed)
# Убеждаемся, что есть модуль main и render
if "module main()" not in code:
code += "\nmodule main() {\n}"
if "render()" not in code:
code += "\nrender()\n}"
# Финальная очистка лишних пустых строк
code = re.sub(r"\n{3,}", "\n\n", code).strip()
return code
```
Этот парсер достаточно прост, но в реальных проектах можно расширить его, добавив проверку соответствия синтаксису OpenSCAD через подпроцесс `openscad -c` и откат к повторному запросу модели при ошибке.
6. Интеграция с OpenSCAD
После получения чистого кода его можно выполнить в OpenSCAD через командную строку или через Python‑интерфейс. Если требуется дальнейшая обработка (например, экспорт STL), удобно создать функцию `execute_openSCAD(code: str) -> str`, которая:
1. Записывает код в временный файл `temp.scad`.
2. Вызывает OpenSCAD в режиме командной строки: `openscad -c temp.scad -o temp.stl`.
3. Возвращает путь к полученному STL‑файлу или содержимое, если нужен только 2D‑проект.
Пример реализации:
```
import os
import tempfile
import subprocess
def execute_openSCAD(code: str) -> str:
# Создаём временный файл
with tempfile.NamedTemporaryFile(mode="w", suffix=".scad", delete=False) as f:
f.write(code)
temp_path = f.name
try:
# Путь к исполняемому файлу OpenSCAD
openscad_path = "openscad" # Предполагаем, что он доступен в PATH
# Формируем команду для генерации STL
cmd = [openscad_path, "-c", temp_path, "-o", temp_path.replace(".scad", ".stl")]
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# Возвращаем путь к STL
stl_path = temp_path.replace(".scad", ".stl")
return stl_path
finally:
# Очистка временных файлов
os.remove(temp_path)
```
Такой подход позволяет полностью автоматизировать процесс генерации, проверки и экспорта модели без ручного вмешательства.
7. Обработка ошибок и рекурсивное уточнение
Несмотря на тщательный парсинг, генерация кода может иногда приводить к синтаксическим ошибкам, особенно при работе с сложными параметрами. LangChain предоставляет механизмы для повторных запросов к LLM. Один из вариантов – использовать `Retryable` в цепочке, или вручную отлавливать исключения парсера и отправлять в модель запрос на «исправьте ошибку: {error_message}» с сохранением прежних ограничений.
Пример функции обратного вызова:
```
def handle_error(error_msg: str) -> str:
return f"Исправь ошибку: {error_msg}. Сгенерируй корректный код, соблюдая исходные параметры."
# Внутри LLMChain можно использовать:
chain_with_retry = LLMChain(
llm=llm,
prompt=prompt,
output_parser=OpenSCADParser(),
verbose=True,
callbacks=[handle_error] # упрощённый пример
)
```
Таким образом, система получает возможность «само‑практиковаться», улучшая качество генерируемого кода.
8. Пример практического сценария
Допустим, пользователь хочет создать модуль «шестерню с радиусом 30 мм, 12 зубцами, толщиной 5 мм». Запрос передаётся агенту, который формирует промпт и запрашивает генерацию кода. Ниже полный пример взаимодействия:
```
user_request = "Создай шестиугольный блок с длиной 50 мм, шириной 30 мм и высотой 20 мм, а также вставь в центр отверстие диаметром 10 мм"
result = chain.run(user_request)
parsed_code = OpenSCADParser().parse(result)
stl_file = execute_openSCAD(parsed_code)
print(f"Файл STL сохранён в {stl_file}")
```
В результате получаем готовый STL‑файл, который можно импортировать в любую программу 3D‑визуализации или отправить на 3D‑печать.
9. Расширение функциональности
После базовой реализации можно добавить несколько типовых расширений:
‑ Параметрические модули: Позволить пользователю задавать функции‑модули, которые потом могут быть переиспользованы.