三次模型调用,占据 92% 的等待时间:使用 OpenTelemetry 实现人工智能 agent 可观测性
作者: 来自 Elastic Jeffrey Rengifo

换用更便宜的模型后,每个问题的成本降低了 76% 至 83%,但五个问题中有四个的响应变慢了。这个 LangChain agent 的 OpenTelemetry 生成式人工智能追踪数据逐步解释了其中的原因。
在你的数据所在之处构建和观测人工智能 agent。从 Elastic agent 构建器( Elastic Agent Builder )开始。你也可以立即开始免费云试用( free cloud trial ),或在你的本地计算机( local machine )上试用 Elastic。
在我们的 LangChain 购物助手中,一次耗时 10 秒的请求,有 92% 的时间花在三次模型调用上。追踪数据揭示了哪次调用是最大的耗时来源,因此我们将负责比较的模型换成了更便宜的模型。结果,每个问题的模型成本降低了 76% 至 83%。但五个问题中有四个的响应时间变长了;这出乎我们的意料,也正是我们同时测量成本和延迟的原因。
单看请求总耗时,无法知道哪个步骤让客户久等,或哪个步骤推高了模型费用,而人工智能 agent 可观测性可以帮你找到答案。购物助手即使给出了正确答案,仍可能耗时过长,花费也可能超出回答该问题所需的成本。agent 的运行具有不确定性,而且每个步骤都可能调用付费模型,因此你需要知道由哪个模型作答,以及它使用了多少词元。
本文将按照 OpenTelemetry 生成式人工智能约定( OpenTelemetry (OTel) generative AI (GenAI) conventions ),为一个 LangChain 购物助手添加插桩,并将追踪数据发送到 Elastic。随后,我们会在瀑布图中找到最慢的模型调用,并将提供商报告的费用关联到每个请求。然后,在追踪数据指向的步骤上测试更便宜的模型。如需背景知识,请进一步阅读使用 Elasticsearch 查询语言(ES|QL)调试大语言模型应用和Elastic agent 构建器的 OpenTelemetry 追踪( OTel tracing for Elastic Agent Builder )。我们将完整跟踪这一项变更,包括它未能带来改善的地方。
前提条件
要跟着操作,你需要:
- 一个 9.5 或更高版本的 Elastic Cloud 托管部署,以及托管 OpenTelemetry 协议端点( Managed OpenTelemetry Protocol (mOTLP) endpoint )地址。
- 一个能够创建和查询某个索引的 Elasticsearch 接口密钥,用于通过部署的默认推理端点,将示例商品目录存储在
semantic_text字段中。 - 一个 OpenRouter 接口密钥。助手通过 OpenRouter 调用 GPT-5.6 Luna 和 GPT-5.6 Sol,整个实验花费几美分。
- agent 构建器,用于最后一节。在 9.4 及更高版本中,它是每个解决方案视图的默认聊天体验,并在 Elastic Cloud 托管部署中提供预配置模型,因此无需人工智能连接器。在经典视图中,请先切换到 agent 构建器( switch to Agent Builder )。
你可以在此处( here )找到配套笔记本。它复现了本文的完整实验,从加载示例商店数据到比较两个模型,并在最后清理实验资源。
应用:LangChain 购物助手
我们的应用帮助客户选购咖啡机。他们可以询问商店政策,例如 How long do I have to return it,也可以比较产品,例如 Which coffee makers fit my budget and counter space? 示例商店包含六款咖啡机,以及退货和配送政策。在下面的示例中,客户希望购买一款价格低于 100 美元、宽度不超过 20 厘米、一次至少能冲煮四杯咖啡的咖啡机:

