Files
agcore/design/prd/1-Usage字段反序列化容错需求.md
T
徐涛 b7d0a7335f docs: 添加 Usage 字段反序列化容错方案文档
为 Usage 结构体及其内嵌字段添加 `#[serde(default)]` 反序列化容错,
使 blocking 调用在火山平台返回不完整 usage 时不再崩溃。

- 新增 PDD:分析问题根因、设计变更方案、验证标准与回滚方案
- 新增 PRD:梳理需求范围、边界假设与验收标准
2026-07-27 09:34:11 +08:00

8.7 KiB
Raw Blame History

PRDUsage 字段反序列化容错

状态Draft 作者proposal 日期2026-07-27

1. 核心目标

agcore::Usage 结构体的必填 token 字段添加 #[serde(default)] 反序列化容错,使 OpenAI Response API 的 blocking(非流式)调用在火山平台返回不完整的 usage 字段时不再崩溃。

2. 目标用户与场景

用户角色 使用场景 核心诉求
dc-management 使用者 AI 探索(产品信息采集) 点击「AI 探索」后能正常返回结果,不因 usage 缺失而报错
dc-management 使用者 LLM 聊天 普通聊天功能正常响应
dc-management 使用者 要素值正则化(Phase 2 数据归一化流程不因 usage 解析失败而中断
agcore 下游 crate 任何使用 agcore::llm 且后端可能不返回完整 usage 的项目 反序列化鲁棒性提升

3. 问题描述

3.1 报错信息

dc-management 项目调用火山引擎 Responses API 时,非流式路径报错:

OpenAI Response 响应解析失败 error=missing field 'prompt_tokens' at line 1 column 1581

3.2 影响面

经排查,以下功能被阻断(4/5 的 LLM 命令受影响):

功能 命令 状态
AI 探索(产品采集) llm_collectworkflow.rs .chat() 不可用
普通聊天 llm_chatllm.rs .chat() 不可用
要素值正则化 llm_normalize_valuesnormalizer.rs .chat() 不可用
数据验证 validator.rs .chat() 不可用
连接验证 verify_llm_connectionchat_stream() 幸免(流式)

3.3 根因

  • llm/types/usage.rs:4-12Usage 结构体的 prompt_tokenscompletion_tokenstotal_tokens 定义为 必填 u32,非 Option<u32>
  • llm/provider/openai_response.rs:207OpenaiResponseBody 内嵌 usage: Usage,也是必填
  • 火山平台的 Responses API 返回的 usage 对象中,三个必填 token 子字段缺失usage 键存在但子字段不完整),触发了 serde 反序列化的 missing field 错误
  • 流式路径不受影响:其 PartialUsage 所有字段均为 Option<u32>,缺失时通过 unwrap_or(0) 兜底

