☰
AI Agent技能管理:让大模型稳定调用工具的工程实践
2026/10/13 23:52:17 网站建设 项目流程

做AI Agent应用最头疼的往往不是模型本身的智商,而是模型能不能“手脚协调”——明明知道该干什么,却不会用你给的工具;或者工具一大堆,模型每次都挑错;又或者你写好了十几个函数,模型一遇到边界情况就乱传参,结果线上疯狂报错。

我在做一个内部智能助手项目时,被这类问题反复折腾,最后沉淀了一个叫agent-skills的技能管理模块。它的核心思路很简单:不要把你代码里的每个函数都直接丢给大模型,而是把它们包装成一份份“技能说明书”,由一套独立的技能运行时来负责注册、描述、调度和结果反馈。模型只跟这套技能层对话,不直接触碰底层实现。这个方向解决了我遇到的大部分实际问题:技能发现率提高了,参数幻觉少了,混用多个工具的场景也稳了。这篇文章就把我整个设计和实践过程掰开讲清楚,希望能给同样折腾Agent落地的朋友一些参考。

这篇内容更适合有一定大模型应用开发经验、正在做Agent或复杂工具调用的开发者。如果你刚接触这个概念,文中也会有比较基础的原理解释和完整代码示例,跟着捋一遍也能跑起来。

1. agent-skills 到底是什么,为什么需要一个独立的技能层

1.1 从“调工具”到“拥有技能”的视角转变

先聊聊我为什么不用现成的“工具调用”方案,而要绕一圈去做“技能层”。

常规做法里,大模型应用会给模型提供一堆函数定义,包括函数名、参数类型、描述等信息,模型在需要时根据这些定义去决定“调用哪个函数,填什么参数”。这种方式对简单场景是够用的,比如就两三个函数,每个函数参数也就一两个。但一旦函数数量超过十五二十个,或者单个函数的参数比较复杂,问题就来了:模型经常在多个相似函数之间犹豫,甚至把A函数的参数硬填给B函数。

我后来换了个思路:与其让模型去理解“函数”,不如让它理解“技能”。“技能”是一个更高层次的概念,它包含着名称、用途、适用条件、参数规范、限制事项等更完整的语义信息。模型不需要纠结“我该调哪个函数”,它只需要判断“现在需要完成什么任务,哪个技能匹配这个任务”。这个视角的转变,本质上把“函数选择的负担”从模型身上部分转移到了技能库本身——通过精心设计的描述和格外的调度逻辑,让选择过程变得更可靠。

1.2 项目要解决的四个核心痛点

我建设agent-skills之前,专门梳理过当时的麻烦,基本上可以归纳为下面四类:

  1. 技能不发现:函数描述写得不够好,或说明里用了模型不认识的术语,导致模型明明有对应能力却不用。比如你有个“查询订单物流”的函数,描述写成“getLogisticsInfoByOrderId”,模型根本不知道这跟“查快递”有什么关系。

  2. 参数幻觉:模型在JSON里编造不存在的参数,或者把枚举值传成别的写法。最典型的是一次用户说“帮我提醒我明天早上八点开会”,模型直接生成了一个“remind_time”字段,而你的函数字段叫“alert_at”。

  3. 上下文污染:每多一个工具,系统提示词里就要多塞一份函数定义。工具到了30个以后,光定义就有上万token,模型注意力被稀释,指令遵循能力肉眼可见地下降。

  4. 错误恢复差:函数抛出异常时,模型不知道发生了什么,下一次继续用同一种错误方式调用,形成死循环。

agent-skills就是冲着这四件事去的。它做了一件事:把所有能力抽象成“可描述、可注册、可调度、可反馈”的完整技能实体,而不是一个个孤立的函数。每个技能除了可执行代码,还有一套“元数据”——描述、参数Schema、使用策略、失败反馈模板。这套设计让模型在绝大多数情况下只跟“少而精”的技能清单打交道,而不是跟几十个裸函数搏斗。

1.3 适合谁参考

