ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent技能体系实战:从提示词到可复用技能框架

Agent技能体系实战:从提示词到可复用技能框架

如果你也在做 Agent,一定会遇到一个特别拧巴的阶段:模型明明很聪明,什么都会一点,但真让它独立干活,它连个表格都整理不好。我前前后后折腾过好几个项目,最大的体会是——真正决定 Agent 上限的,不是模型参数,而是你给它的那套“技能”。

“agent-skills”说起来是个挺新的词,但本质上就是在回答一个问题:怎么把模型的能力,变成能稳定复现、可组合、可验收的实际动作。它介于提示词工程和完整的代码框架之间,既不需要你从零写一个复杂的运行时,也不能靠几句提示词糊弄过去。这篇就聊聊我对 Agent 技能体系的理解,以及从 0 到 1 落地一套技能框架时,值得你留意的细节。

适用人群很明确:正在做 Agent 应用开发,想让模型真正调用工具、执行任务、稳定产出结果的开发者。无论你用的是市面上哪个大模型框架,还是自己封装了一套调用链,这篇文章讲的设计思路和踩坑经验,应该都能直接抄作业。

1. 重新认识 Agent 的“技能”:它到底是什么

1.1 从“能聊天”到“会干活”的关键一跃

很多人对 Agent 的理解还停留在“一个能对话的机器人”。但真正到了生产环境,你会发现问题完全不一样。用户说“帮我整理一下这个月的报销单”,模型如果只是回复一大段话,告诉你“你应该把发票按时间排序、金额汇总”,那这就不叫 Agent,这叫聊天机器人。

Agent 的“技能”,就是把“说”变成“做”的能力封装。举个例子:同样面对整理报销单这个任务,有技能体系的 Agent 会自己去调用文件读取工具、表格处理工具、金额计算工具,最后生成一张整理好的电子表格给你。这里面每一步都是可追踪、可验证、可重复执行的。

我在做某个项目 X 的时候,一开始也想偷懒,把所有逻辑都塞进提示词里。结果就是:模型每次发挥都不稳定,同一个任务换个说法就歇菜。后来我才意识到,提示词是让模型“理解意图”,技能是让模型“执行动作”。两者缺一不可,但真正让 Agent 有价值的一定是可执行的那部分。

1.2 技能不是 API,也不是提示词

这三者经常被搞混,但它们的职责完全是两码事。

API 是一段可调用的代码接口,它只关心“传入参数、返回结果”。API 本身不理解业务语境,也不知道什么时候该被调用。技能则是在 API 之上多包了一层“智能壳”:它告诉模型这个接口是干什么用的、在什么场景下用、参数怎么填、出错怎么办。

提示词是给模型看的指令文本,它负责影响模型的输出风格和推理路径。但提示词有两个致命问题:一是不可程序化校验,二是不可独立测试。你把“调用发送邮件接口”写进提示词,模型可能理解成“写一封邮件草稿给用户看”,而不是真正去执行发送动作。而技能是结构化的,它有名字、有描述、有参数约束、有执行器、有错误处理,每一项都可以被单独测试和验证。

做一个不太严谨但很贴切的类比:提示词是“告诉实习生怎么做”的口头说明,API 是“别人已经焊好的电路板”,而技能则是“带说明书、带接口、带测试用例的标准零件盒”。零件盒里的东西可以被自由组合,也能被快速替换,这才是 Agent 真正需要的形态。

2. 技能体系的三大组成:工具层、编排层、校验层

2.1 工具层:把外部能力包装成标准接口

技能体系的底盘是工具层。它解决的是“Agent 到底能碰哪些东西”的问题。常见的能力包括发起 HTTP 请求、操作本地文件、读写数据库、调用第三方服务等。工具层要做的事非常枯燥但极其重要:给这些能力做统一的输入输出规范、统一的错误码、统一的超时熔断策略。

我见过不少团队直接把第三方 SDK 暴露给模型用,结局通常很惨。因为外部 SDK 的参数格式五花八门,返回的数据结构也不一致,模型被这些杂乱信息一干扰,参数生成就开始“自由发挥”。正确做法是包一层 adapter,把外部能力改造成一个标准函数:入参是 JSON,出参是 JSON,错误也统一格式。模型只需要学会一套规则,就能调用任何一种被包装过的工具。

