Signals
defineSignal 和 createSignals 的全部选项及默认值、列格式、stats() 计数器,以及 @evlog/signals/testing 中的测试辅助工具。

所有可配置内容都集中在此页面。其他页面会链接到这里,而不是重复说明。

Terminal
pnpm add @evlog/signals ai

ai 7.0.105 或更高版本是 peer dependency。入口点为 @evlog/signals 和 @evlog/signals/testing。

选项

defineSignal(options) 会验证并返回一个 Signal。答案形状取决于输入:单独使用 ask 表示是/否,ask + choice 表示选择一个选项,ask + score 表示在评分表中定位。

选项类型说明
namestringevent.signals 下的列名。以字母开头,可包含字母、数字和连字符。已注册的 signal 之间必须唯一。
askstring使用普通英文提出的问题。
when(event) => boolean针对事件的谓词。返回 false 时不会调用模型。使用 keep 时必需。
choiceRecord<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 会在定义时抛出错误。

选择模型

默认模型通过 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 中区分每个模型的判定结果。

任何暴露 evaluationModel() 的其他 provider 都可以用相同方式接入。provider 支持的选项通过 providerOptions 传递,并以 provider 名称为键。

列

判定会写入 event.signals.<name>。启用 stampModel: true 后,回答模型的 ID 会写入 event.signalsModel。

形状valueconfidence额外字段
布尔booleanvalue 的概率
选择选项名称之一模型返回分布时为该选项的概率
评分级别名称之一(最可能的级别)模型返回分布时为该级别的概率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 用脚本替换模型。不需要密钥、网络或预算:

signals.test.ts
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 处理昨天的事件。