简介:这是一套面向在线教育平台评价场景的多智能体情感分析系统,基于CrewAI框架实现多个专业化角色协同分析学习者评论,并输出课程质量评估结果。系统内置登录鉴权、任务选择、数据文件上传、情感分析、结果对话与历史记录查看等模块,默认对接DeepSeek智能体,也支持切换不同智能体模型,适合教育技术研究者、自然语言处理学习者以及在线课程运营人员参考。资源共五十三个文件、约四点四兆,以网页模板、样式脚本、后端代码、配置文件和图片图表为主,另有示例数据、数据库文件与工程配置,便于直接运行和二次开发。从压缩包目录可见,前端页面、后端逻辑、数据导入脚本与说明文档齐全,能够帮助读者理清多智能体任务分配、情感分析流水线及可视化展示的完整流程。已有七十二人学习下载,适合希望快速上手智能体评价系统搭建的开发者。
1. 教育平台评价系统:为什么情感分析要拆给多个智能体
某教育平台上线了匿名评价入口,三千条评语涌进来之后,运营同学当晚就没睡成觉:“课太难了”和“难得好课”同时命中“难”这个关键词,被自动判成负面;“平台卡成PPT”既不骂脏话也不含常见情绪词,被老老实实归入中性。这类评价系统如果只做一个情感极性分类器,注定翻车——因为情感分析在教育场景里真正要回答的不是“好评还是差评”,而是“谁在说话、在说哪个对象、情绪到底有多强、下一步该处理什么”。我拆过一套多智能体教育平台评价系统,思路是把情感分析拆成多个智能体分工:一个负责判情绪,一个负责抓对象,一个负责提建议,最后由仲裁逻辑合并结果。语言模型负责出判断,规则负责兜底,既有容错性也不至于变成黑匣子。这套做法对课程评价、师生反馈、在线教学平台运营都有直接参考价值,下面把架构、参数和踩坑一次讲透。
2. 整体架构与数据流:五个角色的职责边界
2.1 数据链路:从评语提交到看板展示
整套评价系统不是“一个情感模型”,而是一条清晰的数据链路:评语从客户端进来,先经过清洗和匿名化,再并行分发给不同智能体,各自产出结构化结果,最后由汇总服务合并、仲裁、落库,最终呈现在评价看板上。
我一般会把链路分成五段,每段都有明确的输入输出:
评语输入 -> 清洗与匿名化 -> 智能体任务分发 -> 结果仲裁与合并 -> 存储与看板第一段卡得最狠。匿名化不能只做“把学号打码”,评语里常出现“王老师”“李老师”这类称呼,如果不做实体脱敏,后续统计和导出都有合规风险。常见做法是维护一个称呼词典,凡是匹配到“X老师”“X同学”统一替换成“该老师”“该同学”。第二段的清洗策略要以“保留语义”为前提,不能简单删停用词,因为情感判断恰恰依赖那些口语词。
分发阶段才是多智能体真正起作用的地方。同一个标准化评语文本,会同时发给情感判别智能体、主题归因智能体和建议提取智能体,三者彼此独立、互不干扰。每个智能体返回 JSON,统一格式、统一置信度字段,仲裁层拿到后再合并成一条完整评价记录。
2.2 角色拆分:谁来判断情绪、谁负责归因
多智能体在这里不是“一个模型跑三次”这么敷衍,而是真正把任务边界切开。我在这个项目里定了五个角色,职责不能混。
| 智能体角色 | 输入 | 输出 | 典型判断逻辑 |
|---|---|---|---|
| 情感判别智能体 | 清洗后的评语文本 | 极性、情感分数、情绪标签列表 | 分析整体语气、情绪强度和隐含态度 |
| 主题归因智能体 | 评语文本加候选主题列表 | 对象列表及对应情感 | 先抽取评价对象,再判断每个对象的情感 |
| 建议提取智能体 | 评语文本 | 建议句原文或空 | 从“希望”“建议”“能不能”等句式里抓建议 |
| 异常识别智能体 | 评语文本加历史统计 | 是否异常、异常类型 | 重复内容、刷屏词、超长文本识别 |
| 汇总仲裁智能体 | 上述四个结果 | 最终评价记录 | 去重、投票、置信度加权、冲突消解 |
情感判别智能体管“情绪方向”,它的输出是polarity和emotions。主题归因智能体管“在说谁”,很多评语同时包含课程难度和平台卡顿两个对象,如果只给一个整句极性,统计必然失真。建议提取智能体是为了把“改进项”从评价里抽出来直接变成工单,这一步看起来简单,实际过滤掉的无效内容很多。异常识别智能体负责防作弊,比如连续十条“老师很好”重复提交,必须先拦下来,不能进入统计。
五个角色之间不存在上下级,仲裁器只在最终汇总时介入。这种扁平结构的好处是单个角色可以独立替换模型、单独调参,不会牵一发动全身。
2.3 存储设计:评价结果如何回填平台
多智能体的价值要落到数据上,落库结构没设计好,后面做趋势分析会非常痛苦。我建议按评价事实、结构化结果、人工复核状态三块来存,核心表可以这样建:
CREATE TABLE review_result ( review_id VARCHAR(64) PRIMARY KEY, course_id VARCHAR(32), teacher_id VARCHAR(32), raw_text TEXT, clean_text TEXT, polarity VARCHAR(16), -- positive / negative / neutral sentiment_score FLOAT, -- 0~1,越接近1越正面 emotions JSON, -- ["satisfied","confused"] aspect_tags JSON, -- [{"aspect":"teaching","polarity":"negative"}] suggestion_text TEXT, confidence FLOAT, status TINYINT, -- 0待复核 1已复核 2已忽略 created_at DATETIME, reviewed_at DATETIME );这里的emotions和aspect_tags都用 JSON,好处是模型输出的标签数量不固定,不用频繁改表结构。confidence字段非常关键,它是仲裁阶段算出来的最终置信度,低于阈值的记录会进入人工复核队列,而不是直接对用户发布。
看板层不要直接读这张原始表,建议提前聚合出三张统计表:按课程维度的情感分布、按老师维度的负面主题占比、按周粒度的情感趋势。聚合口径最好在写入时就定死,避免看板查了十秒还没出来,运营侧早就没耐心了。
提示:清洗后的
clean_text一定要单独存,不要覆盖raw_text。很多事故都是因为后来想复现误判,结果原始文本已经被洗没了。
3. 情感分析核心模块:语言模型出分数、规则兜底防翻车
3.1 用语言模型做结构化情感输出
情感分析模块是整个系统里被问得最多的部分。早期方案大多用关键词词典或统计分类器,但在“课太难了”和“难得好课”这种场景里识别率非常差。后来切到语言模型之后,准确率才有明显提升,但语言模型不能直接输出一段散文再交给下游,必须用结构化约束把它变成严格 JSON。
我先给一个最精简的调用示意,模型服务用你自己部署的或内部网关都行,关键是输出格式:
import json def llm_chat(messages, temperature=0.1, max_tokens=300): # 调用内部模型服务的封装函数,这里省略传输细节 response = model_service.chat(messages, temperature=temperature, max_tokens=max_tokens) return response["content"] def analyze_sentiment(review_text): schema_hint = ( '返回严格JSON,不要输出任何其他文字。' '格式:{"polarity":"positive/negative/neutral",' '"score":0.0~1.0,' '"emotions":["satisfied","happy","confused","angry"],' '"reason":"一句话说明判断依据"}' ) prompt = f"{schema_hint}\n评语内容:{review_text}" raw = llm_chat([ {"role": "system", "content": "你是教学评价领域的情感分析助手,只做判断、不写评价。"}, {"role": "user", "content": prompt} ], temperature=0.1, max_tokens=300) try: result = json.loads(raw.strip()) except json.JSONDecodeError: # 遇到模型输出不标准的情况,强制走规则兜底 result = rule_based_fallback(review_text) return result这里的温度参数必须压到 0.1 左右,不是玄学,是为了让多次调用结果尽量稳定。max_tokens给 300 足够,因为只输出 JSON 而不是一段话,给太多反而容易让模型自由发挥、夹带解释性文字。system角色里我加了“只做判断、不写评价”,能有效减少模型输出里附带“这句评语表达了……”之类的前缀。
如果模型偶尔返回非 JSON,直接把它判定为失败走规则兜底,而不是尝试二次解析。因为失败通常是生成序列跑偏了,再去拿同一个文本重试往往还是失败的,浪费延迟不说,还可能把错误结果写入数据库。
3.2 用规则修正口语化评语
语言模型再强,在教育评语这种口语化严重的文本上也会飘。最常见的三类问题是:双否定结构、程度词叠加、表情符号和网络梗。规则层不需要覆盖所有语法,只需要兜住模型最不稳定的那几个点。
NEGATION_WORDS = {"不", "没", "无", "别", "难", "非", "未", "莫"} DOUBLE_NEG_PATTERNS = [ ("不是", "不好"), ("没有", "不"), ("不能", "不"), ] def rule_based_fallback(text): polarity = "neutral" score = 0.5 # 双重否定检测:整体语义会被拉回中性或偏正 for first, second in DOUBLE_NEG_PATTERNS: if first in text and second in text: polarity = "positive" if "难得好" not in text else "neutral" score = 0.6 return {"polarity": polarity, "score": score, "source": "rule_fallback"} # 口语程度词修正 intensifiers = {"巨": 0.2, "非常": 0.15, "极其": 0.2, "有点": -0.1, "一般般": -0.15} base_polarity = "positive" if "好" in text or "赞" in text else "negative" return {"polarity": base_polarity, "score": score, "source": "rule_fallback"}这段代码只是示意,真正落地时键值对会维护得很长。双重否定之所以必须用规则,是因为语言模型对“我不是说老师不好”这种句子经常理解成负面,人读出来其实是中偏正。这里的判断顺序也重要,先查双重否定、再做普通极性匹配,否则“不是”会被首轮判成负面。
表情符号也得单独处理。评语里“老师讲得不错”后面跟一个笑脸,文本内容本身不含情绪词,但人一眼就知道是正面。见过不少项目栽在这上面,因为预处理阶段把表情符号直接删掉了。正确姿势是转成语义等价词,笑脸转成“开心”,哭脸转成“难过”,再喂给模型。规则层不是技术的倒退,它是在给模型减负,把稳定可枚举的语言现象先消化掉。
3.3 关键参数怎么选:温度、长度和批量
参数选择这块非常看场景。教育评价情感分析属于“低创造、高稳定性”任务,所有生成式参数都要往保守方向调。
温度建议固定为 0.1,有些团队觉得 0 更稳,但实测 0 时部分模型服务端仍会有浮点采样的微小波动,0.1 反而能避开某些服务的固定坏点。top_p 需要配合调,常见做法是 0.8 到 0.9 之间,太高会引入无关词汇,太低会使输出结构僵硬。最大长度不用给大,评语本身不超过两三百字,JSON 输出控制在 200 token 以内足够,给长了模型容易在结尾补充废话。
批量推理要特别小心。很多人图省事,把一千条评语拼进一个 prompt 让模型一次处理,性能确实上去了,但结果非常不稳定。模型在长上下文里的格式遵循能力会衰减,比如第 20 条之后开始丢字段、混序。我一般控制每个 batch 不超过 32 条,并且每条之间用编号分隔,解析时按编号切分而不是按换行切分,这样能减少漏项。
如果要用 GPU 推理,显存和 batch size 是直接相关的。一个几百 MB 的中等规模模型,16G 显存下 batch 开到 8 比较稳,开到 16 就可能 OOM。优先保证推理速度稳定的办法不是堆 batch,而是把输入文本截断到 128 个 token,评语超过这个长度后截掉后半段,因为情感判断主要依赖前半段的语气和态度词,结尾大多是客套话,截掉对准确率影响不大。
4. 多智能体协同:投票仲裁与结果一致性
4.1 三个角色Prompt的设计
多智能体的效果好坏,八成取决于 Prompt 角色的边界划分和格式约束。情感判别、主题归因、建议提取三个角色的输出目标完全不同,Prompt 不能套同一个模板。下面给出我自己常用的角色 Prompt 设计:
SENTIMENT_ROLE = { "name": "情感判别员", "system_prompt": ( "你是教学评价情感判别员。你的任务只有一项:判断评语的整体情感方向。" "不要抽取评价对象,不要输出建议,只回答情感极性和强度。" "输出必须严格遵循JSON格式。" ) } ASPECT_ROLE = { "name": "主题归因员", "system_prompt": ( "你是教学评价主题归因员。你的任务是找出这条评语在评价哪些对象," "对象限定为:教学、内容、平台、互动、作业、环境。" "每个对象都要单独给出情感方向,允许同时出现多个对象," "输出必须严格遵循JSON格式。" ) } SUGGESTION_ROLE = { "name": "建议提取员", "system_prompt": ( "你是教学评价建议提取员。只提取评语中明确的改进建议," "没有建议就返回空数组。不要使用评语中的负面情绪," "输出必须严格遵循JSON格式。" ) }三个角色之间有个微妙的地方:情感判别员和主题归因员的输出可能冲突,比如整体极性是负面,但“平台美观度”这个对象却是正面的。这种冲突不能丢给下游,必须留到仲裁层处理。角色隔离的另一个好处是模型不会被“既要又要”的复杂指令搞糊涂,实测拆开之后每个角色的字段完整率都更高了。
每个角色的输出示例最好在 prompt 里给一到两条 few-shot。教育评语特有的表达,比如“老师水”“作业巨多”“课件很精美”,一定要放在示例里,模型见过这类口语后输出质量完全两样。
4.2 仲裁机制:从多数投票到置信度加权
仲裁层最容易犯的错是“谁票多听谁的”。教育评语场景里,情绪复杂的句子往往所有角色都拿不准,这时投票投出来的结果也没有意义。我的做法分三档:意见完全一致直接采信;不一致但存在多数派,按置信度加权;完全对立则标注为待人工复核。
from collections import Counter def arbitrate(results): """ results: list[dict], 每个元素包含 polarity, score, confidence """ polarities = [r["polarity"] for r in results] counter = Counter(polarities) if len(counter) == 1: return results[0] # 有明确多数派时,用置信度加权后比较 majority_p, majority_count = counter.most_common(1)[0] if majority_count >= len(results) * 0.6: weighted = {} for r in results: weighted[r["polarity"]] = weighted.get(r["polarity"], 0) + r["confidence"] * r["score"] final_polarity = max(weighted, key=weighted.get) else: # 完全分裂,不做自动判断 return {"polarity": "unknown", "need_review": True} avg_score = sum(r["score"] for r in results if r["polarity"] == final_polarity) / majority_count return { "polarity": final_polarity, "score": round(avg_score, 2), "need_review": False, "confidence": counter[final_polarity] / len(results), }这段仲裁逻辑里有个关键点:need_review字段。分裂场景宁可多进人工复核,也不要硬给结论。置信度加权比简单多数票更适合教育评语,因为负面评语的表达往往更含蓄,比如“老师人很好,就是讲课重点不清晰”,两个角色可能因为侧重点不同给出相反极性,直接投票会丢掉信息。
为了达到投票效果,同一个评语一般让情感判别智能体跑三次再汇总。如果部署资源有限,可以把 temperature 从 0.1 稍调到 0.3 做三次独立采样,再走仲裁,效果可以逼近三个不同模型投票。
4.3 效果评估:一致性系数与人工抽检
多智能体系统上线前必须做一致性评估,否则没人敢信它的输出。我常用两个指标:跟人工标注的准确率一致性,以及多智能体重复调用的稳定性。
def cohens_kappa(y1, y2, k): # y1: 人工标注列表, y2: 系统输出列表, k: 类别数 n = len(y1) matrix = [[0] * k for _ in range(k)] for a, b in zip(y1, y2): matrix[a][b] += 1 p0 = sum(matrix[i][i] for i in range(k)) / n row_sums = [sum(row) for row in matrix] col_sums = [sum(matrix[i][j] for i in range(k)) for j in range(k)] pe = sum(row_sums[i] * col_sums[i] for i in range(k)) / (n * n) return (p0 - pe) / (1 - pe) if pe != 1 else 1.0Cohen's Kappa 值高于 0.7 说明系统与人工标注达到强一致,低于 0.5 就要回去调 Prompt 或规则层。人工抽检不需要全部标注,每次随机抽 200 条就够了,但抽样时要保证正负样本都有,不能全抽正面评语,否则算出来的 Kappa 会虚高。
稳定性评估更简单:取 100 条高复杂度评语,每条调用系统三次,计算三次结果一致的占比。我见过的项目里,这个值通常在 85% 到 95% 之间,低于 85% 说明温度或者 prompt 设计有问题,需要降温。把一致性测出来的 badcase 喂回 few-shot 示例,是目前成本最低的优化手段。
提示:一致性评估一定要在真实语料上做,不要拿人工编的测试集糊弄。真实评语里的口语连招和句式残缺,是测试集里永远造不出来的。
5. 避坑手册:六次真实事故与处理记录
5.1 重复评语刷屏导致数据失真
现象:某门课程一周内收到 400 条评语,其中 180 条是同一句话“老师超级棒,推荐!”,正面占比从 62% 飙到 89%。
原因:清洗阶段没有做文本去重,同源评语被多次提交。
解决:在存储层加唯一键。计算评语文本的哈希值,对原始文本先做归一化(去空格、统一全半角、小写化),再算 MD5 存入review_dedup_hash字段。同一哈希值的评语只在统计里保留第一条,后续全部标记为status=2即已忽略。如果同一用户对同一门课有多条不同评语,也要在业务层限制提交次数。
5.2 模型输出格式不稳定导致解析失败
现象:语言模型在连续调用中出现两次把polarity值返回成“正面”而不是规范的positive,解析层直接抛异常。
原因:模型在低温度下仍然可能受上下文影响改变措辞,尤其是提示词里没有给出合法的枚举值范围。
解决:解析层不信任任何自由文本。先把返回内容strip,再用正则提取 JSON 大括号区域,解析完后要做枚举校验,polarity不在合法值列表内就走规则兜底。规范一点的 prompt 里应该写成“polarity只能是positive、negative、neutral三者之一”,这句话能减少一半的格式问题。
5.3 一条评语里多个对象只统计了第一个
现象:评语“课程内容很实用,但平台播放器经常卡顿,希望优化一下”被标成了“正面”,负面信息完全丢失。
原因:情感判别智能体只输出整体极性,没做评价对象拆分,导致负面对象“平台播放器”消失在聚合统计里。
解决:这条最致命,但不是模型问题,是任务拆分不彻底。必须强制主题归因智能体先抽取对象列表,再给每个对象独立输出极性。聚合统计时按aspect_tags展开,而不是用整句极性。平台方最想看的其实是“负面对象排行榜”,这个榜只有拆对象才能做出来。
5.4 双重否定语料被大量误判
现象:“我不是说老师不好”被标为负面,复核时发现有超过 150 条此类误判。
原因:模型没有理解“不是……不好”的转折逻辑,尤其是上下文里出现了负面词“不好”时,直接把极性压到负面。
解决:规则层单独加双重否定检测,命中后强制改为中性或微偏正面。同时把这些语料补进情感判别智能体的 few-shot 示例里。最直接的做法是在 prompt 里加一句“注意‘不是…不好’等双重否定结构应视为中立或正面表达”。
5.5 长评语截断把核心结论删掉了
现象:一条 500 字的评语末尾写着“总之不推荐”,但预处理时截断到前 256 个 token,模型判成了中性。
原因:截断位置只按长度切,没有考虑语义完整性。
解决:改成按句子边界截断,当累计长度接近上限时,以句号、感叹号、问号作为结束标志。更稳妥的方案是保留评语的后半段,因为中文评语里“总结性结论”经常出现在句尾。我现在的做法是前 128 个 token 取开头,后 128 个 token 取结尾,中间丢掉,这样的情感信息保留度比只取开头高很多。
5.6 只看平均分导致极端情绪被稀释
现象:某门课 10% 的差评情绪很激烈,但合并到 90% 的中好评后,平均分数仍然好看,运营漏掉了真实问题。
原因:聚合看板只呈现平均分,没有看情绪分布。
解决:看板上增加“负面占比”和“高情绪强度负面条数”两个指标。平均分是位置指标,情绪分布是离散指标,两个都要看。同时给负面占比设置阈值,超过 15% 就触发告警,把对应评语列表推送给人审队列。这个告警规则可以在数据库端实现,每天晚上跑一次任务即可。
6. 上线前必须做的三件验证:用可控实验证明系统可信
6.1 构建黄金标注集
多智能体系统让人不放心的点在于:它看起来什么都能说,但没人保证说得对。所以上线前第一件事不是调模型,而是先做一个黄金标注集。我一般从历史真实评语里抽 300 条,覆盖课程难度、平台卡顿、师资评价、作业负荷、正面夸奖五类,每一条都由两组人独立标注情感极性和评价对象,出现冲突时由第三方裁决,最终形成一个“标准答案”。
这个黄金集不参与任何模型训练,只做评估。它要固定版本号,每次模型或 Prompt 有更新,都必须先在黄金集上跑一遍,算准确率和 Kappa 系数。没有这个基线,后面所有的调优都是在黑匣子里乱撞。
6.2 三路对照实验
黄金集准备好了之后,我习惯做一次三路对照:纯规则关键词、单个语言模型、多智能体投票仲裁。对比表每次跑出来都会有差异,但整体趋势基本稳定:
| 方案 | 准确率 | 负面召回率 | 对象级准确率 | 稳定性 |
|---|---|---|---|---|
| 纯规则关键词 | 61% | 42% | 35% | 100% |
| 单语言模型 | 78% | 70% | 62% | 88% |
| 多智能体投票仲裁 | 83% | 78% | 74% | 93% |
单语言模型稳定性反而低于多智能体,是因为单模型在复杂评语上经常“自信地错”,三次调用可能给出三个不同极性;多智能体虽然每次单独调用也有漂移,但仲裁把分裂结果标成待复核,宁可不答也不硬答。这套对照实验的结论很重要:多智能体的价值不完全在提准确率,更关键的是把低置信度的样本分离出来,让系统知道自己不知道。
6.3 漂移监测与每日质检
上线后系统不是一劳永逸的。我踩过一个坑:模型服务端悄悄升级了版本,输出的风格略微变化,导致连续一周的负面检出率下跌了 8%,但没有任何报错,平台运营还以为是课程质量变好了。从那以后,我每天凌晨定时跑一遍黄金集,计算当天的准确率和分布漂移指标,一旦负面占比或调用一致性跌出阈值,就当问题处理,先回滚模型版本,再排查输入数据。
日常监测不需要太重的任务流,一个定时脚本加一张结果表就够了。每次跑完把准确率、召回率、调用稳定性三个值写入model_daily_check表,查询最近七天的曲线就能定位异常发生的时间点。教育评价系统的情感分析是一个偏冷门但业务价值很直接的场景,把架构拆开、把流程做稳、把评估做严,它就能从“看着能用”变成“真的敢用”。希望帮到你。
本文还有配套的精品资源,点击获取