一、背景与挑战
- 初始阈值难以兼顾
一开始往往只能使用经验值(如 0.5)进行设置,但对不同业务场景与数据分布,有时过松会导致误命中,过紧又会漏命中。 - 手工调参成本高
传统做法需要编写大量脚本,手工跑多次 A/B 测试,耗时且易出错。 - 自动化需求
希望提供一种轻量级、可复现的阈值调优流程,兼顾准确率和召回率。
二、CacheThresholdOptimizer:语义缓存阈值优化
1. 场景描述
假设已经搭建了一个语义缓存 sem_cache
,最初将阈值设为 0.5,并在其中存储了以下条目:
from redisvl.extensions.cache.llm import SemanticCache
from redisvl.utils.vectorize import HFTextVectorizer
sem_cache = SemanticCache(
name="sem_cache",
redis_url="redis://localhost:6379",
distance_threshold=0.5,
vectorizer=HFTextVectorizer("redis/langcache-embed-v1")
)
paris_key = sem_cache.store(prompt="what is the capital of france?", response="paris")
rabat_key = sem_cache.store(prompt="what is the capital of morocco?", response="rabat")
此时对无关问题(如 “what’s the capital of Britain?”)仍能命中,说明阈值过于宽松。
2. 定义测试数据
构造一个包含“正例”、“反例”与“多个正例”的列表:
test_data = [
{"query": "What's the capital of Britain?", "query_match": "" }, # 反例
{"query": "What's the capital of France??", "query_match": paris_key }, # 正例
{"query": "What's the capital city of Morocco?", "query_match": rabat_key }, # 正例
]
"query_match"
为空字符串表示该query
不 应该命中任何缓存条目。
3. 运行优化器
from redisvl.utils.optimize import CacheThresholdOptimizer
print(f"优化前阈值:{sem_cache.distance_threshold}\n")
optimizer = CacheThresholdOptimizer(sem_cache, test_data)
optimizer.optimize()
print(f"优化后阈值:{sem_cache.distance_threshold}\n")
- 内部原理:遍历多个可能阈值,通过计算 F1 分数(兼顾准确率与召回率)来选出最佳阈值。
- 优化结果:例如阈值从 0.5 优化为 ~0.1037,使得“Britain”反例不再命中,而“France”与“Morocco”正例仍可命中。
4. 验证效果
# 反例不再命中
assert sem_cache.check("what's the capital of britain?") == []
# 正例仍可命中
hits = sem_cache.check("what's the capital city of france?")
assert hits[0]["response"] == "paris"
三、RouterThresholdOptimizer:语义路由阈值优化
1. 场景描述
假设有一个简单的“问候/告别”语义路由器:
from redisvl.extensions.router import Route
from redisvl.extensions.router import SemanticRouter
from redisvl.utils.vectorize import HFTextVectorizer
routes = [
Route(name="greeting", references=["hello", "hi"], metadata={}, distance_threshold=0.5),
Route(name="farewell", references=["bye", "goodbye"], metadata={}, distance_threshold=0.5),
]
router = SemanticRouter(
name="greeting-router",
vectorizer=HFTextVectorizer(),
routes=routes,
redis_url="redis://localhost:6379",
overwrite=True
)
当前两个路由的阈值都为 0.5,但在遇到多种问候与告别表达时,需要细粒度调优。
2. 定义测试数据
列举大量同义表达与“非路由”反例:
test_data = [
# Greeting 同义扩展
{"query":"hello", "query_match":"greeting"},
{"query":"good morning","query_match":"greeting"},
{"query":"yo", "query_match":"greeting"},
# Farewell 同义扩展
{"query":"bye", "query_match":"farewell"},
{"query":"see you later","query_match":"farewell"},
{"query":"peace out","query_match":"farewell"},
# 反例
{"query":"what's the capital of britain?","query_match":""},
{"query":"tell me a joke", "query_match":""},
]
3. 运行优化器
from redisvl.utils.optimize import RouterThresholdOptimizer
print(f"优化前阈值:{router.route_thresholds}\n")
optimizer = RouterThresholdOptimizer(router, test_data)
optimizer.optimize()
print(f"优化后阈值:{router.route_thresholds}\n")
- 内部原理:同时对所有路由阈值进行随机搜索与 F1 评分,选出最优组合。
- 优化结果:如
{"greeting": 0.58, "farewell": 0.75}
,使更多同义表达被正确分类,同时避免将无关句子误路由。
4. 测试效果
# 匹配 “hi there” 到 greeting
match = router("hi there")
assert match.name == "greeting"
# “goodbye!” 匹配到 farewell
match = router("goodbye!")
assert match.name == "farewell"
# 反例不命中
match = router("what's for dinner?")
assert match.name is None
四、优化流程总结
准备初始实例
- 对于缓存:
SemanticCache(distance_threshold=初始值)
- 对于路由:
SemanticRouter(routes=..., route_thresholds=初始值)
- 对于缓存:
构造测试数据
- 正例:应命中某条缓存或对应路由
- 反例:不应命中任何条目或路由
调用优化器
CacheThresholdOptimizer.optimize()
RouterThresholdOptimizer.optimize()
验证效果
- 使用
.check()
或直接调用路由器,确保正例命中且反例不命中
- 使用
五、最佳实践
- 多样化测试样本:涵盖同义表达、噪声文本与边界情况,确保优化结果稳健。
- 定期复训:当缓存或路由的参考集更新后,需重新运行优化器。
- 性能监控:记录优化前后命中率、错误率与延迟,评估实际业务效果。
- 自动化集成:将优化流程纳入 CI/CD 管道,保持阈值与数据同步演进。
通过上述两种优化器,RedisVL 帮助你从经验值调参,过渡到基于实际业务样本的自动化阈值搜索,大幅减少人工成本,并显著提升语义缓存与路由系统的准确性与鲁棒性。