ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从工具到技能:构建Agent技能系统的完整实战指南

从工具到技能:构建Agent技能系统的完整实战指南 1. 为什么Agent需要“技能”而不只是“工具”这两年做大模型应用我踩过最大的坑就是所有人一上来就怼着Function Calling写代码把一堆工具函数塞给模型然后指望它“智能地”完成复杂任务。结果你也猜到了——模型确实会调用工具但它更像一个拿着瑞士军刀却不知道要先开哪把刀的人东戳一下西戳一下流程走得无比曲折最后还得靠人来兜底。后来我把思路从“给Agent一堆工具”转成“给Agent一套技能”整个局面才打开。这里说的技能英文就是agent-skills指的是把大模型的通用能力和外部工具、业务逻辑、决策路径封装成一个可复用的、面向特定任务的原子能力单元。它和工具的区别我不妨用做饭类比工具是“一把菜刀”你给它它知道能切菜但不知道今天要做什么菜。技能是“一道菜的标准做法”它包含了用什么刀、切什么形状、什么时候下锅、放多少盐、怎么装盘是一个完整的行为闭环。工具解决的是“能做什么”技能解决的是“该怎么做且做成”。这个区分在真实业务里极其关键。我见过很多团队把几十个工具函数扔给Agent结果模型不知道该在什么时机选哪一个上下文里堆满了无关的函数定义Token消耗翻了几倍效果反而变差。而技能化之后每个技能像一位经验丰富的员工你有需求就直接点名“你来处理这个”它自己知道内部该调研、该调库、该回退、该汇报模型不需要替它规划每一步。这篇文章面向的人我默认是在做AI Agent产品、想把LLM真正落进业务流程里的工程师或产品负责人。如果你还处在“调API、写Prompt”的阶段这篇文章也能让你少走半年弯路。2. 技能系统的整体设计思路2.1 技能注册表让Agent知道自己“会什么”设计一套技能系统第一步不是写技能而是写注册表。什么是一个技能它至少包含以下元信息技能名称机器可读的唯一标识比如security_incident_triage自然语言描述说明这个技能在什么场景下用、解决什么问题、大概怎么做输入参数 Schema描述调用它需要哪些字段每个字段的类型、含义、约束执行主体技能内部跑的是代码、是另一个模型、还是调外部API要在这里声明退出条件与输出格式什么算成功、有没有中途退出分支、结果以什么结构返回我见过不少团队忽略注册表直接在代码里写一堆技能函数然后让Agent靠函数名猜用途。这等于把字典撕了让外国人猜中文词的含义效果当然不稳定。我自己的实现习惯是用一个Python字典或者JSON文件集中维护所有技能注册项同时生成一份浓缩版的“技能总览”在每一轮Agent对话前注入到系统提示里。注意这里是“浓缩版”不是把几十个技能的完整描述全塞进去那样上下文就废了。举个例子浓缩版技能总览大概是这样的可用技能 1. security_incident_triage安全事件分级研判输入告警详情输出威胁等级与处理建议。 2. log_analyzer日志异常检测输入日志文件路径与分析目标输出异常摘要与关联指标。 3. memory_searcher基于内部知识库的语义检索输入查询语句输出最相关的3-5条内容。这段总览控制在200字以内目的是让模型“知道有这个技能”而不是“学会怎么用”。具体怎么用模型会在需要时再去看该技能的完整描述。2.2 输入输出契约约束比能力更重要我做了这么多技能最深的体会是一个技能好不好用七成功夫在输入输出契约上三成功夫在实现逻辑上。什么是契约就是输入参数 Schema 和输出格式定义。大模型天生擅长“自由发挥”所以契约里写清楚“什么必须传”“什么可选”“取值范围是什么”“输出字段的语义是什么”是防止模型乱来最有效的手段。比如一个log_analyzer技能输入可以设计成{ log_path: {type: string, required: true, description: 日志文件绝对路径}, time_range: {type: string, required: false, default: 1h, description: 分析时间范围如15m/1h/24h}, focus: {type: string, required: false, description: 分析重点如error/performance/security} }输出设计成{ summary: {type: string, description: 一句话总结}, anomalies: {type: array, items: {type: object}, description: 异常事件列表}, suggested_actions: {type: array, items: {type: string}, description: 建议措施} }为什么要这么较真因为Agent在运行过程中可能会把它从用户那里听来的模糊需求拆解成技能调用参数。如果契约清晰模型拆解成功率就高如果契约含糊模型就会自创参数然后技能函数一执行就报错整个Agent就卡死了。2.3 路由与编排Agent怎么决定用哪个技能技能系统里还有一个绕不开的问题谁来决定当前该用哪个技能我试过三种方案第一种纯靠大模型判断。在系统提示里给技能总览让模型自己在推理过程中选择技能。优点是灵活缺点是模型偶尔会选错特别是两个技能描述相近的时候。第二种规则优先。先写一套关键词/正则规则表命中规则就走对应技能没命中就交给模型判断。优点是可控、省Token缺点是规则覆盖不全长期维护成本高。第三种混合路由。把技能路由拆成两步第一步用轻量规则做预筛比如用户消息里含“报警”“入侵”就直接走安全事件技能第二步把候选技能列表缩小到2-3个再交给模型做最终选择。我实测下来第三种最稳。预筛能挡掉80%的模糊情况模型只在少数场景下拍板既省成本又不容易犯错。路由决策的结果一定要记录下来。我会把所有“用户请求 → 路由选项 → 最终选择 → 执行结果”存成结构化日志定期回放看哪些请求被错误路由然后针对性优化规则表或技能描述。这一步决定了你的技能系统能不能越用越准。3. 核心环节实现从零搭建一个可用的技能框架3.1 技能注册与调度的骨架代码说再多理论不如直接看代码。下面是我在实际项目里用的一个极简技能框架你去掉了业务细节保留了核心骨架。这个框架的核心思路是用装饰器注册技能统一维护技能表然后给模型一个统一的调度接口。# skills_registry.py from dataclasses import dataclass, field from typing import Callable, Any, Optional import inspect import json dataclass class Skill: name: str description: str parameters_schema: dict func: Callable enabled: bool True class SkillsRegistry: def __init__(self): self._skills {} def register(self, name, description, parameters_schemaNone): def decorator(func): schema parameters_schema or {} # 从函数签名自动推导必填参数 sig inspect.signature(func) for param_name, param in sig.parameters.items(): if param.default is inspect.Parameter.empty: schema.setdefault(properties, {}) schema.setdefault(required, []) if param_name not in schema.get(required, []): schema[required] schema.get(required, []) [param_name] schema[properties][param_name] { type: string, description: f参数 {param_name} } self._skills[name] Skill( namename, descriptiondescription, parameters_schemaschema, funcfunc ) return func return decorator def get(self, name): return self._skills.get(name) def list_skills(self): return [ { name: s.name, description: s.description, parameters: s.parameters_schema } for s in self._skills.values() if s.enabled ] def compact_description(self): return \n.join( f- {s.name}{s.description.split()[0]} for s in self._skills.values() if s.enabled ) registry SkillsRegistry() # skill_demo.py from skills_registry import registry registry.register( namecalculator, description数学计算器用于执行基础四则运算输入表达式输出计算结果。, ) def calculator(expression): # 注意生产环境请使用安全求值方案别直接用eval return eval(expression)这个框架最巧妙的地方是对模型隐藏了所有调用细节。模型只需要在生成结果里输出一个结构化的“技能调用请求”你的调度器再把它翻译成真实的函数调用。具体调度逻辑长这样# dispatcher.py import json def dispatch(skill_name, **kwargs): skill registry.get(skill_name) if not skill: return {error: f技能 {skill_name} 不存在} try: result skill.func(**kwargs) return {result: result} except Exception as e: return {error: str(e)}模型那边你只需要在Prompt里告诉它“当你需要执行某些计算、检索、分析时输出一个JSON格式为{skill: 技能名, args: {参数}}}我会自动替你执行。” 然后你的Agent循环里做一次JSON解析和调度即可。3.2 上下文窗口的管理技巧技能系统跑起来之后你马上会撞上第二个问题上下文窗口不够用了。原因很简单每次模型要调用技能它需要知道有哪些技能、每个技能的参数是什么技能执行完结果又要塞回上下文供模型继续推理来回两三轮几千Token就没了。我的做法是三层上下文管理第一层叫“常驻层”放的是技能浓缩总览也就是前文那个200字以内的列表每轮对话都在。第二层叫“按需拉取层”当模型判断需要用某个技能时再由当前技能控制逻辑把那个技能的完整描述含参数Schema追加到上下文中。这个完全靠代码控制不用让模型自己选。第三层叫“遗忘层”就是每轮对话结束后把已经执行完的技能原始输出压缩成一条摘要原始大段结果从上下文里删除。这个可以用一次轻量LLM调用做摘要成本不高但对上下文省得非常可观。这套三层结构我实测可以把每个会话的上下文消耗降低40%到60%。尤其是那种需要Agent连续调用多个不同技能的复杂任务没有遗忘层根本跑不完整条链路。3.3 技能编排串行、并行与条件分支技能不是孤立的。真实任务里经常是“先检索再分析再生成报告”这种流水线。我把技能编排模式总结成三种基本类型串行A技能的输出作为B技能的输入比如先查日志log_analyzer再把日志摘要给另一个技能做根因分析。这个适合流程确定的场景用代码写死就行别让模型自由发挥顺序。并行多个互相独立的技能同时执行比如同时检索外部威胁情报、查内部知识库、读告警上下文。我的实现是用asyncio.gather并行跑能明显缩短整体耗时因为LLM串行调技能实在太慢了。条件分支根据中间结果决定下一步。这种我用一个“决策技能”处理它本身会输出“下一步该调哪个技能”相当于用模型能力做动态编排。这里有个我自己踩过的坑并行调用多个技能时不要一次性把所有技能描述塞进上下文让模型自己选。正确做法是先把相关上下文准备好再让模型做一次小的决策否则上下文会爆决策质量也会急剧下降。4. 一个真实技能的设计全过程事件追踪技能实战4.1 需求场景与技能定义理论讲完了我拿一个实际技能的例子走一遍完整设计流程。这个技能叫spike直译是“尖峰”在SRE语境里指快速侦察、定位一次线上问题。背景是这样我维护的一个客服机器人用户反馈某时段回答质量骤降。我需要一个Agent技能能自动查看最近告警、拉取服务日志、找到变化点然后输出一个“最可能的根因假设”。技能定义如下registry.register( namespike, description快速侦查一次线上异常拉取相关日志、指标与变更记录输出最可能的根因假设。, parameters_schema{ type: object, properties: { incident_time: {type: string, description: 异常发生时间ISO格式如2025-01-15T14:30:00Z}, service: {type: string, description: 目标服务名}, symptom: {type: string, description: 用户可见的异常表现} }, required: [incident_time, service] } ) def spike(incident_time, service, symptom): steps [] # 1. 拉取该时段错误日志 logs query_logs(service, incident_time, levelerror) steps.append(f获取错误日志 {len(logs)} 条) # 2. 拉取关键指标变化 metrics query_metrics(service, incident_time) steps.append(f获取指标序列 {len(metrics)} 个) # 3. 查找变更记录 changes query_changes(service, incident_time) steps.append(f获取变更记录 {len(changes)} 条) # 4. 本地优先级排序返回假设 hypothesis prioritize_hypothesis(logs, metrics, changes, symptom) return { steps: steps, logs_sample: logs[:5], changes: changes, hypothesis: hypothesis }这个技能好在哪好在它把“调研”这种模糊要求变成了一段确定性流程查日志、查指标、查变更、生成假设。模型不需要自己想先干啥再干啥它只要把时间和服务名给我剩下的我来。4.2 每一个技能都应该有成本预算做技能还有一个容易忽略的维度成本。一个技能内部可能要做五次外部API调用、两次LLM推理、一次数据库查询这些叠加起来是很大的开销。所以我在技能注册表里给每个技能加了一个cost_budget字段比如spike技能预算20个成本单位超过就直接停止追加查询返回已有结果。用法很简单技能函数内部每做一次外部调用就把耗时和预估Token消耗累加进一个计数器超过阈值就提前收尾。这样系统整体的成本是可控可预测的不会出现一次Agent任务烧掉几块钱的情况。实际运营中我按月统计每个技能的平均调用成本成本异常高的技能会被拎出来重新审视逻辑。比如有一个序列化技能一开始每个调用都要模型生成两轮后来改成一次性生成成本直接降了35%。4.3 技能版本管理与灰度发布技能也是代码技能逻辑改了之后你不能指望线上立刻生效还不出事。我吃过一次亏把一个技能的内部Prompt改了结果没走全量验证线上Agent行为突然变了用户反馈一堆问题。现在的做法是给每个技能加上版本号在技能执行结果里带上version字段然后支持版本灰度新技能版本先在测试环境跑一套预置case集要求全部通过才能上灰度灰度期只放10%的流量重点看错误率和执行成功率连续稳定运行48小时后逐渐放大到全量技能版本管理的核心是跟上一次行为有差异时你必须能快速回滚。所以我从不直接覆盖线上技能函数而是保留前一个版本的可执行代码切换起来只是改一个配置项的事。5. 常见问题与排查技巧实录技能系统跑久了各种奇怪问题都出过。这里挑几个典型的写成速查表碰到类似问题时能少走弯路。症状可能原因排查顺序与解法Agent不调用任何技能只靠对话硬答技能总览描述太模糊模型没意识到该“动手”先看技能名称和描述里是否包含问题域关键词再检查总览是否被后续上下文挤出窗口最后在总览末尾加一句“如需查询/分析请调用对应技能”技能调用参数三番五次传错参数Schema语义不清楚或参数名太像在Schema的description里给一个示例值把易混淆的字段改名在调度层做参数校验不合法直接返回错误信息让模型重试技能执行超时Agent挂起技能内部外部调用没有设置超时和重试上限给每次外部调用加timeout建议5秒重试最多2次技能内总执行时间超过30秒直接收尾返回部分结果技能产生的结果格式不稳定输出定义里缺少“类型约束”说明在输出Schema里明确字段类型、枚举值、必填项在技能函数返回前做一次格式校验不合法就转成通用错误Agent始终选择同一个技能某个技能描述太宽泛或排在最前面被模型偏爱把技能描述改得更具体突出它专属的场景调整技能注册顺序对部分模型有影响但治本还是靠更精确的描述上下文越长技能调用准确率越低上下文里太多无关历史干扰模型判断启用第3.2节的“遗忘层”策略把历史技能输出压缩成摘要保持技能总览固定位置不被其他上下文挤掉这几个问题里最隐蔽的是第一个。模型不调用技能很多时候不是模型笨而是你的技能总览写得太“业务”了模型根本看不出这个技能跟当前问题有关。我后来养成的习惯是每个技能描述里必须包含“在什么情况下使用”和“在什么情况下不该使用”这个双向约束对模型决策帮助极大。另一个隐蔽问题是技能函数的副作用。有一次我写了一个技能内部会写数据库结果测试时模型连着调用了同一个技能三次数据库被写了三条重复记录。现在我的原则是默认技能设计为无副作用需要写数据的一律单独建一个“写技能”并且每次执行前要向用户或主流程做一次确认。关于模型选型我也多说一句技能系统对模型能力的要求其实不高关键是稳定。当前很多中档模型只要技能描述足够清晰就能稳定完成路由和参数抽取。真正贵的模型反而容易“发散”给技能编排加很多花活所以我更倾向于用响应快、输出稳定的模型跑技能调度只在最后生成面向用户的结果时才调用更强力的模型。再分享一个排查工具习惯我会给每个技能执行加一条日志记录“技能名”、“参数摘要”、“耗时”、“结果状态”、“调用来源”。这样出了问题一条grep命令就能把整条调用链拉出来定位是哪一步的锅。没有这套日志技能系统基本等于盲人摸象。我个人的体会是技能系统不是一个一次性搭完就结束的工程而是一个需要持续运营、迭代、调参的基础设施。每一轮线上数据的回放、每一次错误案例的分析都在让这套系统更懂你的业务。技能不是越多越好而是越精准越好。一个真正有用的技能往往是靠一次次踩坑才把它打磨成那个“一叫就到、一做就对”的可靠状态。
返回列表