资讯详情

资讯详情

别只堆提示词:Agent Skills与工作流如何稳住线上应用

如果你最近在折腾 Agent 应用大概率已经碰到一个很尴尬的分叉点Demo 做得飞起一旦真正上线Agent 的行为就开始飘。同一个功能用户早上问和下午问得到的结果完全两样你加了半天提示词结果它在无关请求里也触发奇怪的动作。我去年做一个某跨平台系统的时候就是卡在这个问题上。后来我把思路从“写提示词”换成了“给 Agent 配一套工程化 Skills”整个项目的稳定性和可维护性立刻拉开了差距。Skills 在我的使用语境里就是“可被 Agent 动态调用、有清晰边界、能独立维护和测试的能力单元”不是一段写在系统提示词里的模糊指令。这篇内容不聊论文概念就讲我在实际项目里怎么拆解 Skills、怎么设计工作流以及哪些坑是真的踩过以后才知道的。无论你是刚入门想搭第一个 Agent 应用还是已经在线上被各种随机行为折磨得头大这套思路都能直接拿去用。1. 为什么我选择把 Agent 能力拆成 Skills而不是继续堆提示词1.1 先搞清楚你说的 Skills 到底是什么很多人在做 Agent 的时候习惯把“能力”直接写进系统提示词比如“你是一个可以查天气的助手天气信息通过调用某个接口获取”。这种写法在演示阶段没什么问题但一旦能力变多提示词就会越来越长模型对能力的边界理解越来越模糊最后所有请求都在“尝试”调用所有工具结果各种误触。Skills 的意思是把某项能力封装成一个独立模块它有名字、有描述、有输入输出协议有可执行的逻辑还有独立的验证方式和错误处理机制。Agent 本身不再“模糊地知道”自己有哪些能力而是通过一个可被检索和调度的机制在需要的时候精确加载对应的技能。打个比方系统提示词方案像是给员工发一本厚厚的手册让他在脑内记住所有流程Skills 方案则是给他一个工具墙上面每个工具都贴了标签这是什么、什么场景用、怎么用、坏了会怎样。员工不需要背手册他只管看标签选工具。这个差异决定了项目的可维护上限。举个具体例子当时我在做一个知识库问答 Agent刚起步时有三个功能文档摘要、关键词检索、相似度检索。用提示词硬写也能跑但很快我发现用户问“帮我把这段内容压缩一下”的时候模型经常也跑去调用检索工具返回一堆不相关的内容。把这三个功能各封装成一个带明确触发条件的 Skill 以后误触率明显下降而且每一个 Skill 都可以单独测试、单独修 bug完全不影响其他能力。1.2 工程化路线到底解决了什么工程化 Skills 直接解决了三个让人头疼的问题。第一是可测试性。提示词方案里你想验证某个能力是否正常必须把整段对话跑一遍出了问题还很难定位是模型的“理解”错了还是工具调用链路错了。Skill 方案下每个 Skill 有独立的输入输出校验和测试用例你可以像测试普通函数一样测试它。某次我改了一个清洗文本的 Skill回归测试直接跑挂 3 个用例这在提示词方案里几乎不可能做到这么早发现问题。第二是权限边界。Agent 在复杂场景里必须知道“哪些事不能做”。如果你把权限规则写进提示词模型可能会被绕过。Skill 本身可以做成权限的最小单元某个 Skill 只能访问某张表另一个 Skill 只能调用某个只读接口。技能未加载对应的能力就不存在这在安全层面意义很大。第三是上下文压缩。把十几个能力的详细说明都塞进提示词会浪费大量模型上下文。通过 Skills 机制Agent 只需要维护一份“技能清单”真正的执行逻辑放在外部代码或子提示里只在需要时载入。上下文占用能减少一半以上模型注意力也更集中。2. 技能系统的整体设计清单、执行器与触发机制2.1 一份技能定义里到底该写什么我在项目中把每个 Skill 定义成这样的结构name、description、input_schema、output_schema、execute函数、validation逻辑、error_handler。看起来很简单但每一个字段背后都有讲究。name必须唯一且语义清晰不能叫func1这种名字因为你后面要让大模型通过名字来路由名字就是它的标识符。description是最容易被低估的字段。它不是写给用户看的而是写给模型“选择器”看的。合格的描述要包含三部分这个技能负责什么任务、适合什么输入、不适合什么输入。例如“从一段长文本中提取结构化摘要适合输入原文内容不适合输入表格数据”。我见过很多项目在这个字段上敷衍写一句“提取摘要”就完了结果路由准确率直线下降。模型不是靠读源码来理解技能的它全靠描述信息做判断所以描述的质量直接决定整个系统的上限。input_schema和output_schema最好是强类型定义。有些实测方案用 JSON Schema有些用带类型标注的伪代码甚至用 Pydantic 这类模型。核心原因是没有校验的情况下任意一个技能的错误输出都可能污染后续流程。我在一个金融问答场景里吃过亏一个技能把数值返回成了字符串下游做聚合计算直接类型报错问题查了半天才发现是上游的输出类型没锁死。execute函数是技能的实际逻辑。它可以是调外部 API、执行一段代码、跑一次数据库查询也可以是调用一个大模型子任务。我建议把技能实现完全独立成函数不要依赖对话上下文里的临时变量。这样既能单测也能在未来把技能迁移到其他 Agent 时省下大量成本。2.2 执行器的四种典型形态根据技能内部做的事情不同我把它们分为四种形态设计的时候心态完全不一样。第一类是 API 代理型。技能本身不处理业务逻辑只是把 Agent 的请求转发到某个外部服务再做结果清洗。比如天气查询、股票行情、订单状态查询。这类技能的关键是做好超时和限流否则外部服务一抖动整个 Agent 也跟着抖动。第二类是代码生成与执行型。技能内部实际上就是一段代码解释器或脚本执行器Agent 生成代码技能执行代码并返回结果比如数据计算、格式转换、爬虫采集。这类技能最危险因为执行权限很难控制。我的做法是让这类技能运行在独立沙箱进程里并限制网络和文件写入权限只开放它真正需要的最小能力。第三类是检索增强型。它的核心不是生成内容而是从外部存储中找信息。文档检索、数据库查询、图数据库查询都算这一类。这类技能我通常会对召回结果做后处理包括去重、按相关度排序、限制返回条数减少无效上下文占用。第四类是流程编排型。这类技能自己不干具体活而是调用其他技能。比如一个“生成周报”技能内部会依次调用“汇总数据”“提取重点”“生成标题”三个子技能。流程编排型技能是天然的工作流入口也是整个系统从“能干活”走向“会干活”的关键。2.3 触发与路由如何避免技能“误伤”技能再多如果路由不准一切都白搭。我在实践中总结了三层路由策略。第一层是硬规则路由。对于边界特别清晰的技能直接用关键词或正则做前置过滤。比如“翻译成英文”这个请求基本可以稳定命中翻译 Skill。硬规则的好处是零成本且不会误触缺点是覆盖不了所有情况。第二层是模型路由。把技能清单只要name和description交给模型让它根据用户输入选择最合适的技能。这种方式灵活但要注意给模型一个“都不合适”的选项。很多人忽略这一点结果模型被迫在所有技能里硬选一个反而出错。我在提示里会明确写“如果没有任何技能匹配直接返回 no_skill”路由错误率立刻下降了很多。第三层是动态路由。根据对话历史或用户画像临时调整某些技能的可见性和权重。比如一个访客用户我干脆不给他暴露“删除数据”类技能一个企业用户进来默认把检索权重调高。这种动态策略在权限管理和个性化场景里非常有用。我得提醒一句路由不是越复杂越好。如果你的技能总数在十个以内模型路由就够了超过三十个才需要考虑分层路由或引入独立的检索模型。过早做复杂路由只会给自己增加排查负担。3. 工作流如何把技能编排成真正的业务流程3.1 从一句用户指令到一份工作流定义技能只是积木真正干活靠的是工作流。工作流是一种编排描述它决定了技能按什么顺序执行、什么条件下跳过、什么条件下重试。我习惯把工作流定义成一个独立文件而不是写死在代码里。这样做的好处是改流程不需要重新发布代码而且可以把流程直接反馈给团队查看。举个例子我做一个“会议纪要自动归档”功能用户在对话里说完会议内容Agent 需要完成四步清洗速记文本、提取议题和结论、根据结论生成待办事项、归档到项目文档库。如果是传统提示词方案这四步全在一轮模型调用里完成任何一个环节出问题都要整轮重跑。用工作流方案我会把它们拆成四个 Skill然后用一个顺序工作流串起来。现实操作中中间某一步失败我可以只重跑那一步而不是重跑整个对话。3.2 线性、并行与动态分支工作流的编排模式我在实际项目里最常用三种。线性编排是最简单的技能 A 执行完输出传给技能 B再传给技能 C。大部分流程都是这种模式关键是要把技能之间的传递标准定义好。我的做法是让每个技能的输出都包装成一个统一结构包含status、data、error三部分。这样下游技能判断状态时非常简单不会出现“拿到一个字符串不知道到底是内容还是报错”的尴尬情况。并行编排是把几个互不依赖的技能同时执行最后汇总结果。比如生成一份市场周报时“销售数据”“用户反馈”“竞品信息”三个技能可以各自独立跑最后再做汇总。并行执行能大幅缩短响应时间但如果三个技能运行完之后还要汇总汇总技能必须能容忍某个子技能失败。我的做法是在并行分支里给每个技能设置超时超时返回超时占位符汇总技能可以感知到“哪个部分没拿到”并显式告诉用户。动态分支是我认为最能体现工程价值的模式。工作流不是死板的线性链条而是根据步骤结果决定下一步走向。例如“内容审核”工作流先跑一个敏感词检测 Skill如果检测通过直接结束如果没通过再调用一个人工复核 Skill甚至进入“拒绝生成”分支。这种 if/else 逻辑放在工作流文件里比让大模型自己“临场判断”要可靠得多因为每个分支都是可测试的。3.3 超时、重试与降级设计给技能配工作流如果不处理异常那线上的 Agent 会教你做人。以下三件事是我认为每个工作流都必须内置的。第一是超时控制。每个技能调用都要设置合理超时比如外部 API 型技能给 5 秒代码生成型技能给 15 秒。超时之后要么终止整个流程要么走降级分支。如果什么超时都不设一个技能卡住整个 Agent 请求就会挂在那里用户直接失去耐心。第二是重试策略。不是所有错误都值得重试。对于网络抖动、临时限流这类瞬时错误我一般建议重试一到两次间隔用指数退避对于输入数据错误、参数不合法这类确定性错误重试一百次也是浪费。我在工作流定义里会给每个技能声明retryable和max_retries让引擎能够在运行时做出判断。第三是降级路径。降级不是做不做的问题而是必须做的。比如知识库检索服务挂了工作流可以降级成“直接调用基础模型生成回答并附带提示检索服务暂时不可用”。又比如“生成图片”技能失败降级成“返回文字描述占位图”。降级路径保证了 Agent 在故障时不至于完全撂挑子这个体验差异用户感知是很明显的。4. 实操在项目里落地一套最小 Skills 工作流4.1 目录与注册结构以及运行时加载前面讲了不少设计思路这一节直接上实操。我在项目里习惯这样组织技能目录skills/ registry.yaml document_summary/ manifest.yaml execute.py test_cases.json web_search/ manifest.yaml execute.py test_cases.json workflows/ meeting_notes.yaml weekly_report.yaml runtime/ loader.py workflow_engine.py每个技能独立一个目录内部放manifest.yaml描述、输入输出、触发条件、超时重试配置、execute.py真正的执行逻辑、test_cases.json本地测试用例。运行时会扫描所有技能目录读取manifest.yaml生成一份统一的注册表保存在内存里供 Agent 调用。registry.yaml由我手工维护一份“技能列表”它的作用不是承载全部技能详情而是只存namedescription方便在路由时以极小的上下文向模型展示全量技能。模型选定某个技能后运行时再加载对应目录下的完整manifest和执行器。这种两级加载挺有效列表轻量详情按需加载不浪费 token。# registry.yaml skills: - name: document_summary description: 对长文本内容生成结构化摘要适合文档、文章、会议记录不适合表格数据 - name: web_search description: 在互联网上检索相关信息返回标题与摘要列表适合实时信息查询 - name: calendar_query description: 查询用户日历中的日程安排支持按日期范围筛选4.2 技能定义和执行器的代码骨架下面写一个简化版但能跑通整个流程的技能定义以document_summary为例。# execute.py (document_summary) import json from typing import Any, Dict def execute(inputs: Dict[str, Any]) - Dict[str, Any]: 执行文档摘要技能。 inputs 里包含 content, max_length 两个字段。 返回值统一为 {status, data, error} 结构。 content inputs.get(content, ) max_length int(inputs.get(max_length, 2000)) if not isinstance(content, str) or len(content) 50: return { status: failed, data: None, error: content must be a string with length 50, } # 这里可以调用一个摘要模型或第三方总结接口简化起见用占位逻辑 summary _call_summary_model(content, max_length) return { status: success, data: {summary: summary}, error: None, } def _call_summary_model(content: str, max_length: int) - str: # 实际项目里替换成摘要模型调用 return content[:max_length] ...[truncated]技能定义文件则长这样# manifest.yaml name: document_summary version: 1.2.0 description: 对长文本内容生成结构化摘要适合文档、文章、会议记录不适合表格数据 input_schema: type: object properties: content: type: string minLength: 50 max_length: type: integer default: 2000 required: - content output_schema: type: object properties: summary: type: string timeout_seconds: 15 retryable: true max_retries: 2这里我特别说一下为什么manifest.yaml里也要写timeout_seconds和retryable而不是在代码里写死。答案很简单工作流引擎在处理流程时需要在不加载执行器的情况下就能判断“这个技能能不能重试、最多重试几次、超时多久”否则引擎就必须执行到一半才知道策略这会拖慢流程且不好观测。把策略外置成元数据以后引擎可以在编排阶段就把耗时和风险算出来。4.3 用一个小示例跑通整个技能编排流程接下来我用一个非常常见的场景串起来收到一份新会议纪要自动清洗、总结、生成待办并归档。# workflows/process_meeting_notes.yaml name: process_meeting_notes steps: - id: step_clean skill: text_cleaner inputs: content: ${user_input} - id: step_summary skill: document_summary inputs: content: ${step_clean.output.data.clean_text} max_length: 1500 - id: step_todo skill: todo_extractor inputs: summary: ${step_summary.output.data.summary} - id: step_archive skill: doc_archiver inputs: content: ${step_summary.output.data.summary} todos: ${step_todo.output.data.todos} on_failure: skip_and_notify这个 YAML 描述了一个四步顺序工作流。${step_clean.output.data.clean_text}是引用上一个技能的输出。最关键的是step_archive里配了on_failure: skip_and_notify意思是归档失败时不阻断整体流程只返回一个提示信息。这种“非关键步骤允许降级”的思路在真实业务里非常重要。对应的工作流引擎核心伪代码如下# workflow_engine.py (简化版) async def run_workflow(workflow, initial_inputs): outputs {} for step in workflow[steps]: skill load_skill(step[skill]) inputs resolve_input_refs(step[inputs], initial_inputs, outputs) result await call_with_policy( skill.execute, inputs, timeoutskill.timeout_seconds, retryableskill.retryable, max_retriesskill.max_retries, ) if result[status] failed: if on_failure in step: outputs[step[id]] handle_failure(step[on_failure]) continue return {status: failed, failed_step: step[id], error: result[error]} outputs[step[id]] result return {status: success, outputs: outputs}这个骨架里核心是resolve_input_refs它负责把${xxx}引用替换成真正的运行时数据。实现时需要注意一点当引用的上游步骤失败时当前步骤不能直接崩掉而是要走上层的on_failure分支。5. 常见问题与排障技巧实录5.1 技能之间互相干扰输出格式对不上这是我把技能分装之后遇到的第一个高频问题。技能 A 输出的是一个列表技能 B 却当成字符串拼接结果整个流程炸掉或者输出混乱。根本原因在于技能之间的契约没有统一标准。排障思路很简单我给每个技能配了test_cases.json里面固定写清楚期望输入和期望输出。任何一个技能调整输出格式马上跑全部依赖它的下游技能测试格式断裂在代码评审阶段就暴露了。这里要说个细节技能的输出永远不要裸返回str或dict哪怕只有一个字段也建议包成{status, data, error}省得下游判断时还要尝试解析。5.2 模型总是不选正确的技能有时我把技能清单丢给模型它却选错了。这里有两个方向排查。第一优先级是看description写得好不好。我之前踩过坑把document_summary的描述写成“总结文档”结果用户问“帮我概括一下这篇文章”的时候模型完全没意识到该用这个技能因为它没有建立“文章”和“文档”的关联。后来我把描述改成“对长文本内容生成结构化摘要适合文档、文章、会议记录不适合表格数据”命中率立刻上升。记住描述要写正例和反例反例尤其重要。第二优先级是看路由策略是否过于简单。如果技能数量超过二十个模型单看清单确实容易混淆。我的经验是给容易混淆的技能配上“互斥说明”例如在清单里额外加一行提示“document_summary 处理长文本整体总结keyword_search 做关键词定位两者不要混用”。这种显式说明在关键路径上很管用。5.3 长上下文拖慢技能调用甚至出现幻觉技能执行过程中上下文越长模型判断就越不稳定。尤其是检索增强型技能如果你把搜索结果按原始长度直接塞给下一个技能很快上下文就爆了模型还会引用不存在的信息。我后来的处理标准是检索结果先做裁剪每条结果保留标题加最多两百字摘要且最多传入前五条。这个裁剪逻辑单独封装成一个post_processor挂载在检索技能内部不进工作流主链路。另一个经验是如果一个技能确实需要长上下文尽量在技能内部独立调用模型子任务而不是把长上下文传递到 Agent 主线程。这样主线程保持短小响应速度和稳定性都更好。5.4 技能升级后旧工作流出现兼容问题这个坑几乎每个人都会踩。某次我升级了text_cleaner的清洗规则输出文本里多了一个字段结果下游document_summary因为输入格式校验失败直接报错。排查半天才发现是版本不匹配。解决办法是给manifest.yaml加version字段工作流定义里按需声明最低技能版本。引擎在加载时先做版本校验如果当前技能版本不满足要求直接走降级流程。同时每个技能升级时把test_cases.json一并更新保证测试覆盖新字段。操作上我还会保留旧版本技能目录等新版本稳定运行几天后再清理这样回滚也很快。6. 一点后续可以扩展的方向技能和工作流的组合做熟练之后还可以继续往两个方向扩展。一个是把技能加进自己的插件生态中做成可分发、可共享、可远程加载的单元。另一个是结合更复杂的监控和可观测体系跟踪每次路由是否合理、每个技能耗时是否波动让 Agent 的行为能数字化度量。当然这些都属于系统越来越庞大以后的事。如果你现在刚起步先把十来个技能用好把工作流跑稳就已经超过大多数停留在提示词阶段的项目了。我个人在实际操作中最大的体会是Agent 能力的上限不由模型决定而由你给它的工程骨架决定。Skills 和工作流这套体系本质上是把“怎么干活”从模型手里接管过来变成稳定可控的底层代码。初期多花一点时间把骨架搭好后面每一轮功能迭代都会顺畅很多。如果你正被线上 Agent 的随机行为折磨不妨从这个方向试一试。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →