☰
Agent技能体系设计:定义、调度与可观测性全解析
2026/10/21 13:08:52 网站建设 项目流程

最近一两年里,“Agent”这个概念在大模型圈子里几乎被聊烂了,但真正落到项目里跑起来,很多人会撞上一堵透明的墙:模型调用单个工具已经学得有模有样,可一旦让它多步骤协作、跨模块完成一个真实任务,就开始频繁出错、忘参数、答非所问。

我参与过不少Agent原型项目,其中一个模拟项目X的结论让我印象很深:同一套底层模型,单工具调用成功率能到90%以上,但组合成5步以内的真实业务流程后,成功率直接掉到40%附近。问题出在哪?我在后来的复盘里发现,大家的精力都花在选模型、买算力、写Prompt上,唯独漏了最基础的一层——你根本没有给Agent一套真正可用的“技能体系”。

这里说的“agent-skills”,不是给Agent塞一堆API文档,也不是写几条漂亮的Prompt模板,而是一套从技能定义、调度执行到故障恢复的完整机制。这篇博文,我想围绕我在这类项目里实际搭建技能体系的过程,聊聊它到底解决了什么问题、核心怎么设计、又会踩哪些坑。内容偏实践,适合正在做Agent产品原型、或者想把自己的工具集升级成“可被Agent调用的技能库”的开发者。

1. 为什么模型变强了,Agent反而更“笨”了:技能缺失的真相

过去我们做自动化脚本,流程靠代码写死:第一步调A接口,第二步解析结果,第三步调B接口。每一步都确定,不存在“语义理解”的需要。但Agent不一样,它的核心价值是用自然语言理解目标,再自己规划执行路径。这意味着,Agent面对的每一个环节,都必须有足够清晰的“操作指引”。

我在某次调试一个客服场景Demo时就遇到过这种事。模型已经能很好地把用户问的“帮我查一下上个月订单”转成意图,也能识别出“上个月”是个时间范围,“订单”是个实体对象。可下一步呢?它不知道该调用哪个查询服务、该传哪几个必填参数、返回结果里哪些字段是关键信息、超时了几次要不要重试。模型自己“猜”了一版调用,结果不是缺参数就是传错类型。

这不是模型能力不行,而是周围没有任何“技能定义”告诉它怎么使用手上的工具。大多数团队做Agent,第一步就是把工具函数列出来,告诉模型“你有这些函数可用”。这个做法对单轮、简单工具有效,对复杂任务基本失效。

原因在于,一个真正可用的Agent技能,至少要包含下面这些层面:

  • 触发条件:什么场景下该使用这个技能,和哪些技能容易混淆,如何区分。
  • 参数Schema:每个参数的语义、类型、取值范围、默认值、必填性。
  • 执行上下文:这个技能适合在任务的哪个阶段被调用,依赖哪些前置状态。
  • 失败语义:返回什么状态表示成功,什么状态表示业务失败,什么状态表示系统异常。
  • 后置动作:调用完成后,什么样的结果可以继续驱动下一步规划,什么样的结果该人工介入。

现在回看那个模拟项目X,Agent每个工具调用都是“裸奔”的,没有以上任何一条约束。成功率低不是玄学,是必然。

我把这种差距叫“技能断层”:底层模型的推理能力已经很强,但你如果不在中间层把它需要的“操作技能”显式组织好,它的大脑再聪明也不知道怎么动手。想想一个刚入职的实习生,聊起业务框架头头是道,真要他去拿一个订单号查流程状态,他连系统入口在哪儿都不知道——不是人笨,是没人给他干活要用的SOP。

所以,agent-skills的核心定位,不是“给Agent加一堆函数”,而是“为Agent建立一套结构化的、可被理解和校验的执行能力档案”。这也是后面所有设计的前提。

2. 技能的本质:它是一份“给模型读的操作手册”,不是一个函数列表

