资讯动态

Agent技能化设计:从单体Prompt到可验证技能库的工程实践

发布时间:2026/10/14 8:36:49 来源:尧图企业网站定制
写agent一段时间后绝大多数人都会遇到同一个瓶颈demo跑得通一上真实场景就原形毕露。我之前给某数据团队的内部模拟项目做跨平台信息采集代理任务不算复杂需要抓页面、识别字段、翻页、清洗。起初我把所有逻辑塞进一个巨型prompt里效果忽好忽坏最离谱的一次是它连续三小时都在同一个分页环节反复兜圈子日志里连错误原因都看不出来。后来把agent-skills当独立模块来设计把能力拆成有边界的技能情况才彻底改观。这篇文章会围绕我在这套代理系统里设计和落地技能体系的全过程展开为什么单体prompt撑不住真实任务、技能该怎么拆和怎么描述、运行时怎么调度、错误怎么反馈、技能库怎么运维。如果你也正在做agent项目或者手里已经有个偶尔好用、大部分时间看运气的代理那这篇内容大概率能帮你理清问题出在哪一层。1. 给代理一肩挑式开发敲响警钟技能先行的背景1.1 为什么一个提示包打天下不持久先聊一个挺反直觉的观察LLM本身很强但让LLM在一个巨型提示里同时承担理解需求、规划步骤、调用工具、校验结果四件事它的表现会随着任务链路变长而快速劣化。原因并不是模型突然变笨了而是所有指令挤在同一层上下文里彼此之间会产生干扰。举个例子。采集代理需要判断当前页面是否需要登录同时需要提取列表页上的每条条目。如果这两条指令写在同一个system prompt里模型很容易把登录检测和内容提取耦合起来——比如它发现UI上有登录按钮就认为整个页面无内容直接返回空结果。这个bug我排查了很久最后发现它不是代码逻辑问题而是提示词叠加导致的语义粘连。另一个问题是上下文长度。即便大模型支持几十万token的窗口把大量规则全部写进prompt也会稀释指令权重尤其当真实页面HTML经过处理后依然很长时模型容易忽略prompt后部的关键约束。这里的本质矛盾是prompt是给模型的说明书它适合描述风格和少量规则但承担不了复杂系统的全部状态和流程。对比一下两种做法维度单体prompt模式技能化模式职责边界所有步骤混在一个提示里每个技能独立描述、独立执行调试粒度只能看整段对话日志能定位到具体技能和参数复用能力换个任务基本要重写技能可跨项目迁移失败处理靠模型临场发挥技能内置校验和失败信号测试方式很难自动验证每个技能可单独回归把表格看完就明白了技能化不是把prompt拆成多个prompt那么表面而是把代理从一条提示驱动的对话流改造成一个由可验证单元组成的执行系统。这个转变是稳定代理的基石。1.2 技能的准确定位介于工具和流程之间很多文章把工具tool和技能skill混着说实际上两者是不同层级的东西。工具是最小的动作原子比如打开网页点击按钮调用某个API技能是基于工具、但包含决策逻辑的完整子任务比如检测登录态并尝试自动登录它可能需要先抓取页面特征、判断是否存在登录框、再决定是否输入账号。我用一个通俗类比来理解技能的地位如果说工具是扳手和螺丝刀技能就是拧下一块面板——它知道该用哪个工具、什么时候停手、面板拧不下来代表什么。真正的agent流程不是LLM直接调用工具而是LLM从技能列表里选择一个技能技能内部再编排一串工具调用。中间多了这一层复杂任务才有机会被拆开、被测试、被改进。我自己实际项目里的划分逻辑很简单一个技能解决一个有明确输入和输出的业务问题。比如抓取当前页的表格数据是一个技能判断是否需要翻页是另一个技能。不要把技能粒度做得比工具还细也不要试图把做完整个报表塞进一个技能里这两头都是坑。1.3 代理循环的真正形态技能化之后代理的运行逻辑就变成了标准四段循环接收任务后先分析任务意图然后从技能库中挑选匹配的技能执行技能并拿到结果最后检查结果是否满足预期。不满足就换参数或换技能重试直到成功或放弃。这个循环看似简单但落地时最容易忽视的一点是循环里的每个环节都需要证据而不是模糊状态。当前内容无效是个模糊状态提取结果为空且页面含登录关键词才是可判断的证据。我后面会详细讲这部分怎么落到技能描述和执行器里。2. 从任务拆分到技能描述设计一个看得懂的技能库2.1 以子任务为粒度先拆出一个案例不举例子的拆分方法都是空话。拿我之前做的某跨平台信息采集代理暂称模拟项目X来说它接收一个数据页面的地址要输出结构化指标并把相邻页面的同类指标合并。最初我把它写成一个大步骤后来拆成了六个技能技能名职责典型输入典型输出check_page_status判断页面是否可达、是否登录、是否异常页面地址状态枚举extract_pagination提取翻页控件信息DOM结构下一页地址/是否存在parse_page_content解析当前页的有效内容页面快照结构化记录列表validate_record校验单条记录的完整度记录对象通过/失败原因merge_metrics合并多页指标并去重多批记录汇总表report_failure记录失败上下文并通知异常信息告警记录这个拆分过程中唯一标准是能否独立写清输入和输出。翻页和解析必须分开因为你翻页成功不代表解析成功校验和解析必须分开因为解析是一种能力判断解析结果对不对是另一种能力。把两类能力揉在一起后面出了问题连定位都难。2.2 技能描述怎么写才不会让模型产生歧义技能拆好了下一个关键动作就是给每个技能写描述。描述不是给开发者看的注释而是给LLM看的调用说明书。写法优劣直接决定路由成功率。我见过太多技能系统死在描述上描述写得含糊模型自然乱调用。写技能描述时我会照着这个结构来组织技能名称使用动词开头比如parse_page_content不要用处理页面这种含混说法。一句话定义说明这个技能解决什么业务问题最好带上任务类型关键词。输入参数写清楚字段名、类型、是否必填、取值来源。输出格式明确返回结构最好标注字段解释。失败信号写明什么情况下技能返回失败以及失败时附带什么信息。不要做什么补充边界防止技能被滥用。下面是一个我实际用过的技能定义示例用YAML维护运行时会解析成结构体供LLM参考name: parse_page_content description: 从当前页面快照中提取业务指标列表仅适用于数据列表页和详情页 input: - field: snapshot type: object required: true - field: page_type type: enum values: [list, detail] required: false output: - field: records type: array description: 每条记录包含date、value、unit、source_url字段 error_signals: - type: no_records condition: 页面存在但未提取到任何记录 - type: login_required condition: 页面出现登录标识且有效内容为空 constraints: - 不要修改输入快照 - 如果页面类型不是list或detail直接返回no_records重点看error_signals这一段。我给模型明确写了什么情况算失败这对后面的重试逻辑特别重要。如果没有这段模型在解析失败时往往会自作聪明返回一段看似合理但无法用于后续处理的内容然后代理拿着脏数据继续跑结果一错错到底。2.3 技能描述本质上是给临时员工的交接说明为什么很多开发者忽视技能描述我觉得是没意识到模型每轮都像一个刚入职的临时员工。它看不到你脑子里对业务的理解只能靠你写下来的文本判断什么任务该调用什么技能。技能描述写得像招聘网站里那种负责相关工作完成领导交办事项的废话它自然就瞎猜。所以我在团队里定了一条规矩每个技能描述必须能独立交给一个不了解项目的人让他看完之后知道什么任务该用这个技能、用完之后能拿到什么、拿到什么样算失败。这个标准听起来简单真正做到位需要一遍遍修改但效果立竿见影——模型技能选择的准确率能提升好几个档次。3. 代理运行时如何调度技能从Function Calling到技能路由3.1 技能注册表与执行引擎的分工技能定义好之后需要一个机制把它们挂到代理运行时上。我采用的是注册表执行器的组合注册表负责存放、索引技能定义执行器负责根据路由结果调用具体函数。注册表内部维护了三类信息技能本身、技能之间的依赖关系、技能的启用状态。有人问为什么不直接调用Function Calling。我的观点是Function Calling只是让模型输出结构化工具调用结果的一种机制它解决的是模型如何表达意图的问题而技能路由要解决的是该选哪个技能以及选完之后按什么顺序执行的问题。两者是不同层次的事可以搭配用但不能画等号。我实际搭建的技能注册表核心代码如下按理解简化class SkillRegistry: def __init__(self): self._skills {} self._exact_routes {} self._domain_index {} def register(self, skill): name skill.name self._skills[name] skill for alias in skill.aliases: self._exact_routes[alias] name for tag in skill.tags: self._domain_index.setdefault(tag, []).append(name) def list_skills_for_domain(self, domain): return self._domain_index.get(domain, [])这个结构很轻但它解决了一个实际问题不是所有任务都需要靠LLM来选技能。任务类型明确的时候直接用任务描述里的特征关键词走精确匹配又快又省还不会出错。3.2 完全依赖LLM选技能会碰到的坑一开始我图省事每个任务进来都直接把全部技能描述拼进prompt让模型自动选。表面上看很智能实际跑起来问题不少。第一个坑是选择幻觉。当技能库超过十几个之后模型偶尔会凭空编出一个不存在的技能名或者把两个技能的名称拼接成新名字。这种幻觉在纯文本对话里几乎察觉不到但在代理系统里就是直接调用失败。第二个坑是选择震荡。同一个任务有时候走技能A有时候走技能B执行结果完全取决于上下文里某句话的措辞。这就导致代理行为不可预测尤其是对稳定性要求高的场景这种随机性不可接受。第三个坑是描述串扰。技能之间的描述都有相似关键词时模型容易混淆。比如parse_page_content和extract_pagination都有页面这个关键词模型就有概率拿解析技能去处理翻页需求结果自然错误。这些坑让我意识到技能调度必须分层能确定性解决的不要交给概率性机制需要智能判断的也要用额外的上下文约束来兜底。3.3 确定性路由和LLM选择结合我的配置方式后来我设计了一套组合路由策略流程是这样先做标准化把任务文本里的意图关键词提取出来转换成标准标签。走精确路由如果任务标签能唯一匹配某个技能别名直接返回该技能不问模型。走领域预筛如果匹配到多个候选技能先把候选技能列表按领域收敛到三四个以内。最后才交给LLM在收敛后的候选集里让模型选一个并把每个技能描述精简后再传给模型。这套流程的关键好处是LLM每次做的都是五选一的简单判断而不是二十选一的盲猜准确率高很多。对应的代码如下def route_task(task, registry): tag normalize_task_tag(task) if tag in registry._exact_routes: return registry._exact_routes[tag] candidates registry.list_skills_for_domain(task[domain]) if len(candidates) 1: return candidates[0] if len(candidates) 4: return llm_select(candidates, task) return llm_select(candidates[:4], task)这个思路其实就是漏斗式筛选。代理本身是一个开放系统不可能所有任务都走固定规则但把规则放在前面做粗筛、把LLM放在后面做精判两者配合远比单一方案可靠。3.4 技能库要控制体积动态分组加载技能多了以后还要注意上下文体积的问题。如果系统有五十个技能每个描述平均三百字那就是一万五千字每次请求都塞进上下文成本和延时都不小。更重要的是模型面对大量技能描述时注意力会被无关技能分散导致选择准确率下降。我的做法是给技能打域标签按域分组加载。任务进来时先识别域然后只把该域关联的技能加载到运行时上下文里。比如采集任务就只加载采集域技能不加载清洗报表域技能。这样技能库总量可以比较大但是单次请求的上下文保持精简。这背后是一个朴素的道理技能库是全量的武器库路由上下文是本次任务随身携带的工具包。只有按需携带武器才能用得准。4. 让技能自己学会失败错误处理与反馈循环4.1 技能的内部防御与外部校验技能执行过程通常包含模型调用、数据转换、外部交互三部分每一部分都可能出错。我后来形成了一套双层校验机制内部防御放在技能函数里外部校验放在技能返回之后。内部防御主要做参数合法性和前提条件检查。比如某个技能要求输入必须是已解析的DOM树节点如果调用方给了一个字符串技能要先拦截返回参数错误而不是硬着头皮处理再抛一个莫名其妙的异常。外部校验则是用独立的检查器判断技能输出是否符合业务约束比如提取到的记录数量是否为零、日期字段是否是预期格式。双层校验的意义在于内部防御保证技能不会带病执行外部校验保证技能就算执行了错误也不会传播到下一环。很多代理出问题回头看都不是模型选错技能而是某个技能产出的脏数据被后续步骤当成了有效数据。校验层就是为了切断这种污染链。4.2 把失败反馈写进技能定义一种低成本高收益的迭代在我维护模拟项目X期间收获最大的一个做法是把线上遇到的失败模式反哺回技能定义里让它越来越懂现实世界的变数。具体操作是每次代理在真实页面上跑出错误结果我都把错误场景的关键特征写进对应技能的description或error_signals字段。举个例子某次采集遇到的页面在未登录状态下会显示一部分数据但关键指标隐藏技能原本认为有数据有权访问于是误把不完整数据当完整结果返回。发现后我在技能定义的error_signals里增加了一条error_signals: - type: session_partial condition: 页面包含登录标识且记录字段缺失率超过50%改完之后同一技能准确识别这种半登录态的概率高了很多。这个迭代方式成本极低不需要训练模型不需要改代码逻辑只需要修改文本描述但效果极其明显。我后来甚至养成了习惯技能每次在真实场景中发现新失败模式就顺手更新定义像给技能写病例报告一样。4.3 一次典型案例如何从假成功里找回真相我详细讲一个排查过程这是技能系统验证机制成型的关键一次。某次代理采集一个报表页面日志显示parse_page_content技能返回成功后续的merge_metrics也正常合并但最终汇总数据比人工核对少了三分之一。第一反应是翻页漏了检查extract_pagination的结果却显示所有页都访问过了。后来我单独用技能跑测试页发现parse_page_content在某个特定布局的页面上只能提取一半字段其余字段被吞掉了。问题出在哪呢技能函数没有对字段完整性做验证它只要提取到任何一个记录元素就算成功。结果页面上大量记录的日期字段缺失函数却照样返回成功。之后我在技能里增加了完整性检查如果记录命中条数小于预期阈值返回incomplete_data信号并触发重试代理运行才恢复正常。这个案例给我的最大启发是在技能系统里执行成功和结果正确是两回事。技能定义里必须有一个结果校验器专门区分操作完成了和结果符合预期。没有这个区分系统就会在错误数据上继续构建错误结果越跑越偏。5. 技能的版本、测试和运维细节5.1 给技能做目录和版本管理技能本质上是可复用资产一旦用起来就会快速增多所以不能随意命名、随意堆放。我在项目里把技能目录组织成下面这样skills/ commerce/ order/ check_order_status.yaml fetch_order_detail.yaml inventory/ query_stock.yaml content/ parse/ parse_page_content.yaml pagination/ extract_pagination.yaml common/ validation/ validate_record.yaml每份技能YAML都有version字段升级时递增并记录changelog。刚开始会有人觉得对个YAML文件还搞版本管理太繁琐但技能的改动经常牵一发动全身因为技能是新技能路由层、校验层、错误反馈都依赖它。有个确切的版本号至少能在出问题时快速确认是哪个版本的技能干了什么。5.2 技能自动测试维护技能库的稳定器我给自己定的要求是技能核心逻辑必须有回归测试尤其是解析类和校验类技能。测试思路和普通单测不太一样它更像是契约测试——验证技能在输入符合定义的样例时输出确实符合输出声明。测试样本通常有三类标准样例、边界样例、异常样例。以parse_page_content为例标准样例是典型列表页边界样例是只有单条记录的页面异常样例是包含登录标识的页面。每个样例都写好期望输出然后跑测试断言。这个思路说起来简单实践价值却很大。有一次我重构技能内部实现跑测试时发现异常样例的结果回归到了旧错误模式当场就拦下了问题。如果没有测试这种回归会以线上事故的形式出现在生产环境里。5.3 技能的生命周期敢下线也要会禁用技能库扩大之后另一个需要留神的问题是废弃技能。业务变了某些技能可能再也不被调用但它们还留在注册表里继续占上下文、继续当候选、甚至偶尔被LLM误选中。所以技能要有启用和禁用开关清理时要区分软禁用和硬删除。软禁用适用于当前不再使用但可能后续复用的技能做法是把它移出候选列表但保留定义文件硬删除适用于确定无用的技能直接移除文件。我之前吃过一个亏一个早期技能因为没有禁用在后期任务里频繁被模型选中每次选中都执行失败。软禁用之后整个系统的失败率肉眼可见地降了下来。技能库的运维原则就一句话技能的描述、测试、生命周期要像管理一套对外API一样严谨。很多代理项目做到后期模型本身没变但因为技能库维护得好整体表现越来越稳这本身就说明了系统工程的权重。6. 做完这套系统后我觉得真正值得注意的地方技能系统落地之后模拟项目X的稳定运行表现说明整体思路是对的但比技术细节更值得留意的是几个关于度的判断。技能粒度不能太细也不能太粗。太细会导致路由开销大模型频繁做无意义的技能切换太粗又会滑回单体prompt的老路。我总结出的经验是一个技能应该对应一次可以被验证的完整子任务粒度判断标准不是代码行数而是这个子任务的输出能不能独立检验。能独立检验的就应该拆出来。技能的描述文本需要和运行时深度绑定。它不是写给文档的摆设而是路由决策的真实上下文。任何想提升技能选准率的人都应该先从优化技能描述下手而不是盲目换模型或加提示。我见过太多团队花大量精力调模型参数却对自己技能描述里的模糊措辞视而不见方向完全跑偏。反馈闭环必须每天维护。技能系统不是上线就完了它需要源源不断地吸收失败案例来进化。我在项目跑了一个自动记录失败的模块把每次技能调用的失败信号、上下文、参数都沉淀下来每过一段时间手工清理一遍把共性特征回写进技能描述。这个循环走得越勤系统越聪明。最后给正在做agent的朋友一个最实用的建议如果你现在只有一个单体prompt代理别急着引入复杂的编排框架先把任务拆成两三个技能每个技能定义好输入、输出和失败信号跑通一个小闭环。你会发现从这个最小改造开始代理的稳定性就已经和之前不在一个档次上了剩下的扩充只是沿着同样的路径逐步加技能而已。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