StructBERT中文语义匹配实战手册:FAQ库动态加载、阈值热更新、结果排序算法说明

StructBERT中文语义匹配实战手册:FAQ库动态加载、阈值热更新、结果排序算法说明

1. 引言:为什么需要智能语义匹配?

想象一下这个场景:你是一家电商公司的客服主管,每天要处理成千上万的用户咨询。用户问“我的快递怎么还没到”,而你的知识库里有“包裹配送状态查询”、“物流延误原因”、“快递追踪方法”等几十个标准问题。怎么快速找到最匹配的那个?

传统的关键词匹配经常闹笑话——“快递没到”匹配到“快递费用怎么算”,因为都有“快递”这个词。用户问“手机充不进电”,系统却推荐“手机充电器购买”,因为都有“充电”。

这就是我们今天要解决的问题:让机器真正理解句子的意思,而不是只看字面

基于百度StructBERT大模型的中文句子相似度计算工具,就是为解决这个问题而生。它能判断两句话在语义上有多接近,0分表示完全不相关,1分表示意思完全相同。更重要的是,它支持FAQ库动态加载、匹配阈值热更新、智能结果排序——这些正是构建实用语义匹配系统的核心。

本文不是简单的使用教程,而是实战手册。我会带你深入这个工具的工程实现细节,分享在实际业务中如何用好它。无论你是要搭建智能客服、内容去重系统,还是语义搜索功能,这里都有可落地的方案。

2. StructBERT相似度服务架构解析

2.1 整体架构设计

这个服务采用经典的Web服务架构,但有几个关键设计值得关注:

服务架构:
┌─────────────────────────────────────────────┐
│                Web前端界面                  │
│   (紫色渐变设计,支持单句/批量对比)         │
├─────────────────────────────────────────────┤
│              Flask应用服务器                │
│   (提供RESTful API,处理业务逻辑)          │
├─────────────────────────────────────────────┤
│           StructBERT语义模型层              │
│   (核心:将句子转换为向量并计算相似度)      │
├─────────────────────────────────────────────┤
│              FAQ管理模块                    │
│   (动态加载、更新、缓存知识库)              │
├─────────────────────────────────────────────┤
│          阈值与排序策略模块                 │
│   (热更新阈值,智能排序结果)                │
└─────────────────────────────────────────────┘

服务已经预装并配置了开机自启,你可以通过这个地址直接访问:

http://gpu-pod698386bfe177c841fb0af650-5000.web.gpu.csdn.net/

如果服务没有运行,用这个命令启动:

cd /root/nlp_structbert_project
bash scripts/start.sh

2.2 核心能力:从字符匹配到语义理解

这个工具最厉害的地方在于,它不只是看字面相似度。让我用几个例子说明:

传统方法(字符匹配)的问题:

  • “我喜欢苹果手机” vs “苹果很好吃” → 都有“苹果”,但意思完全不同
  • “这个产品很棒” vs “产品非常不错” → 用词不同,但意思一样

StructBERT的语义理解:

# 实际测试结果
例子1: "今天天气很好" vs "今天阳光明媚"
相似度: 0.85 (高度相似)

例子2: "怎么修改密码" vs "如何重置密码"  
相似度: 0.82 (高度相似)

例子3: "手机没电了" vs "充电宝在哪借"
相似度: 0.65 (中等相似,但逻辑相关)

例子4: "今天天气很好" vs "我喜欢吃苹果"
相似度: 0.12 (完全不相关)

看到区别了吗?第三个例子中,两句话没有一个字相同,但模型能理解“手机没电”和“充电宝”之间的逻辑关系。这就是语义理解的力量。

3. FAQ库动态加载:让知识库活起来

3.1 为什么需要动态加载?

静态的FAQ库有个致命问题:业务在变,知识库也要变。新产品上线、新政策发布、新问题出现……如果每次更新都要重启服务,用户体验会大打折扣。

动态加载的核心思想是:不重启服务,实时更新知识库。想象一下客服系统,运营人员下午3点添加了新的常见问题,3点01分用户就能匹配到——这就是动态加载的价值。

3.2 实现方案:文件监听+内存缓存

服务内置了FAQ动态加载机制,工作原理是这样的:

# 简化版的动态加载逻辑
import os
import time
import threading
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class FAQManager:
    def __init__(self, faq_file_path):
        self.faq_file_path = faq_file_path
        self.faq_data = self.load_faqs()  # 初始加载
        self.last_modified = os.path.getmtime(faq_file_path)
        
        # 启动文件监听线程
        self.start_file_watcher()
    
    def load_faqs(self):
        """从文件加载FAQ数据"""
        faqs = []
        try:
            with open(self.faq_file_path, 'r', encoding='utf-8') as f:
                for line in f:
                    line = line.strip()
                    if line and not line.startswith('#'):  # 跳过空行和注释
                        # 支持多种格式:问题|答案 或 纯问题
                        if '|' in line:
                            question, answer = line.split('|', 1)
                            faqs.append({
                                'question': question.strip(),
                                'answer': answer.strip()
                            })
                        else:
                            faqs.append({
                                'question': line,
                                'answer': ''  # 答案可后续补充
                            })
            print(f"加载了 {len(faqs)} 个FAQ条目")
            return faqs
        except Exception as e:
            print(f"加载FAQ失败: {e}")
            return []
    
    def check_and_reload(self):
        """检查文件是否更新,如果是则重新加载"""
        current_modified = os.path.getmtime(self.faq_file_path)
        if current_modified > self.last_modified:
            print("检测到FAQ文件更新,重新加载...")
            self.faq_data = self.load_faqs()
            self.last_modified = current_modified
            return True
        return False
    
    def start_file_watcher(self):
        """启动文件监听线程"""
        def watch_loop():
            while True:
                self.check_and_reload()
                time.sleep(5)  # 每5秒检查一次
        
        thread = threading.Thread(target=watch_loop, daemon=True)
        thread.start()
    
    def get_faqs(self):
        """获取当前FAQ列表(只返回问题)"""
        return [item['question'] for item in self.faq_data]
    
    def find_answer(self, matched_question):
        """根据匹配到的问题查找答案"""
        for item in self.faq_data:
            if item['question'] == matched_question:
                return item['answer']
        return "未找到相关答案"

# 使用示例
faq_manager = FAQManager('/path/to/faq.txt')

3.3 实战:构建动态FAQ客服系统

让我们看一个完整的客服系统实现:

import requests
import json
import time
from datetime import datetime

