Agent技能编排实战:从提示词失控到可观测的技能注册与调度
我做 agent-skills 这个项目只有一个朴素动机让 AI Agent 学会计调工具而不是把工具逻辑硬塞进提示词里。之前团队里有两个 Agent一个负责写周报一个负责整理客户反馈每次需求一变就得改一版 system prompt模型偶尔还会把工具名写错。后来我抽了三周时间把所有能力按“技能”重新建模形成了现在这套 skill 注册、编排、观测的框架。如果你也在搞 Agent 应用尤其被多工具、长流程、模型乱调工具搞到崩溃这篇文章应该能帮你省不少时间。下面的配置和代码是我在当前版本里实际跑的和官方示例不完全一样更偏个人实践你可以按自己的场景去改。1. agent-skills 解决的不是“写技能”而是“技能失控”1.1 技能散落在 LLM 提示词里的那段时间我没有一开始就做 agent-skills而是先经历了最原始的阶段把工具清单写进 system prompt。当时的结构大概是“你现在是客服助手你可以使用以下工具query_order、refund_order、send_email……”看起来没问题但一旦工具数量超过十个问题就开始密集出现。模型经常在不需要退款的时候调用退款接口或者在用户只是问“能不能退”的时候直接把订单状态改成已退款。最离谱的一次Agent 把查询接口的返回结果当成最终答案发给用户整个链路完全没走业务校验。我后来复盘发现根因不是模型笨而是技能描述不够结构化。提示词里的工具说明太像“功能列表”缺少触发条件、输入约束、执行边界和失败处理模型只能靠上下文猜猜错了就是线上事故。1.2 一次任务串起多个技能时靠人肉编排根本撑不住单技能调用的问题还能靠多写几段 prompt 缓解真正压垮我的是多技能编排。举个例子做一个“客户投诉自动处理”的流程至少需要调用三个技能先解析投诉内容里的订单号再查订单状态最后决定是补发、退款还是转人工。每一步之间都有依赖关系上一步的输出是下一步的输入。一开始我是写 Python 代码硬编排每个技能写成一个函数先调函数 A再调函数 B最后调函数 C。这么做的问题在于编排逻辑和业务逻辑耦合在一个进程里模型根本参与不了“选择下一步”的决策。一旦遇到投诉内容里既没有订单号、又需要查支付记录的情况代码就只能抛异常。agent-skills 要解决的正是这个把技能变成可描述、可注册、可被模型选择的独立单元再用一个薄薄的编排层把它们串成任务流。1.3 agent-skills 的核心抽象技能协议清单可观测性做了几版重构之后我把技能抽象成三部分协议、清单、可观测性。协议就是输入输出 schema告诉模型这个技能在什么条件下可以调用、需要哪些参数、返回什么结构。清单是注册表所有技能启动时统一登记由调度器根据当前任务状态和模型意图去匹配。可观测性则是每次技能调用的日志、耗时、成功与否这决定了后续能不能调优。这套抽象听起来不复杂但真正落地后效果差别很大。因为很多 Agent 框架只给了一个“tool decorator”却没有解决“技能之间如何碰撞”的问题。agent-skills 把技能看成有生命周期的单元而不是冷冰冰的函数指针。模型会先看到“技能说明书”再决定要不要调用调用之后还能看到执行结果然后决定下一步继续调还是收尾。2. 环境准备三个月前我本地跑通的最小配置2.1 依赖选型为什么不直接全用 LangChain很多人问我为什么不用 LangChain 或直接套 Autogen我只说一个原因我需要的不是聊天体而是可控的技能执行链。LangChain 的 Tool 抽象确实不错但它的定位更偏“给模型提供工具”而我希望让技能自己管理状态、校验参数、汇报执行结果。agent-skills 更像一个轻量的技能运行时底层只需要 Python、一个 schema 校验库和一个小型调度器。我的本地环境是三年前的一台 MacBook AirPython 3.11没有 GPU也没有复杂的分布式依赖。核心依赖只有四个pydantic 做参数校验、PyYAML 做技能编排定义、fastapi 可选用来暴露技能 HTTP 接口、以及 agent-skills 自带的核心库。之所以不引入重型向量数据库是因为初期技能数量只有几十个用关键词和语义描述拼接足以完成匹配。2.2 初始化目录结构与两个易错配置项目目录我建议按“一个技能一个文件”的方式组织不要把所有技能堆在一个大文件里。我的目录结构大致是这样agent-skills/ ├── skills/ │ ├── __init__.py │ ├── file_cleaner.py │ ├── order_query.py │ └── weekly_report.py ├── flows/ │ ├── weekly_report.yaml │ └── customer_complaint.yaml ├── registry.json └── main.py有两个易错配置必须提一下。第一个是技能文件里的函数名必须和注册名保持一致否则调度器按名字加载时会直接跳过技能而且不会报错只在运行日志里给一条 warning。我第一次跑就遇到这种情况排查了很久才发现少了一个下划线。第二个是 registry.json 里的版本号要全局唯一同一个技能更新后如果忘记升版本后续会加载到旧缓存导致你改了代码但模型行为没变。2.3 最小示例用 agent-skills 定义一个“清理临时文件”技能我拿“清理临时文件”这个最简单又实用的技能来演示。它的逻辑非常直接扫描指定目录找出超过多少天未修改的 .tmp、.log、.cache 文件并删除返回释放了多少空间。# skills/file_cleaner.py from agent_skills import skill skill( nameclean_temp_files, description( 清理指定目录下的临时文件。触发条件用户提到磁盘空间不足、系统卡顿 或者需要清理 .tmp/.log/.cache 文件时。该技能需要明确的目录路径 目录不存在时直接返回错误。 ), parameters{ path: { type: string, description: 要扫描的目录绝对路径, required: True }, max_age_days: { type: integer, description: 仅删除超过该天数的文件默认 7 天, default: 7, required: False } } ) def clean_temp_files(path: str, max_age_days: int 7): import os, time if not os.path.exists(path): return {success: False, error: 目录不存在} now time.time() deleted [] freed_bytes 0 for filename in os.listdir(path): if not filename.endswith((.tmp, .log, .cache)): continue filepath os.path.join(path, filename) age_days (now - os.path.getmtime(filepath)) / 86400 if age_days max_age_days: size os.path.getsize(filepath) os.remove(filepath) deleted.append(filename) freed_bytes size return {success: True, deleted: deleted, freed_bytes: freed_bytes}这段代码最关键的其实是 description 里的触发条件。它告诉模型“什么时候该用”而不是只告诉它“能干什么”。如果你只写“清理临时文件”模型会在用户提到任意“清理”场景时都尝试调用写了“磁盘空间不足、系统卡顿”这些具体条件之后误调用率明显下降。3. 技能定义与注册机制schema 就是给 Agent 看的“说明书”3.1 技能描述怎么写模型才能稳定选中我在排查了无数次误调用之后总结出了一套描述模板触发条件 能力边界 典型输入 执行结果。举个例子同样是“发送邮件”新手写法是“给用户发送邮件”我的写法是当用户明确要求发送邮件并且已经提供收件人地址、邮件标题和正文内容时调用。 本技能不负责生成邮件内容不负责查询收件人信息。 典型调用收件人aexample.com标题周报正文这是本周工作总结。 返回值为发送成功或失败失败时返回错误码。这段描述看起来啰嗦但对模型来说信息密度很高。它明确了调用边界、不要做什么、典型场景和返回值。尤其是“不负责生成邮件内容”这一句能避免模型只给了主题就调用邮件接口却把正文生成交给另一个不相干的技能。另一个技巧是给参数定义别名。模型从用户对话中抽取参数时经常把“路径”理解成多个不同说法比如“目录”“文件夹”“位置”。与其让模型猜不如在参数描述里把常见别名直接写进去{ path: { type: string, description: 要扫描的目录绝对路径用户可能说‘目录’‘文件夹’‘这个路径’都映射到此参数 } }3.2 参数回退与退出条件技能失败的兜底设计技能不是只要定义了就一定能执行成功。我在实际使用中最常遇到的是参数缺失和参数格式错误。比如清理临时文件这个技能用户可能只说“帮我清理一下”没有给路径。这时候不要让技能直接抛异常而是定义参数回退规则先尝试取当前工作目录再尝试取用户最近打开过的目录最后返回“需要用户补充路径”。agent-skills 里我用了一个轻量的方式每个技能除了正常执行函数还可以定义一个recover函数。它接收当前缺失参数信息返回一个候选参数或直接决定是否终止。在最大重试次数内调度器会自动调用 recover 补齐参数而不是马上把错误抛给用户。退出条件同样重要。很多 Agent 技能跑完就结束哪怕结果明显不合理。我给每个技能增加了一个validate_result钩子比如清理后如果释放空间为 0就认为“执行了但没产生实际效果”这时候调度器会把技能结果标记为“空操作”继续等待用户确认而不是在日志里记一条成功就算完事。3.3 注册表设计的三个决定技能注册表是整个 agent-skills 的神经中枢。我做过三种方案最后选的是第三种。第一种是把所有技能写在一个 Python 字典里简单但无法热更新。第二种是用 SQLite 存储技能元数据灵活但查询复杂对本地项目来说太重。第三种是 registry.json 加启动时自动扫描兼顾可读性和自动发现能力。registry.json 的核心结构长这样{ version: 7, skills: [ { name: clean_temp_files, version: 1.3.0, entry: skills.file_cleaner:clean_temp_files, tags: [file, system, cleanup], enabled: true } ] }注册表设计的三个决定分别是入口地址要写成“模块路径:函数名”而非只写函数名因为这样能避免同名函数冲突enabled 字段必须默认存在用于灰度下线某个技能而不是直接删代码version 要直接体现语义化版本因为技能描述和逻辑改动会直接影响模型行为版本号就是最直观的审计依据。4. 多技能编排从“技能”到“可执行任务流”4.1 编排层到底编排什么有了单个技能之后下一个问题就是怎么把它们串成一个完整流程。很多人会以为编排就是“先执行 A再执行 B最后执行 C”但真实业务几乎都是条件分支和循环。比如“整一周报”可能先要收集本周的 commit、再收集需求文档、根据用户角色决定要不要加入客户反馈、最后统一格式输出。编排层的核心职责有三个一是定义技能之间的依赖关系二是管理执行状态三是决定失败后的替代路径。依赖关系决定了哪些步骤必须串行、哪些可以并行执行状态决定了断点续跑时从哪个技能开始替代路径决定了某个技能失败后是重试、跳到另一个技能、还是直接转人工。我在 agent-skills 里没有引入工作流引擎只用一个 YAML 文件和一个小型状态机。YAML 负责描述状态机负责跑。这样既保证了编排的可读性又避免了为了编排而编排。4.2 一个真实的编排配置查资料-整理摘要-写入知识库我用一个“技术调研助手”的流程来展示编排配置。这个流程要做的事是输入一个主题先搜索内部文档再生成摘要最后把摘要写入知识库。听起来很线性但中间有一层判断。flow: research_digest description: 从内部文档检索主题生成摘要并写入知识库 steps: - id: search_docs skill: internal_search params: query: {user_input.topic} - id: decide_format type: condition if: {search_docs.result.hits_count} 0 then: ask_user_for_more_info - id: generate_summary skill: summarize_text params: content: {search_docs.result.top_hits} - id: save_to_kb skill: write_knowledge_base params: title: {user_input.topic} content: {generate_summary.result.summary}这段配置里有几个点值得注意。{...}是模板占位符执行时会从上一个技能的结果或用户输入中取值。decide_format不是具体技能而是一个条件节点它的作用是拦截空结果避免下游技能收到一个空列表后仍然硬生成摘要。这样设计之后整个流程的可读性比纯 Python 代码高很多业务同事也能看懂每一步在干嘛。4.3 状态持久化与断点续跑多技能流程一旦超过四个步骤就必然要面对中断问题。比如网络断了、某个技能执行超时、或者模型生成过程中上下文长度超限。我最早没有做状态持久化结果一个跑了一半的调研流程只能从头再来。后来我给每个流程实例分配了一个flow_id每执行完一个技能就把当前结果、上下文摘要、下一步索引写到一个本地状态文件里。下一次启动同一个flow_id时直接从记录的下一个技能继续不需要重新执行前面已经成功的步骤。断点续跑还要注意幂等。不是所有技能都能安全地重跑比如“发送通知邮件”这种技能重跑意味着连发两封邮件。所以我在技能定义里增加了一个side_effect字段如果技能会造成不可逆的外部副作用就默认不自动重试而是在状态文件里标记为“需要人工确认”。这个字段在初期很容易忽略等真正出过事故之后你才会意识到它的重要性。5. 实测踩坑这些问题不遇到不算真正用过 agent-skills5.1 技能冲突两个技能都不该触发时模型反而乱选技能数量超过三十个后我遇到的第一类问题是技能冲突。比如“删除文件”和“清理临时文件”技能在描述上高度相似。模型可能在用户说“帮我删点没用的东西”时随机选择其中一个行为差异却很大前者直接删除指定文件后者只删除临时文件。更麻烦的是当技能描述里都包含“清理”“删除”关键词时模型经常跳过真正的触发条件粗暴做关键词匹配。我的解决办法是给技能匹配设计一个简单的评分函数。先对技能描述和当前任务做关键词重合度评分再对触发条件做条件宽松度惩罚重合度接近时优先选择触发条件更严格的技能。实际调试下来这个规则比单纯依赖模型 intent 更稳因为技能的触发条件本来就是人写的严格程度是可以量化的。另外我强烈建议给高风险的技能加一层显式确认。技能执行前如果评分低于阈值调度器会返回一轮“确认请求”让用户确认“你是否要删除这些文件”。刚开始我觉得这个环节多余但真实用户并不会每次都想触发删除类技能确认一步能挡掉很多不可逆操作。5.2 长上下文下的技能“失忆”第二个坑来自长对话。当用户连续聊了二十分钟上下文里塞满了各种历史信息时Agent 会把之前已经调用过的技能信息冲掉。模型可能在前一轮已经执行了“查询订单”后一轮又忘了重复调用同一个查询接口。agent-skills 本身没有魔法解决这个问题但我在上层做了两个补救操作。第一个是每执行完一个关键技能就把结果压缩成一行摘要放回上下文并且用固定格式标记为“已执行过的技能结果”而不是把完整 JSON 全量塞回去。第二个是维护一个“近期技能调用记录”在每次需要决策时把它和当前对话拼接再交给模型。这样模型能看到最近三次操作不容易重复触发。压缩摘要时要注意保留关键数字和状态。比如查询订单的结果可以压缩为“订单 ORD123 状态已发货物流单号 SF123456”而不是把整个订单对象都留在上下文里。这样既保留了决策所需的信息又避免上下文被无效字段占满。5.3 技能执行的验收口径一次通过率 vs 最终成功率最后这个坑不是技术问题而是评估问题。早期我只看“最终成功率”也就是不管中间失败了多少次只要最后任务完成了就算成功。后来发现这个指标会把系统的问题掩盖掉因为很多失败其实是被重试和用户手动修正兜住了。我开始同时记录两个指标一次通过率和最终成功率。一次通过率指的是从发起任务到结束中间没有经过任何重试、修正、人工介入。最终成功率则允许有重试和人工修正。两者差距越大说明技能定义和编排设计越粗糙。我给自己定的优化目标是核心流程的一次通过率不低于 80%最终成功率不低于 95%。如果某项技能的一次通过率一直上不去我会先检查是不是描述写得太模糊再检查参数校验是否太严格最后才考虑是不是模型能力不够。这个排错顺序帮我避免了很多无效调参。6. 再往前一步技能库的团队共享与动态更新6.1 把技能导出成独立包agent-skills 跑到后期我不满足于自己一个人维护技能团队里其他人也想复用。这时候就需要把技能从单个文件变成可安装的独立包。我的做法是给每个技能增加一个pack.sh脚本把技能代码、schema、示例、版本号打包成一个 Python package发布到内部 PyPI 源。独立包的好处是隔离依赖。比如“发送企业微信通知”这个技能需要requests“解析 PDF”这个技能需要pymupdf如果都装在一个环境里版本冲突迟早会来。拆包之后每个技能可以声明自己的依赖由 agent-skills 运行时按需加载。发布流程我也尽量自动化push 代码到 main 分支后触发 CI自动跑一遍技能自带的测试用例通过后自动打 tag 并发布。这样团队其他人只需要在技能的requirements.txt里加一行包名就能在本地直接使用同一个技能而不用关心底层实现。6.2 热加载机制与灰度规则技能更新频率高起来之后我不可能每次改动都重启整个 Agent 服务。所以我给 agent-skills 加了一个文件监听模块当某个技能文件或 registry.json 发生变化时自动重新加载对应技能。但热加载一定要配合灰度规则使用。我踩过一次坑某个技能新版本描述写错了导致模型疯狂误调用线上流程直接受影响。后来我在技能注册表里增加了enabled_ratio字段默认 1.0发布新版本时可以先设为 0.1只有 10% 的请求会使用新技能定义其余走旧版本。观察一个小时后没有异常再逐步放量到 100%。灰度期间还要保留旧技能的日志。这里我推荐一个简单做法每次技能调用都记录skill_version字段后面分析日志时直接按版本分组对比一次通过率谁好谁坏一目了然。6.3 写在最后一句掏心窝的提醒如果你正准备在项目里引入 agent-skills我的建议是别一上来就追求完善的平台化。先用最少量的技能比如三五个跑通一条真实业务链路把注册、调用、日志闭环做出来再慢慢扩展。技能太多但编排不成熟的时候模型反而会更容易选错。我个人最大的体会是技能描述写得越细模型的表现越稳。很多问题看起来像是模型不够聪明实际上是我们给技能的说明书写得太粗。把时间花在 schema、触发条件和边界定义上比换更大参数的模型更值得。agent-skills 这个名字听起来很专业但本质上它只是把“让模型会干活”这件事变得可维护、可度量、可迭代。你不需要一次性做成完美平台先用一个最小闭环跑起来后面的优化会自然找上门。