AI Agent技能工程:从Function Calling到MCP的实践经验
2026/9/17 0:53:47 网站建设 项目流程

在做 AI Agent 落地的这大半年里,我最大的感受是:模型决定 agent 的上限,技能决定 agent 的下限。这个观点不是我拍脑袋总结的,而是被一堆线上事故逼出来的。我们团队内部有一个叫 agent-skills 的项目,最开始它只是一个存放 prompt 和工具函数的仓库,后来越做越像一个独立的技能运行时,承担了技能定义、注册、调度、评测和灰度发布这些脏活累活。这篇文章就把我在这个项目里踩过的坑、想明白的事,以及实际跑通的方案完整写出来,希望能给正在搞 Agent 的你一点参考。

可能有人会问:模型不是已经很强了吗,直接让它干就行了,为什么还要单独搞一套 agent-skills?我举个真实场景。我们的客服助手接入工单系统时,第一版只给模型塞了一份接口文档,结果模型经常把必填字段传漏,甚至把用户地址写进标题里,准确率只有六成多。后面我们把“创建工单”拆成一个独立技能,定义好输入输出和校验规则,准确率直接拉到 94%。这个差距不是模型智能决定的,而是技能层决定的。这套东西适合谁?适合所有正在做 Agent 落地、尤其是要让 Agent 操作真实业务系统的人。不管你是个人开发者还是团队负责人,搞懂技能设计,比追着换大模型更划算。

1. 为什么我突然开始重仓 agent-skills

1.1 从“会聊”到“会干”的临门一脚

早期做 Agent 的时候,我犯过一个典型错误:把所有精力都放在提示词上,以为把 prompt 写得足够长、足够细,模型就能稳定完成任务。但实际一接入业务系统就露馅了。模型“会聊”和“会干”之间隔着一条很宽的河,这条河就是技能。

举一个最日常的例子:让 Agent 查询订单状态。如果直接把订单查询 API 暴露给模型,模型可能会把“查订单”理解为“查快递”,也可能在用户没给订单号的时候自己编一个出来。就算接口文档写得清清楚楚,不同模型的理解能力也不一样,甚至连同一个模型在不同表达方式下行为都不稳定。后来我们做了个简单的封装,把所有可能的触发场景写进技能描述,再把订单号格式校验放到技能内部,模型的行为立刻就稳住了。

所以我一直觉得,Agent 的落地难点不在于“让模型理解人话”,而在于“让模型稳定地完成人类需要它完成的操作”。能力的载体不是模型本身,而是经过良好封装的 agent-skills。这也是我把项目重心从调 prompt 转向建技能的最直接原因。

1.2 agent-skills 到底是什么

我在项目里给 agent-skills 下了一个很务实的定义:它是一套面向智能体的技能定义、注册、执行、评测与编排框架。一个标准技能至少包含这些部分:

  • 技能唯一标识和名称,比如create_work_orderquery_order_status
  • 触发条件描述,告诉模型什么时候该调用这个技能、什么时候不该调用
  • 输入输出 Schema,定义参数类型、是否必填、取值范围
  • 执行逻辑,可以是一段 Python 函数、一个 HTTP 调用,也可以是一段提示词模板
  • 异常处理和校验规则,包括失败之后的返回结构
  • 版本信息,记录技能变更历史

这听起来有点像把函数注册到大模型里,但 agent-skills 和简单的 function calling 有一个本质区别:它强调“技能说明书”。函数只告诉模型“我能做什么”,技能还要告诉模型“在什么情况下用我”“参数怎么填”“失败了会怎样”。有了这套说明书,模型才能像一位熟悉业务的员工一样,而不是一个随手翻工具的实习生。

1.3 和 function calling、MCP、plugin 是什么关系

很多人会把 agent-skills 和 function calling、MCP、plugin 混为一谈,其实它们解决的是不同层次的问题。我画过一张对比表,现在直接贴出来:

概念定位解决的问题
Function Calling模型输出结构化调用意图的机制让模型“说人话”变成“调用函数”
MCP工具调用的标准化通信协议让不同 Agent 框架能复用同一套工具服务
Plugin能力的分发和扩展形态让第三方能力可安装、可更新
agent-skills技能的定义、校验、组织、编排层让能力可管理、可评测、可复用

在我们的架构里,agent-skills 是站在 function calling 和 MCP 之上的“组织层”。如果一个技能需要暴露给外部系统,我们会通过 MCP 把它包装成一个服务;如果需要被当前 Agent 直接调度,就通过 function calling 暴露给模型。技能本身不与任何通信协议强绑定,这样未来哪怕换了一个模型供应商,技能资产照样能用。

2. 技能怎么拆才顺手:粒度、描述与边界

2.1 技能粒度:不是越细越好

