Langfuse Prompt 缓存策略深度解析:基于 Redis 的版本化 Prompt 缓存与失效机制

发布时间:2026/9/10 9:07:01

Langfuse Prompt 缓存策略深度解析:基于 Redis 的版本化 Prompt 缓存与失效机制 Langfuse Prompt 缓存策略深度解析基于 Redis 的版本化 Prompt 缓存与失效机制【免费下载链接】langfuse Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuseLangfuse 作为开源 AI 工程平台其 Prompt 管理功能支持多版本、多标签label与跨 Prompt 依赖解析。为了在保证数据强一致的前提下降低 Postgres 读压力Langfuse 在PromptService中实现了一套基于 Redis 的缓存机制读取走缓存、写入直接失效、以epoch 命名空间轮换代替逐条删除。本文以仓库中的 prompts 模块缓存策略文档 为骨架结合PromptService源码与createPrompt调用链完整还原该缓存的设计原理、读写路径、失效策略与可观测性配置。一、缓存机制概览为什么需要为 Prompt 设计缓存Prompt 是 Langfuse 中被高频读取的实体LLM 应用每次调用都需要按projectId promptName version/label拉取 Prompt 内容同时 Prompt 又支持多版本、多标签且一个 Prompt 可以通过langfusePrompt:namexxx|versionxxx之类的依赖标签引用其他 Prompt即跨 Prompt 依赖解析。这意味着一次读 Prompt最终可能要经过依赖图的递归解析代价远高于一次简单的 Postgres 单行查询。缓存策略正是为这一场景设计的用 Redis 承载解析后的 Prompt 结果让绝大多数读取命中缓存而写入侧则采用绝不更新缓存条目而是整体失效的策略保证缓存与数据库永远一致。缓存实现核心位于 packages/shared/src/server/services/PromptService/index.ts 中的PromptService类并在 web/src/features/prompts/server/actions/createPrompt.ts 的createPrompt函数中投入使用。本文所引源码行号均以此两文件为准。二、缓存结构Redis Key 的组成与粒度根据原文档缓存使用 Redis 管理Key 形如prompt:project-id:prompt-name:prompt-version ?? label这意味着同一个 Prompt 名称在 Redis 中会有多个 Key——每个version或label对应一个独立条目一个 Prompt 挂多个 label 时会在缓存中重复出现多次——因为每个 label 都是独立的可寻址入口。2.1 源码中的实际 Key 生成逻辑在 PromptService 的 getCacheKey / getCacheKeyPrefix 实现 中实际的 Key 结构比文档示例多了一个 epoch 段prompt:${projectId}:${epoch}:${promptName}:${selector}其中epoch是项目级命名空间令牌详见下文第四节selector由getCacheKey中的这段逻辑决定源码// Numeric labels must not share a cache entry with the same prompt version. const selector typeof params.version number ? version:${params.version} : label:${params.label};注释明确指出数字形式的 label 必须与同名同版本的缓存条目区分开避免数字 label与版本号在 Key 上发生碰撞。这也印证了原文档中prompt-version ?? label二选一的设计version与label不会同时作为 Key 的一部分。2.2 PromptParams 的类型约束PromptParamstypes.ts通过 TypeScript 联合类型强制保证这一点export type PromptParams { projectId: string; promptName: string; resolve?: boolean; } ( | { version: number; label: undefined } | { version: null | undefined; label: string } );也就是说一个读取请求要么按版本号number寻址要么按标签string寻址二者不可兼得从类型层面杜绝了歧义 Key 的产生。三、写入路径创建与更新时绝不更新缓存原文档给出了一条非常关键的设计原则We never update prompts in the cache. Instead, we remove all cache entries for a prompt name of a project when a prompt is updated.即缓存条目从不被原地更新而是创建/更新 Prompt 时把该项目下该 Prompt 名称的全部缓存条目整体失效。文档同时描述了写入侧的经典流程在 Redis 中获取锁失效缓存在 Postgres 中执行操作释放锁。3.1 createPrompt 中的实际调用链在 createPrompt.ts 中PromptService的接入方式如下const promptService new PromptService(prisma, redis); const promptDependencies parsePromptDependencyTags(prompt); // 1. 写库前先校验依赖图循环依赖、嵌套深度等 await promptService.buildAndResolvePromptGraph({ projectId, parentPrompt: { id: newPromptId, prompt, version: ..., name, labels }, dependencies: promptDependencies, }); // 2. 所有 Postgres 写操作打包进一个事务 transactionResult (await prisma.$transaction(create)) as [...]; // 3. 事务提交成功后失效缓存 await promptService.invalidateCache({ projectId });对照原文档描述的锁 → 失效 → 写库 → 释放锁四步可以推断当前实现的对应关系为锁的角色由 Postgres 事务承担prisma.$transaction保证创建 Prompt、写依赖关系、迁移标签等操作原子提交事务内部的其他读取不会看到中间态失效缓存发生在事务提交之后invalidateCache即数据库先成为新的事实来源再让缓存失效保证先更新 DB、后失效缓存的顺序并发写冲突由数据库唯一约束兜底createPrompt中通过isPromptVersionConflict捕获 PrismaP2002唯一约束冲突project_id、name、version三列抛出LangfuseConflictError(A prompt version was created concurrently. Please retry.)源码避免并发创建导致版本错乱。此外invalidateCache失败不会回滚已提交的事务——源码中将其包在 try/catch 里只记录错误日志源码注释明确说明side-effect failures must not report the persisted prompt as failed。这是一个典型的缓存失效是尽力而为的设计取舍缓存最坏情况是多存一段时间旧值受 TTL 约束而不会造成数据丢失。同样的失效调用也出现在duplicatePrompt与duplicateFolder中源码 与 L677-L679。3.2 失效的粒度按项目还是按 Prompt从invalidateCache的签名看它只接收projectId源码public async invalidateCache( params: PickPromptParams, projectId, ): Promisevoid { if (!this.cacheEnabled) return; // Rotate the epoch token to move all prompt reads/writes to a fresh namespace. // Old keys remain untouched and naturally expire via TTL. await this.redis?.set( this.getEpochKey(params), this.newEpochToken(), EX, this.epochTtlSeconds, ); }失效操作以项目为粒度。原因在 getEpochKey 的注释 中写得很清楚Important: epoch is project-scoped (not prompt-scoped) because resolved prompts can include transitive dependencies across multiple prompt names.也就是说一个 Prompt 的解析结果可能内含其他 Prompt 的内容依赖解析。修改任何一个被依赖的 Prompt都会影响所有依赖它的 Prompt 的缓存值因此必须对整个项目的缓存做命名空间级失效而不是只删某个 Prompt 名下的 Key。四、读取路径缓存优先 TTL 续期原文档对读取路径的描述是When reading prompts, we check whether a lock exists. If it does not, we proceed to read the prompt from the cache. Thereby, we reset the ttl of the cache entry to ensure it remains in the cache. If the lock exists, or the entry is not in Redis, we read the prompt from Postgres and store it in the cache.即读取时无锁且缓存命中 → 直接返回缓存并重置该条目的 TTL以保证其驻留有锁或缓存未命中 → 回源 Postgres并把结果写回缓存。4.1 getPrompt 的完整流程PromptService.getPrompt 的源码实现了这一缓存优先的读取模型public async getPrompt(params: PromptParams): PromisePromptResult | null { if (params.resolve false) { return this.getRawPrompt(params); // 不解析依赖直接走 DB } if (this.cacheEnabled) { const cachedPrompt await this.getCachedPrompt(params); this.incrementMetric( cachedPrompt ? PromptServiceMetrics.PromptCacheHit : PromptServiceMetrics.PromptCacheMiss, ); if (cachedPrompt) { return cachedPrompt; } } const dbPrompt await this.getDbPrompt(params); // findPrompt resolvePrompt if (this.cacheEnabled dbPrompt) { await this.cachePrompt({ ...params, prompt: dbPrompt }); } return dbPrompt; }要点解读命中即返回命中缓存后不再触碰 Postgres直接返回解析完成的PromptResult未命中则回源并回填getDbPrompt内部先findPrompt按version或labels has label查询见 findPrompt 实现再resolvePrompt递归解析依赖图随后cachePrompt以EX this.ttlSeconds写回 Redis源码resolve false时完全不缓存读取未解析的原始 Prompt 时走getRawPrompt直接查库且不经缓存层——因为未解析结果不含依赖图缓存收益低且避免与已解析结果混存。4.2 关于重置 TTL的说明需要客观指出的是原文档中读取命中时重置 TTL的描述与当前源码的getCachedPrompt实现存在细微差异——当前实现 仅执行this.redis?.get(key)并未显式调用touch/expire续期。可以推断 TTL 续期在当前版本中主要由每次写入都重新设置EX来承担即每次回源写回都会刷新 TTL。如果你的部署依赖热点条目长期驻留缓存应以当前仓库源码index.ts为准进行验证。五、失效策略深挖epoch 命名空间轮换这是PromptService缓存设计中最值得展开的实现细节。invalidateCache并没有去 Redis 里枚举并删除prompt:*前缀的 Key而是采用**轮换命名空间令牌epoch token**的策略源码private newEpochToken(): string { // 48 bits of entropy in a compact URL-safe string (8 chars). return randomBytes(6).toString(base64url); } private async getOrCreateEpoch(params): Promisestring | null { const epochKey this.getEpochKey(params); // prompt_cache_epoch:${projectId} const currentEpoch await this.redis?.get(epochKey); if (currentEpoch) return currentEpoch; const newEpoch this.newEpochToken(); await this.redis?.set(epochKey, newEpoch, EX, this.epochTtlSeconds, NX); // Return the winner value in case multiple requests initialize concurrently. return (await this.redis?.get(epochKey)) ?? newEpoch; }其工作原理可以概括为三步每个项目在 Redis 中持有一个prompt_cache_epoch:projectId键值为 8 字符的随机令牌48 bit 熵TTL 为 7 天epochTtlSeconds 7 * 24 * 60 * 60见 index.ts#L26-L28所有缓存 Key 都带有当前 epoch 令牌prompt:projectId:epoch:promptName:version|label:...失效缓存时仅更新 epoch 令牌本身set一个新随机值。此后所有新读写都落到新的命名空间旧 Key 无人引用依赖自身的 TTL 自然过期清除。这一设计的优势在于失效成本 O(1)不需要KEYS prompt:*扫描或批量删除避免了大项目下清空缓存的高开销与 Redis 阻塞风险并发安全getOrCreateEpoch使用SET ... NX保证多实例同时初始化时只有一个令牌生效注释明确说明Return the winner value in case multiple requests initialize concurrently自然收敛旧命名空间的数据在 TTL 到期后被 Redis 自动回收无需额外清理任务。六、配置项缓存开关与 TTL缓存行为由两个环境变量控制定义在 packages/shared/src/env.ts环境变量类型默认值说明LANGFUSE_CACHE_PROMPT_ENABLEDtrue/falsetrue是否启用 Prompt 缓存LANGFUSE_CACHE_PROMPT_TTL_SECONDS数字36001 小时缓存条目的过期时间秒在 PromptService 构造函数 中缓存是否启用由两个条件共同决定this.cacheEnabled Boolean(redis) env.LANGFUSE_CACHE_PROMPT_ENABLED true; this.ttlSeconds env.LANGFUSE_CACHE_PROMPT_TTL_SECONDS;即必须配置了 Redis构造函数传入的redis实例非空且环境变量显式开启默认即为开启。两个条件缺一不可。注意cacheEnabled还预留了测试注入入口构造参数cacheEnabled?: boolean注释标注 used for testing方便单元测试直接控制缓存开关而不依赖环境变量。七、可观测性缓存命中/未命中指标PromptService内置了两个 OTel 指标定义在 types.tsexport enum PromptServiceMetrics { PromptCacheHit prompt_cache_hit, PromptCacheMiss prompt_cache_miss, }每次getPrompt无论命中与否都会通过incrementMetric计数index.ts#L56-L61metricIncrementer通过构造函数注入。在 getPromptByName.ts 中可以看到实际注入的是recordIncrement即读取路径Web 端按名称读取 Prompt 的 action会持续上报prompt_cache_hit/prompt_cache_miss两个指标。你可以据此在监控面板上计算缓存命中率hit / (hit miss)并评估是否需要调整 TTL 或检查失效是否过于频繁。八、读取入口getPromptByName 如何组装参数getPromptByName.ts 是缓存读取的典型入口展示了参数组装规则同时传入version和label会直接抛出InvalidRequestError(Cannot specify both version and label)只传version→ 按版本号寻址只传label→ 按标签寻址两者都不传 → 默认按PRODUCTION_LABEL寻址即生产标签等价于读取当前生产环境使用的 Prompt 版本。每次调用都会新建PromptService(prisma, redis, recordIncrement)实例因此缓存开关、TTL 与指标上报逻辑在 Web 与 Worker 的所有读取路径上保持一致。九、边界与一致性保证小结综合原文档与源码这套缓存策略的一致性保证可以归纳为读多写少场景下的强一致写入路径createPrompt、duplicatePrompt、duplicateFolder在 Postgres 事务提交后立即按项目粒度失效缓存任何随后到来的读取都会落入新 epoch 命名空间而回源并发读不阻塞读取路径完全无锁——未命中时直接回源并回填多个并发未命中只会产生重复的 DB 查询不会互相阻塞旧数据有界过期即便失效操作失败如 Redis 短暂不可用旧缓存条目也会在LANGFUSE_CACHE_PROMPT_TTL_SECONDS默认 1 小时内自然过期不会无限期返回陈旧数据依赖解析一致epoch 按项目而非按 Prompt 粒度轮换正是为了覆盖修改被依赖 Prompt 会波及所有引用方的传递依赖场景类型约束前置version/label二选一的类型联合与数字 label 独立 Key的 selector 逻辑从源头防止了缓存 Key 歧义。如果你正在为 Langfuse 做二次开发或自托管调优建议从 PromptService 源码 出发结合 createPrompt 写入链、getPromptByName 读取链 以及 服务端环境变量定义 三处代码即可完整掌握该缓存机制的全部行为边界。【免费下载链接】langfuse Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/9/10 9:02:00

