Agent技能系统设计实战:从工具调用到稳定落地
发布时间:2026/9/26 23:55:48来源:尧图网络
写这次的项目复盘我犹豫了挺久。不是因为它复杂而是因为“agent-skills”这个方向太容易被讲成概念科普。但我想聊的其实是另一件事一个真正能跑起来的技能系统应该怎么设计、怎么落地、怎么在真实业务里不翻车。这个项目我从零搭了一遍中间踩了不少坑也推翻过几次方案。这篇文章就把整个思考过程和实操细节都摊开说清楚适合正在做Agent应用、想给智能体加技能体系或者单纯对工具调用机制感兴趣的人。1. 项目背景为什么要单独做一套“技能库”1.1 从“一个Agent干所有事”到“一组技能件”先交代一下背景。我手上的业务场景是做一个面向内部运营团队的智能助手最初版本就是一个大模型接上几个API让它帮忙查数据、发通知、写周报。刚开始效果还行但随着需求变多问题很快暴露出来每加一个新功能都要改主流程代码模型经常把参数理解错不同场景下的调用逻辑互相纠缠改一处崩一片。后来我意识到问题不在模型而在架构。大模型本质上是一个推理引擎它不应该也不需要知道每个业务功能的实现细节。它只需要知道“在什么情况下、用什么参数、调用哪个能力”至于这个能力内部怎么执行应该由独立的模块去负责。这个模块就是技能Skill而把这些技能组织起来、统一管理、对外暴露给Agent调用的整套体系就是agent-skills。这个思路类似于把一个大而全的机器人拆成一个个可以独立维护的小工具。每个工具只做一件事但做得足够好Agent通过描述信息就能知道该用哪个然后按约定的格式调用。1.2 Agent Skills解决了哪三类痛点我整理了一下这套方案主要解决了我们在实际开发中遇到的三个痛点你们可以对照看看是不是也踩过类似的坑。第一是职责混乱问题。早期版本里业务逻辑、Prompt模板、工具调用全都揉在Agent主循环里每一次功能迭代都要动核心代码风险极高。把技能独立出来之后主循环只负责“决策”技能模块只负责“执行”责权清晰改动隔离。第二是模型理解偏差问题。我们最开始给模型的工具描述写得很随意经常一句话带过结果模型频繁选错工具或填错参数。后来我们把技能描述当成接口文档来写包含触发条件、参数规则、注意事项、典型示例模型的选型准确率提升非常明显。第三是能力复用问题。不同业务线都需要“查数据”这个能力但底层数据源不同、返回格式不同。如果没有技能抽象层每个业务线都得单独接入一遍重复代码一大堆。有了技能层之后接入方只需要适配统一的输入输出格式底层实现隔离各用各的。1.3 Skills、Tools、Function Calling之间的关系这里想顺便说清楚三个容易混淆的概念。Function Calling是大模型API提供的一种能力它让模型在回答中输出一个结构化的调用请求Tools是Function Calling的具体描述单元告诉模型有哪些函数可以调Skills则是一个更上层的概念它把函数的描述、实现、依赖、校验、回退逻辑整个打包成一个完整的功能单元。用个类比来解释Tools相当于菜单上的菜名Function Calling是服务员记下你点了什么菜Skills则是后厨里那道菜完整的做法和食材清单。菜单可以写得很简单但真正把菜做出来靠的是后厨的整套流程。2. 技能系统的核心设计思路2.1 技能描述Skill Description是命根子如果让我只说一条设计经验那就是技能描述的质量直接决定Agent调用的准确率比代码实现本身还重要。很多人在设计技能时把大量精力花在实现逻辑上描述则草草写几句。但实际跑下来你会发现模型毕竟是模型它只能通过文本来理解你的技能是干什么的。描述写得模糊它就只能靠猜。我们项目中每个技能的描述description至少包含五块内容功能概述一句话说明这个技能干什么、触发场景什么情况下应该调用它、参数说明每个参数的类型、取值范围、默认值、典型示例一个完整的调用示例、注意事项比如参数之间的依赖关系、需要避开的坑。举个例子我们要做一个“查询员工信息”的技能。如果描述只写“查询员工信息”模型根本不知道参数怎么填。写成下面这样就靠谱得多{ name: query_employee_info, description: 根据姓名或工号查询员工基本信息包括部门、职级、入职时间。当用户询问某个员工的信息、联系方式或组织归属时使用。如果同时提供姓名和工号以工号为准。, parameters: { name: { type: string, description: 员工姓名支持模糊匹配例如张可以匹配所有张姓员工 }, employee_id: { type: string, description: 员工工号精确匹配优先于姓名 } }, examples: [ { input: 帮我查一下张三在哪个部门, output: { name: 张三, employee_id: ZHANG001 } } ] }我们后来统计过描述从“一句话版”升级到“结构化完整版”之后技能选型准确率从62%提升到了91%。这个提升幅度说明模型的判断能力其实不差差的是我们有没有给它足够的判断依据。2.2 参数校验和标准化要前置第二个关键设计是把参数校验放在Agent调用技能之前而不是技能内部。什么意思就是说当模型决定调用某个技能并填入参数后我们先用一套独立的校验逻辑检查参数是否合法再决定是否执行。正常的流程是这样Agent输出调用请求进入调度层调度层先做参数格式校验必填参数有没有、类型对不对、取值是否在合法范围内校验通过后技能才会真正执行执行结果返回后再经过一层输出标准化转成Agent方便理解的结构化内容。这样做有两个好处。第一避免脏数据进入业务逻辑很多技能内部的Bug其实都是参数异常导致的第二如果模型填错了参数我们可以在执行前就拦截让Agent重新生成一次请求而不是等技能运行到一半才报错浪费时间和资源。我当时在项目中写了一个轻量校验函数核心逻辑大概是这样def validate_and_coerce(skill_schema: dict, raw_args: dict) - tuple[bool, dict, str]: 校验并标准化参数。 返回: (是否合法, 标准化后的参数, 错误信息) required skill_schema.get(required, []) for field in required: if field not in raw_args or raw_args[field] in (None, ): return False, {}, f缺少必要参数: {field} properties skill_schema.get(properties, {}) coerced {} for key, value in raw_args.items(): if key not in properties: continue expected_type properties[key].get(type, string) if expected_type integer: try: coerced[key] int(value) except (ValueError, TypeError): return False, {}, f参数 {key} 需要整数类型实际得到 {value} elif expected_type array: if not isinstance(value, list): return False, {}, f参数 {key} 需要列表类型实际得到 {value} coerced[key] value else: coerced[key] str(value) return True, coerced, 有了这一层技能的内部实现就简单了很多——进到函数体里的参数一定已经是合法且标准化的。2.3 注册表模式技能的统一管理和发现技能多了之后管理和发现就成了新问题。我们初期把所有技能按文件组织目录结构还算清晰但Agent运行时需要一个统一的机制去感知“有哪些技能可用、每个技能的描述是什么、怎么调用”。这里我用的是注册表模式Registry Pattern。核心逻辑不复杂每个技能模块在加载时把自己的描述信息和执行函数注册到一个全局注册表中Agent启动时遍历注册表把所有技能的描述汇总成Tools列表传给大模型当模型输出调用请求时调度器从注册表找到对应的执行函数并调用。注册表的核心数据结构是名称到技能对象的映射技能对象包含描述元数据和执行入口。这样做的好处是技能之间完全解耦新增技能不需要改动既有技能Agent不需要感知技能实现细节只需要看描述元数据不同的应用可以按需加载不同的技能子集。2.4 技能间通信和组合调用的处理单技能跑通之后下一个问题就是多个技能之间的组合。举个实际场景用户说“帮我把上周的销售数据汇总一下然后生成一份PDF周报发给李经理”。这个需求牵涉到三个技能查数据、生成PDF、发邮件。模型需要先调用查数据技能拿到结果之后调生成PDF最后再调发邮件。听起来像是模型一步步来就行但实际会遇到一个麻烦模型每次调用只能拿到结构化结果这个结果往往是JSON或纯文本。如果查询结果很大比如几千行的销售记录模型根本没法把这个结果原封不动地传给下一个技能——上下文窗口也扛不住传输效率也低。我采用的方案是实现一个轻量的暂存机制每个技能执行后的输出如果体积超过阈值会被存入一个暂存区并返回一个引用ID后续技能如果需要引用前序结果在参数中传入这个ID调度层会自动将它解析为实际数据。class SkillContext: 跨技能的数据暂存区, 避免大对象在上下文里反复传输 def __init__(self): self._store {} def put(self, data) - str: ref_id fref_{uuid.uuid4().hex[:12]} self._store[ref_id] data return ref_id def get(self, ref_id: str): return self._store.get(ref_id) # 使用示例 context SkillContext() def generate_report(sales_data): ref context.put(sales_data) # 此时只需要把 ref 传给 PDF 技能 return {ref_id: ref, data_size: len(sales_data)} def send_email_via_ref(ref_id: str, recipient: str): data context.get(ref_id) # 从暂存区取回数据, 继续处理这套机制相当于给技能之间加了一个“中转仓库”大对象不需要经过模型转发直接在技能之间流转效率和稳定性都好很多。3. 实操实现从零搭一个agent-skills最小闭环3.1 目录结构和模块划分直接上一份我们项目初期的目录结构你们可以参考也可以直接拿来改agent-skills/ ├── main.py # 入口, 初始化Agent和技能注册表 ├── registry.py # 技能注册表核心实现 ├── context.py # 跨技能数据暂存区 ├── skills/ │ ├── __init__.py # 自动导入所有技能模块 │ ├── base.py # 技能基类 │ ├── query_employee.py # 员工信息查询技能 │ ├── send_notice.py # 站内通知技能 │ └── daily_report.py # 日报生成技能 └── examples/ └── demo_usage.py # 演示脚本模块之间有一个明确的依赖方向main依赖registryregistry依赖skillsskills内部互不依赖。这个方向一定要守住否则很快就会变成一团乱麻。3.2 技能执行器的设计我们项目中技能是接口体系和执行体系分离的。注册表里注册的是“技能描述”而真正干活的是“技能执行器”。技能执行器负责跟外部系统打交道——查数据库、调HTTP API、读写文件等等。为了不让外部服务的细节污染Agent的主流程每个技能执行器必须遵守同一份协议入参是一个标准化字典出参是一个标准化字典错误信息也是标准化字符串。技能执行器与技能描述分离带来的直接好处是同一个数据服务可以注册成不同发布范围、不同权限级别的多个技能描述而同一个技能描述也可以在后台切换不同的执行器实现比如从测试API切到生产API。这对项目上线前后的联调和灰度发布特别有用。输出标准化也很关键。我给所有技能定了一个统一返回结构包含状态码、提示消息、数据体和耗时信息。Agent可以根据状态码快速判断结果是成功、失败还是空数据然后决定是继续后续动作还是结束对话也可以把耗时信息拼进上下文帮助模型感知延迟。3.3 Agent侧调度逻辑与核心流程有了技能注册表和执行器协议之后Agent的调度逻辑其实就变得很简单了。我将它浓缩成了一段非常核心的循环你们跑起来就能看到一个最简Agent是怎么工作的。调度逻辑精简单之后Agent变成了这样一套流程def agent_loop(user_input: str, registry, model_fn): messages [{role: user, content: user_input}] for _ in range(MAX_STEPS): tools_desc registry.get_tools_description() response model_fn(messagesmessages, toolstools_desc) # 模型没有要求调用技能, 说明已经可以直接给出最终回答 if not response.get(tool_calls): return response[content] # 一个响应里可能同时请求多个技能调用 for tool_call in response[tool_calls]: skill_name tool_call[function][name] skill_args json.loads(tool_call[function][arguments]) ok, standardized_args, error registry.validate_params(skill_name, skill_args) if not ok: messages.append({ role: tool, tool_call_id: tool_call[id], content: f参数校验失败: {error} }) continue # 查注册表, 找执行器, 真正执行 result registry.execute(skill_name, standardized_args) # 结果回填到对话里, 供模型下一步判断 messages.append({ role: tool, tool_call_id: tool_call[id], content: json.dumps(result, ensure_asciiFalse) }) return 执行步骤过多, 已停止这段代码基本就是整个Agent的骨架。核心设计是模型永远不直接接触任何业务逻辑它只做两件事——根据用户问题判断要不要调用技能以及根据技能返回结果组织最终回复。剩下的体力活全部交给注册表和执行器。我自己实测跑通这套流程大概花了一个晚上的时间。你们如果要从头写重点盯三个点注册表的描述格式要跟大模型的Tools格式兼容、参数校验要闭环失败后要能把错误喂回给模型再试一次、最大步数要设一个合理值比如10防止模型陷入死循环。3.4 与LLM协作的边界划分这里要单独强调一下“什么是Agent做的什么不是Agent做的”因为做这个项目过程中我见过太多团队在这里栽跟头。我定的划分原则是Agent只负责“理解意图”和“编排动作”不负责“执行动作”和“记忆数据”。理解意图是模型根据用户输入判断该调用哪个或哪几个技能编排动作是模型决定调用顺序和参数值。但真正去数据库里查数据、真正去调API发通知、真正生成PDF文件这些都是技能的体力活跟模型没关系。数据暂存也是context的职责模型只是转交了一个ref_id它不需要知道实际数据长什么样。这个边界划清楚之后代码写起来非常舒服。模型侧的逻辑始终很薄技能侧的逻辑也很纯粹——不需要考虑意图理解只需要做好输入校验和数据处理。两边各自演进互不拖累。3.5 文本生成型技能的配置要点除了典型的命令型技能还有一类“文本生成型技能”也值得单独说。这类技能本质上是让模型在特定场景下产出符合固定风格的文本比如自动生成周报、产品文案、会议纪要等。我把这类技能和命令型技能在配置上做了区分命令型技能偏向结构化参数文本生成型技能更看重“风格描述”和“约束条件”。为此我在注册表里预留了自由文本字段专门用来放让模型参考的语气风格和内容边界。例如一个写日报的技能我会在配置里描述需要统计当日完成任务、明日计划、遇阻问题语气要求简洁用要点陈列不使用敬语。这样模型在调用文本生成型技能时生成结果基本不用二次修改。实测下来这类技能的配置成本很低但对输出质量的提升立竿见影。另外提醒一点文本生成型技能虽然走的是模型生成但它同样应该走注册表和参数校验的流程不要图省事直接拼接提示词。只有把这类技能也当作一等公民纳入统一管理后续的审计、回退、效果统计才能全面覆盖。4. 常见问题和排查技巧实录4.1 模型总把参数传偏怎么办这是我们在项目里遇到最多的一个问题尤其是在模型版本升级之后参数理解行为会有波动明明之前还正常的场景突然就传错了。排查思路是先确认是“描述不清晰”还是“参数太复杂”。如果是描述问题就按前文说的五要素补齐描述内容特别是Examples部分要尽量覆盖真实场景。如果是参数太复杂比如一个技能有七八个参数同时存在依赖关系那就应该拆技能而不是指望模型自己推理。我一般建议一个技能的参数不要超过五个而且尽量去掉非必填参数。非必填参数越多模型的选择压力越大就越容易出错。如果实在没法避免就给非必填参数设置合理的默认值并在描述里明确“能不用就不用”。4.2 技能执行超时但模型还在等这个问题是这样的技能调用的某个外部API响应很慢比如内部数据分析服务偶尔要跑十几秒才能返回。Agent侧如果设置了十秒超时API还没回来技能就已经报错了。但模型在下一步还是继续等一个完整的结果导致整个对话卡在那。我们的解法是给每个技能专门设计一个“超时反馈”分支当技能判断当前执行可能要超时时主动返回“任务超时但系统还会继续在后台重试”的特殊结果。模型收到这个结果后就不会干等而是走一条独立的重试逻辑或者给用户一个明确提示而不是无限挂起。另一个实用技巧是给技能执行加上同步异步分流需要快速响应的查询类技能走同步调用耗时的数据汇聚类技能直接丢到异步任务队列。Agent先回复一个“已开始处理”等后台跑完再通知。这套模式适配长耗时场景非常有效我们后来几乎所有数据类技能都切到了异步方案。4.3 两个技能职责重叠导致选择混乱当技能数量超过十几个之后一定会出现职责重叠的情况。比如我们有一个“查员工信息”技能和一个“查组织架构”技能两者都能回答“某某在哪个部门”这个问题区别只是返回信息的详细程度不同。模型经常选错。解决思路是重新划分技能边界让技能之间的职责尽量正交。后来我把“查员工信息”定位为“只看单人的基本信息”把“查组织架构”定位为“看部门和汇报关系”并在描述中明确标注各自的使用场景和排他情况。模型选型准确率很快就上来了。如果你不希望频繁改动底层技能也可以考虑加一个上层路由技能也就是一个“metadata技能”专门负责判断哪个技能适合当前请求。但路由依赖模型再走一层会增加额外的调用损耗和出错面我更推荐直接改技能描述。4.4 技能效果的回归测试体系技能系统最容易被忽略的就是回归测试。代码改了一个小地方可能某个调用场景就挂了模型的Prompt微调了语气可能选型逻辑就偏了。我给这个项目搭了一套很轻的回归测试流程每一类技能都保存少量典型输入范例每次改动后自动回放一遍检查技能选择和执行结果是否符合预期。初期靠手工测试后来把回放脚本集成到CI里每次提交代码都自动跑一遍所有技能的全量回测。我这里用了一个分段对比的思路类似于断言包括意图覆盖测试和参数覆盖测试。意图覆盖测试验证的是那些典型问题是否成功命中了预期技能参数覆盖测试验证的是那些典型参数的边界和错误参数是否成功返回可理解的错误信息。两者加在一起能给技能系统上一道基础保险。4.5 数据安全与权限隔离技能系统里的一个大坑是权限隔离尤其是当一个Agent服务于多种角色的时候。比如普通员工和HR看到的数据范围完全不同但技能执行器如果没有权限判断就会把数据泄露出去。安全控制绝对不能只放在前端或代码层要下沉到底层执行器每个技能在入参中必须带上调用者身份标识执行器内部根据身份做数据范围过滤。我在注册表里也为每个技能维护了一个可见等级字段只有调用者权限不低于该等级时才允许执行。这里有一个很实际的教训分享如果技能系统同时被聊天工具、API接口、自动化工作流等多入口调用不能只依赖调用方传来的角色一定要在执行端二次校验。否则一旦某个入口忘记传角色或者传了伪造角色整个权限体系就是形同虚设。5. 后续优化方向和进阶玩法5.1 技能版本管理与回滚随着技能数量增长我们会遇到“升级了一个技能导致其他场景异常”的情况。虽然架构上技能之间已经解耦但业务上是纠缠的——A技能输出格式变了B技能又依赖了它。因此我强烈建议从第一天就建立技能版本管理的意识而不只是保存文件。每个发布版本记录技能代码、描述内容和依赖环境三个层面的快照并支持一键回滚。我用的方案是给注册表的技能描述框架里补上版本号字段和变更原因字段定期归档一个版本签名。回滚这件事如果没有自动备份就是空谈。我在CI流程里加了一步自动打快照的动作每次发布技能库都会先记录当前全部技能描述和执行器代码的哈希值。一旦线上出现异常能迅速回到上一个稳定版本。5.2 动态加载技能不重启Agent进程早期版本的技能注册表是静态的所有技能在启动时一次性注册完成。但业务上有时我们希望某个新技能立即上线不影响正在跑着的Agent服务。后来我把注册表改成了支持动态加载技能包被放到指定目录后系统通过文件监听自动发现并注册新技能整个过程不需要重启进程。这里有一个小技巧为了确保动态加载不出问题每个技能执行器必须遵循纯函数风格不能依赖全局状态。动态加载带来的另一个好处是可以在业务节点上独立做小流量实验先在灰度环境注册一个新技能验证调用准确率和效果指标再决定是否全量推送。这样每次技能上新心里都有底不至于上线后才发现问题。5.3 从单一Agent扩展到多Agent协作技能体系稳定之后下一个自然的需求就是多Agent协作。我最近在尝试的方向是为不同Agent建立不同的技能子集比如数据Agent只能加载数据类技能文案Agent只能加载写作类技能两者通过一个消息总线交换结果。在这种架构下每个Agent都维护一个独立注册表只装入与自身职责相关的技能描述避免上下文被无关技能干扰。交互层则由一个编排Agent统一调度决定哪个子Agent去处理哪一类请求。多Agent的优势是角色隔离清晰、技能上下文短、并发能力强但代价是需要额外维护编排逻辑和线程模型小规模团队需要权衡成本和收益。5.4 技能效果的数据反馈闭环这个优化方向是我认为最值得投入的让技能系统通过调用数据自我进化。我在每次技能调用时都会记录完整的入参、出参、耗时、是否成功、模型选型置信度等信息沉淀成一张技能调用日志表。有了这张表我们就能客观分析哪些技能的调用频率最高哪些技能经常被模型选了但执行结果没人点开看哪些技能频繁因为参数校验失败而返工。下一步是让技能库自动报告这些数据并推荐修改描述或合并技能的方向。虽然现在还没有完全做到自动闭环但数据驱动的思路已经帮我们优化了七八个技能描述让选型准确率又上了一个台阶。这个方向我会继续做下去。6. 最后的经验总结与个人心得这个agent-skills项目做到现在我自己最有感触的一点是真正值钱的核心不是“让大模型会调用工具”而是“怎么把能力模块化、标准化、安全地组织起来让模型和业务系统像齿轮一样咬合”。技能描述、参数校验、执行器协议、注册表这些听起来很基础的东西恰恰是最影响线上稳定性和开发效率的。如果你正准备做一个Agent应用我的建议很直接不要一上来就追求复杂的框架或炫酷的多智能体编排先踏踏实实把技能注册表、校验层和执行器协议这三件套做好。等基础扎实了模型选型准确率上去了再考虑异步化、动态加载、多Agent协作这些进阶能力。地基稳楼才不会塌。如果你们项目里也遇到过类似的问题或者有更好玩的技能设计思路欢迎在评论区聊聊。后续我会再整理一篇关于技能调用数据分析和自动优化描述的文章把这次项目中数据驱动优化那部分再展开讲讲。
网站建设高端定制企业官网