另外,工具层一定要记录调用日志。每个技能什么时候被调用、传了什么参数、返回了什么结果、耗时多久,这些信息是你后面做排查和优化的唯一抓手。没有日志,出了问题就只能对着模型输出猜,那个滋味非常难受。

2.2 编排层:把技能穿成链路

单个技能能做的事情有限,真正复杂一点的任务都需要多个技能配合。编排层就是负责“把技能像拼乐高一样拼起来”的机制。

编排的核心问题是:谁来决定下一步调用哪个技能?两种思路我都试过。第一种是代码写死流程,也就是工作流:比如先调用“解析文档”技能,再调用“提取关键字段”技能,最后调用“写入数据库”技能。这种方式稳定、可控、容易测试,但缺点是笨重,每个新流程都要写代码。

第二种是让模型自己做路由决策,也就是动态编排:模型根据用户请求,自己决定先调哪个技能、再调哪个技能。这种方式灵活,能应对没见过的复杂指令,但代价是结果不确定性很大,需要你在框架层面做很多约束和护栏。

实际项目里,我的经验是混合编排。把那些执行路径固定、输入输出明确的环节做成显式工作流,同时留一个动态决策口给模型去处理意外分支。这套组合用下来,既保住了稳定性,又保留了灵活性。

2.3 校验层:给 Agent 的“手脚”装上刹车

校验层经常被忽略,但它恰恰是技能体系里最值钱的部分。Agent 一旦能动手做事,就意味着它能做错事,而且是在没人盯着的情况下快速做错。

校验要做两件事:执行前校验和执行后校验。执行前校验管的是参数合法性,比如邮件收件人格式对不对、金额是不是负数、日期是否在允许范围内。执行后校验管的是结果正确性,比如写入数据库是否成功、生成的文件是否非空、返回的状态码是否符合预期。

我给你说个真实翻车案例。我做过一个自动发通知的功能,模型在调用发送技能的时候,把号码参数填成了用户 ID,结果一连串无效发送。如果当时执行器里有一道校验“参数必须匹配用户表中的 mobile 字段”,这个错根本不会发生。从那以后我就定了一条规矩:凡是会产生外部影响的技能,一律要有二次确认或强校验。宁可多写几行代码,也不能让 Agent 在无人监管的状态下搞出不可逆的后果。

3. 实战:5 步搭建一个可复用的技能框架

3.1 第一步:定义技能描述与参数 Schema

技能框架的地基是“技能描述文档”。这份文档不是给人看的,是给模型看的路标。每次模型需要决策调用哪个技能时,靠着就是这些描述和参数约束。

一个标准技能定义大概长这样:

{ "name": "send_email", "description": "发送一封标准格式邮件,支持普通文本和 Markdown 正文。当用户要求给指定邮箱发送内容时使用。", "parameters": { "type": "object", "properties": { "to": { "type": "string", "description": "收件人邮箱,必须是合法邮箱地址" }, "subject": { "type": "string", "description": "邮件主题,不超过 100 字" }, "body": { "type": "string", "description": "邮件正文内容" } }, "required": ["to", "subject", "body"] } }

字段看起来简单,其实坑都在 description 里。模型的意图匹配能力高度依赖这里写的文字。写得太泛,模型会不知道该在什么时候用;写得太死,遇到变体表达就没法触发。我的经验是:description 里写清楚三个东西——这个技能是做什么的、在什么业务场景下用、有哪些边界条件和限制。

3.2 第二步:实现技能执行器

定义好描述文档之后,就要写真正的执行代码了。执行器就是一个函数,接收参数,返回标准结构。我习惯用 Python 写执行器,结构大概是这样:

def send_email_executor(params: dict) -> dict: # 1. 参数二次校验,防止模型传了不合法的值 email = params.get("to") if not is_valid_email(email): return {"success": False, "error": {"code": "INVALID_PARAM", "message": "邮箱格式不正确"}} # 2. 调用底层服务,这里包了一层统一接口 result = email_service.send( to=email, subject=params.get("subject"), body=params.get("body") ) # 3. 返回统一结构,success 字段一定不能省 if result["status"] == "ok": return {"success": True, "data": {"message_id": result["id"]}} return {"success": False, "error": {"code": "SEND_FAILED", "message": "邮件服务返回错误"}}

执行器有两条硬性要求。第一,所有返回都必须走{"success": bool, "data": ..., "error": {...}}这个结构,这样模型才能统一理解执行结果,不会因为格式不一致而推理出错觉。第二,执行器内部要自己处理超时和重试,不要把底层服务的超时错误原样抛给模型,模型处理不了那玩意儿,只会越绕越糊涂。

3.3 第三步:设计技能注册与热加载机制

技能不是散落各地让模型碰运气的,它需要一个注册中心统一管理。我做的项目里有一个skills目录,每个技能一个文件,启动时框架会扫描这个目录,把所有技能的描述合并成一份清单,一次性发给模型作为可调用的工具集合。

这里有一个性能与效果的权衡点。模型能看到的技能描述是占用上下文的,技能数量从 10 个涨到 100 个时,光描述就要吃掉不少 token。更麻烦的是,技能太多之后,模型选错的概率会直线上升。我试过一口气塞 80 个技能给模型,结果它经常懵圈,把“查询订单状态”和“查询物流轨迹”搞混。

后来的做法是给技能加标签和分组,先让模型从粗粒度分组里定位方向,再展开细粒度技能。相当于先问它是想处理“订单相关”还是“物流相关”,然后再给具体技能。这个机制不需要大模型推理太深,靠简单的关键字匹配就能做初筛,实测能大幅降低误选率。

3.4 第四步:上下文压缩与记忆裁剪

Agent 连续调用多个技能时,对话历史会像滚雪球一样膨胀。每个技能的描述、参数、返回结果、中间推理过程都会被塞进上下文,等到第 8 个技能执行完,光历史数据就已经接近 token 上限了。这时候模型要么开始遗忘早前的信息,要么直接报错。

解决办法是压缩,不是简单截断。我在框架里做了一个摘要层:每完成一个技能,就把这段交互提炼成一小段结构化的状态记录。比如“用户要求查询订单,查询结果是订单 #1024 状态为已发货”,这段内容只占十几个 token,但保留了完整任务状态。后续模型需要回溯时,读这段摘要就够了。

还有一个比较隐蔽的坑:模型的中间推理过程也会占 token,而且很多时候这些推理毫无保留价值。可以把推理过程设置为可见但不受收藏,也就是不让它进入后续轮次的上下文。这个操作能省下很大一部分空间,尤其适合那些工具调用次数多的任务。

3.5 第五步:技能评估与自动化回归

技能框架搭好之后,最容易被忽视的就是评估。没有评估体系,你根本不知道这次改模型参数到底是变好了还是变差了。

我用的评估方式分三层。第一层是离线断言,准备一批典型请求和期望的技能调用序列,跑完看实际调用是否匹配。第二层是半自动比对,让模型回答一些开放式任务,然后用另一个模型或规则去判定结果是否合理。第三层是线上指标,监控技能调用的成功率、平均耗时长尾分布。

每一层都要沉淀成回归用例库。每次修改技能描述、调整执行器逻辑,都要把整个用例库重跑一遍。这里面有个我特别想强调的点:用例要做版本管理,不要直接覆盖旧用例。因为 Agent 行为本身有随机性,今天跑挂一个用例,未必是代码改坏了,可能是非确定性波动。有历史版本做对照,你才分得清到底是谁的锅。

4. 技能编排里的三个关键设计选择

4.1 显式编排 vs 让模型自己编排

技能编排的第一道分岔路,就是代码写死还是模型自由发挥。这个选择没有绝对正确答案,但可以按任务特征来判断。

显式编排适合那些流程固定、步骤顺序有强依赖的任务。比如“导出发票-校验金额-生成汇总表-发送邮件”,这个链路里的每一步都不能乱序,用代码写死是最稳的。显式编排还有一个隐性好处,就是好排查。用户报问题时,你可以直接看到卡在了第几步,而不是让模型把整个流程都“推理”一遍。

