跳到主要内容

可观测性、护栏与 VoltOps:生产化那一层

30 秒导读: 前几章讲的是 agent「怎么跑」。这一章讲的是让它「敢上线」的横切能力:每次 运行都被记成一棵 OpenTelemetry span 树,同一份数据同时推给实时 Console、写进本地存储、批量导到 VoltOps 云端;护栏(guardrail)在输入进模型前、输出出模型后各拦一道做校验/改写/拦截;跑完还能 挂人类反馈和自动打分(eval);而 VoltOps 反过来当控制面——远程托管 prompt、收集 trace 与评分。

本章聚焦 @voltagent/core 里「生产化」这一层,和 agent 主流程相对独立。想先搞懂 agent 一次生成 怎么跑,请看 01-agent-runtime.md;工具/记忆/子代理/工作流分别在 02/03/04/05


1. 这是什么(零基础也能懂)

一句话定义: 这是 VoltAgent 里「运维视角」的一层——让你看得见 agent 在干什么、拦得住它不该 说的话、事后还能评估它说得好不好。

它解决什么问题。 一个能在本地 demo 里跑通的 agent,离「能放心接生产流量」还差三样东西:

缺口生产上会出的事本章对应能力
看不见线上出错/变慢/烧钱,没有 trace 完全没法排查可观测性(OpenTelemetry span)
拦不住用户塞脏话/PII 进来,模型把敏感信息吐出去护栏(input/output guardrail)
不知好坏上线后质量悄悄退化,没人打分没人反馈反馈(feedback)+ 评估(eval)

再加一个「运营」缺口:prompt 硬编码在代码里,改一句话要重新发版——VoltOps 控制面把 prompt 搬到 云端托管,顺便当上面三样数据的收集端。

用起来什么样。 对使用者,这一层大多是「声明式配置」,平时感知不到它在后台干活:

// 示意,非源码:把生产化能力挂到一个 agent 上
import { Agent } from "@voltagent/core";
import { VoltOpsClient } from "@voltagent/core";

const agent = new Agent({
name: "support",
model: openai("gpt-4o"),
instructions: "You are a support agent",
// 护栏:进出模型各拦一道
inputGuardrails: [blockProfanityInput()],
outputGuardrails: [redactPII()],
// 控制面:远程 prompt + trace/评分回传
voltOpsClient: new VoltOpsClient({
publicKey: process.env.VOLTOPS_PUBLIC_KEY,
secretKey: process.env.VOLTOPS_SECRET_KEY,
}),
});

// 之后正常调用即可;span、护栏、导出全在后台自动发生
const res = await agent.generateText("How do I reset my password?");

一句话直觉。 把这层想成给 agent 装的「行车记录仪 + 安全带 + 年检」:记录仪(可观测性)全程录像、 安全带(护栏)出事时拉住你、年检(eval)定期打分。VoltOps 则是把录像和体检报告上传的云盘。

本节不出现底层细节。下面从全景开始,一层层往下钻。


2. 顶层全景(它大概怎么转)

这一层由四块拼成,彼此松耦合,可单独启用:

部件干什么核心文件
可观测性把每次运行记成 OTel span 树,多路分发observability/index.tsobservability/node/volt-agent-observability.ts
护栏输入进模型前、输出出模型后校验/改写/拦截agent/guardrail.tsagent/streaming/output-guardrail-stream-runner.ts
反馈与评估挂人类反馈、异步跑打分器agent/feedback.tsagent/eval.ts
VoltOps 控制面远程 prompt 托管 + trace/评分回传voltops/client.tsvoltops/prompt-manager.ts

一次生产化运行,数据怎么流(从左到右):

用户输入


┌──────────────┐ 拦/改/放行
│ 输入护栏 │◄── runInputGuardrails (guardrail.ts:287)
└──────┬───────┘

┌──────────────┐ prompt 从哪来?本地>agent>global>fallback
│ VoltOps │◄── createPromptHelperWithFallback (client.ts:1038)
│ 取 prompt │
└──────┬───────┘

