feat: 意图识别优化

This commit is contained in:
qinyong@9artedu.com 2026-07-30 09:29:27 +08:00
parent 560dbd60a2
commit f323ed5f5d
7 changed files with 381 additions and 151 deletions

View File

@ -31,6 +31,47 @@ general_prompt = """
输入文本{text}
"""
general_merge_prompt = """
# 任务
从三类输入中提取与"下一次回复"直接相关的结构化关键要点按优先级融合后输出
# 输入
- 最新聊天内容最高优先级: {message}
- 历史相关知识: {milvus_content}
- 标准内容: {standard}
# 处理规则
1. 优先级去重同一事实优先采用 {message}缺失部分依次补充 {milvus_content}{standard}不重复罗列
2. 个人信息脱敏删除所有个人身份信息姓名年级专业全称学校联系方式等
- 特别注意回复主题同样需要脱敏不得包含具体专业年级姓名等身份标识主题应概括"咨询类型/事项"而非"用户身份"
3. 用户事实过滤严格去除所有与用户主体相关的客观事实信息包括但不限于
- 用户的计划意向时间安排人数规模行程动向
- 用户当前的状态情绪顾虑已做出的决定
- 用户提及的任何"我/我们"视角的行为描述
仅保留销售/服务方需要执行的动作接待安排标准流程适用条件与门槛
4. 主题相关性过滤严格过滤与当前回复主题无关的历史信息包括但不限于
- 已明确终止的咨询记录如用户已拒绝并结束的话题
- 与当前课程方向无关的其他技术栈/工具争议
- 负面情绪或人际冲突类描述"互删""不考虑"
仅保留对当前回复有指导价值的事实要点
5. 纯事实提取只提取客观事实要点不生成完整回复不添加销售话术过渡句或主观推测
6. 空值处理若无核心数字或适用条件对应字段输出为空列表 `[]` 或留空不编造
# 输出格式
严格按以下 YAML 输出禁止添加额外解释
```yaml
回复主题:
关键信息点:
- 信息:
核心数字:
-
适用条件/门槛:
-
"""
class TextExtract:
async def text_extract(self, text:str)->str:
@ -41,3 +82,12 @@ class TextExtract:
chain = prompt_template | llm |str_output_parser
answer = await chain.ainvoke({"text": text})
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

View File

