面试知识库

主题 6 · 线上反馈闭环#

定位:LLM 系统上线后如何收集质量信号、跨会话记忆、缓存加速、持续优化——从”能回答”到”越用越好”。


一、通用知识#

1.1 核心概念与原理#

LLM 可观测性(Observability)

面试口述建议:“传统微服务的 Metrics/Traces/Logs 三件套在 LLM 系统里不够用——多了一个维度:生成质量。你需要知道某条 trace 不仅耗时多少毫秒、花了多少 token,还要知道它的 CRAG 评级、引用覆盖率、置信度分数。这就是 LLM 可观测性比传统可观测性多出来的核心:质量信号可聚合。”

关键概念拆解:

  • Trace/Span/Generation/Score 四级模型:Trace 是一次完整问答的生命周期;Span 是 trace 内每个阶段(query_understanding / retrieval / rerank / generation);Generation 是 Span 下的一次 LLM 调用(记录 prompt、completion、model、token usage);Score 是挂在 trace 上的质量打分(confidence=0.82, crag_grade=HIGH),可跨 trace 聚合、出趋势图、设告警。
  • OTel span 属性 vs Langfuse Scores 的本质区别:span 属性是”逐条看”的元数据(只能在单个 trace 详情页里过滤);Score 是”跨 trace 聚合”的一等公民(可按时间/模型/用户分组做均值、百分位、告警)。这不是功能差异而是数据模型差异——Langfuse 的 OTLP 摄取不会自动把任意 span attribute 映射为 Score,必须走 POST /api/public/scores 提交。

OpenTelemetry 在 LLM 系统中的应用

面试口述建议:“OTel 社区在 2024 年开始定义 GenAI Semantic Conventions(gen_ai.* namespace),标准化了 model、token usage、prompt/completion 等 span 属性。但 OTel 本身不解决’质量信号跨 trace 聚合’——这是 Langfuse/LangSmith 等 LLM 专用可观测平台的核心价值。”

  • gen_ai.systemgen_ai.request.modelgen_ai.usage.input_tokensgen_ai.usage.output_tokens 等是 OTel GenAI Semantic Conventions 定义的标准属性
  • Spring AI 的原生 Instrumentation 自动按这些约定在 generation span 上打属性,无需手动埋点
  • 但 confidence、citation_coverage、crag_grade 这类 RAG 业务指标不在 OTel 标准范围内,需要应用层自行推送到 Langfuse Scores

用户反馈信号

面试口述建议:“显式反馈(thumbs up/down)信噪比高但参与率低(通常 < 5%),隐式反馈(追问率、改写率、停留时间)量大但噪声多。实践中两者结合:显式反馈当 ground truth 标注质量分层级的锚点,隐式反馈跑大盘趋势。”

  • 显式反馈:thumbs up/down(二值)、1-5 rating(连续)、评论文本。优点:信号明确;缺点:参与率低、幸存者偏差(满意的用户不点赞也不投诉)
  • 隐式反馈:reformulation rate(换词重问=上一轮没回答好)、追问深度(连续追问=话题有价值)、点击引用率(点了 source card=引用有帮助)、停留时间
  • 设计要点:反馈入口不打扰主交互流(thumbs 在回答底部而非弹窗)、反馈与 traceId 关联(才能回溯是哪条 pipeline 出了问题)

Memory 系统

面试口述建议:“Memory 系统的核心问题不是’怎么存’而是’什么时候提取、怎么处理矛盾’。Mem0 的方案是把矛盾消解放进提取 prompt——新记忆提取时注入已有记忆,让 LLM 同步判断 ADD/UPDATE/NOOP,一次调用解决提取+消解两件事。”

  • 提取触发(LLM 即 Gate):不做规则预筛——每轮都异步触发提取,由提取 LLM 对无可记轮次返回 NOOP 自裁(对标 Mem0/LangMem/Zep,recall 优先)。早期的两层规则 Gate 因伤 recall 已删,省成本改由异步 + 廉价模型(memory.extract_model,默认 qwen-flash)消化
  • 矛盾消解:用户说”我用 Java”,三个月后说”我转 Go 了”——旧记忆要被 supersede,不能两条并存导致 prompt 里自相矛盾
  • 记忆类型分层:PREFERENCE(偏好:我喜欢简洁回答)、FACT(事实:我的项目用 Spring Boot)、CONTEXT(上下文:上次讨论了 RAG 优化)——不同类型的 TTL 和召回权重不同