┌──────────────┐ 每次调用 = 一个 llm:* span
│ LLM 调用 │──► createLLMSpan (agent.ts:4445)
└──────┬───────┘ 记 token/cost:recordLLMUsage / recordProviderCost

┌──────────────┐ 拦/改/放行(流式则边流边拦)
│ 输出护栏 │◄── runOutputGuardrails (guardrail.ts:435)
└──────┬───────┘

┌──────────────┐ 异步打分,不阻塞返回
│ 反馈 / 评估 │──► enqueueEvalScoring (eval.ts:338)
└──────┬───────┘

返回结果

上面每一步都在往同一棵 span 树里挂子 span;span 树经四个 processor 分发:
├─► WebSocketSpanProcessor → 实时推给 Console UI
├─► LocalStorageSpanProcessor → 存本地(崩溃也留证据)
├─► LazyRemoteExportProcessor → 批量 OTLP 导到 VoltOps 云端
└─► SpanFilterProcessor(包在外层,滤掉无关 instrumentation 的 span)

怎么读这张图: 竖着看是「一次运行」的先后经过(护栏→prompt→LLM→护栏→评估);横着看,每一步 都往同一棵 OpenTelemetry span 树里挂节点,树再被下方四个 processor 各自消费一遍。可观测性是 「贯穿全程的暗线」,其它三块是「特定时点的拦截器」。

下面逐块讲。


3. 可观测性:同一棵 span 树,多路分发

3.1 思路:一次构建,多处消费

VoltAgent 没有自己发明 tracing,而是直接建在 OpenTelemetry(OTel,业界标准的分布式追踪协议) 之上。好处是:你的 agent trace 能和 HTTP、数据库等其它 OTel instrumentation 拼进同一棵树。

关键设计:span 只产生一遍,分发给多个「处理器」各干各的。OTel 的 SpanProcessor 是一个只有 onStart/onEnd/forceFlush/shutdown 的接口——span 开始和结束时会挨个通知所有已注册的 processor。 VoltAgent 就靠往这个列表里塞不同 processor,实现「同一份数据同时喂 UI、存盘、上云」。

入口是 createVoltAgentObservability(observability/index.ts:23),它做一件事:按运行环境挑实现

createVoltAgentObservability(config)

├─ isServerlessRuntime()? ── 是 ─► ServerlessVoltAgentObservability
│ (fetch 导出 + waitUntil,不留后台定时器)

└────────────────────── 否 ─► NodeVoltAgentObservability
(NodeTracerProvider + 四个 processor)

Node 与 Serverless 两实现的差异,是这一层最重要的工程取舍:

维度Node(node/volt-agent-observability.ts)Serverless(serverless/volt-agent-observability.ts)
ProviderNodeTracerProviderBasicTracerProvider(无 Node 专属 API)
远程导出LazyRemoteExportProcessor + BatchSpanProcessorFetchTraceExporter(用 fetch,兼容 Workers)
何时 flush长驻进程,后台定时批量导请求结束前必须 flush,靠 flushOnFinish()
关键约束可以有后台定时器函数一返回就冻结,得用 waitUntil 把导出挂到平台后台

Serverless 的 flushOnFinish()(serverless/volt-agent-observability.ts:415)会优先用宿主平台 (Cloudflare/Vercel)注入的 ___voltagent_wait_until:能拿到就把 flush 挂后台、不阻塞响应;拿不到 就退化成阻塞 flush,保证 span 不丢。

3.2 四个 SpanProcessor:各消费一遍同一棵树

Node 实现的 setupProcessors()(node/volt-agent-observability.ts:124)一次性装好这四个:

Processor职责关键实现
WebSocketSpanProcessorspan 开始/结束都广播成事件,喂 Console 实时 UIwebsocket-span-processor.ts:57(onStart)、:105(onEnd)
LocalStorageSpanProcessor把 span 写进存储适配器,崩溃也留证据local-storage-span-processor.ts:17
LazyRemoteExportProcessor延迟初始化,等 VoltOpsClient 就绪再批量 OTLP 导云端lazy-remote-export-processor.ts:126(tryInitialize)
SpanFilterProcessor包在前三者外层,滤掉非 VoltAgent 的无关 spanspan-filter-processor.ts:63(shouldProcess)

