为了让应用程序运行更快或更省钱,了解其时间与资源主要花费在哪里非常重要。传统软件的常见瓶颈出现在数据库查询、复杂计算或I/O操作中。尽管LLM应用也有类似情况,但它们引入了两个主要的延迟和成本来源,这些来源通常比其他所有因素都更显著:对外部模型API的调用。一个典型的应用,尤其是使用检索增强生成(RAG)的应用,会遵循一个多阶段流程。有些阶段在本地运行,通常速度很快,而另一些则涉及对第三方服务的网络请求,这会带来显著的延迟和费用。digraph G { rankdir=TB; node [shape=box, style="rounded,filled", fontname="Arial", fontsize=10]; edge [fontname="Arial", fontsize=9]; subgraph cluster_prep { label="数据准备(离线/非频繁)"; style="rounded,dashed"; color="#adb5bd"; bgcolor="#f8f9fa"; node [fillcolor="#e9ecef"]; Data [label="原始文档\n(PDF, 网页, TXT)"]; Chunk [label="文本分块"]; Embed [label="嵌入生成", fillcolor="#ffc9c9", style="rounded,filled,bold"]; Data -> Chunk [label="大文件"]; Chunk -> Embed [label="文本块"]; } subgraph cluster_query { label="查询时间(实时)"; style="rounded,dashed"; color="#adb5bd"; bgcolor="#f8f9fa"; node [fillcolor="#a5d8ff"]; Query [label="用户查询"]; Retrieve [label="语义搜索"]; Generate [label="LLM生成", fillcolor="#ffc9c9", style="rounded,filled,bold"]; Response [label="最终响应"]; Query -> Retrieve [label="查询嵌入"]; Embed -> Retrieve [label="文档嵌入\n(来自向量数据库)", style=dashed, constraint=false]; Retrieve -> Generate [label="检索到的上下文"]; Generate -> Response [label="生成的文本"]; } Embed [xlabel="API调用瓶颈 #1\n(成本与延迟)"]; Generate [xlabel="API调用瓶颈 #2\n(成本与延迟)"]; }一个典型的RAG应用工作流程。最主要的瓶颈通常出现在嵌入和生成阶段,这些阶段依赖外部API调用。让我们分析一下性能问题常见于何处。LLM生成调用最明显的瓶颈是对LLM的最终生成调用。当您的应用发送提示并等待响应时,几个因素会增加延迟:网络延迟: 您的请求发送到服务提供商服务器以及响应返回所需的时间。模型推理时间: LLM处理您的提示并生成输出令牌所需的时间。对于非常复杂的查询和长响应,这可能需要几秒到一分钟以上。排队: 在高峰期,您的请求在被模型处理前可能会排队。成本与使用量直接相关。如引言所述,成本 $C$ 是输入(提示)和输出(完成)令牌的函数:$$C = (P_{prompt} \times N_{prompt}) + (P_{completion} \times N_{completion})$$对于接收许多相同或相似查询的应用,这些成本会累积起来。例如,一个重复回答“你们的营业时间是什么?”的客户支持机器人,每次都会发起一个新的、昂贵的API调用。嵌入API调用第二个主要瓶颈是嵌入生成。在RAG系统中,每个文档块都必须转换为向量嵌入,然后才能在向量数据库中进行索引。尽管这通常是一次性的“摄取”成本,但其金额可能很大。如果您有10,000个文档块,您必须向嵌入服务发起数千次API调用。这个过程可能既耗时又昂贵。此外,如果您的应用频繁处理新文档或实时为用户查询生成嵌入,这些调用会增加持续的运营成本和延迟。重复调用以嵌入相同的文本,例如常见的搜索词或文档标题,是对资源的低效使用。衡量应用中的瓶颈优化的第一步是衡量。识别瓶颈的一个简单方法是测量应用工作流程中每个主要阶段的时间。考虑一个模拟RAG查询过程的简化函数。通过为每个步骤添加计时逻辑,您可以精确找出时间主要花在哪里。import time def mock_llm_api_call(prompt): """模拟一个缓慢的LLM API调用。""" time.sleep(2.5) # 模拟2.5秒的延迟 return f"This is a generated response to: {prompt[:50]}..." def mock_embedding_api_call(text): """模拟一个较快但仍有显著耗时的嵌入API调用。""" time.sleep(0.1) # 模拟100毫秒的延迟 return [0.1] * 384 # 返回一个虚拟向量 def run_rag_query(query: str): """模拟完整的RAG查询并测量每一步的时间。""" print(f"\n正在处理查询:'{query}'") # 步骤1:生成查询嵌入 start_time = time.time() query_embedding = mock_embedding_api_call(query) embed_duration = time.time() - start_time print(f" 1. 嵌入生成:{embed_duration:.4f}秒") # 步骤2:检索文档(模拟) start_time = time.time() time.sleep(0.05) # 模拟本地向量搜索 retrieved_context = "这里检索到了一些相关上下文。" retrieve_duration = time.time() - start_time print(f" 2. 文档检索:{retrieve_duration:.4f}秒") # 步骤3:调用LLM进行最终生成 start_time = time.time() prompt = f"Context: {retrieved_context}\n\nQuestion: {query}" final_response = mock_llm_api_call(prompt) generate_duration = time.time() - start_time print(f" 3. LLM生成:{generate_duration:.4f}秒") total_duration = embed_duration + retrieve_duration + generate_duration print(f" -------------------------------------") print(f" 总耗时:{total_duration:.4f}秒") # 运行模拟 run_rag_query("Kerb工具包是什么?")运行此代码会产生类似如下的输出:正在处理查询:'Kerb工具包是什么?' 1. 嵌入生成:0.1002秒 2. 文档检索:0.0501秒 3. LLM生成:2.5003秒 ------------------------------------- 总耗时:2.6506秒结果很明确:LLM生成调用占总请求时间的94%以上。嵌入调用虽然快得多,但仍然比本地检索步骤慢两倍。这个简单的分析立刻告诉我们,优化API调用将带来最大的性能提升。确定了这些瓶颈后,我们现在可以研究解决方案了。以下章节将向您展示如何使用 cache 模块实现缓存策略,通过避免对LLM响应和嵌入的重复API调用,从而大幅减少延迟和成本。