AI Agent工具系统实战:Function Calling、MCP与Skills设计指南
2026/9/23 5:42:56 网站建设 项目流程

1. 为什么说没有工具系统的 Agent 只是个"缸中之脑"

很多人第一次接触 AI Agent 这个概念时,脑子里浮现的画面是一个能自己思考、自己规划、自己执行的全能助手。但真正动手搭过 Agent 的人都知道,一个没有工具系统的 Agent,本质上就是一个被困在对话框里的"缸中之脑"——它能说会道,能分析问题,能给出建议,但它什么都做不了。

你问它"帮我查一下今天北京的天气",它会告诉你"很抱歉,我无法获取实时数据"。你让它"把这段代码保存成文件",它会说"我无法直接操作你的文件系统"。这不是它不够聪明,而是它没有手脚。

工具系统就是 Agent 的手脚。它让 Agent 从"只会说"变成"能去做"。这个转变看起来简单,实际上涉及一整套设计哲学和工程实现。我在过去一年多的 Agent 开发实践中,踩过不少坑,也总结出了一些真正管用的经验。这篇文章会从最基础的概念讲起,逐步深入到 Function Calling 的实现细节、MCP 协议的运作机制、Skills 的设计思路,以及在实际项目中如何把这些东西串起来。

不管你是刚入门 AI Agent 开发的新手,还是已经搭过几个 Demo 但总觉得不够"丝滑"的开发者,这篇文章都应该能给你一些可以直接抄作业的东西。我会尽量用大白话把原理讲清楚,同时给出可复现的代码和配置,让你看完就能动手试。

2. 工具系统的本质:给 LLM 装上可调用的外部能力

2.1 LLM 的能力边界在哪里

要理解工具系统为什么重要,首先得搞清楚 LLM 本身能做什么、不能做什么。以 DeepSeek、GPT 这类大语言模型为例,它们的核心能力是"根据输入预测输出"。你给它一段文字,它根据训练时学到的模式,生成一段最可能的续写。这个过程不涉及任何外部世界的交互。

这意味着 LLM 有几个天然的能力边界:

  • 无法获取实时信息:训练数据有截止日期,之后发生的事情它不知道。
  • 无法执行副作用操作:它不能写文件、发请求、操作数据库。
  • 无法进行精确计算:虽然它能做数学题,但复杂计算容易出错,因为它本质是在"预测"答案而不是"计算"答案。
  • 无法访问私有数据:你公司的内部文档、数据库里的用户信息,它一概不知。

这些边界不是靠"把模型做大"就能解决的。你就算把模型参数翻十倍,它还是没法帮你查今天的天气。因为这不是智能问题,是连接问题。

工具系统的核心思路就是:既然 LLM 不能直接做这些事,那就给它一套"工具",让它能通过调用这些工具来间接完成。LLM 负责理解和决策,工具负责执行和返回结果。

2.2 工具系统的三层架构

在实际工程中,一个完整的工具系统通常包含三个层次:

第一层是工具定义层。这一层负责描述"有哪些工具可用、每个工具接受什么参数、返回什么结果"。这就像是给 Agent 一本工具手册,告诉它工具箱里有什么。

第二层是调用决策层。这一层由 LLM 驱动,负责判断"当前这个任务需不需要调用工具、调用哪个工具、参数怎么填"。这是 Agent 智能的体现。

第三层是执行层。这一层负责真正去执行工具调用,包括参数校验、实际执行、错误处理、结果格式化。这是工程实现的部分。

很多人搭 Agent 时只关注第二层,觉得"只要模型够聪明,它自然知道该调什么工具"。但实际上,第一层的工具描述质量和第三层的执行健壮性,往往才是决定 Agent 好不好用的关键。

2.3 一个生活化的类比

你可以把工具系统想象成给一个刚入职的实习生配电脑和权限。这个实习生(LLM)很聪明,学习能力很强,但他刚来公司,什么系统都没权限,什么工具都不会用。你需要:

  1. 告诉他公司有哪些系统可以用(工具定义)
  2. 教他什么场景下该用哪个系统(调用决策)
  3. 给他开通对应的账号权限(执行层配置)

