Agent技能体系设计:从工具调用到可编排的工作流实战

发布时间:2026/10/14 8:26:02
Agent技能体系设计:从工具调用到可编排的工作流实战 做Agent开发这段时间最大的感悟就是别急着让模型变“聪明”先让它的手够长。“agent-skills”这名字听起来像个玩具项目实际做的却是一件很实在的事——把一套散落在脚本和提示词里的工具调用能力整理成可注册、可编排、可迭代的技能体系。这篇博文就围绕这个项目的设计思路、核心实现和落地时踩过的坑展开给正在做AI Agent、工作流编排或工具调用的朋友一些可复用的参考。1. 项目整体思路与设计拆解1.1 为什么Agent需要一套“技能”体系早期做Agent最常见的方式是把一堆函数塞给模型函数描述写得越详细越好。但随着工具数量增长问题很快暴露一个Agent身上挂了七八十个工具模型每次推理都要读完那几千字的工具说明响应变慢调用错误率也上去了。更麻烦的是工具之间还有依赖关系——比如你要调用“查天气”的API得先拿到用户当前城市而城市可能来自定位、也可能来自历史会话。这种链路没法靠简单罗列函数实现。“agent-skills”这个名字里的“skills”不是指模型能力而是指可复用、可组合、可独立迭代的工具单元。做这个项目的初衷是把工具的“使用说明”和“执行逻辑”解耦成独立模块每个技能有自己的名称、描述、入参定义、触发条件和执行函数。系统层面维护一个技能注册表运行时分配合适技能给Agent调用并且支持技能的动态加载和编排。这套思路如果展开其实解决的是三个层面的问题认知负担模型不需要知道所有技能只用感知当前场景需要的少数技能。注册表里有一百个技能但当一个任务是“整理会议纪要”注入到提示词里的可能只需要三到五个相关技能描述。工程维护没有技能体系时加一个新功能要改主程序的判断逻辑有技能体系后每项技能就是一个独立文件新增功能就是新增目录和注册配置连主进程都不用重启。复用与协作技能是独立模块就自然形成了一种“内部开源”的生态——团队里做数据分析的人写好“Excel处理技能”做爬虫的人写好“网页抓取技能”彼此可以直接挂载不用互相读代码。1.2 技能架构的三层模型这个项目的架构可以拆成三层每层职责相对单一第一层意图识别层这一层的任务是判断用户请求应该触发哪个技能。我采用的是“关键词预筛 模型决策”的双通道方案关键词预筛比较浅但能快速收敛范围。用户提到“发票”“报销”系统先去技能注册表里查一下哪些技能描述里含这些词把候选集缩到十个以内。模型决策把候选技能的“一句话简介”塞给模型让它选出最合适的。因为候选少模型不容易迷糊响应也快。这一层很关键处理不好后面全是隐忧。之前见过直接拿全部技能描述给模型做意图识别的方案测试时技能只有二十个模型还能应付到了六十个技能识别准确率直接掉了十几个点。缩小候选集这一步本质上是在降低模型决策空间的复杂度。第二层技能执行层技能执行层是每个技能的内部实现。一个技能一般包含五部分元信息名称、版本、作者、描述文本给模型看、入参Schema定义参数类型和约束、前置校验函数确认当前上下文是否满足执行条件、核心执行函数真正做事。例如“发送邮件”技能元信息send_emailv1.2.0作者某开发者描述文本用于发送电子邮件支持普通文本和HTML内容可添加多个附件需要在调用前确认用户已经授权邮箱绑定入参Schema收件人数组必填、主题字符串必填、正文字符串可选、附件路径数组可选前置校验检查当前环境里是否已配置邮箱服务地址核心执行调用邮件服务端接口发送邮件并返回消息ID第三层编排层单技能无法覆盖的复杂任务由编排层把它们串起来。比如“做一份销售周报”可能需要三个技能查数据库数据提取技能、做统计数据分析技能、生成图表可视化技能。编排层提供两种工作方式链式编排技能A的输出作为技能B的输入写死在流程模板里。动态编排模型自己决定调哪个技能、按什么顺序调。这种方式灵活但不稳定我会在后文详细说坑在哪里。完整起见这三层对应着项目里的三个核心目录agent-skills/ ├── registry/ # 技能注册中心维护全部技能索引 ├── skills/ # 技能本体每个技能一个子目录 └── orchestrator/ # 编排引擎负责流程串联我在项目里把这套结构叫“口袋里的乐高”——每块积木独立成型拼哪块、怎么拼由编排层决定。1.3 设计取舍通用技能与垂直技能的平衡技能体系的另一个关键决策是通用技能和垂直技能的配比。我的实践经验是前期尽量做垂直后期再沉淀通用。刚开始做Agent时容易掉进“通用陷阱”——想做一个全知全能的上网技能结果每个使用场景都差一点意思。真正好用的技能往往是为特定场景深度打磨的。举例来说通用版“网页内容提取技能”输入URL输出正文文本。听起来没问题实际会遇到反爬、动态页面、登录墙各种破解难题。垂直版“某内部文档站内容提取技能”输入文章ID输出规范的Markdown。因为场景明确、接口固定一次实现就可以长期稳定使用。垂直技能多了以后从中抽取共同的逻辑比如把“内容抓取后净化处理”抽象成公共组件再形成通用技能。这个路径比较稳健也容易被团队接受。反过来假设上来就做通用技能会导致什么问题要么因为过度设计迟迟无法交付要么因为要适配各种场景而接口复杂技能本身的调用门槛变高模型反而不容易用好。这就像你给一个新人一堆万能工具箱不如先给他三个专门干活的工具。2. 核心细节解析与实操要点2.1 如何写好技能描述——让模型“看得懂”是门手艺技能描述是整个体系里技术含量被严重低估的一环。描述不是给用户看的是给模型看的。模型靠描述决定“该不该调这个技能”“参数怎么填”“什么时候不该用”。描述写得差轻则模型瞎调重则参数错误率高到没法上线。我总结的技能描述写法有几个硬性要求一句话定位前半句说明技能做什么后半句说明最典型的使用场景。写清不适用场景明确告诉模型什么时候不要用。例如地址解析技能的描述里加上“仅适用于结构化地址不适用于自由文本中提到的地名”。给出参数建议不用机械地列出JSON Schema而是用自然语言补充说明当用户说“把文件发给张三”时收件人字段应取“张三”而非“张三的邮箱地址”。说明副作用或时间开销如果技能会发起网络请求或需要用户确认一定要注明否则模型可能会在需要低延迟的对话场景里频繁调用。以“数据库查询技能”为例好的描述是这样技能名: db_query 技能描述: 用于执行结构化数据库的只读查询支持SELECT、只适用于数据分析或信息提取场景。 注意 1. 此技能永远不执行INSERT、UPDATE、DELETE操作。 2. 查询前需要确保参数schemaName和query不为空否则直接返回错误。 3. 如果用户没有指定具体数据库请先调用list_databases技能获取可选库列表。 4. 查询结果可能超过单次返回上限若超过1000行系统会自动分页。这里“永远不执行写操作”不是逻辑上的限制而是提示词层面的心理暗示。真要在代码层面限制还需要在函数里拦截双保险。2.2 技能粒度多大算合适技能粒度是另一个容易被忽视但影响巨大的设计点。粒度过粗比如“处理Excel文件”这一个技能涵盖了读写改转粒度过细比如“打开Excel”“读取Sheet”“写入单元格”分别做三个技能模型调用起来次数变多、错误概率也会上升。我实际用的原则是一次技能调用能完成的闭环动作算一个技能。因为Agent的本质是“决策 行动”每个行动对应一个明确结果。把“读取Excel文件夹目录”“解析特定Sheet数据”“对某列做清洗”合成一个数据导入技能好处是模型只需要决策一次。执行细节藏在函数内部由开发人员控制可控性更高。实操时还发现一个规律如果某个技能经常被和其他技能绑定调用就该考虑把它合并或提供组合快捷方式。例如“获取新邮件”和“解析邮件附件”总是被连着调用我已经默认把它们变成一个技能get_mail_with_attachments节省一次模型决策。2.3 技能注册表与动态加载机制技能注册表是系统的“黄页”。我设计的注册表用JSON维护每个技能对应一个条目{ name: get_mail_with_attachments, version: 2.1.0, author: 某开发者, description: 获取指定邮箱账号的未读邮件及其附件信息适用于邮件处理场景, tags: [mail, attachment], entry: skills/mail/skill.py, schema: skills/mail/schema.json, dependencies: [auth_service, mail_server_client], timeout: 30, enabled: true }动态加载方面项目没有用复杂的插件系统而是借助Python的importlib做了模块热加载。运行时会定期检查技能目录是否有新增或修改如果发现变化就加载新版技能函数。团队协作时一个人提交新技能后系统在几秒内就能生效省去了重启服务、发布上线这些流程。不过动态加载有两个安全点必须注意路径校验不允许加载注册表之外的路径防止有人恶意构造技能路径注入代码。版本切换加载新版本前要保证执行中的旧版本调用结束后才切换避免同一技能出现两个版本并发执行。这些点看着基础少了任何一个都会在生产环境埋雷。2.4 技能间的依赖与冲突管理技能不是孤岛。上游技能的输出经常是下游技能输入的一部分。依赖关系处理不好就像送外卖的不知道店里已经没货到用户楼下才发现问题。我在技能Schema里增加了依赖声明并在注册时校验依赖完整性。执行时用依赖图做拓扑排序保证被依赖的技能先执行。当技能A的结果达到技能B的入参要求后链路才能继续。冲突管理则要更难办一些。同一类能力可能有多个技能实现比如“文本翻译”有基于开源模型的离线版本也有调云服务的在线版本。系统层面用优先级处理默认调用在线版若检测到网络不可用或超时降级到离线版。技能描述里会注明“优先使用在线版网络异常时自动切换离线版”这样模型在决策时也能感知到这种容错逻辑不会因为一次失败就放弃整个任务。3. 实操过程与核心环节实现3.1 环境准备从零搭建一个技能运行框架做这个项目时我用的技术栈很轻量Python 3.10 FastAPI。选择FastAPI的原因很简单——原生支持异步、自动生成OpenAPI文档方便调试。Agent调度核心用的是自研的简单状态机没有引入重型工作流引擎因为早期不需要持久化、审批这些企业级能力。依赖库很少pydantic做入参校验importlib做技能动态加载loguru做日志记录httpx做异步请求一句话能用标准库和轻量库解决的事不急着上框架。这也是我一贯做项目的方式——先把核心链路跑通再考虑要不要换重型工具。目录结构可以保持简洁agent-skills/ ├── main.py # FastAPI入口 技能路由 ├── registry.py # 技能注册与发现 ├── executor.py # 技能执行器加载函数、校验参数、超时控制 ├── orchestrator.py # 编排引擎链式/动态 ├── skills/ # 技能目录 │ ├── mail_reader/ # 邮件读取技能 │ ├── data_analyzer/ # 数据分析技能 │ └── ... └── config.yaml # 全局配置3.2 手写一个技能从“文档解析”开始拿最常用的“文档解析技能”当例子。它的目标是输入一个文件路径和解析格式返回解析后的文本内容。先写Schema定义from pydantic import BaseModel, Field from typing import Literal class DocParseInput(BaseModel): file_path: str Field(..., description待解析文件的绝对路径) parse_type: Literal[text, markdown, 表格] Field( text, description解析格式text为纯文本markdown保留标题列表表格模式提取表格数据 ) need_structure: bool Field( False, description是否同时返回文档结构标题层级默认False ) class DocParseOutput(BaseModel): content: str Field(..., description解析后的文本) structure: dict | None Field(None, description文档结构need_structure为True时返回) error: str | None Field(None, description错误信息)再写技能入口函数async def execute(ctx: SkillContext, args: DocParseInput): if not Path(args.file_path).exists(): return DocParseOutput(content, errorf文件不存在: {args.file_path}) # 根据扩展名选择解析器 if args.file_path.endswith(.pdf): content parse_pdf(args.file_path, args.parse_type) elif args.file_path.endswith(.docx): content parse_docx(args.file_path, args.parse_type) else: content read_plain_text(args.file_path) if args.need_structure: struct extract_structure(content) return DocParseOutput(contentcontent, structurestruct) return DocParseOutput(contentcontent)这段代码里最需要注意的是解析函数内部处理超大文件的情况。我是用生成器逐块读取超过一定大小后中止返回前N段。不加这个限制一个100MB的PDF能把Agent的内存吃穿这种事我在测试时遇到过不止一次。技能写完后注册表里加上条目。这一步不能漏否则系统根本不知道该技能存在。3.3 编排引擎链式调用与动态决策的组合实现编排层是我花时间最多的一块因为它属于“看起来很简单做起来各种意外”的部分。先说链式编排。它的核心是一个有向无环图每个节点是一个技能边是数据流转关系。实现时用class ChainStep(BaseModel): skill: str input_mapping: dict # 从上游输出到本技能入参的映射关系 def run_chain(chain_id: str, initial_input: dict): chain get_chain_template(chain_id) current_input initial_input final_output None for step in chain.steps: mapped_input map_fields(step.input_mapping, current_input) final_output execute_skill(step.skill, mapped_input) current_input final_output.model_dump() return final_outputinput_mapping最关键它定义了“上一步的哪个字段填到这一步的哪个入参”。就比如数据提取技能输出“rows”字段是列表数据分析技能入参里对应的是“data”字段映射关系就是{data: rows}。字段匹配错了链路就会断。动态编排实现起来就更有意思了。我给模型一个简化版的编排指令你是一个任务拆解助手。下面给出可用的技能列表。请把用户请求拆成步骤列表每个步骤指定技能名和输入参数来源。 请输出JSON数组例如 [ {skill: data_analyzer, args: {source: 上一步.output.rows}}, {skill: chart_generator, args: {data: 上一步.output}} ]这个方案在测试中确实能自动串联多个技能完成任务。不过稳定性只能打七十分——模型偶尔会产生幻觉技能名或漏掉必要步骤。我的对策是加两步校验技能名校验如果模型输出的技能名不在注册表里系统不直接报错而是尝试用相近名称匹配找不到再要求模型重生成。中间结果校验每一步执行完系统会做一次简单结果检查。如果关键字段缺失就中断并返回错误原因。3.4 提示词组装与技能注入Agent每次调用其实分两层系统层要决定注入哪些技能模型层要基于注入内容做决策。注入策略直接决定响应速度和准确性。最开始的版本是把所有技能描述塞进去结果上下文窗口占用太大模型也容易抓不住重点。后来改成“按需注入”def build_agent_prompt(task: str, skills: list[SkillMeta]) - str: # 先根据关键词预筛 candidates prefilter_by_keywords(task, skills, top_k10) # 再让模型从候选里选3-5个相关技能 selected select_relevant_skills(task, candidates) prompt build_skill_section(selected) return prompt f\n用户请求: {task}这里每一步都有日志方便追踪模型到底选了哪些技能。实测效果比较明显技能总描述从1万多字压到三四千字Agent首字响应时间下降了近一半调用准确率也提升了。需要提醒的是技能描述注入的格式要非常统一用统一的模板包裹否则模型对“技能”的理解会不一致。我自己用Markdown表格和代码块的组合方式效果比较稳定。4. 常见问题与排查技巧实录这一部分都是真实踩过的坑每一节都有对应的教训总结。4.1 技能描述过短导致模型乱调用早期有一个技能描述写得特别简短只有一句话“把用户输入翻译成英文。”结果模型什么都往这里塞用户说“今天天气不错”它也调用翻译技能输出一句英文回给用户用户一脸问号。后来所有技能描述都强制带上场景限制和前提条件乱调用的问题大幅减少。排查这类问题也很简单看日志里哪些技能调用的输入和用户原话语义关联度低就是描述不清的候选者。4.2 上下文窗口被技能说明挤爆有一次项目试运行收到告警上下文token使用量异常高。一排查发现某个模块把十几个技能的完整Schema也塞进了提示词。原因是代码里有一个“获取全部技能详情”的函数被误用于构建Prompt。我的处理是两层在配置里明确“提示词注入只允许使用get_skill_brief()禁止使用get_skill_detail()”。在构建Prompt的函数入口加断言校验注入文本总字符数不超过阈值。这类问题靠人盯很累最好在接口设计上就做隔离把“给模型看的”和“给系统用的”分成两个独立的读写接口。4.3 技能冲突与优先级设计同类型技能多了就可能出现“两个技能都声称能完成任务”的情况。最典型的是“Excel处理技能”和“表格数据清洗技能”任务描述里出现“表格”时模型可能会随机选一个。我从日志里发现同样的任务有时走A技能有时走B技能结果风格还不一样。解决办法是给技能增加“优先级”和“适用条件”字段。比如优先级的取值范围0到100模型决策时如果两个技能都能触发就选优先级高的同时描述里明确边界“任务内容涉及数据统计分析时优先用数据分析技能Excel技能仅做文件层操作。”4.4 性能优化缓存、并行、降级技能调度也要考虑性能。三个优化点是我觉得最实用的缓存同参数技能的执行结果缓存到内存或Redis。比如“获取当前汇率”这种几秒内不会变的数据完全可以走缓存不用每次打外部API。并行编排链路中不存在依赖关系的技能可以并行执行。我把串行循环改成用asyncio.gather处理独立步骤整体耗时能缩短不少。降级外部API超时或报错时自动切换备用方案。比如翻译技能在线版失败后调用离线版不能让用户傻等几分钟然后看到错误。优化要讲究性价比。很多Agent项目死在“过早优化”上——链路还没跑通就开始整缓存、异步、分布式结果复杂度上去了核心问题还没解决。我的原则是先看日志找到真正耗时的环节针对性地优化。4.5 一个易被忽略的细节技能日志的结构化排障速度取决于日志质量。我强烈建议技能执行日志用结构化格式至少包含技能名、版本、调用时间、入参摘要、出参摘要、耗时、错误码。一开始我用的是自由文本日志出问题时只能靠眼睛找效率低。后来改成JSON Lines格式{ts: 2024-01-15T10:00:00Z, skill: doc_parser, ver: 1.2.0, args: {file_path: /tmp/x.pdf, parse_type: markdown}, result: {len: 1523}, dur_ms: 230, error: null}用jq就能快速过滤jq select(.error ! null) skill.log排查效率高太多了。5. 技能评估、迭代与扩展方向5.1 怎么衡量一个技能“够不够好”技能只有用起来、迭代起来才有价值。我给技能定义了三个核心指标指标计算方式健康标准调用准确率模型选择该技能后技能成功完成任务的次数占比 85%平均执行耗时从模型决策到结果返回的时间按技能类型定查询类 3s误用率调用该技能但任务本身不该用它从日志语义判断 10%每个月拉一次数据结合日志定位“准确率高但耗时高”和“误用率高”的技能前者优化执行逻辑后者优化描述文本。5.2 日志驱动的技能迭代闭环做技能迭代时我的流程是从日志里找案例归类问题修改技能描述或执行逻辑回归测试再观察下个月的数据。举个例子某次发现某个技能调用成功率只有70%但执行逻辑没报错。后来看到日志才知道该技能返回值里经常出现“None”模型拿“None”继续往下走链路自然出问题。最后修复方式是在执行函数里增加空值校验和兜底返回一个月后成功率提升到94%。这印证了一件事很多Agent质量问题不是模型的锅是上游数据质量或技能接口设计的问题。技能设计者必须对日志敏感从数据里找线索。5.3 从技能库走向“技能市场”的潜在扩展单个项目的技能库沉淀到一定程度价值就会溢出。我在这套体系之上已经做了一个内部共享页供团队其他人提交自己的技能也可以浏览、评分和试用。这个方向走下去就是一个轻量级的内部技能市场。扩展的考虑还有几点技能评分和反馈机制不只是“好用/不好用”还要能让技能作者看到具体用户反馈。技能组合模板分享把成熟的流程模板固化下来别人一键就能用到自己的场景里。权限和审核机制多人协作时不能只依赖自觉至少要有“提测”和“上线”两个环境的隔离。这些工作已经超出“skills”本身的范畴更像是在搭建一个围绕技能的协作系统。但如果没有扎实的技能抽象做基础上层生态再怎么搭也是空转。写到这里我想多说一句心得做Agent项目与其追求模型“无所不能”不如把精力花在给模型配好一套趁手的技能上。模型本身可以换技能体系却是你和业务深度融合的沉淀也是项目最有护城河价值的部分。如果这篇文章能帮你少走几步弯路那“agent-skills”这个项目也就值了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询