Generative agents 代码分析 Reflection 反思

146 阅读10分钟

反思(reflect)是人格认知模块的核心部分,负责生成焦点、洞察和证据,并根据这些信息运行反思。它通过与 GPT 模型的交互来生成思想,并将这些思想保存到人格的记忆中,以便后续使用。

反思实现思路

反思的基本逻辑:

  1. 设置一个最大感知阈值,越大表示反思越不积极;
  2. 设置一个当前反思值;
  3. 当有感知 event 发生时,会调用评估模型给 event 打分;
  4. 当前反思值 - event 得分 <= 0 时,触发反思。

反思主线流程

我们看代码实现:

def reflect(persona):
  """
  触发反思:根据 reflection_trigger 的返回值判断是否满足反思条件。
  执行反思:如果满足条件,则调用 run_reflect 函数生成反思内容,并调用 reset_reflection_counter 重置相关计数器。
  处理对话结束后的规划和备忘:当对话结束时间接近当前时间时,生成与对话相关的规划思想和备忘录,并将其添加到人格的记忆中。

  触发条件:
  1. persona.scratch.importance_trigger_curr(当前重要性计数器)小于等于 0。
  2. 且人格的记忆中存在事件或思想(persona.a_mem.seq_event + persona.a_mem.seq_thought 非空)。
  """
  if reflection_trigger(persona): 
    run_reflect(persona)
    reset_reflection_counter(persona)

  # 检查人格是否处于对话结束状态(chatting_end_time 不为 None)。
  # 如果当前时间加上 10 秒等于对话结束时间,则构建对话内容字符串 all_utt,用于后续生成规划思想和备忘录。
  if persona.scratch.chatting_end_time: 
    if persona.scratch.curr_time + datetime.timedelta(0,10) == persona.scratch.chatting_end_time: 
      all_utt = ""
      if persona.scratch.chat: 
        for row in persona.scratch.chat:  
          all_utt += f"{row[0]}: {row[1]}\n"

      # 生成规划思维
      ## 1. 获取对话的证据节点 ID(node_id),用于记录该思想的来源。
      evidence = [persona.a_mem.get_last_chat(persona.scratch.chatting_with).node_id]
      ## 2. 调用模型生成规划思想。并重新组织内容格式
      planning_thought = generate_planning_thought_on_convo(persona, all_utt)
      planning_thought = f"For {persona.scratch.name}'s planning: {planning_thought}"

      ## 3. 计算思想的创建时间、过期时间、事件三元组(s, p, o)主谓宾、关键词集合。
      created = persona.scratch.curr_time
      expiration = persona.scratch.curr_time + datetime.timedelta(days=30)
      s, p, o = generate_action_event_triple(planning_thought, persona)
      keywords = set([s, p, o])
      ## 4. 重要性评分和嵌入向量
      thought_poignancy = generate_poig_score(persona, "thought", planning_thought)
      thought_embedding_pair = (planning_thought, get_embedding(planning_thought))
      ## 5. 将生成的思想添加到记忆中。
      persona.a_mem.add_thought(created, expiration, s, p, o, 
                                planning_thought, keywords, thought_poignancy, 
                                thought_embedding_pair, evidence)


      # 生成备忘录
      ## 1. 调用模型生成备忘录,并格式化内容
      memo_thought = generate_memo_on_convo(persona, all_utt)
      memo_thought = f"{persona.scratch.name} {memo_thought}"

      ## 2. 计算备忘录的创建时间、过期时间、事件三元组(s, p, o)、关键词集合。
      created = persona.scratch.curr_time
      expiration = persona.scratch.curr_time + datetime.timedelta(days=30)
      s, p, o = generate_action_event_triple(memo_thought, persona)
      keywords = set([s, p, o])
      ## 3. 调用模型完成重要性评分和嵌入向量。
      thought_poignancy = generate_poig_score(persona, "thought", memo_thought)
      thought_embedding_pair = (memo_thought, get_embedding(memo_thought))

      ## 4. 将生成的备忘录添加到记忆中。
      persona.a_mem.add_thought(created, expiration, s, p, o, 
                                memo_thought, keywords, thought_poignancy, 
                                thought_embedding_pair, evidence)

