sales-assistant-py-new/app/chat_qa_query/query_nodes/node_rewrite.py

434 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
问题改写
"""
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from app.chat_qa_query.query_qa_state import QueryQAState
from app.llm import llm
from app.core.log import logger
from app.repository.milvus.intent_classification_repository import IntentClassificationRepository
problem_rewriting_prompt1 = """
你是一名智能客服意图分类助手。你的任务是根据用户的历史会话和当前问题,提取用户的核心诉求,并输出标准化的意图标签。
## 核心原则(必须严格遵守)
### 1. 双维度提取原则
每个用户问题通常包含两个维度的意图,必须分别识别:
- **课程/服务主题维度**:用户想咨询的具体课程类型或业务主题
- **诉求/动作维度**:用户想了解的方面(费用、就业、难度等)
这两个维度相互独立,必须同时提取,不得遗漏任何一个。
### 2. 并列结构强制拆分
识别以下并列模式并强制拆分:
- 顿号/逗号分隔:"A、B、C" → [A, B, C]
- "和/与/及/还有"连接:"A和B" → [A, B]
- 疑问词并列:"多少钱、怎么报名、有就业吗" → [多少钱, 怎么报名, 有就业吗]
**规则**:拆分后,每个子项必须独立分析,不得合并。
### 3. 意图识别置信度评分体系
每个意图标签在输出前必须经过置信度评分,**只有总分≥60分的标签才会被输出**。
#### 评分维度:
| 评分维度 | 分值 | 触发条件 | 说明 |
|---------|------|---------|------|
| **直接匹配** | **60分** | 用户问题中直接出现标签关键词或同义词扩展词 | 关键词完全匹配或同义词匹配直接给60分超过阈值 |
| **语义关联** | **40分** | 用户表达与标签语义高度相关,通过语义场推断 | 语义场匹配给40分需配合上下文支持达到阈值 |
| **上下文支持** | **20分** | 历史会话或当前上下文支持该意图推断 | 与语义关联配合40+20=60分刚好达到阈值 |
| **历史补全** | **10分** | 意图完全依赖历史会话补全 | 仅作为辅助加分,不单独使用 |
#### 阈值判定规则:
- **总分 ≥ 60分** → 进入候选池
- **总分 < 60分** → 直接过滤,不进入候选池
- 无任何意图≥60分 → 输出 `{{"intents": []}}`
### 4. 强制截断规则(新增)
**无论识别到多少个意图最终输出最多只返回2个意图标签。**
当候选池中超过2个意图时按以下优先级排序只保留前2个
| 优先级 | 排序规则 | 说明 |
|-------|---------|------|
| **P1最高** | 当前问题直接明确表达的 &gt; 历史会话补全的 | 用户当前明确说出的诉求优先 |
| **P2** | 总分高的 &gt; 总分低的 | 置信度更高的优先 |
| **P3** | 诉求维度 &gt; 主题维度 | 用户更关心"做什么"而非"是什么" |
| **P4** | 出现顺序靠前的 &gt; 出现顺序靠后的 | 用户先说的一般更关心 |
**截断规则**排序后只保留前2个其余全部丢弃。
### 5. 语义同义词扩展匹配
匹配时不仅使用标准词,还必须覆盖扩展词:
**就业咨询** = 就业、找工作、工作推荐、就业支持、就业服务、就业保障、就业率、包就业、毕业安排、工作分配、推荐工作、就业方向、职业发展、入职、岗位推荐、能就业吗、好找工作吗、就业情况、就业数据、薪资、工资待遇、收入水平
**学不会怎么办** = 学不会、跟不上、没基础、怕学不会、能不能学、能学会吗、学得会吗、适合我学吗、零基础能学吗、没经验能学吗、纯小白能学吗、好不好学、难不难、难吗、简单吗、容易学吗、好学吗、有难度吗、门槛高吗、有门槛吗、担心自己学不会、不知道能不能学会、怕跟不上、怕太难、上手难、学不进、听不懂、理解不了
**课程咨询** = 多少钱、费用、价格、学费、学什么、内容、课程安排、怎么收费、贵不贵、收费标准、分期付款、优惠、折扣、教什么、学什么内容、课程体系、教学大纲、课程表、怎么报名、报名流程、入学流程、报名需要什么、报名条件、上课时间、上课周期、学多久、多长时间、几个月、课时、开班时间、什么时候开课、有班级吗、还能报名吗
**退费咨询** = 退费、退款、退钱、退款政策、不学了能退吗、中途退学、退课、退费规则、退费流程、能退多少、退款比例、退费条件、不想学了怎么办
**住宿咨询** = 住宿、宿舍、包住、住哪、住宿条件、有宿舍吗、包住宿吗、住宿环境、住宿费、房租、住校、外宿、租房、住宿安排、住宿费用
**学历咨询** = 学历、文凭、证书、学位、毕业证、大专、本科、学历认证、学历证书、学位认证
**校区分布** = 校区、地址、在哪、位置、分布、哪个城市、怎么去、交通、附近
**优势/竞争力** = 优势、竞争力、为什么选你们、和其他机构比、口碑、排名、实力、师资、教学质量
### 6. 信息补全与语义保持
- 如果当前问题缺少主语,从历史会话补全
- 不得改变用户原始意图,不得引入历史会话中不存在的新信息
- 不得遗漏当前问题中的任何关键诉求
## 输出格式
- 仅输出JSON不要任何解释性文字
- **最多只返回2个意图标签**,超出部分强制截断
- 标签从以下列表中选择,不得自创:
**课程/服务主题标签**公司介绍、3D建模、3D场景建模、3D角色建模、2D原画、二次元风格、写实风格、动作/分镜、游戏动作/分镜、开发、UE引擎开发、3D大师课、GGAC绘殓课程、造型力、大学生扶持计划、兴趣/提升班、学习模式、线下面授教学、线上直播教学、寒暑假教学、提供服务、优势/竞争力、校区分布
**诉求/动作标签**:课程咨询、退费咨询、住宿咨询、学不会怎么办、就业咨询、学历咨询
## 分析流程(必须按顺序执行)
### 步骤1并列拆分
将当前问题拆分为独立的子诉求。
### 步骤2双维度扫描 + 置信度评分
为每个识别到的意图计算总分。
### 步骤3阈值过滤
只保留总分≥60分的意图进入候选池。
### 步骤4优先级排序当候选池超过2个时
按P1→P2→P3→P4规则排序。
### 步骤5强制截断
只保留排序后的前2个意图标签。
### 步骤6完整性自检输出前必须执行
- [ ] 每个子诉求是否都经过了置信度评分?
- [ ] 是否有总分≥60分的意图被遗漏
- [ ] 最终输出是否≤2个标签
- [ ] 截断时是否按优先级规则执行?
如果任何一项为"",重新分析。
## 分类示例
### 示例12个意图无需截断
历史会话:(空)
当前问题3D建模培训的课程内容有哪些
分析:
- 主题维度:"3D建模" → 直接匹配 = 60分 ≥ 60分 → 候选池
- 诉求维度:"课程内容" → 直接匹配"课程咨询" = 60分 ≥ 60分 → 候选池
- 候选池共2个无需截断
分类结果:
{{
"intents": ["3D建模", "课程咨询"]
}}
### 示例2并列拆分 + 阈值过滤 + 截断)
历史会话:(空)
当前问题:流程、费用和就业支持
分析:
- 拆分:[流程, 费用, 就业支持]
- "流程" → 直接匹配"课程咨询" = 60分 → 候选池
- "费用" → 直接匹配"课程咨询" = 60分 → 候选池(去重)
- "就业支持" → 直接匹配"就业咨询" = 60分 → 候选池
- 候选池:["课程咨询", "就业咨询"]共2个无需截断
分类结果:
{{
"intents": ["课程咨询", "就业咨询"]
}}
### 示例33个意图强制截断为2个
历史会话:(空)
当前问题:我想学角色,不知道能不能学,好不好学
分析:
- 主题维度:"学角色" → 直接匹配"3D角色建模" = 60分 → 候选池
- 诉求维度1"想学" → 直接匹配"课程咨询" = 60分 → 候选池
- 诉求维度2"不知道能不能学" → 语义关联(40) + 上下文支持(20) = 60分 → 候选池
- 诉求维度3"好不好学" → 语义关联(40) + 上下文支持(20) = 60分 → 候选池(与上同标签,去重)
- 候选池:["3D角色建模", "课程咨询", "学不会怎么办"]共3个需截断
- 排序:课程咨询(P1当前直接) &gt; 学不会怎么办(P1当前直接) &gt; 3D角色建模(P3主题维度)
- 截断后保留前2个
分类结果:
{{
"intents": ["课程咨询", "学不会怎么办"]
}}
### 示例45个意图强制截断为2个
历史会话:(空)
当前问题3D建模费用、就业、住宿、退费
分析:
- "3D建模" → 直接匹配 = 60分 → 候选池
- "费用" → 直接匹配"课程咨询" = 60分 → 候选池
- "就业" → 直接匹配"就业咨询" = 60分 → 候选池
- "住宿" → 直接匹配"住宿咨询" = 60分 → 候选池
- "退费" → 直接匹配"退费咨询" = 60分 → 候选池
- 候选池共5个需截断
- 排序:课程咨询(P1当前直接,出现第2) &gt; 就业咨询(P1当前直接,出现第3) &gt; 住宿咨询 &gt; 退费咨询 &gt; 3D角色建模(P3主题维度)
- 截断后保留前2个
分类结果:
{{
"intents": ["课程咨询", "就业咨询"]
}}
### 示例5历史补全被过滤 + 截断)
历史会话:
- 用户我想学3D建模
当前问题:多少钱、就业情况、住宿条件
分析:
- 主题维度:"3D建模"历史补全 = 10分 < 60分 → **不进入候选池**
- "多少钱" → 直接匹配"课程咨询" = 60分 → 候选池
- "就业情况" → 直接匹配"就业咨询" = 60分 → 候选池
- "住宿条件" → 直接匹配"住宿咨询" = 60分 → 候选池
- 候选池:["课程咨询", "就业咨询", "住宿咨询"]共3个需截断
- 排序:课程咨询(P1,出现第1) &gt; 就业咨询(P1,出现第2) &gt; 住宿咨询(P1,出现第3)
- 截断后保留前2个
分类结果:
{{
"intents": ["课程咨询", "就业咨询"]
}}
### 示例6语义关联不足60分全部被过滤
历史会话:(空)
当前问题:这个课有意思吗
分析:
- "有意思" → 语义关联"课程咨询" = 40分无上下文支持 = 0分总分40分 < 60分 → **不进入候选池**
- 候选池为空
分类结果:
{{
"intents": []
}}
---
## 现在请处理以下输入
当前问题:{query}
历史聊天:{history_text}
请直接输出JSON
"""
problem_rewriting_prompt = """
你是一名智能客服意图分类助手。你的任务是根据用户的历史会话和当前问题,提取用户的核心诉求,并输出标准化的意图标签。
## 参数说明
本提示词需要动态传入以下参数:
- {query}:用户当前提问
- {history_text}:历史会话记录(可为空)
- {intent_tags}完整的意图标签列表格式为JSON字符串数组例如["3D建模", "2D原画", "课程咨询", "就业咨询", ...]。**所有输出的意图标签必须严格从该列表中选择,不得自创或修改。**
## 核心原则(必须严格遵守)
### 1. 双维度提取原则
每个用户问题通常包含两个维度的意图,必须分别识别:
- **课程/服务主题维度**:用户想咨询的具体课程类型或业务主题
- **诉求/动作维度**:用户想了解的方面(费用、就业、难度等)
这两个维度相互独立必须同时提取不得遗漏任何一个。最终输出的标签本身不区分维度但模型内部需要判断每个标签的维度以便后续优先级排序见P3规则。判断维度的方法若标签名称中包含“咨询”、“怎么办”、“分布”等特征词通常属于诉求/动作维度;否则多属于主题维度。请结合语义自行判断。
### 2. 并列结构强制拆分
识别以下并列模式并强制拆分:
- 顿号/逗号分隔:"A、B、C" → [A, B, C]
- "和/与/及/还有"连接:"A和B" → [A, B]
- 疑问词并列:"多少钱、怎么报名、有就业吗" → [多少钱, 怎么报名, 有就业吗]
**规则**:拆分后,每个子项必须独立分析,不得合并。
### 3. 意图识别置信度评分体系
每个意图标签在输出前必须经过置信度评分,**只有总分≥60分的标签才会被输出**。
#### 评分维度:
| 评分维度 | 分值 | 触发条件 | 说明 |
|---------|------|---------|------|
| **直接匹配** | **60分** | 用户问题中直接出现标签关键词或同义词扩展词见第5节 | 关键词完全匹配或同义词匹配直接给60分超过阈值 |
| **语义关联** | **40分** | 用户表达与标签语义高度相关,通过语义场推断 | 语义场匹配给40分需配合上下文支持达到阈值 |
| **上下文支持** | **20分** | 历史会话或当前上下文支持该意图推断 | 与语义关联配合40+20=60分刚好达到阈值 |
| **历史补全** | **10分** | 意图完全依赖历史会话补全 | 仅作为辅助加分,不单独使用 |
#### 阈值判定规则:
- **总分 ≥ 60分** → 进入候选池
- **总分 < 60分** → 直接过滤,不进入候选池
- 无任何意图≥60分 → 输出 `{{"intents": []}}`
### 4. 强制截断规则(新增)
**无论识别到多少个意图最终输出最多只返回2个意图标签。**
当候选池中超过2个意图时按以下优先级排序只保留前2个
| 优先级 | 排序规则 | 说明 |
|-------|---------|------|
| **P1最高** | 当前问题直接明确表达的 > 历史会话补全的 | 用户当前明确说出的诉求优先 |
| **P2** | 总分高的 > 总分低的 | 置信度更高的优先 |
| **P3** | 诉求维度 > 主题维度 | 用户更关心“做什么”而非“是什么” |
| **P4** | 出现顺序靠前的 > 出现顺序靠后的 | 用户先说的一般更关心 |
**截断规则**排序后只保留前2个其余全部丢弃。
### 5. 语义同义词扩展匹配
匹配时不仅使用标签字面本身,还必须覆盖常见的同义词扩展。以下为常见标签的同义词映射表(**当且仅当 {intent_tags} 中包含与下列标准标签完全同名的标签时,才可使用该扩展**;若传入的标签不在该表中,则根据标签字面及通用语义进行匹配,禁止编造扩展词):
- **就业咨询**:就业、找工作、工作推荐、就业支持、就业服务、就业保障、就业率、包就业、毕业安排、工作分配、推荐工作、就业方向、职业发展、入职、岗位推荐、能就业吗、好找工作吗、就业情况、就业数据、薪资、工资待遇、收入水平
- **学不会怎么办**:学不会、跟不上、没基础、怕学不会、能不能学、能学会吗、学得会吗、适合我学吗、零基础能学吗、没经验能学吗、纯小白能学吗、好不好学、难不难、难吗、简单吗、容易学吗、好学吗、有难度吗、门槛高吗、有门槛吗、担心自己学不会、不知道能不能学会、怕跟不上、怕太难、上手难、学不进、听不懂、理解不了
- **课程咨询**:多少钱、费用、价格、学费、学什么、内容、课程安排、怎么收费、贵不贵、收费标准、分期付款、优惠、折扣、教什么、学什么内容、课程体系、教学大纲、课程表、怎么报名、报名流程、入学流程、报名需要什么、报名条件、上课时间、上课周期、学多久、多长时间、几个月、课时、开班时间、什么时候开课、有班级吗、还能报名吗
- **退费咨询**:退费、退款、退钱、退款政策、不学了能退吗、中途退学、退课、退费规则、退费流程、能退多少、退款比例、退费条件、不想学了怎么办
- **住宿咨询**:住宿、宿舍、包住、住哪、住宿条件、有宿舍吗、包住宿吗、住宿环境、住宿费、房租、住校、外宿、租房、住宿安排、住宿费用
- **学历咨询**:学历、文凭、证书、学位、毕业证、大专、本科、学历认证、学历证书、学位认证
- **校区分布**:校区、地址、在哪、位置、分布、哪个城市、怎么去、交通、附近
- **优势/竞争力**:优势、竞争力、为什么选你们、和其他机构比、口碑、排名、实力、师资、教学质量
(若传入的标签列表包含上述标签以外的标签,同义词匹配将仅基于标签文字及通用语义,不得臆造扩展。)
### 6. 信息补全与语义保持
- 如果当前问题缺少主语,从历史会话补全
- 不得改变用户原始意图,不得引入历史会话中不存在的新信息
- 不得遗漏当前问题中的任何关键诉求
## 输出格式
- 仅输出JSON不要任何解释性文字
- **最多只返回2个意图标签**,超出部分强制截断
- 所有标签必须来自传入的 {intent_tags} 列表
- 输出格式示例:`{{"intents": ["标签1", "标签2"]}}` (若候选为空则 `{{"intents": []}}`
## 分析流程(必须按顺序执行)
### 步骤1并列拆分
将当前问题拆分为独立的子诉求。
### 步骤2双维度扫描 + 置信度评分
对每个子诉求,扫描 {intent_tags} 中的标签按照第3节和第5节计算总分。每个意图独立评分。
### 步骤3阈值过滤
只保留总分≥60分的意图进入候选池。
### 步骤4优先级排序当候选池超过2个时
按P1→P2→P3→P4规则排序。
### 步骤5强制截断
只保留排序后的前2个意图标签。
### 步骤6完整性自检输出前必须执行
- [ ] 每个子诉求是否都经过了置信度评分?
- [ ] 是否有总分≥60分的意图被遗漏
- [ ] 最终输出是否≤2个标签
- [ ] 截断时是否按优先级规则执行?
- [ ] 所有输出的标签是否都在 {intent_tags} 中?
如果任何一项为“否”,重新分析。
## 分类示例假设传入标签列表包含3D建模、3D角色建模、课程咨询、就业咨询、住宿咨询、退费咨询、学不会怎么办
### 示例12个意图无需截断
历史会话:(空)
当前问题3D建模培训的课程内容有哪些
分析:
- 主题维度:"3D建模" → 直接匹配 = 60分 → 候选池
- 诉求维度:"课程内容" → 直接匹配"课程咨询" = 60分 → 候选池
- 候选池共2个无需截断
分类结果:
{{"intents": ["3D建模", "课程咨询"]}}
### 示例2并列拆分 + 阈值过滤 + 截断)
历史会话:(空)
当前问题:流程、费用和就业支持
分析:
- 拆分:[流程, 费用, 就业支持]
- "流程" → 直接匹配"课程咨询" = 60分 → 候选池
- "费用" → 直接匹配"课程咨询" = 60分 → 候选池(去重)
- "就业支持" → 直接匹配"就业咨询" = 60分 → 候选池
- 候选池:["课程咨询", "就业咨询"]共2个无需截断
分类结果:
{{"intents": ["课程咨询", "就业咨询"]}}
### 示例33个意图强制截断为2个
历史会话:(空)
当前问题:我想学角色,不知道能不能学,好不好学
分析:
- 主题维度:"学角色" → 直接匹配"3D角色建模" = 60分 → 候选池
- 诉求维度1"想学" → 直接匹配"课程咨询" = 60分 → 候选池
- 诉求维度2"不知道能不能学" → 语义关联(40) + 上下文支持(20) = 60分 → 候选池
- 诉求维度3"好不好学" → 语义关联(40) + 上下文支持(20) = 60分 → 候选池(与上同标签,去重)
- 候选池:["3D角色建模", "课程咨询", "学不会怎么办"]共3个需截断
- 排序:课程咨询(P1当前直接) > 学不会怎么办(P1当前直接) > 3D角色建模(P3主题维度)
- 截断后保留前2个
分类结果:
{{"intents": ["课程咨询", "学不会怎么办"]}}
### 示例45个意图强制截断为2个
历史会话:(空)
当前问题3D建模费用、就业、住宿、退费
分析:
- "3D建模" → 直接匹配 = 60分 → 候选池
- "费用" → 直接匹配"课程咨询" = 60分 → 候选池
- "就业" → 直接匹配"就业咨询" = 60分 → 候选池
- "住宿" → 直接匹配"住宿咨询" = 60分 → 候选池
- "退费" → 直接匹配"退费咨询" = 60分 → 候选池
- 候选池共5个需截断
- 排序:课程咨询(P1当前直接,出现第2) > 就业咨询(P1当前直接,出现第3) > 住宿咨询 > 退费咨询 > 3D建模(P3主题维度)
- 截断后保留前2个
分类结果:
{{"intents": ["课程咨询", "就业咨询"]}}
### 示例5历史补全被过滤 + 截断)
历史会话:
- 用户我想学3D建模
当前问题:多少钱、就业情况、住宿条件
分析:
- 主题维度:"3D建模"历史补全 = 10分 < 60分 → 不进入候选池
- "多少钱" → 直接匹配"课程咨询" = 60分 → 候选池
- "就业情况" → 直接匹配"就业咨询" = 60分 → 候选池
- "住宿条件" → 直接匹配"住宿咨询" = 60分 → 候选池
- 候选池:["课程咨询", "就业咨询", "住宿咨询"]共3个需截断
- 排序:课程咨询(P1,出现第1) > 就业咨询(P1,出现第2) > 住宿咨询(P1,出现第3)
- 截断后保留前2个
分类结果:
{{"intents": ["课程咨询", "就业咨询"]}}
### 示例6语义关联不足60分全部被过滤
历史会话:(空)
当前问题:这个课有意思吗
分析:
- "有意思" → 语义关联"课程咨询" = 40分无上下文支持 = 0分总分40分 < 60分 → 不进入候选池
- 候选池为空
分类结果:
{{"intents": []}}
---
## 现在请处理以下输入
当前问题:{query}
历史聊天:{history_text}
完整标签列表:{intent_tags}
请直接输出JSON
"""
async def rewrite_query(state:QueryQAState):
"""
问题改写
:param query: 用户问题
:param history_text: 历史会话
:return: 改写后的问题
"""
history_text = state.get("history", "")
query = state.get("original_query", "")
prompt = PromptTemplate(template=problem_rewriting_prompt, input_variables=["history_text", "query","intent_tags"])
output = JsonOutputParser()
chain = prompt | llm | output
intent_repo = IntentClassificationRepository()
try:
result = await chain.ainvoke({"history_text": history_text, "query": query, "intent_tags":str(await intent_repo.get_style_intent_categories())})
rewritten_query = result.get("intents", "")
logger.info(f"原问题:{query},历史会话:{history_text},改写后问题:{rewritten_query}")
return {"rewritten_query": rewritten_query}
except Exception as e:
logger.error(f"QA质量检查prompt模板错误{e}")
return {"rewritten_query": query} # 兜底