资讯动态

从提示词到技能:构建可靠AI Agent的工程化实践

发布时间:2026/9/17 12:40:30 来源:尧图企业网站定制
先聊个真实的翻车现场。我上个月给一个内部 Agent 加「写周报摘要」的能力一开始的方案非常简单粗暴把周报模板、历史摘要范例、公司格式要求全部塞进系统提示词里让大模型直接输出。试跑了几轮效果还不错结果换了个参数档位、换了一批真实周报数据之后摘要质量瞬间崩了格式乱了、人名的口径也变了。那一刻我意识到一个问题提示词根本撑不起 Agent 的复杂行为真正需要沉淀下来的是一套可复用、可测试、可编排的「技能」单元。这就是 agent-skills 这个设计思路出现的背景——把 Agent 的能力从「一次性提示词文本」升级成「带契约、带流程、带回退策略的工程化组件」。这篇文章我想用一整篇的篇幅把 agent-skills 从设计动机、技能定义、核心实现到落地踩坑完整拆一遍。适合谁看正在做 Agent 应用、被长提示词折磨、想把自己的工具脚本沉淀成可复用能力的开发者以及那些已经在做多智能体编排、需要给不同 Agent 分配稳定技能的工程团队。我不打算讲花哨的理论框架就讲设计逻辑、代码骨架和我踩过的坑。1. 为什么提示词堆不出可靠的 Agent技能化的动机1.1 一次真实的翻车经历当时那个「周报摘要」的需求其实很明确从团队成员的周报里读取文本按成员维度聚合再按固定的模块顺序输出一段汇报材料。第一版我用提示词硬写系统提示词里放了格式示例、拆了 6 条规则输入侧还把最近 3 次的历史摘要一起作为 few-shot 丢进去。单测场景跑下来没问题可真到了生产数据上问题接连出现。第一个问题是模型对「摘要长度」的把握完全不可控。有时候一段个人周报被扩写成几百字有时候整个团队的数据又被压缩成两句话。第二个问题是输出结构不稳定明明提示词里写了「先按成员分小节再输出风险项」模型偶尔还是会自作主张调整顺序。第三个问题最致命当我给 Agent 同时挂上「周报摘要」和「周报数据统计」两个能力时提示词互相干扰模型经常把统计口径写进摘要里。这三个问题的根子其实不在「模型笨」而在我把「能力」和「提示词」绑死了。提示词没有结构边界、没有状态管理、没有明确的输入输出契约它只是模型的一段上下文背景音。想让它稳定就必须把它变成一种可以被 Agent 显式调用、执行、验证、回退的东西——也就是技能skill。1.2 技能、工具、工作流三个概念的分工在讨论 agent-skills 之前先把三个经常混在一起的概念掰开工具tool、技能skill、工作流workflow。概念本质典型例子能力边界工具 / Tool原子化的单个函数调用发邮件、查询天气、读取文件一次操作无状态输入到输出即结束技能 / Skill面向场景的复合行为单元汇总团队周报并生成摘要包含多个步骤、有内部状态、有回退逻辑工作流 / Workflow多技能/多角色的流程编排每周一自动汇总周报→生成播报→发给领导有时间调度、有分支跳转、有外部触发条件工具是积木技能是搭好的一个功能区工作流则是把功能区串联起来的房屋动线。很多团队把这三层混为一谈结果就是工具里塞流程、工作流里写死细节改一处崩一片。agent-skills 的核心主张是把「面向场景、可复用、可验证」的能力放在技能这一层单独建模不要让流程逻辑泄漏到工具层也不要把具体的操作细节全部塞给模型自由发挥。1.3 技能化的三个直接收益把能力升级为技能之后最直观的收益有三个我实际用下来感受很深。一是可测试性。提示词时代能力对不对全靠肉眼观察输出现在技能是一个有输入 schema、有步骤序列、有预期输出的独立单元我可以直接写单元测试和回归用例输出不对能定位到具体某个步骤。二是可复用性。以前想在另一个 Agent 里复用「周报摘要」能力得把那段提示词复制一份再改改完两边还容易不一致。技能注册到技能库之后任何 Agent 都可以按名字和描述检索到它一份实现到处引用天然消除了复制粘贴带来的漂移。三是可控性。技能内部可以设定超时、限定步骤数、定义降级策略。工具调用失败时Agent 不至于懵在原地而是按照技能预设的 fallback 路径走这对生产环境太重要了。2. 技能的本质一套带「契约」的可执行流程2.1 技能不是提示词技能是行为单元我现在对技能的定义是技能 目标声明 触发条件 输入输出契约 步骤序列 回退策略。它本质上是一个「行为单元」不是一个「文本片段」。类比一下提示词就像是给人类同事写的一张便签上面写着「帮我把周报汇总一下格式参照上次」表达模糊全靠对方的临场发挥。技能则像是 SOP标准作业程序把「汇总周报」拆成了几个明确的操作节点每个节点做什么、产出什么、出现异常怎么办都写清楚了。人类团队靠 SOP 保证交付质量稳定Agent 团队就需要靠技能来干这件事。从底层机制上讲技能的价值在于它为模型划定了「搜索空间」。模型不需要从 token 的海洋里猜测执行路径而是先通过意图匹配锁定一个技能然后在技能预设的步骤框架里调用工具、组装结果。搜索空间变小误差自然变小。2.2 技能定义结构从入口到退出的完整契约一个完整的技能定义我习惯用 YAML 写元信息用 Python 写步骤实现。技能定义文件长这样name: weekly_report_summary description: 汇总一个时间段内的团队周报并生成结构化摘要。当用户提到“周报汇总”“本周大家做了什么”“团队进展”时使用。 version: 1.3.0 input_schema: type: object properties: start_date: type: string description: 开始日期格式 YYYY-MM-DD end_date: type: string description: 结束日期格式 YYYY-MM-DD member_list: type: array items: type: string description: 需要汇总的成员列表缺省时自动获取 required: - start_date - end_date output_schema: type: object properties: summary_markdown: type: string description: 按模板格式生成的摘要内容 steps: - id: fetch_reports tool: get_weekly_reports - id: summarize_by_member tool: llm_extract params: prompt_template: templates/summarize_member.j2 - id: compose_markdown tool: render_template params: template: templates/report_markdown.j2 timeout_seconds: 60 fallback: - on_step: fetch_reports strategy: return_empty_template message: 暂时无法获取周报数据请稍后重试可以看出技能定义分了几层描述层description和input_schema是给 Agent 的「元认知」决定技能什么时候被唤起、参数怎么填。执行层steps定义了一个有顺序的步骤链每个步骤绑定一个工具或另一个技能。保障层timeout_seconds和fallback定义了异常时的行为边界。这套结构强调的是「契约先行」。一个技能能不能被稳定复用五成取决于description写得是否精准四成取决于步骤划分是否合理剩下的一成才是具体实现。2.3 技能生命周期注册、检索、执行、沉淀技能在整个 Agent 系统里有明确的四个阶段注册、检索、执行、沉淀。注册技能编写完成后注册进技能库系统校验 schema 是否合法、依赖的工具是否存在然后建立索引。检索当用户输入到达时Agent 先根据意图从技能库中召回候选技能。这一步通常是 embedding 语义召回加关键词过滤的混合检索。执行技能引擎按 steps 顺序执行每一步的结果会写入技能的上下文状态。执行结束后按 output_schema 校验输出。沉淀执行日志、中间结果、最终结果全部留存作为后续回归测试和技能优化的素材。这四个阶段是 agent-skills 模式的完整闭环。很多团队只做到了前两步注册和检索把技能当成了「高级工具列表」执行和沉淀完全没做其实技能的优势根本没有发挥出来。3. 从零实现一个可用 Agent 技能以「周报汇总」为例3.1 技能注册器与运行时先搭骨架我实现一个极简的技能注册器和执行引擎核心代码不复杂但能说明整个机制是怎么转起来的。# skill_registry.py from typing import Dict, Optional import yaml class SkillRegistry: def __init__(self): self._skills: Dict[str, SkillDefinition] {} def register_from_yaml(self, path: str): with open(path, r, encodingutf-8) as f: raw yaml.safe_load(f) skill_def SkillDefinition(**raw) self._skills[skill_def.name] skill_def return skill_def def search(self, query: str, top_k: int 3): # 真实场景这里会走 embedding 向量召回 关键词过滤 # 简化实现里用描述的关键词重叠度排序 hits [] for skill_def in self._skills.values(): score self._overlap_score(query, skill_def.description) hits.append((score, skill_def)) hits.sort(keylambda x: x[0], reverseTrue) return [item[1] for item in hits[:top_k]]注册器负责把技能 YAML 转成内部的SkillDefinition对象并提供检索入口。search方法里我故意留了一个简化实现实际工程上会用向量检索但核心逻辑是一致的把用户意图和技能描述做匹配返回候选技能列表。执行引擎的重要性一点不亚于注册器。一个技能的执行不是一次性调用一个函数而是要在步骤之间传递中间状态# skill_engine.py class SkillEngine: def __init__(self, registry: SkillRegistry, tool_dispatcher): self.registry registry self.tool_dispatcher tool_dispatcher def execute(self, skill_name: str, params: dict, context: ExecutionContext): skill self.registry.get(skill_name) step_outputs {} for step in skill.steps: try: result self.tool_dispatcher.dispatch( step.tool, input{ params: params, prev_outputs: step_outputs, }, contextcontext, ) step_outputs[step.id] result except Exception: # 查技能定义的 fallback 策略决定是否降级或终止 fallback skill.get_fallback(step.id) if fallback and fallback.strategy return_empty_template: step_outputs[step.id] self._build_empty_result(skill) context.add_warning(fallback.message) break raise return self._validate_output(skill, step_outputs)这段代码揭示了执行引擎的三个关键决策步骤之间有显式状态传递、异常触发 fallback、输出经过 schema 校验后再返回给上层。不要把这三件事交给模型临场决定它们是技能的工程保障。3.2 注册、检索与执行链路的运行逻辑把整个链路串起来看用户输入进来以后实际发生的流程是这样的用户的自然语言输入先经过一个意图理解模块得到查询语句。查询语句进入SkillRegistry.search()语义召回 Top-K 技能。Agent 根据技能描述和用户目标的匹配度决定调用哪个技能、填什么参数。SkillEngine.execute()按步骤执行每一步的工具结果都保留。最终结果经过输出校验后返回给用户。这个链路里最容易出错的地方在第三步。很多人以为只要检索到技能就能用但其实模型需要从几个候选技能里挑一个这依赖两个信息源技能description写得好不好以及模型的工具调用提示词是否清晰地列出了候选技能。我通常在系统提示词里把候选技能列表以name: description的格式呈现让模型按name和参数结构发起调用。比如说候选技能列表可能是这样渲染进模型上下文的Available skills: - weekly_report_summary: 汇总一个时间段内的团队周报并生成结构化摘要。用于“周报汇总”“本周进展”。 - weekly_report_metrics: 统计周报中的量化指标用于“算出勤率”“统计任务完成数量”。这样模型就能非常明确地做出工具调用决策而不是从头开始读一堆实现细节。3.3 为什么把提示词级别的内容下沉到技能步骤里我在第三步里故意把summarize_by_member的提示词模板放到了技能目录的templates/summarize_member.j2而不是放进系统提示词。这不是无意义的抽象是刻意为之。原因有两个。第一技能内部提示词本质上是这个技能的「私有实现细节」它们只服务于该技能的执行不应该暴露给同一个 Agent 下的其他技能。把它们独立成文件技能之间就实现了 prompt 级别的隔离。第二当技能因为输入参数不同而需要调整提示词时我们直接改模板文件不影响系统提示词也不会牵连其他技能改动范围被天然限制住了。这种设计在多个技能并存时优势特别明显。我之前把多个技能的提示词全部放在一个系统提示词里改一个技能就要重新跑全量回归测试生怕影响其他能力。技能的提示词独立之后回归测试的范围可以精确到具体技能效率提升非常大。4. 技能运行时的三个关键机制隔离、回放、降级4.1 上下文隔离别让技能互相污染技能执行过程中最隐蔽的风险是上下文污染。我在设计执行引擎时ExecutionContext是每个请求独立创建的每个技能只能读写自己作用域内的上下文数据不能让技能 A 的中间结果漂移到技能 B 的判断里。举一个实际踩过的坑。我的 Agent 同时挂了「周报摘要」和「周报统计」两个技能。一开始我没有做上下文隔离两个技能共用一个全局 context。模型在统计技能里读取数据时偶尔会「看到」摘要技能里的中间文本于是把摘要的结论当成了统计依据输出错得离谱。定位这个问题花了整整一天。修复方式很简单每个技能执行前创建独立的上下文作用域技能结束后只把输出结构暴露给上层所有内部变量全部销毁。这个约束要在执行引擎层面强制保证不能寄希望于模型自觉。with context.scope(skill_name): result self._run_steps(skill, params, context)4.2 执行轨迹回放错误可复盘的基础技能执行完之后的日志字段别只记最终输出每一步的输入输出都要落盘。我用一个结构化的执行轨迹记录每次执行对应一个密不可分的 trace_id每条轨迹记录触发的技能、被调用的工具、每个步骤的耗时、中间输出的快照、异常发生的位置和堆栈轨迹可以直接回放作为回归测试的输入这个机制的实用价值巨大。技能上线后如果出现输出质量波动我可以直接从线上把出错请求的轨迹捞出来逐步重放精确看到是哪一步开始偏离预期。没有轨迹回放你就只能拿着碎片化的日志猜模型心态效率极低。我在执行引擎里给每个步骤都加了__record__钩子这样执行轨迹不依赖具体技能实现自动生成class TracedTracer: def before_step(self, trace_id, step_id, input): self._spans[trace_id].append({step: step_id, phase: before, input: input, ts: now()}) def after_step(self, trace_id, step_id, output): self._spans[trace_id].append({step: step_id, phase: after, output: output, ts: now()})4.3 降级策略失败也要体面技能执行在真实生产环境里一定会遇到外部工具不可用、数据缺失、格式异常这类问题关键是 Agent 在失败时的表现不能是一堆堆栈和废话。我在技能定义里给每个步骤预设了降级策略。降级策略可以分几个层次终止式降级、部分成功降级、旁路式降级。终止式降级就是直接返回明确错误信息部分成功降级适用于已经执行了一些步骤的场景比如已经拉取到了大部分周报只有某一个人的数据异常那么此时把这个人标记为「数据缺失」继续生成摘要而不是全盘放弃旁路式降级更适合关键流程比如主数据源不可用时改用备份数据源。下面这段是我的部分成功降级逻辑比较有参考价值from copy import deepcopy def partial_success_fallback(output_data, missing_keys, notice): merged deepcopy(output_data) for key in missing_keys: merged[key] {status: missing, reason: notice} return merged最终对用户呈现的结果里缺失项被明确标注而不是瞎编一段内容。这个设计我认为是 agent-skills 模式里最有价值的一点模型的能力边界被显式表达而不是靠概率去赌。5. 踩坑实录技能化落地时最常见的坑5.1 描述即入口写不好描述再好的技能也白搭我在前文反复强调description很重要是因为检索环节的实际效果对描述措辞极其敏感。技能描述是入口理解错了入口后面全是空中楼阁。好的技能描述需要具备三要素功能描述要具体比如「汇总一个时间段内的团队周报并生成结构化摘要」而不是空泛的「处理周报」触发条件要写清列出出现哪些说法时应该调用例如「周报汇总」「本周进展」边界条件也要写明确「不用于」什么场景例如「不要用于统计周报中的量化指标」「不要用于生成周报以外的其他文档摘要」反面描述能显著降低误召率。我之前没写边界条件时用户问「帮我看看大家的数据情况」摘要技能和统计技能经常一起被召回来模型选错概率高。加了「不要用于……」之后召回准确性明显提升。5.2 循环调用与递归陷阱技能之间可以互相调用这是它灵活的原因也是 bug 的温床。A 调 BB 调 A一旦没有深度限制模型会在循环里越陷越深直到 token 耗尽。我处理这个问题的方案有三层通过技能调用深度计数器限制单次任务中技能调用的最大层级我一般设为 5 层if context.current_depth MAX_SKILL_DEPTH: raise SkillDepthExceeded(fskill call depth exceeded at {skill_name})通过调用链白名单禁止特定技能组合的互相调用比如摘要技能禁止回调统计技能防止环状依赖在检索阶段把「互相调用的技能」在描述里显式标注出来让模型明确知道边界而不是放任它自由发挥5.3 技能粒度拆太碎和揉一团都不行技能粒度是整个设计中比较难把握的部分。拆得太碎比如把「读取数据」和「格式化输出」都拆成独立技能会让编排过程非常痛苦模型需要做大量细粒度决策每一层的纯函数式接口在传递中间状态时也很容易出错。揉成一团把「生成周报」「统计指标」「发送邮件」「记录到知识库」全塞进一个技能里又会导致技能难以复用和维护。我判断粒度的经验标准有两条复用频率早于设计合理性考虑。如果一个操作有三个以上场景重用就值得拆成唯一一个独立技能如果某个技能从上线到现在只被调用过一次考虑与相邻技能合并。步骤数量是重要参考线。技能内部步骤超过 7 个就要考虑是否可以把其中的稳定组合抽出来变成子技能。人类工作记忆上限是 7±2技能的复杂度控制在这个范围附近对模型编排负担比较友好。5.4 技能与知识库的边界混淆另一个常见的坑是把技能和知识库的职责搞混。知识库Knowledge Base存的是「信息」比如周报模板的格式要求、公司组织架构、历史周报示例技能存的是「行为」比如如何读取周报、如何调用模型做摘要、如何渲染最终文档。很多团队的技能设计里混入大量静态知识技能描述写了上千字的背景说明导致检索时语义空间污染严重。正确的做法是技能只管执行逻辑它需要的背景知识通过输入参数传入或者技能内部主动查询知识库。不要让执行逻辑和静态知识耦合在一起。6. 从个人技能到团队技能库工程化运营的思考6.1 命名与版本技能库的地基技能写多了之后如果命名混乱、没有版本控制技能库会迅速退化成垃圾堆。我建立技能的命名规范是{领域}_{动作}_{对象}比如hr_generate_candidate_report、weekly_collect_team_progress。领域前缀解决同名冲突问题动作加对象既保持可读性又便于检索分类。版本控制上每个技能目录下放一个version.json记录语义化版本号、变更描述、依赖的工具版本。以下是基本的版本文件样例{ name: weekly_report_summary, version: 1.3.0, changed: 调整 summarize_by_member 的提示词模板增强风险项提取, dependencies: { get_weekly_reports: 2.0.0 } }技能描述这种看似不重要的文本同样要注意变更管理。我之前吃过大亏一次反向修改了技能描述结果导致检索召回率大跌线上 Agent 约半天时间找不到该技能只能临时回滚版本才恢复。6.2 灰度与回滚技能也要发版技能的变更影响的是 Agent 的复杂行为能力所以不能写完直接覆盖发布。推荐的做法是先发布为 beta 版本比如1.4.0-beta只在内部测试流量或特定项目里启用。跑一个星期、收集轨迹回放、确认指标没有恶化后再升级为稳定版。回滚的能力要前置建设。每个技能执行时可以选择固定版本或最新版本。在技能注册表里加一个version_policy字段允许按latest、range、pinned三种策略拉取版本。生产环境默认用pinned策略锁定版本避免线上行为被突发的技能更新影响。灰度阶段则用range策略让部分请求命中新版本。6.3 让技能成长日志驱动迭代技能库最健康的运转模式是持续从使用日志里学到问题并迭代而不是写完后不管。我每次技能迭代都依赖三条信息执行失败日志哪些步骤经常触发异常异常集中在什么输入类型上降级触发记录哪些技能频繁走 fallback 路径说明它依赖的上游数据源不稳定需要考虑替换或缓存用户反馈与修正最终拿到结果后用户有没有修正、追问、或者表达不满如果一个技能持续输出质量低但执行成功率很高问题大概率出在步骤设计上——要么是该拆分了要么是内部提示词模板需要更新。我会直接把历史轨迹作为 few-shot 示例放入templates/summarize_member.j2效果比手动调提示词精准得多。6.4 一个比较稳妥的技能库目录结构按上面这些思路我建议团队级技能库至少长这样skills/ weekly_report_summary/ SKILL.md version.json steps/ fetch_reports.py summarize.py compose.py templates/ summarize_member.j2 report_markdown.j2 tests/ fixtures/ input_weekly_reports.json cases.json logs/ trace_20240501.log这个结构把技能定义、版本信息、步骤实现、模板、测试、日志彻底分开。新人接手一个技能时看SKILL.md和tests/cases.json就能快速搞懂技能行为。上线后如果出问题在logs/里定位轨迹也方便。这里有个小技巧要提醒tests/cases.json里的用例维护要和线上轨迹同步定期把线上真实的好案例抽精选回测试集保证回归测试永远贴近真实数据分布而不是守着几个历史老用例自欺欺人。我自己在经历了前面那一串踩坑之后现在做 Agent 项目的习惯已经彻底变了。接到一个新需求第一反应不再是写一段提示词试试而是先问自己这个需求要沉淀成什么技能输入输出契约是什么步骤怎么拆降级怎么走想明白这些问题代码实现其实花不了太久真正决定 Agent 上限的是技能设计的质量而不是模型推理时的那点临场发挥。agent-skills 这条路我还在持续打磨如果你正在做类似的事希望这篇文章能帮你少走几步弯路。

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

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

免费获取报价