☰
大模型Agent技能化实战:从Function Calling到上下文管理的完整指南
2026/10/8 11:16:31 网站建设 项目流程

把大模型“调教”成能干活的智能体:agent-skills 技能化设计实战笔记

做了快两年大模型应用开发,我有一个很深的体会:现在市面上讨论 Agent 的文章,十篇里有八篇在讲怎么搭框架、怎么接模型、怎么写 ReAct 循环,但真正让 Agent 从“演示能用”变成“生产能打”的,往往是那些不起眼的细节——模型在什么情况下该调用哪个工具、调用的参数怎么传、传错了怎么纠偏、多个工具之间怎么配合。这些细节往深了走,就会触及一个核心概念:技能(Skills)。

最近我在折腾一个内部项目,代号就叫 agent-skills,目标很直接:把模型的能力封装成一套可复用的“技能包”,让智能体像搭积木一样组合出复杂行为。折腾了小两个月,踩了不少坑,也总结了一些比较实用的设计思路。这篇就把我的完整思考、实现方案和排坑记录分享出来,给正在做 Agent 应用开发、尤其是被“模型什么都会但什么都做不好”困扰的朋友一个参考。如果你刚接触这个概念,也不用担心,我会从最基础的“为什么要技能化”讲起,一步步拆到具体实现。

1. 核心思路拆解:为什么 Agent 必须“技能化”,而不是只会“调用工具”

1.1 一个扎心的现实:大模型什么都会,但什么都做不精细

先说一个我反复遇到的场景。早期我做 Agent 原型的时候,喜欢把能力全部押在提示词上:“你现在是一个数据分析助手,你可以读取 CSV 文件、计算统计量、生成图表、输出 Markdown 报告……”模型确实听懂了,回答得也头头是道。但一到了实际执行,问题就全跑出来了:让它读取一个 50MB 的 CSV,它居然想用 Python 代码一行行读然后自己“脑补”分析;让它算个相关系数矩阵,它写出来的 pandas 代码经常列名对不上;更头疼的是,连续对话时间一长,模型会“忘记”自己有哪些能力,甚至开始一本正经地胡说八道,给出根本不存在的函数名。

这不是大模型笨,而是我在设计上就偷懒了。大模型本质上是一个“文本预测器”,它对“能力”的理解全部来自上下文中的描述。如果你只是用自然语言告诉它“你可以做什么”,它在执行时就会按照“文本续写”的逻辑去自由发挥。自由发挥就意味着不可控,不可控就意味着没法用。换句话说,在 Agent 架构里,语言描述是一回事,实际执行是另一回事,中间缺的恰恰是一层“将意图映射到确定性代码”的桥梁。

1.2 技能的本质:给模型一套“标准动作”,而不是一段“自由描述”

我在 agent-skills 项目里做的第一件事,就是把“告诉模型你能做什么”改成“给模型一套标准动作”。每个技能本质上是一个最小可执行的原子能力单元,它有固定的名字、明确的输入输出、可校验的参数约束、以及配套的示例。模型要做的不是“理解”这个技能该怎么实现,而是“选择”当前场景该激活哪个技能,然后把它需要的参数提取出来,剩下的交给确定性的代码去执行。

打个比方,这就好比你去餐厅点菜。如果服务员只是说“我们这里什么菜都会做”,那厨师的发挥就全凭心情了,你可能等来一盘黑暗料理。但如果菜单上每道菜都有编号、主料、辅料、口味说明,你只需要报编号,后厨就知道该怎么做。技能就是这个“菜单编号”,它把大模型从“厨师”的位置上解放出来,让它专心做好“点菜员”的角色。

技能化带来的直接好处有三个。第一是确定性:同样的输入,技能的执行结果永远一致,不会因为模型状态波动而飘忽;第二是可测试性:每个技能可以独立做单元测试,像验证普通函数一样验证它的输入输出是否符合预期;第三是可复用性:一个写得好的技能可以被多个 Agent、多个场景反复调用,不需要每次重新“教”模型一遍。这三个特性,正是 Agent 从“玩具”走向“工程”的分水岭。