class DynamicFAQSystem:
    def __init__(self, similarity_service_url, faq_file_path):
        """
        初始化动态FAQ系统
        
        参数:
            similarity_service_url: 相似度服务地址
            faq_file_path: FAQ文件路径
        """
        self.service_url = similarity_service_url
        self.faq_file_path = faq_file_path
        self.faq_cache = []  # 内存缓存
        self.cache_time = None  # 缓存时间
        self.cache_ttl = 30  # 缓存有效期(秒)
        
        # 初始加载
        self.reload_faqs()
    
    def reload_faqs(self):
        """重新加载FAQ文件"""
        try:
            with open(self.faq_file_path, 'r', encoding='utf-8') as f:
                lines = f.readlines()
            
            new_faqs = []
            for line in lines:
                line = line.strip()
                if line and not line.startswith('#'):
                    # 支持多种格式
                    if '=>' in line:
                        # 格式: 用户问题 => 标准问题|答案
                        parts = line.split('=>')
                        if len(parts) == 2:
                            user_question = parts[0].strip()
                            std_part = parts[1].strip()
                            
                            if '|' in std_part:
                                std_question, answer = std_part.split('|', 1)
                            else:
                                std_question, answer = std_part, ""
                            
                            new_faqs.append({
                                'user_question': user_question,
                                'std_question': std_question.strip(),
                                'answer': answer.strip()
                            })
                    elif '|' in line:
                        # 格式: 标准问题|答案
                        question, answer = line.split('|', 1)
                        new_faqs.append({
                            'user_question': question.strip(),
                            'std_question': question.strip(),
                            'answer': answer.strip()
                        })
                    else:
                        # 格式: 问题
                        new_faqs.append({
                            'user_question': line,
                            'std_question': line,
                            'answer': ""
                        })
            
            self.faq_cache = new_faqs
            self.cache_time = datetime.now()
            print(f"[{datetime.now()}] 已加载 {len(new_faqs)} 个FAQ条目")
            
        except Exception as e:
            print(f"加载FAQ文件失败: {e}")
    
    def should_reload(self):
        """判断是否需要重新加载"""
        if not self.cache_time:
            return True
        
        # 检查文件是否被修改
        try:
            file_mtime = os.path.getmtime(self.faq_file_path)
            cache_time_ts = self.cache_time.timestamp()
            
            if file_mtime > cache_time_ts:
                return True
        except:
            pass
        
        # 检查缓存是否过期
        time_diff = (datetime.now() - self.cache_time).total_seconds()
        return time_diff > self.cache_ttl
    
    def find_best_match(self, user_question, threshold=0.7):
        """
        为用户问题找到最佳匹配
        
        参数:
            user_question: 用户输入的问题
            threshold: 匹配阈值,默认0.7
            
        返回:
            match_result: 匹配结果字典
        """
        # 检查是否需要重新加载
        if self.should_reload():
            self.reload_faqs()
        
        if not self.faq_cache:
            return {
                'matched': False,
                'reason': 'FAQ库为空'
            }
        
        # 准备要匹配的标准问题列表
        std_questions = [item['std_question'] for item in self.faq_cache]
        
        try:
            # 调用相似度服务
            response = requests.post(
                f"{self.service_url}/batch_similarity",
                json={
                    "source": user_question,
                    "targets": std_questions
                },
                timeout=5
            )
            
            if response.status_code == 200:
                results = response.json()['results']
                
                # 找到最佳匹配
                if results:
                    best_match = max(results, key=lambda x: x['similarity'])
                    
                    if best_match['similarity'] >= threshold:
                        # 找到对应的FAQ条目
                        matched_index = std_questions.index(best_match['sentence'])
                        faq_item = self.faq_cache[matched_index]
                        
                        return {
                            'matched': True,
                            'similarity': best_match['similarity'],
                            'user_question': user_question,
                            'matched_question': faq_item['std_question'],
                            'answer': faq_item['answer'],
                            'all_matches': [
                                {
                                    'question': r['sentence'],
                                    'similarity': r['similarity'],
                                    'passed': r['similarity'] >= threshold
                                }
                                for r in results[:5]  # 返回前5个结果
                            ]
                        }
                
                # 没有达到阈值的匹配
                return {
                    'matched': False,
                    'best_similarity': results[0]['similarity'] if results else 0,
                    'best_question': results[0]['sentence'] if results else None,
                    'reason': f'最高相似度 {best_match["similarity"]:.2f} 低于阈值 {threshold}'
                }
            else:
                return {
                    'matched': False,
                    'reason': f'服务请求失败: {response.status_code}'
                }
                
        except Exception as e:
            return {
                'matched': False,
                'reason': f'匹配过程出错: {str(e)}'
            }
    
    def add_faq_via_api(self, question, answer=""):
        """
        通过API动态添加FAQ(不修改文件)
        适用于临时FAQ或测试
        
        参数:
            question: 问题
            answer: 答案(可选)
        """
        new_faq = {
            'user_question': question,
            'std_question': question,
            'answer': answer
        }
        
        self.faq_cache.append(new_faq)
        print(f"已添加临时FAQ: {question}")
        
        return True

# 使用示例
def demo_dynamic_faq():
    """演示动态FAQ系统"""
    
    # 初始化系统
    faq_system = DynamicFAQSystem(
        similarity_service_url="http://127.0.0.1:5000",
        faq_file_path="/data/faq/faq_list.txt"
    )
    
    # 模拟FAQ文件内容
    faq_content = """# 客服FAQ库
# 格式: 用户可能问的问题 => 标准问题|标准答案
    
怎么修改密码 => 如何修改登录密码|您可以在登录页面点击"忘记密码",按照提示操作
密码忘记了怎么办 => 如何修改登录密码|您可以在登录页面点击"忘记密码",按照提示操作
我想改密码 => 如何修改登录密码|您可以在登录页面点击"忘记密码",按照提示操作
    
怎么注册账号 => 如何注册新账号|请访问注册页面,填写手机号和验证码即可注册
我要注册 => 如何注册新账号|请访问注册页面,填写手机号和验证码即可注册
    
会员怎么退款 => 会员如何退款|请在会员中心找到订单,点击退款申请
我要退会员费 => 会员如何退款|请在会员中心找到订单,点击退款申请
    
快递还没到 => 物流配送问题|物流通常1-3天送达,您可以在订单页面查看物流信息
我的包裹什么时候到 => 物流配送问题|物流通常1-3天送达,您可以在订单页面查看物流信息
"""
    
    # 写入测试文件
    with open("/data/faq/faq_list.txt", "w", encoding="utf-8") as f:
        f.write(faq_content)
    
    print("FAQ文件已创建,系统会自动加载...")
    time.sleep(2)  # 等待系统加载
    
    # 测试匹配
    test_questions = [
        "我的密码想改一下",
        "怎么申请退款啊",
        "我要注册一个新账户",
        "快递怎么还没送到"
    ]
    
    for question in test_questions:
        print(f"\n用户问题: {question}")
        result = faq_system.find_best_match(question)
        
        if result['matched']:
            print(f"✓ 匹配成功! 相似度: {result['similarity']:.2f}")
            print(f"  匹配问题: {result['matched_question']}")
            print(f"  标准答案: {result['answer']}")
        else:
            print(f"✗ 未匹配: {result['reason']}")
            if 'best_question' in result and result['best_question']:
                print(f"  最接近的问题: {result['best_question']} (相似度: {result['best_similarity']:.2f})")
    
    # 演示动态更新
    print("\n" + "="*50)
    print("演示动态更新FAQ文件...")
    
    # 添加新的FAQ
    new_faq = "\n怎么联系客服 => 客服联系方式|您可以通过在线客服、电话400-xxx-xxxx或邮件联系我们"
    with open("/data/faq/faq_list.txt", "a", encoding="utf-8") as f:
        f.write(new_faq)
    
    print("FAQ文件已更新,等待系统重新加载...")
    time.sleep(6)  # 等待超过缓存TTL(30秒),触发重新加载
    
    # 测试新FAQ
    new_question = "我要找客服"
    result = faq_system.find_best_match(new_question)
    
    print(f"\n新问题测试: {new_question}")
    if result['matched']:
        print(f"✓ 匹配到新FAQ! 相似度: {result['similarity']:.2f}")
        print(f"  答案: {result['answer']}")

# 运行演示
if __name__ == "__main__":
    demo_dynamic_faq()

这个系统有几个关键特性:

  1. 自动检测文件变化:FAQ文件被修改后,系统会自动重新加载
  2. 内存缓存:避免每次请求都读文件,提升性能
  3. 灵活的FAQ格式:支持多种格式,包括用户问题到标准问题的映射
  4. 详细的匹配结果:不仅返回最佳匹配,还返回所有候选结果

3.4 FAQ文件格式最佳实践

根据我的经验,一个好的FAQ文件应该这样组织:

# 客服FAQ库 - 电商场景
# 最后更新: 2024-01-15
# 格式说明:
# 1. 用户问题 => 标准问题|标准答案
# 2. # 开头的是注释
# 3. 空行会被忽略