如果你只告诉他"公司有 CRM 系统",但没告诉他怎么登录、能查什么数据,他还是干不了活。工具系统的设计也是同样的道理——光有工具列表不够,还得让 Agent 知道每个工具的适用场景、参数含义、返回格式。

3. Function Calling:工具系统最基础的实现方式

3.1 Function Calling 到底在做什么

Function Calling 是目前最主流的工具调用实现方式。它的核心机制其实很简单:你在调用 LLM API 时,除了传入用户的消息,还传入一份"工具清单"。LLM 在生成回复时,如果判断需要调用某个工具,它不会直接生成自然语言回复,而是生成一个结构化的调用请求,包含工具名和参数。

举个例子,你定义了一个查天气的工具:

{ "name": "get_weather", "description": "查询指定城市的当前天气", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,如'北京'、'上海'" } }, "required": ["city"] } }

当用户问"北京今天天气怎么样"时,LLM 不会直接回答,而是返回:

{ "tool_calls": [ { "function": { "name": "get_weather", "arguments": "{\"city\": \"北京\"}" } } ] }

你的代码拿到这个调用请求后,去实际执行查天气的操作,把结果再传回给 LLM,LLM 最后生成自然语言回复:"北京今天晴,气温 15-25 度。"

3.2 工具描述怎么写才有效

这是我在实践中踩坑最多的地方。工具描述写得好不好,直接决定了 LLM 能不能正确调用。我总结了几条经验:

描述要具体,不要抽象。不要写"处理用户数据",要写"根据用户 ID 查询用户的订单历史记录,返回最近 30 天的订单列表"。LLM 需要知道这个工具具体能做什么,才能判断什么时候该用它。

参数说明要包含格式示例。比如日期参数,你要写清楚"格式为 YYYY-MM-DD,如 2024-01-15"。不然 LLM 可能传"今天"或者"1月15日"这种它自己理解的格式。

明确边界条件。如果工具有限制,比如"最多返回 100 条记录",一定要写在描述里。否则 LLM 可能期望返回 1000 条,结果拿到 100 条后不知道怎么处理。

避免功能重叠的工具。如果你有两个工具都能查用户信息,LLM 会困惑该用哪个。要么合并成一个,要么在描述里明确区分场景。

3.3 多轮工具调用的处理逻辑

实际场景中,一个任务往往需要多次工具调用。比如用户说"帮我查一下北京天气,如果下雨就提醒我带伞",这需要:

  1. 调用查天气工具
  2. 根据结果判断是否下雨
  3. 如果下雨,调用提醒工具

处理这种多轮调用的标准流程是:

messages = [{"role": "user", "content": user_input}] while True: response = llm.chat(messages=messages, tools=tools) if response.tool_calls: for tool_call in response.tool_calls: result = execute_tool(tool_call) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result }) else: # 没有工具调用了,返回最终回复 return response.content

这个循环会一直执行,直到 LLM 不再请求调用工具为止。这里有个坑:一定要设置最大循环次数,否则如果 LLM 陷入"调用工具-得到结果-再调用同一个工具"的死循环,你的程序就卡死了。我一般设置 10 次作为上限。

3.4 错误处理:工具调用失败怎么办

工具调用失败是常态,不是异常。网络超时、参数错误、权限不足、返回数据格式不对,这些都会发生。关键是怎么把错误信息有效地传回给 LLM,让它能做出合理的下一步决策。

我的做法是:永远不要把原始异常直接抛给 LLM。比如ConnectionError: timeout这种信息,LLM 看了也不知道怎么办。你应该把它转换成结构化的错误信息:

{ "error": true, "error_type": "timeout", "message": "查询天气服务超时,请稍后重试", "suggestion": "可以尝试重新调用,或告知用户服务暂时不可用" }

这样 LLM 就知道该怎么处理了——要么重试,要么告诉用户稍后再试。