缓存策略

面试口述建议:“LLM 缓存有三个层次:Exact Match(字面一致,简单但召回率低)、Semantic Cache(embedding 近邻,召回率高但要算余弦相似度)、Prompt Caching(Anthropic/OpenAI 的 KV Cache 复用,前缀一致时省 TTFT)。三者不互斥——Semantic Cache 省整个 pipeline,Prompt Caching 省推理层的 prefill。”

  • Exact Match Cache:key = normalize(question),hash 精确匹配。适用于 FAQ 类高频重复问题
  • Semantic Cache:key = query embedding,cosine 相似度 >= 阈值时命中。适用于”同义不同词”场景(“Docker怎么部署” vs “Docker部署方法”)
  • Prompt Caching:Anthropic Cache Control / OpenAI Prompt Caching,在推理侧复用 system prompt 的 KV cache。适用于长 system prompt + 短 user message 场景
  • 失效策略是关键:知识库更新了但缓存还在 → 答案过时。需要 version 机制做精确失效,TTL 做兜底

持续优化闭环(Flywheel)

面试口述建议:“LLM 系统的飞轮效应:收集信号 -> 标注 -> 评测 -> 优化 -> 部署 -> 收集。每转一圈质量提升一档。关键是让这个循环自动化——trace 里的 CRAG LOW 自动进入待审队列,人工标注后进入 golden set,golden set 跑回归测试,回归不降就部署。”

收集信号 ─→ 标注(auto-label / human-label)
    ↑                    ↓
  部署  ←── 优化 ←── 评测(regression test)
plaintext
  • Auto-label:CRAG grade、citation coverage 自动标注(零人工成本但粒度粗)
  • Human-label:thumbs down 的 case 进入审核队列(精度高但成本大)
  • Regression test:golden set 上跑 faithfulness / relevance / citation coverage,阻止退步
  • 优化手段梯度:参数调整(阈值/温度/topK)→ Prompt tuning → Few-shot → Fine-tune

1.2 业界主流方案对比(表格形式)#

LLM 可观测性平台对比#

维度LangfuseLangSmithArize PhoenixHelicone
开源/商业开源 (MIT) + Cloud商业 (闭源)开源 (Apache 2.0)开源 (Apache 2.0) + Cloud
Trace 模型Trace → Observation (Span/Generation/Event) → ScoreRun → LLMRun/ToolRun → FeedbackTrace → Span (OTel 原生)Request → Response → Feedback
集成方式SDK / OTLP / APILangChain 原生 + SDKOTel Collector + SDKProxy / SDK
Score/质量信号一等公民(Score API,可聚合出趋势图)Feedback(绑定 Run,可聚合)Evals + Document Evaluators基础 rating,聚合能力弱
OTel 兼容OTLP 摄取(span 自动映射 observation)无原生 OTel 支持原生 OTel(Phoenix 本身是 OTel collector)无原生 OTel 支持
定价自托管免费 / Cloud 按 trace按 trace 量级(Developer 免费层)完全免费(自托管)按请求量
最佳场景Java/Spring 生态 + OTel 已有LangChain/LangGraph 深度用户已有 OTel 基础设施快速接入 + 代理模式

Memory 系统对比#

维度Mem0MemGPT / LettaZep简单 Redis KV
定位Long-term memory layerOS 级内存管理Session + Fact memory最简实现
核心机制提取时注入已有记忆 + ADD/UPDATE/DELETE 矛盾消解Working memory + Archival storage,自动分页Auto-extract facts + session summary手动 set/get
跨 Session原生支持原生支持(archival)原生支持(fact memory)需自建
矛盾消解LLM 单次调用完成(提取+消解)内置 memory editing无显式矛盾消解
选型考量轻量集成、矛盾消解好、无额外基础设施复杂 agent 场景(长对话/多轮规划)需要 session summary规模小、不需要智能提取

