去年下半年,我们团队接到了来自HR部门的紧急需求。他们每天都要处理大量重复性问题咨询,比如"入职需要准备什么材料"、"出差住宿能报多少钱"这类问题,每个HR同事平均每天要回答几十遍相同的提问。这不仅浪费人力资源,也影响了员工体验。
深入沟通后,我们发现这个需求远比表面看起来复杂。财务部门也提出了类似需求,但他们要求文档访问必须严格控制权限;IT部门希望接入操作手册,但又不希望其他部门看到内部系统配置。这让我们意识到,简单的FAQ系统根本无法满足实际需求。
核心需求可以归纳为三点:
传统的关键词检索方案基于Elasticsearch实现,其优势非常明显:
但实际测试中暴露了严重问题。当员工提问"去上海出差,酒店能报多少钱?"时,系统无法理解"上海"属于"一线城市"、"酒店"等同于"住宿"这样的语义关系。文档中明确写着"一线城市差旅住宿标准为500元/天",却因为关键词不匹配而无法返回正确结果。
向量检索通过将文本转换为高维向量空间中的点,计算相似度来匹配问题与答案。理论上,这种方法能够:
但在测试内部系统"HR-Link"相关问题时,向量检索暴露了致命缺陷。当员工直接提问"HR-Link的登录地址是什么?"时,由于预训练模型不认识这个内部专有名词,系统完全无法给出正确答案。
经过充分测试,我们确认两种检索方式各有优劣:
最终方案采用混合检索架构:
场景1:专有名词查询
员工问:"HR-Link的登录地址是什么?"
场景2:语义理解查询
员工问:"忘记密码怎么办?"
场景3:口语化表达
员工问:"我登不上代码平台了"
系统上线后,我们发现多轮对话场景存在严重问题。当员工先问"一线城市出差住宿标准是多少?"得到"500元/天"的回答后,接着问"二线城市呢?",系统完全无法理解这个省略语境的提问。
问题根源分析:
解决方案:查询重写(Query Rewriting)
在检索前,先让LLM结合对话历史将问题补全:
code复制历史:Q1:"一线城市出差住宿标准是多少?" A1:"500元/天"
当前问题:"二线城市呢?"
重写后:"二线城市出差住宿标准是多少?"
重写后的完整问题再进行检索,准确率大幅提升。
权限隔离是企业知识库的生命线。我们的解决方案是在文档入库时就打好权限标签,检索时严格过滤。
文档元数据示例:
json复制{
"content": "差旅住宿标准...",
"metadata": {
"department": "finance",
"doc_type": "policy",
"is_latest": true,
"update_time": "2024-12-01"
}
}
检索时过滤逻辑:
python复制user_department = get_user_department() # 获取用户部门
es_filter = {"department": user_department} # ES检索过滤
vector_filter = {"department": user_department} # 向量检索过滤
这种设计确保:
python复制from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 经过优化的分块大小
chunk_overlap=50 # 适当的重叠保证上下文连贯
)
chunks = text_splitter.split_documents(documents)
python复制from langchain.retrievers import EnsembleRetriever
from langchain.vectorstores import FAISS
from langchain.retrievers import BM25Retriever
# 初始化两个检索器
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 10 # 获取10条结果
vectorstore = FAISS.from_documents(chunks, embeddings)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
# 混合检索器
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.5, 0.5] # 权重可调整
)
python复制from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatAnthropic
rewrite_prompt = ChatPromptTemplate.from_messages([
("system", "根据对话历史,将用户的简短问题补全为完整的问题。"),
("human", "对话历史:{history}\n当前问题:{question}")
])
llm = ChatAnthropic(model="claude-sonnet-4-20250514")
rewrite_chain = rewrite_prompt | llm
上线三个月后的数据表明:
准确率表现:
召回率对比:
典型成功案例:
初始设置的1000字符分块导致返回内容过于冗长。调整为500字符后,回答精准度显著提升,同时保持了必要的上下文。
英文预训练模型对中文支持不佳,切换为BGE中文模型后,准确率直接提升20%。这提醒我们模型选择必须贴合实际语言环境。
最初在检索后过滤导致结果集过小。改为检索前过滤后,既保证了安全性,又不影响结果质量。
曾因ES和向量库更新不同步导致结果不一致。引入版本号强制同步机制后解决了这个问题。
回顾整个项目,成功的关键在于三个核心设计:
这些设计都不是高深的理论突破,而是针对实际业务场景的务实解决方案。最重要的经验是:不要迷信单一技术方案,要根据具体需求灵活组合各种技术手段。