3.4 已知线索

  • dc-management 的 verify_llm_connection(连接验证函数)已预见到此问题,注释明确说明特意使用流式来规避(llm.rs:368-371
  • 这是已知的设计约束——流式已容错但 blocking 路径未同步加固

4. 功能清单

v1 必做

  • Usage 结构体加 struct-level #[serde(default)]src/llm/types/usage.rs:3):

    - #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
    + #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
    + #[serde(default)]
      pub struct Usage {
          pub prompt_tokens: u32,
          pub completion_tokens: u32,
          pub total_tokens: u32,
          #[serde(skip_serializing_if = "Option::is_none")]
          pub completion_tokens_details: Option<CompletionTokensDetails>,
          #[serde(skip_serializing_if = "Option::is_none")]
          pub prompt_tokens_details: Option<PromptTokensDetails>,
      }
    
    • 作用:JSON 中缺失 Usage任何子字段时,自动使用 Default::default() 取值
    • 三个 token 字段:缺失时默认 0
    • 两个 Option 详情字段(completion_tokens_details / prompt_tokens_details):缺失时默认 None
    • 类型全部保持不动,不改为 Option,不丢失语义
    • 等效代码行数:1 行
  • OpenaiResponseBody.usage#[serde(default)]src/llm/provider/openai_response.rs:207):

      pub(crate) struct OpenaiResponseBody {
          pub id: String,
          pub model: String,
          pub output: Vec<ResponseOutputItem>,
    +     #[serde(default)]
          pub usage: Usage,
          pub status: String,
      }
    
    • 作用:整个 usage 键在 JSON 中完全缺失时,自动默认 Usage::default()
    • 等效代码行数:1 行

变更合计:2 行 serde 属性宏,零逻辑变更,零类型变更。

v1 可选

  • 无(上述两项即可完整修复)

v2 考虑

  • 无——struct-level #[serde(default)] 已覆盖所有子字段,completion_tokens_details / prompt_tokens_details#[serde(default)] 需求已在 v1 中一并解决
  • 流式路径的 ResponseSseMeta 不包含 usage 字段(src/llm/provider/openai_response.rs:335-340),response.completed 事件的 usage 信息未被捕获。这是一个独立的缺陷,与当前问题无关,建议单独处理

非目标

  • 不修改 Usage 字段类型(保持 u32,不改为 Option<u32>
  • 不修改流式路径(PartialUsage 的 usage 捕获问题单独处理)
  • 不修改其他 providerOpenaiChat / Anthropic),它们有自己的反序列化逻辑

5. 边界与假设

边界 / 假设 来源 说明
Usage 已 derive Default 代码确认 #[serde(default)] 直接使用 Default 实现(全字段 0
缺失字段默认 0 对所有消费者安全 代码审查 所有消费方(CostTracker::addMessageResponse 使用处)都在做 saturating_add0 是安全值。流式路径的 PartialUsage.into_usage() 已经 unwrap_or(0),行为一致
序列化行为不受影响 serde 语义 #[serde(default)] 仅在反序列化缺失字段时生效,不影响序列化输出
报错提示的 missing field 确认是子字段缺失而非整个 usage 缺失 错误消息分析 错误消息 missing field 'prompt_tokens' 说明 JSON 路径 usage.prompt_tokens 不存在即键 usage 存在但子字段缺失,因此 #[serde(default)] 加在子字段上是必要条件

6. 术语表

术语 定义 说明
blocking 路径 LlmProvider::chat() 非流式调用 发送单个 HTTP 请求,等待完整 JSON 响应后一次性解析
streaming 路径 LlmProvider::chat_stream() 流式调用 通过 SSE 逐事件推送,usage 可选
#[serde(default)] serde 属性宏 反序列化时如果字段缺失,使用类型的 Default 实现填充
Responses API OpenAI 标准 POST /responses 协议 区别于 Chat CompletionsPOST /chat/completions

7. 验收标准

  • Usage 结构体三个 token 字段在 JSON 缺失时不报错,默认值为 0
  • OpenaiResponseBody.usage 键在 JSON 中完全缺失时也不报错,默认值为 Usage::default()
  • dc-management 的 llm_collectAI 探索)命令正常返回结果
  • dc-management 的 llm_chat(普通聊天)命令正常响应
  • 已有序列化行为不受影响(输出 JSON 仍包含全部 token 字段)
  • cargo test 全部通过
  • cargo clippy 无新增警告
  • 补两条反序列化测试:
    • response_api_missing_usage_fields — mock 响应中 usage 对象缺失 prompt_tokens / completion_tokens / total_tokens,验证不报错且 usage 字段全为 0
    • response_api_missing_usage_key — mock 响应中完全没有 usage 键,验证不报错且默认 Usage::default()

8. 风险评估

风险 影响 可能性 应对方向
下游 crate 升级 agcore 后依赖 usage 字段不缺失 无——#[serde(default)] 不改变已有行为 极低 类型不变、字段名不变
序列化时 #[serde(default)] 影响输出 无——#[serde(default)] 只影响反序列化 极低 serde 明确语义
测试覆盖不足 当前 mock 响应均携带完整 usage,不会触发新路径 建议加一条 usage 缺失的测试用例

9. 发布计划(可选)

阶段 范围 时间
v1 两处变更(usage.rs 3 字段 + openai_response.rs 1 字段) 即日
发布 打 tag(如 v0.3.6v0.3.5-usage-fix),更新 dc-management 引用 即日

10. 历史版本

版本 日期 变更说明
v1 2026-07-27 人工种子(原始)

种子内容

发起方:dc-management 项目 src/routes/products/ai-explore/ 页面「AI 探索」功能报错

报错信息:

OpenAI Response 响应解析失败 error=missing field 'prompt_tokens' at line 1 column 1581

根因:

  • src/llm/types/usage.rsUsage 结构的 prompt_tokens, completion_tokens, total_tokens 为必填 u32
  • src/llm/provider/openai_response.rsOpenaiResponseBody 内嵌 usage: Usage
  • 火山平台 Responses API 返回的 usage 子字段缺失 → serde 反序列化失败

修复方向:

  • Usage 三字段加 #[serde(default)](缺失时默认 0
  • OpenaiResponseBody.usage#[serde(default)](整个缺失时默认 Usage::default()