缓存策略对比#

策略适用场景命中率失效策略典型延迟
Exact MatchFAQ / 高频重复问题低(字面必须一致)TTL / 版本号<1ms
Semantic Cache同义不同词中高(cosine >= 阈值)KB version + TTL 双重~5ms(brute force)
Prompt Caching长 system promptN/A(省 TTFT)前缀变更即失效减少 50-90% prefill

1.3 关键论文与技术要点#

论文/项目核心贡献面试怎么提
Mem0 (2024)提出 long-term memory layer:提取时注入已有记忆让 LLM 做 ADD/UPDATE/DELETE 分类,一次调用完成提取+矛盾消解”我们的记忆系统借鉴了 Mem0 的矛盾消解思路——新记忆提取时注入已有记忆做冲突比对”
CRAG (Corrective RAG, 2024)检索质量分级(HIGH/AMBIGUOUS/LOW)→ 不同分级走不同处理路径”CRAG 的分级思路直接用在了我们的质量信号里——crag_grade 作为 Langfuse Score 推送”
GPTCache (2023)首个系统性的 LLM 语义缓存框架,cosine 近邻 + 失效策略”语义缓存的思路参考了 GPTCache,但失效策略改成了 KB version 精确失效”
OTel GenAI Semantic Conventions (2024)标准化 LLM 调用的 span 属性命名(gen_ai.* namespace)“Spring AI 原生按 OTel GenAI conventions 打 span,我们在此基础上加了 RAG 业务指标”
Langfuse Scores API区分 span attribute(逐条)和 score(跨 trace 聚合),解决”质量信号怎么做大盘监控”的问题”Langfuse 的 Scores 是一等公民,跟 span 属性不一样——Score 能跨 trace 聚合出趋势图”

1.4 常见面试问答#

Q1:LLM 系统的可观测性和传统微服务有什么区别?

A:传统微服务关注延迟/吞吐/错误率三个维度,LLM 系统多了”生成质量”维度。你需要知道某条 trace 的 CRAG 评级是 HIGH 还是 LOW、引用覆盖率是 0.9 还是 0.3、置信度是多少——这些是传统 Prometheus counter/gauge 表达不了的。所以需要 Langfuse 这类 LLM 专用平台,它的 Score 机制是专门为”质量信号跨 trace 聚合”设计的:能出趋势图、设告警、按模型/用户分组对比。OTel 的 span attribute 只能逐条看、不能聚合,两者互补。

Q2:怎么判断一次回答好不好?有哪些信号?

A:分四个层次。第一层是检索质量:CRAG 分级(HIGH/AMBIGUOUS/LOW)+ rerank top1 分数,反映”找到的资料靠不靠谱”。第二层是生成质量:引用覆盖率(答案多少比例有 [n] 标记引用来源),反映”答案是否 grounded”。第三层是组合指标:置信度 = clamp(coverage) * rerank_top1,综合检索和生成两端。第四层是用户反馈:thumbs up/down 是最终裁判,但参与率低,需要和前三层交叉验证——比如某条 confidence=0.85 但 thumbs down,说明置信度阈值可能需要调高。

Q3:用户反馈怎么利用?显式和隐式反馈怎么结合?

A:显式反馈(thumbs up/down)当锚点——参与率虽低但信噪比高,thumbs down 的 case 进入人工审核队列,标注后进入 golden set 做回归测试。隐式反馈跑大盘趋势——reformulation rate(换词重问率)升高说明近期回答质量下降,追问深度增加说明某个话题有价值可以做推荐。两者交叉:如果某类问题 reformulation rate 高 + thumbs down 多,就是最优先需要优化的 case。

Q4:缓存过期怎么处理?知识库更新后缓存怎么失效?

A:两层失效机制。精确失效:每个 KB 有 version 号(数据库 integer),chunk 重切/增删后 version 自增。缓存条目写入时保存 {kbId -> version} 快照,命中时比对当前 version,任一不匹配即失效——精准到单个 KB 粒度。TTL 兜底:即使 version 机制失灵(比如 KB 被物理删除而非逻辑删除),24 小时 TTL 保证条目不会永远留在缓存里。version 查询本身也有 Caffeine 5 秒 TTL 本地缓存,避免每次比对都查数据库。

