关联:AI学习Day 2:搭建一个简单的AgentDay 4:agent经典范式构建

这次实验将尝试复现人工智能历史上一个极具影响力的早期聊天机器人——ELIZA。

0x01 ELIZA 的设计思想

ELIZA是由麻省理工学院的计算机科学家约瑟夫·魏泽鲍姆(Joseph Weizenbaum)于1966年发布的一个计算机程序,是早期自然语言处理领域的著名尝试之一。ELIZA并非一个单一的程序,而是一个可以执行不同 “脚本” 的框架。其中,最广为人知也最成功的脚本是“DOCTOR”,它模仿了一位罗杰斯学派的非指导性心理治疗师。

ELIZA的工作方式极其巧妙:它从不正面回答问题或提供信息,而是通过识别用户输入中的关键词,然后应用一套预设的转换规则,将用户的陈述转化为一个开放式的提问。例如,当用户说“我为我的男朋友感到难过”时,ELIZA可能会识别出关键词“我为……感到难过”,并应用规则生成回应:“你为什么会为你的男朋友感到难过?”

魏泽鲍姆的设计思想并非要创造一个真正能够“理解”人类情感的智能体,恰恰相反,他想证明的是,通过一些简单的句式转换技巧,机器可以在完全不理解对话内容的情况下,营造出一种“智能”和“共情”的假象。然而,出乎他意料的是,许多与ELIZA交互过的人(包括他的秘书)都对其产生了情感上的依赖,深信它能够理解自己。

本节的实践目标即为复现ELIZA的核心机制,以深入理解这种规则驱动方法的优势与根本局限。

该项目本质上就是像我们玩游戏时的剧情对话,只不过是数据增加,并且加上 随机输出 等机制。


0x02 核心算法

ELIZA的算法流程基于模式匹配(Pattern Matching)文本替换(Text Substitution),可被清晰地分解为以下四个步骤:

  1. 关键词识别与排序: 规则库为每个关键词(如 motherdreameddepressed)设定一个优先级。当输入包含多个关键词时,程序会选择优先级最高的关键词所对应的规则进行处理。
  2. 分解规则: 空格分解句子为单词后循环,通过正则匹配关键词
  3. 重组规则: 然后从预定义的回复语句库中提取回答(可以增加多个句子随机回复)
  4. 代词转换: 在重组前,程序会进行简单的代词转换(如 I → youmy → your),以维持对话的连贯性。

通过这套机制,ELIZA成功地将复杂的自然语言理解问题,简化为了一个可操作的、基于规则的模式匹配游戏。


0x03 代码实现

现在,我们将上一节描述的技术原理转化为一个简单的、可运行的Python函数。下面的代码实现了一个迷你版的ELIZA,它包含了一小部分规则,但足以展示其核心工作机制。

import re
import random
 
# 定义规则库:模式(正则表达式) -> 响应模板列表
rules = {
    r'I need (.*)': [
        "Why do you need {0}?",
        "Would it really help you to get {0}?",
        "Are you sure you need {0}?"
    ],
    r'Why don\'t you (.*)\?': [
        "Do you really think I don't {0}?",
        "Perhaps eventually I will {0}.",
        "Do you really want me to {0}?"
    ],
    r'Why can\'t I (.*)\?': [
        "Do you think you should be able to {0}?",
        "If you could {0}, what would you do?",
        "I don't know -- why can't you {0}?"
    ],
    r'I am (.*)': [
        "Did you come to me because you are {0}?",
        "How long have you been {0}?",
        "How do you feel about being {0}?"
    ],
    r'.* mother .*': [
        "Tell me more about your mother.",
        "What was your relationship with your mother like?",
        "How do you feel about your mother?"
    ],
    r'.* father .*': [
        "Tell me more about your father.",
        "How did your father make you feel?",
        "What has your father taught you?"
    ],
    r'.*': [
        "Please tell me more.",
        "Let's change focus a bit... Tell me about your family.",
        "Can you elaborate on that?"
    ]
}
 
# 定义代词转换规则
pronoun_swap = {
    "i": "you", "you": "i", "me": "you", "my": "your",
    "am": "are", "are": "am", "was": "were", "i'd": "you would",
    "i've": "you have", "i'll": "you will", "yours": "mine",
    "mine": "yours"
}
 
def swap_pronouns(phrase):
    """
    对输入短语中的代词进行第一/第二人称转换
    """
    words = phrase.lower().split()
    swapped_words = [pronoun_swap.get(word, word) for word in words]
    return " ".join(swapped_words)
 
def respond(user_input):
    """
    根据规则库生成响应
    """
    for pattern, responses in rules.items():
        match = re.search(pattern, user_input, re.IGNORECASE)
        if match:
            # 捕获匹配到的部分
            captured_group = match.group(1) if match.groups() else ''
            # 进行代词转换
            swapped_group = swap_pronouns(captured_group)
            # 从模板中随机选择一个并格式化
            response = random.choice(responses).format(swapped_group)
            return response
    # 如果没有匹配任何特定规则,使用最后的通配符规则
    return random.choice(rules[r'.*'])
 
# 主聊天循环
if __name__ == '__main__':
    print("Therapist: Hello! How can I help you today?")
    while True:
        user_input = input("You: ")
        if user_input.lower() in ["quit", "exit", "bye"]:
            print("Therapist: Goodbye. It was nice talking to you.")
            break
        response = respond(user_input)
        print(f"Therapist: {response}")
 