关键变量说明:

  1. persona.scratch.importance_trigger_curr:
    整数,当前剩余的“重要性点数”,用于判断是否触发反思。每当一个事件或思想被认为具有重要性时,这个值会减少。在感知时触发 perceive.perceive
    当该值 ≤ 0 时,表示已经积累到足够的“重要性”事件,应触发反思。
  2. persona.scratch.importance_trigger_max:
    整数,最大允许的重要性点数,也就是触发一次反思后重置的值。这个值决定了人格的“反思频率”——数值越大,越不容易触发反思
  3. persona.a_mem.seq_event + persona.a_mem.seq_thought:
    列表,人格记忆中的事件和思想列表。确保记忆中存在事件或思想,避免在空记忆时触发反思

重要性值评估

persona.scratch.importance_trigger_curr减少的逻辑:

def perceive(persona, maze): 
  // 调用模型评估重要性
  event_poignancy = generate_poig_score(persona, 
                                            "event", 
                                            desc_embedding_in)
  // 减少比重
  persona.scratch.importance_trigger_curr -= event_poignancy

评估事件重要性:

def generate_poig_score(persona, event_type, description): 
  # event 和 thought 
  if event_type == "event" or event_type == "thought": 
    return run_gpt_prompt_event_poignancy(persona, description)[0]
  # chat 评估
  elif event_type == "chat": 
    return run_gpt_prompt_chat_poignancy(persona, 
                                         persona.scratch.act_description)[0]

聊天事件重要性评估,调用 GPT 模型评估分数,范围在 1-10 分:

def run_gpt_prompt_chat_poignancy(persona, event_description, test_input=None, verbose=False): 
  # 1. 模型请求参数
  gpt_param = {"engine": "text-davinci-002", "max_tokens": 15, 
               "temperature": 0, "top_p": 1, "stream": False,
               "frequency_penalty": 0, "presence_penalty": 0, "stop": None}
  # 2. 提示词模板
  prompt_template = "persona/prompt_template/v3_ChatGPT/poignancy_chat_v1.txt" ########
  # 3. 生成最终提示词 prompt
  prompt_input = create_prompt_input(persona, event_description)  ########
  prompt = generate_prompt(prompt_input, prompt_template)
  # 4. 设置模型输出格式
  example_output = "5" ########
  special_instruction = "The output should ONLY contain ONE integer value on the scale of 1 to 10." ########
  # 5. 调用模型输出评分结果
  output = ChatGPT_safe_generate_response(prompt, example_output, special_instruction, 3, fail_safe,
                                          __chat_func_validate, __chat_func_clean_up, True)
  if output != False: 
    return output, [output, prompt, gpt_param, prompt_input, fail_safe]

举个例子:

# 加入调用参数如下
persona.scratch.name = "Isabella Rodriguez"
persona.scratch.get_str_iss() = "Maria Lopez just asked her to meet at the park."
event_description = "Isabella Rodriguez: I can meet you at the park in 10 minutes. Maria Lopez: Great, I'll be there."

# 3 生成提示词
Isabella Rodriguez
Maria Lopez just asked her to meet at the park.
Isabella Rodriguez
Isabella Rodriguez: I can meet you at the park in 10 minutes. Maria Lopez: Great, I'll be there.

On a scale of 1 to 10, how important is this chat to Isabella Rodriguez's planning and decision-making?

The output should ONLY contain ONE integer value on the scale of 1 to 10.

# 5 调用模型输出结果,7 分
(7, [7, prompt, gpt_param, prompt_input, 4])

重要性评估模板

poignancy_chat_v1.txt

!<INPUT 1>!: agent name
!<INPUT 1>!: iss
!<INPUT 2>!: name 
!<INPUT 3>!: event description

<commentblockmarker>###</commentblockmarker>
Here is a brief description of !<INPUT 0>!. 
!<INPUT 1>!

