ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Agent技能化改造:从提示词堆砌到可治理的能力体系

2026/10/7 6:46:24 拓冰建站 浏览量
Agent技能化改造:从提示词堆砌到可治理的能力体系 我先把自己最真实的一次翻车现场摆出来。当时我给内部系统做Agent化改造目标很简单让AI助手能查订单、能改地址、能催发货。第一版图省事把所有业务逻辑塞进一个大函数然后用一段又长又绕的系统提示词告诉模型“你看着办”。结果演示当天就崩了——用户说“我的快递什么时候到”模型调用的是“修改收货地址”接口参数传了“快递什么时候到”用户说“帮我联系客服催一下”模型直接触发了退款接口。事后排查锅不在模型在我那批“工具”设计得太含糊。后来我把整套能力往 agent-skills 这个方向重构才算真正理顺。所谓 agent-skills说穿了就是把Agent的一项完整能力封装成带清晰语义描述、严格参数协议和独立执行体的“技能单元”让模型像点菜一样按需挑、按需用。它跟普通函数调用的本质区别在于函数是写给程序员看的技能是写给模型看的。模型不读源码它只能靠你提供的名字、描述、参数约束来判断这个技能是什么、边界在哪、什么时候该触发、参数该填什么。这篇内容我就结合自己从零搭过、又反复改过的一套技能系统把 agent-skills 背后的设计思路、数据结构、注册与调度机制、实际落地过程和常见翻车点完整拆一遍。适合正在做Agent产品的开发者也适合想搞明白“模型之外的能力外挂”是怎么运转的新手。1. agent-skills 到底是什么拆掉“万金油”Agent的最后一堵墙1.1 为什么提示词里写满工具说明还是经常失灵很多人第一反应是工具说明写进系统提示词不就行了吗我在翻车之前也是这么想的而且坚定地认为“提示词足够长模型就足够聪明”。结果发现两个硬伤。第一个硬伤是检索成本。当你的Agent只有三五个工具时提示词里写清楚确实够用。可一旦业务复杂起来工具数量到了二三十个每个工具的说明平均一百字模型每次推理都得把这三千多字从头读一遍Token消耗直接翻倍而且越长越容易漏读——它可能只注意到了前面的工具把你后来补的描述全忽略了。第二个硬伤是边界模糊。写在提示词里的“工具说明”本质上是一段散文两个功能相近的工具描述稍不注意就会重叠模型根本分不清。比如“查订单状态”和“查物流轨迹”字面上看差不多但内部一个走订单系统、一个走快递接口参数和返回结构完全两回事。提示词里并排摆着模型大概率选错。技能化的思路就是把“散文”改成“结构化条目”。每个技能有唯一标识、有语义描述、有JSON Schema参数协议、有可执行的handler模型看到的是一张清晰的能力清单而不是一段需要揣摩的自然语言。1.2 技能的“三件套”语义描述、参数协议、执行体我在项目里给技能下了个很克制的定义一个可被模型识别、可被校验器校验、可被代码执行的最小能力单元。具体拆成三部分。语义描述是给LLM看的说明书它决定了“什么时候该选这个技能”。参数协议是给校验器看的契约通常用JSON Schema表达写明字段名、类型、必填项、取值范围、格式要求它决定了“这个技能接不接受这次调用”。执行体才是真正干活的代码本地函数、远程API封装、甚至一个工作流编排器都可以。这三件套缺一不可。缺了语义描述模型认不出技能缺了参数协议模型敢传任何离谱的值进来缺了执行体前面全是纸上谈兵。我见过不少开源项目把技能的参数定义得很漂亮但handler里只写了个print——看着规范跑起来完全没用。真正要上生产三者必须同时到位。1.3 技能库不是工具列表而是一套可治理的资产把工具收敛成技能最大的收益不是“模型调用更准了”而是你第一次拥有了对Agent能力的治理手段。工具列表只是代码里的一堆函数技能库则是可以被注册、被检索、被观测、被动态启停的资产。举个例子。原来我要加一个新能力得改提示词、改代码、重新部署整个Agent。技能化之后我只需要往注册表里塞一条新技能记录写清楚描述和参数协议再挂上handler就能在下一个请求里生效。想下线某个能力从注册表摘掉即可不用动Agent主流程。想统计哪个能力被用得最多技能执行日志天然就有。这些在“大函数提示词”的模式下全都做不到而做Agent产品没有观测和治理基本等于盲人开车。2. 技能体系的核心设计接口、描述与注册机制2.1 一个技能该长什么样数据结构先定下来技能的数据结构是整个体系的地基。我踩过的坑是一开始把技能定义得特别随意一个技能一个样有的用dict、有的用dataclass、有的干脆就是裸函数导致后面写调度器时被迫做一堆兼容。后来统一成下面这种JSON形态所有技能必须照着填。{ name: book_meeting_room, description: 预订指定时间段的会议室并返回预订编号。当用户需要预约会议场地时使用。若时间不明确应先追问具体开始和结束时间再调用。此技能只负责预订不负责查询空闲场地。, parameters: { type: object, properties: { room_id: {type: string, description: 会议室编号例如 MT-01}, start_time: {type: string, format: date-time, description: 开始时间ISO8601格式例如 2025-05-20T14:00:00}, end_time: {type: string, format: date-time, description: 结束时间ISO8601格式必须晚于开始时间}, attendees: {type: array, items: {type: string}, description: 参会人邮箱列表可为空} }, required: [room_id, start_time, end_time] } }有几个细节值得强调。name必须是全局唯一的短标识模型靠它精确匹配不要用“会议室预订功能”这种带中文长名能短则短但也不能短到失去语义。parameters里每个字段都要写description很多模型的Function Calling能力高度依赖字段注释注释缺失时它经常自己瞎编一个字段。required列表要克制只放真正缺一不可的字段否则模型会因为“凑不齐参数”而干脆不调用。2.2 技能描述怎么写模型才能精准命中这是整个技能体系里最容易被低估的一环。描述写得好不好直接决定调用准确率而且没有捷径只能靠迭代。我自己总结出一个四段式写法动作、场景、触发条件、排除边界。拿查天气的技能举例。差的描述是“查询天气”好的描述是“根据城市名称和日期查询天气情况返回温度、湿度、降水概率。当用户询问‘今天热不热’‘明天会下雨吗’时使用。不用于查询空气质量不用于预测一周以上的长期气候”。动作解决“技能做什么”场景解决“哪些说法该触发它”触发条件进一步细化排除边界则用来和相邻技能划清界限。我后来做了一轮技能描述评审把所有技能的description打印出来逐条读凡是出现“其他”“等等”这类含糊词的统统重写。那段描述是模型唯一能借以判断的依据含糊等于让模型掷骰子。2.3 注册表与动态技能加载几百个技能时该怎么办当技能数量少直接把全部技能塞给模型就行。可当技能库膨胀到几十个甚至上百个时就面临两个问题一是很多模型对tools数量有限制传太多可能被截断二是即使传得下模型在大量技能中做选择的准确率也会下降。我的做法是加一层技能注册表和动态筛选。注册表是一个全量技能池线上请求到来时先根据会话上下文、用户画像、当前业务场景召回一个子集只把这个子集传给模型。比如客服场景只挂订单查询、售后、退款三个技能会议室场景只挂日程、预订、通知三个技能。召回可以用简单的关键词匹配也可以用embedding检索前期关键词足够。这一步做完模型面对的选项从一百个降到了五六个选择和准确率都明显改善。SKILL_REGISTRY {} def register(meta): def decorator(func): skill_meta dict(meta) skill_meta[handler] func SKILL_REGISTRY[skill_meta[name]] skill_meta return func return decorator register({ name: book_meeting_room, description: 预订指定时间段的会议室并返回预订编号。当用户需要预约会议场地时使用。, parameters: {...} }) def book_meeting_room(room_id: str, start_time: str, end_time: str, attendees: list | None None): # 实际调用会议室系统API return {meeting_id: M-10086, status: confirmed}注册表不只是一张dict它同时要承担技能的热更新、上下线和版本管理。我习惯把所有技能以模块方式组织每个技能一个文件注册逻辑集中在入口。上线新技能就是新增一个文件并注册下线就注销整个过程不动Agent主进程。3. 从零搭建一套可落地的技能系统3.1 最小可行版本三层结构技能系统不需要一上来就搞微服务。我的最小可行版本只有三层技能定义层、调度层、执行层。技能定义层负责声明技能元数据并注册进SKILL_REGISTRY。调度层读取当前会话可用技能把tools列表传给模型接收模型的tool_calls返回再逐个调用对应handler。执行层负责真正干活并把返回值整理成模型能继续理解的消息格式。我把调度层写成了一个很薄的循环模型推理 - 解析tool_call - 校验参数 - 执行handler - 把结果追加到对话历史 - 再让模型推理直到模型不再请求调用技能。这个循环是所有Agent Skill调度的基本盘简单但够用。def run_agent(user_message: str, active_skills: list[str]): messages [{role: user, content: user_message}] for _ in range(MAX_TURNS): # 必须设置最大轮数防止死循环 response llm.chat( messagesmessages, tools[to_openai_tool(SKILL_REGISTRY[name]) for name in active_skills] ) if not response.tool_calls: return response.content for tool_call in response.tool_calls: skill SKILL_REGISTRY[tool_call.function.name] raw_args json.loads(tool_call.function.arguments) valid_args validate_with_schema(raw_args, skill[parameters]) result skill[handler](**valid_args) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) raise RuntimeError(Agent调用超过最大轮数)真正上生产时我建议把validate_with_schema这一步做得严格一点不要只做类型转换。用现成的JSON Schema校验库校验失败时把错误信息返回给模型让它重新生成参数比直接抛异常友好得多。3.2 技能组合与编排从单技能到工作流很多场景不是单技能能搞定的而是需要多个技能按顺序或按条件组合。最简单的组合方式是靠模型自己规划在一次推理中并行返回多个tool_call调度层按顺序执行即可。稍微复杂一点的场景需要显式的工作流编排。我举一个实际例子。用户说“帮我约老王明天下午开会订个小会议室再给老王发个会议提醒”。这需要三个技能查日程判断老王是否有空、预订会议室、发送通知。模型在推理时如果一次给出三个tool_call调度层逐个执行就行但要注意顺序——查空闲应该在预订之前。这个时候就不能完全依赖模型的自觉我在技能描述里写了硬性约束“本技能应在确认人员空闲后调用”并且在工作流编排器里配置了依赖关系。如果你不想引入重型工作流引擎一个轻量办法是在代码里写一个固定的编排函数规定步骤顺序并允许某些步骤并行。等场景复杂到需要动态编排时再上状态机或图执行引擎但大部分业务其实用不到那一步。3.3 技能热更新与灰度上线不用重启Agent技能系统的一大优势是可以热更新。我可以给注册表增加一个“版本”字段线上请求打到新的技能版本时记录一个采样率先让5%的流量走新技能观察执行成功率和模型调用准确率稳定后再逐步放量。这个机制在技能改造时特别有用。我经常做的事情是只改某个技能的description然后灰度观察它在真实请求里的命中率变化。描述写得更清晰了命中率应该上升如果下降说明改坏了立即回滚到旧版本。整个过程十几秒不需要重新部署整个应用。对Agent这种“上线了也没法穷尽测试”的系统灰度能力不是加分项而是保命项。4. 实操记录让Agent学会“查日程 订会议室 发通知”三个技能4.1 技能定义与参数设计下面是我在一次实际改造中完整实现的三个技能。先看查日程它需要判断某一时间段内某人是否空闲。{ name: check_calendar_availability, description: 查看某个用户在指定时间段的日程占用情况返回空闲或冲突。当用户想约人开会、需要确认对方是否有空时使用。此技能只读日程不负责创建日程。, parameters: { type: object, properties: { user_email: {type: string, description: 被查询人的邮箱}, start_time: {type: string, format: date-time}, end_time: {type: string, format: date-time} }, required: [user_email, start_time, end_time] } }订会议室的技能就是前面那个book_meeting_room。发送通知的技能则要包含标题、正文、接收人等字段。三个技能放在同一批tools里传给模型然后我用一句自然语言发起了测试。4.2 调度与冲突处理踩过的坑第一轮测试就踩了坑。模型把“约老王明天下午开会”理解成了先订会议室、后查空闲导致房间订了、老王其实没空又得退订。后来我把check_calendar_availability的description改成“在用户明确要邀约他人开会时必须先调用此技能确认对方空闲再调用预订技能”同时把book_meeting_room的描述里也加了“调用本技能前请确认相关参会人日程已确认空闲”。两边都约束模型才老实按顺序走。第二个坑是参数格式。模型输出时间时喜欢用“2025-05-20 14:00:00”这种带空格的格式而我们的会议室系统API只认ISO8601标准格式“2025-05-20T14:00:00”。我把format字段写成date-time后模型还是偶尔犯错。最终我在校验器里加了一步时间格式归一化解析失败就返回错误信息让模型重新传。这里的关键经验是别指望模型输出的参数一次通过校验和重试机制必须兜底。4.3 效果验证什么样的日志才算“技能命中”技能系统上线后我做的第一件事是加结构化日志。每条技能调用记录三块内容入参、出参、执行的latency外加一个“模型是否一次就正确选中了技能”的标记。我把“一次选中”定义为模型在第一个响应里就调用了预期技能且参数校验一次通过。这个指标非常直观地反映技能描述的质量。实测下来优化前“查空闲 - 订房 - 发通知”三步流程的平均一次命中率只有四成左右大量请求要模型自己纠错两三轮。优化描述和加工作流约束后一次命中率提到了七成以上剩下的三成基本归结为模型对时间表达的二义性理解可以通过追问澄清解决已经算可接受水平。5. agent-skills 落地中的常见问题与排查技巧5.1 模型为什么就是不调用技能这个问题几乎每个接入Agent Skills的人都会遇到而且原因五花八门。我归纳下来主要有三类。第一类是描述写得像函数文档。比如“query_order_by_id(params: order_id)”模型看了毫无感觉。解决方法是把描述改成用户场景语言让模型“看懂”而不是“读懂”。第二类是系统提示词里写了互相矛盾的话比如“尽可能不要调用工具除非用户明确要求”这种负向约束会显著抑制模型调用技能的意愿。第三类是技能数量太多被模型上下文截断排在后面的技能模型根本看不见自然不会被调用。定位方法很简单把发给模型的messages和tools完整打印出来人工模拟模型视角读一遍。如果让你来选你都会犹豫那模型不调用就一点都不奇怪。5.2 参数幻觉与类型校验参数幻觉是比“不调用”更隐蔽的坑。模型可能正确选中了技能但参数里出现系统里根本不存在的会议室编号。我经历过模型把“老王的邮箱”编造为“wangexample.com”而真实邮箱是“wangcompany.com”。更可怕的是模型自己补全“默认值”——当参数可填可不填时它倾向于填一个看起来合理但实际错误的值。我的应对是三层防线第一层只要字段允许就设为可选并明确说明缺省行为第二层校验器对枚举值和格式严格校验宁可报错也别容忍脏数据第三层handler执行前做二次权限校验类似“当前用户是否有操作此类数据的权限”特别是涉及改地址、退款这类敏感操作模型一旦传错代价很高。5.3 技能膨胀后的性能与成本控制技能数量一多影响最直接的是Token成本和响应耗时。每个技能的描述和参数Schema都要拼进请求哪怕模型最后只调一个技能其他技能的定义也已经被消耗掉了。我的优化思路是分级高频技能常驻低频技能按场景动态加载。同时给每个技能加“最近30天调用次数”统计定期把零调用技能标记为冷技能从默认加载清单里移除。这一步做完我的请求体积直接降了三分之一响应速度也快了将近20%。如果你们家的技能库有几百个技能强烈建议先做冷热分离再去想换模型。5.4 技能间依赖与死循环防护当多个技能可互相触发时最怕出现死循环——比如“查日程失败”触发了“发送错误通知”“发送通知失败”又触发了“重试发送”最后两者反复调用烧完预算还没结果。我在调度层做三个保护最大迭代轮数限制默认3轮超过即终止同技能连续调用次数限制同一个技能在一轮会话里连续触发超过2次就停下显式禁止技能内部再回调技能所有二次调用必须通过同一个调度入口这样任意的调用图谱都逃不过轮数计数器。不要侥幸不要觉得模型不会循环生产环境里什么离谱路径都跑得出来。6. 个人实践心得与扩展方向最后聊几句实在的。技能系统这个东西第一版真不用做得特别完美但有三件事我会建议你从第一天就做好技能数据结构的Schema字段统一、调度层和业务逻辑彻底分离、技能调用的全量日志。别先纠结算法先把这三个基建打牢后面演进会顺得多。我后来的一个体会是agent-skills 最大的价值不在于单一技能写得多好而在于让整套Agent能力具备了“可被评估、可被治理、可被演化”的骨架。技能描述改一版、灰度一下就能看到真实命中率的变化加一个技能、摘一个技能都是分钟级操作。这套机制跑顺了之后我再也不怕给Agent加新能力了因为它不再是一次赌博而是一次有数据支撑的迭代。接下来我准备在技能评估维度上继续做给每个技能挂一个自动化评测集每次改动先跑评测再上灰度把人为判断进一步交给数据。如果你也正在搭这一套我的建议是先把调度循环写死把日志打全一切从简单开始。