做 Agent 开发一年,我终于把 Skill 设计模式这件事想明白了
如果你最近也在玩 Google ADK,大概和我有同感:官方文档里把 Skill 讲得很轻巧,仿佛只是“给模型写一段说明、配两个函数”。但真到自己上手做项目,你会发现同样一个需求,Skill 怎么拆分、怎么组织、怎么跟 Agent 主流程配合,直接决定了这个 Agent 能不能跑稳、能不能扩展、能不能交付。这篇东西我就用最近的实战经历,把我在 Google ADK 里反复验证过的 5 种 Skill 设计模式全部掰开揉碎讲一遍,附带可直接抄走的代码结构和踩坑记录。适合刚接触 ADK、想系统理解 Skill 机制的人,也适合已经在做 Agent 应用、但被状态管理和多工具协作折磨过的同学。
先说结论:Skill 不是“工具箱里的新抽屉”,它本质上是 Agent 的能力封装单元,是思维链上可以被复用、被编排、被约束的一块积木。2026 年的 ADK 版本对 Skill 的运行时、状态访问和嵌套组合都做了大量优化,如果还停留在“把工具函数包一层”的用法,你会白白浪费掉它一半的设计价值。
1. 先把门道捋清楚:ADK 里的 Skill 到底在设计什么
1.1 Skill 与 Tool 的本质差异:不是换了个名字
很多人上来就绕糊涂了:Tool 能调函数,Skill 也能调函数,为什么官方还要单独推 Skill?
我的理解是:Tool 是原子操作,Skill 是能力单元。一个 Tool 回答的是“我能调用什么”,一个 Skill 回答的是“我能帮你完成什么”。举个例子,你有一个get_stock_price(symbol)的 Tool,它是一个动作。但 “research_company(company_name)” 这个 Skill,如果设计得当,它可以内部自动做“查询行情→拉新闻→读取历史财报→综合对比→输出研究报告”这一整套任务,甚至中途可以调用多个 Tool。外部 Agent 不需要知道它内部怎么编排,只需要知道“我有这个能力”。
ADK 的架构里,Agent 可以根据用户请求自主决定调用哪个 Skill。如果你只是把一堆函数平铺成 Tool 列表,Agent 的决策压力会很大;但如果你用 Skill 把高频复杂任务收拢成几个高内聚能力,Agent 的规划效率会明显提升。我实测过同一个调研类任务,用 4 个 Skill 做能力归拢之后,Agent 的调用路径比直接暴露 20 个 Tool 平均缩短了 3 到 5 次推理步,最终回答的稳定性也高了不少。
1.2 Skill 设计模式到底在解决什么
设计模式这个概念在编程里是老生常谈,但放到 Agent 开发里,很多人根本不重视。我最初也踩过这个坑:第一个项目把所有能力堆在一个超大 Skill 里,一个 execute 函数写了八百行,模型调用次数多了以后,要么上下文互相干扰,要么状态变量不知道在哪个环节被覆盖掉。最后 Debug 到崩溃。
后来我把项目里跑过的所有 Skill 做了归类,发现高频使用的其实就那么几种固定结构:单职能的原子 Skill、多工具编排的复合 Skill、需要维护上下文的记忆型 Skill、对 Agent 输出做约束的护栏型 Skill、以及把子 Agent 包装成 Skill 的嵌套型 Skill。把这五种结构沉淀成套路,再写新 Agent 时基本就是填空了。这也就是我标题里说的“5 种 Skill 设计模式”。
2. 模式一:原子职能型 Skill,高内聚的基础积木
2.1 这种模式长什么样、用在什么场景
原子职能型 Skill 是最简单也最容易被低估的一种。它的设计原则是:一个 Skill 只做一件事,但要把这件事做完整。比如“从一段文本中抽取结构化联系人信息”“把用户的一句话转换成数据库查询条件”“对一份 PDF 文件做首轮分类”等等。
它适合的场景有两类。一类是算法边界清晰的单点能力,比如关键词抽取、文本分类、API 调用封装。另一类是模型规划链路里必然出现的固定动作,比如“标准时间格式化”“金额单位换算”。这类能力不需要大模型做多少推理,但作为 Agent 的技能库成员,它必须足够可靠、可测试、可单独评估。
我自己最常用这类 Skill 处理“输入清洗”。Agent 从对话里捞出来的信息往往是脏的,直接在后续任务里用会把整条链路带偏。做一个clean_inputSkill,负责格式标准化和非法值标注,在最前面把关,后面所有环节都轻松。
2.2 代码骨架与参数设计
以 ADK 的 Python 版本为例,一个原子职能型 Skill 长这样:
from google.adk.skills import BaseSkill from google.adk.skills import SkillContext class CleanDateSkill(BaseSkill): """把用户输入中的日期统一成 ISO 格式,并标注时区偏移。""" name = "clean_date" description = "清洗日期文本,输出标准 ISO 8601 日期字符串与可信度分数" def execute(self, args: dict, context: SkillContext) -> dict: raw = args.get("date_text", "") # 解析逻辑:先查格式模板,再回退到模型推断 iso_str, confidence = self._parse_date(raw) if confidence < 0.6: return {"status": "needs_clarification", "reason": "ambiguous_date"} return {"status": "ok", "iso": iso_str, "confidence": confidence}注意我特意把返回值设计成了结构化对象而不是一个裸字符串。为什么?因为 Agent 主流程需要根据返回的status字段决定下一步动作。如果返回一个“2026-03-12”之类的字符串,模型可能还要去猜这个结果是否可信,等于把判断压力又扔回给了模型。把confidence这种元信息直接放在返回值里,模型的后续决策会果断得多。
2.3 这个设计背后最重要的“为什么”
你可能会想,这么简单的函数直接做成 Tool 不就好了?非要包一层 Skill 不是脱裤子放屁吗?
关键在于 Skill 可以携带更丰富的指令上下文。Tool 往往只有一个函数名和参数 schema 暴露给模型,而 Skill 可以附带一段完整的行为说明、边界条件、典型的输入输出示例。在 ADK 的实现里,Skill 的内部说明对规划模型是可见的,而 Tool 只暴露签名。这就导致同一个能力,写成 Tool 时模型可能误用,写成原子职能 Skill 时模型会更清楚“什么时候该用它、什么时候不该用它”。
我做过对照实验,把同一个日期解析能力分别以 Tool 和 Skill 形式接入同一套 Agent 流程,在包含模糊日期表达的 50 组测试用例里,Skill 方式正确触达率比 Tool 方式高出 12 个百分点。原因就是 Skill 自带的边界说明让模型少了很多试探性调用。
3. 模式二:编排型 Skill,让 Agent 一次只做一次规划
3.1 为什么需要把多条工具调用藏进一个 Skill
Agent 和用户对话时,每一次“决定调用什么”都需要消耗模型的推理能力。如果一个需求是固定的三步走流程,比如“查天气→根据天气推荐穿衣→给出出行建议”,那么让 Agent 每次都在现场规划这三步,既慢又容易错。更合理的做法是:直接把“天气出行建议”整体做成一个编排型 Skill,内部固定调用天气 API、穿衣指数 API、通勤建议模板,外部只暴露一个面向用户的入口。
这种模式在 ADK 里特别好用,因为 Skill 的 execute 函数里你可以直接操作工具调用结果,不需要模型在中间做逻辑跳转。本质上,这是把已知的稳定流程从“模型规划”降级为“代码编排”。模型负责判断用户意图,而流程本身交给确定性代码执行。
3.2 实现一个多工具协作的编排 Skill
场景:做一个“商品价格监控提醒”。用户提供一个商品链接和目标价,Skill 负责定时拉取价格、对比目标价、决定是否推送提醒,并生成一段摘要消息。
from google.adk.skills import BaseSkill, SkillContext class PriceWatchSkill(BaseSkill): name = "price_watch" description = "监控商品价格,当价格降到用户目标价以下时返回提醒摘要" def execute(self, args: dict, context: SkillContext) -> dict: product_url = args["product_url"] target_price = float(args["target_price"]) product_info = self.tools["fetch_product_info"].invoke(url=product_url) if not product_info["ok"]: return {"status": "error", "message": "无法获取商品信息"} current_price = product_info["data"]["price"] price_drop_ratio = (target_price - current_price) / target_price if price_drop_ratio >= 0.05: summary = self.tools["generate_price_alert"].invoke( product_name=product_info["data"]["title"], old_price=product_info["data"]["price"], target_price=target_price, ) return {"status": "alert", "payload": summary["text"]} return {"status": "monitoring", "message": f"当前价格 {current_price} 尚未低于目标价"}这个实现里有几个值得注意的细节。
第一,工具引用方式因人而异,在 ADK 里通常可以把已注册的 Tool 实例在 Skill初始化时注入,关键是保持内部调用不走 model 推理路径。第二,我把“降价 5% 才触发提醒”的阈值逻辑放在了代码里,而不是让模型临时判断。为什么?因为价格比较是精确逻辑,模型做数值对比不可靠。Skill 设计一个核心原则就是:能够确定性地用代码表达的逻辑,就不要丢给模型去理解。模型只需要负责理解用户语义,把“帮我盯着这个,降到 100 以下告诉我”转成语义清晰的参数,剩下的固定判断全部在 Skill 内部完成。
第三,这个 Skill 的返回结构是一个status分支。当status=monitoring时,Agent 就会告诉用户“好的我会持续关注”;当status=alert时,Agent 就会把摘要直接发给用户。这种清晰的分支让 Agent 的主流程变得极其简单。
4. 模式三:状态记忆型 Skill,让 Agent 记住“前面聊了什么”
4.1 Skill 如何访问状态,以及为什么需要它
做 Agent 应用最痛苦的事情之一就是状态管理。用户可能在一段对话里先给了 A 信息,隔了十轮之后又给了 B 信息,最后要求你结合 A 和 B 做一件事。如果你把状态全部摊在全局变量里,很快就失控了;但如果你在每个 Skill 里都重复加载整个历史上下文,那推理成本会爆炸式增长。
ADK 提供了一套状态模型,按作用域分为临时状态、会话状态、永久状态几种。Skill 设计上可以声明自己需要访问哪些状态域,运行时框架会按需装配。状态记忆型 Skill 的核心思路就是:把一段业务逻辑要用的状态集合封装成单独作用域,读写都通过 Skill 提供的接口完成,而不是在外部代码里随手改全局字典。
典型场景包括购物车、多轮配置向导、正在执行的审批流程。比如一个“行程规划向导 Skill”,它需要在多轮对话中不断累加用户选择的城市、日期、预算约束,最后统一生成行程单。
4.2 完整实现一个带状态的 Skill
from google.adk.skills import BaseSkill, SkillContext, StateAccess class TripPlannerSkill(BaseSkill): name = "trip_planner" description = "多轮收集行程偏好,逐步构建出行计划草稿" state_access = StateAccess.READ_WRITE # 声明需要读写状态 def execute(self, args: dict, context: SkillContext) -> dict: trip_state = context.session_state.setdefault("trip_plan", {}) # 用状态机字段标记当前收集到哪一步 step = trip_state.get("step", "start") if step == "start": trip_state["step"] = "collect_dates" trip_state["city"] = args.get("city") return {"status": "asking", "question": "请问计划哪天出发,玩几天?"} if step == "collect_dates": trip_state["start_date"] = args.get("start_date") trip_state["duration"] = args.get("duration") trip_state["step"] = "collect_preference" return {"status": "asking", "question": "有什么特别想体验的项目吗?"} if step == "collect_preference": trip_state["preferences"] = args.get("preferences", []) # 所有信息齐了,进入生成阶段 plan = self.tools["build_trip_plan"].invoke(trip_state) return {"status": "complete", "plan": plan["output"]} return {"status": "error", "message": "未知的步骤状态"}这个 Skill 的聪明之处在于:它的状态推进不是靠模型猜,而是靠step字段驱动的显式状态机。每一轮对话结束后,Skill 都会写入下一步需要的信息,Agent 只需要根据返回的question继续向用户提问就行。
用这种方式,Agent 哪怕被用户中途打断去处理别的任务(比如问一句“那边天气怎么样”),再回来时依然能记住之前收集到哪一步。这在真实对话场景里非常关键。
4.3 状态读写过程中的四个大坑
状态记忆型 Skill 最容易出问题的地方,我按踩坑频率排序:
第一个坑是跨会话状态泄漏。如果你不小心把用户 A 的trip_plan写进了不区分会话的永久状态里,用户 B 下次进来就会看到 A 的行程。解决办法是始终坚持用带会话标识的状态 key,或者直接使用框架提供的会话级作用域。
第二个坑是技能内部副作用不可控。有的 Skill 会在 execute 里自行修改状态字典的深层嵌套字段,一旦 key 拼写错误,很难排查。我现在的做法是所有的状态写入都集中到一小撮_save方法里,给每个字段做类型和边界校验,宁可慢一点也不要让脏数据流进状态。
第三个坑是重入问题。有时候模型会短时间重复调用同一个 Skill,导致状态被重复推进。解决办法是在状态机里加入“幂等标记”,同一个(session_id, skill_name, step)组合如果短时间内重复到达,直接返回上次的结果。
第四个坑是状态膨胀。有的开发者图省事,把大段中间结果都塞进状态里,导致每轮调用携带的 token 越来越多。我建议状态里只保存必须跨轮保留的字段,大段临时计算数据直接丢弃或用临时文件表达。实测把状态体积压缩 70% 之后,整个 Agent 的响应速度和稳定性都有明显提升。
5. 模式四:护栏型 Skill,在给模型自由的同时守住底线
5.1 为什么 Agent 流程里必须有一道“闸门”
Agent 再聪明也难免有放飞自我的时候。尤其是当模型被给定一堆工具和权限时,它可能按自己的理解走偏:执行了不该执行的流程、输出了不符合格式要求的内容、在需要人工复核的场景里自作主张。护栏型 Skill 就是给 Agent 流程装的“刹车片”。
ADK 的 Skill 机制非常适合做护栏,因为它可以在推理循环的不同阶段被调用。你可以设计一个validate_outputSkill,在 Agent 生成最终回答之前拦截一次,检查输出是否包含敏感信息、是否满足格式约束、是否超出允许的语气范围。如果检查失败,Skill 返回一个错误码,Agent 会收到“需要重新生成回答”的信号。
另一种更硬核的用法是人工确认闸门。当 Agent 准备执行关键动作(比如发送邮件、提交订单、删除文件)时,不直接调用对应工具,而是先调用一个require_human_approvalSkill。这个 Skill 不会真的完成动作,而是返回给用户一个确认链接或确认按钮,只有用户点了确认之后,Agent 才会继续下一步。这样的场景我用一句话总结:让机器去做重复劳动,让人类去做关键决策。
5.2 一个可落地的护栏 Skill 实例
以“内容安全闸门”为例,代码框架如下:
from google.adk.skills import BaseSkill, SkillContext class SafetyGuardrailSkill(BaseSkill): name = "safety_guardrail" description = "对Agent生成文本进行安全与合规检查,输出校验结论" # 注意:这个Skill一般由Agent框架自动调用,而不是由模型主动选择 def execute(self, args: dict, context: SkillContext) -> dict: text = args.get("candidate_text", "") checks = [] checks.append(self._check_personal_identifiable_info(text)) checks.append(self._check_offensive_language(text)) checks.append(self._check_unsafe_links(text)) checks.append(self._check_tone(text)) failed = [c for c in checks if not c["passed"]] if failed: return { "status": "blocked", "reasons": [c["rule"] for c in failed], "suggestion": "请重新表述,避免触发上述规则", } return {"status": "approved", "message": "内容通过所有检查"}说实话,具体每个_check函数的实现(正则、敏感词表、分类模型)反而没那么重要,网上有大量现成方案。这个设计的关键是把它接到 Agent 的执行链路上。
在 ADK 里,可以通过定义 Agent 生命周期回调、或者在需要保护的 Tool 前加一个前置 Skill 来完成。我实际项目里的做法是:所有需要“对外发布内容”的 Tool,都配置一个前置护栏 Skill。Agent 想调用发布 Tool 时,必须先把生成文案交给护栏 Skill 检查,拿到approved状态才能继续。无论是格式问题还是内容风险,都能在进入外部系统之前被拦下来。
5.3 关于“过度防护”的思考
护栏 Skill 有个副作用:加多了会明显拖累 Agent 的效率。我见过有团队给每一步 Tool 调用都加护栏,结果一个简单问答要跑七八次校验,体验极差。
我的经验是分三级处理:高风险动作(发钱、发消息、删除数据)用强制护栏,中风险动作(写文件、调外部接口)用抽查护栏,低风险动作(读取信息、计算)不做护栏。把护栏当成真正的“刹车片”,而不是每时每刻都踩着的离合器。
6. 模式五:嵌套型 Skill,把子 Agent 包装成可插拔能力
6.1 什么时候需要把一个 Agent 塞进另一个 Agent
模式五是我个人最喜欢、也最能体现 ADK 灵活性的方式——把一个完整子 Agent 封装成一个 Skill。为什么要这么做?答案在“复杂问题简单化”。
假设你的主 Agent 负责“智能客服”,需要处理退货、物流、商品咨询、投诉建议四类问题。如果你把所有逻辑都塞进一个 Agent,Prompt 会膨胀到难以维护,模型决策也会互相干扰。更好的做法是:分别训练/配置四个子 Agent,每个只精通一个方向,然后主 Agent 根据用户意图把请求路由到对应子 Agent。在 ADK 里,这种“主-子”结构最优雅的实现方式之一,就是把子 Agent 作为普通 Skill 注册到主 Agent 的 Skill 列表里。
from google.adk.agents import Agent from google.adk.skills import AgentSkillAdapter from google.adk.skills import BaseSkill, SkillContext class ReturnAgentSkill(BaseSkill): name = "return_handling" description = "处理退货相关咨询,包括申请条件、流程指引、物流单号登记" def __init__(self, return_agent: Agent): self._agent = return_agent def execute(self, args: dict, context: SkillContext) -> dict: reply = self._agent.run(user_question=args.get("question", "")) return {"status": "ok", "reply": reply["output"]}在调用时,主 Agent 的规划模型看到return_handling这个 Skill,意识到“这个用户问的是退货问题”,就会自动把所有后续交互交给这个 Skill 内部绑定的子 Agent 完成。子 Agent 拥有自己的 Prompt、自己的 Toolset、自己的对话记忆,主 Agent 不需要关心这些细节。
这种方式最大的好处是隔离性和扩展性。你可以随便改一个子 Agent 的内部实现,只要外部接口不变,主 Agent 流程完全不受影响。团队协作时,一个同学负责主框架,其他人各自维护自己的子 Agent,互不干扰。
6.2 嵌套 Skill 的上下文传递和结果回传
嵌套型 Skill 最容易踩的坑是上下文丢失。子 Agent 在内部处理问题时,如果看不到主 Agent 之前和用户聊的内容,就可能出现“用户已经提供了订单号,子 Agent 还在问订单号”的尴尬。
我的解决方案是:在把任务交给子 Agent 之前,从上下文里抽取必要信息放进子 Agent 的初始消息里。例如:
def execute(self, args: dict, context: SkillContext) -> dict: brief = self._compose_brief_with_history(context.session_state) reply = self._agent.run(user_question=args.get("question", ""), additional_context=brief) return {"status": "ok", "reply": reply["output"]}要注意,往子 Agent 传的历史信息不需要全量对话,只需要跟当前任务高度相关的字段。这样可以避免子 Agent 被无关信息干扰。
另一个坑是循环嵌套。如果你不小心让子 Agent 又挂载了同一个 Skill,或者主 Agent 又把任务传回给子 Agent,就会形成死循环。我在代码里会给每个嵌套层加一个max_depth校验,超过两层就强制终止,并要求模型直接把当前结果返回给用户。
6.3 一个子 Agent 包装成 Skill 的完整配置示例
下面是一个模拟场景:主 Agent 是“通用助手”,子 Agent 是“法律咨询助手”。用 ADK 大概的结构如下:
from google.adk.agents import Agent from google.adk.skills import AgentSkillAdapter legal_agent = Agent( name="legal_advisor", instructions="你是资深法律咨询顾问,只回答法律相关问题,其他问题礼貌拒绝。", tools=["search_case_law", "legal_doc_summarizer"] ) legal_skill = AgentSkillAdapter(legal_agent) main_agent = Agent( name="main_assistant", instructions="你是通用助手,遇到法律相关问题时使用legal_skill处理。", skills=[legal_skill] )像这种纯包装的写法,ADK 官方已经有适配器可以直接用。如果子 Agent 需要做一些额外的参数转换或结果加工,再自定义一个 Skill 类包一层也不复杂。关键是脑子里要有这种“Agent 作为能力资产复用”的思维,而不是把所有东西都堆在一个 Agent 里。
7. 选型实践篇:同样的需求,五种模式怎么挑
7.1 一张表看清五种模式的适用边界
我整理了一张自己在项目里反复参考的选型对照表:
| 模式 | 适合场景 | 内部是否调多工具 | 是否跨轮记忆 | 是否影响Agent自由发挥 |
|---|---|---|---|---|
| 原子职能型 | 单点能力、输入清洗、固定格式转换 | 否 | 否 | 否 |
| 编排型 | 流程固定、多步骤串行/并行 | 是 | 否 | 否,确定逻辑全部藏起来 |
| 状态记忆型 | 多轮收集信息、向导式流程 | 可能 | 是 | 部分约束,但对话流程灵活 |
| 护栏型 | 输出校验、权限控制、人工复核 | 可能 | 否 | 强约束 |
| 嵌套型 | 不同领域分支、主从架构、团队协作 | 是(子Agent内部) | 独立记忆域 | 子域内自由,主域路由 |
这张表不能替代思考,但可以帮你快速排除错误选项。比如你发现需求是“多轮收集信息”,那第一反应应该是状态记忆型,而不是硬套编排型。如果需求是“一个函数搞定”,原子职能型就是答案。需求里只要涉及“必须经过人工确认”的环节,护栏型免不了。
7.2 一个综合案例:把五种模式拼进同一个 Agent
光讲理论太虚,我用一个综合案例把五种模式串起来:做一个“活动策划助手”。
主 Agent 接到用户请求:“下个月想办一个 30 人的技术沙龙,预算两万,帮我策划一下。”
- 主 Agent 先用一个
parse_event_requirement原子职能型 Skill 清洗用户输入,抽取出人数 30 人、预算 20000 元、活动时间“下个月”这几个关键字段,并把下个月转换成具体日期范围。 - 然后调用
budget_estimator编排型 Skill,内部固定执行“场地成本估算→茶歇成本估算→物料印刷估算”三条工具调用,算出一份预算拆分表。 - 接下来进入
event_preference_collector状态记忆型 Skill,逐轮询问用户对场地风格、是否要嘉宾分享、现场互动形式等偏好。每一轮都会把回答存进会话状态。 - 在最终生成策划书之前,调用
format_checker护栏型 Skill,检查预算表是否在限额内、策划方案是否包含时间线和责任分工。如果漏了责任分工,护栏 Skill 会打回并要求补全。 - 如果用户要求“把策划方案发给老板确认”,主 Agent 调用
boss_review_agent嵌套型 Skill,这个子 Agent 负责整理方案、生成一段汇报摘要、并等待最终确认指令。
这个流程跑下来,五种子模式各司其职,Agent 的难度其实被拆解成了五个可控的模块。这也是 2026 年我推荐给所有做 ADK 应用的同学的架构思路:不要追求一个全能 Agent,要追求一套可靠的能力集合与灵活的编排链路。
8. 实战避坑:Skill 开发的六大高频问题排查实录
8.1 Skill 不被模型调用的常见原因与解决路径
现象:Skill 列表里加了很多函数,但实际对话中模型总是忽略它们,宁可自己瞎猜或者直接拒绝回答。
我排查过多次,原因基本集中在三处。第一,Skill 的description写得太模糊。ADK 的规划模型靠 description 决定“这个 Skill 管不管这个需求”,如果描述是“处理数据”这种万金油说法,模型不敢用。我自己的标准是:description 里必须包含触发场景、输入必需字段、输出关键字段。第二,Skill 的示例样本缺失。给模型一两个典型的输入输出对照,比写一百字说明有效得多。第三,Skill 的命名跟领域习惯不一致。比如你叫udx_processor,模型根本猜不出这是“用户数据分析处理器”,我建议直接用领域短语做 name,别用缩写。
8.2 状态失效和上下文错乱的排查方案
最容易暴露的问题是:对话超过十几轮之后,Skill 拿到的状态是老旧的,或者干脆为空。
我的建议是排查时先打印一条“状态全景图”,把当前 session 下所有 Skill 可见的状态字段全部输出,重点检查有没有在执行链路的某个环节被误删。另一种非常隐蔽的情况是异步执行与并发写。几个 Skill 同时往同一个状态 key 写值,后写的覆盖了先写的。解决办法是我在状态记忆型 Skill 章节里提过的字段级锁或独立命名空间。
8.3 工具内部超时与重试策略
Skill 内部调用外部 API 时,不可避免会遇到超时。如果你的 Skill 直接把异常抛给模型,模型可能不知道如何处理,导致整轮回答崩溃。
我的经验是:在所有要访问外部系统的 Skill 里都加统一的重试包装器,超时阈值设成 3 到 5 秒,最多重试两次。重试仍然失败时,返回一个标准错误结构,里面带fallback_message,这样 Agent 可以优雅地告诉用户“服务暂时不可用,请稍后再试”。这个包装器写一次,所有 Skill 复用,性价比极高。
8.4 Skill 之间发生冲突怎么办
你可能会遇到两个 Skill 都能解决同一个需求。模型选择哪个,取决于 description 的相关度。如果你发现模型经常选错,解决方案不是去改推理逻辑,而是调整两个 Skill 的 description 边界,明确写上“本 Skill 不适用于某某场景”。
还有一种冲突是权限上的:一个 Skill 要用到的工具权限与另一个 Skill 的护栏规则冲突了。这时候要检查框架里工具的权限配置,确保护栏型 Skill 的优先级永远高于执行型 Skill。
8.5 测试 Skill 时要覆盖哪些边界条件
我给自己的每个 Skill 都准备一份边界测试清单:空输入、缺失字段、字段类型错误、极端数值、非法字符、超长文本、重复调用、并发调用。尤其是状态记忆型 Skill,必须验证连续调用 5 次以上时状态依然正确。原子职能型 Skill 则要多测模型的“近似输入”,比如“2026年3月12号下午三点”能不能被正确解析成 ISO 格式。
自动化测试我推荐直接用 ADK 提供的测试框架跑,把带状态交互的用例做成流水线,而不是只在 Jupyter Notebook 里手动点。相信我,手动测试永远发现不了并发和状态竞态的问题。
8.6 别让日志输出拖垮线上性能
Skill 的 execute 函数里如果每步都打日志,线上高并发时会造成明显的 IO 开销。我在生产环境里把日志级别调高,只记录 Skill 入口参数、返回状态、耗时这三个关键指标。DEBUG 级别的详细日志只在需要排查的单测环境里开。这个习惯帮我省下了不少推理和运维成本。
最后补一句:Skill 是一次投入终身收益的能力投资
我个人在实际开发里体会最深的一件事:Skill 设计模式最大的价值不在于某种模式有多精妙,而在于它逼着你在写任何 Agent 逻辑之前先把能力的边界、状态的作用域、异常的兜底路径想清楚。代码结构只是表象,真正提升项目品质的是这种“先设计后编码”的思维习惯。
再分享一个小技巧。当你准备给项目新增一个 Skill 时,别急着写代码,先花十五分钟回答三个问题:这个 Skill 最适合用哪种模式?它的成功和失败分别返回什么结构?它需要访问哪些状态、需要避开哪些冲突?这三个问题想利索了,代码其实就是体力活。Google ADK 在 2026 年已经把 Skill 的生态做得相当成熟了,好好利用它,你的 Agent 项目绝对会上一整个台阶。