1
0
Files
DiAL/openspec/specs/llm-checker/spec.md
lanyuanxiaoyao 375dd3492b feat: 结构化 observation 替代 statusDetail,API 层动态构造 detail
- CheckResult: statusDetail -> observation (持久化) + detail (API 动态派生)
- 存储: status_detail 列 -> observation TEXT (JSON)
- CheckerDefinition: 新增 buildDetail(observation) 方法
- 各 checker 返回结构化 observation,API 层通过 registry 调用 buildDetail
- HTTP: bodyPreview 在 status/header 失败时也提前采集
- UDP: observation 包含 durationMs,未响应归为 error failure
- CMD: 超时/输出超限时保留已收集 observation
- TCP: connectTimeMs 仅含连接建立耗时,不含 banner 等待
- 新增 buildDetail 单测和 mapCheckResult 覆盖测试
- 同步 openspec 主规范,归档 checker-observation 变更
2026-05-19 22:49:00 +08:00

244 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 状态码数组语义并复用 HTTP status 断言。`expect.headers` SHALL 使用共享 `KeyValueExpect` 且 header key 大小写不敏感。`expect.output` MUST 使用共享 `ContentRules``expect.finishReason``expect.rawFinishReason` SHALL 使用共享 `ValueMatcher``expect.usage.*``expect.stream.firstTokenMs``expect.durationMs` SHALL 使用共享 `ValueMatcher`。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 使用默认 `status: [200]` 语义
#### Scenario: expect headers 通过
- **WHEN** observing fetch 捕获的响应 headers 满足 `expect.headers` 配置
- **THEN** LLM checker SHALL 判定 headers 断言通过
#### Scenario: output ContentRules 通过
- **WHEN** LLM 输出文本满足 `expect.output` 中配置的全部 ContentRules
- **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** LLM checker SHALL 将该规则按 `exists: true` 语义执行
#### Scenario: output 规则按顺序快速失败
- **WHEN** `expect.output` 包含多个规则且第一条规则失败
- **THEN** LLM checker SHALL 返回第一条失败规则的 mismatch failure不继续校验后续 output 规则
### Requirement: LLM Stream 断言
LLM checker SHALL 仅允许 `mode: stream` 使用 `expect.stream``expect.stream.completed` 未配置时LLM checker SHALL 在 stream observation 路径使用默认 `true` 语义。`expect.stream.firstTokenMs` SHALL 使用共享 `ValueMatcher`,并仅统计第一个非空 `text-delta` 事件耗时,不统计 reasoning、tool call 或 source 事件。
#### Scenario: stream completed 默认值
- **WHEN** target 配置 `llm.mode: stream` 且未配置 `expect.stream.completed`
- **THEN** LLM checker SHALL 要求 SDK stream 正常完成
#### 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 通过