1.3 方案选型的取舍:为什么不做“全自动编排”,而是“半自动技能路由”

在技能化的落地路线上,我一开始其实纠结过两条路。一条是现在很多 Agent 框架主打的“全自动编排”:模型自己决定调用顺序、自己决定何时终止,人类只负责给一个最终目标。另一条是我后来实际采用的“半自动技能路由”:预先定义好技能拓扑,模型在允许的技能集合里决策,但关键的流程节点由代码控制。

全自动编排的诱惑力很大,demo 做出来特别炫:你喊一句“帮我把上个月的销售数据分析了并做个可视化”,它自己一步步调用工具、自己检查中间结果、自己生成图表,整个流程丝滑得像个老手。但放进生产环境后我遇到了几个劝退的问题:首先是不可预期性,模型在长任务里偶尔会“自创”步骤,比如在分析销售数据时突然调了一个天气查询技能,逻辑链断了,整个任务失败;其次是成本爆炸,全自动编排意味着模型要在每个步骤都做决策,一次复杂任务的 token 消耗是固定流程的几倍甚至十几倍。

所以最终我选了半自动路线:把“流程”和“技能”分开。流程由代码定义,技能由模型选择。模型只负责在流程节点上决策“这一步用哪个技能、传什么参数”,至于任务分几步、步骤之间怎么衔接,全部由预先编排好的逻辑控制。这样既保留了智能体的灵活性,又不会让失控的代价变得难以接受。项目跑到现在,这个选择被证明是性价比最高的。

2. 技能结构与分类体系:从“零散函数”到“可演进的技能库”

2.1 技能的解剖:一个合格技能包必须具备的五个要素

把概念想清楚之后,接下来的问题就是具体设计了。一个技能在 agent-skills 里到底是什么样的数据结构?经过几轮迭代,我沉淀了一个比较稳定的五段式结构,每个技能包都包含:元信息、触发描述、参数模式(Parameter Schema)、执行逻辑、示例对(Few-shot Examples)。

元信息好理解,就是技能的唯一标识、版本号、所属领域、标签。这一部分看似简单,但有一个坑我踩过:版本号必须纳入运行时的路由判断逻辑,否则老版本技能有 bug 时,你没有办法单独下线它,只能把整个 Agent 灰度回滚。触发描述是整个技能包里对模型最重要的部分,它决定了模型“什么时候该想起”这个技能。这里我强烈建议不要写抽象的能力说明,而应该写“场景触发词 + 排除条件”。比如“当用户要求对表格数据进行统计计算,且数据行数超过 50 行时,调用 dataframe_analyze 技能;单条数据的简单计算不需要调用技能,直接回答即可”。这种写法能大幅降低模型的误召率。

参数模式其实就是 JSON Schema,它定义了技能调用时需要传入哪些字段、每个字段的类型和取值范围。这一层是技能化与普通工具调用的最大区别:普通工具调用往往由模型自由发挥参数,技能化则强制模型按照 Schema 提取参数,不合规的调用直接抛出校验异常。执行逻辑就是技能内部的具体实现了,这里可以是 Python 函数、Shell 脚本、SQL 查询,甚至是对另一个服务的 HTTP 请求。示例对是给模型参考的“标准答案”,我的经验是每个技能给 3-5 组覆盖典型场景的输入输出示例,比在系统提示词里写一百行“你要怎么做”管用得多。

2.2 技能分类:信息获取型、计算分析型、操作执行型、生成创造型

给技能设计好结构之后,我在整理现有技能时发现,如果不对技能做分类管理,技能库一旦超过二十个,路由准确率就会急剧下降,因为模型在决策时面对的选择太多了。后来我把技能按照“动作性质”归成了四个大类,分类本身也参与路由决策。

