
简介这是一份面向人工智能问答系统与聊天机器人开发者的心理咨询语料资源适合从事情感计算、对话系统或心理健康应用方向的学习者与研究者使用。压缩包共8个文件以Python脚本、示例图片、Shell脚本及配置文件为主整体约184KB体量轻便便于快速接入现有项目或作为实验起点。该语料库包含约20000条中文心理咨询多轮对话并附带分类信息是心理咨询领域较早开放的QA数据集之一标注过程面向多轮对话平均每条耗时超过一分钟数据质量与结构均经过较细致整理。资源中提供了数据加载与演示脚本可帮助读者直接跑通问答流程理解多轮对话的组织方式与字段含义并在此基础上开展检索、分类或生成式对话实验。目前已有432人学习下载适合作为心理咨询问答与聊天机器人方向的入门实践素材。1. 心理咨询问答语料库一份能直接跑起来的 2 万条中文对话数据做心理咨询方向的问答系统最头疼的从来不是模型选型而是数据。通用闲聊语料训练出来的机器人用户一开口说“最近总失眠觉得活着没意思”它回一句“哈哈今天天气不错”这种翻车现场我见过太多次。Emotional First Aid Dataset 这个项目就是冲着这个痛点来的——它是目前公开的、规模较大的中文心理咨询 QA 语料库之一包含约 20000 条心理咨询对话数据而且不是单轮的一问一答是多轮对话还带分类标签。整个仓库以efaqa-corpus-zh-master的形式打包里面有setup.py、efaQA_corpus_zh包、data目录、demo.py和publish.sh拿到手就能装、能读、能跑 demo。它适合两类人一类是想做心理咨询问答系统或情感支持聊天机器人的开发者另一类是在做人工智能大作业、毕业设计需要一个真实中文对话数据集来撑起实验的同学。下面我按“这数据长什么样 → 怎么读进来 → 怎么训一版 baseline → 坑在哪”的顺序拆一遍。2. 语料结构拆解20000 条多轮对话到底存了什么2.1 仓库目录与数据组织方式先把包解开看目录结构。efaqa-corpus-zh-master是典型的 Python 包布局核心数据放在efaQA_corpus_zh/data下__init__.py暴露读取接口demo.py给了一个最小可运行示例setup.py负责安装publish.sh是作者发布用的脚本跟使用关系不大。assets下的1.jpg、2.jpg是配图不影响数据。这个语料的关键特征有三个直接决定你后面怎么用多轮对话一条样本不是孤立的一问一答而是带上下文的对话片段。标注过程面向多轮平均每条标记耗时超过 1 分钟说明标签是人工认真打的不是机器批量刷的。带分类信息除了对话文本还有分类标签可以拿来做意图识别或情绪分类的辅助任务。中文心理咨询领域用词、语气、话题都偏专业场景跟微博、贴吧那种野生闲聊语料完全不是一个分布。常见做法是先把数据加载成列表每条是一个 dict包含对话轮次和标签。我一般会先统计一下轮次分布和标签分布确认没有严重的长尾再往下走。2.2 用 Python 把语料读进内存安装和读取的步骤不复杂但有几个参数容易搞错。先装包再加载# 进入仓库根目录后安装-e 表示可编辑安装方便改源码 cd efaqa-corpus-zh-master pip install -e .# demo_read.py from efaqa_corpus_zh import EfaqaCorpus # 常见入口类名以 __init__.py 实际导出为准 # 初始化语料对象data_dir 指向包内 data 目录 corpus EfaqaCorpus(data_direfaQA_corpus_zh/data) # 拉取全部样本 samples corpus.load() # 返回 list每条是一个对话样本 print(样本总数:, len(samples)) # 看第一条长什么样 first samples[0] print(轮次数:, len(first.get(dialog, []))) print(标签:, first.get(label)) for turn in first.get(dialog, [])[:3]: print(turn.get(role), -, turn.get(text)[:40])逻辑说明EfaqaCorpus是包对外暴露的读取入口load()把磁盘上的原始文件解析成结构化对象。参数上data_dir必须指向真实存在的数据目录路径写错会直接抛FileNotFoundError这是新手第一个卡点。dialog字段是轮次列表每轮带角色和文本label是分类标签。不同版本字段名可能略有差异读之前先print(first.keys())确认一遍比对着文档猜要快。2.3 把多轮对话转成训练样本原始多轮对话不能直接喂给模型得先决定任务形态。做检索式问答就把每轮的用户输入和对应回复拆成 pair做生成式就保留上下文窗口。下面这个转换脚本是我常用的写法# build_pairs.py from efaqa_corpus_zh import EfaqaCorpus corpus EfaqaCorpus(data_direfaQA_corpus_zh/data) samples corpus.load() pairs [] for s in samples: dialog s.get(dialog, []) # 滑动窗口把相邻的 user-assistant 组成一对 for i in range(len(dialog) - 1): cur, nxt dialog[i], dialog[i 1] if cur.get(role) user and nxt.get(role) assistant: pairs.append({ query: cur[text].strip(), response: nxt[text].strip(), label: s.get(label), }) # 过滤空样本和过短样本避免噪声进训练集 pairs [p for p in pairs if len(p[query]) 2 and len(p[response]) 2] print(可用问答对:, len(pairs))逻辑说明滑动窗口按角色配对只保留user接assistant的相邻轮次避免把两段用户输入错配成问答对。参数上长度阈值 2是经验值用来滤掉“嗯”“哦”这类无信息样本如果你的任务对短回复敏感可以调低。label一并带出来后面做多任务训练时能直接用。这一步产出的pairs就是检索式问答的标准输入格式。3. 基于这份语料搭一版检索式问答 baseline3.1 为什么先做检索式而不是生成式拿到心理咨询语料很多人第一反应是上大模型做生成。我的血泪经验是先做检索式。原因很实在——心理咨询场景对回复的安全性要求极高生成模型容易一本正经地胡说检索式至少保证回复来自真实语料不会凭空造出危险建议。而且这份语料本身就是 QA 结构天然适合检索。baseline 用 TF-IDF 或句向量做召回几十行代码就能跑通先验证数据质量和任务可行性再决定要不要上生成。3.2 TF-IDF 召回的最小实现# retrieval_baseline.py from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity import jieba # 中文需要先分词 # 假设 pairs 来自上一节的转换结果 queries [p[query] for p in pairs] responses [p[response] for p in pairs] # 中文分词后再喂给 TF-IDF否则按字切效果差 def tokenize(text): return .join(jieba.cut(text)) corpus_tokens [tokenize(q) for q in queries] vectorizer TfidfVectorizer(max_features50000, ngram_range(1, 2)) tfidf_matrix vectorizer.fit_transform(corpus_tokens) def retrieve(user_input, topk3): vec vectorizer.transform([tokenize(user_input)]) sims cosine_similarity(vec, tfidf_matrix)[0] idx sims.argsort()[::-1][:topk] return [(queries[i], responses[i], round(float(sims[i]), 4)) for i in idx] # 试一条 for q, r, score in retrieve(我最近总是睡不着心情很低落): print(score, |, q[:30], -, r[:40])逻辑说明TfidfVectorizer的max_features控制词表规模50000 对 2 万条语料够用ngram_range(1,2)引入二元词组能捕捉“睡不着”“情绪低落”这类固定搭配。中文必须先分词jieba是最省事的方案。retrieve返回相似度最高的 topk 条分数用于人工检查召回质量。这一步跑通你就有了一个能对话的雏形。3.3 换成句向量提升语义召回TF-IDF 的短板是字面不匹配就召回不到比如“睡不着”和“失眠”它认为是两个词。换成句向量能缓解这个问题# embedding_retrieval.py from sentence_transformers import SentenceTransformer import numpy as np model SentenceTransformer(shibing624/text2vec-base-chinese) # 常见中文句向量模型 query_emb model.encode(queries, normalize_embeddingsTrue, batch_size64) resp_emb model.encode(responses, normalize_embeddingsTrue, batch_size64) def retrieve_emb(user_input, topk3): vec model.encode([user_input], normalize_embeddingsTrue)[0] sims np.dot(query_emb, vec) # 已归一化点积即余弦 idx sims.argsort()[::-1][:topk] return [(queries[i], responses[i], round(float(sims[i]), 4)) for i in idx]逻辑说明normalize_embeddingsTrue让向量单位化之后点积直接等于余弦相似度省一次除法。batch_size64是显存和速度的折中2 万条编码一次几分钟能跑完。模型名以你本地实际能加载的为准换模型只需改这一行。句向量对同义改写更鲁棒但计算成本比 TF-IDF 高线上服务要提前把语料向量算好存下来别每次请求都重算。4. 避坑与常见问题排查4.1 数据加载报路径错误现象FileNotFoundError或data_dir not exists。原因data_dir写的是相对路径但你的工作目录不在仓库根目录。解决用os.path.dirname(__file__)拼绝对路径或者先os.chdir到仓库根目录再加载。别用硬编码的绝对路径换台机器就废。4.2 字段名对不上导致 KeyError现象first[dialog]抛KeyError。原因不同版本或不同读取方式返回的字段名不一致有的用dialog有的用conversation或turns。解决加载后先print(samples[0].keys())按实际字段名取值别照抄网上的示例代码。4.3 中文没分词导致召回质量差现象TF-IDF 召回结果驴唇不对马嘴。原因直接把整句中文丢给TfidfVectorizer它按空格切中文整句变成一个 token。解决先jieba.cut再喂进去或者直接用句向量方案绕开分词。这个坑我踩过排查了半天才发现是分词问题。4.4 多轮上下文被截断现象模型回复答非所问因为丢了上文。原因转换时只取了单轮 pair把多轮对话拍平了。解决如果任务需要上下文保留最近 N 轮拼成输入N 一般取 3 到 5或者用带对话历史建模的方案。别默认单轮就够心理咨询里“他刚才说的那件事”这种指代很常见。4.5 标签分布不均导致分类头失效现象分类任务准确率虚高但少数类全错。原因语料标签长尾多数类样本压倒性多。解决先统计label分布对少数类做重采样或调 class weight。别只看整体准确率要看每类的召回。5. 进阶技巧用分类标签做意图路由把召回准确率再抬一档baseline 跑通之后真正拉开效果差距的往往不是换更大的模型而是把语料自带的分类标签用起来。这份语料每条都带分类信息等于免费送了一个意图标签不用白不用。我的做法是做一个两级结构先分类再检索。具体来说训练一个轻量文本分类器输入用户当前这句话输出它属于哪个类别然后在检索阶段只在该类别对应的子语料里做相似度匹配。这样做的收益很直接——候选集从 2 万条缩到几千条既提速又降噪尤其是那些跨类别但字面相似的 query不会再被错误召回。# intent_router.py from sklearn.linear_model import LogisticRegression from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.model_selection import train_test_split import jieba # 用 query 和 label 训练意图分类器 X_text [p[query] for p in pairs] y [p[label] for p in pairs] def tokenize(t): return .join(jieba.cut(t)) vec TfidfVectorizer(max_features30000) X vec.fit_transform([tokenize(t) for t in X_text]) X_tr, X_te, y_tr, y_te train_test_split(X, y, test_size0.2, random_state42) clf LogisticRegression(max_iter1000, class_weightbalanced) clf.fit(X_tr, y_tr) print(分类准确率:, clf.score(X_te, y_te)) def route_and_retrieve(user_input, topk3): pred clf.predict(vec.transform([tokenize(user_input)]))[0] # 只在预测类别内检索 sub [(q, r) for q, r, lb in zip(queries, responses, y) if lb pred] # 这里接上一节的向量检索候选集换成 sub return pred, sub[:topk]逻辑说明class_weightbalanced是应对标签长尾的关键参数不加的话少数类基本学不到。random_state42保证实验可复现。route_and_retrieve先预测类别再把检索范围限制在该类别内。参数上max_iter1000是逻辑回归收敛的保险值语料大时可能需要调高。这里有个容易忽略的点分类器本身也会错一旦路由错了后面检索再准也白搭。所以我的习惯是给分类器设一个置信度阈值低于阈值就回退到全量检索宁可慢一点也别答错。这个阈值用验证集调一般设在 0.6 到 0.7 之间比较稳。验证方法上别只看分类准确率这一个数。我会额外看两个指标一是路由后的 top1 召回率跟全量检索对比确认路由确实带来提升二是路由错误时的回退比例如果回退太频繁说明分类器不够用得回去补数据或换模型。这两个数一起看才能判断这套两级结构到底值不值得上。从那以后我每次拿到带标签的语料都强制先跑一遍标签分布和分类基线再决定要不要上更复杂的方案——因为很多时候把现成的标签用对比换模型管用得多。希望这份拆解能帮到你数据拿到手先跑通 demo再按自己的任务改转换脚本别一上来就堆模型。本文还有配套的精品资源点击获取