很多初次接触这个概念的开发者会问我:我直接把函数的docstring写全,不也等于给Agent文档了吗?真要这么做,效果通常不太好。docstring是给人看的,Agent读起来是“自助餐”,可选择吃哪个,但不知道哪道菜配哪道菜。

技能(Skill)的定义应该是“一个带语义边界的行动单元”:它的粒度介于单个函数和完整的业务流之间。以这份思维重新组织工具之后,Agent看到的不再是“可用函数30个”,而是“技能6个,每个技能内部封装若干步骤”。

我自己习惯用一套类似下面的结构来定义技能:

  • 技能名称:短、唯一、语义聚焦,方便模型在规划里引用。
  • 技能描述:一段话说明这个技能解决什么问题、在什么条件下使用。
  • 参数定义:严格的结构化Schema,附上示例值。
  • 内部动作:一个技能可以包含多个底层操作,对外只暴露一个入口。
  • 前置依赖:在调用它之前,需要具备什么状态。
  • 输出约定:成功/失败/异常如何表现。

假设我们做一个“订单查询”技能,内部可能包含:鉴权、路由到正确的订单服务、拼装返回结果。对外暴露时,模型只需要知道“我要查订单,传入订单号,就返回订单状态”。至于订单服务在两个地区有两套接口、需要根据订单号前缀区分路由,这类细节完全封装进技能内部。

这就是技能体系与函数调用的关键区别:内部复杂度被结构化地“藏”起来,外部语义足够简单。

从模型的角度看,本来要理解30个工具的差异、记忆每个工具的20个参数,现在只需要复用几个清晰技能。Agent规划负担低了,成功率自然上来。我在这类项目里通常会把“技能描述”控制在一两句话内,最多不超过三句。写得越长,模型越容易迷失。

给技能定义加上的核心字段,大致会是这样一份结构(我常用YAML描述,后面会有完整案例):

字段作用示例
name技能唯一标识order_query
description面向模型的语义说明根据订单ID查询订单当前状态和物流进度
parametersJSON Schema 风格的参数约束order_id: string, required
required_context需要依赖的对话/系统状态用户身份已确认
side_effects是否产生写操作false
max_retry失败重试上限2

字段看起来简单,但每个字段背后都有讲究。比如“required_context”这个字段,很多初版设计里没有。可实际测试中,我发现如果不做前置条件约束,模型很可能在用户身份还没确认时就去查别人的订单,这在真实业务里是安全事故级别的漏洞。技能系统不该只解决“能不能调用”,还要解决“该不该调用”。

3. 从零搭一套可落地的技能注册与调度模块

讲完理论,我们直接上手。我在某次内部尝试里,用Python实现过一套轻量技能注册框架,核心大约两百多行代码。这里不谈任何特定框架,只谈通用闭环:定义技能、注册技能、模型规划、执行器调用、结果回传五个环节。

3.1 先定义一份YAML技能清单

skills: - name: order_query description: "根据订单编号查询订单当前状态与物流信息。适用于用户询问'我的订单到哪儿了'、'订单什么状态'。" parameters: type: object properties: order_id: type: string description: "订单编号,通常形如 ORD-2025-0001" include_logistics: type: boolean default: false required: - order_id required_context: - identity_confirmed side_effects: false timeout_ms: 5000 max_retry: 2

为什么把参数设计得这么“啰嗦”?因为模型规划时要依据参数名推测含义。include_logistics这个名字不如include_logistics配上一句“是否返回物流轨迹明细”来得清晰。参数默认值也能减少模型乱传值。给include_logistics设定default: false,模型在不确定时不会瞎猜。

3.2 用Python实现注册与执行器

from dataclasses import dataclass, field from typing import Any, Callable, Dict, List, Optional import json, time @dataclass class Skill: name: str description: str parameters: dict executor: Callable[..., Any] required_context: List[str] = field(default_factory=list) side_effects: bool = False timeout_ms: int = 5000 max_retry: int = 2 class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] = {} def register(self, skill: Skill): if skill.name in self._skills: raise ValueError(f"技能重复注册: {skill.name}") self._skills[skill.name] = skill def unregister(self, name: str): self._skills.pop(name, None) def get_skill(self, name: str) -> Optional[Skill]: return self._skills.get(name) def all_skills(self) -> List[dict]: # 提供给模型的精简技能清单,只保留语义层信息 return [ { "name": s.name, "description": s.description, "parameters": s.parameters, } for s in self._skills.values() ] def execute_skill(skill: Skill, args: dict, context: dict) -> dict: # 检查前置上下文 for req in skill.required_context: if not context.get(req): return { "success": False, "error_type": "CONTEXT_MISSING", "message": f"缺少技能执行前置条件: {req}", } # 参数校验(简单版) required_params = skill.parameters.get("required", []) for rp in required_params: if rp not in args or args[rp] in (None, ""): return { "success": False, "error_type": "PARAM_MISSING", "message": f"缺少必填参数: {rp}", } # 带重试的调用封装 last_error = None for attempt in range(skill.max_retry): try: start = time.time() result = skill.executor(**args) elapsed = (time.time() - start) * 1000 if elapsed > skill.timeout_ms: return { "success": False, "error_type": "TIMEOUT", "message": f"执行超时,耗时{elapsed:.0f}ms", } return {"success": True, "result": result} except Exception as exc: last_error = str(exc) time.sleep(0.5 * (attempt + 1)) return { "success": False, "error_type": "EXECUTION_FAILED", "message": last_error, }

我把“前置上下文检查”和“参数校验”放在执行之前,而不是靠模型自觉。模型难免漏传参数,这些兜底逻辑是Agent任务稳定性的基石。重试也不是盲目重试,而是设计成指数退避模式,避免在一次任务里对下游接口造成瞬时压力。

3.3 模型怎么找到对的技能

规划环节,我通常不去自己写路由规则,而是把技能的name、description、parameters精简后直接提供给模型,让它返回结构化的技能调用意向。换句话说,技能注册表就是模型的“菜单”。

def generate_plan_instruction(registry: SkillRegistry) -> str: skill_list = registry.all_skills() return ( "你有以下技能可供调用,请根据用户需求选择合适的技能并给出参数JSON:\n" + json.dumps(skill_list, ensure_ascii=False, indent=2) )

这个“菜单”里不能放内部执行函数、不能放太多超时/重试逻辑,否则会让模型混掉。菜单给模型看的是“语义”,执行器内部才关心具体机制。

我自己常用的一种做法是:让模型输出一个包含skill_name和arguments的JSON结构,然后执行器根据skill_name到注册表里取技能,做校验后执行。这样一个相对干净的流程,对模型的要求最低,也最不容易出结构混乱。

3.4 返回结果的标准化,直接影响后续规划

技能执行完的结果,很多初版系统直接丢回给模型:“这是API返回的JSON原文”。这不行。比如JSON里有大段嵌套结构,模型下次规划时要从中提取“状态”字段,容易提取错。

我的做法是额外增加一层“结果意图化”:

def normalize_success_result(raw_output: Any) -> dict: return { "status": "OK", "biz_state": "已发货", "biz_message": "订单位于运输途中", "data": raw_output, } def normalize_failure_result(error_type: str, message: str) -> dict: return { "status": "ERROR", "error_type": error_type, "message": message, }

模型看到biz_state: 已发货,比看到一长串物流码更可能做出正确下一步。标准化是“模型友好”的核心实践。我们在模拟项目X里做过一次小实验,仅仅是把结果从原始JSON改成“状态 + 摘要 + 原始数据”的结构,下游步骤规划错误率降低了约25%。这个改动不涉及任何模型升级,完全是工程层的功劳。

4. 技能编排:多技能协同时的调度、优先级与冲突处理

