☰
从函数到技能:大模型Agent智能体落地的核心架构与实践指南
2026/10/8 4:57:52 网站建设 项目流程

先说个最近的真实感受:上个月我接手了一个内部智能体项目,需求是让大模型能主动查库存、算报价、生成合同初稿。功能点看着不多,但真正跑起来才发现,模型的"聪明"全卡在能不能准确调用工具上。折腾了几天后我才意识到,问题根本不在提示词写得够不够细,而是缺少一套完整的技能体系。这个"agent-skills"指的就是这一类东西——给智能体注册的各种可复用技能模块,从天气查询、数据库检索到多步审批流,本质上是把模型跟外部世界连接起来的标准化接口层。

这篇文章适合两类人看:一是正在搞Agent应用的开发者,想把自己散落各处的函数调用整合成可维护的技能系统;二是产品和技术负责人,想搞清楚为什么智能体demo容易做、落地难,问题的关键经常就出在技能层的设计上。我会把技能模块的架构、注册路由、参数约束、错误处理这些环节拆开讲,附上能直接抄的代码和踩坑记录,尽量让看完的人能自己搭一套。

1. 为什么agent-skills成了智能体落地的关键

先想一个问题:一个没有技能的GPT类模型,能做什么?它能对话、能总结、能写代码片段,但一旦碰到实时数据、业务系统、物理动作,它就彻底抓瞎。因为模型本质上是"离线推理器",它的知识停留在训练截止那一刻,没法自己查数据库,也没法调外部API。你可以在提示词里塞一堆操作指令,但那样做很快就会碰到三堵墙:上下文窗口有限、逻辑不可复用、行为不可测试。

技能(skills)解决的正是这三件事。一个技能就是一组"行为封装",对外暴露的只有名字、描述和参数接口,内部实现可以是任意代码。这样做的好处很直接:

  • 复用性强。同一个"查库存"技能,对话机器人能用,自动化脚本能用,报表任务也能用,不用重写。
  • 可测试。技能是独立的函数单元,可以单独写单元测试、mock外部依赖,跑完再挂到Agent上。
  • 可控。可以给技能加权限、加确认机制、加超时和重试策略。这些限制塞在提示词里几乎管不住,但变成代码就完全在掌控中。

我在实际项目里的体会是,技能的边界感决定了智能体的下限。模型会不会胡说八道、会不会调用错误工具、会不会在关键操作上犹豫不决,其实很大程度取决于你提供给它的技能清单是否清晰。模型不是万能的调度器,它更像一个实习生,你给它一份写清楚的工具目录,它能干得漂亮;你给它一堆含糊的功能描述,它就乱猜。

还有个常被忽略的点:技能体系让整个Agent的行为变得可观测。在没有技能层的时候,模型走一步算一步,中间过程无法审计;有了技能调用日志,每一步调用什么工具、传了什么参数、返回了什么结果,全都记录在案。这在调试和合规场景下是刚需,尤其是涉及报价、合同、审批这类敏感业务时。

2. 技能模块的标准架构:一个技能由哪几部分组成

我拆过几个开源Agent框架,也自己从零写过一套,最后沉淀出来的技能模块结构基本是稳定的。它由六个部分组成,缺一个后面维护都会难受。

2.1 技能的基本六要素

要素作用举例
名称(name)模型和编排器定位的唯一标识search_inventory
描述(description)告诉模型"什么时候该用我",这是路由的入口查询仓库即时库存,支持按SKU或类别过滤
参数定义(parameters)用JSON Schema声明模型需要填哪些字段{ sku: string, category?: string }
执行函数(execute)真正干活的代码,内部可以调API、查库def execute(sku: str) -> dict
校验器(validator)调用前检查参数合法性sku必须匹配 [A-Z]{4}\d{6}
安全策略(safety)权限、超时、重试、二次确认等写操作必须人工确认,超时5秒

很多人只写前四项,把校验和策略省了。短期demo没问题,一旦上生产就出事。我见过一个例子,模型把"查张三的订单"里的参数理解成了手机号,校验器要是提前拦住,后面一系列错误都不会发生。

2.2 描述到底怎么写:路由效果的分水岭

技能描述是给模型看的说明书,不是给人看的API注释。这里有个真实的对比,我调试时候遇到过:

  • 差劲的描述:查询天气
  • 稍微好一点:根据城市名称查询实时天气
  • 好用的描述:查询指定城市当前天气和未来三天的预报。当用户提到天气、温度、降雨、出门要不要带伞时使用。城市参数请用中文全称,例如北京、上海,不要用拼音或简称

