ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent Skills实战:从长Prompt到模块化技能体系的设计与落地

Agent Skills实战:从长Prompt到模块化技能体系的设计与落地 我最近在重构一个多智能体项目的时候把原来那套“所有工具全部写进一段超长 system prompt”的做法彻底推翻换成了基于 agent-skills 的模块化技能体系。做完以后最大的感受是Agent 工程里真正难的部分不是选模型也不是调 prompt而是怎么把能力边界划清楚让模型在正确的时间、用正确的参数、调用正确的技能。现在行业里说的 Agent Skills本质上就是一组可注册、可复用、可独立演进的能力模块。它可以是一个工具函数也可以是一段检索流程甚至可以是一整条多步骤的业务工作流。关键不在于它像不像“函数”而在于它得有名字、有描述、有明确的输入输出能被调度、被评估、被回滚、被灰度上线。这套思路解决的核心问题是把 Agent 从“一个大脑什么都干”变成“一个大脑指挥多个专业手臂”。这篇文章我会结合自己项目里的工程实践把 agent-skills 从设计、注册、路由到排障的整套打法拆开讲。内容比较偏落地适合正在做 Agent 应用、被长 prompt 和工具清单折磨过的同学参考。1. 先把“Agent Skills”这个说法掰开揉碎1.1 技能到底是什么不是什么很多人会把 Agent Skills 和“给模型加一段 few-shot 示例”混在一起这是最大的误解。Prompt 里的示例只是让模型“知道有这件事”但技能是真正能执行的代码路径。它不只是口头描述而是注册在运行时里的一段程序有真实的副作用查数据库、调 API、写文件、发消息、改状态。我用一个生活化的类比来理解LLM 本身像一个高学历但没有任何工具的新员工懂推理、能对话但没法开单、没法查库存。Agent Skills 是给这个员工配的标准化工具每个工具都有使用说明书他拿到任务时先看说明书再决定用哪把扳手。如果你把所有说明书写成一本书塞给他他也会看但翻书的时间、找错工具的概率都会急剧上升。而把工具拆成一个一个独立技能他只需要知道“目录”和“索引”真的要用某个技能时才去翻对应说明书。所以技能的本质是“可执行的能力单元 可被模型理解的使用说明”。两者缺一不可。代码写得再漂亮描述写不清楚模型不知道什么时候调它技能就等同于不存在描述写得再妙底层执行代码不稳定那也只是给模型画饼。1.2 技能在 Agent 运行时里处于哪一层我自己在项目里把 Agent 运行时拆成三层感知层、决策层、执行层。感知层负责接收用户消息、系统事件、外部回调决策层是 LLM 主循环负责理解意图、判断下一步、选择技能执行层就是我们说的技能注册表里面挂着所有可被调用的 agent-skills。这三层必须分开不然后期会痛不欲生。感知层管的是“世界发生了什么”决策层管的是“我该做什么”执行层管的是“我怎么做到”。如果这三层搅在一起比如在技能代码里写死用户话术在决策逻辑里直接访问数据库那将来任何一个改动都会牵一发动全身。技能被设计成独立模块后我改一个技能的内部实现决策层完全无感我新增一个技能也不需要重写决策逻辑只要往注册表里加一条记录就行。这样的分层还有一个好处可观测性。之前用超长 prompt 调工具时模型到底调没调、调的顺序是什么只能靠肉眼翻日志。现在技能调用会经过一个统一的执行入口我可以在入口处埋日志、记录参数、追踪耗时所有行为都能量化。1.3 技能抽象层解决的最实际问题最实际的问题是上下文窗口。一个稍微复杂的 Agent 项目工具可能有十几个到几十个。如果把每个工具的全量 JSON Schema 和示例全部塞进 system prompt光工具描述就可能吃掉了上万 token。代价不只是贵更重要的是把模型用来推理的有效空间挤没了还会出现“工具描述互相干扰”的奇怪现象某两个工具的返回字段名类似模型就开始串场。技能抽象层相当于在 LLM 和底层工具之间加了一个“目录服务”。LLM 平时只看到每个技能的精简摘要名字、一句话描述、何时用、何时不用。真正调用时再把完整参数结构动态注入。这个套路能显著降低 prompt 里的噪音也让技能可以独立扩展。项目从 5 个技能扩展到 30 个技能时主 system prompt 几乎不用动只动技能目录这才是 Agent 工程该有的状态。2. 设计技能体系前先想清楚这些事2.1 任务粒度怎么定技能不应该拆成“函数级”我第一次做技能拆分时踩过大坑把“发 HTTP 请求”“解析 JSON”“日期格式化”这种动作都做成了技能。结果模型每次干活都要连续调用七八个技能中间任何一次调用失败整条链路就断掉。后来想明白了技能粒度不该按“函数动作”来划而应该按“决策单元”来划。什么叫决策单元就是模型做一个决定、能拿到一个完整中间结果的单元。以搜索为例“执行一次网页搜索并返回结果列表”是一个决策单元“打开百度搜索链接”不是。再比如查天气“获取某城市未来三天天气”是一个技能“调用天气 API 并解析 JSON”是两个技能但第二步不该让模型去决定因为它是确定的机械动作。我的经验是如果某几个动作永远绑定在一起出现它们就不该是多个技能而是合成一个技能。技能边界尽量贴合“用户可理解的一个能力”否则模型调不齐调用链也难排查。与其让模型编排十个细粒度动作不如给它三个中等粒度的技能让它做三次判断。2.2 命名与描述是路由的命门Agent 技能路由这件事模型没有我们想象的那么聪明。它做技能选择时基本上就是看名字和描述语义上觉得像就选。所以技能命名和描述写得不好后面所有工程优化都是白费。命名上我强烈建议用“动词 宾语”的短语形式比如search_web_pages、create_ticket、send_email_notification。名词命名最容易出问题比如weather_info模型有时候会把它当作一个“对象名”而不是“动作”调用意图就不清晰。另外要避免两个技能名字里关键词大量重叠比如web_search和company_search模型经常选错。我的做法是给两个相近技能都加上场景限定比如search_public_web和search_internal_docs一看到“internal”和“public”就能区分。描述部分比命名更重要。我总结出一个三段式模板第一条写用途第二条写典型触发场景第三条写禁止使用场景。用途要能回答“这个技能帮用户解决什么问题”触发场景要尽量贴近真实用户会说的话比如“当用户询问今天新闻、最新报价、实时股价时”禁止场景非常关键它比正面描述更能纠偏比如“该技能只用于公开互联网信息不要用于查询内部知识库内部文档请使用 search_internal_docs”。很多路由错误加一句负例描述就救回来了。2.3 输入输出契约必须显式化技能的输入输出是给两方看的模型看输入的参数说明程序看真实的函数调用。两边都得约束住。我统一用 JSON Schema 描述输入突出必填字段和默认值。描述里尽量写“人话”比如retrieval_k可以描述成“返回多少条结果默认 5最大 10”模型就能正确理解。输出也要有稳定结构。一个技能无论内部逻辑多复杂对外返回建议统一成三个部分状态、数据、补充说明。状态是成功或失败数据是真正要用的结果补充说明是从技能内部返回给模型的提示比如“结果可能是过时的建议再搜索一次”。模型在拿到结果后会根据补充说明决定要不要继续调用别的技能。这个设计让我在排查问题时省了大量时间因为每条调用记录都能看到模型当时拿到的到底是什么。异常处理同样要写进契约。技能内部报错时不要只返回“error: xxx”最好返回“错误码 对模型可读的下一步建议”。比如“用户身份信息缺失无法创建工单请先询问用户邮箱”。这样模型遇到失败时不是绕圈子重试而是能直接向用户澄清或换一条路线。2.4 技能分类与注册表设计技能数量一旦超过 15 个就必须给技能分门别类。我目前常用四类基础工具类、知识检索类、流程操作类、组合协作类。基础工具类是查天气、发通知、做计算知识检索类包括内部文档、网页搜索、数据库查询流程操作类涉及多步业务动作比如创建订单走到审批流组合协作类是面向多 Agent 场景的外部技能暴露。分类不是为了好看而是为了让路由更稳。我在系统提示里给 LLM 的是“分类目录”而不是一长串平铺的技能列表。比如可用技能分类 - 知识检索search_public_web, search_internal_docs - 事务操作create_ticket, send_email_notification - 数据分析execute_sql, analyze_csv模型先看分类再定位具体技能选择压力小很多。注册表里除了技能名和描述我还记录版本号、所属分类、所需权限、调用成本权重。这些字段平时不注入 prompt但在路由冲突仲裁、成本控制、权限校验时会用到。技能注册表本质上就是 Agent 运行时的“人力资源档案”。3. 把技能真正跑起来的落地步骤3.1 技能注册表与技能加载器我建议技能管理不要靠手写分支而是做一个注册表加一个加载器。每个技能模块暴露一个register入口把自己的元信息交到注册中心。以下是我项目里的简化版 Python 示例class SkillRegistry: def __init__(self): self._skills {} def register(self, skill: BaseSkill): key skill.name if key in self._skills: raise ValueError(fduplicated skill: {key}) self._skills[key] skill def get(self, name: str): return self._skills.get(name) def catalog(self): # 只返回精简摘要供 LLM 做路由 return [ { name: s.name, description: s.description, category: s.category, when_to_use: s.when_to_use, when_not_to_use: s.when_not_to_use, } for s in self._skills.values() ] def list_skill_names(self): return list(self._skills.keys())加载器会扫描配置目录把每个技能文件当作插件加载。新增技能时我只要新建一个目录、写一个类、在配置里加一行完全不用改 Agent 主循环。这背后是“开闭原则”的实践对扩展开放对修改关闭。Agent 主逻辑永远只依赖注册表接口新的技能来了就注册老技能下线就从注册表摘除。这样团队多人协作时冲突也少互相之间不用老碰同一个文件。3.2 让 LLM 学会“翻技能目录”注册表建好了下一步是把技能目录以合适的方式交给模型。这一步最关键的是控制信息量。以 20 个技能为例完整 schema 全部展开大概有 8000 到 12000 token精简目录只需要 1500 到 2500 token。我的策略是在主 system prompt 里只放精简目录包括名字、一句话用途、分类。等到模型明确要调用某个技能时我再动态把完整参数结构注入上下文。动态注入的方式有两种一种是在 LLM 的 tool_choice 机制里按需提供 function schema这是 OpenAI 函数调用标准做法另一种是自己在生成前拼一段“当前技能参数说明”。前者更省事后者更灵活。我目前两种都用内部技能走函数调用外部 Agent 协作走自拼接说明。不管哪种方式核心原则一致让模型在需要时才看到大块 schema平时只看目录。为了让模型“翻目录”做得更好我还会在每次生成前对用户消息做一次轻量意图分类把可能命中的两三个技能排到目录列表的前面。这个排序处理很有效等于给模型划了重点路由准确率能提升不少。3.3 多技能编排的两种常见姿势技能之间怎么组合我总结为两种姿势串联编排和状态机编排。串联编排适合“先做 A再做 B最后 C”这种固定顺序流程。比如用户要生成一个季度报告模型先调用query_database取数再调用chart_generator画图最后render_pdf输出文件。每一步的结果自然传给下一步逻辑清晰最适合单线程对话场景。状态机编排适合有分支、有等待、有人工干预的流程。比如一个报销流程提交报销单、财务审核、出纳打款、通知结果。这种不能靠模型从头到尾一口气调完中间可能有长时间等待和人工步骤。我会把整个流程写成一个 Workflow 技能技能内部管理自己的状态该等就等该通知就通知。模型只负责发起这个工作流后续执行由运行时接管。我见过最糟糕的做法是把状态机逻辑也塞给 LLM 决定比如让模型自己判断“现在审核通过了吗”。模型不是可靠的状态管理工具它每一次生成都可能不一致。记住模型负责“决策”程序负责“状态”这是 Agent 工程的一条铁律。3.4 一个可参考的技能定义示例下面这个 JSON 是我项目里一个真实技能的配置字段不算多但足够体现设计思路{ name: send_email_notification, version: 1.2.0, category: 流程操作, description: 向用户指定邮箱发送一封模板通知邮件, when_to_use: 当用户要求发送邮件、邮件通知、给客户发送确认信时, when_not_to_use: 不要用于回复邮件、创建邮件模板、查看历史邮件这些请使用 mail_reader 或 template_manager, parameters: { to: {type: string, description: 收件人邮箱必填}, subject: {type: string, description: 邮件主题默认空}, template_id: {type: string, description: 模板ID必填}, variables: {type: object, description: 模板变量可选} }, returns: { status: string, message_id: string, suggestion: string }, permission: notification:send, timeout_ms: 10000 }写完这个配置后我还会配套写一个实现类。实现类内部负责调用邮件服务商接口、处理失败重试、记录发送日志。对外暴露的返回值永远遵循状态、数据、补充建议三件套。模型看到这样的返回结构可以轻松决定下一步如果statuserror且suggestion是“请重新确认收件人邮箱”模型就会向用户追问而不瞎重试。4. 工程落地中的关键细节与排查技巧4.1 上下文窗口与中间状态管理技能用得越多上下文里堆积的结果就越多。搜索技能一次可能返回 10 条网页摘要每个摘要几百字几轮下来上下文就被塞满了。我常用的手段是让技能在返回前先做一层摘要优先返回最相关的三到五条并附上“完整结果可进一步查看”的说明。这样模型看到的是浓缩后的信息主决策不会被细节淹没。跨技能传递中间状态也一样要注意。我最开始把临时变量全放进全局字典技能 A 写下一个order_id技能 B 再去读。这个做法在并发和异步下非常容易出问题。后来我引入了“工作记忆”对象每个会话持有一个独立状态容器技能之间通过这个容器交换数据。技能内部只声明自己需要读哪几个字段、写哪几个字段注册表会做字段名校验。状态管理越规范Debug 越轻松。4.2 重试、超时与幂等控制外部 API 不稳定是常态技能执行必须有超时和重试机制。我在注册表里给每个技能配了timeout_ms字段和重试策略。默认超时 10 秒重试两次且只有在错误是“瞬时错误”时才重试比如网络超时、HTTP 503。如果是“确定性错误”比如参数不合法、权限不足重试多少次都没用我直接让技能返回失败建议避免浪费资源。比超时更隐蔽的是幂等。技能一旦涉及写操作比如创建订单、发通知、扣余额重试就可能导致重复执行。我的做法是引入幂等键调用方每次生成一个唯一请求 ID技能执行时带上这个 ID服务端记录已处理过的 ID重复请求直接返回上一次的结果。这个设计是支付和订单系统的老经验但很多 Agent 项目到现在都没想起来用。等你的 Agent 在一次网络抖动后给用户发了三封相同邮件你就会明白幂等键有多重要。4.3 权限、隔离与安全边界技能不是越强大越好而是要权限可控。我给每个技能都标了所需权限比如notification:send、database:read、database:write。Agent 启动时会拿到一个总权限集合技能实际执行前会做一次权限校验没有权限直接拒绝并通知决策层换方案。这样就算模型被诱导去调用一个敏感技能也会在边界处被挡下来。隔离运行同样重要。外部数据源返回的内容不可信。有一次我在技能里接了个搜索服务搜索结果摘要里藏了一段指令文本模型读到后被引导去调用一个不该调的函数。从那以后我把所有外部数据都当作不可信文本处理技能返回给模型之前对内容里的可疑指令做转义处理需要执行代码的算力操作放进沙箱容器里不挂宿主机目录。Agent 的自主性越高安全边界必须越硬。审计日志也是必选项。每个技能调用的入参、出参摘要、耗时、调用者身份、模型当时的决策理由我都记录到结构化日志里。不光是出了问题能回溯更重要的是一周复盘时我可以根据日志发现“模型经常把 A 技能误调成 B 技能”然后用负例描述去持续纠偏。4.4 效果评估与回归测试技能体系上线前我会跑三层测试。第一层是单元测试针对每个技能本身输入固定参数断言返回结构和字段内容。第二层是路由测试准备一批用户意图用例检查模型能不能选到正确技能这一层最常见的失败点是意图相近但技能不同。第三层是端到端测试模拟一个完整用户任务比如“帮我查一下上个月的销售数据然后生成图表”验证多技能串联是否顺畅。评估指标除了任务完成率我还会盯三个偏运营的指标平均调用技能数、平均生成轮次、单任务 token 成本。技能路由准、编排顺这三个数字自然会下降。如果某个技能调用次数异常少或者模型经常在同一个技能上报错重试就说明技能描述有问题或者入口太隐蔽。我用这套测试跑了几轮之后技能误调率明显下降用户体验的提升也直接反映在反馈里。5. 常见问题与避坑清单5.1 技能名“撞车”但日志里发现不了技能多了以后很容易出现两个技能名字里带同一个关键词比如get_store_info和query_store_sales。模型在短上下文里非常容易混淆。我遇到这类问题时第一个动作就是给两个技能的描述里互相加“不要”约束第二个动作是拆分词的冲突点在名字里加上区分词。query_store_sales改成query_store_sales_stats语义边界立刻清晰。5.2 描述写得很全但模型就是不调用这种情况大概率不是描述不够而是描述太“书面”。模型做路由依赖的是语义相似度你要按用户真正会说的话去写触发场景。比如技能是查快递描述写“提供物流信息查询的能力”就不如写“当用户问快递到哪里、物流进度、包裹位置时使用”效果好。把用户语言原样搬进描述是成本最低的优化方式。5.3 把“万能技能”做大最后能力退化我见过有人把一个技能做成“处理所有用户请求”里面挂一堆逻辑分支美其名曰通用技能。结果模型一遇到任务就只调这个技能其他专业技能全部荒废而且因为分支太多技能内部质量很难保证。这个问题的根源是技能设计偷懒。正确的做法是让每个技能聚焦单一职责通用能力通过注册表里的组合来实现而不是靠一个巨型分支函数去堆。5.4 多技能组合时的状态互相污染技能 A 返回的id字段和技能 B 需要的id字段是同一个变量名但语义完全不一样。如果直接按名字共享轻则报错重则把无关数据写到错误位置。我现在的习惯是每个技能在读取外部字段前做类型和范围校验并在工作记忆里用命名空间隔离比如order_system.order_id和notification_system.notification_id。看似多写了几行代码但换来的是半夜不会被线上问题叫醒。5.5 长尾技能被淹没出现“目录遗忘”技能数量超过 30 个以后模型容易忽略排在后面的技能这是我的真实体验。现在我除了用分类目录和意图预排序之外还会对长期不使用的技能做一次“复活”策略在路由阶段不展示它但当用户语句中出现强相关关键词时把它单独提到最前面。这个机制有点像一个搜索引擎的召回与重排先召回候选技能再重排呈现顺序。效果比单纯堆描述好得多。最后再说点实话做 agent-skills 这半年我最大的体会是把模型当作“决策者”而不是“执行百科全书”。模型不需要手写每个 API 的细节它只需要知道有哪些技能、每个技能是干什么的、下一步用什么最合理。真正把代码跑稳、把状态管好、把错排干净的还是工程本身。如果你想从一个已经能跑的 Agent 项目开始优化我建议第一个动作就是打开 system prompt看看工具描述占了多大比例。如果超过三成可以考虑把这些描述抽成技能目录按我上面说的三段式重写一版。你不用一步到位做完美先抽出两三个技能跑通感受一下路由稳定性和调试体验的变化剩下的事情自然就有了方向。技能体系会继续演进设计模式和工具链也一直在变但“能力模块化、边界清晰化、过程可观测”这几个原则不会过时。
返回列表