对话式搜索:当搜索引擎学会了“追问”与“澄清”
·
对话式搜索:当搜索引擎学会了“追问”与“澄清”
引言
传统搜索引擎依赖用户一次性输入精确的关键词,而对话式搜索(Conversational Search) 通过多轮交互主动“追问”与“澄清”,将搜索从“单次检索”升级为“渐进式信息获取”。2026年的技术核心在于意图模糊识别与动态检索策略,让搜索引擎具备“追问”能力,解决用户意图不明确、信息过载等痛点。
技术背景
- 痛点:用户查询往往简短、模糊(如“苹果”指水果还是公司?),传统搜索只能返回宽泛结果,用户需手动筛选。
- 趋势:基于LLM + RAG的混合架构,通过Query Rewrite(查询重写)和Clarification Question(澄清提问)动态调整搜索策略。
- 关键:系统需判断“何时该直接搜索”与“何时该反问用户”,平衡搜索效率与准确性。
应用使用场景
- 电商导购:用户输入“想买手机”,系统追问“预算范围?”、“用途?(游戏/拍照)”,再返回精准商品列表。
- 知识库检索:员工提问“报销流程”,系统识别需澄清“是国内差旅还是海外项目?”,再定位具体文档。
- 垂直搜索(法律/医疗):用户描述症状“头痛发热”,系统追问“持续时间?”、“有无咳嗽?”,再提供初步诊断建议。
核心架构与原理
原理流程图
核心特性
- 主动澄清:当查询置信度低或槽位缺失时,自动生成选择题或填空式提问。
- 上下文感知:利用对话历史(History)进行指代消解(如“它”指代上轮提到的商品)。
- 混合检索:结合关键词(BM25)与向量检索(Dense Retrieval),提升召回率。
环境准备(Python 3.11+)
# 核心框架
pip install fastapi uvicorn pydantic
# 检索与向量库
pip install faiss-cpu sentence-transformers
# LLM集成(以Ollama本地模型为例)
pip install ollama langchain
# 对话状态管理
pip install redis
场景一:电商导购对话搜索(FastAPI + 槽位填充)
场景描述
用户输入模糊查询(如“买笔记本”),系统通过多轮追问补全预算、品牌、用途等槽位,最终返回精准商品。
代码实现
# conversational_search_ecommerce.py
import json
import re
from enum import Enum
from typing import Dict, List, Optional, Tuple
from pydantic import BaseModel
from fastapi import FastAPI, HTTPException
app = FastAPI()
# 商品数据库模拟(实际应为MySQL/ES)
PRODUCTS = [
{"id": 1, "name": "MacBook Pro 16", "category": "laptop", "brand": "Apple", "price": 18999, "tags": ["办公", "设计"]},
{"id": 2, "name": "ThinkPad X1", "category": "laptop", "brand": "Lenovo", "price": 12999, "tags": ["商务", "便携"]},
{"id": 3, "name": "ROG枪神7", "category": "laptop", "brand": "ASUS", "price": 14999, "tags": ["游戏", "高性能"]},
]
class Intent(str, Enum):
BUY_LAPTOP = "buy_laptop"
UNKNOWN = "unknown"
class SlotStatus(str, Enum):
UNKNOWN = "unknown"
FILLED = "filled"
class ConversationState(BaseModel):
intent: Intent = Intent.UNKNOWN
slots: Dict[str, str] = {} # 槽位:budget, brand, usage
missing_slots: List[str] = ["budget", "brand", "usage"]
history: List[Dict] = []
class EcommerceSearchEngine:
def __init__(self):
self.state_manager = {}
def get_user_state(self, user_id: str) -> ConversationState:
return self.state_manager.get(user_id, ConversationState())
def update_state(self, user_id: str, state: ConversationState):
self.state_manager[user_id] = state
def nlu_parse(self, text: str, current_state: ConversationState) -> Tuple[Intent, Dict]:
"""自然语言理解:提取意图与槽位"""
intent = Intent.UNKNOWN
slots = {}
# 关键词+规则匹配(实际可用NER模型)
if re.search(r"(笔记本|电脑|MacBook|ThinkPad)", text):
intent = Intent.BUY_LAPTOP
# 提取预算
budget_match = re.search(r"(\d+)元", text)
if budget_match:
slots["budget"] = budget_match.group(1)
# 提取品牌
if "苹果" in text or "Mac" in text:
slots["brand"] = "Apple"
elif "联想" in text or "ThinkPad" in text:
slots["brand"] = "Lenovo"
# 提取用途
if "游戏" in text:
slots["usage"] = "游戏"
elif "办公" in text or "商务" in text:
slots["usage"] = "办公"
return intent, slots
def generate_clarification(self, missing_slots: List[str]) -> str:
"""生成澄清问题"""
slot_questions = {
"budget": "请问您的预算范围是?(例如:5000-10000元)",
"brand": "您偏好哪个品牌?(苹果/联想/华硕/其他)",
"usage": "主要用途是?(游戏/办公/设计/日常使用)"
}
# 优先询问第一个缺失的槽位
slot_to_ask = missing_slots[0] if missing_slots else None
return slot_questions.get(slot_to_ask, "请提供更多细节。")
def search_products(self, slots: Dict) -> List[Dict]:
"""根据槽位检索商品"""
filtered = PRODUCTS
# 预算过滤
if "budget" in slots:
budget = int(slots["budget"])
filtered = [p for p in filtered if p["price"] <= budget]
# 品牌过滤
if "brand" in slots:
filtered = [p for p in filtered if p["brand"] == slots["brand"]]
# 用途过滤(标签匹配)
if "usage" in slots:
filtered = [p for p in filtered if slots["usage"] in p["tags"]]
return filtered
def process_query(self, user_id: str, query: str) -> Dict:
# 1. 获取当前对话状态
state = self.get_user_state(user_id)
# 2. NLU解析
intent, new_slots = self.nlu_parse(query, state)
# 3. 更新槽位状态
if intent != Intent.UNKNOWN:
state.intent = intent
for slot, value in new_slots.items():
if slot in state.missing_slots:
state.missing_slots.remove(slot)
state.slots[slot] = value
# 4. 决策:继续澄清 or 返回结果
if state.missing_slots:
# 生成澄清问题
reply = self.generate_clarification(state.missing_slots)
action = "clarify"
else:
# 执行搜索
products = self.search_products(state.slots)
if products:
product_names = [p["name"] for p in products]
reply = f"为您找到以下商品:{', '.join(product_names)}"
else:
reply = "未找到匹配商品,请调整条件。"
action = "answer"
# 重置状态(可选)
state = ConversationState()
# 5. 保存状态
state.history.append({"query": query, "reply": reply})
self.update_state(user_id, state)
return {"reply": reply, "action": action, "missing_slots": state.missing_slots}
# API端点
search_engine = EcommerceSearchEngine()
class ChatRequest(BaseModel):
user_id: str
query: str
class ChatResponse(BaseModel):
reply: str
action: str
@app.post("/search", response_model=ChatResponse)
async def conversational_search(request: ChatRequest):
try:
result = search_engine.process_query(request.user_id, request.query)
return ChatResponse(**result)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
运行结果与测试
# 测试命令序列
curl -X POST "http://localhost:8000/search" -H "Content-Type: application/json" -d '{"user_id": "u123", "query": "想买笔记本"}'
# 输出: {"reply": "请问您的预算范围是?(例如:5000-10000元)", "action": "clarify"}
curl -X POST "http://localhost:8000/search" -H "Content-Type: application/json" -d '{"user_id": "u123", "query": "8000元左右"}'
# 输出: {"reply": "主要用途是?(游戏/办公/设计/日常使用)", "action": "clarify"}
curl -X POST "http://localhost:8000/search" -H "Content-Type: application/json" -d '{"user_id": "u123", "query": "办公"}'
# 输出: {"reply": "为您找到以下商品:ThinkPad X1", "action": "answer"}
场景二:知识库多轮RAG追问(向量检索 + 置信度检测)
场景描述
用户提问“报销流程”,系统先检索知识库,若发现存在“国内/国际”不同流程且用户未指定,主动追问澄清,再返回精准答案。
代码实现
# conversational_rag.py
import numpy as np
from sentence_transformers import SentenceTransformer
import faiss
from typing import List, Tuple
class KnowledgeBase:
def __init__(self):
self.docs = [
"国内差旅报销流程:1. 填写申请单 2. 主管审批 3. 财务审核",
"国际差旅报销流程:1. 填写申请单 2. 外事处备案 3. 财务审核",
"办公用品采购流程:1. 提交采购申请 2. 行政部确认"
]
self.embedder = SentenceTransformer('BAAI/bge-small-zh-v1.5')
self.index = faiss.IndexFlatIP(384) # 假设维度384
self._build_index()
def _build_index(self):
vectors = self.embedder.encode(self.docs, normalize_embeddings=True)
self.index.add(vectors.astype('float32'))
def search(self, query: str, top_k: int = 3) -> List[Tuple[str, float]]:
query_vec = self.embedder.encode([query], normalize_embeddings=True)
scores, indices = self.index.search(query_vec.astype('float32'), top_k)
return [(self.docs[i], scores[0][j]) for j, i in enumerate(indices[0])]
class ClarificationRAG:
def __init__(self):
self.kb = KnowledgeBase()
self.clarify_threshold = 0.7 # 置信度阈值
def needs_clarification(self, query: str, results: List[Tuple[str, float]]) -> Tuple[bool, str]:
"""判断是否需要澄清:若top结果相似度低或存在歧义,则追问"""
if not results:
return True, "未找到相关文档,请重新描述您的问题。"
top_score = results[0][1]
if top_score < self.clarify_threshold:
return True, "您的问题比较宽泛,请问您是指国内差旅还是国际差旅?"
# 检查歧义:如果前两个结果属于不同类别且分数接近
if len(results) >= 2:
doc1, score1 = results[0]
doc2, score2 = results[1]
if abs(score1 - score2) < 0.1 and ("国内" in doc1) != ("国内" in doc2):
return True, "请问您是指国内差旅还是国际差旅?"
return False, ""
def answer(self, query: str, context: str = "") -> Dict:
# 1. 检索
search_query = f"{context} {query}".strip()
results = self.kb.search(search_query)
# 2. 澄清判断
need_clarify, clarify_msg = self.needs_clarification(query, results)
if need_clarify:
return {"reply": clarify_msg, "action": "clarify", "context": query}
# 3. 生成答案
top_doc = results[0][0]
answer = f"根据知识库:{top_doc}"
return {"reply": answer, "action": "answer", "context": ""}
# 测试
if __name__ == "__main__":
rag = ClarificationRAG()
# 第一轮:模糊查询触发澄清
result1 = rag.answer("报销流程")
print("Round 1:", result1)
# 输出: {"reply": "您的问题比较宽泛,请问您是指国内差旅还是国际差旅?", "action": "clarify", "context": "报销流程"}
# 第二轮:补充信息后精准回答
result2 = rag.answer("国内", context="报销流程")
print("Round 2:", result2)
# 输出: {"reply": "根据知识库:国内差旅报销流程:1. 填写申请单 2. 主管审批 3. 财务审核", "action": "answer", "context": ""}
场景三:生产级部署(Redis状态管理 + 异步检索)
场景描述
高并发场景下,使用Redis持久化对话状态,并利用异步IO提升检索效率。
代码实现
# production_conversational_search.py
import asyncio
import json
import redis.asyncio as redis
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
class RedisStateManager:
async def get_state(self, user_id: str) -> Optional[ConversationState]:
state_json = await redis_client.get(f"conv_state:{user_id}")
if state_json:
return ConversationState(**json.loads(state_json))
return ConversationState()
async def save_state(self, user_id: str, state: ConversationState):
await redis_client.setex(
f"conv_state:{user_id}",
3600, # 1小时过期
json.dumps(state.dict())
)
@app.post("/v2/search")
async def search_v2(request: ChatRequest):
state_manager = RedisStateManager()
engine = EcommerceSearchEngine() # 复用之前的引擎
# 异步获取状态
state = await state_manager.get_state(request.user_id)
# 处理查询(假设process_query已适配异步)
result = await asyncio.to_thread(engine.process_query, request.user_id, request.query, state)
# 异步保存状态
await state_manager.save_state(request.user_id, state)
return result
疑难解答
- 澄清死循环:设置最大澄清轮次(如3次),超时后强制返回当前最佳结果或转人工。
- 状态混乱:用户突然切换话题时,通过意图变化检测自动重置对话状态(如检测到“查天气”后清空之前的“买电脑”槽位)。
- 性能瓶颈:向量检索耗时较长,可对澄清阶段的查询使用关键词检索(BM25) 快速过滤,仅在最终回答时使用向量检索。
未来展望与技术趋势
- LLM原生控制:不再依赖规则判断,由LLM直接决策“是否追问”及“问什么”,实现更自然的对话流。
- 多模态澄清:支持图片+文本混合输入(如用户上传商品图片,系统追问具体型号)。
- 个性化追问:基于用户历史行为预测缺失信息,减少提问次数(如老用户默认使用上次的预算范围)。
总结
对话式搜索的“追问”能力本质是信息熵的降低过程。通过槽位填充(结构化信息)与检索置信度(语义匹配)的双重判断,系统在“猜测”与“确认”间找到平衡。2026年的最佳实践是**“规则兜底 + LLM驱动”**:用规则保证可控性,用LLM提升对话的自然度,最终实现搜索从“工具”到“助手”的进化。
更多推荐


所有评论(0)