LangChain agent 会在两个工具之间选择:
answer_store_question查询退货和配送政策,然后使用价格较低的模型作答。compare_products检索六款咖啡机,并使用价格较高的模型,根据客户要求比较它们的价格、宽度、容量和功能。
在这个初始版本中,GPT-5.6 Luna 负责选择工具,并将工具的答案返回给客户。政策工具也使用 Luna;产品比较工具则使用 GPT-5.6 Sol。因此,每次完成请求都会进行三次模型调用:
- 选择工具。( Choose a tool. ) agent 读取问题,选择
answer_store_question或compare_products。 - 根据检索到的记录作答。( Answer using the retrieved records. ) 所选工具使用 Elasticsearch 中的商店记录调用模型。
- 返回答案。( Return the answer. ) agent 将工具的输出转达给客户。
Elasticsearch 提供商店记录。所选工具使用这些记录调用模型,然后由 agent 返回结果。
Agent 读取问题,从两个工具中选择一个,并返回该工具的答案;它本身不执行搜索。每个工具都是一个小型检索增强生成步骤:从 Elasticsearch 检索商店记录,并要求模型仅根据这些记录作答。模型决定执行哪种检索,代码决定如何执行。
Agent 的定义很简短:
def build_agent(comparison_model=LARGE_MODEL):
@tool
def answer_store_question(question: str) -> str:
"""Answer questions about returns or shipping policies."""
return answer(question, "policy", size=2, model_name=SMALL_MODEL)
@tool
def compare_products(question: str) -> str:
"""Compare coffee makers with the customer's requirements."""
return answer(question, "product", size=6, model_name=comparison_model)
return create_agent(
model=model(SMALL_MODEL),
tools=[answer_store_question, compare_products],
name="shop_assistant",
system_prompt=SHOP_INSTRUCTIONS,
)
两个工具都调用同一个 answer 辅助函数。该函数对商品目录的 semantic_text 字段执行 semantic 查询,筛选出政策或产品记录,再将匹配记录连同客户问题一起传递给模型。由于只有六款产品,`size=6` 会返回整个商品目录,因此实验的两个版本采用完全相同的检索。完整源代码和示例商品目录均位于配套笔记本中。有关这种检索模式的更多信息,请阅读如何使用 LangChain 和 Elasticsearch 构建 agent 知识库。
使用 OpenTelemetry 生成式人工智能约定实现人工智能 agent 可观测性
单看请求总耗时,无法了解内部的模型调用和检索过程。传统应用性能监控插桩可以看到发往模型提供商的出站加密超文本传输协议调用,却无法知道哪个模型作答、使用了多少词元,也无法知道哪个工具发起了调用。OpenTelemetry 生成式人工智能 LangChain 插桩工具( OTel GenAI LangChain instrumentor )会将 agent、工具及模型调用记录为相互关联的跨度。每个跨度描述一项操作及其时间信息;聊天跨度还包含模型和词元计数。
Elastic 没有提供自己的 LangChain 插桩工具。Elastic OpenTelemetry 发行版 Python 版本( Elastic Distribution of OpenTelemetry (EDOT) Python )是 Elastic 提供的 OpenTelemetry Python 软件开发工具包发行版,能够直接为 OpenAI 客户端、Amazon Bedrock 和 Google Vertex AI 调用插桩,但不支持 LangChain。本文使用的插桩工具来自 OpenTelemetry 项目,Elastic 像接收其他 OpenTelemetry 协议数据源一样接收其跨度。此前可观测性实验室的文章使用第三方库追踪 LangChain,例如 OpenLLMetry、OpenLIT 和 Langtrace。它们同样可用,但不同库的字段名称可能有所不同,而本文的查询以使用上游插桩工具为前提。
生成式人工智能语义约定( GenAI semantic conventions )按操作为这些跨度命名。本条追踪使用其中三种:
| 跨度名称( Span name ) | gen_ai.operation.name | 记录的内容( What it records ) |
|---|---|---|
invoke_agent shop_assistant | invoke_agent | 整个 agent 运行过程,包含 gen_ai.agent.name |
execute_tool compare_products | execute_tool | 一次工具调用,包含 gen_ai.tool.name |
chat openai/gpt-5.6-sol | chat | 一次模型调用,包含 gen_ai.request.model、`gen_ai.response.model`、`gen_ai.provider.name`、`gen_ai.usage.input_tokens` 和 gen_ai.usage.output_tokens |
遥测数据通过 OpenTelemetry 协议传入追踪数据流,本文中的每个视图都读取相同的跨度:

