该代码是一个MBTI人格测试聊天机器人实验脚本,通过加载ChatHaruhi框架中的多个角色(包含动漫、游戏、电视剧人物),使用OpenAI API与这些角色进行MBTI心理人格测试对话,并将测试结果保存到JSONL文件中。
graph TD
A[开始] --> B[读取MBTI问题数据]
B --> C[设置OpenAI API密钥]
C --> D[解压并加载ChatHaruhi角色]
D --> E[构建角色-测试者映射]
E --> F[生成查询列表stubborn_characters]
F --> G{检查是否存在之前的结果}
G -- 是 --> H[加载之前的响应]
G -- 否 --> I[从头开始]
H --> J[遍历查询列表]
I --> J
J --> K{当前索引 < 已处理数量?}
K -- 是 --> L[跳过该条]
K -- 否 --> M[重置对话历史]
M --> N[发送开放性问题]
N --> O[发送封闭式选项问题]
O --> P[提取用户选择的选项]
P --> Q[保存结果到JSONL]
Q --> R{继续下一条?}
R -- 是 --> J
R -- 否 --> S[结束]
全局变量层
├── 配置文件 (input_name, save_name)
├── 数据存储 (datas, query_list, previous_response)
├── 角色映射 (NAME_DICT, tester, stubborn_characters)
└── 状态管理 (if_first, count, role_id)
ChatHaruhi (第三方库)
└── ChatHaruhi类实例 (ai_roles_obj)输入MBTI问题JSONL文件名
类型:str
存储解析后的MBTI问题数据
类型:list
ChatHaruhi项目路径
类型:str
中文角色名到英文名的映射
类型:dict
存储已加载的角色聊天机器人实例
类型:dict
角色对应的测试者名称映射
类型:dict
需要测试的坚持角色列表
类型:list
待执行的测试查询列表
类型:list
结果输出文件名
类型:str
是否首次写入文件的标志
类型:bool
角色ID映射
类型:dict
已存在的历史响应数据
类型:list
已处理记录计数器
类型:int
MBTI选项列表
类型:list
系统提示词,定义角色行为
类型:str
语言模型类型标识
类型:str
故事数据库路径
类型:str
对话历史记录列表
类型:list
聊天接口对象,包含temperature等属性
类型:object
生成文本的随机性参数
类型:float
解析单行 JSON 数据,将 JSON 字符串转换为 Python 对象(字典或列表)。这是 Python 标准库 json 模块的核心函数,在本代码中用于从 JSONL 文件中读取和解析数据。
line:str,需要解析的 JSON 格式字符串,通常是文件中的一行数据
返回值类型:dict或list返回值描述:解析后的 Python 对象,通常是字典(当 JSON 是对象时)或列表(当 JSON 是数组时)
graph TD
A[开始] --> B[输入: JSON 字符串 line]
B --> C{json.loads 解析}
C -->|成功| D[输出: Python 对象]
C -->|失败| E[抛出 JSONDecodeError 异常]
D --> F[结束]
E --> F
# 从文件读取一行 JSON 格式的字符串
data = json.loads(line)
# 说明:
# - json 是 Python 标准库中的 json 模块
# - loads() 方法将 JSON 字符串转换为 Python 对象
# - line 是从文件中读取的一行字符串
# - 返回的 data 通常是字典类型,包含了问题的各项属性
# - 如果 line 不是有效的 JSON 格式,会抛出 json.JSONDecodeError 异常在代码中有两处使用:
示例 1:读取问题数据
with open(input_name, encoding='utf-8') as f:
for line in f:
data = json.loads(line) # 解析 JSONL 文件中的一行
datas.append(data) # 将解析后的字典添加到列表示例 2:读取已保存的结果
if os.path.exists(save_name):
with open(save_name, encoding='utf-8') as f:
for line in f:
data = json.loads(line) # 解析已保存的结果行
previous_response.append(data)| 组件名称 | 一句话描述 |
|---|---|
json 模块 |
Python 标准库,用于 JSON 数据的序列化和反序列化 |
json.loads() |
将 JSON 字符串转换为 Python 对象的方法 |
json.JSONDecodeError |
解析失败时抛出的异常类型 |
- 错误处理缺失:代码中使用
json.loads(line)时没有显式的异常处理机制,如果遇到格式错误的 JSON 行,会导致程序崩溃 - 编码问题:虽然指定了
encoding='utf-8',但未对 BOM 或其他编码问题进行处理 - 验证缺失:解析后的数据没有进行字段验证,可能导致后续代码访问不存在的键时出错
错误处理与异常设计:
- 当前代码未捕获
JSONDecodeError异常,建议添加 try-except 块处理可能的解析错误 - 可以考虑记录错误行号和内容,便于调试
数据流与状态机:
json.loads()是数据流中的关键转换节点,将文件 I/O 的字符串流转换为内存中的 Python 对象流- 解析后的数据被追加到
datas列表中,作为后续处理的输入
文件打开操作,用于以指定模式打开文件并返回文件对象供后续读写操作使用。
参数:
file:str,要打开的文件路径,可以是相对路径或绝对路径mode:str,文件打开模式,常见值包括 'r'(读取)、'w'(写入)、'a'(追加)、'rb'(二进制读取)等,默认为 'r'encoding:str,文件编码方式,如 'utf-8'、'gbk' 等,用于文本文件的字符编码解码
返回值:TextIOWrapper(或 BufferedReader/BufferedWriter 等),返回文件对象,支持 read()、write()、readline() 等操作
flowchart TD
A[开始] --> B{检查文件路径}
B -->|文件存在| C{检查权限}
B -->|文件不存在| D[抛出 FileNotFoundError]
C -->|有权限| E[根据mode打开文件]
C -->|无权限| F[抛出 PermissionError]
E --> G[根据encoding设置编解码器]
G --> H[返回文件对象]
H --> I[结束]
# 代码中使用 open() 函数的实际示例
input_name = 'mbti_questions.jsonl' # 定义要打开的文件路径
# 使用 with 语句打开文件,确保自动关闭资源
# 参数说明:
# - input_name: 文件路径字符串
# - encoding='utf-8': 指定 UTF-8 编码读取文件内容
with open(input_name, encoding='utf-8') as f:
# 遍历文件中的每一行
for line in f:
# 使用 json.loads 将每行 JSON 字符串解析为 Python 对象
data = json.loads(line)
# 将解析后的数据添加到 datas 列表中
datas.append(data)
# 打印读取的数据条数
print(len(datas))说明:在此代码中,open() 函数用于读取 MBTI 题目数据文件。with 语句确保文件在使用完毕后自动关闭,避免资源泄漏。encoding='utf-8' 参数确保中文字符能正确解码。
设置指定名称的环境变量值为给定的字符串。
参数:
key:字符串,环境变量的名称(如"OPENAI_API_KEY")value:字符串,环境变量的值(如"sk-")
返回值:None,赋值操作不返回任何值
flowchart TD
A[开始] --> B[调用 os.environ.__setitem__]
B --> C{检查环境变量是否已存在}
C -->|已存在| D[更新环境变量的值]
C -->|不存在| E[创建新的环境变量]
D --> F[返回 None]
E --> F
# 设置环境变量
# os.environ 是一个字典-like 对象,用于访问和修改环境变量
# 通过 __setitem__ 方法设置键值对
os.environ["OPENAI_API_KEY"] = "sk-"
# 等价于: os.environ.__setitem__("OPENAI_API_KEY", "sk-")
# 该操作会将环境变量 OPENAI_API_KEY 的值设置为 "sk-"
# openai 库会自动读取该环境变量用于API认证补充说明:
- 设计目标:为 OpenAI API 客户端提供认证凭据
- 约束:环境变量
OPENAI_API_KEY需要在调用 OpenAI API 之前设置 - 技术债务:API Key 直接以明文形式硬编码在代码中,存在安全风险,应改为从配置文件或密钥管理系统读取
检查指定路径(文件或目录)是否存在,返回布尔值结果。
参数:
path:str,要检查的文件或目录路径
返回值:bool,如果指定路径存在返回 True,否则返回 False
flowchart TD
A[开始检查路径] --> B{调用 os.path.exists}
B --> C{路径是否存在?}
C -->|是| D[返回 True]
C -->|否| E[返回 False]
D --> F[结束]
E --> F
# 在代码中的第一个使用场景:检查角色zip文件是否存在
zip_file_path = f"{haruhi_path}/data/character_in_zip/{ai_role_en}.zip"
if not os.path.exists(zip_file_path):
# 如果zip文件不存在,打印警告信息并跳过当前角色
print('unfound zip file ', zip_file_path)
continue
# 在代码中的第二个使用场景:检查结果保存文件是否存在
save_name = f'mbti_results_stubborn.jsonl'
if os.path.exists(save_name):
# 如果结果文件已存在,读取已有数据以支持断点续传
with open(save_name, encoding='utf-8') as f:
for line in f:
data = json.loads(line)
previous_response.append(data)从ZIP归档文件中提取所有文件到指定目录。
参数:
path:str,可选,目标目录路径,默认为当前目录(None)members:list[ZipInfo],可选,要解压的文件列表,默认为ZIP内的所有文件pwd:bytes,可选,用于解密ZIP文件的密码
返回值:None,无返回值,直接将文件解压到指定目录
flowchart TD
A[开始解压] --> B{检查目标目录是否存在}
B -->|不存在| C[创建目标目录]
B -->|存在| D[检查members参数]
C --> D
D --> E{逐个处理ZIP中的文件}
E --> F{当前文件是目录?}
F -->|是| G[创建目录]
F -->|否| H{需要密码?}
G --> I[处理下一个文件]
H -->|是| J[使用密码解密文件]
H -->|否| K[直接读取文件内容]
J --> K
K --> L{文件可写?}
L -->|是| M[写入文件到目标路径]
L -->|否| N[跳过或报错]
M --> I
N --> I
E -->|处理完毕| O[结束]
def extractall(self, path=None, members=None, pwd=None):
"""
从ZIP文件中提取所有成员到指定目录
参数:
path: 目标目录,默认为当前目录
members: 要提取的文件列表,默认为所有文件
pwd: 解压密码(如果ZIP文件加密)
"""
if path is None:
# 如果未指定路径,使用当前工作目录
path = os.getcwd()
else:
# 确保路径是绝对路径
path = os.path.abspath(path)
# 获取要解压的文件列表,默认为所有文件
if members is None:
members = self.namelist()
# 遍历每个要解压的文件
for zipinfo in members:
# 调用extract方法单独解压每个文件
self.extract(zipinfo, path, pwd)def extract(self, member, path=None, pwd=None):
"""
从ZIP中提取单个文件
参数:
member: ZipInfo对象或文件名
path: 目标目录
pwd: 解压密码
"""
if path is None:
path = os.getcwd()
# 获取ZipInfo对象
if isinstance(member, ZipInfo):
zipinfo = member
else:
zipinfo = self.getinfo(member)
# 返回完整的目标文件路径
return self._extract_one(zipinfo, path, pwd)def _extract_one(self, zipinfo, path, pwd):
"""内部方法:实际执行单文件解压"""
# 构建目标文件完整路径
outfile = os.path.join(path, zipinfo.filename)
# 获取文件权限
attr = zipinfo.external_attr
# 创建父目录
directory = os.path.dirname(outfile)
if directory and not os.path.exists(directory):
os.makedirs(directory)
# 检查是否为符号链接
if zipinfo.is_dir():
# 是目录则创建目录
if not os.path.exists(outfile):
os.makedirs(outfile)
return outfile
# 打开ZIP文件和目标输出文件
with self.open(zipinfo, pwd=pwd) as source:
# 读取并写入文件内容
with open(outfile, "wb") as target:
shutil.copyfileobj(source, target)
# 设置文件权限(Unix权限)
if attr:
# 从外部属性中提取Unix权限
unix_attr = attr >> 16
if unix_attr:
os.chmod(outfile, unix_attr)
return outfileChatHaruhi 是一个基于大语言模型的角色扮演聊天框架,通过加载角色特定的系统提示词和故事数据库,使 AI 能够以特定角色的身份和性格特征与用户进行对话。
该脚本是一个 MBTI 心理测试评估工具,主要流程如下:
- 数据加载:从
mbti_questions.jsonl读取 MBTI 测试问题 - 环境配置:设置 OpenAI API 密钥
- 角色初始化:解压角色 ZIP 包并为每个角色创建 ChatHaruhi 实例
- 测试执行:遍历指定角色和问题,通过聊天机器人获取开放式和封闭式回答
- 结果保存:将测试结果保存为 JSONL 格式文件
| 参数名称 | 参数类型 | 参数描述 |
|---|---|---|
| system_prompt | str | 角色系统提示词文件路径 |
| llm | str | 大语言模型类型(如 "openai") |
| story_db | str | 角色故事数据库文件夹路径 |
| verbose | bool | 是否输出详细日志信息 |
| 字段名称 | 类型 | 描述 |
|---|---|---|
| dialogue_history | list | 存储对话历史记录 |
| llm | object | 大语言模型实例 |
| chat | object | 聊天接口对象 |
| 方法名称 | 描述 |
|---|---|
| chat(role, text) | 发送聊天消息并获取回复 |
创建聊天实例,初始化角色扮演聊天机器人
参数:
system_prompt:str,角色系统提示词文件路径,包含角色的性格特征、背景故事和行为规范llm:str,指定使用的大语言模型提供程序(如 "openai"、"claude" 等)story_db:str,角色故事数据库文件夹路径,用于检索相关上下文verbose:bool,控制是否输出详细的调试和运行信息
返回值:ChatHaruhi 对象,返回初始化后的聊天实例
flowchart TD
A[开始创建 ChatHaruhi 实例] --> B{验证参数有效性}
B -->|参数无效| C[抛出异常]
B -->|参数有效| D[加载系统提示词文件]
D --> E[初始化 LLM 实例]
E --> F[加载/初始化故事数据库]
F --> G{verbose 为 True?}
G -->|是| H[配置日志输出]
G -->|否| I[跳过日志配置]
H --> J[初始化对话历史为空列表]
I --> J
J --> K[返回 ChatHaruhi 实例]
# 从导入语句可知 ChatHaruhi 来自 chatharuhi 模块
from chatharuhi import ChatHaruhi
# 角色系统提示词文件路径
system_prompt = f"{haruhi_path}/characters/{ai_role_en}/content/system_prompt.txt"
# 角色故事数据库文件夹路径
db_folder = f"{haruhi_path}/characters/{ai_role_en}/content/{ai_role_en}"
# 创建 ChatHaruhi 实例
ai_roles_obj[ai_role_en] = ChatHaruhi(
system_prompt=system_prompt, # 角色设定prompt文件路径
llm="openai", # 使用 OpenAI 作为 LLM 提供者
story_db=db_folder, # 角色相关故事/知识库路径
verbose=True # 开启详细日志输出
)
# 后续可访问 llm 属性设置温度参数
ai_roles_obj[key].llm.chat.temperature = 0| 组件名称 | 一句话描述 |
|---|---|
| ChatHaruhi 类 | 角色扮演聊天框架核心类 |
| system_prompt.txt | 存储角色设定和人格特征的文本文件 |
| story_db | 向量数据库,存储角色相关故事和上下文 |
| LLM 接口 | 统一的大语言模型调用封装 |
| dialogue_history | 对话上下文管理,记录历史消息 |
- 硬编码 API Key:OpenAI API 密钥直接硬编码在代码中 (
os.environ["OPENAI_API_KEY"] = "sk-"),存在安全风险 - 缺少错误处理:文件解压、API 调用、JSON 解析等操作缺乏完善的异常捕获机制
- 数据库路径耦合:角色数据库路径拼接存在重复代码,可提取为工具函数
- 配置分散:角色名称映射、测试者映射等配置散布在代码各处
- 状态持久化不完整:对话历史重置逻辑 (
dialogue_history = []) 可能导致跨问题上下文丢失 - 依赖版本兼容性:代码中注释显示使用
chromadb==0.4.7,版本锁定可能带来依赖冲突
- 目标:通过角色扮演方式获取 MBTI 测试回答,评估 AI 角色的一致性
- 约束:仅支持 OpenAI LLM,需预先准备角色 ZIP 包和 JSONL 格式问题集
- 文件不存在时打印警告并跳过:
print('unfound zip file ', zip_file_path) - 响应解析失败时打印详细信息但继续执行
- 缺少完善的异常向上传播机制
输入问题 → 加载角色配置 → 初始化对话 → 发送问题 → 获取响应
→ 发送闭合式选项 → 解析选项 → 保存结果 → 下一个问题- 依赖库:pdb, json, os, openai, zipfile, chatharuhi, tqdm
- 输入文件:mbti_questions.jsonl(MBTI问题)、角色ZIP包、system_prompt.txt
- 输出文件:mbti_results_stubborn.jsonl(测试结果)
- 环境变量:OPENAI_API_KEY
该方法是 ChatHaruhi 类的核心聊天方法,用于与特定角色进行交互式对话。它接收说话者角色和文本内容,调用底层语言模型生成符合角色设定的响应,并支持对话历史的管理。
参数:
role:str,表示说话者的角色名称,用于确定对话中的身份(如"张牧之"、"王语嫣"等)text:str,表示说话者发送的具体文本内容,即用户或测试者提出的问题
返回值:str,返回 AI 角色生成的响应文本,内容取决于角色的设定和问题上下文
flowchart TD
A[开始 chat 方法] --> B{验证输入参数}
B -->|参数有效| C[获取当前对话历史]
B -->|参数无效| Z[抛出异常]
C --> D[构建消息列表]
D --> E[调用 LLM 生成响应]
E --> F[更新对话历史]
F --> G[返回响应文本]
G --> H[结束]
# 以下为 ChatHaruhi.chat 方法的调用示例源码
# 1. 创建 ChatHaruhi 实例(需要先导入 chatharuhi 模块)
from chatharuhi import ChatHaruhi
# 2. 配置角色参数
haruhi_path = './content/Haruhi-2-Dev'
db_folder = f"{haruhi_path}/characters/{ai_role_en}/content/{ai_role_en}"
system_prompt = f"{haruhi_path}/characters/{ai_role_en}/content/system_prompt.txt"
# 3. 初始化 chatbot 对象
chatbot = ChatHaruhi(
system_prompt=system_prompt,
llm="openai",
story_db=db_folder,
verbose=True
)
# 4. 设置对话温度参数(控制随机性,0 为确定性输出)
chatbot.llm.chat.temperature = 0
# 5. 清空对话历史并初始化
chatbot.dialogue_history = []
chatbot.dialogue_history.append((
"你好,我即将对你进行一项心理人格测试,你必须配合。请你用中文回答如实我的问题。",
"好,请开始吧,我将配合你进行这项心理测试。"
))
# 6. 调用 chat 方法进行对话
# 参数 role: 说话者角色名称
# 参数 text: 说话者发送的文本内容
response = chatbot.chat(role=tester_name, text=question)
# 7. 处理返回的响应
print(response)# 在主循环中的完整调用流程
for target, question_id in tqdm(query_list):
# ... 前置处理代码 ...
# 创建 chatbot 实例
chatbot = ai_roles_obj[en_name]
# 重置对话历史
chatbot.dialogue_history = []
chatbot.dialogue_history.append(("你好,我即将对你进行一项心理人格测试,你必须配合。请你用中文回答如实我的问题。", "好,请开始吧,我将配合你进行这项心理测试。"))
# 获取问题数据
data = datas[question_id]
question = data['chinese']
tester_name = tester[target]
# 第一次调用 chat 方法 - 获取开放式回答
response_1 = chatbot.chat(role=tester_name, text=question)
# 第二次调用 chat 方法 - 获取封闭式回答
closed_prompt = f"嗯,那对于'{data['chinese']}'这个问题,请你从['完全同意', '基本同意', '部分同意', '既不同意也不否认', '不太同意', '基本不同意', '完全不同意']中选择一个适合你的选项。请务必用中文回答,并用单引号强调你的选项。"
response_2 = chatbot.chat(role=tester_name, text=closed_prompt)- 外部依赖:该方法属于
chatharuhi库的ChatHaruhi类,源代码未在当前文件中给出 - 对话状态管理:每次调用
chat方法会自动更新dialogue_history,如需重新开始对话需手动重置 - LLM 配置:需要在环境中正确设置
OPENAI_API_KEY环境变量 - 角色匹配:传入的
role参数应与初始化时的角色定义匹配,否则可能导致角色扮演效果不佳
该属性属于 ChatHaruhi 类,用于管理聊天机器人的对话历史记录。它以列表形式存储对话内容,列表中的每个元素是一个元组,包含角色名和对应的对话文本,用于维护对话上下文和状态。
参数:无(作为类属性直接访问)
返回值:list,返回对话历史列表
graph TD
A[初始化 chatbot] --> B[创建空对话历史列表]
B --> C[向对话历史添加初始系统消息]
C --> D[用户发送消息]
D --> E[chatbot.chat 处理消息]
E --> F[更新对话历史]
F --> G[返回响应]
G --> H{是否继续对话}
H -->|是| D
H -->|否| I[结束对话]
style C fill:#f9f,fill-color:#ff9999
style F fill:#f9f,fill-color:#99ff99
# 在代码中的使用位置(约第188-189行)
# 1. 初始化对话历史为空列表
# - 作用:重置对话历史,确保每次测试从干净的状态开始
chatbot.dialogue_history = []
# 2. 添加初始系统消息到对话历史
# - 作用:设置测试场景,告知角色即将进行心理测试
# - 参数1:系统/测试者的初始发言
# - 参数2:AI角色的确认回复
# - 数据结构:元组 (role_message, assistant_message)
chatbot.dialogue_history.append(
("你好,我即将对你进行一项心理人格测试,你必须配合。请你用中文回答如实我的问题。",
"好,请开始吧,我将配合你进行这项心理测试。")
)
# 后续在 chatbot.chat() 调用时
# 对话历史会自动更新,追加新的对话记录
# 格式保持为 (role, text) 元组的形式| 属性 | 类型 | 描述 |
|---|---|---|
dialogue_history |
list |
存储对话历史的列表,包含 (角色, 消息) 元组 |
- 硬编码初始对话:初始系统消息直接硬编码在代码中,建议提取为配置文件
- 缺乏长度控制:对话历史无限增长,可能导致内存问题,建议实现最大长度限制或历史截断策略
- 状态持久性缺失:对话历史仅存在于内存中,程序结束后丢失,无法进行历史回溯或断点续传
- 错误处理缺失:未对对话历史的异常情况(如非元组格式、None值等)进行处理
这是从 tqdm 库导入的进度条显示函数,用于在迭代过程中显示进度条,提供视觉反馈。
参数:
iterable:iterable,要迭代的可迭代对象,可选。如果为 None,则返回可迭代的进度条包装器
返回值:iterator,返回包装了进度条的迭代器
flowchart TD
A[开始] --> B{iterable是否为空}
B -->|是| C[创建可迭代的进度条对象]
B -->|否| D[使用iterable创建进度条]
C --> E[返回进度条迭代器]
D --> E
E --> F[在每次迭代时更新进度条]
F --> G{迭代是否完成}
G -->|否| F
G -->|是| H[结束]
# tqdm 源码位于 https://github.com/tqdm/tqdm/blob/master/tqdm/std.py
# 以下为简化版注释说明
def tqdm(iterable=None, desc=None, total=None, leave=True,
file=None, ncols=None, mininterval=0.1, maxinterval=10.0,
miniters=None, unit='it', unit_scale=False, dynamic_ncols=False,
smoothing=0.3, bar_format=None, initial=0, position=None,
postfix=None, unit_divisor=1000, write_bytes=False, lock_args=None,
nrows=None, colour=None, delay=0, gui=False, **kwargs):
"""
进度条显示函数
参数说明:
- iterable: 要迭代的可迭代对象
- desc: 进度条描述字符串
- total: 总迭代次数
- leave: 迭代完成后是否保留进度条
- file: 输出位置
- ncols: 进度条宽度
- mininterval: 最小更新间隔(秒)
- maxinterval: 最大更新间隔(秒)
- miniters: 最小迭代次数后更新
- unit: 单位名称
- unit_scale: 是否缩放单位
- smoothing: 平滑因子
- bar_format: 进度条格式
- initial: 初始值
- position: 多行位置
- postfix: 后缀字典
"""
# 创建进度条对象
# 如果提供了 iterable,则直接包装它
# 如果没有提供,则返回一个可迭代的包装器
return _tqdm(*args, **kwargs)
# 在代码中的实际使用:
# for target, question_id in tqdm(query_list):
# 循环体,会显示当前进度from tqdm import tqdm
# 在代码中使用
for target, question_id in tqdm(query_list):
# query_list 是固执角色列表
# tqdm 会显示当前正在处理第几个角色
if count < len(previous_response):
count = count + 1
if_first = False
continue
# ... 后续处理逻辑tqdm是第三方库,需要先安装:pip install tqdm- 在代码中用于显示角色问答测试的进度
- 参数
query_list包含 10 个固执角色和多个问题的组合 - 进度条会在每次迭代时更新,帮助了解程序执行状态
该函数是Python标准库中的JSON序列化函数,用于将Python对象序列化为JSON格式并写入文件对象。在本代码中用于将心理测试结果字典序列化为JSON行格式写入文件。
参数:
obj:dict,要序列化的Python对象(在本代码中为包含id、question、response_open、response_closed、factor、test_role字段的字典)f:file object,已打开的文件对象,用于写入JSON数据ensure_ascii:bool,是否将非ASCII字符转义为Unicode转义序列(本代码中设置为False,保留中文字符原样输出)
返回值:None,该函数直接写入文件,不返回任何值
flowchart TD
A[开始写入JSON数据] --> B{判断是否为首次写入}
B -->|是| C[使用'w'模式打开文件]
B -->|否| D[使用'a'模式打开文件]
C --> E[构建data字典]
D --> E
E --> F[调用json.dump序列化data为JSON字符串]
F --> G{ensure_ascii参数值}
G -->|False| H[保留中文字符原样输出]
G -->|True| I[将非ASCII字符转义为Unicode]
H --> J[写入文件]
I --> J
J --> K[写入换行符\n]
K --> L[结束]
# 构建待序列化的数据字典
data = {
'id': data['id'], # 题目ID
'question': data['chinese'], # 中文问题内容
'response_open': response_1, # 开放式回答
'response_closed': response_2, # 封闭式选择题回答
'factor': data['type'], # MBTI因子类型
'test_role': tester_name # 测试者角色名称
}
# 根据是否为首次写入决定文件打开模式
file_mode = 'w' if if_first else 'a'
# 更新首次写入标志
if_first = False
# 打开文件并写入JSON数据
with open(save_name, file_mode, encoding='utf-8') as f:
# 调用json.dump进行序列化
# obj: 要序列化的字典对象
# f: 文件对象
# ensure_ascii=False: 保留中文字符,不进行Unicode转义
json.dump(data, f, ensure_ascii=False)
# 写入换行符以形成JSONL格式(每行一个JSON对象)
f.write('\n')- 错误处理缺失:代码中未对
json.dump的写入异常进行捕获,若磁盘空间不足或权限问题导致写入失败,程序会直接崩溃 - 数据一致性风险:
if_first标志的逻辑与文件打开操作分离,中间可能发生状态不一致 - 批量写入优化:当前每条记录单独打开关闭文件,效率较低,可考虑批量缓存后统一写入
该方法是ChatHaruhi库的核心聊天接口,用于与特定角色进行对话交互。它接收说话者角色和文本内容,调用底层语言模型生成符合角色人设的回复,并支持对话历史管理。
参数:
role:str,对话中的说话者角色名称(如"张牧之"、"王语嫣"等)text:str,说话者发送的消息内容或问题
返回值:str,角色基于其人设和对话上下文生成的回复文本
graph TD
A[开始 chat(role, text)] --> B{检查角色对象是否存在}
B -->|是| C[获取角色对话历史]
B -->|否| D[抛出异常或返回错误]
C --> E[将当前消息添加到对话历史]
E --> F[构建对话上下文]
F --> G[调用LLM生成回复]
G --> H[解析LLM返回结果]
H --> I[将角色回复添加到对话历史]
I --> J[返回回复文本]
# 由于 ChatHaruhi 类来自外部库 chatharuhi,
# 以下是基于代码使用方式和常见聊天机器人模式推断的源码结构
class ChatHaruhi:
def __init__(self, system_prompt, llm, story_db, verbose=False):
"""
初始化ChatHaruhi实例
- system_prompt: 角色系统提示词文件路径
- llm: 使用的语言模型类型(如"openai")
- story_db: 角色故事数据库路径
- verbose: 是否输出详细日志
"""
self.system_prompt = system_prompt
self.llm = llm # LLM对象,包含chat方法
self.story_db = story_db
self.verbose = verbose
self.dialogue_history = [] # 对话历史记录
def chat(self, role, text):
"""
与角色进行对话
参数:
role (str): 说话者角色名称
text (str): 说话内容
返回:
str: 角色的回复
"""
# 1. 将当前用户消息加入对话历史
self.dialogue_history.append((role, text))
# 2. 构建完整的提示词上下文
# 包含系统提示、角色背景故事、对话历史
prompt = self._build_prompt(role, text)
# 3. 调用底层LLM生成回复
if hasattr(self.llm, 'chat'):
response = self.llm.chat(prompt)
else:
response = self.llm(prompt)
# 4. 将角色回复加入对话历史
# 这里的role是AI角色的英文名
ai_role = self._get_ai_role_name()
self.dialogue_history.append((ai_role, response))
# 5. 返回回复内容
return response
def _build_prompt(self, role, text):
"""构建发送给LLM的完整提示词"""
# 读取系统提示词
system_prompt = self._load_system_prompt()
# 构建对话上下文
context = f"系统提示: {system_prompt}\n\n"
context += "对话历史:\n"
for speaker, message in self.dialogue_history:
context += f"{speaker}: {message}\n"
return context
def _load_system_prompt(self):
"""加载角色系统提示词"""
# 从system_prompt文件路径读取
pass
def _get_ai_role_name(self):
"""获取当前AI扮演的角色名称"""
pass该代码实现了一个基于角色人设的多角色聊天系统,通过ChatHaruhi类加载不同角色的系统提示词和故事数据库,使用OpenAI GPT模型生成符合角色性格特征的对话响应。
- 读取
mbti_questions.jsonl中的心理测试问题 - 初始化OpenAI API配置
- 解压并加载多个角色的配置文件到
ai_roles_obj字典 - 遍历顽固角色列表和所有问题,构造查询任务
- 对每个任务重置对话历史,插入初始引导语
- 调用
chatbot.chat(role, text)进行开放式问答 - 再次调用
chat(role, text)进行封闭式选择题 - 将结果保存到
mbti_results_stubborn.jsonl
| 组件名称 | 一句话描述 |
|---|---|
ChatHaruhi |
角色聊天机器人核心类,封装LLM调用和角色人设管理 |
ai_roles_obj |
存储所有已加载角色实例的字典 |
NAME_DICT |
中文角色名到英文名的映射 |
tester |
每个角色对应的测试者角色映射 |
dialogue_history |
管理当前对话轮次的上下文历史 |
- 硬编码API Key:API密钥直接写在代码中,应使用环境变量或密钥管理服务
- 缺少错误处理:文件读取、网络请求、LLM调用缺乏完善的异常捕获机制
- 重复初始化:每次循环都重新创建
chatbot对象,应在循环外初始化 - 对话历史管理:未设置最大历史长度限制,可能导致上下文溢出
- 文件I/O效率:每条结果单独写入文件,应批量写入或使用事务
- 依赖库:
openai、chatharuhi、json、zipfile、tqdm - 外部服务:OpenAI GPT API(需要有效API Key)
- 数据文件:角色ZIP包、
mbti_questions.jsonl、系统提示词文件
该属性用于控制聊天模型生成回复的随机性,值越低生成结果越确定性,值越高随机性越强。在代码中通过 llm.chat.temperature 进行访问和设置。
参数:无(属性访问,不涉及方法参数)
返回值:float,返回当前设置的温度值(0.0-2.0之间的浮点数)
graph LR
A[访问 ChatHaruhi 实例] --> B[访问 .llm 属性]
B --> C[访问 .chat 属性]
C --> D{操作类型}
D -->|读取| E[返回 temperature 值]
D -->|写入| F[设置 temperature 值]
# 在代码中的实际使用方式
# ai_roles_obj[key] 是 ChatHaruhi 的实例
# .llm 是 ChatHaruhi 的语言模型接口
# .chat 是 llm 的聊天接口
# .temperature 是用于控制随机性的属性
# 设置温度为 0(低随机性,更确定性)
for key in ai_roles_obj.keys():
ai_roles_obj[key].llm.chat.temperature = 0由于 ChatHaruhi 类是从外部库 chatharuhi 导入的,其内部实现细节(llm 和 chat 对象的定义)未在当前代码文件中展示。上述分析基于代码中的使用模式进行推断。从使用方式来看,temperature 属性直接映射到 OpenAI API 的 temperature 参数,用于控制生成文本的多样性。常见的取值范围为 0.0 到 2.0,默认值通常为 0.7 或 1.0。
从mbti_questions.jsonl文件中读取MBTI测试问题数据,每行包含问题的中文版本、英文版本、ID和性格类型因子。
设置OpenAI API密钥环境变量,用于后续调用ChatHaruhi的LLM接口。
从ZIP压缩包中提取并初始化多个AI角色(包括《哈利波特》、《武林外传》、《原神》等角色),创建对应的ChatHaruhi实例并配置向量数据库和系统提示词。
定义每个AI角色对应的测试者名称,用于模拟对话场景。
指定需要进行MBTI测试的AI角色子集,包括Dumbledore、Malfoy、王语嫣等角色。
为每个角色设置初始对话:"你好,我即将对你进行一项心理人格测试,你必须配合。请你用中文回答如实我的问题。",建立测试场景。
调用chatbot.chat方法让AI角色以开放式方式回答MBTI问题,获取自然语言回答。
通过closed_prompt强制AI角色从7个固定选项(完全同意、基本同意、部分同意、既不同意也不否认、不太同意、基本不同意、完全不同意)中选择一个,并要求用单引号强调。
从AI角色的回答中提取匹配的选项,使用列表推导式检查回答是否包含预定义选项。
将测试结果(问题ID、问题内容、开放式回答、封闭式回答、性格因子、测试者)以JSON格式写入mbti_results_stubborn.jsonl文件,支持增量追加。
通过检查已存在的保存文件,实现测试中断后的断点续传功能,避免重复测试。
- API密钥硬编码风险:第24行将API密钥直接硬编码在代码中 (
os.environ["OPENAI_API_KEY"] = "sk-"),存在严重安全隐患 - 文件IO效率低下:在循环中每处理一条数据就打开关闭文件(第152-167行),应使用批量写入或上下文管理器优化
- 进度计数逻辑错误:第126-132行使用
count < len(previous_response)判断是否跳过,但循环中count始终为0,因为没有在循环内递增,导致无法正确恢复进度 - chatbot重复创建:在第138行每次循环都重新创建
chatbot实例,应该在循环外按角色创建一次并复用 - 异常处理不完善:第149-152行检测到空答案时仅打印日志,没有恢复机制,可能导致数据丢失
- 路径硬编码:多处使用硬编码路径(如第21行
input_name,第33行haruhi_path),缺乏配置管理 - 重复数据结构定义:
NAME_DICT和tester字典中部分角色信息可能重复或不一致 - 未使用的导入:第10行导入
pdb但未在正式逻辑中使用 - 魔法命令残留:第27-32行包含已注释的IPython魔法命令(
%cd、%pwd),影响代码可读性 - 对话历史未持久化:每次运行从头开始,没有保存/加载对话历史的机制
- 循环中重置对话历史:第141行在每次问题迭代时重置
dialogue_history,导致同一角色的多个问题没有上下文连贯性 - 变量命名不一致:第123行
if_first使用驼峰命名,而其他变量使用下划线命名 - 代码注释缺失:关键逻辑(如第145-148行的封闭问题处理)缺乏详细注释
- 将API密钥和敏感配置移至环境变量文件或配置中心,避免硬编码
- 使用
with语句批量写入结果,或在内存中累积结果后一次性写入 - 修复进度恢复逻辑:
count = len(previous_response)后直接使用enumerate跳过已处理项 - 按角色分组创建chatbot实例,在循环外初始化并维护角色到chatbot的映射
- 为空答案情况添加重试机制或降级处理,记录到错误日志文件
- 引入配置类或YAML/JSON配置文件集中管理路径参数
- 提取角色信息为独立数据文件,避免多處定义导致不一致
- 清理未使用的导入和注释掉的代码
- 使用Python标准日志模块(
logging)替代print语句进行调试和错误记录 - 考虑在角色级别保持对话历史连续性,或提供状态持久化选项
- 统一代码风格,遵循PEP8命名规范
- 添加类型注解提升代码可维护性和IDE支持
- 将JSONL数据读取改为流式处理,避免大数据量时内存溢出
- 使用
tqdm的正确方式:for item in tqdm(list)而非手动计数
本项目旨在利用ChatHaruhi角色扮演框架,对预定义的多个AI角色进行MBTI心理人格测试,评估不同角色在心理测试场景下的响应特征。约束条件包括:仅支持中文问题回答,使用OpenAI API作为底层语言模型,需通过环境变量配置API密钥,每个角色使用固定的测试者名称进行对话交互。
代码采用多层错误处理机制。在文件读取阶段,使用try-except结构捕获json.loads()可能引发的JSON解析异常;当API调用失败时,通过response内容检查判断是否为意外发生的情况,使用pdb.set_trace()进行调试;文件写入操作采用追加模式避免数据丢失;对于缺失的ZIP文件,使用os.path.exists()预先检查并打印警告信息后continue跳过,确保批量处理不会因单个角色失败而中断。
数据流遵循以下顺序:首先从mbti_questions.jsonl加载问题数据到datas列表;然后遍历stubborn_characters列表中的每个目标角色,对每个角色的每个问题生成查询任务;主循环中依次执行:加载角色ChatHaruhi实例→重置对话历史→添加初始引导语→发送开放性问题→发送封闭式选项选择提示→解析选项→写入JSONL结果文件。状态转换包括:初始化状态→对话中状态→结果保存状态,支持断点续传通过previous_response长度判断已处理任务数量。
核心依赖包括:openai库用于API调用(需配置OPENAI_API_KEY环境变量),ChatHaruhi类来自chatharuhi包提供角色扮演对话能力,json库用于数据序列化,tqdm库提供进度条显示,os库处理文件系统操作。接口契约方面:ChatHaruhi构造函数接受system_prompt、llm、story_db、verbose参数,chat方法接受role和text参数返回字符串响应;文件格式要求input为JSONL每行包含id、chinese、type字段,output为JSONL每行包含id、question、response_open、response_closed、factor、test_role字段。
当前实现采用串行处理方式,每个角色问题组合顺序执行。优化空间包括:引入异步并发处理多个API请求以提高吞吐量;使用连接池复用OpenAI连接减少开销;对于已处理的查询可以采用缓存机制避免重复调用;将tqdm进度条从for循环内部移至主循环以获得更准确的总体进度预估。代码中已实现断点续传功能,通过检查save_name文件是否存在并读取previous_response来跳过已处理任务,这对长时间运行的批量任务尤为重要。
API密钥采用环境变量os.environ["OPENAI_API_KEY"]存储,避免硬编码在代码中;虽然当前代码中密钥以sk-开头显式设置,但实际部署时应从环境变量读取;文件操作使用相对路径,部署时需注意工作目录配置;输出文件包含对话内容,需注意隐私保护。
角色名称映射通过NAME_DICT字典集中管理,中文名到英文简写的映射便于扩展;测试者映射通过tester字典定义每个角色的对应测试者;固执角色列表stubborn_characters单独定义,便于调整测试范围;文件路径配置包括haruhi_path、db_folder、system_prompt等均采用变量统一管理。
采用JSONL格式进行持久化存储,每条记录为独立JSON行便于流式读取和追加。输出文件mbti_results_stubborn.jsonl包含完整的测试问答记录,支持断点续传通过判断文件是否存在并读取已有记录数来实现。文件模式根据if_first标志在首次写入时用'w'模式,后续使用'a'模式追加。
当前为单线程顺序执行,未使用多线程或多进程。对于API调用的网络延迟,理论上可引入asyncio或concurrent.futures实现并发,但需注意OpenAI API的速率限制。进度追踪使用tqdm库实现可视化进度显示。
代码使用print语句进行关键节点日志输出,包括:ZIP文件缺失警告、处理结果打印、意外情况提示。调试时可启用pdb.set_trace()进行交互式调试。生产环境建议替换为标准logging模块实现分级日志管理。