☰
Agent技能层(Agent Skills)设计与工程落地实践指南
2026/10/7 6:42:56 网站建设 项目流程

做了两年多 Agent 工程化落地,我最大的感受是:大部分团队根本不是在做一个 Agent,而是在堆一堆可有可无的“工具函数”。今天聊的 agent-skills,指的就是那层让 Agent 真正拥有某类专业能力的技能层——它不是写死流程,也不是简单把 API 包一层,而是把任务拆解、工具编排、上下文管理、自我纠错这些东西揉进一套可复用的机制里。这篇内容适合两类人:一是正准备从零搭建 Agent 技能体系,想知道第一步该怎么迈的;二是已经在做相关功能,但发现技能边界模糊、复用性差、一换场景就崩,想听听别人的解法。

说实话,网上讲 Agent 的帖子满天飞,但大多数都停在“什么是 ReAct”“怎么调 LangChain”这个层面,真正把技能设计当工程做的很少。我这篇文章会讲清楚 agent-skills 的三层结构、命名与参数设计、上下文传递机制、错误恢复策略,以及怎么用一套轻量框架把技能跑起来。全文不含概念堆砌,全是可以直接抄作业的方案和踩坑实录。

1. Agent Skills 到底是什么,为什么值得单独设计

1.1 从工具集合到技能版图的思维转变

很多人最开始做 Agent,就是把能调的外部接口都塞进函数列表,让模型自己选。这在 demo 阶段没问题,但一旦进入真实业务,就会暴露一个尴尬的现实:模型根本不知道在什么场景下该用哪个函数,上下文一长还会选错。你把“发送邮件”和“解析附件”两个工具放在一起,模型就可能在需要解析附件的时候先去发邮件,甚至在邮件正文里塞一段附件解析结果,场面相当混乱。

agent-skills 解决的不是“模型会不会调用工具”,而是“调用之后能不能把事情做完整”。技能是一组围绕某个业务目标组织起来的能力单元,它内部可以包含多个工具调用、校验逻辑、异常分支。比如“处理发票报销”这个技能,对外只暴露一个意图和一个输入参数,内部却要完成文件上传、OCR 识别、金额校验、审批流触发、结果回执五件事。模型只需要说“帮我报销这张发票”,剩下的全部交给技能内部去调度。

这里的关键是抽象层次。你把技能当成一个黑盒能力,模型就不需要关心内部实现了多少步。这跟我们在代码里做接口设计是一个道理:把复杂逻辑封装在背后,调用方只关心入参和出参。设计得好的技能列表,对模型来说就是一张清晰的能力地图;设计得烂的技能列表,对模型来说就是一堆不知道何时该用的杂技道具。

1.2 技能体系的三层结构:意图层、执行层、资源层

我把 agent-skills 拆成三层,这个结构几乎适合所有业务场景。

第一层是意图层,解决“模型怎么知道这个技能该不该用”的问题。每个技能必须有清晰的触发条件、适用场景、不适用场景。很多人在这一层偷懒,只写一句“用于报销”,结果模型在用户咨询报销政策的时候也调用了报销技能,直接把用户带偏。后来我要求每个技能必须写清楚 triggers、constraints、examples,模型选错的概率立刻下降了一大截。

第二层是执行层,解决“技能内部怎么把事情干完”的问题。执行层里是这个技能特有的步骤编排逻辑:先加载什么数据、调用什么接口、失败后怎么办。我见过不少团队把执行层写成一长串 if-else,这种写法在技能稍复杂后就完全失控。正确做法是像写一个微型状态机,把每个步骤的输入输出明确下来,让技能内部有清晰的推进路径和回退路径。

第三层是资源层,解决“技能依赖的东西从哪里来”的问题。包括外部 API 的认证信息、数据模型定义、提示词模板、缓存策略。资源层独立出来的好处是,换环境部署时只需要切换资源配置,技能逻辑一行不用改。比如在本地测试用一个 mock API,上线切换到真实服务,通过资源层的配置替换就能完成。

1.3 什么场景才值得做一个技能

