作为一个常年折腾AI应用开发的工程师,我最近在做一个叫“agent-skills”的项目,核心就一件事:给AI智能体搭一套可扩展的技能系统,让大模型不只会聊天,还能真正动手干活。这个项目解决了一个很实际的痛点——很多开发者把LLM接进应用之后,发现模型生成的文本很漂亮,但一旦需要它去执行实际操作(查数据库、调接口、读写文件),就完全没辙了。Agent Skills这套思路,本质上是把大模型和真实世界之间的“手指”接上。它适合正在做Agent应用开发的工程师、对AI自动化感兴趣的研究者,也适合想把手头重复性工作交给AI处理的普通技术玩家。
这套系统的出发点其实很朴素:把“技能”做成可注册、可发现、可组合、可观测的独立模块,让Agent在运行时动态决定自己想调用什么工具、按什么顺序调用。听起来不复杂,但真正落地的时候会撞上不少坑。我这篇文章会把整个设计思路、核心实现、踩坑记录和扩展方向都摊开来讲,代码可以直接抄走改改就用。
1. 为什么Agent需要一套“技能系统”
1.1 从对话到行动的跨越
纯LLM应用的本质是“文本进,文本出”。用户问一句话,模型生成一段回答,仅此而已。但真实业务场景里,用户需要的不是一段回答,而是一个结果:“帮我查一下昨天华南区的销售数据”“把这30份PDF里的表格提取出来整理成Excel”“定时监控这个网站的价格并在超过阈值时发提醒”。
这些需求都有一个共性:需要模型之外的工具来执行动作。查询数据要连数据库,提取表格要跑解析脚本,监控价格要发起HTTP请求。没有技能系统,模型就只能“干说说”,什么也做不了。
早期方案是写一堆if-else,把可能的用户意图硬编码进代码里。比如用户提到“天气”两个字就调一次天气接口,提到“翻译”就调一次翻译接口。这在Demo阶段够用,但意图一复杂、组合一多,代码就变成意大利面条。更麻烦的是每次加一个新功能都要改核心逻辑,牵一发动全身,而且老功能还可能被新逻辑无意间破坏。
Agent Skills要解决的,就是把“技能”从业务逻辑里剥离开,做成一套可注册、可发现、可组合的独立模块。模型看到用户请求后,自己判断该调哪个技能、按什么顺序调。业务逻辑里不再散落着各种“如果用户说X就调用Y”的硬编码分支,而是变成了一张技能清单和一套执行框架,Agent的动态决策能力被真正释放出来。
1.2 技能系统的设计目标
我在设计这个系统的第一天,给自己定了四个目标,后面所有功能和取舍都围绕这四个目标展开:
技能可插拔。加一个新技能就像插一个U盘,只需要写一个独立的技能文件,不需要改动Agent的主流程、不需要改注册列表、不需要动其他技能。这个目标决定了架构必须去中心化。
技能可发现。LLM需要能实时感知当前有哪些技能可用、每个技能需要什么参数、什么时候该用哪个技能。如果模型压根不知道这个技能存在,那技能写得再好也没用。
技能可组合。单一技能太弱,要让Agent能把多个技能串起来完成复杂任务。比如“整理桌面”这个需求,拆开来看就是列目录、建文件夹、移动文件、删除临时文件这几个动作的排列组合。系统要允许而非阻碍这种组合。
技能可观测。技能执行过程必须能记录日志、能追溯,方便排查“Agent为什么会这样操作”。没有可观测性,Agent一旦执行了意外操作,你会连原因都查不出来。
这四个目标听起来简单,实际做起来每一步都有很多隐藏细节。尤其是“可组合”和“可观测”这两点,做到位比想象中难得多,后面我会逐一展开。
2. 技能框架的整体设计
2.1 技能的抽象与接口定义
一个技能,在我的设计里,本质上是“元数据 + 函数实现 + 参数Schema”三件套的组合。元数据告诉Agent这个技能是干什么用的;参数Schema告诉Agent调用这个技能需要提供哪些字段、每个字段的格式是什么;函数实现则是真正执行动作的代码。
我参考了业界常见的工具调用规范,给每个技能定义了一个统一结构。所有技能都长一个样子,Agent执行的逻辑才能统一。
from dataclasses import dataclass from typing import Callable, Dict, Any @dataclass class AgentSkill: name: str # 技能名称,LLM用这个来引用 description: str # 功能描述,帮助LLM判断何时使用 parameters: dict # JSON Schema形式的参数定义 execute: Callable # 真正执行的函数 version: str = "1.0.0" # 技能版本 timeout: float = 30.0 # 执行超时时间(秒) risk_level: str = "read" # 风险等级:read/write/destructive在这个基础结构上,我又加了几个运行时管理字段,比如超时时间、风险等级、技能版本。这些字段前期用不上,但一旦技能数量多起来,它们是排查问题、做安全控制的关键抓手。
很多人会忽略description的重要性。我真实踩过一次坑:一开始给某个技能写的description很随意,结果模型频繁误调用——用户明明只是问了一句“这个文件多大”,模型就去调了“读取文件内容并返回全文”的技能,白白浪费了几千token。后来我把description改成了带触发条件和边界说明的写法,“当用户询问文件大小、属性或存在性时使用;当用户要求读取文件内容时改用read_file”,误调用的情况一下子就消失了。
2.2 技能注册与发现机制
技能系统需要一个注册中心,所有技能在系统启动时注册进来,然后暴露给LLM。
注册的方式有两种。一种是静态注册,在代码里显式调用register函数,把所有技能集中在一个文件里管理。另一种是装饰器自动发现,模块加载时自动收集所有标记为@skill的函数,技能散落在各自独立的文件里。
我最终选了装饰器方案。原因很简单——加新技能时不用改注册列表,只需要在项目目录下新建一个文件,写上函数、加上装饰器,系统启动时就能自动发现。这跟我“可插拔”的目标完全一致。
skills_registry = {} def skill(name, description, parameters, **kwargs): def decorator(func): skills_registry[name] = AgentSkill( name=name, description=description, parameters=parameters, execute=func, **kwargs ) return func return decorator每个技能文件长这样:
# skills/file_ops.py @skill( name="read_file", description="读取文本文件内容并返回。当用户要求读取文件、查看文件内容时使用。", parameters={ "type": "object", "properties": { "file_path": {"type": "string", "description": "文件的绝对路径"} }, "required": ["file_path"] }, risk_level="read" ) def read_file(file_path: str) -> str: with open(file_path, "r", encoding="utf-8") as f: return f.read()系统启动时,扫描skills目录下的所有文件,import一遍,注册表就自动填满了。这种动态发现机制用起来非常舒服,团队协作时每个人维护自己的技能文件,互不干扰。
2.3 技能编排与组合
单一技能解决不了复杂任务,Agent需要把多个技能按顺序组装成一条“工作流”。我在系统里做了一个简单但实用的编排机制——让LLM自己来决定技能调用顺序,而不是在代码里硬编码工作流。这个机制基于大模型的多步推理能力:模型先拆解任务,然后规划先调哪个技能、后调哪个技能,每一步根据前一步的结果决定下一步的动作。
举例来说,用户说“把我Downloads文件夹里所有PDF的首页截取成图片,然后放到桌面”。这个任务拆解下来需要几件事:列出文件夹里的PDF清单(list_files)、逐个截取首页(pdf_to_image)、把生成结果移入指定目录(move_file)。如果这些技能都是独立的、通过统一接口暴露给模型,模型自己就能编排整个流程。
但如果这些逻辑是硬编码在Agent主流程里的,每遇到一个新的复合需求就要改代码。技能系统的价值就在于:让组合的复杂度由模型承担,而不是由开发者承担。开发者只需要维护好原子技能的质量,剩下的交给Agent的推理能力。
实践下来,这种“模型驱动编排”在小规模技能库下非常好用。但在技能数量超过二十个以后,模型偶尔会选错技能、漏掉必要步骤。后来我加了一个“工作流记忆”模块:把用户常见的高频任务流程缓存下来(比如“整理桌面”的成功路径),下次遇到相同场景时优先参考历史成功路径,而不是让模型从头推理。这个优化带来的效果非常明显,复合任务的完成率提升了接近三成。
3. 核心实现与实操要点
3.1 技能定义的数据结构
一个技能最核心的部分是参数Schema。LLM需要根据描述和参数Schema来填充调用参数,如果Schema定义得含糊,模型生成的参数就经常不合法、不完整。
参数Schema说白了就是一份JSON Schema。我的习惯是先定义参数名、类型、必填还是选填,以及每个参数的说明。这个说明特别重要,模型就靠它来理解该填什么、格式是什么。
parameters = { "type": "object", "properties": { "file_path": { "type": "string", "description": "要读取的文件的绝对路径" }, "encoding": { "type": "string", "description": "文件编码,默认utf-8", "enum": ["utf-8", "gbk", "latin-1"] }, "start_line": { "type": "integer", "description": "起始行号,从1开始,默认1" } }, "required": ["file_path"] }这里有几个实打实的经验,都是改了很多轮才摸出来的:
能用enum约束的取值范围,尽量用enum。模型生成enum内的值,成功率比自由文本高得多。比如上面的encoding,如果只是写“string”,模型可能会填“utf8”“UTF_8”“utf-8”各种变体,你解析代码就得做归一化。用enum直接杜绝这个问题。
参数的description里尽量包含值的格式示例。比如日期参数写成“日期格式为YYYY-MM-DD,例如2025-03-14”,模型照着填的成功率会比只写“日期”高很多。这背后是模型对格式示例的模仿能力远强于对抽象格式描述的遵循能力。
别把参数设计得太细碎。一个技能超过七八个参数时,模型经常搞混。宁可把相关参数组合成嵌套对象,比如“query_params”和“headers”作为http_request的两个对象参数,也好过平铺十几个字符串字段。
3.2 技能调用的上下文传递
技能执行不是孤立的。很多技能需要知道对话的上下文——用户当前处于什么状态、前面已经做了哪些操作、工作目录在哪里。我在系统里引入了一个Context对象,作为技能执行的“共享黑板”。
@dataclass class AgentContext: user_id: str workspace: str skill_history: list session_state: dict每个AgentSkill的execute函数统一接收一个context作为第一个参数。技能可以读它、写它,但要有规范——同一字段的写法要统一,不要在技能里随便塞东西污染全局状态。
这块看起来简单,但最容易埋雷。我一开始没有隔离session_state的作用域,结果一个技能写了一个名为“path”的字段,另一个技能把它当成了自己需要的路径参数,直接就给覆盖了。排查这个bug花了整整一个下午。后来我规定所有技能写入状态必须带技能名前缀,比如file_ops.last_export_path而不是last_path。同时规范了读写:读操作统一走context.get("技能名.字段名"),写操作统一走context.set("技能名.字段名", value)。
另一个经验是:技能之间不要直接共享可变对象。比如文件操作技能生成了一个列表,另一个技能要消费这个列表,应该通过context传递序列化后的结果(比如JSON字符串),而不是直接传一个Python对象引用。原因还是隔离性——你永远不知道其他技能会不会修改这个对象,序列化传递能把耦合降到最低。
3.3 技能执行的安全与隔离
给Agent开放“动手”能力,就意味着允许它执行真实操作。这一点不控制好,Agent系统会变成定时炸弹。安全性不是可选项,是上线前必须敲定的基础能力。
我做了一套不算复杂但足够有效的防线:
技能执行前做参数校验。凡是文件路径类参数,必须校验是否在允许的目录范围内。我这边的实现是解析出绝对路径后,检查是否以workspace目录为前缀,不是就直接拒绝。克隆仓库到本地做测试时,发现这个校验拦下了不少模型“想当然”生成的路径,比如直接往系统根目录写文件。
技能执行超时熔断。每个技能设定超时时间,超时就直接返回错误。否则一个技能卡住,整个Agent就无限等下去。我用的是threading + future的简单超时机制,防止网络请求类技能长时间挂起。
敏感操作二次确认。删除、覆盖、发送消息这类动作,需要在回复里先征求用户确认。这是最后的兜底。连Agent自己都可能判断错,用户确认是最后一道防线。
举个例子,我在做文件清理技能时,有一个delete_file技能。用户说“把temp目录清空”,模型生成一个删除动作,这个动作的影响范围可能很大。我的系统会在执行前检查受影响文件数量,如果超过阈值就要求用户说“确认”才真正执行。
这个“影响评估”的思路值得推荐。不需要做得很复杂,只要在技能层加一个可选的precheck函数,在执行前跑一遍,返回影响报告,Agent就能在回复里把这个报告展示给用户,让用户做决策。
4. 从零搭建一个Agent技能系统
4.1 第一步:列出你的技能清单
不管框架多先进,先想清楚你的Agent需要具备哪些能力。我建议从一个真实的用户任务出发,比如“整理下载文件夹”“生成周报”“查询订单状态”“发送审批提醒”,然后把这些任务拆成原子操作,这个原子操作就是技能的合适颗粒度。
在agent-skills这个项目里,我第一批实现的就是几个常用的基础技能:read_file、write_file、list_dir、move_file(文件与目录管理);sql_query(数据库只读查询);http_request(通用HTTP请求);pdf_extract_text、image_convert(文档处理);web_search(搜索并带回来源链接)。
技能颗粒度的把握是个很有意思的问题。拆得太细,Agent需要编排的步骤太多,容易出现中途断链;拆得太粗,技能本身太复杂,参数定义困难且复用性差。我的经验是:一个技能的职责边界,最好控制在“一个正常人用一句话能说清它干什么”的范围内。比如pdf_to_image是合适的,handle_pdf_document就太粗了。判断标准很简单:如果你在给这个技能写description时需要写两三段才能说清边界,那就该再拆细一点。
4.2 第二步:实现技能注册表
注册表是技能系统的核心基础设施。我实现了一个SkillRegistry类,它只负责三件事:注册技能、列举所有技能供LLM感知、按名称获取技能执行。
class SkillRegistry: def __init__(self): self._skills = {} def register(self, skill: AgentSkill): self._skills[skill.name] = skill def list_skill_specs(self): return [ { "name": s.name, "description": s.description, "parameters": s.parameters } for s in self._skills.values() ] def get(self, name: str) -> AgentSkill: return self._skills.get(name)list_skill_specs这个方法,就是要把技能列表喂给LLM的输入模板。LLM看到的是类似这样的内容:
可用技能: - list_files: 列出目录下的文件列表。参数:path(必填,string) - pdf_to_image: 将PDF文件的指定页转为PNG图片。参数:pdf_path(必填,string), page_number(选填,int), output_dir(必填,string) - move_file: 移动文件或目录。参数:source(必填,string), dest(必填,string) - delete_file: 删除文件。参数:file_path(必填,string)。注意:影响不可恢复。这段描述会被拼进System Prompt或者作为tools参数传给模型。它决定了模型对技能库的第一印象,值得花时间打磨。
4.3 第三步:接上LLM的Function Calling
这一步是核心。选用的模型需要支持Function Calling(工具调用)。目前主流的大模型都支持这个能力,接入方式大同小异,关键是把前面的技能注册表跟模型的工具调用机制对接好。
我的主循环流程是这样:
- 把技能清单转成模型要求的tools格式,传给LLM。
- 模型返回响应:如果它决定调用技能,会返回一个包含function name和arguments的消息;如果不需要调用,就直接返回自然语言结果。
- 解析这个消息,从registry里取出对应技能,执行并把结果追加进对话上下文。
- 将执行结果返回给LLM,让它继续推理或结束。
核心代码可以精简成下面这个结构:
def run_agent(user_query: str, registry: SkillRegistry): messages = [{"role": "user", "content": user_query}] max_rounds = 10 for _ in range(max_rounds): tools = registry.list_skill_specs() response = llm.chat(messages, tools=tools) if not response.tool_calls: return response.content for call in response.tool_calls: skill = registry.get(call.function.name) if skill is None: messages.append({ "role": "tool", "tool_call_id": call.id, "content": "错误:未知技能,请检查技能名称" }) continue try: args = json.loads(call.function.arguments) result = skill.execute(**args) result_str = json.dumps(result, ensure_ascii=False) except Exception as e: result_str = f"技能执行失败: {str(e)}" messages.append({ "role": "tool", "tool_call_id": call.id, "name": call.function.name, "content": result_str }) return "已达到最大推理轮数,任务未完成"这块我试过几个不同的实现路径,踩过两个比较典型的坑。
第一个坑是:把工具调用结果放回对话时,忘了带上对应的tool_call_id。模型会“失忆”,不知道上一步执行了什么、结果属于哪个调用。一定要按标准格式把工具调用的ID、名称、参数、结果一一对应。这也是排查时最先检查的地方。
第二个坑是:参数解析失败。模型有时会返回一个不是合法JSON的arguments字符串,比如带注释、带多余逗号。我在解析时做了宽容处理:先尝试json.loads,失败则尝试用正则抽取key-value对,或者使用ast.literal_eval做后备。注意捕获异常,把错误信息回传给模型,让它自己修正参数重试。这个重试机制能让成功率提升一个量级。
4.4 第四步:测试与验证
技能系统的测试有一个很特殊的困难:同样的输入,模型生成的调用序列可能每次都不一样。传统“输入-输出”的测试模式在这里不完全适用。
我最后用的测试方案分三层:
单技能单元测试。直接调用execute函数,传固定参数,验证结果。这层完全不涉及LLM,跑得又快又稳。每个技能必须至少有正常分支和异常分支两个用例。
意图-动作对测试。给定一组“用户请求、期望的动作序列”,跑Agent,断言执行的动作序列是否覆盖了期望的关键项。这里不做严格相等,只看关键步骤是否出现。比如“整理桌面”这个任务,断言必须出现list_dir和move_file,且不能出现delete_file(除非用户明确要求)。
端到端回归测试。用真实场景跑完整流程,检查最终产物是否生成。比如“把某个文件夹下所有PDF的首页转成图片”这个任务,跑完检查输出目录里图片文件的数量和内容是否正常。
在CI里我写了一个定时任务,每天跑一次全量意图测试。一旦上游模型升级或者接口输出格式有变化(比如arguments里多了一个空格),测试就会立刻报警。这套三层测试帮我提前发现了很多问题,比等用户反馈再排查高效太多。
5. 常见问题与排查技巧
5.1 技能调用失败,报JSON解析错误
这个问题特别常见。根源往往不是代码,而是模型输出的arguments不规范。模型返回的arguments可能是带Markdown标记的、有尾逗号的、甚至里面嵌了注释的文本。
排查步骤:先把模型原始返回的工具调用报文完整打印出来看,确认是格式问题还是内容问题。格式问题就加强解析容错(比如用更宽容的解析库、或者写一个修复函数把常见格式错误修掉)。内容问题就看是不是参数的description写得太含糊、enum/格式约束没写全。
我比较常用的一招是:把JSON解析失败的消息回传给模型,附上“解析失败的具体原因”,让模型自己重新生成一次参数。这个反馈重试机制通常比前端做一堆字符串修复更可靠。
5.2 模型选不到正确的技能,或者干脆不调用技能
大多数情况下是技能的description写得太粗糙。我最后给每个技能的description定了一个模板:
[{触发场景}] 当用户要求{动作}或{场景描述}时使用。{补充说明:何时不使用}。输出参数包括{关键参数},其中{必填参数}必须由用户提供或从上下文中推断。比如一个“查询订单状态”的技能,description可以写成:
当用户查询订单状态、物流进度或配送信息时使用。注意:如果用户只是询问订单政策或退换货规则,不使用本技能,改用faq_lookup。这个模板背后有一个认知:LLM做工具选择的依据,就是输入文本与工具描述之间的语义匹配。描述写得越贴近用户的自然表达,匹配成功率越高。另外,如果Agent有多个相似技能,彼此的description里必须写明边界条件,减少误选。
还有一个容易被忽略的点:技能清单太长时,模型会“看漏”。我曾把一百多个技能描述全部拼进上下文中,模型明显变“笨”了,不仅选错率高,响应也慢。后来改成技能分组展示:先让模型看技能目录(技能名+一句话简介),等它选了候选技能组,再展开组内详细参数。这招对长技能库非常管用,不是所有技能详情都需要一次性暴露给模型。
5.3 技能执行顺序混乱,任务步骤容易断
复合任务里,模型经常会“跳步”或者“倒退”。比如用户在“整理桌面”这个任务中,先让Agent把图片移到当前文件夹,又让它把所有PDF转成Word。Agent跑了三步之后突然忘了之前刚创建的文件夹叫什么,又回去重新列目录。
我解决这个问题的办法,是在每轮工具调用返回后,往消息历史里加一条轻量的执行摘要,格式是“已完成:X;当前待处理:Y;原任务的最终目标是Z”。这样模型每看一次历史记录,都能快速想起自己进行到哪一步了,断链情况大幅减少。
同时,在提示词里要求模型在每一步工具调用前输出简短的执行理由,比如“需要读取该文件内容后才能判断后续处理方式”。这个理由会进入消息历史,成为模型后续决策的上下文。它既是思维链的一部分,也是排查Agent决策逻辑的重要依据。
5.4 性能问题:Agent响应太慢、消耗太多Token
Agent类应用的Token消耗比普通对话高一个数量级,这是正常现象,但可以优化。不优化的话,一个简单任务可能消耗几万token,成本直接爆炸。
几个具体的建议:
把工具description做“摘要化+详情折叠”。给LLM的输入里只放一句话摘要,等模型选择到该技能时,再追加完整详情。这能显著减少base input的token量。
长对话场景,对工具执行结果做截断。比如一次性读取了大文件,只把关键摘要反馈给模型,完整内容存到本地临时文件,等需要时再让模型用另一个技能去读。
设置合理的最大轮数。通常在十轮以内就能完成大部分任务,超过就主动结束并给用户生成一份进展报告。死循环比错误答案更浪费。
多级模型策略。规划阶段用强模型,单步技能参数的生成用快模型,能省不少成本。规划通常只需要一次,而技能执行可能要好多次,快模型省下的token积少成多。注意两个模型的能力差距不能太大,否则快速模型生成的参数质量会拖累整体。
6. 实战心得与后续扩展
6.1 我在项目里踩过的坑与调整
做agent-skills这个项目的过程中,我修改最多的不是具体技能代码,而是系统的“边界意识”。一开始我让Agent自由调用所有注册技能,结果在一次内部测试中,Agent为了完成“整理桌面”的需求,自己组合出一长串文件操作序列,其中一步把用户的一个同名文件覆盖了。虽然是在测试环境,但这个教训很深刻。
从那以后,我引入两个原则:最小权限和操作确认。每个技能注册时声明自己的风险等级:只读操作无需确认,写入类操作需要确认,删除/覆盖类操作必须确认并记录审计日志。日志里至少要包含谁调用的、参数是什么、执行结果是什么、花了多长时间。这个日志在出问题的时候能救命。
另一个调整是“技能失败时的降级策略”。刚开始技能执行失败就直接返回错误,让模型自己决定怎么办。但模型有时会反复尝试同一个失败技能四五次,白白浪费Token。后来我加了失败计数,同一技能连续失败三次,就自动转入“人工接管模式”,让模型把当前进度和问题整理给用户,请用户决策。这样既不浪费资源,也避免了Agent在同一个问题上原地打转。
6.2 技能系统的下一步扩展方向
这套架构还能继续延伸,我已经在规划几个方向。
技能学习。现在的技能都是开发者手工编写的,下一步打算记录用户在Agent辅助下完成的复合任务序列,自动提炼出可复用的技能模板。这相当于把“用户会教模型干活”这件事变成系统能力。做法并不复杂:每次成功的多步骤任务都存一份轨迹,跑聚类分析找出高频动作组合,再让大模型为这个组合生成技能描述和参数定义,最后人工审核后抽成正式技能。
技能市场。每个技能是一个独立的包,有版本、有权限声明、有测试用例。Agent可以按需从远端市场拉取技能并安装,而不是所有技能都预装在本地。这个方向对大型组织尤其有价值——不同部门维护自己的技能包,通过统一的安全审查后发布,别的小组就能直接复用,不需要重新造轮子。
还有一个技术细节是技能并发。目前一个Agent同一时刻只会串行执行技能,但真实场景里很多任务完全可以并行。比如做市场分析时,同时抓取三个网站的数据、同时查询两个数据库,把这些步骤做成并发调度,能够显著缩短整体任务时间。并发之后要注意的是状态隔离和结果汇总顺序,这两个问题不处理好,并发带来的收益会被复杂度抵消掉。
6.3 最后分享两个小细节
这两个细节不算是核心功能,但很影响日常使用的顺滑度。
一个是技能的“干跑模式”。我在系统里加了一个开关,打开后技能执行时只打印“将要执行XXX,参数是XXX”,但不真正执行。这个模式对调试编排逻辑特别有用,尤其是在Agent调错技能、参数填错的时候,干跑模式能让你一眼看出模型“想干什么”,而不必承受真实执行带来的副作用。
另一个是技能的幂等性设计。重复调用同一个技能两次,应该得到等价的结果。文件读取、搜索这些天然幂等,但发送消息、创建订单这类操作就需要特殊处理——在技能执行前检查是否已经生成过相同参数的操作记录,如果存在就返回已有结果而不是重复执行。这个设计在Agent自动重试场景下能避免很多麻烦,尤其是模型在超时后自动重试时,幂等性可以防止重复下单、重复发信这类事故。
我个人的体会是,做Agent技能系统,最难的从来不是代码,而是想清楚“边界”——技能与业务逻辑的边界、Agent与用户的边界、自动化与安全之间的边界。一个技能系统能走多远,往往取决于这几个边界有没有划清楚。如果你也正在做类似的项目,我建议先从一两个最基础的文件操作技能开始,把注册、调用、回传、失败的整条链路跑通,再逐步扩展技能库。这套路看起来慢,但后面加技能的时候是真的省心。