可观测性、护栏与 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.ts、observability/node/volt-agent-observability.ts |
| 护栏 | 输入进模型前、输出出模型后校验/改写/拦截 | agent/guardrail.ts、agent/streaming/output-guardrail-stream-runner.ts |
| 反馈与评估 | 挂人类反馈、异步跑打分器 | agent/feedback.ts、agent/eval.ts |
| VoltOps 控制面 | 远程 prompt 托管 + trace/评分回传 | voltops/client.ts、voltops/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 各自消费一遍。可观测性是 「贯穿全程的暗线」,其它三块是「特定时点的拦截器」。
下面逐块讲。