On the scale of 1 to 10, where 1 is purely mundane (e.g., routine morning greetings) and 10 is extremely poignant (e.g., a conversation about breaking up, a fight), rate the likely poignancy of the following conversation for !<INPUT 2>!.

Conversation: 
!<INPUT 3>!

Rate (return a number between 1 to 10):
变量名描述示例值
!<INPUT 0>!当前时间戳或上下文描述(用于提供时间背景)2023-10-01 14:30:00
!<INPUT 1>!人格名字(重复两次,可能是格式要求)Isabella Rodriguez
!<INPUT 2>!人格当前的“关注焦点”(ISS,Importance-Salient Stimuli)"Maria Lopez just asked her to meet at the park."
!<INPUT 3>!对话内容,格式为 "说话者: 内容"
,每条记录用换行符分隔
Isabella Rodriguez: I can meet you at the park in 10 minutes.
Maria Lopez: Great, I'll be there.
  1. 提供上下文信息:包括时间、人格名字和当前关注焦点。
  2. 明确评分标准:定义 1~10 的评分范围,并给出示例。
  3. 提供完整对话内容:供模型分析。
  4. 要求简洁输出:仅返回一个整数评分。

生成反思内容

generate_memo_on_convo

def run_gpt_prompt_memo_on_convo(persona, all_utt, test_input=None, verbose=False): 

  # 1. 生成 prompt
  gpt_param = {"engine": "text-davinci-002", "max_tokens": 15, 
               "temperature": 0, "top_p": 1, "stream": False,
               "frequency_penalty": 0, "presence_penalty": 0, "stop": None}
  prompt_template = "persona/prompt_template/v3_ChatGPT/memo_on_convo_v1.txt" ########
  prompt_input = create_prompt_input(persona, all_utt)  ########
  prompt = generate_prompt(prompt_input, prompt_template)
  example_output = 'Jane Doe was interesting to talk to.' ########
  special_instruction = 'The output should ONLY contain a string that summarizes anything interesting that the agent may have noticed' ########
  # 2. 获取失败标准,返回“...”表示失败
  fail_safe = get_fail_safe() ########

  # 3. 调用模型,如果失败可以内部重试 3 次
  output = ChatGPT_safe_generate_response(prompt, example_output, special_instruction, 3, fail_safe,
                                          __chat_func_validate, __chat_func_clean_up, True)
  # 4. 成功,返回结果
  if output != False: 
    return output, [output, prompt, gpt_param, prompt_input, fail_safe]
  # ChatGPT Plugin ===========================================================

  # 5. 内部重试 3 次依然失败后,换个更好的模型,使用更大的 token 数量继续重试,提示词模板也换了
  gpt_param = {"engine": "text-davinci-003", "max_tokens": 50, 
                 "temperature": 0, "top_p": 1, "stream": False,
                 "frequency_penalty": 0, "presence_penalty": 0, "stop": None}
  prompt_template = "persona/prompt_template/v2/memo_on_convo_v1.txt"
  prompt_input = create_prompt_input(persona, all_utt)
  prompt = generate_prompt(prompt_input, prompt_template)

  fail_safe = get_fail_safe()
  # 6. 调用模型,这次重试 5 次
  output = safe_generate_response(prompt, gpt_param, 5, fail_safe,
                                  __func_validate, __func_clean_up)

  return output, [output, prompt, gpt_param, prompt_input, fail_safe]

模板

memo_on_convo_v1.txt

Variables: 
!<INPUT 0>! -- All convo utterances
!<INPUT 1>! -- persona name
!<INPUT 2>! -- persona name
!<INPUT 3>! -- persona name

<commentblockmarker>###</commentblockmarker>
[Conversation]
!<INPUT 0>!

Write down if there is anything from the conversation that !<INPUT 1>! might have found interesting from !<INPUT 2>!'s perspective, in a full sentence. 