Q5:Memory 系统怎么处理矛盾信息?

A:借鉴 Mem0 的方案——提取新记忆时,把该用户已有的同类型记忆注入 prompt,让 LLM 在同一次调用中同时做三件事:判断是否值得提取(NOOP)、判断是新增还是更新(ADD/UPDATE)、标记被取代的旧记忆 ID(supersedes)。被标记的旧记忆做软删除(invalidate),新记忆写入。整个过程零额外 LLM 调用——仅 prompt 变长,不多一次请求。

Q6:怎么做在线 A/B 测试?

A:最简单的方案是参数级 A/B:同一个问题,控制组用 rerank top10 + 温度 0.7,实验组用 rerank top5 + 温度 0.3,trace 上打 experiment tag。两组分别跑 Langfuse Score 聚合——看 confidence 均值、citation coverage 均值、thumbs down 率。更细粒度的可以做 prompt A/B(两套 system prompt 随机分流)或模型 A/B(qwen-plus vs qwen-max)。关键是 trace 上的 experiment 标签要打对,否则聚合时混在一起就废了。

Q7:trace 太多怎么采样?

A:按 trace 的业务价值分级采样。全量保留:thumbs down + CRAG LOW + confidence < 0.5 的”坏 case”(这些是优化的原材料)。采样保留(如 10%):正常回答。丢弃:非知识查询的短路路径(emergency/chitchat/out_of_scope),这些对 RAG 优化没价值。实现上用 OTel 的 Sampler 接口,在 span 创建前按 trace.name 和 langfuse.observation.level 决定是否采样。

Q8:Semantic Cache 和 Prompt Caching 有什么区别?分别用在哪?

A:Semantic Cache 省的是整个 pipeline(检索 + 重排 + 生成),命中后直接回放答案,适用于同一知识库上的相似问题。Prompt Caching 省的是推理层的 prefill 阶段(复用 system prompt 的 KV cache),只在前缀不变时生效,适用于长 system prompt + 短 user message 的场景。两者不互斥:Semantic Cache 没命中时走完整 pipeline,此时如果 system prompt 前缀与上次一样,Prompt Caching 仍然能省 TTFT。


二、DocMind 实践#

30 秒口述版#

DocMind 上线后做了两件关键事把系统从”能回答”提升到”越用越好”。第一件是语义缓存 + KB version 精确失效SemanticCacheService 用 cosine 近邻命中相似问题,但用三维隔离保安全(userId 防跨用户串答、kbScope 防跨知识库、KB version 快照校验防过时),写入侧有频次门控和低质量过滤——低质答案不写缓存,避免错误固化。失效机制是 KB version 精确失效(chunk 变更后 bump() 自增)+ 24h TTL 兜底,version 读热路径用 Caffeine 5s TTL 本地缓存防数据库放大。第二件是可观测驱动优化闭环LangfuseScoreClient 推送四维质量 Score(confidence / coverage / rerank_top1 / crag_grade),跨 trace 聚合发现一批 CRAG LOW + 零覆盖的 case,trace 定位出根因是”有 Web 证据但因 rerank 分偏低被丢弃”,修复后 CRAG LOW 有证据时走 assembleLowConfidence 保留引用——从质量信号到根因定位到代码修复的完整闭环。

详细展开#

背景/痛点(Situation)#

系统上线后面临三个核心问题:

  1. 回答质量不可度量:只知道”回答了”但不知道”答得好不好”。用户投诉说”回答不准”,但无法定位是检索没找到、重排排错了、还是生成幻觉了。OTel span 属性只能逐条 trace 看,不能聚合出”这周平均置信度是否下降了”。
  2. 用户偏好不记忆:用户第一轮说”我用 Spring Boot 3”,第三轮问”怎么集成 Redis”时系统已经忘了技术栈背景,回答不够针对性。每个 session 从零开始,没有跨会话记忆。
  3. 高频查询重复计算:多个用户问同样的问题(如”Docker 部署文档在哪”),每次都跑完整的 embedding + 检索 + 重排 + 生成 pipeline,浪费算力。旧的 Exact Match 缓存命中率低(字面必须一致)。

