Prhub

#43255 [CI] Add composed-schema regression tests for DeepSeek V3.2/V4 parsers

原始 PR 作者 alexeldeib 合并时间 2026-05-21 08:36 文件变更 2 提交数 2 评论 1 代码增减 +186 / -0

执行摘要

DeepSeek V3.2/V4 解析器 composed schema 测试

如果解析器仅读取直接顶层的 schema['type'],通过 anyOf、oneOf、allOf 等组合 schema 定义类型的参数可能回退为 string 类型,导致客户端接收到 JSON 编码的字符串而非结构化对象。PR #43019 通过共享 coerce_to_schema_type 修复了该行为,但缺少针对 composed schema 的回归测试。

建议阅读,该 PR 展示了如何为工具调用解析器编写针对复杂 JSON Schema 类型的回归测试,对理解 DSML 解析器的类型强制转换逻辑有参考价值。

讨论亮点

该 PR 仅包含测试代码,没有实质性讨论。自动审查后由 sfeng33 快速批准合并。

实现拆解

  1. V3.2 解析器测试:在 tests/tool_parsers/test_deepseekv32_tool_parser.py 中新增三个测试方法:
    • test_composed_schema_converts_object_and_array_params:验证 anyOf 对象参数转换为 dict,oneOf 数组参数转换为 list。
    • test_string_attr_true_preserves_literal_for_composed_schema:验证 string="true" 时强制保留原始字符串。
    • test_composed_schema_conversion_in_streaming:验证流式场景下 composed schema 仍能正确转换。
  2. V4 解析器测试:在 tests/tool_parsers/test_deepseekv4_tool_parser.py 中新增一个测试方法 test_composed_schema_converts_object_and_array_params,验证 anyOf 对象和 allOf 数组参数的转换。
  3. 所有测试均使用 @pytest.mark.skip_global_cleanup 标记,避免全局自动清理对测试状态的影响。
文件 模块 状态 重要度
tests/tool_parsers/test_deepseekv32_tool_parser.py V3.2 解析器 modified 6.4
tests/tool_parsers/test_deepseekv4_tool_parser.py V4 解析器 modified 5.19

关键符号

test_composed_schema_converts_object_and_array_params test_string_attr_true_preserves_literal_for_composed_schema test_composed_schema_conversion_in_streaming

关键源码片段

tests/tool_parsers/test_deepseekv32_tool_parser.py test-coverage

新增三个 composed schema 回归测试,覆盖 V3.2 解析器的主要场景(非流式对象 / 数组转换、string=true 保留、流式转换)。

# 测试包含 anyOf/oneOf 组合 schema 的参数能否正确转换为 dict 和 list
# 该测试位于 DeepSeekV32ToolParserTest 类中
@pytest.mark.skip_global_cleanup
def test_composed_schema_converts_object_and_array_params(self):
    # 构造工具定义:wait 属性使用 anyOf 中的两个对象 schema,patches 属性使用 oneOf(数组或 null)
    tool = ChatCompletionToolsParam(
        function=FunctionDefinition(
            name='set_timer',
            parameters={
                'type': 'object',
                'properties': {
                    'wait': {
                        'anyOf': [
                            {'type': 'object', 'properties': {'type': {'const': 'until'}, 'date': {'type': 'string'}}},
                            {'type': 'object', 'properties': {'type': {'const': 'for'}, 'minutes': {'type': 'number'}}},
                        ],
                    },
                    'patches': {
                        'oneOf': [
                            {'type': 'array', 'items': {'type': 'object'}},
                            {'type': 'null'},
                        ],
                    },
                },
            },
        ),
    )
    parser = make_parser(tools=[tool])
    # 构造 DSML 输入,string='false' 表示解析器应根据 schema 进行类型转换
    model_output = (
        f'{FC_START}\n'
        f'{INV_START}set_timer">\n'
        f'{PARAM_START}wait" string=\'false\'>{{'type':'for','minutes':2880}}{PARAM_END}\n'
        f'{PARAM_START}patches" string=\'false\'>[{{'op':'replace','path':'/schedule','value':'quiet'}}]{PARAM_END}\n'
        f'{INV_END}\n'
        f'{FC_END}'
    )
    result = parser.extract_tool_calls(model_output, None)
    assert result.tools_called
    args = json.loads(result.tool_calls[0].function.arguments)
    # 验证对象参数解码为 dict,而非 JSON 字符串
    assert isinstance(args['wait'], dict)
    # 验证数组参数解码为 list,而非 JSON 字符串
    assert isinstance(args['patches'], list)
    assert args == {
        'wait': {'type': 'for', 'minutes': 2880},
        'patches': [{'op': 'replace', 'path': '/schedule', 'value': 'quiet'}],
    }

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

风险极低,仅增加测试代码。但需注意:测试用例依赖具体的 schema 结构,若未来 shared coercion 逻辑修改可能需同步更新测试用例。另外 @pytest.mark.skip_global_cleanup 标记若与全局 cleanup 配置冲突可能导致测试跳过或失败。

对用户透明,不涉及生产代码。对团队增加了针对 composed schema 的回归保障,防止后续重构破坏由 PR #43019 引入的修复。

关联 Issue

#43019 [Bugfix] Use shared coerce_to_schema_type in DeepSeekV32 tool parser

完整报告

参与讨论