"!<INPUT 3>!
变量名描述示例值
!<INPUT 0>!包含完整对话内容的字符串,格式为 "说话者: 内容",每条记录用换行符 \n分隔
Isabella Rodriguez: 今天天气不错,你想去哪里玩?
Maria Lopez: 我想去公园散步。
Isabella Rodriguez: 那我们去公园吧!
!<INPUT 1>!需要记住对话内容的人格名字,对话人 1Isabella Rodriguez
!<INPUT 2>!从其视角分析对话内容的人格名字,对话人 2Maria Lopez
!<INPUT 3>!用于格式化输出的前缀人格名字Isabella Rodriguez
  1. 提供完整的对话内容:供模型分析。
  2. 指定视角:要求模型从特定人格的视角出发,找出对话中值得关注的内容。
  3. 生成完整句子:确保输出是一个完整的句子,便于后续记忆存储和使用。

函数功能解析

以下是 reflect.py 文件中各个函数的详细功能分析:

1. reflect(persona)

  • 功能: 人格反思的主函数。
  • 输入:
    • persona: 一个 Persona 类的实例,代表当前的人格。
  • 输出:
    • 无直接输出,但会根据反思结果修改人格的记忆和状态。

2. generate_insights_and_evidence(persona, nodes, n=5)

  • 功能: 生成与给定节点相关的洞察和证据。
  • 输入:
    • persona: 一个 Persona 类的实例,代表当前的人格。
    • nodes: 一个节点列表,每个节点包含嵌入键(embedding key)。
    • n: 生成的洞察数量,默认为 5。
  • 输出:
    • 一个字典,键为生成的洞察,值为与该洞察相关的节点 ID 列表。

3. generate_action_event_triple(act_desp, persona)

  • 功能: 生成与动作描述相关的事件三元组(主语、谓语、宾语)。
  • 输入:
    • act_desp: 动作描述,例如 "sleeping"。
    • persona: 一个 Persona 类的实例,代表当前的人格。
  • 输出:
    • 一个字符串,表示事件三元组,例如 "🧈🍞"。

4. generate_poig_score(persona, event_type, description)

  • 功能: 计算事件、思想或聊天的重要性评分。
  • 输入:
    • persona: 一个 Persona 类的实例,代表当前的人格。
    • event_type: 事件类型,可以是 "event"、"thought" 或 "chat"。
    • description: 事件的描述。
  • 输出:
    • 一个整数,表示重要性评分。

5. generate_planning_thought_on_convo(persona, all_utt)

  • 功能: 生成与对话相关的规划思想。
  • 输入:
    • persona: 一个 Persona 类的实例,代表当前的人格。
    • all_utt: 包含对话内容的字符串。
  • 输出:
    • 一个字符串,表示生成的规划思想。

6. generate_memo_on_convo(persona, all_utt)

  • 功能: 生成与对话相关的备忘录。
  • 输入:
    • persona: 一个 Persona 类的实例,代表当前的人格。
    • all_utt: 包含对话内容的字符串。
  • 输出:
    • 一个字符串,表示生成的备忘录。

7. run_reflect(persona)

  • 功能: 运行实际的反思过程。首先需要检查出发条件是否满足,如果满足则开始 reflect 并重置相关的计数器。
  • 输入:
    • persona: 一个 Persona 类的实例,代表当前的人格。
  • 输出:
    • 无直接输出,但会将生成的思想保存到人格的记忆中。

8. reflection_trigger(persona)

  • 功能: 判断是否需要运行一次新的反思。
  • 输入:
    • persona: 一个 Persona 类的实例,代表当前的人格。
  • 输出:
    • 一个布尔值,表示是否需要运行新的反思。

9. reset_reflection_counter(persona)

  • 功能: 重置用于触发反思的计数器。
  • 输入:
    • persona: 一个 Persona 类的实例,代表当前的人格。
  • 输出:
    • 无直接输出,但会修改人格的内部状态。

10. generate_focal_points(persona, n=3)

  • 功能: 生成焦点(focal points),这些焦点用于后续的反思过程。
  • 输入:
    • persona: 一个 Persona 类的实例,代表当前的人格。
    • n: 生成的焦点数量,默认为 3。
  • 输出:
    • 一个包含焦点的列表,每个焦点是一个字符串。