做了什么(Action)#

以下按面试讲述权重排列:语义缓存和可观测闭环是重点展开的两个系统设计故事,记忆系统和反馈机制作为补充简述。

核心设计 1:语义缓存 + KB version 精确失效(SemanticCacheService + KbVersionService)

这是一个完整的缓存系统设计题——命中策略、失效策略、安全隔离、写入门控四个维度:

  • 命中策略:4 条件同时满足才算命中——userId 严格相等(防跨用户串答/个性化信息泄露)→ kbScope 严格相等(防跨知识库串答)→ cosine >= 0.92(语义近邻)→ KB version 快照校验(KbVersionService.isSnapshotValid())。userId 是第一优先校验,在 cosine 比对之前就做——不匹配直接跳过,避免个性化信息泄露(用户 A 的记忆可能被注入到答案里)
  • 失效策略精确失效 + TTL 兜底双层设计。精确失效:每个 KB 有 version 号(DB integer),chunk 重切/增删后 bump() 自增。缓存条目写入时保存 {kbId -> version} 快照,命中时比对当前 version,任一不匹配即失效——精准到单个 KB 粒度。TTL 兜底:24h 过期,防 version 机制失灵(如 KB 被物理删除而非逻辑删除)
  • 读热路径优化:version 写频率极低(只在 chunk 变更时)但读频率极高(每次缓存 lookup 查 N 个 KB),用 Caffeine 5s TTL 本地缓存抵挡高频 DB 调用——JVM 本地缓存比 Redis 快一个数量级,5 秒延迟对”文档重切后旧答案多活 5 秒”可接受
  • 写入门控:频次 >= 阈值才写(putIfFrequent()),fallback 或 LOW 置信度答案不写入——宁可慢一点也不能让错误答案固化。如果某个问题 KB 里确实没有好资料,每次都不缓存保证每次都有机会走完整 pipeline 拿到最新结果
  • 容量管理:条目上限 1000 条,超限按 createdAt 淘汰最老的;brute force cosine 比对 1000 x 1024 维在 Java 里约 5ms,规模够用
  • 缓存回放:回填 confidenceScoreconfidenceBandagentTraceJson,避免命中缓存的对话在历史里失去 trace/confidence 信息

核心设计 2:可观测驱动优化闭环(LangfuseScoreClient → trace 分析 → 代码修复)

这是一个从质量信号到产品改进的完整闭环故事:

  • 质量信号回灌LangfuseScoreClient 通过 POST /api/public/scores 推送四维 Score:confidence(01)、citation_coverage(01)、rerank_top1(0~1)、crag_grade(HIGH/AMBIGUOUS/LOW),每个 Score 用 traceId 关联,可跨 trace 聚合
  • Scores vs span attributes 的关键区别:span 属性只能逐条 trace 看,Langfuse Score 是一等公民——可跨 trace 聚合出趋势图、设阈值告警。这不是功能差异而是数据模型差异
  • 非侵入设计:fire-and-forget(守护线程池 + 失败只记日志 + langfuse.enabled=false 时 no-op),强制 HTTP/1.1(规避 Langfuse Next.js 网关的 HTTP/2 协商问题),失败重试一次
  • 真实闭环实例:通过 Score 聚合发现一批 crag_grade=LOW + citation_coverage=0 的 case → trace 定位发现 CRAG LOW 时证据被丢弃(原逻辑走零证据兜底模板),但实际 compressed 非空——有 Web 证据只是 rerank 分偏低 → 修复:CRAG LOW 且证据非空时走 assembleLowConfidence(保留引用),新增 ungrounded 信号标记零覆盖 + CRAG LOW 为 Langfuse span WARNING
  • 降级回答标 WARNING:使 Langfuse 可按 level 过滤降级回答,否则与正常回答无法区分

补充:跨会话记忆(MemoryExtractor)

  • 提取触发用「LLM 即 Gate」:每轮都触发,无规则预筛,由提取 LLM 对无可记轮次返回 NOOP 自裁(ExtractionResult.isEmpty() 跳过写入)。对标 Mem0/LangMem/Zep——主流无一用规则启发式决定是否提取,因其伤 recall
  • Mem0 风格矛盾消解:提取时注入已有记忆让 LLM 同时输出 ADD/UPDATE/NOOP + supersedes 列表,被 supersede 的旧记忆软删除
  • 异步执行:CompletableFuture.runAsync() 不阻塞 SSE 返回,异常静默降级

