Построение надёжных систем из ненадёжных агентов
Большие языковые модели можно применять для разных практических целей. Одно из самых интересных направлений — это автономные AI-агенты. Если сгенерировать большое количество агентов по заданному запросу и заставить их конкурировать друг с другом, то теоретически можно получить оптимальный результат по данной проблеме. Это можно использовать и в информационной безопасности, и в других сферах программной разработки.
Кроме того, можно создавать агентов, то есть софт, который самостоятельно эволюционирует и улучшает себя на базе обратной связи от пользователей.
Например, разработчики из компании Rainforest QA выстроили довольно чёткий процесс создания продуктов на основе LLM, которые справляются с присущей им ненадёжностью. Благодаря механикам использования AI-агентов эти программные системы самостоятельно вырабатывают свойство надёжности и стабильности.
Процесс укладывается в пять высокоуровневых шагов, причём первых четырёх достаточно для решения большинства проблем:
- Написать простые промты для решения проблемы
- Использовать этот опыт для создания системы оценки с целью изменения промтов и принципиального улучшения производительности
- Развернуть свою систему ИИ с хорошей наблюдаемостью, продолжать собирать примеры и улучшать свои оценки
- Внедрить систему генерации ответов с учётом дополнительно найденной релевантной информации (Retrieval Augmented Generation, RAG)
- Уточнить модель, используя данные, собранные на предыдущих этапах
Вспомогательный шаг:
- Задействовать вспомогательных (дополнительных) агентов
Всё это крайне итеративный процесс. Невозможно спроектировать и создать систему ИИ за одну попытку, пишут разработчики. Нельзя предсказать, что работает и как пользователи будут использовать инструмент, поэтому обязательно нужна обратная связь: «Вы создадите первую, неадекватную версию, будете использовать её, замечать недостатки, улучшать, расширять использование и т. д. А если добьётесь успеха и создадите что-то реально полезное, то результатом будет ещё больше итераций и улучшений продукта». То есть это практически бесконечный цикл разработки и совершенствования.
Плюс в том, что для использования этой техники не требуется большой опыт программирования LLM, она доступна практически всем. Хотя опыт работы с ML в наше время просто бесценен.
Авторы приводят примеры кода, который использовали в создании собственной системы автоматизированного тестирования.
Простые промты
Создаём простой промт и прогоняем его несколько раз для оценки результата:
Как видим, результаты ненадёжны и отличаются друг от друга.
На этом этапе AI-компонент можно интегрировать в свой продукт, чтобы отправлять запросы к LLM не через консоль, а через API. Для обработки ответов удобно использовать питоновскую библиотеку instructor.
Система оценки для изменения промтов
Речь идёт об итеративном улучшении промтов на основе измеримых критериев, где ключевыми фразами являются «итеративное» и «измеримые критерии».
На данном этапе создаётся цикл оценки, а затем он максимально ускоряется, чтобы эффективно выполнять итерации:
Подробнее о практическом опыте prompt engineering см. здесь, а это статья о системе оценки с быстрыми итерациями.
После установки цикла оценки можно создать проверочный набор из примеров входных данных и соответствующих выходных данных, которые мы хотим получить от агента.
При оценке различных стратегий внесения изменений в промты лучше всего постоянно использовать одну и ту же метрику, например, процент правильных ответов.
Наблюдение и обратная связь
К этому моменту у нас уже есть какая-то работающая альфа-версия продукта, которую можно развернуть в продакшне. Это нужно сделать как можно скорее, чтобы получить реальные данные и отзывы пользователей.
Без реальной обратной связи разработка может зайти в тупик, это абсолютно необходимый компонент для постоянного улучшения системы с течением времени.
Здесь есть множество вариантов: от известных систем мониторинга до опенсорсных библиотек вроде openllmetry и openinference, которые используют OpenTelemetry под капотом.
Система RAG
Когда мы исчерпали все возможности по изменению промтов, а производительность вышла на плато, можно встроить в конвейер систему генерации ответов с учётом дополнительно найденной релевантной информации (Retrieval Augmented Generation, RAG). Грубо говоря, это разработка промтов в реальном времени, когда мы динамически добавляем релевантный контент в промт перед тем, как попросить агента дать ответ.
В качестве примера можно привести ответы на вопросы о совсем недавних событиях. Несложно провести поиск и включить в промт несколько наиболее актуальных новостных статей, прежде чем просить ответ у LLM.
Другой пример — агент, взаимодействующий с UI приложения через простые промты на английском языке. На основе собственной собранной статистики можно давать агенту промты вроде «Похоже, что большинство людей-тестеров, выполняющих аналогичные задания, нажали на кнопку X, а затем ввели Y в поле Z».
Для извлечения данных на этом этапе можно использовать внешнего провайдера или собственное решение, или комбинацию из двух вариантов (например, эмбеддинги OpenAI с хранением векторов в своём инстансе Postgres.
Особо стоит отметить библиотеку RAGatouille, которая создана как раз для RAG, хотя заставить её работать не так просто. В упомянутой статье разработчики использовали BigQuery для получения данных, OpenAI для создания эмбеддингов и Pinecone для хранения и поиска векторов. Они пишут, что это самый простой способ развернуть систему, не создавая много новой инфраструктуры. Pinecone позволяет очень легко хранить и эмбеддинги и проводить их поиск со связанными метаданными для дополнения промтов.
Тюнинг модели
Это спорный шаг, без которого иногда можно (и нужно обойтись), потому что в реальности неаккуратные действия могут ухудшить качество модели.
При этом нерешённые проблемы: OpenAI позволяет настраивать только старые модели, а Anthropic вроде как обещает разрешить тюнинг в ближайшее время с кучей оговорок.
Примеры
Можно найти конкретные примеры применения такого подхода. Например, простая модель overkiLLM гарантирует получение надёжного результата (вывода) из массы ненадёжных входных данных (входов). Здесь не используются агенты, просто автору было интересно попробовать этот подход.
В качестве задачи он выбрал написание заголовка для веб-страницы H1
, но аналогичный подход подойдёт для любого короткого блока текста. Скрипт генерирует массу вариантов, а затем сводит их в голосовании «один на один» для выбора лучшего из пары. Всё выполняется локально/бесплатно на софте Ollama, которое удобно запускает различные LLM на своём сервере.
Скрипт работает по простому алгоритму:
- Вход: выбрать авторов (например, Стивена Кинга и др.).
- Направление: выбрать тему или фразу.
- Генерация: скрипт создаёт варианты в стиле авторов.
- Оценка: варианты соревнуются между собой, отсеивая слабые работы.
- Ранжирование: производится ранжирование вариантов по количеству побед и поражений.
OLLAMA_MODEL = 'knoopx/hermes-2-pro-mistral:7b-q8_0'
NUM_OF_VARIATIONS = 15
NUM_OF_MATCHES = 1000
import os
import random
import ollama
import uuid
import json
import pandas as pd
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)
pd.set_option('display.max_colwidth', 2000)
authors = [
{
"name": "Seth Godin",
"description": "An author, entrepreneur, and marketer, Godin writes about marketing, the spread of ideas, and managing both customers and employees with respect."
},
{
"name": "Paul Graham",
"description": ""
},
{
"name": "James Clear",
"description": "Author of \"Atomic Habits,\" Clear writes about habits, decision-making, and continuous improvement."
},
{
"name": "Derek Sivers",
"description": "An entrepreneur, author, and musician, Sivers writes about creativity, life philosophy, and the lessons he's learned from founding and selling CD Baby."
},
{
"name": "David Ogilvy",
"description": "Often referred to as the Father of Advertising, Ogilvy was known for his emphasis on research and consumer insights. His work for brands like Rolls-Royce and Hathaway shirts has become legendary."
},
{
"name": "Stephen King",
"description": "A prolific author of horror, suspense, and fantasy novels, King has written over 60 books and is known for his detailed character development and storytelling."
},
]
def parse_numbered_responses(input_str, tone):
if not input_str.strip():
raise ValueError("Input string is empty.")
lines = input_str.strip().split('\n')
parsed_responses = []
for line in lines:
try:
# Attempt to split each line at the first period followed by a space.
number, text = line.split('. ', 1)
number = int(number.strip()) # Convert number to integer.
text = text.strip() # Trim whitespace from text.
generated_uuid = uuid.uuid4()
parsed_responses.append({'number': number, 'text': text, 'tone': tone, 'uuid': generated_uuid})
except ValueError:
# Skip lines that do not conform to the expected format.
continue
if not parsed_responses:
raise ValueError("No valid numbered responses found in the input.")
return parsed_responses
def update_item_by_number(items, uuid, k):
"""Updates the text of an item in a list of dictionaries, identified by its number."""
for item in items:
if item['uuid'] == uuid:
if not item.get(k):
item[k] = 0
item[k] = item[k] + 1
return True
return False
def get_one_tone(h1, context, tone, n):
system_message = 'You are a helpful copywriting assistant. Only reply with a numbered list of variations of the text provided.'
user_prompt = f'''Context: {context}.\nPlease generate {n} variations of the following text written in the voice of {tone}.'''
user_prompt += f'''Do not mention {tone} in the text:\n{h1}'''
response = ollama.chat(model=OLLAMA_MODEL,
messages=[
{
'role': 'system',
'content': system_message,
},
{
'role': 'user',
'content': user_prompt,
},
],
options = {
'temperature': 1.5
}
)
parsed_responses = parse_numbered_responses(response['message']['content'], tone)
return parsed_responses
all_variations = []
n = NUM_OF_VARIATIONS
context = "I'm using this as the H1 for my website. Please write variations that are unique and engaging."
h1 = "Definite combines ETL, a data warehouse and BI in one modern platform."
for author in authors:
print(f"Generating variations for {author['name']}...")
tone = author['name']
parsed_responses = get_one_tone(h1, context, tone, n)
all_variations.extend(parsed_responses)
df = pd.DataFrame(all_variations)
print('Number of variations: ', len(df))
i = 0
while i < NUM_OF_MATCHES:
print('i:', i)
selected_items = random.sample(all_variations, 2)
system_message = 'You are a helpful copywriting assistant. Only reply with "AAA" or "BBB". Do not include any other text or explaination.'
user_prompt = f'''Please tell me which copy is more unique and engaging. Please reply in JSON format with the key "answer" and the value of your response. The only valid options for "answer" are "AAA" or "BBB". Do not include any other text or explaination.\n
AAA: {selected_items[0]['text']}\n\n
BBB: {selected_items[1]['text']}
'''
response = ollama.chat(model=OLLAMA_MODEL,
messages=[
{
'role': 'system',
'content': system_message,
},
{
'role': 'user',
'content': user_prompt,
},
],
format='json',
options = {
'temperature': 0.0
}
)
try:
j = json.loads(response['message']['content'])
if j['answer'] == 'AAA':
update_item_by_number(all_variations, selected_items[0]['uuid'], 'wins')
update_item_by_number(all_variations, selected_items[1]['uuid'], 'losses')
elif j['answer'] == 'BBB':
update_item_by_number(all_variations, selected_items[1]['uuid'], 'wins')
update_item_by_number(all_variations, selected_items[0]['uuid'], 'losses')
else:
print('Invalid response:', j)
except:
print('Invalid response:', response)
pass
i += 1
df = pd.DataFrame(all_variations)
df['wins'] = df['wins'].fillna(0).astype(int)
df['losses'] = df['losses'].fillna(0).astype(int)
df['total'] = df['wins'] + df['losses']
df['win_rate'] = df['wins'] / df['total']
df = df.sort_values('win_rate', ascending=False)
winner = df.iloc[0]
print('Author win rates: ', df.groupby('tone').win_rate.mean())
print('Top 20: ', df.head(20))
print('Winner: ', winner.text)
См. примеры с результатами работы.
Таким образом, современные методы машинного обучения можно использовать в различных областях IT, в том числе в информационной безопасности для построения надёжных и безопасных систем из ненадёжных компонентов.
Более того, эти методы позволяют создавать софт, способный самостоятельно принимать решения в сложных ситуациях (чатботы, агенты, симуляции и др.). С помощью инструментария вроде Burr софт запускается на своём сервере. Возможно, в будущем такие агенты можно будет использовать для задач информационной безопасности.