如果你正面临下面任一情况,这篇内容会比较对路:

  • 你的Agent需要调用超过10个工具,且未来还会继续增加。
  • 模型经常选错或不选工具,而你不想一味换更大参数的模型。
  • 你希望工具的接入门槛降低,最好是让非算法工程师也能在配置文件中加一个新技能。
  • 你对“Agent可靠落地”有要求,不能接受线上出现反复的调用异常。

这类问题的共同点在于:瓶颈不在模型能力,而在工程侧如何组织、描述、调度能力。agent-skills恰好就是围绕这一点设计的。如果你的场景只是两三个固定工具,那直接上函数调用就够了,不需要这篇的复杂度——这点前置说明很重要,别被我带偏,过度设计也是坑。

2. 技能库的整体设计与模块划分

agent-skills不是一个单文件的小工具,它内部拆成了四个相对独立的模块,职责边界比较清楚。我在设计时参考了很多主流Agent项目的做法,也踩过不少坑,最终定型的结构如下。

2.1 技能描述层:模型“看懂”技能的说明书

技能描述层解决的是“模型怎么知道有这个技能、什么时候该用”的问题。每一个技能都对应一段结构化的描述,包含下面这些字段:

字段作用我踩过的坑
技能名称(skill_name)技能的唯一标识,必须语义化起名太短或太通用,如process,模型会搞混
技能说明(description)告诉模型这个技能是干什么的,什么时候用写得太长太啰嗦,模型反而不抓重点
适用场景(when_to_use)列举典型的触发场景、用户话术示例一开始没写,模型经常在不该用的场景强行调用
参数Schema(parameters)定义参数名、类型、取值约束、默认值写过一次枚举值不写枚举列表,模型自由发挥乱传参
使用禁忌(not_to_use)明确什么情况禁用这个技能不写则相似技能极易冲突
返回值摘要(returns)说明调用后会返回什么形态的数据忽略后模型不会正确解读返回结果

我把这段描述称为“技能的说明书”。它最大的作用是让技能筛选变成一次纯文本匹配问题:模型读这段说明书,判断“当前用户请求是否落在该技能的适用范围内”。只要说明书质量够高,模型做这个判断比做“从多个函数签名中选一个”要稳得多。

写说明书有几个很实用的技巧,我代码里也一直在用:

  • 说明和适用场景中的用词,尽量模仿真实用户的表达习惯。比如一个“查询天气”的技能,适用场景里写“用户提到天气/气温/下雨/出行穿衣建议”,模型命中率明显提升。
  • 每个技能都要写“不适用”的场景。比如“查天气”技能里注明“如果用户问的是历史气候趋势,请交给气候数据分析技能”。这一点特别能降低相似技能之间的冲突概率。
  • 参数Schema里不要只写类型,还要写“如何从用户话术中提取这个参数”的提示。例如一个“remind_at”参数,描述里写“用户输入的绝对时间或相对时间,如‘今天下午三点’需解析为具体时间戳”。

2.2 技能注册层:把函数变成可发现的技能

注册层是技能层与底层代码之间的桥。它的职责是把一个普通函数、一个API封装、甚至一段配置好的Prompt模板,统一包装成技能对象,并登记到技能库中。

我倾向于用装饰器或者注解的方式来完成注册。打个比方:

@agent_skill( skill_name="check_weather", description="查询某城市最近几天的天气情况", when_to_use=[ "用户询问天气、气温、是否会下雨", "用户行程安排中涉及出行穿衣建议", "用户说今天/明天/周末天气如何" ], not_to_use=[ "用户询问的是气候统计趋势,而非实时天气预报" ], parameters={ "city": {"type": "string", "description": "城市名称,如北京、上海,用户可能使用简称", "required": True}, "days": {"type": "integer", "description": "查询未来几天的天气,默认为1,取值范围1-7", "required": False} }, returns="返回JSON,包含城市、日期、天气状况、最高气温、最低气温、建议" ) def check_weather(city: str, days: int = 1): # 真实业务逻辑... return {"city": city, "days": days, "forecast": []}

这里的装饰器其实就是“把函数包装成技能对象”的语法糖。技能对象内部除了保存函数引用和说明字段外,还会做几件额外的事:

  • 参数校验:在调用前对模型传来的参数做类型检查和枚举校验,不合法就立刻返回可读错误,而不是让底层函数抛异常。
  • 字段名映射:有些函数参数名不直观,但你又不能随便改底层代码。注册时可以做一次映射,比如底层函数参数是c,对外技能参数叫city。
  • 权限/限流检查:如果技能涉及敏感操作,注册时可以挂上权限标识;涉及外部API,可以在这一层做统一限流。

