能不能让模型写函数,取决于你能不能在提示词里先把“什么算正常”写死。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,这时可以直接在提示词里要求:不写异常处理,只写主流程。提示词中替换掉函数名、类型和流程步骤即可复用到其他函数。
列出该函数可能遇到的异常输入
异常清单最好在写实现之前列完,因为它决定了后面要写几个分支。以上面的函数为例,逐项标注触发条件和出现位置,位置写清楚能让实现时少漏一处:
- 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 里写明,否则读代码的人会误以为是漏判。
用参数化测试覆盖正常路径与异常分支
把清单固化成表,一次执行就能同时盯住两类路径。用例名称要能说明意图,出问题时不看代码也知道挂在哪:
| 用例名称 | 输入 | 期望结果 |
|---|---|---|
| 正常_解析单个键 | 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为None | raw=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,以及是否新增了日志、重试或状态写入这类副作用。发现正常路径被动过,就把那部分还原到骨架版本,异常分支单独保留。