不是所有功能都需要做成 agent-skill。我见过最极端的情况,有人把“获取当前时间”也做成了一个技能——其实 OpenAPI 直接带 system time 就行。这里分享一个判断标准:如果这个功能只需要一次工具调用、不需要中间状态、没有分支逻辑,那它就是工具,不是技能。只有满足下面至少两条,才值得做成技能:

  • 内部需要两个及以上工具调用的协作
  • 需要维护跨步骤的状态(比如临时文件、会话快照)
  • 有明确的失败恢复或人工确认分支
  • 需要依赖多份动态配置(模型、API、模板的切换)

之前做客服系统时,有个“订单查询”接口,查询到订单后还要查物流轨迹、计算预计送达时间、格式化反馈文本。如果散成三个独立工具,模型需要连续调用三次,成功率会骤降。把它封装成“订单全链路查询”技能后,模型只要给出订单号,剩下的事交给技能内部,成功率从 71% 直接提到 96%。这就是技能化封装的价值。

2. 动手设计一套技能:命名、参数与上下文规范

2.1 技能命名的三个黄金原则

技能名称直接影响模型的选择准确率。我最早做了一套技能,命名追求简洁,比如“check_status”,结果模型经常把它当成通用工具到处用。做了三个调整后,情况才好转:

第一,名字必须包含业务语境。不要叫process,要叫process_refund_request;不要叫send,要叫send_meeting_invitation。模型通过名称判断语义,名称里信息量越大,误选率越低。第二,动词开头,宾语明确。Agent 技能本质是“做一件事”,所以extract_invoice_amount比amount_extraction好,前者直接描述了动作。第三,同一业务域的技能名前缀保持一致。比如所有财务类技能都以finance_开头,所有客服类技能都以support_开头,模型在上下文里看到多个技能时,前缀带来的聚类效应非常明显。

2.2 参数设计原则:让模型少猜、少编

技能参数的 Schema 设计对结果稳定性影响极大。最典型的错误是参数描述写得太简略。举个例子,一个“创建日程”的技能,参数只写title: str,模型可能会把一段超长的纪要全文当作标题传进来。你需要把每个参数的约束写清楚:“title 是简洁的日程标题,不超过 20 字,不要包含时间地点信息”。

另外一个关键点是必填参数要少,默认值要多。模型在调用技能时,如果缺少一个非必填参数,它倾向于自己编一个值补上——这是最可怕的。宁可把参数设成可选并给合理默认值,也不要逼模型现场编造。比如“发送周报”技能,recipients参数如果为空,默认发给直属主管,而不是让模型猜收件人。

还有一点:不要设计“万能参数”。有个同学在设计技能时加了个data参数,声称可以接任何格式的数据,结果模型每次调用都把乱七八糟的内容塞进去。技能的参数应该精准对应业务实体的字段,比如order_id、message_type、target_amount,而不是一个泛化的data或payload。

2.3 上下文传递:技能之间的隐式契约

Agent 在并行执行多个技能时,技能与技能之间的数据传递最容易出问题。比如技能 A 生成了一个临时文件路径,技能 B 要用这个路径继续处理。如果这些信息只存在于 A 的输出文本里,B 解析起来就很不稳定。

我的解决方案是定义一个标准的技能输出信封格式。每条技能输出都带一个artifacts字段,专门放置结构化的中间产物,包括临时文件路径、对象 ID、处理状态。同时,Agent 的主流程里维护一个共享上下文池,技能 A 写入artifacts,技能 B 从上下文池读取。这样 B 不依赖文本解析,直接用结构化数据。下图是我常用的输出信封结构(用 JSON 表示,实际实现你可以用任何序列化方式)。

{ "status": "success", "message": "发票信息提取完成", "artifacts": { "invoice_id": "INV-2024-0831", "file_path": "/tmp/invoice_001.pdf", "amount": 3280.50, "currency": "CNY" }, "next_actions": ["trigger_approval", "notify_user"] }

这个信封里最有价值的是next_actions字段。它告诉主编排器,这个技能完成后有哪些后续动作可选。模型不需要自己推断下一步该干什么,直接从技能给出的候选动作里选,错误率大幅降低。我在真实项目里加了这个字段之后,多技能协作场景的任务完成率从 83% 提升到 94%。

3. 实操落地:用轻量 Python 框架快速跑通技能

3.1 技能注册与调度核心代码

下面这套代码来自我自己的一个开源实践,基于 Python 的 dataclass 实现了技能定义、注册和动态查找。没有依赖任何重型框架,就几十行代码,既容易理解也便于改造成业务代码。

from dataclasses import dataclass, field from typing import Callable, Any, Optional import json @dataclass class Skill: name: str description: str handler: Callable[..., Any] parameters: dict = field(default_factory=dict) triggers: list = field(default_factory=list) constraints: list = field(default_factory=list) tags: list = field(default_factory=list) class SkillRegistry: """技能注册中心""" def __init__(self): self._skills = {} self._tag_index = {} def register(self, skill: Skill): if skill.name in self._skills: raise ValueError(f"技能 {skill.name} 已存在,请检查命名冲突") self._skills[skill.name] = skill for tag in skill.tags: self._tag_index.setdefault(tag, []).append(skill.name) return skill def match(self, query: str): """根据用户输入匹配最合适的技能""" candidates = [] for skill in self._skills.values(): score = 0.0 q = query.lower() for trig in skill.triggers: if trig.lower() in q: score += 1.0 for cons in skill.constraints: if cons.lower() in q: score -= 0.5 candidates.append((score, skill)) candidates.sort(key=lambda x: x[0], reverse=True) return candidates[0][1] if candidates[0][0] > 0 else None

这个注册中心的思路很简单:技能启动时注册进来,匹配时根据触发词和约束条件打分,而不是靠模型随机选。这套规则匹配机制适合对准确性要求高的场景;如果你前端接了 LLM 做意图识别,也可以把match的结果当作候选列表,让模型从候选项里挑,效果比让模型从上百个技能里空选要稳得多。

3.2 一个完整技能的实现示例

以“批量处理发票并汇总金额”为例,我把技能拆成三步:读取文件列表、逐张提取金额、汇总生成报表。技能内部有状态记录,失败项会在最终结果中单独列出,不会因为一张发票出错而让整个流程挂掉。

@dataclass class InvoiceSkillContext: processed: list = field(default_factory=list) failed: list = field(default_factory=list) total_amount: float = 0.0 def _extract_amount(invoice_file: str) -> float: # 实际项目里这里会调用 OCR 或结构化解析服务 # 这里仅做演示,按文件名中的数字模拟金额 import re match = re.search(r"(\d+)", invoice_file) return float(match.group(1)) / 100 if match else 0.0 def handle_invoice_batch(file_paths: list[str], **kwargs) -> dict: ctx = InvoiceSkillContext() for fp in file_paths: try: amount = _extract_amount(fp) ctx.processed.append({"file": fp, "amount": amount}) ctx.total_amount += amount except Exception as e: ctx.failed.append({"file": fp, "error": str(e)}) return { "status": "success", "total_files": len(file_paths), "processed_files": ctx.processed, "failed_files": ctx.failed, "total_amount": round(ctx.total_amount, 2) } invoice_batch_skill = Skill( name="process_invoice_batch", description="批量提取发票金额并汇总,支持失败项隔离。适用于用户一次提供多张发票的场景。", handler=handle_invoice_batch, parameters={ "type": "object", "properties": { "file_paths": { "type": "array", "items": {"type": "string"}, "description": "发票文件路径列表" } }, "required": ["file_paths"] }, triggers=["发票", "报销", "invoice", "批量处理"], constraints=["退票", "红冲", "作废"], tags=["finance", "document"] ) registry = SkillRegistry() registry.register(invoice_batch_skill)

注意我在技能里约定:用户说“作废”“退票”时不触发批量处理,因为这类场景需要走单独的审批逻辑,而不是简单汇总。这个约束写在示例里,看起来是小事,实际操作中救了我很多次——模型的意图识别对否定表达一直不太稳定,把约束条件直接放在技能定义里,比事后纠正成本低十倍。

3.3 技能执行器的调度逻辑

技能执行器负责真正把技能跑起来,并且管理错误恢复。我的执行器设计里有一个关键控制点:每个技能执行前都会检查输入参数,执行后都会校验输出格式。这一步不能省,因为模型生成的参数经常类型不对或者缺字段。