4. MCP 协议:让工具系统标准化的尝试

4.1 MCP 解决了什么问题

Function Calling 虽然能用,但有个大问题:每个 Agent 框架都有自己的工具定义格式。你在 LangChain 里写的工具,搬到另一个框架里就得重写。这就像每个手机品牌都有自己的充电接口,换个手机就得换一堆线。

MCP(Model Context Protocol)就是为了解决这个问题而提出的。它定义了一套标准的工具描述和调用协议,让工具可以跨框架、跨平台复用。你可以把它理解为"工具系统的 USB-C 接口"。

MCP 的核心概念包括:

  • Server:提供工具的一方,负责定义和实现工具
  • Client:使用工具的一方,通常是 Agent 框架
  • Resources:工具可以访问的数据源
  • Tools:具体的可调用工具
  • Prompts:预定义的提示模板

4.2 MCP Server 的实际写法

写一个 MCP Server 其实不复杂。以 Python 为例,你可以用官方提供的 SDK:

from mcp.server import Server from mcp.types import Tool, TextContent server = Server("weather-server") @server.list_tools() async def list_tools(): return [ Tool( name="get_weather", description="查询指定城市的当前天气", inputSchema={ "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"} }, "required": ["city"] } ) ] @server.call_tool() async def call_tool(name: str, arguments: dict): if name == "get_weather": city = arguments["city"] # 实际查询逻辑 result = await fetch_weather(city) return [TextContent(type="text", text=result)]

这个 Server 写好后,任何支持 MCP 的 Client 都可以连接它并使用这些工具。你不需要为每个框架单独适配。

4.3 MCP 在实际项目中的取舍

MCP 听起来很美好,但实际用起来也有一些需要注意的地方。我在项目中总结了几点:

适合场景:当你需要把工具能力开放给多个不同的 Agent 或框架使用时,MCP 的价值最大。比如你公司内部有一套数据查询工具,想让不同团队开发的 Agent 都能用,那封装成 MCP Server 就很合适。

不太适合的场景:如果你的 Agent 只用一种框架,工具也只在这个 Agent 里用,那直接写 Function Calling 可能更简单。引入 MCP 会增加一层抽象,调试起来也更麻烦。

性能考虑:MCP 通常涉及进程间通信,比直接函数调用多了一层开销。对于高频调用的工具,这个开销可能不可忽略。我一般会把高频工具直接内置,低频工具才走 MCP。

调试难度:MCP 的调用链路比直接 Function Calling 长,出问题时排查起来更费劲。建议在开发阶段加详细的日志,记录每次工具调用的请求和响应。

4.4 MCP 生态的现状

目前 MCP 生态还在快速发展中。已经有一些现成的 MCP Server 可以直接用,比如文件系统操作、数据库查询、网页抓取等。也有一些工具平台开始支持 MCP 协议,让用户可以直接把平台能力接入 Agent。

但要注意的是,MCP 协议本身还在演进,不同版本之间可能有兼容性问题。如果你打算在生产环境使用,建议锁定版本,不要盲目追新。

5. Skills:比工具更高一层的抽象

5.1 Skills 和工具的区别

如果说工具是 Agent 的"手脚",那 Skills 更像是"技能包"。一个 Skill 可能包含多个工具的调用逻辑,加上特定的提示词和处理流程,形成一个完整的能力单元。

举个例子,"查天气"是一个工具,但"根据天气情况给出穿衣建议"就是一个 Skill。这个 Skill 内部会调用查天气工具,然后根据温度、湿度、风力等数据,结合预设的规则或额外的 LLM 推理,给出穿衣建议。

Skills 的价值在于:它把"怎么做一件事"的完整逻辑封装起来,Agent 只需要知道"有这个技能可用",不需要关心内部细节。这大大降低了 Agent 的决策复杂度。

5.2 Skills 的典型结构

一个 Skill 通常包含以下几个部分:

  • 名称和描述:告诉 Agent 这个技能是做什么的
  • 触发条件:什么情况下应该使用这个技能
  • 执行步骤:具体的操作流程,可能包含多个工具调用
  • 输入输出定义:需要什么参数,返回什么结果
  • 异常处理:出错时怎么处理