# 账户相关
怎么修改密码 => 如何修改登录密码|您可以在登录页面点击"忘记密码",按照提示重置密码。如遇问题可联系客服。
密码忘记了怎么办 => 如何修改登录密码|您可以在登录页面点击"忘记密码",按照提示重置密码。如遇问题可联系客服。
我想改密码 => 如何修改登录密码|您可以在登录页面点击"忘记密码",按照提示重置密码。如遇问题可联系客服。

怎么注册账号 => 如何注册新账号|请访问注册页面,填写手机号和验证码即可完成注册。注册后请完善个人信息。
我要注册 => 如何注册新账号|请访问注册页面,填写手机号和验证码即可完成注册。注册后请完善个人信息。
新用户注册 => 如何注册新账号|请访问注册页面,填写手机号和验证码即可完成注册。注册后请完善个人信息。

# 订单相关
我的订单怎么还没发货 => 订单发货时间|普通商品24小时内发货,预售商品按页面显示时间发货。您可以在订单详情查看预计发货时间。
什么时候发货 => 订单发货时间|普通商品24小时内发货,预售商品按页面显示时间发货。您可以在订单详情查看预计发货时间。
发货要多久 => 订单发货时间|普通商品24小时内发货,预售商品按页面显示时间发货。您可以在订单详情查看预计发货时间。

快递还没到 => 物流配送问题|物流通常1-3天送达,偏远地区可能延长。您可以在订单页面查看实时物流信息。
我的包裹什么时候到 => 物流配送问题|物流通常1-3天送达,偏远地区可能延长。您可以在订单页面查看实时物流信息。
物流信息不更新 => 物流配送问题|物流通常1-3天送达,偏远地区可能延长。您可以在订单页面查看实时物流信息。

# 售后相关
怎么申请退货 => 退货退款流程|在订单完成页点击"申请退货",选择退货原因,上传凭证,等待审核通过后寄回商品。
我要退货 => 退货退款流程|在订单完成页点击"申请退货",选择退货原因,上传凭证,等待审核通过后寄回商品。
商品不满意想退 => 退货退款流程|在订单完成页点击"申请退货",选择退货原因,上传凭证,等待审核通过后寄回商品。

会员怎么退款 => 会员退款政策|会员费支持7天无理由退款,超过7天按剩余时间比例退款。请在会员中心申请。
我要退会员费 => 会员退款政策|会员费支持7天无理由退款,超过7天按剩余时间比例退款。请在会员中心申请。
取消会员怎么退钱 => 会员退款政策|会员费支持7天无理由退款,超过7天按剩余时间比例退款。请在会员中心申请。

# 其他
客服电话是多少 => 客服联系方式|客服电话: 400-xxx-xxxx (工作日9:00-18:00),在线客服: 24小时,邮箱: support@example.com
怎么联系你们 => 客服联系方式|客服电话: 400-xxx-xxxx (工作日9:00-18:00),在线客服: 24小时,邮箱: support@example.com
找人工客服 => 客服联系方式|客服电话: 400-xxx-xxxx (工作日9:00-18:00),在线客服: 24小时,邮箱: support@example.com

这种格式的好处:

  • 易于维护:非技术人员也能看懂和修改
  • 支持同义映射:一个标准问题对应多个用户问法
  • 包含完整答案:匹配后直接返回答案,无需二次查询
  • 分类清晰:注释帮助组织和管理

4. 阈值热更新:让匹配更智能

4.1 固定阈值的问题

很多语义匹配系统用固定阈值,比如相似度大于0.7就算匹配成功。但实际业务中,这会导致两个问题:

  1. 误匹配:阈值太低,不相关的问题也被匹配上
  2. 漏匹配:阈值太高,相关的问题被过滤掉

更糟糕的是,不同业务场景需要不同的阈值:

  • 客服问答:0.65可能就够了(用户问法多样)
  • 论文查重:0.95才安全(要求严格一致)
  • 内容推荐:0.4就能推荐(相关性即可)

4.2 动态阈值方案

阈值热更新的核心思想是:根据实时反馈调整阈值。系统不是拍脑袋定一个阈值,而是根据用户行为、匹配效果动态调整。

class AdaptiveThresholdManager:
    """自适应阈值管理器"""
    
    def __init__(self, base_threshold=0.7, min_threshold=0.4, max_threshold=0.95):
        """
        初始化阈值管理器
        
        参数:
            base_threshold: 基础阈值
            min_threshold: 最小阈值
            max_threshold: 最大阈值
        """
        self.base_threshold = base_threshold
        self.min_threshold = min_threshold
        self.max_threshold = max_threshold
        self.current_threshold = base_threshold
        
        # 反馈数据存储
        self.feedback_data = {
            'total_queries': 0,  # 总查询数
            'auto_matches': 0,    # 自动匹配数
            'user_confirms': 0,   # 用户确认数
            'user_rejects': 0,    # 用户拒绝数
            'manual_searches': 0  # 用户手动搜索数
        }
        
        # 阈值调整历史
        self.adjustment_history = []
    
    def record_feedback(self, query, matched, similarity, user_action):
        """
        记录用户反馈
        
        参数:
            query: 用户查询
            matched: 是否匹配成功
            similarity: 相似度分数
            user_action: 用户行为 ('confirm', 'reject', 'manual_search')
        """
        self.feedback_data['total_queries'] += 1
        
        if matched:
            self.feedback_data['auto_matches'] += 1
        
        if user_action == 'confirm':
            self.feedback_data['user_confirms'] += 1
        elif user_action == 'reject':
            self.feedback_data['user_rejects'] += 1
        elif user_action == 'manual_search':
            self.feedback_data['manual_searches'] += 1
        
        # 每100次查询调整一次阈值
        if self.feedback_data['total_queries'] % 100 == 0:
            self.adjust_threshold()
    
    def adjust_threshold(self):
        """根据反馈数据调整阈值"""
        total = self.feedback_data['total_queries']
        if total < 50:  # 数据太少,不调整
            return
        
        # 计算关键指标
        auto_match_rate = self.feedback_data['auto_matches'] / total
        confirm_rate = self.feedback_data['user_confirms'] / max(self.feedback_data['auto_matches'], 1)
        reject_rate = self.feedback_data['user_rejects'] / max(self.feedback_data['auto_matches'], 1)
        manual_rate = self.feedback_data['manual_searches'] / total
        
        print(f"\n=== 阈值调整分析 ===")
        print(f"自动匹配率: {auto_match_rate:.2%}")
        print(f"用户确认率: {confirm_rate:.2%}")
        print(f"用户拒绝率: {reject_rate:.2%}")
        print(f"手动搜索率: {manual_rate:.2%}")
        
        old_threshold = self.current_threshold
        
        # 调整策略
        if reject_rate > 0.3:  # 拒绝率太高,说明阈值太低
            # 用户经常拒绝自动匹配的结果,提高阈值
            self.current_threshold = min(self.current_threshold + 0.05, self.max_threshold)
            print(f"拒绝率过高,提高阈值: {old_threshold:.2f} -> {self.current_threshold:.2f}")
            
        elif manual_rate > 0.4:  # 手动搜索率太高,说明匹配率太低
            # 用户经常需要手动搜索,降低阈值
            self.current_threshold = max(self.current_threshold - 0.03, self.min_threshold)
            print(f"手动搜索率过高,降低阈值: {old_threshold:.2f} -> {self.current_threshold:.2f}")
            
        elif confirm_rate > 0.8 and auto_match_rate < 0.6:
            # 确认率很高但匹配率低,可以适当降低阈值
            self.current_threshold = max(self.current_threshold - 0.02, self.min_threshold)
            print(f"高确认率但低匹配率,微降阈值: {old_threshold:.2f} -> {self.current_threshold:.2f}")
        
        else:
            print(f"指标正常,保持阈值: {self.current_threshold:.2f}")
        
        # 记录调整历史
        self.adjustment_history.append({
            'timestamp': datetime.now().isoformat(),
            'old_threshold': old_threshold,
            'new_threshold': self.current_threshold,
            'metrics': {
                'auto_match_rate': auto_match_rate,
                'confirm_rate': confirm_rate,
                'reject_rate': reject_rate,
                'manual_rate': manual_rate
            }
        })
        
        # 重置计数(滑动窗口)
        self.feedback_data = {k: 0 for k in self.feedback_data}
    
    def get_threshold_for_category(self, category):
        """获取分类特定阈值"""
        # 不同分类可以有不同的基准阈值
        category_thresholds = {
            'account': 0.75,      # 账户相关:要求高精度
            'order': 0.70,        # 订单相关:中等精度
            'after_sale': 0.65,   # 售后相关:可以宽松些
            'general': 0.60,      # 一般咨询:更宽松
            'strict': 0.85,       # 严格场景:如密码重置
        }
        
        base = category_thresholds.get(category, self.base_threshold)
        
        # 在基准值基础上,根据当前自适应阈值微调
        adjustment = self.current_threshold - self.base_threshold
        return max(self.min_threshold, min(self.max_threshold, base + adjustment))
    
    def should_match(self, similarity, category='general'):
        """判断是否应该匹配"""
        threshold = self.get_threshold_for_category(category)
        return similarity >= threshold
    
    def get_threshold_info(self):
        """获取阈值信息"""
        return {
            'current_threshold': self.current_threshold,
            'base_threshold': self.base_threshold,
            'min_threshold': self.min_threshold,
            'max_threshold': self.max_threshold,
            'adjustment_history_count': len(self.adjustment_history),
            'last_adjustment': self.adjustment_history[-1] if self.adjustment_history else None
        }

