:讓 AI Agent 從黑盒到透明)
1. 為什么“能跑通”的 Agent 項目上線三天就變成了黑盒我最早接觸 AI Agent 是在一個內(nèi)部知識庫問答項目上。當(dāng)時用 LangChain 把檢索、工具調(diào)用、生成串起來本地跑得挺順回答質(zhì)量也還行。上線第一周用戶反饋開始變多“同一個問題昨天答得對今天答錯了”“有時候轉(zhuǎn)圈十幾秒才回”“它到底查了哪些文檔能不能給我看看”。我打開日志看到的只有一行AgentExecutor finished中間發(fā)生了什么完全靠猜。這就是大多數(shù) Agent 項目從 Demo 走向生產(chǎn)時撞上的第一堵墻調(diào)用鏈?zhǔn)呛诤?。一次用戶提問背后可能?jīng)歷了意圖識別、多輪工具調(diào)用、向量檢索、重排、大模型生成、結(jié)果校驗等七八個環(huán)節(jié)任何一個環(huán)節(jié)的參數(shù)變化、超時、返回異常都會讓最終結(jié)果面目全非。而傳統(tǒng)日志只能記錄“入口”和“出口”中間過程要么沒打要么打了一堆沒法關(guān)聯(lián)的散點。Langfuse 解決的正是這個問題。它把自己定位成 LLM 應(yīng)用的可觀測性平臺核心能力可以概括成三件事Trace追蹤把一次完整請求的所有環(huán)節(jié)串成一條時間線Span跨度記錄每個子步驟的輸入輸出、耗時、模型參數(shù)Score評分把人工反饋或自動評估的結(jié)果掛到具體的 Trace 上。這三件事組合起來Agent 的每一次“思考”都變得可回放、可對比、可量化。這篇文章適合兩類人看一類是已經(jīng)把 Agent 跑起來、但被線上問題搞得焦頭爛額的工程師另一類是正準(zhǔn)備搭 Agent 系統(tǒng)、想從一開始就把可觀測性設(shè)計進去的開發(fā)者。我會從接入方式、數(shù)據(jù)模型、評測體系、并發(fā)與成本、踩坑經(jīng)驗幾個角度把 Langfuse 在真實 Agent 項目里的用法講透。文中涉及的具體參數(shù)和配置一部分來自官方文檔一部分是我在實際項目中反復(fù)調(diào)試后總結(jié)的實踐值你可以直接參考但建議結(jié)合自己的業(yè)務(wù)量級做調(diào)整。2. Langfuse 的數(shù)據(jù)模型Trace、Span、Generation 到底怎么擺很多人第一次看 Langfuse 的界面會覺得信息很多不知道從哪看起。其實它的數(shù)據(jù)模型非常清晰理解了這個模型后面接入和排查都會順很多。2.1 一次請求就是一條 TraceTrace 是 Langfuse 里最大的容器單位對應(yīng)“一次完整的用戶請求”。比如用戶在對話框里問了一句“幫我查一下上個月的銷售數(shù)據(jù)”從這句話進入系統(tǒng)到最終答案返回整個過程就是一條 Trace。Trace 有一個唯一的trace_id你可以自己生成也可以讓 SDK 自動生成。我習(xí)慣在請求入口處手動生成并透傳這樣即使跨服務(wù)調(diào)用也能把同一條 Trace 的各個部分關(guān)聯(lián)起來。Trace 上可以掛很多元數(shù)據(jù)用戶 ID、會話 ID、標(biāo)簽、環(huán)境production/staging、版本號。這些字段看起來不起眼但在排查問題時極其有用。比如你可以按user_id過濾看某個用戶最近的所有請求也可以按version對比新舊版本的表現(xiàn)差異。2.2 Span 是 Trace 里的一個步驟Span 代表 Trace 內(nèi)部的一個操作單元。在 Agent 場景里一次工具調(diào)用、一次向量檢索、一次重排都可以是一個 Span。Span 可以嵌套形成樹狀結(jié)構(gòu)。比如“工具調(diào)用”這個 Span 下面可以再掛“參數(shù)構(gòu)造”和“HTTP 請求”兩個子 Span。Span 最核心的價值是耗時歸因。當(dāng)用戶抱怨“怎么這么慢”時你打開 Trace 一看如果檢索 Span 花了 3 秒生成 Span 花了 8 秒那優(yōu)化重點就很明確了。我見過一個項目排查了半天以為是模型慢結(jié)果發(fā)現(xiàn)是重排服務(wù)在高峰期排隊Span 一拉出來問題一目了然。2.3 Generation 是專門給大模型調(diào)用用的 SpanGeneration 是 Span 的一種特殊類型專門用來記錄大模型調(diào)用。它比普通 Span 多了幾個關(guān)鍵字段model模型名稱、model_parameters溫度、top_p 等、prompt輸入提示詞、completion模型輸出、usagetoken 消耗。這些字段是后續(xù)做成本分析和質(zhì)量評估的基礎(chǔ)。我特別想強調(diào)usage字段。很多團隊做 Agent 時只關(guān)心“能不能答對”不關(guān)心“花了多少錢”。等到月底賬單出來才發(fā)現(xiàn)某個工具調(diào)用因為提示詞寫得太啰嗦每次都要消耗幾千 token。Langfuse 會把每次 Generation 的 token 數(shù)記錄下來你可以在儀表盤上按模型、按天、按用戶維度看消耗趨勢。這個數(shù)據(jù)對于控制成本是剛需。2.4 三者關(guān)系用一張表說清楚層級對應(yīng)概念關(guān)鍵字段典型用途Trace一次完整請求trace_id, user_id, session_id, tags全鏈路回放、用戶行為分析Span請求內(nèi)的一個步驟name, start_time, end_time, input, output耗時歸因、步驟排查Generation一次模型調(diào)用model, prompt, completion, usage成本分析、提示詞優(yōu)化、質(zhì)量評估理解這個模型之后接入 Langfuse 就變成了“在合適的位置埋點”的問題。埋點位置的選擇直接決定了你后續(xù)能看到什么。3. 接入實戰(zhàn)在 Agent 的關(guān)鍵路徑上埋點Langfuse 提供了 Python 和 JavaScript 的 SDK也支持通過 API 直接寫入。對于大多數(shù) Agent 項目我推薦用 SDK 的裝飾器或上下文管理器方式接入侵入性小維護成本低。3.1 環(huán)境準(zhǔn)備與初始化先裝 SDKpip install langfuse然后在項目啟動時初始化客戶端。這里有個細(xì)節(jié)不要在每次請求里都 new 一個客戶端那樣會反復(fù)建立連接既慢又浪費資源。正確做法是在應(yīng)用啟動時初始化一個全局客戶端通過依賴注入或模塊級變量共享。from langfuse import Langfuse langfuse Langfuse( public_keypk-lf-..., secret_keysk-lf-..., hosthttps://your-langfuse-host, releaseagent-v1.2.0, environmentproduction )release和environment這兩個參數(shù)建議一定要填。release可以填 Git commit 短哈?;虬姹咎杄nvironment區(qū)分生產(chǎn)和測試。后面做版本對比時這兩個字段就是篩選依據(jù)。3.2 用裝飾器給函數(shù)自動埋點Langfuse 提供了observe()裝飾器加在函數(shù)上就能自動生成 Span。這是最省事的接入方式。from langfuse.decorators import observe observe() def retrieve_documents(query: str): # 向量檢索邏輯 return docs observe() def call_llm(prompt: str): # 模型調(diào)用邏輯 return response裝飾器會自動記錄函數(shù)的入?yún)?、返回值、?zhí)行耗時。如果函數(shù)內(nèi)部拋異常異常信息也會被記錄到 Span 上。這一點在排查線上問題時特別有用你能直接看到是哪一步炸了、炸的時候輸入是什么。不過裝飾器有個局限它記錄的是函數(shù)的輸入輸出但如果你想把模型調(diào)用的 token 數(shù)、模型名稱這些信息也記下來需要配合update_current_generation或update_current_span手動補充。3.3 手動控制 Trace 的粒度對于 Agent 這種多步驟場景我建議在入口處手動創(chuàng)建 Trace然后在關(guān)鍵步驟手動創(chuàng)建 Span。這樣粒度更可控。from langfuse import Langfuse langfuse Langfuse(...) def handle_user_query(user_id: str, query: str): trace langfuse.trace( nameagent-query, user_iduser_id, input{query: query}, metadata{channel: web} ) # 檢索步驟 retrieval_span trace.span(nameretrieval, input{query: query}) docs retrieve_documents(query) retrieval_span.end(output{doc_count: len(docs)}) # 生成步驟 generation trace.generation( nameanswer-generation, modelgpt-4o, inputbuild_prompt(query, docs) ) answer call_llm(...) generation.end( outputanswer, usage{input: 1200, output: 350} ) trace.update(output{answer: answer}) return answer這種寫法的好處是你能精確控制每個 Span 的邊界和記錄內(nèi)容。比如檢索 Span 里你可以把召回的文檔 ID 列表記進去后面排查“為什么答錯了”時就能直接看到當(dāng)時檢索到了什么。3.4 在 LangChain 和 LangGraph 里的接入方式如果你的 Agent 是基于 LangChain 或 LangGraph 搭的Langfuse 有現(xiàn)成的 CallbackHandler接入成本很低。from langfuse.callback import CallbackHandler handler CallbackHandler( public_keypk-lf-..., secret_keysk-lf-..., hosthttps://your-langfuse-host, session_iduser-session-123, user_iduser-456 ) chain.invoke({input: query}, config{callbacks: [handler]})LangGraph 的接入類似把 handler 傳進config即可。它會自動把每個節(jié)點執(zhí)行、每次模型調(diào)用都記錄成 Span 和 Generation。我實測下來LangGraph 的節(jié)點名稱會自動成為 Span 名稱所以給節(jié)點起個好名字很重要別用node_1、node_2這種后面看 Trace 時會很痛苦。提示CallbackHandler 在高并發(fā)場景下會頻繁創(chuàng)建對象建議復(fù)用或使用連接池。如果 QPS 很高可以考慮異步寫入模式避免阻塞主流程。4. 評測體系讓 Agent 的回答質(zhì)量從“感覺還行”變成“有數(shù)可查”可觀測性解決的是“看得見”的問題但看得見不等于管得住。Agent 的回答質(zhì)量到底怎么樣需要一套評測體系來量化。Langfuse 的 Score 功能就是干這個的。4.1 人工評分最直接但最貴的方式最簡單的做法是在 Trace 上掛人工評分。Langfuse 支持在界面上直接給某條 Trace 打分也支持通過 API 寫入。langfuse.score( trace_idtrace.id, nameuser-feedback, value1, # 1 表示好評0 表示差評 comment回答準(zhǔn)確引用了正確的文檔 )人工評分的優(yōu)點是準(zhǔn)確缺點是貴且慢。我的經(jīng)驗是不要對所有請求都做人工評分而是抽樣。按用戶反饋點贊/點踩自動掛分再定期人工復(fù)核一部分。這樣成本可控數(shù)據(jù)也有代表性。4.2 自動評估用模型給模型打分對于需要規(guī)?;u估的場景可以用另一個模型來做自動評分。Langfuse 本身不提供評估模型但你可以自己寫評估邏輯然后把結(jié)果寫回 Score。常見的自動評估維度有幾個相關(guān)性回答是否切題有沒有答非所問忠實度回答是否基于檢索到的文檔有沒有編造完整性是否覆蓋了問題的所有方面格式合規(guī)是否滿足輸出格式要求比如 JSON 結(jié)構(gòu)def evaluate_answer(query, answer, docs): eval_prompt f 請評估以下回答的質(zhì)量從相關(guān)性、忠實度、完整性三個維度打分1-5分。 問題{query} 檢索文檔{docs} 回答{answer} 請以 JSON 格式輸出評分和理由。 result call_llm(eval_prompt) scores parse_json(result) for dim, score in scores.items(): langfuse.score( trace_idtrace.id, namefauto-{dim}, valuescore[value], commentscore[reason] )這里有個坑要注意評估模型和被評估模型不要用同一個。用同一個模型評估自己容易產(chǎn)生“自我偏好”分?jǐn)?shù)虛高。我一般用能力更強的模型做評估或者用不同廠商的模型交叉評估。4.3 用數(shù)據(jù)集做回歸測試Agent 系統(tǒng)最怕的是“改了一個提示詞修好了 A 問題弄壞了 B 問題”。Langfuse 的 Dataset 功能可以幫你做回歸測試。做法是把一批有代表性的問題包括邊界 case整理成數(shù)據(jù)集每次發(fā)版前跑一遍對比新舊版本的評分。如果某個維度的分?jǐn)?shù)明顯下降就說明這次改動有問題。dataset langfuse.get_dataset(agent-regression-v1) for item in dataset.items: # 用新版本跑一遍 output run_agent(item.input) # 掛到數(shù)據(jù)集條目上 item.link( trace_or_observationtrace, run_namev1.3.0-release )跑完之后在 Langfuse 界面上可以按run_name對比不同版本的評分。這個流程跑順了之后發(fā)版心里就有底了。4.4 評測頻率與成本控制自動評估是要花錢的每次評估都是一次模型調(diào)用。我的建議是場景評估頻率評估方式日常線上抽樣 5%-10%自動評估 用戶反饋發(fā)版前全量數(shù)據(jù)集自動評估 人工抽檢重大改動全量數(shù)據(jù)集 線上灰度自動評估 人工全檢這樣既能控制成本又能在關(guān)鍵節(jié)點拿到足夠的質(zhì)量信號。5. 高并發(fā)下的 Langfuse別讓可觀測性拖垮主流程Agent 系統(tǒng)本身就比普通接口重一次請求可能涉及多次模型調(diào)用和工具調(diào)用延遲本來就高。如果可觀測性的埋點再同步阻塞主流程用戶體驗會更差。這一節(jié)講幾個高并發(fā)場景下的實踐要點。5.1 異步寫入與批量上報Langfuse SDK 默認(rèn)是同步發(fā)送數(shù)據(jù)的也就是說每次trace.span()、generation.end()都會觸發(fā)一次網(wǎng)絡(luò)請求。在低 QPS 下沒問題但 QPS 一高這些請求會堆積拖慢主流程。解決辦法是開啟異步模式。Langfuse 的 Python SDK 支持通過環(huán)境變量或初始化參數(shù)配置異步langfuse Langfuse( public_key..., secret_key..., host..., flush_at100, # 攢夠 100 條再發(fā) flush_interval1.0, # 或者每 1 秒發(fā)一次 threads4 # 后臺發(fā)送線程數(shù) )flush_at和flush_interval是兩個關(guān)鍵參數(shù)。flush_at控制批量大小flush_interval控制最大等待時間。兩者是“或”的關(guān)系滿足任一條件就發(fā)送。我一般設(shè)flush_at50、flush_interval2.0在延遲和數(shù)據(jù)實時性之間取平衡。注意異步模式下如果應(yīng)用崩潰緩沖區(qū)里沒發(fā)出去的數(shù)據(jù)會丟失。對于關(guān)鍵業(yè)務(wù)建議在請求結(jié)束時手動調(diào)用langfuse.flush()確保數(shù)據(jù)落庫。5.2 采樣不是所有請求都值得全量記錄高并發(fā)場景下全量記錄 Trace 的成本很高包括存儲成本、網(wǎng)絡(luò)成本、以及 Langfuse 服務(wù)端的處理成本。這時候需要采樣。采樣的策略有幾種固定比例采樣比如只記錄 20% 的請求。實現(xiàn)簡單但可能漏掉關(guān)鍵 case。基于錯誤采樣正常請求少記出錯請求全記。這個策略最實用因為排查問題主要看出錯的?;谟脩舨蓸訉?VIP 用戶全量記錄普通用戶抽樣?;谘舆t采樣慢請求全記快請求抽樣。我通常組合使用錯誤請求 100% 記錄慢請求超過 P95100% 記錄正常請求按 10% 采樣。這樣既控制了數(shù)據(jù)量又保證了問題排查時有足夠的信息。import random def should_sample(trace_metadata): if trace_metadata.get(has_error): return True if trace_metadata.get(latency_ms, 0) 5000: return True return random.random() 0.15.3 敏感信息脫敏Agent 系統(tǒng)處理的往往是用戶真實數(shù)據(jù)直接把這些數(shù)據(jù)寫到 Langfuse 上是有合規(guī)風(fēng)險的。Langfuse 支持在 SDK 層面做脫敏。from langfuse import Langfuse def mask_sensitive(data): # 對手機號、郵箱、身份證號做脫敏 if isinstance(data, str): data re.sub(r\d{11}, ***********, data) data re.sub(r[\w\.-][\w\.-], ******, data) return data langfuse Langfuse( ..., maskmask_sensitive )mask函數(shù)會在數(shù)據(jù)發(fā)送前被調(diào)用你可以在這里做各種脫敏處理。這一步千萬別省尤其是涉及用戶隱私的業(yè)務(wù)。5.4 并發(fā)壓測下的表現(xiàn)我在一個 QPS 200 左右的 Agent 服務(wù)上做過壓測對比開啟和關(guān)閉 Langfuse 的延遲差異。結(jié)論是開啟異步寫入后P99 延遲增加在 5% 以內(nèi)如果不開異步P99 延遲會增加 30% 以上因為同步網(wǎng)絡(luò)請求在高峰期會排隊。所以結(jié)論很明確生產(chǎn)環(huán)境一定要開異步并且做好采樣和脫敏。可觀測性是為了讓系統(tǒng)更可控不能反過來成為系統(tǒng)的負(fù)擔(dān)。6. 踩坑記錄那些文檔里不會寫的細(xì)節(jié)這一節(jié)記錄幾個我在實際項目中踩過的坑都是文檔里不會寫、但實際會遇到的。6.1 Trace 丟失請求還沒結(jié)束進程就退出了Agent 服務(wù)如果用 Serverless 或短生命周期容器部署請求處理完進程可能立刻退出導(dǎo)致 Langfuse 緩沖區(qū)里的數(shù)據(jù)還沒發(fā)出去就丟了。表現(xiàn)就是 Langfuse 上只能看到部分 Trace或者干脆看不到。解決辦法是在請求處理結(jié)束時顯式 flushtry: result handle_user_query(user_id, query) finally: langfuse.flush()flush()會阻塞直到緩沖區(qū)清空。雖然會增加一點延遲但能保證數(shù)據(jù)不丟。如果對延遲極其敏感可以把這個 flush 放到后臺線程里做。6.2 Span 嵌套層級過深界面加載慢Agent 如果步驟很多Span 嵌套層級可能達(dá)到十幾層。Langfuse 界面在渲染深層嵌套時會變慢排查問題時展開也很費勁。我的做法是控制嵌套深度一般不超過 5 層。對于特別復(fù)雜的流程把一些細(xì)節(jié)步驟合并成一個 Span只在需要時展開。比如“工具調(diào)用”這個 Span 下面不需要把參數(shù)構(gòu)造、序列化、HTTP 請求都拆開合并成一個 Span 記錄關(guān)鍵信息就夠了。6.3 時間戳不一致導(dǎo)致 Trace 順序錯亂Langfuse 依賴時間戳來排序 Span。如果 Agent 服務(wù)部署在多臺機器上機器時間不同步Span 的順序就會亂看起來像是“后面的步驟先執(zhí)行了”。解決辦法是確保所有機器開啟 NTP 時間同步。另外Langfuse SDK 支持傳入自定義時間戳如果確實有時序問題可以在埋點時手動指定。6.4 評分?jǐn)?shù)據(jù)寫入失敗但沒報錯Langfuse 的score()方法在寫入失敗時默認(rèn)不拋異常只是靜默失敗。這導(dǎo)致我以為評分寫進去了實際上沒有。排查方法是看 SDK 的日志。把日志級別調(diào)到 DEBUG能看到每次寫入的結(jié)果。如果發(fā)現(xiàn)失敗檢查網(wǎng)絡(luò)、認(rèn)證信息、以及 trace_id 是否正確。6.5 模型名稱不規(guī)范導(dǎo)致統(tǒng)計混亂generation.end()里的model字段如果寫法不統(tǒng)一比如有時寫gpt-4o有時寫gpt-4o-2024-08-06統(tǒng)計時會被當(dāng)成兩個模型成本分析就不準(zhǔn)了。建議在項目里維護一個模型名稱常量表所有埋點統(tǒng)一引用。這樣統(tǒng)計維度才干凈。7. 從可觀測性到持續(xù)優(yōu)化把數(shù)據(jù)用起來埋點、評測、排查都做完之后最后一步是把這些數(shù)據(jù)轉(zhuǎn)化成實際的優(yōu)化動作。否則可觀測性就只是“看個熱鬧”。7.1 用 Trace 對比找出提示詞退化每次改提示詞后用同一批測試問題跑一遍然后在 Langfuse 上按release篩選對比新舊版本的評分和輸出。我遇到過好幾次“改了一個詞整體分?jǐn)?shù)沒變但某類問題的準(zhǔn)確率掉了 20%”的情況全靠這個對比發(fā)現(xiàn)。7.2 用耗時分布定位性能瓶頸在 Langfuse 儀表盤上看 Span 的耗時分布找出 P95 最高的幾個 Span。這些就是優(yōu)化重點。常見的瓶頸包括向量檢索沒有加緩存、重排服務(wù)并發(fā)度不夠、模型調(diào)用沒有做流式輸出等。7.3 用用戶反饋驅(qū)動數(shù)據(jù)集迭代用戶點踩的 Trace 是最寶貴的數(shù)據(jù)。定期把這些 Trace 整理出來補充到回歸測試數(shù)據(jù)集里。這樣數(shù)據(jù)集會越來越貼近真實場景回歸測試的有效性也會越來越高。7.4 成本優(yōu)化的幾個切入點通過 Langfuse 的 token 統(tǒng)計我總結(jié)出幾個成本優(yōu)化的方向提示詞精簡很多提示詞里有大量冗余說明精簡后 token 數(shù)能降 30% 以上緩存復(fù)用相同或相似的查詢結(jié)果緩存減少重復(fù)模型調(diào)用模型分級簡單任務(wù)用小模型復(fù)雜任務(wù)用大模型通過路由分發(fā)輸出長度控制設(shè)置合理的max_tokens避免模型“話癆”這些優(yōu)化做完成本能降一半以上而質(zhì)量通過評測體系監(jiān)控不會明顯下降。8. 寫在最后可觀測性是 Agent 工程化的入場券我從最早用print調(diào)試 Agent到后來用日志系統(tǒng)再到接入 Langfuse最大的感受是Agent 系統(tǒng)的復(fù)雜度不在于“能不能跑”而在于“跑得好不好、為什么好、為什么不好”。沒有可觀測性這些問題全靠猜有了可觀測性每個問題都能定位到具體的 Trace、具體的 Span、具體的參數(shù)。Langfuse 不是唯一的選擇但它在這個方向上的數(shù)據(jù)模型設(shè)計得比較合理接入成本也低。如果你正在搭 Agent 系統(tǒng)我的建議是從第一天就把可觀測性設(shè)計進去別等到線上出問題了再補。補的時候你會發(fā)現(xiàn)很多關(guān)鍵信息當(dāng)時沒記事后根本查不到。最后分享一個我自己的習(xí)慣每次發(fā)版前除了跑回歸測試我還會隨機抽 20 條線上 Trace 人工看一遍。這個動作花不了多少時間但經(jīng)常能發(fā)現(xiàn)一些自動化指標(biāo)覆蓋不到的問題比如回答語氣不對、格式雖然合規(guī)但可讀性差、某些邊界 case 處理得生硬。這些細(xì)節(jié)才是 Agent 從“能用”到“好用”的關(guān)鍵。