注册层还会维护一份索引,记录每个技能的描述片段。这个索引用在前面说的技能筛选中,模型只需要看这个索引就能快速判断技能存不存在,而不用翻完整代码。

2.3 调度与执行层:谁来决定调哪个技能

调度层解决“面对多个技能,到底调哪个”的问题。我试过两种调度方式,最后根据场景混合使用。

方式一:模型直接选择(LLM Router)

把技能清单(通常只保留描述,不保留完整参数Schema)交给模型,让模型从里面挑最匹配的一项,并给出参数JSON。这是最灵活的方式,适合技能多、场景开放、难以写死规则的情况。缺点是会增加一次模型请求,而且模型仍可能选错。我一般把技能清单的长度控制在20个以内,太多模型就记不住了。

方式二:规则优先(Rule-based Router)

对一组高确定性场景,先做关键词/意图匹配。比如用户话术中包含“天气”一词,规则层直接锁定天气技能,跳过大模型选择环节。这样做既省延迟,又更稳。规则优先层本质是给技能库加了一个“快捷键”。

我最终采用的是两层结合的调度策略:

  1. 先做规则层匹配,命中高置信度规则,直接执行。
  2. 未命中规则时,进入模型选择层,把技能清单(名称+适用场景)发给模型做选择。
  3. 模型选择完毕,参数抽取再走一次模型或模板解析,然后进入校验。

纯规则层的准确性远高于模型选择层,但在长尾场景中覆盖不足;模型选择层能活得糙但覆盖面广。两层的组合,是我调试了两个月之后找到的成本与效果平衡点。

2.4 结果反馈层:让模型知道技能调用到底发生了什么

这一层容易被忽视,但恰恰是Agent可靠性的关键。

以前模型调用函数后,我直接把函数返回值塞给模型,不附加任何说明。结果模型经常把一段JSON错误地理解成它自己该输出的内容,或者把它当成对话上下文继续瞎编。现在我明白了:模型不是缺乏信息,而是缺乏“如何理解这段信息”的指引。

在agent-skills里,每个技能执行完毕后,返回给模型的内容会被包装成统一的结果对象:

{ "skill": "check_weather", "status": "success", "execution_time_ms": 234, "result_summary": "北京今天的天气是晴,最高26度,最低15度,建议穿薄外套", "data": { "city": "北京", "condition": "晴", "high": 26, "low": 15 } }

如果执行失败,结果对象是这样:

{ "skill": "check_weather", "status": "failed", "error_type": "parameter_invalid", "error_message": "参数city不能为空", "suggestion_to_model": "请向用户询问所在城市名称,或者使用历史会话中已提到的城市信息" }

这个结果对象有三个妙处:

  1. 模型知道这是技能结果,而不是用户话语。状态标识清晰,模型不会混淆。
  2. result_summary 把冗长的原始数据提炼成一句话摘要。这对节省上下文和提升模型理解都很有帮助。
  3. suggestion_to_model 直接告诉模型下一步该怎么走。错误恢复从“重新猜”变成“按提示修正”,可靠性大大提升。

我一直坚持一个原则:技能运行时要像个“可靠的同事”,不但能干活,干完活还要会“汇报”,干砸了也要会“说明原因和改进方案”。能做到这一点,Agent 的崩溃率会明显下降。

3. 实操:从零搭一个可用的技能库

这一节我拆解一套最小可跑通的实现,代码是Python风格,但思路完全可以迁移到其他语言。核心就四个文件:技能对象类、注册器、调度器、Agent主循环。

3.1 定义技能的统一Schema

先定义一个技能数据类,这里我用Python的dataclass做容器:

from dataclasses import dataclass, field from typing import Callable, Any, Optional, List @dataclass class Skill: name: str description: str when_to_use: List[str] not_to_use: List[str] parameters: dict func: Callable returns: str = "" param_mapping: dict = field(default_factory=dict) required_permission: Optional[str] = None

