跳到主要内容

Langfuse 提示词管理实现

约 15 分钟3,401 字更新于 2026/10/04

返回 概览 · 对照阅读 coze-loop · 横向对比与建议 · 交互页面

  • 仓库: wzqwtt/langfuse
  • 提交: f75c661dbe8c6b85523c81486b39e8403ac2c141
  • 范围: packages/shared(Prisma、PromptService、模板)、web(tRPC、公开 REST、Playground)、worker(实验、摄取、自动化)。
  • 规则: 每条结论都带源码永久链接。行号对应上述提交。
  • 注意: SDK 代码不在本仓库。客户端缓存与客户端渲染无法从本仓库验证。

目录​

  1. 结论速览
  2. 术语
  3. 架构
  4. 数据模型
  5. 流程一:创建版本、移动标签、删除
  6. 流程二:公开 API 获取
  7. 流程三:发送给 LLM
  8. 组合(提示词依赖)
  9. 模板
  10. 缓存
  11. 风险与观察

1. 结论速览​

主题结论证据
存储一个提示词是 Postgres prompts 表中的一组不可变版本行。没有父表。提示词就是它的名字。schema.prisma#L786-L813
版本版本号是整数,自增。唯一键 (projectId, name, version)。createPrompt.ts#L160-L175
标签标签是可移动指针。同一事务中,标签从其他版本移除。新版本总是带 latest。createPrompt.ts#L129、updatePromptLabels.ts#L3-L43
默认读取不指定版本和标签时,读取 production。getPromptByName.ts#L21-L56
受保护标签项目可以保护标签。修改受保护标签需要额外 RBAC 权限。authorizeProtectedLabelMutation.ts#L70-L152
组合内联标签 @@@langfusePrompt:name=X|label=L@@@。最多 5 层,拒绝环,只能引用 text 提示词。PromptService/index.ts#L243-L404
模板基于正则的 {{var}},不是真正的 Mustache。chat 提示词支持 placeholder 消息。prompts.ts#L14-L31
服务端渲染公开 API 不渲染。返回原始模板。渲染由 SDK、Playground 或 worker 完成。promptNameHandler.ts#L40-L53
缓存Redis 缓存解析后的提示词,TTL 1 h。任何写操作轮换项目级 epoch 来失效。PromptService/index.ts#L195-L241
限流prompts 资源在所有套餐上都不限流。RateLimitService.ts#L280-L285
关联追踪摄取时按 promptName + promptVersion 查找提示词,写入 ClickHouse 的 prompt_id。IngestionService/index.ts#L1294-L1326
写副作用每次写:轮换缓存 epoch、写审计日志、发 EntityChangeQueue 事件(驱动 webhook / Slack)。promptChangeEventSourcing.ts#L14-L56

2. 术语​

术语含义代码中的名字
提示词同名的一组版本行name
版本一行不可变记录,整数版本号Prompt 行、version
标签指向某个版本的可移动指针,存为行上的数组labels[]
标记(tag)作用于整个提示词名的分类,写到每个版本上tags[]
依赖一个提示词内联引用另一个提示词prompt_dependencies
渲染把变量值填入模板compileTemplateString / compileChatMessages
解析把依赖标签展开成子提示词文本resolvePrompt / buildAndResolvePromptGraph
项目租户隔离单位projectId

3. 架构​

  • 所有提示词写操作在 web。
  • web 和 worker 都通过共享的 PromptService 读提示词。
  • tRPC 把路由注册为 prompts: promptRouter。root.ts#L98
