本文共计2664字 预计阅读时长8分钟
一、引言
本文主要面向对RAG概念有基础了解的读者,因此不再赘述RAG基础概念。在写这篇最佳实践之前,先回答一个问题:在上千万tokens上下文窗口模型出现后,为什么我们仍然需要RAG?
原因很简单,将所有数据都加载到模型上下文存在如下挑战:
1.可扩展性:企业知识库以TB或PB为单位,而不是以token为单位,即使有1000万个tokens上下文窗口,仍然只能看到可用信息的一小部分;
2.准确性:实际的上下文窗口与产品发布时宣传的差异巨大,研究一致表明,早在模型达到官方宣传极限之前,准确性就已经下降到不可用的情况;
3.延迟:将所有内容加载到模型的上下文中会导致响应时间显著延长。对于面向用户的应用程序来说,用户在得到答案之前就会放弃交互,体验很差;
4.效率:每次回答一个简单的问题,你都要翻阅整本教科书吗?
在RAG中,高效精准的检索是非常关键的,混合搜索以及结构化数据支持方面的进步,有助于在知识库中找到正确信息,从而解决上述的问题。同时RAG与「长上下文」、「微调」、「MCP」这些概念并不排斥,他们可以通过最佳的协同方式去解决前沿模型的局限性:
最近腾讯云ES发布的新特性「智能搜索开发」,以AI搜索增强版内核为底座,进一步优化了对全文与向量混合搜索的能力支持,从原始文档解析、向量化等原子能力,到查询性能、混合排序效率、搜索结果精准度等提供了全方位的支持和优化,让搜索有了更多想象空间。 在此基础上,还可以与混元、DeepSeek等大语言模型无缝集成,从而帮助企业进一步高效、灵活的构建知识问答等RAG应用,在关键环节也可介入调优,整体流程如下:
接下来具体看下搭建过程。
二、AI助手搭建
其实,你只需要运行下面的文件成功,你就可以快速构建一个AI问答应用
想运行上述文件(文件在文末扫码下载)成功,你得获得下述信息,可以参照【前置条件】步骤获取。
# 腾讯云API客户端配置,获取方式见【3.获取API秘钥】
SECRET_ID = "AKID******" # 替换为你的云API SecretId
SECRET_KEY = "******" # 替换为你的云API SecretKey
# es集群相关配置,获取方式见【1.购买集群】【2.获取ES访问地址】
# 用户名为 elastic、密码在创建集群时设置。用本地环境测试时,可开启公网访问,实际生产时,建议使用内网访问地址。
ES_ENDPOINT = "https://es-******.tencentelasticsearch.com:9200"
ES_USERNAME = "elastic" # Elasticsearch集群用户名
ES_PASSWORD = "******" # Elasticsearch集群密码
INDEX_NAME = "索引名称" # 在写入时要保证索引未被创建过,会默认创建索引并设置mapping,写入数据和在线问答的索引保持一致
# 数据入库时,选择COS方式需设置你的COS文件路径
# 要处理的文档URL(推荐将文件存放到COS存储,并开启「公有读私有写」权限)
document_url = "https://xxx.cos.xxx/sample.doc"
当然,如果需要深入了解代码细节,或想调整相关参数达到匹配业务的效果,可参阅下一章节【核心代码解析】。
前置条件
1.购买ES集群
(1)登录腾讯云ES控制台,单击「新建」按钮。
(2)进入新建集群界面,用于测试验证计费模式可选「按量计费」,产品版本选择「AI 搜索增强版」,专用于对搜索能力、查询性能和稳定性有极高要求的搜索和RAG场景,Elasticsearch版本选择「8.16.1」,向量能力主要集中在8.x以上版本。
(3)ES节点配置,测试验证可选择为「2核4G」,节点数默认为「3」,磁盘为「增强型 SSD云硬盘」,磁盘容量为「20GB」。
(4)其余配置选择默认即可,注意保存集群访问密码。
2.获取ES访问地址
对应集群 下可获取相关信息,用户名为elastic,密码在创建集群时所设置,如已忘记可以重置。使用本地mac测试时,可开启公网访问,实际生产时,建议使用内网访问地址。
3.开通服务
现在集群创建好了,在数据入库以及查询时需要用到很多模型服务,腾讯云 ES 智能搜索开发已经集成了文档解析、文本切片、Embedding、Rerank、LLM服务,只需开通服务即可,点击「立即开通」,确认免费开通。
4.获取API秘钥
进入API密钥管理界面,获取SecretID、SecretKey。
搭建步骤
1.安装依赖
# Python版本:3.9及以上
pip install elasticsearch==8.16.0
pip install tencentcloud-sdk-python
pip install urllib3 certifi
# 安装文档处理包
pip install python-docx pypdf2 lxml
pip install streamlit #使用COS数据入库时,无需安装
2.数据入库
这里提供了两种方式,如果有大批量的文档需要进行处理,或者你需要上生产,那建议按【2.1.COS上传】方式入库,这种方式更加稳定和安全;如果你只是简单体验一下AI问答demo,并通过界面可视化的方式操作,那可以按【2.2.本地上传】的方式入库。
2.1.COS上传(可选)
(1)先将需要处理的文档拖入对应COS桶,具体操作步骤:进入COS控制台->点击「存储桶列表」->对应文件存储桶->点击「上传文件」->出现以下弹框,拖拽需要上传的文件即可
(2)然后设置文档权限,这里需要对文档进行解析切片处理,所以需要开放「公有读私有写」权限,具体操作:点击「存储桶列表」->对应文件存储桶->对应文件详情->开启「公有读私有写」权限
(3)在data_input_inference.py文件中,修改main方法中的document_url为你上传的COS文件地址
# 要处理的文档URL(推荐将文件存放到COS存储,并开启「公有读私有写」权限。
document_url = "https://xxx.cos.xxx/sample.doc"
然后,在终端运行该文件:
python data_input_inference.py
2.2.本地上传(可选)
(1)在终端运行data_input_inference_streamlit.py文件,在终端执行如下命令:
streamlit run data_input_inference_streamlit.py
(2)将要上传的文档拖拽进去,或者点击「Browse files」按钮,选择文件上传,效果如下:
3.在线问答
这步主要是运行search_inference.py文件,在终端执行如下命令:
streamlit run search_inference.py
效果如下,在输入框中提出相关问题,系统会进行混合搜索-重排序-组装Prompt-大模型回答一系列动作,整个流程中间内容可见,最后生成回答内容。
三、核心代码解析
1.数据入库
对应data_input_inference.py和data_input_inference_streamlit.py文件,主要讲述一个非结构化文档从解析、切片、到Embedding后写入ES索引的过程,具体如下:
1.1.文档解析/切片
企业大部分数据都是以文档形式存在的非结构化数据,如何让文档数据变得可读,是实现RAG的第一步。我们通过调用智能搜索开发提供的文档解析/切片服务对文档数据先进行文档识别转MD格式,然后进行语义切片预处理。
常量MAX_CHUNK_SIZE即为文本切片单条文本最大字符数,不小于1000,可以根据你的具体业务,确定合适的最大字符数。在Embedding过程中,选定的模型Tokens限制应大于你设定的最大字符数,否则会出现截断的可能。
常量DOCUMENT_CHUNK_MODEL_NAME即为选择的文档解析切片模型,“doc-tree-chunk”包含了文档解析和切片部分,这里可以直接用。
MAX_CHUNK_SIZE = 2000 # 文本切片单条文本最大字符个数(根据具体选用的Embedding模型tokens设定,最小值不小于1000)
DOCUMENT_CHUNK_MODEL_NAME = "doc-tree-chunk"
# 调用ChunkDocumentAsync接口创建文档切片任务
params = {
"Document": {
"FileType": file_type,
"FileUrl": file_url,
"FileName": os.path.basename(urlparse(file_url).path)
},
"Config": {
"MaxChunkSize": MAX_CHUNK_SIZE # 可根据需要调整切片大小
},
"ModelName": DOCUMENT_CHUNK_MODEL_NAME
}
response = retry_request(client.call_json, "ChunkDocumentAsync", params)
task_id = response['Response']['TaskId']
logger.info(f"Created document chunking task with ID: {task_id}")
因为文档解析/切片是个异步任务,所以需要通过获取切片结果。
解压获取到的结果,切片好的数据存放在xxx.jsonl文件内,对应page_content即为切片数据。
try:
with zipfile.ZipFile(io.BytesIO(response.content)) as zip_file:
# 检查zip中是否有.jsonl文件
jsonl_files = [f for f in zip_file.namelist() if f.endswith('.jsonl')]
if not jsonl_files:
error_msg = "No .jsonl file found in the zip archive"
logger.error(error_msg)
raise Exception(error_msg)
# 处理每个jsonl文件
for file_name in jsonl_files:
with zip_file.open(file_name) as f:
# 逐行读取jsonl文件
for line in f:
try:
data = json.loads(line.decode('utf-8'))
if 'page_content' in data:
doc_list.append(data['page_content'])
except json.JSONDecodeError as e:
logger.warning(f"Failed to parse JSON line: {str(e)}")
continue
1.2.切片Embedding并写入ES
(1)创建Embedding服务,如:model_bge_base_zh-v1.5
INFERENCE_MODEL_ID = "model_bge_base_zh-v1.5"
es_client.perform_request(
'PUT',
f'/_inference/text_embedding/{INFERENCE_MODEL_ID}',
body=inference_config
)
(2)创建基于原子服务模型的pipeline,如:model_bge_base_zh-v1.5_embeddings_pipeline的pipeline
PIPELINE_NAME = "model_bge_base_zh-v1.5_embeddings_pipeline"
pipeline_config = {
"processors": [
{
"inference": {
"model_id": INFERENCE_MODEL_ID,
"input_output": {
"input_field": "content",
"output_field": "content_embedding"
}
}
}
]
}
es_client.ingest.put_pipeline(
id=PIPELINE_NAME,
body=pipeline_config
)
(3)文本&向量数据写入
创建向量索引,定义索引Schema,注意向量维度需与上述Embedding模型维度保持一致。
通过 Bulk API 批量写入数据。原始文档信息和向量化之后的数据均进行存储,方便ES混合搜索,提高召回率。
from elasticsearch.helpers import bulk
actions = [
{
"_index": INDEX_NAME,
"_source": {
"content": content, # 原始文本内容,根据定义的pipeline,默认向量化后写入pipeline中定义的向量字段
}
}
for index, content in enumerate(chunk_list)
]
success, failed = bulk(
es_client,
actions,
pipeline=PIPELINE_NAME
)
2.在线问答
对应search_inference.py文件,主要讲述用户的一个Query语句进来后,经过向量化,从ES中进行混合检索召回TopK,然后经过Rerank再精排过滤后,结合Prompt工程喂给大模型进行总结回答,具体如下:
2.1.Query 向量化
对Query语句进行向量化,具体调用方式本质上是与切片向量化一致,不再赘述。
2.2.文本&向量混合检索
指定向量字段,对Query向量进行向量相似度检索,获取前k条向量数据,同时进行全文检索,在最后将两者进行融合排序。
# 使用8.16集群的混合搜索语法(并行召回:返回 query 和 knn 召回结果的并集)
mix_query = {
"query": {"match": {"content": query}},
"knn": [{
"field": "content_embedding",
"query_vector_builder": {
"text_embedding": {
"model_id": INFERENCE_MODEL_ID,
"model_text": query
}
},
"k": 5,
"num_candidates": 100
}],
"size": 5
}
2.3.召回结果重排序
调用智能搜索开发提供的Rerank模型进行重排序。
TopN即为排序返回的top文档数量, 如果没有指定则返回全部候选doc,如果指定的TopN值大于输入的候选doc数量,返回全部doc。
RERANK_MODEL_NAME = "bge-reranker-large"
json_object = {
"ModelName": RERANK_MODEL_NAME,
"Query": query,
"ReturnDocuments": True,
"Documents": docs,
"TopN":5 # 排序返回的top文档数量, 如果没有指定则返回全部候选doc,如果指定的TopN值大于输入的候选doc数量,返回全部doc
}
2.4.组装Prompt
设置自己的Prompt模板,注意预留user_query和documents_str占位符,分别代表问题与检索到最相关的切片上下文。
PROMPT_TEMPLATE = """
# 任务说明
你是一位专业的AI助手,需要基于提供的参考文档准确回答用户问题。请严格遵循以下要求:
# 用户问题
{user_query}
# 参考文档(已按相关性排序)
{documents_str}
# 回答要求
1. 必须基于上述参考文档内容生成答案,不得编造文档中不存在的信息
2. 回答结构应包含:
- 简明扼要的核心答案(1-2句话)
- 详细解释或分点说明(3-5点,视问题复杂度而定)
- 可选的补充信息或建议(如适用)
3. 根据问题类型采用合适的回答方式:
- 对于事实性问题:提供准确数据或事实
- 对于观点性问题:呈现不同角度的分析
- 对于操作性问题:给出步骤清晰的指导
- 对于比较性问题:进行客观对比分析
4. 若文档内容与问题无关或信息不足,请回复:
"根据现有资料,我暂时无法提供令人满意的答案。建议您补充更多相关信息或重新提问。"
# 特别注意
1. 保持回答专业、准确且流畅
2. 如文档中有数据或研究结果,请优先引用
4. 对于专业术语,必要时提供简单解释
5. 避免使用"根据文档"等冗余表述,直接整合信息到回答中
"""
2.5.调用DeepSeek回答
调用deepseek-v3大模型进行总结回答,当然你可以切换不同hunyuan以及deepseek大模型进行体验。如果期望返回值是流式响应时,则设置Stream为True。
# 设置deepseek-v3模型(这里可以设置为你想要的模型,参考:https://cloud.tencent.com/document/api/845/117810)
LLM_MODEL_NAME = "deepseek-v3"
# 调用大模型回答,content即为build_llm_prompt返回组装后的prompt
def generate_llm_response(content: str) -> Dict[str, Any]:
"""
调用大模型生成回答
Args:
content: 要发送给大模型的prompt内容
Returns:
大模型的响应结果
Raises:
Exception: 如果调用大模型过程中出现错误
"""
if not content.strip():
raise ValueError("空内容无法生成回答")
json_object = {
"Messages": [{"Role": "user", "Content": content}],
"ModelName": LLM_MODEL_NAME,
"Stream": False # 可以设置成True,流式输出
}
try:
response = tencent_cloud_client.call_json("ChatCompletions", json_object)
return response
except Exception as e:
logger.error(f"大模型调用失败: {e}")
raise
四、总结
腾讯云ES凭借其在传统PB级日志和海量搜索场景中积累的丰富经验,通过深度重构底层系统,成功地将多年的性能优化、索引构建和运营管理经验应用于RAG领域,并积极探索向量召回与传统搜索技术的融合之道,旨在充分发挥两者的优势,为用户提供更加精准、高效的搜索体验。未来,腾讯云ES将持续深耕智能检索领域,在成本、性能、稳定性等方面持续提升,帮助客户降本增效的同时实现业务价值持续增长。