这个类就是技能的统一Schema。后续注册、筛选、调用、反馈全部围绕这个类进行。参数里的param_mapping我实际用得非常频繁,它专门用来适配底层函数和对外参数名不一致的情况。

@dataclass class SkillResult: skill: str status: str # success / failed result_summary: str data: Any = None error_type: str = "" error_message: str = "" suggestion_to_model: str = ""

SkillResult就是结果反馈层的载体。注意result_summary和suggestion_to_model这两个字段,它们虽然不直接参与业务,但对模型行为影响极大,我在后面会专门讲怎么生成这些字段。

3.2 实现注册器与技能发现

注册器维护一个技能总表,并提供三种查询能力:

class SkillRegistry: def __init__(self): self._skills = {} self._descriptions = [] def register(self, skill: Skill): self._skills[skill.name] = skill self._descriptions.append({ "name": skill.name, "description": skill.description, "when_to_use": skill.when_to_use, "not_to_use": skill.not_to_use }) def get(self, name: str) -> Optional[Skill]: return self._skills.get(name) def all_skills(self) -> list: return list(self._skills.values()) def descriptions_for_llm(self) -> list: # 这段列表是在模型路由层给模型看的内容,刻意去掉了parameters细节 return self._descriptions

这个注册器很简单,但有几个容易忽视的细节:

  • descriptions_for_llm输出的内容刻意不包含完整参数Schema。这样模型在一轮思考中看到的token量更少,注意力更集中。
  • 技能索引在每次注册后重建,为的是后面做关键词检索时不用每次遍历技能表。技能多了以后一定要加这一步,否则性能会很难看。

我还顺手封装了一个装饰器,让技能注册的代码更简洁。装饰器本质就是实例化Skill并调用register:

from functools import wraps def agent_skill(skill_registry: SkillRegistry, **skill_meta): def decorator(func): skill = Skill(name=skill_meta["name"], func=func, **skill_meta) skill_registry.register(skill) @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator

这样一个业务函数加上装饰器,就立即成为技能库的一员。团队里其他人不需要理解内部机制,照着现有技能抄一份描述就能接入新能力。

3.3 接入大模型调用循环

接下来是核心的Agent执行循环。我简化一下,但保留关键流程:

class AgentLoop: def __init__(self, registry: SkillRegistry, llm): self.registry = registry self.llm = llm def run(self, user_message: str, history: list): # 第一步:判断是否需要技能 intent = self.analyze_intent(user_message, history) if intent == "chat": return self.llm.chat(user_message, history) # 第二步:规则层调度 selected_skill = self.route_by_rules(user_message) if selected_skill is None: # 规则没命中,交给模型路由 selected_skill = self.route_by_llm(user_message) if selected_skill is None: # 没有匹配到任何技能,走普通对话 return self.llm.chat(user_message, history) # 第三步:参数抽取与校验 params = self.extract_params(user_message, selected_skill) # 第四步:执行并包装结果 result = self.execute_skill(selected_skill, params) # 第五步:把结果反馈给模型,生成最终回答 return self.llm.chat(user_message, history, skill_result=result)

我把五步拆开实现,每一个环节都可以单独调试。实际项目里最常见的问题是:不是模型选技能选错了,而是参数抽取那一步出的错。所以我在extract_params里做了两层校验,先做规则提取,再做模型补全,没法通过校验就返回“缺参数”而不是硬调用。

def execute_skill(self, skill: Skill, params: dict) -> SkillResult: try: # 参数映射 real_params = {} for k, v in params.items(): real_key = skill.param_mapping.get(k, k) real_params[real_key] = v data = skill.func(**real_params) summary = self.generate_summary(skill, data) return SkillResult( skill=skill.name, status="success", result_summary=summary, data=data ) except Exception as e: return SkillResult( skill=skill.name, status="failed", error_type=type(e).__name__, error_message=str(e), suggestion_to_model=self.build_failure_suggestion(skill, e) )

generate_summary和build_failure_suggestion是两个容易被低估的函数。一个把结构化数据转成一句话摘要;一个把异常信息转换成模型能直接使用的改进指引。这两个函数写得好不好,比模型本身的调优还影响效果。