WebSocketSpanProcessor 的巧处:单例广播。 它内部拿的是一个单例 WebSocketEventEmitter (websocket-span-processor.ts:25),onStart/onEnd 都只是把 span 转成一个轻量对象再 emit。谁想收(比如 Console 的 WebSocket server)就 subscribe。processor 自己不持有连接, 广播和消费彻底解耦。

LocalStorageSpanProcessor 的巧处:先存后补。 onStart 时就把一个「status=UNSET、无 endTime」 的半成品 span 写进存储(:49),onEndupdateSpan 补全(:67)。这样即便进程中途崩了, 存储里也留着「开始了但没结束」的 span——排查线上挂起/超时特别有用。

LazyRemoteExportProcessor 的巧处:解决初始化竞态。 VoltOpsClient(带 API key)可能比 observability 晚构建。这个 processor 不在构造时连云端,而是每 100ms 轮询一次全局注册表 (startInitializationCheck,:101),等 getGlobalVoltOpsClient() 就绪再真正建 BatchSpanProcessor;在此之前 onEnd 的 span 先攒进 pendingSpans 缓冲(上限 1000,超了丢最老的, :65),就绪后一次性回放。轮询 5 秒(50 次)还没等到就放弃。

SpanFilterProcessor 的巧处:装饰器隔离。 它是个「包装」processor——applySpanFilter (node/volt-agent-observability.ts:169)把前面三个各包一层,只有 instrumentationScopeservice.name 命中白名单的 span 才转发给被包的 processor (shouldProcess,span-filter-processor.ts:63)。这样即使宿主 app 里还跑着别的 OTel instrumentation,VoltAgent 的管线也不会把它们的 span 误当自己的处理。

采样也在这里挂:voltOpsSync.sampling.strategynever 时干脆不建远程导出;为 ratio 时给 LazyRemoteExportProcessor 再套一层 SamplingWrapperProcessor(:143-146)。

3.3 把每次 LLM 调用记成 span

这是可观测性里最「有料」的部分——token 用量、成本、模型参数,全靠这里往 span 上挂属性

Agent 每次真正调模型前,都先建一个 span。核心方法 createLLMSpan(agent/agent.ts:4445):

// 真实调用点之一,agent.ts:2021(streamText 路径)
const llmSpan = this.createLLMSpan(oc, {
operation: "streamText",
modelName: resolvedModelName,
isStreaming: true,
messages, tools, providerOptions,
callOptions: { temperature, maxOutputTokens, topP, maxRetries, attempt, modelId },
});
const finalizeLLMSpan = this.createLLMSpanFinalizer(llmSpan);

span 的名字是 llm:${operation}(如 llm:streamText),kind 为 CLIENT,挂在当前运行的 span 树下 (createChildSpan,:4469)。属性由 buildLLMSpanAttributes(:4510)组装,分三类:

属性类例子说明
模型与参数llm.modelllm.temperaturellm.max_output_tokensllm.top_pcallOptions 提取,只收有限数字
provider 推断llm.provider模型名含 / 时取斜杠前段(如 openrouter/...)
上下文快照llm.messages.countllm.messages(只留最后 10 条)避免超长 prompt 撑爆属性

收尾时才记用量与成本。 createLLMSpanFinalizer(:4477)返回一个幂等的收尾函数(内部 ended 标志防重复 end),模型返回后调用它,把 usage / cost / finishReason 一次性落到 span 上:

  • recordLLMUsage(:4595):归一化后写 llm.usage.prompt_tokens / completion_tokens / total_tokens;cached_tokensreasoning_tokens 只在 >0 时才写(省噪声)。
  • recordProviderCost(:4627):从 providerMetadata 里抽 OpenRouter 的成本明细,写 usage.costusage.is_byokusage.cost_details.*——真实美元成本直接进 trace,能按 trace 算钱。
一次 LLM 调用的 span 生命周期:

createLLMSpan ──► buildLLMSpanAttributes (模型/参数/messages 快照)