技能拆分的粒度,是 agent-skills 项目里最影响成败的决策之一。我见过两种极端:一种是把整个业务流程做成一个超大技能,比如“生成日报”函数里又查数据又排版又发消息,没过多久就变成没人敢动的巨型泥球;另一种是把技能拆成“字符串拼接”这种原子操作,结果模型要完成一个简单任务得编排十几个技能,既慢又容易出错。

我们后来总结出一个标准:一个技能应该对应一个可以独立校验的“原子任务”,并且这个任务的结果对用户来说是可见、可验证的。以“生成日报”为例,我不会把整个流程塞成一个技能,而是拆成三个技能:get_report_data负责拉取原始数据,format_report负责把数据组装成日报正文,send_message负责通过 IM 发送。这三个技能单独都能复用,比如send_message不只用于发日报,还能用于告警通知。

判断技能粒度是否合适,我会问自己三个问题:这个技能能被其他场景复用吗?失败时影响面可控吗?我能为这个技能单独写评测用例吗?如果三个答案都是肯定的,这个粒度就是合适的。

2.2 技能描述怎么写才能让模型稳定触发

技能描述是给大模型看的说明书,不是给程序员看的代码注释。写得好不好,直接决定模型能不能在正确时机调用技能。我们实践下来,一份好的技能描述应该包含五段信息:

  • 技能名称:动词开头的短句,比如create_work_order
  • 一句话说明:这个技能做什么
  • 触发时机:明确列出哪些用户意图应该触发
  • 反例:明确列出哪些情况不应该触发
  • 参数说明:每个参数的含义、类型、默认值

我拿create_work_order写过两个版本。第一个版本只有一句话:“创建一个客服工单”,结果模型在用户问“我之前的工单咋样了”时也会去调它。第二个版本我加了反例和参数默认值,效果立刻不一样。建议大家写描述时,一定要把“不要用这个技能”的场景写清楚,模型对负向信号的敏感度很高。

2.3 边界条件与异常处理

技能不能只写“正常流程”,边界和异常必须是一等公民。我们的技能执行结果统一返回三个字段:status(success / failed / need_human)、data(执行结果)、message(给模型的自然语言反馈)。这样模型能根据结果决定下一步动作,而不是在一堆异常堆栈里瞎猜。

我踩得最深的坑是:技能执行一半失败了,Agent 往往会尝试“弥补”,比如连续重试三次导致重复下单。后来我们在技能入口强制校验参数,在关键操作前生成request_id,并让技能具备幂等性。也就是说,同一个request_id如果之前已经成功执行,再次调用就直接返回上一次结果。这样做以后,即便模型重复触发技能,也不会造成脏数据。

3. 三种落地实现方式:函数注册、MCP与提示词技能

3.1 函数注册型技能

最直接的落地方式是把技能写成一个函数,再通过模型供应商的 function calling 机制注册给模型。这一步没什么高深技术,但参数 Schema 的规范程度决定了技能能不能被正确调用。

我之前习惯手写 JSON Schema,后来发现手写容易漏字段、写错类型,尤其在枚举值多的时候。现在直接使用 pydantic 定义参数模型,让框架自动生成 Schema。比如一个创建工单的技能:

from pydantic import BaseModel, Field class CreateWorkOrderParams(BaseModel): title: str = Field(..., description="工单标题,一句话概括问题") content: str = Field(..., description="问题详细描述,尽量包含复现步骤") priority: str = Field("P2", description="优先级,只能传 P0/P1/P2") category: str = Field("general", description="工单分类") def create_work_order(params: CreateWorkOrderParams): # 内部校验订单号、调用工单系统 work_order_id = call_internal_api(params) return {"status": "success", "data": {"work_order_id": work_order_id}}

在注册到模型时,我们需要把函数的namedescriptionparameters交给模型。这里有一个非常容易被忽视的细节:函数的description要填技能描述,而不是函数注释。我在代码里特意把描述写得像业务文档,模型对触发时机的判断会准确很多。另外在开发环境,我会把工单系统 API 替换成 Mock 服务,避免模型乱调导致线上产生大量测试工单。

3.2 基于 MCP 的技能服务

如果说函数注册是“单机版”技能,那 MCP 就是“网络版”技能。MCP 的全称是 Model Context Protocol,它把工具调用抽象成标准化的 JSON-RPC 接口,主要包含tools/listtools/call。我们团队把一部分常用技能封装成独立 MCP Server,这样同一个技能服务可以同时服务多个 Agent 框架,不用为每个框架各实现一遍。

MCP Server 的配置很简单,本质上是启动一个本地服务,再告诉 Agent 框架去哪连它:

{ "mcpServers": { "work-order-skill": { "command": "python", "args": ["server.py"], "env": { "API_BASE": "http://internal-api.example.com" } } } }