3.4 一个完整例子:查询天气 + 设定提醒的组合技能

为了演示技能之间的联动,我用一个组合场景:用户说“北京明天会下雨吗?如果下的话,明早提醒我带伞”。

这个请求同时涉及“查天气”和“设定提醒”两个技能。模型选择品牌可能要两次调用。我在技能库中加了一个“编排型技能”,它内部可以串联其他技能:

@agent_skill( name="arrange_daily_plan", description="根据天气安排出行提醒", when_to_use=["用户希望根据天气设置提醒", "用户提到带伞/加衣/是否需要出行装备"], not_to_use=["用户只问天气不设置提醒"], parameters={ "city": {"type": "string", "description": "城市名"}, "date": {"type": "string", "description": "要查询的日期"}, "remind_time": {"type": "string", "description": "提醒时间,如明早8点"} } ) def arrange_daily_plan(city: str, date: str, remind_time: str): weather = registry.get("check_weather").func(city=city, days=1) if "雨" in weather.get("condition", ""): registry.get("create_reminder").func( remind_at=remind_time, content=f"今天{city}有雨,记得带伞" ) return {"action": "reminder_created", "weather": weather} return {"action": "no_reminder_needed", "weather": weather}

在技能库里加这种“编排技能”,本质是把一些固定的多步逻辑固化成流程,减少模型自主编排的不可控性。对于很多业务场景,我强烈建议:能固化成编排技能的,就不要让模型自由发挥。你提前想好的流程永远比模型推理时临时决定更稳定。

4. 工具选型与实现方案对比

做技能管理,实现路线不止一种。我分别试过三条路,各有取舍。

4.1 方案A:基于主流Agent框架的扩展

市面上的Agent框架大多已经内置了工具注册和调用能力。直接在上面扩展的好处是生态完善,社区里能找到大量现成工具封装。弊病也很明显:

  • 框架封装的层级太厚,出了问题不好排查,尤其涉及流式输出和异常恢复的时候。
  • 工具描述格式受框架约束,想加自定义路由逻辑得绕过框架的默认行为。
  • 要支持“编排型技能”,往往得写很多框架不接受的hack。

我最初就是这样做的,在框架里塞了二十来个工具后,发现调试成本快赶超业务开发了,才决定自建。

4.2 方案B:自研轻量技能运行时

也就是这篇文章讲的路线。优点很直接:

  • 所有行为都在掌控内,规则路由、参数校验、结果摘要、失败反馈都能定制。
  • 代码量不大,核心逻辑可以压到一千行以内,维护成本可控。
  • 没有框架依赖,迁移到其他应用时非常简单,复制核心模块即可。

缺点是要自己处理一些边角,比如并发安全、日志规范、监控埋点。这些问题用现成框架时是直接有的。对长期迭代的Agent项目来说,我认为自研的边际收益更高,尤其是你手上已经积累了一套工具之后。

4.3 我的建议

如果你的目标是快速验证几个工具联动,用方案A没有问题,省时省力。但如果你要做的是一个持续增长的Agent平台,技能数量会长期增加,建议抽一点时间做方案B。哪怕一开始用方案A,也要在设计层面向技能抽象靠拢:把工具描述集中管理,把执行结果统一包装,把路由逻辑独立出来。这样将来换方案的时候,成本很低。

还有个折中路线:用框架做底层消息循环,用自研技能库做上层调度。这个组合我实测最顺手——框架处理模型交互和流式输出,技能库专管工具编排和结果反馈。两边各取所长。

5. 常见问题与排查技巧实录

5.1 模型就是不调用技能怎么办

这是最常见的抱怨。排查顺序我建议这样来:

  1. 看技能清单是不是太长。超过20个的话,优先精简描述,并加强规则层路由。
  2. 看技能描述是否与用户话术存在“词汇鸿沟”。用户通常说“查快递”,模型找不到技能,因为你的描述里写的是“获取订单物流信息”。我处理的方式是在when_to_use里把用户高频话术原样写进去,覆盖越好,激发效果越明显。
  3. 看是不是技能说明本身被系统提示词淹没了。很多框架把工具定义跟聊天指令一起放进提示词,一旦聊天指令太长,模型更容易忽略技能列表。技能描述单独发一段,效果会好很多。