▼ [模型真正执行]

finalizeLLMSpan ──┬─► recordLLMUsage (prompt/completion/total/cached/reasoning tokens)
├─► recordProviderCost (usage.cost 等,OpenRouter)
└─► span.setAttribute("llm.finish_reason", ...) + span.end()

4. 护栏:进出模型时拦一道

4.1 直觉:两个卡点,三种动作

护栏(guardrail)的模型很简单——在两个卡点各放一队校验器:

  • 输入护栏:用户输入进模型之前跑。典型用途:拦脏话、拦 prompt 注入、脱敏。
  • 输出护栏:模型产出之后跑。典型用途:PII 脱敏、超长截断、拦不当内容。

每个护栏返回一个决策,决定这条数据的命运,共三种动作:

action含义后果
allow放行继续下一个护栏
block拦截GUARDRAIL_*_BLOCKED 错误,整次运行终止
modify改写modifiedInput/modifiedOutput 替换后继续

创建护栏用两个工厂:createInputGuardrail(guardrail.ts:69)、createOutputGuardrail(:83)。 框架也内置了一批开箱即用的(脏话/邮箱/电话/PII/超长),在 agent/guardrails/defaults.ts。护栏挂到 agent 上是声明式的(agent.ts:1103-1104,inputGuardrails/outputGuardrailsnormalizeInputGuardrailList/normalizeOutputGuardrailList 归一化)。

4.2 输入护栏:顺序管道,可改写可拦截

runInputGuardrails(guardrail.ts:287)把护栏排成顺序管道:一个的改写结果喂给下一个。 每个护栏都开一个 guardrail.input.* 子 span,把决策记进 trace。

核心流程(简化):

// 示意,非源码:输入护栏管道的骨架
let currentInput = input;
for (const guardrail of guardrails) {
const span = createChildSpan(`guardrail.input.${id}`, "guardrail");
const decision = await guardrail.handler({ input: currentInput, ... });

if (!decision.pass || decision.action === "block") {
span.setStatus(ERROR); // 记成失败 span
oc.traceContext.end("error", err); // 终止整棵 trace
throw guardrailError; // 抛 GUARDRAIL_INPUT_BLOCKED
}
if (decision.action === "modify") {
currentInput = decision.modifiedInput; // 改写,喂给下一个护栏
}
span.end();
}
return currentInput;

真实实现里额外处理了并行执行限制:并行模式下护栏返回 modify 会直接报 GUARDRAIL_INPUT_MODIFY_UNSUPPORTED(guardrail.ts:381)——并行护栏只能 allow/block,不能改写 (否则多个改写无法确定先后)。管道跑完若输入被改过,会 traceContext.setInput(currentInput) 把最终输入同步进 trace(:417)。

4.3 输出护栏:同一套逻辑,外加流式桥接

非流式输出护栏走 runOutputGuardrails(guardrail.ts:435),逻辑和输入侧对称:顺序管道、 三种动作、逐个开 guardrail.output.* span。差别在它多了一个「和流式护栏共享 span」的机制—— 从 oc.context 里取 STREAM_GUARDRAIL_SPANS_KEY 存的 span(:452),流式阶段已经开了 span 就复用, 避免同一个护栏被记两遍。

4.4 流式护栏:边流边拦,holdUntilPass

难点:流式输出是一段段吐的,怎么在「还没吐完」时就拦? VoltAgent 的做法是给每个输出护栏一个 可选的 streamHandler,由 OutputGuardrailStreamRunner(output-guardrail-stream-runner.ts:55) 逐块喂:

模型流式吐字 (text-delta, ...)
│ 每来一个 part

┌────────────────────────────┐
│ OutputGuardrailStreamRunner│ processPart(part)
│ 对每个护栏依次: │
│ handler({ part, state, │ ← state 跨块累积(如"已见多少数字")
│ abort }) │
│ ├─ 返回改写后的 part │ → 脱敏后的 part 继续往下游
│ ├─ 返回 null │ → 丢弃这一块(过滤)
│ └─ 调 abort(reason) │ → 整条流中止
└────────────────────────────┘

