
1. “Skill”不是功能模块而是Agent世界的“原子操作单元”最近在好几个技术群里看到新人发问“我用Coze搭了个天气Bot加了‘查天气’‘设提醒’‘讲笑话’三个按钮这算不算实现了三个Skill”——答案是否定的。这种把界面按钮、API调用、甚至一段Python脚本直接冠以“Skill”之名的做法正在快速污染整个Agent开发圈的认知基线。我带过六支Agent产品团队从金融风控Agent到工业设备巡检Agent踩过最深的坑恰恰就始于对“Skill”二字的轻率定义。Skill不是功能列表里的一个条目不是前端UI上的一块按钮区域更不是后端服务里一个暴露出来的HTTP接口。它是一套有明确定义边界、可独立验证、具备语义完整性、能被Agent运行时统一调度的最小行为单元。你可以把它理解成操作系统里的“系统调用”syscallopen()、read()、write()这些不是随便起的名字它们代表内核对外暴露的、经过严格契约约束的原子能力。同理search_web(query: str) → List[Result]是Skillcall_weather_api(city: str) → dict是Skill但show_weather_card()就不是——后者是UI渲染逻辑属于Presentation Layer和Skill所在的Execution Layer根本不在同一抽象层级。为什么这个区分如此关键因为一旦混淆工程实现就会立刻崩塌。我们曾在一个政务问答Agent项目中把“生成PDF报告”封装成一个Skill结果上线后发现当用户连续发起5次请求时PDF生成服务因临时文件未清理而OOM当并发量超过80QPS时字体嵌入失败导致文档乱码更致命的是该“Skill”内部硬编码了本地路径/tmp/report/导致在K8s集群多副本部署时不同Pod写入冲突报告内容相互覆盖。问题根源不是代码写得差而是从设计第一天起就把一个依赖强状态、强IO、强环境耦合的复合操作错误地当成了无状态、可组合、可重入的Skill。真正的Skill必须满足四个刚性条件契约明确性输入参数类型、取值范围、必填项输出结构、成功/失败标识、错误码分类全部通过TypeScript Interface或OpenAPI Schema显式声明不能靠文档“约定俗成”。副作用隔离性执行过程不修改全局状态不依赖外部文件系统路径不持有长连接句柄。所有外部依赖数据库、HTTP Client必须通过Dependency Injection注入且在测试中可被Mock。幂等可重试性同一输入在相同上下文如Agent Session ID下重复执行应返回相同结果或明确的幂等响应如{status: already_done, result_id: xxx}而非抛出“记录已存在”异常。可观测可审计性每次调用必须生成结构化日志含Skill Name、Input Hash、Execution Duration、Exit Code并支持按Session ID、User ID、Skill Name三维度实时聚合分析。这四条不是理想主义的教条而是我们在生产环境用27次严重事故换来的血泪清单。比如某次电商Agent的“下单”Skill因未实现幂等性在网络抖动重试时导致用户被扣款两次某次教育Agent的“生成错题解析”Skill因未做副作用隔离将中间缓存写入共享Redis致使A学生看到B学生的解题思路。这些故障的根因90%以上都指向同一个源头把“能跑通”的代码当成了“可信赖”的Skill。提示判断一个操作是否够格成为Skill最朴素的测试法是——把它从当前Agent框架中完全剥离单独部署为一个独立微服务哪怕只是本地HTTP Server看它能否仅凭输入JSON稳定返回符合Schema的输出JSON。如果需要额外传入Session Context、User Profile、Config Map才能工作那它大概率还不是Skill而是某个Skill的“执行器”Executor。2. Skill的本质是“意图-动作”的精准映射而非代码片段的简单打包很多开发者构建Skill时习惯性打开IDE新建一个Python文件写个def get_stock_price(ticker: str)再加个skill装饰器就宣告完成。这种做法看似高效实则埋下了巨大的语义鸿沟。Skill的核心价值从来不在代码本身而在于它如何将人类自然语言意图无损、无歧义、可追溯地翻译为机器可执行的动作。这个翻译过程才是Skill工程化的真正战场。我们以一个真实案例切入某银行智能投顾Agent收到用户指令“帮我把活期账户里30%的钱转到余额宝”。表面看这只是一个转账操作但拆解其背后意图链会发现至少包含五个不可跳过的语义节点账户识别用户说的“活期账户”指代哪个是工资卡I类户还是网银绑定的II类电子账户需结合用户KYC数据与账户标签体系动态解析资产计算30%是基于当前可用余额还是基于昨日收盘后总资产是否排除冻结资金需调用实时资金引擎获取精确快照目标产品匹配“余额宝”在银行系统内对应的是“天弘基金-增利宝货币A”但用户可能用“零钱通”“理财通”等别名需建立标准化产品ID与别名词典合规校验单日转账是否超监管限额用户风险评级是否匹配该理财产品需同步调用反洗钱引擎与适当性管理服务执行确认转账前必须向用户展示预估收益、到账时间、手续费并获得显式授权非默认勾选否则违反《金融消费者权益保护实施办法》。如果把上述全部逻辑塞进一个叫transfer_to_money_market()的Skill里它就成了典型的“上帝函数”God Function职责爆炸、测试困难、无法复用、难以审计。正确的工程实践是将其拆解为五个正交的Skillresolve_account(intent: str) → AccountIdcalculate_available_balance(account_id: str) → Decimalmap_product_alias(alias: str) → FundProductIdvalidate_compliance(account_id: str, fund_id: str, amount: Decimal) → ComplianceResultexecute_transfer(account_id: str, fund_id: str, amount: Decimal) → TransferReceipt这五个Skill之间通过标准Context对象传递数据每个Skill只负责解决一个原子语义问题。当用户指令变更如“转5万元到招行朝朝宝”只需调整map_product_alias的映射规则其余Skill完全无需修改。这才是Skill作为“意图-动作映射单元”的威力所在——它让Agent的决策链路变得像乐高积木一样可插拔、可替换、可审计。那么如何确保这种映射不丢失语义关键在于引入Skill Schema Intent Parser双轨验证机制。我们强制要求每个Skill必须配套一个.skill.yaml描述文件例如transfer_to_money_market.skill.yamlname: transfer_to_money_market version: 1.2.0 description: 将指定账户资金转入货币基金产品执行前自动完成合规校验 input_schema: type: object properties: source_account_id: type: string description: 源账户唯一标识符由resolve_account Skill返回 pattern: ^ACC_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ target_fund_id: type: string description: 目标货币基金产品ID由map_product_alias Skill返回 pattern: ^FUND_[A-Z]{3,6}_\\d$ amount: type: number minimum: 0.01 maximum: 10000000.00 multipleOf: 0.01 output_schema: type: object properties: receipt_id: type: string description: 转账回执单号可用于后续查询 expected_arrival: type: string format: date-time description: 预计到账时间ISO 8601格式 fee: type: number description: 本次转账手续费元这个YAML文件不是文档而是编译期契约。Agent框架在加载Skill时会用JSON Schema Validator严格校验输入输出任何类型不符、字段缺失、数值越界都会在调用前抛出SkillContractViolationError而非让错误流入下游服务。同时Intent Parser如基于LLM的Router在分发指令前会根据此Schema反向生成Prompt模板“请从用户输入中提取source_account_id、target_fund_id、amount三个字段若任一字段缺失或模糊请返回NOT_ENOUGH_INFO”。这样从用户输入到Skill执行全程处于契约约束之下彻底杜绝“传参错位”“字段误解”这类低级但高频的故障。注意不要试图用LLM直接解析原始用户输入生成Skill调用参数。我们做过AB测试纯LLM解析的准确率在复杂场景下仅为68.3%而“LLM粗筛Schema精校验”组合方案可达99.2%。前者把信任交给黑盒后者把控制权握在自己手中。3. SKILL.md不是README而是Skill的“数字身份证”与协作协议书在开源社区和内部项目中我见过太多名为SKILL.md的文件内容不过是几行代码片段加一句“调用此函数即可”。这种文档本质上是对Skill工程化精神的背叛。真正的SKILL.md绝不是给开发者看的“怎么用”而是给Agent运行时、监控系统、审计员、安全团队、甚至未来接手的同事看的“它到底是什么、能做什么、不能做什么、出了问题怎么查”。一份合格的SKILL.md必须包含七个不可省略的模块缺一不可。我们以一个真实的generate_tax_reportSkill为例逐项说明其内容构成与设计逻辑3.1 基础元信息Machine-Readable Header--- name: generate_tax_report version: 2.1.0 author: finance-teamcompany.com license: Proprietary last_updated: 2024-05-12 schema_version: 1.0 ---这不是装饰性YAML。schema_version字段决定了Agent框架如何解析后续内容license字段在跨团队调用时触发合规检查如开源组件禁止调用闭源Skilllast_updated与Git Commit Hash绑定用于灰度发布时精准定位版本。3.2 语义定义Human-Readable Intent Mapping## 语义定义 本Skill响应以下用户意图 - “帮我导出上季度个税申报表” - “生成2024年Q1的纳税汇总报告” - “我要查看公司代扣代缴明细” **不响应以下意图** - “怎么报税”这是FAQ类问答应路由至Knowledge Base Skill - “个税起征点是多少”这是政策查询应路由至Regulation API Skill - “把报表发到邮箱”发送动作需由Notification Skill执行本Skill只生成PDF二进制流这里用正反例明确划清Skill的语义边界。我们曾因缺少“不响应”条款在税务Agent中误将政策咨询路由至此Skill导致LLM反复尝试生成不存在的政策文本最终触发Rate Limit熔断。3.3 输入输出契约Code-First Schema## 输入输出契约 ### 输入参数JSON Schema json { type: object, properties: { user_id: {type: string}, report_period: { type: string, enum: [Q1-2024, Q2-2024, H1-2024, YEAR-2023] }, format: {type: string, enum: [pdf, xlsx]} }, required: [user_id, report_period] }输出结构Protobuf Message Definitionmessage TaxReportResponse { bytes report_data 1; // PDF or XLSX binary content string report_id 2; // UUID for audit trail int32 status_code 3; // 0success, 1data_missing, 2permission_denied string error_message 4; }契约必须同时提供JSON Schema供HTTP调用和Protobuf供gRPC调用确保跨协议一致性。status_code枚举值必须与公司统一错误码中心对齐避免各Skill自定义error: not found这类模糊表述。3.4 执行约束Operational Boundaries## 执行约束 - **时效性**单次执行耗时 ≤ 8.5秒P95超时自动终止并返回status_code4TIMEOUT - **数据范围**仅处理用户本人及直系亲属配偶、未成年子女的纳税数据需通过auth_service.verify_relationship()二次鉴权 - **资源消耗**内存占用 ≤ 1.2GBCPU使用率 ≤ 70%由K8s Resource Limit强制保障 - **重试策略**网络超时可重试2次数据源不可用如税务API返回503则立即失败不重试这些约束不是性能指标而是SLOService Level Objective承诺。监控系统会实时采集execution_duration_ms、memory_usage_mb等指标一旦连续3分钟违反约束自动触发告警并降级至备用Skill。3.5 安全与合规Compliance Artifacts## 安全与合规 - **数据脱敏**输出PDF中所有身份证号、银行卡号、手机号均按****-****-****-1234格式掩码掩码规则由data_masking_engine统一执行 - **审计日志**每次调用生成两条日志 - audit.log: {skill:generate_tax_report,user_id:U123,report_period:Q1-2024,ip:10.1.2.3,timestamp:2024-05-12T08:30:45Z} - security.log: {event:PII_ACCESS,fields:[id_card,bank_account],masking_applied:true,timestamp:...} - **合规认证**已通过等保三级测评证书编号DJBZ-2024-XXXXX支持GDPR Right-to-Erasure请求没有这一节Skill在金融、医疗等强监管领域根本无法上线。security.log专为安全团队设计确保PIIPersonally Identifiable Information访问行为100%可追溯。3.6 故障诊断Troubleshooting Guide## 故障诊断 | 现象 | 可能原因 | 排查命令 | 解决方案 | |------|----------|----------|----------| | status_code1 | 用户纳税数据未同步至数仓 | curl -X GET http://data-warehouse/api/v1/users/U123/tax?periodQ1-2024 | 触发手动数据同步Job sync_tax_data --user U123 --period Q1-2024 | | status_code2 | 用户权限不足非本人/亲属 | curl -X GET http://auth-service/api/v1/relationship/U123 | 检查relationship_type字段是否为SELF、SPOUSE或CHILD | | status_code4 | PDF生成服务OOM | kubectl top pods -n tax-reporting | 扩容pdf-generator Deployment至3副本 |这不是运维手册而是给一线工程师的“秒级定位指南”。表格中每条“排查命令”都经过实测确保复制粘贴即可执行避免出现kubectl get pods -n xxx这种无效指令。3.7 版本演进Version History## 版本演进 ### v2.1.0 (2024-05-12) - 新增formatxlsx支持输出结构化Excel列收入类型、金额、税率、已缴税额 - 将status_code3INTERNAL_ERROR拆分为4TIMEOUT和5SERVICE_UNAVAILABLE提升错误可读性 - 移除对旧版税务API的兼容强制升级至v3.2 ### v1.8.0 (2024-02-20) - 首次发布支持PDF格式基础个税计算逻辑 - 依赖tax-calculation-lib1.4.0已通过FIPS 140-2加密认证版本历史不是流水账而是影响面评估清单。v2.1.0的“移除旧API兼容”意味着所有调用方必须同步升级此条目会自动触发CI/CD Pipeline中的兼容性检查。提示SKILL.md必须纳入Git Hooks校验。我们配置了pre-commit hook强制要求name字段必须与文件名一致generate_tax_report.skill.md→name: generate_tax_reportversion字段必须遵循SemVer 2.0规范input_schema必须能被jsonschema库成功加载。任何一条不满足commit直接被拒绝。文档即代码失信于文档就是失信于整个系统。4. Skill工程化的四大反模式你正在踩中的那些坑在Review超过1200个内部Skill实现后我总结出四个高频、隐蔽、且后果严重的反模式。它们不像语法错误那样立刻报红而是在系统规模扩大、流量峰值到来、合规审计启动时才集中爆发。识别并规避这些反模式是Skill工程化落地的生死线。4.1 反模式一Skill即胶水层The Glue-Anti-Pattern典型症状Skill代码里充斥着requests.get()、subprocess.run()、open(/path/to/config.json)核心逻辑只有3行其余全是适配不同API的转换代码。开发者美其名曰“统一接入层”实则是把Skill变成了技术债黑洞。为什么危险可维护性归零当上游API变更如微信支付接口升级v3你需要修改所有调用它的Skill而非集中更新一个Client SDK可观测性断裂requests.get()的耗时、错误码、重试次数无法被统一Metrics Collector捕获监控大盘上只剩一个模糊的skill_execution_duration安全漏洞温床硬编码的API Key、未校验的SSL证书、未过滤的用户输入拼接URL全部在Skill内部裸奔。正确解法Skill必须只依赖抽象接口而非具体实现。我们强制推行“三层依赖架构”Skill层只调用payment_gateway.charge(amount: Decimal, currency: str)这样的抽象方法Adapter层由专门团队维护WechatPayAdapter、AlipayAdapter、StripeAdapter各自实现charge()方法并封装重试、熔断、日志Client SDK层提供标准化HTTP Client如httpx.AsyncClientwithRetryMiddleware所有Adapter复用同一套网络栈。这样当微信支付升级只需更新WechatPayAdapter所有调用它的Skill自动受益。我们曾用此架构在2小时内完成全站17个支付相关Skill的v3接口迁移零 downtime。4.2 反模式二状态外溢The State-Leak-Anti-Pattern典型症状Skill函数内部创建全局变量、缓存字典、单例连接池甚至直接操作threading.local()存储用户上下文。开发者认为“反正只在本Skill里用没问题”。为什么危险并发安全崩溃在异步框架如FastAPI中threading.local()无法隔离协程A用户的Session数据被B用户意外读取内存泄漏雪球缓存字典随请求累积GC无法回收K8s Pod内存持续增长直至OOM Kill分布式失效单机缓存无法在多副本间同步导致同一用户在不同Pod上看到不一致的结果。正确解法Skill必须是纯函数Pure Function或显式状态管理。纯函数路径输入→计算→输出零副作用。适用于90%的Skill如calculate_compound_interest(principal, rate, years)显式状态路径若必须维护状态如对话状态机则通过context: SkillContext参数传入且SkillContext必须实现__slots__限制属性禁止动态添加字段。SkillContext由Agent Runtime统一创建、注入、销毁确保生命周期可控。我们用Pydantic V2定义SkillContextclass SkillContext(BaseModel): session_id: str user_id: str timestamp: datetime # 显式声明允许的状态字段禁止动态扩展 __slots__ (session_id, user_id, timestamp, _cache) def set_cache(self, key: str, value: Any) - None: if not hasattr(self, _cache): self._cache {} self._cache[key] value def get_cache(self, key: str) - Any: return self._cache.get(key)任何试图ctx.new_field boom的操作都会触发AttributeError在开发阶段就暴露问题。4.3 反模式三错误处理即静默吞咽The Silent-Swallow-Anti-Pattern典型症状Skill代码里大量try...except Exception as e: logger.error(e); return None或者更糟——except:后面直接pass。开发者觉得“别让错误崩掉整个Agent”。为什么危险故障不可见监控系统收不到错误指标告警静默直到用户投诉才发觉数据腐化return None被上游Skill当作有效结果导致空报告、零余额、错误跳转根因难追溯日志只有Exception occurred没有堆栈、没有输入、没有上下文排查耗时翻倍。正确解法错误必须分类、可量化、可追溯。我们定义三类错误UserError4xx类用户输入非法如amount 0返回{status: USER_ERROR, code: INVALID_AMOUNT, message: 金额不能为负数}SystemError5xx类服务暂时不可用如DB连接超时返回{status: SYSTEM_ERROR, code: DB_TIMEOUT, retry_after: 1000}并自动触发重试FatalErrorPanic类程序逻辑崩溃如None被当作int相加必须raise原始异常由Agent Runtime捕获并记录完整堆栈。关键创新点在于所有错误响应必须携带trace_id和input_hash。当用户投诉“生成报告失败”客服只需提供trace_id运维就能在ELK中秒级检索到该次调用的完整输入、输出、日志、堆栈无需用户复述操作步骤。4.4 反模式四测试即Hello WorldThe Hello-World-Test-Anti-Pattern典型症状Skill目录下有个test_basic.py里面只有一行assert skill_function(test) success。开发者认为“能跑通就算测试覆盖”。为什么危险边界场景全裸奔amount0、amount999999999.99、user_id../../../etc/passwd等边界值从未被验证契约漂移无感知Schema变更后旧测试用例仍通过因为没校验输出结构性能瓶颈被掩盖单次调用10ms但100并发时飙升至5s测试从未模拟真实负载。正确解法测试必须覆盖契约、边界、性能、混沌四维度。我们要求每个Skill必须有四个测试套件ContractTest用Pydantic Model加载input_schema和output_schema对任意输入生成随机合法/非法数据验证Schema校验逻辑BoundaryTest针对每个数值型参数测试min-1、min、max、max1、null、NaNLoadTest用Locust模拟100并发测量P95延迟、错误率、内存增长曲线阈值写死在pyproject.toml中ChaosTest在测试环境注入故障——随机Kill DB Pod、注入500ms网络延迟、篡改ConfigMap验证Skill的降级与熔断行为。一次真实的ChaosTest救了我们在模拟Redis宕机时get_user_profileSkill未能触发降级继续阻塞等待导致整个Agent链路超时。我们紧急修复增加了redis_client.ping(timeout200)健康检查并配置circuit_breaker在连续3次失败后自动熔断。经验之谈在Code Review Checklist中我永远把“是否提供了ContractTest”放在第一条。没有契约测试的Skill就像没有驾照就上高速——不是能不能开的问题而是迟早出事的问题。5. 从零构建一个Production-Ready Skill以“智能会议纪要生成”为例理论终需落地。现在让我们亲手构建一个真实场景下的Skillgenerate_meeting_minutes。它接收一段会议录音转写的文本ASR Result输出结构化Markdown格式的会议纪要包含议题摘要、待办事项Action Items、责任人Owner、截止时间Due Date。这个Skill将贯穿前述所有原则成为可直接投入生产的样板。5.1 第一步定义不可妥协的Skill Schema先写generate_meeting_minutes.skill.yaml这是整个工程的基石name: generate_meeting_minutes version: 1.0.0 description: 将非结构化会议文本提炼为结构化Markdown纪要自动识别Action Items、Owner、Due Date input_schema: type: object properties: asr_text: type: string minLength: 100 maxLength: 50000 description: ASR转写后的原始文本需包含发言者标记如[张三]今天讨论...[李四]我建议... meeting_id: type: string pattern: ^MTG_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ description: 会议唯一标识用于审计追踪 timezone: type: string enum: [Asia/Shanghai, America/New_York, Europe/London] default: Asia/Shanghai required: [asr_text, meeting_id] output_schema: type: object properties: markdown_content: type: string description: 生成的Markdown格式纪要必须包含## 议题摘要、## 待办事项两个一级标题 action_items: type: array items: type: object properties: description: type: string description: 待办事项描述如整理Q3销售数据 owner: type: string description: 责任人姓名需与公司通讯录匹配 due_date: type: string format: date description: 截止日期格式YYYY-MM-DD execution_time_ms: type: integer description: 实际执行耗时毫秒 required: [markdown_content, action_items, execution_time_ms]注意几个关键设计点asr_text设定了minLength: 100过滤掉无效的短文本如“嗯”、“啊”meeting_id强制UUID格式确保审计可追溯action_items数组的每个元素都定义了owner和due_date杜绝“待办事项无人认领”的模糊输出execution_time_ms是性能SLO的量化依据必须由Skill自身精确测量。5.2 第二步编写契约驱动的Skill实现generate_meeting_minutes.pyfrom pydantic import BaseModel, ValidationError, validator from typing import List, Dict, Any, Optional import time import re from datetime import datetime, timedelta # 严格遵循output_schema定义的Pydantic Model class ActionItem(BaseModel): description: str owner: str due_date: str validator(due_date) def validate_due_date(cls, v): try: datetime.strptime(v, %Y-%m-%d) except ValueError: raise ValueError(due_date must be in YYYY-MM-DD format) return v class SkillOutput(BaseModel): markdown_content: str action_items: List[ActionItem] execution_time_ms: int def generate_meeting_minutes( asr_text: str, meeting_id: str, timezone: str Asia/Shanghai ) - SkillOutput: start_time time.time_ns() # Step 1: 输入校验契约第一道防线 if len(asr_text) 100: raise ValueError(asr_text too short (100 chars)) if not re.match(r^MTG_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$, meeting_id): raise ValueError(invalid meeting_id format) # Step 2: 核心逻辑 - 提炼议题摘要此处用规则引擎非LLM确保确定性 summary _extract_summary(asr_text) # Step 3: 核心逻辑 - 识别Action Items正则NER模型 raw_items _parse_action_items(asr_text) # 标准化Owner匹配通讯录 normalized_items _normalize_owners(raw_items) # 推断Due Date基于“下周”、“月底”等相对时间 final_items _infer_due_dates(normalized_items, timezone) # Step 4: 构建Markdown严格遵循格式要求 markdown _build_markdown(summary, final_items) # Step 5: 构造输出Pydantic自动校验 output SkillOutput( markdown_contentmarkdown, action_itemsfinal_items, execution_time_msint((time.time_ns() - start_time) / 1_000_000) ) return output # 内部函数实现略重点在契约与结构 def _extract_summary(text: str) - str: # 实现细节提取高频关键词、发言者共识点、结论性语句 pass def _parse_action_items(text: str) - List[Dict[str, str]]: # 实现细节匹配请XXX负责...、需要在YYY前完成...等模式 pass def _normalize_owners(items: List[Dict]) - List[ActionItem]: # 实现细节调用HR API将小王、王工映射为王建国 pass def _infer_due_dates(items: List[ActionItem], tz: str) - List[ActionItem]: # 实现细节将下周三转换为具体日期 pass def _build_markdown(summary: str, items: List[ActionItem]) - str: md ## 议题摘要\n\n summary \n\n## 待办事项\n\n for i, item in enumerate(items, 1): md f{i}. **{item.description}**\n - 责任人{item.owner}\n - 截止时间{item.due_date}\n\n return md关键点解析输入校验前置在业务逻辑开始前用正则和长度检查拦截非法输入避免无效计算输出强制PydanticSkillOutput(...)构造时Pydantic会自动校验due_date格式、action_items数组结构任何不合规都将抛出ValidationError执行时间精确测量用time.time_ns()而非time.time()避免浮点精度误差所有内部函数命名清晰_extract_summary、_parse_action_items体现单一职责。5.3 第三步编写四维测试套件test_generate_meeting_minutes.pyimport pytest from unittest.mock import patch, MagicMock from generate_meeting_minutes import generate_meeting_minutes, SkillOutput, ActionItem # ContractTest验证Schema校验 def test_input_schema_validation(): # 测试非法meeting_id with pytest.raises(ValueError, matchinvalid meeting_id format): generate_meeting_minutes( asr_texttest text, meeting_idINVALID_ID ) def test_output_schema_validation(): # 测试非法due_date with pytest.raises(ValueError, matchdue_date must be in YYYY-MM-DD format): SkillOutput( markdown_content# Test, action_items[ActionItem(descriptiondo it, ownerme, due_date2024/01/01)], execution_time_ms100 ) # BoundaryTest测试边界值 def test_asr_text_min_length(): # 刚好100字符应通过 valid_text A * 100 result generate_meeting_minutes(valid_text, MTG_123e4567-e89b-12d3-a456-426614174000) assert isinstance(result, SkillOutput) def