第一类是信息获取型技能,负责从指定数据源拉取信息,例如查数据库、读文件、调 API。这类技能的关键设计点是输出格式标准化,因为后续步骤要拿到结构化的结果才能继续处理。第二类是计算分析型技能,负责处理数据、做统计、执行算法,这类技能的关键点是参数约束要严格,输入数据格式不规范时宁可报错也不要去猜。第三类是操作执行型技能,负责触发实际操作,比如发送邮件、创建工单、执行部署,这类技能的安全设计最重要,必须加授权校验和操作确认,不能模型一调就真干了。第四类是生成创造型技能,负责调用模型完成文本生成、代码生成、创意设计,这类技能与其他三类有本质区别:它的输入输出都是自然语言,校验难度最大,需要额外的质量评估环节。

在实际的项目里,这四类技能通常不是孤立使用的,它们会按照一定的拓扑关系组合成“工作流”。比如一个“竞品动态监控 Agent”的工作流是:信息获取型技能去爬取竞品官网更新 → 计算分析型技能对比版本差异 → 生成创造型技能撰写分析摘要 → 操作执行型技能把摘要推送到钉钉群。每一类技能有自己专属的调试策略和数据规范,分开管理会清爽很多。

2.3 影响范围评估:技能库的规模决定了 Agent 能力的边界

设计技能分类体系时,我顺带做了一次影响范围评估,结论也成了后来项目迭代的重要依据。技能库并不是越大越好,而是要看模型在一次决策中实际需要面对的技能候选空间有多大。做过一组对照实验:技能库 5 个时,路由准确率可以达到 95% 以上;技能库增加到 20 个时,准确率会掉到 88% 左右;超过 30 个后,准确率就跌破 80% 了。这个数据说明,无脑堆技能数量,不如给技能做分层路由:全局模型只面对十几个粗粒度的技能组,进组之后再细粒度匹配具体技能。

这个发现直接影响了我对“agent-skills 到底能做多大”的预期。它本来是我一个人的内部工具库,现在架构上变成了“技能分组 + 路由决策 + 执行层”的三层结构,每一层都可以按需扩展。单就我们团队正在跑的数智员工试点项目来看,目前接了 14 个技能,三个信息获取型、四个计算分析型、两个操作执行型、五个生成创造型,每个技能都在这套标准结构下独立开发、独立测试、独立部署。这个规模下,Agent 的日常任务完成率比没有技能化之前提升了将近一倍。

3. 实操过程与核心环节实现:从零搭建一套技能化 Agent

3.1 第一件事:先把技能注册中心建起来,别急着写技能

很多朋友做 Agent 的时候,习惯先去写工具函数,写完了再一股脑塞给模型。我在 agent-skills 里没有这么做,而是先花了一天时间搭建了一个非常轻量的技能注册中心。这个注册中心的核心是一个 Python 装饰器加一个服务端登记表,每一个技能在被定义时自动向登记表写入元信息、参数 Schema、以及版本号。代码结构大概是这样的:

# skill_registry.py from dataclasses import dataclass, field from typing import Callable, Any, Dict import inspect import json @dataclass class SkillMetadata: name: str version: str category: str description: str trigger_keywords: list parameter_schema: dict function: Callable = field(repr=False) class SkillRegistry: def __init__(self): self._skills: Dict[str, SkillMetadata] = {} self._versions: Dict[str, str] = {} def register(self, name, version, category, description, trigger_keywords, parameter_schema): def decorator(func): self._skills[name] = SkillMetadata( name=name, version=version, category=category, description=description, trigger_keywords=trigger_keywords, parameter_schema=parameter_schema, function=func ) self._versions[name] = version return func return decorator def get(self, name: str): return self._skills[name] def list_skills(self): return {name: skill.parameter_schema for name, skill in self._skills.items()} registry = SkillRegistry()

别小看这个注册中心,它解决了我之前遇到的一个隐性问题:当技能数量多起来之后,模型系统提示词里的技能描述必须动态生成。每次新增或更新技能,系统提示词跟着变化,如果没有注册中心这个中间层,你在提示词工程和技能代码之间就得手动同步,早晚会出乱子。有了注册中心之后,系统提示词里那一大段技能列表就是从 registry.list_skills() 自动渲染出来的,更新技能时提示词同步更新,彻底杜绝了“技能已上线但模型不知道”的问题。