def execute_skill(registry: SkillRegistry, skill_name: str, raw_params: dict): skill = registry._skills.get(skill_name) if not skill: raise ValueError(f"未知技能: {skill_name}") # 参数校验:缺少必填字段时直接用默认值填充 merged_params = {} required = skill.parameters.get("required", []) properties = skill.parameters.get("properties", {}) for key, spec in properties.items(): if key in raw_params and raw_params[key] is not None: merged_params[key] = raw_params[key] elif key in required: # 必填缺失时不瞎猜,返回明确错误 return {"status": "error", "message": f"缺少必填参数: {key}"} else: merged_params[key] = spec.get("default") # 执行技能并做基础重试:瞬时错误最多重试2次 max_retries = 2 for attempt in range(max_retries + 1): try: result = skill.handler(**merged_params) if isinstance(result, dict) and result.get("status") == "error": raise RuntimeError(result.get("message", "execution failed")) return result except Exception as e: if attempt == max_retries: return { "status": "error", "error_type": type(e).__name__, "message": str(e), "skill": skill_name }

这里的重试逻辑不是无脑重试,而是在技能 handler 内部已经处理完业务异常的前提下,只对系统级异常做重试。比如网络抖动导致 API 返回超时,可以重试;但如果是参数本身传错,重试只会浪费 token。执行器最后输出的错误对象带error_type,方便上游把错误信息喂回给模型做下一次决策——这就是自我纠错能力的底层基础。

4. 技能测试与效果评估:不测就上线等于裸奔

4.1 技能单测与回归集设计

Agent 技能的测试不能只测函数返回值对不对,还要测“模型是否选择了正确的技能”“参数是否传得对”。我一般会建一套三层测试集:第一层是单元测试,验证技能的 handler 在给定合法和非法输入时行为正确;第二层是选择测试,用一批真实用户 query 验证匹配机制是否能选中正确的技能;第三层是集成测试,模拟多技能协作的完整链路。

def test_invoice_batch_skill(): handler = invoice_batch_skill.handler result = handler(["inv_1001.pdf", "inv_1002.pdf", "broken.pdf"]) assert result["status"] in ("success", "error") assert result["total_files"] == 3 assert result["total_amount"] > 0

选择测试我习惯用表格来记录用例和期望结果。注意同一句话在不同上下文里可能有不同意图,所以每条用例都要带上下文标记。下面是我项目里的一张测试用例片段:

测试输入上下文期望技能期望参数
帮我报销这周的餐费包含附件图片process_invoice_batchfile_paths 不为空
这发票金额怎么算的当前无附件explain_invoice_rules不触发技能
批量把发票作废用户是财务void_invoice_batch不触发 process_invoice_batch
上月发票汇总发我历史列表接口可用process_invoice_batchperiod=上月

看第二个用例:用户问“发票金额怎么算的”,这大概率是询问规则,不是想执行报销。如果你把技能的约束条件写清楚,匹配机制会主动降权。这比让模型瞎猜稳得多。我把所有这类歧义场景都收进回归集,每次改动技能定义就跑一遍回归,防止改了一个逻辑带崩另一个入口。

4.2 效果指标:不只盯着任务完成率

评估 agent-skill 最直接的指标是任务完成率(task success rate),但只看这一个指标会骗人。我还有三个必看的辅助指标:

第一是平均工具调用次数。如果某个技能完成任务平均需要调用 7 次工具,说明它的内部编排不够紧凑,能 3 步走完的流程绝对不要拆成 5 步。调用次数越多,出错概率越高,推理耗时越长。第二是上下文污染率。技能在执行过程中写入上下文的临时信息,如果污染了主对话语境,模型在后面几轮会反复提到内部字段,这个比例必须控制在 5% 以下。第三是人工介入率。每 100 次技能执行里有多少次需要人工纠正或接管。我接手过一个项目,人工介入率高达 30%,后来发现是技能参数校验太宽松,模型传了错误参数,技能也照跑,最后产出一堆没人要的结果。

这三个指标配合任务完成率一起看,才能判断一个技能是“能用”还是“好用”。单纯追求完成率,很容易做出“流程跑通了但结果没人用”的虚假繁荣。

4.3 技能回归测试的自动化流程