在实际实现中,Skills 可以是一个配置文件,也可以是一段代码。关键是要让 Agent 能够理解和使用。

5.3 设计 Skills 的实践经验

我在设计 Skills 时总结了几条原则:

一个 Skill 只做一件事。不要设计"处理用户请求"这种大而全的 Skill,要拆成"查询订单"、"修改地址"、"申请退款"等具体技能。粒度太粗的 Skill 会让 Agent 难以判断何时使用。

Skill 的描述要包含使用场景。不要只写"查询订单",要写"当用户询问订单状态、物流信息、预计送达时间时使用此技能"。这样 Agent 才能准确匹配。

Skill 之间尽量解耦。一个 Skill 不应该依赖另一个 Skill 的内部实现。如果确实需要组合,应该通过 Agent 的规划能力来编排,而不是硬编码依赖关系。

提供清晰的失败反馈。Skill 执行失败时,要返回足够的信息让 Agent 知道下一步该怎么做。是重试、换一个 Skill、还是告知用户,这些决策依赖于失败信息的质量。

5.4 Skills 的复用与管理

当 Skill 数量增多时,管理就成了问题。我的做法是建立一个 Skill 注册中心,所有 Skill 在这里注册,Agent 启动时动态加载。这样新增 Skill 不需要改 Agent 代码,只需要在注册中心添加配置。

同时,我会给每个 Skill 打标签,比如"查询类"、"操作类"、"分析类"。Agent 在规划时可以先按标签筛选,缩小选择范围,提高决策效率。

6. 把工具、MCP、Skills 串起来:一个完整的 Agent 工具系统

6.1 整体架构设计

在实际项目中,我通常会把这三者组合使用:

  • 底层用 Function Calling 实现具体工具:这是最基础的能力单元
  • 中层用 MCP 做工具的标准封装和跨框架复用:当工具有复用需求时
  • 上层用 Skills 做能力编排:把多个工具组合成完整的业务能力

这样的分层设计,既保证了灵活性,又兼顾了复用性和可维护性。

6.2 一个实际案例:智能客服 Agent

假设我们要做一个电商智能客服 Agent,它需要处理用户的订单查询、退换货、物流跟踪等请求。工具系统的设计可能是这样的:

工具层

  • query_order(order_id):查询订单详情
  • query_logistics(order_id):查询物流信息
  • create_return_request(order_id, reason):创建退货申请
  • send_notification(user_id, message):发送通知

MCP 层:把订单系统和物流系统的工具封装成 MCP Server,这样其他 Agent(比如售后 Agent、推荐 Agent)也能复用。

Skills 层

  • order_status_inquiry:订单状态查询技能,组合了查订单和查物流
  • return_process:退货处理技能,组合了查订单、创建退货、发通知
  • logistics_tracking:物流跟踪技能,调用查物流并格式化输出

Agent 收到用户请求后,先匹配 Skill,然后执行 Skill 内部的工具调用流程,最后生成回复。

6.3 性能优化的几个关键点

工具系统的性能直接影响 Agent 的响应速度。我总结了几条优化经验:

工具结果缓存:对于查询类工具,如果短时间内多次查询同样的参数,可以缓存结果。比如用户连续问"我的订单到哪了",没必要每次都查一遍物流接口。

并行调用:如果多个工具调用之间没有依赖关系,可以并行执行。比如同时查订单和查物流,而不是串行等待。

超时控制:每个工具调用都要设置超时,避免因为某个工具卡住导致整个 Agent 无响应。我一般设置 5-10 秒的超时。

结果裁剪:工具返回的数据可能很大,但 LLM 只需要关键信息。在传给 LLM 之前,先做一轮裁剪,只保留必要字段。这能显著减少 token 消耗和响应时间。

6.4 安全与权限控制

