feat: 意图识别优化
This commit is contained in:
parent
560dbd60a2
commit
f323ed5f5d
@ -31,6 +31,47 @@ general_prompt = """
|
|||||||
输入文本:{text}
|
输入文本:{text}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
general_merge_prompt = """
|
||||||
|
# 任务
|
||||||
|
从三类输入中提取与"下一次回复"直接相关的结构化关键要点,按优先级融合后输出。
|
||||||
|
|
||||||
|
# 输入
|
||||||
|
- 最新聊天内容(最高优先级): {message}
|
||||||
|
- 历史相关知识: {milvus_content}
|
||||||
|
- 标准内容: {standard}
|
||||||
|
|
||||||
|
# 处理规则
|
||||||
|
1. 【优先级去重】同一事实优先采用 {message},缺失部分依次补充 {milvus_content}、{standard},不重复罗列。
|
||||||
|
2. 【个人信息脱敏】删除所有个人身份信息(姓名、年级、专业全称、学校、联系方式等)。
|
||||||
|
- ⚠️ 特别注意:「回复主题」同样需要脱敏,不得包含具体专业、年级、姓名等身份标识。主题应概括"咨询类型/事项",而非"用户身份"。
|
||||||
|
3. 【用户事实过滤】严格去除所有与用户主体相关的客观事实信息,包括但不限于:
|
||||||
|
- 用户的计划、意向、时间安排、人数规模、行程动向;
|
||||||
|
- 用户当前的状态、情绪、顾虑、已做出的决定;
|
||||||
|
- 用户提及的任何"我/我们"视角的行为描述。
|
||||||
|
仅保留销售/服务方需要执行的动作、接待安排、标准流程、适用条件与门槛。
|
||||||
|
4. 【主题相关性过滤】严格过滤与当前回复主题无关的历史信息,包括但不限于:
|
||||||
|
- 已明确终止的咨询记录(如用户已拒绝并结束的话题);
|
||||||
|
- 与当前课程方向无关的其他技术栈/工具争议;
|
||||||
|
- 负面情绪或人际冲突类描述(如"互删""不考虑"等)。
|
||||||
|
仅保留对当前回复有指导价值的事实要点。
|
||||||
|
5. 【纯事实提取】只提取客观事实要点,不生成完整回复,不添加销售话术、过渡句或主观推测。
|
||||||
|
6. 【空值处理】若无核心数字或适用条件,对应字段输出为空列表 `[]` 或留空,不编造。
|
||||||
|
|
||||||
|
# 输出格式
|
||||||
|
严格按以下 YAML 输出,禁止添加额外解释:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
回复主题:
|
||||||
|
|
||||||
|
关键信息点:
|
||||||
|
- 信息:
|
||||||
|
|
||||||
|
核心数字:
|
||||||
|
-
|
||||||
|
|
||||||
|
适用条件/门槛:
|
||||||
|
-
|
||||||
|
"""
|
||||||
class TextExtract:
|
class TextExtract:
|
||||||
|
|
||||||
async def text_extract(self, text:str)->str:
|
async def text_extract(self, text:str)->str:
|
||||||
@ -41,3 +82,12 @@ class TextExtract:
|
|||||||
chain = prompt_template | llm |str_output_parser
|
chain = prompt_template | llm |str_output_parser
|
||||||
answer = await chain.ainvoke({"text": text})
|
answer = await chain.ainvoke({"text": text})
|
||||||
return answer
|
return answer
|
||||||
|
|
||||||
|
async def message_extract(self, message:str, milvus_content:str, standard:str)->str:
|
||||||
|
|
||||||
|
prompt_template = PromptTemplate(template=general_merge_prompt, input_variables=["message", "milvus_content", "standard"])
|
||||||
|
str_output_parser = StrOutputParser()
|
||||||
|
chain = prompt_template | llm |str_output_parser
|
||||||
|
answer = await chain.ainvoke({"message": message, "milvus_content": milvus_content, "standard": standard})
|
||||||
|
return answer
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -5,56 +5,35 @@ from langchain_core.output_parsers import StrOutputParser
|
|||||||
from langchain_core.prompts import PromptTemplate
|
from langchain_core.prompts import PromptTemplate
|
||||||
|
|
||||||
from app.llm import llm
|
from app.llm import llm
|
||||||
from app.repository.milvus.intent_classification_repository import IntentClassificationRepository
|
|
||||||
|
|
||||||
intent_prompt = """
|
intent_prompt = """
|
||||||
你是一个专业的意图提取专家。请分析以下文本内容,并结合当前已有的意图标签,判断是否符合;若不符合则生成一个新意图,符合则直接返回原意图。
|
你是一个意图分类专家。你的任务是根据给定的文本内容,判断它是否匹配当前意图。如果不匹配,则从意图列表中选择最合适的意图。
|
||||||
|
|
||||||
## 输入说明
|
## 输入信息
|
||||||
- **文本内容**:`{text}`
|
- **文本内容**:{text}(可能为多段文本,需综合判断整体核心意图,而非逐段独立判断)
|
||||||
- **目前的意图**:`{intent}`(可能为空)
|
- **当前意图**:{intent}(可能为空)
|
||||||
|
- **意图列表**:{intent_list}
|
||||||
|
|
||||||
## 核心规则
|
## 意图层级说明
|
||||||
|
意图列表中存在层级关系,分类时须优先识别:
|
||||||
|
- **概括性意图**:能够涵盖多个独立的具体子意图,例如 `学习模式`(涵盖线上直播教学、线下面授教学)、`课程咨询`(涵盖费用、周期、内容等综合问题)、`提供服务` 等。
|
||||||
|
- **具体意图**:描述单一、明确的独立事项,例如 `线上直播教学`、`线下面授教学`、`UE引擎开发`、`住宿咨询`、`上课时间` 等。
|
||||||
|
|
||||||
### 1. 意图提取与对比规则
|
## 判断规则
|
||||||
- 首先从文本中提取1个核心意图(提取方法见下方“意图提取规则”)。
|
1. 如果当前意图为空,直接从意图列表中选择最匹配的意图。
|
||||||
- 将提取出的意图与给定的`{{intent}}`进行对比:
|
2. **多意图混合优先原则(最重要)**:仅当文本**同时涉及意图列表中两个或以上独立意图的实质性并列、对比或综合介绍**时,才优先选择能够概括这些内容的上层意图(如 `学习模式`、`课程咨询`)。**严禁**把单一意图内部的多维度展开(如某课程包含多个技术点、案例类型、学习阶段)误判为多意图混合。
|
||||||
- 若两者**完全相同**(字符串完全一致),则最终输出意图为原意图`{{intent}}`。
|
3. **当前意图保持的严格条件**:只有当文本内容**主要且专门**围绕当前意图展开,且**不涉及其他独立意图的实质性并列或对比内容**时,才保持当前意图不变。如果文本在匹配当前意图的同时,还明显涉及其他独立意图的对比、补充或并列说明,则必须重新从意图列表中选择最合适的概括性意图,**不得因当前意图非空而强行保持**。
|
||||||
- 若两者不同,或`{{intent}}`为空,则最终输出意图为新提取的意图。
|
4. 如果文本内容与当前意图完全不匹配(包括语义偏差、主题不符、情感不一致等),从意图列表中选择最匹配的意图进行替换。
|
||||||
- 无论采用原意图还是新意图,都必须来自或可扩充自“意图分类体系”。
|
5. 如果意图列表中没有完全匹配的意图,选择最接近的一个,**不要自行创造新意图**。
|
||||||
|
6. 如果文本内容模糊、无法判断,选择当前意图(若为空则选择意图列表中的"其他"或第一个兜底意图)。
|
||||||
|
|
||||||
### 2. 意图提取规则(用于从文本中提取核心意图)
|
## 分析步骤(必须执行)
|
||||||
- 若文本包含用户明确的问题,提取其核心诉求对应的意图。
|
1. **提取核心主题**:综合分析文本列表的整体核心主题,忽略边缘细节。
|
||||||
- 若文本为销售介绍/陈述内容,**聚焦于其最想传达的核心功能或核心信息**,提取总结性意图。**不得将补充说明信息(如附带提到的地点、时间等)误判为核心意图。**
|
2. **判定是否多意图混合**:检查文本是否涉及**意图列表中两个或以上独立意图**的并列、对比或综合。注意:单一意图内部的技术点、案例、学习阶段等展开**不构成**多意图混合。
|
||||||
- 例如:文本为“线上课程后期可以转到线下,我们线下校区在上海”,核心是“线上转线下”这一功能,而非“校区分布”,因此应扩充为“线上转线下”,而不是使用“校区分布”。
|
3. **层级匹配**:若确认为多意图混合,匹配概括性意图;若为核心单一意图,匹配最具体的意图。
|
||||||
- 意图必须基于实际内容,不编造。
|
4. **对比当前意图**:如果当前意图不为空,检查当前意图是否能准确概括文本**整体**内容。若不能,则放弃当前意图,重新选择。
|
||||||
- 仅提取1个意图标签。
|
|
||||||
|
|
||||||
### 3. 单意图选择规则(关键)
|
## 输出要求
|
||||||
**只能输出一个意图标签,不能以顿号、逗号或“和”分隔多个意图。**
|
|
||||||
|
|
||||||
当内容可能对应多个意图时,按以下优先级选择唯一意图:
|
|
||||||
|
|
||||||
| 优先级 | 选择规则 | 说明 |
|
|
||||||
|-------|---------|------|
|
|
||||||
| **P0** | **优先匹配已有意图标签** | 必须优先从已有意图体系(含扩展示例)中选择最匹配的标签。**若已有标签中无准确对应,则按扩充规则创建新标签,严禁使用仅部分相关或语义模糊的已有标签。** 当销售单方面介绍课程内容(无用户明确提问)时,直接使用对应的主题标签(如“3D场景建模”),若主题为已有标签未覆盖的功能/服务(如“线上转线下”),则应扩充新标签,不得强行归类为“校区分布”等。 |
|
|
||||||
| **P1** | **核心诉求优先** | 选择最能代表文本核心诉求的意图,**忽略次要的补充信息**。例如:文本“线上课程可转线下,校区在上海” → 核心是“线上转线下”,而非“校区分布”;报名流程说明 → “报名咨询”,而非“线上直播教学”。 |
|
|
||||||
| **P2** | **主题维度优先于动作维度** | 当主题和动作诉求冲突时,优先选择主题维度。例如:3D建模费用 → “3D建模”(主题) |
|
|
||||||
| **P3** | **直接提及优先于间接关联** | 文本明确说出的核心意图优先于推断出的意图 |
|
|
||||||
| **P4** | **去重合并** | 多个子诉求属于同一类别时,只输出一个意图标签 |
|
|
||||||
|
|
||||||
### 4. 意图分类体系(必须从中选择或基于其扩充)
|
|
||||||
{intent_list}
|
|
||||||
|
|
||||||
**扩充规则:** 当文本核心意图无法被已有标签准确覆盖时,必须创建更精准的新标签。创建时:
|
|
||||||
- 新标签应简洁体现核心语义(如“线上转线下”)。
|
|
||||||
- 严格对比已有标签,确保无重复语义。
|
|
||||||
- **示例**:文本“线上课程后期可以转到线下” → 已有标签无对应,扩充为“线上转线下”;文本“原画好就业吗” → 扩充为“原画就业”。
|
|
||||||
|
|
||||||
### 5. 多意图处理
|
|
||||||
若同一段文本明确涉及多个不重叠的核心意图,仅选择最能代表核心内容的1个意图。
|
|
||||||
|
|
||||||
## 输出格式要求
|
|
||||||
- 输出纯JSON,不要任何解释、不要markdown标记、不要注释
|
- 输出纯JSON,不要任何解释、不要markdown标记、不要注释
|
||||||
- 确保所有字符串用双引号包裹
|
- 确保所有字符串用双引号包裹
|
||||||
- 输出格式固定为:
|
- 输出格式固定为:
|
||||||
|
|||||||
@ -45,7 +45,7 @@ if __name__ == '__main__':
|
|||||||
archive_messages_mysql_repository = ArchiveMessagesRepository(db_session)
|
archive_messages_mysql_repository = ArchiveMessagesRepository(db_session)
|
||||||
qa_milvus_repository = QARepository()
|
qa_milvus_repository = QARepository()
|
||||||
|
|
||||||
state: QueryQAState = QueryQAState(original_query="电脑需要什么配置", history=['2026-07-10T15:42:13 EXTERNAL 我想学角色,我不知道能不能学,好不好学\n',"老师 现在原画好就业吗","好的,老师,你这边就业,是推荐就业吗?还是包就业呀"])
|
state: QueryQAState = QueryQAState(original_query="学习原画好就业吗", history=['2026-07-28T17:37:14 EXTERNAL 还有 3D 场景建模的试听课,你发我一下。另外,住宿要多少钱呢?一个月\n'])
|
||||||
context = QueryQAContext(meta_mysql_repository=archive_messages_mysql_repository,
|
context = QueryQAContext(meta_mysql_repository=archive_messages_mysql_repository,
|
||||||
qa_milvus_repository=qa_milvus_repository
|
qa_milvus_repository=qa_milvus_repository
|
||||||
)
|
)
|
||||||
|
|||||||
@ -216,64 +216,107 @@ problem_rewriting_prompt1 = """
|
|||||||
请直接输出JSON:
|
请直接输出JSON:
|
||||||
"""
|
"""
|
||||||
problem_rewriting_prompt = """
|
problem_rewriting_prompt = """
|
||||||
|
# 智能客服意图分类助手提示词
|
||||||
|
|
||||||
你是一名智能客服意图分类助手。你的任务是根据用户的历史会话和当前问题,提取用户的核心诉求,并输出标准化的意图标签。
|
你是一名智能客服意图分类助手。你的任务是根据用户的历史会话和当前问题,提取用户的核心诉求,并输出标准化的意图标签。
|
||||||
|
|
||||||
## 参数说明
|
## 参数说明
|
||||||
|
|
||||||
本提示词需要动态传入以下参数:
|
本提示词需要动态传入以下参数:
|
||||||
- {query}:用户当前提问
|
|
||||||
- {history_text}:历史会话记录(可为空)
|
- `{query}`:用户当前提问
|
||||||
- {intent_tags}:完整的意图标签列表,格式为JSON字符串数组,例如:["3D建模", "2D原画", "课程咨询", "就业咨询", ...]。**所有输出的意图标签必须严格从该列表中选择,不得自创或修改。**
|
- `{history_text}`:历史会话记录(可为空)
|
||||||
|
- `{intent_tags}`:完整的意图标签列表,格式为 JSON 字符串数组,例如:`["3D建模", "2D原画", "课程咨询", "就业咨询", ...]`。**所有输出的意图标签必须严格从该列表中选择,不得自创或修改。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 核心原则(必须严格遵守)
|
## 核心原则(必须严格遵守)
|
||||||
|
|
||||||
### 1. 双维度提取原则
|
### 1. 双维度提取原则
|
||||||
|
|
||||||
每个用户问题通常包含两个维度的意图,必须分别识别:
|
每个用户问题通常包含两个维度的意图,必须分别识别:
|
||||||
|
|
||||||
- **课程/服务主题维度**:用户想咨询的具体课程类型或业务主题
|
- **课程/服务主题维度**:用户想咨询的具体课程类型或业务主题
|
||||||
- **诉求/动作维度**:用户想了解的方面(费用、就业、难度等)
|
- **诉求/动作维度**:用户想了解的方面(费用、就业、难度等)
|
||||||
|
|
||||||
这两个维度相互独立,必须同时提取,不得遗漏任何一个。最终输出的标签本身不区分维度,但模型内部需要判断每个标签的维度,以便后续优先级排序(见P3规则)。判断维度的方法:若标签名称中包含“咨询”、“怎么办”、“分布”等特征词,通常属于诉求/动作维度;否则多属于主题维度。请结合语义自行判断。
|
这两个维度相互独立,必须同时提取,不得遗漏任何一个。最终输出的标签本身不区分维度,但模型内部需要判断每个标签的维度,以便后续优先级排序(见 P3 规则)。
|
||||||
|
|
||||||
|
**维度判定规则(必须按语义本质判断,禁止仅看标签字面后缀):**
|
||||||
|
|
||||||
|
- **主题维度**:表示用户想学习的**具体课程类型、专业方向或业务领域**。
|
||||||
|
例如:`3D建模`、`3D场景建模`、`3D角色建模`、`2D原画`、`UE引擎开发`、`游戏特效`、`地编`、`技术美术`、`游戏动作/分镜`、`二次元风格`、`写实风格` 等。
|
||||||
|
|
||||||
|
- **诉求/动作维度**:表示用户**想了解什么、想做什么、担心什么、需要什么服务**。
|
||||||
|
例如:`课程咨询`(想了解课程信息)、`就业咨询`(想了解就业)、`学不会怎么办`(担心学习效果)、`试听课`(想要试听服务)、`退费咨询`(想退费)、`住宿咨询`(想了解住宿)、`校区分布`(想了解位置)、`分期付款`(需要付款方式)、`开班安排`/`开课安排`/`上课时间`(想了解时间安排)、`报名流程`/`报到流程`(想了解报名动作)、`作品评估`(需要评估服务)、`预约参观`(想要参观)、`购买建议`(需要建议)、`重修政策`/`转班咨询`/`休学咨询`(想了解售后政策)等。
|
||||||
|
|
||||||
|
**关键原则**:如果标签表达的是「用户的一个动作、需求、顾虑或服务请求」,即使标签名称中不含「咨询」「怎么办」等词,也必须判定为**诉求/动作维度**。禁止仅凭标签名称是否包含特定后缀来判断维度。
|
||||||
|
|
||||||
|
**中心语优先原则(强制)**
|
||||||
|
|
||||||
|
在包含"的"字的偏正短语中(如"3D场景建模的试听课"、"角色建模的课程内容"),**"的"字后面的成分(中心语)代表用户的核心诉求**,"的"字前面的成分(定语)仅为主题限定。
|
||||||
|
- 中心语(诉求维度)的优先级 **必须高于** 定语(主题维度)。
|
||||||
|
- 出现顺序判定以**中心语**在短语中的位置为准,而非字面首个词。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 2. 并列结构强制拆分
|
### 2. 并列结构强制拆分
|
||||||
|
|
||||||
识别以下并列模式并强制拆分:
|
识别以下并列模式并强制拆分:
|
||||||
- 顿号/逗号分隔:"A、B、C" → [A, B, C]
|
|
||||||
- "和/与/及/还有"连接:"A和B" → [A, B]
|
- 顿号/逗号分隔:`"A、B、C"` → `[A, B, C]`
|
||||||
- 疑问词并列:"多少钱、怎么报名、有就业吗" → [多少钱, 怎么报名, 有就业吗]
|
- `"和/与/及/还有"` 连接:`"A和B"` → `[A, B]`
|
||||||
|
- 疑问词并列:`"多少钱、怎么报名、有就业吗"` → `[多少钱, 怎么报名, 有就业吗]`
|
||||||
|
|
||||||
**规则**:拆分后,每个子项必须独立分析,不得合并。
|
**规则**:拆分后,每个子项必须独立分析,不得合并。
|
||||||
|
|
||||||
### 3. 意图识别置信度评分体系
|
---
|
||||||
每个意图标签在输出前必须经过置信度评分,**只有总分≥60分的标签才会被输出**。
|
|
||||||
|
|
||||||
#### 评分维度:
|
### 3. 意图识别置信度评分体系
|
||||||
|
|
||||||
|
每个意图标签在输出前必须经过置信度评分,**只有总分 ≥ 60 分的标签才会被输出**。
|
||||||
|
|
||||||
|
#### 评分维度
|
||||||
|
|
||||||
| 评分维度 | 分值 | 触发条件 | 说明 |
|
| 评分维度 | 分值 | 触发条件 | 说明 |
|
||||||
|---------|------|---------|------|
|
|---------|------|---------|------|
|
||||||
| **直接匹配** | **60分** | 用户问题中直接出现标签关键词或同义词扩展词(见第5节) | 关键词完全匹配或同义词匹配,直接给60分,超过阈值 |
|
| **直接匹配** | **60分** | 用户问题中直接出现标签关键词或同义词扩展词(见第5节) | 关键词完全匹配或同义词匹配,直接给 60 分,超过阈值 |
|
||||||
| **语义关联** | **40分** | 用户表达与标签语义高度相关,通过语义场推断 | 语义场匹配给40分,需配合上下文支持达到阈值 |
|
| **语义关联** | **40分** | 用户表达与标签语义高度相关,通过语义场推断 | 语义场匹配给 40 分,需配合上下文支持达到阈值 |
|
||||||
| **上下文支持** | **20分** | 历史会话或当前上下文支持该意图推断 | 与语义关联配合:40+20=60分,刚好达到阈值 |
|
| **上下文支持** | **20分** | 历史会话或当前上下文支持该意图推断 | 与语义关联配合:40 + 20 = 60 分,刚好达到阈值 |
|
||||||
| **历史补全** | **10分** | 意图完全依赖历史会话补全 | 仅作为辅助加分,不单独使用 |
|
| **历史补全** | **10分** | 意图完全依赖历史会话补全 | 仅作为辅助加分,不单独使用 |
|
||||||
|
|
||||||
#### 阈值判定规则:
|
#### 阈值判定规则
|
||||||
- **总分 ≥ 60分** → 进入候选池
|
|
||||||
- **总分 < 60分** → 直接过滤,不进入候选池
|
|
||||||
- 无任何意图≥60分 → 输出 `{{"intents": []}}`
|
|
||||||
|
|
||||||
### 4. 强制截断规则(新增)
|
- **总分 ≥ 60 分** → 进入候选池
|
||||||
**无论识别到多少个意图,最终输出最多只返回2个意图标签。**
|
- **总分 < 60 分** → 直接过滤,不进入候选池
|
||||||
|
- 无任何意图 ≥ 60 分 → 输出 `{{"intents": []}}`
|
||||||
|
|
||||||
当候选池中超过2个意图时,按以下优先级排序,只保留前2个:
|
---
|
||||||
|
|
||||||
|
### 4. 强制截断规则
|
||||||
|
|
||||||
|
**无论识别到多少个意图,最终输出最多只返回 2 个意图标签。**
|
||||||
|
|
||||||
|
当候选池中超过 2 个意图时,按以下优先级排序,只保留前 2 个:
|
||||||
|
|
||||||
| 优先级 | 排序规则 | 说明 |
|
| 优先级 | 排序规则 | 说明 |
|
||||||
|-------|---------|------|
|
|-------|---------|------|
|
||||||
| **P1(最高)** | 当前问题直接明确表达的 > 历史会话补全的 | 用户当前明确说出的诉求优先 |
|
| **P1(最高)** | 当前问题直接明确表达的 > 历史会话补全的 | 用户当前明确说出的诉求优先 |
|
||||||
| **P2** | 总分高的 > 总分低的 | 置信度更高的优先 |
|
| **P2** | 总分高的 > 总分低的 | 置信度更高的优先 |
|
||||||
| **P3** | 诉求维度 > 主题维度 | 用户更关心“做什么”而非“是什么” |
|
| **P3** | 诉求维度 > 主题维度 | 用户更关心"做什么"而非"是什么" |
|
||||||
| **P4** | 出现顺序靠前的 > 出现顺序靠后的 | 用户先说的一般更关心 |
|
| **P4** | 核心诉求出现顺序靠前的 > 靠后的 | 同一子诉求内,中心语(诉求维度)优先于定语(主题维度);跨子诉求时,按子诉求在原文中的先后顺序 |
|
||||||
|
|
||||||
**截断规则**:排序后,只保留前2个,其余全部丢弃。
|
**P4 执行细则(强制)**
|
||||||
|
|
||||||
|
排序时必须严格遵循 P1 → P2 → P3 → P4 的层级顺序,**严禁跨层级比较**。即:仅当 P1、P2、P3 均相同或无法区分时,才启用 P4。
|
||||||
|
在"主题+诉求"的复合表达(如"X的Y")中,Y(诉求维度)视为该子诉求的核心,其 P4 顺序位始终优先于 X(主题维度)。
|
||||||
|
|
||||||
|
**截断规则**:排序后,只保留前 2 个,其余全部丢弃。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 5. 语义同义词扩展匹配
|
### 5. 语义同义词扩展匹配
|
||||||
匹配时不仅使用标签字面本身,还必须覆盖常见的同义词扩展。以下为常见标签的同义词映射表(**当且仅当 {intent_tags} 中包含与下列标准标签完全同名的标签时,才可使用该扩展**;若传入的标签不在该表中,则根据标签字面及通用语义进行匹配,禁止编造扩展词):
|
|
||||||
|
匹配时不仅使用标签字面本身,还必须覆盖常见的同义词扩展。以下为常见标签的同义词映射表(**当且仅当 `{intent_tags}` 中包含与下列标准标签完全同名的标签时,才可使用该扩展**;若传入的标签不在该表中,则根据标签字面及通用语义进行匹配,禁止编造扩展词):
|
||||||
|
|
||||||
- **就业咨询**:就业、找工作、工作推荐、就业支持、就业服务、就业保障、就业率、包就业、毕业安排、工作分配、推荐工作、就业方向、职业发展、入职、岗位推荐、能就业吗、好找工作吗、就业情况、就业数据、薪资、工资待遇、收入水平
|
- **就业咨询**:就业、找工作、工作推荐、就业支持、就业服务、就业保障、就业率、包就业、毕业安排、工作分配、推荐工作、就业方向、职业发展、入职、岗位推荐、能就业吗、好找工作吗、就业情况、就业数据、薪资、工资待遇、收入水平
|
||||||
- **学不会怎么办**:学不会、跟不上、没基础、怕学不会、能不能学、能学会吗、学得会吗、适合我学吗、零基础能学吗、没经验能学吗、纯小白能学吗、好不好学、难不难、难吗、简单吗、容易学吗、好学吗、有难度吗、门槛高吗、有门槛吗、担心自己学不会、不知道能不能学会、怕跟不上、怕太难、上手难、学不进、听不懂、理解不了
|
- **学不会怎么办**:学不会、跟不上、没基础、怕学不会、能不能学、能学会吗、学得会吗、适合我学吗、零基础能学吗、没经验能学吗、纯小白能学吗、好不好学、难不难、难吗、简单吗、容易学吗、好学吗、有难度吗、门槛高吗、有门槛吗、担心自己学不会、不知道能不能学会、怕跟不上、怕太难、上手难、学不进、听不懂、理解不了
|
||||||
@ -284,46 +327,61 @@ problem_rewriting_prompt = """
|
|||||||
- **校区分布**:校区、地址、在哪、位置、分布、哪个城市、怎么去、交通、附近
|
- **校区分布**:校区、地址、在哪、位置、分布、哪个城市、怎么去、交通、附近
|
||||||
- **优势/竞争力**:优势、竞争力、为什么选你们、和其他机构比、口碑、排名、实力、师资、教学质量
|
- **优势/竞争力**:优势、竞争力、为什么选你们、和其他机构比、口碑、排名、实力、师资、教学质量
|
||||||
|
|
||||||
(若传入的标签列表包含上述标签以外的标签,同义词匹配将仅基于标签文字及通用语义,不得臆造扩展。)
|
(若传入的标签列表包含上述标签以外的标签,同义词匹配将仅基于标签文字及通用语义,不得臆造扩展词。)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 6. 信息补全与语义保持
|
### 6. 信息补全与语义保持
|
||||||
|
|
||||||
- 如果当前问题缺少主语,从历史会话补全
|
- 如果当前问题缺少主语,从历史会话补全
|
||||||
- 不得改变用户原始意图,不得引入历史会话中不存在的新信息
|
- 不得改变用户原始意图,不得引入历史会话中不存在的新信息
|
||||||
- 不得遗漏当前问题中的任何关键诉求
|
- 不得遗漏当前问题中的任何关键诉求
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 输出格式
|
## 输出格式
|
||||||
- 仅输出JSON,不要任何解释性文字
|
|
||||||
- **最多只返回2个意图标签**,超出部分强制截断
|
- 仅输出 JSON,不要任何解释性文字
|
||||||
- 所有标签必须来自传入的 {intent_tags} 列表
|
- **最多只返回 2 个意图标签**,超出部分强制截断
|
||||||
- 输出格式示例:`{{"intents": ["标签1", "标签2"]}}` (若候选为空则 `{{"intents": []}}`)
|
- 所有标签必须来自传入的 `{intent_tags}` 列表
|
||||||
|
- 输出格式示例:`{{"intents": ["标签1", "标签2"]}}`(若候选为空则 `{{"intents": []}}`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 分析流程(必须按顺序执行)
|
## 分析流程(必须按顺序执行)
|
||||||
|
|
||||||
### 步骤1:并列拆分
|
### 步骤 1:并列拆分
|
||||||
将当前问题拆分为独立的子诉求。
|
将当前问题拆分为独立的子诉求。
|
||||||
|
|
||||||
### 步骤2:双维度扫描 + 置信度评分
|
### 步骤 2:双维度扫描 + 置信度评分
|
||||||
对每个子诉求,扫描 {intent_tags} 中的标签,按照第3节和第5节计算总分。每个意图独立评分。
|
对每个子诉求,扫描 `{intent_tags}` 中的标签,按照第 3 节和第 5 节计算总分。每个意图独立评分。
|
||||||
|
|
||||||
### 步骤3:阈值过滤
|
### 步骤 3:阈值过滤
|
||||||
只保留总分≥60分的意图,进入候选池。
|
只保留总分 ≥ 60 分的意图,进入候选池。
|
||||||
|
|
||||||
### 步骤4:优先级排序(当候选池超过2个时)
|
### 步骤 4:优先级排序(当候选池超过 2 个时)
|
||||||
按P1→P2→P3→P4规则排序。
|
按 P1 → P2 → P3 → P4 规则排序。
|
||||||
|
|
||||||
### 步骤5:强制截断
|
### 步骤 5:强制截断
|
||||||
只保留排序后的前2个意图标签。
|
只保留排序后的前 2 个意图标签。
|
||||||
|
|
||||||
|
### 步骤 6:完整性自检(输出前必须执行)
|
||||||
|
|
||||||
### 步骤6:完整性自检(输出前必须执行)
|
|
||||||
- [ ] 每个子诉求是否都经过了置信度评分?
|
- [ ] 每个子诉求是否都经过了置信度评分?
|
||||||
- [ ] 是否有总分≥60分的意图被遗漏?
|
- [ ] 是否有总分 ≥ 60 分的意图被遗漏?
|
||||||
- [ ] 最终输出是否≤2个标签?
|
- [ ] 最终输出是否 ≤ 2 个标签?
|
||||||
- [ ] 截断时是否按优先级规则执行?
|
- [ ] 截断时是否按优先级规则执行?
|
||||||
- [ ] 所有输出的标签是否都在 {intent_tags} 中?
|
- [ ] 所有输出的标签是否都在 `{intent_tags}` 中?
|
||||||
|
- [ ] 每个标签的维度判定是否基于语义本质而非字面后缀?
|
||||||
|
- [ ] 是否遵循了中心语优先原则("的"字短语中诉求维度优先于主题维度)?
|
||||||
|
|
||||||
如果任何一项为“否”,重新分析。
|
如果任何一项为"否",重新分析。
|
||||||
|
|
||||||
## 分类示例(假设传入标签列表包含:3D建模、3D角色建模、课程咨询、就业咨询、住宿咨询、退费咨询、学不会怎么办)
|
---
|
||||||
|
|
||||||
|
## 分类示例
|
||||||
|
|
||||||
|
(假设传入标签列表包含:`3D建模`、`3D角色建模`、`课程咨询`、`就业咨询`、`住宿咨询`、`退费咨询`、`学不会怎么办`、`试听课`、`3D场景建模`)
|
||||||
|
|
||||||
### 示例1(2个意图,无需截断)
|
### 示例1(2个意图,无需截断)
|
||||||
历史会话:(空)
|
历史会话:(空)
|
||||||
@ -356,7 +414,7 @@ problem_rewriting_prompt = """
|
|||||||
- 诉求维度2:"不知道能不能学" → 语义关联(40) + 上下文支持(20) = 60分 → 候选池
|
- 诉求维度2:"不知道能不能学" → 语义关联(40) + 上下文支持(20) = 60分 → 候选池
|
||||||
- 诉求维度3:"好不好学" → 语义关联(40) + 上下文支持(20) = 60分 → 候选池(与上同标签,去重)
|
- 诉求维度3:"好不好学" → 语义关联(40) + 上下文支持(20) = 60分 → 候选池(与上同标签,去重)
|
||||||
- 候选池:["3D角色建模", "课程咨询", "学不会怎么办"],共3个,需截断
|
- 候选池:["3D角色建模", "课程咨询", "学不会怎么办"],共3个,需截断
|
||||||
- 排序:课程咨询(P1当前直接) > 学不会怎么办(P1当前直接) > 3D角色建模(P3主题维度)
|
- 排序:课程咨询(P1当前直接) > 学不会怎么办(P1当前直接) > 3D角色建模(P3主题维度)
|
||||||
- 截断后保留前2个
|
- 截断后保留前2个
|
||||||
分类结果:
|
分类结果:
|
||||||
{{"intents": ["课程咨询", "学不会怎么办"]}}
|
{{"intents": ["课程咨询", "学不会怎么办"]}}
|
||||||
@ -371,7 +429,7 @@ problem_rewriting_prompt = """
|
|||||||
- "住宿" → 直接匹配"住宿咨询" = 60分 → 候选池
|
- "住宿" → 直接匹配"住宿咨询" = 60分 → 候选池
|
||||||
- "退费" → 直接匹配"退费咨询" = 60分 → 候选池
|
- "退费" → 直接匹配"退费咨询" = 60分 → 候选池
|
||||||
- 候选池共5个,需截断
|
- 候选池共5个,需截断
|
||||||
- 排序:课程咨询(P1当前直接,出现第2) > 就业咨询(P1当前直接,出现第3) > 住宿咨询 > 退费咨询 > 3D建模(P3主题维度)
|
- 排序:课程咨询(P1当前直接,出现第2) > 就业咨询(P1当前直接,出现第3) > 住宿咨询 > 退费咨询 > 3D建模(P3主题维度)
|
||||||
- 截断后保留前2个
|
- 截断后保留前2个
|
||||||
分类结果:
|
分类结果:
|
||||||
{{"intents": ["课程咨询", "就业咨询"]}}
|
{{"intents": ["课程咨询", "就业咨询"]}}
|
||||||
@ -381,12 +439,12 @@ problem_rewriting_prompt = """
|
|||||||
- 用户:我想学3D建模
|
- 用户:我想学3D建模
|
||||||
当前问题:多少钱、就业情况、住宿条件
|
当前问题:多少钱、就业情况、住宿条件
|
||||||
分析:
|
分析:
|
||||||
- 主题维度:"3D建模"历史补全 = 10分 < 60分 → 不进入候选池
|
- 主题维度:"3D建模"历史补全 = 10分 < 60分 → 不进入候选池
|
||||||
- "多少钱" → 直接匹配"课程咨询" = 60分 → 候选池
|
- "多少钱" → 直接匹配"课程咨询" = 60分 → 候选池
|
||||||
- "就业情况" → 直接匹配"就业咨询" = 60分 → 候选池
|
- "就业情况" → 直接匹配"就业咨询" = 60分 → 候选池
|
||||||
- "住宿条件" → 直接匹配"住宿咨询" = 60分 → 候选池
|
- "住宿条件" → 直接匹配"住宿咨询" = 60分 → 候选池
|
||||||
- 候选池:["课程咨询", "就业咨询", "住宿咨询"],共3个,需截断
|
- 候选池:["课程咨询", "就业咨询", "住宿咨询"],共3个,需截断
|
||||||
- 排序:课程咨询(P1,出现第1) > 就业咨询(P1,出现第2) > 住宿咨询(P1,出现第3)
|
- 排序:课程咨询(P1,出现第1) > 就业咨询(P1,出现第2) > 住宿咨询(P1,出现第3)
|
||||||
- 截断后保留前2个
|
- 截断后保留前2个
|
||||||
分类结果:
|
分类结果:
|
||||||
{{"intents": ["课程咨询", "就业咨询"]}}
|
{{"intents": ["课程咨询", "就业咨询"]}}
|
||||||
@ -395,11 +453,31 @@ problem_rewriting_prompt = """
|
|||||||
历史会话:(空)
|
历史会话:(空)
|
||||||
当前问题:这个课有意思吗
|
当前问题:这个课有意思吗
|
||||||
分析:
|
分析:
|
||||||
- "有意思" → 语义关联"课程咨询" = 40分,无上下文支持 = 0分,总分40分 < 60分 → 不进入候选池
|
- "有意思" → 语义关联"课程咨询" = 40分,无上下文支持 = 0分,总分40分 < 60分 → 不进入候选池
|
||||||
- 候选池为空
|
- 候选池为空
|
||||||
分类结果:
|
分类结果:
|
||||||
{{"intents": []}}
|
{{"intents": []}}
|
||||||
|
|
||||||
|
### 示例 7(中心语优先 + P3 排序生效)
|
||||||
|
历史会话:(空)
|
||||||
|
当前问题:还有 3D 场景建模的试听课,你发我一下。另外,住宿要多少钱呢?
|
||||||
|
分析:
|
||||||
|
拆分:[3D场景建模的试听课, 住宿要多少钱]
|
||||||
|
- 子诉求1:"3D场景建模的试听课"
|
||||||
|
- 主题维度:"3D场景建模" → 直接匹配 = 60分 → 候选池
|
||||||
|
- 诉求维度(中心语):"试听课" → 直接匹配 = 60分,语义为「用户想要试听服务」→ 候选池
|
||||||
|
- 子诉求2:"住宿要多少钱"
|
||||||
|
- 诉求维度:"住宿咨询" → 直接匹配 = 60分 → 候选池
|
||||||
|
- 候选池:["3D场景建模", "试听课", "住宿咨询"],共 3 个,需截断
|
||||||
|
- 排序:
|
||||||
|
- P1:三者均为当前直接表达,平级
|
||||||
|
- P2:三者均为60分,平级
|
||||||
|
- P3:试听课(诉求) = 住宿咨询(诉求) > 3D场景建模(主题)
|
||||||
|
- P4:试听课(子诉求1中心语)> 住宿咨询(子诉求2中心语)> 3D场景建模(子诉求1定语)
|
||||||
|
- 截断后保留前 2 个
|
||||||
|
分类结果:
|
||||||
|
{{"intents": ["试听课", "住宿咨询"]}}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 现在请处理以下输入
|
## 现在请处理以下输入
|
||||||
|
|||||||
@ -19,12 +19,12 @@ async def search_embed(state: QueryQAState):
|
|||||||
intent_class = IntentClassificationRepository()
|
intent_class = IntentClassificationRepository()
|
||||||
result: List[Dict[str, Any]] = await intent_class.search_by_intent_category(intent_category=rewritten_query, top_k=3)
|
result: List[Dict[str, Any]] = await intent_class.search_by_intent_category(intent_category=rewritten_query, top_k=3)
|
||||||
logger.info(f"原始问题:{original_query},改写后的问题:{rewritten_query},匹配结果:{result}")
|
logger.info(f"原始问题:{original_query},改写后的问题:{rewritten_query},匹配结果:{result}")
|
||||||
|
|
||||||
qa_pairs = []
|
qa_pairs = []
|
||||||
for item in result:
|
for item in result:
|
||||||
qa_pairs.append({
|
qa_pairs.append({
|
||||||
"intent_category": item.get("intent_category", ""),
|
"intent_category": item.get("intent_category", ""),
|
||||||
"content": item.get("content", ""),
|
"content": item.get("content", "") + "," +item.get("review_content", ""),
|
||||||
"score": item.get("score", 0)
|
"score": item.get("score", 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -35,6 +35,7 @@ class IntentClassificationRepository:
|
|||||||
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True, auto_id=True)
|
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True, auto_id=True)
|
||||||
schema.add_field(field_name="intent_category", datatype=DataType.VARCHAR, max_length=128)
|
schema.add_field(field_name="intent_category", datatype=DataType.VARCHAR, max_length=128)
|
||||||
schema.add_field(field_name="content", datatype=DataType.VARCHAR, max_length=65535)
|
schema.add_field(field_name="content", datatype=DataType.VARCHAR, max_length=65535)
|
||||||
|
schema.add_field(field_name="review_content", datatype=DataType.VARCHAR, max_length=65535)
|
||||||
schema.add_field(field_name="content_vector", datatype=DataType.FLOAT_VECTOR, dim=1024)
|
schema.add_field(field_name="content_vector", datatype=DataType.FLOAT_VECTOR, dim=1024)
|
||||||
schema.add_field(field_name="created_at", datatype=DataType.INT64)
|
schema.add_field(field_name="created_at", datatype=DataType.INT64)
|
||||||
|
|
||||||
@ -51,6 +52,90 @@ class IntentClassificationRepository:
|
|||||||
index_params=index_params
|
index_params=index_params
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
# 迁移
|
||||||
|
def migrate_add_review_content(self) -> bool:
|
||||||
|
if not milvus_client.has_collection(self.collection_name):
|
||||||
|
logger.error(f"集合 {self.collection_name} 不存在,无法迁移")
|
||||||
|
return False
|
||||||
|
|
||||||
|
temp_collection = f"{self.collection_name}_temp"
|
||||||
|
|
||||||
|
if milvus_client.has_collection(temp_collection):
|
||||||
|
milvus_client.client.drop_collection(temp_collection)
|
||||||
|
|
||||||
|
schema = milvus_client.client.create_schema(
|
||||||
|
auto_id=True,
|
||||||
|
enable_dynamic_field=True,
|
||||||
|
description="意图分类向量集合(迁移:新增review_content)"
|
||||||
|
)
|
||||||
|
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True, auto_id=True)
|
||||||
|
schema.add_field(field_name="intent_category", datatype=DataType.VARCHAR, max_length=128)
|
||||||
|
schema.add_field(field_name="content", datatype=DataType.VARCHAR, max_length=65535)
|
||||||
|
schema.add_field(field_name="review_content", datatype=DataType.VARCHAR, max_length=65535)
|
||||||
|
schema.add_field(field_name="content_vector", datatype=DataType.FLOAT_VECTOR, dim=1024)
|
||||||
|
schema.add_field(field_name="created_at", datatype=DataType.INT64)
|
||||||
|
|
||||||
|
index_params = milvus_client.client.prepare_index_params()
|
||||||
|
index_params.add_index(
|
||||||
|
field_name="content_vector",
|
||||||
|
index_type="HNSW",
|
||||||
|
metric_type="COSINE",
|
||||||
|
params={"M": 32, "efConstruction": 300}
|
||||||
|
)
|
||||||
|
milvus_client.client.create_collection(
|
||||||
|
collection_name=temp_collection,
|
||||||
|
schema=schema,
|
||||||
|
index_params=index_params
|
||||||
|
)
|
||||||
|
logger.info(f"创建临时集合 {temp_collection} 成功")
|
||||||
|
|
||||||
|
all_data = []
|
||||||
|
offset = 0
|
||||||
|
batch_size = 1000
|
||||||
|
while True:
|
||||||
|
batch = milvus_client.client.query(
|
||||||
|
collection_name=self.collection_name,
|
||||||
|
output_fields=["intent_category", "content", "content_vector", "created_at"],
|
||||||
|
limit=batch_size,
|
||||||
|
offset=offset
|
||||||
|
)
|
||||||
|
if not batch:
|
||||||
|
break
|
||||||
|
all_data.extend(batch)
|
||||||
|
if len(batch) < batch_size:
|
||||||
|
break
|
||||||
|
offset += batch_size
|
||||||
|
|
||||||
|
logger.info(f"查询到旧数据 {len(all_data)} 条,开始迁移")
|
||||||
|
|
||||||
|
migrated = 0
|
||||||
|
for i in range(0, len(all_data), batch_size):
|
||||||
|
batch = all_data[i:i + batch_size]
|
||||||
|
insert_batch = []
|
||||||
|
for item in batch:
|
||||||
|
insert_batch.append({
|
||||||
|
"intent_category": item.get("intent_category", ""),
|
||||||
|
"content": item.get("content", ""),
|
||||||
|
"review_content": "",
|
||||||
|
"content_vector": item.get("content_vector", []),
|
||||||
|
"created_at": item.get("created_at", 0)
|
||||||
|
})
|
||||||
|
milvus_client.client.insert(
|
||||||
|
collection_name=temp_collection,
|
||||||
|
data=insert_batch
|
||||||
|
)
|
||||||
|
migrated += len(insert_batch)
|
||||||
|
logger.info(f"迁移进度: {migrated}/{len(all_data)}")
|
||||||
|
|
||||||
|
logger.info(f"数据迁移完成,共 {len(all_data)} 条")
|
||||||
|
|
||||||
|
milvus_client.client.drop_collection(self.collection_name)
|
||||||
|
logger.info(f"旧集合 {self.collection_name} 已删除")
|
||||||
|
|
||||||
|
milvus_client.client.rename_collection(temp_collection, self.collection_name)
|
||||||
|
logger.info(f"临时集合重命名为 {self.collection_name},迁移完成")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def save_intent_data(self, data: List[Dict[str, Any]]) -> List[int]:
|
async def save_intent_data(self, data: List[Dict[str, Any]]) -> List[int]:
|
||||||
@ -61,6 +146,7 @@ class IntentClassificationRepository:
|
|||||||
insert_data.append({
|
insert_data.append({
|
||||||
"intent_category": item.get("intent_category", ""),
|
"intent_category": item.get("intent_category", ""),
|
||||||
"content": item.get("content", ""),
|
"content": item.get("content", ""),
|
||||||
|
"review_content": item.get("review_content", ""),
|
||||||
"content_vector": item.get("content_vector", []),
|
"content_vector": item.get("content_vector", []),
|
||||||
"created_at": created_at
|
"created_at": created_at
|
||||||
})
|
})
|
||||||
@ -107,6 +193,7 @@ class IntentClassificationRepository:
|
|||||||
data=[{
|
data=[{
|
||||||
"intent_category": category,
|
"intent_category": category,
|
||||||
"content": content,
|
"content": content,
|
||||||
|
"review_content": item.get("review_content", ""),
|
||||||
"content_vector": vector,
|
"content_vector": vector,
|
||||||
"created_at": created_at
|
"created_at": created_at
|
||||||
}]
|
}]
|
||||||
@ -128,7 +215,7 @@ class IntentClassificationRepository:
|
|||||||
result = milvus_client.client.query(
|
result = milvus_client.client.query(
|
||||||
collection_name=self.collection_name,
|
collection_name=self.collection_name,
|
||||||
filter=expr,
|
filter=expr,
|
||||||
output_fields=["intent_category", "content", "created_at"],
|
output_fields=["intent_category", "content", "review_content", "created_at"],
|
||||||
limit=top_k
|
limit=top_k
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
@ -143,7 +230,7 @@ class IntentClassificationRepository:
|
|||||||
"metric_type": "COSINE",
|
"metric_type": "COSINE",
|
||||||
"efSearch": 300
|
"efSearch": 300
|
||||||
},
|
},
|
||||||
output_fields=["intent_category", "content", "created_at"]
|
output_fields=["intent_category", "content", "review_content", "created_at"]
|
||||||
)
|
)
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
@ -152,6 +239,7 @@ class IntentClassificationRepository:
|
|||||||
item = {
|
item = {
|
||||||
"intent_category": hit.entity.get("intent_category"),
|
"intent_category": hit.entity.get("intent_category"),
|
||||||
"content": hit.entity.get("content"),
|
"content": hit.entity.get("content"),
|
||||||
|
"review_content": hit.entity.get("review_content"),
|
||||||
"created_at": hit.entity.get("created_at"),
|
"created_at": hit.entity.get("created_at"),
|
||||||
"score": hit.get("distance", 0)
|
"score": hit.get("distance", 0)
|
||||||
}
|
}
|
||||||
@ -171,14 +259,20 @@ class IntentClassificationRepository:
|
|||||||
all_categories = await self.get_all_intent_categories()
|
all_categories = await self.get_all_intent_categories()
|
||||||
return [c for c in all_categories]
|
return [c for c in all_categories]
|
||||||
|
|
||||||
async def get_content_by_intent_category(self, intent_category: str) -> List[str]:
|
async def get_content_by_intent_category(self, intent_category: str) -> List[Dict[str, str]]:
|
||||||
result = milvus_client.client.query(
|
result = milvus_client.client.query(
|
||||||
collection_name=self.collection_name,
|
collection_name=self.collection_name,
|
||||||
filter=f'intent_category == "{intent_category}"',
|
filter=f'intent_category == "{intent_category}"',
|
||||||
output_fields=["content"],
|
output_fields=["content", "review_content"],
|
||||||
limit=1000
|
limit=1000
|
||||||
)
|
)
|
||||||
return [item.get("content", "") for item in result if item.get("content")]
|
return [
|
||||||
|
{
|
||||||
|
"content": item.get("content", ""),
|
||||||
|
"review_content": item.get("review_content", "")
|
||||||
|
}
|
||||||
|
for item in result if item.get("content")
|
||||||
|
]
|
||||||
|
|
||||||
async def delete_today_data(self) -> int:
|
async def delete_today_data(self) -> int:
|
||||||
today_start = int(datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp())
|
today_start = int(datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp())
|
||||||
@ -199,16 +293,62 @@ class IntentClassificationRepository:
|
|||||||
logger.info(f"删除今日数据 {len(ids_to_delete)} 条")
|
logger.info(f"删除今日数据 {len(ids_to_delete)} 条")
|
||||||
return len(ids_to_delete)
|
return len(ids_to_delete)
|
||||||
|
|
||||||
|
def sync_content_to_review_content(self) -> int:
|
||||||
|
result = milvus_client.client.query(
|
||||||
|
collection_name=self.collection_name,
|
||||||
|
output_fields=["id", "intent_category", "content", "review_content", "content_vector", "created_at"],
|
||||||
|
limit=10000
|
||||||
|
)
|
||||||
|
if not result:
|
||||||
|
logger.info("无数据需要同步")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
updated = 0
|
||||||
|
batch_size = 1000
|
||||||
|
for i in range(0, len(result), batch_size):
|
||||||
|
batch = result[i:i + batch_size]
|
||||||
|
ids_to_delete = []
|
||||||
|
insert_batch = []
|
||||||
|
for item in batch:
|
||||||
|
ids_to_delete.append(item["id"])
|
||||||
|
insert_batch.append({
|
||||||
|
"intent_category": item.get("intent_category", ""),
|
||||||
|
"content": item.get("content", ""),
|
||||||
|
"review_content": item.get("content", ""),
|
||||||
|
"content_vector": item.get("content_vector", []),
|
||||||
|
"created_at": item.get("created_at", 0)
|
||||||
|
})
|
||||||
|
milvus_client.client.delete(
|
||||||
|
collection_name=self.collection_name,
|
||||||
|
ids=ids_to_delete
|
||||||
|
)
|
||||||
|
milvus_client.client.insert(
|
||||||
|
collection_name=self.collection_name,
|
||||||
|
data=insert_batch
|
||||||
|
)
|
||||||
|
updated += len(insert_batch)
|
||||||
|
logger.info(f"同步进度: {updated}/{len(result)}")
|
||||||
|
|
||||||
|
logger.info(f"content → review_content 同步完成,共更新 {updated} 条")
|
||||||
|
return updated
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
milvus_client.init()
|
milvus_client.init()
|
||||||
repo = IntentClassificationRepository()
|
repo = IntentClassificationRepository()
|
||||||
style_categories = await repo.delete_today_data()
|
|
||||||
|
# 1. 首次部署:迁移新增 review_content 字段
|
||||||
|
# success = repo.migrate_add_review_content()
|
||||||
|
# print(f"迁移结果: {success}")
|
||||||
|
|
||||||
|
# 2. 将 content 同步写入 review_content
|
||||||
|
# count = repo.sync_content_to_review_content()
|
||||||
|
# print(f"同步完成,共 {count} 条")
|
||||||
|
|
||||||
|
style_categories = await repo.get_style_intent_categories()
|
||||||
print(style_categories)
|
print(style_categories)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# today_start = int(datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp())
|
|
||||||
# print(today_start)
|
|
||||||
import asyncio
|
import asyncio
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user