所有可配置内容都集中在此页面。其他页面会链接到这里,而不是重复说明。
pnpm add @evlog/signals ai
ai 7.0.105 或更高版本是 peer dependency。入口点为 @evlog/signals 和 @evlog/signals/testing。
选项
defineSignal(options) 会验证并返回一个 Signal。答案形状取决于输入:单独使用 ask 表示是/否,ask + choice 表示选择一个选项,ask + score 表示在评分表中定位。
| 选项 | 类型 | 说明 |
|---|---|---|
name | string | event.signals 下的列名。以字母开头,可包含字母、数字和连字符。已注册的 signal 之间必须唯一。 |
ask | string | 使用普通英文提出的问题。 |
when | (event) => boolean | 针对事件的谓词。返回 false 时不会调用模型。使用 keep 时必需。 |
choice | Record<option, description> | 至少两个选项。创建 choice signal。 |
score | [level, level, ...] | 按从低到高排列,至少两个级别。创建 score signal。 |
criteria | { true?, false? } | 仅适用于布尔 signal。说明每个答案成立的条件。 |
keep | (verdict) => boolean | 返回 true 时让事件通过采样。永远不会丢弃事件。判定结果会根据形状获得对应类型。 |
cacheKey | (event) => string | undefined | 为共享键的事件复用判定结果。undefined 表示跳过缓存。每个进程保留最近 1,000 个判定结果。 |
当名称无效、ask 为空、没有 when 却使用 keep、选项或级别少于两个时,defineSignal 会在定义时抛出错误。
createSignals(options) 返回一个 SignalsPlugin:一个带有 keep、enrich 和 stats() 的 evlog 插件。唯一必需的字段是 signals。
| 选项 | 默认值 | 说明 |
|---|---|---|
signals | 要运行的 signal。名称重复会在启动时抛出错误。 | |
model | 'typesafe-ai/jev' | AI SDK 评估模型:Gateway ID 或 provider 实例。 |
budget.perMinute | 600 | 每分钟模型调用次数上限,由每个进程中的所有 signal 共享。 |
budget.cooldownMs | 30000 | 调用失败后的暂停时间。 |
timeoutMs | 2000 | 单次调用超时时间。超时会中止调用并计为错误。 |
state | 去掉 signals 和 signalsModel 的完整事件 | 模型读取的内容。也可以返回字符串。 |
maxStateChars | 100000 | 发送状态的最大 JSON 字符数。更大的事件会被跳过。 |
providerOptions | 传递给模型调用的选项,例如 { gateway: { zeroDataRetention: true } }。 | |
evaluate | AI SDK experimental_evaluate | 替换模型调用。用于测试、记录和重放。 |
stampModel | false | 将回答模型的 ID 写入 event.signalsModel。比较两个模型时可以启用,以便在 drain 中区分判定结果;否则每个判定事件都会多出一列。 |
model 可以接受任何 AI SDK 评估模型。参见选择模型。
SignalsPlugin 是标准的 evlog 插件。注册位置取决于框架暴露插件的方式:
| 框架 | 注册方式 |
|---|---|
| Nuxt、Nitro | 在服务端插件中使用:nitroApp.hooks.hook('evlog:emit:keep', plugin.keep) 和 nitroApp.hooks.hook('evlog:enrich', plugin.enrich) |
| Next.js、独立模式 | initLogger({ plugins: [plugin] }) |
| Hono、Express、Fastify、Elysia、oRPC、SvelteKit、React Router | 中间件的 plugins 选项:evlog({ plugins: [plugin] }) |
createMiddlewareLogger(toolkit) | 调用时传入 plugins: [plugin] |
选择模型
默认模型通过 AI Gateway 调用:一个密钥即可使用所有决策模型,只需修改字符串即可切换。直接 provider 的用法相同,只是传入实例而不是字符串。
默认模型。读取 AI_GATEWAY_API_KEY;在 Vercel 上无需密钥,会使用 OIDC。
createSignals({
model: 'typesafe-ai/jev', // the default; 'liquid/d1' is the other decision model
signals: [fault, silentFailure],
})
两个 Gateway 模型都会返回概率分布,因此每列都有 confidence。比较它们时只需修改 ID 字符串;同时设置 stampModel: true,这样就能在 drain 中区分每个模型的判定结果。
直接访问默认模型背后的模型。安装 @ai-sdk/typesafe-ai;它会读取 TYPESAFE_AI_API_KEY。
import { typeSafeAi } from '@ai-sdk/typesafe-ai'
createSignals({
model: typeSafeAi.evaluationModel('jev-latest'),
signals: [fault, silentFailure],
})
概率分布和列与通过 Gateway 时相同。
使用 prompt 要求通用模型回答。安装 @ai-sdk/openai;它会读取 OPENAI_API_KEY。
import { openai } from '@ai-sdk/openai'
createSignals({
model: openai.evaluationModel('gpt-6-luna'),
signals: [fault, silentFailure],
})
带 prompt 的模型会为是否问题返回 P(true),为 choice 和 score 返回不带分布的答案,因此这些列只有 value 而没有 confidence。调用速度预计会慢于决策模型;如果 stats().errors 增加,请提高 timeoutMs。
任何暴露 evaluationModel() 的其他 provider 都可以用相同方式接入。provider 支持的选项通过 providerOptions 传递,并以 provider 名称为键。
列
判定会写入 event.signals.<name>。启用 stampModel: true 后,回答模型的 ID 会写入 event.signalsModel。
| 形状 | value | confidence | 额外字段 |
|---|---|---|---|
| 布尔 | boolean | value 的概率 | |
| 选择 | 选项名称之一 | 模型返回分布时为该选项的概率 | |
| 评分 | 级别名称之一(最可能的级别) | 模型返回分布时为该级别的概率 | score: number,从 0 到 levels - 1 的按概率加权位置 |
让事件通过采样的 signal 会在其列中添加 kept: true。
interface BooleanVerdict { value: boolean; confidence: number }
interface ChoiceVerdict<Option extends string> { value: Option; confidence?: number }
interface ScoreVerdict<Level extends string> { value: Level; score: number; confidence?: number }
type SignalColumn = Verdict & { kept?: true }
列会在 enrich 中、控制台行之前写入。它们会到达 stdout、Vercel 等平台日志以及每个 drain。此时响应已经发送;等待评估的是控制台行,并受 timeoutMs 限制,因此开发终端中较晚出现的日志来自模型,而不是请求变慢。
stats()
plugin.stats() 返回每个进程计数器的快照:
| 字段 | 计数内容 |
|---|---|
calls | 已进行的模型调用。 |
skipped | 应运行 signal 但未判定的事件:预算耗尽、断路器打开或状态超过 maxStateChars。 |
errors | 失败或超时的调用。 |
cached | 通过 cacheKey 提供、未发起调用的判定结果。 |
inputTokens | 模型报告的计费 token 数。 |
将 cached 与 calls 对照即可看到 cacheKey 节省的调用。skipped 增加而 errors 保持不变,说明预算低于流量,而不是模型不可用。
测试
@evlog/signals/testing 用脚本替换模型。不需要密钥、网络或预算:
import { createSignals } from '@evlog/signals'
import { answers, scriptedEvaluate } from '@evlog/signals/testing'
const mock = scriptedEvaluate((name, question) => {
if (name === 'fault') return answers.choice(question, 'upstream', 0.93)
return answers.first(question)
})
const plugin = createSignals({ signals, evaluate: mock.evaluate })
// after a request has run through evlog
expect(mock.calls).toHaveLength(1)
expect(mock.calls[0].questions).toHaveProperty('fault')
| 导出项 | 作用 |
|---|---|
scriptedEvaluate(answer, modelId?) | 返回 { evaluate, calls }。每个问题运行一次 answer(name, question, state);calls 记录每次请求。 |
answers.boolean(p) | P(true) = p 的是否答案。 |
answers.choice(question, pick, p) | 以概率 p 选择 pick,其余概率分配给其他选项。 |
answers.score(question, levelIndex, p) | 以概率 p 选择 levelIndex 级别,并计算加权 score。 |
answers.first(question) | 对应类型的高置信度默认答案:第一个选项为 0.9,最后一级为 0.8,是为 0.94。 |
同一个钩子也可以重放已记录的判定:通过 requestId 查找答案的 scriptedEvaluate 可以在不发起调用的情况下,让你的 signal 处理昨天的事件。