▼ 下游拿到的是"净化过"的流

每个护栏在流开始时就建好自己的 span 并塞进 STREAM_GUARDRAIL_SPANS_KEY 映射 (registerStreamSpan,:366),这样流结束后 runOutputGuardrails 做最终校验时能复用同一个 span。

关键设计:两种流式策略。 输入护栏归一化时会带一个 streamPolicy,默认 holdUntilPass (guardrail.ts:178)。它的意思是:流式场景下,先把内容压住,等护栏判定通过了再放给下游—— 宁可牺牲一点「首字延迟」,也不让未经校验的内容先漏出去。这是「安全优先于体感」的取舍。 护栏的 execution 默认 blocking(:177),即护栏跑不完就不放行。

内置的流式护栏(如脱敏)靠 state 跨块累积上下文——比如一个信用卡号被切成两块吐出来, streamHandlerstate 记住「上一块结尾有几个悬空数字」,拼起来再判断该不该脱敏 (guardrails/defaults.ts:130 起的几个内置实现)。


5. 反馈与评估:跑完之后的质量闭环

5.1 人类反馈:给某条回复挂个「赞/踩」的把手

agent/feedback.ts 负责把「用户对某条 assistant 消息的反馈」落到记忆里。它不做打分逻辑,只做 元数据管理:

  • markFeedbackProvided(feedback.ts:72):按 userId/conversationId/messageId 找到目标 消息,往它的 metadata.feedback 上盖 provided: true + 时间戳,再写回记忆。
  • createFeedbackHandle(feedback.ts:202):返回一个「反馈把手」对象,带 isProvided()markFeedbackProvided() 两个非枚举方法,让调用方能优雅地问「反馈给了吗」并补记。
  • findFeedbackMessageId(feedback.ts:146):从后往前找带匹配 tokenId(或 traceId+key+url) 的 assistant 消息——把「一个反馈令牌」对回「哪条消息」。

反馈令牌本身由 VoltOps 签发(见 §6 的 createFeedbackToken),前端拿令牌就能让终端用户打分, 分数最终对回 trace。

5.2 自动评估:异步打分,不阻塞返回

agent/eval.ts 是「打分器(scorer)」的运行时。设计上最重要的一点:评估是异步的,绝不拖慢 用户拿到回复。入口 enqueueEvalScoring(eval.ts:338):

agent 产出结果


enqueueEvalScoring(host, { output, operation })
│ ① 没配 scorers → 直接 return(零开销)
│ ② 在 root span 上盖 eval.* 属性(scorer 数量/触发源/采样率)
│ ③ buildEvalPayload 打包这次运行的输入输出

scheduleAsync(() => runEvalScorers(...)) ← setImmediate/setTimeout,丢到下一个 tick

▼ [此时用户已经拿到回复了]
每个 scorer 跑一遍 → 分数/阈值/是否通过 → 写进 span + 回传 VoltOps

scheduleAsync(eval.ts:36)优先用 setImmediate,把打分推迟到当前调用栈之后,所以对主流程 零阻塞。AgentEvalHost(eval.ts:322)是 eval 和 agent 之间的窄接口——只暴露 id/name/logger/ evalConfig 和「拿 observability、拿 VoltOpsClient」两个回调,让 eval 模块不依赖整个 Agent。

每个 scorer 的结果会被建成一个 span(createScorerSpanAttributes,eval.ts:125),挂在 root span 下,分数(score)、阈值(threshold)、是否通过(thresholdPassed)都进 trace—— 于是「质量分」和「运行 trace」在 VoltOps 里天然对齐,能一起看。


6. VoltOps 控制面:远程 prompt 与数据回传

6.1 VoltOpsClient:一个客户端,多种职责

VoltOpsClient(voltops/client.ts:75)是 VoltAgent 连到 VoltOps 云端的统一客户端。历史上它还管 observability 导出,现在那部分已迁到 VoltAgentObservability,它如今集中在四件事:

