
上个月我接手了一个agent项目客户的需求看上去特别朴素让大模型自己查库存、看报表、生成下单建议。听起来只是把几个API串起来但真正跑起来才发现模型会在某一步突然开始“自由发挥”跳过校验直接输出一个看起来合理、实则错得离谱的数字。后来我把整套流程拆成若干个可复用的技能块才终于把它拉回正轨。这也是我最近格外关注agent-skills方向的原因。这篇文章把我在这个方向上的设计思路、踩坑记录和可复用的实现框架整理出来给正在搞agent落地的朋友一个参考。你不需要懂很多底层原理只要有基础的Python能力和一点prompt工程经验就能看懂并动手实现。我的目标是帮你建立一套完整的技能化思维让agent不再是一堆不可控的工具调用而是一组边界清晰、可以独立测试、独立复用、独立上线的功能块。1. Agent Skills到底是什么——先把它和“工具调用”区分开1.1 从一次失败案例说起那个查库存的项目第一版是这么设计的在System Prompt里写了一大段说明告诉模型可以先调用库存API再对结果做异常判断最后生成结论。前端也接了Function Calling看起来没什么问题。但模型不是人。它不会因为你在prompt里写了“请务必先调用库存API”就真的每次都调。实测中出现了三种让人头大的情况模型在回答一个具体sku的库存时直接凭训练数据里的记忆编了一个数字根本没去查API。模型调了API但在拿到数据后没有按既定格式解析而是自己重新组织了字段导致下游系统无法消费。当两个业务动作查库存、生成建议同时在一次对话中出现时模型经常漏掉其中一个步骤而且它自己意识不到。这不是prompt写得不够长也不是选错了模型而是我们把任务组织得太粗糙了。Agent在执行任务时需要的是“技能”不是“工具”。工具是单点能力技能则是一套完整的问题解决协议它包含了输入输出格式、中间推理步骤、验证机制和兜底策略。1.2 技能的五个核心特征我在实践里总结下来一个合格的Agent技能必须具备五个特征第一协议化接口。技能不是一个函数那么草率它有明确的JSON Schema输入输出格式字段名、类型、必填约束都是提前定义好的模型无法自由发挥。这样做的好处是下游系统不用猜。第二自包含上下文。技能包内部自带prompt、工具列表、示例集和验证器不依赖外部某个隐藏配置文件。也就是说同一个技能包可以被不同Agent项目直接加载不需要重新“教”一遍。第三可验证。每个技能在产出结果前都要经过一个validator层不符合格式就重试或者回退。这不是prompt层面的“请确保”而是代码层面的强制约束。第四可组合。技能之间可以互相调用。比如“生成订单”技能可以内部调用“校验库存”技能而不是每次都从头写一遍流程。第五可评估。技能有离线测试集和回归测试机制改一次prompt跑一遍全部历史测试用例能直观看到改动是变好了还是变坏了。1.3 哪些事情天生适合做成技能并不是所有能力都值得做进技能体系选错对象反而增加维护成本。我自己的筛选标准是凡是流程固定、判断规则明确、工具链路长、失败代价高的任务都适合做成技能。典型例子包括数据报表生成取数、清洗、统计、出图、写结论。订单处理校验库存、锁定库存、创建订单、返回回执。客户工单分类读取工单、提取关键词、查知识库、打标、分配。代码仓库分析读取仓库结构、定位关键文件、生成摘要、产出报告。反过来纯闲聊、头脑风暴、开放式写作这类任务不适合做技能化封装。它们太依赖发散性强行约束反而让效果变差。2. 为什么需要这套设计——背后的工程逻辑2.1 模型的真正短板是“临场发挥”我一开始觉得只要模型够聪明给几个工具它自己就能完成多步任务。后来发现这个想法错得离谱。大模型本质上是概率生成器每一步都在做“概率最大的下一个token”。它的特长是泛化和生成不是稳定执行。你可以把模型理解成一个博学但偶尔走神的新员工。它知道很多知识但你交给他的流程如果不落到一个强制性的SOP里他会漏步骤、改格式、凭印象作答。技能包就是那张SOP它把“知识”和“操作规范”捆绑在一起。另外大模型的上下文窗口是有限的。无论模型支持200k还是1M真把一整套业务流程的说明文档塞进去既浪费token也会稀释注意力。技能化之后prompt里只保留当前这一步需要的指令其他逻辑都被外部代码接管模型只需要在一个窄小的空间里做推理准确率明显更高。2.2 技能与工具、工作流、插件的边界很多人会把技能、工具、工作流、插件这几个词混着用但它们的定位是完全不同的。概念粒度核心特征典型例子工具/API单点能力一次调用输入输出简单无内部推理查天气、发邮件工作流固定流程按预定义顺序执行几乎没有模型决策数据同步任务插件应用扩展把第三方系统能力暴露给模型浏览器插件、支付插件Agent技能问题解决单元模型参与推理但存在约束协议和验证器客服工单处理、报表分析我个人的理解是技能层是介于模型和工具之间的一层“可编排的推理单元”。工具是技能执行时的原子操作技能则是模型和工具之间的翻译官和守门员。2.3 核心设计原则窄而专、协议先行、纯函数、示例驱动、可验证做技能这半年我总结出五条铁律。窄而专一个技能只解决一类问题。比如“CSV数据分析技能”就不要在里面塞“自动写SQL”的功能。边界越窄prompt越聚焦模型的发挥空间越小可靠性越高。协议先行先定义输入输出Schema再写prompt。很多工程同学习惯先写prompt后补接口导致prompt里提了一堆字段代码里根本没有。协议先行能避免这种空转。纯函数技能的内部逻辑尽量无副作用。同一个输入理论上应该得到同一种结构的结果。不要在一个技能里既读数据库又发邮件又写日志那会把它变成一个外部依赖过重的怪物。示例驱动不要只用自然语言描述期望还要给2到3个完整输入输出示例。模型看示例比读说明书学得快得多。可验证每个技能必须配套验收标准。没有验证机制你根本无法判断一次改动是优化还是劣化。3. 技能包的标准结构拆解3.1 输入输出协议Input/Output Schema整个技能包的地基就是Schema。我用Pydantic来定义因为它在运行时可以做类型校验和字段约束。一个合格的输入Schema至少要包含必需字段、字段描述、类型约束、取值范围。输出Schema则要多加一层“业务校验”比如金额必须大于0日期格式必须合法。下面是一个CSV数据分析技能的输入输出定义。from pydantic import BaseModel, Field from typing import List, Optional class CSVInput(BaseModel): csv_path: str Field(descriptionCSV文件的绝对路径) question: str Field(description用户想问数据的一个自然语言问题) class CSVOutput(BaseModel): summary: str Field(description针对问题的文字结论) metrics: dict Field(description计算得到的关键指标) chart_path: Optional[str] Field( defaultNone, description生成的图表图片路径可为空 )这里有一个容易被忽略的细节对输出字段的描述要写“面向模型的解释”而不只是“字段注释”。模型会通过字段描述来判断自己要往里面填什么内容。描述越具体返回质量越好。3.2 System Prompt该怎么写才算“技能化”技能里的System Prompt不是写作文而是一份带约束的任务说明书。常见的错误写法是“你需要分析CSV数据回答用户的问题你可以调用工具最后输出结果。”这句话等于什么都没说。模型只能猜。我更推荐这套结构技能目标用一句话说清这个技能存在的意义。边界声明明确不要做什么比如“不要推测不存在的数据”。执行顺序按编号列出必须执行的步骤。输出要求明确数据格式和调用顺序。禁止行为列出最容易出错的动作直接禁止。我用一个实际生效过的版本作为示例你是一个CSV数据分析助手。你的任务是根据用户提供的问题分析给定的CSV文件并输出结构化结论。 执行顺序 1. 调用load_csv加载文件确认数据可读。 2. 调用describe_data了解字段分布和缺失情况。 3. 调用run_analysis完成问题所需的统计计算。 4. 调用generate_chart生成可视化图表可选。 5. 调用save_analysis保存最终结果。 约束 - 只能使用工具返回的真实数据禁止猜测或补充数据。 - 所有百分比保留小数点后两位。 - 如果问题无法基于现有字段回答直接说明原因不要编造结论。 - 最终必须调用save_analysis否则视为执行失败。可以看出这里面没有一句废话每个句子都在限制模型的自由度。我实测过加了这段prompt之后工具调用率从78%提升到96%。3.3 Tool Schema与“子步骤”编排技能内部的工具定义也需要严格的Schema。以CSV技能为例我定义了四个工具load_csv、describe_data、run_analysis、generate_chart、save_analysis。每个工具的description字段要写清楚“这个工具做了什么”和“应该什么时候调用”。这是模型选择工具的唯一依据。tools [ { type: function, function: { name: load_csv, description: 加载CSV文件返回DataFrame的结构信息包括列名、类型和前几行样例。在技能开始时必须最先调用。, parameters: { type: object, properties: { path: {type: string, description: CSV文件路径} }, required: [path] } } } ]子步骤编排上我建议把“必须串行调用”的逻辑放到代码里做而不是靠prompt让模型自觉。比如加载CSV和后续分析之间有严格的依赖关系可以在代码层面设置状态机只有load_csv完成之后run_analysis才是可选工具。3.4 验证器输出层的最后一道闸门prompt可以被模型无视但validator不会。这是技能可靠性的最后一层保障。我习惯在技能包内部放一个validate函数接收模型的输出结果按Schema做严格校验。校验失败的时候不直接返回报错而是把错误信息作为反馈重试一次。如果连续两次失败则放弃把结果标记为需人工介入。def validate_output(data: dict): try: output CSVOutput(**data) if len(output.summary) 10: raise ValueError(summary过短无法形成有效结论) if output.metrics and not isinstance(output.metrics, dict): raise TypeError(metrics必须是字典类型) return output, None except Exception as e: return None, str(e)这段代码的价值在于只要validator在模型编造的数字就没法不过闸。3.5 示例集与回归测试技能的效果能不能持续稳定全靠回归测试托底。我在每个技能目录下放了一个examples/文件夹里面至少有5组完整输入输出样例。每次修改prompt或代码之后跑一遍全部样例。如果任何一个指标下降我能立刻感知而不是等上线后让用户来骂。回归测试的核心指标就三个输出格式合法率、工具调用成功率、端到端准确率。这三个指标如果有任何一个低于90%我认为这个技能还不具备上生产环境的资格。3.6 技能的生命周期与状态管理技能不是写完就不动了它有自己的生命周期开发、测试、灰度、上线、监控、迭代。这里最容易被团队忽略的是状态管理。技能内部如果有关联多次工具调用那就要考虑中间状态该怎么保存。很多agent项目的bug都来源于“模型忘了之前调用的结果”。我的方案是把中间状态放到一个dataclass里由代码维护不让模型自己记。dataclass class SkillState: csv_loaded: bool False df_shape: tuple None analysis_result: dict None模型每次调用工具时代码更新状态下一次工具选择时再把状态摘要注入上下文。这样模型就不需要自己“记住”任何东西。3.7 与MCP的关系MCP是总线技能是服务最近很多人聊MCP我要说句实话MCP不是技能技能也不是MCP。MCP解决的是“模型如何发现和调用外部工具”的标准化问题它定义了工具发现、调用、认证的协议。而技能解决的是“如何把多步工具调用组织成一个可靠的问题解决单元”。两者是互补的。一个技能内部可以通过MCP调用远程工具也可以直接把工具注册成MCP server对外暴露。我的实践经验是如果技能要跨团队或跨系统复用那就包装成MCP服务如果只在单个Agent内部用直接代码嵌入反而更轻。4. 实操演示从零实现一个“CSV数据分析”Agent技能4.1 需求定义为了让你能直接跟着做我挑一个足够典型的场景运营人员上传一个CSV文件让Agent回答“过去30天哪个品类的销售额增幅最大并生成图表”。这个需求看起来简单但足够包含一个技能包的完整要素多工具调用、数据分析、图表生成、结论输出。4.2 技能包完整代码框架我先给出完整的技能执行器框架。import pandas as pd import matplotlib.pyplot as plt import litellm from dataclasses import dataclass from typing import Optional from pydantic import BaseModel, Field # 输入输出协议 class CSVInput(BaseModel): csv_path: str Field(descriptionCSV文件绝对路径) question: str Field(description用户的问题) class CSVOutput(BaseModel): summary: str Field(description文字分析结论) metrics: dict Field(description关键指标) chart_path: Optional[str] Field(defaultNone, description图表路径) dataclass class SkillState: df: Optional[pd.DataFrame] None analysis: Optional[dict] None # 工具层 def load_csv(path: str) - dict: df pd.read_csv(path) return { columns: list(df.columns), dtypes: df.dtypes.astype(str).to_dict(), head: df.head(3).to_dict(orientrecords), } def run_analysis(df: pd.DataFrame, question: str) - dict: # 这里按问题类型分发到不同的统计逻辑 # 示例固定处理按category分组计算销售额总和及环比增幅 category_sales df.groupby(category)[sales].sum().sort_values(ascendingFalse) return { top_category: category_sales.index[0], top_sales: float(category_sales.iloc[0]), top_growth: float(category_sales.pct_change().fillna(0).iloc[-1] * 100), } def generate_chart(df: pd.DataFrame, output_path: str) - str: grouped df.groupby(category)[sales].sum().sort_values(ascendingFalse) plt.figure(figsize(10, 6)) grouped.plot(kindbar) plt.savefig(output_path) plt.close() return output_path # 技能系统提示词 SYSTEM_PROMPT 你是一个CSV数据分析助手。你必须严格按以下顺序执行 1. 调用load_csv加载数据。 2. 调用run_analysis进行分析。 3. 调用generate_chart生成图表。 4. 最终调用save_analysis保存结果。 禁止编造数据。所有数值保留两位小数。 # 技能执行器 class CSVAgentSkill: def __init__(self, modelgpt-4o-mini): self.model model self.state SkillState() def run(self, csv_path: str, question: str): user_input CSVInput(csv_pathcsv_path, questionquestion) messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: fCSV路径{csv_path}\n问题{question}}, ] tools self._register_tools(csv_path) response litellm.completion( modelself.model, messagesmessages, toolstools, tool_choiceauto, ) # 解析工具调用 while response.choices[0].message.tool_calls: for tool_call in response.choices[0].message.tool_calls: func_name tool_call.function.name args json.loads(tool_call.function.arguments) if func_name load_csv: result load_csv(args[path]) self.state.df pd.read_csv(args[path]) elif func_name run_analysis: result run_analysis(self.state.df, args[question]) self.state.analysis result elif func_name generate_chart: result generate_chart(self.state.df, args[output_path]) elif func_name save_analysis: result {status: saved} messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result) }) response litellm.completion( modelself.model, messagesmessages, toolstools, tool_choiceauto, ) final_text response.choices[0].message.content output CSVOutput( summaryfinal_text, metricsself.state.analysis or {}, chart_pathchart_path ) return output4.3 把技能接入Agent运行时上面这个CSVAgentSkill类已经是一个完整的技能包。接入Agent运行时有几种方式最直接的方式就是作为代码库被主Agent调用。主Agent在意图识别阶段判断用户问题是否指向“数据分析”如果是就直接把用户的问题和文件路径交给CSVAgentSkill处理。第二种方式是注册成MCP server。写一个极简的serverfrom mcp.server.fastmcp import FastMCP mcp FastMCP(csv-analysis) mcp.tool() def analyze_csv(csv_path: str, question: str) - str: skill CSVAgentSkill() result skill.run(csv_path, question) return json.dumps(result.dict(), ensure_asciiFalse) mcp.run()这样其他任何支持MCP协议的Agent客户端都能发现并调用这个技能。4.4 回归测试与上线指标技能写好之后不能直接上线至少要先过三道关第一验证离线示例集。准备五组CSV数据和对应问题跑通全流程确认输出格式合法率100%。第二做负面测试故意给一个空CSV或者给一个与数据无关的问题看技能会不会强行答非所问。第三压测时间开销记录单次的平均调用时长正常情况下应该在10秒以内。上线之后我建议在日志系统里记录三个指标工具调用成功率、端到端准确率、人工介入率。人工介入率高于5%说明技能还不可靠要继续优化。5. 高频问题与排查实录5.1 模型进入“自嗨模式”怎么办这是最常见的故障。模型不调用工具直接生成结果。原因通常是工具描述不清晰或者prompt里的约束不够强。排查方法查看日志看看模型是在哪一步跳过的。修复方向有两个一是修改tool description把“必须先加载数据”改成“load_csv未成功调用之前禁止分析”二是在validator里做强制检查如果分析结果缺少数据来源标记直接拒绝。我遇到过一个案例改一次tool description就把违规率从30%降到了8%。5.2 技能掌握不熟练的表现表现是模型调用了工具但调用参数明显错误。比如把CSV路径填成了问题文本或者把Pandas函数名当作工具名。这通常是因为模型对工具还不够熟悉尤其是一些自定义工具名。解决办法就是加示例。在prompt里放一组“工具调用示例对话”让模型照着格式走。注意示例里的字段名和值要与真实场景高度一致否则模型反而会被带偏。5.3 上下文被污染当技能在一个长会话中被多次调用前面的历史消息会干扰当前逻辑。最典型的表现是第二次分析时模型还在引用上一次的字段名。我的解决方案是每次调用技能时开启独立会话上下文不把外部对话历史传入技能内部。技能内部只处理当前这一轮输入技能结束后只把最终输出返回给主对话。这样各层互不污染排查问题也更快。5.4 失败恢复机制失效技能执行过程中某一步工具调用报错模型经常不知道怎么恢复。比如load_csv遇到编码错误模型可能会反复用错误参数重试。我的建议是在工具调用返回错误信息时同时把“修复建议”一并返回。比如“编码错误请将encoding参数改为utf-8-sig重试”。模型根据提示修正的概率会高得多。重试次数必须封顶最多两次否则陷入循环。5.5 变量名、日志、权限等工程债最后说点和模型无关的坑。技能包如果散落在多个仓库里没有统一的注册中心过半个月你就会发现某个技能被改坏了但根本没人知道。建议给每个技能包配一个metadata.yaml记录技能名称、版本、作者、依赖工具、上线时间、性能基线。这样无论是回滚还是排查都有据可依。6. 工具选型与工程化落地建议6.1 常见框架对比框架定位优点缺点LiteLLM模型网关支持上百种模型统一接口接入简单只管调用不管技能编排LangChainAgent框架组件丰富生态成熟抽象层次多排错麻烦LlamaIndexRAG/数据框架对数据和文档场景支持强大偏RAG技能化支持较弱DyadAgent工作流可视化编排适合非重型逻辑相对较新资料少自研技能层内部平台完全贴合自己业务逻辑清爽前期投入大6.2 我现在的推荐组合如果你是从零搭一套Agent技能体系我的建议是别盲目上重框架。先用LiteLLM做模型统一网关用Pydantic做协议层自己写一个轻量技能注册器。把50行左右的注册逻辑跑通之后再考虑要不要引入LangChain之类的大框架。原因很简单技能核心是协议和验证不是流程编排框架。框架提供的那套chain、memory机制对技能场景来说很多时候是噪音。6.3 上线前必须做好的三件事第一权限最小化。技能内部调用的工具只给最低权限。比如CSV技能只需要文件读取权限不需要写入生产数据库。权限范围写死在技能配置里不建议让模型动态申请权限。第二全链路日志。每轮工具调用记录请求参数、返回结果、耗时、模型名、prompt版本。日志结构要统一方便检索。第三设置失败兜底。技能连续出错的请求一定要进入人工队列而不是让模型无限重跑。7. 我在这个方向上的体会做了将近大半年agent技能化我最大的感觉是不要相信模型要相信协议。模型的价值在于理解和生成但任务执行的确定性必须由代码来保证。技能包就像一个经验丰富的带教老师把大模型这个能力强但容易走神的新员工一步步带回正轨。在实际工作里我总结出一个比较粗暴但有效的验收标准如果一个技能在上线第三周还需要人工介入才能跑通那它不算技能只是另一个需要维护的定时任务。技能必须做到大多数时候无人值守才值得封装。另外一个小建议新手做技能别一上来就追求复杂。先做一个单输入、单输出、工具链路不超过三个的技能把它彻底做稳再逐步增加复杂度。这样你积累出的经验和调试方法才是通用的。