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()
这个系统有几个关键特性:
- 自动检测文件变化:FAQ文件被修改后,系统会自动重新加载
- 内存缓存:避免每次请求都读文件,提升性能
- 灵活的FAQ格式:支持多种格式,包括用户问题到标准问题的映射
- 详细的匹配结果:不仅返回最佳匹配,还返回所有候选结果
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就算匹配成功。但实际业务中,这会导致两个问题:
- 误匹配:阈值太低,不相关的问题也被匹配上
- 漏匹配:阈值太高,相关的问题被过滤掉
更糟糕的是,不同业务场景需要不同的阈值:
- 客服问答: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:




被折叠的 条评论
为什么被折叠?