补充:用户反馈 + 延伸阅读

  • QaMessage.feedback 字段(1/-1/0),前端 thumbs up/down 写入,与 trace 关联
  • RecommendationGenerator 基于已检索 chunk 的 tags/category 反查同主题文档,随 done payload 返回最多 3 条延伸阅读

代码锚点#

类/方法路径职责
LangfuseScoreClientconfig/LangfuseScoreClient.javafire-and-forget 推送 Score(numeric + categorical),HTTP/1.1 + 重试一次
LangfuseScoreClient.numeric()同上推送数值型 Score(confidence / citation_coverage / rerank_top1)
LangfuseScoreClient.categorical()同上推送分类型 Score(crag_grade = HIGH/AMBIGUOUS/LOW)
MemoryExtractorservice/rag/MemoryExtractor.java异步记忆提取:LLM 即 Gate(无规则预筛)+ Mem0 矛盾消解
MemoryExtractor.extractAndStore()同上提取 + 矛盾消解 + 存储 + supersedes 软删除;无可记内容时 LLM 返回 NOOP,isEmpty() 跳过
MemoryExtractor.formatExistingMemories()同上加载已有记忆格式化为 prompt 注入段
SemanticCacheServiceservice/rag/SemanticCacheService.java语义缓存:cosine 近邻 + 四条件命中 + 频次门控写入
SemanticCacheService.lookup()同上4 条件命中判断(userId/kbScope/cosine/KB version)
SemanticCacheService.putIfFrequent()同上频次门控 + 低质量过滤 + 写入缓存
KbVersionServiceservice/rag/KbVersionService.javaKB version 管理:bump 自增 + Caffeine 5s TTL + snapshot 快照
KbVersionService.bump()同上KB 内容变更后自增 version,失效本地缓存
KbVersionService.isSnapshotValid()同上校验快照是否仍有效(所有 kbId version 一致)
QaMessage.feedbackentity/QaMessage.java用户反馈字段(1/-1/0)
DocMindAgent.stageAsyncMemoryExtract()agent/DocMindAgent.java异步记忆提取入口:无规则预筛,每轮触发 + CompletableFuture 异步执行(LLM 即 Gate)
DocMindAgent.stageGenerateAndPersist()同上生成后推送 Langfuse Score + 写语义缓存 + 持久化
DocMindAgent.replayFromSemanticCache()同上缓存回放:模拟流式输出 + 回填 confidence/trace
RecommendationGeneratorservice/rag/RecommendationGenerator.java基于 tags/category 反查同主题文档生成延伸阅读推荐
RecommendationGenerator.generate()同上策略 1: 同 category;策略 2: 共享 tags;最多 3 条

量化数据#

指标数值基线来源
语义缓存命中率45%30%(Exact Match 时代)阈值从 3 调至 2 后的观测值
提取触发策略每轮触发,LLM 即 Gate(无规则预筛)旧版两层规则 Gate(已删,伤 recall)对标 Mem0/LangMem/Zep
语义缓存 cosine 阈值>= 0.92SemanticCacheService.DEFAULT_DISTANCE_THRESHOLD
语义缓存 TTL24hSemanticCacheService.DEFAULT_TTL_HOURS
语义缓存条目上限1000 条SemanticCacheService.MAX_ENTRIES
Brute force cosine 延迟~5ms100-500 条 x 1024 维 Java 内计算
Caffeine TTL(KB version)5sKbVersionService.versionCache
LangfuseScoreClient 连接超时3sLangfuseScoreClient HttpClient 配置
LangfuseScoreClient 请求超时5sLangfuseScoreClient HttpRequest 配置
LangfuseScoreClient 重试次数1 次send() 循环 attempt <= 2

三、追问应对#

