Agent技能库实战:从提示词失控到可维护的技能编排
1. agent-skills要解决的核心矛盾能力堆在提示词里的失控先讲一段真实经历。去年我在做内部工具型Agent的时候接的需求其实不复杂帮用户解析上传的PDF、提取关键字段、和公司内部的客户系统做比对最后生成一份跟进摘要。最开始的做法想必很多人都熟悉——把所有能力都写在system prompt里告诉模型你是一个解析助手遇到PDF要调用解析库遇到客户信息要调用CRM接口遇到日期要格式化成YYYY-MM-DD然后就是不断往里面追加规则。前两周看起来很美好模型确实能处理七八成的问题。但第三周开始就失控了。用户问上周的会议纪要和日程能整合成周报吗模型把生成周报和解析纪要两个动作搅在一起一会儿把纪要文件路径当成参数传给日历接口一会儿又把周报模板当成了PDF解析配置。我试着往提示词里继续加约束结果system prompt膨胀到三千多行每次微调一个功能都要全量回归测试改一处崩三处。这个阶段让我彻底明白了一件事把能力堆在提示词里是不可持续的。规则之间互相干扰参数边界模糊模型根本分不清什么时候该用哪个能力。后来我转向了agent-skills的思路——把每一项可复用的能力封装成独立可注册的技能单元配上清晰的触发条件、输入输出协议和执行体由Agent在运行时动态选择与组合。这条路走下来系统的稳定性和可维护性都有了质的变化。1.1 从规则到技能的本质区别很多人在刚开始接触agent-skills时会觉得这不过是换了个词把提示词里的规则拆出来。但在实际落地后我发现规则和技能有本质区别。规则是如果遇到X就做Y的线性判断它依附于某个具体的指令离开了上下文就失效。而技能是能独立完成某类操作的最小能力单元它有明确的输入输出边界、有自包含的执行逻辑、有可观测的成功失败信号。换句话讲规则告诉模型怎么做技能则向模型承诺我能做什么你只需要给我正确的输入。拿我刚才的例子来说解析PDF就可以做成一个技能。它接收一个文件路径和要提取的字段列表返回结构化的字段值。模型不用知道里面用了正则还是OCR只用按照技能的输入协议传参再消费返回结果。1.2 技能四要素一个合格技能的最低配置在agent-skills的实践里我把一个技能拆成四个要素这也是我在代码里定义技能数据结构的基座触发条件when_to_use一段给模型看的能力描述说明这个技能适合解决什么问题、不适合解决什么问题。这也是后面做语义检索的核心字段。输入输出协议io_schema定义参数名、类型、必填项、输出结构。这一部分我会用JSON Schema来承载既能让模型按格式填参也能在代码侧做运行时校验。执行体executor真正干活的部分可以是本地函数、HTTP接口调用、一段脚本甚至是另一个Agent的子任务。校验与回退validation fallback执行完成后判断结果是否可用的逻辑以及失败时如何回到模型侧重新规划。这里要强调一下第四个要素。很多初版技能库都忽略反馈闭环结果技能执行失败后模型拿着错误结果继续往下编最后给用户的是一份看起来完整、其实全是幻觉的报告。校验与回退机制是agent-skills区别于普通函数调用库的关键一点后面我会专门展开讲。1.3 技能化改造后三个我没想到的直接收益说实话我最初做技能化改造只是为了解决提示词混乱但落地之后发现了三个额外的收益一是可测试性。技能是独立单元我可以为每个技能单独写单元测试不用每次都在多轮对话里手动验证。产品验收时直接跑测试集比人工点几十条对话高效得多。二是可复用性。技能天然是跨场景的。一个格式化日期范围的技能可以同时被周报生成流程和日程冲突检查流程引用同一个解析PDF技能在合同审核Agent和客户跟进Agent里都能用。三是可观测性。每个技能的调用记录、入参出参、执行时长、失败原因都能落日志。出了问题不再是模型某次表现不好这种玄学而是某个技能在某种输入下触发了回退这种明确的故障点。2. 技能库的骨架设计注册、检索、执行三层结构概念讲清楚了接下来是设计问题。我实践的agent-skills架构基本分三层注册中心、检索路由、执行器。这三层各有各的难点分别处理技能怎么存、模型怎么找到对的技能、技能怎么被安全地跑起来。2.1 注册层把技能变成可被机器理解的元数据注册层要解决的第一个问题不是存代码而是存技能的描述。我建议每个技能至少包含下面这些元数据字段说明示例id技能唯一标识pdf_parse_contractname技能短名合同PDF解析description给模型看的能力描述用于从合同类PDF中提取甲方、乙方、金额、期限等字段适合格式相对固定的商务合同parametersJSON Schema格式的参数定义{file_path: {type: string, required: true}}returns输出结构说明返回对象字段及类型说明tags领域标签辅助检索[pdf, contract, extract]executor执行器标识映射到真实处理函数handlers.parse_contract_pdfenabled是否启用truedescription字段是我踩坑最多的一个。写得太长模型检索时注意力会被无关信息干扰写得太短模型又分不清这个技能和另一个技能的区别。后面我在第5节会专门讲description的写法暴力调试经验。注册中心的存储用什么我建议视规模而定。技能数量在几十个量级时一个SQLite表加一个内存缓存就够了不用上来就上向量数据库。技能数量到了几百上千再把description和tags做embedding扔进向量库做语义召回。2.2 检索层关键词与语义的双通道召回检索层是Agent决定用哪个技能的关卡。我的做法是采用双通道召回再合并排序。第一通道是关键词与标签匹配。技能库里每个技能都有一组tags模型解析用户输入时可以先做一次词典匹配例如发现合同PDF提取这些词就把候选技能缩到几个。第二通道是语义相似度。把用户的原始请求和每个技能的description做embedding余弦相似度计算取Top-K。这个通道的好处是应对说人话的情况。比如用户说的是帮我看看这份协议里一年给多少钱它没有提合同也没有提PDF但语义上指向的就是合同解析技能。合并排序的策略我用过两种。前期用简单的加权求和关键词命中加1.0分语义相似度乘0.8再加进去后期技能变多之后改成了两步式路由——先用关键词找出可能相关的技能集合再在这个集合里做语义排序效果更稳定也减少了向量检索的整体时延。需要特别提醒的是检索层一定要有没有合适技能的出口。模型的原话是当所有技能的相关性都低于阈值时明确告诉模型当前没有可用技能你需要向用户说明能力边界或建议其他解决方式而不是强行从现有技能里选一个最不差的。这一步能挡住绝大部分错误调用。2.3 执行层参数校验、超时控制与审计日志执行层是真正让代码跑起来的地方也是稳定性最容易翻车的位置。我从线上事故中学到的教训是一定要在调用真实执行体前做三件套参数校验、超时控制、审计落盘。参数校验需要严格按照注册时声明的JSON Schema来。这一步不是走形式。模型填参偶尔会给出不存在的字段名甚至把字符串填成数组。校验不通过时正确做法不是直接抛异常喊执行失败而是把校验错误信息结构化地反馈给模型让它重新补参。超时控制同样不能省。有些技能调用外部API耗时不可控。我默认给外部调用类技能设8秒超时纯本地计算类技能设3秒。超时之后返回一个明确的状态码让Agent走重试或换路线的逻辑。最后是审计日志。每个技能的调用记录包含请求原文、路由命中的技能ID、入参、出参、执行时长、校验是否通过、回退是否触发。这条日志链是后期排查一切问题的起点。早期我没重视出问题了只能靠用户复述效率极低。3. 技能编排从单一技能到组合工作流单一技能只能解决点状问题。真实业务里用户的需求往往是链状的解析PDF → 提取字段 → 查重比对 → 生成摘要。这要求agent-skills不只是一个技能工具箱还必须有编排能力让多个技能按顺序、按条件地组合起来。3.1 技能之间的依赖关系与数据传递做编排之前先要建立技能之间的依赖表达。最常见的依赖有两种前置依赖requires执行技能B之前必须已经执行过技能A。比如生成周报技能依赖解析会议纪要技能的输出结果。并行依赖requires_any只需要多个前置技能中的某一个成功即可继续。比如获取用户信息可以通过CRM接口也可以通过本地数据库两者有一个能跑通就行。数据传递是另一个容易被忽略的问题。技能A的输出到技能B的输入字段名往往不一致。比如解析技能输出的是party_a_name查重技能的入参却是company_name。我的做法是引入一个轻量的字段映射表在编排定义里声明source_field - target_field的映射关系。这样技能本身可以保持通用性不需要为了配合某一个下游而改自己的输出结构。3.2 编排模板与条件分支实际操作中我不建议一开始就上重型的工作流引擎。大部分Agent任务的编排逻辑是相对固定的我倾向于用一个声明式的模板来描述。下面是一个简化版的周报生成编排定义workflow: weekly_report steps: - id: step1_parse skill: meeting_minutes_parser input: file_path: {user_uploaded_file} - id: step2_calendar_check skill: schedule_conflict_detector input: events: {step1_parse.output.events} - id: step3_summarize skill: weekly_report_generator input: parsed_events: {step1_parse.output.events} conflicts: {step2_calendar_check.output.conflicts} when: {step1_parse.status success}这个模板的核心价值是它可审计、可定向调试。编排逻辑和模型推理分开哪一步失败了就直接定位到对应技能和对应的输入数据而不是在几十轮对话记录里翻找。另外我建议在编排模板里加入分支字段when。它支持的表达式不多无非是等于、不等于、存在、为空这几种但足够覆盖绝大多数真实场景。分支出现得越来越复杂的时候就说明流程本身需要拆解而不是把编排模板做成一个图灵完备的语言。3.3 反馈闭环技能自省与动态修复这是agent-skills最值钱也最容易被忽略的一块。技能执行失败后不能只返回一个error_code就结束。要形成一个反馈闭环把失败信息变成模型下一步决策的输入。我在实现里的做法是定义三种失败类型参数类失败invalid_argument模型给的参数不符合Schema。此时把具体的校验错误返回给模型让它重新给参。环境类失败environment_error依赖的外部服务挂了。此时通知模型换用备选技能或者直接告知用户当前服务暂不可用。执行类失败execution_error代码内部错误。此时把堆栈摘要脱敏后写入日志同时触发一个技能诊断动作让另一个Agent分析是否需要调整技能描述或参数定义。这个闭环的价值在于它让技能库拥有了自愈能力。第一次遇到某类错误时系统可能会回退到联系人工处理但同一类错误出现三次以上我会给技能库加一个已知问题的描述字段提醒模型下次遇到类似情况时提前规避。4. 一个可运行的技能库最小实现讲了这么多设计给出一个可以直接跑起来的最小实现。我用Python写的主要是因为它写原型快而且生态里不管是做embedding还是做工具调用都很方便。生产环境换成Java或TypeScript也完全没问题核心思路是一样的。4.1 定义技能从JSON Schema到执行函数先看技能定义。我把每个技能的元数据用一个dataclass承载底层用JSON Schema做参数校验。import json import jsonschema from dataclasses import dataclass, field from typing import Callable, Any dataclass class Skill: id: str name: str description: str parameters_schema: dict returns_schema: dict tags: list[str] field(default_factorylist) enabled: bool True fallback_message: str def __post_init__(self): # 预编译参数校验器避免每次调用时重复compile jsonschema.Draft202012Validator.check_schema(self.parameters_schema) def validate_parameters(self, params: dict) - list[str]: errors [] try: jsonschema.validate(params, self.parameters_schema) except jsonschema.ValidationError as e: errors.append(str(e.message)) return errors实际注册一个技能时还需要执行函数。执行函数我建议统一收口成一个函数签名接收params字典返回一个ExecutionResult。dataclass class ExecutionResult: status: str # success | failed output: Any None error_code: str error_message: str execution_time_ms: int 0下面是一个最简单的计算统计数据技能的完整实现def execute_statistics(params): import time start time.time() numbers params.get(numbers, []) if not isinstance(numbers, list) or not all(isinstance(x, (int, float)) for x in numbers): return ExecutionResult(statusfailed, error_codeinvalid_argument, error_messagenumbers must be a list of int/float) try: output { count: len(numbers), sum: sum(numbers), mean: sum(numbers) / len(numbers) if numbers else 0, } return ExecutionResult(statussuccess, outputoutput, execution_time_msint((time.time()-start)*1000)) except Exception as e: return ExecutionResult(statusfailed, error_codeexecution_error, error_messagestr(e))4.2 注册与加载技能管理器技能管理器负责把skill定义和executor绑定起来并提供检索和调用接口。核心代码如下class SkillRegistry: def __init__(self): self._skills: dict[str, Skill] {} self._executors: dict[str, Callable] {} def register(self, skill: Skill, executor: Callable): if skill.id in self._skills: raise ValueError(fduplicate skill id: {skill.id}) self._skills[skill.id] skill self._executors[skill.id] executor def list_skills(self) - list[Skill]: return [s for s in self._skills.values() if s.enabled] def get_skill(self, skill_id: str) - Skill | None: return self._skills.get(skill_id) def execute(self, skill_id: str, params: dict) - ExecutionResult: skill self._skills.get(skill_id) if skill is None or not skill.enabled: return ExecutionResult(statusfailed, error_codeskill_not_found, error_messagefskill {skill_id} not found or disabled) validation_errors skill.validate_parameters(params) if validation_errors: return ExecutionResult(statusfailed, error_codeinvalid_argument, error_message; .join(validation_errors)) executor self._executors[skill_id] return executor(params)4.3 模型侧的调用链路上面都是基础管道真正的Agent调用逻辑在于一个Router函数把用户请求转化成选技能填参数看结果的循环。用一个简化的伪代码表示def run_agent(user_request, registry, llm_client): # 步骤1把用户请求和技能列表交给LLM让模型做技能选择 skills_meta [ {id: s.id, name: s.name, description: s.description, parameters_schema: s.parameters_schema} for s in registry.list_skills() ] tool_choice llm_client.choose_skills(user_requestuser_request, skillsskills_meta) # 步骤2校验模型输出 if not tool_choice or tool_choice[skill_id] not in [s.id for s in registry.list_skills()]: return {response: 目前我没有找到合适的能力来处理这个需求。} # 步骤3执行技能 result registry.execute(tool_choice[skill_id], tool_choice[arguments]) # 步骤4根据执行结果决定是继续调用还是输出给用户 if result.status success: final_answer llm_client.generate(user_requestuser_request, tool_outputresult.output) return {response: final_answer, trace: {skill_id: tool_choice[skill_id]}} elif result.error_code invalid_argument: # 把校验错误塞回LLM让它重新给参 return run_agent(user_request, registry, llm_client) else: return {response: 这个能力暂时不可用请稍后再试或调整输入。, trace: {error: result.error_message}}注意步骤4里的一个细节对于invalid_argument错误我选择让Agent自动重试一次。但重试要有次数上限否则模型会陷入无限循环。我的经验是同一个技能最多重试两次两次都失败就切换其他技能或直接拒绝。4.4 最小验证跑一个多技能任务上面实现跑通后拿一个组合任务验证。假设技能库里注册了meeting_minutes_parser和weekly_report_generator两个技能用户输入是把这份会议记录整理成周报。实际运行链路是模型选择meeting_minutes_parser传入file_path参数解析技能返回结构化的事件列表系统把事件列表作为weekly_report_generator的输入生成技能返回最终周报模型把周报包装成最终给用户的话术。这个最小实现里没有向量检索几十个技能规模下靠LLM直接选择也够用。但我建议规格提升到上百技能时再在步骤1之前加一道检索预筛避免把全部技能元数据一次性塞给模型。5. 上线之后的真实翻车现场与补救方案设计说得再漂亮不如一次上线跑回来的教训多。下面这几类问题基本是每个做agent-skills项目的人都会遇到的我把自己的排查链路和最终方案写出来算是给大家排雷。5.1 翻车一description膨胀导致技能乱选上线一周后我遇到最诡异的问题是用户问今天天气怎么样模型却调用了周报生成器。查日志发现周报生成器的description里写了整合一段或多段非结构化信息而天气信息在小模型的语义空间里也被归到了非结构化信息里意图匹配直接偏了。问题根源是description写得太泛覆盖了大量不该覆盖的场景。我用的补救方案有三步收敛描述范围description只写最能代表该技能的2-3个典型场景并明确加上否定边界不适用于计算类问题不适用于实时数据查询。以周报生成器为例描述改为仅用于把一组结构化事件或会议记录合并为固定模板的周报不用于回答事实性问题。引入典型触发词在tags里补充周报、weekly report、会议记录、meeting notes这类数据让关键词通道把一个技能稳稳钉在它该在的位置。做路由回归测试集准备50条典型用户输入和对应期望技能每次改description都要全量跑一遍防止修好一个又弄坏另一个。5.2 翻车二技能之间参数抢食第二个问题是两个技能都有file_path参数但一个期望是本地磁盘路径另一个期望是URL。模型在选择技能时经常把刚才那个技能的参数直接带过来传过去导致校验失败。我采取了两层措施。第一层是参数作用域隔离——在技能描述里明确写本技能的file_path仅接受本地路径如果输入是URL请先调用文件下载技能。第二层是加了适配器两个技能之间如果经常出现参数转换需求就直接做一个组合技能把转换逻辑包在里面对外只暴露一个上层参数模型就没机会用错参数结构了。5.3 翻车三模型把技能当摆设还有一种头疼的情况是技能都注册好了description也写了但模型就是不主动用用户问题明明能由技能解决模型硬是靠自己的知识编了个答案。这类问题主要出现在两类模型上一类是工具调用能力比较弱的另一类是研发为了省token把技能列表砍得太短了。我的排查顺序如下先看技能元数据有没有进入模型的实际可见上下文。有些框架层把技能列表截断了模型根本不知道有技能存在。再检查技能的description是不是太口语化。模型对工具描述这种文体的响应是最稳定的太像闲聊的话模型反而不会触发调用。最后给2-3条few-shot样本在系统提示里给出相似的用户问题→技能调用完整示例。很多情况下这一招比修改description更有效。5.4 我的技能准入清单三个硬性指标有了这些教训我现在给一个技能入库定了一个简单的准入清单宁可少一点也要保证每个技能是可靠的单一职责一个技能只干一类事情。如果一个技能里又有解析又有计算又有格式化就拆开重做。明确失败模式写代码的时候就必须定义好三类失败可能参数错、环境错、执行错并在返回结构里留好字段。这也是我要求必须做压测的标准。有独立的验收用例至少5条标准输入和对应预期输出入库之前先跑通再来注册。这三个标准把技能库的质量门槛从能跑提升到了可维护。至少在我这边遵循这套清单之后线上因为技能本身质量问题引起的故障少了大半。最后说一点个人体会。agent-skills本质上不是银弹它解决的是Agent系统里能力组织方式的问题而不是模型够不够聪明的问题。如果你的模型在多轮推理上本来就弱那再好的技能库也只是降低了它犯错的概率。但反过来当你发现Agent项目里prompt越来越长、规则互相打架、每次改功能都像是在解耦一团乱麻时先别急着换模型试试把能力搬进独立的技能单元里。我实际做下来这个改造的性价比比调prompt高得多。