# 使用示例
def demo_adaptive_threshold():
    """演示自适应阈值"""
    
    # 初始化阈值管理器
    threshold_mgr = AdaptiveThresholdManager(base_threshold=0.7)
    
    # 模拟用户反馈数据
    feedback_scenarios = [
        # (查询, 是否匹配, 相似度, 用户行为)
        ("怎么修改密码", True, 0.82, 'confirm'),
        ("密码忘记了", True, 0.78, 'confirm'),
        ("改密码", True, 0.65, 'reject'),  # 阈值0.7时匹配,但用户拒绝
        ("登录问题", False, 0.45, 'manual_search'),
        ("账户问题", True, 0.72, 'confirm'),
        ("密码重置", True, 0.88, 'confirm'),
        ("修改登录密码", True, 0.95, 'confirm'),
        ("密码", True, 0.55, 'reject'),  # 阈值0.7时不匹配,但假设匹配了用户拒绝
        ("忘记密码怎么办", True, 0.81, 'confirm'),
        ("重设密码", True, 0.76, 'confirm'),
    ]
    
    print("模拟用户反馈数据...")
    for i, (query, matched, similarity, action) in enumerate(feedback_scenarios, 1):
        # 模拟当前阈值下的匹配决策
        would_match = similarity >= threshold_mgr.current_threshold
        
        # 记录反馈(注意:这里记录的是实际发生的匹配,不是模拟的)
        threshold_mgr.record_feedback(query, matched, similarity, action)
        
        print(f"\n查询 {i}: {query}")
        print(f"  相似度: {similarity:.2f}, 当前阈值: {threshold_mgr.current_threshold:.2f}")
        print(f"  系统匹配: {'是' if would_match else '否'}, 用户行为: {action}")
    
    # 查看阈值信息
    print("\n" + "="*50)
    print("阈值调整结果:")
    info = threshold_mgr.get_threshold_info()
    for key, value in info.items():
        if key != 'last_adjustment':
            print(f"  {key}: {value}")
    
    if info['last_adjustment']:
        print(f"\n最后一次调整:")
        for key, value in info['last_adjustment'].items():
            if key != 'metrics':
                print(f"  {key}: {value}")
    
    # 测试不同分类的阈值
    print("\n" + "="*50)
    print("不同分类的阈值:")
    categories = ['account', 'order', 'after_sale', 'general', 'strict']
    for category in categories:
        threshold = threshold_mgr.get_threshold_for_category(category)
        print(f"  {category}: {threshold:.2f}")
    
    # 测试匹配决策
    print("\n" + "="*50)
    print("匹配决策测试:")
    test_cases = [
        ("怎么改密码", 0.68, 'account'),
        ("物流查询", 0.72, 'order'),
        ("退货", 0.62, 'after_sale'),
        ("客服", 0.58, 'general'),
        ("重置密码", 0.89, 'strict'),
    ]
    
    for query, similarity, category in test_cases:
        should_match = threshold_mgr.should_match(similarity, category)
        threshold = threshold_mgr.get_threshold_for_category(category)
        print(f"  查询: {query}")
        print(f"    分类: {category}, 相似度: {similarity:.2f}, 阈值: {threshold:.2f}")
        print(f"    是否匹配: {'是' if should_match else '否'}")

# 运行演示
if __name__ == "__main__":
    demo_adaptive_threshold()

4.3 基于时间段的阈值调整

实际业务中,不同时间段的用户行为和需求可能不同。比如:

  • 工作日 vs 周末
  • 白天 vs 夜间
  • 促销期 vs 平常期

我们可以实现基于时间段的阈值调整:

class TimeBasedThresholdManager:
    """基于时间段的阈值管理器"""
    
    def __init__(self):
        # 定义时间段和对应的基准阈值
        self.time_slots = {
            'workday_day': {  # 工作日白天 (9:00-18:00)
                'threshold': 0.70,
                'description': '工作日白天,用户较专业,阈值适中'
            },
            'workday_night': {  # 工作日晚上 (18:00-22:00)
                'threshold': 0.65,
                'description': '工作日晚上,用户可能较疲惫,阈值稍低'
            },
            'weekend_day': {  # 周末白天
                'threshold': 0.68,
                'description': '周末白天,用户较放松,阈值适中'
            },
            'weekend_night': {  # 周末晚上
                'threshold': 0.63,
                'description': '周末晚上,用户可能较随意,阈值较低'
            },
            'late_night': {  # 深夜 (22:00-9:00)
                'threshold': 0.60,
                'description': '深夜时段,用户可能急需帮助,阈值最低'
            },
            'promotion': {  # 促销期间
                'threshold': 0.75,
                'description': '促销期间,咨询量大,提高阈值减少误匹配'
            }
        }
        
        # 当前时间段
        self.current_slot = None
        self.current_threshold = 0.7
        
        # 更新当前时间段
        self.update_time_slot()
    
    def update_time_slot(self):
        """根据当前时间更新时间段"""
        now = datetime.now()
        hour = now.hour
        weekday = now.weekday()  # 0=周一, 6=周日
        
        # 判断是否促销期(简化逻辑,实际应从配置读取)
        # 这里假设每月1-3号为促销期
        is_promotion = now.day <= 3
        
        if is_promotion:
            self.current_slot = 'promotion'
        elif 22 <= hour or hour < 9:
            self.current_slot = 'late_night'
        elif weekday < 5:  # 工作日
            if 9 <= hour < 18:
                self.current_slot = 'workday_day'
            else:
                self.current_slot = 'workday_night'
        else:  # 周末
            if 9 <= hour < 18:
                self.current_slot = 'weekend_day'
            else:
                self.current_slot = 'weekend_night'
        
        self.current_threshold = self.time_slots[self.current_slot]['threshold']
        
        return self.current_slot
    
    def get_current_threshold(self):
        """获取当前阈值"""
        self.update_time_slot()  # 确保时间更新
        return self.current_threshold
    
    def get_threshold_info(self):
        """获取阈值信息"""
        self.update_time_slot()
        
        return {
            'current_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'current_slot': self.current_slot,
            'current_threshold': self.current_threshold,
            'slot_description': self.time_slots[self.current_slot]['description'],
            'all_slots': self.time_slots
        }