能力入口说明
远程 promptclient.prompts(VoltOpsPromptManager)从云端拉 prompt,带缓存和模板
反馈令牌/反馈createFeedbackToken(:255)、createFeedback(:297)签发令牌、回传用户打分
评估回传evals.runs.*(:111)创建/追加/完成评估 run
托管记忆managedMemory(:455)走云端的消息/向量/工作流状态存储

key 校验是启用开关。 构造时校验 publicKeypk_ 开头、secretKeysk_ 开头 (hasValidKeys,:203);无效就什么服务都不初始化——所以填错 key 不会崩,只是静默降级。 getAuthHeaders(:231)把两把 key 塞进 X-Public-Key/X-Secret-Key,给所有请求和上面 LazyRemoteExport 的 OTLP 导出复用。

6.2 远程 prompt:缓存 + 模板 + 四级优先级

为什么要远程 prompt? 把 prompt 从代码里搬到云端,改文案不用发版;还能给不同环境挂不同版本、 灰度、A/B。核心是 VoltOpsPromptManagerImpl(voltops/prompt-manager.ts:42)。

getPrompt(prompt-manager.ts:69)的路径:

getPrompt({ promptName, version, variables })


命中缓存? ── 是 ─► 用缓存内容 ─┐
│ │
否 │
▼ │
apiClient.fetchPrompt │ 从 VoltOps 拉,按 name:version 做 key
│ │
▼ │
写入缓存(TTL 默认 5 分钟, │
maxSize 100,满了 LRU 淘汰) │
│ │
└──────────┬───────────────┘

processPromptContent:用模板引擎把 {{variables}} 替换掉

返回最终 PromptContent(text 或 chat)

模板引擎故意做得极简。 createSimpleTemplateEngine(voltops/template-engine.ts:19)只支持 {{variable}} 的字符串替换,零依赖。它不是 Liquid/Handlebars——刻意不引入完整模板语言, 换取轻量和可预测(替换失败就原样返回,不抛错让 prompt 拉取整个失败)。

四级优先级 fallback。 这是 prompt 解析最精巧的地方,createPromptHelperWithFallback (client.ts:1038)+ createPromptHelperFromSources(:1113)按下面顺序找,命中即用:

优先级来源何时用
1本地 prompt(.voltagent/promptsVOLTAGENT_PROMPTS_PATH)离线/本地开发,volt prompts pull 拉下来的
2agent 自带的 VoltOpsClientagent 级覆盖,最高远程优先级
3全局 VoltOpsClient(VoltAgent 构造时传的)全局默认
4代码里写的 fallback instructions前三个都没有时兜底

用本地 prompt 时它还会顺手和云端比版本:warnOnOutdatedLocalPrompt(client.ts:1146)后台拉一次 latest,本地版本落后就打一条 warn(只警告不阻断),提醒你 volt prompts pull


7. 边界与局限(诚实地说)

  • 成本追踪偏 OpenRouter。 recordProviderCost(agent.ts:4627)目前只从 OpenRouter 的 providerMetadata 抽真实美元成本(extractOpenRouterUsageCost);其它 provider 只有 token 数, 想折算成本得自己按单价算。
  • 护栏是应用层校验,不是安全边界。 guardrail 拦的是「内容」,不是「越权/沙箱逃逸」——它无法 阻止工具真正执行了危险操作,只能在文本层面拦/改。真正的执行安全靠工具层自己把关。
  • LazyRemoteExport 有 5 秒/1000 span 的窗口。 VoltOpsClient 5 秒内没就绪,导出直接放弃 (:107);等待期 span 缓冲上限 1000,超了丢最老的(:65)。极早期或极高频冷启动可能丢少量 span。这是「不阻塞启动」换来的代价。
  • prompt 缓存最长 5 分钟不感知云端改动。 默认 TTL 5 分钟(prompt-manager.ts:27),云端改了 prompt 到本地生效有最多 5 分钟延迟(可按 prompt 覆盖 TTL)。
  • eval 是「尽力而为」的旁路。 异步打分若失败只记 error 不重试(eval.ts:396.catch), 不保证每次运行都有分。它服务「趋势观测」,不是「逐条审计」。
  • 模板引擎太弱是有意的。 只有 {{var}},没有条件/循环/过滤器。复杂 prompt 逻辑得在应用层拼好 再传进去。