@ -5,56 +5,35 @@ from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from app.llm import llm
from app.repository.milvus.intent_classification_repository import IntentClassificationRepository
intent_prompt = """
你是一个专业的意图提取专家请分析以下文本内容并结合当前已有的意图标签判断是否符合若不符合则生成一个新意图符合则直接返回原意图
你是一个意图分类专家你的任务是根据给定的文本内容判断它是否匹配当前意图如果不匹配则从意图列表中选择最合适的意图
## 输入说明
- **文本内容**`{text}`
- **目前的意图**`{intent}`可能为空
## 输入信息
- **文本内容**{text}可能为多段文本需综合判断整体核心意图而非逐段独立判断
- **当前意图**{intent}可能为空
- **意图列表**{intent_list}
## 核心规则
## 意图层级说明
意图列表中存在层级关系分类时须优先识别
- **概括性意图**能够涵盖多个独立的具体子意图例如 `学习模式`涵盖线上直播教学线下面授教学`课程咨询`涵盖费用周期内容等综合问题`提供服务`
- **具体意图**描述单一明确的独立事项例如 `线上直播教学``线下面授教学``UE引擎开发``住宿咨询``上课时间`
### 1. 意图提取与对比规则
- 首先从文本中提取1个核心意图提取方法见下方意图提取规则
- 将提取出的意图与给定的`{{intent}}`进行对比
- 若两者**完全相同**字符串完全一致则最终输出意图为原意图`{{intent}}`
- 若两者不同`{{intent}}`为空则最终输出意图为新提取的意图
- 无论采用原意图还是新意图都必须来自或可扩充自意图分类体系
## 判断规则
1. 如果当前意图为空直接从意图列表中选择最匹配的意图
2. **多意图混合优先原则最重要**仅当文本**同时涉及意图列表中两个或以上独立意图的实质性并列对比或综合介绍**才优先选择能够概括这些内容的上层意图 `学习模式``课程咨询`**严禁**把单一意图内部的多维度展开如某课程包含多个技术点案例类型学习阶段误判为多意图混合
3. **当前意图保持的严格条件**只有当文本内容**主要且专门**围绕当前意图展开**不涉及其他独立意图的实质性并列或对比内容**才保持当前意图不变如果文本在匹配当前意图的同时还明显涉及其他独立意图的对比补充或并列说明则必须重新从意图列表中选择最合适的概括性意图**不得因当前意图非空而强行保持**
4. 如果文本内容与当前意图完全不匹配包括语义偏差主题不符情感不一致等从意图列表中选择最匹配的意图进行替换
5. 如果意图列表中没有完全匹配的意图选择最接近的一个**不要自行创造新意图**
6. 如果文本内容模糊无法判断选择当前意图若为空则选择意图列表中的"其他"或第一个兜底意图
### 2. 意图提取规则(用于从文本中提取核心意图)
- 若文本包含用户明确的问题提取其核心诉求对应的意图
- 若文本为销售介绍/陈述内容**聚焦于其最想传达的核心功能或核心信息**提取总结性意图**不得将补充说明信息如附带提到的地点时间等误判为核心意图**
- 例如文本为线上课程后期可以转到线下我们线下校区在上海核心是线上转线下这一功能而非校区分布因此应扩充为线上转线下而不是使用校区分布
- 意图必须基于实际内容不编造
- 仅提取1个意图标签
## 分析步骤(必须执行)
1. **提取核心主题**综合分析文本列表的整体核心主题忽略边缘细节
2. **判定是否多意图混合**检查文本是否涉及**意图列表中两个或以上独立意图**的并列对比或综合注意单一意图内部的技术点案例学习阶段等展开**不构成**多意图混合
3. **层级匹配**若确认为多意图混合匹配概括性意图若为核心单一意图匹配最具体的意图
4. **对比当前意图**如果当前意图不为空检查当前意图是否能准确概括文本**整体**内容若不能则放弃当前意图重新选择
### 3. 单意图选择规则(关键)
**只能输出一个意图标签不能以顿号逗号或分隔多个意图**
当内容可能对应多个意图时按以下优先级选择唯一意图
| 优先级 | 选择规则 | 说明 |
|-------|---------|------|
| **P0** | **优先匹配已有意图标签** | 必须优先从已有意图体系含扩展示例中选择最匹配的标签**若已有标签中无准确对应则按扩充规则创建新标签严禁使用仅部分相关或语义模糊的已有标签** 当销售单方面介绍课程内容无用户明确提问直接使用对应的主题标签3D场景建模若主题为已有标签未覆盖的功能/服务线上转线下则应扩充新标签不得强行归类为校区分布 |
| **P1** | **核心诉求优先** | 选择最能代表文本核心诉求的意图**忽略次要的补充信息**例如文本线上课程可转线下校区在上海 核心是线上转线下而非校区分布报名流程说明 报名咨询而非线上直播教学 |
| **P2** | **主题维度优先于动作维度** | 当主题和动作诉求冲突时优先选择主题维度例如3D建模费用 3D建模主题 |
| **P3** | **直接提及优先于间接关联** | 文本明确说出的核心意图优先于推断出的意图 |
| **P4** | **去重合并** | 多个子诉求属于同一类别时只输出一个意图标签 |
### 4. 意图分类体系(必须从中选择或基于其扩充)
{intent_list}
**扩充规则** 当文本核心意图无法被已有标签准确覆盖时必须创建更精准的新标签创建时
- 新标签应简洁体现核心语义线上转线下
- 严格对比已有标签确保无重复语义
- **示例**文本线上课程后期可以转到线下 已有标签无对应扩充为线上转线下文本原画好就业吗 扩充为原画就业
### 5. 多意图处理
若同一段文本明确涉及多个不重叠的核心意图仅选择最能代表核心内容的1个意图
## 输出格式要求
## 输出要求
- 输出纯JSON不要任何解释不要markdown标记不要注释
- 确保所有字符串用双引号包裹
- 输出格式固定为

View File

@ -45,7 +45,7 @@ if __name__ == '__main__':
archive_messages_mysql_repository = ArchiveMessagesRepository(db_session)
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,
qa_milvus_repository=qa_milvus_repository
)

View File

@ -216,64 +216,107 @@ problem_rewriting_prompt1 = """
请直接输出JSON
"""
problem_rewriting_prompt = """
# 智能客服意图分类助手提示词
你是一名智能客服意图分类助手你的任务是根据用户的历史会话和当前问题提取用户的核心诉求并输出标准化的意图标签
## 参数说明
本提示词需要动态传入以下参数
- {query}用户当前提问
- {history_text}历史会话记录可为空
- {intent_tags}完整的意图标签列表格式为JSON字符串数组例如["3D建模", "2D原画", "课程咨询", "就业咨询", ...]**所有输出的意图标签必须严格从该列表中选择不得自创或修改**
- `{query}`用户当前提问
- `{history_text}`历史会话记录可为空
- `{intent_tags}`完整的意图标签列表格式为 JSON 字符串数组例如`["3D建模", "2D原画", "课程咨询", "就业咨询", ...]`**所有输出的意图标签必须严格从该列表中选择不得自创或修改**
---
## 核心原则(必须严格遵守)
### 1. 双维度提取原则
每个用户问题通常包含两个维度的意图必须分别识别
- **课程/服务主题维度**用户想咨询的具体课程类型或业务主题
- **诉求/动作维度**用户想了解的方面费用就业难度等
这两个维度相互独立必须同时提取不得遗漏任何一个最终输出的标签本身不区分维度但模型内部需要判断每个标签的维度以便后续优先级排序见P3规则判断维度的方法若标签名称中包含咨询怎么办分布等特征词通常属于诉求/动作维度否则多属于主题维度请结合语义自行判断
这两个维度相互独立必须同时提取不得遗漏任何一个最终输出的标签本身不区分维度但模型内部需要判断每个标签的维度以便后续优先级排序 P3 规则
**维度判定规则必须按语义本质判断禁止仅看标签字面后缀**
- **主题维度**表示用户想学习的**具体课程类型专业方向或业务领域**
例如`3D建模``3D场景建模``3D角色建模``2D原画``UE引擎开发``游戏特效``地编``技术美术``游戏动作/分镜``二次元风格``写实风格`
- **诉求/动作维度**表示用户**想了解什么想做什么担心什么需要什么服务**
例如`课程咨询`想了解课程信息`就业咨询`想了解就业`学不会怎么办`担心学习效果`试听课`想要试听服务`退费咨询`想退费`住宿咨询`想了解住宿`校区分布`想了解位置`分期付款`需要付款方式`开班安排`/`开课安排`/`上课时间`想了解时间安排`报名流程`/`报到流程`想了解报名动作`作品评估`需要评估服务`预约参观`想要参观`购买建议`需要建议`重修政策`/`转班咨询`/`休学咨询`想了解售后政策
**关键原则**如果标签表达的是用户的一个动作需求顾虑或服务请求即使标签名称中不含咨询怎么办等词也必须判定为**诉求/动作维度**禁止仅凭标签名称是否包含特定后缀来判断维度
**中心语优先原则强制**
在包含""字的偏正短语中"3D场景建模的试听课""角色建模的课程内容"**""字后面的成分中心语代表用户的核心诉求**""字前面的成分定语仅为主题限定
- 中心语诉求维度的优先级 **必须高于** 定语主题维度
- 出现顺序判定以**中心语**在短语中的位置为准而非字面首个词
---
### 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分超过阈值 |
| **语义关联** | **40** | 用户表达与标签语义高度相关通过语义场推断 | 语义场匹配给40分需配合上下文支持达到阈值 |
| **上下文支持** | **20** | 历史会话或当前上下文支持该意图推断 | 与语义关联配合40+20=60刚好达到阈值 |
| **直接匹配** | **60** | 用户问题中直接出现标签关键词或同义词扩展词见第5节 | 关键词完全匹配或同义词匹配直接给 60 超过阈值 |
| **语义关联** | **40** | 用户表达与标签语义高度相关通过语义场推断 | 语义场匹配给 40 需配合上下文支持达到阈值 |
| **上下文支持** | **20** | 历史会话或当前上下文支持该意图推断 | 与语义关联配合40 + 20 = 60 刚好达到阈值 |
| **历史补全** | **10** | 意图完全依赖历史会话补全 | 仅作为辅助加分不单独使用 |
#### 阈值判定规则:
- **总分 60** 进入候选池
- **总分 < 60** 直接过滤不进入候选池
- 无任何意图60 输出 `{{"intents": []}}`
#### 阈值判定规则
### 4. 强制截断规则(新增)
**无论识别到多少个意图最终输出最多只返回2个意图标签**
- **总分 60 ** 进入候选池
- **总分 &lt; 60 ** 直接过滤不进入候选池
- 无任何意图 60 输出 `{{"intents": []}}`
当候选池中超过2个意图时按以下优先级排序只保留前2个
---
### 4. 强制截断规则
**无论识别到多少个意图最终输出最多只返回 2 个意图标签**
当候选池中超过 2 个意图时按以下优先级排序只保留前 2
| 优先级 | 排序规则 | 说明 |
|-------|---------|------|
| **P1最高** | 当前问题直接明确表达的 > 历史会话补全的 | 用户当前明确说出的诉求优先 |
| **P2** | 总分高的 > 总分低的 | 置信度更高的优先 |
| **P3** | 诉求维度 > 主题维度 | 用户更关心做什么而非是什么 |
| **P4** | 出现顺序靠前的 > 出现顺序靠后的 | 用户先说的一般更关心 |
| **P1最高** | 当前问题直接明确表达的 &gt; 历史会话补全的 | 用户当前明确说出的诉求优先 |
| **P2** | 总分高的 &gt; 总分低的 | 置信度更高的优先 |
| **P3** | 诉求维度 &gt; 主题维度 | 用户更关心"做什么"而非"是什么" |
| **P4** | 核心诉求出现顺序靠前的 &gt; 靠后的 | 同一子诉求内中心语诉求维度优先于定语主题维度跨子诉求时按子诉求在原文中的先后顺序 |
**截断规则**排序后只保留前2个其余全部丢弃
**P4 执行细则强制**
排序时必须严格遵循 P1 P2 P3 P4 的层级顺序**严禁跨层级比较**仅当 P1P2P3 均相同或无法区分时才启用 P4
"主题+诉求"的复合表达"X的Y"Y诉求维度视为该子诉求的核心 P4 顺序位始终优先于 X主题维度
**截断规则**排序后只保留前 2 其余全部丢弃
---
### 5. 语义同义词扩展匹配
匹配时不仅使用标签字面本身还必须覆盖常见的同义词扩展以下为常见标签的同义词映射表**当且仅当 {intent_tags} 中包含与下列标准标签完全同名的标签时才可使用该扩展**若传入的标签不在该表中则根据标签字面及通用语义进行匹配禁止编造扩展词
匹配时不仅使用标签字面本身还必须覆盖常见的同义词扩展以下为常见标签的同义词映射表**当且仅当 `{intent_tags}` 中包含与下列标准标签完全同名的标签时才可使用该扩展**若传入的标签不在该表中则根据标签字面及通用语义进行匹配禁止编造扩展词
- **就业咨询**就业找工作工作推荐就业支持就业服务就业保障就业率包就业毕业安排工作分配推荐工作就业方向职业发展入职岗位推荐能就业吗好找工作吗就业情况就业数据薪资工资待遇收入水平
- **学不会怎么办**学不会跟不上没基础怕学不会能不能学能学会吗学得会吗适合我学吗零基础能学吗没经验能学吗纯小白能学吗好不好学难不难难吗简单吗容易学吗好学吗有难度吗门槛高吗有门槛吗担心自己学不会不知道能不能学会怕跟不上怕太难上手难学不进听不懂理解不了
@ -284,46 +327,61 @@ problem_rewriting_prompt = """
- **校区分布**校区地址在哪位置分布哪个城市怎么去交通附近
- **优势/竞争力**优势竞争力为什么选你们和其他机构比口碑排名实力师资教学质量
若传入的标签列表包含上述标签以外的标签同义词匹配将仅基于标签文字及通用语义不得臆造扩展
若传入的标签列表包含上述标签以外的标签同义词匹配将仅基于标签文字及通用语义不得臆造扩展词
---
### 6. 信息补全与语义保持
- 如果当前问题缺少主语从历史会话补全
- 不得改变用户原始意图不得引入历史会话中不存在的新信息
- 不得遗漏当前问题中的任何关键诉求
---
## 输出格式
- 仅输出JSON不要任何解释性文字
- **最多只返回2个意图标签**超出部分强制截断
- 所有标签必须来自传入的 {intent_tags} 列表
- 输出格式示例`{{"intents": ["标签1", "标签2"]}}` 若候选为空则 `{{"intents": []}}`
- 仅输出 JSON不要任何解释性文字
- **最多只返回 2 个意图标签**超出部分强制截断
- 所有标签必须来自传入的 `{intent_tags}` 列表
- 输出格式示例`{{"intents": ["标签1", "标签2"]}}`若候选为空则 `{{"intents": []}}`
---
## 分析流程(必须按顺序执行)
### 步骤1并列拆分
### 步骤 1并列拆分
将当前问题拆分为独立的子诉求
### 步骤2双维度扫描 + 置信度评分
对每个子诉求扫描 {intent_tags} 中的标签按照第3节和第5节计算总分每个意图独立评分
### 步骤 2双维度扫描 + 置信度评分
对每个子诉求扫描 `{intent_tags}` 中的标签按照第 3 节和第 5 节计算总分每个意图独立评分
### 步骤3阈值过滤
只保留总分60分的意图进入候选池
### 步骤 3阈值过滤
只保留总分 60 分的意图进入候选池
### 步骤4优先级排序当候选池超过2个时
P1P2P3P4规则排序
### 步骤 4优先级排序当候选池超过 2 个时)
P1 P2 P3 P4 规则排序
### 步骤5强制截断
只保留排序后的前2个意图标签
### 步骤 5强制截断
只保留排序后的前 2 个意图标签
### 步骤 6完整性自检输出前必须执行
### 步骤6完整性自检输出前必须执行
- [ ] 每个子诉求是否都经过了置信度评分
- [ ] 是否有总分60分的意图被遗漏
- [ ] 最终输出是否2个标签
- [ ] 是否有总分 60 分的意图被遗漏
- [ ] 最终输出是否 2 个标签
- [ ] 截断时是否按优先级规则执行
- [ ] 所有输出的标签是否都在 {intent_tags}
- [ ] 所有输出的标签是否都在 `{intent_tags}`
- [ ] 每个标签的维度判定是否基于语义本质而非字面后缀
- [ ] 是否遵循了中心语优先原则""字短语中诉求维度优先于主题维度
如果任何一项为重新分析
如果任何一项为""重新分析
## 分类示例假设传入标签列表包含3D建模、3D角色建模、课程咨询、就业咨询、住宿咨询、退费咨询、学不会怎么办
---
## 分类示例
假设传入标签列表包含`3D建模``3D角色建模``课程咨询``就业咨询``住宿咨询``退费咨询``学不会怎么办``试听课``3D场景建模`
### 示例12个意图无需截断
历史会话
@ -356,7 +414,7 @@ problem_rewriting_prompt = """
- 诉求维度2"不知道能不能学" 语义关联(40) + 上下文支持(20) = 60 候选池
- 诉求维度3"好不好学" 语义关联(40) + 上下文支持(20) = 60 候选池与上同标签去重
- 候选池["3D角色建模", "课程咨询", "学不会怎么办"]共3个需截断
- 排序课程咨询(P1当前直接) > 学不会怎么办(P1当前直接) > 3D角色建模(P3主题维度)
- 排序课程咨询(P1当前直接) &gt; 学不会怎么办(P1当前直接) &gt; 3D角色建模(P3主题维度)
- 截断后保留前2个
分类结果
{{"intents": ["课程咨询", "学不会怎么办"]}}
@ -371,7 +429,7 @@ problem_rewriting_prompt = """
- "住宿" 直接匹配"住宿咨询" = 60 候选池
- "退费" 直接匹配"退费咨询" = 60 候选池
- 候选池共5个需截断
- 排序课程咨询(P1当前直接,出现第2) > 就业咨询(P1当前直接,出现第3) > 住宿咨询 > 退费咨询 > 3D建模(P3主题维度)
- 排序课程咨询(P1当前直接,出现第2) &gt; 就业咨询(P1当前直接,出现第3) &gt; 住宿咨询 &gt; 退费咨询 &gt; 3D建模(P3主题维度)
- 截断后保留前2个
分类结果
{{"intents": ["课程咨询", "就业咨询"]}}
@ -381,12 +439,12 @@ problem_rewriting_prompt = """
- 用户我想学3D建模
当前问题多少钱就业情况住宿条件
分析
- 主题维度"3D建模"历史补全 = 10 < 60 不进入候选池
- 主题维度"3D建模"历史补全 = 10 &lt; 60 不进入候选池
- "多少钱" 直接匹配"课程咨询" = 60 候选池
- "就业情况" 直接匹配"就业咨询" = 60 候选池
- "住宿条件" 直接匹配"住宿咨询" = 60 候选池
- 候选池["课程咨询", "就业咨询", "住宿咨询"]共3个需截断
- 排序课程咨询(P1,出现第1) > 就业咨询(P1,出现第2) > 住宿咨询(P1,出现第3)
- 排序课程咨询(P1,出现第1) &gt; 就业咨询(P1,出现第2) &gt; 住宿咨询(P1,出现第3)
- 截断后保留前2个
分类结果
{{"intents": ["课程咨询", "就业咨询"]}}
@ -395,11 +453,31 @@ problem_rewriting_prompt = """
历史会话
当前问题这个课有意思吗
分析
- "有意思" 语义关联"课程咨询" = 40无上下文支持 = 0总分40分 < 60 不进入候选池
- "有意思" 语义关联"课程咨询" = 40无上下文支持 = 0总分40分 &lt; 60 不进入候选池
- 候选池为空
分类结果
{{"intents": []}}
### 示例 7中心语优先 + P3 排序生效)
历史会话
当前问题还有 3D 场景建模的试听课你发我一下另外住宿要多少钱呢
分析
拆分[3D场景建模的试听课, 住宿要多少钱]
- 子诉求1"3D场景建模的试听课"
- 主题维度"3D场景建模" 直接匹配 = 60 候选池
- 诉求维度中心语"试听课" 直接匹配 = 60语义为用户想要试听服务 候选池
- 子诉求2"住宿要多少钱"
- 诉求维度"住宿咨询" 直接匹配 = 60 候选池
- 候选池["3D场景建模", "试听课", "住宿咨询"] 3 需截断
- 排序
- P1三者均为当前直接表达平级
- P2三者均为60分平级
- P3试听课(诉求) = 住宿咨询(诉求) &gt; 3D场景建模(主题)
- P4试听课子诉求1中心语&gt; 住宿咨询子诉求2中心语&gt; 3D场景建模子诉求1定语
- 截断后保留前 2
分类结果
{{"intents": ["试听课", "住宿咨询"]}}
---
## 现在请处理以下输入

View File

@ -24,7 +24,7 @@ async def search_embed(state: QueryQAState):
for item in result:
qa_pairs.append({
"intent_category": item.get("intent_category", ""),
"content": item.get("content", ""),
"content": item.get("content", "") + "," +item.get("review_content", ""),
"score": item.get("score", 0)
})

View File

@ -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="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)
@ -51,6 +52,90 @@ class IntentClassificationRepository:
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
async def save_intent_data(self, data: List[Dict[str, Any]]) -> List[int]:
@ -61,6 +146,7 @@ class IntentClassificationRepository:
insert_data.append({
"intent_category": item.get("intent_category", ""),
"content": item.get("content", ""),
"review_content": item.get("review_content", ""),
"content_vector": item.get("content_vector", []),
"created_at": created_at
})
@ -107,6 +193,7 @@ class IntentClassificationRepository:
data=[{
"intent_category": category,
"content": content,
"review_content": item.get("review_content", ""),
"content_vector": vector,
"created_at": created_at
}]
@ -128,7 +215,7 @@ class IntentClassificationRepository:
result = milvus_client.client.query(
collection_name=self.collection_name,
filter=expr,
output_fields=["intent_category", "content", "created_at"],
output_fields=["intent_category", "content", "review_content", "created_at"],
limit=top_k
)
return result
@ -143,7 +230,7 @@ class IntentClassificationRepository:
"metric_type": "COSINE",
"efSearch": 300
},
output_fields=["intent_category", "content", "created_at"]
output_fields=["intent_category", "content", "review_content", "created_at"]
)
results = []
@ -152,6 +239,7 @@ class IntentClassificationRepository:
item = {
"intent_category": hit.entity.get("intent_category"),
"content": hit.entity.get("content"),
"review_content": hit.entity.get("review_content"),
"created_at": hit.entity.get("created_at"),
"score": hit.get("distance", 0)
}
@ -171,14 +259,20 @@ class IntentClassificationRepository:
all_categories = await self.get_all_intent_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(
collection_name=self.collection_name,
filter=f'intent_category == "{intent_category}"',
output_fields=["content"],
output_fields=["content", "review_content"],
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:
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)}")
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():
milvus_client.init()
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)
if __name__ == "__main__":
# today_start = int(datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp())
# print(today_start)
import asyncio
asyncio.run(main())