[Corner项目实战]Spring Boot + LangChain4j Tool Calling实战:让AI自动选择推荐策略

Corner项目实战 Spring Boot + LangChain4j Tool Calling实战:让AI自动选择推荐策略

本期聚焦内容

  • 问题场景:如何让 AI 根据用户数据量,自动选择不同推荐策略
  • 系统提示词设计:展示那三条判断规则(优先收藏+情绪 → 官方库 → 联网搜索)
  • Tool 定义方式:如何让模型能"调用"你的 Java 方法
  • 简化代码示例:Tool 的定义、注册、调用流程

适合读者:想了解 LangChain4j 中 Tool 如何实际落地的人。

问题场景

在学习、制作Corner项目中,前端需要根据用户历史记录的多少,调用不同推荐接口,导致逻辑复杂

于是,我结合Langchain的工具Tool,根据用户上下文,自主判断调用哪个Tool

选择原因

维度 Tool Calling(Langchain4j) 传统 if-else 硬编码
可扩展性 新增推荐策略 = 新增一个 @Tool 方法,零修改调用方 新增策略需要改 Controller + Service + 前端
灵活性 AI 根据语义自主组合调用多个 Tool,甚至决定调用顺序 逻辑在代码中写死,无法动态调整
解耦程度 前端只发一条消息,后端 AI 模型做路由决策 前端必须知道后端有多少种推荐策略
维护成本 策略之间彼此独立,互不干扰 一个 if-else 分支出错可能影响整个链路
多模态融合 AI 可自然融合历史 + 位置 + 情绪多源信息 需要手写融合逻辑,代码臃肿

三层tool的定义

围绕目前的核心业务,设计了三个推荐Tool:

Tool 名称 触发场景 数据来源 返回内容
queryUserHistoryTool 用户有历史出行记录 UserPlaceMemory 表 + Redis 去过的地方、频率、情绪标签
queryOfficialPlacesTool 用户给定城市/区域但无历史 PlaceEmotionLibrary 官方收录的景点 + 情绪匹配
searchOnlineTool 用户问实时/热门/未收录地点 百度地图 API / 在线搜索 实时 POI + 路线信息

  • 当前在线搜索实在是太多问题:查询输出内容乱码 内容不准确等问题降级成LLM大模型应用

三、代码实现

3.1定义Tool接口与实现

文件:RecommendAITools

java 复制代码
@Component
public class RecommendAITools {
@Resource
private UserPlaceMemoryRepository memoryRepo;
@Resource
private PlaceService placeService;
@Resource
private BaiduMapService baiduMapService;

/**
 * Tool 1:查询用户历史足迹
 * AI 判定用户有历史记录时自动调用
 */
@Tool(name = "queryUserHistoryTool", description = """
    查询用户的历史出行记录和偏好。
    当用户提到去过某地、或当前用户有历史足迹数据时调用此工具。
    返回:用户去过的地点列表、访问频率、当时的情绪标签
    """)
public List<UserHistory> queryUserHistory(Long userId) {
    // 从 UserPlaceMemory 表查询用户历史足迹
    List<UserPlaceMemory> memories = memoryRepo.findByUserIdOrderByVisitCountDesc(userId);
    if (memories.isEmpty()) {
        return Collections.emptyList();
    }
    return memories.stream()
        .map(m -> new UserHistory(m.getPlaceName(), m.getVisitCount(), m.getEmotionTag()))
        .limit(5)  // ⚠️ 截断防止 Token 超限
        .toList();
}

/**
 * Tool 2:查询官方收录景点
 * AI 需要推荐目的地但用户无历史记录时调用
 */
@Tool(name = "queryOfficialPlacesTool", description = """
    查询系统中官方收录的推荐景点。
    当用户询问某个城市的推荐去处、或用户没有历史记录需要推荐时调用。
    参数 city:城市名称,如"北京"
    返回:该城市的官方景点列表及相关情绪标签
    """)
public List<PlaceCard> queryOfficialPlaces(String city) {
    // 从 PlaceEmotionLibrary 按城市查询
    return placeService.getPlacesByCity(city).stream()
        .map(p -> new PlaceCard(p.getName(), p.getEmotionTags(), p.getSummary()))
        .limit(8)  // ⚠️ 截断
        .toList();
}

/**
 * Tool 3:在线搜索实时地点(百度地图)
 * AI 需要实时/热门/未收录地点时调用
 */
@Tool(name = "searchOnlineTool", description = """
    在线搜索实时地点信息(调用百度地图 API)。
    当用户询问实时路线、热门景点、或官方未收录的地点时调用。
    参数 query:搜索关键词
    参数 location:位置坐标或城市名
    返回:实时搜索结果列表
    """)
public List<PlaceCard> searchOnline(String query, String location) {
    // 调用百度地图 POI 搜索
    return baiduMapService.searchPoi(query, location).stream()
        .map(p -> new PlaceCard(p.getName(), null, p.getAddress()))
        .limit(5)  // ⚠️ 关键:截断防止Token超限
        .toList();
	}
}

