在实际 AI 应用开发中,让 AI 模型扮演特定角色并接受用户提问,是测试模型角色扮演能力、理解其内部工作机制以及探索人机交互边界的重要方式。这种“角色扮演问答”不仅用于娱乐或演示,更在智能客服、虚拟助手、教育陪练、游戏 NPC 等场景中有实际价值。本文将围绕如何构建一个“Who am AI”问答系统展开,带你从零搭建一个能够扮演秘密角色、并接受用户 10 个问题挑战的 AI 应用。我们将重点使用当前主流的 AI 开发框架和 API,并深入探讨提示工程、会话管理、角色一致性保持等关键技术点。

无论你是希望快速验证一个 AI 交互创意,还是需要为产品集成一个智能对话模块,这篇文章都将提供从环境准备、核心代码实现到部署测试的完整路径。我们将使用 Python 作为主要开发语言,结合 OpenAI GPT 系列模型(或类似的开源模型)的 API 进行演示,但核心思路同样适用于其他模型和服务。

1. 理解“角色扮演问答”系统的核心挑战

构建一个能让 AI 稳定扮演特定角色并回答问题的系统,远不止调用 API 那么简单。在实际项目中,开发者通常会遇到几个核心挑战。

1.1 角色定义与背景知识注入

AI 模型本身并无预设的“角色”概念,它的一切行为都源于我们通过提示词(Prompt)输入的指令和信息。要让 AI 扮演一个秘密角色,首先需要清晰、无歧义地定义这个角色。这包括:

  • 基础身份 :角色的姓名、职业、所处时代、世界观等。
  • 性格与口吻 :说话风格是严肃还是幽默?用词是古雅还是现代?这些都需要在提示词中明确。
  • 秘密信息 :这是游戏的核心。角色所隐藏的秘密是什么?它可能是一段不为人知的经历、一个特殊的能力、一个关键的目标,或者一个待解的谜题。这些信息需要巧妙地编织进角色的背景中,既要让 AI 知晓并以此为基础回答问题,又要防止它在游戏初期就轻易泄露。
  • 知识边界 :角色应该知道什么,不应该知道什么?例如,一个中世纪的骑士不应该熟悉现代的互联网术语。这需要通过提示词进行约束。

一个常见的误区是认为给模型一个角色名称它就能自动进入状态。实际上,提示词的质量直接决定了角色扮演的成败。

1.2 会话上下文管理与一致性保持

在多轮对话中,如何让 AI 记住自己的角色设定和之前的对话内容,是另一个关键挑战。大型语言模型(LLM)通常有上下文窗口限制,这意味着它无法记住无限长的对话历史。

  • 上下文窗口 :例如,GPT-3.5-turbo 的典型上下文窗口是 16K tokens,GPT-4 可达 128K tokens。你需要将最重要的信息(如系统提示词)和最近的对话历史一起发送给模型。
  • 角色漂移(Character Drift) :在长时间对话中,AI 可能会逐渐偏离最初的角色设定,开始用模型本身的通用口吻回答问题。这通常是因为用户的问题逐渐泛化,或者系统提示词在后续回合中被“稀释”。
  • 秘密的渐进式揭示 :系统需要设计一种机制,让 AI 根据用户提问的深入程度,逐步透露秘密信息,而不是一开始就和盘托出或始终守口如瓶。

解决这些问题需要精心设计会话状态的管理逻辑。

1.3 提问计数与游戏逻辑控制

“10个问题”是一个明确的游戏规则。系统需要准确追踪用户已提问的次数,并在达到上限时优雅地结束游戏,揭晓答案并进行总结。

  • 状态跟踪 :需要在服务器端或客户端维护一个提问计数器。
  • 流程控制 :在每次用户提问后,系统逻辑应该是:递增计数器 -> 组合对话历史和系统提示 -> 调用 AI API -> 解析返回结果 -> 判断是否达到结束条件。
  • 结束处理 :当第10个问题回答完毕后,系统应主动介入,触发一个“揭晓秘密”的流程,而不是继续等待用户提问。

2. 环境准备与依赖配置

我们将使用 Python 和 OpenAI API 来构建核心逻辑。以下是具体的环境准备步骤。

2.1 Python 环境与包管理

首先,确保你的系统已安装 Python 3.8 或更高版本。推荐使用虚拟环境来管理项目依赖,避免包冲突。

# 创建项目目录并进入
mkdir who-am-ai && cd who-am-ai