注册中心建好之后,技能的执行接口我也做了统一规范:所有技能函数的第一个参数必须是 **kwargs 或一个 dict,避免模型传参时跟位置参数混淆;返回值必须是 dict,且至少包含 success 和 data 两个字段。这个约定换来的是路由层的极度简单——它只管拿参数、调函数、取返回值,完全不用关心每个技能内部是什么,也不用把异常逻辑散落在各处。统一契约的威力在后续接入可视化工具、日志系统时体现得特别明显。

3.2 第二件事:设计模型决策接口,用 Function Calling 接收结构化指令

技能注册中心有了,接下来就要解决“模型怎么调用技能”的问题。我选择的方式是基于 OpenAI 的 Function Calling 协议,也就是把每个技能的参数 Schema 直接透传给模型,让模型在对话过程中产出结构化的函数调用指令。这里的优势在于:Function Calling 输出的参数是 JSON 格式的,天然结构化,模型很少会给出格式错误的内容,省去了解析自然语言再实体提取的麻烦。

但直接透传裸 Schema 是不够的。在真实对话里,模型经常会发出“空调用”或者“试探性调用”,比如用户只是问“你们能不能做数据分析?”,模型就傻乎乎地调用了数据分析技能。为了应对这种情况,我在技能 Schema 的描述字段上做了一次细化加工:除了说明“这个技能是什么”,还强制加上“在什么场景下应该调用/不应该调用”。这一步极其有效,我实测下来,加了否定描述之后,空调用率至少下降了 60%。例如“analyze_data”这个技能的描述我最终改成了这样:

{ "name": "analyze_data", "description": "对用户提供的结构化数据执行统计分析与计算。当用户给出数据表格、CSV 内容或多条数据记录并明确要求分析、统计、对比时调用。如果用户只是问通用性的数据分析方法论,或数据量很小且可以直接心算,不要调用此技能。", "parameters": { "type": "object", "properties": { "data": { "type": "string", "description": "需要分析的数据内容,保持原始格式。" }, "analysis_type": { "type": "string", "enum": ["descriptive", "correlation", "trend", "comparison"], "description": "分析类型。" } }, "required": ["data", "analysis_type"] } }

决策接口的另一半是温度设置。发现技能调用相关的推理线程温度必须设低,我统一设了 0.1,这样模型在判断“要不要调用、调用哪个、传什么参数”时会更保守,不会天马行空。相反,在生成创造型技能内部,调用模型做文本创作时才把温度调到 0.7 甚至更高。温度参数在 Agent 里应该按执行阶段拆分,而不是全局一套设置,这是我调了很久才悟出来的。

3.3 第三件事:实现技能路由与结果回填,把“调用了”变成“调用对了”

决策接口把模型的调用请求接进来之后,路由层要做三件事:校验、执行、回填。校验过程使用了 jsonschema 库,对模型传出的参数做严格校验。这一层常被新手忽略,但它的价值在出错时才能体现出来:如果不校验就硬着头皮执行,模型传错一个字段名,技能内部可能就崩了,而且报错信息往往莫名其妙;加了校验后,你可以很优雅地返回一条“参数缺失:analysis_type”的消息给模型,让模型自己修正再重新调用。

执行环节看起来简单,就是把参数塞进技能函数跑一趟。但有一个容易被忽略的细节:技能之间可能互相调用,尤其是信息获取型技能的结果往往需要喂给计算分析型技能。所以我在路由层保留了“技能依赖声明”的字段,每个技能可以声明自己的前置依赖条件,比如 analyze_data 依赖 fetch_db_data 的输出。当模型直接要求 analyze_data 但上下文里没有 fetch_db_data 的执行结果时,路由层会先自动触发 fetch_db_data,再把数据拼装进 analyze_data 的参数里。