让模型自己编排则适合探索型任务,比如“帮我把这个文档按主题整理成章节”。这种任务没有标准答案,每一步怎么走都在变化,写死流程等于自废武功。但模型自由编排的前提是,前面说的校验层要足够硬。不然模型可能会跳过关键步骤,或者用错误的参数去调用一个后果不可逆的技能。

我的建议是,别急着站队。先把手头最核心的两三个场景做成显式编排,保证用户体验的下限。然后再用动态编排去处理长尾场景,把上限打开。混搭确实会让架构复杂一点,但对用户来说是性价比最高的方案。

4.2 技能粒度:太碎会乱,太粗会废

技能粒度是另一个看起来不起眼、实际影响巨大的设计决策。我见过两种极端:一种把“发送邮件”拆成了“获取用户邮箱-生成邮件标题-拼接邮件正文-调用邮件接口”四个技能;另一种把一个完整“处理订单”流程做成一个技能,内部又臭又长。

第一种的问题在于,技能拆得太碎,模型要做太多决策,每一步决策都可能出错,错误率还会叠加。第二种的问题在于,技能内部逻辑写得太死,失去了技能的灵活组合价值,而且遇到需求变更是真的难改。

我后来总结出一套简单的粒度判断法:一个技能应该对应一个“人类员工能在 5 分钟内独立完成的动作单元”。比如“发送邮件”是一个动作单元,因为它做完就结束了;“处理订单”不是一个动作单元,因为它里面有好多子步骤,这些子步骤的排列组合方式可能多种多样。

按这个标准去拆,你会发现技能数量不会特别多,但又足够灵活。80 个技能的情况我从来没遇到过,大多数项目也就 15 到 30 个,这个数量对模型来说是最友好的区间。

4.3 失败重试与降级策略

技能执行失败是常态,不是异常。关键看失败之后怎么处理。

最简单的是直接重试。但要注意,重试不是无脑再来一次,要用指数退避原则,第一次失败等 1 秒再试,第二次等 2 秒,第三次等 4 秒,最多重试 2 到 3 次。这里有一个铁律:凡是写操作,重试之前必须先确认上一次请求是否真的没成功。因为有时候底层服务其实已经处理成功了,只是响应超时,这时候你再发一次,就会造成重复操作。解决办法是给每个技能请求带上一个唯一的幂等键,底层服务根据这个键去重。

重试不行就要降级。比如天气服务挂了,可以降级成返回默认文案“天气信息暂时无法获取”,而不是让整个任务卡死。又比如主用的支付渠道失败,可以降级成备选渠道。降级策略要提前写在技能定义里,不能等到出问题了再现场想。

最容易被忽略的是失败信息的上报格式。执行器返回的 error 信息不能太笼统,至少包含错误码、可读描述、建议的替代方案三块内容。模型拿到这段信息后,才有依据去调整参数或者换一个技能,而不是瞎猜。

5. 常见问题与排查技巧实录

5.1 模型明明有工具,却总选错技能怎么办

这个问题的第一反应别急着怪模型,先检查技能描述写得好不好。我排查过大量误选案例,八成以上都是描述含糊导致的。比如一个技能描述写“用于处理用户咨询”,另一个写“用于回答用户关于退款的问题”。模型遇到“我想退货怎么操作”,它自然会去调退款那个技能,但如果你的技能描述只写了“处理用户咨询”,它就可能二选一瞎蒙。

另一个常见原因是技能名称太像。“query_order”和“query_order_status”,模型不是看不懂,而是在长上下文里容易混淆。解决方法是给技能加“trigger_scene”字段,明确列举哪些说法会触发这个技能,甚至加上不会触发的反例。反例特别管用,比如在“send_email”技能的描述里加一句“注意:仅用于发送邮件,不要用于查询邮件列表”,模型选错的概率立刻降下来。

还有一个办法是调技能的权重或优先级。大多数框架允许给技能加一个排序字段,排列靠前的技能更容易被选中。把高频技能往前排,冷门技能往后放,效果立竿见影。