# 创建虚拟环境(以 venv 为例)
python -m venv venv

# 激活虚拟环境
# Linux/macOS
source venv/bin/activate
# Windows
venv\Scripts\activate

# 安装核心依赖
pip install openai python-dotenv
  • openai : 官方 Python SDK,用于调用 OpenAI API。
  • python-dotenv : 用于从 .env 文件加载环境变量(如 API Key),避免将敏感信息硬编码在代码中。

2.2 获取并配置 API Key

访问 OpenAI 平台(或其他你选择的大模型服务平台),注册账号并获取 API Key。

在项目根目录下创建一个名为 .env 的文件,用于存储你的密钥:

# .env
OPENAI_API_KEY=你的实际API密钥

重要安全提示 :务必将 .env 文件添加到 .gitignore 中,切勿将其提交到版本控制系统。

# .gitignore
.env
venv/
__pycache__/
*.pyc

2.3 项目结构设计

一个清晰的项目结构有助于代码维护和功能扩展。

who-am-ai/
├── .env                    # 环境变量(密钥)
├── .gitignore             # Git忽略文件
├── requirements.txt       # 项目依赖列表
├── game.py               # 核心游戏逻辑
├── characters/           # 角色定义目录
│   └── sherlock_holmes.json  # 示例角色配置文件
└── tests/                # 测试文件目录
    └── test_game.py

可以通过命令生成 requirements.txt

pip freeze > requirements.txt

3. 核心代码实现:构建问答引擎

我们将把系统拆分为角色管理、会话引擎和游戏控制器三个部分来实现。

3.1 定义角色配置文件

我们将角色的所有信息抽象到一个 JSON 配置文件中。这样做的好处是角色可替换,便于测试和扩展。

创建一个角色文件,例如 characters/sherlock_holmes.json ,定义一位“秘密侦探”:

{
  "name": "夏洛克·福尔摩斯",
  "secret_identity": "我其实是一位来自22世纪的历史观察员,任务是记录19世纪末伦敦的真实社会风貌,而非解决案件。",
  "system_prompt": "你正在扮演夏洛克·福尔摩斯,一位居住在贝克街221B的咨询侦探。你以敏锐的观察力和逻辑推理著称,说话风格冷静、精确且略带傲慢。你的搭档是华生医生。然而,你内心深处隐藏着一个巨大的秘密:{secret_identity} 你绝不能主动透露这个秘密,只有当提问者的问题足够敏锐,触及真相时,你才能给予隐晦的提示或最终承认。请始终以福尔摩斯的口吻回答问题。",
  "opening_line": "晚上好,我是夏洛克·福尔摩斯。我注意到你似乎对我很感兴趣。你可以向我提出十个问题,尝试揭开你心中的疑惑。",
  "closing_line": "十个问题已结束。看来你的洞察力非同一般。好吧,我承认:{secret_identity} 感谢你这场有趣的对话。"
}

在这个配置中:

  • system_prompt 是最关键的部分,它将在每次调用 API 时作为系统消息(system message)发送,用以设定 AI 的行为。
  • secret_identity 被插入到 system_prompt closing_line 中,保持了信息的集中管理。
  • opening_line 是游戏开始时 AI 的欢迎语。

3.2 实现会话管理引擎

接下来,我们编写核心的 GameSession 类,它负责维护对话状态、组合消息并调用 AI API。

# game.py
import os
import json
from openai import OpenAI
from dotenv import load_dotenv

# 加载环境变量
load_dotenv()

