TL;DR
Arize Phoenix 是一套開源、OpenTelemetry-native 的 LLM observability 工具,自架只要一個 docker container(不像 Langfuse 還要外接 ClickHouse + Redis + S3)。我把它接進一個醫療 CDSS 的 LangGraph 8-step agent,目的不是看 latency,是要回答醫師那句「它為什麼開這個藥」。這篇記錄怎麼接、三個讓我 debug 半天的踩坑,以及真正值錢的那段——把醫師回饋以 annotation 回掛到對應 trace,做成 human-in-the-loop 的評估閉環。
📌 目錄
- 我為什麼接 Phoenix:CDSS 是個黑箱
- Arize Phoenix 是什麼
- Phoenix vs Langfuse vs LangSmith 怎麼選
- 怎麼接:安裝三件套
- 一次請求的 trace 樹長怎樣
- 三個踩坑
- 真正的 ROI:醫師回饋回掛 trace
- 常見問題 FAQ
- 延伸資源
⚡ 我為什麼接 Phoenix
我在做一個臨床決策支援系統(CDSS)。簡單講就是醫師打開病人頁,按一下,後端一條 LangGraph 8-step pipeline 跑完,吐出一份處方建議:要不要加某個藥、劑量多少、要不要升階到次線療法。
問題是它是個黑箱。決策中間經過 8 個節點、每個節點可能呼叫 LLM、LLM 又可能呼叫好幾個工具(讀臨床 skill、查 few-shot 範例、算公式)。當醫師問我「它為什麼建議這個劑量」,我打開 log 看到的是一堆 httpx POST 200,根本還原不了當下的推理路徑。
我一開始接 observability 工具的時候,腦袋裡想的也只是「看看哪個 step 慢、quota 燒多少」。是後來做到「醫師回饋」那段,才發現真正的價值不在效能監控,而在能不能把一次決策完整攤開來看,還能把醫師的評分黏回那次決策上。這就是我選 Phoenix 的原因。
⭐ Arize Phoenix 是什麼
Arize Phoenix 是 Arize AI 開源的 AI observability 與 evaluation 平台,截至 2026 年 6 月最新版是 v17.2.0,GitHub 上 10,000+ stars,授權是 Elastic License 2.0。
它的技術底座是兩層:
- OpenTelemetry(OTel):業界標準的分散式追蹤協定。意思是你的 LLM 應用送出的 trace 走的是通用格式,理論上任何 OTel-compatible 後端都能收。
- OpenInference:Arize 自己定的一套語意慣例(span kind、屬性命名),補足 OTel 在「AI 應用」這個場景缺的東西——例如怎麼標記一個 span 是
LLM、TOOL、CHAIN還是AGENT。
對我來說最關鍵的一句話是:它可以用單一 docker container 自架,trace 資料留在本地 volume,不外傳第三方。醫療資料這條紅線下,這幾乎是硬需求。
⚖️ Phoenix vs Langfuse vs LangSmith
接之前我比過三個主流選項。結論先講:要全開源 + 好自架 + OTel 標準 → Phoenix;要框架無關 + 透明計價 → Langfuse;重度綁 LangChain 生態 → LangSmith。
| 維度 | Arize Phoenix | Langfuse | LangSmith |
|---|---|---|---|
| 開源授權 | ✓ Elastic 2.0 | ✓ 100% 開源 | ✗ 閉源 |
| 自架難度 | ✓ 單 container | △ 要外接 ClickHouse+Redis+S3 | ✗ 自架限 Enterprise |
| OTel native | ✓ OpenInference | ✓ 走 OTel | △ 偏自家 SDK |
| Prompt Playground / LLM-as-judge | ✓ 免費 | △ 部分鎖付費 | ✓ |
| 框架深度 | LlamaIndex / Agents SDK 佳 | 任意框架但較淺 | LangChain/LangGraph 最深 |
| eval 嚴謹度 | ✓ ML observability 底子深 | △ | △ |
| 計價 | 依 span 數 / 資料量 | 透明 volume-based | — |
🔗 怎麼接:安裝三件套
接 Phoenix 到一個既有的 FastAPI + LangGraph 服務,本質上只有三件事:起 Phoenix 容器、裝 instrument 套件、設一個環境變數開關。
1. Prereq
| 依賴 | 最低版本 | 驗證 |
|---|---|---|
| Docker | 24+ | docker --version |
| Python | 3.11+ | python --version |
| 既有 LLM 服務 | 走 OpenAI 相容 API | — |
2. 起 Phoenix 容器
我把 Phoenix 做成一個獨立、可單獨啟停的 compose,跟主服務解耦:
name: clinical-phoenix
services:
phoenix:
image: arizephoenix/phoenix:latest
container_name: clinical-phoenix
ports:
- "6006:6006" # UI + OTLP/HTTP
- "4317:4317" # OTLP/gRPC
environment:
PHOENIX_WORKING_DIR: /mnt/data
volumes:
- phoenix_data:/mnt/data
restart: unless-stopped
volumes:
phoenix_data:
6006 同時是 Web UI 和 OTLP/HTTP 接收埠,4317 是 OTLP/gRPC,trace 落在本地 phoenix_data volume。
3. 一鍵安裝 + 啟用
# 裝 instrument 套件(加進 requirements.txt)
pip install \
"arize-phoenix-otel>=0.6.0" \
"openinference-instrumentation-openai>=0.1.0" \
"arize-phoenix-client>=1.0.0"
# 起 Phoenix UI
docker compose -f deploy/phoenix/docker-compose.phoenix.yml up -d
# 設一個 env 開關(沒設就完全 no-op)
export PHOENIX_COLLECTOR_ENDPOINT="http://host.docker.internal:6006"
4. 驗證
開 http://localhost:6006 看到 Phoenix UI、左側 project 列表出現你的服務名 → 接上了。打一次請求後,trace 樹會即時出現在 UI。這才算 round-trip 成功,不是看 log 寫了 tracing enabled 就算。
這裡我做了一個刻意的設計:整套追蹤是 opt-in 的。PHOENIX_COLLECTOR_ENDPOINT 沒設值,setup_tracing() 直接 return,所有 span 的 context manager 都降級成 no-op,連追蹤套件沒裝都不會讓服務啟動失敗——因為每個 import 都包在 try/except 裡。正式環境不想開追蹤?不設那個 env 就好,零成本、零風險。
def setup_tracing() -> None:
"""未設 PHOENIX_COLLECTOR_ENDPOINT 則 no-op。"""
endpoint = os.environ.get("PHOENIX_COLLECTOR_ENDPOINT")
if not endpoint:
return
try:
from phoenix.otel import register
tracer_provider = register(
project_name="clinical-ai-service",
auto_instrument=False, # 關鍵:不開 langchain 自動 instrument,原因見踩坑段
batch=True,
)
from openinference.instrumentation.openai import OpenAIInstrumentor
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
except Exception as e:
# 追蹤失敗絕不可拖垮服務
logger.warning("Phoenix tracing setup failed, continuing: %s", e)
🎨 一次請求的 trace 樹長怎樣
接好以後,一次 /analyze 請求在 Phoenix 上就是一棵 trace,從進來到出處方完整攤開:
每一層都看得到輸入輸出:哪個節點吃了什麼 state、step5 的 LLM 第幾輪呼叫了哪個工具、工具回了什麼。醫師問「它為什麼開這個劑量」,我現在可以直接點開那棵 trace,看到 step5 第二輪呼叫了 query_few_shots、抄到了某筆 positive 範例的劑量字串——推理路徑一目了然。
📝 三個踩坑
接的過程不是一路順。這三個都讓我 debug 了一陣子,寫下來給後面接的人省時間。
踩坑一:langchain 自動 instrument 會讓 trace 散成好幾棵
Phoenix 的賣點之一是「auto_instrument 自動抓 langchain」。我一開始當然是直接開,結果 UI 上的 trace 散成一堆孤兒 span——LLM 呼叫、工具呼叫各自成一棵,根本兜不回那次請求的節點底下。
根因:langchain/langgraph 的 instrumentor 用 callback 機制建節點 span,但它不會把節點 span 設成 OTel 的「當前 span」。於是底下的 openai span、工具 span 一查當前 context,找不到父節點,就各自飄成獨立 trace。
解法是關掉 langchain 自動 instrument(auto_instrument=False),改用我自己包的 traced_node() 手動串。關鍵在 start_as_current_span——它會把節點 span 設成當前 span,底下的 LLM 和工具才能正確巢狀:
def traced_node(name: str, fn):
"""把 langgraph 節點包一層 OTel current-span。"""
tracer = trace.get_tracer("clinical-ai-service.graph")
async def _async_wrapped(state):
with tracer.start_as_current_span(name) as span:
span.set_attribute(_KIND_KEY, _CHAIN)
result = await fn(state)
_set_io(span, state, result) # 記節點進出 state
return result
return _async_wrapped
在 graph 定義處,每個節點包一層就好:
g.add_node("step5_llm_decide", traced_node("step5_llm_decide", llm_decide))
教訓:自動 instrument 能抓到「有沒有呼叫」,但抓不到「階層」。要漂亮的巢狀 trace,節點層得自己用 start_as_current_span 串。
踩坑二:phoenix-client 預設讀的 env 跟 phoenix-otel 不一樣
這個是真的隱藏雷,靜默到不會報錯。
我要把醫師回饋以 annotation 寫回 trace,用的是 phoenix.client.Client。問題是:register()(送 trace 那端)讀的是 PHOENIX_COLLECTOR_ENDPOINT,但 Client()(送 annotation 那端)預設讀的是另一個 env PHOENIX_BASE_URL。
我的容器只設了前者。於是 Client() 無參數初始化時,fallback 到預設的 http://localhost:6006——也就是容器自己,不是 Phoenix server。結果 annotation 送出去石沉大海,UI 上完全看不到,也沒有任何錯誤。我盯著「trace 看得到、annotation 看不到」這個現象查了好一陣子。
解法很簡單,但你得先知道有這個坑:顯式把 endpoint 當 base_url 傳進去。
@lru_cache(maxsize=1)
def _phoenix_client():
endpoint = os.environ.get("PHOENIX_COLLECTOR_ENDPOINT")
if not endpoint:
return None
from phoenix.client import Client
# 必須顯式傳 base_url,否則 Client() 會 fallback 到 localhost:6006(容器自己)
return Client(base_url=endpoint)
教訓:同一個產品的「送資料 SDK」跟「讀/寫 API client」吃的環境變數可能不一樣。設定靜默送錯位、又不報錯時,先懷疑「它到底連到哪台」。
踩坑三:自家工具不是 langchain tool,OpenInference 抓不到
OpenInference 的自動 instrument 抓得到 openai 的 LLM 呼叫,但我 step5 那 6 個工具(讀 clinical skill、查 few-shot、算公式、RAG 文獻檢索)不是 langchain tool,是我自己分派的函式。自動 instrument 看不到它們,trace 樹上那層就是空的。
解法是在工具分派的地方手動包一層 OpenInference TOOL span,把 args 和結果都記進去:
@contextmanager
def tool_span(name, args):
tracer = trace.get_tracer("clinical-ai-service.tools")
with tracer.start_as_current_span(name) as span:
span.set_attribute(_KIND_KEY, OpenInferenceSpanKindValues.TOOL.value)
span.set_attribute(SpanAttributes.TOOL_NAME, name)
span.set_attribute(SpanAttributes.INPUT_VALUE,
json.dumps(args or {}, ensure_ascii=False, default=str))
yield span
教訓:自動 instrument 只覆蓋它認得的框架原語。自己手刻的工具分派、自訂的 agent loop,這些 span 得自己補。Phoenix 給了 OpenInferenceSpanKindValues 這套語意,補的時候照著標 TOOL / CHAIN / AGENT,UI 上才會分類正確。
💰 真正的 ROI:醫師回饋回掛 trace
前面都還只是「把黑箱攤開」。Phoenix 真正讓我覺得值得的,是這段閉環。
流程是這樣的:
/analyze 回 response 時,順手帶上這次 trace 的 phoenix_span_id 和 phoenix_trace_iddose_too_high/feedback/annotatedef log_doctor_annotation(span_id, *, rating, label, comment, doctor_id):
client = _phoenix_client()
if client is None:
return False
# score 直接用原始星等 1~5,不正規化成 0~1,臨床端篩低分直接 < 3
client.spans.add_span_annotation(
span_id=span_id,
annotation_name="doctor_feedback",
annotator_kind="HUMAN",
label=f"{rating}★ / {label}", # 例:2★ / dose_too_high
score=float(rating),
explanation=comment or None,
metadata={"doctor_id": doctor_id, "rating": rating},
sync=True,
)
return True
這段做完,我在 Phoenix UI 上可以直接撈出所有醫師打 ≤2 星的決策,一鍵點開那次完整的推理 trace:它當時讀了哪些 skill、抄了哪筆 few-shot、LLM 怎麼一步步收斂到那個劑量。這就是 LLM 產品該有的 human-in-the-loop evaluation 基建——不是測 latency,是把「真人專家的判斷」黏回「模型的決策過程」上,變成可以回看、可以累積、可以拿去做下一輪改進的資料。
我在 annotation 上刻意留了兩個設計:score 用原始 1~5 星不正規化(臨床端直覺,篩低分直接 < 3),以及整段是 best-effort 不碰主流程——Phoenix 沒開、送失敗一律回 False,因為醫師回饋本身已經落地在前端資料庫,標註失敗絕不能反向害得回饋送不出去。追蹤永遠是附加品,不能傷害主流程,這條原則貫穿整個 323 行的 tracing 模組。
值得提醒的一個合規點:我的 traced_node 會把節點的進出 state 序列化進 span,裡面包含病人資料(病摘、檢驗值、ICD10)。如果你的場景也有 PII,務必確認 Phoenix 部署在受控環境、存取有上鎖——這也是我堅持本地自架、trace 不外傳的原因。
❓ 常見問題
Q1:Arize Phoenix 是免費的嗎?
開源版(self-host)是免費的,Elastic License 2.0,功能包含 tracing、evaluation、prompt playground、LLM-as-judge。Arize 另有商業版 AX(雲端託管、依 span 數計價)。自架版對絕大多數團隊夠用。
Q2:Phoenix 跟 Langfuse 我該選哪個?
要框架無關、要透明 volume-based 計價 → Langfuse。要全開源 + 單容器好自架 + eval 嚴謹(尤其受監管 / 高準確度要求的場景)→ Phoenix。我做醫療 CDSS 選 Phoenix,主因是單容器自架 + 資料不外傳。
Q3:一定要用 LangChain / LangGraph 才能接嗎?
不用。Phoenix 走 OpenTelemetry + OpenInference,框架無關。我這次甚至刻意關掉 langchain 自動 instrument、改手動串節點(見踩坑一)。只要你的 LLM 呼叫走 OpenAI 相容 API,OpenAIInstrumentor 就抓得到。
Q4:接了會不會拖慢正式環境 / 增加風險?
我的做法是 opt-in:沒設 PHOENIX_COLLECTOR_ENDPOINT 就完全 no-op,連追蹤套件沒裝都不會讓服務啟動失敗(每個 import 都包 try/except)。正式環境不想開就不設那個 env,零成本。
Q5:trace 資料會外傳嗎?
self-host 版本 trace 全部落在你本地的 docker volume(phoenix_data),不外傳第三方。處理敏感資料時這是關鍵——但要注意 span 裡若記了 PII,得自己管好 Phoenix 的存取權限。
🔗 延伸資源
- Arize Phoenix GitHub — 原始碼、release notes、docker 自架說明
- Phoenix 官方文件 — tracing / evaluation / OpenInference 慣例
- OpenInference 規範 — OTel 之上的 AI 語意慣例(span kind、屬性命名)
- Phoenix vs Langfuse 官方比較 — 兩邊各自的定位