5.2 调用参数老是幻觉出多怎么办

模型生成参数时多传一些 schema 里没定义的字段,这个现象非常常见。比如你的“send_email”技能只定义了 to、subject、body,模型却多传了一个“cc”字段。执行器如果严格校验,就会报“参数不在 schema 中”,模型拿到报错后通常会自己修正。

这个机制本身就是纠正幻觉的有效手段。所以执行器一定要做严格的参数 key 校验,不能大度地忽略多余字段。忽略一次两次问题不大,时间久了模型会越来越放飞自我,因为它发现传错参数也没事。

为了减少模型产出多余字段的概率,可以在参数 schema 里给每个字段加 examples。模型在若干示例的引导下,会更容易按模板生成参数。另外,尽量不要在 schema 里放可传可不传的字段。三维可选字段会让模型的选择成本急剧上升,宁可把可能用到的字段都设为必填,再在描述里说明哪些情况可以填空值。

5.3 工具返回结果太大导致上下文爆掉

技能调用外部服务时,返回的数据常常超出预期。一次“查询所有客户信息”的操作,可能把几万字的数据全部塞进上下文,后续轮次直接撑爆。

这个问题最好在工具层解决,而不是靠事后清理。具体做法是给工具层加上限和分页。技能执行器内部先截断返回结果,只回传必要字段,比如分页后的第一页数据,每条记录只保留关键字段。更高级一点,可以让技能在返回前先做一个本地摘要,用最少的 token 表达核心信息。比如把一万条订单记录压缩成“共 10000 条,总金额 25 万,按金额排序的前 10 条为……”。

这个经验是我被教育过之后才长记性的。有一段时间我总在上下文侧做截断,结果模型每轮都要问“你刚才给我的数据不全,能再补充一下吗”,来回浪费了好几轮 token。后来老老实实把数据加工搬到工具内部,类似问题再没出现过。

5.4 技能回归测试里的“幽灵失败”

回归测试没过,去看代码又看不出任何问题,重新跑一遍又全绿了。这种“幽灵失败”在 Agent 项目里几乎是每周都会遇到的事,原因主要有两个:模型输出的随机性和外部依赖的波动。

处理方式分两步。第一步是给结果比对增加容错。不要用 AI 生成的结果文本和期望文本做严格相等比较,而应该做关键字段匹配。比如验证邮件发送技能,只要到收件人、主题、邮件 ID 都对上了,就算通过。正文有时候模型会自己优化措辞,完全一样反而很不现实。

第二步是引入多次运行的通过率概念。同一个用例连续跑 5 遍,4 遍通过 1 遍失败,不一定是 bug,更可能是随机波动。真正需要警惕的是同一个用例连续失败 3 次以上。另外,测试用例要固定模型版本和采样参数,不然你连失败原因都定位不到。我之前就吃过这个亏,同一套用例换了个模型版本,结果页面全红,我那叫一个崩溃,后来一查是评分基准都漂移了,根本不是功能坏了。

最后再聊几句实在的

技能框架这个东西,真要做起来没有什么神秘魔法,无非是工程量加细心。我个人的经验是:不要一开始就想着设计一个完美的通用框架,先从两三个核心场景切入,把技能定义、执行器、校验、评估这一整套闭环跑通,再慢慢往里面加场景。每加一个技能,都要让它经过整套流程的考验,而不是写完函数就丢进去。

另外就是日志这件事,我念叨再多也不为过。技能执行过程一定要把所有关键节点都打成结构化日志,包括模型输入、模型选择的技能、生成的参数、执行器返回、校验结果。没出问题的时候,这些日志是废纸。一旦出问题,这些日志就是你唯一的救命稻草。我很少信任“我觉得模型在这里理解错了”这种判断,我只信日志里实际记录下来的东西。

最后分享一个我后来才悟到的原则:技能的本质是降低模型做事的难度,而不是增加模型做事的难度。每加一层封装、一道校验、一套描述,你都要问自己,它到底是帮模型更清楚自己该干什么,还是在给模型添乱。保持这个视角,你的技能体系就会越做越顺手,而不是越做越沉重。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进