我把大半年的Agent开发经验浓缩成了一套技能系统。这本来是我们团队内部为了解决“提示词越加越长、智能体越用越傻”这个问题而搭建的框架,后来整理成开源项目时,我给它起名就叫agent-skills。这个东西解决的核心痛点很直接:当你的智能体要干的事情从“聊天”变成“执行任务”时,怎么把能力组织得清晰、可扩展、可维护。
如果你正在用LangChain、LlamaIndex这类框架,或者直接用大模型API封装自己的Agent,又或者你只是好奇“别人口中说的Agent Skills到底是什么”,这篇文章应该能帮你省掉好几天的试错时间。
1. 为什么我觉得“提示词堆叠”已经走不通了
先说一段真实的经历。今年年初我在做一个法律咨询类的Agent产品,最初需求很简单:用户问问题,Agent根据法条给出回答。当时我的做法和大多数人一样——把所有的背景知识、回答风格、引用格式、免责声明全部塞进System Prompt里,效果确实不错。但随着我们加入文书生成、赔偿金额计算、诉讼策略分析等能力后,System Prompt膨胀到了4000多字符,整套系统开始变得不对劲:回答不再稳定,有时候明明记住了A规则,转头就把A规则忘了;有时候用户问了一个超出预设范围的简单问题,模型反而开始一本正经地编造错误答案。
这个问题的本质不是提示词写得不到位,而是把所有能力揉在一起,超出了模型在上下文中的“注意力预算”。就像一个人同时学十门课程,每门课的笔记都混在一个本子里,考试时翻笔记的速度根本跟不上答题节奏。
我当时意识到一件事:Agent真正需要的不是“更大更强的提示词”,而是“可被按需调用的技能模块”。
1.1 暴力加提示词的三个隐藏代价
我这几年见过大量的Agent项目,凡是做到后期开始失控的,十有八九都是提示词堆得太狠。这里有三个藏得很深的代价,很多人前期根本感知不到:
检索干扰问题。大模型的注意力机制不是无限平均分配的,当你的System Prompt里同时存在“赔偿金额计算公式”“证据清单格式”“法条引用顺序”“语气要求”这些内容时,模型在处理特定任务时会被无关内容干扰。实验测试中,仅仅是把Prompt从1500字符增加到3000字符,同一道数学推理题的准确率就下降了7%。这不是模型变笨了,而是它的推理被冗余信息干扰了。
更新成本失控。业务规则一定会变。今天修改一个赔偿计算标准,你需要在4000字符的Prompt里找到那一句话,然后小心翼翼地替换,还要担心会不会影响其他功能的表达。这种维护方式在规则单一的时候还行,规则一旦多起来,每次改动都像拆弹。
版本管理几乎不可能。我见过很多团队的Prompt文件夹里躺着十几个以“final_v2”“new_final_v3”命名的文件。Prompt本身没有结构,没有函数边界,没有测试入口,改坏了只能回滚文件,但你根本不知道改坏了哪里。
1.2 Agent Skills本质上在干一件什么事
Agent Skills的核心思路特别朴素:把大模型能力的“大而全”拆成“小而专”。每个技能模块负责一个具体的、边界清晰的子任务,比如“查法条”是一个技能,“计算赔偿金额”是另一个技能,“生成起诉状”又是一个技能。Agent在运行时根据用户的意图动态选择和加载技能,而不是把所有技能描述全部塞进上下文里。
这和人做事的方式是一样的。一个律师不会在每次接电话前把所有法律知识都回忆一遍,他是根据客户的问题判断“这案子可能需要劳动法条文”,然后专门去调取相关知识。引入Agent Skills后,大模型的工作模式就变成了:理解用户意图、选择合适的技能、加载该技能对应的提示词和工具、执行任务。
听起来很简单,但实施起来有非常多的细节。接下来的内容我按从设计到落地的顺序,把我认为最关键的决策点都拆开来讲清楚。
2. 设计一套不臃肿的技能描述规范
这一节其实是整个体系的地基。我在网上看过不少人写的技能定义,普遍的问题是“技能描述”写得太模糊。比如一个技能描述字段写着“用于回答用户关于劳动法的问题”,这种描述放在Agent的决策器里,几乎等于没有信息量,因为模型根本无从判断“什么时候该用这个技能”。
2.1 真正有效的技能描述长什么样
我的经验是,一个技能描述必须回答三个问题:这个技能负责什么、什么时候触发、什么时候不要用。说白了就是“是什么、何时用、何时不用”。
以法律场景为例,我最终精简出的技能描述结构是这样:
- name: 法条精确匹配查询 trigger: - 用户询问具体法律条文的内容 - 用户要求确认某一行为是否违反法律规定 not_trigger: - 用户只需要一般性法律常识 - 用户没有明确指向任何具体法规 output: - 法律名称与条款编号 - 具体条文内容 - 该条文在案情中的适用性说明 prompt: skills/legal_article_search/prompt.md tools: ["search_legal_db"]这个结构里有几个容易被忽略的关键点。
trigger字段的否定式写法。我见过很多技能描述只写“什么情况用”,不写“什么情况不用”,结果就是各种技能之间抢任务。比如“法条精确匹配查询”和“法律常识问答”这两个技能如果边界不清晰,模型经常把常识问题丢给法条检索技能,检索结果里出现一大堆不相关的条文,回答效率反而变低。我加了not_trigger字段之后,技能选择准确率提升了20%。
output字段明确产出物。很多技能只写了描述和工具,却不说清楚“你负责交出什么东西”。这样会导致一个问题:模型调用了技能,但产出的格式不符合下游需求。比如计算结果时,下游模块需要的是一个JSON数字,模型却给了一段解释文字。把output字段写清楚,等于提前跟模型对齐了交付标准。
prompt文件独立存放。技能的提示词不要写在YAML配置里,建议单独放在一个prompt.md文件中。因为这个提示词文件往往是变动的,单独成文件便于管理和版本控制。
2.2 技能描述的设计案例对照
为了让你更直观地感受“模糊描述”和“精准描述”的差距,我做了个对照表,左边是错误示例,右边是优化后的描述。这是我当时实际重构过程中的一部分记录:
| 维度 | 模糊设计 | 精准设计 |
|---|---|---|
| 技能目标 | 回答法律问题 | 根据用户提供的案情,检索并引用对应的劳动法条款,输出条款原文及适用范围 |
| 触发条件 | 当用户问法律问题时 | 当用户提到“合同纠纷”“劳动关系”“裁员补偿”等关键词或同义表述时 |
| 边界限制 | 无 | 不适用于刑事犯罪指控分析,不推荐直接代替律师出具法律意见书 |
| 输出格式 | 一段文字说明 | 包含条款编号、原文引用、适用性评分(0-100)、风险提示四个部分的JSON |
| 依赖资源 | 法条数据库 | legal_db_api(实时索引,更新频率:每周) |
你注意看最后一行,我连“数据更新时间”都写进去了。这是一个小细节,但价值很大。模型在回答时会结合上下文里的信息,如果你告诉它这个数据源是每周更新的,它就不会在引用法条时给你加一句“以上信息可能已经过时”之类的冗余话术。
2.3 技能定义文件里的一个常见误区
很多人在写技能定义时,喜欢把“模型怎么思考”写进去。比如一个技能描述是这样的:“请你仔细分析用户的问题,思考其背后的法律诉求,再检索法条……”这种写法的问题在于,它把“操作指令”和“推理过程”混在了一起。
实际执行的效果经常是模型在选用技能时就开始“思考”,在正式执行时反而懒得思考了。我个人的建议是:技能定义只描述“功能边界”,不描述“思维过程”。“如何推理”这件事留给技能prompt.md文件去处理,因为那是模型执行阶段的事情,不是决策阶段的事情。决策阶段的任务只有两个:选对技能、带着正确的参数进入技能。
这里我踩过坑的核心教训是什么呢,简单说:不要试图用一份技能描述同时完成“路由决策”和“执行指导”两个功能。你可能会觉得“那我就把描述写详细一点呗”,但实测下来,描述过长的技能在意图识别阶段的准确率反而下降。原因是模型在有限上下文里被大量执行细节吸引了注意力,反而忽略了触发条件的语义。
3. 从零搭建技能库:目录规划与代码骨架
设计好描述规范之后,第二步就是落地一个物理层面的技能库。很多人在“设计”上花了很多心思,但在文件系统层面却非常随意,最后写出来的代码东一块西一块,技能之间互相引用,改一个技能要连带改三个文件。这里我给出一个经历了两个真实项目打磨的目录结构。
3.1 一套清晰可复用的技能目录结构
我在实际项目里使用的结构是这样的:
agent-skills/ ├─ manifest.yaml # 技能总清单,含版本号 ├─ interface.py # 技能加载与调用的统一接口 ├─ skills/ │ ├─ legal_search/ │ │ ├─ skill.yaml # 技能定义(描述、触发条件、参数) │ │ ├─ prompt.md # 技能执行时的提示词模板 │ │ └─ executor.py # 与外部API交互的执行逻辑 │ ├─ compensation_calc/ │ │ ├─ skill.yaml │ │ ├─ prompt.md │ │ └─ executor.py │ └─ ... ├─ memory/ # 技能执行过程中产生的中间状态 │ ├─ cache.db │ └─ session_store/ └─ evaluations/ # 每个技能的回归测试案例 ├─ legal_search_cases.json └─ compensation_calc_cases.json这个结构看起来并不复杂,但每个目录都有它存在的理由:
manifest.yaml 是技能库的“总索引”。它记录着当前项目总共注册了多少个技能、每个技能对应的定义文件路径、技能之间的依赖关系。Agent启动时只需要加载这一个文件,就能知道“我现在会哪些技能”,不需要扫描整个文件系统。这个设计在大规模技能场景下特别有用,你会遇到需要禁用某个技能或调整技能优先级的场景,直接改manifest比改每个技能定义文件效率高得多。
skills目录下每个技能独立成文件夹。技能定义、提示词、执行逻辑放在同一个文件夹里,这是“内聚”的思想——一个技能的所有相关文件在一起,开发的时候只需要打开一个文件夹。
memory目录非常容易被人忽略。很多Agent技能执行时会依赖一些中间状态,比如检索历史、上一次会话的上下文变量。如果没有一个专门的目录管理这些状态,开发者会忍不住把这些临时数据塞进技能定义文件里,久而久之文件就变得又大又乱。
evaluations目录是技能库的“质检部”。这一点我在第6节详细展开,这里先强调一个观点:没有测试用例的技能库,生命周期一定不会长久。
3.2 技能接口类的实现思路
有了目录结构,下一步是用代码把“加载技能”和“执行技能”这两个动作标准化。我在interface.py里维护了一个基础类,所有技能的executor都继承自这个类:
from abc import ABC, abstractmethod from typing import Any, Dict class BaseSkill(ABC): # 技能的唯一标识 skill_id: str = "" # 技能依赖的工具列表 required_tools: list = [] def __init__(self, memory_service=None): self.memory_service = memory_service self.execution_context = {} @abstractmethod def validate_params(self, params: Dict[str, Any]) -> bool: """参数校验:在技能真正执行前,检查调用参数是否合法。""" pass @abstractmethod def run(self, params: Dict[str, Any]) -> Dict[str, Any]: """技能的主执行逻辑,返回结构化结果。""" pass def get_memory(self, key: str, default=None): """技能内部可访问的轻量级记忆服务。""" if self.memory_service: return self.memory_service.get(key, default) return default def set_memory(self, key: str, value: Any): if self.memory_service: self.memory_service.set(key, value)这个接口类的设计有三个核心的约束值得说明。
validate_params来自我对线上事故的反思。早期我的技能没有参数校验,有一次法律检索技能收到了一个缺失“案由”字段的调用,执行器直接崩溃,导致整个Agent进程报错。后来我规定所有技能在run之前必须先过validate_params这一关,执行前检查所有必填参数存在且类型正确,不合法就直接返回错误信息,而不是让异常一路向上抛。
run方法强制返回字典类型。这是我定的规矩。无论技能内部逻辑多复杂、调用了多少个外部API,最终都以结构化的字典返回结果。这样做的好处是后续的处理层可以统一解析结果,不需要对每个技能做特判。如果你让技能返回不同结构的数据,Agent的路由层就沦为if-else的集中营了。
memory_service的可选设计。不是所有技能都需要记忆功能,比如纯粹的计算技能无状态也无所谓。但是某些场景下技能之间需要共享上下文,比如“检索法条”技能可以把检索到的关键条文缓存下来,“生成法律文书”技能直接从缓存里取用,而不需要让大模型重新读一遍原始检索结果。这个设计让技能与技能之间的数据传递不依赖Agent上层的大上下文。
3.3 技能注册与发现
接口类定义好之后,最后一步是注册机制。我的实现方式是维护一个全局注册表:
class SkillRegistry: def __init__(self): self._skills = {} def register(self, skill: BaseSkill): if skill.skill_id in self._skills: raise ValueError(f"Skill ID 重复: {skill.skill_id}") self._skills[skill.skill_id] = skill def get_skill(self, skill_id: str) -> BaseSkill: skill = self._skills.get(skill_id) if not skill: raise KeyError(f"技能未注册: {skill_id}") return skill def list_skills(self) -> list: return [{"id": s.skill_id, "desc": s.get_skill_desc()} for s in self._skills.values()]有了注册表之后,Agent的主控制流就变得非常干净:意图识别后拿到技能ID,从注册表取出技能对象,校验参数,调用run,返回结果。你不需要在代码里为每个技能单独写分支,以后每新增一个技能,只需要写一个继承BaseSkill的类并注册进注册表即可。
这套模式我用了大半年,最大的体会是:当技能数量超过20个时,如果注册机制设计得好,新增技能根本不需要改动主流程代码。如果哪天你发现自己加一个技能还要改动Agent核心文件,那说明这个架构该重构了。
4. 技能编排:多技能协作时的几个实战模式
单个技能其实价值有限,真正让Agent变得“聪明”的,是多个技能之间的编排。打个比方,技能好比是工具箱里的工具,编排就是告诉你“先拿螺丝刀拆开外壳,再拿万用表测电压”。没有编排,工具只是一堆铁块;有编排,工具才能完成复杂的任务。
4.1 顺序型编排:技能链
最简单的编排模式是顺序执行。A技能的输出作为B技能的输入,形成一条流水线。我在法律场景里最喜欢的例子是“案情分析与赔偿计算”这条链路:
# 注意:这里省略了上层的意图路由逻辑,重点展示技能链接的编排示意 def run_claim_analysis_chain(user_case_desc: str): # 技能1:从案情描述中提取要素 res1 = skill_registry.get_skill("fact_extractor").run({ "case_desc": user_case_desc }) # 技能2:依据要素检索适用的法条 res2 = skill_registry.get_skill("legal_search").run({ "facts": res1["facts"] }) # 技能3:计算赔偿金额 res3 = skill_registry.get_skill("compensation_calc").run({ "law_ids": res2["relevant_law_ids"], "facts": res1["facts"] }) return res3这个模式的好处是简单、可靠、每一步都可以单独测试和调试。但它有一个前提条件:前一个技能的输出必须完整包含后一个技能所需的信息。如果前置技能漏掉了一个关键字段,后面整条链路就会出错,所以参数设计时一定要舍得“多传信息”,让下游有足够的数据可用。
4.2 路由型编排:选择器
现实世界的Agent很少只走一条固定流程,更多的场景是根据用户问题的类型走不同的分叉。这时需要“路由型编排”,核心是一个意图分类器:它接收用户输入,输出一个技能ID,然后由主控制器调用对应的技能执行。
from pydantic import BaseModel class IntentResult(BaseModel): skill_id: str params: dict def route_and_execute(user_input: str): # 这里用大模型做一次简洁的意图分类 intent: IntentResult = llm_classify(user_input) skill = skill_registry.get_skill(intent.skill_id) return skill.run(intent.params)这个模式有一个独到的坑:意图分类绝对不能把“技能描述全文”塞进Prompt里。当技能数量超过15个,即使每个描述只有150字,加起来也有2000多字,加上用户问题、历史会话和格式约束,很轻易就突破了大模型的上下文窗口。我的解决方案是先做一次粗粒度分类,把技能归成几个大组,模型先判断属于哪个组,然后在组内做细粒度路由,这样每一层的候选数量都控制在5个以内,准确率显著提升。
4.3 并行型编排:聚合器
有些任务可以被拆成互相独立的子任务同时执行,等所有结果返回后再合并。比如用户要求写一份“交通事故赔偿分析报告”,可以同时并行执行“事故责任分析”“医疗费用计算”“误工费评估”“精神损害赔偿分析”四个技能,最后汇总成一份完整报告。
import asyncio async def run_parallel_analysis(case_data: dict): tasks = [ skill_registry.get_skill("liability_analysis").run_async(case_data), skill_registry.get_skill("medical_cost_calc").run_async(case_data), skill_registry.get_skill("lost_wages_calc").run_async(case_data), skill_registry.get_skill("mental_distress_calc").run_async(case_data) ] results = await asyncio.gather(*tasks, return_exceptions=True) return { "liability": results[0], "medical_cost": results[1], "lost_wages": results[2], "mental_distress": results[3], }并行编排省时间,但它对技能的设计提出了更高的要求:并行执行的技能必须没有相互依赖,绝对不能出现“A技能执行时依赖B技能的中间结果”这种矛盾。在动手写并行代码之前,先画一张数据依赖图:谁需要在谁之后运行,一目了然。
4.4 编排中的关键事故复盘
上面介绍的三种模式各有优缺点,但实际项目里我踩过的最深刻的坑不在这三种模式的代码逻辑上,而在技能之间共享上下文时产生的幻觉。
有一次,A技能给B技能传了一个参数叫“案件类型”,但由于技能定义的输出字段不够明确,B技能收到的是一个“相似但不对”的值。B技能的执行器没有做参数校验,拿这个错误值算了一通,最后还振振有词地生成了一份长达3000字的分析报告。排查这个bug花了整整两天,最终发现问题源头是A技能的output字段里有两个相似的键名,一个叫case_id,一个叫case_type_code,大模型在执行时把case_id当作case_type_code传给了下游。
这个经历让我定下了一条铁律:参与编排的每个技能,validate_params和输出结构的字段命名必须单独定义,不允许沿用“人觉得差不多”的名字。如果两个技能之间的对接字段超过5个,直接用JSON Schema校验,别相信模型不会犯错。
5. 技能切换与状态管理:避免上下文污染的几个实操技巧
技能编排解决了“多个技能怎么合作”的问题,但Agent在实际运行中还有另一个杀手级问题:上下文污染。指的是前一个技能执行后残留的信息影响到了后一个技能的表现。这个问题在连续多轮对话中尤为严重。
5.1 上下文污染是怎么发生的
给你描述一个我真实遇到过的场景。用户问:“我工作三年,公司要裁员我,能赔多少?”Agent先调用了“劳动法检索”技能,从法条数据库里查出了N+1的赔偿规则,然后调用计算技能,算出赔偿金额。这是第一轮对话,一切正常。
第二轮用户问:“那如果我自己提辞职呢?”我的Agent当时的错误表现是:它记住了第一轮的法律检索结果,直接基于“裁员”的规则回答,而没有重新检索“劳动者主动辞职”的相关条款。最终的回答张冠李戴,把裁员赔偿规则用在了辞职场景上,结论完全错误。
这个问题的根因是:技能A运行时检索到的法条被塞进了对话历史,模型在第二轮看到这段“历史”,误以为它仍然适用于当前问题。这就是上下文污染。
5.2 解决方案:为技能建立独立的临时工作区
我的方案是把“全局对话记忆”和“技能局部记忆”分隔开。技能的中间结果只存在自己的临时工作区里,技能执行结束后,只有“最终结论”会被放入对话上下文;那些检索的依据、计算过程、中间变量,统统留在临时工作区,由专门的清理机制处理。
具体实现上,我在memory目录下为每次会话单独建一个session_id,用SQLite存储键值对。技能在执行过程中往session_store写入临时数据,执行完成后只把“结论”通过返回值交给上层,上层再决定哪些内容需要保留到全局对话记忆。
还有一个小技巧:在每次技能执行前,给技能一个干净的“上下文窗口”。我实现了一个reset_context_for_skill()函数,清空该技能专用命名空间下的所有缓存数据,确保每次执行都是“无偏见的开始”。
5.3 技能计数与上下文预算管理
大模型的上下文窗口不是无限的,当你的Agent技能栈越来越深,每一轮都可把大量信息写进历史。这时候学会“预算是谁、谁在花、花了多少”就是保住Agent稳定性的关键。
我建议为每个技能设置一个“预算分档”:普通技能预算300-500 token,复杂技能预算1500-2500 token,对话历史本身单独预算。主控制器在每次技能执行前检查当前已用的token数量,如果接近上限,自动拒绝继续向上下文中追加技能结果,改为输出摘要文本。
这个方法本质上是在逼着自己优化输出结构。如果每个技能的返回结果都控制得足够精简,整体上下文占用必然会下降,Agent的响应速度和准确率都会跟着提升。我在实际项目中做了个粗略统计,精简技能输出结构之后,整体token消耗减少了22%,响应时间缩短了15%。看起来不多,但在一个日均上万次调用的生产系统上,这意味着实实在在的计算成本节省。
5.4 技能内部的“状态快照”
还有一个很多人会忽略的细节:当技能执行过程中调用了外部API,而这些API可能因为网络问题失败或超时时,技能的“状态”应该怎么办?
我的建议是执行器内部实现一个“状态快照”机制。关键技术点在于:每个技能在run方法中会分阶段执行,比如“解析参数—调用外部API—后处理结果”。在每个阶段开始前,把当前输入参数和中间状态存一份到临时工作区。一旦某个阶段抛异常,栈异常信息里可以提取出“从哪个阶段失败的”,以及“当时的参数是什么”,调试效率能提升一个量级。
这个思路说白了就是技能的“断点续跑”,只不过我们通常用不上续跑功能,只把它用于日志追踪。
6. 技能质量的评估与迭代机制
技能库建起来了,跑起来了,你以为这就结束了?不,真正麻烦的工作才刚刚开始。技能和其他代码模块一样,会随着业务的变化而“腐化”,比如外部API升级了返回格式、业务规则修改了计算逻辑、新的法律法规生效了需要更新检索范围。如果没有一套评估机制,你根本不知道技能是“还能用”还是“早就坏了”。
6.1 每个技能都应该有一份回归测试集
我在项目启动时的要求是:每个技能在开发完成时,必须至少配10个回归测试用例。这些用例保存在evaluations目录下,每个用例包含输入参数、期望输出(或输出的关键字段)和备注说明。测试用例的来源主要有三类:
- 开发时的典型场景
- 线上收集到的失败case(只要出过一次错,必须沉淀为该技能的回归用例)
- 边界条件(比如输入为空、参数类型错误、数量级过大的情况)
我用一个定时脚本每晚跑一遍全量回归测试,把技能的执行结果和期望结果做对比。一开始跑出来错很多,但坚持迭代两个月之后,技能出错的概率大幅下降,因为绝大多数问题在回归阶段就被拦截住了,根本不会流到线上。
6.2 大模型类技能的“模糊断言”怎么设计
照搬传统软件的测试思路有麻烦:大模型生成的输出是自然语言,不是结构化的、可以精确对比的结果。你不能简单断言“输出等于某段文字”,因为同一个意思可以有一百种不同的表达方式。
我的策略是字段级结构化断言加语义相似度校验。对于技能返回的JSON结构化部分,直接对比字段值和类型;对于自然语言部分,用语义相似度模型计算生成文本和期望要点之间的相似度,超过阈值就算通过。如果技能生成的是一个数字结果(比如赔偿金额),那么直接比较数值的正确范围即可,这一类技能反而是最容易测试的,只要输入对应的参数,输出就必须等于正确数字,没有模糊空间。
6.3 线上观察与技能进化
测试用例提供了“离线反馈”,但还不能完全替代“线上观察”。我在Agent系统的请求日志里,为每个技能的执行记录增加了几个专用字段:技能ID、参数摘要、执行耗时、返回结果摘要、用户是否满意(通过点踩/纠错按钮表达)。
这些数据最终汇总到一个分析面板上,我可以直观地看到:哪些技能调用最频繁、哪些技能的平均执行时间在变长、哪些技能的“用户不满意”比例居高不下。每次版本迭代前,先看这个面板决定优先优化哪些技能。技能的定义、提示词、工具集都是可以持续迭代的,关键是迭代之前有数据支撑,而不是全凭感觉。
这个评估循环跑顺之后,我的一个强烈感受是:技能不是一次性开发完成就结束的,它其实和技术产品一样,需要运营和维护。没有评估机制就等于盲人骑瞎马,你根本不知道哪一天一个不起眼的改动会让技能彻底“发疯”。
7. 踩坑复盘:技能设计中最容易被忽略的三个细节
文章写到这儿,核心架构和流程都讲得差不多了。最后我复盘三个在技能设计中特别容易翻车的细节,希望能帮你避免重复走这些弯路。
第一个坑:技能prompt.md里忘记写“不要做什么”。我早期写的提示词只告诉模型“你负责做什么,需要输出什么”,却遗漏了“碰到什么情况时停止或转人工”。结果就是技能在拿不准的时候硬着头皮瞎编。后来我在每个技能提示词末尾都加了一个“限制与拒绝”小节,明确列出不能擅自处理的情况以及应有的反馈姿势。
举个例子,法条检索技能里写的是:如果用户提供的案情信息不足以确定应适用的法律,必须直接告诉用户“信息不足,请补充如下要素”,而不是自行猜测法条。这个简单补充把无意义输出的比例降低了不少。
第二个坑:执行器代码里的硬编码魔数。为了图省事,我早期在计算技能里直接写死了几个赔偿标准数值。结果政策调整时,忘了改这段代码,线上计算出来的赔偿金额整整错了一个月,直到用户投诉才发现。后来所有“可变规则”都迁移到配置文件中,且配置文件变更时会自动触发技能回归测试,从机制上杜绝了这类事故。
第三个坑:没有为每个技能设置“超时熔断”。外部API偶尔不稳定,技能会长时间卡在等待响应中,连带着整个Agent会话被拖死。我给技能注册表的每个技能都配置了超时时间和失败回退策略(失败时直接返回错误信息还是换备选方案),这个设计极大提升了系统的健壮性。而且我会刻意用一个慢速mock服务做演练,人为制造超时来验证熔断逻辑确实是有效的,而不是只在代码里写了判断语句就以为万事大吉了。
我始终觉得,Agent能不能真正落地为可靠的生产工具,拼的不是模型有多强,而是工程化细节有多扎实。技能体系只是把“工程化”这件事前置了:用清晰的边界和可测试的结构,把不确定性挡在核心链路之外。使用agent-skills这套方法论半年多来,我最明显的感觉是:新增一个能力不再让人心惊胆战,而是在现有框架上按部就班地填内容、挂测试、放流量。这才是做工程该有的状态。
如果你也在搭建自己的Agent技能体系,我建议你不用一口气把这套架构全部实现,先挑一个核心场景,手工拆出一个技能,配好描述和prompt,再逐步扩展。做技能这件事,最怕的不是慢,而是第一步就忘了给以后的技能留位置。