大语言模型基于海量语料训练而成,因此内化了大量已有的知识和信息,但除了输入外,其无法获取到最新信息或私域知识,从而导致幻觉产生。通过引入工具调用机制,AI智能体(AI Agent)可以让大语言模型以ReAct(Reasoning+Acting)方式逐步思考、判断是否需要调用工具,并通过调用工具获取外部信息和知识,并对外部环境施加影响,从而扩展大语言模型的能力。OpenAI于2023年推出Function Call,将工具调用能力标准化,随后,各家大语言模型也跟进支持Function Call。AI智能体可以基于格式化的协议和大语言模型交互,输入格式化的工具集合,由大语言模型返回格式化的需调用工具和参数,进而调用工具,再将工具调用结果输入大语言模型。Anthropic Claude于2024年推出MCP(Model Context Protocol),其设计了客户端——服务器架构,可以基于该架构将工具发布至MCP服务器(MCP Server),并通过MCP客户端(MCP Client)访问MCP服务器上的工具集合并调用工具。AI智能体可以针对自身的应用场景,选择所需的工具集合,引入相应的MCP服务器,从而无需重复建设工具。2025年,Manus作为通用AI智能体的代表之一,一经推出,便吸引了业界广泛的关注。Manus能够完成端到端的任务闭环,其针对用户提出的各类问题,能够分步思考、规划解决方案,完成操作浏览器、执行代码等各类操作。Manus本身并未开源,但从其发布的视频以及其联合创始人——季逸超透露的技术细节来看,其参考了CodeAct。CodeAct是基于ReAct的改进,其在ReAct逐步思考、规划的基础上,将大语言模型每步给出单一的工具和参数,升级为每步给出包含多个工具调用的代码,通过代码执行相对更为复杂的逻辑,从而减少ReAct的思考步数,减少上下文的长度。OpenManus是由MetaGPT参照Manus、快速复刻的开源通用AI智能体,其基于ReAct方式思考,能够通过工具操作浏览器、读写文件、执行代码。在本文中,笔者对AI智能体工具调用的相关概念进行梳理。
另外,Python目前是大语言模型和AI智能体领域的主要开发语言,其在数据分析、模型训练、模型推理、AI智能体等方面的开发上均有不少功能完备、发展迅速、生态健全的框架。而Java则在服务端开发上拥有大量相对比较成熟的框架。随着大语言模型和AI智能体的飞速发展,Java体系中的Spring框架生态也推出了相应的大语言模型和AI智能体开发框架——Spring AI,使得基于Java能够快速地开发AI智能体。在本文中,笔者基于Spring AI和Spring AI Alibaba对AI智能体及其工具调用的开发进行工程实践,介绍其中的工程结构、交互流程和底层原理。工程实践的代码已发布至GitHub仓库——spring-ai-example,读者可以下载该代码,申请并配置所需的阿里云模型服务和心知天气服务的API KEY,即可在本地Java开发环境中运行代码中的示例。
文中如有不足之处,欢迎指正。
概念梳理
智能体
基于大语言模型的智能体(Agent)其核心是大语言模型,大语言模型承担着大脑的角色,用于思考和规划,而围绕着大语言模型,Agent还包含记忆和工具,记忆用于存储短期上下文信息和长期知识信息,工具则承担着感官和四肢的角色,在大语言模型的思考和规划下,Agent一方面可以通过工具获取外部的各种信息用于进一步的思考和规划,另一方可以通过工具执行动作对外部环境施加影响。
《LLM Powered Autonomous Agents》中使用图1描述了基于大语言模型的Agent的整体架构,其中包含以下核心组件:
- 规划(Planning),大语言模型作为Agent的大脑负责思考和规划,而思考和规划的方式又可以分为两部分:
- 分而治之(Task Decomposition):对于复杂的任务,大语言模型会将其分解为多个相对简单的子任务,每个子任务包含独立的子目标,从而分而治之、逐步求解;
- 自我反思(Self-Reflection):大语言模型会对过去的规划和执行进行自我反思,分析其中的错误,并对后续的思考和规划进行改进,完善最后输出的结果。
- 记忆(Memory),记忆是对大语言模型本身由模型结构和参数所蕴含知识的补充,记忆又可以分为短期记忆和长期记忆:
- 短期记忆,即大语言模型的上下文学习,包括提示、指示、前序步骤的大语言模型推理结果和工具执行结果等;
- 长期记忆,即外部可快速检索的向量索引,这也就是目前比较流行的一种大语言模型应用的解决方案——RAG(Retrieval-Augmented Generation,检索增强生成)。RAG的流程可以简单概括为,将包含知识的文档切分为块,并对块向量化,构建块向量索引,然后将问题也向量化,然后从块向量索引中检索和问题相关的块,最后将块和问题合并作为大语言模型的输入进行推理。RAG可以有效缓解大语言模型无法扩展知识、由知识局限产生的“幻觉”的问题。
- 工具(Tool),工具作为Agent的感官和四肢,Agent一方面可以通过工具获取外部的各种信息用于进一步的思考和规划,例如通过搜索引擎搜索某个关键词的最新信息,另一方可以通过工具执行动作对外部环境施加影响,例如调用外部系统的接口执行指令并获取执行结果。
ReAct
2023年由Google和普林斯顿大学发表的论文《ReAct: Synergizing Reasoning and Acting in Language Models》提出了一种大语言模型思考和规划方式——ReAct。CoT(Chain of Thought)如图2左侧所示,仅依赖大语言模型进行多步推理,是一种“Reason Only”的思考和规划方式,缺乏执行,不能从外部获取信息,容易造成幻觉,并且错误会在多步推理中传播,另外也不能对外部施加影响,限制了Agent的作用。而ReAct的思考和规划方式如图2右侧所示,整体也是将复杂问题拆分为多个简单问题分步思考和规划,但和CoT不同的是,在每步思考和规划中,ReAct同时包含了推理(Reason)和执行(Action),通过大语言模型的思考和规划,既输出由自然语言描述的思考过程,也输出格式化的工具指令,而论文中设计的工具是可以对维基百科进行关键词检索的API,因此执行大语言模型输出的工具指令可以获取外部信息,将外部信息作为观察结果,和思考过程、工具指令合并,作为大语言模型下一步思考和规划的上下文。论文通过组合推理和执行,能够有效获取外部信息作为大语言模型的知识补充,从而在知识问答(HotpotQA)和事实验证(FEVER)等任务上取得较好的效果,而ReAct的命名即“Reason”(推理)和“Action”(执行)的组合、缩写。
具体如何实现ReAct?图3右侧是论文中给出的一个示例,其通过提示工程让大语言模型对复杂问题分步思考和规划,每步思考和规划按以下格式进行推理和执行:
Thought:[思考过程]
Action:[工具指令]
Observation:[观察结果]
每步思考和规划时,首先由大语言模型在“Thought”和“Action”后分别输出思考过程和工具指令,然后Agent解析“Action”后输出的工具指令,调用相应的工具执行指令,获取执行结果后,再拼接到“Observation”的后面。论文中设计的工具是可以对维基百科进行关键词检索的API,其包含三种格式的指令:
- Search[entity]:返回和“entity”最相关的5条语句,若不存在,则返回和“entity”最相似的5个实体;
- Lookup[string]:返回包含“string”的语句;
- Finish[answer]:结合上下文和大语言模型本身蕴含的知识,已获取当前任务的答案并返回。
图3右侧的示例,原问题是“Aside from the Apple Remote, what other device can control the program Apple Remote was originally designed to interact with?”,即“除了苹果遥控器之外,还有哪些设备可以控制最初设计用于和苹果遥控器交互的程序”,大语言模型第一步思考和规划给出的思考过程和工具指令分别是“I need to search App Remote and find the program it was originally designed to interact with.”和“Search[Apple Remote]”,即检索和“Apple Remote”相关的语句,查找和其交互的程序,因此Agent调用工具的API获取相关信息,拼接到“Observation”的后面,和第一步的思考过程、工具指令合并,作为大语言模型第二步思考和规划的上下文,大语言模型第二步思考和规划给出的思考过程和工具指令分别是“Apple Remote was originally designed to control the Front Row media center program. I need to search Front Row next and find what other device can control it.”和“Search[Front Row]”,即检索和“Front Row”相关的语句,查找可控制“Front Row”的设备,如此经过共4步的思考和规划过程,最终大语言模型输出正确的答案“keyword function keys”,即“keyword function keys”也可以控制最初设计用于和苹果遥控器交互的程序。
如何控制大语言模型对于原始问题分步思考和规划,且每步按照固定格式分别输出思考过程和工具指令?论文采用的方案是在提示中增加多个示例以引导大语言模型对后续的问题按照ReAct方式进行分步思考和规划。
Function Call
智能体需要大语言模型能够针对问题从多个候选工具中选择合适的工具进行调用,并给出调用工具所传递的参数,因此大语言模型的输入中既需要包括问题,也需要包括每个候选工具的功能、参数说明,还需要包括让大模型以何种格式输出需要调用工具的名称和参数值。
为了对候选工具的功能、参数的输入以及待调用工具的名称和参数值的输出进行规范化,OpenAI于2023年6月推出Function Call,首次将工具调用能力标准化,其采用JSON格式描述工具的名称、参数。后续不少厂商提供的大语言模型服务也支持Function Call形式的工具调用,并兼容OpenAI的API协议,可以按照OpenAI的API协议或直接使用OpenAI提供的SDK(仅需指定其他厂商提供的兼容OpenAI协议的API地址),访问其他厂商提供的大语言模型服务,并实现Function Call形式的工具调用。
下面以阿里云大语言模型服务为例,说明Function Call的交互协议和流程。
若AI智能体的系统提示(System Prompt)如下:
你是一个智能助手。今天的日期是:2025-05-16。
若用户提问如下:
明天北京的天气适合跑步吗?
因为大语言模型是基于过去某个时间点之前积累的语料训练得到的,所以其内化了过去的信息和知识,能够知道哪种天气适合跑步,但其并不知道最新的天气预报信息,因此只靠大语言模型本身并不能回答上述问题,需要先依赖外部工具获取最新的天气预报信息,再将最新的天气预报信息作为模型输入的上下文,由大语言模型基于最新的天气预报信息思考、回答用户提问。
若已有天气预报工具,其函数签名如下:
@Tool(description = "获取所传入城市今天、明天和后天的天气")
public String getDailyWeather(@ToolParam(description = "城市名称") String city)
其中,输入参数为字符串类型,表示城市名称,返回也为字符串类型,表示所传入城市今天、明天和后天的天气。若输入为“北京”,则返回如下,其中每天的天气信息包括白天天气、夜间天气、最高温度、最低温度、降水概率、降水量、风向、风速、风力等级、相对湿度:
北京后续几天的天气是:
2025-05-16的天气是:白天天气是雷阵雨,夜间天气是雷阵雨,最高温度是29度,最低温度是14度,降水概率是42.00%, 降水量是0.11mm,风向是南,风速是8.4km/h,风力等级是2,相对湿度是59%。
2025-05-17的天气是:白天天气是多云,夜间天气是多云,最高温度是25度,最低温度是12度,降水概率是0.00%, 降水量是0.00mm,风向是西北,风速是23.4km/h,风力等级是4,相对湿度是31%。
2025-05-18的天气是:白天天气是雷阵雨,夜间天气是多云,最高温度是25度,最低温度是17度,降水概率是96.00%, 降水量是29.50mm,风向是西南,风速是3.0km/h,风力等级是1,相对湿度是30%。
阿里云大语言模型既可以通过其封装的各类语言的SDK访问,也可按照OpenAI的API协议或直接使用OpenAI提供的SDK访问。对于上面所述的系统提示、用户提问和已有天气预报工具,若按照OpenAI的API协议访问阿里云大语言模型服务,则请求体如下所示:
{
"model": "qwen-plus",
"temperature": 0.8,
"messages": [
{
"role": "system",
"content": "你是一个智能助手。今天的日期是:2025-05-16。"
},
{
"role": "user",
"content": "明天北京的天气适合跑步吗?"
}
],
"tools": [
{
"type": "function",
"function":
{
"name": "getDailyWeather",
"description": "获取所传入城市今天、明天和后天的天气",
"parameters": {
"type": "object",
"properties": {
"arg0": {
"type": "string",
"description": "城市名称"
}
},
"required": ["arg0"]
}
}
}
]
}
其中,除了指定具体使用的模型类别是“qwen-plus”及相关模型参数外,其主要包含消息列表(messages)和工具列表(tools)两部分。
消息列表(messages)一般包含系统提示,以及用户、大语言模型和工具各方的当前和历史消息,每个消息通过“role”表示消息来源,“role”包含以下几类:
- system,表示系统提示;
- user,表示用户发出的消息;
- assistant,表示大语言模型发出的消息;
- tool,表示工具发出的消息。
每个消息通过“content”记录具体的消息内容,对于大语言模型发出的消息,可能还会通过“tool_calls”记录结构化的待调用工具和参数值信息。上述请求体的消息列表包含两个消息,分别是系统提示和用户提问。
工具列表(tools)包含候选的工具。对于每一个工具,记录工具的名称(name)、描述(description)和参数集合(parameters),对于每个参数,记录参数的名称、类型(type)和描述(description),并标记参数是否必须(required)。上述请求体的工具列表包含一个天气预报工具。
目前大语言模型底层模型结构源自Google于2017年提出的Transformer,其本质是通过模型对输入的一段自然语言文本进行续写。而对于上述输入模型的结构化信息(消息列表和工具列表),如何转化为自然语言文本。目前不同的大语言模型有不同的会话模板(Chat Template)。GitHub项目chat_templates记录了常见大语言模型的会话模板,其中qwen2.5-instruct的会话模板如下所示:
{%- if tools %}
{{- '<|im_start|>system
' }}
{%- if messages[0]['role'] == 'system' %}
{{- messages[0]['content'] }}
{%- else %}
{{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}
{%- endif %}
{{- "
# Tools
You may call one or more functions to assist with the user query.
You are provided with function signatures within <tools></tools> XML tags:
<tools>" }}
{%- for tool in tools %}
{{- "
" }}
{{- tool | tojson }}
{%- endfor %}
{{- "
</tools>
For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
<tool_call>
{\"name\": <function-name>, \"arguments\": <args-json-object>}
</tool_call><|im_end|>
" }}
{%- else %}
{%- if messages[0]['role'] == 'system' %}
{{- '<|im_start|>system
' + messages[0]['content'] + '<|im_end|>
' }}
{%- else %}
{{- '<|im_start|>system
You are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>
' }}
{%- endif %}
{%- endif %}
{%- for message in messages %}
{%- if (message.role == "user") or (message.role == "system" and not loop.first) or (message.role == "assistant" and not message.tool_calls) %}
{{- '<|im_start|>' + message.role + '
' + message.content + '<|im_end|>' + '
' }}
{%- elif message.role == "assistant" %}
{{- '<|im_start|>' + message.role }}
{%- if message.content %}
{{- '
' + message.content }}
{%- endif %}
{%- for tool_call in message.tool_calls %}
{%- if tool_call.function is defined %}
{%- set tool_call = tool_call.function %}
{%- endif %}
{{- '
<tool_call>
{"name": "' }}
{{- tool_call.name }}
{{- '", "arguments": ' }}
{{- tool_call.arguments | tojson }}
{{- '}
</tool_call>' }}
{%- endfor %}
{{- '<|im_end|>
' }}
{%- elif message.role == "tool" %}
{%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") %}
{{- '<|im_start|>user' }}
{%- endif %}
{{- '
<tool_response>
' }}
{{- message.content }}
{{- '
</tool_response>' }}
{%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
{{- '<|im_end|>
' }}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- if add_generation_prompt %}
{{- '<|im_start|>assistant
' }}
{%- endif %}
其通过模板语言Jinja将结构化的消息列表和工具列表转化为采用OpenAI的ChatML格式的自然语言文本,该文本中使用“<|im_start|>...<|im_end|>”表示一个消息。上述请求体的消息列表和工具列表,通过上述会话模板转化得到的自然语言文本如下所示:
<|im_start|>system
你是一个智能助手。今天的日期是:2025-05-16。
# Tools
You may call one or more functions to assist with the user query.
You are provided with function signatures within <tools></tools> XML tags:
<tools>
{"type":"function","function":{"name":"getDailyWeather","description":"获取所传入城市今天、明天和后天的天气","parameters":{"type":"object","properties":{"arg0":{"type":"string","description":"城市名称"}},"required":["arg0"]}}}
</tools>
For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
<tool_call>
{"name": <function-name>, "arguments": <args-json-object>}
</tool_call><|im_end|>
<|im_start|>user
明天北京的天气适合跑步吗?<|im_end|>
<|im_start|>assistant
其中,每个“<|im_start|>...<|im_end|>”段表示一个消息,共两个消息,第一个消息是系统提示,第二个消息是用户提问。第一个消息——系统提示中除了智能体自定义的系统提示外,还在“<tools>...</tools>”段中包含结构化的工具信息,并指示大语言模型若判断需调用工具,则在续写时,在“<tool_call>...</tool_call>”段中包含结构化的待调用工具信息(工具名称及其参数值)。第二个消息——用户提问即原始的用户提问消息。上述自然语言文本输入大语言模型后,由模型进行续写,直接输出答案或按指示输出待调用工具。
不同的大语言模型有不同的会话模板,而各大语言模型会基于自身的会话模板所转化的会话和工具调用语料数据进行微调,因此具有很好的指令跟随能力。
大语言模型直接输出答案或按指示输出待调用工具后,大语言模型服务会再将其转化为结构化的输出返回给请求侧。采用OpenAI的API协议的输出如下所示:
{
"choices": [{
"message": {
"content": "",
"role": "assistant",
"tool_calls": [{
"index": 0,
"id": "call_d3e6934af47a424a81ccc0",
"type": "function",
"function": {
"name": "getDailyWeather",
"arguments": "{\"arg0\": \"北京\"}"
}
}]
},
"finish_reason": "tool_calls",
"index": 0,
"logprobs": null
}],
"object": "chat.completion",
"usage": {
"prompt_tokens": 205,
"completion_tokens": 19,
"total_tokens": 224,
"prompt_tokens_details": {
"cached_tokens": 0
}
},
"created": 1747452699,
"system_fingerprint": null,
"model": "qwen-plus",
"id": "chatcmpl-c626e296-9dd0-9949-b906-ed383297d2b5"
}
其中,“choices”表示模型生成内容的数组,可以包含一个或多个“choices”对象,上例中模型返回一个“choice”对象,该对象的“finish_reason”为“tool_calls”,表示模型认为需调用工具,消息的“content”为空,但在“tool_calls”中包含需调用的工具名称和参数值。后续智能体可以调用相应的工具。完整的Function Call示例后续会在工程实践部分介绍。
MCP
Function Call将工具调用能力标准化。智能体按照协议将结构化的工具信息传递给大语言模型,大语言模型也按照协议将结构化的待调用工具和参数值返回给智能体。通过Function Call提升了智能体和大语言模型交互、由大语言模型决定使用哪种工具的效率,并且避免了若智能体自行设计工具调用的提示、与大语言模型工具调用的微调语料格式不一致、进而影响大语言模型在工具调用上的指令跟随效果的问题。
虽然Function Call解决了大语言模型如何高效、便捷地为智能体选择哪个工具调用的问题,但是Function Call并没有解决如何高效、便捷地为智能体实现和管理工具的问题。在FunctionCall这一节的示例中,智能体需要事先设计名称为“getDailyWeather”的获取城市天气预报的工具,描述其信息,实现其功能。若该智能体还计划引入其他工具能力,则需要进一步设计和实现其他工具。而若希望已有的工具能被其他智能体复用,虽然可以通过API的方式对外提供调用接口,但缺乏标准化的工具描述和调用协议,即使是以API方式对接已有工具、非完全重新实现工具功能,对于智能体来说,引入已有工具、实现工具调用也有一定的开发成本。
MCP解决了如何高效、便捷地为智能体实现和管理工具的问题。当然,MCP并不仅限于工具调用,它还包括对资源和提示的管理。但本篇主要聚焦在工具调用方面的介绍。
以下关于MCP的介绍摘抄自《Claude MCP 简介》。Claude MCP,即模型上下文协议(Model Context Protocol),是Anthropic Claude开源的一个开放协议,旨在建立大语言模型和开发环境之间的统一上下文交互,通过提供标准化的上下文信息访问,使大语言模型能够更好地理解和处理代码,就像给它们之间搭建了一座桥梁,使得开发者可以通过一套标准将智能体应用和数据源连接起来。MCP协议遵循客户端(MCP Client)- 主机(MCP Host)- 服务器(MCP Server)架构,其中每个主机(MCP Host)可以运行多个客户端(MCP Client)实例。
MCP协议架构如图4所示,这里主要涉及到三个角色:
- 主机是发起连接的智能体应用。主机进程是MCP协议的中心组件,负责管理客户端实例的生命周期,并协调客户端与服务器之间的通信。主机可以运行多个客户端实例,每个客户端实例可以连接到不同的服务器。主机通过JSON-RPC协议与客户端和服务器进行通信。
- 客户端在主机应用程序内部与服务器保持一对一的连接。客户端用于维护主机与服务器的独立连接,并处理用户的输入和输出。
- 服务器向客户端提供上下文、工具和提示。服务器是MCP协议的资源存储和处理中心,负责存储和管理本地和远程资源,并提供给客户端使用。服务器可以运行多个服务器实例,每个服务器实例可以处理不同的资源类型。服务器通过JSON-RPC协议与主机和客户端进行通信。
MCP协议定义了两种标准的客户端——服务器通信传输层:
- STDIO。在STDIO传输层中,客户端将MCP服务器作为子进程启动,服务器通过标准输入(STDIN)接收JSON-RPC消息,并通过标准输出(STDOUT)写入响应,STDIO传输层交互流程如图5所示。
- HTTP SSE。SSE全称是Server-Sent Events,是一种HTTP服务器推送技术,允许服务器向客户端发送实时更新。在HTTP SSE传输层中,服务器作为独立进程运行,可以处理多个客户端连接。当客户端连接时,服务器必须发送一个包含客户端用于发送消息的URI的endpoint事件。所有后续的客户端消息必须作为HTTP POST请求发送到此端点。服务器消息作为SSE message事件发送,消息内容以JSON格式编码在事件数据中。HTTP SSE传输层交互流程如图6所示。
在工具调用方面,基于MCP,我们可以把已开发的工具以MCP Server的方式对外发布,而各个智能体应用可以通过MCP Client对接MCP Server,每个MCP Client对应于一个MCP Server,MCP Client负责与MCP Server建立连接,连接的数据传输层可以是跨进程的本地访问,也可以是基于HTTP SSE协议的远程访问。MCP Client作为MCP Server的代理,可以获取该MCP Server的可用工具集的详细信息,包括工具名称、描述、参数,这些详细信息兼容Function Call的协议,因此,可以通过Function Call的协议将MCP Server中的可用工具集的详细信息发送给大语言模型。而大语言模型服务返回需调用的工具名称和参数值后,智能体应用可以通过MCP Client向工具所在的MCP Server发起对调用工具的请求。
基于上述这套机制,智能体应用无需投入成本建设工具,只需要根据自己的应用场景设计需要采用哪些工具和大语言模型,而具体工具的实现可以通过引入包含该工具的MCP Server,通过构建MCP Client建立与这些MCP Server的连接即可发起对该工具的调用。
目前不少互联网厂商均对外提供了自身服务的MCP Server,比如支付宝对外提供支付相关的MCP Server,高德地图对外提供了地图相关的MCP Server,厂商均希望在AI发展的浪潮中,自身服务能够被更多的智能体应用所引用,从而抢占相应的市场。另外,不少企业也开始建设MCP Server的市场,收集和组织各方的MCP Server,从而抢占MCP Server分发的流量入口,比如MCP.so。图7是MCP.so收录的高德地图对外提供地图相关的MCP Server。
对于如何实现MCP Client和MCP Server?MCP官方提供了多种语言的SDK,包括Python SDK、Java SDK等,这些SDK中定义的核心组件包括McpClient和McpServer,用于分别实现MCP Client和MCP Server。
工程实践
笔者基于Spring AI和Spring AI Alibaba对AI智能体及其工具调用的开发进行工程实践,后续内容将介绍其中的工程结构、交互流程和底层原理。工程实践的代码已发布至GitHub仓库——spring-ai-example,读者可以下载该代码,申请并配置所需的阿里云模型服务和心知天气服务的API KEY,即可在本地Java开发环境中运行。
工程实践的代码包含以下几个模块:
- chat-example,不支持工具调用的会话示例,大语言模型使用阿里云模型服务提供的通义千问-Plus(qwen-plus)。
- tool-example,工具示例,封装心知天气的API,获取所传入城市今天、明天和后天的天气。
- agent-example,支持工具调用的会话示例,将本地方法作为工具进行调用。
- mcp-server-example,MCP Server示例,数据传输层采用STDIO,即智能体应用可以跨进程本地访问该MCP Server。
- mcp-server-webflux-example,MCP Server示例,数据传输层采用HTTP SSE,即智能体应用可以基于HTTP SSE协议远程访问该MCP Server。
- mcp-client-example,支持工具调用的会话示例,将MCP Server作为工具进行调用。
- manus-example,参照OpenManus,Manus的简版复刻示例,以ReAct方式思考,引入的工具包括浏览器操作、文件读写、代码执行。
Spring AI和Spring AI Alibaba
以下关于Spring AI和Spring AI Alibaba的介绍摘抄自摘抄自《Spring AI Alibaba》。Spring AI Alibaba开源项目基于Spring AI构建,是阿里云通义系列模型及服务在Java AI应用开发领域的最佳实践,提供高层次的AI API抽象与云原生基础设施集成方案,帮助开发者快速构建AI应用。
Spring AI Alibaba作为开发AI应用程序的基础框架,定义了以下抽象概念与API,并提供了API与通义系列模型的适配:
- 开发复杂AI应用的高阶抽象Fluent API -
ChatClient。 - 提供多种大模型服务对接能力,包括主流开源与阿里云通义大模型服务(百炼)等。
- 支持的模型类型包括聊天、文生图、音频转录、文生语音等。
- 支持同步和流式API,在保持应用层API不变的情况下支持灵活切换底层模型服务,支持特定模型的定制化能力(参数传递)。
- 支持Structured Output,即将AI模型输出映射到POJOs。
- 支持矢量数据库存储与检索。
- 支持函数调用Function Calling。
- 支持构建AI Agent所需要的工具调用和对话内存记忆能力。
- 支持 RAG 开发模式,包括离线文档处理如
DocumentReader、Splitter、Embedding、VectorStore等,支持Retrieve检索。
本篇笔记中所用的Spring AI的版本是1.0.0-M6,Spring AI Alibaba的版本是1.0.0-M6.1。Spring AI中包含多种组件,在后续的工程实践中,主要使用的组件包括ChatModel和ChatClient。
ChatModel表示大语言模型,ChatModel的接口定义如下,其核心方法就是传入提示(Prompt,其内部成员变量就是一组消息)或一组消息,由大语言模型返回回答:
package org.springframework.ai.chat.model;
import java.util.Arrays;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.Model;
public interface ChatModel extends Model<Prompt, ChatResponse>, StreamingChatModel {
default String call(String message) {
Prompt prompt = new Prompt(new UserMessage(message));
Generation generation = call(prompt).getResult();
return (generation != null) ? generation.getOutput().getText() : "";
}
default String call(Message... messages) {
Prompt prompt = new Prompt(Arrays.asList(messages));
Generation generation = call(prompt).getResult();
return (generation != null) ? generation.getOutput().getText() : "";
}
@Override
ChatResponse call(Prompt prompt);
default ChatOptions getDefaultOptions() {
return ChatOptions.builder().build();
}
default Flux<ChatResponse> stream(Prompt prompt) {
throw new UnsupportedOperationException("streaming is not supported");
}
}
不同的大语言模型有各自的ChatModel的实现,例如OpenAI的ChatModel的实现是OpenAiChatModel,阿里云模型服务的ChatModel的实现是DashScopeChatModel。
智能体应用可以直接通过ChatModel访问大语言模型,但Spring AI进一步对ChatModel进行封装,对外提供了ChatClient接口,及其默认实现DefaultChatClient。DefaultChatClient中封装了ChatModel、拦截器和工具等,从而支持Function Call以及其他复杂的业务逻辑。拦截器的接口是Advisor,通过实现该接口,可以在智能体应用和大语言模型交互的各个环节中增加业务逻辑,比如会话记忆、会话日志等。工具的接口是ToolCallback,Spring AI默认已有多个ToolCallback实现,例如,MethodToolCallback支持将本地方法作为工具进行调用,SyncMcpToolCallback支持对MCP Server中的工具进行同步调用。
无工具调用
该部分完整代码参见chat-example模块,这里不列出pom.xml的具体内容,其中主要引入spring-ai-alibaba-starter依赖,实现对智能体应用的配置化启动。
配置文件application.yml的内容如下所示:
spring:
main:
web-application-type: none
ai:
dashscope:
api-key: ${AI_DASHSCOPE_API_KEY}
logging:
level:
org.springframework.ai.chat.client.advisor: DEBUG
其中通过spring.main.ai.dashscope.api-key配置阿里云模型服务的API KEY。需事先登录阿里云模型服务(dashscope.console.aliyun.com/overview)创建… KEY,并将API KEY设置为环境变量:
export AI_DASHSCOPE_API_KEY=sk-xxx
引入的spring-ai-alibaba-starter中包含DashScopeAutoConfiguration,其会根据配置自动初始化DashScopeChatModel实例。
启动类ChatApplication代码如下,其中主要逻辑包括:
- 初始化
ChatClient实例,构造ChatClient实例时,传入由系统自动初始化的ChatModel实例,以及实现会话记忆和日志的Advisor实例,并设置系统提示。 - 初始化
CommandLineRunner示例,在控制台中等待用户输入,对于用户输入,请求大语言模型,输出返回。
package com.magicwt;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.time.LocalDate;
import java.util.Scanner;
@SpringBootApplication
public class ChatApplication {
public static void main(String[] args) {
SpringApplication.run(ChatApplication.class, args);
}
@Bean
ChatClient chatClent(ChatModel chatModel) {
SimpleLoggerAdvisor loggerAdvisor = new SimpleLoggerAdvisor(
advisedRequest -> ModelOptionsUtils.toJsonStringPrettyPrinter(advisedRequest),
chatResponse -> ModelOptionsUtils.toJsonStringPrettyPrinter(chatResponse), 0);
return ChatClient.builder(chatModel).defaultSystem("你是一个智能助手。今天的日期是:{current_date}。")
.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()), loggerAdvisor)
.build();
}
@Bean
CommandLineRunner cli(ChatClient chatClient) {
return args -> {
var scanner = new Scanner(System.in);
System.out.println("\n让我们开始聊天!");
while (true) {
System.out.print("\n用户: ");
System.out.println("助手: "
+ chatClient.prompt().system(s -> s.param("current_date", LocalDate.now().toString()))
.user(scanner.nextLine()).call().content());
}
};
}
}
启动上述示例,若用户输入“什么样的天气适合跑步?”,针对该用户提问,以OpenAI的API协议输入大语言模型的消息列表是:
[
{
"role": "system",
"content": "你是一个智能助手。今天的日期是:2025-05-19。"
},
{
"role": "user",
"content": "什么样的天气适合跑步?"
}
]
因为大语言模型基于过去一段时间积累的大量语料训练而成,已内化了大量知识,因此能够比较准确地回答上述问题,最终大语言模型返回如下:
适合跑步的天气通常具备以下几个条件:
1. **温度适中**:理想的跑步温度一般在10°C到20°C之间。这样的温度不会让身体过热,也不会导致过度失水。
2. **湿度较低**:湿度低于60%是比较理想的。高湿度会让人感觉更热,并增加中暑的风险。
3. **无风或微风**:轻微的风可以帮助降温,但如果风太大,可能会增加跑步的难度和能量消耗。
4. **晴朗或多云**:晴朗或多云的天气有助于保持良好的能见度和心情,但要避免强烈的阳光直射以减少脱水和晒伤的风险。
5. **无雨或小雨**:轻微的小雨可能不会影响跑步,但大雨会导致路面湿滑,增加受伤风险,并使跑步变得不舒适。
总之,在选择跑步的时间和地点时,尽量避开极端天气条件,如高温、严寒、暴雨或大风等,以确保安全和舒适的运动体验。
而若用户输入“明天北京的天气适合跑步吗?”,以OpenAI的API协议输入大语言模型的消息列表是:
[
{
"role": "system",
"content": "你是一个智能助手。今天的日期是:2025-05-19。"
},
{
"role": "user",
"content": "明天北京的天气适合跑步吗?"
}
]
由于模型无法获取到最新的天气预报,无法给出准确地回答,最终大语言模型返回如下:
很抱歉,我无法实时获取天气信息或预测具体某一天的天气状况。不过,如果你想了解明天北京的天气是否适合跑步,可以通过以下方式判断:
1. **查看天气预报**:通过天气应用、网站(如中国天气网、墨迹天气等)或智能助手查询北京2025年5月20日的天气情况。
- 如果是晴天或多云,气温在15°C至25°C之间,风速较小,湿度适中,通常是适合跑步的好天气。
- 如果有雨、大风或极端高温/低温,则可能不适合跑步。
2. **空气质量指数(AQI)**:跑步时还需要关注北京的空气质量。如果AQI超过100(尤其是达到“重度污染”级别),建议减少户外活动或佩戴口罩。
如果你能提供具体的天气数据,我可以帮你进一步分析是否适合跑步!
天气预报工具
该部分完整代码参见tool-example模块。这里没有按照Spring AI Alibaba的官方示例,使用WeatherApi提供的天气预报接口,而是使用的心知天气提供的"未来15天逐日天气预报和昨日天气"预报接口,该接口文档地址是:seniverse.yuque.com/hyper_data/…,请求参数说明如下:
| 参数名称 | 类型 | 默认值 | 必填 | 备注 |
|---|---|---|---|---|
| key | String | 无 | 是 | 你的 API 密钥 |
| location | Location | 无 | 是 | 所查询的位置 |
| language | Language | zh-Hans | 否 | 语言 |
| unit | Unit | c | 否 | 单位 |
| start | Start | 0 | 否 | 起始时间 |
| days | Int | 权限允许的最多天数 | 否 | 天数 |
请求示例如下(需事先开通免费版,获取相应的API KEY),获取北京最近3天(包括今天)的天气预报:
https://api.seniverse.com/v3/weather/daily.json?key=xxx&location=北京&language=zh-Hans&unit=c&start=0&days=3
请求返回如下,每天的天气预报包括白天天气、夜间天气、最高温度、最低温度、降水概率、降水量、风向、风速、,风力等级、相对湿度等:
{
"results": [{
"location": {
"id": "xxx",
"name": "北京",
"country": "CN",
"path": "北京,北京,中国",
"timezone": "Asia/Shanghai",
"timezone_offset": "+08:00"
},
"daily": [
{
"date": "2025-05-19",
"text_day": "晴",
"code_day": "0",
"text_night": "晴",
"code_night": "1",
"high": "31",
"low": "17",
"rainfall": "0.00",
"precip": "0.00",
"wind_direction": "南",
"wind_direction_degree": "180",
"wind_speed": "8.4",
"wind_scale": "2",
"humidity": "57"
},
{
"date": "2025-05-20",
"text_day": "多云",
"code_day": "4",
"text_night": "多云",
"code_night": "4",
"high": "33",
"low": "17",
"rainfall": "0.00",
"precip": "0.00",
"wind_direction": "东北",
"wind_direction_degree": "45",
"wind_speed": "8.4",
"wind_scale": "2",
"humidity": "55"
},
{
"date": "2025-05-21",
"text_day": "多云",
"code_day": "4",
"text_night": "多云",
"code_night": "4",
"high": "29",
"low": "18",
"rainfall": "0.00",
"precip": "0.00",
"wind_direction": "东北",
"wind_direction_degree": "45",
"wind_speed": "3.0",
"wind_scale": "1",
"humidity": "47"
}
],
"last_update": "2025-05-19T08:00:00+08:00"
}]
}
将上述接口封装为工具类WeatherTool的一个方法,以下仅列出方法签名:
@Tool(description = "获取所传入城市今天、明天和后天的天气")
public String getDailyWeather(@ToolParam(description = "城市名称") String city)
该方法输入为字符串类型,表示需要查询天气的城市名称,输出为字符串类型,表示所传入城市今天、明天和后天的天气。例如输入“北京”,返回如下:
北京后续几天的天气是:
2025-05-19的天气是:白天天气是晴,夜间天气是晴,最高温度是31度,最低温度是17度,降水概率是0.00%, 降水量是0.00mm,风向是南,风速是8.4km/h,风力等级是2,相对湿度是57%。
2025-05-20的天气是:白天天气是多云,夜间天气是多云,最高温度是33度,最低温度是17度,降水概率是0.00%, 降水量是0.00mm,风向是东北,风速是8.4km/h,风力等级是2,相对湿度是55%。
2025-05-21的天气是:白天天气是多云,夜间天气是多云,最高温度是29度,最低温度是18度,降水概率是0.00%, 降水量是0.00mm,风向是东北,风速是3.0km/h,风力等级是1,相对湿度是47%。
注意,使用该工具前,需要事先将心知天气的API KEY设置为环境变量:
export WEATHER_API_KEY=xxx
Function Call
该部分完整代码参见agent-example模块,其代码与chat-example模块基本一致,只是在初始化ChatClient时,引入工具,启动类AgentApplication代码如下:
package com.magicwt;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.time.LocalDate;
import java.util.Scanner;
@SpringBootApplication
public class AgentApplication {
public static void main(String[] args) {
SpringApplication.run(AgentApplication.class, args);
}
@Bean()
ChatClient chatClent(ChatModel chatModel) {
SimpleLoggerAdvisor loggerAdvisor = new SimpleLoggerAdvisor(
advisedRequest -> ModelOptionsUtils.toJsonStringPrettyPrinter(advisedRequest),
chatResponse -> ModelOptionsUtils.toJsonStringPrettyPrinter(chatResponse), 0);
return ChatClient.builder(chatModel).defaultSystem("你是一个智能助手。今天的日期是:{current_date}。")
.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()), loggerAdvisor)
.defaultTools(new WeatherTools()).build();
}
@Bean
CommandLineRunner cli(ChatClient chatClient) {
return args -> {
var scanner = new Scanner(System.in);
System.out.println("\n让我们开始聊天!");
while (true) {
System.out.print("\n用户: ");
System.out.println("助手: "
+ chatClient.prompt().system(s -> s.param("current_date", LocalDate.now().toString()))
.user(scanner.nextLine()).call().content());
}
};
}
}
上述代码中,初始化WeatherTool示例,最终该实例会被MethodToolCallbackProvider扫描其中被Tool注解的方法,生成相应的MethodToolCallback实例,而MethodToolCallback实例作为工具,其工具信息会被传至大语言模型,待大语言模型返回需调用该工具时,通过调用MethodToolCallback实例的call方法实现工具调用。MethodToolCallbackProvider中生成MethodToolCallback实例的代码如下:
@Override
public ToolCallback[] getToolCallbacks() {
var toolCallbacks = toolObjects.stream()
.map(toolObject -> Stream.of(ReflectionUtils.getDeclaredMethods(toolObject.getClass()))
.filter(toolMethod -> toolMethod.isAnnotationPresent(Tool.class))
.filter(toolMethod -> !isFunctionalType(toolMethod))
.map(toolMethod -> MethodToolCallback.builder()
.toolDefinition(ToolDefinition.from(toolMethod))
.toolMetadata(ToolMetadata.from(toolMethod))
.toolMethod(toolMethod)
.toolObject(toolObject)
.toolCallResultConverter(ToolUtils.getToolCallResultConverter(toolMethod))
.build())
.toArray(ToolCallback[]::new))
.flatMap(Stream::of)
.toArray(ToolCallback[]::new);
validateToolCallbacks(toolCallbacks);
return toolCallbacks;
}
启动上述示例,若用户输入“明天北京的天气适合跑步吗?”,则智能体应用和大语言模型、工具的交互流程如图9所示,智能体应用请求大语言模型两次,第一次请求输入包括系统提示和用户提问,大语言模型返回需要调用工具查询天气,智能体应用调用相应的工具查询天气,第二次请求输入除包括系统提示和用户提问外,还包括第一次请求大语言模型的返回,以及调用工具的返回(即天气预报信息),大语言模型返回最终的答案。下面会较详细地分析该交互流程。
针对上述用户输入,以OpenAI的API协议输入大语言模型的消息列表如下所示:
[
{
"role": "system",
"content": "你是一个智能助手。今天的日期是:2025-05-05。"
},
{
"role": "user",
"content": "明天北京的天气适合跑步吗?"
}
]
同时,输入大语言模型的工具列表如下所示:
[
{
"type": "function",
"function":
{
"name": "getDailyWeather",
"description": "获取所传入城市今天、明天和后天的天气",
"parameters": {
"type": "object",
"properties": {
"arg0": {
"type": "string",
"description": "城市名称"
}
},
"required": ["arg0"]
}
}
}
]
大模型输出的响应如下所示:
[
{
"message": {
"role": "assistant",
"content": "",
"tool_calls": [{
"index": 0,
"id": "call_286e28b4883d4eb3ac4685",
"type": "function",
"function": {
"name": "getDailyWeather",
"arguments": "{\"arg0\": \"北京\"}"
}
}]
},
"finish_reason": "tool_calls",
"index": 0,
"logprobs": null
}
]
可见,大语言模型输出的响应并没有给出直接回答,而是需要调用工具,并给出调用工具的名称和参数值。而在DashScopeChatModel的call方法中,其会判断大语言模型是否返回需要调用工具,若返回需要调用工具,则执行以下分支逻辑:
if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(chatResponse,
Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) {
var toolCallConversation = handleToolCalls(prompt, chatResponse);
// Recursively call the call method with the tool call message
// conversation that contains the call responses.
return this.call(new Prompt(toolCallConversation, prompt.getOptions()));
}
其中先调用工具,再递归调用call方法,而不是直接返回。而具体调用工具的方法在AbstractToolCallSupport中,如下所示:
protected ToolResponseMessage executeFunctions(AssistantMessage assistantMessage, ToolContext toolContext) {
List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList<>();
for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
var functionName = toolCall.name();
String functionArguments = toolCall.arguments();
if (!this.functionCallbackRegister.containsKey(functionName)) {
throw new IllegalStateException("No function callback found for function name: " + functionName);
}
String functionResponse = this.functionCallbackRegister.get(functionName)
.call(functionArguments, toolContext);
toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), functionName, functionResponse));
}
return new ToolResponseMessage(toolResponses, Map.of());
}
其中根据大语言模型返回的functionName从functionCallbackRegister中找到相应的ToolCallBack实例,并调用其中的call方法,完成工具调用,获取结果。本示例中ToolCallBack实例的信息如下:
MethodToolCallback{toolDefinition=DefaultToolDefinition[name=getDailyWeather, description=获取所传入城市今天、明天和后天的天气, inputSchema={
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"arg0" : {
"type" : "string",
"description" : "城市名称"
}
},
"required" : [ "arg0" ],
"additionalProperties" : false
}], toolMetadata=DefaultToolMetadata[returnDirect=false]}
其类型是MethodToolCallback,包含两个成员变量:toolObject(类型为Object)和toolMethod(类型为Method)。这两个成员变量的值分别是系统启动时初始化的WeatherTools实例以及WeaterTools实例中的getDailyWeather方法。而MethodToolCallback的call方法最终通过反射机制调用WeaterTools实例中的getDailyWeather方法以实现工具调用,代码如下所示:
@Nullable
private Object callMethod(Object[] methodArguments) {
if (isObjectNotPublic() || isMethodNotPublic()) {
toolMethod.setAccessible(true);
}
Object result;
try {
result = toolMethod.invoke(toolObject, methodArguments);
}
catch (IllegalAccessException ex) {
throw new IllegalStateException("Could not access method: " + ex.getMessage(), ex);
}
catch (InvocationTargetException ex) {
throw new ToolExecutionException(toolDefinition, ex.getCause());
}
return result;
}
完成调用工具后,智能体应用递归第二次调用DashScopeChatModel的call方法请求大语言模型。第二次调用,输入大语言模型的消息列表如下所示:
[
{
"role": "system",
"content": "你是一个智能助手。今天的日期是:2025-05-05。"
},
{
"role": "user",
"content": "明天北京的天气适合跑步吗?"
},
{
"role": "assistant",
"tool_calls": [
{
"index": 0,
"id": "call_286e28b4883d4eb3ac4685",
"type": "function",
"function": {
"name": "getDailyWeather",
"arguments": "{\"arg0\": \"北京\"}"
}
}
]
},
{
"role": "tool",
"content": "北京后续几天的天气是:\r\n2025-05-05的天气是:白天天气是雷阵雨,夜间天气是多云,最高温度是21度,最低温度是11度,降水概率是44.00%, 降水量是0.18mm,风向是西南,风速是8.4km/h,风力等级是2,相对湿度是64%。\r\n2025-05-06的天气是:白天天气是多云,夜间天气是晴,最高温度是23度,最低温度是11度,降水概率是0.00%, 降水量是0.00mm,风向是北,风速是8.4km/h,风力等级是2,相对湿度是46%。\r\n2025-05-07的天气是:白天天气是晴,夜间天气是多云,最高温度是24度,最低温度是14度,降水概率是0.00%, 降水量是0.00mm,风向是西南,风速是15.3km/h,风力等级是3,相对湿度是58%。\r\n",
"tool_call_id": "call_286e28b4883d4eb3ac4685"
}
]
大语言模型输出的响应如下所示:
[
{
"message": {
"role": "assistant",
"content": "明天(2025年5月6日)北京的天气是白天多云,夜间晴朗,气温在11度到23度之间,降水概率为0%,风向为北风,风速为8.4km/h,风力等级为2级,相对湿度为46%。\r\n根据这些信息来看,明天北京的天气非常适合跑步。没有降水,风力不大,湿度和温度都处于较为舒适的范围。祝你有一个愉快的跑步体验!"
},
"finish_reason": "stop",
"index": 0,
"logprobs": null
}
]
大语言模型输出的响应已直接给出答案,不再需要工具调用,因此DashScopeChatModel的call方法不再递归调用自身,向上返回大语言模型输出的响应。最终返回给用户的响应如下所示:
明天(2025年5月6日)北京的天气是白天多云,夜间晴朗,气温在11度到23度之间,降水概率为0%,风向为北风,风速为8.4km/h,风力等级为2级,相对湿度为46%。
根据这些信息来看,明天北京的天气非常适合跑步。没有降水,风力不大,湿度和温度都处于较为舒适的范围。祝你有一个愉快的跑步体验!
MCP
MCP Server
STDIO
该部分完整代码参见mcp-server-example模块,该模块实现数据传输层为STDIO的MCP Server示例。这里不列出pom.xml的具体内容,其主要引入spring-ai-mcp-server-spring-boot-starter,实现数据传输层为STDIO的MCP Server的配置化启动,并指定Main Class,引入Spring Boot的Maven插件,编译、打包成包含所有依赖的可执行的JAR包。
配置文件application.yml的内容如下所示:
spring:
main:
web-application-type: none
banner-mode: off
ai:
mcp:
server:
name: weather-tools
version: 0.0.1
其中通过spring.main.ai.mcp.server.name和spring.main.ai.mcp.server.version配置MCP Server的名称和版本。
启动类McpServerApplication的代码如下所示,其中主要逻辑包括初始化WeatherTool实例和相应的ToolCallbackProvider实例:
package com.magicwt;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public WeatherTools weatherTools() {
return new WeatherTools();
}
@Bean
public ToolCallbackProvider toolCallbackProvider(WeatherTools weatherTools) {
return MethodToolCallbackProvider.builder().toolObjects(weatherTools).build();
}
}
已引入的spring-ai-mcp-server-spring-boot-starter会进一步引入MpcServerAutoConfiguration,MpcServerAutoConfiguration会根据已有的spring.main.ai.mcp.server配置自动初始化STDIO方式的服务端数据传输层(StdioServerTransport),并从启动类中已初始化的ToolCallbackProvider实例中获取ToolCallback实例,进而在默认配置下初始化McpSyncServer实例,并将ToolCallback实例注册至McpSyncServer实例,后续请求侧就可以从McpSyncServer实例中获取可用的工具信息并调用工具。MpcServerAutoConfiguration的部分代码如下所示:
package org.springframework.ai.autoconfigure.mcp.server;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import io.modelcontextprotocol.server.McpAsyncServer;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServer.AsyncSpec;
import io.modelcontextprotocol.server.McpServer.SyncSpec;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpServerFeatures.AsyncPromptRegistration;
import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceRegistration;
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration;
import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptRegistration;
import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceRegistration;
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolRegistration;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.StdioServerTransport;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.Implementation;
import io.modelcontextprotocol.spec.ServerMcpTransport;
import reactor.core.publisher.Mono;
import org.springframework.ai.mcp.McpToolUtils;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.log.LogAccessor;
import org.springframework.util.CollectionUtils;
@AutoConfiguration(after = { MpcWebMvcServerAutoConfiguration.class, MpcWebFluxServerAutoConfiguration.class })
@ConditionalOnClass({ McpSchema.class, McpSyncServer.class })
@EnableConfigurationProperties(McpServerProperties.class)
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public class MpcServerAutoConfiguration {
private static final LogAccessor logger = new LogAccessor(MpcServerAutoConfiguration.class);
/* 初始化STDIO方式的服务端数据传输层 */
@Bean
@ConditionalOnMissingBean
public ServerMcpTransport stdioServerTransport() {
return new StdioServerTransport();
}
@Bean
@ConditionalOnMissingBean
public McpSchema.ServerCapabilities.Builder capabilitiesBuilder() {
return McpSchema.ServerCapabilities.builder();
}
/* 从启动类中已初始化的ToolCallbackProvider实例中获取ToolCallback实例 */
@Bean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
public List<McpServerFeatures.SyncToolRegistration> syncTools(ObjectProvider<List<ToolCallback>> toolCalls) {
var tools = toolCalls.stream().flatMap(List::stream).toList();
return McpToolUtils.toSyncToolRegistration(tools);
}
/* 在默认配置下初始化McpSyncServer实例,并将ToolCallback实例注册至McpSyncServer实例 */
@Bean
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
public McpSyncServer mcpSyncServer(ServerMcpTransport transport,
McpSchema.ServerCapabilities.Builder capabilitiesBuilder, McpServerProperties serverProperties,
ObjectProvider<List<SyncToolRegistration>> tools, ObjectProvider<List<SyncResourceRegistration>> resources,
ObjectProvider<List<SyncPromptRegistration>> prompts,
ObjectProvider<Consumer<List<McpSchema.Root>>> rootsChangeConsumers,
List<ToolCallbackProvider> toolCallbackProvider) {
/* ... */
}
/* ... */
}
编译、打包后得到可执行的JAR包——mcp-server-example-1.0-SNAPSHOT.jar。如何执行该JAR包,调用其中封装的工具?一个方法是直接基于MCP提供的Java SDK初始化McpClient来访问,代码如下所示:
package com.magicwt;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.Map;
public class ClientStdio {
public static void main(String[] args) {
var stdioParams = ServerParameters.builder("java").args("-Dspring.ai.mcp.server.stdio=true",
"-Dspring.main.web-application-type=none", "-Dlogging.pattern.console=", "-jar",
"/Users/wangtao/workspace/spring-ai-example/mcp-server-example/target/mcp-server-example-1.0-SNAPSHOT.jar")
.build();
// 初始化传输层和客户端
var transport = new StdioClientTransport(stdioParams);
var client = McpClient.sync(transport).build();
client.initialize();
// 列出可用工具
McpSchema.ListToolsResult listToolsResult = client.listTools();
System.out.println(listToolsResult.toString());
// 调用天气预报工具
McpSchema.CallToolResult callToolResult =
client.callTool(new McpSchema.CallToolRequest("getDailyWeather", Map.of("arg0", "北京")));
System.out.println(callToolResult.toString());
client.closeGracefully();
}
}
其中先初始化STDIO方式的客户端数据传输层(StdioClientTransport),并初始化相应的McpClient实例,然后可以通过McpClient实例的listTools方法获取可用工具的信息,并通过McpClient实例的callTool方法,调用某个工具。执行后,控制台输出结果如下:
ListToolsResult[tools=[Tool[name=getDailyWeather, description=获取所传入城市今天、明天和后天的天气, inputSchema=JsonSchema[type=object, properties={arg0={type=string, description=城市名称}}, required=[arg0], additionalProperties=false]]], nextCursor=null]
CallToolResult[content=[TextContent[audience=null, priority=null, type=text, text="北京后续几天的天气是:\r\n2025-05-06的天气是:白天天气是晴,夜间天气是晴,最高温度是23度,最低温度是11度,降水概率是0.00%, 降水量是0.00mm,风向是东南,风速是8.4km/h,风力等级是2,相对湿度是44%。\r\n2025-05-07的天气是:白天天气是晴,夜间天气是多云,最高温度是25度,最低温度是11度,降水概率是0.00%, 降水量是0.00mm,风向是南,风速是8.4km/h,风力等级是2,相对湿度是46%。\r\n2025-05-08的天气是:白天天气是雷阵雨,夜间天气是雷阵雨,最高温度是24度,最低温度是14度,降水概率是98.00%, 降水量是29.50mm,风向是南,风速是3.0km/h,风力等级是1,相对湿度是57%。\r\n"]], isError=false]
其中列出了可用的工具,并且调用工具,获取北京的天气预报。
另一个访问MCP Server的方法是使用MCP官方提供的用于调试MCP Server的工具——MCP Inspector。MCP Inspector的安装和运行需要先安装Node.js 包运行器npx,然后执行以下命令:
npx @modelcontextprotocol/inspector java -Dspring.ai.mcp.server.stdio=true -Dspring.main.web-application-type=none -Dlogging.pattern.console= -jar /Users/wangtao/workspace/spring-ai-example/mcp-server-example/target/mcp-server-example-1.0-SNAPSHOT.jar
随后在浏览器中通过地址http://127.0.0.1:6274/访问MCP Inspector,如图10所示,其中列出可用工具,并调用工具。
HTTP SSE
该部分完整代码参见mcp-server-webflux-example模块,该模块实现数据传输层为HTTP SSE的MCP Server示例。这里不列出pom.xml的具体内容,其主要引入spring-ai-mcp-server-webflux-spring-boot-starter,实现数据传输层为HTTP SSE的MCP Server的配置化启动。
配置文件application.yml的内容如下所示:
spring:
ai:
mcp:
server:
name: weather-tools
version: 0.0.1
和mcp-server-example基本一致,通过spring.main.ai.mcp.server.name和spring.main.ai.mcp.server.version配置MCP Server的名称和版本。
启动类McpServerWebFluxApplication的代码如下所示:
package com.magicwt;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class McpServerWebFluxApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerWebFluxApplication.class, args);
}
@Bean
public WeatherTools weatherTools() {
return new WeatherTools();
}
@Bean
public ToolCallbackProvider toolCallbackProvider(WeatherTools weatherTools) {
return MethodToolCallbackProvider.builder().toolObjects(weatherTools).build();
}
}
和mcp-server-example也基本一致。
当前模块和mcp-server-example的主要区别在引入的spring-ai-mcp-server-webflux-spring-boot-starter中,其还会引入mcp-spring-webflux的依赖,从而引入WebFluxSseServerTransport类。在这种情况下,MpcWebFluxServerAutoConfiguration优先于MpcServerAutoConfiguration生效。而MpcWebFluxServerAutoConfiguration会自动初始化HTTP SSE方式的服务端数据传输层(WebFluxSseServerTransport),后续在MpcServerAutoConfiguration中,则不再初始化STDIO方式的服务端数据传输层(StdioServerTransport),但会继续从已初始化的ToolCallbackProvider实例中获取ToolCallback实例,并在默认配置下初始化McpSyncServer实例,并将ToolCallback实例注册至McpSyncServer实例。MpcWebFluxServerAutoConfiguration的部分代码如下所示:
package org.springframework.ai.autoconfigure.mcp.server;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransport;
import io.modelcontextprotocol.spec.ServerMcpTransport;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.server.RouterFunction;
@AutoConfiguration
@ConditionalOnClass({ WebFluxSseServerTransport.class })
@ConditionalOnMissingBean(ServerMcpTransport.class)
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "stdio", havingValue = "false",
matchIfMissing = true)
public class MpcWebFluxServerAutoConfiguration {
/* 初始化HTTP SSE方式的服务端数据传输层 */
@Bean
@ConditionalOnMissingBean
public WebFluxSseServerTransport webFluxTransport(McpServerProperties serverProperties) {
return new WebFluxSseServerTransport(new ObjectMapper(), serverProperties.getSseMessageEndpoint());
}
// Router function for SSE transport used by Spring WebFlux to start an HTTP server.
@Bean
public RouterFunction<?> webfluxMcpRouterFunction(WebFluxSseServerTransport webFluxTransport) {
return webFluxTransport.getRouterFunction();
}
}
执行启动类的main方法,启动数据传输层为HTTP SSE的MCP Server。访问该MCP Server的一个方法还是直接基于MCP提供的Java SDK初始化McpClient来访问,代码如下所示:
package com.magicwt;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.Map;
public class ClientSse {
public static void main(String[] args) {
// 初始化传输层和客户端
var transport =
new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:8080"));
var client = McpClient.sync(transport).build();
client.initialize();
// 列出可用工具
McpSchema.ListToolsResult listToolsResult = client.listTools();
System.out.println(listToolsResult.toString());
// 调用天气预报工具
McpSchema.CallToolResult callToolResult =
client.callTool(new McpSchema.CallToolRequest("getDailyWeather", Map.of("arg0", "北京")));
System.out.println(callToolResult.toString());
client.closeGracefully();
}
}
上述代码和STDIO中的基本相同,只是传输层的初始化不同,后续均是初始化客户端,然后列出可用工具,并调用工具。执行后,控制台输出结果如下:
ListToolsResult[tools=[Tool[name=getDailyWeather, description=获取所传入城市今天、明天和后天的天气, inputSchema=JsonSchema[type=object, properties={arg0={type=string, description=城市名称}}, required=[arg0], additionalProperties=false]]], nextCursor=null]
CallToolResult[content=[TextContent[audience=null, priority=null, type=text, text="北京后续几天的天气是:\r\n2025-05-07的天气是:白天天气是晴,夜间天气是多云,最高温度是25度,最低温度是12度,降水概率是0.00%, 降水量是0.00mm,风向是南,风速是23.4km/h,风力等级是4,相对湿度是71%。\r\n2025-05-08的天气是:白天天气是雷阵雨,夜间天气是雷阵雨,最高温度是25度,最低温度是14度,降水概率是98.00%, 降水量是29.50mm,风向是南,风速是8.4km/h,风力等级是2,相对湿度是66%。\r\n2025-05-09的天气是:白天天气是小雨,夜间天气是晴,最高温度是18度,最低温度是10度,降水概率是100.00%, 降水量是50.28mm,风向是北,风速是15.3km/h,风力等级是3,相对湿度是99%。\r\n"]], isError=false]
访问该MCP Server的另一个方法也还是使用MCP官方提供的MCP Inspector。和STDIO中的也基本相同。使用以下命令,启动MCP Inspector:
npx @modelcontextprotocol/inspector
但传输层协议选择SSE,URL中输入http://localhost:8080/sse,成功连接后,就可以列出工具,并调用工具,如图11所示。
MCP Client
该部分完整代码参见mcp-client-example模块,实现支持工具调用的会话示例,且通过MCP Client连接MCP Server,调用MCP Server上的工具。通过修改配置,既可以跨进程访问本地MCP Server,也可以基于HTTP SSE访问远程MCP Server。
STDIO
这里不列出pom.xml的具体内容,其在引入spring-ai-alibaba-starter的基础上,再引入spring-ai-mcp-client-spring-boot-starter,实现对MCP Client的配置化启动。
配置文件application.yml的内容如下所示:
spring:
main:
web-application-type: none
ai:
dashscope:
api-key: ${AI_DASHSCOPE_API_KEY}
mcp:
client:
stdio:
servers-configuration: classpath:/mcp-servers-config.json
logging:
level:
org.springframework.ai.chat.client.advisor: DEBUG
其中,通过spring.ai.mcp.client.stdio.servers-configuration指定MCP Server的配置文件地址,而mcp-servers-config.json的内容如下所示:
{
"mcpServers": {
"weather": {
"command": "java",
"args": [
"-Dspring.ai.mcp.server.stdio=true",
"-Dspring.main.web-application-type=none",
"-Dlogging.pattern.console=",
"-jar",
"/Users/wangtao/workspace/spring-ai-example/mcp-server-example/target/mcp-server-example-1.0-SNAPSHOT.jar"
],
"env": {}
}
}
}
其中指定通过“java”命令启动子进程作为MCP Server。
启动类McpClientApplication的代码如下所示:
package com.magicwt;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.time.LocalDate;
import java.util.Scanner;
@SpringBootApplication
public class McpClientApplication {
public static void main(String[] args) {
SpringApplication.run(McpClientApplication.class, args);
}
@Bean()
ChatClient chatClent(ChatModel chatModel, ToolCallbackProvider toolCallbackProvider) {
SimpleLoggerAdvisor loggerAdvisor = new SimpleLoggerAdvisor(
advisedRequest -> ModelOptionsUtils.toJsonStringPrettyPrinter(advisedRequest),
chatResponse -> ModelOptionsUtils.toJsonStringPrettyPrinter(chatResponse), 0);
return ChatClient.builder(chatModel).defaultSystem("你是一个智能助手。今天的日期是:{current_date}。")
.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()), loggerAdvisor)
.defaultTools(toolCallbackProvider).build();
}
@Bean
CommandLineRunner cli(ChatClient chatClient) {
return args -> {
var scanner = new Scanner(System.in);
System.out.println("\n让我们开始聊天!");
while (true) {
System.out.print("\n用户: ");
System.out.println("助手: "
+ chatClient.prompt().system(s -> s.param("current_date", LocalDate.now().toString()))
.user(scanner.nextLine()).call().content());
}
};
}
}
上述代码和Function Call这一节中的AgentApplication类似,只是工具的初始化不同。工具的初始化由系统自动完成。初始化ChatClient时直接传入由系统自动初始化的ToolCallbackProvider实例,该实例的具体类型是SyncMcpToolCallbackProvider。
分析自动初始化工具的实现原理。ToolCallbackProvider实例由MCP Client Starter在启动时初始化,相关Auto Configuration的类包括:
StdioTransportAutoConfiguration,实现STDIO传输层的自动初始化;SseHttpClientTransportAutoConfiguration,实现基于HttpClient的HTTP SSE传输层的自动初始化;SseWebFluxTransportAutoConfiguration,实现基于WebClient的HTTP SSE传输层的自动初始化;McpClientAutoConfiguration,实现MCP Client的自动初始化;McpToolCallbackAutoConfiguation,实现ToolCallbackProvider的自动初始化。
上述Auto Configuration的类根据是否配置spring.ai.mcp.client决定是否初始化,先由StdioTransportAutoConfiguration、SseHttpClientTransportAutoConfiguration、SseWebFluxTransportAutoConfiguration初始化传输层,再由McpClientAutoConfiguration初始化MCP Client,最后由McpToolCallbackAutoConfiguation初始化ToolCallbackProvider。本例中最终初始化的是SyncMcpToolCallbackProvider,代码如下所示:
@Bean
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
public ToolCallbackProvider mcpToolCallbacks(ObjectProvider<List<McpSyncClient>> syncMcpClients) {
List<McpSyncClient> mcpClients = syncMcpClients.stream().flatMap(List::stream).toList();
return new SyncMcpToolCallbackProvider(mcpClients);
}
而SyncMcpToolCallbackProvider中获取ToolCallback的方法如下所示,其对于每个MCP Client,调用其listTools方法获取可用工具,并对每个工具,生成相应的SyncMcpToolCallback实例:
@Override
public ToolCallback[] getToolCallbacks() {
var toolCallbacks = new ArrayList<>();
this.mcpClients.stream().forEach(mcpClient -> {
toolCallbacks.addAll(mcpClient.listTools()
.tools()
.stream()
.filter(tool -> toolFilter.test(mcpClient, tool))
.map(tool -> new SyncMcpToolCallback(mcpClient, tool))
.toList());
});
var array = toolCallbacks.toArray(new ToolCallback[0]);
validateToolCallbacks(array);
return array;
}
SyncMcpToolCallback包含两个成员变量:mcpClient(类型为McpSyncClient)和tool(类型为Tool,记录工具的名称、描述、参数)。SyncMcpToolCallback中实现工具调用的call方法的代码如下所示:
@Override
public String call(String functionInput) {
Map<String, Object> arguments = ModelOptionsUtils.jsonToMap(functionInput);
CallToolResult response = this.mcpClient
.callTool(new CallToolRequest(this.getToolDefinition().name(), arguments));
return ModelOptionsUtils.toJsonString(response.content());
}
其中,工具调用底层是依赖McpClient实例向其连接的MCP Server发起工具调用请求。
对于用户提问“明天北京的天气适合跑步吗?”,大语言模型推理过程和Function Call这一节中的AgentApplication基本一致(和大语言模型存在两次交互,一次交互返回待调用工具,一次交互返回最终结果,如图9所示),只是工具调用的实现方式不同,因此这里不再详述交互过程,直接给出该示例大语言模型返回的结果:
北京明天(2025-05-08)的天气是白天和夜间都会出现雷阵雨,最高温度为25度,最低温度为14度,降水概率高达99%,降水量预计为29.50mm。此外,风向为南,风速为8.4km/h,风力等级为2级,相对湿度为68%。
基于这样的天气情况,明天可能并不适合在户外跑步,尤其是雷阵雨期间外出运动可能会让您感到不适,并且存在一定的安全风险。如果您非常想跑步,建议选择室内场馆进行锻炼或者调整到后天进行户外跑步。不过后天有中雨,请注意天气变化合理安排您的运动计划。
HTTP SSE
在上一节代码的基础上,仅需修改配置文件applicaton.yml,其他均不变。修改后applicaton.yml的内容如下所示,其中指定MCP Server的HTTP地址:
spring:
main:
web-application-type: none
ai:
dashscope:
api-key: ${AI_DASHSCOPE_API_KEY}
mcp:
client:
sse:
connections:
weather-tools:
url: http://localhost:8080
logging:
level:
org.springframework.ai.chat.client.advisor: DEBUG
启动过程和上一节基本相同,唯一区别是MCP的传输层,修改前是由StdioTransportAutoConfiguration初始化StdioClientTransport实例,跨进程访问本地MCP Server,修改后是由SseWebFluxTransportAutoConfiguration初始化WebFluxSseClientTransport实例,基于HTTP SSE协议访问远程MCP Server。这里需要补充说明的一点是,MCP提供了两种基于HTTP SSE协议的传输层实现:
WebFluxSseClientTransport。基于Spring WebFlux的响应式编程模型,通过WebClient实现非阻塞式HTTP流通信,适用于需要高并发和异步处理的场景,通过SseWebFluxTransportAutoConfiguration初始化WebFluxSseClientTransport实例。HttpClientSseClientTransport。依赖Java原生HttpClient,采用同步阻塞式通信,适合传统的Servlet或Spring MVC环境,通过SseHttpClientTransportAutoConfiguration初始化HttpClientSseClientTransport实例。
上述两种传输层的自动初始化本来应该是互斥的,若存在WebFluxSseClientTransport类,则初始化WebFluxSseClientTransport实例,若不存在WebFluxSseClientTransport类,则初始化HttpClientSseClientTransport实例。但目前Spring AI的初始化存在bug,导致两种传输层均会初始化,因此同一个MCP Server会初始化两个MCP Client,进而报“Multiple tools with the same name”的错误。为了在当前版本(1.0.0-M6)解决该问题,临时先在启动类的SpringBootApplication注解中排除SseHttpClientTransportAutoConfiguration。
对于用户提问“明天北京的天气适合跑步吗?”,大语言模型推理过程和Function Call这一节中的AgentApplication基本一致(和大语言模型存在两次交互,一次交互返回待调用工具,一次交互返回最终结果,如图9所示),只是工具调用的实现方式不同,因此这里不再详述交互过程,直接给出该示例大语言模型返回的结果:
北京明天(2025-05-09)的天气是白天中雨,夜间多云,最高温度17度,最低温度10度,降水概率为100.00%,降水量为53.19mm。这样的天气状况可能不太适合跑步,因为有较大的降雨可能性,路面可能会比较湿滑,并且气温较低。建议您根据实际情况调整锻炼计划,或者选择室内运动。
Manus
目前,Spring AI Alibaba已实现Java版本的Manus——Spring AI Alibaba Java Manus。笔者这里参考OpenManus实现一个简单复刻的版本,主要复刻OpenManus以下特性:
- 思考方式采用ReAct;
- 工具调用支持浏览器操作、文件读写和代码执行。
关于OpenManus的详细介绍可以阅读笔者整理的《从ReAct到CodeAct再到OpenManus,解析Manus背后的技术》。
工具实现
笔者采用MCP实现工具,其中浏览器操作和文件读写采用MCP官方提供的MCP Server,以STDIO作为传输层,代码执行笔者自行实现了一版简单的能够执行Python代码、但缺少沙箱隔离、依赖安装能力的MCP Server,以HTTP SSE作为传输层。
浏览器操作
浏览器操作采用了MCP官方提供的MCP Server——Puppeteer,其底层基于同名的JavaScript库——Puppeteer,该JavaScript库提供了一个高级API来通过开发工具协议或WebDriver双向控制Chrome或Firefox。Puppeteer默认在无头(无可见UI)模式下运行,详细信息可参见其官网:pptr.nodejs.cn。Puppeteer MCP Server提供以下工具:
puppeteer_navigate,在浏览器中打开某个页面;puppeteer_screenshot,对整个页面或特定元素进行截屏;puppeteer_click,点击页面中的某个元素;puppeteer_hover,移至页面中的某个元素;puppeteer_fill,填充页面中的某个输入框;puppeteer_evaluate,执行JavaScript代码。
通过以下命令,可以打开MCP Inspector对Puppeteer MCP Server进行调试:
npx @modelcontextprotocol/inspector npx -y @modelcontextprotocol/server-puppeteer
在MCP Inspector中,先用puppeteer_navigate打开“www.baidu.com/s?wd=%E5%8C…
文件读写
文件读写采用了MCP官方提供的Filesystem MCP Server。Filesystem MCP Server提供以下工具:
read_file,读文件;read_multiple_files,读多个文件;write_file,写文件;edit_file,修改文件;create_directory,创建目录;list_directory,列出目录;move_file,移动文件;search_files,搜索文件;get_file_info,获取文件信息;list_allowed_directories,列出有权限的目录。
通过以下命令,可以打开MCP Inspector对Filesystem MCP Server进行调试:
npx @modelcontextprotocol/inspector npx -y @modelcontextprotocol/server-puppeteer
在MCP Inspector中,先用write_file写文件,再用read_file读文件,如图13所示。
代码执行
笔者基于FastMCP实现代码执行的MCP Server,支持输入一段Python代码,使用Python提供的exec方法执行代码,但这里只是简单实现代码执行,并没有考虑依赖安装和沙箱隔离。代码执行的MCP Server的代码如下所示,其中提供工具python_execute,该工具执行输入的一段Python代码,并返回控制台的输出:
from fastmcp import FastMCP
import sys
from io import StringIO
from typing import Dict
mcp = FastMCP("Python Executor")
@mcp.tool()
def python_execute(code: str) -> Dict:
"""Executes Python code string. Note: Only print outputs are visible, function return values are not captured. Use print statements to see results."""
result_dict = {"observation": "", "success": False}
original_stdout = sys.stdout
try:
output_buffer = StringIO()
sys.stdout = output_buffer
exec(code)
result_dict["observation"] = output_buffer.getvalue()
result_dict["success"] = True
except Exception as e:
result_dict["observation"] = str(e)
result_dict["success"] = False
finally:
sys.stdout = original_stdout
return result_dict
if __name__ == "__main__":
mcp.run()
使用以下命令启动MCP Server(需先安装FastMCP),并以HTTP SSE作为传输层对外提供工具调用,默认端口是8000:
fastmcp run server.py --transport sse
在MCP Inspector中,对上述实现代码执行的MCP Server进行工具调用的调试,如图14所示。
极简复刻
该部分完整代码参见manus-example模块,代码与mcp-client-example基本一致,只是通过配置引入浏览器操作、文件读写和代码执行的MCP Server。配置文件application.yml的内容如下所示:
spring:
main:
web-application-type: none
ai:
dashscope:
api-key: ${AI_DASHSCOPE_API_KEY}
mcp:
client:
stdio:
servers-configuration: classpath:/mcp-servers-config.json
sse:
connections:
python-executor:
url: http://localhost:8000
logging:
level:
org.springframework.ai.chat.client.advisor: DEBUG
其中,配置了代码执行的MCP Server(以HTTP SSE作为传输层)的HTTP地址,浏览器操作和文件读写的MCP Server(以STDIO作为传输层)的配置在mcp-servers-config.json中,如下所示:
{
"mcpServers": {
"puppeteer": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
},
"filesystem": {
"command": "npx",
"args": [
"-y", "@modelcontextprotocol/server-filesystem", "/Users/wangtao/workspace/spring-ai-example/file"
]
}
}
}
启动类ManusApplication的代码如下所示:
package com.magicwt;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.time.LocalDate;
import java.util.Scanner;
@SpringBootApplication
public class ManusApplication {
public static void main(String[] args) {
SpringApplication.run(ManusApplication.class, args);
}
@Bean()
ChatClient chatClent(ChatModel chatModel, ToolCallbackProvider toolCallbackProvider) {
SimpleLoggerAdvisor loggerAdvisor = new SimpleLoggerAdvisor(
advisedRequest -> ModelOptionsUtils.toJsonStringPrettyPrinter(advisedRequest),
chatResponse -> ModelOptionsUtils.toJsonStringPrettyPrinter(chatResponse), 0);
return ChatClient.builder(chatModel).defaultSystem("你是一个智能助手。今天的日期是:{current_date}。")
.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()), loggerAdvisor)
.defaultTools(toolCallbackProvider).build();
}
@Bean
CommandLineRunner cli(ChatClient chatClient) {
return args -> {
var scanner = new Scanner(System.in);
System.out.println("\n让我们开始聊天!");
while (true) {
System.out.print("\n用户: ");
System.out.println("助手: "
+ chatClient.prompt().system(s -> s.param("current_date", LocalDate.now().toString()))
.user(scanner.nextLine()).call().content());
}
};
}
}
使用示例
示例1(浏览器操作)
用户提问是“从百度上搜索明天北京的天气,并回答明天北京的天气适合跑步吗?”。智能体应用和大语言模型、工具的交互流程如图15所示,智能体应用请求大语言模型五次(由于大语言模型输出的多样性设置,每次执行交互流程可能略有不同),第一次请求输入包括系统提示和用户提问,大语言模型返回需要调用工具在浏览器中打开百度搜索,智能体应用调用相应的工具,第二次请求输入除包括系统提示和用户提问外,还包括第一次请求大语言模型的返回,以及调用工具的返回(即是否成功在浏览器中打开百度搜索),大语言模型返回需要调用工具解析网页中的天气预报信息并给出解析脚本,智能体应用调用相应的工具,第三次、第四次请求继续包含历史的所有消息,大语言模型返回新的解析脚本,直至第四次请求返回的解析脚本能够正确解析网页中的天气预报信息后,第五次请求携带天气预报信息,大语言模型返回最终的答案。
智能体应用调用工具在浏览器中打开百度搜索如图16所示。
智能体应用最后一次请求大语言模型携带的消息列表如下所示:
[
{
"role": "system",
"content": "你是一个智能助手。今天的日期是:2025-05-12。"
},
{
"role": "user",
"content": "从百度上搜索明天北京的天气,并回答明天北京的天气适合跑步吗?"
},
{
"role": "assistant",
"tool_calls": [
{
"index": 0,
"id": "call_d1adf8357b2940c5a1129a",
"type": "function",
"function": {
"name": "puppeteer_navigate",
"arguments": "{\"url\": \"https://www.baidu.com/s?wd=%E5%8C%97%E4%BA%AC%E5%A4%A9%E6%B0%94\"}"
}
}
]
},
{
"role": "tool",
"content": "Navigated to https://www.baidu.com/s?wd=%E5%8C%97%E4%BA%AC%E5%A4%A9%E6%B0%94",
"tool_call_id": "call_d1adf8357b2940c5a1129a"
},
{
"role": "assistant",
"tool_calls": [
{
"index": 0,
"id": "call_7b043caf114e420f809544",
"type": "function",
"function": {
"name": "puppeteer_evaluate",
"arguments": "{\"script\":\"document.querySelector(\\\"#content_left .c-container:nth-child(3) .op_weather4_twoicon_wrap table tbody tr:nth-child(2) td:nth-child(2)\\\").innerText\"}"
}
}
]
},
{
"role": "tool",
"content": "Script execution failed: Cannot read properties of null (reading 'innerText')",
"tool_call_id": "call_7b043caf114e420f809544"
},
{
"role": "assistant",
"tool_calls": [
{
"index": 0,
"id": "call_ddc7f56567134673956567",
"type": "function",
"function": {
"name": "puppeteer_evaluate",
"arguments": "{\"script\":\"document.querySelector('#content_left .c-container:nth-child(3) .op_weather4_twoicon_wrap table tbody tr:nth-child(2) td:nth-child(2)').textContent\"}"
}
}
]
},
{
"role": "tool",
"content": "Script execution failed: Cannot read properties of null (reading 'textContent')",
"tool_call_id": "call_ddc7f56567134673956567"
},
{
"role": "assistant",
"tool_calls": [
{
"index": 0,
"id": "call_f16a3f20a1664d768acfc4",
"type": "function",
"function": {
"name": "puppeteer_evaluate",
"arguments": "{\"script\":\"document.querySelector('.c-container').innerText\"}"
}
}
]
},
{
"role": "tool",
"content": "Execution result:\n\"北京 \\n17:15更新\\n27°\\n阴西南风2级\\n17~28°C\\n110 轻度\\n气温\\n风力\\n降水量\\n紫外线\\n现在\\n18:00\\n19:00\\n20:00\\n21:00\\n22:00\\n23:00\\n05/13\\n01:00\\n02:00\\n03:00\\n04:00\\n05:00\\n06:00\\n07:00\\n08:00\\n09:00\\n10:00\\n11:00\\n12:00\\n13:00\\n14:00\\n15:00\\n16:00\\n17:00\\n18:00\\n19:00\\n20:00\\n21:00\\n22:00\\n23:00\\n05/14\\n01:00\\n02:00\\n03:00\\n04:00\\n05:00\\n06:00\\n07:00\\n08:00\\n09:00\\n10:00\\n11:00\\n12:00\\n13:00\\n14:00\\n15:00\\n16:00\\n轻度\\n°\\n27°\\n轻度\\n°\\n26°\\n轻度\\n°\\n25°\\n轻度\\n°\\n24°\\n轻度\\n°\\n23°\\n轻度\\n°\\n22°\\n轻度\\n°\\n22°\\n轻度\\n°\\n21°\\n轻度\\n°\\n20°\\n轻度\\n°\\n20°\\n轻度\\n°\\n19°\\n轻度\\n°\\n18°\\n轻度\\n°\\n17°\\n轻度\\n°\\n19°\\n轻度\\n°\\n21°\\n轻度\\n°\\n21°\\n轻度\\n°\\n22°\\n轻度\\n°\\n24°\\n轻度\\n°\\n25°\\n轻度\\n°\\n26°\\n轻度\\n°\\n27°\\n轻度\\n°\\n29°\\n轻度\\n°\\n29°\\n轻度\\n°\\n29°\\n轻度\\n°\\n29°\\n轻度\\n°\\n28°\\n轻度\\n°\\n27°\\n轻度\\n°\\n26°\\n轻度\\n°\\n25°\\n轻度\\n°\\n24°\\n轻度\\n°\\n22°\\n良\\n°\\n22°\\n良\\n°\\n21°\\n良\\n°\\n20°\\n90%\\n良\\n°\\n19°\\n60%\\n良\\n°\\n19°\\n60%\\n良\\n°\\n18°\\n60%\\n良\\n°\\n18°\\n60%\\n良\\n°\\n19°\\n60%\\n良\\n°\\n20°\\n60%\\n良\\n°\\n21°\\n良\\n°\\n21°\\n良\\n°\\n22°\\n良\\n°\\n23°\\n良\\n°\\n24°\\n良\\n°\\n25°\\n良\\n°\\n25°\\n良\\n°\\n25°\\n\\n15天预报\\n趋势\\n列表\\n昨天\\n05/11\\n\\n西南风\\n1级\\n良\\n今天\\n05/12\\n\\n南风\\n1级\\n轻度\\n明天\\n05/13\\n\\n60%\\n\\n南风\\n1级\\n轻度\\n周三\\n05/14\\n\\n北风\\n1级\\n良\\n周四\\n05/15\\n\\n西南风\\n1级\\n良\\n周五\\n05/16\\n\\n南风\\n1级\\n轻度\\n周六\\n05/17\\n\\n北风\\n1级\\n良\\n周日\\n05/18\\n\\n90%\\n\\n东北风\\n1级\\n良\\n周一\\n05/19\\n\\n北风\\n1级\\n周二\\n05/20\\n\\n南风\\n1级\\n周三\\n05/21\\n\\n80%\\n\\n东南风\\n1级\\n周四\\n05/22\\n\\n60%\\n\\n80%\\n\\n东风\\n1级\\n周五\\n05/23\\n\\n60%\\n\\n东风\\n2级\\n周六\\n05/24\\n\\n60%\\n\\n东风\\n1级\\n周日\\n05/25\\n\\n60%\\n\\n60%\\n\\n南风\\n2级\\n周一\\n05/26\\n\\n南风\\n1级\\n°\\n28°\\n°\\n14°\\n°\\n28°\\n°\\n17°\\n°\\n29°\\n°\\n18°\\n°\\n26°\\n°\\n16°\\n°\\n27°\\n°\\n17°\\n°\\n28°\\n°\\n19°\\n°\\n26°\\n°\\n16°\\n°\\n24°\\n°\\n15°\\n°\\n22°\\n°\\n12°\\n°\\n24°\\n°\\n14°\\n°\\n29°\\n°\\n15°\\n°\\n27°\\n°\\n12°\\n°\\n17°\\n°\\n12°\\n°\\n25°\\n°\\n13°\\n°\\n23°\\n°\\n15°\\n°\\n29°\\n°\\n17°\\n\\n40天预报\\n\\n\\n专业天气指数\\n\\n体感温度\\n28°\\n人体感觉稍有些热\\n紫外线指数\\n弱\\n未来将持续偏弱\\n湿度\\n44%\\n露点温度为11°\\n降水量\\n0.0\\n毫米\\n过去一小时\\n日出日落\\n\\n05:02\\n\\n日升\\n\\n19:19\\n日落\\n\\n生活气象指数\\n\\n下午好,今日天气晴好,心情也要保持晴朗哦\\n穿衣: 暖\\nweather\\n紫外线: 轻微防护\\n紫外线弱\\n旅游: 适宜\\n温度宜有微风\\n洗车: 较适宜\\n未来一天无雨\\n百度天气\"\n\nConsole output:\n",
"tool_call_id": "call_f16a3f20a1664d768acfc4"
}
]
智能体应用每次请求大语言模型携带的工具列表如下所示,包括浏览器操作、文件读写和代码执行相关的所有工具:
[
{
"type": "function",
"function":
{
"name": "puppeteer_navigate",
"description": "Navigate to a URL",
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "URL to navigate to"
},
"launchOptions": {
"type": "object",
"description": "PuppeteerJS LaunchOptions. Default null. If changed and not null, browser restarts. Example: { headless: true, args: ['--no-sandbox'] }"
},
"allowDangerous": {
"type": "boolean",
"description": "Allow dangerous LaunchOptions that reduce security. When false, dangerous args like --no-sandbox will throw errors. Default false."
}
},
"required": ["url"]
}
}
},{
"type": "function",
"function":
{
"name": "puppeteer_screenshot",
"description": "Take a screenshot of the current page or a specific element",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name for the screenshot"
},
"selector": {
"type": "string",
"description": "CSS selector for element to screenshot"
},
"width": {
"type": "number",
"description": "Width in pixels (default: 800)"
},
"height": {
"type": "number",
"description": "Height in pixels (default: 600)"
}
},
"required": ["name"]
}
}
},{
"type": "function",
"function":
{
"name": "puppeteer_click",
"description": "Click an element on the page",
"parameters": {
"type": "object",
"properties": {
"selector": {
"type": "string",
"description": "CSS selector for element to click"
}
},
"required": ["selector"]
}
}
},{
"type": "function",
"function":
{
"name": "puppeteer_fill",
"description": "Fill out an input field",
"parameters": {
"type": "object",
"properties": {
"selector": {
"type": "string",
"description": "CSS selector for input field"
},
"value": {
"type": "string",
"description": "Value to fill"
}
},
"required": ["selector","value"]
}
}
},{
"type": "function",
"function":
{
"name": "puppeteer_select",
"description": "Select an element on the page with Select tag",
"parameters": {
"type": "object",
"properties": {
"selector": {
"type": "string",
"description": "CSS selector for element to select"
},
"value": {
"type": "string",
"description": "Value to select"
}
},
"required": ["selector","value"]
}
}
},{
"type": "function",
"function":
{
"name": "puppeteer_hover",
"description": "Hover an element on the page",
"parameters": {
"type": "object",
"properties": {
"selector": {
"type": "string",
"description": "CSS selector for element to hover"
}
},
"required": ["selector"]
}
}
},{
"type": "function",
"function":
{
"name": "puppeteer_evaluate",
"description": "Execute JavaScript in the browser console",
"parameters": {
"type": "object",
"properties": {
"script": {
"type": "string",
"description": "JavaScript code to execute"
}
},
"required": ["script"]
}
}
},{
"type": "function",
"function":
{
"name": "read_file",
"description": "Read the complete contents of a file from the file system. Handles various text encodings and provides detailed error messages if the file cannot be read. Use this tool when you need to examine the contents of a single file. Only works within allowed directories.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
},
"required": ["path"]
}
}
},{
"type": "function",
"function":
{
"name": "read_multiple_files",
"description": "Read the contents of multiple files simultaneously. This is more efficient than reading files one by one when you need to analyze or compare multiple files. Each file's content is returned with its path as a reference. Failed reads for individual files won't stop the entire operation. Only works within allowed directories.",
"parameters": {
"type": "object",
"properties": {
"paths": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["paths"]
}
}
},{
"type": "function",
"function":
{
"name": "write_file",
"description": "Create a new file or completely overwrite an existing file with new content. Use with caution as it will overwrite existing files without warning. Handles text content with proper encoding. Only works within allowed directories.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
},
"content": {
"type": "string"
}
},
"required": ["path","content"]
}
}
},{
"type": "function",
"function":
{
"name": "edit_file",
"description": "Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Returns a git-style diff showing the changes made. Only works within allowed directories.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
},
"edits": {
"type": "array",
"items": {
"type": "object",
"properties": {
"oldText": {
"type": "string",
"description": "Text to search for - must match exactly"
},
"newText": {
"type": "string",
"description": "Text to replace with"
}
},
"required": ["oldText","newText"]
}
},
"dryRun": {
"type": "boolean",
"default": false,
"description": "Preview changes using git-style diff format"
}
},
"required": ["path","edits"]
}
}
},{
"type": "function",
"function":
{
"name": "create_directory",
"description": "Create a new directory or ensure a directory exists. Can create multiple nested directories in one operation. If the directory already exists, this operation will succeed silently. Perfect for setting up directory structures for projects or ensuring required paths exist. Only works within allowed directories.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
},
"required": ["path"]
}
}
},{
"type": "function",
"function":
{
"name": "list_directory",
"description": "Get a detailed listing of all files and directories in a specified path. Results clearly distinguish between files and directories with [FILE] and [DIR] prefixes. This tool is essential for understanding directory structure and finding specific files within a directory. Only works within allowed directories.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
},
"required": ["path"]
}
}
},{
"type": "function",
"function":
{
"name": "directory_tree",
"description": "Get a recursive tree view of files and directories as a JSON structure. Each entry includes 'name', 'type' (file/directory), and 'children' for directories. Files have no children array, while directories always have a children array (which may be empty). The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
},
"required": ["path"]
}
}
},{
"type": "function",
"function":
{
"name": "move_file",
"description": "Move or rename files and directories. Can move files between directories and rename them in a single operation. If the destination exists, the operation will fail. Works across different directories and can be used for simple renaming within the same directory. Both source and destination must be within allowed directories.",
"parameters": {
"type": "object",
"properties": {
"source": {
"type": "string"
},
"destination": {
"type": "string"
}
},
"required": ["source","destination"]
}
}
},{
"type": "function",
"function":
{
"name": "search_files",
"description": "Recursively search for files and directories matching a pattern. Searches through all subdirectories from the starting path. The search is case-insensitive and matches partial names. Returns full paths to all matching items. Great for finding files when you don't know their exact location. Only searches within allowed directories.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
},
"pattern": {
"type": "string"
},
"excludePatterns": {
"type": "array",
"items": {
"type": "string"
},
"default": []
}
},
"required": ["path","pattern"]
}
}
},{
"type": "function",
"function":
{
"name": "get_file_info",
"description": "Retrieve detailed metadata about a file or directory. Returns comprehensive information including size, creation time, last modified time, permissions, and type. This tool is perfect for understanding file characteristics without reading the actual content. Only works within allowed directories.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
},
"required": ["path"]
}
}
},{
"type": "function",
"function":
{
"name": "list_allowed_directories",
"description": "Returns the list of directories that this server is allowed to access. Use this to understand which directories are available before trying to access files.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},{
"type": "function",
"function":
{
"name": "python_execute",
"description": "Executes Python code string. Note: Only print outputs are visible, function return values are not captured. Use print statements to see results.",
"parameters": {
"type": "object",
"properties": {
"code": {
"title": "Code",
"type": "string"
}
},
"required": ["code"]
}
}
}
]
最终返回给用户的响应是:
根据从百度获取的天气信息,明天(5月13日)北京的天气预报显示有60%的降水概率,并伴有南风1级,空气质量轻度污染。温度范围在24°C左右。
考虑到有较高的降水概率和轻度污染,明天在北京跑步可能不是最佳选择。如果一定要跑步,建议携带雨具并选择空气较流通的地方,同时关注实时天气变化以确保安全。
示例2(文件读写)
用户提问是“准备一个关于强化学习的分享提纲,该分享面向有一定机器学习和深度学习基础的研发人员,该分享需要包括强化学习的发展历程、主要算法、应用场景、未来发展等章节,该分享需要按章节组织内容,可进一步划分子章节,该分享提纲需要包括各个章节的标题和主要内容,最终将该分享提纲的内容提供以Markdown格式输出到本地文件file/rl.md。”。智能体应用和大语言模型、工具的交互流程如图17所示,智能体应用请求大语言模型两次,第一次请求输入包括系统提示和用户提问,大语言模型返回提纲内容,并返回需要调用工具向文件写入提纲内容,智能体应用调用相应的工具,第二次请求输入除包括系统提示和用户提问外,还包括第一次请求大语言模型的返回,以及调用工具的返回(即是否成功向文件写入提纲内容),大语言模型返回最终的答案。
智能体应用最后一次请求大语言模型携带的消息列表如下所示:
[
{
"role": "system",
"content": "你是一个智能助手。今天的日期是:2025-05-12。"
},
{
"role": "user",
"content": "准备一个关于强化学习的分享提纲,该分享面向有一定机器学习和深度学习基础的研发人员,该分享需要包括强化学习的发展历程、主要算法、应用场景、未来发展等章节,该分享需要按章节组织内容,可进一步划分子章节,该分享提纲需要包括各个章节的标题和主要内容,最终将该分享提纲的内容提供以Markdown格式输出到本地文件file/rl.md。"
},
{
"role": "assistant",
"content": "为了完成这个任务,我将首先创建一个关于强化学习的分享提纲,并以Markdown格式组织内容。然后,我将使用`write_file`函数将生成的内容保存到本地文件`file/rl.md`。\n\n以下是关于强化学习的分享提纲:\n\n```markdown\n# 强化学习分享提纲\n\n## 1. 引言\n- 强化学习的基本概念\n- 强化学习与其他机器学习方法的区别\n\n## 2. 发展历程\n### 2.1 起源与早期发展\n- 操作性条件反射理论\n- 动物学习实验的影响\n\n### 2.2 现代强化学习的开端\n- Markov决策过程(MDP)的引入\n- 值迭代和策略迭代算法的发展\n\n### 2.3 深度强化学习的兴起\n- 深度Q网络(DQN)的提出\n- AlphaGo的成功及其影响\n\n## 3. 主要算法\n### 3.1 基于值的算法\n- Q-learning\n- Deep Q-Network (DQN)\n- Double DQN, Dueling DQN\n\n### 3.2 基于策略的算法\n- REINFORCE\n- Actor-Critic\n- Proximal Policy Optimization (PPO)\n\n### 3.3 基于模型的算法\n- Dyna-Q\n- Model Predictive Control (MPC)\n\n### 3.4 其他重要算法\n- Asynchronous Advantage Actor-Critic (A3C)\n- Soft Actor-Critic (SAC)\n\n## 4. 应用场景\n### 4.1 游戏\n- Atari游戏\n- 围棋(AlphaGo)\n- 星际争霸等复杂策略游戏\n\n### 4.2 机器人学\n- 运动控制\n- 导航与路径规划\n\n### 4.3 自动驾驶\n- 决策制定\n- 道路导航\n\n### 4.4 其他领域\n- 金融交易\n- 推荐系统\n- 能源管理\n\n## 5. 当前挑战\n- 样本效率问题\n- 可扩展性与计算资源需求\n- 算法的稳定性和可解释性\n\n## 6. 未来发展\n- 结合无监督学习和自监督学习\n- 多智能体强化学习\n- 在现实世界中的更广泛应用\n- 新兴技术的融合(如量子计算)\n\n## 7. 结论\n- 强化学习的重要性\n- 对未来的展望\n```\n\n接下来,我将把这个提纲内容写入文件`file/rl.md`。",
"tool_calls": [{
"index": 0,
"id": "call_d2bf99c0494541919ab9de",
"type": "function",
"function": {
"name": "write_file",
"arguments": "{\"path\": \"file/rl.md\", \"content\": \"# 强化学习分享提纲\n\n## 1. 引言\n- 强化学习的基本概念\n- 强化学习与其他机器学习方法的区别\n\n## 2. 发展历程\n### 2.1 起源与早期发展\n- 操作性条件反射理论\n- 动物学习实验的影响\n\n### 2.2 现代强化学习的开端\n- Markov决策过程(MDP)的引入\n- 值迭代和策略迭代算法的发展\n\n### 2.3 深度强化学习的兴起\n- 深度Q网络(DQN)的提出\n- AlphaGo的成功及其影响\n\n## 3. 主要算法\n### 3.1 基于值的算法\n- Q-learning\n- Deep Q-Network (DQN)\n- Double DQN, Dueling DQN\n\n### 3.2 基于策略的算法\n- REINFORCE\n- Actor-Critic\n- Proximal Policy Optimization (PPO)\n\n### 3.3 基于模型的算法\n- Dyna-Q\n- Model Predictive Control (MPC)\n\n### 3.4 其他重要算法\n- Asynchronous Advantage Actor-Critic (A3C)\n- Soft Actor-Critic (SAC)\n\n## 4. 应用场景\n### 4.1 游戏\n- Atari游戏\n- 围棋(AlphaGo)\n- 星际争霸等复杂策略游戏\n\n### 4.2 机器人学\n- 运动控制\n- 导航与路径规划\n\n### 4.3 自动驾驶\n- 决策制定\n- 道路导航\n\n### 4.4 其他领域\n- 金融交易\n- 推荐系统\n- 能源管理\n\n## 5. 当前挑战\n- 样本效率问题\n- 可扩展性与计算资源需求\n- 算法的稳定性和可解释性\n\n## 6. 未来发展\n- 结合无监督学习和自监督学习\n- 多智能体强化学习\n- 在现实世界中的更广泛应用\n- 新兴技术的融合(如量子计算)\n\n## 7. 结论\n- 强化学习的重要性\n- 对未来的展望\"}"
}
}]
},
{
"role": "tool",
"content": "Successfully wrote to file/rl.md",
"tool_call_id": "call_d2bf99c0494541919ab9de"
}
]
最终返回给用户的响应是:
提纲内容已成功写入到文件`file/rl.md`中。以下是提纲的简要内容:
# 强化学习分享提纲
## 1. 引言
- 强化学习的基本概念
- 强化学习与其他机器学习方法的区别
## 2. 发展历程
### 2.1 起源与早期发展
- 操作性条件反射理论
- 动物学习实验的影响
...
## 7. 结论
- 强化学习的重要性
- 对未来的展望
你可以通过打开`file/rl.md`来查看完整的分享提纲。
示例3(代码执行)
用户提问是“求解以下方程的根的倒数的和:,并以数字的形式输出”。智能体应用和大语言模型、工具的交互流程如图18所示,智能体应用请求大语言模型两次,第一次请求输入包括系统提示和用户提问,大语言模型返回求解过程,并返回需要调用工具执行代码进行数值计算,智能体应用调用相应的工具,第二次请求输入除包括系统提示和用户提问外,还包括第一次请求大语言模型的返回,以及调用工具的返回(即执行代码后输出的数值计算结果),大语言模型返回最终的答案。
智能体应用最后一次请求大语言模型携带的消息列表如下所示:
[
{
"role": "system",
"content": "你是一个智能助手。今天的日期是:2025-05-14。"
},
{
"role": "user",
"content": "求解以下方程的根的倒数的和:$x^2-13x+4=0$,并以数字的形式输出"
},
{
"role": "assistant",
"content": "给定方程 \\(x^2 - 13x + 4 = 0\\),我们可以利用求根公式来找出它的根。对于形如 \\(ax^2 + bx + c = 0\\) 的二次方程,其根由以下公式给出:\n\n\\[\nx = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}\n\\]\n\n在这个特定的例子中,\\(a = 1\\),\\(b = -13\\),以及 \\(c = 4\\)。\n\n设根为 \\(r_1\\) 和 \\(r_2\\)。我们要求的是这些根的倒数之和,即 \\(\\frac{1}{r_1} + \\frac{1}{r_2}\\)。根据根与系数的关系,我们知道\n\n\\[\nr_1 + r_2 = -\\frac{b}{a}, \\quad r_1r_2 = \\frac{c}{a}.\n\\]\n\n因此,\n\n\\[\n\\frac{1}{r_1} + \\frac{1}{r_2} = \\frac{r_1 + r_2}{r_1r_2} = \\frac{-b/a}{c/a} = \\frac{-b}{c}.\n\\]\n\n将 \\(b = -13\\) 和 \\(c = 4\\) 代入得,\n\n\\[\n\\frac{1}{r_1} + \\frac{1}{r_2} = \\frac{-(-13)}{4} = \\frac{13}{4}.\n\\]\n\n接下来,我们将使用 Python 来验证这个结果。",
"tool_calls": [{
"index": 0,
"id": "call_7e7712286ddc420bb8768b",
"type": "function",
"function": {
"name": "python_execute",
"arguments": "{\"code\": \"print(13 / 4)\"}"
}
}]
},
{
"role": "tool",
"content": "{\n \"observation\": \"3.25\\n\",\n \"success\": true\n}",
"tool_call_id": "call_7e7712286ddc420bb8768b"
}
]
最终返回给用户的响应是:
根据计算和验证,方程 \(x^2 - 13x + 4 = 0\) 的根的倒数之和为 \(\frac{13}{4}\),即 3.25。