这两年做大模型应用,一个特别直观的感受是:把Agent的能力拆成一个个可以复用的“技能”来管理,比把所有逻辑都塞进系统提示词里靠谱得多。我在自己负责的几个智能体项目里反复踩坑、迭代,最终沉淀出一套叫agent-skills的轻量技能库设计方式。这篇文章就把这套东西掰开揉碎讲清楚,从技能怎么定义、怎么注册,到Agent如何调度执行,再到组合技能和问题排查,全部给出可直接抄作业的实践方案。无论你是刚接触AI应用开发,还是已经在做复杂的多轮任务系统,这套思路应该都能帮你少走不少弯路。
1. 为什么要把Agent能力拆成“技能”而不是硬写逻辑
刚开始做Agent应用的人,多半都会经历一个阶段:把要做的所有事情、所有规则、所有示例全部塞进提示词,然后指望大模型一次输出完美答案。早年我带的模拟项目X也是这么干的,结果就是提示词膨胀到几千字,模型偶尔抽风,稍微换一个用户问法就答非所问。后来我才意识到,本质上不是模型不够聪明,而是我们给模型的任务形式错了。
1.1 核心需求:Agent需要的是“工具感”而不是“说明书”
大模型真正擅长的,是理解意图、拆解用户需求,但它并不擅长稳定执行格式严格的步骤。比如你要它算一个复杂报表的汇总,如果直接在提示词里描述“请先计算A列总和,再计算B列平均,然后做对比”,模型可能每一步都理解,但最终输出格式不一致、计算逻辑不稳定,甚至凭空编出几个数字。如果把这个步骤封装成一个技能函数,由Agent调用,模型只需要决定“什么时候用、用哪个参数”,剩下的计算交给确定性的代码,准确率几乎可以做到100%。
我在实践中体会最深的一点是:Agent技能的本质,是把模型中容易出错的部分换成可靠代码,只留“决策”给模型。决策有不确定性没关系,执行必须绝对可靠。这个思路其实就是后来业界常说的“让模型做大脑,让代码做手脚”,但在个人项目里,很多人一开始意识不到这一点,宁可反复调提示词也不愿意写一层函数封装。
1.2 技能库要解决的问题清单
一套合格的Agent技能库,至少需要解决下面几个问题:
- 技能的描述与暴露:模型必须知道有哪些技能、每个技能是干什么的、需要什么参数。
- 技能的注册与发现:新增一个技能时,不能去改动Agent主流程代码,最好做到“写一个函数、加一个装饰器”就能被自动发现。
- 技能的可靠执行:参数校验、异常兜底、超时控制都要有,不能让一次失败把整条对话流程打断。
- 技能的组合与编排:多个技能按顺序、按条件组合起来,形成更复杂的任务流。
- 可观测性:每次技能调用都留下日志,方便复盘模型选错了技能还是参数传递错了。
我当时设计agent-skills就是围绕这五个点来的。它不追求做成一个大而全的框架,而是核心保持轻量,方便自己在不同项目里快速迁移。
1.3 适用人群与典型场景
这套技能库设计适合以下场景:
- 正在做一个多轮对话助手,希望它能查数据、写文档、发消息、调API。
- 正在开发个人知识库问答系统,希望Agent能检索、摘要、翻译、归档。
- 正在构建自动化工作流,希望让模型根据用户自然语言自动编排步骤。
- 想给自己手头的RAG方案加上一层可复用的工具层,而不是每个工具写一堆重复的解析代码。
如果你目前只是用现成的Agent框架做一些demo,理解技能库的设计思路也能帮你更好地掌控框架内部的调度逻辑,出了问题知道去哪个环节排查。
2. 技能怎么设计:从需求到接口定义
技能设计是整个体系里最容易出错的部分。很多人一上来就写函数,写着写着发现参数、描述、返回格式全都很随意,到后面Agent根本不知道这个技能什么时候该用,或者调用时参数全部传错。我总结下来,技能设计阶段需要先定义清楚几个核心元素,再写实现。
2.1 技能的五要素:名称、描述、参数、执行、返回
我定义技能的模板是这样的:
| 要素 | 说明 | 示例 |
|---|---|---|
| 技能名称 | 全局唯一,简短小写,用下划线连接 | fetch_weather |
| 技能描述 | 用一两句话说明用途、适用场景、典型参数 | 获取指定城市的实时天气,适合用户询问天气时调用 |
| 参数Schema | 结构化定义参数名、类型、是否必填、取值范围 | city: string, required |
| 执行函数 | 真正干活的代码,参数来自于模型解析后的结构 | 调用天气API并格式化为文本 |
| 返回结构 | 统一结构,让模型能读懂结果 | {"status": "ok", "data": "{天气信息}"} |
这里最容易被忽略的是“技能描述”。描述写得含糊,后面调度阶段模型就会选错技能。比如你写“查询天气信息”,模型可能在一个“获取天气预报”的任务里选了它,也可能在“帮我推荐适合跑步的天气”场景里犹豫半天。好的描述应该包含三块:功能用途、推荐使用时机、典型反面场景。我在实际项目里都会给技能描述增加一句“当用户只要求...时不要使用本技能”,效果非常明显。
2.2 用装饰器做技能注册:一个函数就是一个技能
刚开始做技能库,我使用的是手工注册表,所有技能函数汇集到一个列表里。问题是每加一个技能,就要去列表里加一行,多改几个文件,很容易漏。后来我改成装饰器注册,本质上就是利用Python的元编程,在函数定义时自动把函数信息写入全局注册表。
示例代码:
# skills_core.py import inspect import json from functools import wraps from typing import Callable, Dict, Any SKILL_REGISTRY: Dict[str, Dict[str, Any]] = {} def skill(name: str, description: str, parameters: Dict[str, Any]): """技能注册装饰器""" def decorator(func: Callable) -> Callable: SKILL_REGISTRY[name] = { "name": name, "description": description, "parameters": parameters, "handler": func, } @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator使用的时候:
# weather_skill.py from skills_core import skill @skill( name="fetch_weather", description="获取指定城市的实时天气。当用户询问今天是否适合出门、天气怎么样时使用。只处理城市级天气,不做多日预报。", parameters={ "type": "object", "properties": { "city": {"type": "string", "description": "城市名,如北京、上海"}, }, "required": ["city"] } ) def fetch_weather(city: str) -> str: # 实际项目中这里会调用天气服务 data = f"{city} 晴,25℃,东南风3级" return json.dumps({"status": "ok", "data": data}, ensure_ascii=False)这个设计的好处是,技能与技能之间完全解耦,新增技能只需要新增文件、在项目入口import一下即可。但要注意一点:import必须被触发,否则装饰器不会执行。我在项目里会用一个skills_loader.py统一加载skills/目录下所有模块,避免手动遗漏。
2.3 攻击自己:避免把Agent技能做成“万能函数”
技能设计上最容易犯的错误是“一个技能试图干所有事”。一开始我把“文件操作”做成一个技能,参数里有action(读/写/删/列目录),结果模型经常传错action,而且描述很难写清楚什么场景用哪个action。后来拆成read_file、write_file、delete_file、list_files四个独立技能,胜率大幅提升。
拆分的判断标准很简单:如果两个函数分支处理的业务逻辑完全不同、参数也基本不重叠,那就拆成两个独立技能。技能粒度偏细,模型选择成功率会更高,但也不是越细越好。太细会导致技能数量爆炸,模型从几百个描述里选一个反而更容易出错。我的经验是,项目里控制在二三十个以内的技能,效果是最好的,超过五十个最好给技能做分层分组。
3. 技能注册与调度:让Agent“知道”有什么能用
技能定义好了之后,核心问题变成了:给定用户的自然语言输入,Agent怎么决定调用哪个技能、传什么参数。这块我把注册表做成两层:先是“硬路由”,再是“软调度”。看起来绕,其实是两套互补机制。
3.1 注册表不只是存函数,还要存元信息
注册表里除了handler,还应该包含技能描述、参数Schema、调用次数统计、最近错误信息等。我设计的注册表项会额外增加一个stats字段,用来记录调用次数与失败次数。这个信息现阶段可能只是给人看的,但后续做自动评估、技能质检的时候非常有用。
def get_skill_list_for_llm() -> str: """生成适合给LLM看的技能清单""" lines = [] for name, meta in SKILL_REGISTRY.items(): param_desc = json.dumps(meta["parameters"], ensure_ascii=False) lines.append(f"{name}: {meta['description']} 参数:{param_desc}") return "\n".join(lines)这里有一个细节:给模型看的技能清单最好是纯文本,而不是一个Python字典结构。模型对自然语言的注意力分布更稳定,而复杂的JSON嵌套反而容易让模型漏掉字段。我把参数Schema也转成紧凑文本拼进去,实测模型解析参数的准确率更高。
3.2 调度策略一:让大模型从技能清单里选
最常用也最简单的调度方案,就是把技能清单拼进系统提示词,让模型在回复中输出一个结构化的调用指令。我的调用协议长这样:
{"skill": "fetch_weather", "arguments": {"city": "北京"}}模型在某次回复里输出了这个结构,解析器就把它提取出来,去注册表里找对应的处理函数并执行,执行完的结果再塞回给模型继续生成最终回复。这个“工具调用循环”其实就是OpenAI function calling和各类Agent框架的底子。自己实现一遍的好处是能真正理解里面的边界情况。
一个经典循环实现:
def run_agent_once(user_query: str, chat_func, max_steps: int = 5) -> str: messages = [{"role": "system", "content": f"你可以调用以下技能:\n{get_skill_list_for_llm()}"}, {"role": "user", "content": user_query}] for _ in range(max_steps): response = chat_func(messages) if "skill" in response: skill_name = response["skill"] arguments = response["arguments"] result = execute_skill(skill_name, arguments) messages.append({"role": "assistant", "content": json.dumps(response, ensure_ascii=False)}) messages.append({"role": "user", "content": f"技能返回结果:{result}"}) else: return response["content"] return "执行步骤超过上限,已终止"这段代码故意写得偏底层,没有依赖任何Agent框架,就是为了展示核心调度逻辑。真实项目里,你完全可以用成熟框架替代chat_func的实现,但外层的技能选择、执行、结果回填逻辑是通用的。
3.3 调度策略二:关键词与规则目录兜底
并不一定所有交互都要经过大模型。很多确定性场景,比如用户说“帮助”或“你好”,我可以直接走规则路径,不走模型推理。我在技能库里加了一个简单的能力:每个技能注册时可以带一个keywords列表,命中关键词就直接触发,未命中才走模型调度。
@skill( name="show_help", description="显示帮助信息", parameters={}, keywords=["帮助", "help", "怎么用"] ) def show_help() -> str: return "我可以帮你查询天气、整理文档、搜索资料。"这种“规则优先、模型兜底”的设计能显著省token,也让高频场景更稳定。尤其在一个技能需要快速响应时,路由延迟几乎是零。要注意的是,关键词路由只适合意图极明确的场景,如果用户输入“你能干什么”,关键词“帮助”不一定会触发,最后还是交给模型判断。
3.4 调度策略三:基于描述的向量检索
技能数量超过二三十个以后,把全部技能清单塞进系统提示词,既浪费token,也可能让模型漏选。我后来做了一版优化:把每个技能的描述向量化,用户请求进来先做一次语义召回,只把Top K个技能描述发给模型。这个方案适合技能库比较大的场景,比如你要做一个企业内部的助手,技能可能有上百个。
向量召回我用的方案是:用通用Embedding模型给技能描述生成向量,存在本地缓存;用户问题来了,用同样模型生成向量,计算余弦相似度取Top K。由于技能描述里已经写清楚了“什么场景使用”,召回效果相当不错。这一步本质上是做粗筛,模型在Top K里做精排选择。需要注意的是,向量模型的选择会影响召回质量,尽量选择在中文意图理解上表现较好的模型,并且定期检查工具描述是否需要更新,否则描述和实际功能不匹配时召回会失败。
4. 技能执行与组合:从单技能到工作流
技能不仅能单独被调用,更重要的一点是可以被组合。组合的形态可以是简单的顺序执行,比如先查天气、再根据天气生成穿衣建议;也可以是带条件的编排,比如如果用户问题里包含“对比”,我就同时调用两个搜索技能再把结果合并。
4.1 执行器:统一入口,统一异常处理
我实现了一个execute_skill函数,所有技能调用都走这一条路。它的职责包括参数校验、调用处理函数、捕获异常、记录日志、格式化返回。
def execute_skill(name: str, arguments: Dict[str, Any]) -> str: if name not in SKILL_REGISTRY: return json.dumps({"status": "error", "error": f"未知技能: {name}"}, ensure_ascii=False) meta = SKILL_REGISTRY[name] try: result = meta["handler"](**arguments) SKILL_REGISTRY[name]["stats"]["calls"] += 1 if not isinstance(result, str): result = json.dumps({"status": "ok", "data": result}, ensure_ascii=False) return result except TypeError as e: SKILL_REGISTRY[name]["stats"]["errors"] += 1 return json.dumps({"status": "error", "error": f"参数错误: {e}", "expected": meta["parameters"]}, ensure_ascii=False) except Exception as e: SKILL_REGISTRY[name]["stats"]["errors"] += 1 return json.dumps({"status": "error", "error": f"执行失败: {e}"}, ensure_ascii=False)这个执行器最核心的价值是“让模型拿到结构化错误信息”。Agent最怕的不是技能报错,而是技能报错后模型不知道发生了什么。把错误信息和期望参数返回给模型后,很多情况下模型会自动换参数重试,或者向用户解释失败原因,整条对话就不容易断掉。
4.2 技能组合:用另一个技能去编排已有技能
一种非常灵活的扩展方式,是把一个组合技能也注册成普通技能,它的内部去调用其他技能。比如recommend_activity这个技能,内部先调用fetch_weather,再根据天气规则返回建议。这种组合技能对模型来说只是一个技能,不用操心组合逻辑,内部编排完全由代码控制。
@skill( name="recommend_activity", description="根据城市天气推荐适合的户外活动。当用户询问天气适合做什么时使用。", parameters={ "type": "object", "properties": { "city": {"type": "string", "description": "城市名"} }, "required": ["city"] } ) def recommend_activity(city: str) -> str: weather = execute_skill("fetch_weather", {"city": city}) # 这里简化处理,实际要做JSON解析 if "晴" in weather: return f"{city}天气晴朗,适合跑步、野餐或骑行。" return f"{city}当前天气一般,建议室内活动,比如逛博物馆或看电影。"组合技能的思路很像代码里的“函数复用”。设计上要注意:组合技能内部调用的子技能,仍然要记录日志和统计,否则问题出现时只能看到顶层技能失败,很难定位到是哪一步挂了。我在日志里给每次execute_skill调用增加了一个trace_id,这样组合技能的所有子调用都能串联起来。
4.3 异步任务与重试策略:别让阻塞型技能拖垮对话
有些技能执行时间比较长,比如下载一个文件、批量处理图片、调用外部API。如果同步阻塞在那里,用户会感觉对话卡住了。我的处理方案是:技能注册时加一个async_mode选项,执行器发现这个选项后就先返回“任务已开始,任务ID是xxxx”,后台用线程池执行真正的处理逻辑,等模型下一轮再通过query_task_status技能查询结果。
from concurrent.futures import ThreadPoolExecutor task_pool = ThreadPoolExecutor(max_workers=4) TASK_STORE = {} def submit_async_task(name: str, arguments: Dict[str, Any]) -> str: task_id = uuid4().hex future = task_pool.submit(execute_skill, name, arguments) TASK_STORE[task_id] = {"status": "running", "future": future} return json.dumps({"status": "started", "task_id": task_id}, ensure_ascii=False)模型在后台执行期间可以继续和用户聊天,需要结果时调用技能查任务状态。这个设计看起来简单,但实际极大改善了长耗时技能的用户体验。另外,对于外部API请求类的技能,我还会在内部做超时控制和指数退避重试,最多重试3次,避免临时网络抖动导致整个Agent流程失败。
4.4 工作流状态机:比组合技能更进一步的编排
当技能编排需要多个步骤且有分支时,我会在技能库之上加一个轻量状态机。状态机里的每个节点都对应一个技能执行动作,边表示条件转移。比如一个“项目周报生成”流程:先读取项目数据,再检查是否有异常指标,有异常则生成警报段落,最后调用文档生成技能输出周报。流程中的每个节点都记录状态,失败时可以安全重试当前步骤,不用重新跑全部流程。
状态机的好处是流程可视化且可复现。我在项目里用它处理所有多步骤、有条件的任务。但状态机也有代价:配置比硬编码要复杂,所以我的原则是“三步以内用组合技能,三步以上且分支较多时上状态机”,不要一上来就整重工具。
5. 实操示例:搭一个可用的技能库服务
前面讲了这么多设计,这一节给一套可以直接跑起来的最小示例。我会把核心文件结构和完整代码都贴出来,你照着建就能用,然后根据自己的业务往里面加技能即可。
5.1 项目目录结构与依赖
我推荐的最小结构是这样:
agent-skills/ ├── skills_core.py # 注册表、执行器、调度循环 ├── skills_loader.py # 自动加载skills目录下所有技能 ├── skills/ │ ├── __init__.py │ ├── weather_skill.py # 示例技能 │ └── help_skill.py # 示例技能 ├── main.py # 命令行交互入口 └── requirements.txt依赖我控制在最少的三个库:openai或任意模型SDK(用于对话)、requests(用于HTTP调用)、numpy或任意向量计算库(用于向量检索,可选)。不需要引入高重量级Agent框架,因为核心调度逻辑自己写也就一百行左右。
5.2 完整代码:注册、加载、执行、交互
核心代码前三节已经给了一部分,这里补全skills_loader.py和main.py。
技能加载器:
# skills_loader.py import importlib import pkgutil import skills def load_all_skills(): for module_info in pkgutil.iter_modules(skills.__path__): if module_info.name.startswith("_"): continue importlib.import_module(f"skills.{module_info.name}")命令行交互入口:
# main.py from skills_core import run_agent_once, get_skill_list_for_llm from skills_loader import load_all_skills def mock_chat(messages): """这里用你自己的模型接口替换。mock版本只用于展示调度流程。""" last_user = messages[-1]["content"] if "天气" in last_user and "skill" not in last_user: return {"skill": "fetch_weather", "arguments": {"city": "北京"}} if "技能" in last_user or "help" in last_user: return {"content": "你可以问我:北京天气怎么样?"} return {"content": "我还不理解这个问题。"} if __name__ == "__main__": load_all_skills() print("技能列表:") print(get_skill_list_for_llm()) print("---") output = run_agent_once("北京天气怎么样?", chat_func=mock_chat) print("最终回复:", output)你实际使用的时候,把mock_chat替换成真实的LLM调用,并注意处理好流式输出与多轮上下文。这个最小实现已经能让你体会完整的技能调度闭环了。
5.3 技能库的扩展:加上向量检索与多级路由
当技能增加到三四十个以后,我在skills_core.py里增加了向量召回模块。核心思路是:初始化时把所有技能描述编码为向量,请求进来后取用户问题的向量,用余弦相似度找出Top K。这一步只做粗筛,给模型看的技能清单从全部技能变为“候选技能 + 说明”。
def get_relevant_skill_list(query: str, top_k: int = 5) -> str: # 假设已经有一个 embed_func query_vec = embed_func(query) scored = [] for name, meta in SKILL_REGISTRY.items(): vec = meta.get("embedding") if vec is not None: sim = cosine_similarity(query_vec, vec) scored.append((sim, name)) scored.sort(reverse=True) selected = [name for _, name in scored[:top_k]] lines = [] for name in selected: meta = SKILL_REGISTRY[name] lines.append(f"{name}: {meta['description']} 参数:{json.dumps(meta['parameters'], ensure_ascii=False)}") return "\n".join(lines)把系统提示词里的get_skill_list_for_llm()换成get_relevant_skill_list(user_query),路由效果会有显著提升。实际项目中要注意:向量模型对技能描述的中文表达要够好;技能描述里不要放场景无关的废话,否则召回分数会被噪声拉低。
5.4 可观测性设计:每一次技能调用都要有迹可循
我专门在技能库里加了一个日志装饰器,所有技能执行都会自动记录:调用时间、技能名、参数、返回状态、耗时。这样评估Agent效果时,直接看日志就能知道是哪一步选错了、哪一步报错了、哪一步耗时太长。
日志示例:
{"time": "2025-01-01 12:00:01", "skill": "fetch_weather", "args": {"city": "北京"}, "status": "ok", "elapsed": 0.32}除了结构化日志,我还会记录每一轮Agent调用的完整消息序列。复盘的时候打开对话ID,能看到模型思考路径、技能选择、执行结果。这个习惯帮我发现了不少隐藏问题,比如模型反复调用同一个失败技能,或者技能A的结果一直在被错误传给技能B。
6. 常见问题与排查技巧实录
技能库在实操中会遇到很多看起来很玄学的问题。这里挑几个我真实踩过的坑,按问题现象、排查思路、解决方案的顺序整理成速查表。
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 模型经常选错技能 | 技能描述太模糊或重叠度高 | 查看日志中用户query和实际选择 | 重写描述,增加“不使用”场景;拆分技能 |
| 参数传递总是缺字段 | 参数Schema没有给全 | 查看执行器捕获的TypeError | 每个参数在Schema里写明必填、默认值、示例 |
| 技能执行成功但回复不对 | 返回结果格式不适合模型阅读 | 打印技能返回原文,看是否包含完整语义 | 让技能返回简洁自然语言,而不是裸JSON结构 |
| 新技能不生效 | 模块未被加载 | 确认技能文件是否被skills_loader扫描到 | 检查文件是否在skills/目录,模块名是否以_开头 |
| 并发下技能状态错乱 | 共享变量或全局状态未加锁 | 复现并发场景,查看日志中的request_id | 技能内部避免修改全局状态;必要时引入contextvars |
| 向量召回总是漏掉正确技能 | 技能描述和用户表达语义差距大 | 单独测试query与各技能相似度 | 扩充技能描述的同义词、典型提问样例 |
6.1 模型“看不见”技能或者死活不调用,怎么办
这个问题最常见的根源是技能清单里描述写得太简略,模型不确定当前场景是否该调用。另一个原因是你给模型看的技能清单格式过于复杂,被系统提示词淹没。我的做法是:单独开一个“工具区”段落,把技能清单一字排开,前后加明确分隔线,再给一两个“什么时候用”的示例。另外,模型回复如果不包含调用结构,就用正则或提示词强制格式约束,让模型必须输出{"skill": ...}结构,最稳妥的方案是使用JSON输出模式并关闭发散输出。
6.2 参数类型错误防不胜防
即使有参数Schema,模型还是会时不时把city传成带引号的列表、把数字传成字符串。我在执行器里加了轻量类型归一化工具:如果Schema声明某个参数是string,但模型传了array,我就取数组第一个元素;如果声明integer,但传了"5",我就尝试转int。这个处理能救回很多本该失败的调用,但要注意在日志里标记“参数已归一化”,方便后续排查。我自己不敢让归一化太激进,遇到无法处理的情况就返回错误信息,让模型自己纠错。
6.3 技能组合时上下文丢失
组合技能内部调用子技能后,子技能的返回结果只作为字符串交给了外层技能,等外层技能再执行时,子技能的中间状态已经丢了。解决办法是把必要状态放到上下文字典里,或者让子技能把结构化结果也一并放入日志。我在设计中让execute_skill支持一个context参数,子技能可以往里面写入临时变量,组合技能流程结束后清理。 这个做法让多步骤任务里的信息传递不再是临时的字符串拼接。
7. 一些项目上最好早点知道的事
技能库方案听起来简单,但真正落地时有一些项目层面的经验还是值得提前了解。这里再补几个我没在别处见到的点。
第一点,技能库要“易删”而不是“易增”。很多人看到一个新场景就急着加技能,结果技能越堆越多,后期维护成本飙升。我给自己定的规矩是:一个技能如果连续两周没有被调用,就考虑下线或合并。下线不是删除代码,而是从注册表里标记active=False,模型就看不到了,随时可以恢复。
第二点,技能评估离不开“黄金语料集”。我会准备五十条左右的标准用户问题,每条标注期望调用的技能和参数,每次改动技能描述或新增技能后,用这批语料跑一遍调度,统计选择准确率。没有这个回归测试,你很难判断改动到底把效果变好了还是变差了。我一开始偷懒没做,后来加了一批语料,立刻发现好几个技能改动其实都是无效甚至负面的。
第三点,技能描述是产品的一部分。别让技能描述变成工程师自嗨的技术说明。我后来让团队里非技术角色也参与描述评审,因为他们的表达更接近真实用户的问法。比如程序员写“获取远程服务器磁盘使用率”,产品同事改成“当用户反映电脑空间不足时,查询服务器磁盘剩余空间”,模型选择准确率立刻上来了。
第四点,不要在技能函数内部依赖外部环境的全局状态。Agent应用经常会做并发处理,如果技能里用了全局变量缓存,又没加锁,很容易出现数据串台。我在早期踩过一次,两个人同时问天气,结果参数互相覆盖。后来所有技能函数内部都只使用传入参数,外部状态一律通过上下文注入。这一点在开发时就要立好规矩,别等出了问题再改。
8. 扩展方向:从技能库到完整Agent平台
agent-skills这套东西做完以后,我当时只觉得它是一个工具集,但后面用得越深越发现,它天然可以向上扩展成一个更完整的Agent能力底座。
一个自然的扩展方向是增加“技能学习”能力。比如用户在对话中给了一个新的操作流程,Agent可以把流程中的步骤结构化,自动生成新的技能描述并注册到技能库。早期的做法是人工写代码添加技能,进阶的做法是让大模型生成参数Schema和描述文本,人工确认后入账。我这里目前是半自动,模型生成候选技能,我审核后一键启用,效率比纯手写好很多。
另一个方向是给技能增加权限控制。企业场景里,不同角色能调用的技能不一样。比如普通用户可以使用查询天气技能,但只有管理员可以调用用户管理技能。在技能注册表里加一个permission字段,执行器里做鉴权判断,整个方案就很容易接入现有权限体系。
还有一个方向是技能运行时的可视化。给每次技能调用加上Trace ID,然后在界面上展示技能调用链路、耗时、失败点。我见过很多商用平台都有这个能力,但自己动手在技能库上实现一层也不复杂,核心还是在日志和上下文结构上留好字段,后面想怎么展示都行。
大体上,技能库的边界可以很灵活:小到日常脚本的AOP扩展,大到多智能体协作的底层能力单元。关键是先把基础设计做对,后面的扩展就不会到处推到重来。