# 使用示例
def demo_time_based_threshold():
    """演示基于时间段的阈值"""
    
    threshold_mgr = TimeBasedThresholdManager()
    
    # 测试不同时间
    test_times = [
        ("2024-01-15 10:30:00", "工作日白天"),
        ("2024-01-15 20:30:00", "工作日晚上"),
        ("2024-01-13 14:00:00", "周末白天"),
        ("2024-01-13 21:30:00", "周末晚上"),
        ("2024-01-15 02:00:00", "深夜"),
        ("2024-01-02 15:00:00", "促销期"),
    ]
    
    print("基于时间段的阈值调整演示:")
    print("="*50)
    
    for time_str, description in test_times:
        # 模拟设置时间
        test_time = datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S")
        original_now = datetime.now
        
        # 临时替换datetime.now
        import time as time_module
        class MockDateTime:
            @staticmethod
            def now():
                return test_time
        
        # 保存原函数
        original_datetime_now = datetime.now
        
        try:
            # 模拟当前时间
            datetime.now = MockDateTime.now
            
            # 获取阈值
            threshold_mgr.update_time_slot()
            info = threshold_mgr.get_threshold_info()
            
            print(f"\n时间: {time_str} ({description})")
            print(f"  时间段: {info['current_slot']}")
            print(f"  阈值: {info['current_threshold']:.2f}")
            print(f"  说明: {info['slot_description']}")
            
        finally:
            # 恢复原函数
            datetime.now = original_datetime_now
    
    # 实际当前时间
    print("\n" + "="*50)
    print("实际当前时间:")
    actual_info = TimeBasedThresholdManager().get_threshold_info()
    print(f"  当前时间: {actual_info['current_time']}")
    print(f"  时间段: {actual_info['current_slot']}")
    print(f"  阈值: {actual_info['current_threshold']:.2f}")
    print(f"  说明: {actual_info['slot_description']}")

# 运行演示
if __name__ == "__main__":
    demo_time_based_threshold()

4.4 综合阈值策略

把自适应阈值和基于时间的阈值结合起来,就是完整的阈值热更新系统:

class ComprehensiveThresholdManager:
    """综合阈值管理器:结合自适应和基于时间的调整"""
    
    def __init__(self):
        self.adaptive_mgr = AdaptiveThresholdManager()
        self.time_mgr = TimeBasedThresholdManager()
        
        # 权重配置
        self.weights = {
            'adaptive': 0.6,   # 自适应权重
            'time_based': 0.3,  # 时间权重
            'category': 0.1     # 分类权重
        }
        
        # 分类基准阈值
        self.category_base = {
            'account': 0.75,
            'order': 0.70,
            'after_sale': 0.65,
            'general': 0.60,
            'strict': 0.85,
        }
    
    def get_threshold(self, category='general', query_context=None):
        """
        获取综合阈值
        
        参数:
            category: 问题分类
            query_context: 查询上下文(可选)
        """
        # 获取各个组件的阈值
        adaptive_threshold = self.adaptive_mgr.current_threshold
        time_threshold = self.time_mgr.get_current_threshold()
        category_threshold = self.category_base.get(category, 0.7)
        
        # 计算加权平均值
        weighted_sum = (
            adaptive_threshold * self.weights['adaptive'] +
            time_threshold * self.weights['time_based'] +
            category_threshold * self.weights['category']
        )
        
        # 考虑查询上下文(如果有)
        final_threshold = weighted_sum
        if query_context:
            # 可以根据上下文微调
            # 例如:查询长度、复杂度等
            if len(query_context.get('query', '')) < 3:  # 短查询
                final_threshold = min(final_threshold + 0.05, 0.95)
            elif len(query_context.get('query', '')) > 20:  # 长查询
                final_threshold = max(final_threshold - 0.03, 0.4)
        
        # 确保在合理范围内
        final_threshold = max(0.4, min(0.95, final_threshold))
        
        return {
            'threshold': final_threshold,
            'components': {
                'adaptive': adaptive_threshold,
                'time_based': time_threshold,
                'category': category_threshold,
                'weights': self.weights
            },
            'breakdown': {
                'adaptive_contrib': adaptive_threshold * self.weights['adaptive'],
                'time_contrib': time_threshold * self.weights['time_based'],
                'category_contrib': category_threshold * self.weights['category']
            }
        }
    
    def record_feedback(self, query, matched, similarity, user_action, category='general'):
        """记录反馈(传递给自适应管理器)"""
        self.adaptive_mgr.record_feedback(query, matched, similarity, user_action)
    
    def should_match(self, similarity, category='general', query_context=None):
        """判断是否应该匹配"""
        threshold_info = self.get_threshold(category, query_context)
        return similarity >= threshold_info['threshold'], threshold_info

# 使用示例
def demo_comprehensive_threshold():
    """演示综合阈值管理"""
    
    threshold_mgr = ComprehensiveThresholdManager()
    
    # 模拟不同场景
    test_scenarios = [
        {
            'query': '怎么修改密码',
            'similarity': 0.82,
            'category': 'account',
            'context': {'query': '怎么修改密码', 'length': 5}
        },
        {
            'query': '密码',
            'similarity': 0.55,
            'category': 'account',
            'context': {'query': '密码', 'length': 2}
        },
        {
            'query': '物流什么时候到',
            'similarity': 0.72,
            'category': 'order',
            'context': {'query': '物流什么时候到', 'length': 6}
        },
        {
            'query': '我要退货因为商品质量有问题而且包装也破损了',
            'similarity': 0.68,
            'category': 'after_sale',
            'context': {'query': '我要退货因为商品质量有问题而且包装也破损了', 'length': 20}
        },
    ]
    
    print("综合阈值管理演示:")
    print("="*60)
    
    for i, scenario in enumerate(test_scenarios, 1):
        query = scenario['query']
        similarity = scenario['similarity']
        category = scenario['category']
        context = scenario['context']
        
        should_match, threshold_info = threshold_mgr.should_match(
            similarity, category, context
        )
        
        print(f"\n场景 {i}: {query}")
        print(f"  分类: {category}, 相似度: {similarity:.2f}")
        print(f"  综合阈值: {threshold_info['threshold']:.3f}")
        print(f"  匹配决策: {'✓ 匹配' if should_match else '✗ 不匹配'}")
        
        print(f"\n  阈值构成:")
        print(f"    自适应分量: {threshold_info['breakdown']['adaptive_contrib']:.3f}")
        print(f"    时间分量: {threshold_info['breakdown']['time_contrib']:.3f}")
        print(f"    分类分量: {threshold_info['breakdown']['category_contrib']:.3f}")
        
        print(f"\n  各组件值:")
        print(f"    自适应阈值: {threshold_info['components']['adaptive']:.3f}")
        print(f"    时间阈值: {threshold_info['components']['time_based']:.3f}")
        print(f"    分类基准: {threshold_info['components']['category']:.3f}")
        
        # 记录反馈(模拟)
        user_action = 'confirm' if should_match else 'reject'
        threshold_mgr.record_feedback(query, should_match, similarity, user_action, category)
    
    # 显示当前配置
    print("\n" + "="*60)
    print("当前配置:")
    print(f"  自适应权重: {threshold_mgr.weights['adaptive']}")
    print(f"  时间权重: {threshold_mgr.weights['time_based']}")
    print(f"  分类权重: {threshold_mgr.weights['category']}")
    
    # 测试不同时间的阈值变化
    print("\n" + "="*60)
    print("不同时间的阈值变化:")
    
    # 模拟不同时间(简化演示)
    times = ['workday_day', 'workday_night', 'weekend_day', 'late_night']
    for time_slot in times:
        # 这里简化处理,实际应该修改时间管理器的当前时间段
        print(f"\n  时间段: {time_slot}")
        
        # 获取该时间段的基准阈值
        time_mgr = TimeBasedThresholdManager()
        time_mgr.current_slot = time_slot
        time_mgr.current_threshold = time_mgr.time_slots[time_slot]['threshold']
        
        # 创建临时阈值管理器
        temp_mgr = ComprehensiveThresholdManager()
        temp_mgr.time_mgr = time_mgr
        
        threshold_info = temp_mgr.get_threshold('general')
        print(f"    综合阈值: {threshold_info['threshold']:.3f}")
        print(f"    说明: {time_mgr.time_slots[time_slot]['description']}")

