1. 项目概述当AI智能体遇上测试驱动开发最近在GitHub上看到一个挺有意思的项目叫agent-skill-tdd来自Shelpuk-AI-Technology-Consulting。光看这个名字就让我这个在软件工程和AI应用领域摸爬滚打了十几年的人眼前一亮。它把两个看似不直接相关的概念——“AI智能体技能”和“测试驱动开发”——给拧到了一块儿。这可不是简单的技术堆叠背后反映的是当前AI应用开发特别是基于大语言模型的智能体开发正在从“玩具”和“演示”走向“工程化”和“生产化”的必然趋势。简单来说这个项目探讨的核心问题是我们如何像开发传统软件一样用严谨、可重复、自动化的方式来开发、验证和维护AI智能体的“技能”这里的“技能”可以理解为智能体能够执行的一个个具体任务单元比如解析用户指令、调用特定API、处理结构化数据、生成特定格式的回复等。而TDD即测试驱动开发是一种先写测试用例再编写实现代码通过测试来驱动设计和保障质量的经典软件开发实践。把TDD引入AI技能开发想法非常大胆也直击痛点。传统软件开发中函数的输入输出相对确定逻辑可追溯TDD玩得转。但AI技能尤其是基于大模型的技能其输出具有概率性和一定的不可预测性同样的输入可能产生不同的输出虽然核心语义一致。那么为这样的“黑盒”或“灰盒”组件写测试到底测什么怎么测agent-skill-tdd这个项目就是在尝试回答这些问题。它瞄准的是那些希望构建可靠、可维护、能持续集成/持续部署的AI应用系统的开发者、架构师和技术负责人。2. 核心理念与架构设计拆解2.1 为什么AI技能需要TDD要理解这个项目首先要跳出“AI模型训练”的思维进入“AI应用工程”的领域。当我们基于大模型构建一个客服智能体、一个编码助手或一个数据分析工具时这个智能体是由多个“技能”模块组装而成的。例如一个客服智能体可能包含“意图识别”、“知识库检索”、“多轮对话管理”、“工单创建”等技能。这些技能的开发如果沿用早期AI应用“手工作坊”的模式会面临诸多挑战质量不可控技能的效果严重依赖开发者的临时测试和“感觉”没有客观的、自动化的质量标尺。回归困难当底层模型升级、提示词优化、或依赖的API变更时我们很难快速、全面地评估这些改动对所有已有技能的影响容易引发“按下葫芦浮起瓢”的问题。协作成本高在团队开发中如何清晰地定义某个技能的验收标准如何让其他成员能放心地复用或修改你的技能缺乏契约化的测试沟通成本巨大。交付信心不足无法建立自动化的测试流水线每次上线都像一次冒险阻碍了快速迭代和持续交付。TDD的核心理念——“红-绿-重构”循环——在这里被赋予了新的内涵。“红”阶段我们不是为一段还不存在的代码写测试而是为一个尚未被满足的、对AI技能的“功能预期”编写可执行的验收用例。“绿”阶段我们通过设计提示词、配置模型参数、编写后处理逻辑等方式让技能的实现能够通过测试。“重构”阶段我们优化提示词的结构、提取通用模板、改善处理逻辑同时确保测试依然全部通过。2.2 项目核心架构猜想虽然无法看到项目的全部源码但根据其命名和领域我们可以推断其架构设计必然围绕以下几个核心组件展开技能抽象层定义一个统一的“技能”接口。一个技能可能接收一些上下文如对话历史、用户信息和输入参数返回一个结构化的结果或一个后续动作。这个接口屏蔽了底层是调用大模型、规则引擎还是混合实现。测试框架适配层这是项目的关键创新点。它需要将传统的单元测试框架如Python的pytest、unittest进行扩展使其能够适应AI技能测试的特殊性。断言机制的扩展不能简单用assert output “expected answer”。需要支持语义相似度断言、JSON结构验证、关键信息提取验证、甚至基于另一个AI模型轻量级进行结果评估的断言。测试夹具管理提供便捷的方式管理测试用的“提示词模板”、“示例对话”、“模拟API响应”等。非确定性处理提供机制来处理AI输出的非确定性例如设置随机种子、进行多次采样取共识、或允许一个“可接受”的结果集合。技能实现与运行环境提供技能的具体实现载体。这可能是一个类、一个函数或者一个配置文件。项目需要集成大模型调用SDK如OpenAI, Anthropic或本地模型库并提供技能运行时所需的上下文管理、工具调用等支持。测试用例集这是体现TDD思想的具体成果。每个技能对应一个测试文件里面包含了从简单到复杂、从正向到负向的各种用例。这些用例共同构成了该技能的“契约”和“活文档”。一个简化的概念性数据流可能是测试用例-测试运行器-调用技能实现-获取AI输出-经过扩展的断言库进行验证-输出测试报告。注意这种架构下测试用例本身可能也是一种“数据”它们定义了技能的预期行为边界。维护好测试用例集其价值有时甚至高于技能的实现代码因为它是业务需求最直接的体现。3. 关键实现技术与实操要点3.1 如何为“非确定性”输出编写测试这是AI技能TDD面临的首要技术挑战。我们不能要求AI每次生成一字不差的答案但必须保证其答案在“功能”上是正确的。项目需要提供一套强大的断言工具。1. 语义相似度断言这是最基础也是最重要的。使用句子嵌入模型如Sentence-BERT计算生成文本与期望文本的余弦相似度设定一个阈值如0.85。# 伪代码示例 def test_answer_question(): skill QuestionAnsweringSkill() context “...” question “项目的主要目标是什么” output skill.execute(context, question) expected_semantic “为AI技能引入测试驱动开发实践” # 使用项目提供的语义断言 assert semantic_similarity(output, expected_semantic) 0.82这里的阈值0.82不是随便定的需要通过历史数据验证来确定一个既能保证质量又不至于太严苛导致测试不稳定的值。2. 结构化数据验证很多技能的输出应该是结构化的比如JSON。我们可以验证其Schema。def test_extract_contact_info(): skill InfoExtractionSkill() text “请联系张三邮箱zhangsanemail.com电话13800138000。” output skill.execute(text) # 验证输出是JSON且包含特定字段 assert isinstance(output, dict) assert “name” in output and output[“name”] “张三” assert “email” in output and “” in output[“email”] # 使用正则表达式验证电话格式 import re assert re.match(r’^1[3-9]\d{9}$‘ output.get(“phone”, “”))3. 关键信息点断言不关心全文只关心是否包含了几个必需的信息点。def test_generate_meeting_summary(): skill SummarySkill() transcript “...长达一小时的会议记录...” output skill.execute(transcript) required_key_points [“决策事项” “负责人” “截止日期”] for point in required_key_points: assert point in output, f“总结中缺少关键信息‘{point}’”4. 基于LLM的评估断言元评估对于非常复杂或主观的输出可以用一个更轻量、更便宜的LLM如小型开源模型来评估主技能的输出是否合理。这相当于请了一个“AI测试员”。def test_creative_writing(): skill CreativeWritingSkill() prompt “写一个关于机器人的短故事开头。” output skill.execute(prompt) # 调用评估技能 eval_skill LLMEvaluationSkill() evaluation eval_skill.execute( instruction“评估以下故事开头是否有趣、包含机器人元素、语法正确。只需回答‘是’或‘否’。”, textoutput ) assert “是” in evaluation.lower()实操心得在实际项目中我们通常会混合使用多种断言方式。对于一个技能先确保其输出结构正确Schema验证再确保核心信息无误关键点断言最后对自由文本部分进行语义校验。一开始阈值可以设低一些先让流程跑通再随着技能优化逐步提高标准。切忌一开始就追求完美的、严格的文本匹配那会让自己陷入无穷无尽的测试调试中。3.2 测试数据与提示词的管理TDD中测试数据的设计至关重要。对于AI技能测试数据主要分为两部分输入数据和期望的提示词。1. 输入数据的构造典型场景用例覆盖技能设计要处理的主要用户场景。边界用例输入为空、极长、包含特殊字符、模糊歧义等情况测试技能的鲁棒性。对抗性用例故意输入一些诱导性、误导性或带有偏见的文本测试技能的安全性和稳定性。 建议将测试用例按类别组织并使用参数化测试功能避免代码重复。2. 提示词作为测试的一部分在AI技能开发中提示词Prompt就是“代码”的一部分。因此测试用例也应该对提示词的变化敏感。但这带来一个难题如果优化提示词后所有测试用例都要改期望值成本太高。 一个实用的方法是将提示词模板化并将模板本身纳入版本控制。测试时使用固定的模板版本。当需要修改提示词时相当于一次“代码重构”你需要评估新提示词在原有测试用例上的表现并更新那些因提示词优化而预期结果发生合理改变的测试用例。对于那些只是表达更优但语义不变的输出应使用语义断言从而减少不必要的测试更新。3. 模拟Mock外部依赖AI技能常常需要调用外部API如数据库、搜索引擎、第三方服务。在单元测试中必须将这些依赖模拟掉以保证测试的独立性和速度。项目框架应提供便捷的Mock机制。# 假设一个技能需要调用天气API def test_weather_recommendation(mocker): # 假设使用pytest-mock # 1. Mock掉技能内部使用的天气客户端 mock_client mocker.patch(‘skills.weather.WeatherClient.get_temperature’) mock_client.return_value {“temp”: 25 “condition”: “sunny”} # 2. 执行技能 skill OutfitRecommendationSkill() output skill.execute(city“北京”) # 3. 验证技能根据模拟的25度晴天给出了正确推荐 assert “轻薄” in output or “短袖” in output # 4. 也可以验证技能是否以正确的参数调用了API mock_client.assert_called_once_with(“北京”)4. 完整的TDD工作流实践让我们以一个具体的例子——“会议纪要生成技能”来走一遍完整的AI技能TDD循环。4.1 第一步红——编写一个失败的测试我们首先定义一个技能的目标输入一段会议录音转写的文本输出一个包含“会议主题”、“参会人”、“关键决议”、“待办事项”的结构化摘要。我们先不写任何实现代码而是创建一个测试文件test_meeting_summarizer.pyimport pytest from skills.meeting_summarizer import MeetingSummarizationSkill class TestMeetingSummarizationSkill: pytest.fixture def skill(self): return MeetingSummarizationSkill() def test_summarize_short_meeting(self, skill): “””测试对简短会议内容的摘要提取。””” transcript “”” 王总今天我们主要讨论Q3的营销预算。我建议增加数字渠道投入。 李经理我同意但需要具体数据支持。小张你下周能出个方案吗 小张没问题我下周三前给初稿。 王总好那暂定增加20%预算等小张方案。散会。 “”” result skill.execute(transcript) # 断言结果是一个字典 assert isinstance(result, dict) # 断言包含必需的顶级字段 assert all (key in result for key in [“topic” “attendees” “key_decisions” “action_items”]) # 断言会议主题包含“营销预算” assert “营销预算” in result[“topic”] # 断言参会人名单正确允许不全但必须有主要人物 assert “王总” in result[“attendees”] # 断言关键决议包含“增加预算” assert any(“增加” in decision and “预算” in decision for decision in result[“key_decisions”]) # 断言待办事项包含“小张”和“方案” assert any(“小张” in item and “方案” in item for item in result[“action_items”])运行这个测试毫无疑问会失败因为MeetingSummarizationSkill这个类还不存在。这就是“红”的状态。4.2 第二步绿——实现最简单的技能通过测试现在我们创建技能的最小实现。为了最快通过测试我们甚至可以先“硬编码”。# skills/meeting_summarizer.py class MeetingSummarizationSkill: def execute(self, transcript: str) - dict: # 这是一个最简陋的、仅仅为了通过测试的实现 return { “topic”: “营销预算” “attendees”: [“王总” “李经理” “小张”], “key_decisions”: [“增加20%预算”], “action_items”: [“小张下周三前出营销方案初稿”] }再次运行测试通过了这就是“绿”的状态。虽然这个实现毫无智能但它让我们建立了测试与代码之间的连接定义清楚了技能的接口和预期输出格式。4.3 第三步重构与迭代——引入真正的AI能力现在我们替换掉硬编码用真正的LLM调用和提示词工程来实现这个技能。1. 设计提示词模板我们在一个配置文件或模板文件中定义提示词{ # skills/prompts/meeting_summary.jinja2 # } 你是一个专业的会议秘书请根据下面的会议转录文本生成一份结构化的会议纪要。 转录文本 {{ transcript }} 请严格按照以下JSON格式输出不要有任何额外的解释 { “topic”: “会议的核心主题一句话概括” “attendees”: [“参会人1” “参会人2” ...], “key_decisions”: [“决议1” “决议2” ...], “action_items”: [“待办事项1负责人xxx 截止时间yyy” ...] }2. 实现技能类# skills/meeting_summarizer.py import json from jinja2 import Template from llm_client import LLMClient # 假设有一个封装好的LLM客户端 class MeetingSummarizationSkill: def __init__(self, llm_client: LLMClient): self.llm_client llm_client with open(‘skills/prompts/meeting_summary.jinja2’ ‘r’ encoding‘utf-8’) as f: self.prompt_template Template(f.read()) def execute(self, transcript: str) - dict: # 渲染提示词 prompt self.prompt_template.render(transcripttranscript) # 调用LLM raw_response self.llm_client.complete(prompt, temperature0.1) # 低随机性保证输出稳定 # 解析JSON响应 try: result json.loads(raw_response) except json.JSONDecodeError: # 如果解析失败可以加入一些后处理逻辑比如尝试提取JSON部分 # 这里简单抛出一个错误测试会失败驱动我们改进提示词或后处理 raise ValueError(f“LLM返回的不是有效JSON: {raw_response}”) return result3. 更新测试以注入Mock的LLM客户端我们需要修改测试提供一个模拟的LLM客户端让它返回我们期望的JSON字符串。# test_meeting_summarizer.py (更新版) import pytest from unittest.mock import Mock from skills.meeting_summarizer import MeetingSummarizationSkill class TestMeetingSummarizationSkill: pytest.fixture def mock_llm(self): # 创建一个模拟的LLM客户端 mock Mock() # 配置它返回一个符合我们提示词要求的JSON字符串 mock.complete.return_value “””{ “topic”: “关于Q3营销预算的讨论” “attendees”: [“王总” “李经理” “小张”], “key_decisions”: [“原则同意增加数字渠道营销预算” “具体增幅待方案确定”], “action_items”: [“小张负责在下周三前完成营销方案初稿负责人小张 截止时间下周三”] }“”” return mock pytest.fixture def skill(self, mock_llm): # 将模拟的客户端注入技能 return MeetingSummarizationSkill(llm_clientmock_llm) def test_summarize_short_meeting(self, skill): # ... 测试体不变但现在技能会调用模拟的LLM并返回我们预设的JSON # ... 运行测试应该通过现在测试依然通过但我们的技能已经具备了真实的AI调用能力尽管在测试中被Mock了。我们完成了第一次重构。4.4 第四步扩展测试用例驱动技能增强现有的技能还很脆弱。比如如果会议转录文本很长怎么办如果LLM返回的JSON格式稍有偏差怎么办我们通过添加更多测试来驱动改进。1. 添加处理长文本的测试def test_summarize_long_meeting(self, skill, mock_llm): # 模拟一个很长的转录文本 long_transcript “...” # 很长的文本 # 我们可以配置mock根据不同的输入返回不同的值 # 这里简化处理假设mock已配置好 result skill.execute(long_transcript) # 断言即使输入很长输出结构依然稳定 assert isinstance(result, dict) assert “topic” in result # 可能还需要断言摘要的浓缩程度等这个测试可能会失败因为长文本可能超出模型上下文长度。这会驱动我们改进技能实现文本分块、摘要合并或选择具有更长上下文的模型。2. 添加鲁棒性测试处理格式错误的LLM响应def test_handles_malformed_llm_response(self, mock_llm): # 模拟LLM返回非JSON或格式错误的响应 mock_llm.complete.return_value “抱歉我无法处理这个请求。” skill MeetingSummarizationSkill(llm_clientmock_llm) transcript “一些测试内容” # 我们期望技能能优雅地处理这种错误而不是崩溃 # 可能返回一个错误标识或一个默认的空结构 result skill.execute(transcript) # 断言技能没有抛出异常并返回了一个可识别的错误状态或安全值 assert “error” in result or result {}这个测试会失败驱动我们在execute方法中添加更健壮的异常处理和错误恢复逻辑。通过这样一个“红-绿-重构-扩展”的循环我们就能像打磨传统软件一样逐步构建出健壮、可靠的AI技能。每一个测试用例都代表了产品需求的一个切片整个测试套件共同确保了技能的质量边界。5. 工程化集成与团队协作实践将AI技能TDD融入真实的工程开发流程才能最大化其价值。这涉及到CI/CD、代码组织和团队协作规范。5.1 目录结构规划一个清晰的项目结构有助于管理技能和测试。agent-skills-project/ ├── skills/ # 技能实现目录 │ ├── __init__.py │ ├── meeting_summarizer.py # 会议纪要技能 │ ├── email_parser.py # 邮件解析技能 │ └── prompts/ # 提示词模板目录 │ ├── meeting_summary.jinja2 │ └── email_parser.jinja2 ├── tests/ # 测试目录 │ ├── __init__.py │ ├── conftest.py # 全局测试配置如公共fixture │ ├── test_meeting_summarizer.py │ └── test_email_parser.py ├── llm_client.py # 封装的LLM调用客户端 ├── config.yaml # 配置文件API密钥、模型选择等 ├── requirements.txt # 项目依赖 └── .github/workflows/ # CI/CD流水线 └── test.yml5.2 CI/CD流水线配置在.github/workflows/test.yml中配置自动化测试每次提交或PR时自动运行。name: AI Skills Tests on: [push pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: ‘3.10’ - name: Install dependencies run: | pip install -r requirements.txt pip install pytest pytest-mock - name: Run unit tests (fast) run: | pytest tests/ -v --tbshort env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # 如果测试需要真实API调用非Mock使用加密密钥 # 可以添加一个使用小型/廉价模型进行集成测试的步骤作为单元测试的补充实操心得在CI中默认运行所有使用Mock的单元测试保证速度。可以设置一个每日或每周定时运行的“集成测试”任务使用真实的开发环境API密钥和较便宜的模型如gpt-3.5-turbo对核心技能进行小规模的真实调用以捕获因模型服务更新或提示词漂移带来的潜在问题。切记不要用生产环境的密钥或昂贵模型跑CI。5.3 团队协作与代码审查要点在团队中推行AI技能TDD代码审查的重点会发生变化审查测试用例新的技能或功能是否配备了充分的测试用例测试用例是否覆盖了主流程、边界情况和错误场景测试的断言方式精确匹配、语义相似、结构验证是否合理审查提示词提示词是否清晰、无歧义是否遵循了团队约定的模板规范是否包含了必要的系统指令如输出格式限制以防止提示词注入审查技能实现除了代码逻辑更要关注技能对LLM响应的后处理是否健壮错误处理是否完备。审查Mock策略测试中的Mock是否合理是否过度Mock导致测试失去了意义6. 常见问题、挑战与应对策略在实际推行AI技能TDD的过程中你会遇到一些特有的挑战。6.1 测试的稳定性问题Flaky Tests这是最大的挑战。由于LLM输出的非确定性即使设置了低温度temperature同一提示词在不同时间、不同批次模型下也可能产生细微差异导致语义相似度得分波动测试时而过关时而失败。应对策略降低随机性在测试环境中将LLM调用的temperature参数设为0或接近0如果模型支持并设置固定的seed。使用更宽松的断言对于非关键的自由文本使用关键信息点断言代替全文语义匹配。提高语义相似度的接受阈值容差。采样与投票对于关键测试可以让技能在测试中多次运行如3次取多数一致的结果作为最终输出进行断言。黄金标准数据集维护一个“黄金标准”测试集其中的期望输出是人工精校过的。这类测试不纳入每次CI的必跑项而是作为定期如每周的验收测试用于监控技能质量的整体漂移。标记非稳定测试对于确实难以稳定的测试可以用pytest的pytest.mark.flaky装饰器标记允许其重试几次或者将其从阻塞性测试套件中移出作为参考性测试。6.2 测试成本与速度调用真实的LLM即使是Mock进行测试可能比传统单元测试慢。Mock虽然快但过度使用会降低测试的真实性。应对策略分层测试策略单元测试层快速大量使用Mock测试技能的组装逻辑、错误处理、后处理代码。运行速度极快。集成测试层中速使用本地部署的轻量级开源模型如Phi-3 Mini Llama 3 8B或模型的轻量化API对核心技能进行真实调用。这部分测试在CI中可选或定时运行。端到端测试层慢速使用与生产环境相同的模型对完整的工作流进行测试。仅在发布前或重大变更后手动运行。响应缓存在开发阶段可以将LLM对特定提示词的响应缓存到本地文件或内存中。在测试时优先使用缓存如果没有再真实调用并更新缓存。这能极大提升开发迭代速度。但需注意缓存过期问题当提示词修改后需要清理缓存。6.3 如何测试“创造力”或“开放性”技能对于一些创意写作、头脑风暴等开放性任务其输出没有唯一正确答案传统断言方式几乎失效。应对策略测试约束条件而非内容断言输出长度在范围内、遵循了指定的格式如是一首诗、一个列表、不包含违禁词汇、情绪是积极的等。使用评估模型如前所述引入一个轻量级的“评估技能”作为测试断言的一部分。这个评估技能本身也需要经过充分的测试和校准。转向人工评估与自动化结合对于核心的开放性技能建立一个小规模的人工评估测试集。自动化测试可以运行这些用例并将结果保存下来供人工定期复审而不是在CI中自动断言通过与否。6.4 技能依赖与组合测试复杂的智能体往往由多个技能组合而成。如何测试技能间的协作应对策略技能接口契约化每个技能都有明确的输入输出规范。测试组合时可以Mock上游技能的输出作为下游技能的输入测试下游技能的处理逻辑。编排层测试单独测试负责技能调度和组合的“编排器”Orchestrator逻辑。编排器本身不包含AI调用可以用传统单元测试充分覆盖。集成场景测试针对关键的用户旅程构建小型的、部分集成的测试场景。例如用真实的“意图识别”技能但其后连接的“数据库查询”技能被Mock。这样既测试了流程又控制了复杂度。agent-skill-tdd这个项目所倡导的理念其价值远不止于一个工具库。它代表了一种思维模式的转变将AI应用开发从“炼金术”转向“工程学”。它要求开发者像重视代码一样重视提示词像编写业务逻辑一样设计测试用例。这条路刚开始走可能会觉得繁琐测试的编写和维护甚至比技能本身还费劲。但当你经历过一次因为模型版本升级导致核心技能行为异常而你的测试套件在几分钟内就精准定位到问题场景时当新同事能够通过阅读测试用例迅速理解某个技能的所有边界行为时你就会深刻体会到这种“笨功夫”所带来的长期收益——可控、可信、可协作的AI应用开发能力。这或许是AI技术真正深度融入各行各业生产流程的必经之路。