单技能调用只是基础,真实业务极少靠一个技能打天下。更常见的场景是:用户说“我想看看昨天下的订单到没到”,Agent需要先调用身份验证技能,再调用订单列表技能,再调用订单详情技能,甚至还要关联天气预案决定是不是要发延迟提醒。多技能协同起来,问题就从“怎么执行”变成了“怎么决定执行顺序”。

4.1 技能的依赖编排

我给技能定义里加过一类字段叫after和before,用来声明执行顺序关系。后来发现硬编码顺序太死,模型规划能力遭到压制。更合理的方式是:把依赖信息放进“前置条件”,让模型根据上下文自然编排。比如“订单详情查询”技能声明前置条件是order_id_known,那么模型如果发现当前上下文中没有订单ID,就会自动先查列表,再进详情。

这样既保障了逻辑顺序,又不至于把Agent写成一棵流程树。模型本质上是在做动态规划,技能系统只负责给它提供“可用节点”和“约束信息”。想让Agent灵活,就要相信规划层的能力,不要在技能层把所有路径堵死。

4.2 决策冲突:两个技能都能完成同一个目标,选哪个

实践中最常见的冲突是“查余额”和“查明细”都能回答“我卡里还有多少钱”。这类冲突,靠模型自己临场发挥不稳定。我建议在技能定义上直接明确适用边界:

  • 查余额:只返回可用余额,不返回流水。
  • 查明细:返回近期流水,可推导余额。

描述写得足够清晰,模型在多数情况下能自行选择。若还是容易混,可以在技能清单的“菜单”里把这两个技能描述改成互斥语气:“当用户只咨询金额时使用该技能;当用户提到交易记录时使用另一技能”。

4.3 共享技能与私有技能

我在一个模拟多Agent协作的项目里,还尝试把技能注册表分成了两层:共享层和私有层。共享层放着公共能力,比如订单查询、物流查询;私有层放着当前Agent专属的逻辑,比如客服Agent的“退款预审”。

这套设计的价值很明显:技能即权限边界。一个技能能不能被某个Agent调用,在注册表里就能控住,而不需要去改业务代码。安全审计的时候,只要看每个Agent挂在注册表上能访问哪些技能即可。对做企业级Agent的团队来说,这比“告诉模型别乱调”可靠得多。

多Agent场景下,技能系统的角色不只是一个“工具箱”,而是协作的语义枢纽:技能被谁注册、被谁调用、产生什么副作用,必须有日志可查。我在后面第5节会展开讲可观测性设计。

5. 可观测性:技能调用的日志链路与沙箱验证

技能系统上线后,最怕的是“黑盒”:模型说它调用了一个技能,可是调用时传了什么参数、执行器做了什么、结果为什么异常,全都没有记录。一旦Agent在复杂任务里出错,排查难度不亚于大海捞针。

5.1 给每次技能调用生成唯一的调用ID

我在执行器入口处会生成一个skill_call_id,类似sk-7f3a91c2-20250101-001。这个ID串起整条调用链:模型规划记录、技能参数快照、执行器日志、返回结果、耗时、重试次数。每次调用都会写入结构化日志,至少保留30天。

{ "skill_call_id": "sk-7f3a91c2-20250101-001", "skill_name": "order_query", "model": "gpt-4o-mini", "params": { "order_id": "ORD-2025-0001", "include_logistics": true }, "context_snapshot": { "identity_confirmed": true, "conversation_id": "conv_8821" }, "result": { "status": "OK", "biz_state": "已发货" }, "latency_ms": 231, "retry_count": 0 }

这个日志不只是调试用,也是后续优化技能描述的重要依据。比如某天我发现order_query这个技能里,模型频繁把order_id传成“用户ID”,这就是技能描述有歧义的信号,需要回炉改描述。

5.2 验证环境与沙箱隔离

技能代码的验证,我强烈建议做到“三层”:

  • 第一层:本地单测,只测执行器函数本身,不接真实服务。
  • 第二层:沙箱环境,用Mock数据模拟模型调用真实技能接口。
  • 第三层:生产灰度,只对白名单用户开放新技能。