# 运行演示
if __name__ == "__main__":
    demo_comprehensive_threshold()

5. 结果排序算法:从简单到智能

5.1 基础排序:按相似度降序

最简单的排序就是按相似度分数从高到低排:

def basic_sort_by_similarity(results):
    """基础排序:按相似度降序"""
    return sorted(results, key=lambda x: x['similarity'], reverse=True)

但这样有问题:两个句子相似度都是0.85,哪个应该排前面?我们需要更智能的排序。

5.2 多维度排序算法

在实际应用中,除了相似度,我们还要考虑其他因素:

class SmartResultSorter:
    """智能结果排序器"""
    
    def __init__(self, weights=None):
        """
        初始化排序器
        
        参数:
            weights: 各维度权重,默认值如下
        """
        self.weights = weights or {
            'similarity': 0.6,      # 相似度权重
            'length_match': 0.15,    # 长度匹配权重
            'word_overlap': 0.15,    # 词汇重叠权重
            'popularity': 0.10,      # 热度权重
        }
        
        # 热度数据(示例,实际应从数据库或缓存获取)
        self.popularity_data = {
            "如何修改登录密码": 95,
            "密码忘记了怎么办": 88,
            "怎么注册账号": 76,
            "物流配送问题": 82,
            "如何退货退款": 71,
            "客服联系方式": 65,
        }
    
    def calculate_length_score(self, query, candidate):
        """
        计算长度匹配分数
        查询和候选文本长度越接近,分数越高
        """
        query_len = len(query)
        candidate_len = len(candidate)
        
        if query_len == 0 or candidate_len == 0:
            return 0
        
        length_ratio = min(query_len, candidate_len) / max(query_len, candidate_len)
        
        # 长度完全匹配得1分,差异越大分数越低
        return length_ratio
    
    def calculate_word_overlap(self, query, candidate):
        """
        计算词汇重叠分数
        使用Jaccard相似度:交集大小 / 并集大小
        """
        # 简单分词(实际应用应使用更好的分词器)
        query_words = set(query)
        candidate_words = set(candidate)
        
        if not query_words or not candidate_words:
            return 0
        
        intersection = query_words.intersection(candidate_words)
        union = query_words.union(candidate_words)
        
        return len(intersection) / len(union)
    
    def get_popularity_score(self, candidate):
        """获取热度分数"""
        # 归一化到0-1
        max_popularity = max(self.popularity_data.values()) if self.popularity_data else 100
        popularity = self.popularity_data.get(candidate, 50)  # 默认50
        
        return popularity / max_popularity
    
    def calculate_composite_score(self, query, candidate, similarity):
        """
        计算综合分数
        
        参数:
            query: 查询文本
            candidate: 候选文本
            similarity: 语义相似度
            
        返回:
            composite_score: 综合分数
            score_breakdown: 各维度分数详情
        """
        # 计算各维度分数
        length_score = self.calculate_length_score(query, candidate)
        overlap_score = self.calculate_word_overlap(query, candidate)
        popularity_score = self.get_popularity_score(candidate)
        
        # 计算加权综合分数
        composite_score = (
            similarity * self.weights['similarity'] +
            length_score * self.weights['length_match'] +
            overlap_score * self.weights['word_overlap'] +
            popularity_score * self.weights['popularity']
        )
        
        # 构建分数详情
        score_breakdown = {
            'similarity': similarity,
            'length_score': length_score,
            'overlap_score': overlap_score,
            'popularity_score': popularity_score,
            'composite_score': composite_score,
            'weights': self.weights
        }
        
        return composite_score, score_breakdown
    
    def smart_sort(self, query, candidates_with_similarity):
        """
        智能排序
        
        参数:
            query: 查询文本
            candidates_with_similarity: 包含相似度的候选列表
               格式: [{'sentence': '文本', 'similarity': 0.85}, ...]
                
        返回:
            sorted_results: 排序后的结果
        """
        scored_results = []
        
        for item in candidates_with_similarity:
            candidate = item['sentence']
            similarity = item['similarity']
            
            # 计算综合分数
            composite_score, breakdown = self.calculate_composite_score(
                query, candidate, similarity
            )
            
            scored_results.append({
                'sentence': candidate,
                'similarity': similarity,
                'composite_score': composite_score,
                'score_breakdown': breakdown
            })
        
        # 按综合分数降序排序
        sorted_results = sorted(
            scored_results,
            key=lambda x: x['composite_score'],
            reverse=True
        )
        
        return sorted_results
    
    def sort_with_explanations(self, query, candidates_with_similarity, top_n=5):
        """
        排序并给出解释
        
        返回:
            sorted_results: 排序后的结果,包含解释
        """
        sorted_results = self.smart_sort(query, candidates_with_similarity)
        
        # 只返回前N个
        top_results = sorted_results[:top_n]
        
        # 为每个结果添加解释
        for i, result in enumerate(top_results, 1):
            breakdown = result['score_breakdown']
            
            # 生成解释文本
            explanations = []
            
            if breakdown['similarity'] >= 0.8:
                explanations.append("语义高度相似")
            elif breakdown['similarity'] >= 0.6:
                explanations.append("语义较为相似")
            
            if breakdown['length_score'] >= 0.9:
                explanations.append("长度匹配度很高")
            elif breakdown['length_score'] >= 0.7:
                explanations.append("长度较为匹配")
            
            if breakdown['overlap_score'] >= 0.5:
                overlap_percent = breakdown['overlap_score'] * 100
                explanations.append(f"词汇重叠度{overlap_percent:.0f}%")
            
            if breakdown['popularity_score'] >= 0.8:
                explanations.append("热门问题")
            
            result['explanation'] = ",".join(explanations) if explanations else "综合匹配"
            result['rank'] = i
        
        return top_results