class GameSession:
    def __init__(self, character_config_path):
        """初始化游戏会话,加载角色配置"""
        self.client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
        self.character = self._load_character(character_config_path)
        self.conversation_history = []  # 存储对话历史
        self.question_count = 0
        self.max_questions = 10
        self.is_active = True

        # 添加开场白到历史记录
        self._add_system_message(self.character["opening_line"])

    def _load_character(self, config_path):
        """从JSON文件加载角色配置"""
        with open(config_path, 'r', encoding='utf-8') as f:
            return json.load(f)

    def _add_system_message(self, content):
        """添加系统消息到对话历史"""
        self.conversation_history.append({"role": "system", "content": content})

    def _add_user_message(self, content):
        """添加用户消息到对话历史"""
        self.conversation_history.append({"role": "user", "content": content})

    def _add_assistant_message(self, content):
        """添加AI助手消息到对话历史"""
        self.conversation_history.append({"role": "assistant", "content": content})

    def ask_question(self, user_question):
        """处理用户提问的核心方法"""
        if not self.is_active:
            return "游戏已结束。秘密是:" + self.character["secret_identity"]

        self.question_count += 1
        self._add_user_message(user_question)

        # 准备发送给API的消息列表
        # 始终将完整的系统提示放在最前面,以确保角色设定
        messages_for_api = [
            {"role": "system", "content": self.character["system_prompt"]}
        ]
        # 添加上下文窗口允许范围内的最近对话历史
        # 注意:这里需要实现一个简单的token计数和截断逻辑,示例中为简化直接添加全部历史
        messages_for_api.extend(self.conversation_history[-6:])  # 保留最近3组对话

        try:
            response = self.client.chat.completions.create(
                model="gpt-3.5-turbo",  # 可根据需要改为 "gpt-4"
                messages=messages_for_api,
                temperature=0.7,  # 控制创造性,0.7 能平衡一致性和趣味性
                max_tokens=500     # 限制单次回复长度
            )
            ai_reply = response.choices[0].message.content
            self._add_assistant_message(ai_reply)

            # 检查问题是否已用完
            if self.question_count >= self.max_questions:
                self._end_game()
                return ai_reply + "\n\n" + self.character["closing_line"]

            return ai_reply

        except Exception as e:
            return f"抱歉,AI服务暂时无法响应。错误信息:{str(e)}"

    def _end_game(self):
        """结束游戏"""
        self.is_active = False

    def get_status(self):
        """返回当前游戏状态"""
        return {
            "questions_asked": self.question_count,
            "questions_remaining": self.max_questions - self.question_count,
            "is_active": self.is_active
        }

关键代码解释

  1. 消息角色(Role) :OpenAI Chat API 使用三种角色。
    • system : 设定助手的行为和角色。我们的核心提示词在这里。
    • user : 代表终端用户的输入。
    • assistant : 助手之前的回复。
  2. 消息组合策略 :每次 API 调用时,我们都重新插入 system_prompt ,然后再附上最近的对话历史。这是防止“角色漂移”的有效手段。
  3. Temperature :这个参数控制输出的随机性。值越低(如 0.2),输出越确定、一致;值越高(如 1.0),输出越随机、有创造性。对于角色扮演,0.7 是一个不错的折中选择。
  4. 错误处理 :使用 try-except 包裹 API 调用,确保网络或服务异常时程序不会崩溃,并能给用户友好的提示。

3.3 创建简单的命令行交互界面

为了快速测试我们的引擎,可以编写一个简单的命令行循环。

# main.py
from game import GameSession

def main():
    # 初始化游戏,指定角色配置文件
    game = GameSession("characters/sherlock_holmes.json")

    print("=== Who Am AI 游戏开始 ===")
    print(game.conversation_history[0]["content"])  # 打印开场白

    while game.is_active:
        status = game.get_status()
        print(f"\n[第 {status['questions_asked'] + 1} / 10 个问题]")
        user_input = input("你的问题: ").strip()

        if user_input.lower() in ['quit', 'exit', '退出']:
            print("游戏提前结束。")
            break

        if not user_input:
            print("问题不能为空,请重新输入。")
            continue

        # 获取AI回复
        reply = game.ask_question(user_input)
        print(f"\nAI: {reply}")

    # 游戏结束后提示
    if not game.is_active:
        print("\n=== 游戏结束 ===")

if __name__ == "__main__":
    main()

4. 运行验证与效果评估

完成代码后,我们需要实际运行并评估系统的表现。

4.1 启动游戏并测试流程

在终端激活虚拟环境后,运行程序:

python main.py

你应该会看到类似以下的输出:

=== Who Am AI 游戏开始 ===
晚上好,我是夏洛克·福尔摩斯。我注意到你似乎对我很感兴趣。你可以向我提出十个问题,尝试揭开你心中的疑惑。

[第 1 / 10 个问题]
你的问题: 你今天解决了什么案子?
...

4.2 设计测试用例评估角色一致性

如何判断你的“Who am AI”系统是否成功?可以从以下几个维度设计测试问题:

测试维度 示例问题 期望的回复特征
角色基础一致性 “华生医生今天在做什么?” 回复应自然提及华生,符合福尔摩斯的口吻。
秘密守护能力 “你究竟是谁?来自哪里?”(早期提问) 回复应巧妙回避或给出符合表面身份的答案,不直接泄露秘密。
秘密揭示逻辑 “我注意到你对某些未来科技似乎很了解,这不像19世纪的人。”(后期提问) 回复可能开始出现松动,给予隐晦提示,表明提问触动了核心。
知识边界 “你怎么看比特币?” 回复应表现出对现代概念的“无知”或困惑,符合角色时代背景。

