一次 LLM 调用如何变成 span/event/metric
30 秒导读: 上一章 讲了
openlit.init()怎么用 wrapt 把client.chat.completions.create换成带遥测的壳。这一章钻进那层壳里:一次真实的 chat 调用返回后, OpenLIT 怎么把它拆成"输入/输出消息、token 数、美元成本、首字延迟",再一次性写成三种遥测信号—— 一条 trace span、一个结构化 event、一组 metric。这是整个 SDK 工程含量最高、也最值钱的一段: 别的可观测工具只告诉你"调了一次模型",OpenLIT 告诉你"这次调用花了 0.0023 美元、首字等了 380ms"。
1. 这章解决什么(先看全景)
OpenLIT 的卖点是把一次 LLM 调用变成可计量的遥测。难点不在"知道调用发生了"——wrapt 打桩已经解决; 难点在从 provider 返回的原始对象里,精确抽出人和机器都能用的结构化事实,并且这些事实要同时以三种 OTel 信号落地,还要对齐上游 OpenTelemetry 的 GenAI 语义约定(下称 gen-ai semconv)。
三种信号各自的角色,不要混:
| 信号 | 是什么 | 装什么 | 谁消费 |
|---|---|---|---|
| span(trace) | 这次调用的一段耗时记录 | 请求参数、响应元数据、token、成本、(可选)消息全文 | 看单次调用的瀑布图 |
| event(log) | 一条结构化日志事件 | 与 span 同源的元数据 + 结构化消息 | 消息级检索、审计、回放 |
| metric | 可聚合的数字 | token 计数、时延、成本(按维度打标签) | 仪表盘、告警、按模型/环境聚合 |
一次 chat 调用的信号生产线(从左到右,数据只在这一条链上流动):
被打桩的 create() 返回
│
▼
┌───────────────────────┐ 非流式: 一次性拿到完整 response
│ wrapper (openai.py) │ 流式: 每个 chunk 累积, 收尾时结算
└───────────────────────┘
│ 把响应塞进一个 scope 对象(鸭子类型的属性袋)
▼
┌────────────────────────────────────────────────┐
│ common_chat_logic() ← 本章主角(utils.py:1342) │
│ ① 结算 TBT/TTFT ② 估/取 token ③ 算成本 │
│ ④ 抽取&格式化消息 ⑤ 写 span 属性 │
│ ⑥ 发 event ⑦ 记 metric │
└────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
span event metric
(trace) (log) (聚合数字)
读法: 上半段是"谁把数据喂进来"(wrapper),下半段是"怎么把一份数据同时变成三种信号"
(common_chat_logic)。本章几乎全部篇幅在讲下半段。
本章聚焦 chat 路径。embedding / image / audio / responses API 是同构的——同样的
scope→common_*_logic→ span+event+metric 套路,只是抽取字段和成本公式不同,末尾一句话带过。
2. 入口:wrapper 怎么把响应交给遥测层
2.1 两条路径,一个汇合点
被打桩的 chat 调用有两种形态,wrapper 分开处理,但最终都汇到同一个函数 common_chat_logic:
| 形态 | 入口函数 | 特点 |
|---|---|---|
| 非流式 | process_chat_response(utils.py:1706) | 一次拿到完整 response,当场结算 |
| 流式 | process_streaming_chat_response(utils.py:1677) | 先逐 chunk 累积,StopIteration 时结算 |
非流式的调用点在 wrapper 里,response 到手就交给它(openai.py:212 process_chat_response)。
流式则包了一层 TracedSyncStream:每 __next__ 一个 chunk 就调 process_chat_chunk 累积状态,
迭代耗尽(StopIteration)时才在 with self._span: 里结算(openai.py:98-119 TracedSyncStream.__next__)。
2.2 scope:一个鸭子类型的"属性袋"
两条路径都先造一个空对象当临时状态容器,OpenLIT 里到处叫它 scope。非流式版是当场 new 一个空壳,
把 response 里的字段一项项挂上去:
# 真实源码节选 · utils.py:1727 process_chat_response
scope = type("GenericScope", (), {})() # 空对象,纯当属性袋用
scope._start_time = start_time
scope._llmresponse = " ".join( # 把各 choice 的正文拼成一条字符串
(choice.get("message", {}).get("content") or "")
for choice in response_dict.get("choices", [])
)
scope._input_tokens = response_dict.get("usage", {}).get("prompt_tokens", 0)
scope._output_tokens = response_dict.get("usage", {}).get("completion_tokens", 0)
流式版的 scope 就是 TracedSyncStream 自己:self._llmresponse、self._tools、self._timestamps
等在 __init__ 里初始化(openai.py:64-82),然后逐 chunk 往上累加。
为什么这么设计: common_chat_logic 不关心数据来自"一次完整响应"还是"一堆 chunk 拼出来的",
它只认 scope.* 上那些约定好的下划线属性。于是流式和非流式能共用同一段结算逻辑——这是 OpenLIT
每个 provider instrumentation 的通用模式。
先把 response/chunk 归一成 dict 的是 response_as_dict(__helpers.py:241):Pydantic 模型走
model_dump,httpx 原始响应走 .parse(),已经是 dict 就原样返回。后面所有 .get(...) 都建立在
这层归一之上。
3. 流式计时:TTFT 与 TBT
非流式没有"逐字"概念,时延就是"从发出到拿到"一个数。流式才有两个关键体验指标:
| 指标 | 全称 | 白话 | 语义约定 key |
|---|---|---|---|
| TTFT | time to first token | 首字延迟:发出到第一个 chunk 多久 | gen_ai.server.time_to_first_token |
| TBT | time between tokens | 字间延迟:相邻 chunk 的平均间隔 | gen_ai.server.time_per_output_token |
计时的原料是每个 chunk 到达的时间戳,在 process_chat_chunk 里逐个记下,并在第一个 chunk
到达时立刻算出 TTFT:
# 真实源码节选 · utils.py:665 process_chat_chunk
end_time = time.time()
scope._timestamps.append(end_time)
if len(scope._timestamps) == 1: # 恰好第一个 chunk
scope._ttft = calculate_ttft(scope._timestamps, scope._start_time)
同一个函数还负责把碎片拼回整体:正文 delta.content 累加到 scope._llmresponse;工具调用参数
按 index 分片续接(utils.py:685-710);token 用量在带 include_usage 的末尾那个 choices 为空的
chunk里才出现,所以取 usage 的代码特意放在 choices 判断块之外(utils.py:726-729)。
TTFT/TBT 的实际公式很朴素(__helpers.py:465 calculate_ttft、:475 calculate_tbt):
- TTFT =
第一个时间戳 - start_time。 - TBT = 相邻时间戳差值的算术平均;只有一个时间戳时为 0。
TBT 在结算时才算,因为要等所有 chunk 到齐:
# 真实源码节选 · utils.py:1358 common_chat_logic 开头
scope._end_time = time.time()
if len(scope._timestamps) > 1:
scope._tbt = calculate_tbt(scope._timestamps)
非流式路径把 scope._timestamps 设成空列表、_ttft 直接等于总耗时、_tbt=0(utils.py:1748-1749),
于是同一段结算逻辑对非流式也成立——又是"归一化后共用"的体现。
4. common_chat_logic 主干(七步)
这是本章的心脏(utils.py:1342 common_chat_logic)。它的输入是填好的 scope,输出是三种信号。
按执行顺序拆成七步:
① 结算时延 _tbt = calculate_tbt(...) (utils.py:1358)
② 定 token 有 usage 用真值; 否则 general_tokens 估 (utils.py:1374)
③ 算成本 get_chat_model_cost(...) (utils.py:1385)
④ 写公共属性 common_span_attributes(...) (utils.py:1396)
⑤ 写请求/响应 温度/seed/tool/token/cost → span 属性 (utils.py:1418-1543)
⑥ 抽取&格式化 build_input/output_messages(...) (utils.py:1548-1579)
⑦ 发 event emit_inference_event(...) (utils.py:1596)
记 metric record_completion_metrics(...) (utils.py:1655)
下面三节分别展开②③(成本)、⑥(消息抽取)、④⑤⑦+metric(三种落地)——这三块是全章精华。
5. 消息抽取与格式化
模型的输入输出要以两种形态落地,对应两类消费者:
- 一条给人读的字符串——用于 token 估算的原料。由
format_content产出。 - 一个符合 gen-ai semconv 的结构化数组——用于 span 属性和 event。由
build_*_messages产出。
5.1 format_content:压平成一条字符串
format_content(utils.py:99)把 messages 数组压成 role: content\n role: content 的纯文本,同时:
- 多模态 content(list)只保留文本和非 data-URI 的图片 URL——base64 图不塞进遥测(utils.py:138-157)。
content=None的工具调用回合退化成[N tool call(s)]摘要(utils.py:120-127)。- 同时认 Chat Completions 格式(
type: text)和 Responses API 格式(type: input_text)。
它的产物主要喂给 token 估算(见 §6),不是最终写进 span 的结构。
5.2 build_input_messages / build_output_messages:结构化成 OTel schema
真正写进 span/event 的是结构化数组,对齐上游 OTel 的 gen-ai-input-messages / output-messages schema (函数 docstring 直接贴了 spec 链接,utils.py:167-168)。形态是:
[ { "role": "user",
"parts": [ {"type":"text","content":"..."},
{"type":"uri","modality":"image","uri":"https://..."} ] } ]
四个抽取器各管一段,都写成"dict 和 Pydantic 对象都能吃"的防御式代码:
| 函数 | 位置 | 抽什么 | 要点 |
|---|---|---|---|
build_input_messages | utils.py:165 | 请求侧消息 | 图片跳过 data URI;tool 角色转成 tool_call_response part(utils.py:286-300) |
build_output_messages | utils.py:323 | 响应正文 + 工具调用 | OpenAI 的 name/arguments 嵌在 function 键下,兼容扁平结构的其他 provider(utils.py:348-372) |
build_system_instructions_from_messages | utils.py:478 | system 角色消息 | 单独抽出 → gen_ai.system_instructions |
build_tool_definitions | utils.py:422 | 请求的 tools | 抽 {type,name,description,parameters} |
build_output_messages 还做了一件对齐 semconv 的小事:把 OpenAI 的 finish reason 映射成 OTel 词表——
tool_calls/function_call 都归一成 tool_call(utils.py:400-406)。
5.3 finish reason 映射(OpenAI → OTel)
| OpenAI finish_reason | OTel finish_reason |
|---|---|
stop | stop |
length | length |
content_filter | content_filter |
tool_calls | tool_call |
function_call | tool_call |
5.4 截断:capture_message_content 与 max_content_length
消息全文是否落地由 capture_message_content 开关控制。关键设计:即使不落全文,消息结构照样构建——
因为下游的 event 还要用它们算 agent 版本哈希、发元数据(utils.py:1566-1579 注释说得很清楚)。真正把全文
写进 span 的只有开关打开时的这一步(utils.py:1584):
# 真实源码节选 · utils.py:1584 common_chat_logic
if capture_message_content:
_set_span_messages_as_array(scope._span, input_msgs, output_msgs)
_set_span_messages_as_array(utils.py:508)在 json.dumps 之前,先对每条消息就地截断:
# 真实源码节选 · utils.py:511
truncate_message_content(input_messages)
truncate_message_content(output_messages)
截断规则在 truncate_content(__helpers.py:177):读全局配置 OpenlitConfig.max_content_length,
None/0/-1 都表示不截断;正整数则截到那么多字符并补 ...。truncate_message_content
(__helpers.py:201)负责走 parts 结构,对每个 text content 和 tool_call_response 的 response
字段逐个应用。
6. 成本计算:token → 美元
这是 OpenLIT 区别于普通 tracing 的核心增量。分三步:定 token → 查价 → 套公式。
6.1 token 从哪来:真值优先,估算兜底
# 真实源码节选 · utils.py:1374 common_chat_logic
if hasattr(scope, "_input_tokens") and scope._input_tokens:
input_tokens = scope._input_tokens # provider 回的真值
output_tokens = scope._output_tokens
else:
input_tokens = general_tokens(prompt) # 没有 usage 时的粗估
output_tokens = general_tokens(scope._llmresponse)
兜底的 general_tokens(__helpers.py:287)是个极粗的近似:math.ceil(len(text) / 2)——按"两个
字符约等于一个 token"拍脑袋。这只在 provider 没返回 usage(如某些流式没开 include_usage)时启用,
真值永远优先。
6.2 fetch_pricing_info:价格表从哪来
成本要有单价。单价来自一张 pricing 表,在 init 时一次性拉取(init.py:422
fetch_pricing_info),之后作为 pricing_info 一路透传进每次结算,不重复拉。
fetch_pricing_info(__helpers.py:414)的取值优先级:
传了 pricing_json?
├─ 是 URL → requests.get 拉远程 JSON
├─ 是文件路径 → 本地 open 读 JSON
└─ 没传 → 拉官方默认表
raw.githubusercontent.com/openlit/openlit/main/assets/pricing.json
任何失败 → 返回 {} (于是后续成本一律算 0,不炸)
表的结构是按模态分区的嵌套 dict:{"chat": {model: {...}}, "embeddings": {...}, "images": {...}, "audio": {...}}。
6.3 get_chat_model_cost:套公式 + 缓存感知
# 真实源码节选 · utils.py:1385 common_chat_logic
cost = get_chat_model_cost(
request_model, pricing_info, input_tokens, output_tokens,
cache_read_tokens=getattr(scope, "_cache_read_input_tokens", 0),
cache_creation_tokens=getattr(scope, "_cache_creation_input_tokens", 0),
prompt_tokens_include_cache=True, # OpenAI 的 prompt_tokens 已含缓存读
)
get_chat_model_cost(__helpers.py:295)的基本公式很简单——按千 token 计价:
cost = (prompt_tokens/1000)*promptPrice + (completion_tokens/1000)*completionPrice
精妙在缓存感知定价(__helpers.py:343-357):某些模型有 cacheReadPrice/cacheCreationPrice,
命中缓存的 token 按更便宜的缓存价单独结算。这里有个双重计费 陷阱——不同 provider 对 prompt_tokens
的口径不一样:
| provider 口径 | prompt_tokens 含义 | 处理 |
|---|---|---|
| Anthropic 原生 | 不含缓存 token,需另加 | prompt_tokens_include_cache=False(默认) |
| OpenAI / LangChain 归一 | 已含缓存 token | prompt_tokens_include_cache=True,把缓存 token 从计费基数里减掉 |
OpenAI 走后者:先按缓存价给缓存 token 计费,再把这部分从 billable_prompt_tokens 里扣掉,避免同一批
token 被普通价和缓存价算两遍。没有配置缓存价的模型完全不受影响,行为和旧版一致。model 名带 /
(如 azure/gpt-4o)时会退一步用斜杠后半段再查一次表(__helpers.py:332-333)。
6.4 其他模态一句话带过
同构:embedding 用 get_embed_model_cost(按 token 单价,__helpers.py:369);image 用
get_image_model_cost(按 [model][quality][size] 三级查表直接取价,__helpers.py:387);audio 用
get_audio_model_cost(按字符数或时长,__helpers.py:399)。都遵循"查 pricing_info 对应分区,查不到
返回 0"的同一套路。
7. 落到三种信号
token 和成本算完,common_chat_logic 把这份数据同时写进 span、event、metric。
7.1 span:公共属性 + 自定义属性 + 消息
先写一批所有 provider 共享的骨架属性(common_span_attributes,__helpers.py:802):operation、
provider、server 地址端口、环境、service name、is_stream、TBT、TTFT、SDK 版本。它的最后一步会调
_apply_custom_span_attributes。
再写 chat 特有的请求/响应/成本属性(utils.py:1418-1543):温度、seed、frequency/presence penalty、
stop、tool 的 name/id/args、gen_ai.usage.input_tokens、output_tokens、gen_ai.usage.cost、缓存
token(即使是 0 也写,utils.py:1533-1543)。
_apply_custom_span_attributes(__helpers.py:1014)是用户注入自定义标签的挂钩:先盖全局属性
(init 时设的),再盖 context 级属性(using_attributes 注入的),后者覆盖前者。这让用户能给某段代码
下的所有 span 打统一业务标签(如 tenant_id)。
7.2 event:结构化 inference 事件
无论 capture_message_content 开没开,只要配了 event provider,就发一个 inference event
(utils.py:1596)——因为 token 数、finish reason、agent 版本哈希这些元数据也值得进事件流:
# 真实源码节选 · utils.py:1630 common_chat_logic
emit_inference_event(
event_provider=event_provider,
operation_name=SemanticConvention.GEN_AI_OPERATION_TYPE_CHAT,
request_model=request_model,
response_model=scope._response_model,
input_messages=input_msgs if capture_message_content else [], # 关就发空数组
output_messages=output_msgs if capture_message_content else [],
tool_definitions=tool_defs,
**extra, # response_id、finish_reasons、温度、token 计数、缓存 token...
)
emit_inference_event(utils.py:531)干两件事:把一堆 **extra_attrs 映射到 semconv 的正式 key
(一大串 if key == ...,utils.py:590-644),然后交给 otel_event 造出一条 LogRecord
(事件名 gen_ai.client.inference.operation.details),body 留空——所有数据都进 attributes,
这是 spec 的要求(utils.py:646-651)。
otel_event(__helpers.py:672)在造 LogRecord 前也会合并全局/context 自定义属性,和 span 那边对称。
OpenlitConfig.disable_events 为真时不发(utils.py:653)。
7.3 metric:可聚合的数字
最后记 metric(utils.py:1655 record_completion_metrics)。这段负责把数字打上维度标签、灌进
OTel 的 instrument。标签由 create_metrics_attributes(__helpers.py:488)统一构造:service、环境、
operation、provider、请求/响应模型、server 地址端口——都是低基数维度,能安全聚合。
record_completion_metrics(__helpers.py:845)记的这几组数字,注意 token type 的 semconv 约束:
| metric | key | 备 注 |
|---|---|---|
| token 用量 | gen_ai.client.token.usage | 只能打 input/output 两种 type,不能记 reasoning/total |
| 操作时长 | gen_ai.client.operation.duration | 出错时带 error.type |
| 首字延迟 | gen_ai.server.time_to_first_token | 仅流式 |
| 字间延迟 | gen_ai.server.time_per_output_token | 有 TBT 才记 |
| 成本 | gen_ai.usage.cost | OpenLIT 自家扩展,不属 OTel gen-ai semconv |
失败路径也记 metric:wrapper 里 provider 抛异常时,用 0 token / 0 成本 + error_type 记一条,让失败
调用也进得了聚合(openai.py:189-208)。
8. 语义约定层:SemanticConvention 与三层对齐
上面所有 SemanticConvention.XXX 都来自一个集中的常量类(semcov/init.py)。它的价值是:让
OpenLIT 尽量用上游 OTel 定义的官方 key,官方没有的才自己造,并且分三层标清来源:
| 层 | 是什么 | 例子 |
|---|---|---|
| TIER 1 | 官方 OTel gen-ai / server / error 属性 | gen_ai.operation.name、server.address、error.type |
| TIER 2 | OpenAI / provider 专有 | openai.api.type(chat_completions/responses) |
| TIER 3 | OpenLIT 自家扩展 | gen_ai.usage.cost、gen_ai.usage.input_tokens |
TIER 1 的常量不是硬编码字符串,而是从上游库里取,取不到再兜底。这层兜底靠 _get_otel_attr
(semcov/init.py:11):
# 真实源码节选 · semcov/__init__.py:180
GEN_AI_REQUEST_MODEL = _get_otel_attr(
OTelGenAIAttributes, "GEN_AI_REQUEST_MODEL", "gen_ai.request.model"
)
_get_otel_attr 尝试从 opentelemetry.semconv._incubating.attributes 读官方常量;若这个可选依赖没装
(HAS_OFFICIAL_OTEL_SEMCONV=False,semcov/init.py:33-53),或某个属性在当前版本里不存在,就退回
第三个参数那个字面量。好处: 上游 semconv 升级、常量改名时,只要装了官方库就自动跟随;没装也能靠兜底
字符串照常工作——既对齐标准又不硬依赖。
至于 §7.2 里那个 gen_ai.client.inference.operation.details 事件名和 gen_ai.input.messages/
gen_ai.output.messages/gen_ai.system_instructions,是 OTel semconv v1.29+ 才有的新约定,直接写成
常量(semcov/init.py:124-132)。
9. 巧妙之处(值得借鉴)
- 归一化 + 共用结算:流式和非流式先各自把状态填进
scope,再共用common_chat_logic。避免两套 几乎一样的结算代码各写各的漂移。(utils.py:1342) - 消息构建与内容捕获解耦:
build_*_messages永远执行,只有全文落地受capture_message_content控制。于是关掉内容捕获后,token/finish reason/版本哈希等元数据仍进 event。(utils.py:1566-1584) - 缓存感知定价防双重计费:用一个
prompt_tokens_include_cache布尔吸收 provider 间 token 口径差异, 只在配了缓存价时才动手,无缓存价的模型行为零变化。(__helpers.py:307-357) - semconv 软依赖:
_get_otel_attr让"对齐官方标准"和"不强制安装官方库"两全。(semcov/init.py:11) - 成本永不炸:查价链路任何一步失败都返回 0 而非抛异常——遥测绝不能拖垮业务调用。 (__helpers.py:364、fetch_pricing_info 全 try/except)
10. 边界与局限(诚实)
- 成本准确性依赖那张 pricing 表。表里没有的模型一律算 0(__helpers.py:334-335);新模型上线到表更新 之间有空窗。
- 无 usage 时 token 是粗估。
general_tokens的"字符数/2"和真实 tokenizer 差得远,只是兜底 (__helpers.py:292)。想要准,流式得开stream_options={"include_usage": True}。 - base64 图片、超长文本不进遥测:data URI 被跳过(utils.py:144),文本按
max_content_length截断。 这是刻意的隐私/体积取舍,不是 bug。 - 成本 metric 不是 OTel 标准:
gen_ai.usage.cost是 OpenLIT 私有扩展(semcov/init.py:516), 换成纯 OTel 后端消费时要知道它不在 gen-ai semconv 里。
11. 代码地图(导航索引)
用符号名 grep 定位,比行号抗上游漂移。
| 主题 | 文件 | 符号 |
|---|---|---|
| chat 结算主干(七步) | sdk/python/src/openlit/instrumentation/openai/utils.py | common_chat_logic |
| 非流式入口 | sdk/python/src/openlit/instrumentation/openai/utils.py | process_chat_response |
| 流式入口 | sdk/python/src/openlit/instrumentation/openai/utils.py | process_streaming_chat_response |
| 流式逐 chunk 累积 + 计时 | sdk/python/src/openlit/instrumentation/openai/utils.py | process_chat_chunk |
| 消息压平成字符串 | sdk/python/src/openlit/instrumentation/openai/utils.py | format_content |
| 结构化输入消息 | sdk/python/src/openlit/instrumentation/openai/utils.py | build_input_messages |
| 结构化输出消息 + finish 映射 | sdk/python/src/openlit/instrumentation/openai/utils.py | build_output_messages |
| 抽 system 指令 / tool 定义 | sdk/python/src/openlit/instrumentation/openai/utils.py | build_system_instructions_from_messages / build_tool_definitions |
| 消息写进 span(含截断) | sdk/python/src/openlit/instrumentation/openai/utils.py | _set_span_messages_as_array |
| 发 inference event | sdk/python/src/openlit/instrumentation/openai/utils.py | emit_inference_event |
| chat 成本(缓存感知) | sdk/python/src/openlit/__helpers.py | get_chat_model_cost |
| embed/image/audio 成本 | sdk/python/src/openlit/__helpers.py | get_embed_model_cost / get_image_model_cost / get_audio_model_cost |
| 拉价格表 | sdk/python/src/openlit/__helpers.py | fetch_pricing_info |
| token 粗估兜底 | sdk/python/src/openlit/__helpers.py | general_tokens |
| TTFT / TBT | sdk/python/src/openlit/__helpers.py | calculate_ttft / calculate_tbt |
| 公共 span 属性 | sdk/python/src/openlit/__helpers.py | common_span_attributes |
| 自定义 span 属性挂钩 | sdk/python/src/openlit/__helpers.py | _apply_custom_span_attributes |
| 造 LogRecord 事件 | sdk/python/src/openlit/__helpers.py | otel_event |
| 记 completion metric | sdk/python/src/openlit/__helpers.py | record_completion_metrics |
| metric 维度标签 | sdk/python/src/openlit/__helpers.py | create_metrics_attributes |
| 内容截断 | sdk/python/src/openlit/__helpers.py | truncate_content / truncate_message_content |
| 语义约定常量 + 软依赖兜底 | sdk/python/src/openlit/semcov/init.py | SemanticConvention / _get_otel_attr |
| wrapper(打桩壳 + 流式包装) | sdk/python/src/openlit/instrumentation/openai/openai.py | chat_completions / TracedSyncStream |
相邻章节: 上游装配见 01-instrumentation-core.md;遥测落库见 03-backend-collector-clickhouse.md;全景见 index.md。