用 MCP 最大的好处是隔离和复用:技能服务可以独立部署、独立扩容,权限也能在服务层统一控制。但它也有代价——每次工具调用多一层网络开销,本地调用通常几毫秒,走 MCP 可能会到几十毫秒。我的建议是:对延迟敏感的核心技能,优先用函数注册;需要跨团队、跨系统复用的通用技能,再考虑 MCP。

3.3 纯提示词技能库

不是所有技能都需要写代码。像“整理会议纪要”“生成竞品摘要”这类任务,本质上是文本处理,不依赖外部系统,直接给模型一段操作手册就够了。我们把这些提示词技能也纳入 agent-skills 统一管理,每个技能是一个结构化的 prompt 模板,包含技能 ID、触发条件、正文模板、输入输出示例。

一个典型的提示词技能长这样:先让模型扮演某个角色,再给它明确的处理步骤,最后要求按 JSON 格式输出。为了稳定,我会在模板里写两三个 few-shot 示例,并在输出要求里加一条“如果信息不足,请在 response 里填 null,不要编造”。这样的技能迭代速度非常快,一般调几次模板就能稳定上线。

3.4 如何选择:一张表说清

我把三种实现方式的适用场景整理成表,方便大家直接对号入座:

场景推荐方案原因
操作外部业务系统,比如创建工单、查订单函数注册参数校验强、延迟低、能控制权限
技能需要跨多个 Agent 框架复用MCP Server标准化协议,一次封装处处调用
文本处理类任务,不依赖外部系统提示词技能库迭代快、成本低,不用写服务
复杂业务状态,需要分布式事务函数注册 + 状态机方便管理事务边界和回滚

这里需要强调一点:一个项目里混用三种方式完全没问题,但所有技能必须注册到同一个技能目录中,由统一的入口做检索和调度。否则技能散落在各个仓库里,越到后面越难维护。

4. 技能编排:当多个技能同时被选中

4.1 从单技能到技能链

单技能能做简单任务,但真实业务往往是多技能协作。比如用户说“我要投诉订单 12345,物流太慢了”,Agent 实际要做的是:查订单状态、创建投诉工单、给用户一个反馈。如果模型自己乱编排顺序,很容易先创建工单再查订单,导致工单内容缺少关键信息。

我在 agent-skills 项目里引入了一个朴素的编排层:先显式定义技能链,再让模型只在技能链的约束范围内执行。上面这个场景的技能链就是query_order_details->create_work_order->send_user_message。只要模型按这个顺序走,每个技能的输出都能作为下一个技能的输入。

4.2 显式编排与动态规划

编排有两种思路,一种是“显式编排”,一种是“动态规划”。显式编排是我们大多数业务场景的首选,因为它稳定、可控、好排查问题。用代码或配置把技能列表写死,模型只负责在当前步骤做决策。

对于探索型任务,比如“帮我研究一下这个行业近期的变化”,动态规划会更合适。模型需要自己从技能库里挑选检索、摘要、写报告等技能。但动态规划有一个很大的风险:模型可能陷入循环,或者在一个无关技能上浪费大量 token。我经验是,必须给动态规划设置最大步骤数、终止条件和兜底回复,否则线上迟早会出事故。

从我实际测试的数据来看,80% 的业务场景用显式编排就够了。动态规划看起来很酷,但用户真正需要的往往不是“自由”,而是“稳定”。

4.3 冲突消解和优先级

当用户的一句话同时匹配多个技能时,就需要一个顶层仲裁器。最简单的仲裁逻辑是使用一个独立的意图分类模型,先判断当前用户意图属于哪个类别,再决定使用哪个技能。但分类模型也会有误差,所以我们还加了人工规则兜底。

在代码层面,仲裁器可以是一个很轻量的函数:

matched_skills = skill_router.match(user_message) if len(matched_skills) == 1: run_skill(matched_skills[0], user_context) elif len(matched_skills) > 1: chosen_skill = arbiter.select(matched_skills, user_context) run_skill(chosen_skill, user_context)

对于会修改外部资源的技能,比如“下单”“删除”,我会在仲裁器层强制串行执行,并带上幂等键。简单说,如果两个技能同时命中,优先执行用户意图更符合的那个,而不是让模型随机选。这个规则看着朴素,但能避免很多并发写导致的脏数据问题。

5. 让技能越用越稳:评测与运维

5.1 给每个技能建一套评测集

很多团队做 Agent 只关心“能用”,却不关心“是否一直能用”。技能改动一次,可能把模型触发率拉低 10 个百分点而不自知。我们在 agent-skills 里强制要求:每个技能必须有一个评测集,至少 50 条测试用例,覆盖正常场景、边界场景和敏感场景。

评测集用 JSONL 文件维护,每个用例包含用户原话、期望触发的技能、期望参数、期望输出。比如:

