
一、背景單次 LLM 調(diào)用這種形態(tài)下鏈路線性日志即全景打一行日志就記錄「調(diào)用了什么模型、花了多久、返回了什么」就結(jié)束了用戶提問 ──→ 調(diào)用一次模型 ──→ 返回答案而 Agent 具備多輪推理、工具嵌套及強因果依賴等特征執(zhí)行過程呈樹狀拓撲且耗時跨度大扁平日志無法承載層級耗時統(tǒng)計與因果依賴關(guān)系的表達。從 1 次調(diào)用變成幾十次日志行數(shù)變多人眼看不過來從毫秒變成分鐘耗時本身成了需要定位的問題從線性變成有因果第 3 輪依賴第 2 輪的工具結(jié)果平鋪日志表達不了這種關(guān)系。日志記錄的是「發(fā)生了什么」而 Agent 的執(zhí)行還有一層結(jié)構(gòu)做了什么、按什么順序做、時間花在哪一步。OpenTelemetry簡稱 OTel它用一套標準的數(shù)據(jù)模型把一次執(zhí)行記錄成一棵有層級、有因果、帶耗時的樹Trace / Span并統(tǒng)一描述、采集和傳輸。Agent 的執(zhí)行結(jié)構(gòu)本身就是一棵樹Agent 關(guān)心的「層級 因果 耗時」正好是 span 樹的三個能力有父子層級、有調(diào)用因果、有起止耗時。二、核心概念2.1 OTel 的定位與核心數(shù)據(jù)OTel 有點像 HTTP 協(xié)議HTTP 定義了請求/響應(yīng)長什么樣、怎么傳但它不是瀏覽器、也不是服務(wù)器OTel 定義了運行數(shù)據(jù)長什么樣、怎么傳。OTel 記錄的最小單元叫span{ name: claude, trace_id: 4bf92f3577b34da6a3ce929d0e0e4736, span_id: 00f067aa0ba902b7, parent_id: 1c987a38b1408550, start_time: 2026-09-21T03:15:55.631Z, end_time: 2026-09-21T03:15:56.736Z, attributes: { gen_ai.request.model: claude }, events: [ { name: first-token } ], status: UNSET, kind: CLIENT }name這個 span 在干什么顯示在瀑布圖上trace_id屬于哪一次完整調(diào)用整棵樹共享span_id自身編號parent_id父 span 的編號start_time / end_time時間區(qū)間相減就是耗時attributes附加的結(jié)構(gòu)化信息events瞬時事件statusUNSET / OK / ERRORkindCLIENT / SERVER 等后端靠它畫拓撲圖。兩個關(guān)鍵點span 是時間區(qū)間不是時間點。 這是它和日志最根本的區(qū)別。parent_id 是拼出一棵樹的唯一依據(jù)。 trace 一次完整調(diào)用產(chǎn)生的所有 span 拼成的樹trace_id表示屬于哪棵樹把散落的 span 歸攏成一個 tracespan_id標識自身parent_span_id指向父節(jié)點是拼樹的唯一依據(jù)。后端只做兩件事按 trace_id 分組 → 按 parent_id 建樹判斷根節(jié)點很簡單看它有沒有父節(jié)點沒有父節(jié)點的就是根。2.2 上下關(guān)系確定在同一個程序里OTel 會自動把新 span 接到當前的父節(jié)點上with tracer.start_as_current_span(turn-0): # 當前是 turn-0 with tracer.start_as_current_span(llm): # llm 自動成為 turn-0 的子節(jié)點 pass跨進程就不一樣兩個程序互相看不見各記各的。比如 Agent 要查訂單它自己查不了得讓訂單服務(wù)去查。在 OTel 里這兩個值叫trace_id和span_id通常放在 HTTP 請求頭里SDK 會自動帶上和讀取??邕M程時如果沒有這個傳遞兩邊的記錄就永遠對不上只會得到兩棵互不相干的樹。2.3 三大信號Trace、Metric、LogOTel 只定義三類數(shù)據(jù)叫信號它們通過trace_id關(guān)聯(lián)形成從粗到細的下鉆鏈Trace 和 Metric 的成本差異很直接100 萬個請求不可能每個都存一棵完整的樹metrics 只存聚合數(shù)字100 萬個請求也只是幾個數(shù)。2.4 組件與鏈路從 SDK 到 CollectorOTel 分兩個包opentelemetry-api只有接口opentelemetry-sdk給應(yīng)用依賴。如果 openai 庫內(nèi)部埋點、依賴的是api應(yīng)用裝了 SDK埋點生效沒裝API 調(diào)用變成空操作什么都不發(fā)生也不報錯。這樣庫可以零成本內(nèi)置埋點。五個核心組件對應(yīng)代碼resource Resource.create({service.name: my-agent}) # ← Resource provider TracerProvider(resourceresource) # ← Provider provider.add_span_processor(BatchSpanProcessor(exporter)) # ← Processor Exporter trace.set_tracer_provider(provider) tracer trace.get_tracer(__name__) # ← TracerOTLP 和 Collector。OTLP 是傳輸協(xié)議。Collector 是獨立進程生產(chǎn)環(huán)境常用它做四件事接收統(tǒng)一入口、處理采樣、脫敏、富化、路由一份數(shù)據(jù)發(fā)多個后端、緩沖。改策略不用改應(yīng)用——把采樣率從 10% 調(diào)到 5%改 Collector 配置即可不用重新部署 Agent。完整架構(gòu)三、解決的問題3.1 日志與 Trace 的分工出錯看堆棧、看某個變量的值 → 日志看某一步花了多久、時間花在哪一步 → trace在幾十條運行里找最慢的、統(tǒng)計工具失敗率、對比兩個模型 → trace。日志回答「發(fā)生了什么」trace 回答「為什么慢、誰拖累了誰」。兩者都真實存在不是二選一。OTel 不取代現(xiàn)有日志。掛一個 handler業(yè)務(wù)代碼不用改日志同時進兩個地方OTel 那份自動帶上trace_id一條帶trace_id的日志把散落的文本變成樹上的一個錨點可以從日志直接跳到對應(yīng)的 trace。3.2 從日志到執(zhí)行樹同樣是記錄一次運行日志是平鋪文本trace 是有層級的樹日志記錄發(fā)生了什么但很難表達這些事情之間的關(guān)系哪個 LLM 調(diào)用屬于哪一輪、哪個 Tool 是哪次決策觸發(fā)的、整個 Agent 在哪里耗時。一個排障例子① Metric 發(fā)現(xiàn)異常P99 從 80ms 漲到 210ms慢在模型調(diào)用 ② Trace 定位到具體會話turn-1 里 tool:query_order 被調(diào)了 3 次每次都 1.5s ③ Log 找到根因從 trace_id 一鍵跳過來 ERROR query_order 超時第 3 次放棄 → 不是模型慢是訂單服務(wù)接口超時模型被迫反復重試3.3 Attributes記錄屬性信息span 自帶的信息只有四樣name → 這是什么操作只能有 1 個值 start / end → 什么時候、多久 parent_id → 誰在誰里面 attributes → 其余一切name必須低基數(shù)它是聚合主鍵只能承擔分類。實際要記錄的還包括模型、token 數(shù)、輪次、輸入輸出、失敗原因這些放在 attributes 里。同一棵 span 樹跑兩遍A 不 set 屬性、B set 屬性結(jié)構(gòu)、名字、耗時完全一樣差別在能回答的問題哪一步最慢兩者都能答這是唯一只靠時間就能答的token花費、上下文第幾輪開始漲、哪個工具最容易失敗只有 B 能答。只看 span 結(jié)構(gòu)能分析的只有耗時要分析業(yè)務(wù)就得靠 attributes。屬性之間有依賴位置也有要求要畫 token 增長曲線turn.index和gen_ai.usage.input_tokens必須同時存在少一個就只剩三個孤立數(shù)字排不出順序?qū)傩远加浟说珤戾e span分析也用不了。Trace 決定能不能還原過程Attributes 決定能不能分析過程。四、編碼與實測4.1 字段規(guī)范核心是用行業(yè)約定的名字后端靠字段名自動渲染面板——用model_name而不是gen_ai.request.model后端的面板、成本計算、LLM 視圖都會失效。LLM / Agent 場景已有標準OTel 語義約定模型 spangen_ai.request.model、gen_ai.usage.input_tokens、gen_ai.usage.output_tokens、gen_ai.server.time_to_first_token工具 spantool.name、tool.arguments輸入輸出input.value / output.value自定義業(yè)務(wù)字段加自己的前綴比如cs.intent、cs.prompt_version避免和規(guī)范撞名。是 span 還是 attribute用兩步判斷第一步這一步有自己的耗時嗎 是 → 開 span 第二步這是某一步的產(chǎn)出/結(jié)果嗎 是 → 掛 attribute有耗時的步驟用 span步驟的產(chǎn)出用 attribute。比如智能客服前置三步——意圖識別、query 改寫、槽位抽取——各自都耗時所以各是一個 span各自的產(chǎn)出意圖、槽位是 attributewith tracer.start_as_current_span(cs:intent) as s: intent classify_intent(text) # ← 耗時發(fā)生在這一步 s.set_attribute(cs.intent, intent.name) # ← 產(chǎn)出掛屬性如果壓成一個 span 三個屬性就丟掉了「哪一步最慢」而調(diào)優(yōu)時需要這個信息。4.2 完整示例一個智能客服 Agent初始化一個程序只做一次from opentelemetry import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter resource Resource.create({service.name: my-agent-runner}) provider TracerProvider(resourceresource) provider.add_span_processor( BatchSpanProcessor(OTLPSpanExporter(endpointhttp://localhost:4318/v1/traces)) ) trace.set_tracer_provider(provider) # ← 全進程只能調(diào)一次 tracer trace.get_tracer(__name__)把前面的規(guī)則串起來規(guī)范字段和業(yè)務(wù)自定義字段混用def handle_session(user_text: str) - str: # 根 span一次會話的邊界。業(yè)務(wù)屬性走自己的 cs. 命名空間 with tracer.start_as_current_span(agent:customer-service) as agent: agent.set_attribute(openinference.span.kind, AGENT) agent.set_attribute(cs.tenant_id, t-001) agent.set_attribute(cs.prompt_version, v3.2) # user.id / session.id 是高基數(shù) → 放進日志不塞屬性 # 前置 pipeline每一步都有耗時 → 各自一個 span產(chǎn)出掛屬性 with tracer.start_as_current_span(cs:intent) as s: intent classify_intent(user_text) s.set_attribute(cs.intent, intent.name) with tracer.start_as_current_span(cs:slot_filling) as s: slots fill_slots(user_text) s.set_attribute(cs.slots, json.dumps(slots)) # 多輪生成規(guī)范字段掛在模型 span 上 for i in range(MAX_TURNS): with tracer.start_as_current_span(fturn-{i}) as turn: turn.set_attribute(turn.index, i) with tracer.start_as_current_span(claude) as llm: reply call_model(user_text, slots) llm.set_attribute(gen_ai.request.model, claude) llm.set_attribute(gen_ai.usage.input_tokens, reply.usage.input) llm.set_attribute(gen_ai.server.time_to_first_token, reply.ttft_ms) llm.set_attribute(llm.cost.usd, reply.cost_usd) llm.set_attribute(output.value, reply.text[:10240]) # 截斷 for call in reply.tool_calls: with tracer.start_as_current_span(ftool:{call.name}) as tool: tool.set_attribute(tool.name, call.name) tool.set_attribute(status, ok if execute(call).ok else error)生成的樹每個字段對應(yīng)一個具體問題這次會話花了多少tokengen_ai.usage.* llm.cost.usdtoken 花在哪一輪上面 turn.index必須同層前置三步誰最慢三個 cs:* span 的時長本身用戶意圖分布cs.intent哪個工具最容易掛tool.name statuswith tracer.start_as_current_span(turn-0) as turn:這行和with open()結(jié)構(gòu)一致括號里的字符串 → 是名字進后端別人看 → 有約束低基數(shù) as 后面的變量 → 是把手只在代碼里用 → 隨便取零語義 with → 退出時自動結(jié)束 span三個細節(jié)with 就是 span 的開始和結(jié)束。不需要自己調(diào) end()也不要漏掉結(jié)束——不 end() 的 span 永遠不會被導出而且不報錯。as 后面的名字隨便取它只是個 Python 局部變量括號里的字符串才是 span 的名字。樹長什么樣完全由代碼縮進決定。4.3 三信號協(xié)同Metric 和 Log 怎么配合真實系統(tǒng)里三條信號一起用同一個事件寫三遍各寫各的。tracer trace.get_tracer(__name__) meter metrics.get_meter(__name__) logger logging.getLogger(__name__) # instruments模塊級各建一次別放在函數(shù)里 tokens meter.create_counter(gen_ai.client.token.usage, unit{token}) latency meter.create_histogram(gen_ai.client.operation.duration, units) with tracer.start_as_current_span(agent:customer-service) as agent: logger.info(會話開始) # ③ log —— 自動帶上 trace_id with tracer.start_as_current_span(MODEL) as span: t0 time.monotonic() reply call_model(user_text) dt time.monotonic() - t0 span.set_attribute(gen_ai.usage.input_tokens, reply.usage.input) # ② metric —— 同一個事件換個記法從這一次變成一個數(shù)字 lbl {gen_ai.request.model: MODEL} tokens.add(reply.usage.input reply.usage.output, lbl) latency.record(dt, lbl)跑完之后手上同時有三樣東西靠trace_id縫合trace是一棵樹metric是幾條數(shù)字log是幾行帶trace_id的文本。4.4 實測一次 Agent Trial鏈路是完整的SDK 埋點 → OTLP/HTTP → Jaeger 進程 → Jaeger 的 HTTP API → 取回來出圖。中間隔著一個真實的后端進程驗證的是端到端鏈路不是內(nèi)存里的對象。結(jié)果service.name agent-eval14 個 span端到端771.8ms。其中 LLM 調(diào)用總共 120.1ms占比約 15.6%這次實驗里 LLM 不是主要耗時來源。沒有 Trace容易先懷疑模型有 Trace 可以先確認時間花在哪。4.5 能回答的問題與模型對比常用查詢模式都是按某個字段聚合哪一步最慢 → 按 span name 聚合耗時排序哪個工具最容易失敗 → 按 tool.name 分組統(tǒng)計 status ERRORtoken 花在哪 → 按 turn.index 排開 input_tokens 看增長曲線。對比對評測場景尤其有用最終分數(shù) 輪數(shù) 總 token 工具調(diào)用 模型 A 0.85 3 2.1k 2 模型 B 0.85 12 11.4k 9 看起來一樣 差 4 倍 差 5 倍 差 4.5 倍分數(shù)一樣成本差 5 倍這個差異日志里看不出來span 樹里能直接看到。形狀本身也是信息4.6 后端選型與落地節(jié)奏后端選型Jaeger入門、看瀑布圖開箱即用適合起步MLflow適合實驗對比Langfuse是 LLM 專用面板字段約定不同要驗證渲染效果自建 Collector 存儲等量上來再說。OTel 的失敗大多是「數(shù)據(jù)沒到」而不是「程序崩了」需要先有一個能看數(shù)據(jù)的地方否則問題出在哪都不好判斷。分階段落地① 先讓一條軌跡能看 ← 最重要成本低② 再批量導入歷史數(shù)據(jù)③ 再定字段規(guī)范④ 最后建對比工作流、上生產(chǎn)加 Collector、配采樣、接多后端OTel 的價值在第一次跑的時候不明顯只看到一棵樹會像「好看點的日志」。價值出現(xiàn)在有第二個版本可以對比的時候。所以先跑通、先有數(shù)據(jù)字段可以之后再補。五、總結(jié)OpenTelemetry 把一次 Agent 執(zhí)行記錄成一棵有層級、有因果、帶屬性的樹Trace / Span 還原「做了什么、什么順序、花了多久」Attributes 記錄「用的什么、token花了多少、為什么失敗」Metric / Log 分別回答「整體怎么樣」和「那一瞬發(fā)生了什么」靠 trace_id 串起來。一次 771.8ms 的 Agent TrialLLM 只占 120.1ms15.6%慢的不一定是模型。當 Agent 從「調(diào)用一個模型」變成「自主完成一項任務(wù)」理解它怎么跑就成了可觀測性的核心問題。學AI大模型的正確順序千萬不要搞錯了2026年AI風口已來各行各業(yè)的AI滲透肉眼可見超多公司要么轉(zhuǎn)型做AI相關(guān)產(chǎn)品要么高薪挖AI技術(shù)人才機遇直接擺在眼前有往AI方向發(fā)展或者本身有后端編程基礎(chǔ)的朋友直接沖AI大模型應(yīng)用開發(fā)轉(zhuǎn)崗超合適就算暫時不打算轉(zhuǎn)崗了解大模型、RAG、Prompt、Agent這些熱門概念能上手做簡單項目也絕對是求職加分王給大家整理了超全最新的AI大模型應(yīng)用開發(fā)學習清單和資料手把手幫你快速入門學習路線:?大模型基礎(chǔ)認知—大模型核心原理、發(fā)展歷程、主流模型GPT、文心一言等特點解析?核心技術(shù)模塊—RAG檢索增強生成、Prompt工程實戰(zhàn)、Agent智能體開發(fā)邏輯?開發(fā)基礎(chǔ)能力—Python進階、API接口調(diào)用、大模型開發(fā)框架LangChain等實操?應(yīng)用場景開發(fā)—智能問答系統(tǒng)、企業(yè)知識庫、AIGC內(nèi)容生成工具、行業(yè)定制化大模型應(yīng)用?項目落地流程—需求拆解、技術(shù)選型、模型調(diào)優(yōu)、測試上線、運維迭代?面試求職沖刺—崗位JD解析、簡歷AI項目包裝、高頻面試題匯總、模擬面經(jīng)以上6大模塊看似清晰好上手實則每個部分都有扎實的核心內(nèi)容需要吃透我把大模型的學習全流程已經(jīng)整理好了抓住AI時代風口輕松解鎖職業(yè)新可能希望大家都能把握機遇實現(xiàn)薪資/職業(yè)躍遷這份完整版的大模型 AI 學習資料已經(jīng)上傳CSDN朋友們?nèi)绻枰梢晕⑿艗呙柘路紺SDN官方認證二維碼免費領(lǐng)取【保證100%免費】