结果回填是另一个决定 Agent 体验的细节。执行完技能拿到结果后,我会把结果以“系统消息”的形式追加到对话上下文里,让模型基于这个结果生成面向用户的回复。这里的关键是要控制回填的文本量:如果技能返回的是一张 500 行的表,你不能全量塞给模型,否则上下文窗口直接爆炸。实践中我总结了一套回填压缩规则——数据量超过一定阈值时,只回填统计摘要加前 10 行预览,并附一句提示“完整数据请调用 get_full_result 获取”。这些规则的代码示例在项目里非常琐碎,但每一个都是在真实失败的教训里磨出来的。

3.4 第四件事:沉淀一套四步排查法,让 Agent 在出错后能自我修复

AGI 走向实用的前提是 Agent 会自己纠错。与其追求一次调用就成功,不如先想好失败后的恢复路径。我在实际项目中按下面的四步顺序让 Agent 处理技能调用的失败:

第一步,检查参数错误。使用 jsonschema 解析模型返回的 JSON,如果发现缺少必要字段,就把缺的字段名和原因返回给模型,让它重新生成一次。这一步能解决大约 80% 的“看起来调用了但没调对”问题。

第二步,检查技能本身是否异常。如果参数没问题,技能内部抛异常了,那就要区分是数据问题还是逻辑问题。数据问题的处理是自动重试一次并尝试做数据格式转换;逻辑问题则需要返回错误码,让模型换一个技能或者换一种策略。

第三步,检查是否选错了技能。这一步是最难排查的,往往要回溯日志看模型当时的决策上下文。我开发的这套机制是,当执行后出现明显的“结果与意图不匹配”,比如用户要趋势分析却调用了描述统计,就把这个错误样本记录到本地 eval 数据集里,后续用这批数据做路由模型测试。

第四步,向用户澄清。如果前三步都没能解决问题,就别装了,直接让模型对用户说“这个问题我当前的处理能力还不太够,你可以尝试换个方式描述”,不要让模型硬编一个错误的答案。这种“坦诚”策略在用户体验上反而比强行给错答案要好很多,因为它至少保住了信任感。

这四步法的核心逻辑是“越修越窄”,从技术层逐步过渡到产品层。每步都有明确的退出条件,不会陷入死循环。我在代码里加了一个 max_retry 计数器,超过三次就直接转人工澄清,防止模型在某个技能调用上无限重试浪费 token。

4. 上下文管理和提示词架构:决定 Agent 上限的“暗线”

4.1 动态技能选择:别让你的 Agent 被“能力列表”冲昏头脑

提示词架构在技能化 Agent 里是个容易被低估的模块。我在第一版实现里,把全部技能的名字和描述一股脑放进了系统提示词里,结果前面提到过:技能一多,模型决策准乱。后来我在提示词层引入了“动态技能选择”,而不是静态大列表。

动态技能选择的核心是一个前置的粗筛模型调用。在每轮用户输入到达时,先由模型判断输入最可能涉及哪些技能组,然后把系统的提示词裁剪成只包含这些技能组对应的技能包描述。比如用户说“帮我查一下上个月的销售额”,粗筛模型会打上【数据获取】【数据计算】【可视化】三个组的标签,提示词里就只放与这三个组相关的 8 个技能描述,而不是全部 30 个。这个机制带来的效果非常明显:路由的准确率重新回到了 90% 以上,单次对话的 token 消耗降低了大半,模型推理时也更“专注”。

这个方案的代价是要多一次模型调用,但以当前的模型成本来看,这点开销相对整体任务的消耗几乎可以忽略。更关键的是,粗筛模型本身也可以做小动作,比如它可以在返回组标签时带上置信度,低于阈值的就直接问用户“你是想看报表还是想写报告?”,避免瞎猜。消息越窄、目标越明确,Agent 的稳定度越高。在这个问题上,我的感受是“少即是多”。

4.2 技能执行日志的格式化:可观测性是调优 Agent 的唯一抓手