面试官想听到的信号#

  • 缓存系统设计能力:不是”加个 Redis 就完了”,而是命中策略(四条件)、失效策略(精确 + TTL 双层)、安全隔离(userId 优先)、写入门控(低质不写 + 频次过滤)四维一体的完整设计
  • 可观测与传统监控的区别:知道 OTel span 属性和 Langfuse Score 的数据模型差异——span 属性逐条看,Score 跨 trace 聚合
  • 闭环意识:trace 中的质量信号 → 定位根因(CRAG LOW 丢证据)→ 代码修复 → 验证(Score 趋势图),不是”有监控”而是”监控驱动改进”
  • 非侵入原则:所有反馈/记忆/缓存组件都 fire-and-forget 或异步执行,失败静默降级,绝不阻塞主流程——“缓存是加速器不是必须品”

追问预判与应答#

Q1(通用):Langfuse 的 Score 和 OTel 的 span attribute 到底有什么不同?为什么不直接用 span attribute?

A:数据模型不同。Span attribute 是键值对,属于单个 span,只能在 trace 详情页里过滤——你没法做”本周所有 trace 的 confidence 均值”这种聚合查询。Langfuse Score 是一等公民,有自己的 name/value/dataType/traceId,可以跨 trace 聚合出趋势图、设阈值告警(比如 confidence 周均值低于 0.7 就发 Slack 通知)。技术上这也不是选择问题——Langfuse 的 OTLP 摄取没有”span 属性自动映射为 Score”的约定,只认 langfuse.observation.* 等固定键。所以 confidence 这类质量指标必须走 Scores API 提交,我们实现了 LangfuseScoreClient 做 fire-and-forget 推送。

Q2(通用):trace 量太大,怎么控制成本?

A:按业务价值分级采样。“坏 case”全量保留(CRAG LOW、confidence < 0.5、thumbs down),这些是优化的原材料。正常回答 10% 采样。非知识查询短路路径(emergency/chitchat/out_of_scope)直接丢弃或极低采样——这些对 RAG 优化没价值。实现上用 OTel 的 Sampler 接口,在我们的项目里有 BusinessRootSpanSampler 按 trace.name 和 observation level 做决策,对应的白名单机制保证 RAG pipeline 的 trace 不被默认丢弃。

Q3(项目):语义缓存用 brute force cosine 比对,规模大了怎么办?

A:当前设计是 MAX_ENTRIES=1000,1000 条 x 1024 维 brute force 在 Java 里约 5ms,完全够用。如果规模扩到万级,有两条路:第一是 Caffeine 本地缓存一份 entry 列表避免每次 HVALS 全量拉 Redis;第二是在 Milvus 里开一个独立 collection 做 ANN 索引(IVF_FLAT 或 HNSW),lookup 时走向量检索。但 1000 条内 brute force 的优势是零额外基础设施依赖、实现简单、延迟可控。

Q4(项目):KB version 用 Caffeine 5 秒 TTL,为什么不用 Redis 做分布式缓存?

A:因为 version 的写频率极低(只在 chunk 重切/增删时 bump),读频率极高(每次缓存 lookup 要查 N 个 KB 的 version)。对于这种”读多写少、容忍秒级延迟”的场景,JVM 本地缓存比 Redis 快一个数量级(0 网络开销),而且 version 是 KB 元数据的一部分、存在 MySQL 里,不需要额外的 Redis 键。5 秒 TTL 意味着 KB 更新后最多 5 秒缓存才生效——对于”文档重切后旧答案多活 5 秒”这个业务场景完全可接受。

Q5(项目):记忆提取怎么决定要不要触发?为什么不用规则 Gate 省 LLM 成本?

A:用「LLM 即 Gate」——每轮都异步触发提取,不做任何规则判断,由提取 prompt 对无可记轮次返回 NOOP 自裁(isEmpty() 跳过写入)。早期我做过两层规则 Gate(长度硬过滤 + memoryAware/complexity/个人正则),原则是”宁可少提取不浪费 LLM”,预估省 60-70% 调用。但对标 Mem0/LangMem/Zep 后整体删除了——没有一个主流系统用规则启发式决定是否提取。核心原因是规则 Gate 伤 recall:最典型的漏掉场景正是”用户在简单问题追问里偶然提到背景信息”(如”对了我们用的是 MySQL 8”),这恰恰是 Layer 1 长度过滤 / Layer 2 SIMPLE 无信号最容易误杀的,而记忆系统的核心 KPI 就是 recall——漏记是永久损失,比多花一次廉价异步调用糟得多。所以我反向重构:删掉 Gate(做减法),省成本改由”异步不阻塞 + 廉价小模型”消化——记忆提取单独配了 memory.extract_model(默认 qwen-flash),和主回答的 qwen-plus 分层。这体现的是”记忆系统 recall 优先、且优先对齐主流验证过的范式”的判断。