工具系统让 Agent 有了操作外部世界的能力,这也带来了安全风险。一个设计不当的工具系统,可能被恶意用户利用来执行未授权的操作。

我的做法是:

  • 最小权限原则:每个工具只授予完成其功能所需的最小权限
  • 参数校验:在工具执行前,严格校验参数格式和范围
  • 操作审计:记录所有工具调用的日志,便于追溯
  • 敏感操作二次确认:对于删除、支付等敏感操作,要求 Agent 先向用户确认

7. 常见问题与排查思路

7.1 Agent 不调用工具怎么办

这是新手最常遇到的问题。Agent 明明有工具可用,但就是不用,直接用自己的知识回答。原因通常有几个:

工具描述不够清晰:LLM 没理解这个工具是干什么的,自然不知道什么时候该用。解决方法是把描述写得更具体,包含使用场景。

系统提示词没引导:你需要在系统提示词里明确告诉 Agent"当遇到需要实时数据或外部操作时,优先使用工具"。有时候 LLM 会"偷懒",觉得自己的知识够用就不调工具了。

工具太多导致选择困难:如果一次给 LLM 几十个工具,它可能反而不知道该用哪个。解决方法是按场景分组,每次只给相关的工具。

7.2 工具调用参数错误怎么排查

参数错误通常表现为 LLM 传了错误的参数名、格式不对、或者缺少必填参数。排查步骤:

  1. 打印 LLM 返回的原始 tool_calls,看它实际传了什么
  2. 对比工具定义的 schema,找出不匹配的地方
  3. 检查工具描述里有没有明确说明参数格式
  4. 如果问题持续,考虑在描述里加更多示例

我遇到过一个典型案例:LLM 总是把日期传成"2024年1月15日"而不是"2024-01-15"。后来在参数描述里加了"格式必须为 YYYY-MM-DD,例如 2024-01-15",问题就解决了。

7.3 工具调用陷入死循环怎么处理

死循环的表现是 Agent 反复调用同一个工具,每次都得到相似的结果,但就是不结束。原因可能是:

  • 工具返回的结果 LLM 无法理解,它以为调用失败了,就重试
  • 任务本身无法完成,但 LLM 没有放弃机制
  • 工具描述有歧义,LLM 不确定是否已经完成

解决方法包括:设置最大调用次数、在工具返回中明确标注成功或失败、在系统提示词里加入"如果连续两次调用同一工具得到相同结果,应该停止并告知用户"。

7.4 工具响应太慢影响体验怎么办

工具响应慢是常见问题,尤其是涉及外部 API 调用时。优化思路:

  • 加缓存,减少重复调用
  • 设置合理的超时,超时后返回降级结果
  • 对于非关键工具,考虑异步执行,先给用户一个初步回复
  • 如果工具本身慢,考虑优化工具实现,比如加索引、减少数据传输量

8. 我在实际项目中的几点体会

工具系统的设计没有标准答案,不同的业务场景需要不同的取舍。但有几条原则我觉得是通用的:

工具描述的重要性被严重低估。很多人花大量时间优化 Agent 的提示词,却忽略了工具描述。实际上,工具描述是 LLM 理解工具能力的唯一途径,它的质量直接决定了工具调用的准确率。

错误处理比成功路径更重要。工具调用失败是常态,一个好的工具系统应该能优雅地处理各种失败情况,而不是一出错就崩溃。

不要过度设计。刚开始搭 Agent 时,用最简单的 Function Calling 就够了。等确实遇到复用问题、跨框架问题,再考虑引入 MCP。Skills 也是,等工具数量多了、组合逻辑复杂了,再抽象成 Skills。

持续观察和迭代。工具系统上线后,要持续收集调用日志,分析哪些工具调用频繁、哪些经常出错、哪些从来没被调用过。根据这些数据不断优化工具描述和系统提示词。

工具系统是 Agent 从"玩具"变成"工具"的关键一步。它让 Agent 真正能做事,而不只是能聊天。希望这篇文章能帮你少走一些弯路,更快地搭出好用的 Agent。

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

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

立即咨询