Agent 的调试体验和传统编程完全不同,你很难用断点调试来定位“为什么模型刚才没调用技能”。我花了不少时间在这个问题上,最终靠的是一套严格的技能执行日志规范。每个技能调用,必然留下几条结构化日志:触发前上下文快照、模型生成的原始调用指令、参数校验结果、技能执行耗时与结果摘要、用户可见回复。这些日志统一输出为 JSON Lines 格式,落在本地的 logs 目录里,方便检索和分析。

日志格式化的最大好处是可以把 Agent 的决策过程“回放”。我复盘时常用这个流程:从用户问题开始,一步步回溯模型拿到的是什么上下文、看到了哪些技能描述、最终生成了什么调用指令。很多时候“模型为什么要那么做”一目了然。有一次我排查一个 Agent 总是选错分析类型的 bug,回放日志后发现,上下文里靠前位置残留了前一轮任务的系统消息,模型被带偏了。切掉冗余上下文并加了一个输入长度限制之后,问题立刻消失。

这类问题在非结构化的日志里可能要排查几个小时,但在规范化的调用链日志里,几分钟就能定位。所以如果你的 Agent 项目还没有日志系统,我建议立刻补上。不要等出问题再补,Agent 的出错本来就是概率性的,没有日志,就意味着你只能靠猜和碰运气。

4.3 上下文窗口中技能输出的权重管理:一视同仁不行的

除了技能选择,上下文管理还有一个很多人没意识到的问题:技能输出的内容在后续对话里的“影响力”是不同的。比如 execute_sql 技能返回了一个表格的 JSON 结构,这个 JSON 里有 200 个字段的元数据。在后续生成报告时,模型如果看到这一大坨原始 JSON,很容易被无关字段“带偏”,写出的报告抓不住重点。

我采用的方案是对技能输出做“重要性标记”。每类技能在返回时附带一个 summary 字段和一个 detail 字段,summary 是模型生成报告时需要的主要信息,放在上下文里;detail 是供其他技能或用户按需获取的完整数据,不进主上下文。例如 execute_sql 的 summary 只保留表结构、行数、前三条样例记录,detail 保留完整查询结果。这样做之后,上下文瘦身了,模型更难被无关信息干扰。Agent 的“注意力”是一种稀缺资源,应该花在决策最相关的地方,这跟人开会是一样的道理——会议室里只有跟议题相关的人,会议效率才会高。

关联到影响范围评估,技能输出权重管理的效果是可以量化的。在我记录的一组对照中,同样场景下,不加 summary/detail 分离时,Agent 生成报告的相关性评分大概在 0.71;加入分离后提升到了 0.86。这个提升不是来自提示词调优,而是单纯靠上下文管理做到的,说明这种结构性的优化比话术层面的优化更有杠杆效应。

5. 踩坑实录与排查技巧:那些文档里不会告诉你的真实教训

5.1 模型坚持调用已下线技能:注册过期技能导致的幽灵调用

有一次我在版本迭代时下线了一个旧技能,但没有同步更新注册中心的反向索引,结果发现模型在对话里偶尔还会尝试调用这个已经不存在的技能。排查了很久才发现,原因是大模型的系统提示词是从注册中心动态渲染的,但历史对话消息里的函数定义仍然是旧的,模型会参考历史消息中的既有格式,依然输出旧技能的调用。这看起来是个蠢问题,但它非常隐蔽,也很容易复现。

解决方案是给注册中心加上“技能启停状态”的枚举,并在渲染动态系统提示词时不仅过滤已下线的技能,还要维护一份“最近已下线技能列表”。把这个列表也一并写进系统提示词,明确提示模型“以下技能不可用,不要调用”。加了这个之后,幽灵调用彻底消失了。这里也印证了一个经验:Agent 的“记忆污染”是真实存在的,模型会看历史消息里的工具定义,而不是只依赖最新系统提示词。

5.2 技能调通但效果拉胯:典型的“调用成功≠任务完成”