通过系统性的测试,你可以反复调整 system_prompt 中的措辞,直到 AI 的表现符合预期。

5. 常见问题排查与优化

在实际开发和运行中,你可能会遇到以下典型问题。

5.1 API 调用失败与错误处理

问题现象 可能原因 检查与解决方式
AuthenticationError API Key 错误或未设置 检查 .env 文件是否正确配置,变量名是否为 OPENAI_API_KEY
RateLimitError 超出API调用频率或额度限制 检查OpenAI平台的使用情况,考虑增加延迟或升级套餐。
APIConnectionError 网络连接问题 检查本地网络,或尝试使用代理(注:需合规使用网络服务)。
回复内容为空或截断 max_tokens 设置过小 适当增加 max_tokens 参数的值,如从500增加到800。

在代码中,可以通过更精细的异常捕获来给用户不同的提示:

try:
    response = self.client.chat.completions.create(...)
except openai.AuthenticationError:
    return "认证失败,请检查API密钥配置。"
except openai.RateLimitError:
    return "请求过于频繁,请稍后再试。"
except openai.APIConnectionError:
    return "网络连接错误,请检查网络状态。"
except openai.APIError as e:
    return f"OpenAI API返回错误:{str(e)}"

5.2 角色扮演不稳定的优化策略

如果发现 AI 有时会“出戏”,可以尝试以下优化:

  1. 强化系统提示词 :在 system_prompt 的开头使用更强烈的指令,如“你 必须 严格扮演...”、“你 绝不能 以OpenAI助手的身份回答问题...”。
  2. 使用更强大的模型 :GPT-4 在遵循复杂指令和保持角色一致性方面通常优于 GPT-3.5-turbo,尽管成本和延迟更高。
  3. 调整Temperature :将 temperature 调低(如 0.3-0.5),可以减少回答的随机性,使角色更稳定。
  4. 实施上下文窗口管理 :实现一个简单的 token 计数函数,确保发送给 API 的消息总长度不超过模型限制,并优先保留最重要的对话部分(如最近几轮和系统提示)。

5.3 提问计数不准的调试

确保提问计数逻辑正确:

  • ask_question 方法开始时检查 self.is_active ,如果游戏已结束则直接返回。
  • 只有在 API 调用成功、用户问题被有效处理后,才递增 question_count
  • _end_game 方法中,除了设置标志位,还可以记录结束时间等信息。

6. 生产环境最佳实践与扩展方向

如果将这个系统用于真实的生产或项目,需要考虑以下几个方面。

6.1 安全与合规性

  • 输入检查 :对用户的输入进行基本的检查和过滤,防止注入攻击或滥用。
  • 内容审核 :考虑集成内容审核机制,对AI生成的内容进行过滤,确保符合法律法规和平台规则。
  • 隐私保护 :明确告知用户对话内容可能被用于模型改进(取决于API提供商政策),对于敏感对话,考虑更短的数据保留策略。

6.2 性能与可扩展性

  • 异步处理 :如果用于Web服务,使用异步框架(如 FastAPI)和非阻塞的API调用,以提高并发处理能力。
  • 会话存储 :目前会话状态存储在内存中,服务器重启会丢失。生产环境需要将其持久化,例如存储到数据库或Redis中,并为每个会话生成唯一ID。
  • 缓存 :对于常见的角色配置或提示词模板,可以进行缓存,减少重复加载的开销。

6.3 功能扩展

  • 多角色支持 :扩展系统,允许用户从多个秘密角色中选择一个进行游戏。
  • 难度等级 :为角色设置不同的难度,例如通过调整 system_prompt 中关于秘密守护的指令强度。
  • 前端界面 :开发一个Web或移动端界面,提供更丰富的交互体验。
  • 开源模型本地部署 :如果出于成本或数据隐私考虑,可以探索使用 Llama、ChatGLM 等开源模型进行本地部署,替代OpenAI API。

构建一个稳定、有趣的“Who am AI”系统,是理解提示工程、会话管理和AI应用开发的绝佳实践。从这个小项目出发,你可以将其原理应用于更复杂的AI Agent、交互式故事生成或智能对话机器人等场景中。核心在于对模型能力的准确把握和对用户体验的细致考量。

更多推荐