Arize Phoenix:LLM观测框架
"如果你深入研究,一切都会很有趣。" — 理查德·费曼
现代LLM系统之所以令人印象深刻,很大程度上是因为它们的行为隐藏在抽象之后。提示进去,答案出来——在这之间,一个"智能"系统做出难以检查、更难调试、几乎无法在规模上信任的决策。
本文故意走向相反的方向。与其将智能体、工具和模型视为魔法,我们剥开层次,追踪幕后实际发生的事情:LangChain/LangGraph智能体如何推理、何时调用工具、vLLM托管的模型如何响应,以及Arize Phoenix如何捕获每个步骤作为可观测、可测量的数据。通过深入研究——进入追踪、跨度、延迟和令牌流——我们将一个不透明的LLM工作流转变为你能够理解、推理并自信演进的系统。
在本指南中,我们将介绍如何为基于本地LLM的应用程序设置完整的可观测性栈。我们的栈将包括:
- vLLM提供Qwen编码模型(Qwen3-Coder-Next,FP8),直接在2节点NVIDIA DGX-Spark系统的一个节点上运行,作为本地OpenAI兼容API暴露。
- 一个LangChain/LangGraph智能体,在同一DGX-Spark系统上运行,调用本地vLLM端点并集成Tavily进行网络搜索。
- Arize Phoenix,在同一2节点DGX-Spark系统上的Kubernetes上自托管,由PostgreSQL支持,用于捕获追踪并提供可观测性UI。
我们将介绍每个组件如何组合在一起、如何通过Kustomize在K8s上部署Phoenix(带Postgres)、如何使用OpenTelemetry为智能体设置追踪工具,以及如何解决常见问题。让我们开始吧!
1、架构概述
完全本地运行在2节点DGX-Spark系统上。
我们的目标是一个生产级的LLM可观测性设置,完全运行在双节点NVIDIA DGX-Spark系统上。
vLLM模型服务器(DGX-Spark节点A)我们在一个DGX-Spark节点上运行vLLM,托管一个大型、专注于代码的模型(Qwen3-Coder-Next-FP8),位于OpenAI兼容的REST API之后。vLLM通过连续批处理和FP8量化提供高吞吐量推理,暴露如/v1/chat/completions的端点。在此设置中,vLLM直接在DGX-Spark上启动并监听端口8888。
LangChain + LangGraph智能体(DGX-Spark节点A)LLM智能体与模型服务器一起运行在同一DGX-Spark系统上,通常来自Jupyter笔记本或Python运行时。它使用OpenAI兼容接口调用本地vLLM端点,并将Tavily集成为实时网络搜索的工具。从智能体的角度来看,这 behaves 完全像调用OpenAI——只是所有内容都保持本地。
Arize Phoenix + PostgreSQL(DGX-Spark节点B)第二个DGX-Spark节点通过Kubernetes运行Arize Phoenix和PostgreSQL。Phoenix从智能体接收OpenTelemetry追踪,将其存储在Postgres中,并暴露一个UI用于检查LLM调用、工具使用、延迟和推理步骤。虽然Phoenix部署在Kubernetes上,但它仍然是同一DGX-Spark系统的一部分。
逻辑分离的组件,单一的物理系统——这保持整个可观测性循环本地、可复现且易于理解。
为什么这个设置? 它允许你在具有完整可观测性的情况下本地迭代LLM应用程序,而无需将数据发送到外部服务。vLLM + Qwen给你一个类似于OpenAI的"AI服务器",Tavily添加实时搜索能力,Phoenix带来类似于LangSmith或OpenAI可观测性的工具,但完全自托管。你将能够精确检查你的智能体正在做什么一步一步——它生成了什么提示、每个调用花了多长时间、使用了多少令牌等。这对于调试和优化复杂的LLM智能体来说是无价的。
在我们动手之前,让我们先设置所需的基础设施组件。
2、基础设施设置:一台DGX-Spark,两个节点
此部署使用单台NVIDIA DGX-Spark系统,两个节点:
- 节点A运行vLLM模型服务器和LangChain/LangGraph智能体运行时。
- 节点B运行Kubernetes工作负载,用于Arize Phoenix和PostgreSQL。
没有外部GPU服务器或单独的开发机器。智能体、模型服务和可观测性栈都在同一DGX-Spark环境内执行。
Kubernetes仅在增加价值的地方使用(Phoenix + Postgres)。vLLM服务器和智能体可以直接在主机或容器中运行,但仍保持在DGX-Spark本地。网络是集群内部的,Phoenix通过Kubernetes LoadBalancer或kubectl port-forward访问。
接下来,我们将在K8s集群上部署Phoenix,包括其Postgres数据库,并确保为我们的环境配置(命名空间、节点调度、存储等)。
3、在Kubernetes上部署Arize Phoenix
我们通过Kustomize使用Postgres。
Arize提供了一个我们可以部署在自己集群上的Phoenix开源发行版。我们将使用官方GitHub仓库的Kustomize模板来设置它。默认情况下,Kustomize部署将启动Phoenix(服务器)和PostgreSQL,并带有持久卷。我们将自定义一些东西:命名空间、节点亲和性和存储类。
步骤1:克隆Phoenix仓库并准备命名空间首先,克隆仓库并为Phoenix创建命名空间:
git clone https://github.com/Arize-ai/phoenix.git
cd phoenix
kubectl create namespace phoenix # 选择一个命名空间(例如"phoenix")
默认情况下,Kustomize清单没有指定命名空间,因此创建并定位一个可以保持整洁。
步骤2:(可选)自定义部署参数我们可以使用Kustomize覆盖或补丁来定制部署。例如,让我们将Phoenix和Postgres固定到特定节点,并确保使用Longhorn进行存储。创建一个文件(例如kustomize/local/patches.yaml),包含以下内容,根据需要调整标签/名称:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: phoenix # 在"phoenix"命名空间中部署所有资源
resources:
- ../base
# 自定义基础资源的补丁
patches:
# 将Phoenix服务器部署固定到特定节点(例如,具有标签或主机名"spark-ba63"的节点)
- target:
kind: Deployment
name: phoenix-server
patch: |-
spec:
template:
spec:
nodeSelector:
kubernetes.io/hostname: spark-ba63 # 替换为你的节点名称或使用标签
# 将Postgres固定(假设它是一个名为"postgres"的StatefulSet)
- target:
kind: StatefulSet
name: postgres
patch: |-
spec:
template:
spec:
nodeSelector:
kubernetes.io/hostname: spark-ba63
# 对Postgres PVC使用Longhorn存储类(假设PVC名为"postgres-data")
- target:
kind: PersistentVolumeClaim
name: postgres-data
patch: |-
spec:
storageClassName: longhorn # 确保这与你的Longhorn SC名称匹配
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi # 或你需要的任何大小
此覆盖层做三件事:在我们的phoenix命名空间中部署所有内容,在节点spark-ba63上调度pod(你可以使用目标节点上存在的任何标签,或不同的nodeSelector表达式),并强制执行Postgres数据PVC使用Longhorn进行存储(在此示例中为10Gi卷请求)。节点固定是可选的;如果你的集群可以在具有存储访问权限的任何节点上调度pod,你可以跳过它或根据需要使用反亲和性。Longhorn存储类确保我们的Postgres数据被复制并在节点故障时存活,而不是使用可能不受Longhorn支持的默认值。
步骤3:部署Phoenix和Postgres应用Kustomize配置来设置Phoenix。如果你创建了如上所示的覆盖层,应用它;否则,你可以直接应用基础(但请记住单独指定命名空间):
# 使用我们的自定义覆盖层
kubectl apply -k kustomize/local
# (或者,在phoenix命名空间中应用基础)
# kubectl apply -k kustomize/base -n phoenix
Kustomize将创建Phoenix部署和Postgres数据库部署(或StatefulSet)以及用于存储的PVC。验证pod是否已启动并正在运行:
kubectl get pods -n phoenix
NAME READY STATUS RESTARTS AGE
phoenix-dc8c8d9c-ktwdc 1/1 Running 2 (13h ago) 13h
postgres-0 1/1 Running 0 13h
还要检查Phoenix的服务是否存在。在基础配置中,它可能默认为ClusterIP服务。要外部访问Phoenix,让我们暴露它:
步骤4:暴露Phoenix UI如果你的集群中有LoadBalancer,你可以编辑Phoenix服务以使用type: LoadBalancer。例如:
kubectl patch service phoenix-server -n phoenix -p '{"spec": {"type": "LoadBalancer"}}'
片刻之后,kubectl get svc -n phoenix phoenix-server应该会显示一个外部IP(如果配置了MetalLB或云LB)。注意,Phoenix默认监听端口6006,它在同一端口上提供Web UI和HTTP追踪收集器。
如果你没有LoadBalancer,你可以在需要时使用端口转发:
kubectl port-forward svc/phoenix-server -n phoenix 6006:6006
这将使Phoenix UI在你的本地机器上可访问于http://localhost:6006(并且也允许向此端口发送OTLP追踪)。
此时,Phoenix已部署并且(假设默认情况下没有身份验证)其Web界面已准备好。你可以通过在浏览器中打开URL(LoadBalancer URL或如果端口转发的localhost:6006)来测试。你应该看到Phoenix UI正在加载。最初它将没有数据,这是预期的。
步骤5:配置Phoenix(Postgres等)Kustomize部署已设置为使用Postgres DB(通过环境变量或Phoenix默认值)。Phoenix将使用提供的PHOENIX_SQL_DATABASE_URL或单独的Postgres主机/用户/密码环境变量。在我们的设置中,容器可能从ConfigMap或直接规范中获取这些。如果你需要调整(例如,如果你设置了自己的Postgres实例),你可以设置:
# 在Phoenix部署环境中:
- name: PHOENIX_POSTGRES_HOST
value: <postgres-service-host>
- name: PHOENIX_POSTGRES_USER
value: <user>
# ... 以及密码、数据库名称等
但使用捆绑的Postgres,使用默认值(检查Phoenix日志以查看它是连接到SQLite还是Postgres)。Arize文档指出,如果没有提供SQL URL,Phoenix将默认以基于文件的SQLite启动。我们的Kustomize应该覆盖这一点以指向Postgres,为我们提供持久存储用于追踪。
这就是部署Phoenix的全部!我们现在有一个本地运行的可观测性服务。接下来,我们将设置开发环境以使用它。
4、设置智能体环境(LangChain + Phoenix工具)
在开发端(DGX-Spark),我们需要为智能体和可观测性工具安装必要的Python包。重要的是要安装Arize的Phoenix客户端/SDK和OpenInference工具包,而不是任何类似命名的包,以避免混淆。特别要注意的是,不要从PyPI安装遗留的phoenix==0.9.1包——那不是Arize的Phoenix(它是一个不相关的包,会导致错误)。相反,使用官方的arize-phoenix库。
打开你的Python环境(这可以是conda env或Jupyter中的venv)并安装:
pip install arize-phoenix arize-phoenix-otel openinference-instrumentation-langchain langchain-openai langchain-core
让我们分解这些:
- arize-phoenix — Phoenix平台客户端(和CLI)库。严格来说,仅发送追踪不是必需的(OTEL包可以独立运行),但如果需要使用Phoenix的Python API或CLI,它可能很有用。它还确保你为任何子包提供匹配的版本。
- arize-phoenix-otel — Phoenix OpenTelemetry SDK。这提供了phoenix.otel模块,带有便捷的register()函数来设置追踪。将其视为使用Phoenix特定默认值配置OpenTelemetry的更简单方法。
- OpenInference工具包 — 我们特别包括openinference-instrumentation-langchain(用于追踪LangChain)。如果auto_instrument=True,Phoenix OTEL SDK将自动获取任何已安装的OpenInference工具,因此通过安装它,我们启用了LangChain内部的详细追踪。(Phoenix/OpenInference还有其他框架的工具,如OpenAI API、LlamaIndex等,可以根据需要安装,但对于我们的情况,LangChain覆盖了智能体并间接覆盖了OpenAI调用。)
- LangChain包 — 我们安装langchain-openai和langchain-core,它们对应于LangChain的新模块化设置(在LangChain v1.x中,像OpenAI这样的提供商是单独的子包)。这将使我们能够访问ChatOpenAI和智能体工具。或者,你可以直接安装langchain,但拆分确保我们有需要的部分。如果我们计划直接使用其工具,我们还包括langgraph(可选,但推荐,因为我们的智能体是"基于LangGraph的")。事实上,类似示例的推荐安装是:pip install arize-phoenix-otel openinference-instrumentation-langchain langgraph langchain-openai langchain-core。
安装后,验证你没有安装错误的Phoenix包:
import phoenix
print(phoenix.__version__)
# 例如 12.33.1
它应该显示一个版本 >= 1.x(或最新的,例如,撰写本文时为0.13.x或更高,因为Phoenix从0.9开始进行了重大更新)。如果你不小心看到0.9.1,请卸载它并重新安装arize-phoenix。
5、工具化和创建LangChain智能体
有了依赖项,我们可以编写智能体代码。大纲是:
- 注册Phoenix追踪器在其他任何操作之前,以便所有后续导入和调用都被工具化。
- 设置OpenAI API基础以指向我们的vLLM服务器(以便ChatOpenAI调用转到Qwen)。
- 使用LangChain的create_agent或LangGraph的create_react_agent定义我们的智能体——包括任何工具(Tavily搜索等)。
- 运行智能体并观察追踪被发送到Phoenix。
让我们一步一步地在代码中进行:
import os
from phoenix.otel import register
# 1. 初始化Phoenix追踪
tracer_provider = register(
project_name="local-llm-app",
auto_instrument=True
)
register()调用连接到我们的Phoenix实例并配置OpenTelemetry。默认情况下,除了项目名称之外没有其他参数,它将读取环境变量以获取收集器端点。我们需要告诉它在哪里发送追踪。如果你在同一台机器上使用端口转发运行Phoenix,则使用默认的http://localhost:6006。为了清楚起见,让我们明确设置它:
# 在你的shell或笔记本环境中设置:
export PHOENIX_COLLECTOR_ENDPOINT="http://localhost:6006"
此环境变量被register()拾取以将追踪发送到Phoenix的收集器。如果你为Phoenix有LoadBalancer IP或域名,请使用它(例如,http://<phoenix_host>:6006)。注意: 对于我们的自托管实例(禁用身份验证),不需要API密钥;对于Phoenix Cloud,你还需要设置PHOENIX_API_KEY。
使用auto_instrument=True,Phoenix的SDK将自动激活所有已安装的相关OpenInference工具。这意味着一旦我们导入LangChain或OpenAI库,它们就会被修补以发出遥测数据。(在底层,它等同于手动执行类似LangChainInstrumentor().instrument()或OpenAIInstrumentor().instrument()的操作,但我们是免费获得它的。)
现在,让我们配置OpenAI聊天模型以使用我们的vLLM Qwen端点:
from langchain_openai import ChatOpenAI
# 2. 配置OpenAI API以使用本地vLLM服务器
os.environ["OPENAI_API_KEY"] = "not-needed" # vLLM不使用它,但LangChain/OpenAI SDK可能需要一个变量
llm = ChatOpenAI(
model_name="Qwen/Qwen3-Coder-Next-FP8", # vLLM期望的模型名称(如加载的)
temperature=0,
openai_api_base="http://localhost:8888/v1", # vLLM端点;如果不是本地则调整主机
openai_api_key="unused"
)
在这里,我们创建一个ChatOpenAI实例指向localhost:8888/v1(vLLM正在提供我们的模型的地方)。我们传递model_name为Qwen/Qwen3-Coder-Next-FP8,因为我们就是这样启动vLLM的(它将在请求中接受此作为模型标识符)。如果你用不同的名称或别名启动vLLM,请使用它。openai_api_base包括/v1路径以模仿完整的OpenAI API URL。我们还设置了一个虚拟API密钥以满足客户端;vLLM默认不需要身份验证,因此任何字符串(或可能是空的)都可以。现在llm的行为就像一个指向我们本地模型的OpenAI ChatCompletion代理。
接下来,我们为智能体定义工具。我们将集成Tavily搜索,以便智能体可以获取实时信息。Tavily有一个API;假设我们在TAVILY_API_KEY中设置了API密钥。LangChain没有像JS那样为Tavily内置的Python包装器,因此我们将使用简单的requests调用或Tavily的SDK(如果可用)实现一个自定义工具。为了说明:
import requests
from langchain_core.tools import Tool
TAVILY_API_KEY = os.environ.get("TAVILY_API_KEY", "<your Tavily key>")
def tavily_search(query: str) -> str:
"""使用Tavily API获取查询的搜索结果。"""
url = "https://api.tavily.com/search"
params = {"query": query, "max_results": 5, "topic": "general"}
headers = {"x-api-key": TAVILY_API_KEY}
resp = requests.get(url, params=params, headers=headers)
if resp.status_code == 200:
data = resp.json()
# 将响应简化为LLM可以使用的字符串。
results = [item.get("snippet", "") for item in data.get("results", [])]
return " ; ".join(results) # 返回片段连接
else:
return f"搜索错误(状态 {resp.status_code})"
# 将此函数包装为LangChain工具
tavily_tool = Tool.from_function(
func=tavily_search,
name="TavilySearch",
description="使用Tavily搜索网络获取最新信息"
)
(上面是一个基本示例。在实践中,你可能会使用LangChain或LangGraph的@tool装饰器来更优雅地创建工具。另外,考虑速率限制和错误——如果查询太宽泛,Tavily可能需要处理。)
我们还可以包括其他工具或基础工具。例如,如果我们指定,LangGraph的create_react_agent会自动添加一些基础工具(如计算器)。为了聚焦,我们将在此智能体中仅使用我们的Tavily工具。
现在,让我们创建智能体。我们有两个选择:— 使用LangChain的新create_agent函数(它在内部使用LangGraph构建基于图的智能体)。— 直接使用LangGraph的create_react_agent(非常相似的结果)。
我们将使用LangChain的create_agent以获得熟悉度。如果需要,我们还为智能体的行为准备系统提示。
from langchain.agents import create_agent
SYSTEM_PROMPT = "你是一个有用的研究助手,可以访问网络搜索工具。在需要时使用该工具查找信息,然后提供详细答案。"
# 3. 创建智能体
agent = create_agent(
model=llm,
tools=[tavily_tool],
system_prompt=SYSTEM_PROMPT
)
就这样——我们现在有一个智能体,它将使用我们的Qwen模型(llm),并可以在适当时调用Tavily搜索工具。该智能体本质上实现了ReAct循环:它将决定是否/何时使用TavilySearch工具,然后使用模型来形成最终答案。
使用LangChain v1.x,智能体暴露一个invoke()方法来在提示上运行它。让我们用一个简单的查询来测试它。
# 4. 使用示例问题运行智能体
user_question = "最新的Python版本是什么,它的主要新功能有哪些?"
result = agent.invoke({"messages": [
{"role": "user", "content": user_question}
]})
print(result["messages"][-1]["content"])
我们以预期的格式传递用户查询(一个带有"messages"列表的字典,包括用户角色内容)。智能体现在将经历推理:模型可能会决定使用搜索工具(Tavily)来查找最新Python版本的信息,通过我们的tavily_search函数执行搜索,获取结果,然后继续对话以产生答案。
关键的是,因为我们在开始时注册了追踪器和工具,所有这些步骤都被追踪。LangChain的OpenInference工具将自动为智能体的操作创建跨度。对于每次对Qwen的LLM调用(通过ChatOpenAI),将有一个跨度,对于每次工具调用(TavilySearch),也将有一个跨度——包括工具的输入和输出。Phoenix正在实时捕获这些。
6、在Phoenix中观察追踪
通过智能体运行一个或多个查询后,前往Phoenix UI(端口6006上的Web界面)。你应该在主页上看到追踪列表——每个追踪对应一次智能体调用(每次agent.invoke调用)。例如,如果你运行了上面的user_question,将出现一个追踪条目。
每个追踪条目将有元数据,如追踪ID、项目名称(例如"local-llm-app")和时间戳。点击追踪以打开追踪详细视图。在这里,你将看到智能体的完整执行流程,可视化为跨度树(通常以时间线/甘特图样式或分层列表显示):
将有一个顶级跨度用于整体智能体运行(可能标有智能体或链名称)。
在它下面,你将看到每个步骤的跨度。例如,LLM思考步骤的跨度,然后是TavilySearch工具调用的跨度(带有它搜索的查询),然后是智能体处理工具结果的另一个LLM调用跨度,最后是LLM答案生成跨度。
每个跨度可以点击以显示详细信息,例如:
- 输入:例如,发送到模型的提示或工具输入。
- 输出:例如,模型生成的文本或工具的返回值。
- 延迟:该调用花费了多长时间(跨度持续时间)。
- 属性:元数据,如模型名称、令牌数等(Phoenix追踪令牌计数,甚至成本(如果适用),对于支持的提供商)。
因为我们安装了LangChain工具,Phoenix使用OpenInference语义约定来有意义地标记这些跨度(它知道什么是工具与LLM)。例如,智能体的每个步骤、工具执行和最终响应都会自动追踪到Phoenix。你应该在追踪中看到我们的工具名称(例如"TavilySearch")。如果你展开跨度,你可能会看到像input.value这样的属性,包含工具调用的JSON(在Phoenix UI中可能格式很好)和output.value作为工具结果。对于LLM跨度,你将看到提示和模型的原始响应。如果可用,Phoenix还可能突出显示令牌使用情况(对于OpenAI API调用它会;对于我们的vLLM,由于它模仿OpenAI,如果vLLM在响应中返回它们,我们可能会获得令牌计数)。
此外,在Phoenix UI中,你有过滤或搜索追踪、比较追踪等功能。例如,Phoenix可以在表格中显示所有追踪,列有延迟、总令牌数等,你可以按这些排序以找到异常值。这是一个强大的AI可观测性工具包,为你提供类似于APM(应用程序性能监控)工具为微服务所做的洞察,但专门针对LLM应用程序。我们现在基本上有了自己的OpenAI风格的开发者控制台,但是自托管的——我们可以实时观看我们的提示和工具的表现如何。
以下是单个查询追踪你应该在Phoenix中看到的内容的快速回顾:
- 追踪时间线: 按时间顺序排列的动作序列(例如"LLM:思考"、"工具:TavilySearch"、"LLM:回答")。
- 跨度详细信息: 点击动作显示内容。对于Tavily搜索跨度,你可能会看到智能体构建的搜索查询(可能包括原始问题或修改版本)和返回的顶级结果。对于LLM跨度,你将看到提示(包括对话上下文、系统提示等)和模型的输出。
- 性能指标: 每个跨度的持续时间是可见的,在顶层你可能会看到总追踪时间。如果使用OpenAI或类似,你还会看到令牌计数和成本;使用vLLM,除非我们添加自定义工具,否则这些可能不会出现,但延迟是有的。
- 项目和会话信息: 由于我们在注册时设置了project_name="local-llm-app",Phoenix在该项目下对追踪进行分组。你可以有多个项目(例如dev vs prod)。我们没有手动设置会话ID,但如果你提供span config中的session_id,Phoenix可以对多轮会话进行分组。(在我们的示例代码中,我们没有,但你可以传递config={"configurable": {"thread_id": "some-session-id"}}给agent.invoke,如某些LangGraph示例中所示。)
这种可见性让我们调试和优化智能体:— 我们可以看到智能体是否不必要地或太多次调用工具。— 我们可以测量模型花费的时间与工具调用的时间。— 我们可能会发现模型提示不是我们预期的(可能系统提示没有正确应用或工具的说明需要调整)。— 如果某些东西失败(工具中的异常等),追踪将在该跨度上显示错误状态。
7、操作技巧和辅助脚本
管理设置涉及一些重复性任务,因此这里有一些辅助脚本/命令和技巧:
- 部署/更新脚本
如果你需要重新部署Phoenix(比如你更新了版本或更改了配置),你可以创建一个简单的脚本deploy_phoenix.sh:
#!/bin/bash
kubectl apply -k path/to/phoenix/kustomize/local -n phoenix
这将重新应用Kustomize配置。因为我们对Postgres使用持久存储,重新部署Phoenix(或甚至删除其pod)将保持PVC上的数据完整。
- 删除脚本
要拆除部署,例如delete_phoenix.sh:
kubectl delete -k path/to/phoenix/kustomize/local -n phoenix
# 如果你想完全删除命名空间:
# kubectl delete namespace phoenix
注意: 这也会删除PVC(取决于你的Kustomize设置和回收策略),因此如果你想保留追踪,请谨慎使用。
- 端口转发
对于无需LB的快速访问,请使用:
kubectl port-forward svc/phoenix-server -n phoenix 6006:6006 >/dev/null 2>&1 &
echo "Phoenix UI available at http://localhost:6006"
&将其置于后台。你可以将此脚本化为forward_phoenix.sh。完成后记得终止端口转发。
- 环境配置
在你的开发环境中,而不是每次都设置PHOENIX_COLLECTOR_ENDPOINT,你可以将其放在.env或Jupyter内核启动中。或者,如果你不想依赖环境变量,你可以在代码中通过register(endpoint="http://:6006")提供它。Phoenix文档显示两种选项;环境变量往往更简单。
- 项目命名
你可以在register()中使用不同的project_name来逻辑上分离追踪(例如"my-llm-app-dev" vs "my-llm-app-prod")。Phoenix的UI允许你按项目过滤。对于本地开发,它可能都是"default"或一个名称——由你决定。
8、故障排除和常见问题
即使有最好的指南,事情也可能出错。以下是此设置的一些故障排除技巧:
- Phoenix UI中没有显示追踪
如果你在运行智能体后看到Phoenix UI但没有追踪,首先确保智能体代码在调用register()后实际执行。如果是,可能的问题是连接性。检查PHOENIX_COLLECTOR_ENDPOINT是否正确设置为Phoenix服务器的可达URL。如果使用端口转发,应该是http://localhost:6006(不需要/v1/traces——SDK会附加路径)。如果使用LoadBalancer,可能是http://:6006。你可以通过从开发机器运行如下内容来测试可达性:
curl -X POST http://<phoenix_host>:6006/v1/traces -H "Content-Type: application/x-protobuf" --data-binary @/dev/null
没有适当数据它不会完全工作,但应该返回400左右,确认你可以命中端点。另外,检查Phoenix服务器日志(kubectl logs phoenix-server)以查看任何传入的追踪日志或错误。如果端点不正确或被防火墙阻止,追踪永远不会到达。明确设置环境变量(如上所示)通常可以修复不匹配。
- 安装了错误的phoenix包
如前所述,如果你不小心执行了pip install phoenix(这给出2013年的0.9.1版本),你将遇到错误(特别是在Python 3.11+上)。解决方法是卸载它并改为安装arize-phoenix。始终使用arize-前缀的包名。
- 版本不匹配
确保arize-phoenix-otel和openinference-instrumentation-langchain版本兼容。这些更新很快;如果你在不同时间安装它们,较新的Phoenix可能期望较新的工具。最好一起更新它们。例如,如果Phoenix是0.13.x版本,使用类似版本的arize-phoenix-otel。openinference-instrumentation-langchain应该与OpenInference规范保持最新。如果你在追踪期间看到错误(如缺少属性或关于跨度处理的警告),请考虑升级这些包(pip install -U …)。
- LangChain版本问题
我们的智能体代码使用LangChain v1风格API(create_agent,agent.invoke)。确保你有langchain>=1.0.0。如果你有旧的0.xx版本,create_agent函数将不存在。单独的langchain-openai包通常会拉入LangChain 1.x核心。如果你遇到麻烦,请显式安装langchain==1.2.(或最新)以获得正确的版本。还要注意,LangChain v1更改了导入路径(例如,像我们那样使用langchain_openai)。如果你看到导入错误,请仔细检查你是否安装了正确的子包。
- Tavily API问题
如果智能体没有从搜索中获得好的结果,或者出错,可能是Tavily集成。确保你的Tavily API密钥有效,并且你没有耗尽任何配额。你可以在测试期间打印/记录tavily_search函数的行为。由于这是第三方工具调用,它不会...· 如果LoadBalancer IP未分配: 如果kubectl get svc phoenix-server显示外部IP为,你的集群可能没有LoadBalancer供应程序。在裸机上,你需要安装MetalLB并用IP池配置它。如果没有完成,请坚持使用端口转发或NodePort服务作为变通方法。
- OpenTelemetry数据量
如果你生成大量追踪(比如在循环中运行许多查询),请注意内存/存储。Phoenix将把追踪数据存储在Postgres中——对于文本数据来说可能会增长。对于开发来说这很好(Phoenix UI有过滤功能并且可以导出数据),但要注意如果你以某种方式摄取了数千条追踪;你可能需要分配更多Postgres存储或修剪旧追踪。Phoenix有数据保留设置(默认无限期保留),你可以通过环境变量配置。
- 性能考虑
在vLLM上运行Qwen FP8模型是资源密集型的。如果智能体很慢,请考虑Qwen可能很重——原始生成约43令牌/秒,但如果你的提示很大或需要多个步骤,响应将需要几秒钟。对于单个GPU上的7B+模型来说这是正常的。可观测性增加了最小的开销(OpenTelemetry追踪非常轻量级),但如果你怀疑有开销,你可以在register()中切换batch=False以立即发送跨度(非批处理)或根据需要调整协议为gRPC。默认情况下它使用HTTP/protobuf,这对于开发来说很好。
最后,请记住可观测性是一种迭代辅助。通过Phoenix追踪,你可能会识别出使智能体不必要循环的提示或耗时太长的工具。然后你可以优化系统提示或代码,重新部署(Phoenix端无需更改),然后再次测试。Phoenix将捕获新的追踪,你可以比较前后。这种紧密的循环加速了LLM应用程序的调试,否则可能会感觉像黑盒。
9、演示:运行智能体 + 在Phoenix中查看追踪
本节是一个实用的"健全性演示",用于确认完整循环正在工作:
智能体(LangChain/LangGraph)→ vLLM(Qwen)→ Tavily工具调用 → OpenTelemetry跨度 → Phoenix UI
9.1 使用可用的Jupyter笔记本 + requirements
我保持一个可用的、可复现的演示笔记本(加上requirements.txt)在这里。
在你的DGX-Spark节点(或你的智能体运行时所在的地方)克隆它:
git clone https://github.com/dorangao/article-scripts.git
cd article-scripts/arize
创建一个环境并安装依赖项(选择一条路径):
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
9.2 设置环境变量(vLLM + Phoenix + Tavily)
export OPENAI_API_KEY="unused" # vLLM不需要它,但SDK通常期望一个值
export OPENAI_BASE_URL="http://localhost:8888/v1"
export PHOENIX_COLLECTOR_ENDPOINT="http://localhost:6006" # 或 http://<phoenix-lb-ip>:6006
export TAVILY_API_KEY="<your_tavily_key>"
如果Phoenix在Kubernetes上而你未使用LoadBalancer,请在另一个终端中运行端口转发:
kubectl port-forward svc/phoenix-server -n phoenix 6006:6006
9.3 运行笔记本演示
打开笔记本并从上到下运行单元格。你应该看到:
- 至少一个LLM跨度(vLLM调用)
- 至少一个工具跨度(Tavily搜索)
- 每次智能体调用的单个追踪
9.4 在Phoenix UI中验证
打开Phoenix:
http://localhost:6006(端口转发),或http://<LB-IP>:6006(LoadBalancer)
在追踪视图中,点击追踪并确认你可以看到:
- 智能体步骤
- LLM请求/响应
- Tavily工具输入/输出
- 每个跨度的时间(延迟)
10、结束语
我们已经设置了一个强大的本地LLM可观测性栈:一个vLLM服务器托管一个强大的开放模型,一个由LangChain/LangGraph增强的智能体具有实时搜索功能,以及Arize Phoenix来追踪每一步。这种组合允许MLOps工程师和LLM开发人员在本地或本地环境中自由实验,同时对智能体的行为和性能保持清晰的可见性。我们涵盖了每个组件的架构和目的(vLLM用于本地模型服务,Tavily用于知识增强,Phoenix用于追踪和评估),部署了必要的基础设施(使用Kustomize YAML在K8s上部署Phoenix + Postgres),使用现代的基于OpenTelemetry的工具对代码进行工具化,并演示了如何运行查询和检查追踪。
通过遵循本指南,你可以构建自己的"OpenAI风格"开发栈,它是完全自包含的。你将能够回答不仅仅是 "模型回复了什么?"而是"它为什么那样回复,以及它做了什么才得到那个答案?"——所有这些都在本地设置的舒适环境中。愉快的追踪!
原文链接: Building a Local LLM Observability Stack with vLLM, Tavily, and Arize Phoenix
汇智网翻译整理,转载请标明出处