3.2@AiService接口定义+System Prompt引导

文件RecommendAIService.java

java 复制代码
@AiService  // Langchain4j注解
public interface RecommendAIService {
/**
 * 统一推荐接口:
 * - message: 用户自然语言输入(如"推荐一个适合散心的地方")
 * - memoryId: 用户 ID,用于对话记忆 + 查询历史
 * 
 * AI 模型会根据 SystemPrompt 的引导,自主决定调用哪个 Tool
 */
@SystemPrompt("""
    你是 Corner 旅行助手的推荐引擎。你的核心职责是根据用户上下文,自主选择合适的工具获取信息,然后给出个性化推荐。

    ## 工具调用决策规则(按优先级):
    1. **先查用户历史**:调用 queryUserHistoryTool 获取用户去过的地方。如果用户历史丰富(≥3条),优先推荐用户未去过但情绪匹配的新地点。
    2. **历史不足查官方库**:如果用户历史为空或不足,调用 queryOfficialPlacesTool 获取官方推荐景点。
    3. **实时需求在线查**:如果用户明确要求"实时""热门""最新",或官方库没有匹配结果,调用 searchOnlineTool。
    4. **多源融合**:允许同时调用多个工具(如查历史 + 查官方),将结果融合后给出综合推荐。

    ## 回答原则:
    - 每次推荐给出 3-5 个地点,附上推荐理由
    - 如果用户有历史记录,对比"之前去过"和"这次推荐"的差异
    - 语气温暖、个性化,像老朋友推荐地方一样
    """)
@UserMessage("""
    用户请求:{message}
    请根据用户上下文自主决策调用合适的工具,生成个性化推荐。
    """)
Flux<String> chat(@UserMessage String message, @MemoryId String memoryId);
}

3.3 Controller层------收敛成一个统一接口

文件RecommendController.java

java 复制代码
@RestController
@RequestMapping("/api/recommend")
public class RecommendController {
@Resource
private RecommendService recommendService;

/**
 * 统一推荐接口(取代之前多个推荐接口)
 * 
 * 前端不再需要判断用户有没有历史记录,
 * 只需将要推荐的自然语言描述发过来即可。
 */
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(@RequestBody @Valid RecommendRequest request) {
    // 直接转发给 AI Service,由 AI 自己决定调哪个 Tool
    return recommendService.chat(request.getMessage(), request.getUserId());
}
}

文件RecommendServiceImpl.java

java 复制代码
@Service
public class RecommendServiceImpl implements RecommendService {
@Resource
private RecommendAIService recommendAIService;

@Override
public Flux<String> chat(String message, Long userId) {
    // memoryId 传入 userId,AI 服务会自动关联对话记忆
    return recommendAIService.chat(message, String.valueOf(userId));
}
}

微总结

场景 原因
决策链路不固定,依赖上下文 AI 自主判断比硬编码更灵活
多数据源需要融合 AI 天然擅长多源信息融合
需求频繁变化/新增策略 新增一个 @Tool 方法即可,零改动
希望自然语言交互 Tool Calling + LLM = 对话式交互

四、踩坑与解决

模型不按预期调用Tool

有时用户说"推荐被禁好玩的地方",AI没有调用queryOfficialPlacesTool,而是出现凭空捏造景点

解决->提示词调优

"当用户提到城市名时,必须调用 queryOfficialPlacesTool 获取数据,

禁止凭空编造景点信息。如果官方库没有结果,再调用 searchOnlineTool 补充。"


  • 使用强约束词
  • 同时给出反例约束
  • 设定一个优先级:先查询什么,再查询什么

Tool返回数据过多导致Token超出限制

一个用户历史记录有200多条,Tool返回全部数据->Token额度用尽->AI回答被截断

解决->结果截断+摘要策略

在项目中通过窗口函数限制只返回前5条用户信息

java 复制代码
// 方案 A:限制返回条数(推荐)
List<UserHistory> histories = memories.stream()
    .limit(5)   // 只取 Top-5
    .toList();

  • 一般情况下返回的元数据不要超过2000tokens,在Corner项目中我限定了8-10条

多个Tool结果如何融合成最终回答