如果说上面那个问题是显性的机制缺陷,那技能调通但效果差就是一个更隐蔽的质量问题了。我一度很困惑:技能本身写了单测,跑起来没有异常,返回的数据结构也对,但用户实际体验就是觉得 Agent 并不聪明。后来我在日志里仔细看才发现,问题不在技能执行上,而在于模型传给技能的参数经常是“形式正确但语义偏差”。例如用户问“对比这周和上周的订单量变化”,模型确实调用了 compare_trend 技能,但传进去的字段名把“本周订单数”写成了“total_orders_for_this_week_v2”,技能接收后虽然校验通过,但内部逻辑拿到的数据压根对不上。

解决办法是给技能参数增加“别名纠偏”逻辑。在参数 Schema 的 description 里写明每个字段可能的别名说法,同时在技能内部加一层映射解析,把常见变体名规范化到标准字段。这个写法确实显得有点笨,但在跟大模型打交道时不这么干你就等着被它坑。“模型按它理解的意思传参,而不会严格阅读你的字段设计文档”,这个认知越早建立越好。

5.3 多技能并行调用与竞态冲突:同一技能被重复执行的幂等问题

有段时间我给技能路由加了并行调用能力,本意是加快任务处理速度,结果引入了一个新的麻烦:模型在上下文里多次表达了同一个意图,导致路由层对同一个技能提交了多次并行的调用请求。技能本身没有做幂等保障,比如一个发送通知的技能被同时调了两次,用户就收到了两条重复的消息。虽然概率不高,但每次出现都很尴尬。

解决方案是给每个技能加“幂等键”(Idempotency Key),在路由层用一个 Redis 计数器做去重:同一会话中同一技能并且参数哈希一致时,第二次调用直接返回第一次的结果,不再真正执行。这其实是后端系统设计的常见做法,只不过在 Agent 场景下被重新踩了一遍。吃一堑长一智:任何有副作用的操作型技能,幂等设计必须前置,事后补救会痛苦到怀疑人生。

5.4 调试时的模型固执:模型宁可用自创函数也不调用可用技能

最后记录一个特别恼人的问题。有时候我明明已经在系统提示词里把一个技能描述写得很清楚了,但模型在收到用户请求后,宁愿自己编一个函数名去“回答”——比如用户问“能不能帮我查一下天气”,模型不调用我的 get_weather 技能,而是直接胡写了一个 get_temparature_today 函数返回了一个感觉上是值的东西。这种问题主要集中在复杂多轮对话的中间轮次发生,模型“走神”了。

我的排查思路有两个。第一个思路是看生成采样参数,如果 temperature 较高,模型的自由发挥倾向会增加,技能路由线程的温度我已经降到 0.1,但有时候上下文里的历史消息太多,模型仍然会飘。第二个思路是检查技能描述是否太抽象,把“查天气”写成了“获取气象数据”,模型的联想就容易跑偏。我发现把技能描述从名词性描述改成“当用户想……时使用”的句式,误用率会显著下降。总而言之,与其说这是一个技术问题,不如说这是一个 Prompt 与模型心理模型的适配问题,需要不厌其烦地用具体场景磨。

6. 性能评估与效果复盘:技能化改造前后,数据发生了什么变化

6.1 三个核心指标:任务完成率、路由准确率、平均处理时延

项目推进到阶段性验收的时候,我专门整理了一份技能化改造前后的对照评估,跟踪的指标主要是三个:任务完成率、路由准确率、平均处理时延。任务完成率按端到端的效果判定,路由准确率按“是否选对技能且参数正确”判定,平均处理时延从用户提问到最终结果回复统计。