看出差别了吗?好用的描述做了三件事:说明了功能边界、给出了触发场景、约束了参数格式。模型读完这种描述,匹配准确率能提高不少。因为大模型的工具选择本质上是语义匹配,你给的文本越贴近用户口语,它越容易命中。

2.3 技能和普通函数调用有什么区别

可能有人会问:我写个函数然后让模型调用,这不就是技能吗?其实还差一层。普通函数调用是"点对点"的,模型被告知"必须用这个函数",不存在选择问题;而技能体系要考虑的是"模型自主决定用哪个"。这个差别导致技能必须额外具备两个能力:一是"自我描述"能力,让模型能基于描述做判断;二是"注册发现"能力,让技能能被动态挂载和枚举。

我用一个生活类比解释:普通函数调用像你把菜谱直接摊开告诉厨师"按这个做";技能体系是给厨师一本菜单,上面写着每道菜的做法要点、适合什么场合,让他自己选。后者灵活,但对菜单的编写质量要求更高。

3. 手写一套技能框架:从注册到执行的完整链路

理论讲完,直接上代码。我用Python写一个极简但够用的技能系统,覆盖注册、路由、校验、执行、错误处理五个环节。你可以直接抄回去改,也可以拿它当骨架理解主流框架的实现思路。

3.1 技能注册表的设计

注册表是所有技能的中枢。它负责收集技能定义、生成模型可读的工具清单、分发调用。先定义基础数据结构:

# skill_registry.py from dataclasses import dataclass, field from typing import Any, Callable, Optional, Dict import inspect @dataclass class Skill: name: str description: str parameters: Dict[str, Any] execute: Callable[..., Any] validator: Optional[Callable[[Dict[str, Any]], Optional[str]]] = None requires_confirmation: bool = False timeout_seconds: float = 8.0 max_retries: int = 1 def to_openai_schema(self) -> dict: """转换成LLM工具调用的标准schema""" return { "type": "function", "function": { "name": self.name, "description": self.description, "parameters": { "type": "object", "properties": self.parameters, "required": [ k for k, v in self.parameters.items() if v.get("required") ], }, }, } class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] = {} def register(self, skill: Skill) -> None: if skill.name in self._skills: raise ValueError(f"技能 {skill.name} 已存在,不允许重复注册") self._skills[skill.name] = skill def get(self, name: str) -> Optional[Skill]: return self._skills.get(name) def all(self) -> list[Skill]: return list(self._skills.values()) def schemas(self) -> list[dict]: return [s.to_openai_schema() for s in self._skills.values()]

这里有个细节值得特别注意:注册时要做重名检查。技能多了以后,重名是排查成本极高的错误。如果两个技能都叫search,模型随机选一个,结果完全不可控。我倾向于在注册阶段就强制唯一性,宁可报错也不让含糊过关。

3.2 具体技能实现:天气和库存示例

注册表有了,往里面塞几个技能看看效果。我挑两个有代表性的:一个查询类,一个带业务规整类。

# skills_def.py import random import re def execute_weather(city: str) -> dict: # 真实项目中这里换成天气API调用 realtime = random.choice(["晴", "多云", "小雨"]) return {"city": city, "current": realtime, "forecast": ["晴", "多云", "小雨"]} def validate_weather(params: dict) -> Optional[str]: city = params.get("city", "") if not city: return "缺少city参数" if not re.match(r"^[\u4e00-\u9fa5]{2,4}$", city): return f"city参数必须是2-4个汉字的中文城市名,当前值: {city}" return None weather_skill = Skill( name="get_weather", description="查询指定城市当前天气和未来三天预报。当用户提到天气、温度、下雨、出门是否带伞时使用。city用中文全称。", parameters={ "city": { "type": "string", "description": "城市中文全称,例如北京、上海、广州", "required": True } }, execute=execute_weather, validator=validate_weather, timeout_seconds=5.0, )

再写一个库存查询技能。注意这里我在返回结果里做了"人话化"处理,直接返回模型友好的文本,而不是裸JSON。这个改动很关键,模型拿到结构化数据后经常不知道怎么组织语言,你帮它翻译成半成品句子,输出质量会上一个台阶。

def execute_inventory(sku: str) -> dict: # 实际项目里查数据库或ERP stock = random.randint(0, 100) status = "有货" if stock > 10 else "紧张" if stock > 0 else "缺货" return {"sku": sku, "stock": stock, "status": status, "summary": f"SKU {sku} 当前库存 {stock} 件,状态: {status}"} inventory_skill = Skill( name="check_inventory", description="查询指定SKU的实时库存。当用户问到现货、库存、还有没有货、什么时候能发货时使用。SKU严格使用字母加数字组合。", parameters={ "sku": { "type": "string", "pattern": "^[A-Z]{4}\\d{6}$", "description": "SKU编码,由4个大写字母和6位数字组成,例如ABCE123456", "required": True } }, execute=execute_inventory, validator=lambda p: None if re.match(r"^[A-Z]{4}\d{6}$", p.get("sku", "")) else "SKU格式不正确", )