Q6(项目):低质量答案不写缓存,但如果用户频繁问一个确实没有好答案的问题,岂不是每次都要跑完整 pipeline?

A:是的,这是有意为之。如果某个问题确实在 KB 里没有好资料(CRAG LOW / fallback),每次都缓存低质答案意味着后续相似问题直接绕过完整检索——即使 KB 后来补充了相关文档也不会改善。不写缓存保证了每次都有机会走完整 pipeline 拿到最新结果。等 KB 补充了资料、回答质量提升到 MEDIUM/HIGH 后,频次门控自然会让它进入缓存。这个权衡是”宁可慢一点也不能让错误答案固化”。

Q7(项目):推送 Score 用的是 fire-and-forget,怎么保证推送成功率?

A:分两层看。可靠性层面:HTTP/1.1(规避 HTTP/2 协商问题)+ 失败重试一次(新连接,规避半关闭的池化连接),在稳定网络下成功率 > 99%。语义层面:Score 是辅助可观测信号而非业务关键路径——即使某条推送失败,该 trace 仍然有完整的 span attribute(confidence/coverage 都在 span 上打了一份),只是不参与跨 trace 聚合。如果需要更高可靠性,可以改成本地队列 + 批量推送(类似 OTel 的 batch exporter),但当前规模下逐条推送的成功率已经足够。

Q8(项目):Mem0 矛盾消解用 LLM 判断 ADD/UPDATE/NOOP,LLM 判断错了怎么办?

A:矛盾消解的判断错误有两种:一是该 UPDATE 判成 ADD(新旧两条并存),二是该 NOOP 判成 ADD(存了无价值记忆)。前者影响有限——prompt 里多一条冗余记忆不会导致错误回答,只是 prompt 变长了;后者也是有限影响——无价值记忆不会在后续问答中被 recall(因为语义相关度低不会被检索到)。真正危险的是该 NOOP 判成 UPDATE(把有价值的旧记忆覆盖了),但这种情况在实际中极少发生。保底措施是旧记忆做软删除(invalidate 标记 supersededBy)而非物理删除,必要时可以恢复。

Q9(项目):为什么缓存要做 userId 隔离?同一个问题不同用户的答案不一样吗?

A:是的。因为答案可能包含个性化信息——比如用户 A 的 Memory 里有”我的项目用 Spring Boot 3”,他问”怎么集成 Redis”时答案会提到 Spring Boot 3 的配置方式。如果用户 B 命中了这个缓存,就会拿到针对 A 技术栈的答案。更严重的是隐私泄露——A 的记忆可能包含”我在 XX 公司”这类信息被注入到答案里。所以 userId 是第一优先校验,在 cosine 比对之前就做,不匹配直接跳过。

反击引导#

  • 讲完闭环后引到可信生成(05):“这些质量信号的上游来源就是我们的 CRAG 分级 + 结构化引用解析,具体怎么做的可以展开讲——”
  • 讲完缓存失效后引到检索召回(03):“缓存没命中后走的就是完整的混合检索 + 重排 pipeline,这块的召回率优化也做了不少工作——”
  • 讲完 Memory 后引到Query 改写(02):“记忆在回答后提取,在提问时被召回——Query Understanding 阶段会把 memoryAware 信号打上,供记忆工具决定是否注入历史偏好——”
  • 讲完可观测驱动优化(CRAG LOW 修复)后引到架构收敛(07):“这个修复其实是我们架构演进中’加了又删’故事的一部分——自反思机制删掉后,质量信号从 LLM 反思转移到了 coverage + CRAG 的组合指标——”