AI Agent技能工程:从Function Calling到MCP的实践经验

发布时间:2026/9/17 0:54:19
AI Agent技能工程:从Function Calling到MCP的实践经验 在做 AI Agent 落地的这大半年里我最大的感受是模型决定 agent 的上限技能决定 agent 的下限。这个观点不是我拍脑袋总结的而是被一堆线上事故逼出来的。我们团队内部有一个叫 agent-skills 的项目最开始它只是一个存放 prompt 和工具函数的仓库后来越做越像一个独立的技能运行时承担了技能定义、注册、调度、评测和灰度发布这些脏活累活。这篇文章就把我在这个项目里踩过的坑、想明白的事以及实际跑通的方案完整写出来希望能给正在搞 Agent 的你一点参考。可能有人会问模型不是已经很强了吗直接让它干就行了为什么还要单独搞一套 agent-skills我举个真实场景。我们的客服助手接入工单系统时第一版只给模型塞了一份接口文档结果模型经常把必填字段传漏甚至把用户地址写进标题里准确率只有六成多。后面我们把“创建工单”拆成一个独立技能定义好输入输出和校验规则准确率直接拉到 94%。这个差距不是模型智能决定的而是技能层决定的。这套东西适合谁适合所有正在做 Agent 落地、尤其是要让 Agent 操作真实业务系统的人。不管你是个人开发者还是团队负责人搞懂技能设计比追着换大模型更划算。1. 为什么我突然开始重仓 agent-skills1.1 从“会聊”到“会干”的临门一脚早期做 Agent 的时候我犯过一个典型错误把所有精力都放在提示词上以为把 prompt 写得足够长、足够细模型就能稳定完成任务。但实际一接入业务系统就露馅了。模型“会聊”和“会干”之间隔着一条很宽的河这条河就是技能。举一个最日常的例子让 Agent 查询订单状态。如果直接把订单查询 API 暴露给模型模型可能会把“查订单”理解为“查快递”也可能在用户没给订单号的时候自己编一个出来。就算接口文档写得清清楚楚不同模型的理解能力也不一样甚至连同一个模型在不同表达方式下行为都不稳定。后来我们做了个简单的封装把所有可能的触发场景写进技能描述再把订单号格式校验放到技能内部模型的行为立刻就稳住了。所以我一直觉得Agent 的落地难点不在于“让模型理解人话”而在于“让模型稳定地完成人类需要它完成的操作”。能力的载体不是模型本身而是经过良好封装的 agent-skills。这也是我把项目重心从调 prompt 转向建技能的最直接原因。1.2 agent-skills 到底是什么我在项目里给 agent-skills 下了一个很务实的定义它是一套面向智能体的技能定义、注册、执行、评测与编排框架。一个标准技能至少包含这些部分技能唯一标识和名称比如create_work_order、query_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 边界条件与异常处理技能不能只写“正常流程”边界和异常必须是一等公民。我们的技能执行结果统一返回三个字段statussuccess / 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}}在注册到模型时我们需要把函数的name、description、parameters交给模型。这里有一个非常容易被忽视的细节函数的description要填技能描述而不是函数注释。我在代码里特意把描述写得像业务文档模型对触发时机的判断会准确很多。另外在开发环境我会把工单系统 API 替换成 Mock 服务避免模型乱调导致线上产生大量测试工单。3.2 基于 MCP 的技能服务如果说函数注册是“单机版”技能那 MCP 就是“网络版”技能。MCP 的全称是 Model Context Protocol它把工具调用抽象成标准化的 JSON-RPC 接口主要包含tools/list和tools/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 落地我建议你从最常被调用的那个操作开始把它拆出来写清楚描述建一套评测集跑通之后再往第二个、第三个技能扩展。