# 使用示例
def demo_smart_sorting():
    """演示智能排序"""
    
    # 初始化排序器
    sorter = SmartResultSorter()
    
    # 测试查询
    query = "密码忘记了怎么办"
    
    # 候选句子(带相似度)
    candidates = [
        {"sentence": "如何修改登录密码", "similarity": 0.82},
        {"sentence": "密码重置方法", "similarity": 0.78},
        {"sentence": "登录密码修改", "similarity": 0.85},
        {"sentence": "怎么改密码", "similarity": 0.88},
        {"sentence": "账户密码找回", "similarity": 0.76},
        {"sentence": "用户密码修改指南", "similarity": 0.72},
        {"sentence": "忘记密码如何处理", "similarity": 0.90},
        {"sentence": "密码管理", "similarity": 0.65},
        {"sentence": "安全密码设置", "similarity": 0.58},
        {"sentence": "账号注册流程", "similarity": 0.32},
    ]
    
    print("查询:", query)
    print("\n候选句子(按语义相似度排序):")
    print("-" * 80)
    
    # 1. 按相似度排序(传统方法)
    similarity_sorted = sorted(candidates, key=lambda x: x['similarity'], reverse=True)
    
    for i, item in enumerate(similarity_sorted[:5], 1):
        print(f"{i:2d}. 相似度: {item['similarity']:.3f} - {item['sentence']}")
    
    print("\n智能排序结果(考虑多维度):")
    print("-" * 80)
    
    # 2. 智能排序
    smart_sorted = sorter.sort_with_explanations(query, candidates, top_n=5)
    
    for result in smart_sorted:
        print(f"{result['rank']:2d}. 综合分: {result['composite_score']:.3f} (相似度: {result['similarity']:.3f})")
        print(f"     句子: {result['sentence']}")
        print(f"     解释: {result['explanation']}")
        
        # 显示详细分数
        breakdown = result['score_breakdown']
        print(f"     详情: 长度分{breakdown['length_score']:.2f}, "
              f"重叠分{breakdown['overlap_score']:.2f}, "
              f"热度分{breakdown['popularity_score']:.2f}")
        print()
    
    # 对比分析
    print("\n排序对比分析:")
    print("-" * 80)
    
    # 找出排序变化最大的
    similarity_ranking = {item['sentence']: i+1 for i, item in enumerate(similarity_sorted)}
    smart_ranking = {item['sentence']: item['rank'] for item in smart_sorted}
    
    print("句子                   相似度排序  智能排序  变化")
    print("-" * 50)
    
    for item in smart_sorted:
        sentence = item['sentence']
        sim_rank = similarity_ranking.get(sentence, "N/A")
        smart_rank = item['rank']
        
        if isinstance(sim_rank, int):
            change = sim_rank - smart_rank
            change_str = f"↑{abs(change)}" if change > 0 else f"↓{abs(change)}" if change < 0 else "="
        else:
            change_str = "N/A"
        
        print(f"{sentence:20} {sim_rank:10} {smart_rank:10} {change_str:>5}")

# 运行演示
if __name__ == "__main__":
    demo_smart_sorting()

5.3 业务规则增强排序

在某些业务场景下,我们需要加入特定的业务规则:

class BusinessRuleSorter:
    """业务规则增强的排序器"""
    
    def __init__(self):
        # 业务规则配置
        self.business_rules = {
            'priority_keywords': {
                '紧急': 2.0,      # 紧急相关权重加倍
                '立即': 1.8,
                '尽快': 1.8,
                '故障': 1.7,
                '错误': 1.7,
                '无法': 1.6,
                '不能': 1.6,
            },
            'category_boost': {
                'account': 1.3,    # 账户类问题提升30%
                'security': 1.5,   # 安全类问题提升50%
                'payment': 1.2,    # 支付类问题提升20%
                'general': 1.0,    # 一般问题不变
            },
            'time_sensitive_boost': {
                'hour': {
                    9: 1.1,   # 9点:上班时间,提升10%
                    14: 1.0,  # 14点:正常
                    20: 1.2,  # 20点:晚上,提升20%
                    2: 1.3,   # 2点:深夜,提升30%
                }
            },
            'user_value_boost': {
                'vip': 1.4,    # VIP用户提升40%
                'regular': 1.1, # 常规用户提升10%
                'new': 1.0,     # 新用户不变
            }
        }
        
        # 分类器(简化版,实际应使用更复杂的分类模型)
        self.category_keywords = {
            'account': ['密码', '登录', '注册', '账户', '账号'],
            'security': ['盗号', '诈骗', '安全', '风险', '验证'],
            'payment': ['支付', '退款', '扣款', '账单', '费用'],
            'order': ['订单', '发货', '物流', '配送', '收货'],
            'after_sale': ['退货', '换货', '售后', '维修', '保修'],
        }
    
    def detect_category(self, text):
        """检测文本分类"""
        text_lower = text.lower()
        
        for category, keywords in self.category_keywords.items():
            for keyword in keywords:
                if keyword in text_lower:
                    return category
        
        return 'general'
    
    def calculate_business_score(self, query, candidate, similarity, user_context=None):
        """
        计算业务规则增强的分数
        
        参数:
            query: 查询文本
            candidate: 候选文本
            similarity: 基础相似度
            user_context: 用户上下文(可选)
                {
                    'user_type': 'vip' | 'regular' | 'new',
                    'query_time': datetime对象,
                    'is_urgent': bool
                }
        """
        # 基础分数
        base_score = similarity
        
        # 1. 关键词优先级提升
        keyword_boost = 1.0
        for keyword, boost in self.business_rules['priority_keywords'].items():
            if keyword in query or keyword in candidate:
                keyword_boost *= boost
        
        # 2. 分类提升
        category = self.detect_category(query)
        category_boost = self.business_rules['category_boost'].get(category, 1.0)
        
        # 3. 时间敏感度提升
        time_boost = 1.0
        if user_context and 'query_time' in user_context:
            hour = user_context['query_time'].hour
            # 找到最接近的小时
            closest_hour = min(self.business_rules['time_sensitive_boost']['hour'].keys(),
                              key=lambda x: abs(x - hour))
            time_boost = self.business_rules['time_sensitive_boost']['hour'][closest_hour]
        
        # 4. 用户价值提升
        user_boost = 1.0
        if user_context and 'user_type' in user_context:
            user_boost = self.business_rules['user_value_boost'].get(
                user_context['user_type'], 1.0
            )
        
        # 5. 紧急程度提升
        urgent_boost = 1.3 if (user_context and user_context.get('is_urgent')) else 1.0
        
        # 计算最终分数(使用乘法而不是加法,避免分数超出范围)
        final_score = base_score * keyword_boost * category_boost * time_boost * user_boost * urgent_boost
        
        # 确保分数在0-1之间
        final_score = min(1.0, max(0.0, final_score))
        
        # 返回结果
        return {
            'final_score': final_score,
            'breakdown': {
                'base_similarity': similarity,
                'keyword_boost': keyword_boost,
                'category_boost': category_boost,
                'time_boost': time_boost,
                'user_boost': user_boost,
                'urgent_boost': urgent_boost,
                'category': category
            }
        }
    
    def sort_with_business_rules(self, query, candidates_with_similarity, user_context=None, top_n=5):
        """
        使用业务规则排序
        
        参数:
            query: 查询文本
            candidates_with_similarity: 候选列表
            user_context: 用户上下文
            top_n: 返回前N个结果
        """
        scored_candidates = []
        
        for candidate_item in candidates_with_similarity:
            candidate = candidate_item['sentence']
            similarity = candidate_item['similarity']
            
            # 计算业务增强分数
            business_score_info = self.calculate_business_score(
                query, candidate, similarity, user_context
            )
            
            scored_candidates.append({
                'sentence': candidate,
                'similarity': similarity,
                'business_score': business_score_info['final_score'],
                'score_breakdown': business_score_info['breakdown']
            })
        
        # 按业务分数排序
        sorted_candidates = sorted(
            scored_candidates,
            key=lambda x: x['business_score'],
            reverse=True
        )
        
        # 返回前N个
        return sorted_candidates[:top_n]