我在某次项目里跳过沙箱,直接把新技能上到灰度环境,结果技能内部因为第三方接口返回格式变化导致批量报错,还把错误数据写脏到下游存储。从那以后,沙箱验证成了硬规矩。

沙箱验证里我会自动跑这四类用例:

用例类型示例场景预期结果
正常成功传合法的订单号返回业务成功
参数缺失不传订单号返回PARAM_MISSING
外部异常第三方服务返回500进入重试,重试2次后返回EXECUTION_FAILED
上下文缺失未确认身份时调用返回CONTEXT_MISSING

这些用例看起来简单,但能把大量低级错误挡在生产之前。只要沙箱里有一类用例不通过,就不允许把技能注册进生产环境的注册表。

5.3 数值化的技能健康度

我自己还会定期看每个技能的“健康度评分”,公式大概是这样:

健康度 = 正常返回率 * 0.5 + 平均耗时达标率 * 0.3 + 首调成功率 * 0.2

正常返回率和首调成功率是关键。如果首调成功率低,说明技能描述或参数Schema让模型困惑,需要在语义层优化,而不是加更多重试。平均耗时主要反映外部依赖质量,耗时过高要排查下游服务。这个健康度评分会在每次灰度评审时展示给团队,谁负责的技能分数低,谁就去优化文档和Schema。

6. 那些让技能体系崩溃的细节:参数命名、描述风格、粒度控制

最后这部分,聊几个我们在多次项目迭代里总结出的“看不见但致命”的细节。

6.1 参数命名不一致是最大的隐性坑

同一个含义,有的技能叫order_id,有的技能叫orderNo,还有的技能叫id。模型面对这种混乱命名,100%会传错。建议一套系统里统一命名规范:实体名都用小写+下划线,user_id、order_id、product_id,避免缩写歧义,禁止无上下文的id。字段语义要做到“一看就懂”,不给模型留想象空间。

6.2 技能描述别写成长篇小说

有些开发者喜欢把技能描述写得面面俱到,每个细节都塞进去。结果模型遇到长描述时,注意力被稀释,反而抓不住关键触发条件。我的经验是:

  • 第一句:什么时候用。
  • 第二句:做什么事。
  • 第三句(可选):什么人/什么场景不能用。

不超过三句话,效果最好。咱们的目标是给模型做“菜单”,不是写需求文档。

6.3 技能粒度:太料理包难组合,太颗粒无意义

技能粒度决定Agent的灵活度。如果你把“查询订单 + 计算运费 + 生成退款单”绑死在同一个技能里,那它就无法单独复用“查询订单”这一步。反过来,如果每个动作都拆成一个技能,模型规划时又会陷入选择困难。

我一般用“这个技能是否能独立回答一类用户问题”来衡量。不能独立回答用户问题的,就不是技能,而是技能内部动作。比如“校验手机号”不能独立回答“我的订单到哪了”,所以它应该封装进“订单查询”技能内部,而不是暴露给模型。

6.4 别让技能系统怀疑自己的设计

最后还有一点想说。好多人做技能系统,做着做着就开始怀疑:是不是我这个设计不行?是不是应该换成某商业平台的能力?我觉得,技能体系这个思路本身没有过时,问题往往出在实现细节上没有耐心打磨。我在内部测试中见过太多“半途而废”的原型了。系统的失败通常不是架构失败,而是细节失败:参数描述不清、粒度失衡、上下文漏了、日志没留。如果能把前面讲到的每一层都踏实落地,哪怕模型版本旧一点,整体效果依然能打个不错的水平。

以我的个人体验来看,给Agent写技能,最后拼的其实不是代码能力,而是“把一件复杂事情解释清楚的能力”。你对业务理解得越透,技能结构就越清晰,模型跑起来就越顺手。技能体系不会替Agent变聪明,但它决定了一个聪明的大脑能不能把聪明用在正地方。

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

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

立即咨询