213 lines
8.6 KiB
Python
213 lines
8.6 KiB
Python
import json
|
||
|
||
from langchain_core.messages import SystemMessage, HumanMessage
|
||
from langchain_core.output_parsers import JsonOutputParser
|
||
from langchain_core.prompts import PromptTemplate
|
||
from langgraph.runtime import Runtime
|
||
|
||
from app.chat_qa.context import QAContext
|
||
from app.chat_qa.qa_state import QAState
|
||
from app.dto.quality_score import QualityScore
|
||
from app.llm import llm
|
||
from app.core.log import logger
|
||
|
||
|
||
#检查消息质量
|
||
async def quality_check(chat_history_text:str)->int:
|
||
prompt = """
|
||
你是一位培训质量评估专家。请分析以下客服/讲师与学员的对话记录,判断是否值得生成标准化的问答对(QA Pair)。
|
||
|
||
## 对话记录
|
||
{chat_history_text}
|
||
## 说明
|
||
- [用户]:表示用户的聊天
|
||
- [销售]:表示客服/销售的回复/提问
|
||
## 评估任务
|
||
请从以下5个维度打分(1-10分),并给出综合判断。
|
||
|
||
## 核心评分原则(优先级从高到低)
|
||
|
||
**第一原则:相关性优先**
|
||
- 对话是否属于培训咨询场景?(课程价格、报名条件、学习内容、就业方向、学习周期等)
|
||
- 如果是,基础分不低于40分
|
||
|
||
**第二原则:问答对应关系**
|
||
- 用户是否提出了明确的问题?
|
||
- 销售/客服是否给出了直接对应的答案?
|
||
- 如果"问题明确 + 答案对应",基础分不低于60分
|
||
|
||
**第三原则:信息完整性(在已回答问题的前提下评估)**
|
||
- 不要苛求答案必须覆盖用户所有潜在需求
|
||
- 只要答案直接回应了用户提出的问题,即视为完整
|
||
|
||
---
|
||
|
||
## 五个评分维度(调整后权重)
|
||
|
||
### 【相关性】(权重30%,1-10分)
|
||
- 是否属于培训咨询场景?
|
||
- 课程价格、费用、优惠 → 10分
|
||
- 报名条件、适用人群、学历要求 → 10分
|
||
- 课程内容、学习周期、上课方式 → 10分
|
||
- 就业方向、证书、师资 → 10分
|
||
- 其他培训相关 → 7-9分
|
||
- 与培训无关(如闲聊、技术支持) → 0-3分
|
||
|
||
### 【问答对应度】(权重25%,1-10分)
|
||
- 用户问题是否明确?(3分)
|
||
- 销售答案是否直接对应问题?(4分)
|
||
- 答案是否存在明显错误或答非所问?(3分,无错误即满分)
|
||
- 例如:[用户] XXXXXX [销售] XXXXX 表示销售回答了用户的问题
|
||
**评分细则:**
|
||
- 用户问A,销售答A = 10分
|
||
- 用户问A,销售答A+B(额外信息) = 10分
|
||
- 用户问A,销售答了一半A,又问了B = 7分
|
||
- 用户问A,销售答非所问 = 0-3分
|
||
|
||
### 【答案准确性】(权重20%,1-10分)
|
||
- 答案事实是否正确?(5分)
|
||
- 答案是否无歧义、不会误导用户?(5分)
|
||
|
||
### 【可复用价值】(权重15%,1-10分)
|
||
- 该问题是否是学员常问的问题?(5分)
|
||
- 该答案是否可以直接用于未来同类咨询?(5分)
|
||
|
||
**重要:只要问题和答案对应,且属于培训场景,此项默认不低于6分**
|
||
|
||
### 【表达清晰度】(权重10%,1-10分)
|
||
- 答案是否能让普通学员看懂?(5分)
|
||
- 答案是否有明显语病或混乱?(5分,无明显问题即满分)
|
||
|
||
---
|
||
|
||
## 硬性否决规则(仅以下情况否决,其他一律不否决)
|
||
|
||
1. **非培训场景**:对话内容与培训咨询完全无关(如技术支持、物流查询、闲聊)
|
||
2. **答案错误**:销售给出了错误信息且可能误导学员
|
||
3. **答非所问**:用户问的是A,销售回答的是完全不相关的B
|
||
|
||
**以下情况不再作为否决条件:**
|
||
- 答案只有数字/简单事实
|
||
- 对话只有1轮
|
||
- 销售抛出了新问题未闭环
|
||
- 答案没有覆盖用户的所有潜在需求
|
||
- 答案缺少"下一步行动建议"
|
||
|
||
---
|
||
|
||
|
||
## 输出格式(必须严格遵循JSON格式)
|
||
```json
|
||
{{
|
||
"relevance": 10,
|
||
"qa_alignment": 10,
|
||
"accuracy": 10,
|
||
"reusability": 8,
|
||
"clarity": 8
|
||
}}
|
||
"""
|
||
|
||
prompt = PromptTemplate(template=prompt, input_variables=["chat_history_text"])
|
||
output = JsonOutputParser()
|
||
chain = prompt | llm | output
|
||
result = {}
|
||
try:
|
||
result = await chain.ainvoke({"chat_history_text": chat_history_text})
|
||
except Exception as e:
|
||
logger.error(f"QA质量检查prompt模板错误:{e}")
|
||
logger.info(f"消息质量评估:{result}")
|
||
score_obj = QualityScore(
|
||
relevance=result.get("relevance", 0),
|
||
qa_alignment=result.get("qa_alignment", 0),
|
||
accuracy=result.get("accuracy", 0),
|
||
reusability=result.get("reusability", 0),
|
||
clarity=result.get("clarity", 0)
|
||
)
|
||
total = score_obj.total
|
||
return total
|
||
|
||
"""
|
||
生成QA
|
||
"""
|
||
async def generate_qa(state: QAState, runtime: Runtime[QAContext]):
|
||
retry_count = state.get("retry_count", 0)
|
||
chat_history = state.get("chat_history", 0)
|
||
|
||
# 生成QA的系统提示
|
||
system_prompt = """你是一个专业的QA提取专家。请分析以下聊天记录,提取或生成1-2个高质量的QA对。
|
||
规则:
|
||
1. 优先提取用户明确提出的问题及其对应的回答
|
||
2. 如果对话中没有明确的问题,根据讨论内容生成一个总结性问题
|
||
3. 答案必须基于聊天记录中的实际内容,不能编造
|
||
4. 如果某个问题在聊天记录中没有明确答案,则不生成QA
|
||
5. 最多生成2个QA对,优先选择最有价值的信息
|
||
例如:
|
||
聊天内容:[用户] 表明自己为准大学生,高考完,非在校大学生。询问线上课程价格是否为1800。[销售] 价格为11800
|
||
输出:{"question": "询问线上课程价格", "answer": "价格为11800"}
|
||
JSON规则(按优先级):
|
||
1. 将中文引号 "" '' 替换为英文引号 "" ''
|
||
2. 删除所有注释(// 和 /* */)
|
||
3. 删除 markdown 代码块标记(```json ```)
|
||
4. 删除 JSON 前后多余的文本说明
|
||
5. 修复末尾多余的逗号(如 },] 或 },})
|
||
6. 确保所有字符串用双引号包裹
|
||
7. 确保布尔值是小写的 true/false,null 是小写的
|
||
8. 如果键名没有引号,给键名加上双引号
|
||
|
||
只输出修复后的纯 JSON,不要任何解释、不要 markdown 标记、不要注释。
|
||
输出格式必须是JSON,不要任何解释、不要 markdown 标记、不要注释:
|
||
{
|
||
"qa_pairs": [
|
||
{"question": "...", "answer": "..."},
|
||
{"question": "...", "answer": "..."}
|
||
]
|
||
}
|
||
|
||
"""
|
||
current_qas_list = []
|
||
|
||
logger.info(f"聊天内容:{chat_history}")
|
||
total = await quality_check(chat_history)
|
||
if total <= 75:
|
||
return {"retry_count" : (retry_count+1)}
|
||
# 调用LLM
|
||
messages = [
|
||
SystemMessage(content=system_prompt),
|
||
HumanMessage(content=f"聊天记录:\n{chat_history}")
|
||
]
|
||
|
||
chain = llm
|
||
result = await chain.ainvoke(messages)
|
||
try:
|
||
res = json.loads(result.content)
|
||
# 提取生成的问答对
|
||
new_qa = res.get("qa_pairs", [])
|
||
except Exception :
|
||
new_qa = is_valid_json(result.content) # 解析失败赋值字符串,后续调用 .get() 崩溃
|
||
logger.info(f"生成QA内容:{new_qa}")
|
||
current_qas_list.append(new_qa)
|
||
return {"retry_count" : (retry_count+1), "current_qas": current_qas_list}
|
||
|
||
# 创建一个校验json格式的函数
|
||
async def is_valid_json(json_str: str)->str:
|
||
prompt = """你是一个 JSON 修复专家。请修复以下损坏的 JSON 文本,使其成为合法的标准 JSON。
|
||
修复规则(按优先级):
|
||
1. 将中文引号 "" '' 替换为英文引号 "" ''
|
||
2. 删除所有注释(// 和 /* */)
|
||
3. 删除 markdown 代码块标记(```json ```)
|
||
4. 删除 JSON 前后多余的文本说明
|
||
5. 修复末尾多余的逗号(如 },] 或 },})
|
||
6. 确保所有字符串用双引号包裹
|
||
7. 确保布尔值是小写的 true/false,null 是小写的
|
||
8. 如果键名没有引号,给键名加上双引号
|
||
|
||
只输出修复后的纯 JSON,不要任何解释、不要 markdown 标记、不要注释。
|
||
|
||
待修复的 JSON:
|
||
{input_json}
|
||
"""
|
||
prompt = PromptTemplate(template=prompt, input_variables=["chat_history_text"])
|
||
output = JsonOutputParser()
|
||
chain = prompt | llm | output
|
||
result = await chain.ainvoke({"input_json": json_str})
|
||
return result |