通过上述的编程实践,我们可以直观地总结出规则驱动系统的根本局限性:

  • 缺乏语义理解:系统不理解词义。例如,面对“I am not happy”的输入,它仍会机械地匹配 I am (.*) 规则并生成语义不通的回应,因为它无法理解否定词“not”的作用。
  • 无上下文记忆:系统是无状态的(Stateless),每次回应仅基于当前单句输入,无法进行连贯的多轮对话。
  • 规则的扩展性问题:尝试增加更多规则会导致规则库的规模爆炸式增长,规则间的冲突与优先级管理将变得极其复杂,最终导致系统难以维护。

然而,尽管存在这些显而易见的缺陷,ELIZA在当时却产生了著名的“ELIZA效应”,许多用户相信它能理解自己。这种智能的幻觉主要源于其巧妙的对话策略(如扮演被动的提问者、使用开放式模板)以及人类天生的情感投射心理。

ELIZA的实践清晰地揭示了符号主义方法的核心矛盾:系统看似智能的表现,完全依赖于设计者预先编码的规则。然而,面对真实世界语言的无限可能性,这种穷举式的方法注定不可扩展。系统没有真正的理解,只是在执行符号操作,这正是其脆弱性的根源。

0x04 功能强化

简单做一点提提升:

  • 为ELIZA添加3-5条新的规则,使其能够处理更多样化的对话场景(如谈论工作、学习、爱好等)
  • 实现一个简单的”上下文记忆”功能:让ELIZA能够记住用户在对话中提到的关键信息(如姓名、年龄、职业),并在后续对话中引用
  1. 首先思考一下怎么添加规则,这个应该比较简单,直接在rules中按照上面的写法加上工作这些东西就好了 例如(添加的以注释形式给出):
rules = {
    r'I need (.*)': [
        "Why do you need {0}?",
        "Would it really help you to get {0}?",
        "Are you sure you need {0}?"
    ],
    r'Why don\'t you (.*)\?': [
        "Do you really think I don't {0}?",
        "Perhaps eventually I will {0}.",
        "Do you really want me to {0}?"
    ],
    r'.* father .*': [
        "Tell me more about your father.",
        "How did your father make you feel?",
        "What has your father taught you?"
    ],
    #r'.*\b(work|job|career|company)\b.*': [
    #    "How do you feel about your work?",
    #    "What part of your job affects you the most?",
    #    "Does your work bring you satisfaction or stress?"
    #],
    #r'I am (?:studying|learning) (.*)':[
    #    "How do you feel about studying {0}?",
    #    "What made you start learning {0}?",
    #    "Do you find {0} challenging?"
    #],
    r'.*': [
        "Please tell me more.",
        "Let's change focus a bit... Tell me about your family.",
        "Can you elaborate on that?"
    ]
 
}

无非就是写上正则匹配规则:对应回复的键值对 2. 现在想一下怎么实现上下文记忆,我这里的想法就是:

  • 首先需要一个容器存储上下文记忆,我这里选择创建一个memory字典存储关键信息:
memory = {
    "name": None,
    "age": None,
    "role": None,
    "topic": None
}
  • 然后我要从用户的输入中提取关键信息(就是在一些特定的输入中提取),我这里实现了一个提取关键信息的函数:
def extract_information(user_input):
    """
    从用户输入中抽取关键信息并更新上下文记忆
    """
    # 姓名
    match = re.search(r'my name is (\w+)', user_input, re.IGNORECASE)
    if match:
        memory["name"] = match.group(1)
 
    # 年龄
    match = re.search(r'i am (\d{1,3}) years old', user_input, re.IGNORECASE)
    if match:
        memory["age"] = match.group(1)
 
    # 身份 / 职业
    match = re.search(r'i am a (student|teacher|developer|engineer)', user_input, re.IGNORECASE)
    if match:
        memory["role"] = match.group(1)
 
    # 当前主题(非常粗糙,但够用)
    if re.search(r'\b(work|job|career)\b', user_input, re.IGNORECASE):
        memory["topic"] = "work"
    elif re.search(r'\b(study|exam|school)\b', user_input, re.IGNORECASE):
        memory["topic"] = "study"
 

同样和之前的,根据对应输入选择对应输出一样。使用正则匹配关键字来确定是否需要提取 提取到之后会存入memory字典中的

  • 有了上下文记忆之后,还需要在输出的回复中有所体现,我这里的方法是(简单的):对于名字,会在每一句回复前加上名字,其他就是在最后输出对应的预定义回复语句,这就是修改respond模块来实现(修改以注释形式给出):
def respond(user_input):
    """
    根据规则库生成响应
    """
    extract_information(user_input)
 
    for pattern, responses in rules.items():
        match = re.search(pattern, user_input, re.IGNORECASE)
        if match:
            # 捕获匹配到的部分
            captured_group = match.group(1) if match.groups() else ''
            # 进行代词转换
            swapped_group = swap_pronouns(captured_group)
            # 从模板中随机选择一个并格式化
            response = random.choice(responses).format(swapped_group)
 
         ##   if memory["name"]:
         ##       response = f"{memory['name']}, {response}"
 
                ## 根据身份或主题稍作调整
         ##   if memory["role"] == "student" and memory["topic"] == "study":
         ##       response += " How are your studies going lately?"
 
         ##   if memory["topic"] == "work":
         ##       response += " Work can have a big impact on our emotions."
 
 
            return response
    # 如果没有匹配任何特定规则,使用最后的通配符规则
    return random.choice(rules[r'.*'])

实现效果

注意: 这里:memory是自己加上了便于调试的代码(修改以注释形式给出):

# 主聊天循环
if __name__ == '__main__':
    print("Therapist: Hello! How can I help you today?")
    while True:
        user_input = input("You: ")
        #if user_input == ":memory":
        #    print(memory)
        #    continue
        if user_input.lower() in ["quit", "exit", "bye"]:
            print("Therapist: Goodbye. It was nice talking to you.")
            break
        response = respond(user_input)
        print(f"Therapist: {response}")