要自行运行该应用:
- 按照托管 OpenTelemetry 协议 Python 操作指南( Managed OTLP Python walkthrough )获取端点和凭据。
- 安装固定版本的依赖包。
- 加载八条示例商店记录(六款咖啡机和两项政策)。
- 导出下面的三个环境变量。
步骤 2 和步骤 3 详见配套笔记本。
该示例已使用 Python 3.12、LangChain 1.4.0、OpenTelemetry 软件开发工具包 1.44.0,以及 opentelemetry-instrumentation-genai-langchain 1.1b1 进行测试。下方截图和结果来自 Elastic Cloud 托管服务 9.6 部署:
export OTEL_EXPORTER_OTLP_ENDPOINT="https://YOUR_MANAGED_OTLP_ENDPOINT:443"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=ApiKey%20YOUR_INGEST_API_KEY"
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT="NO_CONTENT"
前两项设置将追踪数据发送到 Elastic。第三项关闭提示词和响应内容的采集,但仍会记录词元计数和时间信息。请在创建 LangChain agent 之前启用插桩工具:
provider = TracerProvider(resource=Resource.create({
"service.name": "shop-assistant",
}))
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)
LangChainInstrumentor().instrument()
使用 Elastic OpenTelemetry 发行版 Python 版本实现零代码插桩
要实现零代码插桩,请安装 Elastic OpenTelemetry 发行版 Python 版本( elastic-opentelemetry )和 opentelemetry-instrumentation-genai-langchain,然后使用 opentelemetry-instrument 启动应用。`edot-bootstrap` 目前尚不会安装 LangChain 插桩包,而 OpenLLMetry 的 opentelemetry-instrumentation-langchain 注册了相同的 langchain 名称,因此不要同时安装这两个包。
应用会为每个客户请求添加一个根跨度 shop.request,并为 Elasticsearch 搜索添加一个 retrieve products 跨度。它还会向 agent.invoke 传入 config={"metadata": {"agent_name": "shop_assistant"}}:插桩工具(1.1b1)会记录顶层 LangGraph 运行,也就是 create_agent 返回的对象;除非运行元数据指定了 agent 名称,否则该运行会被记录为 invoke_workflow。
在追踪瀑布图中找到最慢的模型调用
在 Kibana 中,打开可观测性( Observability ) > 应用( Applications ) > 服务清单( Service inventory ),选择 shop-assistant,再选择 shop.request 事务,然后选取产品问题对应的一条追踪。瀑布图会显示路由调用、所选工具、该工具的检索和模型调用,以及最终响应调用:

这次请求耗时 10.10 秒。`compare_products` 工具耗时 4.93 秒,展开后便能看出原因:Sol 的比较调用耗时 4.20 秒,而检索产品仅耗时 690 毫秒。初始路由调用另耗时 1.91 秒,最终响应调用耗时 3.15 秒。三次模型调用约占此次请求总耗时的 92%,为我们指明了值得重点优化的地方。
工具跨度的耗时包含其子跨度的工作,因此将两者相加会重复计算。应使用根跨度查看客户实际等待的时间,再展开子跨度,找出开销较大的步骤。
选择 Sol 聊天跨度,打开其 生成式人工智能( GenAI )选项卡,查看模型和词元用量。此次 Sol 调用读取了 680 个输入词元,生成了 268 个输出词元。即使禁用消息采集,模型标识和词元计数仍然可见。生成式人工智能选项卡和瀑布图词元标记( GenAI tab and the waterfall token badges )在 Elastic 技术栈 9.6 和 Elastic Cloud 无服务器服务中以技术预览版提供。在 9.5 中,请改为打开跨度的元数据( Metadata )选项卡,并按 gen_ai 筛选:

使用 ES|QL 比较每次模型调用
ES|QL 同样可以访问这些 gen_ai.* 字段,因此你可以比较该服务的所有模型调用,而不只是某一条追踪。在 数据探索( Discover )中切换到 ES|QL,并选择本次运行的时间范围。然后,按作答模型对聊天跨度分组:
FROM traces-generic.otel-default
| WHERE resource.attributes.service.name == "shop-assistant"
AND attributes.gen_ai.operation.name == "chat"
| STATS calls = COUNT(*), avg_ms = ROUND(AVG(duration) / 1e6),
input_tokens = SUM(attributes.gen_ai.usage.input_tokens),
output_tokens = SUM(attributes.gen_ai.usage.output_tokens)
BY model = attributes.gen_ai.response.model
| SORT avg_ms DESC
| KEEP model, calls, avg_ms, input_tokens, output_tokens
| LIMIT 10