AI会出现同时调用queryUserHistoryToolqueryOfficialPlacesTool,但是回答时只用了其中一个,另一个被忽略

解决->在SystemPrompt中明确融合策略:

java 复制代码
@SystemPrompt("""
    ## 多源融合规则(重要):
    当你调用了多个工具时,必须按以下方式融合:
    1. 用 queryUserHistoryTool 的结果排除用户去过的地方
    2. 用 queryOfficialPlacesTool 的结果作为推荐候选
    3. 如果用户情绪标签匹配,优先推荐匹配项
    4. 最终回答中注明"根据你的历史足迹,为你推荐..."
    """)

Corner实际做法:让AI成为融合中枢------Tool只负责获取数据,AI负责理解、对比、筛选、生成推荐理由,不再写额外的Java融合代码

Tool被重复调用导致API费用翻倍

用户发一条消息,日志显示同一个Tool被调用了3次。

根因:LangChain4j在某些版本中,如果Tool返回结果不符合模型的预期(比如返回空列表),模型会尝试重新调用。

解决 :在Tool实现中,返回空结果时附带一句提示,如"暂无历史记录,请调用其他工具",让模型明白不是调用失败,而是确实没数据。

五、效果比对

维度 改造前(if-else) 改造后(Tool Calling)
用户输入 必须选择"无历史记录"按钮 直接说"推荐北京好玩的地方"
前端请求 调用 /api/recommend/byCity?city=北京 调用 /api/recommend/chat
后端逻辑 Controller 查是否登录 → 查历史 → 返回 AI 自动调用 queryOfficialPlacesTool
用户体验 僵硬,需要用户手动选择模式 自然对话,零学习成本

六、总结与适用场景

✅ 什么时候该用 Tool Calling?

场景 原因
决策链路不固定,依赖上下文 AI 自主判断比硬编码更灵活
多数据源需要融合 AI 天然擅长多源信息融合
需求频繁变化/新增策略 新增一个 @Tool 方法即可,零改动
希望自然语言交互 Tool Calling + LLM = 对话式交互

❌ 什么时候该用传统 if-else?

场景 原因
决策逻辑确定且简单(如 if userId == null) if-else 更轻量、延迟更低
对延迟极度敏感(< 100ms) LLM 推理有延迟,不适合高频调用
数据源固定(如只查一张表) 没有必要引入 AI 推理开销
预算有限 LLM API 调用有成本

Corner 项目的平衡之道

markdown 复制代码
Controller 层:统一入口(Tool Calling)
    ↓
AI Service 层:决策路由(Tool Calling)
    ↓
Tool 实现层:具体数据查询(if-else 处理边缘情况)
    ↓
数据源:MySQL / Redis / 百度地图 API
  • 上层(Controller → Service):用 Tool Calling,灵活决策
  • 下层(Tool 实现内部):用 if-else,处理数据查询的边界情况
  • 各司其职,发挥各自优势

最后

当然,Tool Calling 并非银弹。它会带来额外的 LLM 推理延迟(约 1-2 秒,处理不好会导致输出结果时间延长,影响用户体验),且提示词设计不当可能导致模型决策失误。因此建议:只在决策链路不固定的场景使用,核心链路仍然保留 if-else 兜底

📚 本文是Corner项目实战系列第2篇

相关推荐
字节跳动数据库1 小时前
文章分享——好代码 - 半点没用的话题
人工智能·程序员
xcLeigh1 小时前
数学之美:数字革命背后的底层逻辑
人工智能·数学·ai·数学原理·书籍·数学之美·绝对边界
Deepoch1 小时前
VLA多模态架构赋能无人机 拓展全域智能巡检应用
人工智能·机器人·无人机·具身模型·deepoc
机智的大狸子1 小时前
我给一个仓库系统写了个"会自己点界面"的 AI 测试 Agent,踩平了 WPF 自动化的所有坑
后端
未秃头的程序猿1 小时前
别再重复适配了!用MCP给AI配个"万能工具箱",Java项目接入新能力再也不改代码
后端·ai编程·mcp
羊羊小栈1 小时前
基于GraphRAG的医疗健康知识诊断系统(Neo4j_大语言模型)
人工智能·语言模型·毕业设计·知识图谱·创业创新·neo4j·大作业
Python私教1 小时前
002 Pandas 的流行原因
人工智能·后端·机器学习
Jul1en_1 小时前
【SpringCloud】SkyWalking 链路追踪知识详解及部署教程
java·后端·spring·spring cloud·skywalking
雷工笔记1 小时前
MES系列51-人防门行业 MES 质检分类体系
人工智能·分类·数据挖掘