然后注册:

registry = SkillRegistry() registry.register(weather_skill) registry.register(inventory_skill) for schema in registry.schemas(): print(schema)

3.3 模型路由:怎么让大模型自己选技能

技能系统的心脏在路由。我用的方案是让大模型基于技能清单生成结构化调用,然后解析结果。以OpenAI兼容接口为例,核心逻辑是这样:

# router.py from openai import OpenAI client = OpenAI() # 如果接国产模型就换base_url def agent_run(user_query: str, registry: SkillRegistry) -> str: messages = [ {"role": "system", "content": "你需要根据用户请求选择合适的技能并传入正确参数。如果技能信息不足,请直接说明缺少什么信息。"}, {"role": "user", "content": user_query} ] response = client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=registry.schemas(), tool_choice="auto", ) msg = response.choices[0].message if not msg.tool_calls: return msg.content or "我没找到合适的技能来处理这个问题。" results = [] for call in msg.tool_calls: fn_name = call.function.name args = json.loads(call.function.arguments) skill = registry.get(fn_name) if skill is None: results.append(f"技能 {fn_name} 不存在") continue results.append(exec_skill(skill, args)) # 把工具结果喂回模型,让它生成最终回答 messages.append(msg) for i, call in enumerate(msg.tool_calls): messages.append({ "role": "tool", "tool_call_id": call.id, "content": json.dumps(results[i], ensure_ascii=False) }) final = client.chat.completions.create(model="gpt-4o-mini", messages=messages) return final.choices[0].message.content

注意这一步非常关键:工具结果要回填给模型,而不是直接用工具结果当答案。因为用户往往期待自然语言回复,而模型需要结合工具返回的数据组织语言。如果直接把JSON怼给用户,体验会非常生硬。

另外,parsing模型的返回时,工具参数一定是字符串形式的JSON,一定要包try/except。模型偶尔会返回非法JSON,不catch的话整个链路直接崩。

3.4 执行器:校验、超时、重试、确认

这个执行器是技能系统里最容易被低估的模块。我把校验、超时、重试、二次确认全都收拢在这里,让业务技能保持纯粹。

import time import json from concurrent.futures import ThreadPoolExecutor, TimeoutError def exec_skill(skill: Skill, params: dict) -> str: # 1. 参数校验 if skill.validator: err = skill.validator(params) if err: return json.dumps({"error": f"参数校验失败: {err}"}, ensure_ascii=False) # 2. 二次确认(危险的写操作) if skill.requires_confirmation: # 实际项目中这里会推送给用户在IM工具里点确认 print(f"[需要确认] 技能 {skill.name} 参数: {params}") return "该操作需要人工确认,请先获得审批。" # 3. 带超时的执行 + 重试 last_error = None for attempt in range(skill.max_retries + 1): try: with ThreadPoolExecutor(max_workers=1) as executor: future = executor.submit(skill.execute, **params) result = future.result(timeout=skill.timeout_seconds) return json.dumps(result, ensure_ascii=False) except TimeoutError: last_error = f"技能执行超时(> {skill.timeout_seconds}s)" except Exception as e: last_error = f"执行异常: {str(e)}" time.sleep(0.5) return json.dumps({"error": last_error}, ensure_ascii=False)

说说超时。外部API调用的时间不可控,模型等着结果的时候,用户也在等。我项目里80%的技能超时设为5秒,重试1次。有个坑是超时用time.sleep会阻塞整个Agent进程,所以要用线程池隔离。

二次确认这块,在纯对话场景可以做成"确认后继续"的交互循环,不是简单打印一句就完事。不过核心逻辑是一致的:危险操作必须在技能层拦截,不能指望模型判断。模型对"删除"和"标记"的分寸感远没有代码可靠。

4. 技能系统最常见的坑与排查方法

这部分是我最想写的,因为理论看了再多,实战里的坑才让人长记性。我按踩坑频率从高到低排了个速查表,每个都附上排查思路。

4.1 技能描述路由不准:大模型理解偏差

现象:用户说"帮我看看北京的库存",模型却调用了天气技能,返回了北京的天气。听起来很离谱,但实际发生频率不低。

原因:技能描述里的触发词和用户表达没有重叠。模型不是真的"理解",它只是在做语义相似度匹配,如果你的描述过于概括,它就靠猜。

