- 重命名 ContentRules→ContentExpectations, KeyValueExpect→KeyedExpectations - 新增 Raw/Resolved 双层模型:resolve 阶段物化为执行计划,store 持久化 Raw 快照 - HTTP body 按需读取:status/headers 失败或无 body expectation 时不读取 body - 新增 displayValueExpectation() 解包 failure.expected 用户可读展示 - 修复 checkEarlyTimeout 独立 lte/lt 检查,修复 KeyedExpectations JSON Schema - 新增 expect/value.ts(resolve/check/display)、keyed.ts、content.ts、headers.ts、status.ts - 删除旧 normalize.ts/matcher.ts/validate-matcher.ts/key-value.ts - 更新 DEVELOPMENT.md:expect 五层管线表、displayValueExpectation、1.7↔1.10 交叉引用 - 同步 13 个 main specs,归档 refactor-expect-type-model 变更(62/62 tasks)
248 lines
17 KiB
Markdown
248 lines
17 KiB
Markdown
## Purpose
|
||
|
||
定义 LLM checker 的配置模型、Provider/Mode 支持、执行观测、Expect 断言、失败 Phase 和测试行为。
|
||
|
||
## Requirements
|
||
|
||
### Requirement: LLM Checker 注册与模块结构
|
||
系统 SHALL 提供 `type: llm` checker,用于大模型服务的应用层拨测。LLM checker MUST 位于 `src/server/checker/runner/llm/` 自包含目录,并通过 `src/server/checker/runner/index.ts` 注册到 `CheckerRegistry`。LLM checker SHALL 复用现有 checker 抽象、配置 schema 组装、启动期语义校验、引擎调度、存储序列化和共享 expect 基础设施。
|
||
|
||
#### Scenario: 注册 LLM checker
|
||
- **WHEN** 系统初始化默认 checker registry
|
||
- **THEN** registry SHALL 包含 `llm` 类型,且 `/api/meta` 返回的 `checkerTypes` SHALL 包含 `llm`
|
||
|
||
#### Scenario: LLM checker 目录自包含
|
||
- **WHEN** 开发者查看 `src/server/checker/runner/llm/` 目录
|
||
- **THEN** 该目录 SHALL 包含 LLM checker 的类型、schema、语义校验、provider 创建、observation 构建、expect 断言、执行逻辑和模块入口
|
||
|
||
#### Scenario: 不扩展存储和 API 结构
|
||
- **WHEN** LLM checker 写入检查结果
|
||
- **THEN** 系统 SHALL 使用现有 `CheckResult`、`targets`、`check_results` 和 Dashboard API 结构,不新增 LLM 专用存储列或 Dashboard 指标字段
|
||
|
||
### Requirement: LLM Provider 与调用模式
|
||
LLM checker SHALL 支持 `openai`、`openai-responses`、`anthropic` 三类 provider。`mode: http` SHALL 调用 AI SDK `generateText`。`mode: stream` SHALL 调用 AI SDK `streamText`。所有模型调用 MUST 将 `maxRetries` 固定为 `0`,并 MUST 使用引擎注入的 `ctx.signal` 响应超时和取消。
|
||
|
||
#### Scenario: OpenAI Chat Completions provider
|
||
- **WHEN** target 配置 `llm.provider: openai`
|
||
- **THEN** LLM checker SHALL 使用 `@ai-sdk/openai` 的 `openai.chat(model)` 创建模型调用对象
|
||
|
||
#### Scenario: OpenAI Responses provider
|
||
- **WHEN** target 配置 `llm.provider: openai-responses`
|
||
- **THEN** LLM checker SHALL 使用 `@ai-sdk/openai` 的 `openai.responses(model)` 创建模型调用对象
|
||
|
||
#### Scenario: Anthropic provider
|
||
- **WHEN** target 配置 `llm.provider: anthropic`
|
||
- **THEN** LLM checker SHALL 使用 `@ai-sdk/anthropic` 的 `anthropic.messages(model)` 创建模型调用对象
|
||
|
||
#### Scenario: 非流式调用模式
|
||
- **WHEN** target 配置 `llm.mode: http` 或省略 `llm.mode`
|
||
- **THEN** LLM checker SHALL 调用 `generateText` 并从返回结果构建非流式 observation
|
||
|
||
#### Scenario: 流式调用模式
|
||
- **WHEN** target 配置 `llm.mode: stream`
|
||
- **THEN** LLM checker SHALL 调用 `streamText` 并消费 `fullStream` 构建流式 observation
|
||
|
||
#### Scenario: 超时取消传递给 SDK
|
||
- **WHEN** 引擎注入的 `ctx.signal` 被 abort
|
||
- **THEN** LLM checker SHALL 将该 signal 传递给 AI SDK 调用并将取消或超时结果记录为检查失败
|
||
|
||
### Requirement: LLM 配置解析与默认值
|
||
LLM checker SHALL 解析 `llm.provider`、`llm.url`、`llm.model`、`llm.prompt`、`llm.mode`、`llm.key`、`llm.authToken`、`llm.headers`、`llm.ignoreSSL`、`llm.options` 和 `llm.providerOptions`。`llm.options` SHALL 支持 `maxOutputTokens`(默认 `16`)、`temperature`(默认 `0`)、`topP`、`topK`、`presencePenalty`、`frequencyPenalty`、`stopSequences`(字符串数组)和 `seed`。`llm.mode` 默认值 SHALL 为 `http`,`llm.key` 默认值 SHALL 为空字符串,`llm.ignoreSSL` 默认值 SHALL 为 `false`。LLM checker MUST NOT 隐式读取 AI SDK 默认环境变量。
|
||
|
||
#### Scenario: 最简 LLM target 解析
|
||
- **WHEN** 系统读取只包含 `type: llm` 以及 `llm.provider`、`llm.url`、`llm.model`、`llm.prompt` 的 target
|
||
- **THEN** 系统 SHALL 解析为 LLM target,并填充 `mode=http`、`key=""`、`ignoreSSL=false`、`options.maxOutputTokens=16`、`options.temperature=0`
|
||
|
||
#### Scenario: headers 默认值合并
|
||
- **WHEN** `defaults.llm.headers` 和 target `llm.headers` 同时配置同名 header
|
||
- **THEN** LLM checker SHALL 按原始 header key 浅合并 headers,并由 target `llm.headers` 覆盖 defaults 中同名 key
|
||
|
||
#### Scenario: options 默认值合并
|
||
- **WHEN** `defaults.llm.options` 和 target `llm.options` 同时配置同名 option
|
||
- **THEN** LLM checker SHALL 浅合并 options,并由 target `llm.options` 覆盖 defaults 中同名字段
|
||
|
||
#### Scenario: providerOptions 默认值合并
|
||
- **WHEN** `defaults.llm.providerOptions` 和 target `llm.providerOptions` 同时配置同名 provider namespace
|
||
- **THEN** LLM checker SHALL 按 provider namespace 浅合并 providerOptions,并由 target namespace 覆盖 defaults 中同名 namespace
|
||
|
||
#### Scenario: Anthropic Bearer token
|
||
- **WHEN** target 配置 `llm.provider: anthropic` 和非空 `llm.authToken`
|
||
- **THEN** LLM checker SHALL 将 `authToken` 映射到 Anthropic SDK 的 Bearer token 认证字段
|
||
|
||
#### Scenario: key 不隐式读取环境变量
|
||
- **WHEN** target 未配置 `llm.key`
|
||
- **THEN** LLM checker SHALL 将 SDK provider 的 api key 设置为空字符串,而不是隐式读取 SDK 默认环境变量
|
||
|
||
### Requirement: LLM HTTP Metadata 与 TLS
|
||
LLM checker SHALL 通过 AI SDK provider 的 custom fetch 注入 observing fetch。observing fetch SHALL 调用 Bun `fetch`,在不消费 response body 的前提下记录 HTTP status、statusText 和 headers。`llm.ignoreSSL: true` 时,observing fetch SHALL 仅对当前 target 的 provider 请求使用 Bun `tls.rejectUnauthorized=false`。
|
||
|
||
#### Scenario: 捕获 HTTP metadata
|
||
- **WHEN** AI SDK provider 发起模型 HTTP 请求并收到响应
|
||
- **THEN** observing fetch SHALL 记录 status code 和响应 headers,供 `expect.status` 与 `expect.headers` 使用
|
||
|
||
#### Scenario: 不消费响应体
|
||
- **WHEN** observing fetch 捕获 HTTP metadata
|
||
- **THEN** observing fetch SHALL 返回原始 response 给 AI SDK,不提前读取或克隆消费 body
|
||
|
||
#### Scenario: 忽略证书校验
|
||
- **WHEN** target 配置 `llm.ignoreSSL: true`
|
||
- **THEN** observing fetch SHALL 对当前 target 的 provider 请求设置 `tls.rejectUnauthorized=false`
|
||
|
||
### Requirement: LLM Observation
|
||
LLM checker SHALL 在 SDK 调用结果和 expect 断言之间构建 `LlmCheckObservation`。observation SHALL 包含 provider、model、mode、outputText、finishReason、rawFinishReason、usage、stream、http 和 warnings 中可观测的字段。`mode: http` 的 `outputText` SHALL 来自 `generateText.text`。`mode: stream` 的 `outputText` SHALL 来自 `fullStream` 中 `text-delta` 的原始文本聚合。
|
||
|
||
#### Scenario: 非流式 observation
|
||
- **WHEN** `generateText` 调用成功
|
||
- **THEN** LLM checker SHALL 从 SDK result 中提取 outputText、finishReason、rawFinishReason、usage、response headers 和 HTTP metadata
|
||
|
||
#### Scenario: 流式 observation
|
||
- **WHEN** `streamText` 调用成功且 stream 正常完成
|
||
- **THEN** LLM checker SHALL 从 `fullStream` 聚合 outputText,并记录 stream.completed、firstTokenMs、finishReason、rawFinishReason、usage 和 HTTP metadata
|
||
|
||
#### Scenario: APICallError observation
|
||
- **WHEN** AI SDK 抛出带 statusCode 或 responseHeaders 的 `APICallError`
|
||
- **THEN** LLM checker SHALL 构建包含可用 HTTP metadata 的 observation,并继续执行可执行的 status、headers 和 duration 断言
|
||
|
||
#### Scenario: 无 HTTP metadata 的 SDK 错误
|
||
- **WHEN** AI SDK 抛出不带 statusCode 和 responseHeaders 的错误
|
||
- **THEN** LLM checker SHALL 返回 `phase: "request"` 的 error failure
|
||
|
||
### Requirement: LLM Expect 断言
|
||
LLM checker SHALL 支持 `expect.status`、`expect.headers`、`expect.output`、`expect.finishReason`、`expect.rawFinishReason`、`expect.usage.inputTokens`、`expect.usage.outputTokens`、`expect.usage.totalTokens`、`expect.stream.completed`、`expect.stream.firstTokenMs` 和 `expect.durationMs`。`expect.status` SHALL 保持 HTTP 状态码数组语义并复用共享 status code 断言,未配置时在 Resolved expect 中物化默认 `[200]`。`expect.headers` SHALL 使用共享 `RawKeyedExpectations` 输入并在运行期使用 `KeyedExpectations`,header key 大小写不敏感。`expect.output` MUST 使用共享 `RawContentExpectations` 输入并在运行期使用 `ContentExpectations`。`expect.finishReason` 和 `expect.rawFinishReason` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation`。`expect.usage.*`、`expect.stream.firstTokenMs` 和 `expect.durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation`。LLM checker MUST 按固定顺序快速失败,非流式顺序为 status、headers、output、finishReason、rawFinishReason、usage、durationMs;流式顺序为 status、headers、stream.completed、stream.firstTokenMs、output、finishReason、rawFinishReason、usage、durationMs。
|
||
|
||
#### Scenario: 默认 status 断言
|
||
- **WHEN** LLM target 未配置 `expect.status`
|
||
- **THEN** LLM checker SHALL 在 Resolved expect 中使用默认 `status: [200]` 语义
|
||
|
||
#### Scenario: expect headers 通过
|
||
- **WHEN** observing fetch 捕获的响应 headers 满足 `expect.headers` 配置
|
||
- **THEN** LLM checker SHALL 通过共享 header expectation 包装函数判定 headers 断言通过
|
||
|
||
#### Scenario: output ContentExpectations 通过
|
||
- **WHEN** LLM 输出文本满足 `expect.output` 中配置的全部 ContentExpectations
|
||
- **THEN** LLM checker SHALL 判定 output 阶段通过
|
||
|
||
#### Scenario: finishReason ValueMatcher 通过
|
||
- **WHEN** observation.finishReason 为 `stop` 且 target 配置 `expect.finishReason: {equals: "stop"}`
|
||
- **THEN** LLM checker SHALL 判定 finishReason 阶段通过
|
||
|
||
#### Scenario: rawFinishReason regex 通过
|
||
- **WHEN** observation.rawFinishReason 为 `end_turn` 且 target 配置 `expect.rawFinishReason: {regex: "^(stop|end_turn)$"}`
|
||
- **THEN** LLM checker SHALL 判定 rawFinishReason 阶段通过
|
||
|
||
#### Scenario: usage matcher 通过
|
||
- **WHEN** observation.usage.totalTokens 为 14 且 target 配置 `expect.usage.totalTokens: {lte: 20}`
|
||
- **THEN** LLM checker SHALL 判定 usage 阶段通过
|
||
|
||
#### Scenario: durationMs matcher 失败
|
||
- **WHEN** LLM target 配置 `expect.durationMs: {lte: 1000}` 且实际执行耗时为 1500ms
|
||
- **THEN** LLM checker SHALL 返回 phase=`duration` 的 mismatch failure
|
||
|
||
#### Scenario: 首个 expect 失败
|
||
- **WHEN** 多个 LLM expect 中某个较早顺序的断言失败
|
||
- **THEN** LLM checker SHALL 立即返回该断言对应的 mismatch failure,不继续执行后续断言
|
||
|
||
#### Scenario: 期望认证失败状态
|
||
- **WHEN** AI SDK 抛出带 HTTP status 401 的 `APICallError`,且 target 仅配置 `expect.status: [401]`
|
||
- **THEN** LLM checker SHALL 判定本次检查为 `matched=true`
|
||
|
||
#### Scenario: APICallError 缺失模型输出
|
||
- **WHEN** AI SDK 抛出带 HTTP status 的 `APICallError`,且 target 同时配置需要模型结果的 `expect.output`
|
||
- **THEN** LLM checker SHALL 因 `outputText` 缺失返回 `phase: "output"` 的 mismatch failure
|
||
|
||
### Requirement: LLM Output 规则
|
||
LLM checker SHALL 使用共享 `ContentRules` 校验 `expect.output`。每个 output rule SHALL 为直接 `ValueMatcher`,或 `json`、`css`、`xpath` extractor 规则之一。直接 matcher SHALL 作用于原始输出字符串。`equals` SHALL 对原始输出字符串做严格相等比较。`contains` SHALL 判断原始输出是否包含子串。`regex` SHALL 对原始输出执行无 flags 正则匹配。`json` SHALL 将原始输出解析为 JSON,并用现有 JSONPath 子集和 `ValueMatcher` 校验提取值。`json.equals` SHALL 支持任意 JSON value。`css` 和 `xpath` 在 schema 层面可用,但 LLM 输出通常为纯文本或 JSON,实际场景中仅 `json` 提取器有意义。
|
||
|
||
#### Scenario: 原始输出严格相等
|
||
- **WHEN** `outputText` 为 `"OK\n"` 且 target 配置 `expect.output: [{ equals: "OK" }]`
|
||
- **THEN** LLM checker SHALL 判定 output 断言失败,因为 equals 不自动 trim
|
||
|
||
#### Scenario: output contains 通过
|
||
- **WHEN** `outputText` 包含配置的子串
|
||
- **THEN** LLM checker SHALL 判定该 output contains 规则通过
|
||
|
||
#### Scenario: output regex 通过
|
||
- **WHEN** `outputText` 匹配配置的合法 regex
|
||
- **THEN** LLM checker SHALL 判定该 output regex 规则通过
|
||
|
||
#### Scenario: output JSONPath 字符串 equals 通过
|
||
- **WHEN** `outputText` 是 JSON 字符串且 JSONPath 提取字符串值满足 `equals`
|
||
- **THEN** LLM checker SHALL 判定该 output json 规则通过
|
||
|
||
#### Scenario: output JSONPath 对象 equals 通过
|
||
- **WHEN** `outputText` 是 JSON 字符串且 JSONPath 提取对象值满足 `equals`
|
||
- **THEN** LLM checker SHALL 使用深度相等判定该 output json 规则通过
|
||
|
||
#### Scenario: output JSONPath 存在性默认语义
|
||
- **WHEN** `outputText` 是 JSON 字符串且 target 配置 `expect.output: [{json: {path: "$.status"}}]`
|
||
- **THEN** resolve 阶段 SHALL 将该 expectation 的 matcher 物化为 `{exists: true}`,运行期按存在性语义执行
|
||
|
||
#### Scenario: output 规则按顺序快速失败
|
||
- **WHEN** `expect.output` 包含多个 expectation 且第一条 expectation 失败
|
||
- **THEN** LLM checker SHALL 返回第一条失败 expectation 的 mismatch failure,不继续校验后续 output expectation
|
||
|
||
### Requirement: LLM Stream 断言
|
||
LLM checker SHALL 仅允许 `mode: stream` 使用 `expect.stream`。仅当用户配置了 `expect.stream` 且未配置 `expect.stream.completed` 时,resolve 阶段 SHALL 在 Resolved expect 中物化默认 `completed: true`;LLM checker MUST NOT 因为 `llm.mode: stream` 自动添加 `stream.completed` 断言。`expect.stream.firstTokenMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation`,且仅统计第一个非空 `text-delta` 事件耗时,不统计 reasoning、tool call 或 source 事件。
|
||
|
||
#### Scenario: stream completed 默认值
|
||
- **WHEN** target 配置 `llm.mode: stream` 且配置 `expect.stream: {}`
|
||
- **THEN** resolve 阶段 SHALL 在 Resolved expect 中物化 `stream.completed: true` 并要求 SDK stream 正常完成
|
||
|
||
#### Scenario: 未配置 expect.stream 不添加 completed
|
||
- **WHEN** target 配置 `llm.mode: stream` 但未配置 `expect.stream`
|
||
- **THEN** resolve 阶段 SHALL NOT 自动添加 `stream.completed` 断言
|
||
|
||
#### Scenario: stream error
|
||
- **WHEN** `fullStream` 产生 error part
|
||
- **THEN** LLM checker SHALL 返回 `phase: "stream"` 的 failure
|
||
|
||
#### Scenario: firstTokenMs 达标
|
||
- **WHEN** target 配置 `expect.stream.firstTokenMs: {lte: 1000}` 且首个非空 text delta 耗时满足 matcher
|
||
- **THEN** LLM checker SHALL 判定 firstTokenMs 断言通过
|
||
|
||
#### Scenario: firstTokenMs 缺失
|
||
- **WHEN** target 配置 `expect.stream.firstTokenMs` 但 stream 未产生非空 text delta
|
||
- **THEN** LLM checker SHALL 返回 `phase: "stream"` 的 mismatch failure
|
||
|
||
#### Scenario: APICallError 不被默认 completed 阻断
|
||
- **WHEN** `mode: stream` 的 SDK 调用在 stream 启动前抛出带 HTTP status 的 `APICallError`
|
||
- **THEN** 默认 `stream.completed=true` SHALL NOT 阻断基于 status 和 headers 的 APICallError 状态探测
|
||
|
||
### Requirement: LLM Failure Phase 与状态摘要
|
||
LLM checker SHALL 使用 `request`、`status`、`headers`、`stream`、`output`、`finishReason`、`rawFinishReason`、`usage`、`duration` 作为第一版 failure phase。成功结果的 API detail SHALL 从持久化 observation 动态构造,简短描述 provider、mode、HTTP status、finish reason、raw finish reason、first token、输出长度和 token usage 中可用的信息。observation 和 detail MUST NOT 写入完整 prompt、完整输出或 key。
|
||
|
||
#### Scenario: request failure
|
||
- **WHEN** 模型请求因网络错误、认证调用异常、AbortSignal 或无 HTTP metadata 的 SDK 错误失败
|
||
- **THEN** LLM checker SHALL 返回 `phase: "request"` 的 error failure
|
||
|
||
#### Scenario: output mismatch failure
|
||
- **WHEN** 模型输出不满足 `expect.output`
|
||
- **THEN** LLM checker SHALL 返回 `phase: "output"` 的 mismatch failure
|
||
|
||
#### Scenario: 非流式成功摘要
|
||
- **WHEN** `provider: openai` 的非流式检查成功
|
||
- **THEN** detail SHALL 使用类似 `LLM openai http 200 finish=stop, output=2 chars, usage=12/2 tokens` 的简短格式
|
||
|
||
#### Scenario: 流式成功摘要
|
||
- **WHEN** `provider: anthropic` 的流式检查成功且存在 raw finish reason
|
||
- **THEN** detail SHALL 使用类似 `LLM anthropic stream 200 finish=stop raw=end_turn, firstToken=624ms, output=2 chars` 的简短格式
|
||
|
||
#### Scenario: serialize 展示文本
|
||
- **WHEN** store 同步 LLM target
|
||
- **THEN** LLM checker `serialize()` SHALL 返回类似 `openai:gpt-4o-mini @ https://api.openai.com/v1` 的 target 展示文本和 resolved config JSON
|
||
|
||
### Requirement: LLM Checker 测试策略
|
||
LLM checker 的自动化测试 MUST 不访问真实外部模型服务。测试 SHALL 使用本地 mock HTTP/SSE 服务模拟 OpenAI Chat Completions、OpenAI Responses 和 Anthropic Messages 的成功、错误和流式响应。测试 SHALL 覆盖 schema、语义校验、defaults 合并、变量替换、provider factory、observation、expect、execute、registry 注册、配置加载和 JSON Schema 导出。
|
||
|
||
#### Scenario: 本地 mock provider 测试成功路径
|
||
- **WHEN** 测试运行 LLM checker 的 OpenAI、OpenAI Responses 和 Anthropic 成功路径
|
||
- **THEN** 测试 SHALL 使用本地 mock 服务返回 provider 响应,不依赖外部网络或真实 API key
|
||
|
||
#### Scenario: 本地 mock provider 测试错误路径
|
||
- **WHEN** 测试运行 401、429、500、超时、stream error、stream abort、缺 usage 或无文本输出路径
|
||
- **THEN** 测试 SHALL 断言 LLM checker 返回符合 spec 的 matched、failure phase、actual、detail 和 observation
|
||
|
||
#### Scenario: 质量检查覆盖 LLM checker
|
||
- **WHEN** 实现完成后执行质量检查
|
||
- **THEN** `bun run schema:check`、`bun run check` SHALL 通过
|