Prhub

#46827 [Rust Frontend] Keep literal "null" string for string-typed tool params

原始 PR 作者 blasrodri 合并时间 2026-06-29 21:46 文件变更 2 提交数 2 评论 4 代码增减 +48 / -17

执行摘要

修复 string 类型工具参数文字 null 被错误转为 JSON null

Blasrodri 在 PR body 中指出:工具参数的 schema 类型为 string 且值为文字 "null"(如 location="null")时,会被强制转为 JSON null,丢失真实字符串值。这与 Python 端的 coerce_to_schema_type 行为不一致——Python 端在声明类型为 string 时保留 "null" 字符串。此问题影响所有途经 convert_with_optional_schema 的 XML 风格解析器(qwen_coder, glm_xml, deepseek_dsml, minimax_m2/m3, hy_v3)。

该 PR 值得所有 Rust 前端和工具调用相关开发者精读。它清晰地展示了如何通过精确的类型推导处理边界 case,并保持了与 Python 端的行为一致性。两处核心逻辑的修改(convert_with_optional_schemafrom_schema)是设计良好的示例:在修复一个 bug 的同时,通过 review 发现并修复了相关的 enum 边缘情况,体现了谨慎的设计权衡。

讨论亮点

chatgpt-codex-connector[bot] 在 review 评论中(P2 级别)指出:当 schema 省略 type 而使用 {"enum": [null, "auto"]} 时,JsonParamType::from_schema 会将其推导为 String,从而导致 string 类型的 null 保护逻辑将文字 "null" 序列化为字符串 "null" 而非 JSON null。'Before this change it still coerced to null, and the Python schema helper infers null from enum values.' 这一评论直接触发了第二次提交(sha 0193ae2):Blasrodri 在 enum 推导逻辑中增加了对 null 成员的检测,当 enum 包含 null 时返回 OneOf([String, Null]),从而保持了 null 的传输语义。

实现拆解

  1. 修改 convert_with_optional_schema 中的 null 转换逻辑rust/src/parser/src/tool/parameters.rs):将原来的无条件 null 转换改为仅在 param_type != Some(&JsonParamType::String) 时进行。即当参数 schema 类型明确为 string 时,保留文字 "null" 作为字符串;其他类型(integer、object、array)或无 schema 时仍转换为 JSON null。
  2. 改进 JsonParamType::from_schema 中 enum 的推导逻辑(同一文件):当 enum 数组包含 null 成员时,不再简单推导为 String,而是推导为 OneOf([String, Null]),使得文字 "null" 仍能转换为 JSON null,与 Python 端行为一致。
  3. 调整测试用例:新增 string_param_preserves_literal_null_text 测试验证 string 类型参数保留 "null"/"NULL" 字符串;新增 nullable_enum_param_coerces_literal_null 测试验证含 null 成员的 enum 仍将文字 "null" 转为 JSON null。同时,在 deepseek_v32.rs 中将原有测试期望值中的 "empty": null 改为 "empty": "null",以反映变更后的正确行为。
文件 模块 状态 重要度
rust/src/parser/src/tool/parameters.rs 解析器 modified 8.14
rust/src/parser/src/tool/deepseek_dsml/deepseek_v32.rs 解析器 modified 4.49

关键符号

convert_with_optional_schema from_schema

关键源码片段

rust/src/parser/src/tool/parameters.rs core-logic

核心实现文件,包含 `convert_with_optional_schema` 的 null 转换逻辑修改和 `JsonParamType::from_schema` 的 enum 推导改进,以及新增的两个测试用例。

// rust/src/parser/src/tool/parameters.rs/// 将参数输入转换为规范化的 JSON 值。
/// 对于文字 `null`,除了 string 类型的参数外,都转换为 JSON null。
/// string 类型参数必须保留文字 "null" 字符串,因为模型输出文字 "null"
/// 表示字符串本身,而非缺失值。
fn convert_with_optional_schema(param_type: Option<&JsonParamType>, input: &ParamInput) -> Value {
    // 如果输入为文字 "null",且参数类型不是 String,则转为 JSON null
    if let ParamInput::Text(value) = input
        && value.eq_ignore_ascii_case("null")
        && param_type != Some(&JsonParamType::String)
    {
        return Value::Null;
    }    // 如果有 schema,尝试根据类型转换
    if let Some(param_type) = param_type
        && let Some(value) = try_convert_value(param_type, input)
    {
        return value;
    }
    // 无 schema 或转换失败,回退为字符串
    match input {
        ParamInput::Text(value) => Value::String(value.clone()),
        ParamInput::Elements(elements) => {
            Value::Object(convert_elements_to_object(elements, &BTreeMap::new(), None))
        }
    }
}// 在 from_schema 中,对于 enum 类型的处理:
// 如果 enum 包含 null 成员,则推导为 OneOf([String, Null]),
// 使得文字 "null" 仍能转换为 JSON null(与 Python 行为一致)
if let Some(values) = schema.get("enum").and_then(Value::as_array) {
    if values.iter().any(Value::is_null) {
        return Some(Self::one_of(vec![Self::String, Self::Null]));
    }
    return Some(Self::String);
}
rust/src/parser/src/tool/deepseek_dsml/deepseek_v32.rs test-coverage

测试文件,调整了一个现有测试的期望值以反映修复后的正确行为。

// rust/src/parser/src/tool/deepseek_dsml/deepseek_v32.rs
#[test]
fn deepseek_v32_parse_complete_converts_schema_types() {
    // ... 测试输入包含 <parameter name="empty" string="false">null</parameter>
    // 现在期望 empty 字段保留为字符串 "null",因为它在 schema 中被定义为 string 类型
    assert_eq!(
        serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
        json!({
            "whole": 5.0,
            "flag": true,
            "payload": { "nested": true },
            "items": [1, 2],
            "empty": "null", // 修复前为 null,修复后为 "null"
        })
    );
}

评论区精华

Enum 包含 null 时的类型推导 正确性

chatgpt-codex-connector[bot] 指出:当 schema 省略 type 并使用 enum 如 `{"enum": [null, "auto"]}` 时,`from_schema` 将其推导为 `String`,导致新加的 string 类型保护逻辑将文字 "null" 变为字符串 "null" 而非 JSON null,破坏了之前的行为。建议 enum 包含 null 时也推导出 null 类型。

结论:Blasrodri 接受建议,在 `from_schema` 中 enum 处理分支增加了 `null` 成员检测:若 enum 包含 null,则返回 `OneOf([String, Null])`,使得文字 "null" 仍能转换为 JSON null。同时新增 `nullable_enum_param_coerces_literal_null` 测试。 · 已解决

风险与影响

风险较低。变更集中且测试覆盖充分:新增了两个专门测试,并调整了一个现有测试的期望值。核心逻辑仅修改两处:convert_with_optional_schema 增加了一个条件判断,from_schema 中 enum 推导增加了 null 成员检测。所有变更均与 Python 端行为对齐,回退路径明确(只需 revert 该 PR)。但需要注意:若未来有开发者依赖原有“总是将文字 null 转为 JSON null”的行为,可能会在 string 类型参数上遇到行为变化,但这正是本 PR 要修复的正确行为。

影响所有使用 convert_with_optional_schema 的 XML 风格工具调用解析器:qwen_coder、glm_xml、deepseek_dsml、minimax_m2/m3、hy_v3。用户在使用这些模型时,若工具参数 schema 类型为 string 且值为文字 "null",现在将正确保留为字符串 "null",而非丢失。对于其他类型,行为不变。影响范围明确,向后兼容性良好(仅修复了不符合预期的行为)。

依赖特定类型推导行为

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论