8. 横向对比:同类框架怎么做这一层

VoltAgent 这一层的取舍,放到 ai-agent-reference 货架里看:

关切VoltAgent 的做法兄弟框架的常见另一种取舍
可观测性押注 OpenTelemetry,span 树是一等公民,自带 processor 多路分发不少框架用自研事件/回调(如 callback handler)或 print 日志,可移植性弱但更轻
数据归属Console/本地/云端三路并存,离线也能看有的框架 trace 强绑自家云 SaaS,离线基本看不到
护栏输入/输出对称管道,并原生支持流式边流边拦很多框架只有「输出后一次性校验」,流式护栏是短板
prompt 管理远程托管 + 四级 fallback + 极简模板常见是纯代码内 prompt,或引入完整模板语言(Liquid)但更重
评估异步旁路,和 trace 对齐,零阻塞主流程有的把 eval 做成离线批处理脚本,不在运行时闭环

一句话:VoltAgent 的差异化在于**「一切建在 OTel 上、且离线可用」+「流式护栏」+「prompt 云端托管 但保留本地兜底」**。它更像「自带运维栈的 agent 框架」,而不是「纯 SDK 让你自己接监控」。

要理解这层数据从哪来,回看 01-agent-runtime.md 里 span 树怎么随一次生成 生长;工具/子代理各自也会往同一棵树挂 span,见 0204


9. 代码地图(导航索引)

主题文件关键符号
可观测性入口/环境选择packages/core/src/observability/index.tscreateVoltAgentObservabilityVoltAgentObservability
Node 实现 + processor 装配packages/core/src/observability/node/volt-agent-observability.tssetupProcessorsapplySpanFilterflushOnFinish
Serverless 实现packages/core/src/observability/serverless/volt-agent-observability.tsServerlessVoltAgentObservabilityflushOnFinish
实时 WebSocket 广播packages/core/src/observability/processors/websocket-span-processor.tsWebSocketSpanProcessorWebSocketEventEmitter
本地存储持久化packages/core/src/observability/processors/local-storage-span-processor.tsLocalStorageSpanProcessor
延迟远程导出packages/core/src/observability/processors/lazy-remote-export-processor.tsLazyRemoteExportProcessortryInitialize
span 过滤packages/core/src/observability/processors/span-filter-processor.tsSpanFilterProcessorshouldProcess
观测配置类型packages/core/src/observability/types.tsObservabilityConfigObservabilitySamplingConfig
LLM 调用 spanpackages/core/src/agent/agent.tscreateLLMSpan(4445)、buildLLMSpanAttributes(4510)、recordLLMUsage(4595)、recordProviderCost(4627)、createLLMSpanFinalizer(4477)
护栏工厂/管道packages/core/src/agent/guardrail.tscreateInputGuardrailcreateOutputGuardrailrunInputGuardrails(287)、runOutputGuardrails(435)
流式输出护栏packages/core/src/agent/streaming/output-guardrail-stream-runner.tsOutputGuardrailStreamRunnerprocessPart
流式护栏管道packages/core/src/agent/streaming/guardrail-stream.tscreateGuardrailPipeline
内置护栏packages/core/src/agent/guardrails/defaults.tscreateInputGuardrail/createOutputGuardrail 组装的脏话/PII/超长护栏
人类反馈packages/core/src/agent/feedback.tsmarkFeedbackProvidedcreateFeedbackHandlefindFeedbackMessageId
自动评估packages/core/src/agent/eval.tsenqueueEvalScoring(338)、AgentEvalHost(322)、buildEvalPayload(1005)
VoltOps 客户端packages/core/src/voltops/client.tsVoltOpsClienthasValidKeysgetAuthHeaderscreateFeedbackTokencreatePromptHelperWithFallback(1038)
远程 prompt 管理packages/core/src/voltops/prompt-manager.tsVoltOpsPromptManagerImplgetPrompt
模板引擎packages/core/src/voltops/template-engine.tscreateSimpleTemplateEngine