DeepSeek-Coder 生成函数骨架、人补边界条件和异常分支

文章导读
能不能让模型写函数,取决于你能不能在提示词里先把“什么算正常”写死。DeepSeek-Coder 在补全有明确输入输出描述的骨架时表现稳定,但它默认不会替你做取舍:空值该抛异常还是走默认、越界该截断还是拒绝,这些设计决策必须由人先定下来。可行的分工是三步——先用契约约束模型只出正常路径骨架,再由人补齐异常分支,最后用参数化测试把两类路径都固化下来。
📋 目录
  1. 先写清函数的输入输出契约和正常路径再交给模型
  2. 列出该函数可能遇到的异常输入
  3. 为每个异常分支写出期望行为并人工实现
  4. 用参数化测试覆盖正常路径与异常分支
  5. 提交前检查骨架部分是否被改了逻辑
A A

能不能让模型写函数,取决于你能不能在提示词里先把“什么算正常”写死。DeepSeek-Coder 在补全有明确输入输出描述的骨架时表现稳定,但它默认不会替你做取舍:空值该抛异常还是走默认、越界该截断还是拒绝,这些设计决策必须由人先定下来。可行的分工是三步——先用契约约束模型只出正常路径骨架,再由人补齐异常分支,最后用参数化测试把两类路径都固化下来。

骨架生成适合用在契约清晰、正常流程线性的函数上:入参类型、取值范围、返回结构、主流程步骤先写成注释或 docstring,再让模型补函数体。异常输入清单、异常抛出或降级行为、默认值策略属于设计决策,建议人工列清单并逐条实现,不要交给模型自由发挥。验证方式是用参数化测试同时覆盖正常路径与每个异常分支,并在提交前用 diff 确认骨架逻辑未被改动。边界:涉及金额、权限、并发写等场景,异常语义需结合具体业务确认,不能靠模型猜。

先写清函数的输入输出契约和正常路径再交给模型

契约至少要写四样东西:入参名称与类型、每个入参的取值范围或允许形态、返回结构的字段与类型、正常流程的分步描述。把它放在函数顶部,既当提示词输入,也当后续的人工核对依据。例如一个解析重试策略字符串的函数,可以这样写:

def parse_retry_policy(raw: str, default_max: int = 3) -> dict:
    """
    入参:
      raw: str, 形如 'max=5,backoff=2',允许为 None 或空串
      default_max: int, 合法区间 1..10,由调用方保证
    返回:
      {'max': int >= 1, 'backoff': float > 0}
    正常路径:
      1. raw 为 None 或空串 -> 返回 {'max': default_max, 'backoff': 1.0}
      2. 按逗号切分键值对
      3. 逐项解析,命中白名单键则覆盖默认值
    异常分支: 待人工补充
    """

把这段契约连同“只实现正常路径”的要求一起给模型,生成的骨架通常就是直白的顺序流程。如果契约里写了“异常分支待补充”,模型可能顺手塞一个宽泛的 try/except,这时可以直接在提示词里要求:不写异常处理,只写主流程。提示词中替换掉函数名、类型和流程步骤即可复用到其他函数。

列出该函数可能遇到的异常输入

异常清单最好在写实现之前列完,因为它决定了后面要写几个分支。以上面的函数为例,逐项标注触发条件和出现位置,位置写清楚能让实现时少漏一处:

DeepSeek-Coder 生成函数骨架、人补边界条件和异常分支
  • raw 为 None —— 触发条件:调用方传入 None;出现位置:函数入口第一行。
  • raw 只含空白字符 —— 触发条件:raw.strip() 为空;出现位置:入口归一化之后。
  • 片段缺少等号 —— 触发条件:某个逗号片段中不含 '=';出现位置:逐项解析循环内。
  • 数值无法转换 —— 触发条件:int() 或 float() 抛 ValueError;出现位置:值转换处。
  • 数值越界 —— 触发条件:max 小于 1 或大于 10,backoff 小于等于 0;出现位置:转换后的校验步。
  • 出现未知键 —— 触发条件:键不在白名单内;出现位置:键匹配分支。
  • 同一键重复出现 —— 触发条件:某键出现两次;出现位置:赋值前。

