山东大学软件学院创新项目实训 —— 基于UE与LLM的医患沟通模拟与评价系统(七)
文章目录
- 前言
- 一、如何实现 AI 医生顾问
- 1. 整体思路
- 2. 涉及改动的文件
- 3. 核心代码
- 二、第一次测试:功能跑通,但发现明显缺陷
- 三、完善:接入启用条目与实时规则检测
- 1. 问题分析
- 2. 改动方案
- 四、完善后再次测试
前言
在实际使用过程中,发现了一个明显的体验缺口:医生(使用者)在问诊过程中有时会陷入不知道该问什么的困境,只能等到对话结束后看评分报告才知道哪里没问到。这种"事后复盘"的模式对于训练效率来说是不够的。
因此,在前端加一个"询问 AI"的按钮,使用者点击后,大模型切换为医生顾问角色,根据当前对话进度给出下一步提问的方向和话术建议,实现"实时教练"的效果。
一、如何实现 AI 医生顾问
1. 整体思路
这个功能本质上是一次单轮问答:
- 从 Redis 取出当前会话的对话历史
- 去掉原来的患者 system message(避免模型被病人的角色设定干扰)
- 换上培训专家的 system prompt,把对话历史作为上下文传入
- 模型给出建议,返回给前端
2. 涉及改动的文件
| 文件 | 改动内容 |
|---|---|
llm_service.py | 新增get_doctor_advice函数 |
endpoints.py | 新增/chat/advice接口,补充 import |
chat.py | 新增AdviceResponse响应模型 |
3. 核心代码
chat.py— 新增响应模型
classAdviceResponse(BaseModel):status:str="success"advice:strendpoints.py— 新增接口
接口直接复用了已有的ChatRequest(只需要session_id+scene_key),不需要新建请求模型。
@router.post("/chat/advice",response_model=AdviceResponse)asyncdefget_advice(request:ChatRequest):""" AI 医生顾问接口:前端点击"询问AI"按钮时调用。 读取当前会话的对话历史,以培训专家视角给出下一步提问的方向与话术建议。 不修改对话历史,不影响正在进行的问诊流程。 """try:advice=awaitget_doctor_advice(request.session_id,request.scene_key)returnAdviceResponse(advice=advice)exceptExceptionase:raiseHTTPException(status_code=500,detail=str(e))llm_service.py— 核心函数
asyncdefget_doctor_advice(session_id:str,scene_key:str)->str:redis_key=f"chat_history:{session_id}:{scene_key}"# 从 Redis 读取当前对话历史history_str=awaitredis_pool.get(redis_key)messages=json.loads(history_str)ifhistory_strelse[]# 从数据库读取场景配置scene_data=awaitget_scene_config(scene_key)advisor_system_prompt=f""" 你是一位经验丰富的医患沟通培训专家,正在辅助一名医生进行问诊训练。 【患者背景信息】{scene_data['patient_basic']}【本阶段问诊目标】{scene_data['patient_prompt']}【你的任务】 根据下方的对话历史,判断医生目前的问诊进展,给出下一步提问建议。 【输出格式要求】 按以下结构输出,不超过150字: ➤ 建议方向:(一句话说明应该往哪个维度继续问) ➤ 参考话术:(给出1~2句医生可以直接说的口语示例) 注意: - 建议要贴合当前对话的上下文,不要重复已经问过的内容 - 话术要口语化、自然,符合医生和患者交流的语气 - 不要评价医生之前说的对不对,只给下一步建议 """# 去掉原患者 system message,避免角色混淆dialogue_only=[mforminmessagesifm.get("role")!="system"]advisor_messages=[{"role":"system","content":advisor_system_prompt},*dialogue_only,{"role":"user","content":"请根据以上对话,给我下一步的提问建议。"},]response=awaitllm_client.chat.completions.create(model="deepseek-chat",messages=advisor_messages,temperature=0.5,max_tokens=300,)returnresponse.choices[0].message.content二、第一次测试:功能跑通,但发现明显缺陷
测试流程如下:
- 先调用
/chat接口几次,让 Redis 里积累一定的对话历史 - 再调用
/chat/advice,传入相同的session_id和scene_key
测试结果如下:
功能上是跑通了,建议方向和话术也比较自然。但仔细分析后发现了一个关键问题:
模型给出的建议完全依赖对上下文的"感觉",不知道这个场景实际需要覆盖哪些评分条目。我们设定的不同场景有不同的评分条目,所以有更侧重的提问点。比如某个场景只启用了第6、7、8、10条,模型却可能因为对话里提到了疼痛就一直围绕生理症状建议,忽略了社会心理维度(第8条)、疾病对生活的影响(第10条)等同样需要覆盖的条目。
此外,顾问完全不知道当前对话里是否已经触发了黑话违规或命令式提问,无法在给建议的同时提醒医生注意规范用语。
三、完善:接入启用条目与实时规则检测
1. 问题分析
数据库scenario_scenes表中有一列active_items,存储了该场景启用的评分条目编号(JSON 数组格式,如[1, 6, 7, 8, 10, 12, 19, 22, 24, 25])。第一版的get_doctor_advice完全没有读取这一列,导致顾问的引导缺乏针对性。
同时,系统已经有一套完整的规则检测函数(run_all_rule_checks、detect_item12_commanding、detect_item19_jargon等),评分时才调用。顾问功能可以复用这些函数,在建议里加入实时违规提醒。
2. 改动方案
只需修改llm_service.py中的get_doctor_advice函数,其他文件不动。改动分三步:
第一步:新增 SEGUE 条目描述表
把 25 个条目的编号和描述写成字典,用于把active_items中的数字还原成可读文字:
SEGUE_ITEMS={1:"有礼貌地称呼病人",6:"让病人主动讲述对健康问题/疾病发展的看法",7:"系统询问影响疾病的物理、生理因素",8:"系统询问影响疾病的社会、心理、情感因素",10:"讨论疾病对病人生活质量的影响",12:"避免诱导性提问/命令式提问",19:"根据病人理解能力调整表达,避免或解释专业术语",# ... 共25条}第二步:解析 active_items,生成条目描述列表
try:active_items=json.loads(scene_data.get("active_items")or"[]")exceptException:active_items=[]active_items_desc="\n".join(f" 第{i}条:{SEGUE_ITEMS.get(i,'未知条目')}"foriinactive_items)第三步:对当前对话跑规则检测,生成违规提醒块
复用已有的检测函数,在给建议之前先扫一遍当前对话:
dialogue_only=[mforminmessagesifm.get("role")!="system"]rule_results=run_all_rule_checks(dialogue_only,patient_education_level)warning_lines=[]# 第12条:命令式/诱导式提问item12=rule_results.get("item12",{})ifitem12.get("triggered")and12inactive_items:phrases=[pforvinitem12.get("violations",[])forpinv.get("matched_phrases",[])]warning_lines.append(f"⚠️ 第12条违规:已检测到命令式/诱导式用语({', '.join(set(phrases))}),请注意避免。")# 第19条:专业术语未解释item19=rule_results.get("item19",{})ifitem19.get("triggered")and19inactive_items:unexp=[wforvinitem19.get("violations",[])forwinv.get("jargon",[])]warning_lines.append(f"⚠️ 第19条违规:以下专业词尚未通俗解释({', '.join(set(unexp))}),"f"{'患者文化程度低,这将直接导致该条扣分。'ifpatient_education_level=='低'else'建议补充解释。'}")# 第1条:礼貌称呼if1inactive_items:item1=detect_item1_greeting(dialogue_only)ifnotitem1.get("triggered"):warning_lines.append("⚠️ 第1条提醒:目前尚未出现礼貌称呼(如您好、先生、女士等)。")最终 system prompt 结构
把条目列表和违规提醒一起注入 prompt,输出格式也新增了"注意事项"一项(无违规时自动省略):
advisor_system_prompt=f""" 你是一位经验丰富的医患沟通培训专家,正在实时辅助一名医生进行问诊训练。 【患者背景信息】{scene_data['patient_basic']}【本阶段问诊目标】{scene_data['patient_prompt']}【本场景需要覆盖的评分条目】 以下是本场景会被评分的条目,医生需要在对话中尽量全部覆盖:{active_items_desc}【当前规则检测结果】{warnings_block}【输出格式】 按以下结构输出,总字数不超过180字: ➤ 建议方向:(优先选尚未覆盖的条目) ➤ 参考话术:(1~2句口语示例) ➤ 注意事项:(有违规时提醒;没有则省略此项) """四、完善后再次测试
保持相同的对话历史,再次调用/chat/advice:
测试结果如下:
对比两次测试结果,改进效果非常明显:
| 对比项 | 第一版 | 完善后 |
|---|---|---|
| 建议依据 | 仅凭对话上下文感觉 | 明确指向未覆盖的评分条目(第6、8条) |
| 条目意识 | 无,不知道场景要评哪些 | 有,优先引导覆盖缺失条目 |
| 违规提醒 | 无 | 有,实时检测并在注意事项中反馈 |
| 话术质量 | 自然但方向随机 | 自然且有针对性,贴合具体条目要求 |