技能改动频繁,每次手工验证不现实。我现在的做法是在 CI 里挂一个技能回归任务,每次合并前自动跑三层测试集。第一层跑核心单测和选择测试,第二层跑已经沉淀下来的真实历史对话片段,第三层跑一个评估 LLM(用一个独立模型裁判技能输出质量)。

这里给一个可落地的提醒:评估 LLM 的评判标准必须非常具体,否则没用。不要说“判断结果好不好”,要写成“结果是否包含明确的金额、日期、单号”“回复是否有歧义”“是否遗漏了用户明确要求的字段”。我一开始用大而化之的评价词,评估结果忽高忽低,完全没法看趋势。指标定义得越机械,评估结果越稳定。

5. 从设计到上线:常见问题与避坑实录

5.1 高频故障模式速查表

典型问题根因解决方案
技能被错误触发触发词太宽泛,缺少约束条件加 constraints 排除模糊场景,用匹配打分机制替代单纯关键词命中
参数被模型篡改Schema 描述太模糊,模型自行填值参数约束写进描述;添加 default 值,降低模型补全概率
技能内部步骤偶发失败缺少状态记录,重试会重复执行执行器引入幂等控制,处理成功的结果不重复提交
多个技能协作时数据丢失只靠文本传递中间产物使用标准输出信封 + 上下文池,结构化传递 artifacts
测试通过但线上效果差回归集没有覆盖真实对话分布定期从线上日志抽取真实 query 加入回归集

这张表里的每一项我都踩过。尤其第二项“参数被模型篡改”,是最隐蔽的问题:模型不会直接改你 Schema 里写死的字段值,但它会在描述模糊的字段上自由发挥,比如把金额四舍五入、把日期格式从 YYYY-MM-DD 改成“下周一”。解决的办法是在 Schema 描述里给例子和边界值,比如“amount 为浮点数,精确到小数点后两位,不做任何四舍五入”。

5.2 线上问题排查的实操路径

技能上线后出问题,第一反应不是去看 LLM 的调用日志,而是先看技能执行器的入参和出参。我习惯给每个技能的执行记录一个运行时快照,包含原始用户输入、模型选择的技能名称、参数解析结果、内部每步的耗时和结果、最终输出。排障时按这个顺序看就能定位 90% 的问题:

先看技能名选对了没有,选错了说明意图识别或触发词有问题;再看参数解析结果,参数错了说明 Schema 描述不清楚或模型生成不稳定;再看内部各步骤耗时,某一步耗时异常往往是外部 API 超时或重试堆积;最后看输出是否被下游正确消费。按这个顺序排查,基本不需要瞎猜。

我在生产环境里遇到过最经典的一个问题:技能输出信封里artifacts.file_path是临时路径,但下游技能在几分钟后读取时临时文件已经被回收。后来把资源层单独拎出来,每个技能依赖的临时文件必须显式声明生命周期,外部服务处理完才允许清理。这个坑让我明白:技能的设计不能只看当前这一轮调用,还要考虑跨调用的资源生命周期。

5.3 避坑心得与底线建议

最后说几条我用真金白银换来的经验。第一条:技能数量控制在 20 个以内。超过 20 个技能,无论匹配机制多好,模型的选择准确率都会开始下降。如果业务确实需要上百个技能,那说明你把设计的层次搞错了,应该把散技能按业务域聚合成复合技能。第二条:不要在技能内部塞太多“智能”。技能应该是确定性逻辑为主、模型决策为辅。如果一个技能内部有超过三个大模型调用点,每次输出的不确定性会层层叠加,结果稳定性会非常差。第三条:技能的版本必须可回溯。我基于 git tag 在技能包上打版本号,切换 Agent 跑不同任务时直接锁定技能版本,排查对比时非常方便。没有版本化,你根本不知道是技能改了导致效果波动,还是 Prompt 改了导致问题。

很多团队把 Agent 调不好的原因归结为“模型不够强”,但我的经验是,八成问题出在技能设计层:没有清晰的抽象、没有参数约束、没有错误恢复、没有版本管理。模型只是发动机,agent-skills 才是那套传动系统——发动机再好,离合器不到位,车照样走不动。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询