ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent Skills实战指南:从函数调用到技能库的设计与实现

Agent Skills实战指南:从函数调用到技能库的设计与实现 Agent Skills这一两年在AI工程圈里是实打实的热词尤其是做LLM应用的朋友几乎每个技术群里都有人问Agent到底怎么落地Skills和Tools到底有什么区别为什么别人家的Agent能自动拆解任务、自己家的却整天答非所问我自己的团队从去年开始把一套基于大模型的内部助手从“堆函数”改造成“技能库”之后效果提升非常明显——模型不乱调了代码好维护了新同事上手也快了。这篇就聊聊我在这个方向上踩过的坑、总结出的设计原则以及一套可以直接抄作业的最小实现。不管你是刚开始接触Agent还是已经折腾过几轮function calling这篇文章都值得你花十分钟读完。需要先说清楚下面写的所有内容都来自我的实际项目经验不是概念科普。我会尽量把每个设计的“为什么”讲透而不是只丢结论。1. Agent Skills是什么先把它放回它该在的位置1.1 从函数调用到技能抽象这是一段绕不开的演进史最早一批做LLM应用的人一定都经历过“function calling”阶段。开发者把系统里的每个API包装成一个function然后在请求里把这些function的说明全部塞给模型让模型在对话过程中自己决定要不要调用、调用哪一个。我在项目早期也这么干过结果很快翻车。第一步函数一多Prompt就失控了。当时我们接了一个内部数据平台有查询用户、查订单、查库存、创建工单、更新状态、发送通知……粗算一下四十多个function。每个function就算只写一百字的描述这一坨丢进Prompt也占掉好几千token模型光读说明就费了不少力气。第二步函数一多模型的选择准确率直线下降。同一个意图模型有时候选A函数有时候选B函数而且经常把两个高度相似的函数搞混传参更是靠猜。后来社区里逐渐形成了一个共识想让模型在复杂系统里稳定工作就不能让它面对一堆“原子操作”。原因很朴素——模型擅长的是“决定做一件什么事情”而不是“决定调用哪一个底层接口”。于是“技能Skill”这个概念就火了。一个技能不是简单的“函数换了个名字”而是把完成一个任务所需的多步操作、中间判断、异常处理、以及可能需要的LLM推理全部封装成一个整体。比如我的系统里有一个“代码审查”技能它内部做的事情包括读取代码内容、运行静态检查、把代码和检查结果一起交给LLM做深度分析、最后把结果整理成结构化报告。对模型来说它只需要知道“有一个叫code_review的技能当用户想检查代码质量时可以调用它”剩下的事情完全不用它操心。这就是Skills存在的核心逻辑把LLM的决策粒度从“工具操作”提升到“任务意图”。1.2 Agent、Tool、Skill这三者到底是什么关系很多人把Agent、Tool、Skill这三个词混着用但在我个人的理解里它们的分工是明确的Agent决策者。它理解用户意图拆解目标判断下一步该做什么然后在自己的技能列表里选择最合适的一项。Tool原子能力。只做一件事比如“发送一个HTTP请求”“读取某个文件”。它没有业务语义。Skill面向任务的能力封装。一个Skill内部可以调用多个Tool可以有自己的中间状态判断甚至可以内嵌一次独立的LLM推理最后对外输出一个完整的结果。我用一个生活化的类比来解释Tool像螺丝刀、扳手这些零件Skill像一套“换轮胎”的标准作业流程——它会自动选好要哪些工具、按什么顺序用、中途出现螺丝拧不动了怎么办Agent像维修师傅听完车主描述之后判断现在是该换轮胎还是该查刹车然后把对应的Skill调出来执行。这个分层最大的好处是让“最会做选择题”的模型去做选择题让“最怕不确定性”的工程系统去处理确定性的执行细节。模型不需要关心HTTP请求怎么发、文件怎么解析它只需要在有限的几个技能里选一个“最像的”。而技能内部的所有逻辑我们都可以用传统工程手段来保证它稳定可靠。1.3 为什么不是简单堆函数复杂度要转移到工程侧我在和一些做Agent的朋友讨论时发现大家踩过的坑惊人一致凡是“把功能拆得特别碎全丢给模型自己编排”的方案最后都变成了维护灾难。问题出在哪模型编排的每一步都有出错概率。如果让模型自己规划“先查订单再查库存再算运费再调用发货接口”四步下来成功率是四个概率的乘积。比如每一步模型都有90%的概率做对那整体成功率就是65%。而如果你把“查询订单并计算预计发货时间”做成一个技能模型只需要做一次决策成功率可以提到90%以上。所以我的核心观点是能封装进技能里的复杂度就不要留给模型去临场发挥。Agent Skills不是为了花哨而是为了把这套系统的出错率从“乘法”降为“加法”。2. 技能系统设计的底层逻辑2.1 描述文本是给模型看的不是给人看的我刚做第一个技能的时候Description写的是“代码审查”。很短很人类语言结果模型经常在用户问“这段代码跑起来报警了怎么办”的时候不调用它而在用户说“帮我看看代码”的时候才调用。后来我意识到技能的description是给模型做语义匹配用的必须把“触发场景”写进去。现在我的技能描述有一个固定的模板这个技能是干什么的能力边界在什么情况下应该调用它触发条件调用之后会返回什么结果输出形态什么情况下不应该调用它负面排除举个例子我系统里“数据分析”技能的description是这样写的对用户提供的结构化数据文件CSV、Excel进行统计分析、绘制图表并返回分析结论。适用于用户要求查看趋势、对比指标、统计分布、生成报表或“分析一下这个文件”的场景。如果用户只是询问数据分析方法而不涉及具体文件请勿调用此技能。后面那句“请勿调用”特别重要。模型见到“分析”两个字就容易兴奋给它一个否定条件能挡掉很多误触发。另外我强烈建议description里不要写太抽象的形容词比如“高效”“智能”“强大”模型对这类词无感纯属浪费token。把场景、输入、输出写清楚比什么都强。2.2 参数即协议好的参数设计让模型少犯错技能的参数设计本质上是一种“协议设计”。模型不是人它没法理解业务含义你给它什么字段定义它就按什么字段去填。所以参数的约束越清晰模型犯错的可能性就越低。我总结出三个经验第一每个参数必须有明确的type和description。description里最好带一个格式示例。比如一个接收日期范围的参数如果描述只写“日期”模型极有可能传“最近三天”这种模糊值。但如果描述写“日期范围格式为YYYY-MM-DD到YYYY-MM-DD例如2025-01-01到2025-01-31”模型的输出就稳很多。第二减少必填参数的数量。能通过默认值解决的就不要让模型去决定。模型每多做一个决定就多一个出错的机会。比如“代码审查”技能里的language参数我直接默认成“python”只有当用户在请求里明确提到其他语言时模型才需要额外传。第三所有参数都要考虑“模型传了空值或错误值”的情况。handler里必须做容错。模型不会像人一样在填错的时候主动道歉它只会默默把错的东西发给你。2.3 技能版图的组织方式目录、命名与依赖技能多了以后物理组织方式很重要。我目前的项目里技能目录长这样skills/ code_review/ SKILL.md main.py requirements.txt data_analysis/ SKILL.md main.py doc_summarizer/ SKILL.md main.py每个技能一个文件夹SKILL.md里写面向模型的定义名称、描述、参数main.py里面写真正的执行逻辑。这样做的好处是第一技能的元信息和代码分离我在调试的时候改描述不用翻代码第二每个技能的依赖可以独立管理某个技能需要pandas装在自己的requirements.txt里不会污染全局环境。命名方面我用统一的snake_case语义要完整。比如data_analysis而不是dadoc_summarizer而不是ds。模型对语义完整的名字识别准确率更高这个我实测过。还有一个很容易被忽略的点技能之间的依赖关系要克制。理想情况下每个技能是独立的但现实里经常有一个技能内部要调用另一个技能的情况。我的处理方式是只允许“上层技能”调用“下层基础技能”并且通过Register接口显式调用不允许直接跨目录import。否则技能图会变成一团乱麻。2.4 技能的粒度太大和太小都难受粒度问题是我认为整个技能体系里最难把握的。第一次做的人通常有两个极端一个极端是把所有功能揉成一个巨大技能叫“全能助手”让模型有请求就调它另一个极端是把每个功能都拆成独立技能结果还是退回了function calling时代。我的判断标准很简单一个技能应该对应一个“用户能一句话说清楚的任务”并且这个任务在执行链路里是相对完整的一段。“分析一个csv文件”是一个技能“用matplotlib画图”不是一个技能——因为用户不会单独说“帮我用matplotlib画图”用户会说“帮我把这个文件的趋势图给出来”。技能的设计应该贴近用户心智里的“任务边界”而不是贴近工程实现里的“模块边界”。3. 从0到1搭建一个最小技能系统3.1 动手前需要准备什么这个最小系统不依赖任何重型框架只需要Python 3.10我用的是3.11其实3.9也能跑一个可以调用的LLM接口我用的是兼容OpenAI SDK的接口比如国内云厂商的模型网关、本地的vLLM服务都行一个用于测试的技能比如“大写的问候语”这种我在选型时坚持不用现成的Agent框架原因很简单框架会给你很多抽象概念和约定但如果你不理解底层原理出了问题根本无从下手。先徒手写一遍把整个链路搞清楚后面再引入框架或平台心里才有底。3.2 最小的Skill定义与注册表这一段我写一个最核心的数据结构。先定义技能# skill.py from dataclasses import dataclass, field from typing import Any, Callable, Dict, List dataclass class Skill: name: str description: str parameters: Dict[str, Any] # JSON Schema 风格 handler: Callable[..., Any] tags: List[str] field(default_factorylist)这里的parameters我直接使用了JSON Schema风格因为后面要把技能列表序列化给模型看JSON Schema是目前模型理解最好的参数描述格式。比如一个技能可以这样定义def hello_handler(name: str): return f你好{name} hello_skill Skill( namehello, description向指定的人发送问候语。适用于用户要求打招呼、问候的场景。, parameters{ type: object, properties: { name: { type: string, description: 要问候的人名, } }, required: [name], }, handlerhello_handler, )然后做一个注册表# registry.py from typing import Dict, List from skill import Skill class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] {} def register(self, skill: Skill) - None: if skill.name in self._skills: raise ValueError(fduplicate skill: {skill.name}) self._skills[skill.name] skill def list_skills_metadata(self) - List[dict]: return [ { name: s.name, description: s.description, parameters: s.parameters, } for s in self._skills.values() ] def execute(self, name: str, arguments: dict): skill self._skills.get(name) if not skill: raise KeyError(fskill not found: {name}) return skill.handler(**arguments)注册表的作用不仅是存技能更重要的是在运行时给模型提供一份“可调用技能的清单”以及根据模型决定去执行对应技能。这个模式在后面接LLM的时候你会看到它的价值——所有技能都通过Registry统一管理Agent不需要感知技能的内部实现。3.3 Agent主循环让模型自己决策现在进入关键环节怎么让LLM用上这套技能。我的方案是“两步走”——第一步把技能列表的元信息塞进system prompt让模型输出一个结构化的调用决定第二步执行技能后把结果拼回对话让模型基于结果生成最终回复。代码长这样# agent_loop.py import json from typing import Callable from registry import SkillRegistry def run_agent( registry: SkillRegistry, user_input: str, llm_client, model: str your-model, ): skills_meta registry.list_skills_metadata() system_prompt ( 你是一个通过技能库工作的智能助手。 当用户请求到达时先从技能列表中选择最适合的一个技能。 如果你确定需要调用技能只需要输出一个JSON对象格式如下\n {skill: 技能名, arguments: {参数名: 参数值}}\n 不要输出任何多余内容。\n 技能列表如下\n f{json.dumps(skills_meta, ensure_asciiFalse)} ) messages [ {role: system, content: system_prompt}, {role: user, content: user_input}, ] # 第一次请求让模型做技能选择与参数填充 resp llm_client.chat.completions.create( modelmodel, messagesmessages, temperature0, response_format{type: json_object}, ) decision json.loads(resp.choices[0].message.content) skill_name decision[skill] arguments decision.get(arguments, {}) # 执行技能 result registry.execute(skill_name, arguments) # 第二次请求把技能结果交给模型生成最终回复 messages.append( {role: tool, content: json.dumps(result, ensure_asciiFalse)} ) final_resp llm_client.chat.completions.create( modelmodel, messagesmessages, temperature0, ) return final_resp.choices[0].message.content这段代码里有几个细节我特意做了处理temperature0技能选择阶段绝对不能让模型“发挥创造力”必须让它输出确定性最高的结果。之前我用过默认的0.7模型偶尔会自己编造技能名改成0之后几乎没再出现过。response_format{type: json_object}强制模型输出JSON省去从一堆废话里解析JSON的痛苦。如果你的LLM接口不支持这个参数你就得在后端加解析逻辑。第二次请求时把技能执行结果以tool角色的消息放回去这是让模型“看着结果说话”。比如数据分析技能返回了“销售额环比下降12%”模型就能自然地对用户说“你的销售额这个月下降了12%”。这套最简流程跑通之后你就有了“Agent可以调用技能”的地基。后续要加技能只需要往Registry里注册其他代码基本不用动。3.4 技能链与组合编排当一个任务需要多个技能时单技能决策只是第一步。实际业务里很多任务需要多个步骤。我见过很多团队在这里掉入陷阱——让模型自己规划多技能调用顺序。比如用户问“分析这个文件然后做成一页PPT总结”模型需要先调用data_analysis再调用create_presentation。如果把这个编排权完全交给模型成功率就会显著下降因为每一步都可能出错。我习惯的做法是把这个“两步流程”再包成一个新技能。技能内部通过Registry去调用其他技能而不是把中间步骤暴露给模型。def create_report_skill(registry: SkillRegistry): def data_report(file_path: str, output_format: str markdown): # 第一步调用底层分析技能 analysis registry.execute( data_analysis, {file_path: file_path}, ) # 第二步调用演示文档生成技能 if output_format pptx: deck_url registry.execute( create_presentation, {title: 数据分析报告, content: analysis}, ) return {deck_url: deck_url} return analysis return Skill( namedata_report, description对数据文件进行分析并生成报告或演示文稿。 适用于用户要求统计分析并输出成果物PPT/报告的场景。, parameters{ type: object, properties: { file_path: {type: string, description: 数据文件路径}, output_format: { type: string, description: 输出格式可选markdown或pptx默认markdown, enum: [markdown, pptx], }, }, required: [file_path], }, handlerdata_report, )这里有个很微妙的设计点对模型来说它只看到一个data_report技能它需要做的决策仍然只有一个。而内部的分步编排由我们工程师通过代码来保证稳定。这就是我前面说的“把复杂度转移到工程侧”的具体实践。4. 三个真实场景的实战拆解4.1 代码审查技能先让静态工具干脏活代码审查是很多团队做Agent落地的第一个技能因为它见效快、反馈直接。但如果你直接把“原始代码”丢给LLM让它全权审查你会得到一堆大而化之的建议什么“代码不够模块化”之类的废话真正的低级错误反而被漏掉。我的做法是先用静态检查工具从代码里捞出一批“确定性错误”交给LLM去审查结构性和语义性问题。import json import subprocess import tempfile def code_review_handler(code_snippet: str, language: str python): # 1. 静态检查 with tempfile.NamedTemporaryFile( modew, suffixf.{language}, deleteFalse ) as f: f.write(code_snippet) tmp_path f.name lint_issues [] if language python: res subprocess.run( [pylint, tmp_path, --output-formatjson], capture_outputTrue, textTrue, ) lint_issues json.loads(res.stdout or []) # 2. 把静态检查问题与代码一起交给LLM做深度审查 # 这里省略实际的LLM调用逻辑与agent_loop类似 # 3. 合并静态与深度审查结果按严重级别排序 report { critical: [], warning: [], suggestion: [], } for issue in lint_issues: severity warning if issue[type] in (warning, error) else suggestion report[severity].append({ line: issue[line], message: issue[message], }) return report为什么先静态检查因为LLM处理缩进错误、未使用变量、明显的方法签名问题这类低层次问题时又慢又不稳定而静态分析工具处理这些问题又快又准。让工具做工具擅长的事让模型做模型擅长的事各拿一段这个原则在几乎所有“用LLM审查代码”的场景都适用。4.2 数据分析技能pandas打底输出要结构化数据分析技能的封装我也有一套固定的“套路”加载数据 → 清洗字段 → 跑描述统计 → 按需生成图表 → 返回结构化结果。import pandas as pd import json def data_analysis_handler(file_path: str, target_column: str None): # 1. 根据扩展名选择读取方式 if file_path.endswith(.csv): df pd.read_csv(file_path) elif file_path.endswith(.xlsx): df pd.read_excel(file_path) else: raise ValueError(不支持的文件类型请提供CSV或Excel文件) # 2. 基础清洗 df df.dropna(howall) # 3. 描述性统计 desc df.describe(includeall).to_dict() # 4. 对指定列做趋势分析 trend None if target_column and target_column in df.columns: numeric_series pd.to_numeric(df[target_column], errorscoerce) trend { mean: float(numeric_series.mean()) if not numeric_series.isna().all() else None, max: float(numeric_series.max()) if not numeric_series.isna().all() else None, min: float(numeric_series.min()) if not numeric_series.isna().all() else None, } return { shape: list(df.shape), columns: list(df.columns), descriptive_stats: desc, trend_analysis: trend, }这里有个细节技能返回的数据结构必须高度结构化。你不能让技能返回一堆print出来的文本因为后续模型要基于这个结果去生成面向用户的回复。你返回的key命名越清晰模型越知道该怎么用。比如我这里返回了mean、max、min模型看到就很清楚这些是统计值如果我返回一个stats: {...}嵌套对象模型在理解上就容易打折扣。4.3 文档摘要技能长文本要自己想办法处理文档摘要看起来简单真正的坑在于长文本超出上下文窗口。我在早期直接把整篇文档塞给模型结果经常报context length exceeded。现在的设计是技能内部做分块摘要然后在最终汇总时再用一次LLM做合并整个过程对Agent完全透明。def doc_summarizer_handler(text: str, chunk_size: int 3000): chunks [text[i:ichunk_size] for i in range(0, len(text), chunk_size)] chunk_summaries [] llm_client get_llm_client() # 之前的client抽象 for chunk in chunks: prompt f请用三句话概括以下内容\n{chunk} resp llm_client.chat.completions.create( modelyour-model, messages[{role: user, content: prompt}], temperature0, ) chunk_summaries.append(resp.choices[0].message.content) # 合并所有分段摘要生成最终摘要 prompt ( 以下是一篇长文的分段摘要请把它们合并成一个逻辑连贯的整体摘要 控制在500字以内。\n\n \n.join(f- {s} for s in chunk_summaries) ) resp llm_client.chat.completions.create( modelyour-model, messages[{role: user, content: prompt}], temperature0, ) return {summary: resp.choices[0].message.content}这种“分治法”在长文本处理里几乎是必备技巧。要注意的是分块的边界尽量选在自然段结束的位置不要硬切句子否则摘要的质量会明显下降。5. 常见问题与排查技巧实录5.1 模型看不见技能或者看见了就是不调用这是我最常被问到的问题“我的技能列表都放在system prompt里了但模型就是不调怎么办”先别急着怀疑模型按下面这个顺序排查第一步确认技能描述对“触发场景”的覆盖够不够。很多技能描述写得太抽象比如“提供数据分析功能”模型看到“分析”两个字才想到调用而用户如果问“这个月的销售额怎么样”模型反而觉得不该调。把触发场景写具体能解决一半问题。第二步确认技能数量是不是太多了。我的经验是一次给模型提供的技能数量最好控制在5个以内。技能超过这个数模型的选择准确率就开始明显下滑。技能多的时候我建议分层先提供一个“技能路由”技能让模型根据用户意图选择进入哪一层再由这一层去决策具体技能。第三步在system prompt里加一两句“用技能做什么”的示例。Few-shot示例对模型行为的引导作用极大比你在description里写一百遍“请使用技能”都有用。5.2 参数传错、JSON解析失败的坑参数这块我踩过的坑最多。常见错误有几种模型把数字参数传成了字符串模型漏掉必填参数模型在JSON里输出None、undefined等非JSON值更离谱的是模型有时候会输出“好的我现在调用xxx技能”之类的废话而不是纯JSON。我应对的办法是三层防护第一层在prompt里明确要求“只输出JSON对象不要任何解释”。同时用系统支持的强制JSON模式。第二层在解析JSON时做容错。如果response_format不管用就自己写一个抽取器从输出里找第一个{和最后一个}截取出来再用json.loads解析。第三层在执行handler之前做一个“参数清洗”函数。把所有数值型参数尝试强转成int/float把必填参数检查一遍缺了就补默认值补不了的直接返回一个“参数缺失”的错误给模型让模型自己决定怎么补。5.3 Prompt越来越长Token越来越贵技能系统的天然毛病就是元信息膨胀。每个技能的description和parameters加在一起动辄几百token技能一多光是system prompt就能吃掉几万token。我目前的做法是把技能描述压缩成“摘要详情”两层。Agent系统提示词里只放每个技能的一句话摘要当模型判断需要某个技能时再把完整的JSON Schema和长描述注入进来。这样常规对话的上下文占用小很多只有真正要调用技能时才花这笔“巨款”。另外定期清理description里的废话。我见过同事写的技能描述光“智能”“高效”这类词就占了三行全删掉完全不心疼。5.4 技能版本与治理技能库也会“腐烂”技能库和代码库一样不做治理就会烂。最常见的问题是同一个功能被不同人注册成多个技能功能重叠某个技能的依赖库升级了但调用方没跟上线上报错旧技能没有下线机制越来越多地占领模型视野。我的团队现在有一套简单的治理约定每个技能注册时必须填写owner和变更日志。新增技能前必须先检索一遍现有技能确认没有重复能力。技能上下线通过操作后台统一做不直接改代码注册。每隔一段时间跑一次“技能健康检查”统计每个技能的被调用次数、失败率和平均耗时把调用率极低的技能暂时下线。这套约定执行了几个月对系统稳定性的提升非常明显。我再强调一次技能系统本身是一层工程抽象它解决的是“让模型在合适的粒度上做决策”这件事。真正决定Agent好不好的还是业务数据、接口稳定性、日志和权限这些基本功。先把最常被用到的那两三个技能打磨到极致比一次性铺开五十个半成品技能有用得多。这是我在几次推翻重来之后最想对你说的一句话。
返回列表