与大型语言模型(LLM)的交互常常是LangChain应用中延迟和运营成本的最主要原因。因此,在准备生产部署时,优化这些交互是首要关注点。这里详细介绍了几种使LLM调用更高效的实用方法。缓存LLM响应许多LLM应用会遇到重复的请求或子请求。为相同的输入多次执行相同的LLM调用会浪费时间和金钱(API成本)。缓存提供了一种直接的机制来存储和重用之前的LLM响应。LangChain提供了内置的缓存机制。最简单的形式是内存缓存,适用于开发或单进程应用:import langchain from langchain.cache import InMemoryCache langchain.llm_cache = InMemoryCache() # 现在,此进程中任何后续的相同LLM调用(相同模型、参数、提示) # 都将命中缓存。 # llm.invoke("Why is the sky blue?") # 第一次调用 - 访问LLM # llm.invoke("Why is the sky blue?") # 第二次调用 - 命中缓存对于生产环境,特别是涉及多个服务器实例或需要持久化的场景,外部缓存是必需的。可选方案包括数据库支持的缓存(例如用于SQL数据库的SQLAlchemyCache或RedisCache)或专用向量存储缓存(GPTCache,尽管需要单独设置)。# 使用Redis的示例(需要Redis服务器和redis-py包) # from langchain.cache import RedisCache # import redis # client = redis.Redis(decode_responses=True) # langchain.llm_cache = RedisCache(client)缓存失效: 缓存的一个重要难题是判断缓存数据何时过期。如果LLM可能访问的底层知识发生变化,或者特定提示的预期行为发生演变,缓存就需要更新。策略包括:生存时间(TTL): 在设定的持续时间后使缓存条目过期。简单但可能导致在过期前提供过时数据。事件驱动失效: 当相关数据源更新时清除特定的缓存条目(实现起来更复杂)。语义缓存: 不基于精确的提示匹配,而是根据提示的语义相似性进行缓存。这可以提高缓存命中率,但如果未仔细调整,会增加复杂性并可能导致不正确的匹配。需要对提示进行嵌入并比较向量。实施适当的缓存策略很大程度上取决于应用对潜在过时数据的容忍度,以及对低延迟和成本降低的需求。减少令牌使用LLM API的成本通常根据输入和输出令牌的数量计算。延迟也常与处理的令牌数量相关。因此,最小化令牌数量是直接的优化手段。1. 提示工程:简洁性: 重新措辞提示,使其更短,同时保留必要的上下文和指令。删除冗余的短语或示例。指令调整: 对于支持此功能的模型,微调特定的指令格式有时可以用主提示中更少的显式指令获得所需输出。小样本学习优化: 分析是否可以使用更少的示例(样本)达到所需性能,从而减少提示令牌数量。2. 上下文管理:摘要: 不直接传递整个长对话历史或文档,而是使用另一个(可能更便宜/更快)的LLM调用来总结相关部分,然后再进行主要的生成调用(如内存相关章节3所述)。选择性上下文: 实现逻辑,只在提示中包含历史记录或检索到的文档中最相关部分(例如,根据新近度或与当前查询的语义相似性)。章节4中讨论的RAG方法也适用于此。3. 输出限制:明确指示模型简明扼要或限制其响应长度(例如,“在100字以内回应”)。尽管并非总能完美遵守,但这可以显著减少输出令牌。在适用情况下使用结构化输出解析(章节1)。请求JSON或特定字段通常会产生比自由格式文本生成更简洁、可预测的输出。模型选择并非所有任务都需要最强大(且通常最昂贵和最慢)的LLM。请考虑模型能力、速度和成本之间的权衡。分层方法: 对于复杂推理或生成任务,使用高能力模型(如GPT-4);但对于摘要、分类、数据提取或基本问答等更简单的任务,则使用更小、更快、更便宜的模型(如GPT-3.5-Turbo、Claude Haiku或Llama 3 8B、Mistral 7B等开源替代品)。路由: 在您的链或代理中实现逻辑,根据复杂性、预估成本或用户要求将请求路由到不同的模型。微调: 在特定数据集上微调小型模型有时可以针对狭窄任务达到与大型通用模型可比的性能,通常只需一小部分成本和延迟。下图展示了比较:{"layout": {"title": "模型成本与延迟权衡", "xaxis": {"title": "相对延迟(越低越快)"}, "yaxis": {"title": "每百万令牌相对成本(越低越便宜)"}, "showlegend": true}, "data": [{"type": "scatter", "mode": "markers+text", "x": [1.0, 0.3, 0.2, 0.15], "y": [1.0, 0.1, 0.05, 0.02], "marker": {"size": [20, 14, 12, 10], "color": ["#fa5252", "#ff922b", "#40c057", "#228be6"]}, "text": ["大型模型(GPT-4类)", "中型模型(GPT-3.5类)", "小型微调模型", "微型模型(开源)"], "textposition": "top right", "name": "模型"}]}不同类别语言模型的相对成本和延迟。实际值根据提供商和具体模型版本差异很大。并行化和异步执行如果您的LangChain应用涉及多个独立的LLM调用(例如,处理列表中的项、查询主题的多个方面),并发执行这些调用可以显著减少总的实际耗时。LangChain的表达式语言(LCEL)内置支持异步操作和并行执行,如章节1所述。使用ainvoke、abatch和astream等方法,结合Python的asyncio库,可以使LLM API调用等I/O密集型操作并发运行。import asyncio from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnableParallel # 假设 llm 是一个已初始化的 ChatOpenAI 实例 prompt = ChatPromptTemplate.from_template("Tell me a joke about {topic}") chain = prompt | llm # 并发运行多个独立调用的示例 topics = ["bears", "programmers", "cheese"] coroutines = [chain.ainvoke({"topic": t}) for t in topics] # 并发运行它们 results = await asyncio.gather(*coroutines) # results 将包含每个主题的响应对于更复杂的工作流程,LCEL的RunnableParallel允许在链中定义并行步骤:# 在链中使用 RunnableParallel 进行并行 LLM 调用的示例 joke_chain = ChatPromptTemplate.from_template("Tell me a joke about {topic}") | llm poem_chain = ChatPromptTemplate.from_template("Write a short poem about {topic}") | llm map_chain = RunnableParallel(joke=joke_chain, poem=poem_chain) # 这将并发执行 joke_chain 和 poem_chain result = await map_chain.ainvoke({"topic": "robots"}) # result 将是一个字典:{'joke': ..., 'poem': ...}使用异步执行对于构建响应式应用非常重要,这类应用每个用户请求会执行多个LLM交互。流式响应对于涉及长文本生成(如聊天机器人或内容创建工具)的应用,在显示任何内容之前等待整个LLM响应可能会导致糟糕的用户体验。流式传输允许应用在LLM生成输出时逐个令牌接收并显示。大多数LangChain LLM集成通过stream或astream方法支持流式传输。# 流式输出示例 async for chunk in llm.astream("Write a short story about a brave knight."): # 处理每个到达的块(例如,打印到控制台,发送到UI) print(chunk.content, end="", flush=True)尽管流式传输不会减少总处理时间或成本,但它显著改善了最终用户的感知延迟,使应用感觉更具响应性。实现流式传输通常需要改变应用前端处理传入数据的方式,但这是生产级对话式AI的标准实践。通过系统地应用这些方法,即缓存、减少令牌、选择合适的模型、并行化和流式传输,您可以大幅提高LangChain应用的性能和成本效益,使其适用于要求严格的生产工作负载。接下来的部分将探讨扩展其他组件,例如数据检索系统。