sales-assistant-py-new/app/chat_qa/nodes/generate_qa_node.py

213 lines
8.6 KiB
Python
Raw Permalink 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.

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/falsenull 是小写的
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/falsenull 是小写的
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