数字信号处理核心:z变换与DTFT的工程实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/10 9:02:00

在VMware Workstation中安装RHEL 8虚拟机:从配置到排错全指南

1. 为什么是RHEL 8,为什么偏偏用VMware Workstation来装先说个常见的场景。很多朋友第一次装Linux,往往图省事选了Ubuntu,图形界面漂亮、驱动齐全、遇到问题百度一下全是答案。但当你开始准备红帽认证,或者公司内部的开发、测试、…

2026/9/10 9:02:00

Linux内核模块加载顺序详解:从modprobe到softdep与initramfs

1. 模块加载顺序的核心逻辑:内核怎么决定“谁先上”1.1 模块到底是什么,为什么顺序会翻车很多刚接触Linux内核的人容易把“内核”想成一个单一庞大的程序,但实际上,现代Linux内核更像一个“核心调度台 一堆随时可插拔的零件”。这…

2026/9/10 9:42:06

Simulink仿真生成输电线路故障数据的技术实践

1. 项目背景与核心价值在电力系统运行维护中,输电线路故障数据的获取与分析一直是行业痛点。传统方式依赖现场实测,不仅成本高昂,且难以覆盖所有故障类型。这个项目通过Simulink仿真批量生成六类典型故障数据(单相接地、两相接地、…

2026/9/10 9:42:06

arXiv学术信息流操作系统:结构化切片+技术谱系树

1. 这不是“爬虫教程”,而是一份可落地的学术信息流操作系统你有没有过这种体验:每天早上打开arXiv,面对3000篇新提交论文,像站在瀑布前接水——手忙脚乱,接满一杯,下一秒又被冲走;收藏夹里躺着…

2026/9/10 9:42:06

Arduino ESP32 开发环境搭建:四层拆解,首次烧录一次跑通

Arduino ESP32 开发环境搭建:四层拆解,首次烧录一次跑通 【免费下载链接】arduino-esp32 Arduino core for the ESP32 family of SoCs 项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32 搭 Arduino ESP32 开发环境时最常见的卡点…

2026/9/10 9:42:06

DeepSeek Harness 通用设置与 Agent 预设实战:从零配置你的智能助手

上一篇把 DeepSeek Harness 装好之后,很多人问得最多的其实不是“怎么让它跑起来”,而是“装完以后到底应该先动哪些设置,才能让这个 Agent 真的听我指挥”。这一篇就把通用设置和 Agent 预设这两块掰开揉碎讲清楚。我自己刚开始用的时候&…

2026/9/10 9:42:06

AutoHedge:基于Python与Solana的轻量级AI智能体协同框架

1. 项目概述:AutoHedge 不是“自动对冲”,而是智能体协同决策的底层范式重构 AutoHedge 这个名字乍看像金融领域的自动对冲工具,但结合 swarm intelligence(群体智能)、AI agents(AI智能体)、So…

2026/9/10 9:37:05

CANN/GE ACL模型内存查询接口

aclmdlQuerySize 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、TensorFlo…

2026/9/9 13:11:35

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/8 7:15:15

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/9 16:31:09

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/10 0:00:55

目录对比去重实战:用哈希算法精准清理重复文件

我电脑里现在还有一块换了三次机的“数据墓地”硬盘,里面存着2016年以前所有旧笔记本的完整备份。平时不觉得有什么,直到前阵子想把它整理归档,发现同一个安装包、同一批照片、同一份论文草稿,在几个不同的备份目录里反复出现。更…

2026/9/10 0:00:55

Leaflet离线地图完整Demo合集:内网部署与坐标纠偏实战

简介:这是一份面向Web GIS开发者的LeafLet离线地图示例合集,帮助开发者快速掌握离线地图从搭建到交互的完整流程。压缩包共723个文件,大小14.06MB,以319个js脚本、175个html页面和29个css样式文件为主体,配合png/svg图…

2026/9/10 0:00:55

MATLAB读取Rinex 3.02观测文件:多系统GNSS数据解析实战

简介:基于MATLAB开发的Rinex3.02版观测文件(o文件)读取代码包,面向卫星定位导航方向的学习者与研究人员,用于解决新版观测文件的数据解析、历元提取与时间转换问题。压缩包共4个文件,包含两个m脚本、一个19…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/9 10:21:54

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码