8. Spring AI tools/function-call
<h1 id="8-spring-ai--toolsfunction-call">8. Spring AItools/function-call</h1><p>@</p><div class="toc"><div class="toc-container-header">目录</div><ul><li>8. Spring AItools/function-call<ul><li>链接多个模型协调工作实战 - 初代tools:<ul><li><ul><li>背景:</li><li>票务助手</li><li><font style="color: rgba(6, 8, 31, 0.88)">效果</font></li><li>代码:</li></ul></li></ul></li><li>tools/function-call<ul><li>使用</li><li>原理</li><li>源码</li><li>tools注意事项:</li></ul></li></ul></li><li>最后:</li></ul></div><p></p>
<h2 id="链接多个模型协调工作实战---初代tools">链接多个模型协调工作实战 - 初代tools:</h2>
<h4 id="背景">背景:</h4>
<p>大模型如果它无法和企业API互联那将毫无意义! 比如我们开发一个智能票务助手, 当用户需要退票, 基础大模型它肯定做不到, 因为票务信息都存在了我们系统中, 必须通过我们系统的业务方法才能进行退票。 那怎么能让大模型“调用”我们自己系统的业务方法呢? 今天叫大家通过结构化输入连接多个模型一起协同完成这个任务:</p>
<h4 id="票务助手">票务助手</h4>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155646825-924851305.png" class="lazyload"></p>
<h4 id="效果"><font style="color: rgba(6, 8, 31, 0.88)">效果</font></h4>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155646824-2114504058.png" class="lazyload"></p>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155646840-1146246760.png" class="lazyload"></p>
<p>输入姓名和预定号:</p>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155646812-1145948622.png" class="lazyload"></p>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155646817-1777410197.png" class="lazyload"></p>
<p>普通对话:</p>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155646872-1949712343.png" class="lazyload"></p>
<h4 id="代码">代码:</h4>
<pre><code class="language-java">public class AiJob {
record Job(JobType jobType, Map<String,String> keyInfos) {
}
public enum JobType{
CANCEL,
QUERY,
OTHER,
}
}
</code></pre>
<pre><code class="language-java">/**
*
*/
@Configuration
public class AiConfig {
@Bean
public ChatClient planningChatClient(DashScopeChatModel chatModel,
DashScopeChatProperties options,
ChatMemory chatMemory) {
DashScopeChatOptions dashScopeChatOptions = DashScopeChatOptions.fromOptions(options.getOptions());
dashScopeChatOptions.setTemperature(0.7);
returnChatClient.builder(chatModel)
.defaultSystem("""
# 票务助手任务拆分规则
## 1.要求
### 1.1 根据用户内容识别任务
## 2. 任务
### 2.1 JobType:退票(CANCEL) 要求用户提供姓名和预定号, 或者从对话中提取;
### 2.2 JobType:查票(QUERY) 要求用户提供预定号, 或者从对话中提取;
### 2.3 JobType:其他(OTHER)
""")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.defaultOptions(dashScopeChatOptions)
.build();
}
@Bean
public ChatClient botChatClient(DashScopeChatModel chatModel,
DashScopeChatProperties options,
ChatMemory chatMemory) {
DashScopeChatOptions dashScopeChatOptions = DashScopeChatOptions.fromOptions(options.getOptions());
dashScopeChatOptions.setTemperature(1.2);
returnChatClient.builder(chatModel)
.defaultSystem("""
你是XS航空智能客服代理, 请以友好的语气服务用户。
""")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.defaultOptions(dashScopeChatOptions)
.build();
}
}
</code></pre>
<pre><code class="language-java">@RestController
public class MultiModelsController {
@Autowired
ChatClient planningChatClient;
@Autowired
ChatClient botChatClient;
@GetMapping(value = "/stream", produces = "text/stream;charset=UTF8")
Flux<String> stream(@RequestParam String message) {
// 创建一个用于接收多条消息的 Sink
Sinks.Many<String> sink = Sinks.many().unicast().onBackpressureBuffer();
// 推送消息
sink.tryEmitNext("正在计划任务...<br/>");
new Thread(() -> {
AiJob.Job job = planningChatClient.prompt().user(message)
.call().entity(AiJob.Job.class);
switch (job.jobType()){
case CANCEL ->{
System.out.println(job);
// todo.. 执行业务
if(job.keyInfos().size()==0){
sink.tryEmitNext("请输入姓名和订单号.");
}
else {
sink.tryEmitNext("退票成功!");
}
}
case QUERY -> {
System.out.println(job);
// todo.. 执行业务
sink.tryEmitNext("查询预定信息:xxxx");
}
case OTHER -> {
Flux<String> content = botChatClient.prompt().user(message).stream().content();
content.doOnNext(sink::tryEmitNext) // 推送每条AI流内容
.doOnComplete(() -> sink.tryEmitComplete())
.subscribe();
}
default -> {
System.out.println(job);
sink.tryEmitNext("解析失败");
}
}
}).start();
return sink.asFlux();
}
}
</code></pre>
<h2 id="toolsfunction-call">tools/function-call</h2>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155646969-1711604824.png" class="lazyload"></p>
<p>想做企业级智能应用开发, 你肯定会有需求要让大模型和你的企业 API 能够互连,</p>
<p>因为对于基础大模型来说, 他只具备通用信息,他的参数都是拿公网进行训练,并且有一定的时间延迟, 无法得知一些具体业务数据和实时数据, 这些数据往往被各软件系统存储在自己数据库中:</p>
<p>比如我问大模型:“中国有多少个叫徐庶的” 他肯定不知道, 我们就需要去调用政务系统的接口。</p>
<p>比如我现在开发一个智能票务助手, 我现在跟AI说需要退票, AI怎么做到呢? 就需要让AI调用我们自己系统的退票业务方法,进行操作数据库。</p>
<p>在之前我们可以通过链接多个模型的方式达到, 但是很麻烦, 那用tools, 可以轻松完成。</p>
<p>tool calling也可以直接叫tool(也称为function-call), 主要用于提供大模型不具备的信息和能力:</p>
<ol>
<li><strong><font style="color: rgba(25, 30, 30, 1)">信息检索:</font></strong><font style="color: rgba(25, 30, 30, 1)">可用于从外部源(如数据库、Web 服务、文件系统或 Web 搜索引擎)检索信息。目标是增强模型的知识,使其能够回答无法回答的问题。例如,工具可用于检索给定位置的当前天气、检索最新的新闻文章或查询数据库以获取特定记录。 这也是一种检索增强方式。</font></li>
<li><strong><font style="color: rgba(25, 30, 30, 1)">采取行动:</font></strong><font style="color: rgba(25, 30, 30, 1)">例如发送电子邮件、在数据库中创建新记录、提交表单或触发工作流。目标是自动执行原本需要人工干预或显式编程的任务。例如,可以使用工具为与聊天机器人交互的客户预订航班,在网页上填写表单等。</font></li>
</ol>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155646851-1167002532.png" class="lazyload"><font style="background-color: rgba(248, 206, 211, 0.5)">需要使用tools必须要先保证大模型支持。 比如ollama列出了支持tool的模型</font></p>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155646845-671015823.png" class="lazyload"></p>
<h3 id="使用">使用</h3>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155646882-1336586988.png" class="lazyload"></p>
<ol>
<li><strong>声明 tools (大模型调用的方法工具)的类:</strong></li>
</ol>
<pre><code class="language-java">@Service// 注意要注入到 IOC容器当中
class NameCountsTools {
// @Tool 注解表示,告诉大模型提供的方法类,可以被你大模型调用使用,标识是可以被大模型调用的方法工具
@Tool(description = "长沙有多少名字的数量")
String LocationNameCounts(
// @ToolParam()使用该上述 @Tool标识的方法,要那些参数才可以调用,大模型会自动从用户的历史对话当中提取
// 出需要的“名字”信息,然后作为参数,去调用该 @Tool()标识的方法工具,如果用户对话当中没有提供
// 大模型就会告知用户需要提供“名字”
@ToolParam(description = "名字,可以是英文名") // description = "名字,可以是英文名" 这个是用于让大模型识别,
// 从而正确的从用户的历史对话当中提取的,赋值上去。
String name) {
return "10个";
}
}
</code></pre>
<ol>
<li>
<p>将Tool类配置为bean(非必须)</p>
</li>
<li>
<p><strong>@Tool 用户告诉大模型提供了什么工具</strong></p>
</li>
<li>
<p><strong>@ToolParam 用于告诉大模型你要用这个工具需要什么参数(非必须)</strong></p>
</li>
<li>
<p>将上面声明的 Tools 类 绑定到 ChatClient(对应的大模型当中去)</p>
</li>
</ol>
<pre><code class="language-java">@SpringBootTest
public class ToolTest {
ChatClient chatClient;
@BeforeEach
publicvoid init(@Autowired
DashScopeChatModel chatModel,
@Autowired// 因为 NameCountsTools Tools 工具类,已经被我们加入到了IOC容器了
NameCountsTools nameCountsTools) {
chatClient = ChatClient.builder(chatModel)
.defaultTools(nameCountsTools) // 给大模型附加上我们的 Tools 工具类
.build();
}
@Test
public void testChatOptions() {
String content = chatClient.prompt()
.user("长沙有多少个叫徐庶的/no_think")
// .tools() 也可以单独绑定当前对话,绑定上 Tools 工具类
.call()
.content();
System.out.println(content);
}
}
</code></pre>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155646835-1197707019.png" class="lazyload"></p>
<h3 id="原理">原理</h3>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155646852-412204390.png" class="lazyload"></p>
<ol>
<li>当我们设置了defaultTools 相当于就告诉了大模型我提供了什么工具, 你需要用我的工具必须给我什么参数, 底层实际就是将这些信息封装了json提供给大模型</li>
<li>当大模型识别到我们的对话需要用到工具, 就会响应需要调用tool</li>
</ol>
<h3 id="源码">源码</h3>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155647079-616608440.png" class="lazyload"></p>
<h3 id="tools注意事项">tools注意事项:</h3>
<ol>
<li><strong>参数或者返回值不支持:</strong></li>
</ol>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155646903-1592391196.png" class="lazyload"></p>
<p><strong><font style="color: rgba(223, 42, 63, 1)">推荐</font></strong>: pojo record java基础类型 list map</p>
<ol start="2">
<li><strong>Tools参数无法自动推算问题</strong></li>
</ol>
<p><strong>问题:大模型无法将我们历史对话当中的信息,赋值转换到我们对应的 name 属性值当中。</strong></p>
<ul>
<li><em><font style="color: rgba(6, 8, 31, 0.88)">温度</font></em><font style="color: rgba(6, 8, 31, 0.88)">(即模型随机性)太低,AI可能缺失自由度变得比较拘谨(从一定程度可以解决, 但是不推荐)</font></li>
<li><font style="color: rgba(6, 8, 31, 0.88)">也可以通过描述 @ToolParam(description = "经度") 和@Tool(description)的 </font><strong><font style="color: rgba(6, 8, 31, 0.88)">description 的值</font></strong><font style="color: rgba(6, 8, 31, 0.88)"> 更加明确</font></li>
</ul>
<pre><code class="language-java">@Tool(description = "获取指定位置天气,根据位置自动推算经纬度")
public String getAirQuality(@ToolParam(description = "纬度") double latitude,
@ToolParam(description = "经度") double longitude) {
return "天晴";
}
</code></pre>
<ol start="3">
<li><strong><font style="color: rgba(6, 8, 31, 0.88)">大模型“强行适配”Tool参数的幻觉问题</font></strong></li>
</ol>
<p><strong><font style="color: rgba(6, 8, 31, 0.88)">问题:就是比如大模型将我们 的 “男,女”识别成了我们的姓名 name 赋值上了。</font></strong></p>
<ul>
<li><font style="color: rgba(6, 8, 31, 0.88)">加严参数描述与校验</font></li>
</ul>
<pre><code class="language-java">@Parameter(description = "真实人名(必填,必须为人的真实姓名,严禁用其他信息代替;如缺失请传null)")
String name
</code></pre>
<ul>
<li><strong><font style="color: rgba(6, 8, 31, 0.88)">后端代码加强校验和兜底保护,比较稳,靠谱的方案。</font></strong></li>
<li><font style="color: rgba(6, 8, 31, 0.88)">系统 Prompt 设定限制</font></li>
</ul>
<pre><code class="language-java">“严禁随意补全或猜测工具调用参数。
参数如缺失或语义不准,请不要补充或随意传递,请直接放弃本次工具调用。”
</code></pre>
<ul>
<li><font style="color: rgba(6, 8, 31, 0.88)">特别:高风险接口(如资金、风控等)</font><font style="color: rgba(6, 8, 31, 0.88)">tools方法</font><strong><font style="color: rgba(6, 8, 31, 0.88)">加强人工确认,多走一步校验。</font></strong></li>
</ul>
<ol start="4">
<li><strong><font style="color: rgba(6, 8, 31, 0.88)">工具暴露的接口名、方法名、参数名要可读、业务化</font></strong></li>
</ol>
<ul>
<li><font style="color: rgba(6, 8, 31, 0.88)">AI是“看”你的签名和注释来决定用不用工具的;</font></li>
<li><font style="color: rgba(6, 8, 31, 0.88)">尽量避免乱码、缩写等。</font></li>
</ul>
<ol start="5">
<li><strong><font style="color: rgba(6, 8, 31, 0.88)">方法参数数量不宜过多</font></strong></li>
</ol>
<ul>
<li><font style="color: rgba(6, 8, 31, 0.88)">建议每个工具方法尽量少于5个参数,否则AI提示会变复杂、出错率高。</font></li>
</ul>
<p><strong><font style="color: rgba(6, 8, 31, 0.88)">工具方法不适合做超耗时操作, 更长的耗时意味着用户延迟响应时间变长,</font></strong></p>
<p>性能优化 能异步处理就异步处理、 查询数据 redis</p>
<p><strong><font style="color: rgba(6, 8, 31, 0.88)">6. 关于Tools的权限控制</font></strong><br>
可以利用SpringSecurity限制</p>
<pre><code class="language-java">@Tool(description = "退票")
@PreAuthorize("hasRole('ADMIN')")
public String cancel(
// @ToolParam告诉大模型参数的描述
@ToolParam(description = "预定号,可以是纯数字") String ticketNumber,
@ToolParam(description = "真实人名(必填,必须为人的真实姓名,严禁用其他信息代替;如缺失请传null)") String name
) {
// 当前登录用户名
String username = SecurityContextHolder.getContext().getAuthentication().getName();
// 先查询 --->先校验
ticketService.cancel(ticketNumber, name);
return username+"退票成功!";
}
</code></pre>
<p>将tools和权限资源一起存储, 然后动态设置tools</p>
<pre><code class="language-java">.defaultToolCallbacks(toolService.getToolCallList(toolService))
</code></pre>
<p>根据当前用户读取当前用户所属角色的所有tools</p>
<pre><code class="language-java">public List<ToolCallback> getToolCallList(ToolService toolService) {
// 1 获取 Tools 处理的方法
Method method = ReflectionUtils.findMethod(ToolService.class, "cancel",String.class,String.class);
// 构建 Tool 定义信息 动态配置的方式 @Tool @ToolParam 都无效
ToolDefinition toolDefinition = ToolDefinition.builder()
.name("cancel")
.description("退票")// 对应@Tool注解当中的 description
// 对应@ToolParam() 注解
.inputSchema("""
{
"type": "object",
"properties": {
"ticketNumber": {
"type": "string",
"description": "预定号,可以是纯数字"
},
"name": {
"type": "string",
"description": "真实人名"
}
},
"required": ["ticketNumber", "name"]
}
""")
.build();
// 一个 ToolCallback 对应一个 tool
ToolCallback toolCallback = MethodToolCallback.builder()
.toolDefinition(toolDefinition) // 将对应的 toolDefinition = @ToolParam 传入
.toolMethod(method)// method = @Tools 配置
.toolObject(toolService) // 不能自己 new ,自己 new 的无法解析依赖注入
.build();
return List.of(toolCallback);
}
</code></pre>
<ol start="7">
<li><strong><font style="color: rgba(6, 8, 31, 0.88)">tools过多导致AI出现选择困难证 </font></strong></li>
</ol>
<p>问题:<br>
a. token上限<br>
b. 选择困难证<br>
tools的描述作用 保存 向量数据库。<br>
实现方式:</p>
<ol>
<li>把所有的tools描述信息存入到向量数据库,做相似性检索。</li>
<li>每次对话的时候根据当前对话信息检索到相似的tools(RAG)</li>
<li>然后动态设置tools</li>
</ol>
<h1 id="最后">最后:</h1>
<blockquote>
<p>“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”</p>
<p><img alt="在这里插入图片描述" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929155646893-1984292460.gif" class="lazyload"></p>
</blockquote><br><br>
来源:https://www.cnblogs.com/TheMagicalRainbowSea/p/19118941
頁:
[1]