改造前的基线是传统“提示词驱动+原生工具调用”的 Agent。当时的任务完成率只有 61%,也就是说十次请求里有四次是拿不到可用的结果的。路由这件事几乎没有概念,因为模型经常不按规矩调用工具,有一搭没一搭。平均处理时延反而很低,因为模型经常直接“编答案”,不怎么调用函数。改造后的数据变化比较明显:任务完成率提升到了 84%,路由准确率在动态技能选择的加持下达到 90% 出头,平均处理时延因为多了一次粗筛模型调用和更严格的执行,涨了约 0.8 秒,但换来的是结果质量的质变。0.8 秒的延迟完全可以在产品体验上接受,毕竟用户等来的是靠谱的答案而不是一本正经的胡话。

6.2 失败案例分析:一次典型的“意图偏移”是如何被日志揪出来的

我还特意留了一个失败样本做复盘。当时用户的问题是:“帮我看看 A/B 两个方案在过去一周的转化率差异,并告诉我哪个更好。”理想的分析链是:fetch_metrics 获取数据,compare_rates 做显著性检验,最后生成结论。但真实执行是:模型直接调用了 generate_report 技能,把分析步骤一股脑丢给文本生成模型去编。结果显示,报告里有大段的文字描述,但没有任何真实数据支撑,转化率数字全是编的。

回放日志之后,定位的根因有两个。第一,技能描述中的“generate_report”字段过于宽泛,让模型误以为它是一个全能型技能,可以在没有数据的情况下生成报告。第二,缺少一项前置强制依赖:generate_report 技能声明了“需要输入前置数据摘要,如果没有则返回错误”,但这个依赖声明在配置时被我忽略了。修复方式是拆分技能职责:写报告只能基于已有数据,不承担数据获取职能;同时给 generate_report 加上“禁止在无可信数据情况下直接输出KPI数值”的约束描述。这类问题在设计上是可以提前预防的,我的教训是技能职责必须足够原子化,粒度太粗就会给模型留下“偷懒”和“幻觉”的空间。

6.3 后续规划:技能库的多元化和跨场景复用

agent-skills 这套方案目前的形态已经能支撑日常使用,但离我理想的状态还有距离。接下来我计划做两件比较有杠杆的事。第一件事是完善技能库的“跨场景复用”能力,把一个在数据分析场景下沉淀好的技能,直接迁移到另一个场景(比如运营报表)时,只替换数据源连接参数和输出格式模板,不改技能内部核心逻辑。这就需要在技能包结构里把业务相关的配置和逻辑相关的代码彻底分离,我目前正在抽一个 configs 目录专门放这类环境差异配置。

第二件事是把技能评估从手工标记推进到自动化回归。当前的路由准确性评估还是靠人工攒 eval 集,成本高且覆盖不足。我准备引入一套基于 LLM-as-a-judge 的评估管线:给定历史对话和正确技能命中序列,让大模型对当前路由结果打分,再把低分样本沉淀为新的训练与调优素材。这套机制打通后,技能库的迭代周期就可以从两周缩短到两三天,真正支撑起“技能也是产品”的定位。

写在最后的个人体会

如果你现在也要做 Agent 技能化,我最大的建议是:先不要急着写一堆技能,而是先把技能注册、路由、日志和评估这套“骨架”撑起来。技能本身是业务资产,会越积累越多,但骨架决定了这些资产能不能被高效地组织和使用。我在这套系统中投入的前几周,几乎全在处理骨架而没怎么碰具体业务技能,但正是那段时间的积累让后续的开发变快了。

另外一点体会是,Agent 技能化和传统工具调用的关键差异在“设计边界”。传统编程里,调用方总是知道自己在做什么;Agent 场景下,调用方是模型,它总是猜着来。你所有的设计都得围绕“怎么减少猜测空间”来做:技能描述用具体样例,参数校验用严格 Schema,输出格式统一标准化,失败时按字典序排查。每一步都在压缩不确定性,压缩到最后,模型的选择自然就对了。

agent-skills 这个项目到今天还在不断迭代中,但它已经从一个“实验性玩具”变成了我日常工作中离不开的基础设施。希望这篇分享能给你一些启发,少走一些我走过的弯路。如果你也在做类似的事情,欢迎在实践中多记录,多复盘,把属于你自己的“技能库”一点点养大。

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

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

立即咨询