第二十节 — 测试体系
这一节回答:FinBayes 用什么测试体系覆盖业务正确性 / 子系统协作 / 端到端场景 / LLM 不确定性?
测试金字塔在 LLM 应用中的调整
传统软件的测试金字塔是 "单元 > 集成 > E2E",但 LLM 应用多一层评估测试(详见 CHAP-21)—— 因为 LLM 输出无确定预期,必须用统计指标而非断言。
这张图表达什么:从下到上数量递减,从下到上跨越的范围递增。评估测试单独一层是 LLM 应用的特殊性,不能用传统 assert 覆盖(详见 CHAP-21)。
这张图特意不表达什么:每层具体覆盖率指标(在工程实现仓 CI 配置);性能基准测试(在 CHAP-21 评估体系)。
单元测试
覆盖范围
| 类别 | 覆盖对象 |
|---|---|
| 业务对象逻辑 | Session / Watchlist / Judgment Record / Profile / Fin Object / StateCandidate 的方法 |
| 状态机转移 | CHAP-11 的 5 个状态机的每个转移 |
| 工具实现 | 每个工具的纯函数部分(不依赖外部 API) |
| 数据契约校验 | Pydantic schema 的字段级约束 |
| Provider Adapter 内部归一化 | OpenAI-compatible 转换逻辑(不真调 Provider) |
| 输入边界 hook 规则 | 凭证识别正则的命中率(CHAP-17) |
| 综合层输出 schema 校验 | 含反方 / 风险 / 失效条件字段的强约束 |
| 错误处理 | 各种错误码 / 异常路径 |
关键约束
- 单元测试不调真 LLM / 不调真外部 API(速度 + 成本 + 稳定性)
- 单元测试不读真 State Store(用内存 SQLite 或 tmpdir)
- 单元测试不依赖时间(用 freeze_time 等手段冻结时间)
- 每个单元测试 <100ms
LLM 相关的单元测试
LLM 相关的纯函数也走单元测试:
| 测试对象 | 测试内容 |
|---|---|
| Prompt 模板渲染 | 给定输入变量,渲染出预期 Prompt 文本 |
| LLM 响应解析 | 给定 mock 响应(含异常格式),解析逻辑健壮 |
| Function Calling schema 生成 | 给定工具池,生成的 schema 符合 OpenAI 规范 |
| 多轮上下文压缩 | 给定长上下文,压缩后保留关键信息 + 长度合规 |
集成测试
覆盖范围
| 类别 | 覆盖对象 |
|---|---|
| 子系统间协作 | CHAP-09 各子系统接口 + CHAP-10 各 sequence 流转 |
| State Store 完整 CRUD | 真 SQLite(tmpdir 隔离)+ 事务原子性 |
| Provider Adapter Pool | mock LLM Provider + 4 层降级链触发 |
| Capability Registry | 工具注册 + LLM Function Calling mock + 工具调用 |
| Cache | 真 Redis(容器化)或内存 LRU |
| 审计 trail 写入 | 所有事件类型按预期写入 |
| 并发任务执行 | asyncio.TaskGroup + 失败隔离 + 取消(CHAP-12) |
Mock 策略
| 真 / Mock | 对象 |
|---|---|
| 真 | State Store / Cache / 内部子系统 |
| Mock | LLM Provider(用 fixture 录制响应 + 重放)/ 外部数据 Provider(用 fixture) |
LLM Mock 是集成测试的关键工程,下文专门讨论。
关键约束
- 集成测试单次跑应该 <30 秒(不可被开发者跳过)
- 每个子系统至少有 3 个跨子系统的协作场景测试
端到端测试
覆盖范围
CHAP-06 列举的 9 个关键业务场景每个对应至少一组端到端测试:
| 场景 | 端到端测试核心断言 |
|---|---|
| S1 即时认知请求 | 首屏 <X 秒 + 流式输出完整 + 含反方与失效条件 |
| S2 状态确认 | 候选 → 用户确认 → 持久化到 Watchlist |
| S3 复盘 | 历史 Judgment 引用 + 综合层输出含"信息缺口" |
| S4 关注流 | 主动信号触发 → Channel 通知投递 |
| S5 长 Session 上下文 | 多轮后压缩 + 关键 Judgment 保留 |
| S6 边界拒收凭证 | 凭证类输入识别 + 不进入 Task / LLM / 状态对象 / 审计 trail |
| S7-S9(按当前任务类型清单) | 业务约束按 CHAP-06 描述 |
LLM 调用策略
端到端测试分两档:
| 档 | LLM 调用 | 何时跑 |
|---|---|---|
| 快档 | Mock LLM(录制 fixture) | 每次 PR / CI |
| 真档 | 真 LLM(用低成本 Provider,如 DeepSeek) | nightly / release 前 |
约束:快档必须每次 CI 跑(覆盖业务流转);真档跑发现的真实 LLM 问题用于评估闭环(CHAP-21)。
数据隔离
- 端到端测试绝不用用户真实 State Store
- 每次端到端测试在 tmpdir 起独立 State Store + Cache
- 测试结束清理(含 OS Keychain 中的测试凭证 —— 用专属测试命名空间)
LLM Mock 工程
LLM Mock 是 FinBayes 测试体系的关键 —— 既要快、便宜、稳定,又要逼真。
三种 Mock 模式
| 模式 | 描述 | 用途 |
|---|---|---|
| 录制重放 | 真实跑过一次,把请求/响应 fixture 化 | 集成 + 端到端快档 |
| 手工 fixture | 工程写死的固定响应(如"返回工具调用 X") | 单元测试 / 特定路径覆盖 |
| 简化模型 stub | 用简单规则模拟(如 echo / 关键词路由) | 性能测试 / 并发测试 |
录制重放的关键
关键约束:
- fixture 文件纳入 Git(可 review + 可追溯 LLM 行为变化)
- fixture 不含真实凭证 / 用户敏感信息(录制前脱敏)
- fixture 中的请求 hash 基于关键参数(model / messages / tools)+ 忽略时间戳等
- Provider 端 API 变化导致 fixture 失效时,CI 给明确报错(不悄悄通过)
降级路径测试
CHAP-13 列举的每条降级路径必须有测试:
| 降级层 | 测试方法 |
|---|---|
| L1 → L1' | mock 用户 Provider 全部失败 → 验证走系统默认 |
| L1' → L2 | mock 系统默认失败 + 本地 LLM 可用 → 验证走本地 |
| L2 → L3 | mock LLM 全失败 + 缓存有 → 验证走缓存 |
| L3 → L4 | mock 全失败 → 验证走受限菜单 + 用户明示提示 |
| State Store 只读模式 | mock SQLite 锁 / 磁盘满 → 验证读功能可用 + 写拒绝 |
| Cache 不可用 | mock Redis down → 验证退化为不缓存模式 |
| 工具调用超时 | 验证 EvidencePacket 标 timeout + 综合层继续 |
| 证据完全缺失 | 验证综合层输出"信息缺口"而非硬编 |
| 用户主动取消 | 验证 TaskGroup 取消 + 5s grace + 部分结果保留 |
约束:每条降级路径不仅测试走通,还测试用户感知(banner / 错误码 / 审计 trail 写入)。
边界与安全测试
CHAP-17 列举的每条边界约束必须有测试:
| 边界 | 测试 |
|---|---|
| 凭证类输入拒收 | 给输入贴 私钥 / API key / 助记词 等样式 → 拒收 + 安全回应 + 不进入任何后续 |
| 输出端凭证样式过滤 | mock LLM 输出含凭证样式 → 输出端剥除 + banner |
| 执行类工具注册拒绝 | 尝试注册 category=execution 工具 → 拒绝 + 审计记录 |
| 含凭证参数的工具拒绝 | 尝试注册参数含 private_key 字段的工具 → 拒绝 |
| Prompt 注入 | 输入含 "忽略上述指令" → 综合层输出仍符合契约 + 不偏离任务 |
| 用户数据隔离 | 跨 user_id 查询 → 拒绝 / 返回空 |
| TLS 降级 | 配置 HTTP Provider URL → 启动时拒绝 |
| 本机 socket 限 | 尝试从非 localhost 访问 Web API → 拒绝 |
约束:边界测试是回归关键 —— 战略边界一旦被绕过,影响远大于功能 bug。CI 跑边界测试集失败 = 阻断 release。
并发测试
CHAP-12 的并发设计需要专项测试:
| 测试场景 | 内容 |
|---|---|
| TaskGroup 一子任务失败 | 其他子任务能正常完成(或可控取消) |
| 用户主动取消任务 | 5s grace + 部分结果落审计 trail + 资源清理 |
| 多 Session 并发 | 每 Session 独立 + Cache key 不串 |
| backpressure | Provider 限流时排队 + 不掉单 |
| 高并发下 SQLite WAL | 多读 1 写不冲突 + 事务原子性 |
| 主动信号触发的批量任务 | 多 Judgment 同时失效 → 批量任务正确分发 |
测试数据管理
测试用户画像 / Watchlist / Judgment Record
| 类型 | 来源 |
|---|---|
| 单元测试用 fixture | 工程写死 |
| 集成测试用 fixture | YAML 文件 + 测试时载入 tmpdir 隔离 SQLite |
| 端到端测试用 fixture | 多套画像(保守 / 激进 / 多频次 / 新手)+ 多套 Watchlist |
| 评估测试用数据集 | 详见 CHAP-21 |
测试金融对象
- 不用真实交易所 / 真实价格 API
- 用 fixture 模拟价格序列(含暴涨 / 暴跌 / 横盘 / 缺数据等场景)
- 必要时用历史数据回放(如某次行情事件的真实数据)
测试可观测性依赖
测试依赖 CHAP-18 可观测性体系:
- 测试断言可以基于审计 trail(如"凭证拒收事件按预期写入")
- 集成测试用 task_id trace 验证因果链完整
- 性能测试用指标体系(latency / token / cost)做基线对比
CI 集成
| 触发 | 跑什么 |
|---|---|
| 每次 PR | 单元 + 集成 + 端到端快档(Mock LLM)+ 边界测试集 + verify-kb |
| nightly | 全部 + 端到端真档(真 LLM,低成本 Provider)+ 评估测试(详见 CHAP-21) |
| release 前 | nightly 全跑 + 完整评估数据集 + 性能基线对比 |
约束:
- 每次 PR 测试不超过 15 分钟(开发者反馈循环)
- nightly 测试不超过 1 小时(含真 LLM 调用)
- 测试报告进入审计 trail(哪次测试失败 / 哪条 fixture 过期 / 等)
第一阶段不做的测试能力
明示不做:
- 跨平台兼容性矩阵测试(仅主流 macOS + Linux + Windows 各一基线)
- 浏览器 UI 自动化测试(第一阶段 Web UI 仅做最小可用,重点在 CLI / TUI)
- 性能回归基准跑(评估闭环里有,但不在 CI 跑)
- 模糊测试(fuzzing)—— 可演化能力
与其他章节的关系
- 测试覆盖的场景来源 → CHAP-06 关键业务场景
- 测试覆盖的子系统接口 → CHAP-09 子系统组件
- 测试覆盖的流转 → CHAP-10 关键场景流转图
- 测试覆盖的状态机 → CHAP-11 状态对象生命周期
- 并发测试覆盖的设计 → CHAP-12 并发与异步处理
- 降级路径测试覆盖 → CHAP-13 故障与降级路径
- 边界测试覆盖 → CHAP-17 边界与安全
- 测试依赖的可观测性 → CHAP-18 可观测性
- 评估测试详见 → CHAP-21 评估闭环