# 使用示例
def demo_business_rule_sorting():
    """演示业务规则排序"""
    
    sorter = BusinessRuleSorter()
    
    # 测试查询
    query = "密码被盗了怎么办"
    
    # 候选句子
    candidates = [
        {"sentence": "如何修改登录密码", "similarity": 0.75},
        {"sentence": "账户安全设置", "similarity": 0.82},
        {"sentence": "密码重置方法", "similarity": 0.78},
        {"sentence": "账号被盗处理流程", "similarity": 0.88},
        {"sentence": "紧急安全求助", "similarity": 0.70},
        {"sentence": "一般账户问题", "similarity": 0.65},
        {"sentence": "密码管理指南", "similarity": 0.72},
        {"sentence": "登录问题解决", "similarity": 0.68},
    ]
    
    # 不同用户上下文
    user_contexts = [
        {
            'name': '普通用户白天咨询',
            'context': {
                'user_type': 'regular',
                'query_time': datetime(2024, 1, 15, 14, 30, 0),  # 下午2:30
                'is_urgent': False
            }
        },
        {
            'name': 'VIP用户深夜紧急',
            'context': {
                'user_type': 'vip',
                'query_time': datetime(2024, 1, 15, 2, 30, 0),   # 凌晨2:30
                'is_urgent': True
            }
        },
        {
            'name': '新用户晚上咨询',
            'context': {
                'user_type': 'new',
                'query_time': datetime(2024, 1, 15, 20, 30, 0),  # 晚上8:30
                'is_urgent': False
            }
        }
    ]
    
    print("查询:", query)
    print("\n候选句子:")
    for i, cand in enumerate(candidates, 1):
        print(f"  {i:2d}. {cand['sentence']} (相似度: {cand['similarity']:.3f})")
    
    # 测试不同用户上下文
    for user_context_info in user_contexts:
        context_name = user_context_info['name']
        user_context = user_context_info['context']
        
        print(f"\n{'='*60}")
        print(f"用户场景: {context_name}")
        print(f"用户类型: {user_context['user_type']}")
        print(f"查询时间: {user_context['query_time'].strftime('%H:%M')}")
        print(f"是否紧急: {'是' if user_context['is_urgent'] else '否'}")
        print("-" * 60)
        
        # 使用业务规则排序
        sorted_results = sorter.sort_with_business_rules(
            query, candidates, user_context, top_n=5
        )
        
        print("排序结果:")
        for i, result in enumerate(sorted_results, 1):
            breakdown = result['score_breakdown']
            print(f"\n{i:2d}. {result['sentence']}")
            print(f"    业务分数: {result['business_score']:.3f} (原始相似度: {result['similarity']:.3f})")
            print(f"    分类: {breakdown['category']}")
            print(f"    提升因子: 关键词×{breakdown['keyword_boost']:.2f}, "
                  f"分类×{breakdown['category_boost']:.2f}, "
                  f"时间×{breakdown['time_boost']:.2f}, "
                  f"用户×{breakdown['user_boost']:.2f}, "
                  f"紧急×{breakdown['urgent_boost']:.2f}")
    
    # 对比分析
    print(f"\n{'='*60}")
    print("排序对比分析(不同场景下的排名变化):")
    print("-" * 60)
    
    # 计算每个句子在不同场景下的排名
    sentence_rankings = {}
    
    for cand in candidates:
        sentence = cand['sentence']
        sentence_rankings[sentence] = []
    
    for user_context_info in user_contexts:
        context_name = user_context_info['name']
        user_context = user_context_info['context']
        
        sorted_results = sorter.sort_with_business_rules(
            query, candidates, user_context, top_n=len(candidates)
        )
        
        # 记录排名
        for rank, result in enumerate(sorted_results, 1):
            sentence = result['sentence']
            if sentence not in sentence_rankings:
                sentence_rankings[sentence] = []
            sentence_rankings[sentence].append((context_name, rank))
    
    # 显示对比
    print("\n句子                   原始相似度  普通用户  VIP紧急  新用户")
    print("-" * 70)
    
    for cand in candidates:

相关推荐

CAD+说明书履带拖拉机变速器改进设计Ⅳ

CAD+说明书履带拖拉机变速器改进设计 Ⅳ

深度学习算法FAQ中的应用

1:摘要 在问答系统的应用中,用户输入一个问题,系统需要根据问题去寻找最合适的答案。 针对该应用场景,有三种处理方式: 1:采用句子相似度的方式。根据问题的字面相似度选择相似度最高的问题对应的答案,但是采用这种字面相似度的方式并不能有效的捕获用户意图的能力有限。 2:IBM早期应用在watson系统中的DeepQa算法。该算法通过人工抽取一系列的特征,然后将这些特征输入一个回归模型。该算法普适性较强,并且能有效的解决实际中的问题,但是准确率和召回率一般。 3:深度学习算法。依然是IBM的watson

shenyuan12的专栏 694

科技中介如何高效匹配技术供需?.docx

科技中介如何高效匹配技术供需?

算法相关FAQ

一基础编程模型 1考察溢出 问: Math.abs(-2147483648)的返回值是什么? 答: -2147483648。这个奇怪的结果就是证数溢出的典型例子。int整型范围为-2147483648~2147483647。 编程: package AlgorithmFAQ; /** * Copyright (C), 2020-2020, XXX有限公司 * FileNam...

实践求真知 408

基础课16——FAQ 问答引擎

智能客服FAQ问答引擎是一种基于人工智能技术的问答系统,旨在自动回答用户的问题和解决常见问题。这种引擎利用自然语言处理技术和机器学习算法来理解用户的问题,并从预先定义的常见问题列表中匹配最佳答案。

2202_75469062的博客 2522

!qhyb.rar

当 CAD 缺失对应字体时,图纸文字会显示异常,出现乱码、问号。将下载好的字体文件复制到 AutoCAD 的 Fonts 文件夹中,即可恢复正常显示。

政府科技管理部门如何实现科技成果高效转化?.docx

政府科技管理部门如何实现科技成果高效转化?

国有企业如何推动科技创新与产业升级融合?.docx

科易网基于40亿+科创知识图谱数据,深度探索AI技术在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,研究科技创新领域的AI+数智化解决方案,推动科技创新与产业创新智能化发展。

科技中介服务如何实现数字化升级?.docx

科易网基于40亿+科创知识图谱数据,深度探索AI技术在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,研究科技创新领域的AI+数智化解决方案,推动科技创新与产业创新智能化发展。

librdkafka Windows 包(可直接使用)

librdkafka/win ├─ bin/ # 全部运行时dll + pdb调试符号 + plugins插件目录 ├─ include/ # 头文件 rdkafka.h └─ lib/ # .lib静态导入(链接阶段使用)

兄弟MFC-J2330DW 中文维修手册

兄弟MFC-J2330DW 中文维修手册

CesiumForUnitySamples-v1.25.1

Cesium for Unity 是一个官方插件,它将 Cesium 强大的3D 地理空间平台能力深度集成到了 Unity 引擎中。简单来说,它让你能在 Unity 里创建出一个高精度的真实地球,并加载和探索全球范围内的海量地理数据

政府科技管理部门如何降低平台建设成本?.docx

科易网基于40亿+科创知识图谱数据,深度探索AI技术在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,研究科技创新领域的AI+数智化解决方案,推动科技创新与产业创新智能化发展。

sam l part3 niubi666

sam l part3 niubi666

内窥镜摄像系统,全球前10强生产商排名及市场份额.docx

内窥镜摄像系统,全球前10强生产商排名及市场份额

RIS 智能超表面:6G 的"可编程无线电环境"如何靠 N² 定律改写覆盖规则

`RIS` `智能超表面` `6G` `可编程无线电环境` `波束成形` `N²定律` `相位优化` `Sionna` `毫米波` `通信仿真`

上一篇: SPIRAN ART SUMMONER步骤详解:祈祷词编写→同步率调节→唤醒祈之子全流程
下一篇: YOLOE官版镜像开源可部署:YOLOE-v8m-seg支持私有化部署,保障数据安全
魔王不造反
博客等级 码龄9年 2699粉丝 2986原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值