ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Onyx(Danswer)Prompt 缓存框架:多 LLM 提供商的提示词缓存统一接口实现解析

Onyx(Danswer)Prompt 缓存框架:多 LLM 提供商的提示词缓存统一接口实现解析 Onyx(Danswer)Prompt 缓存框架:多 LLM 提供商的提示词缓存统一接口实现解析【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer本篇技术指南围绕 Onyx(原 Danswer)后端backend/onyx/llm/下的 Prompt 缓存框架展开,讲解它如何通过一个统一入口process_with_prompt_cache()抹平 OpenAI 隐式缓存、Anthropic 与 Vertex AI 显式缓存的差异。读完本文,你能掌握缓存前缀/后缀拆分、continuation合并语义、提供商适配器的行为边界,以及该框架在聊天主链路与上下文 RAG 索引流水线中的真实落地方式,并能在自己的项目中复用这套前缀可缓存、后缀动态的成本优化模式。一、为什么需要 Prompt 缓存框架Onyx 是一个兼容所有 LLM的开源 AI 平台,不同提供商对提示词缓存(provider-side prompt token caching)的支持方式完全不同:OpenAI:隐式缓存,无需任何特殊参数,提供商自动缓存超过约 1024 token 的前缀,缓存生命周期最长约 1 小时,命中后缓存 token 有 50% 折扣;Anthropic:显式缓存,调用方必须在消息上附加cache_control{type: ephemeral}参数才能启用,默认 5 分钟生命周期,单请求最多支持 4 个缓存断点;Vertex AI(Gemini):支持显式缓存,cache_control需要挂在 content block 级别而非消息级别,5 分钟生命周期。如果每个调用点都自行处理这些差异,业务代码会迅速膨胀且难以维护。Onyx 的 Prompt 缓存框架就是为解决这个问题而生:它提供一个统一接口,把哪些消息是可缓存的静态前缀、哪些是动态后缀这一抽象交给业务层,把如何按提供商规则改写消息交给适配器层。框架目录位于 backend/onyx/llm/prompt_cache/,核心模块如下:文件职责processor.py主入口process_with_prompt_cache()cache_manager.py缓存元数据的存取与缓存键哈希生成models.pyPydantic 缓存元数据模型CacheMetadataproviders/提供商适配器(base/openai/anthropic/vertex/noop/factory)utils.py前缀/后缀合并与消息再验证等共享工具函数二、核心概念:cacheable_prefix 与 suffix整个框架的心智模型是把一次 LLM 请求拆成两部分:cacheable_prefix(可缓存前缀):在多次请求之间保持不变的静态内容,如系统提示词、静态上下文、指令模板;suffix(动态后缀):每次请求都变化的内容,如用户问题、检索结果。此外还有一个continuation标志:为True时,suffix 会被追加拼接到前缀最后一条消息的 content 中,而不是作为独立消息存在。这一点在字符串前缀 动态片段的场景中非常关键(见后文 Contextual RAG 实例)。基本用法(Pydantic 消息输入)cacheable_prefix和suffix都支持str或Sequence[ChatCompletionMessage]两种输入形态。以 Pydantic 消息模型为例:from onyx.llm.prompt_cache import process_with_prompt_cache from onyx.llm.models import SystemMessage, UserMessage # llm 为任意 Onyx LLM 实例,使用其 config 属性(而非实例本身) # 定义可缓存前缀(静态上下文) cacheable_prefix [ SystemMessage(rolesystem, contentYou are a helpful assistant.), UserMessage(roleuser, contentContext: ...), # 静态上下文 ] # 定义后缀(动态用户输入) suffix [UserMessage(roleuser, contentWhat is the weather?)] # 传入 llm_config 处理 processed_prompt, cache_metadata process_with_prompt_cache( llm_configllm.config, cacheable_prefixcacheable_prefix, suffixsuffix, continuationFalse, ) # 用处理后的 prompt 调用 LLM response llm.invoke(processed_prompt)字符串输入前后缀也可以直接是字符串,适合单轮短请求:cacheable_prefix You are a helpful assistant. Context: ... suffix What is the weather? processed_prompt, cache_metadata process_with_prompt_cache( llm_configllm.config, cacheable_prefixcacheable_prefix, suffixsuffix, continuationFalse, )continuation 标志continuationFalse(默认)时,前后缀各自成消息,结果为[system_msg, prefix_user_msg, suffix_user_msg];continuationTrue时,suffix 拼入前缀最后一条消息,结果为[system_msg, prefix_user_msg suffix_user_msg]:processed_prompt, _ process_with_prompt_cache( llm_configllm.config, cacheable_prefixcacheable_prefix, suffixsuffix, continuationTrue, # 把 suffix 合并进最后一条前缀消息 )注意:即使continuationTrue,若cacheable_prefix是字符串,它仍保留在独立的内容块中。这一行为由 utils.py 中的combine_messages_with_continuation()实现——它对字符串 content 做直接拼接,对列表(多模态)content 则做块级合并;合并后的消息会经revalidate_message_from_original()用原 Pydantic 类重新验证,以保持按 role 判别的 union 结构不破坏。三、主处理器:process_with_prompt_cache 的完整决策流processor.py 的签名与语义值得逐条拆解:def process_with_prompt_cache( llm_config: LLMConfig, cacheable_prefix: LanguageModelInput | None, suffix: LanguageModelInput, continuation: bool False, with_metadata: bool True, ) - tuple[LanguageModelInput, CacheMetadata | None]:其执行路径(见 processor.py):全局开关检查:若ENABLE_PROMPT_CACHING为false,直接走NoOpPromptCacheProvider仅做前后缀合并,不附加任何缓存参数;空前缀短路:cacheable_prefix is None时原样返回suffix,不做任何缓存尝试;适配器路由:调用get_provider_adapter(llm_config)根据提供商选择适配器;能力检查:若provider_adapter.supports_caching()为False,同样退化为 no-op 合并;消息改写:调用provider_adapter.prepare_messages_for_caching(),在此阶段按提供商规则注入cache_control等参数;元数据生成:当with_metadataTrue时,用generate_cache_key_hash()对整个前缀做 SHA256 哈希,并构建CacheMetadata返回;源码注释特别说明了with_metadataFalse的用途——跳过对大型 Agent 提示词前缀的整段 SHA256 哈希,节省可观的 CPU;异常兜底:任何环节抛异常都只记录 warning 并回退到 no-op 合并,绝不阻断 LLM 调用。返回的第二个元素cache_metadata当前阶段主要用于缓存使用情况的追踪,隐式缓存场景下提供商自行处理缓存,后续显式缓存增强将在此字段上扩展。四、提供商适配层:工厂路由与行为差异工厂路由逻辑factory.py 的get_provider_adapter()比文档概述覆盖了更多真实场景:openai→OpenAIPromptCacheProvider;anthropic,以及Bedrock 上模型名含anthropic.前缀的模型 →AnthropicPromptCacheProvider;vertex_ai→VertexAIPromptCacheProvider;openrouter网关:按模型名前缀路由,anthropic/前缀走 Anthropic 适配器、google/前缀走 Vertex 适配器、openai/前缀走 OpenAI 适配器,其余返回NoOpPromptCacheProvider。源码中注明复用 Vertex 适配器处理 OpenRouter 的 Google 模型是安全的,因为当前 Vertex 适配器只做隐式缓存、不改写消息;其他提供商一律返回NoOpPromptCacheProvider。适配器接口所有适配器继承抽象基类 PromptCacheProvider,实现四个方法:方法说明supports_caching()是否支持缓存prepare_messages_for_caching()按提供商规则改写前后缀消息extract_cache_metadata()从 API 响应中提取缓存元数据get_cache_ttl_seconds()返回该提供商的缓存 TTL(秒)各适配器一览:文件类说明base.pyPromptCacheProvider抽象基类openai.pyOpenAIPromptCacheProvider隐式缓存,消息零改写,get_cache_ttl_seconds()返回 3600anthropic.pyAnthropicPromptCacheProvider显式缓存,给可缓存前缀的最后一条消息加cache_control,TTL 300 秒vertex.pyVertexAIPromptCacheProvider针对 content block 的cache_control处理,TTL 300 秒noop.pyNoOpPromptCacheProvider不支持缓存提供商的兜底,只做前后缀合并,TTL 返回 0OpenAI:隐式缓存,零改写OpenAIPromptCacheProvider.prepare_messages_for_caching()直接以transform_cacheableNone调用共享工具,即规范化 合并之外的任何事都不做——OpenAI 会自动缓存超过 1024 token 的前缀,框架只需保证前缀在字节层面稳定即可。Anthropic:显式缓存,消息级 cache_controlanthropic.py 的_add_anthropic_cache_control()把cache_control挂在可缓存前缀的最后一条消息上:last_message_dict dict(messages[-1]) last_message_dict[cache_control] {type: ephemeral} last_message revalidate_message_from_original( originalmessages[-1], mutatedlast_message_dict ) return list(messages[:-1]) [last_message]改写后的字典会经revalidate_message_from_original()重新通过原消息的 Pydantic 类验证,保证SystemMessage/UserMessage等按 role 判别的 union 类型不被破坏。Vertex AI:block 级缓存与显式缓存的延后Vertex/Gemini 要求cache_control挂在 content block 内部而非消息级别。vertex.py 中实现了_add_vertex_cache_control():字符串 content 会被转换为带cache_control的数组格式,数组 content 则在最后一个 block 上附加参数。值得注意的是,从当前源码结构看,主流程prepare_messages_for_caching()仍以transform_cacheableNone走隐式路径,block 级改写函数与完整显式上下文缓存(含 block 编号管理)被源码 TODO 明确标记为后续 PR 的演进方向,这与文档Future Enhancements一节的描述一致。五、真实落地场景:聊天链路与 Contextual RAG框架不是孤立存在的,仓库中至少有两处生产调用链:聊天主链路按消息切分前缀在 chat/llm_step.py 中,多轮聊天请求组装完全部消息后,会依据ChatMessageSimple的should_cache标记确定切分点last_cacheable_msg_idx,然后把历史中可缓存的消息作为前缀、其后消息作为后缀交给处理器:if last_cacheable_msg_idx ! -1: processed_messages, _ process_with_prompt_cache( llm_configllm_config, cacheable_prefixmessages[: last_cacheable_msg_idx 1], suffixmessages[last_cacheable_msg_idx 1 :], continuationFalse, )这正是把动态内容放在 suffix最佳实践的直接体现:历史上下文稳定可缓存,最新一轮的动态消息永远落在后缀里。Contextual RAG 流水线:字符串前缀 continuationindexing/indexing_pipeline.py 为每个文档分块生成上下文时,文档级 prompt 是静态的、分块内容是动态的,是continuationTrue的典型场景:processed_prompt, _ process_with_prompt_cache( llm_configllm.config, cacheable_prefixUserMessage(contentcontext_prompt1), # 文档上下文(静态) suffixUserMessage(contentcontext_prompt2), # 分块内容(动态) continuationTrue, # 把分块追加到文档上下文中 )由于前缀是字符串消息,合并结果是文档上下文 分块拼接后的单一 UserMessage,既满足了 Gemini 等对同角色消息合并的要求,又让昂贵的文档前缀在多分块间被提供商复用。六、缓存键、元数据与多租户隔离cache_manager.py 负责缓存元数据的持久化与寻址:缓存键哈希:generate_cache_key_hash()将消息列表递归 JSON 化(sort_keysTrue、紧凑分隔符,保证序列化确定性),连同provider、model、tenant_id一起做 SHA256,只纳入 content/role/顺序,排除时间戳等动态字段;存储键格式:prompt_cache:{tenant_id}:{provider}:{model_name}:{cache_key_hash},租户 ID 缺省时从shared_configs.contextvars的上下文变量取当前租户,天然实现多租户隔离;存储介质:CacheManager基于PgRedisKVStore,即 Redis 缓存 PostgreSQL 持久化的双层 KV 存储;TTL 策略:模块常量CACHE_TTL_MULTIPLIER取环境配置PROMPT_CACHE_REDIS_TTL_MULTIPLIER(默认 1.2),即元数据保留期略长于提供商实际缓存 TTL,容忍时钟偏差、避免元数据过早失效;元数据模型:models.py 的CacheMetadata包含cache_key、provider、model_name、tenant_id、created_at、last_accessed,源码注释还预留了vertex_block_numbers与anthropic_cache_id两个字段,等待显式缓存支持落地;尽力而为的容错:store_cache_metadata()/retrieve_cache_metadata()/delete_cache_metadata()三个方法内部全部 try/except,失败只记日志,缓存存储异常永远不会影响 LLM 请求本身。七、配置项说明框架通过两个环境变量控制(定义于 backend/onyx/configs/model_configs.py):环境变量默认值说明ENABLE_PROMPT_CACHINGtrue全局开关。解析规则为非false字面量均视为开启。关闭后所有提供商走 no-op 合并PROMPT_CACHE_REDIS_TTL_MULTIPLIER1.2缓存元数据保留期相对提供商 TTL 的放大倍数关闭缓存:export ENABLE_PROMPT_CACHINGfalse # 禁用缓存八、最佳实践与错误处理最佳实践静态内容进前缀:系统提示词、静态上下文、不变指令放入cacheable_prefix;动态内容进后缀:用户查询、检索结果等每次变化的内容放入suffix;监控缓存效果:关注日志中的缓存命中/未命中情况(提供商 usage 字段中的cached_tokens等指标),据此调整切分策略;按提供商特性选择策略:隐式缓存提供商靠前缀字节稳定即可,显式缓存提供商则依赖框架自动注入的参数。错误处理:全程 best-effort框架的设计底线是缓存失败绝不损害请求,具体表现为四条回退路径:缓存元数据查找失败:记录日志,继续无缓存请求;提供商适配器失败:回退到 no-op 适配器;缓存存储失败:记录日志,继续执行(缓存本身即 best-effort);非法缓存元数据:清理后继续,不携带缓存。主处理器第 5 步的try/except统一兜底(见 processor.py)正是这一承诺的代码体现。九、测试验证与后续演进测试用例backend/tests/external_dependency_unit/llm/test_prompt_caching.py:直接调用真实 LLM 提供商的集成测试,通过 litellm 的completion_cost()与 usage 中的cache_creation_input_tokens/cached_tokens字段验证缓存确实降低了 token 成本;backend/tests/unit/onyx/llm/prompt_cache/test_processor.py:针对处理器的单元测试。计划中的增强按文档规划,后续演进方向包括:Vertex AI 的完整显式缓存(block 编号跟踪与管理)、缓存分析(命中率与成本节省指标)、更精细的缓存键生成与失效策略,以及跨实例的分布式共享缓存。这些方向与源码中CacheMetadata的预留字段、工厂路由中的 TODO 注释相互印证。十、小结Onyx 的 Prompt 缓存框架用前缀/后缀切分 提供商适配器两个抽象,把各 LLM 提供商差异显著的提示词缓存机制收敛为一个统一入口process_with_prompt_cache():业务代码只需声明哪部分是稳定的、哪部分是动态的,框架负责按提供商规则注入cache_control、生成租户隔离的缓存键、并在任何环节失败时静默回退。这套模式对任何需要同时支持多家 LLM 的 AI 平台都是可直接参考的成本优化设计。【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表