这份清单的粒度可以按项目习惯调整,但建议每条都能对应到代码中的一行或一个条件判断。列不清的项,往往就是后面测试覆盖不到的项。

为每个异常分支写出期望行为并人工实现

期望行为要二选一写明白:要么抛出一个具体异常类型并带上可定位的信息,要么返回一个明确的降级结果。最需要避免的是吞掉异常后返回默认值,这会让调用方分不清“没配”和“配错了”。下面是一个可替换的实现骨架:

class PolicyError(ValueError):
    pass


def parse_retry_policy(raw, default_max=3):
    if raw is None:
        raise PolicyError('raw 不能为 None')
    text = raw.strip()
    if not text:
        return {'max': default_max, 'backoff': 1.0}

    allowed = {'max', 'backoff'}
    seen = set()
    result = {'max': default_max, 'backoff': 1.0}

    for item in text.split(','):
        if '=' not in item:
            raise PolicyError('无法解析片段: %r' % item)
        key, _, val = item.partition('=')
        key = key.strip()
        if key not in allowed:
            raise PolicyError('未知配置项: %r' % key)
        if key in seen:
            raise PolicyError('配置项重复: %r' % key)
        seen.add(key)

        if key == 'max':
            try:
                num = int(val)
            except ValueError:
                raise PolicyError('max 不是整数: %r' % val)
            if num < 1 or num > 10:
                raise PolicyError('max 超出范围: %r' % num)
            result['max'] = num
        else:
            try:
                num = float(val)
            except ValueError:
                raise PolicyError('backoff 不是数值: %r' % val)
            if num <= 0:
                raise PolicyError('backoff 必须为正: %r' % num)
            result['backoff'] = num

    return result

异常类型建议统一成项目里已有的一类,或自定义一个继承 ValueError 的子类,便于调用方按类型捕获。返回降级结果的场景(例如空串走默认值)要在 docstring 里写明,否则读代码的人会误以为是漏判。

DeepSeek-Coder 生成函数骨架、人补边界条件和异常分支

用参数化测试覆盖正常路径与异常分支

把清单固化成表,一次执行就能同时盯住两类路径。用例名称要能说明意图,出问题时不看代码也知道挂在哪:

用例名称输入期望结果
正常_解析单个键raw='max=5'{'max': 5, 'backoff': 1.0}
正常_解析两个键raw='max=5,backoff=2'{'max': 5, 'backoff': 2.0}
正常_空串走默认值raw=' '{'max': 3, 'backoff': 1.0}
异常_raw为Noneraw=None抛出 PolicyError
异常_片段缺等号raw='max5'抛出 PolicyError
异常_max非整数raw='max=abc'抛出 PolicyError
异常_max越界raw='max=0'抛出 PolicyError
异常_未知键raw='timeout=3'抛出 PolicyError
异常_重复键raw='max=2,max=3'抛出 PolicyError

在 pytest 里可以用 parametrize 直接映射这张表,异常用例通过 pytest.raises 断言类型;换用其他语言的测试框架时,保持“输入、期望、名称”三列结构即可,验证方式是跑一遍测试看正常与异常用例是否都通过、失败信息能否指到具体行。

提交前检查骨架部分是否被改了逻辑

常见做法是分两次提交:第一次只提交模型生成的骨架与契约注释,第二次提交人工补充的异常分支。补完之后执行 git diff 看这两次之间的差异,用 git diff -w 过滤纯空白变化,必要时用 git diff `--color-moved` 观察哪些行只是位移。重点复核这几类语句:return 语句的返回值是否被顺手改过,if 条件里的布尔表达式是否被调整,循环的起止与切分符号是否变化,异常类型是否从具体类型换成了宽泛的 Exception,以及是否新增了日志、重试或状态写入这类副作用。发现正常路径被动过,就把那部分还原到骨架版本,异常分支单独保留。