После успешного создания ReAct агента с гидом я продолжаю делать более сложную вещь — Генеративного агента. Это исследование было опубликовано командой Стэнфорда и Google на Arxiv 7 апреля 2023 года. Они пытаются создать небольшую деревню, полную генеративных агентов. Генеративные агенты контролируются LLM (например, ChatGPT). Это The Sims, где NPC могут вести себя как люди. Используя LLM, агенты могут думать, составлять план и принимать решения, а также взаимодействовать с другими.

Существует несколько реализаций этого исследования, но я не могу найти хорошую, которая может работать на локальных LLM с полным набором функций. Langchain также предоставляет реализации ноутбука. Кажется, он отлично работает с ChatOpenAI, но я не могу правильно запустить его с моей локальной моделью Winzard-Vicuna. Основная проблема та же, что и в моем предыдущем посте, где модель иногда не соответствует шаблону. Кроме того, в реализации по-прежнему отсутствуют некоторые функции: составление планов, нормализация оценок поиска и создание полного агента сводки. Поэтому я решил реализовать его самостоятельно.

Обзор

Моя цель — максимально приблизить реализацию к статье. Тем не менее, возможно, есть несколько частей, которые необходимо улучшить. Надеюсь, моя реализация поможет тем, кто хочет поиграть с генеративными агентами. Код легко подключить к виртуальным средам для игры.

Код можно найти в моем GitHub Repository. Я использовал Guidance для создания подсказок, в то время как поток памяти основан на Langchain Time Weighted VectoreStore. Код основан на Langchain Generative Agent. У моего агента есть все черты оригинальной бумаги:

  • Память и поиск
  • Отражение
  • Планирование (нужно улучшить)
  • Реагирование и перепланирование
  • Генерация диалогов (нужно улучшить)
  • Сводка агента
  • Интервью

Итак, выделяют две основные части генеративных агентов: память и логические компоненты (Планирование, Рефлексия, Реагирование и т. д.).

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

Document(page_content='Sam is a Ph.D student', metadata={'importance': 7, 'created_at': datetime.datetime(2023, 6, 1, 21, 37, 5, 793909), 'last_accessed_at': datetime.datetime(2023, 6, 1, 21, 40, 24, 610502), 'buffer_idx': 0})

Чтобы сделать поток памяти, я реализую TimeWeightedVectorStoreRetrieverModified на основе VectorStoreRetriever из Langchain.

Логические компоненты получают полезную информацию из памяти для работы. Мы извлекаем соответствующие воспоминания методом get_relevant_documents.

def get_relevant_documents(self, query: str, current_time: Optional[Any]) -> List[Document]:
        """Return documents that are relevant to the query."""
        if current_time is None:            
            current_time = datetime.datetime.now()
        docs_and_scores = {
            doc.metadata["buffer_idx"]: (doc, self.default_salience)
            for doc in self.memory_stream[-self.k :]
        }
        # If a doc is considered salient, update the salience score
        docs_and_scores.update(self.get_salient_docs(query))
        rescored_docs = [
            (doc, self._get_combined_score_list(doc, relevance, current_time))
            for doc, relevance in docs_and_scores.values()
        ]
        
        score_array = [b for a,b in rescored_docs]
        score_array_np = np.array(score_array)
        delta_np = score_array_np.max(axis=0)-score_array_np.min(axis=0)
        delta_np = np.where(delta_np == 0, 1, delta_np)
        x_norm = (score_array_np-score_array_np.min(axis=0))/delta_np
        # Weight time and importance score less
        x_norm[:,0] = x_norm[:,0]*0.15
        x_norm[:,1] = x_norm[:,1]*0.15
        x_norm_sum = x_norm.sum(axis=1)
        rescored_docs = [
            (doc, score)
            for (doc, _), score in zip(rescored_docs,x_norm_sum)
        ]                                                     
        
        rescored_docs.sort(key=lambda x: x[1], reverse=True)
        result = []
        # Ensure frequently accessed memories aren't forgotten
        for doc, _ in rescored_docs[: self.k]:
            # TODO: Update vector store doc once `update` method is exposed.
            buffered_doc = self.memory_stream[doc.metadata["buffer_idx"]]
            buffered_doc.metadata["last_accessed_at"] = current_time
            result.append(buffered_doc)
        return result

Он получает соответствующие документы (воспоминания) с query вself.get_salient_docs(query). Затем отсортируйте их по баллам поиска. В моей реализации я использую меньшие веса оценки новизны и важности, чем оценка релевантности (не стесняйтесь ее менять):

# Weight time and importance score less
  x_norm[:,0] = x_norm[:,0]*0.15
  x_norm[:,1] = x_norm[:,1]*0.15

Другими логическими компонентами являются «Планирование», «Размышление», «Сводка», «Реакция», «Перепланирование» и «Интервью». Я стараюсь следовать оригинальной инструкции для создания этих деталей. Например, сводная часть агента:

def get_summary(self, force_refresh=False, now=None):
    current_time = self.get_current_time() if now is None else now
    since_refresh = (current_time - self.last_refreshed).seconds

    if (
        not self.summary
        or since_refresh >= self.summary_refresh_seconds
        or force_refresh
    ):
        core_characteristics = self._run_characteristics()
        daily_occupation = self._run_occupation()
        feeling = self._run_feeling()

        description = core_characteristics + '. ' + daily_occupation + '. ' + feeling            
        self.summary = (f"Name: {self.name} (age: {self.age})" + f"\nSummary: {description}")
        self.last_refreshed = current_time            
    return self.summary

Следуя Приложению A, я задаю три вопроса: «основные характеристики [имя]», «текущее ежедневное занятие [имя]» и «чувство [имя] о своем недавнем прогрессе в жизни». Затем обобщите эту информацию, чтобы дать резюме агента. Вот один из основных характеристик (с использованием руководства):

PROMPT_CHARACTERISTICS = """### Instruction:
{{statements}}

### Input:
How would one describe {{name}}’s core characteristics given the following statements?

### Response:
Based on the given statements, {{gen 'res' stop='\\n'}}"""
def _run_characteristics(self,):
        docs = self.retriever.get_relevant_documents(self.name + "'s core characteristics", self.get_current_time())
        statements = get_text_from_docs(docs, include_time = False)

        prompt = self.guidance(PROMPT_CHARACTERISTICS, silent=self.silent)
        result = prompt(statements=statements, name=self.name)
        return result['res']

Давайте начнем

Хорошо, давайте сделаем генеративного агента — Сэма. Мы можем определить некоторые начальные воспоминания для него, чтобы начать с:

description = "Sam is a Ph.D student, his major is CS;Sam likes computer;Sam lives with his friend, Bob;Sam's farther is a doctor;Sam has a dog, named Max"
sam = GenerativeAgent(guidance=guidance, 
                      name='Sam',
                      age=23, 
                      des=description, 
                      trails='funny, like football, play CSGO', 
                      embeddings_model=embeddings_model)

Согласно описанию, Сэм является доктором философии. студент, который живет со своим другом Бобом. У него есть собака — Макс. Давайте установим часы на 7:25 и добавим больше воспоминаний.

now = datetime.now()
new_time = now.replace(hour=7, minute=25)
sam.set_current_time(new_time)

sam_observations = [
    "Sam wake up in the morning",
    "Sam feels tired because of playing games",
    "Sam has a assignment of AI course",
    "Sam see Max is sick",
    "Bob say hello to Sam",
    "Bob leave the room",
    "Sam say goodbye to Bob",
]

sam.add_memories(sam_observations)

Как видите, для каждого воспоминания мы оцениваем его значение важности и сохраняем его в потоке памяти. Хорошо, теперь подведем итоги Сэма:

summary = sam.get_summary(force_refresh=True)
print(summary)

Это выглядит хорошо. Давайте проверим, как он составляет план на сегодняшний вечер.

new_time = now.replace(hour=18, minute=15)
sam.set_current_time(new_time)

status = sam.update_status()

Это неплохой план для студента ИТ на вечер пятницы. Теперь узнайте, как он реагирует на наблюдение, напоминающее ему о его больной собаке:

bool_react, reaction, context = sam.react(observation='The dog bowl is empty', 
                                          observed_entity='Dog bowl', 
                                          entity_status='The dog bowl is empty')

Увидев пустую собачью миску, его реакция разумна. А еще Сэм меняет свой план на:

Далее мы позволим Сэму взаимодействовать с другими агентами. Итак, определяем его друга Боба:

description = "Bob is a Ph.D student; His major is Art; He likes reading; He lives with his friend, Sam; He likes playing Minecraft"
bob = GenerativeAgent(guidance=guidance, 
                      name='Bob', 
                      age=23, 
                      des=description, 
                      trails='like pizza, frinedly, lazy', 
                      embeddings_model=embeddings_model)

Теперь ситуация такова, что Сэм видит, как Боб возвращается с новым компьютером, и Боб пытается настроить свой новый компьютер:

bool_react, reaction, context = sam.react(observation='Bob come room with a new PC', 
                                          observed_entity=bob,
                                          entity_status='Bob is setting up his new PC')

О, Сэм решает поговорить с Бобом о его новом компьютере. Он также меняет свой план. Проверим диалог:

Теперь добавим больше воспоминаний и зададим Сэму несколько вопросов.

sam_observations = [
    "Sam call his farther to ask about work",
    "Sam try to finish his assignment",
    "Sam remember his friend ask him for playing game",
    "Sam wake up, feel tired",
    "Sam have breakfast with Bob",
    "Sam look for a intership",
    "The big concert will take place in his school today",    
    "Sam's bike is broken",
    "Sam go to school quickly",
    "Sam find a good internship",
    "Sam make a plan to prepare his father birthday"
]
sam.add_memories(sam_observations)
summary = sam.get_summary(force_refresh=True)
status = sam.update_status()

Хорошо, первый вопрос:

response = sam.intereview('Friend', 'Give an introduction of yourself.')

Как насчет его друга?

response = sam.intereview('Friend', 'Who do you live with?')

И, наконец, его собака — Макс:

response = sam.intereview('Friend', 'Who is Max?')

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