我之前有个技能叫create_reminder,模型怎么都不调用。后来我去翻了用户语料,发现用户说“记一下”远多于“设置提醒”,我把“记一下”、“别忘了”、“提醒我”写进when_to_use之后,调用率立刻翻了一倍。

5.2 参数总被模型填错怎么办

参数填错是第二大类顽疾。我带过的排查思路是:

  • 检查参数名是否符合直觉。remind_at用户说不出来,但模型容易生成time。我的解决办法是参数名用语义化命名,并保留别名映射。
  • 检查参数描述里是否给出了“提取依据”。比如remind_at的描述写清楚:支持绝对时间和相对时间,如果用户说“明早八点”,请解析为具体时间戳,然后附上示例。
  • 对枚举值,直接把限制条件写进Schema,强制校验。模型生成不允许的值时,反馈层会立刻拒绝并提示,而不是让业务代码抛异常。

参数校验必须有反馈闭环。光校验不反馈,模型不会自我修正。我强烈建议在失败反馈里明确写“你传了X,但这里只接受Y”,让模型能马上纠错。

5.3 技能多了以后上下文被撑爆怎么办

技能数量上去后,每次请求把所有技能描述全塞进提示词显然不现实。我的解法是两个方向:

  • 技能分群:把几十个技能按领域分组,比如“生活服务”“企业办公”“数据分析”。规则路由先判断领域,再只把该领域的技能描述传给模型。这样模型每次看到的技能数量会控制在很小的范围。
  • 技能索引+分步路由:第一步让模型只判断“用户请求可能涉及哪几个技能”,把候选范围缩小到3-5个;第二步再让模型在候选里做精细选择和参数抽取。多花一次请求,但换来了稳定性和上下文节约,我认为非常值。

这一步本质上是把“穷举所有技能”改成“先粗筛后精排”,跟搜索引擎的思路类似。做完整套后,几十个技能同时在线也不再是压力。

5.4 技能内部异常导致对话崩溃怎么办

技能内部异常不可避免,尤其是涉及外部API时。崩溃点往往在于:异常没有被包装,模型拿到一段原始报错后不知所措。

我的兜底策略是:所有技能执行都包在统一异常处理里,并保证返回的SkillResult永远是合法的结构化对象。模型拿到的永远是两种情况之一:成功或失败。失败时,有明确的error_type和改进建议。这样模型永远不会面对“一段看不懂的StackTrace”这种处境。

还有一点经验:技能里尽量不要直接对外抛出异常,而是返回一种“业务错误”状态码。比如查天气API返回超时,你不要让技能函数抛TimeoutError,而是返回一个带{"error": "weather_api_timeout"}的结果对象。这样反馈层能把错误翻译成模型听得懂的描述,用户体验会平滑很多。

注意:技能内部不要吞掉异常后用一些“乱码”或空值返回。宁可明确告诉模型“没查到”,也不要让它以为“查询成功但结果为空”。模型对“没有结果”和“结果确实为空”这两件事的处理逻辑完全不同,混在一起会引发很多隐蔽的误解。

6. 最后再分享一个我在落地过程中的体会

全程做下来,我最大的感触是:所谓Agent跑得稳,是“描述、路由、校验、反馈”这四个环节一起在起作用,而不是某一个大模型有多聪明。模型的能力上限当然重要,但大量实际问题是工程层可以解决的。

我建议你拿到这套思路后,先不要急着把所有功能都技能化。挑两三个你高频使用、且关系紧密的功能,用文中的代码搭一个最小闭环,观察模型的行为变化。先跑通一个小场景,再逐渐增加技能数量,你会对这个体系的理解比一次性看完整篇文章深得多。

如果之后再遇到模型行为不可控的情况,不妨先从技能说明和结果反馈这两个最不起眼的地方检查。这两个地方优化成本最低,但往往影响力最大。agent-skills没有用什么高深技术,所有设计的核心只有一个:让模型和工具之间的沟通像两个靠谱的人之间协作一样,有明确的边界、清晰的指令和及时的反馈。能做到这一点,Agent 的可用性就有了基本盘。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询