层职责关键文件
packages/sharedPrisma schema、zod 契约、PromptService、依赖解析、模板函数、LLM 调用封装prisma/schema.prisma、src/server/services/PromptService/*、src/utils/prompts.ts、src/server/llm/*
web(Next.js)UI、tRPC promptRouter、公开 REST、写操作、受保护标签授权、审计、发送变更事件web/src/features/prompts/**、web/src/pages/api/public/v2/prompts/**
worker(BullMQ)实验执行、摄取时关联提示词、消费变更事件触发自动化worker/src/features/experiments/*、worker/src/services/IngestionService/index.ts、worker/src/features/entityChange/*

4. 数据模型​

4.1 表​

prompts(schema.prisma#L786-L813)

  • 字段:
    • id、projectId(外键,级联删除)、createdBy
    • name、version Int
    • type:text 或 chat,默认 text
    • prompt Json:字符串或 chat 消息数组
    • config Json:默认 {}
    • labels String[]、tags String[]
    • commitMessage String?
    • isActive Boolean?:已废弃,由 production 标签代替
  • 索引:
    • @@unique([projectId, name, version])
    • tags 上有 GIN 索引
    • labels 上没有索引。对比 Skill 表有标签 GIN 索引。schema.prisma#L869

prompt_dependencies(schema.prisma#L815-L833)

  • 字段:parentId(外键,级联)、childName、childLabel? 或 childVersion?。
  • 子提示词按"名字 + 标签或版本"引用,不用外键。依赖是延迟绑定的:标签依赖会跟随标签移动。

prompt_protected_labels(schema.prisma#L835-L845):唯一键 (projectId, label)。

ClickHouse 观测列:prompt_id、prompt_name、prompt_version。0002_observations.up.sql#L25-L27

4.2 契约(zod)​

源码:types.ts。

  • 类型:enum PromptType { Chat, Text }。types.ts#L25-L29
  • 标签:最多 36 字符,正则 ^[a-z0-9_\-.]+$。constants.ts#L22-L25
  • 名字:保留 new、metrics、prompt-detail。名字不能含 |,因为 | 是依赖标签的分隔符。constants.ts#L12-L19
  • 创建:text 的 prompt 是字符串,chat 的 prompt 是消息数组。commitMessage 最多 500 字符。types.ts#L37-L86
  • 读取:promptName、version?、label?、resolve(默认 true)。types.ts#L126-L135
  • chat 消息:{role, content} 或 placeholder {type:"placeholder", name}。llm/types.ts#L236-L243
  • config 是自由 JSON。服务端只在实验中读取其中的工具配置。promptToolConfig.ts#L59

5. 流程一:创建版本、移动标签、删除​

5.1 入口​

入口鉴权证据
UI 表单 → tRPC prompts.createprompts:CUD;含受保护标签时另需 promptProtectedLabels:CUDNewPromptForm/index.tsx#L125-L170、promptRouter.ts#L312-L364
Playground "保存到提示词"同上SaveToPromptButton.tsx#L39-L51
REST POST /api/public/v2/promptsAPI Key + prompts:CUDpromptsHandler.ts#L29-L45、prompt-api-service.ts#L41-L96
REST v1 POST /api/public/prompts(旧)isActive 映射为 production 标签prompts.ts#L43-L70

5.2 createPrompt 步骤​

源码:createPrompt.ts#L93-L290。

  1. 按版本倒序读取最新版本。新版本类型必须与旧版本相同。(L106-L116)
  2. chat 提示词:变量名与 placeholder 名不能冲突。(L118-L127)
  3. 标签:finalLabels = [...labels, "latest"]。(L129)
  4. 标记:未传时继承上一版本。(L131-L132)
  5. 依赖:解析依赖标签,然后试运行依赖图解析。缺失、成环、过深、非 text 都会失败。(L136-L158)
  6. 在 1 个 $transaction([...]) 中:
    • 创建版本行,version = latest + 1 或 1。
    • 每个依赖创建 1 行 prompt_dependencies。
    • 从其他版本移除这些标签。
    • 标记变化时,重写所有版本的标记。updatePromptTags.ts#L3-L36
  7. 并发:没有锁。两个并发创建都算出 version + 1。唯一索引拒绝第二个(P2002),系统返回"请重试"。(L55-L69、L222-L236)
  8. 提交后:失效缓存、发变更事件。失败只记日志并吞掉,避免调用方重试产生重复版本。(L239-L287)

5.3 移动标签(发布 / 回滚)​

  • REST:PATCH /api/public/v2/prompts/{name}/versions/{version},请求体 {newLabels}。不能包含 latest。promptVersionHandler.ts#L10-L41
  • 授权只检查新增的受保护标签。prompt-api-service.ts#L98-L164
  • updatePrompt(updatePrompts.ts#L15-L165):
    1. 在交互式事务中对目标行执行 SELECT … FOR UPDATE。(L24-L44)
    2. 新标签 = 旧标签 ∪ 新标签。该接口实际只增不减。(L52)
    3. 阻止移除被其他提示词依赖的标签。(L63-L98)
    4. 从其他版本移除新标签,设到目标版本。(L107-L132)
    5. 失效缓存,发 updated 事件。(L137、L154-L162)
  • tRPC setLabels 是 UI 路径。promptRouter.ts#L819-L1013
  • 受保护标签管理:latest 不能被保护。promptRouter.ts#L1407-L1529

5.4 删除​

  • REST DELETE /api/public/v2/prompts/{name}?version|label。不带过滤时删除所有版本。promptNameHandler.ts#L55-L75
  • deletePrompt(deletePrompt.ts#L13-L127):
    1. 通过 child_name 加载依赖方。
    2. 只在依赖会真正断开时阻止删除。(L56-L92)
    3. 删除 latest 时,把它挂到剩余的最高版本。(L96-L119)
    4. deleteMany,然后失效缓存。(L121-L126)
    • 第 3 步和第 4 步不在同一事务中。

5.5 复制与文件夹​

5.6 变更事件​

6. 流程二:公开 API 获取​

6.1 路由​

方法与路径处理器证据
GET /api/public/v2/prompts(列表元数据)getPromptsMeta:1 条原生 SQL,按名字分页,lateral join 聚合版本、标签、标记promptsHandler.ts#L14-L28、getPromptsMeta.ts#L12-L80
GET /api/public/v2/prompts/{promptName}promptNameHandler.GETpromptNameHandler.ts#L17-L54
GET /api/public/prompts(v1 旧)不允许应用内 agent keyprompts.ts#L17-L72

6.2 按名字获取​

  1. 中间件:withMiddlewares(CORS、错误映射)包装 createAuthedProjectAPIRoute。
  2. 鉴权:shadowAuth,动作 prompts:read,只接受项目级 API Key(Basic auth,公钥 + 私钥)。createAuthedProjectAPIRoute.ts#L229-L254
  3. 限流:
  4. 处理器:同时给出版本和标签时返回 400。否则按版本、标签或默认 production 读取。getPromptByName.ts#L21-L56
  5. 响应:返回已解析依赖的 prompt、resolutionGraph,以及计算出的 isActive。promptNameHandler.ts#L40-L53
  6. 服务端不渲染变量。响应中保留原始 {{var}} 和 placeholder。

6.3 SDK 侧​

  • SDK 提示词客户端不在本仓库。契约在 Fern:prompts.yml。
  • 文档说 resolve=false 绕过缓存,只用于调试或一次性任务。

7. 流程三:发送给 LLM​

Langfuse 有 4 条与 LLM 相关的路径。只有"实验"在服务端用 Prompt 表渲染并调用模型。

路径渲染位置是否关联提示词版本证据
用户应用 + SDK客户端是:摄取时按名字 + 版本关联IngestionService/index.ts#L1294-L1326
提示词实验(worker)服务端 worker是:TraceSinkParams.promptexperimentServiceClickhouse.ts#L159-L246
Playground浏览器否chatCompletionHandler.ts#L29-L170
LLM-as-judge服务端 worker否:使用独立的 EvalTemplatellmEvaluatorExecution.ts#L40-L87

7.1 提示词实验​

  • web 侧:createExperiment 要求 promptExperiments:CUD,检查工具与结构化输出冲突,创建 datasetRuns(metadata 含 prompt_id),放入 ExperimentCreateQueue。router.ts#L213-L300
  • worker 侧:
    1. fetchPrompt 用 findUnique 按 id 加载,再 resolvePrompt。它展开依赖,但不走缓存(缓存按名字做键)。utils.ts#L63-L71
    2. 计算所有变量和 placeholder 名。utils.ts#L233-L245
    3. 对每个数据集条目渲染:text 提示词变成 1 条 system 消息。chat 提示词先填 placeholder,再替换变量。utils.ts#L73-L158
    4. 调用 generateLLMText,追踪参数带 prompt: {name, version}。错误被吞掉,不重试。experimentServiceClickhouse.ts#L181-L243
  • 关联方式:

7.2 Playground​

7.3 LLM-as-judge​

7.4 摄取时关联​

8. 组合(提示词依赖)​

  • 语法:@@@langfusePrompt:name=X|version=N@@@ 或 @@@langfusePrompt:name=X|label=L@@@。
  • 解析器:先对内容 JSON.stringify,所以 text 和 chat 都适用。去重后要求恰好 2 段,name= 在前。parsePromptDependencyTags.ts#L3-L61
  • 解析:buildAndResolvePromptGraph(PromptService/index.ts#L243-L404)
    • 最大深度 5。用 seen 集合检测环。提示词不能引用自己的其他版本。(L20、L267-L284)
    • 子提示词必须存在,且类型必须是 text。(L329-L349)
    • 替换步骤:递归解析子提示词 → 父提示词转 JSON → 正则匹配子提示词的版本标签或任意当前标签 → 替换为 JSON 转义后的子文本 → 解析回对象。(L360-L380)
  • 结果类型:PromptResult = Prompt & { resolutionGraph }。PromptService/types.ts#L3-L36

9. 模板​

仓库中没有 Mustache 库。全部基于正则。

函数行为证据
extractVariables模式 /{{([^{}]*)}}/g。变量名须匹配 ^\p{L}[\p{L}\p{N}_]*$(Unicode,字母开头)stringChecks.ts#L7-L47
compileTemplateString替换 {{ key }}。未知键原样保留。null / undefined 变空串。其他值用 String(v)prompts.ts#L14-L31
compileEvalPromptLLM-as-judge 使用。值先经 parseUnknownToStringprompts.ts#L33-L46
compileChatMessages把 placeholder 展开为消息数组。缺值或非数组时抛错。然后替换变量compileChatMessages.ts#L21-L91
compileChatMessagesWithIdsPlayground 变体,保留或分配消息 idcompileChatMessages.ts#L93-L124
  • 边界情况:replaceTextVariables 用变量名拼正则,不转义。UI 和提取器限制了变量名,但该函数本身不强制。

10. 缓存​

源码:PromptService/index.ts#L23-L421。

  • 开关:有 Redis 且 LANGFUSE_CACHE_PROMPT_ENABLED=true(默认 true)。TTL 默认 3600 s。env.ts#L147-L148
  • 键:prompt:{projectId}:{epoch}:{name}:version:{N} 或 …:label:{L}。(L195-L215)
  • 值:已解析的提示词 JSON,包含依赖展开结果。(L167-L178)
  • 失效:任何写操作把 prompt_cache_epoch:{projectId} 设为新随机令牌(6 字节,7 天 TTL)。旧键不可达,按 TTL 自然过期。(L180-L193、L217-L241)
    • epoch 是项目级的,因为解析后的提示词含跨名字的传递依赖。
    • getOrCreateEpoch 用 SET NX 再读回,并发初始化收敛到同一个值。
  • 容错:缓存读写错误只记日志,回退到数据库。(L153-L177)
  • resolve=false:绕过缓存,返回 resolutionGraph: null。(L51-L53、L90-L101)
  • 所有写路径都调用失效:创建、改标签、删除、复制、tRPC 路由。

11. 风险与观察​

#观察影响证据
1版本不可变,只有标签和标记会变内容修改总是产生新版本createPrompt.ts#L160-L175
2标记变更重写所有版本每个版本 1 条 UPDATEupdatePromptTags.ts#L3-L36
3标签唯一性靠应用代码保证,不靠数据库约束依赖事务和 FOR UPDATE 的正确使用updatePrompts.ts#L24-L44
4创建不加锁,靠唯一索引拒绝并发并发创建时调用方须重试createPrompt.ts#L222-L236
5项目级 epoch 失效项目内任何一次写都让整个项目的提示词缓存冷启动PromptService/index.ts#L217-L241
6prompts 资源不限流获取被视为热路径;也失去了滥用保护RateLimitService.ts#L280-L285
7模板是极简正则,无条件和循环复杂逻辑须放到调用方prompts.ts#L14-L31
8组合只支持引用 text 提示词,基于 JSON 字符串替换chat 提示词不能被复用为片段PromptService/index.ts#L329-L349
9删除路径非原子:latest 重挂和 deleteMany 分开执行中途失败可能留下不一致状态deletePrompt.ts#L96-L123
10labels 列无索引按标签查询依赖 (project, name, version) 前缀后过滤schema.prisma#L786-L813
11Playground 不关联提示词;LLM-as-judge 用独立 EvalTemplate这两类调用无法按提示词版本统计chatCompletionHandler.ts#L29-L170、evalExecutionDeps.ts#L270-L322
12保留 v1 路由和 isActive 字段由 production 标签派生,兼容旧客户端prompts.ts#L17-L72