{"input": "我要投诉订单12345,物流太慢了", "expected_skill": "create_work_order", "expected_params": {"order_id": "12345", "reason": "物流慢", "priority": "P2"}}

每次技能代码或描述有改动,就在开发环境跑一遍离线回归。流程是:准备 Mock 外部服务的测试环境,然后加载评测集,逐一让 Agent 处理,最后统计三档指标——技能召回率(该触发的时候有没有触发)、参数准确率(传参对不对)、任务完成率(结果是否符合预期)。只要任何一档指标下降,就阻止发布。这个机制帮我们拦下了至少三次会导致线上事故的变更。

5.2 线上日志与技能监控

评测集解决的是“已知问题”,但线上总会出现评测集没覆盖的新情况。所以我非常依赖线上日志。每一条 Agent 交互我们都会记录结构化日志,字段包括:用户消息、命中的技能、匹配分数、传入参数、执行结果、错误信息、耗时。这些日志会进入分析系统,用来统计每个技能的调用量、成功率和平均耗时。

我每周都会做一次“失败案例复盘”,把那些任务没完成的日志捞出来,看是模型没触发技能、参数传错,还是技能内部报错。复盘之后,把共性问题补进评测集。举个例子,我们发现用户经常说“我的货到哪了”,但技能描述里只写了“查订单”,没写“查物流”,导致模型频繁不调用技能。后来在描述里补上“物流”这个触发词,下一周调用率立刻回升。这个过程很朴素,但非常有效。

5.3 版本管理与灰度发布

技能虽然名字里带“技能”,但本质上是一段代码加一段描述。它需要像软件一样做版本管理。我们每个技能一个目录,目录下包含 README、源代码或提示词模板、Schema 定义、评测集。技能发布流程是:开发分支 -> 离线评测 -> staging 灰度 -> 全量发布。

灰度的时候最需要注意技能描述的影响。我遇到过一个问题:只是把技能描述里的“添加”改成“新增”,结果某个场景下模型反而不触发技能了。出问题后我们再也不敢只看代码变更,凡是描述有改动,都会先跑一遍完整评测集再灰度。逻辑很简单:技能描述是给模型看的,同样一句话在不同模型上的敏感度完全不一样。

6. 踩坑记录与实操心得

6.1 常见问题速查表

我在 agent-skills 开发和上线过程中积累了一些高频问题,整理成速查表,希望能帮你少走弯路:

现象可能原因解决办法
模型一直不调用某个技能技能描述太宽泛,参数太多精简描述,增加正向和负向触发示例,减少必填参数
技能经常参数传错Schema 和模型预期不一致用 pydantic 自动生成 Schema,给枚举值加默认值
同样的一句话时好时坏多个技能描述存在重叠明确技能边界,在仲裁器层显式决策
技能执行成功但结果不对Mock 数据与真实业务不一致用影子模式回放历史请求,对比真实输出
技能并发执行产生脏数据没有幂等和并发控制为每个请求生成 request_id,关键操作加锁
提示词技能输出不稳定few-shot 太少,格式约束弱增加示例,要求按 JSON 输出并做二次字段校验

除了这些问题,我还想强调一个高发隐患:不要在技能描述里写“如果 xxx 就返回成功”,看起来给模型留了灵活性,实际上等于告诉模型可以跳过校验。技能内部要有自己的校验逻辑,不能把判断权全交给模型。

6.2 团队协作与“技能评审”

agent-skills 做大了以后,就不再是某个程序员手里的工具,而是一个团队资产。为了不让它变成一堆无人维护的野代码,我们建立了几条很实在的规范。

第一,技能必须有唯一负责人。技能名称全局唯一,不能出现两个技能做类似功能但命名不同的情况。第二,新增或修改技能要走“技能评审”,流程很简单:写清楚技能描述、边界、参数和评测集,然后拉上相关同学过一遍,重点看是否和现有技能冲突。第三,要有“技能市场”意识,公共技能沉淀下来之后,多个 Agent 都可以直接调用,避免每个项目重复造轮子。

这些规范看起来不像技术,但正是它们让 agent-skills 从一个项目变成了一套可持续运转的机制。

最后再分享一个我在实际使用中的体会

如果只让我给一条建议,我会说:不要把 Agent 做成一个大而全的对话系统,要把业务拆成一个个可以被单独验证的技能。agent-skills 这个项目教会我的不是模型调优,而是工程化思维——技能描述就是需求文档,评测集就是验收标准,线上日志就是复盘依据。这个方向后续还能扩展很多,比如跨团队技能复用、技能自动生成、基于用户反馈的技能修正,但基础一定是先把“一个技能”做到稳定。如果你也在做 Agent 落地,我建议你从最常被调用的那个操作开始,把它拆出来,写清楚描述,建一套评测集,跑通之后再往第二个、第三个技能扩展。

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

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

立即咨询