简介:面向自动化测试工程师与AI应用开发者的PDF技术文档,系统讲解如何借助DeepSeek API构建智能用例生成方案。文档从自动化测试框架的基础类型与优势切入,详细说明DeepSeek API的语言理解、文本生成、可定制性及处理速度等技术特点,并围绕需求理解、用例生成、用例评估优化三个阶段阐述核心原理。实现部分覆盖环境搭建、需求文档处理、测试用例模板定义、调用API生成用例、覆盖率计算等关键环节,附带完整Python代码示例与详细解释;同时给出性能评估指标、优化策略以及电商平台、在线教育平台等应用场景分析,帮助读者将方案直接用于实际项目。资源为单个PDF文件,共23页,压缩包大小约1.83MB,图文目录清晰、无缺页乱码,目前已有187人学习浏览。对希望借助DeepSeek提升测试效率、探索AI辅助用例生成的开发者而言,这份资料兼具原理讲解与实操参考价值。1. 智能用例生成到底解决了什么自动化测试用例生成一直是人力密集的活。一个中型业务系统,需求文档三五十页,光梳理正常流程、异常分支、边界值就能耗掉测试同学两三天。基于 DeepSeek API 做智能用例生成,方向是把需求文本变成结构化用例集,让模型先理解业务规则,再按边界、异常、权限、数据组合这些维度批量产出测试用例,最后交给自动化测试框架直接执行。这套方案的适用人群很明确:手里有接口测试或 UI 自动化框架、但用例维护成本高的团队;以及想给测试提效的开发者。这里不聊玄学,只讲怎么把这条链路从零搭起来、参数怎么设、会在哪里翻车。2. 选 DeepSeek API 做用例生成:接口形态与 Prompt 工程调用 DeepSeek API 之前要认清一件事:它是个文本补全服务,不是聊两句就会自动帮你把测试计划写好的。你传一段 prompt,它返回一段文本。所以智能用例生成的核心工作不在 API 文档上,而在 Prompt 怎么写。常见做法是走 chat completions 接口,messages 数组里放系统角色和用户内容,系统提示词里设定角色约束,用户消息里放需求文本和输出格式要求。选它而不是本地小模型,图的是按 token 计费、中文理解到位,而且不用自己准备显卡;代价是需求文本要出网,敏感项目别走这条路。2.1 关键参数:温度、Top-P、Max Tokens 怎么影响用例生成用例生成和聊天不一样,参数得按“稳定优先”来调。先说结论,我一般用下面这套起步值:参数起步值对用例生成的影响temperature0.2低温度减少随机性,但太低会导致同一套路反复输出,多样性下降top_p0.9配合 temperature 控制候选词范围,一般不要和 temperature 同时拉到很高max_tokens4000决定单次返回上限,太短用例被截断,太长增加等待时间streamfalse用例生成整体解析,不关流式容易拿到半截内容下面是带着超时控制的基础调用函数,这是整个生成管线的地基:import requests import os def call_deepseek(prompt, temperature=0.2, max_tokens=4000, top_p=0.9): api_key = os.environ.get("DEEPSEEK_API_KEY") if not api_key: raise RuntimeError("缺少 DEEPSEEK_API_KEY 环境变量") resp = requests.post( "https://api.deepseek.com/v1/chat/completions", headers={"Authorization": f"Bearer {api_key}"}, json={ "model": "deepseek-chat", "messages": [ {"role": "system", "content": "你是资深测试设计专家,输出严格遵循用户要求的格式。"}, {"role": "user", "content": prompt}, ], "temperature": temperature, "top_p": top_p, "max_tokens": max_tokens, "stream": False, }, timeout=120, ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"]逻辑说明:这个函数把 API key、模型名、角色设定都集中在一处,后面所有生成任务都复用它。messages里 system 消息写角色约束,user 消息写实际任务,这是 chat completions 接口的标准用法。timeout=120很重要,长 prompt 加 4000 token 的输出可能跑四五十秒,默认超时太短会误判失败。参数怎么改,要分场景说。temperature 在 0.2 到 0.4 之间,是我试过比较稳的区间。拉到 0.8 以上,模型会开始发挥,把“登录密码错误提示”编成“短信验证码错误提示”,用例长得漂亮但内容不可信。top_p 保持 0.9 就行,如果需求文本里专有名词多、模型容易联想,降到 0.85 能少一点幻觉。max_tokens 别抠门,一条完整用例平均 200 token,十条带前置条件和步骤说明就要 2000 以上,我一般给到 4000,配合后面要讲的分批生成,不会截断。2.2 从需求文本到用例集:两段式 Prompt 模板设计直接把整份需求文档塞给模型,要求“生成 100 条测试用例”,效果很差。原因有两个:需求很长时,业务规则被埋在一堆背景描述里,模型抓不住重点;其次,一次性生成太多条,容易出现前面的数据定义到后面就变了。我一般用两段式 Prompt,把“理解需求”和“生成用例”分开,这也是项目里最值得抄的设计:REQ_EXTRACT_PROMPT = """ 你是测试需求分析器。阅读下面的原始需求,提取并输出 JSON,不要输出其他内容: { "business_flows": [{"name": "", "steps": []}], "business_rules": [{"condition": "", "expected": ""}], "error_scenes": [{"trigger": "", "expected": ""}], "permissions": [{"role": "", "allowed_ops": []}] } 原始需求: {requirement_text} """ CASE_GEN_PROMPT = """ 你是自动化测试用例设计器。基于结构化需求,为每个业务规则生成测试用例。 输出必须是 JSON 数组,每个用例对象格式: { "id": "TC_001", "title": "", "precondition": "", "steps": [], "test_data": {}, "expected": "", "priority": "P0|P1|P2", "type": "positive|negative|boundary|permission|combo" } 要求:每个业务规则至少 2 条正例、1 条反例;边界值必须单独成条; 只能使用结构化需求里出现的操作和字段,不得新增任何元素名。 结构化需求: {structured_json} """逻辑说明:第一段让模型把自然语言需求压成结构化 JSON,相当于先做一轮信息抽取;第二段再基于这个 JSON 派生用例。这么拆有两个实际好处。一是需求文本两万字时,第一段先压缩成几千 token 的关键规则,第二段不需要再背完整上下文,token 成本直接降一截。二是中间的结构化 JSON 可以被开发同学 review,业务规则错了直接改 JSON 再重跑第二段,不用把第一段也重新生成一遍。这里几个参数和写法值得注意。第一段 temperature 可以给到 0.4,信息抽取需要一点归纳能力;第二段必须压到 0.2,用例设计阶段要的是稳定而不是发散。两段式还会遇到一个新问题:第二段生成的结果可能不是合法 JSON。先别急着骂模型,这是必然现象,后面第 5 章专门讲怎么兜底。常见误区是有人把 few-shot 示例塞在系统消息里而不是用户消息尾部,导致模型模仿了示例的格式却忽略了任务要求;我一般把示例直接写进 CASE_GEN_PROMPT 的结构说明里,让模型照着 JSON schema 填,比单纯说“请生成 JSON”稳得多。两段式串起来的调度逻辑很简单,核心就是先调一次提取,再调一次生成:import json def generate_case_set(requirement_text: str): structured = call_deepseek( REQ_EXTRACT_PROMPT.format(requirement_text=requirement_text), temperature=0.4, ) cases_text = call_deepseek( CASE_GEN_PROMPT.format(structured_json=structured), temperature=0.2, ) # 这里先直接解析,真实项目要套容错解析,见第 5 章 return json.loads(cases_text)这个函数看起来短,但它是整个智能生成方案的心脏。后面要加覆盖增强、去重、反馈迭代,都是在这个调用链路上挂新逻辑。3. 搭一个最小可运行的用例生成管线:从需求文档到 JSON 用例有了 Prompt 模板,接下来要做的是把它变成一条能反复使用的生成管线:吃进一份需求文档,吐出一批可执行的结构化用例。第 2 章讲的是单次生成,这一章讲的是工程化。目标是让项目里任何一个测试同学都能跑,而不是只有写脚本的人能玩。3.1 项目结构与核心脚本我习惯的最小目录长这样,四个文件,不引入任何测试框架依赖:case_generator/ ├── config.py # API key、模型名、参数默认值 ├── deepseek_client.py # 封装 call_deepseek 和重试 ├── generator.py # 两段式生成主逻辑 └── run_pipeline.py # 命令行入口,串起整个流程这个结构的选型理由很简单:配置、客户端、业务逻辑、入口四层分离。config.py 里放环境变量读取,deepseek_client.py 放网络调用和重试,generator.py 放 Prompt 模板和组装逻辑,run_pipeline.py 只负责命令行参数解析和流程编排。等后面要接 pytest 或者企业微信通知,只改入口层,不动生成逻辑。run_pipeline.py 的核心入口用 argparse 接收参数,这是最小可用的命令行设计:import argparse import json import os from generator import generate_case_set def load_requirement(path: str) -> str: with open(path, "r", encoding="utf-8") as f: return f.read() def save_jsonl(cases: list, output_path: str): with open(output_path, "w", encoding="utf-8") as f: for case in cases: f.write(json.dumps(case, ensure_ascii=False) + "\n") def main(): parser = argparse.ArgumentParser(description="DeepSeek 智能用例生成管线") parser.add_argument("--input", required=True, help="需求文档路径,支持 txt/md") parser.add_argument("--output", default="cases.jsonl", help="输出文件路径") parser.add_argument("--batch-size", type=int, default=10, help="每批生成用例数量上限") args = parser.parse_args() requirement = load_requirement(args.input) cases = generate_case_set(requirement) save_jsonl(cases, args.output) print(f"生成完成:{len(cases)} 条用例 -> {args.output}") if __name__ == "__main__": main()运行方式就一行:export DEEPSEEK_API_KEY=你的key python run_pipeline.py --input docs/订单需求.md --output cases/订单需求.jsonl逻辑说明:入口脚本不直接调模型,而是调用 generator 里的 generate_case_set,后面换参数、加去重都不影响命令行。jsonl 输出是我刻意选的,不用一个大 JSON 文件,因为每条用例独立一行,测试框架逐行读取时哪一行出错就定位哪一行,多人同时改文件也不容易冲突。print 那行是给命令行交互看的,真实项目可以替换成日志。3.2 生成、格式校验、入库的完整流程光把用例生成出来还不够,大模型输出不可控,必须加一道格式校验闸门,不合格的用例不能进文件。常见做法是生成后先做 schema 校验,再写入 jsonl。校验函数长这样:REQUIRED_FIELDS = {"id", "title", "precondition", "steps", "expected", "priority", "type"} VALID_PRIORITY = {"P0", "P1", "P2"} VALID_TYPE = {"positive", "negative", "boundary", "permission", "combo"} def validate_case(case: dict) -> list: errors = [] missing = REQUIRED_FIELDS - case.keys() if missing: errors.append(f"缺少字段: {missing}") if not isinstance(case.get("steps"), list) or not case.get("steps"): errors.append("steps 必须是非空数组") if case.get("priority") not in VALID_PRIORITY: errors.append(f"priority 非法: {case.get('priority')}") if case.get("type") not in VALID_TYPE: errors.append(f"type 非法: {case.get('type')}") return errors def validate_batch(cases: list) -> tuple: valid, invalid = [], [] for i, case in enumerate(cases): errs = validate_case(case) if errs: case["_error"] = errs invalid.append(case) else: valid.append(case) return valid, invalid参数说明:REQUIRED_FIELDS 集合定义了用例的字段契约,这是为了和自动化测试框架对齐。priority 和 type 用枚举校验,防止模型输出“高中低”这种我们框架不认的写法。validate_batch 返回两个列表,而不是直接抛异常,是为了让流程继续处理合法用例,同时把不合法的单独落盘,供人工 review。这一步看起来多余,但它能拦截掉大模型至少三成的“长得像用例但没法执行”的输出。完整流程还缺一个:需求文档往往很长,一次生成 50 条用例容易截断。所以 generate_case_set 内部要按 batch_size 拆,每批只让模型生成 10 条左右。拆批不是简单切片,而是让模型每批聚焦一个业务规则,避免上下文被冲散。调整第 2 章的 generate_case_set,让它接受 batch_size,并且对结构化 JSON 里的 business_rules 循环生成:def generate_case_set(requirement_text: str, batch_size: int = 10): structured_text = call_deepseek( REQ_EXTRACT_PROMPT.format(requirement_text=require文本前2000字) ) structured = json.loads(structured_text) rules = structured["business_rules"] all_cases = [] for i in range(0, len(rules), batch_size): batch = rules[i:i + batch_size] batch_json = json.dumps({"batch_index": i // batch_size, "rules": batch}, ensure_ascii=False) cases_text = call_deepseek( CASE_GEN_PROMPT.format(structured_json=batch_json) ) cases = parse_cases(cases_text) all_cases.extend(cases) return all_cases逻辑说明:外层循环按业务规则分批,每批的 prompt 里只放当前这批规则。这样做的好处是每批的上下文小、模型聚焦,坏处是调用次数变多、耗时增加。batch_size 的设置我一般压在 5 到 10 条规则之间,超过 15 条,输出质量和稳定性都会下降。如果你遇到“第一批很好,第二批开始重复”的现象,优先把 batch_size 调小,而不是去调 temperature。第 2 章生成的是单批用例,这一章把它变成循环调度、批量校验、落盘的标准管线。到这里,你已经可以用一份需求文档跑出一份 jsonl 用例文件了。但实测跑下来你会发现,用例是有了,覆盖经常偏科,正例一抓一大把,反例和边界值少得可怜。这是下一章要解决的问题。4. 覆盖增强与去重策略:不只要生成,还要生成得稳用例数量多不等于质量好。直接用两段式流程跑出来的用例集,最常见的问题是覆盖集中:模型默认先写正常流程,反例写几条意思一下,边界值基本靠运气。这章讲两个手段:一个是在生成侧做覆盖增强,按维度拆解而不是一次大而全;另一个是在校验侧做去重和合并,把模型爱说车轱辘话的毛病治掉。4.1 覆盖增强:按维度拆解需求,不靠一次大 Prompt我用的方法很朴素:把用例按维度拆成五类,每个维度单独生成,单独评估覆盖情况。五个维度是 positive 正常流程、negative 异常流程、boundary 边界值、permission 权限、combo 多条件组合。每个维度给不同的数量和指令,temperature 也可以不一样。DIMENSIONS = { "positive": { "count": 5, "instruction": "覆盖主流程和关键分支流程,每个流程至少一条用例,步骤必须完整。", "temperature": 0.2, }, "negative": { "count": 5, "instruction": "覆盖非法输入、异常操作、依赖服务不可用、中间步骤失败等场景。", "temperature": 0.2, }, "boundary": { "count": 3, "instruction": "覆盖边界上下限、长度限制、分页边界、数值精度边界。", "temperature": 0.2, }, "permission": { "count": 3, "instruction": "覆盖无权限、部分权限、越权访问、角色切换后的权限变化。", "temperature": 0.3, }, "combo": { "count": 2, "instruction": "覆盖多个边界条件同时出现、多个异常场景叠加的组合用例。", "temperature": 0.3, }, } def generate_by_dimensions(structured_json: str) -> list: all_cases = [] for dim, conf in DIMENSIONS.items(): prompt = f""" 你是自动化测试用例设计器。 针对维度“{dim}”生成测试用例,数量 {conf['count']} 条。 {conf['instruction']} 输出 JSON 数组,字段格式不变。 结构化需求: {structured_json} """ cases_text = call_deepseek(prompt, temperature=conf["temperature"]) all_cases.extend(parse_cases(cases_text)) return all_cases逻辑说明:把一次大生成拆成五次小生成,单次返回更稳定,每个维度还能单独肉眼检查。temperature 设置上,positive 和 boundary 用低温度保证步骤准确,permission 和 combo 用 0.3 稍微放一点发散,因为权限组合和条件组合需要模型多做一点“排列组合”。count 的配比可以根据业务调整,我上面的 5/5/3/3/2 是针对订单类接口的考古调法,如果你的系统登录相关功能多,permission 的占比可以提到 5。这里有个关键点:维度拆分之后,结构化需求 JSON 不需要重新抽取,一次抽取结果能喂给五个维度,省了重复 token。这也正是第 2 章两段式设计的收益。实际跑的时候,你会看到 negative 维度经常生成一些“网络断开”“数据库超时”这类依赖注入的场景,这些用例在接口测试里能直接模拟,在 UI 自动化里执行起来成本很高。所以我会在 negative 的 instruction 里加一句“优先选择可模拟的异常,如接口返回错误码、字段非法”,把玄学场景挡在外面。4.2 去重与合并:相似度判重和路径冲突处理维度拆完,下一步是去重。你跑几轮之后会发现,模型特别喜欢用不同措辞描述同一个操作,“点击登录按钮”和“按下登录键”意思完全一样。去重不能只靠完全匹配字符串,得算相似度。轻量方案不引入向量库,用步骤序列的 Jaccard 相似度就够:import re def normalize_step(step: str) -> str: """归一化:替换同义词、去掉标点空白,降低无关干扰""" step = step.replace("登录", "登录").replace("登陆", "登录") step = re.sub(r"点击|按下|点一下", "点击", step) step = re.sub(r"[\s,。!?、,.:;]", "", step) return step def jaccard_similarity(steps_a: list, steps_b: list) -> float: if not steps_a or not steps_b: return 0.0 set_a = set(normalize_step(s) for s in steps_a) set_b = set(normalize_step(s) for s in steps_b) inter = len(set_a & set_b) union = len(set_a | set_b) return inter / union if union else 0.0 def dedup_cases(cases: list, threshold: float = 0.85) -> list: result = [] for case in cases: steps = case.get("steps", []) is_dup = False for existing in result: if jaccard_similarity(steps, existing["steps"]) >= threshold: is_dup = True break if not is_dup: result.append(case) return result逻辑说明:Jaccard 计算的是“两个步骤集合的交集占比”,值越接近 1 越相似。threshold 取 0.85,意思是有八成五的步骤是重合的就认为是重复用例。这个阈值对 10 步以上的用例比较稳,步骤太少时容易误判,比如一个三步的正例和一个三步的反例,步骤完全一样,期望结果不一样,Jaccard 会打出 1.0 的分数,但它们明显不该被删。所以判重只看 steps,但最终补偿要结合 expected。对策是加一个合并函数:steps 完全一样、expected 不同的用例不该删,而应该合成一条参数化用例。示例:def merge_by_expected(cases: list) -> list: groups = {} for case in cases: key = tuple(case["steps"]) prep = case.get("precondition", "") key = (prep, key) groups.setdefault(key, []).append(case) merged = [] for (prep, steps), group in groups.items(): base = group[0] extra_expected = [c["expected"] for c in group[1:] if c["expected"] != base["expected"]] if extra_expected: base["expected"] = base["expected"] + ";" + ";".join(extra_expected) base["type"] = "combo" merged.append(base) return merged参数说明:合并时把 precondition 也放进 key,关键原因:同样的步骤在“已登录”和“未登录”前置条件下是完全不同的两条用例,不能合并。合并后把 type 改成 combo,是为了让框架执行时知道这条用例有多组期望结果,需要参数化跑。这个细节容易踩坑,我见过有人合并后只保留第一个 expected,结果反例被正例吞掉,看起来用例数少了,实际是测试覆盖悄悄缩水了。到这里,你已经能生成覆盖面相对均衡、无明显重复的用例集。但这套链路真正开始考验人的,是接入 API 之后的各种不稳定。下一章讲我在这条链路上踩过的五个坑,每一个都是真金白银换来的血泪经验。5. 智能用例生成避坑指南:DeepSeek API 接入中的 5 个常见问题大模型生成用例,最大的特点就是不可控。前面几章的设计都是在和不可控做对抗,但有些坑光靠 Prompt 设计绕不过去,必须在工程层兜底。这一章写五个我实际遇到的问题,每条按现象、原因、解决三步讲。5.1 响应被截断:输出长度限制与分段生成现象:生成的 JSON 到一半就断了,json.loads 直接抛 “Expecting value”。第一次遇到我还以为是温度太高模型疯了,后来看返回内容,明显是 max_tokens 不够用。原因:一次让模型生成 40 条完整用例,每条带步骤、测试数据、期望结果,token 消耗早就超过了单次返回上限。模型不是不想写完,是写到字数上限被强行截断。解决:分两层。第一层是第 3 章的 batch_size 控制,每批少生成一点;第二层是写一个“检测残缺 + 续写”的兜底函数。检测逻辑很简单:JSON 解析失败时,尝试把已生成的文本作为前缀,拼上“继续输出剩余 JSON,不要重复已输出内容”再调一次:def generate_complete_cases(prompt, max_retries=2): text = call_deepseek(prompt) for _ in range(max_retries): try: return json.loads(text) except json.JSONDecodeError: text = call_deepseek( "继续输出上一条 JSON 的剩余部分,只输出 JSON 数组内容,不要复述前面内容:\n" + text[-500:] ) raise RuntimeError("连续续写失败,请减小 batch_size")参数说明:续写时补上text[-500:]是为了让模型知道从哪里接着写,但只给末尾 500 字符,避免把整段重复内容再算一遍 token。这个兜底能救回一部分截断,但不是万能的,如果连续两次续写都失败,最有效的做法是改小 batch_size,而不是继续硬凹。5.2 幻觉用例:不存在的按钮和数据现象:生成的用例里出现需求文档里根本没有的功能,比如订单系统里冒出一个“一键迁移订单”,操作步骤里引用了一个“订单状态重置按钮”,实际上是系统里不存在的东西。原因:需求文本没有给出页面元素清单和字段范围,模型在开放性生成时用训练数据里的常识补全了“该有的功能”。这是大模型幻觉在测试场景中的典型表现,不是模型坏了,而是你给它的约束不够。解决:在 Prompt 里强制加入“可用操作清单”和“字段范围表”,并明确写“只能使用以下元素”。具体做法是把结构化 JSON 里增加一个 allowed_ops 字段,把系统里真实存在的接口、按钮、字段名列进去。格式类似:"allowed_ops": ["创建订单", "取消订单", "查询订单", "修改收货地址"], "field_ranges": {"price": "0.01-999999.99", "coupon_id": "数字,可空"}然后在 CASE_GEN_PROMPT 里把“不得新增任何元素名”这句话变成硬约束,放在用户消息末尾,而不是埋在系统消息里。模型对近处指令的遵从度明显高于远处的声明。这算是我踩坑换来的经验:靠 Prompt 中间一句“请使用正确的元素”是不够的,元素白名单本身必须进结构化 JSON。5.3 延迟与重试:超时、限流、退避策略现象:几十个用例批量生成时,偶尔会有单次请求 pending 四五十秒不返回,或者 HTTP 429。跑一次全量生成,总有几条因为超时失败,整个脚本中断。原因:长 prompt 加 4000 token 输出,单次推理时间长是正常的;并发请求一多,限流就来了。请求失败本身不意外,意外的是我们没写重试,导致一次失败拖垮整批生成。解决:必须加指数退避重试,并且区分可重试和不可重试的错误。HTTP 429、502、503 可以重试,401 认证失败、400 参数错误重试多少次都没用。下面是简化版重试逻辑:import time def call_with_retry(prompt, max_retries=3, base_delay=2.0, **kwargs): last_exc = None for attempt in range(max_retries): try: return call_deepseek(prompt, **kwargs) except requests.exceptions.HTTPError as e: status = e.response.status_code if status in (401, 400, 422): raise if attempt == max_retries - 1: raise delay = base_delay * (2 ** attempt) print(f"HTTP {status},第 {attempt + 1} 次重试,等待 {delay}s") time.sleep(delay) except requests.exceptions.Timeout: if attempt == max_retries - 1: raise time.sleep(base_delay * (2 ** attempt)) raise RuntimeError("重试失败")参数说明:base_delay=2.0 是指第一次重试等 2 秒,第二次 4 秒,第三次 8 秒。指数退避的核心是别把重试请求同时打进服务端造成雪崩。timeout 那段单独捕获,因为超时不一定真的失败了,可能只是响应慢,重试的代价比直接放弃小。我习惯把 max_retries 压到 3,重试次数太多,最后一批结果跑完了,前面的缓存已经被冲掉,工程上不值得。5.4 成本控制:Token 消耗与缓存现象:跑完一轮全量生成,打开账单发现消耗比预期多不少。一查,同一份需求文档在调试 prompt 时反复提交了十几次,每次都是全量 token。原因:成本主要烧在输入 token 上。调用一次生成,需求文本几千 token 进去,模型输出同等量级回来,输入输出各计费。调试 Prompt 时以为自己在调一句话,实际每次都在为完整需求文本付费。解决:加一层语义缓存。同一份需求文本,不管跑哪个维度,结构化结果都是一样的,没必要每次重新抽取。实际操作用需求文本的哈希值做缓存 key:import hashlib cache = {} def cached_generate_case_set(requirement_text: str): req_hash = hashlib.sha256(requirement_text.encode("utf-8")).hexdigest() if req_hash in cache: return cache[req_hash] cases = generate_case_set(requirement_text) cache[req_hash] = cases return cases参数说明:这个方案简单粗暴,进程内字典缓存,适合脚本场景。如果要落到工程级,把哈希 key 和结果存到 Redis 或本地 sqlite。重点是把结构化抽取结果单独缓存,因为它是后续所有维度生成的公共依赖,缓存一份能省下五分之四的输入 token。调试 Prompt 时,先用截断的需求片段调通格式,再跑全量,也是省钱的习惯。5.5 格式漂移:JSON 解析失败现象:模型偶尔会在 JSON 外面包一层 markdown 代码块,开头是```json,结尾是```;或者 JSON 内部多了一个逗号。这类输出用标准 json.loads 会直接失败,但内容本身是完整的。原因:模型在训练数据里见过大量代码块示例,生成时模仿了那种格式。这和模型能力关系不大,纯粹是输出风格漂移,每天都有概率出现。解决:写一个容错解析函数,第一步把文本里的 markdown 代码块标记剥掉,第二步裁剪出第一个{到最后一个}之间的内容,第三步再尝试 json.loads:def robust_json_loads(text: str): text = text.strip() if text.startswith("```"): text = re.sub(r"^```[a-zA-Z]*\\n|```$", "", text) start = text.find("{") end = text.rfind("}") if start == -1 or end == -1: raise ValueError("找不到 JSON 对象边界") candidate = text[start:end + 1] try: return json.loads(candidate) except json.JSONDecodeError: candidate = candidate.rstrip(",") candidate = re.sub(r",\\s*([}\\]])", r"\\1", candidate) return json.loads(candidate)逻辑说明:正则把,后面紧跟右括号或中括号的多余逗号去掉,处理的是模型最爱犯的“最后一个元素后面多逗号”问题。这个方法不是银弹,如果模型输出中途丢失字段,裁剪也没用,会走到 ValueError。所以 robust_json_loads 要放在前面 5.1 的续写逻辑之前:先做格式清洗,清洗不掉再续写,最后还不行就丢弃这条结果并记录日志。丢几条用例不可怕,可怕的是整个流程因为一条脏数据中断。这五个问题基本覆盖了 API 接入层的主要雷区。解决了它们,生成链路才算真正稳定。最后一章把这批用例接进自动化测试框架,形成一个能迭代的闭环。6. 把生成的用例接进自动化测试框架:适配层与反馈闭环6.1 用例格式与框架的适配层生成的 JSON 用例是数据,不是可执行脚本。接进 pytest 这类框架时,要写一个适配层,把用例字典翻译成可调用的测试函数。我的做法是做一个 builder:def build_test_function(case: dict): def test_case(): for step in case["steps"]: # 每个 step 映射到 API 调用、断言或页面动作 run_step(step, case.get("test_data", {})) assert check_expected(case["expected"]) return test_case逻辑说明:适配层不关心用例是怎么生成的,只关心 steps 和 expected。这样生成管线换模型、换 Prompt 都不会污染测试框架。test_data 单独传是因为同一个步骤在参数化用例里会有多组数据,拆开更干净。6.2 用上一轮的失败反馈做第二轮生成把执行结果做成反馈,喂回给模型,生成回归用例或修正原用例。这是我的习惯:每条失败用例跑完,把断言错误摘要和对应步骤拼成反馈 prompt,让模型补一条“针对该失败场景的回归用例”。跑完第一轮只是开始,真正让用例集越用越准的,是这个反馈回路。REGRESSION_PROMPT = """ 以下是执行失败的用例: {case_json} 失败信息: {error_summary} 请生成一条针对该失败场景的回归用例,要求覆盖相同路径但增加断言强度。 """时间久了你会养成一个习惯:每次跑完测试,先存一份失败摘要到固定目录,下一轮生成前把它作为上下文传进去。不要嫌麻烦,这个动作能让用例集从“通用”慢慢长出“业务敏感”。希望帮到你。本文还有配套的精品资源,点击获取
简介:面向自动化测试工程师与AI应用开发者的PDF技术文档,系统讲解如何借助DeepSeek API构建智能用例生成方案。文档从自动化测试框架的基础类型与优势切入,详细说明DeepSeek API的语言理解、文本生成、可定制性及处理速度等技术特点,并围绕需求理解、用例生成、用例评估优化三个阶段阐述核心原理。实现部分覆盖环境搭建、需求文档处理、测试用例模板定义、调用API生成用例、覆盖率计算等关键环节,附带完整Python代码示例与详细解释;同时给出性能评估指标、优化策略以及电商平台、在线教育平台等应用场景分析,帮助读者将方案直接用于实际项目。资源为单个PDF文件,共23页,压缩包大小约1.83MB,图文目录清晰、无缺页乱码,目前已有187人学习浏览。对希望借助DeepSeek提升测试效率、探索AI辅助用例生成的开发者而言,这份资料兼具原理讲解与实操参考价值。1. 智能用例生成到底解决了什么自动化测试用例生成一直是人力密集的活。一个中型业务系统,需求文档三五十页,光梳理正常流程、异常分支、边界值就能耗掉测试同学两三天。基于 DeepSeek API 做智能用例生成,方向是把需求文本变成结构化用例集,让模型先理解业务规则,再按边界、异常、权限、数据组合这些维度批量产出测试用例,最后交给自动化测试框架直接执行。这套方案的适用人群很明确:手里有接口测试或 UI 自动化框架、但用例维护成本高的团队;以及想给测试提效的开发者。这里不聊玄学,只讲怎么把这条链路从零搭起来、参数怎么设、会在哪里翻车。2. 选 DeepSeek API 做用例生成:接口形态与 Prompt 工程调用 DeepSeek API 之前要认清一件事:它是个文本补全服务,不是聊两句就会自动帮你把测试计划写好的。你传一段 prompt,它返回一段文本。所以智能用例生成的核心工作不在 API 文档上,而在 Prompt 怎么写。常见做法是走 chat completions 接口,messages 数组里放系统角色和用户内容,系统提示词里设定角色约束,用户消息里放需求文本和输出格式要求。选它而不是本地小模型,图的是按 token 计费、中文理解到位,而且不用自己准备显卡;代价是需求文本要出网,敏感项目别走这条路。2.1 关键参数:温度、Top-P、Max Tokens 怎么影响用例生成用例生成和聊天不一样,参数得按“稳定优先”来调。先说结论,我一般用下面这套起步值:参数起步值对用例生成的影响temperature0.2低温度减少随机性,但太低会导致同一套路反复输出,多样性下降top_p0.9配合 temperature 控制候选词范围,一般不要和 temperature 同时拉到很高max_tokens4000决定单次返回上限,太短用例被截断,太长增加等待时间streamfalse用例生成整体解析,不关流式容易拿到半截内容下面是带着超时控制的基础调用函数,这是整个生成管线的地基:import requests import os def call_deepseek(prompt, temperature=0.2, max_tokens=4000, top_p=0.9): api_key = os.environ.get("DEEPSEEK_API_KEY") if not api_key: raise RuntimeError("缺少 DEEPSEEK_API_KEY 环境变量") resp = requests.post( "https://api.deepseek.com/v1/chat/completions", headers={"Authorization": f"Bearer {api_key}"}, json={ "model": "deepseek-chat", "messages": [ {"role": "system", "content": "你是资深测试设计专家,输出严格遵循用户要求的格式。"}, {"role": "user", "content": prompt}, ], "temperature": temperature, "top_p": top_p, "max_tokens": max_tokens, "stream": False, }, timeout=120, ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"]逻辑说明:这个函数把 API key、模型名、角色设定都集中在一处,后面所有生成任务都复用它。messages里 system 消息写角色约束,user 消息写实际任务,这是 chat completions 接口的标准用法。timeout=120很重要,长 prompt 加 4000 token 的输出可能跑四五十秒,默认超时太短会误判失败。参数怎么改,要分场景说。temperature 在 0.2 到 0.4 之间,是我试过比较稳的区间。拉到 0.8 以上,模型会开始发挥,把“登录密码错误提示”编成“短信验证码错误提示”,用例长得漂亮但内容不可信。top_p 保持 0.9 就行,如果需求文本里专有名词多、模型容易联想,降到 0.85 能少一点幻觉。max_tokens 别抠门,一条完整用例平均 200 token,十条带前置条件和步骤说明就要 2000 以上,我一般给到 4000,配合后面要讲的分批生成,不会截断。2.2 从需求文本到用例集:两段式 Prompt 模板设计直接把整份需求文档塞给模型,要求“生成 100 条测试用例”,效果很差。原因有两个:需求很长时,业务规则被埋在一堆背景描述里,模型抓不住重点;其次,一次性生成太多条,容易出现前面的数据定义到后面就变了。我一般用两段式 Prompt,把“理解需求”和“生成用例”分开,这也是项目里最值得抄的设计:REQ_EXTRACT_PROMPT = """ 你是测试需求分析器。阅读下面的原始需求,提取并输出 JSON,不要输出其他内容: { "business_flows": [{"name": "", "steps": []}], "business_rules": [{"condition": "", "expected": ""}], "error_scenes": [{"trigger": "", "expected": ""}], "permissions": [{"role": "", "allowed_ops": []}] } 原始需求: {requirement_text} """ CASE_GEN_PROMPT = """ 你是自动化测试用例设计器。基于结构化需求,为每个业务规则生成测试用例。 输出必须是 JSON 数组,每个用例对象格式: { "id": "TC_001", "title": "", "precondition": "", "steps": [], "test_data": {}, "expected": "", "priority": "P0|P1|P2", "type": "positive|negative|boundary|permission|combo" } 要求:每个业务规则至少 2 条正例、1 条反例;边界值必须单独成条; 只能使用结构化需求里出现的操作和字段,不得新增任何元素名。 结构化需求: {structured_json} """逻辑说明:第一段让模型把自然语言需求压成结构化 JSON,相当于先做一轮信息抽取;第二段再基于这个 JSON 派生用例。这么拆有两个实际好处。一是需求文本两万字时,第一段先压缩成几千 token 的关键规则,第二段不需要再背完整上下文,token 成本直接降一截。二是中间的结构化 JSON 可以被开发同学 review,业务规则错了直接改 JSON 再重跑第二段,不用把第一段也重新生成一遍。这里几个参数和写法值得注意。第一段 temperature 可以给到 0.4,信息抽取需要一点归纳能力;第二段必须压到 0.2,用例设计阶段要的是稳定而不是发散。两段式还会遇到一个新问题:第二段生成的结果可能不是合法 JSON。先别急着骂模型,这是必然现象,后面第 5 章专门讲怎么兜底。常见误区是有人把 few-shot 示例塞在系统消息里而不是用户消息尾部,导致模型模仿了示例的格式却忽略了任务要求;我一般把示例直接写进 CASE_GEN_PROMPT 的结构说明里,让模型照着 JSON schema 填,比单纯说“请生成 JSON”稳得多。两段式串起来的调度逻辑很简单,核心就是先调一次提取,再调一次生成:import json def generate_case_set(requirement_text: str): structured = call_deepseek( REQ_EXTRACT_PROMPT.format(requirement_text=requirement_text), temperature=0.4, ) cases_text = call_deepseek( CASE_GEN_PROMPT.format(structured_json=structured), temperature=0.2, ) # 这里先直接解析,真实项目要套容错解析,见第 5 章 return json.loads(cases_text)这个函数看起来短,但它是整个智能生成方案的心脏。后面要加覆盖增强、去重、反馈迭代,都是在这个调用链路上挂新逻辑。3. 搭一个最小可运行的用例生成管线:从需求文档到 JSON 用例有了 Prompt 模板,接下来要做的是把它变成一条能反复使用的生成管线:吃进一份需求文档,吐出一批可执行的结构化用例。第 2 章讲的是单次生成,这一章讲的是工程化。目标是让项目里任何一个测试同学都能跑,而不是只有写脚本的人能玩。3.1 项目结构与核心脚本我习惯的最小目录长这样,四个文件,不引入任何测试框架依赖:case_generator/ ├── config.py # API key、模型名、参数默认值 ├── deepseek_client.py # 封装 call_deepseek 和重试 ├── generator.py # 两段式生成主逻辑 └── run_pipeline.py # 命令行入口,串起整个流程这个结构的选型理由很简单:配置、客户端、业务逻辑、入口四层分离。config.py 里放环境变量读取,deepseek_client.py 放网络调用和重试,generator.py 放 Prompt 模板和组装逻辑,run_pipeline.py 只负责命令行参数解析和流程编排。等后面要接 pytest 或者企业微信通知,只改入口层,不动生成逻辑。run_pipeline.py 的核心入口用 argparse 接收参数,这是最小可用的命令行设计:import argparse import json import os from generator import generate_case_set def load_requirement(path: str) -> str: with open(path, "r", encoding="utf-8") as f: return f.read() def save_jsonl(cases: list, output_path: str): with open(output_path, "w", encoding="utf-8") as f: for case in cases: f.write(json.dumps(case, ensure_ascii=False) + "\n") def main(): parser = argparse.ArgumentParser(description="DeepSeek 智能用例生成管线") parser.add_argument("--input", required=True, help="需求文档路径,支持 txt/md") parser.add_argument("--output", default="cases.jsonl", help="输出文件路径") parser.add_argument("--batch-size", type=int, default=10, help="每批生成用例数量上限") args = parser.parse_args() requirement = load_requirement(args.input) cases = generate_case_set(requirement) save_jsonl(cases, args.output) print(f"生成完成:{len(cases)} 条用例 -> {args.output}") if __name__ == "__main__": main()运行方式就一行:export DEEPSEEK_API_KEY=你的key python run_pipeline.py --input docs/订单需求.md --output cases/订单需求.jsonl逻辑说明:入口脚本不直接调模型,而是调用 generator 里的 generate_case_set,后面换参数、加去重都不影响命令行。jsonl 输出是我刻意选的,不用一个大 JSON 文件,因为每条用例独立一行,测试框架逐行读取时哪一行出错就定位哪一行,多人同时改文件也不容易冲突。print 那行是给命令行交互看的,真实项目可以替换成日志。3.2 生成、格式校验、入库的完整流程光把用例生成出来还不够,大模型输出不可控,必须加一道格式校验闸门,不合格的用例不能进文件。常见做法是生成后先做 schema 校验,再写入 jsonl。校验函数长这样:REQUIRED_FIELDS = {"id", "title", "precondition", "steps", "expected", "priority", "type"} VALID_PRIORITY = {"P0", "P1", "P2"} VALID_TYPE = {"positive", "negative", "boundary", "permission", "combo"} def validate_case(case: dict) -> list: errors = [] missing = REQUIRED_FIELDS - case.keys() if missing: errors.append(f"缺少字段: {missing}") if not isinstance(case.get("steps"), list) or not case.get("steps"): errors.append("steps 必须是非空数组") if case.get("priority") not in VALID_PRIORITY: errors.append(f"priority 非法: {case.get('priority')}") if case.get("type") not in VALID_TYPE: errors.append(f"type 非法: {case.get('type')}") return errors def validate_batch(cases: list) -> tuple: valid, invalid = [], [] for i, case in enumerate(cases): errs = validate_case(case) if errs: case["_error"] = errs invalid.append(case) else: valid.append(case) return valid, invalid参数说明:REQUIRED_FIELDS 集合定义了用例的字段契约,这是为了和自动化测试框架对齐。priority 和 type 用枚举校验,防止模型输出“高中低”这种我们框架不认的写法。validate_batch 返回两个列表,而不是直接抛异常,是为了让流程继续处理合法用例,同时把不合法的单独落盘,供人工 review。这一步看起来多余,但它能拦截掉大模型至少三成的“长得像用例但没法执行”的输出。完整流程还缺一个:需求文档往往很长,一次生成 50 条用例容易截断。所以 generate_case_set 内部要按 batch_size 拆,每批只让模型生成 10 条左右。拆批不是简单切片,而是让模型每批聚焦一个业务规则,避免上下文被冲散。调整第 2 章的 generate_case_set,让它接受 batch_size,并且对结构化 JSON 里的 business_rules 循环生成:def generate_case_set(requirement_text: str, batch_size: int = 10): structured_text = call_deepseek( REQ_EXTRACT_PROMPT.format(requirement_text=require文本前2000字) ) structured = json.loads(structured_text) rules = structured["business_rules"] all_cases = [] for i in range(0, len(rules), batch_size): batch = rules[i:i + batch_size] batch_json = json.dumps({"batch_index": i // batch_size, "rules": batch}, ensure_ascii=False) cases_text = call_deepseek( CASE_GEN_PROMPT.format(structured_json=batch_json) ) cases = parse_cases(cases_text) all_cases.extend(cases) return all_cases逻辑说明:外层循环按业务规则分批,每批的 prompt 里只放当前这批规则。这样做的好处是每批的上下文小、模型聚焦,坏处是调用次数变多、耗时增加。batch_size 的设置我一般压在 5 到 10 条规则之间,超过 15 条,输出质量和稳定性都会下降。如果你遇到“第一批很好,第二批开始重复”的现象,优先把 batch_size 调小,而不是去调 temperature。第 2 章生成的是单批用例,这一章把它变成循环调度、批量校验、落盘的标准管线。到这里,你已经可以用一份需求文档跑出一份 jsonl 用例文件了。但实测跑下来你会发现,用例是有了,覆盖经常偏科,正例一抓一大把,反例和边界值少得可怜。这是下一章要解决的问题。4. 覆盖增强与去重策略:不只要生成,还要生成得稳用例数量多不等于质量好。直接用两段式流程跑出来的用例集,最常见的问题是覆盖集中:模型默认先写正常流程,反例写几条意思一下,边界值基本靠运气。这章讲两个手段:一个是在生成侧做覆盖增强,按维度拆解而不是一次大而全;另一个是在校验侧做去重和合并,把模型爱说车轱辘话的毛病治掉。4.1 覆盖增强:按维度拆解需求,不靠一次大 Prompt我用的方法很朴素:把用例按维度拆成五类,每个维度单独生成,单独评估覆盖情况。五个维度是 positive 正常流程、negative 异常流程、boundary 边界值、permission 权限、combo 多条件组合。每个维度给不同的数量和指令,temperature 也可以不一样。DIMENSIONS = { "positive": { "count": 5, "instruction": "覆盖主流程和关键分支流程,每个流程至少一条用例,步骤必须完整。", "temperature": 0.2, }, "negative": { "count": 5, "instruction": "覆盖非法输入、异常操作、依赖服务不可用、中间步骤失败等场景。", "temperature": 0.2, }, "boundary": { "count": 3, "instruction": "覆盖边界上下限、长度限制、分页边界、数值精度边界。", "temperature": 0.2, }, "permission": { "count": 3, "instruction": "覆盖无权限、部分权限、越权访问、角色切换后的权限变化。", "temperature": 0.3, }, "combo": { "count": 2, "instruction": "覆盖多个边界条件同时出现、多个异常场景叠加的组合用例。", "temperature": 0.3, }, } def generate_by_dimensions(structured_json: str) -> list: all_cases = [] for dim, conf in DIMENSIONS.items(): prompt = f""" 你是自动化测试用例设计器。 针对维度“{dim}”生成测试用例,数量 {conf['count']} 条。 {conf['instruction']} 输出 JSON 数组,字段格式不变。 结构化需求: {structured_json} """ cases_text = call_deepseek(prompt, temperature=conf["temperature"]) all_cases.extend(parse_cases(cases_text)) return all_cases逻辑说明:把一次大生成拆成五次小生成,单次返回更稳定,每个维度还能单独肉眼检查。temperature 设置上,positive 和 boundary 用低温度保证步骤准确,permission 和 combo 用 0.3 稍微放一点发散,因为权限组合和条件组合需要模型多做一点“排列组合”。count 的配比可以根据业务调整,我上面的 5/5/3/3/2 是针对订单类接口的考古调法,如果你的系统登录相关功能多,permission 的占比可以提到 5。这里有个关键点:维度拆分之后,结构化需求 JSON 不需要重新抽取,一次抽取结果能喂给五个维度,省了重复 token。这也正是第 2 章两段式设计的收益。实际跑的时候,你会看到 negative 维度经常生成一些“网络断开”“数据库超时”这类依赖注入的场景,这些用例在接口测试里能直接模拟,在 UI 自动化里执行起来成本很高。所以我会在 negative 的 instruction 里加一句“优先选择可模拟的异常,如接口返回错误码、字段非法”,把玄学场景挡在外面。4.2 去重与合并:相似度判重和路径冲突处理维度拆完,下一步是去重。你跑几轮之后会发现,模型特别喜欢用不同措辞描述同一个操作,“点击登录按钮”和“按下登录键”意思完全一样。去重不能只靠完全匹配字符串,得算相似度。轻量方案不引入向量库,用步骤序列的 Jaccard 相似度就够:import re def normalize_step(step: str) -> str: """归一化:替换同义词、去掉标点空白,降低无关干扰""" step = step.replace("登录", "登录").replace("登陆", "登录") step = re.sub(r"点击|按下|点一下", "点击", step) step = re.sub(r"[\s,。!?、,.:;]", "", step) return step def jaccard_similarity(steps_a: list, steps_b: list) -> float: if not steps_a or not steps_b: return 0.0 set_a = set(normalize_step(s) for s in steps_a) set_b = set(normalize_step(s) for s in steps_b) inter = len(set_a & set_b) union = len(set_a | set_b) return inter / union if union else 0.0 def dedup_cases(cases: list, threshold: float = 0.85) -> list: result = [] for case in cases: steps = case.get("steps", []) is_dup = False for existing in result: if jaccard_similarity(steps, existing["steps"]) >= threshold: is_dup = True break if not is_dup: result.append(case) return result逻辑说明:Jaccard 计算的是“两个步骤集合的交集占比”,值越接近 1 越相似。threshold 取 0.85,意思是有八成五的步骤是重合的就认为是重复用例。这个阈值对 10 步以上的用例比较稳,步骤太少时容易误判,比如一个三步的正例和一个三步的反例,步骤完全一样,期望结果不一样,Jaccard 会打出 1.0 的分数,但它们明显不该被删。所以判重只看 steps,但最终补偿要结合 expected。对策是加一个合并函数:steps 完全一样、expected 不同的用例不该删,而应该合成一条参数化用例。示例:def merge_by_expected(cases: list) -> list: groups = {} for case in cases: key = tuple(case["steps"]) prep = case.get("precondition", "") key = (prep, key) groups.setdefault(key, []).append(case) merged = [] for (prep, steps), group in groups.items(): base = group[0] extra_expected = [c["expected"] for c in group[1:] if c["expected"] != base["expected"]] if extra_expected: base["expected"] = base["expected"] + ";" + ";".join(extra_expected) base["type"] = "combo" merged.append(base) return merged参数说明:合并时把 precondition 也放进 key,关键原因:同样的步骤在“已登录”和“未登录”前置条件下是完全不同的两条用例,不能合并。合并后把 type 改成 combo,是为了让框架执行时知道这条用例有多组期望结果,需要参数化跑。这个细节容易踩坑,我见过有人合并后只保留第一个 expected,结果反例被正例吞掉,看起来用例数少了,实际是测试覆盖悄悄缩水了。到这里,你已经能生成覆盖面相对均衡、无明显重复的用例集。但这套链路真正开始考验人的,是接入 API 之后的各种不稳定。下一章讲我在这条链路上踩过的五个坑,每一个都是真金白银换来的血泪经验。5. 智能用例生成避坑指南:DeepSeek API 接入中的 5 个常见问题大模型生成用例,最大的特点就是不可控。前面几章的设计都是在和不可控做对抗,但有些坑光靠 Prompt 设计绕不过去,必须在工程层兜底。这一章写五个我实际遇到的问题,每条按现象、原因、解决三步讲。5.1 响应被截断:输出长度限制与分段生成现象:生成的 JSON 到一半就断了,json.loads 直接抛 “Expecting value”。第一次遇到我还以为是温度太高模型疯了,后来看返回内容,明显是 max_tokens 不够用。原因:一次让模型生成 40 条完整用例,每条带步骤、测试数据、期望结果,token 消耗早就超过了单次返回上限。模型不是不想写完,是写到字数上限被强行截断。解决:分两层。第一层是第 3 章的 batch_size 控制,每批少生成一点;第二层是写一个“检测残缺 + 续写”的兜底函数。检测逻辑很简单:JSON 解析失败时,尝试把已生成的文本作为前缀,拼上“继续输出剩余 JSON,不要重复已输出内容”再调一次:def generate_complete_cases(prompt, max_retries=2): text = call_deepseek(prompt) for _ in range(max_retries): try: return json.loads(text) except json.JSONDecodeError: text = call_deepseek( "继续输出上一条 JSON 的剩余部分,只输出 JSON 数组内容,不要复述前面内容:\n" + text[-500:] ) raise RuntimeError("连续续写失败,请减小 batch_size")参数说明:续写时补上text[-500:]是为了让模型知道从哪里接着写,但只给末尾 500 字符,避免把整段重复内容再算一遍 token。这个兜底能救回一部分截断,但不是万能的,如果连续两次续写都失败,最有效的做法是改小 batch_size,而不是继续硬凹。5.2 幻觉用例:不存在的按钮和数据现象:生成的用例里出现需求文档里根本没有的功能,比如订单系统里冒出一个“一键迁移订单”,操作步骤里引用了一个“订单状态重置按钮”,实际上是系统里不存在的东西。原因:需求文本没有给出页面元素清单和字段范围,模型在开放性生成时用训练数据里的常识补全了“该有的功能”。这是大模型幻觉在测试场景中的典型表现,不是模型坏了,而是你给它的约束不够。解决:在 Prompt 里强制加入“可用操作清单”和“字段范围表”,并明确写“只能使用以下元素”。具体做法是把结构化 JSON 里增加一个 allowed_ops 字段,把系统里真实存在的接口、按钮、字段名列进去。格式类似:"allowed_ops": ["创建订单", "取消订单", "查询订单", "修改收货地址"], "field_ranges": {"price": "0.01-999999.99", "coupon_id": "数字,可空"}然后在 CASE_GEN_PROMPT 里把“不得新增任何元素名”这句话变成硬约束,放在用户消息末尾,而不是埋在系统消息里。模型对近处指令的遵从度明显高于远处的声明。这算是我踩坑换来的经验:靠 Prompt 中间一句“请使用正确的元素”是不够的,元素白名单本身必须进结构化 JSON。5.3 延迟与重试:超时、限流、退避策略现象:几十个用例批量生成时,偶尔会有单次请求 pending 四五十秒不返回,或者 HTTP 429。跑一次全量生成,总有几条因为超时失败,整个脚本中断。原因:长 prompt 加 4000 token 输出,单次推理时间长是正常的;并发请求一多,限流就来了。请求失败本身不意外,意外的是我们没写重试,导致一次失败拖垮整批生成。解决:必须加指数退避重试,并且区分可重试和不可重试的错误。HTTP 429、502、503 可以重试,401 认证失败、400 参数错误重试多少次都没用。下面是简化版重试逻辑:import time def call_with_retry(prompt, max_retries=3, base_delay=2.0, **kwargs): last_exc = None for attempt in range(max_retries): try: return call_deepseek(prompt, **kwargs) except requests.exceptions.HTTPError as e: status = e.response.status_code if status in (401, 400, 422): raise if attempt == max_retries - 1: raise delay = base_delay * (2 ** attempt) print(f"HTTP {status},第 {attempt + 1} 次重试,等待 {delay}s") time.sleep(delay) except requests.exceptions.Timeout: if attempt == max_retries - 1: raise time.sleep(base_delay * (2 ** attempt)) raise RuntimeError("重试失败")参数说明:base_delay=2.0 是指第一次重试等 2 秒,第二次 4 秒,第三次 8 秒。指数退避的核心是别把重试请求同时打进服务端造成雪崩。timeout 那段单独捕获,因为超时不一定真的失败了,可能只是响应慢,重试的代价比直接放弃小。我习惯把 max_retries 压到 3,重试次数太多,最后一批结果跑完了,前面的缓存已经被冲掉,工程上不值得。5.4 成本控制:Token 消耗与缓存现象:跑完一轮全量生成,打开账单发现消耗比预期多不少。一查,同一份需求文档在调试 prompt 时反复提交了十几次,每次都是全量 token。原因:成本主要烧在输入 token 上。调用一次生成,需求文本几千 token 进去,模型输出同等量级回来,输入输出各计费。调试 Prompt 时以为自己在调一句话,实际每次都在为完整需求文本付费。解决:加一层语义缓存。同一份需求文本,不管跑哪个维度,结构化结果都是一样的,没必要每次重新抽取。实际操作用需求文本的哈希值做缓存 key:import hashlib cache = {} def cached_generate_case_set(requirement_text: str): req_hash = hashlib.sha256(requirement_text.encode("utf-8")).hexdigest() if req_hash in cache: return cache[req_hash] cases = generate_case_set(requirement_text) cache[req_hash] = cases return cases参数说明:这个方案简单粗暴,进程内字典缓存,适合脚本场景。如果要落到工程级,把哈希 key 和结果存到 Redis 或本地 sqlite。重点是把结构化抽取结果单独缓存,因为它是后续所有维度生成的公共依赖,缓存一份能省下五分之四的输入 token。调试 Prompt 时,先用截断的需求片段调通格式,再跑全量,也是省钱的习惯。5.5 格式漂移:JSON 解析失败现象:模型偶尔会在 JSON 外面包一层 markdown 代码块,开头是```json,结尾是```;或者 JSON 内部多了一个逗号。这类输出用标准 json.loads 会直接失败,但内容本身是完整的。原因:模型在训练数据里见过大量代码块示例,生成时模仿了那种格式。这和模型能力关系不大,纯粹是输出风格漂移,每天都有概率出现。解决:写一个容错解析函数,第一步把文本里的 markdown 代码块标记剥掉,第二步裁剪出第一个{到最后一个}之间的内容,第三步再尝试 json.loads:def robust_json_loads(text: str): text = text.strip() if text.startswith("```"): text = re.sub(r"^```[a-zA-Z]*\\n|```$", "", text) start = text.find("{") end = text.rfind("}") if start == -1 or end == -1: raise ValueError("找不到 JSON 对象边界") candidate = text[start:end + 1] try: return json.loads(candidate) except json.JSONDecodeError: candidate = candidate.rstrip(",") candidate = re.sub(r",\\s*([}\\]])", r"\\1", candidate) return json.loads(candidate)逻辑说明:正则把,后面紧跟右括号或中括号的多余逗号去掉,处理的是模型最爱犯的“最后一个元素后面多逗号”问题。这个方法不是银弹,如果模型输出中途丢失字段,裁剪也没用,会走到 ValueError。所以 robust_json_loads 要放在前面 5.1 的续写逻辑之前:先做格式清洗,清洗不掉再续写,最后还不行就丢弃这条结果并记录日志。丢几条用例不可怕,可怕的是整个流程因为一条脏数据中断。这五个问题基本覆盖了 API 接入层的主要雷区。解决了它们,生成链路才算真正稳定。最后一章把这批用例接进自动化测试框架,形成一个能迭代的闭环。6. 把生成的用例接进自动化测试框架:适配层与反馈闭环6.1 用例格式与框架的适配层生成的 JSON 用例是数据,不是可执行脚本。接进 pytest 这类框架时,要写一个适配层,把用例字典翻译成可调用的测试函数。我的做法是做一个 builder:def build_test_function(case: dict): def test_case(): for step in case["steps"]: # 每个 step 映射到 API 调用、断言或页面动作 run_step(step, case.get("test_data", {})) assert check_expected(case["expected"]) return test_case逻辑说明:适配层不关心用例是怎么生成的,只关心 steps 和 expected。这样生成管线换模型、换 Prompt 都不会污染测试框架。test_data 单独传是因为同一个步骤在参数化用例里会有多组数据,拆开更干净。6.2 用上一轮的失败反馈做第二轮生成把执行结果做成反馈,喂回给模型,生成回归用例或修正原用例。这是我的习惯:每条失败用例跑完,把断言错误摘要和对应步骤拼成反馈 prompt,让模型补一条“针对该失败场景的回归用例”。跑完第一轮只是开始,真正让用例集越用越准的,是这个反馈回路。REGRESSION_PROMPT = """ 以下是执行失败的用例: {case_json} 失败信息: {error_summary} 请生成一条针对该失败场景的回归用例,要求覆盖相同路径但增加断言强度。 """时间久了你会养成一个习惯:每次跑完测试,先存一份失败摘要到固定目录,下一轮生成前把它作为上下文传进去。不要嫌麻烦,这个动作能让用例集从“通用”慢慢长出“业务敏感”。希望帮到你。本文还有配套的精品资源,点击获取