排查方法:把用户实际输入和模型选择的技能打印出来,做一个"路由对照表"。收集至少50条真实用户问题,看哪些被路由错了,然后针对性补触发词。我维护了一个词表,每个技能至少对应10种常见用户问法,描述里写不下就写"当用户提到XX、XX、XX时优先使用此技能"。

4.2 参数幻觉和格式问题

现象:技能需要的参数是日期,模型传了"明天";技能需要ISO格式,模型传了"2024-03-1"这种不符合规范的日期。

原因:模型在生成参数时是在做"概率补全",它可能没见过你参数的格式要求,或者忽略了description里的格式说明。

排查方法:两层防御。第一层在参数schema里尽量加pattern、enum这样的硬约束;第二层在validator里做兜底。我给日期参数加过正则,给枚举字段加过候选列表,效果立竿见影。如果模型反复传错同一个参数,就把这个参数从"free text"改成"选一个提供的选项",让模型做选择题而不是填空题。

4.3 上下文污染:技能返回内容太长

现象:技能返回了一个超长的JSON,里面包含几百条库存记录,模型还没来得及总结,上下文先爆了。

原因:技能层把原始API响应直接丢给模型,没有做裁剪。

排查方法:在技能执行函数里做结果压缩。库存技能只返回总数、缺货数、前5个明细;文件搜索只返回文件名列表;数据库查询只返回聚合结果。原则是"给模型足够回答问题的信息,但不要给全部"。

4.4 技能之间的状态冲突

现象:先调用了"生成合同编号"技能,再调用"发送合同"技能,合同编号没有传给发送技能,导致发错文档。

原因:技能之间缺一个"共享上下文"的机制。每个技能都是独立执行的,如果不显式把上一步的结果传给下一步,信息就会断。

排查方法:在设计技能时提前规划好流程型技能的输入输出。从"技能A输出"到"技能B输入"的映射要写清楚,最好在编排器里显式声明。我后来给技能之间的数据流画过一张简单的表格,每个技能标出"依赖的上游技能"和"产出的下游字段",排错效率提升了很多。

4.5 超时和外部依赖不稳定

现象:技能偶尔失败,但没有任何提示,用户只看到模型说"我查不到"。

原因:异常被吞掉了,或者超时时间太短导致正常操作也失败。

排查方法:所有技能都要有明确的错误返回值,不能静默失败。执行器捕获到异常后,要把错误信息格式化后返回给模型,让模型能够向用户解释"暂时无法获取,请稍后再试"。超时时间要根据真实API的P95延迟来设,我一般是测三轮取中位数,然后乘以2作为超时阈值。

4.6 技能测试难做:回归认证

技能系统最怕的是"改了一个,坏了一片"。因为技能之间可能互相依赖,模型路由也可能因为新技能加入而整体漂移。

排查方法:给技能系统建立"回放测试集"。准备一组典型的用户问题,记录每次运行时的技能调用轨迹,包括调用了哪些技能、参数是什么、最终回答是什么。每次改动后跑一遍,对比新旧轨迹的diff。这一步虽然不是特别聪明,但却是最实用的回归手段。

5. 技能系统还需要想清楚的事

前面讲的都是怎么把技能跑起来,最后还有三个问题,是架构层面不能回避的。

第一,权限边界。技能接管了真实世界的操作,查天气无所谓,但"删除订单""转账"这类技能必须走独立的权限体系。我在技能里加了角色标识,普通用户技能集里根本不注册管理类技能,管理员账户才看得到。

第二,可观测性。技能调用日志要单独存,不能只在控制台打印。因为Agent的使用场景经常需要事后翻查——"当时为什么做了这个决定",没有日志就无从追溯。

第三,技能的生命周期。技能会过时,外部API会改版,参数会变。技能注册表要支持版本号和废弃标记,老版本技能在一段时间内仍然可用但不推荐,给下游调用方一个缓冲期。

我个人在实际操作中的体会是:技能系统的复杂度是慢慢长出来的。刚开始不要追求一步到位,从5个以内的核心技能起步,跑一两个真实场景,再根据模型路由的失败案例逐步迭代。技能描述、参数约束、错误处理这三样东西,每优化一轮,线上效果都会有肉眼可见的提升。

最后再分享一个调试技巧吧。很多人改技能描述后感觉"好像变了但不知道变在哪",我给Agent加了个中间层,把模型每次路由前的全部候选技能及其得分打印出来。这样你能亲眼看到,你改的那句话是让模型从"第3个候选"跳到了"第1个候选",还是仍然是"第3个但分数略有变化"。有了这个可见性,调整描述就再也不是玄学,而变成一种可以积累经验的操作了。

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

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

立即咨询