用 Langfuse 打磨 Langfuse:v4 搜索栏 Ask AI 过滤器(searchBar.generateFilter)的改进与自我狗粮计划解析
【免费下载链接】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_FILTER_PLAN.md 为骨架,完整解析 Langfuse v4 事件表中"Ask AI"自然语言过滤器的现状、问题与调优路线图。文章将带你理解searchBar.generateFilter端到端的数据流(自然语言 → LLM → 可编辑 FilterState pills)、其基于字段注册表(FieldRegistry)生成的系统提示词设计、为修复"数据感知缺失"而注入的观测上下文(observed context),以及作者团队"用 Langfuse 自己来打磨 Langfuse 的 AI 功能"的完整狗粮闭环——提示词管理、版本化、数据集与确定性评估。读完你将掌握:这套混合提示词架构(托管模板 + 代码注入变量)为何能同时保证"可人工调优"与"永不与语法漂移",以及如何用parseGeneratedFilters+filterStateToQueryText构造一个无需 LLM judge 的确定性评分器。
一、功能定位:v4 搜索栏的 "Ask AI" 模式是什么
搜索栏(Search Bar)是 v4 事件表上一个基于语法的查询条(详见 search-bar/README.md 的 "AI filter mode" 一节)。它的 "Ask AI" 模式把一段自然语言请求转换成FilterState,直接应用到 v4 的 observations/events 表上。整体数据流如下:
prompt (+ 当前 filters 作为 refine 上下文) → searchBar.generateFilter → LLM → JSON singleFilter[] → parseGeneratedFilters(校验 + 丢弃不可表示项) → 经 setFilterState 应用 → 重新派生为可编辑的 grammar pills几个关键设计点(均可在源码中验证):
- 功能是 opt-in 的:受组织级开关
aiFeaturesEnabled控制,仅 Cloud 可用,服务端和客户端双重门控(见 router.ts 中的 FORBIDDEN 分支)。 - Refine 模式:打开 AI 模式时,搜索栏当前的完整 query 文本会被作为 refine 上下文发送给模型,让模型"在现有过滤条件之上做编辑",而不是从零构建(router.ts 的
currentQuery输入参数)。 - Scope 严格限定 v4:只有
searchBar.generateFilter这一个代码路径。v3 时代的 legacy 自然语言过滤器(naturalLanguageFilters.createCompletion,远程托管 promptget-filter-conditions-from-query)是完全独立的代码路径,不受本文任何改动影响。
二、现状诊断:2026-06-23 UX 会话发现的五大问题
质量是双峰分布且由模型驱动(当时本地测量用的是claude-3-haiku),而非管道问题。特定性描述(specific prompts)准确,而简短描述或 refine 请求会退化到 few-shot 示例。问题清单如下:
| 严重级 | 发现 | 复现 |
|---|---|---|
| 🔴 Major | Refine 会在简短指令下静默破坏现有过滤器。模型复读 few-shot 示例,同时忽略用户请求和可见的 refine 上下文 | 从level:ERROR+ 最近一小时,"only in production" → 生成environment:production+latency:>5(幻觉)+startTime:>24h(窗口错误);level:ERROR被丢弃。"also only errors" 同样可复现 |
| 🔴 Major | 对项目数据无感知(不了解真实的 metadata keys / observed values) | "routing queue should be membership-support" → 生成traceName:membership-support(应为metadata.routing.queue:*membership-support*) |
| 🟠 Mod | 一次生成后焦点丢失。disabled={pending}让输入框 blur 到<body>且不再聚焦;生成失败/为空后,Esc 不再取消,必须重新点击输入 | 失败生成后document.activeElement === body |
| 🟠 Mod | 日期时间 pill 晦涩。"last hour" 渲染为startTime:>"2026-06-23T11:12:37.255Z"(毫秒精度 ISO)。属全栏渲染问题,由 AI 功能暴露 | — |
| 🟡 Minor | Refine 上下文以扁平灰色等宽字符串展示(被截断),而非其他地方使用的彩色 pills | — |
| 🟡 Minor | 可发现性——"Ask AI" 是一个小而暗淡的按钮,占位符却引导用户使用 DSL;没有线索表明 pills 是 AI 生成的 | — |
做得好的部分(保留):特定性构建准确("errors in the last hour"、"user alice slower than 10s"、多过滤器组合);结果是以透明可编辑 pills 呈现;键盘路径(Tab→按钮→Enter、Esc、back)可用;空结果错误提示干净;应用是无损的(被跳过的过滤器得以保留);v3/v4 隔离成立。
三、本地提示词实验:生产模型(Opus 4.8)上的表现
Piece 1 将 v4 prompt 拿到生产模型eu.anthropic.claude-opus-4-8(经 playground SSO profile,走 Bedrock)上,用生产环境的parseGeneratedFilters解析、对照期望的过滤列集合打分,跑出一组场景:12/13,跨运行稳定。
核心发现:§2 中的 refine 泄漏是claude-3-haiku的弱点——Opus 4.8(生产实际使用的模型)能正确处理 refine(增/删/改,且保留上下文)。既然生产跑的是 Opus 4.8,MVP prompt 保持原样即可上线,无需为发布而调优。
| 场景 | Prompt(+ refine ctx) | 期望列 | Opus 4.8 |
|---|---|---|---|
| build | errors in the last hour | level, startTime | ✅ |
| build | slow traces in production | latency, environment | ✅ |
| build | failed traces from user alice | level, userId | ✅ |
| build | traces tagged billing | traceTags | ✅ |
| build | accuracy score below 0.8 | scores_avg | ✅ |
| build | output mentions refund | output | ✅ |
| build | root observations only | isRootObservation | ✅ |
| build | expensive gpt-4 calls over $0.5 | totalCost, providedModelName | ✅ |
| refine (add) | level:ERROR startTime:>…+ "also only in production" | level, startTime, environment | ✅ preserved + added |
| refine (remove) | latency:>2+ "drop latency, show only errors" | level | ✅ removed + added |
| refine (change) | environment:production+ "make it staging instead" | environment | ✅ value changed |
| edge | gibberish | (无) | ✅ empty |
| gap | routing queue is membership-support | metadata.routing.queue | ❌ guesses traceName/sessionId |
唯一失败的是数据感知缺口:模型不知道项目真实的 metadata keys(或实际的type/name 值),于是猜了一个列。
数据感知修复(已合入 MVP)
一个 "project data context" 数据块被注入 prompt——每列的观测值 + metadata keys(从可见行中采样的扁平化点路径)+ 当前结果数量。该块在客户端基于已加载的filterOptions+ 可见行构建(见 lib/ai-context.ts),并做成本封顶。在 Opus 4.8 上带该上下文重新验证:
| Prompt | Before | After(带 context) |
|---|---|---|
| only support chat sessions | type:chat→ 0 行 | traceName:SupportChatSession✅ |
| routing queue is membership-support | traceName:…/sessionId:… | metadata.routing.queue:membership-support✅ |
| errors in the last hour(对照组) | ✅ | 仍然 ✅ |
从源码看,buildAiContext的封顶策略非常具体:每列最多 25 个值(MAX_VALUES_PER_COL)、最多 30 个 metadata key(MAX_METADATA_KEYS)、单值最长 40 字符(MAX_VALUE_LEN)、总上下文硬上限 12000 字符(MAX_CONTEXT_CHARS)——后者严格低于端点输入校验dataContext: z.string().max(16000)的 16000 字符上限,避免触发 Zod 400。metadata 对象会被递归压平为点路径 key(深度上限 3),并附带一个示例叶子值。这些数字在 lib/ai-context.ts 中都有明确定义。
全表面能力覆盖(同样在 MVP 中)
prompt 被扩展为教授完整v4 语法——不止简单的列匹配——并且在 metadata 上大胆使用。在 Opus 4.8 上用变体(而非逐字示例)验证,7/7:
| 请求 | 生成结果 |
|---|---|
| filter to the acme tenant | metadata.tenant:acme |
| mention 'password reset' in the response | output:"password reset" |
| where the sentiment is positive | scores.sentiment:positive |
| traces missing a user id | -has:userId |
| exclude debug and warning levels | -level:(DEBUG OR WARNING) |
| tagged with both experiment and baseline | traceTags:(experiment AND baseline) |
| expensive claude calls in staging that aren't errors | providedModelName:claude totalCost:>0.5 environment:staging -level:ERROR |
也就是说,生成器现在覆盖:metadata、内容(input/output)搜索、数值/分类分数、null/has 检查、tag 的 any/all/none 组、以及否定——即从多样化示例中泛化出的完整表面。
测试装备与注意点:一次性 tsx 脚本,对每个场景 shell 调用aws bedrock-runtime converse --profile playground(需要 Bedrock 凭据)。关键 Caveat:质量是模型相关的——本地claude-3-haiku无法通过 refine 用例,因此自托管用户若使用较弱模型会看到更差的输出;Piece-2 的 eval 将量化模型选择的影响。
四、为什么用 Langfuse 自己来打磨这个功能(Dogfooding)
这是典型的 LLM 质量问题。靠肉眼手调 prompt 是在猜,而且会静默回归(§2 的示例泄漏正是如此)。纪律性的修复方式是:数据集 + evals + 版本化提示词,度量每一次改动——而这恰恰就是 Langfuse 卖的产品。用 Langfuse 打磨自己的 AI 功能,既是正确的工程循环,也是地道的 dogfooding。
五、当前可观测性基线(Baseline)
- ✅Tracing 已接通。
searchBar.generateFilter每次调用都会经traceSinkParams发送到 AI-features Langfuse 项目(envlangfuse-natural-language-filter,traceNamesearch-bar-filter,targetProjectId = LANGFUSE_AI_FEATURES_PROJECT_ID),受组织级aiTelemetryEnabled门控。generateLLMText记录 system+user 消息与原始 completion。 - ❌Prompt 在代码里(buildFilterPrompt.ts,由注册表派生)→ 没有版本、无法免部署迭代、没有 prompt↔trace 关联、没有可评估对象。
- ❌缺少结构化 trace 捕获:mode(build/refine)、
currentQuery、解析后的 filters、droppedCount、applied-vs-empty 都没有记录。 - ❌没有 dataset、没有 evals。
值得注意的是,源码已经比文档基线往前走了一步:tracing 的 metadata 中已包含langfuse_refine_mode、langfuse_current_query、langfuse_data_context_chars等调试字段(见 router.ts),并且已新增 parseOutcomeScoring.ts——它把解析结果(parse-empty-result、parse-dropped-filters、parse-unknown-score-names、filter-count、output-markdown-fenced五个分数)以 fire-and-forget 的方式写回生成 trace,让生产流量自我采集质量信号。output-markdown-fenced是专门盯 haiku 是否在回答开头输出 ```markdown 围栏的信号。
六、架构决策:混合提示词(Hybrid Prompt)
不要二选一("注册表派生的代码内 prompt" vs "Langfuse 托管 prompt")——拆开它:
- 托管模板(Langfuse Prompt Management,AI-features 项目下的新 prompt
search-bar-filter,与 v3 的 prompt 分离):人类可调优的部分——role、输出格式、intent hints、示例、refine 指令——带变量:{{fieldCatalog}}、{{currentDatetime}}、{{currentFilters}}、{{observedContext}}。 - 代码注入变量(buildFilterPrompt.ts 收缩为
buildFilterVariables()):字段目录仍然由FIELDS生成(因此不会与语法漂移),外加观测上下文(metadata keys + values)用于数据感知修复。
端点按 label 拉取 prompt、用这些变量编译,并把prompt 版本链接到每条 trace(traceSinkParams中的prompt:字段,v3 路径已经这么做了)。这是整个方案的支点:它让示例和 refine 指令变成可 A/B 的版本化数据——而这恰恰是泄漏发生的地方。
源码中的混合解析路径
resolveFilterPrompt.ts 完整实现了这一决策:
- 优先托管:当 AI-features 的 public/secret key 存在时(仅事件注册表
events使用托管 prompt,其他视图仍用本地注册表派生 prompt),通过getLangfuseClient拉取search-bar-filter聊天 prompt,并用{{catalog}}(buildFieldCatalog)、{{nullable_ids}}(nullableFieldIds)、{{current_datetime}}三个注册表派生变量编译。fetch 带fetchTimeoutMs: 2000, maxRetries: 0,保证慢速/出错的 AI-features 项目不会拖住用户请求。 - 健壮性校验:编译结果若不是合法的 chat 消息数组(坏编辑可能编译出字符串、空数组或 malformed 消息),
logger.warn后回退到代码骨架,绝不 500。 - 代码回退兜底:自托管(无 key)是预期状态,直接返回代码骨架;fetch 抛错也只降级为回退并打 warn。
- 门控语义:托管 prompt 的读取只受 AI-features keys 门控(读取自己的 prompt 不发送任何组织数据),不受
aiTelemetryEnabled门控;后者只门控 trace 写入与版本链接。
仓库还维护了托管 prompt 的种子文件 server/prompts/search-bar-filter.prompt.json(标签含production、latest,内容与代码 fallback 的 System Prompt 对齐),以及推送脚本 scripts/ask-ai/sync-search-bar-filter-prompt.sh——该脚本需由持有真实密钥的人类手动运行,Agent 不应执行。
字段注册表驱动的提示词:永不漂移的关键
buildFilterPrompt.ts 的核心思想是:模型的全部词汇表就是搜索栏语法本身。specForField根据字段的kind/syncMode派生每个字段允许的type/运算符/值形状,镜像 filter-state-to-query.ts 中lowerSingle的反向方向,二者不能发散:
number→ 运算符"=", ">", "<", ">=", "<=",值必须是数字datetime→">", "<", ">=", "<=",ISO 8601 字符串boolean→"=", "<>",JSON 布尔exactOption文本 →stringOptions,"any of"/"none of",字符串数组arrayOption文本 →arrayOptions,"any of"/"none of"/"all of",字符串数组textSearch文本 →string,"=" / "contains" / "does not contain" / "starts with" / "ends with",单个字符串
buildFieldCatalog从EVENTS_FIELD_REGISTRY的fields(directFilter !== false)逐行生成目录,附带别名、单位、描述与可空标注;nullableFieldIds生成支持 null 检查的字段 id 列表。这套目录文本同时被代码构建路径和托管 prompt 编译路径使用,保证两条路径看到完全一致的目录。README 还提到一个单元测试(__tests__/server/unit/searchBarFilterPrompt.servertest.ts)断言每个字段的 prompt 推荐type都能 round-trip,防止 prompt 与反向适配器漂移。
提示词的结构化:指令与数据分离
buildFilterSystemPrompt只承载静态指令(Role、输出格式、目录、规则、示例、当前时间);动态请求数据(被 refine 的当前 query、观测到的项目数据)由buildFilterContextMessage作为独立的 user 消息发送(见 buildFilterPrompt.ts)。这样做的三个好处(源码注释明示):trace 中"提示词"与"喂给模型的数据"是清晰分开的两条消息;后续用户轮次无法压过原本搭乘在 user 消息里的规则;也为把骨架升级为托管 prompt 铺路——动态值不会烤进模板文本。
七、狗粮闭环(The Dogfood Loop)
- Prompt management + labels:按
productionlabel 拉取;在latest上迭代,eval 改善后 promote。SDK 按 TTL 缓存以保持请求低延迟。 - 数据集
search-bar-filter-evals(AI-features 项目)。条目形如{ input: { prompt, currentQuery? }, expected_output: { queryText } }。种子数据包含能工作的用例 + 失败用例:必须保留上下文的简短 refine、routing queue → metadata.routing.queue、时间表达式、分数、多过滤器。 - 确定性评分器(输出是结构化的,无需 LLM judge):把期望与实际输出都解析为归一化的
FilterState(复用parseGeneratedFilters+filterStateToQueryText),然后评分:精确集合匹配、过滤器级 precision/recall/F1,以及针对 refine 条目的context_preserved标志。复用的是已有且已测试的代码。 - 跨 prompt 版本和模型跑数据集(haiku vs sonnet),在 experiments UI 中对比。这就是可度量地消灭泄漏的方式:改示例 → 跑 → 看
context_preserved从 0→1 → promote。 - 闭环:把真实失败的 production traces 策展进数据集;可选地加一个在线隐式反馈分数:用户是保留了 AI 生成的 filters,还是几秒内清掉/编辑了?这能按真实使用情况对 prompt 版本排序。
数据感知修复(#2)作为{{observedContext}}变量折叠进来,数据集让我们能证明它有效(在 routing-queue 用例上做有/无的消融)。
评分器的地基:parseGeneratedFilters 的三道防线
文档计划"复用parseGeneratedFilters",而 parseFilterCompletion.ts 的实际实现给出了完整的三道防护,值得展开:
- Registry 契约兼容性检查(
isRegistryContractCompatible):过滤器的type必须与其列契约兼容(例如scores_avg不能用裸number过滤器,必须用带 key 的numberObject),否则events.all会 500——这类过滤器直接丢弃。 - 分数名校验(
validateScoreNames):分数过滤器按名字寻址(key),拼写错误的分数名能通过 Zod、契约表、语法 round-trip,却会成为一个静默匹配不到任何东西的死过滤器。所以把 key 与观测到的分数名集合比对:精确匹配保留,唯一归一化匹配(_/-/空格/大小写不敏感)就地纠正,其余丢弃并在unknownScoreNames中上报。 - Round-trip 丢弃:任何不能 round-trip 成搜索栏语法的东西(未知列、不可表示列)进入
skippedFilters,绝不会到达客户端。
此外parseFilterArray有一个非常实用的细节:按平衡的顶层[...]子串、从后往前尝试解析——模型有时先输出一版草稿数组、再输出一版自我纠正后的数组,从后往前能应用模型的自我纠正而不是丢弃正确答案;显式空数组[]被视为模型的"无过滤器"最终答案并立即返回。
生成后的质量信号:parse-outcome scoring
parseOutcomeScoring.ts 把解析结果写成 trace 上的可查询分数(parse-empty-result、parse-dropped-filters、parse-unknown-score-names、filter-count(附带截断到 500 字符的 queryText 注释)、output-markdown-fenced),使生产流量自我采集质量信号。它刻意用独立的 Langfuse 单例客户端(带fetchRetryCount: 0、requestTimeout: 3000、FLUSH_TIMEOUT_MS = 2000竞速),全程 fire-and-forget,任何失败都只打 warn、绝不拖慢用户响应——注释里还解释了为什么不能复用natural-language-filters/server/utils.ts的getLangfuseClient(该 helper 按首次调用参数记忆化,一旦有人先以enabled: false构造,后续所有.score()都会静默变 no-op)。
八、实施路线图(TODO)
Phase 0 — 丰富 tracing(小而可并行)
- 每条 trace 增加:mode(build/refine)、
currentQuery、解析后的 filters、droppedCount、applied-vs-empty;给 trace 打 tag。 - 确认 trace 落在 AI-features 项目内,且可按 env 查询。
Phase 1 — prompt → Prompt Management(混合式)
- 在 AI-features 项目创建托管 chat prompt
search-bar-filter(含上述变量),以当前代码内 prompt 为种子。 - 重构 buildFilterPrompt.ts →
buildFilterVariables()(目录来自FIELDS+ 观测上下文)。 generateFilter:按 label 拉取 prompt、编译、把版本链接到 trace。- 基于 label 的灰度发布(
production/latest);fetch 失败保留代码回退。
Phase 2 — eval harness + baseline
- 构建
search-bar-filter-evals数据集(种子用例含失败用例)。 - 实现确定性评分器(复用
parseGeneratedFilters+filterStateToQueryText):exact-set、F1、context_preserved。 - 通过 Langfuse SDK 写 dataset-run 脚本;在改动任何东西之前为当前 prompt+模型记录baseline。
Phase 3 — 用数据调优
- 迭代 prompt 版本:去粘/抽象 few-shot 示例;强化 refine 的"保留当前 filters;只改被要求的部分"。
- 加入
{{observedContext}}(metadata keys + observed values);消融以证明 routing-queue 修复有效。 - 评估模型选择(haiku vs sonnet);按分数/成本决策。
- 考虑服务端 refine 守卫:除非请求明确移除,否则不丢弃上下文 filters;或展示 before→after 差异,让任何静默丢失可见。
Phase 4 — 闭环(持续)
- 把失败的 production traces 策展进数据集。
- 在线隐式反馈分数(N 秒内保留 vs 清除/编辑)。
快速 UX 修复(独立于 eval 循环)
- 生成后恢复焦点(不让输入框 blur 到 body)。
- Refine 上下文渲染为只读 pills,而非扁平截断字符串。
- 更友好的日期时间 pill 渲染(全栏范围,独立工作流)。
- "Ask AI"入口的可发现性打磨。
九、悬而未决的决策(Open Decisions)
- 混合 prompt(托管模板 + 代码注入目录/观测变量)——支点,一切围绕它。
- 评分器:确定性集合匹配(从这里起步,复用现有代码)vs 面向"语义意图"的 LLM judge vs 两者并用。
- 模型:留在 Bedrock haiku vs 为该功能换更强模型(用 eval 决策,而不是凭感觉)。
十、关键文件地图
| 文件 | 职责 |
|---|---|
| server/router.ts | searchBar.generateFilter:门控、LLM 调用、tracing、parse-outcome 分数写入 |
| server/buildFilterPrompt.ts | 代码内 prompt → 将演变为buildFilterVariables();提供buildFieldCatalog/nullableFieldIds/buildFilterContextMessage |
| server/resolveFilterPrompt.ts | 混合解析:优先托管search-bar-filterprompt,回退代码骨架 |
| server/parseFilterCompletion.ts | parseGeneratedFilters(评分器将复用它) |
| server/parseOutcomeScoring.ts | 解析结果 → trace 分数(自我采集质量信号) |
| server/prompts/search-bar-filter.prompt.json | 托管 prompt 的仓库版本种子(labels: production/latest) |
| lib/fields.ts | FIELDS字段注册表(目录数据源) |
| lib/filter-state-to-query.ts | filterStateToQueryText(评分器归一化) |
| lib/ai-context.ts | buildAiContext:观测值 + metadata keys + 结果数的成本封顶注入 |
| components/SearchBarAiPrompt.tsx | AI 子模式 UI |
| web/src/features/natural-language-filters/server/router.ts | v3 参照实现:已使用 Prompt Management + AI-features trace sink(混合模式的范本) |
| scripts/ask-ai/sync-search-bar-filter-prompt.sh | 将托管 prompt 种子推送到各区域(需人类手动执行) |
总结:这份改进计划最值得借鉴的不是某一段 prompt 文本,而是它的工程方法论——把"自然语言 → 结构化过滤器"这种看似玄学的 LLM 质量问题,拆解为注册表驱动的可验证提示词 + 混合托管架构 + 确定性评分器 + 数据集驱动的迭代闭环,并用 Langfuse 自身的 Prompt Management、tracing、datasets 与 evals 功能来完成这整个循环。对任何正在构建"NL → 结构化查询"类功能的团队,这套"先修数据感知、再上版本化评估、最后闭环策展"的路线图,都是一份可以直接迁移的参考。
【免费下载链接】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),仅供参考