在追踪中跟踪每次请求的大语言模型成本
词元计数和计费金额是不同的字段,来自不同来源:
| 数值( Number ) | 来源( Where it comes from ) | 在追踪中的位置( Where it lands in the trace ) |
|---|---|---|
| 每次调用的模型和词元数 | LangChain 插桩工具 | 每个 chat 跨度上的 gen_ai.response.model、`gen_ai.usage.input_tokens` 和 gen_ai.usage.output_tokens |
| 每次调用的计费成本 | OpenRouter 的用量响应( OpenRouter's usage response ),由 ChatOpenRouter 复制( copies )到 response_metadata["cost"] | 应用记录之前,不会出现在追踪中 |
| 每次请求的成本 | 应用将三次调用的成本相加 | shop.request 根跨度上的 app.model_cost.usd |
应用通过 LangChain 回调收集每次调用的费用,并将总和记录在根跨度上。app.model_cost.usd 仅涵盖模型用量,包括提供商的缓存定价;Elasticsearch、检索推理和基础设施另行计费。如果你改为根据词元数估算成本,应对缓存词元单独计价。gen_ai.usage.input_tokens 已经包含( includes ) gen_ai.usage.cache_read.input_tokens 和 gen_ai.usage.cache_write.input_tokens。对于 GPT-5.6 等推理模型,还要检查 gen_ai.usage.output_tokens 是否包含推理词元( gen_ai.usage.reasoning.output_tokens ),因为它们按输出词元计费。在我们的运行中,没有任何调用使用缓存或报告推理词元,因此词元数乘以提供商费率,与实际计费金额一致。
更便宜的模型会让 agent 更快吗?
追踪数据指出了一个具体的实验目标:compare_products 内部的模型。该模型读取一个小型商品目录,并检查价格、宽度和容量限制。尝试用 Luna 替换 Sol,同时保持问题、指令、产品记录和输出格式不变。agent 的其余部分保持原样。有关将工作分配给不同模型的更多示例,请阅读你的常见问题机器人不需要博士学位和让大模型思考,让小模型工作。
两个版本回答相同的五个产品问题,每个问题各回答三次。每个答案都必须通过检查,因此,如果更便宜的答案推荐了错误产品,就不能算节省成本:
| 案例( Case ) | 客户要求( Customer asks for ) | 答案通过检查的条件( The answer passes when it ) |
|---|---|---|
compact | 价格低于 100 美元,宽度不超过 20 厘米,容量至少四杯 | 列出紧凑冲煮、迷你晨光和台面适配三款咖啡机 |
timer | 带可编程定时器,并找出最便宜的一款 | 列出迷你晨光、家庭冲煮和纤薄专业三款咖啡机 |
family | 一次至少冲煮 10 杯 | 列出家庭冲煮咖啡机 |
no_match | 价格低于 60 美元,容量至少六杯 | 说明没有产品满足全部要求 |
unknown | 最安静的型号,并提供分贝数 | 说明商品目录不包含噪声数据,不编造数值 |
应用会在每个 shop.request 根跨度上添加 app.runset(实验名称)、app.case、app.variant( baseline 使用 Sol 进行比较,candidate 使用 Luna )和 app.check_passed 标签,与 app.model_cost.usd 一同记录。在数据探索( Discover )中切换到 ES|QL,选择覆盖实验的时间范围,再按问题和版本对这些跨度求平均值。`LIKE` 模式会匹配该实验的每次运行:
FROM traces-generic.otel-default
| WHERE resource.attributes.service.name == "shop-assistant"
AND name == "shop.request"
AND attributes.app.runset LIKE "shopping-model-swap*"
| STATS requests = COUNT(*), seconds = ROUND(AVG(duration) / 1e9, 2),
usd_per_1k = ROUND(AVG(attributes.app.model_cost.usd) * 1000, 3),
checks = COUNT(*) WHERE attributes.app.check_passed == true
BY question = attributes.app.case, variant = attributes.app.variant
| SORT question, variant
| KEEP question, variant, requests, checks, seconds, usd_per_1k
| LIMIT 10
usd_per_1k 是每 1,000 次请求的平均成本,以美元计,比不足一美分的数值更便于比较。每行涵盖三次请求:

对于预算和台面空间问题( compact ),每 1,000 次同类请求的模型成本从 4.465 美元降至 0.889 美元,降低约 80%。两个版本都推荐了相同的三款符合条件的产品,且三次重复测试均通过检查。平均响应时间从 8.89 秒变为 8.54 秒,变化幅度小得多。
上面的瀑布图追踪对应其中一次 compact 基线请求。按我们运行时的计费标准(每百万输入词元 2 美元,每百万输出词元 10 美元;此后 OpenRouter 已上调 Sol 的标价),该请求总成本为 0.00454 美元,其中 Sol 调用占 0.00404 美元。候选版本的三次 Luna 调用成本约为 0.0009 美元,因此每次请求节省约 0.0036 美元。
请结合追踪数据和成本数字,一同检查客户收到的答案。候选版本的答案列出了相同的三款产品及其规格:

其他四个问题的成本也有所降低,但在本次样本中,它们的平均响应时间都增加了。更低的模型价格带来了值得评估的成本节省,但延迟仍需单独测量。根据提供商报告的模型用量,这 30 次请求共花费 0.05347 美元。这是一项小规模顺序测试,每个问题和模型组合重复三次,温度设为零,并禁用重试。在更换生产环境中的模型之前,请同时审核答案文本和自动检查结果,并扩大评估范围。
大语言模型追踪会采集什么,又会省略什么
模型名称、词元数、时间信息和结果字段已足够用于本次比较,因此应用会在初始化插桩工具之前设置 OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=NO_CONTENT。`NO_CONTENT` 也是默认值。显式设置可以在配置中明确体现这一选择,并防止提示词和响应进入追踪存储。
默认情况下,不应将完整的提示词和响应文本放入跨度属性中。跨度属性会随每条追踪一起建立索引,因此原始内容可能增加存储用量、超出属性大小限制,并向任何能够读取追踪的人暴露个人信息和机密。该变量接受四个值,不区分大小写:
| 值( Value ) | 消息内容的去向( Where message content goes ) |
|---|---|
NO_CONTENT(默认) | 不存储消息内容;跨度保留模型、词元和时间信息 |
SPAN_ONLY | 聊天跨度的属性( gen_ai.input.messages、`gen_ai.output.messages` ) |
EVENT_ONLY | 独立的 OpenTelemetry 日志事件,详见生成式人工智能事件约定( GenAI event conventions ) |
SPAN_AND_EVENT | 同时存入跨度属性和日志事件 |
如果需要对话内容,优先选择 EVENT_ONLY。事件以日志记录的形式随追踪一同传输,因此你可以在存储前对其进行脱敏,并为其设置独立的访问规则和保留策略,无需扩大对所有追踪的访问权限。代价是,**生成式人工智能( GenAI )**选项卡的对话视图读取的是跨度属性( span attributes ),因此使用 EVENT_ONLY 时,该视图会保持空白。无论选择哪种模式,都应在启用采集前决定哪些内容需要脱敏,以及保留多久。
询问 Elastic 人工智能 agent 下一步该优化什么
Elastic 人工智能 agent 是 agent 构建器( Agent Builder )中的默认 agent,可以通过内置的 platform.core.execute_esql 工具查询相同的追踪数据。向它提供服务名称、追踪数据流和实验名称,再提出一个明确的问题。对于模型调用,它可以查询 attributes.gen_ai.response.model、`attributes.gen_ai.usage.output_tokens`,以及每个跨度的耗时。打开工具的查询和结果,检查其答案背后的证据。
哪个步骤占据了 agent 请求的大部分耗时?
服务:shop-assistant,追踪数据流:traces-generic.otel-default。追踪:`<trace_id>`。检查这条 shop-assistant 追踪。分别统计模型耗时和检索耗时,避免重复计算父跨度和子跨度的耗时。哪个调用最值得优先调查?

人工智能 agent 找到了产品工具内部的 Sol 调用,并将模型耗时与检索耗时区分开来。它指出,请求总耗时 10.10 秒中,有 9.26 秒用于模型调用。690 毫秒的检索耗时占比小得多,因此,优化搜索对此次请求的影响有限。
Agent 是否需要最后一次模型调用?
在每条追踪中,`compare_products` 返回后,agent 都会再调用一次模型。此时工具已经给出了完整答案,因此最后一次调用只是将答案转达给客户。询问人工智能 agent,这次转达的开销是多少:
服务:shop-assistant,追踪数据流:traces-generic.otel-default。追踪:`<candidate_trace_id>`。在这条候选版本追踪中,compare_products 返回最终答案后,agent 又调用了一次模型。最后这次调用耗时多久,生成了多少输出词元,占整个请求耗时的比例是多少?请提出一项可测试的改动。

在这条候选版本追踪中,`compare_products` 内部的 Luna 调用耗时 3.77 秒,生成了 250 个输出词元。最后一次调用又耗时 2.33 秒,生成了 184 个输出词元:整个请求耗时 9.14 秒,其中 25.5% 用于重复工具已经写好的答案。人工智能 agent 建议直接返回工具的答案。在 LangChain 中,只需为工具添加一个参数:`@tool(return_direct=True)` 会以工具输出结束 agent 运行,跳过最后一次模型调用。将其作为下一项实验:运行相同的五个问题,检查答案是否仍能通过验证。然后,使用相同的 ES|QL 查询测量节省的时间。
成本节省是否适用于不同问题?
服务:shop-assistant,追踪数据流:traces-generic.otel-default。实验:`<runset>`(根跨度名称为 shop.request;属性包括 app.case、app.variant、app.check_passed、app.model_cost.usd)。按产品问题比较基线版本和候选版本。哪些问题节省了成本,却没有明显改善延迟?请包含请求数量和未通过的检查。

人工智能 agent 查询 shop.request 跨度,并确认全部 30 次请求都已记录。在这个小规模样本中,候选版本在五个问题上的成本都更低,但其中四个问题的平均响应时间更长。这一结果有助于选择哪些问题需要更充分地测试,而不是假定更便宜的模型会让每个答案都更快返回。
将追踪数据转化为决策
OpenTelemetry 生成式人工智能约定将 LangChain agent 的运行过程转化为可以逐步查看的追踪:`invoke_agent`、`execute_tool` 和 chat 跨度记录每次调用的模型和词元数。在 Elastic 中,瀑布图显示,三次模型调用占据了一次 10 秒请求约 92% 的耗时。在根跨度上记录提供商报告的费用后,ES|QL 查询显示,价格更低的比较模型将每个问题的模型成本降低了 76% 至 83%,但并未带来一致的延迟改善。
利用这些发现选择下一项实验,再使用相同的查询和答案检查来衡量结果。对于这个助手,真正有用的判断是:某项改动能否在减少客户等待时间、降低模型成本的同时,仍然提供正确的推荐。
后续步骤
同一批追踪数据可以支持下一轮工作:
- 测试人工智能 agent 建议的改动。( Test the change that the AI agent suggested. ) 为
compare_products添加return_direct=True,再次运行这五个问题。然后,使用相同的 ES|QL 查询比较延迟、成本和检查结果。 - 针对成本和延迟设置告警。( Alert on cost and latency. ) 将实验查询转化为针对
shop.request跨度的 Elasticsearch 查询规则( Elasticsearch query rule ),以便在模型或提示词变更导致app.model_cost.usd或平均耗时上升时触发告警。 - 扩大评估范围。( Widen the evaluation. ) 在更换生产环境中的模型之前,增加问题数量和重复次数。将每项结果记录为根跨度上的属性,方便 ES|QL 进行比较。
- 仅在需要时采集内容。( Capture content only where you need it. ) 如果必须审核对话,可以尝试使用
EVENT_ONLY,对这些日志事件进行脱敏,并设置独立的保留策略。
为其他框架插桩。( Instrument other frameworks. ) opentelemetry-python-genai 代码仓库还提供了适用于 LlamaIndex、OpenAI agent 软件开发工具包及多家模型提供商的插桩工具。这些跨度遵循相同的生成式人工智能约定,因此本文的查询仍然适用。
原文:AI agent observability: LLM tracing with OpenTelemetry | Elastic Observability Labs
更多推荐


所有评论(0)