Agent Skills实战:把AI Agent能力模块化,打造可复用的技能包

发布时间:2026/9/23 5:03:57
Agent Skills实战:把AI Agent能力模块化,打造可复用的技能包
过去大半年我一直在折腾 AI Agent 相关的项目从最早只会调 API 拼 prompt到后来给 Agent 套工具、做记忆、跑任务流踩了不少坑。最近一个多月我把大部分精力放在了一件事上把常用的能力沉淀成标准化的“技能包”让 Agent 不止会聊天还能稳定地完成具体工作。这个概念在圈子里一般叫 agent-skills也是我今天想认真聊聊的主题。先给不熟悉的朋友一个定位agent-skills 不是什么新框架也不是某一家公司的专有名词它描述的是一整套“把 Agent 会用到的能力做成可复用模块”的工程方法。你可以把它理解成给 Agent 装上的“插件化能力层”——写一个清晰的技能描述文件配上实现逻辑、输入输出约定、依赖和环境说明Agent 在运行时会根据当前任务主动选择、加载并调用这些技能。它解决的核心痛点是零散堆在 prompt 里的能力描述没法沉淀、没法复用、也没法测试而一旦技能模块化就能像搭积木一样快速组合Agent 可以专注在“规划”上把具体执行交给一套更可控的技能系统。这篇文章适合这几类人看正在做 Agent 应用但总觉得“效果不稳定、能力不可控”的开发者准备做企业内部助手、想把内部系统能力开放给 Agent 调用的工程师以及刚接触 Agent 开发、想少走弯路的学习者。我尽量把思路、原理和可落地的代码都放出来大家拿到手可以直接照着抄。1. agent-skills 到底解决什么问题1.1 从一个真实的需求开始先说一个我自己的例子。之前做一个企业内部的招聘助手需求很简单HR 把一堆候选人简历丢进来Agent 要能自动识别关键信息、生成结构化摘要、还能根据岗位要求做初步匹配。最早我直接把这些指令全部塞进系统提示词里写了两千多字结果效果很糟——提示词越长模型越抓不住重点输入稍微偏一点输出格式就乱套。后来我换了个思路把“简历解析”做成一个独立技能输入是原始简历文本输出是固定结构的 JSON包含姓名、工作年限、技能列表、项目经历等。Agent 主进程只负责判断“现在需要解析简历”然后调用这个技能。改完之后效果立刻稳定了而且这个技能可以被招聘助手之外的其他 Agent 复用——比如入职流程里要提取员工信息表同一个技能直接拿来用。这就是 agent-skills 最朴素的价值让能力可沉淀、可复用、可独立优化。1.2 skills 和 tools、plugins 到底有什么区别很多朋友会问这不就是 function calling 或者 tool use 吗名字不一样罢了。我和团队折腾下来觉得还是有本质差异的值得掰开讲清楚。Tools工具是最小的可调用单元通常就是一个函数输入输出结构明确。比如“读取文件”“发送邮件”“查询天气”。它是点状的粒度很细。Skills技能是围绕一个完整任务组织起来的工具集和指令集。比如“简历解析”这个技能内部可能调用了文件读取、文本清洗、LLM 结构化抽取、JSON 校验等多个工具还包含一段指导模型如何处理边界情况的描述。它是任务级的粒度更粗也更贴近真实业务。Plugins插件更偏向“能力包”的商业化概念比如很多产品里的插件市场本质是把若干个 skills 和相关资源打成一个可分发的包。从工程角度理解tools 是“手”skills 是“一套工作流”。我平时开发时会先想这个 Agent 需要完成哪几类完整任务把它们定义成技能再为每个技能准备内部需要的最小工具集。而不是反过来先写一堆工具再靠 prompt 拼凑。1.3 当前主流的几种实现路线agent-skills 现在没有一个统一标准各家实现方式有差异但思路趋同。我梳理一下主流的几种路线方便大家按自己的技术栈选择。第一种是模型厂商官方支持的方式典型代表是 Anthropic 在 Claude 产品里推的 Agent Skills 机制以及 OpenAI 的 function calling / Assistants API 中工具定义。这类方案的特点是和底层模型深度整合Agent 在选择技能、解析参数时准确率最高但通常绑定自家生态跨平台复用稍弱。第二种是开发者生态的方案比如 LangChain 的 Custom Tool、Hugging Face 的 smolagents 里的 Skill 机制、以及很多开源项目实现的 skills 目录约定。这类方案灵活、不绑定厂商适合自建 Agent 或做研究原型但模型能不能选对技能部分取决于你自己写的描述和调度逻辑。第三种是自研方案也就是把技能描述、实现代码、校验规则全部放进一个统一目录由你自己的 Agent 运行时去加载和调度。这种方式成本最高但可定制性也最强适合企业级场景。我的观点是初期从官方方案入手摸清机制中期切换到自研目录结构掌握主动权。下面要讲的实操部分我会采用第三种自研方案因为只有理解了底层机制才能写出跨平台可迁移的技能包。2. 一个 skill 到底应该包含什么2.1 技能包的标准组成要素我自己在实际项目中沉淀了一套技能包规范每个技能都是一个独立目录里面至少包含四部分内容。这套规范和 Anthropic 官方推荐的 SKILL.md 结构比较接近但做了一些面向工程化落地的扩展。一个标准技能包的目录结构大概是这样的skills/ 简历解析/ SKILL.md # 技能说明文件Agent 靠它来判断何时用、怎么用 schema.json # 输入输出参数的 JSON Schema 定义 requirements.txt # 依赖清单 scripts/ parse_resume.py # 主实现脚本 extract_contact.py # 辅助脚本 examples/ example_input.md # 示例输入 example_output.json # 示例输出这里最关键的是SKILL.md和schema.json。SKILL.md 是给 Agent尤其是大模型看的说明书决定了它能不能在合适的时机想到调用这个技能schema.json 是给程序看的契约决定了参数传进来之后能不能被正确解析和执行。2.2 SKILL.md 该怎么写很多第一次写技能的人容易犯一个错误把 SKILL.md 写成给人类看的文档大段大段的背景介绍和原理说明。但你要知道这份文件的主要读者是模型它需要的是精准的“触发条件”和“执行规范”而不是一篇优美的散文。我写 SKILL.md 的模板经过多次迭代目前固定下来几个段落。先给大家看看一个实际可用的简历解析技能的 SKILL.md--- name: resume_parser description: 当用户提供候选人简历文本或简历文件路径时使用。 用于提取结构化候选人信息包括基本信息、工作经历、教育背景、技能列表。 不要用于非简历文档的信息提取。 --- # 简历解析技能 ## 何时使用 - 输入包含完整简历文本或简历文件路径时。 - 用户明确要求提取候选人信息、生成候选人画像时。 ## 输入要求 - text: 简历的纯文本内容必须是 UTF-8 编码。 - source_type: 可选值为 text/file默认 text。 ## 输出要求 - 输出必须是合法 JSON且符合 schema.json 中的定义。 - 字段缺失时使用 null不要自行编造内容。 - 工作经历按时间倒序排列。 ## 执行步骤 1. 读取输入文本。 2. 使用 scripts/parse_resume.py 进行分块解析。 3. 对解析结果做字段校验补全缺失字段。 4. 返回结构化 JSON。 ## 关键注意事项 - 提取邮箱和电话时使用正则不要依赖模型推理。 - 技能列表必须保留简历原文表述不要做同义词替换。 - 如果输入文本长度超过 3 万字符先切分再逐段解析。你发现没有这份文件没有任何废话每一行都在告诉模型“什么时候用、输入是什么、输出怎么做、有什么坑”。描述里的触发条件写得越精确模型选错技能的概率越低。2.3 schema.json 定义输入输出的契约schema.json 的作用是让技能的使用者不管是模型还是其他程序能准确理解参数格式。我用 JSON Schema 的语法来定义好处是生态成熟各种语言的库都能直接校验。下面是一个简化版的 schema{ name: resume_parser, description: 解析简历文本并输出结构化候选人信息, input: { type: object, properties: { text: { type: string, description: 简历纯文本内容 }, source_type: { type: string, enum: [text, file], default: text } }, required: [text] }, output: { type: object, properties: { basic_info: { type: object, properties: { name: { type: [string, null] }, phone: { type: [string, null] }, email: { type: [string, null] }, years_of_experience: { type: [integer, null] } } }, work_experience: { type: array, items: { type: object, properties: { company: { type: string }, position: { type: string }, duration: { type: string }, highlights: { type: array, items: { type: string } } } } }, skills: { type: array, items: { type: string } } }, required: [basic_info, work_experience, skills] } }我特别强调“输出必须是合法 JSON”这一点因为后续流程——比如存数据库、渲染到前端——都依赖稳定的结构。如果模型输出自由文本下游处理逻辑会变成一场灾难。2.4 把任务切成“可验证”的最小单元写技能的时候一个很重要的设计原则是每个技能只做一件事边界清晰输出可验证。早期我总想做一个“万能技能”比如把所有文本处理任务都塞进一个技能里结果这个技能的描述写得极其含糊Agent 经常搞不清楚该在什么时候调用它参数解析也经常错。后来我学乖了把任务拆细。比如简历解析这个场景我拆成了三个技能resume_parser输入完整简历输出结构化候选人信息。resume_normalizer输入一段非标准格式的简历文本输出标准化的 Markdown 格式。candidate_scorer输入候选人结构化信息和岗位要求输出匹配分数和理由。每个技能都很简单测试起来也方便——输入一个确定的样例看输出是否符合预期完全可以自动化验证。组合起来之后Agent 的规划能力就有了用武之地先归一化再解析最后评分。这样反而比一个复杂技能更可靠。3. 手把手实操从零写一个可用的 agent-skill3.1 环境准备实操之前先把开发环境准备好。我用的是一台普通的 Linux 服务器Python 3.10不需要 GPU因为这个技能的核心逻辑是规则的归脚本理解的归模型。这里给大家列一下我用到的核心依赖# Python 3.10 pip install anthropic # 用于调用 Claude API pip install openai # 用于调用 OpenAI 兼容接口 pip install pydantic # 用于 schema 校验 pip install beautifulsoup4 # 用于网页/HTML 内容清洗 pip install lxml # 解析器当然你也可以直接用别的模型下面的代码里我会把 LLM 调用部分做成可替换的接口。为了方便大家复现整个流程我用一个“网页招聘信息结构化提取”的技能来举例——它比简历解析更通用适合各种信息抓取与整理场景也更能体现出 agent-skills 里“规则模型”混合执行的思路。3.2 开发“网页招聘信息提取”技能这个技能要完成的任务是输入一个或多个招聘页面 URL自动抓取页面内容提取岗位名称、公司、薪资范围、任职要求、岗位描述输出结构化 JSON。先建目录mkdir -p skills/job_extractor/scripts mkdir -p skills/job_extractor/examples cd skills/job_extractor接着写核心脚本scripts/extract_job.py。这个脚本要处理两类事情一是纯粹程序能搞定的部分比如抓网页、清洗 HTML、提取文本二是需要语义理解的部分比如从一段随意的描述中抽取“任职要求”这部分我会调用 LLM 接口。#!/usr/bin/env python3 Extract structured job info from a job posting page URL. import argparse import json import re import sys from urllib.parse import urlparse import requests from bs4 import BeautifulSoup # 你可以替换成任何兼容 OpenAI 接口的模型服务 from openai import OpenAI client OpenAI() # 默认读取环境变量 OPENAI_API_KEY def fetch_html(url: str) - str: 抓取网页并返回清洗后的可见文本。 headers { User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ) } resp requests.get(url, headersheaders, timeout15) resp.raise_for_status() soup BeautifulSoup(resp.text, lxml) # 去掉 script、style 等无关内容 for tag in soup([script, style, noscript]): tag.decompose() text soup.get_text(separator\n, stripTrue) # 压缩连续空行 text re.sub(r\n{3,}, \n\n, text) return text def extract_with_llm(text: str) - dict: 用 LLM 从页面文本中抽取结构化信息。 prompt f请从下面的招聘页面文本中提取以下字段 - job_title: 岗位名称 - company: 公司名称 - salary_range: 薪资范围如果原文没有则返回 null - location: 工作地点 - job_description: 岗位描述保留关键细节 - requirements: 任职要求列表形式每项一句话 只输出 JSON不要输出其他内容。 页面文本 {text[:12000]} resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}], temperature0, ) content resp.choices[0].message.content # 防御去掉可能的 markdown 代码块标记 content content.strip() if content.startswith(): content re.sub(r^(?:json)?\s*, , content) content re.sub(r\s*$, , content) return json.loads(content) def validate_output(data: dict) - dict: 对输出做基本校验缺失字段补 null。 fields [ job_title, company, salary_range, location, job_description, requirements, ] for f in fields: if f not in data: data[f] null # 实际会报错下面用 None # 修正上面的笔误Python 里是 None return data def main(): parser argparse.ArgumentParser() parser.add_argument(--url, requiredTrue) parser.add_argument(--output, defaultNone) args parser.parse_args() url args.url # 防御URL 必须合法 parsed urlparse(url) if parsed.scheme not in (http, https) or not parsed.netloc: print(json.dumps({error: invalid_url}), filesys.stderr) sys.exit(1) page_text fetch_html(url) result extract_with_llm(page_text) with open(args.output, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2) print(json.dumps(result, ensure_asciiFalse)) if __name__ __main__: main()代码里面有一个细节要说明调用 LLM 时我把 temperature 设置成了 0因为在信息抽取这类任务里我们不希望模型“自由发挥”输出要尽可能稳定。另外对大模型来说输入文本不可能无限长我这边临时截断到 12000 字符。如果你的页面特别长更稳妥的做法是先按标题、正文分块或者用摘要模型压缩这个后面会在问题排查章节专门讲。3.3 编写 SKILL.md 和 schema接下来补上技能的灵魂——SKILL.md 和 schema.json。核心脚本再厉害如果 Agent 不知道该在什么时候调用它那也没有用。--- name: job_extractor description: 输入招聘页面的 URL自动抓取页面内容并提取结构化岗位信息。 适用于 BOSS直聘、拉勾、Indeed 等招聘网站的职位页面。 不要用于批量爬取整个网站仅用于单页信息提取。 --- # 网页招聘信息提取技能 ## 何时使用 - 用户给出一个招聘职位页面的链接希望了解岗位详情。 - 用户要求整理多个招聘链接的关键信息。 - 输入不是招聘页面时不要使用本技能。 ## 输入参数 - url: 招聘职位页面的完整 URL必填。 - output: 输出文件的路径可选。 ## 输出格式 - 输出 JSON 对象字段定义见 schema.json。 - 所有字段均应为字符串或字符串数组。 - 若页面中不存在某字段置为 null不要编造。 ## 执行步骤 1. 校验 URL 合法性。 2. 用 fetch_html 抓取页面可见文本。 3. 调用 extract_with_llm 抽取结构化信息。 4. 对输出做字段校验。 5. 返回 JSON。 ## 注意事项 - 部分招聘网站有反爬机制若请求被拒绝尝试更换 User-Agent。 - 页面文本过长时需要截断或分块避免超出模型上下文。 - 薪资范围字段保留原文格式例如 20K-40K·14薪。schema.json 这里就不重复贴完整的了结构跟前面简历解析的那版类似字段换成 job_title、company、salary_range、location、job_description、requirements。重点提一嘴薪资这种自由文本字段我故意没有用 number 类型因为真实网页里的表述五花八门“20K-40K·14薪”“面议”“200-300/天”都有强行转数值反而会丢失信息。3.4 注册技能并跑通一次完整调用技能包写好了怎么让 Agent 用起来这里我写一个简单的 Agent 运行时它维护一个技能注册表根据用户输入决定调用哪个技能。#!/usr/bin/env python3 A minimal agent runner with skill routing. import json import subprocess import sys from openai import OpenAI client OpenAI() SKILL_REGISTRY { job_extractor: { description: ( 从招聘页面 URL 中提取结构化岗位信息。 当用户提供 job URL 并希望了解详情时使用。 ), command: [python, skills/job_extractor/scripts/extract_job.py], parameters: { type: object, properties: { url: {type: string}, output: {type: string, default: None} }, required: [url] } } } def route_to_skill(user_input: str): 让模型从注册表中选择合适的技能并给出参数。 registry_desc \n.join( f- {name}: {meta[description]} for name, meta in SKILL_REGISTRY.items() ) prompt f你是一个 Agent 调度器。用户输入如下 {user_input} 可用技能 {registry_desc} 请从可用技能中选择一个并输出 JSON {{skill: 技能名, arguments: {{...}}}} 如果没有任何匹配输出{{skill: null, arguments: {{}}}} 只输出 JSON。 resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}], temperature0, ) content resp.choices[0].message.content.strip() content content.removeprefix(json).removesuffix().strip() return json.loads(content) def execute_skill(skill_name: str, arguments: dict): meta SKILL_REGISTRY[skill_name] cmd meta[command] [] for key, value in arguments.items(): if value is None: continue cmd.append(f--{key}) cmd.append(str(value)) result subprocess.run(cmd, capture_outputTrue, textTrue) if result.returncode ! 0: return {error: result.stderr} # 解析最后一行 JSON 输出 lines result.stdout.strip().splitlines() return json.loads(lines[-1]) def main(): user_input sys.argv[1] if len(sys.argv) 1 else ( 帮我看看这个岗位值不值得投https://example.com/jobs/123 ) decision route_to_skill(user_input) print(路由结果:, json.dumps(decision, ensure_asciiFalse)) if decision.get(skill) is None: print(未匹配到技能请补充更多信息。) return output execute_skill(decision[skill], decision[arguments]) print(执行结果:, json.dumps(output, ensure_asciiFalse, indent2)) if __name__ __main__: main()跑一次python run_agent.py 帮忙看下这个岗位 https://example.com/jobs/54321路由模型会输出{skill: job_extractor, arguments: {url: https://example.com/jobs/54321}}然后子进程执行抓取和抽取逻辑最后打印出结构化 JSON。这个流程虽然简陋但已经具备了一个最小 Agent 技能系统的雏形模型负责规划和路由脚本负责稳定执行。4. 多个技能如何协作而不是各自为战4.1 让 Agent 自主选择技能路由与匹配策略上面的例子是单技能场景真实业务往往同时挂十几个技能。技能多了以后最大的问题就是路由准确率下降——模型分不清该用哪个或者干脆选错。我总结了几条提升路由准确率的实操经验技能描述开头第一句必须是一个“触发判断题”。比如“当用户提供招聘页面 URL 时使用”比“这个技能可以提取招聘信息”清晰得多。相似技能要写出区分条件。比如你有job_extractor和resume_parser就要写清楚前者输入是 URL后者输入是简历文本。路由模型的 temperature 一定要设 0。我之前吃过亏temperature 设成 0.7同样的输入有时路由对、有时路由错后来直接设 0准确率立刻稳定在 95% 以上。如果路由经常出错还可以考虑用规则前置先做关键词或者 URL 模式匹配匹配到唯一技能就直接执行匹配不到再让 LLM 判断。这样既省 token又减少错误的可能性。比如url.startswith(https://example.com/jobs/)几乎可以确定是job_extractor根本不需要问模型。4.2 技能之间的数据传递规范多个技能协作时一个很容易翻车的点就是数据格式对不上。技能 A 输出的是一个自由文本技能 B 却期望收到 JSON——两边都觉得自己没做错但串起来就是出错。我的解决办法是每个技能的输出必须有明确的 JSON 格式而且尽量复用公共的数据结构定义。比如我建了一个common_types.py里面定义了JobInfo、CandidateProfile这些数据类所有技能都用 pydantic 来定义输入输出模型from pydantic import BaseModel from typing import Optional, List class JobInfo(BaseModel): job_title: Optional[str] None company: Optional[str] None salary_range: Optional[str] None location: Optional[str] None job_description: Optional[str] None requirements: List[str] [] class CandidateProfile(BaseModel): name: Optional[str] None years_of_experience: Optional[int] None skills: List[str] [] work_experience: List[dict] []然后每个技能脚本 import 这个公共模块内部先解析成模型对象再序列化成 JSON 输出。这样只要数据契约统一技能之间传数据就像流水线一样顺畅。4.3 失败回退与重试机制技能调用不是每次都成功的。网页可能反爬、接口可能超时、模型输出可能解析失败所以一套健壮的重试和回退机制不可或缺。我在实际项目里的做法是每个技能脚本内部要做局部重试。比如网络请求超时自动重试两次间隔递增1 秒、3 秒。脚本要区分“可重试错误”和“不可重试错误”。参数校验失败这种属于不可重试直接报错网络超时、模型接口 5xx 这种属于可重试。Agent 调度层要兜底。如果某个技能执行失败Agent 要能感知并切换策略比如“抓取失败改成让用户手动粘贴文本”。下面这段是在调度层做兜底的示意def execute_skill_with_retry(skill_name, arguments, max_retries2): meta SKILL_REGISTRY[skill_name] for attempt in range(max_retries 1): result subprocess.run( meta[command] flatten_args(arguments), capture_outputTrue, textTrue ) if result.returncode 0: return json.loads(result.stdout.strip().splitlines()[-1]) # 判断 stderr 是否可重试 err result.stderr if http_429 in err or http_5xx in err or timeout in err: time.sleep(2 ** attempt) continue break return {error: fskill {skill_name} failed after retries}有了这一层兜底Agent 在遇到临时故障的时候不会直接崩掉而是给用户一个明确的错误信息或者自动换一条执行路径。5. 常见问题与排查技巧实录5.1 模型调用不到正确的技能这个问题出现的频率最高。你明明注册了技能但模型就是不用或者用错了。排查顺序一般是第一步看技能描述是否足够精确。把“技能名 描述 一份示例输入”拼起来扔给模型看它能否正确判断。如果连你手动测试都选不对那说明描述写得太含糊需要重写。第二步确认注册表里技能数量是否爆炸。超过 20 个技能以后即使描述写得好路由准确率也会明显下降。这时候需要做技能分层先路由到“技能组”再在组内路由到具体技能。第三步检查是否在 prompt 里给了模型足够的上下文。有些 Agent 框架默认不会把所有技能描述塞进上下文需要你显式配置。5.2 参数格式总是出错模型生成的参数偶尔不遵守 schema比如把数字写成字符串、漏传必填字段。我在前面已经用过一招在 schema 里写明字段类型和描述。再补充两个实操技巧在路由 prompt 里加一句“参数必须符合以下 JSON Schema 定义”并且把 schema 原文贴进去。在技能脚本入口做严格校验不合法就直接报错返回。这样即使模型传错了参数也不会引发更深层的问题而是可以触发重写。def validate_arguments(args: dict) - dict: # 简化示例实际可用 jsonschema 库 if url not in args or not args[url].startswith(http): raise ValueError(missing or invalid url) return args5.3 一次真实的翻车记录我调 job_extractor 的时候遇到过一个很典型的问题输入一个招聘页面的 URL 给 Agent模型路由到了技能技能脚本也顺利执行了但返回的 JSON 里job_title一直为空。抓下来的页面文本我看过明明是有的模型怎么会提取不到后来我把页面 HTML 打出来才发现这个页面的正文是用 JavaScript 动态渲染的requests拿到的 HTML 里根本没有岗位数据只有一堆空的 div 和一段初始化脚本。这是一个非常经典的坑静态抓取只能拿到服务端渲染的内容SPA 页面的数据需要等浏览器执行完 JS 才有。解法的思路有几种优先找页面里的__NEXT_DATA__或__INITIAL_STATE__之类的内嵌 JSON很多 SPA 首屏数据都在里面。用无头浏览器渲染比如 Playwright 或 Selenium代价是速度和资源占用。找该网站是否提供 API 接口直接在网络请求里抓 XHR 数据。我的建议是先看内嵌 JSON实在不行再上无头浏览器。千万不要一上来就搞无头浏览器性能开销会让你怀疑人生。5.4 常见问题速查表问题现象可能原因排查与解法模型不使用已注册技能描述不精确 / 技能过多重写描述前两行做技能分层调用技能但参数错误路由 prompt 缺少 schema在 prompt 中附上完整 JSON Schema脚本返回乱码 / 空数据网页是 JS 动态渲染抓内嵌 JSON 或用无头浏览器输出 JSON 解析失败模型输出混入额外文字正则剥离代码块标记或用更严格的提示词技能执行超时页面过大或网络慢分块截断设置合理超时和重试招聘网站返回 403反爬策略拦截换 User-Agent加 Cookie降低抓取频率这张表是我在实际开发中反复用到的一张自查表建议直接收藏。遇到问题不要慌按照表格顺序排查大部分问题五分钟内能定位。6. 技能系统的设计原则与扩展思路6.1 技能数量要多还是要少我在和很多同行交流时发现一个共识技能数量不是越多越好而是越“聚焦”越好。一个 Agent 挂 5 个高质量技能效果通常优于挂 50 个互相重叠的糙技能。因为技能多了路由就会开始糊模型会搞不清边界。我个人的控制线是一个 Agent 核心技能不超过 10 个。多余的能力往下沉淀成子技能或者合并成组合技能。比如“简历筛选”是一个上层组合技能它内部调度 resume_parser 和 candidate_scorer 两个子技能。对 Agent 主进程来说它只感知到一个“简历筛选”技能路由的负担就不会太重。6.2 技能的可观测性问题最后分享一个容易被忽视但极其重要的话题可观测性。Agent 系统最大的特点是不确定性同一个输入今天走技能 A明天可能走技能 B。如果你不记录执行过程出了问题就只能抓瞎。我在每个技能脚本里都加了日志统一格式方便后面追溯import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(name)s: %(message)s ) logger logging.getLogger(job_extractor) logger.info(fetching url%s, url) logger.info(llm extraction token_usage%s, usage) logger.warning(page text truncated, original_len%d, original_len)除了日志我还会把每次调用的路由结果、参数、执行耗时、输出摘要写入一个本地 SQLite 数据库。这样我可以定期统计“哪些技能被调用得多”“哪些技能经常失败”“路由准确率是多少”据此持续优化技能包。6.3 把这个机制扩展到团队和企业级agent-skills 这套模式还有一个有意思的延伸——它很适合作为团队内部知识库与能力开放层。企业里存在大量“只有某个老员工知道怎么做”的流程比如“申请阿里云资源要填三张表”“客户退款分四种场景走不同审批流”。这些流程完全可以封装成技能包让 Agent 在对话中直接引导用户完成而不是让新员工到处问人。具体到工程实现可以考虑给技能包加上权限控制、审计、版本管理等能力把技能变成一个公司级平台。现在市面上已经有一些开源项目在做类似的事情比如把 skills 放在 git 仓库里管理通过 CI/CD 做自动化测试然后分发到各个 Agent 实例。在我看来这套东西的发展方向有点类似当年微服务对单体架构的重塑——把大而全的 Agent 拆成小而专的能力集合各有各的维护方和版本再通过一套标准协议组合起来。我在自己的项目里验证过这个方向把简历解析、招聘信息抓取、候选人评分做成技能包之后同一个团队里的另一个项目——一个自动生成面试反馈的助手——直接复用了其中两个技能几乎零成本接入。这种“一次沉淀、多处复用”的体验是以前写死 prompt 时完全感受不到的。如果你现在正在做 Agent 项目我特别建议尽早把技能化的思路引入进来。不需要一上来追求完美框架先把一两个最常用的能力封装成技能包让 Agent 通过技能系统调用起来跑通流程后再逐步扩展。你会发现Agent 的行为会变得可控得多调试和优化也会从一个模糊的“调 prompt”变成一项更清晰的“调代码”工作。这套模式的收益会随着技能数量的积累越来越大。