ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

智能体面试准备(二十):可观测性实战——Trace 建模、OpenTelemetry、成本归因与故障定位

智能体面试准备(二十):可观测性实战——Trace 建模、OpenTelemetry、成本归因与故障定位 智能体面试准备二十可观测性实战——Trace 建模、OpenTelemetry、成本归因与故障定位上一篇《RAG 评估体系》解决的是离线怎么度量质量这一篇解决线上正在发生什么。这是 B 系列第二十篇也是智能体质量工程这条线的收官。可观测性Observability在传统微服务里已经是成熟话题但智能体带来了三个全新挑战执行路径非确定、单次请求内嵌套多层 LLM 调用、成本按 token 实时计费。一个 Agent 请求失败了是模型规划错了、工具超时了、还是上下文被截断了没有 trace 就只能靠猜。本文按为什么 Agent 特别需要可观测性 → Trace/Span 数据模型 → OpenTelemetry GenAI 语义约定 → 手写一个可用的 tracer → 成本与延迟归因 → 故障定位与告警 → 落地选型展开结尾给面试速答和高频追问清单。一、Agent 的可观测性难在哪1.1 和传统微服务的四点本质差异维度 传统微服务 LLM Agent ───────────────────────────────────────────────────────── 执行路径 确定代码写死 非确定模型每次决策可能不同 失败形态 抛异常、超时、5xx 成功返回但内容是错的最可怕 成本模型 按 CPU/内存/时长 按 input/output token 实时计费 调试单元 一次函数调用 一次 LLM 调用 prompt 采样参数 输出 重放 输入相同则输出相同 temperature0 时无法精确重放第二行是关键Agent 最危险的故障是HTTP 200 但答案错了。传统监控盯的错误率、延迟、饱和度这套黄金指标对这种故障完全失明。所以 Agent 可观测性必须同时覆盖系统健康和输出质量两条线。1.2 一次典型 Agent 请求的复杂度用户: 帮我查一下上季度华东区销售额跟去年同期比一下做成表格 ├─ [LLM] 规划: 拆成 3 步 1.2s 1.8k tok ¥0.018 ├─ [Tool] sql_query(上季度华东) 0.4s ¥0 ├─ [Tool] sql_query(去年同期华东) 0.5s ¥0 ├─ [LLM] 反思: 数据口径不一致需要重查 0.9s 3.2k tok ¥0.032 ├─ [Tool] sql_query(统一口径重查) 0.6s ¥0 ├─ [LLM] 生成表格 2.1s 4.5k tok ¥0.045 └─ 总计 5.7s 9.5k tok ¥0.095 问题来了 - 如果总延迟 P95 突然从 5.7s 涨到 15s是哪一步 - 如果单请求成本从 ¥0.095 涨到 ¥0.5是谁在烧钱 - 如果反思重查这一步开始频繁出现说明什么退化了 没有 trace这三个问题一个都答不了。二、Trace / Span 数据模型2.1 基本概念沿用 OpenTelemetry 的模型但要为 Agent 场景做扩展。Trace 一次完整的用户请求全局唯一 trace_id │ └── Span 一个可计时的操作单元有 span_id 和 parent_span_id │ └── 通过 parent 关系构成一棵树 Agent 场景的 Span 类型这套分类是重点 AGENT 整个 agent 的一次运行根 span CHAIN 一个逻辑步骤/子链 LLM 一次模型调用 ← 记 token、成本、采样参数 TOOL 一次工具调用 ← 记入参、出参、是否超时 RETRIEVER 一次检索 ← 记 query、召回数、topk 分数 RERANKER 一次重排 EMBEDDING 一次向量化 GUARDRAIL 一次安全/格式校验2.2 树形结构示意Trace: 7f3a9c... (总 5.7s, ¥0.095) │ └─ AGENT sales_report_agent [5.70s] ├─ LLM plan [1.20s] in1200 out600 ├─ CHAIN step_1_fetch [0.90s] │ └─ TOOL sql_query [0.40s] rows128 ├─ CHAIN step_2_fetch [1.10s] │ └─ TOOL sql_query [0.50s] rows131 ├─ LLM reflect [0.90s] in2800 out400 ← 异常步骤 ├─ CHAIN step_3_refetch [0.70s] │ └─ TOOL sql_query [0.60s] rows259 └─ LLM render_table [2.10s] in4100 out4002.3 每类 Span 必须记录的字段这张表是面试的硬货能背下来说明真做过。Span 类型必记字段通用trace_id, span_id, parent_id, name, kind, start_ts, end_ts, status, errorLLMmodel, provider, prompt可脱敏/哈希, completion, input_tokens, output_tokens, cached_tokens, temperature, top_p, max_tokens, stop_reason, cost, ttft首 token 延迟, retry_countTOOLtool_name, arguments, result截断, is_error, latency, timeout_flagRETRIEVERquery, top_k, num_returned, score_top1, score_mean, doc_ids, index_versionAGENTuser_id, session_id, agent_version, total_steps,终止原因完成/超步数/超时/异常三个容易漏的字段提到会加分1.ttfttime to first token流式场景用户感知的延迟是 ttft 而非总时长必须单独记。2.cached_tokensprompt caching 命中数直接影响成本不记就没法算真实节省。3.stop_reasonlength意味着被 max_tokens 截断这是静默故障的重要信号——输出被砍断但 HTTP 依然 200。三、OpenTelemetry GenAI 语义约定不要自己发明属性名OTel 已经有 GenAI 的语义约定semantic conventions用标准名才能被 Langfuse、Phoenix、Datadog 等后端自动识别。核心属性gen_ai.* 命名空间 ──────────────────────────────────────────── gen_ai.system 提供方如 openai / anthropic gen_ai.operation.name chat / text_completion / embeddings gen_ai.request.model 请求的模型名 gen_ai.response.model 实际返回的模型名可能有别名映射 gen_ai.request.temperature 采样温度 gen_ai.request.max_tokens 最大输出 gen_ai.usage.input_tokens 输入 token gen_ai.usage.output_tokens 输出 token gen_ai.response.finish_reasons 结束原因列表 gen_ai.conversation.id 会话 id Agent/Tool 扩展 ──────────────────────────────────────────── gen_ai.agent.name / gen_ai.agent.id gen_ai.tool.name / gen_ai.tool.call.id / gen_ai.tool.type用 OTel SDK 埋点的样子from opentelemetry import trace from opentelemetry.trace import Status, StatusCode tracer trace.get_tracer(my.agent) def call_llm(messages, modelgpt-4o-mini, temperature0.2): with tracer.start_as_current_span( fchat {model}, attributes{ gen_ai.system: openai, gen_ai.operation.name: chat, gen_ai.request.model: model, gen_ai.request.temperature: temperature, }, ) as span: try: resp client.chat.completions.create( modelmodel, messagesmessages, temperaturetemperature ) u resp.usage span.set_attribute(gen_ai.usage.input_tokens, u.prompt_tokens) span.set_attribute(gen_ai.usage.output_tokens, u.completion_tokens) span.set_attribute(gen_ai.response.model, resp.model) span.set_attribute( gen_ai.response.finish_reasons, [c.finish_reason for c in resp.choices], ) span.set_status(Status(StatusCode.OK)) return resp except Exception as e: span.record_exception(e) span.set_status(Status(StatusCode.ERROR, str(e))) raise为什么坚持用标准约定一是换后端不用改埋点代码二是社区的自动 instrumentation如opentelemetry-instrumentation-openai开箱即用三是 dashboard 模板可以直接复用。自定义属性名的团队后来都会为迁移付出代价。四、手写一个够用的 Tracer不引入重型依赖也能做出可用的追踪。下面这份代码可以直接放进项目。import time, uuid, json, threading, contextvars from dataclasses import dataclass, field, asdict from typing import Any, Optional _current_span contextvars.ContextVar(current_span, defaultNone) dataclass class Span: name: str kind: str # AGENT/CHAIN/LLM/TOOL/RETRIEVER/... trace_id: str span_id: str field(default_factorylambda: uuid.uuid4().hex[:16]) parent_id: Optional[str] None start_ts: float field(default_factorytime.time) end_ts: Optional[float] None status: str OK error: Optional[str] None attrs: dict[str, Any] field(default_factorydict) events: list[dict] field(default_factorylist) property def duration_ms(self) - float: return round(((self.end_ts or time.time()) - self.start_ts) * 1000, 2) class Tracer: def __init__(self, exporterNone): self.exporter exporter or (lambda spans: None) self._buf: list[Span] [] self._lock threading.Lock() def span(self, name: str, kind: str CHAIN, **attrs): return _SpanCtx(self, name, kind, attrs) def _finish(self, sp: Span): with self._lock: self._buf.append(sp) def flush(self): with self._lock: batch, self._buf self._buf, [] if batch: self.exporter(batch) class _SpanCtx: def __init__(self, tracer, name, kind, attrs): self.tracer, self.name, self.kind, self.attrs tracer, name, kind, attrs self.sp None self.token None def __enter__(self) - Span: parent _current_span.get() self.sp Span( nameself.name, kindself.kind, trace_idparent.trace_id if parent else uuid.uuid4().hex, parent_idparent.span_id if parent else None, attrsdict(self.attrs), ) self.token _current_span.set(self.sp) return self.sp def __exit__(self, exc_type, exc, tb): self.sp.end_ts time.time() if exc: self.sp.status ERROR self.sp.error f{exc_type.__name__}: {exc} _current_span.reset(self.token) self.tracer._finish(self.sp) return False # 不吞异常用contextvars而不是全局变量或 threading.local是因为它在 asyncio 协程里也能正确传递父子关系——这是 Agent 场景大量并发 IO的必需品。这个细节面试提到会加分。4.1 配套的成本计算与埋点装饰器# 单位元 / 1K token示例价格实际以账单为准 PRICING { gpt-4o: {in: 0.018, out: 0.072, cached_in: 0.009}, gpt-4o-mini: {in: 0.001, out: 0.004, cached_in: 0.0005}, deepseek-chat: {in: 0.001, out: 0.002, cached_in: 0.0001}, } def calc_cost(model: str, in_tok: int, out_tok: int, cached_tok: int 0) - float: p PRICING.get(model) if not p: return 0.0 fresh_in max(in_tok - cached_tok, 0) return round( (fresh_in * p[in] cached_tok * p.get(cached_in, p[in]) out_tok * p[out]) / 1000, 6, ) def traced_llm(tracer: Tracer, model: str): 装饰 LLM 调用自动记录 token、成本、ttft。 def deco(fn): def wrapper(*args, **kwargs): with tracer.span(fchat {model}, kindLLM, **{gen_ai.request.model: model}) as sp: t0 time.time() resp fn(*args, **kwargs) u resp.usage cached getattr(getattr(u, prompt_tokens_details, None), cached_tokens, 0) or 0 sp.attrs.update({ gen_ai.usage.input_tokens: u.prompt_tokens, gen_ai.usage.output_tokens: u.completion_tokens, gen_ai.usage.cached_tokens: cached, gen_ai.response.finish_reasons: [c.finish_reason for c in resp.choices], cost_cny: calc_cost(model, u.prompt_tokens, u.completion_tokens, cached), ttft_ms: round((time.time() - t0) * 1000, 2), }) # 静默故障探针被 max_tokens 截断 if any(c.finish_reason length for c in resp.choices): sp.events.append({name: truncated_output, ts: time.time()}) sp.status DEGRADED return resp return wrapper return deco def traced_tool(tracer: Tracer): def deco(fn): def wrapper(*args, **kwargs): with tracer.span(ftool {fn.__name__}, kindTOOL, **{gen_ai.tool.name: fn.__name__}) as sp: sp.attrs[arguments] json.dumps(kwargs, ensure_asciiFalse)[:2000] out fn(*args, **kwargs) sp.attrs[result_preview] str(out)[:1000] return out return wrapper return deco4.2 敏感数据处理prompt 里很可能有用户隐私。三种处理策略按合规要求选import re, hashlib PII_PATTERNS [ (re.compile(r1[3-9]\d{9}), PHONE), (re.compile(r[\w.\-][\w\-]\.\w), EMAIL), (re.compile(r\d{17}[\dXx]), IDCARD), (re.compile(r\d{16,19}), BANKCARD), ] def sanitize(text: str, mode: str mask) - str: mode: mask(脱敏保留结构) / hash(只留指纹用于去重) / drop(完全不记) if mode drop: return if mode hash: return sha256: hashlib.sha256(text.encode()).hexdigest()[:16] for pat, repl in PII_PATTERNS: text pat.sub(repl, text) return text[:8000] # 顺便截断避免 trace 存储爆炸生产环境的常见配置开发/测试环境记全量 prompt生产环境默认 mask金融医疗类业务用 hash 或 drop 并单独申请审计权限查看。五、成本与延迟归因5.1 成本归因的三个维度按会话 哪些 session 最烧钱→ 找出异常长会话、死循环 按步骤 哪个 step 占成本最大→ 通常是带全量上下文的最后一次生成 按工具 哪个工具触发的后续 LLM 调用最贵→ 返回内容过长的工具是元凶from collections import defaultdict def cost_attribution(spans: list[Span]) - dict: by_trace, by_name, by_model defaultdict(float), defaultdict(float), defaultdict(float) tok_in tok_out cached 0 for s in spans: if s.kind ! LLM: continue c s.attrs.get(cost_cny, 0.0) by_trace[s.trace_id] c by_name[s.name] c by_model[s.attrs.get(gen_ai.request.model, unknown)] c tok_in s.attrs.get(gen_ai.usage.input_tokens, 0) tok_out s.attrs.get(gen_ai.usage.output_tokens, 0) cached s.attrs.get(gen_ai.usage.cached_tokens, 0) total sum(by_trace.values()) n max(len(by_trace), 1) return { total_cost: round(total, 4), avg_cost_per_trace: round(total / n, 6), p95_cost_per_trace: round(sorted(by_trace.values())[int(n * 0.95) - 1], 6) if n 1 else 0, cache_hit_rate: round(cached / tok_in, 4) if tok_in else 0, in_out_ratio: round(tok_in / tok_out, 2) if tok_out else 0, top_steps: sorted(by_name.items(), keylambda x: -x[1])[:5], by_model: dict(by_model), }两个要重点盯的派生指标in_out_ratio输入输出比。Agent 场景这个值通常在 10:1 到 50:1因为每轮都要带完整历史。如果它持续攀升说明上下文在无节制膨胀这是成本失控最常见的原因解法是历史摘要、工具结果截断、或滑动窗口。cache_hit_rateprompt 缓存命中率。system prompt 和工具定义是每轮都重复的命中缓存能省 50%~90% 的输入成本。命中率低通常是因为在 prompt 开头放了变化的内容如时间戳、随机 id把它们挪到末尾就能大幅提升命中率——这是一个投入产出比极高的优化。5.2 延迟归因总延迟 Σ LLM 延迟 Σ 工具延迟 编排开销 排查顺序 1) 看 span 树里最宽的那条 —— 关键路径 2) LLM 慢区分 ttft 慢排队/prompt 太长还是生成慢输出太长 3) 工具慢是外部 API 慢还是没做并发本可以并行的串行了 4) 编排开销大框架本身的序列化/校验损耗LangChain 深嵌套时明显def latency_breakdown(spans: list[Span], trace_id: str) - dict: ss [s for s in spans if s.trace_id trace_id] root next((s for s in ss if s.parent_id is None), None) if not root: return {} llm sum(s.duration_ms for s in ss if s.kind LLM) tool sum(s.duration_ms for s in ss if s.kind TOOL) retr sum(s.duration_ms for s in ss if s.kind RETRIEVER) total root.duration_ms return { total_ms: total, llm_ms: llm, tool_ms: tool, retriever_ms: retr, orchestration_ms: round(total - llm - tool - retr, 2), # 可能为负有并行 slowest_span: max(ss, keylambda s: s.duration_ms).name, parallel_detected: (llm tool retr) total * 1.05, }orchestration_ms为负是个有用的信号——说明确实存在并行执行。反过来如果它是个很大的正数说明框架开销异常值得深挖。六、故障定位与告警6.1 Agent 特有的故障模式故障模式 信号 处置 ────────────────────────────────────────────────────────────── 死循环/来回横跳 同名 tool 连续调用 3 次 硬性步数上限 检测重复 或 step 数触顶 上下文膨胀 in_tokens 随 step 单调递增 历史摘要 工具结果截断 且超过阈值 静默截断 finish_reason length 提高 max_tokens 或分段生成 工具幻觉 调用了不存在的 tool_name schema 校验 白名单拦截 参数幻觉 JSON 解析失败 / 缺必填字段 结构化输出约束 重试 降级未察觉 provider 自动降级到小模型 校验 response.model 字段 检索空转 retriever 返回 0 条但继续生成 低置信兜底见 B19 超时雪崩 tool 超时 → 重试 → 更慢 熔断 超时预算分配6.2 循环检测的实现def detect_anomalies(spans: list[Span], trace_id: str) - list[dict]: ss sorted([s for s in spans if s.trace_id trace_id], keylambda s: s.start_ts) issues [] # 1. 同一工具用相同参数重复调用 → 死循环 seen {} for s in ss: if s.kind ! TOOL: continue key (s.attrs.get(gen_ai.tool.name), s.attrs.get(arguments)) seen[key] seen.get(key, 0) 1 if seen[key] 3: issues.append({type: tool_loop, tool: key[0], count: seen[key]}) # 2. 上下文单调膨胀 ins [s.attrs.get(gen_ai.usage.input_tokens, 0) for s in ss if s.kind LLM] if len(ins) 3 and all(b a for a, b in zip(ins, ins[1:])) and ins[-1] 3 * max(ins[0], 1): issues.append({type: context_explosion, trend: ins}) # 3. 静默截断 for s in ss: if length in (s.attrs.get(gen_ai.response.finish_reasons) or []): issues.append({type: silent_truncation, span: s.name}) # 4. 模型被悄悄降级 for s in ss: req, resp s.attrs.get(gen_ai.request.model), s.attrs.get(gen_ai.response.model) if req and resp and not str(resp).startswith(str(req)): issues.append({type: model_mismatch, requested: req, served: resp}) return issues6.3 告警指标设计传统的 REDRate/Error/Duration不够用Agent 要加两组。基础层 (RED) 请求量 QPS / 错误率 / P50-P95-P99 延迟 ───────────────────────────────────────────── 成本层 单请求平均成本、P95 成本、日累计成本 in_out_ratio、cache_hit_rate → 告警单请求 P95 成本环比涨 50% ───────────────────────────────────────────── 质量层Agent 特有最重要 任务完成率 agent 正常结束 / 总请求 平均步数 突增 规划退化 工具错误率 分工具统计 JSON 解析失败率 截断率 finish_reasonlength 占比 兜底触发率 低置信走兜底的比例 → 告警任务完成率跌破 90%、平均步数环比涨 30%平均步数是最灵敏的先行指标。模型侧任何退化换版本、改 prompt、上下文变长导致遵循度下降都会先反映为要绕更多弯才能完成此时完成率可能还没跌但成本和延迟已经在涨了。这是一个很有说服力的面试点。6.4 采样策略全量存 trace 成本太高prompt 和 completion 都是大文本。分层采样import random def should_sample(trace_summary: dict) - tuple[bool, str]: 返回 (是否采样, 采样原因)。异常必存正常抽样。 if trace_summary.get(status) ERROR: return True, error # 错误 100% 存 if trace_summary.get(cost, 0) 1.0: return True, high_cost # 高成本 100% 存 if trace_summary.get(latency_ms, 0) 10000: return True, slow # 慢请求 100% 存 if trace_summary.get(user_feedback) down: return True, negative_feedback # 点踩 100% 存 if trace_summary.get(steps, 0) 8: return True, many_steps # 步数异常 100% 存 return (random.random() 0.05), baseline # 其余 5% 基线采样原则异常样本全存正常样本抽样。基线采样是为了保留分布参照——只存异常会导致无法判断这个现象是不是本来就常见。七、工具选型方案特点适用Langfuse开源可自托管LLM 场景功能最全trace/评测/prompt 管理/数据集首选尤其数据不能出境时Arize Phoenix开源OTel 原生评测能力强重视评测与 OTel 生态LangSmithLangChain 官方体验最顺已深度使用 LangChainDatadog / 阿里云 ARMS与现有 APM 打通运维统一已有成熟 APM 体系自建OTel ClickHouse Grafana完全可控成本最低大规模时量大、有平台团队选型建议先用 Langfuse 或 Phoenix 快速起步埋点严格遵循 OTel GenAI 语义约定等规模上来再迁自建。因为埋点符合标准迁移只需换 exporter几乎零改造成本。这个标准先行的思路比选哪个具体产品更值得在面试里强调。一个最小可用的落地清单第 1 周 接入 tracer覆盖 LLM/TOOL/RETRIEVER 三类 span 记全 token、成本、finish_reason 第 2 周 建 dashboard完成率、平均步数、P95 延迟、单请求成本 配前三个告警 第 3 周 接入用户反馈点赞点踩打到 trace 上 异常 trace 全采样 5% 基线采样 第 4 周 把线上 badcase trace 一键导出成评测集对接 B19 的回归集 形成线上发现 → 离线固化 → CI 拦截的闭环最后这一步是精髓可观测性的终点不是看板而是闭环。线上抓到的 badcase 要能沉淀成回归用例下次改动时自动拦截否则同一个问题会反复发生。八、面试速答QAgent 的可观测性和传统微服务有什么不同A四点本质差异。第一执行路径非确定模型每次决策可能走不同分支无法像微服务那样靠代码推断链路。第二失败形态不同最危险的是HTTP 200 但答案是错的传统的错误率、延迟、饱和度这套黄金指标对这种故障完全失明。第三成本按 token 实时计费必须把成本做成一等公民指标而不是月底看账单。第四调试单元变了一次 LLM 调用要连 prompt、采样参数、输出一起记录才有意义而且 temperature 大于 0 时无法精确重放。所以 Agent 可观测性要同时覆盖系统健康和输出质量两条线。Q一次 Agent 请求你会记录哪些数据A按 span 类型分。通用字段是 trace_id、span_id、parent_id、name、kind、起止时间、状态。LLM span 记模型名、脱敏后的 prompt 和 completion、输入输出 token、缓存命中 token、温度等采样参数、finish_reason、成本、ttft、重试次数。Tool span 记工具名、入参、结果预览、是否超时。Retriever span 记 query、top_k、召回数、top1 分数、索引版本。三个最容易漏但很关键的字段是 ttft流式场景用户感知的就是它、cached_tokens不记就算不出真实成本节省、finish_reason等于 length 说明被静默截断是重要的隐性故障信号。Q为什么要用 OpenTelemetry 的 GenAI 语义约定A三个理由。一是可移植用标准属性名gen_ai.request.model、gen_ai.usage.input_tokens 这些后换 Langfuse、Phoenix、Datadog 都不用改埋点代码只换 exporter。二是能直接吃到社区的自动 instrumentation主流 SDK 都有现成的 instrumentor。三是 dashboard 模板可以复用。自定义命名的团队后面迁移时都要还债。我的实践是可以先用轻量的自建 tracer但属性命名必须从第一天就对齐 OTel 标准。QAgent 成本失控最常见的原因是什么怎么发现A最常见是上下文无节制膨胀。Agent 每一步都把完整历史带上步数越多输入 token 增长越快而且是平方级的累积效应。发现手段是盯两个派生指标一是 in_out_ratio输入输出 token 比Agent 场景正常在 10:1 到 50:1持续攀升就是膨胀信号二是 input_tokens 随 step 的单调递增趋势可以直接写规则检测。解法是历史摘要、工具结果截断、滑动窗口。另一个被忽视的点是 prompt 缓存命中率——system prompt 和工具定义每轮都重复命中缓存能省一大半输入成本很多团队命中率低是因为在 prompt 开头放了时间戳或随机 id挪到末尾就能大幅改善这是投产比极高的优化。Q怎么发现 Agent 陷入死循环A三层防护。第一层是硬性上限最大步数和总超时预算必须有这是兜底。第二层是重复检测把工具名加参数哈希作为 key 计数同一组合连续出现三次就判定为循环并中断。第三层是语义层面的检测比如连续两次反思的内容相似度过高、或者规划输出在两个动作间来回横跳。在监控上平均步数是最灵敏的先行指标——任何模型侧的退化都会先表现为要绕更多弯此时完成率可能还没跌但成本延迟已经在涨了。Qtrace 数据量太大怎么控制成本A分层采样加字段治理。采样上异常样本 100% 存——错误、高成本、慢请求、用户点踩、步数异常都必存正常样本 5% 基线采样这个基线不能省否则只有异常数据会导致无法判断某现象是不是本来就常见。字段上prompt 和 completion 是存储大头生产环境默认做 PII 脱敏加长度截断8K 字符金融医疗类业务用哈希指纹或干脆不记原文单独申请审计权限查看。存储上冷热分离最近 7 天全字段在热存储历史数据只保留结构化指标不留原文。Q如何用可观测性数据反哺模型质量A做成闭环这是可观测性的终点。具体路径是线上 trace 中筛出 badcase用户点踩、走了兜底、步数异常、JSON 解析失败人工确认后一键导出成评测用例加入离线回归集回归集接入 CI下次改 prompt 或换模型时自动跑挂了就阻断发布。这样同一个问题不会反复发生。再进一步高质量的成功 trace 可以沉淀为 few-shot 示例库或 SFT 数据。判断一个团队可观测性做得好不好就看有没有这个闭环——只有看板没有闭环本质上只是把问题可视化了没有解决问题。QAgent 有哪些静默故障怎么监控A最典型的四种。一是输出被 max_tokens 截断finish_reason 等于 length 但 HTTP 依然 200监控靠统计截断率。二是模型被 provider 悄悄降级请求的是大模型实际服务的是小模型靠校验 response.model 和 request.model 是否一致来发现。三是检索空转retriever 返回 0 条但流程继续往下走生成模型只能凭空编靠检索结果数和 top1 分数阈值告警。四是工具返回了错误内容但没抛异常比如 SQL 查出空表需要在工具层做结果合理性校验。这类故障的共同特点是不抛异常、不报错只能靠业务语义层面的探针主动发现。九、高频追问清单为什么用 contextvars 而不是 threading.local 来传递 span 上下文流式输出场景下span 的结束时间应该记在什么时候ttft 怎么准确测量多智能体协作时trace 树应该怎么组织子 agent 是子 span 还是独立 trace采样率 5% 的情况下怎么保证 P99 延迟统计的准确性prompt 缓存命中率低除了前缀变化还有哪些原因如何做跨 provider 的成本对比token 计数口径不同怎么归一化trace 里存了脱敏后的 prompt但用户要求删除个人数据怎么做定向清除如果 Agent 用了流式加并行工具调用延迟归因的口径该怎么定义怎么把线上 trace 自动转化成可复现的回归测试用例非确定性怎么处理OTel 的 Metrics 和 Traces 在 Agent 场景怎么分工哪些指标该走 metrics如果一次 Agent 请求跨了多个服务网关、编排、模型代理trace 怎么串起来可观测性系统本身挂了怎么办埋点会不会拖慢主链路至此 B 系列前二十篇完成了从 ReAct、记忆、MCP、多智能体、安全到规划、Function Calling、RAG 评估、可观测性的完整覆盖。今天这两篇B19 B20是一对前者管离线怎么度量质量后者管线上正在发生什么合起来才是完整的智能体质量工程。配合 A 系列今天的《模型量化》和《评测体系》看刚好构成模型侧优化—模型侧评测—系统侧评测—系统侧观测的完整闭环。下一阶段 B 系列会转向更工程化的主题Agent 的部署架构、状态管理与容灾、以及大规模并发下的编排优化。
返回列表