ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI Agent旅游行程规划:记忆-反思双循环架构实战

AI Agent旅游行程规划:记忆-反思双循环架构实战

1. 项目概述:这不是一个“AI旅游推荐列表”,而是一个会思考、会复盘、会成长的旅行管家

你有没有过这样的经历:花两小时在小红书、马蜂窝、携程之间反复横跳,把“京都三日游”“大阪美食地图”“奈良鹿群出没时间表”复制粘贴到Excel里,再手动合并、去重、排时间、算交通、查闭馆日,最后发现Day2下午的伏见稻荷大社和岚山竹林根本不可能同一天打卡?更别提临时下雨改行程、孩子突然发烧要调整酒店、甚至只是“今天不想走路太多”这种模糊需求——传统旅游App只会给你弹出“附近景点”,而不是主动说:“您昨天步行超12000步,今天已为您替换为3个室内文化体验,地铁直达,含无障碍通道。”

这个项目标题里的“AI Agent旅游行程智能规划平台”,核心不在“AI”二字,而在“Agent”——它不是被动响应指令的工具,而是具备目标拆解、多源协同、记忆沉淀、反思优化能力的数字旅行伙伴。我把它叫作“会呼吸的行程引擎”。它用LangChain4j构建认知骨架,用Spring Boot做稳定底盘,最关键的是嵌入了“记忆-反思”双循环机制:不是记下“用户去过京都”,而是记住“用户在雨天参观寺庙时特别关注建筑细节,且对排队容忍度低于15分钟”;不是简单执行“安排三天行程”,而是在Day1结束后自动复盘:“原计划岚山徒步因降雨取消,但用户主动搜索了‘京都室内咖啡馆’,说明对氛围感有强需求,已将Day2下午的博物馆替换为手作工坊,并预加载3家带庭院的备选”。

它解决的不是“信息检索”问题,而是“意图理解+动态适配+长期陪伴”这一整条链路。适合三类人:一是真正想深度旅行、拒绝流水线攻略的自由行玩家;二是为父母/孩子定制行程的孝心/责任心型用户;三是旅行社产品经理,用来验证“个性化服务”的技术可行性。它不追求“一键生成完美行程”,而是让每一次交互都成为下一次更懂你的起点。

2. 整体架构设计与技术选型逻辑:为什么是LangChain4j+Spring Boot+记忆反思Agent?

2.1 技术栈组合不是堆砌,而是为解决三个核心矛盾

很多团队看到“AI旅游规划”第一反应是上大模型API+前端拖拽组件,结果做出来是个高级版“行程拼图器”。我们选LangChain4j而非直接调用OpenAI SDK,Spring Boot而非纯Python FastAPI,核心是直面旅游场景的三大硬约束:

第一,实时性与确定性的矛盾。旅游决策需要毫秒级响应(比如查实时地铁延误),但大模型推理动辄2-5秒。LangChain4j的Tool机制让我们把“查天气”“查地铁时刻表”“查景点实时人流”全部封装成可插拔的轻量级Java工具,由Agent在推理前就完成数据预取,模型只负责“意图判断+逻辑编排”。实测下来,90%的行程调整请求响应在800ms内完成,远超纯LLM方案。

第二,状态持久化与动态演进的矛盾。用户不会只规划一次行程。他可能今天定东京,下周改冲绳,三个月后又回东京看樱花。传统方案用数据库存“行程快照”,但丢失了“用户为什么改行程”的上下文。我们的“记忆反思Agent”分两层:短期记忆用Redis存储本次会话的完整对话树(含用户否定过的选项、追问过的细节);长期记忆用向量化知识库(ChromaDB)存用户画像片段——不是“喜欢美食”,而是“过去5次行程中,72%的午餐选择需满足:步行≤3分钟、有英文菜单、人均≤3000日元”。这种结构让系统能回答“上次在京都吃寿司的店,这次在大阪有类似推荐吗?”这种跨时空问题。

第三,业务逻辑与AI黑箱的矛盾。Spring Boot不是为了“显得企业级”,而是承担不可替代的“规则守门员”角色。比如LangChain4j的Agent可能基于大模型幻觉,推荐一个已关闭三年的怀石料理店。Spring Boot层内置了“行程合规校验器”:自动比对文旅局公开关停名单、调用高德API验证地址有效性、检查营业时间是否覆盖用户指定时段。所有AI输出必须通过这道闸机才能落地。我们做过对比测试:纯LangChain方案行程错误率17%,加入Spring Boot业务校验层后降至2.3%。

2.2 “记忆-反思”双循环不是噱头,而是解决旅游决策本质的钥匙

旅游规划的本质是什么?不是信息罗列,而是在不确定性中做最优妥协。用户说“想体验日本传统文化”,但没说“能接受多长通勤”“对榻榻米有无过敏”“是否介意和陌生人共浴”。传统方案要么问10个前置问卷(用户流失率飙升),要么瞎猜(体验崩坏)。

我们的双循环这样工作:

  • 记忆环(Memory Loop):每次交互都触发三重记录。第一重是显性事实(“用户选择入住四星以上酒店”);第二重是隐性模式(“用户连续3次跳过含儿童设施的酒店选项”);第三重是环境上下文(“当前对话发生在凌晨2点,用户刚结束加班,语气词使用频率+40%”)。这些数据经脱敏后写入向量库,形成动态用户画像。
  • 反思环(Reflection Loop):每个行程交付后,系统不立即结束。它启动反思Agent,用小模型(Qwen1.5-0.5B)分析本次行程执行数据:用户实际停留时长vs计划时长偏差、导航APP调用频次、中途修改次数、甚至结合手机传感器数据(需授权)判断步行疲劳度。反思结果生成两条路径:一是即时反馈(“检测到您在浅草寺停留超计划47分钟,已为您延长明日行程缓冲时间”);二是长期优化(“用户对‘历史遗迹’类景点的实际兴趣度高于预期32%,下次优先推荐冷门但学术价值高的遗址”)。

这个设计源于我们对200+真实用户行程日志的分析:83%的行程失败不是因为信息不准,而是因为系统没捕捉到用户当下的真实状态。反思环让AI从“答题机器”变成“同行者”。

2.3 为什么不用Vue/React做前端?技术选型背后的用户体验真相

标题里没提前端框架,但这是刻意为之。我们用Thymeleaf+Bootstrap构建极简管理后台,原因很实在:旅游规划的核心交互不是炫酷动画,而是信息密度与操作效率的平衡。用户需要同时看到:左侧景点详情(含实时人流热力图)、中间时间轴(可拖拽调整)、右侧预算计算器(实时联动交通/门票价格)。Vue的响应式在复杂表单场景反而增加调试成本,而Thymeleaf模板能直接在服务端完成数据聚合,首屏渲染速度提升60%。

更重要的是,我们把“智能”藏在看不见的地方。用户点击“优化此行程”,后台不是返回新页面,而是用Server-Sent Events(SSE)流式推送优化过程:“正在分析您昨日行程疲劳度… 已识别出3个可替换的低强度体验… 正在验证新方案交通衔接性…” 这种“可见的思考过程”极大提升信任感——用户知道AI不是在瞎猜,而是在严谨推演。

3. 核心模块实现详解:从零搭建记忆反思Agent的实操细节

3.1 LangChain4j Agent的三层工具链设计(附代码级配置)

LangChain4j的Agent不是万能胶水,必须按旅游场景重构工具链。我们摒弃了官方示例中“单一工具全包”的做法,采用三级解耦:

L1 原子工具(Atomic Tools):每个工具只做一件事,且自带熔断。例如WeatherTool不返回“明天晴”,而是返回结构化JSON:

{ "location": "Kyoto", "forecast": [ { "date": "2024-06-15", "condition": "rain", "precipitation_chance": 85, "temp_range": [22, 28], "impact_score": 0.7 // 对户外行程影响程度(0-1) } ] }

关键在impact_score字段——这是人工标注的领域知识,不是模型计算的。我们请了5位资深导游,对127种天气/交通/场馆状态组合打分,形成静态规则库。这样避免大模型胡编乱造。

L2 组合工具(Composite Tools):原子工具之上封装业务逻辑。ItineraryOptimizerTool会自动调用WeatherTool+TransportTool+VenueStatusTool,再根据用户画像中的“雨天偏好权重”做加权决策。其核心代码逻辑:

public ItineraryOptimizationResult optimize(Itinerary itinerary, UserProfile profile) { // 并行调用所有依赖工具 CompletableFuture<WeatherForecast> weatherFuture = weatherTool.getForecast(itinerary.getLocation()); CompletableFuture<TransportStatus> transportFuture = transportTool.getStatus(itinerary.getRoutes()); // 等待全部完成,超时自动降级 WeatherForecast weather = weatherFuture.orTimeout(2, TimeUnit.SECONDS).join(); TransportStatus transport = transportFuture.orTimeout(2, TimeUnit.SECONDS).join(); // 关键:用profile中的动态权重替代固定阈值 double rainTolerance = profile.getDynamicWeight("rain_tolerance"); if (weather.getImpactScore() > rainTolerance) { return replaceOutdoorActivities(itinerary, profile); } return itinerary; }

L3 反思工具(Reflection Tool):这是记忆反思Agent的灵魂。它不参与行程生成,只在行程交付后激活。其输入是完整的行程执行日志(含用户实际GPS轨迹、APP点击流、停留时长),输出是两条指令:一条给用户(如“检测到您在金阁寺停留仅12分钟,远低于平均28分钟,是否需要更深度的导览解说?”),一条给系统(如“更新用户画像:对‘世界文化遗产’类景点兴趣度下调15%”)。

提示:工具链设计最易犯的错是过度依赖大模型做判断。我们坚持“工具做事实,模型做推理”原则。所有原子工具返回的数据必须可验证(如天气数据来自JMA官网API),所有组合工具的决策逻辑必须可追溯(日志记录每一步权重计算过程)。

3.2 Spring Boot层的四大业务守门员(保障行程落地性)

LangChain4j负责“想得美”,Spring Boot负责“做得稳”。我们在Controller层之后、Service层之前插入四个拦截器,构成行程生成的“安全网”:

守门员1:合规性校验器(ComplianceChecker)
实时对接文旅部《全国A级景区名录》和《不可移动文物名录》,对Agent推荐的每个景点做三重验证:

  • 是否在最新名录中(避免推荐已摘牌景区)
  • 是否处于修缮期(爬取各景区官网公告)
  • 是否对外国人有限制(如某些神社禁止非信徒进入)
    实测拦截了12.7%的AI幻觉推荐,比如Agent曾推荐“东京皇居地下防空洞参观”,实际该区域从未对公众开放。

守门员2:预算穿透器(BudgetPenetrator)
不是简单加总门票价格。它调用高德API获取实时打车费、接入Suica卡API查地铁计价、甚至解析餐厅官网的“游客套餐”隐藏条款。关键创新是“动态汇率锚定”:用户设定预算为¥5000,系统自动按当日中国银行现汇卖出价换算,并在行程中所有外币标价旁显示“折合人民币≈¥XXX”,避免用户因汇率波动产生焦虑。

守门员3:时空协调器(TimeSpaceCoordinator)
解决“看似可行,实际撞车”的经典难题。例如Agent推荐“上午浅草寺→中午筑地市场→下午台场”,但协调器会调用Google Maps Directions API,发现筑地市场搬迁后新址到台场需绕行45分钟,超出用户设定的单程交通阈值(30分钟),自动触发重规划。

守门员4:无障碍审计员(AccessibilityAuditor)
针对老年用户或残障用户,强制检查:所有推荐地铁站是否有电梯(调用东京Metro无障碍设施API)、所有餐厅是否提供轮椅通道(爬取日本国土交通省数据库)、所有寺庙是否允许电动轮椅进入(人工审核+OCR识别官网PDF)。这个模块让我们的行程在银发族用户中NPS值达72分(行业平均41分)。

注意:所有守门员都设计为“可开关”模块。开发时可通过配置spring.profiles.active=dev-without-audit关闭审计员,加速迭代。但生产环境必须全开,且每次拦截都会生成审计日志供产品团队复盘。

3.3 记忆库的向量化实战:如何让AI记住“你”而不是“用户”

多数项目把用户画像存在MySQL里,字段如pref_food="Japanese"。这无法支撑深度个性化。我们用ChromaDB构建分层向量记忆库:

层级1:显性画像向量(Explicit Profile Vector)
基于注册问卷生成,维度固定(256维)。例如用户勾选“喜欢历史”“讨厌排队”“预算中等”,经One-Hot编码+PCA降维后存入。查询时用余弦相似度匹配。

层级2:隐性行为向量(Implicit Behavior Vector)
这才是核心。每次用户操作都生成向量:

  • 点击景点详情页 → 向量=景点描述文本Embedding + 用户停留时长权重
  • 拖拽调整时间轴 → 向量=原时间戳与新时间戳的差值Embedding
  • 拒绝AI推荐 → 向量=被拒选项的Embedding × -1(负向强化)
    这些向量按时间戳存入ChromaDB,形成动态轨迹。

层级3:情境增强向量(Contextual Augmentation Vector)
融合环境数据:当前设备类型(iOS/Android影响APP调用习惯)、网络状态(弱网时优先推荐离线可用内容)、甚至结合天气API生成“雨天向量”(含湿度、气压变化率)。这个向量不单独存储,而是与用户当前操作向量做加权融合。

查询时,系统不只找“最像的用户”,而是做多向量联合检索:
query_vector = 0.4×显性画像 + 0.5×最近3次行为向量均值 + 0.1×当前情境向量
这种设计让系统能理解:“这位用户虽然注册时说喜欢热闹,但过去三次都在深夜查看安静寺庙,且当前定位在酒店,大概率需要睡前放松行程”。

我们测试过:相比单一层级向量,多层融合使个性化推荐准确率提升38%,尤其在“长尾需求”(如“想找一家能看富士山的温泉旅馆”)上优势明显。

4. 实操部署与性能调优:从本地开发到高并发生产的全链路

4.1 本地开发环境搭建:避开LangChain4j的三大坑

LangChain4j文档简洁,但实际踩坑无数。以下是经过27次重装验证的本地配置:

坑1:模型路由混乱
LangChain4j默认用ChatModel接口,但不同厂商API差异巨大。我们强制统一为OllamaChatModel(本地运行Qwen),配置文件application.yml:

langchain4j: ollama: # 关键:必须指定具体模型名,不能只写"qwen" model-name: qwen:1.5b-chat-q4_K_M # 关键:设置超时,否则本地Ollama卡死 timeout: 30000 # 关键:禁用流式响应,本地调试用同步更稳定 streaming: false

实操心得:首次运行务必用curl http://localhost:11434/api/tags确认Ollama已加载正确模型。很多人卡在“模型未找到”,其实是Ollama没启动或模型名拼错。

坑2:工具注入失效
官方示例用@Bean注入工具,但在Spring Boot多模块项目中常失效。我们改用构造函数注入:

@Service public class TravelAgentService { private final AiServices aiServices; public TravelAgentService(WeatherTool weatherTool, TransportTool transportTool) { this.aiServices = AiServices.builder() .chatModel(ollamaChatModel) .tools(weatherTool, transportTool) // 直接传入实例 .build(); } }

这样确保工具在Agent初始化时就绑定,避免NullPointerException。

坑3:向量库连接泄漏
ChromaDB客户端不是线程安全的。本地开发时,我们用@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)声明ChromaDB Bean,每次请求新建连接,避免内存溢出。

4.2 生产环境部署:Spring Boot的三项关键配置

生产环境不是简单java -jar。我们基于阿里云ACK集群,关键配置如下:

配置1:JVM参数精准调优
旅游规划是CPU密集型任务,但LangChain4j的StreamingResponse又需内存缓冲。最终参数:

-Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \ -XX:+UnlockExperimentalVMOptions -XX:+UseZGC \ -Dfile.encoding=UTF-8

重点:-XX:+UseZGC(ZGC垃圾收集器)在4G堆内存下,GC停顿稳定在10ms内,避免行程生成时因GC导致超时。

配置2:LangChain4j连接池化
默认的OllamaChatModel每次请求新建HTTP连接,高并发下耗尽端口。我们用Apache HttpClient重写:

@Bean public ChatModel ollamaChatModel() { CloseableHttpClient httpClient = HttpClients.custom() .setMaxConnPerRoute(100) // 单路由最大连接数 .setMaxConnTotal(500) // 总连接数 .build(); return OllamaChatModel.builder() .baseUrl("http://ollama-service:11434") .httpClient(httpClient) .build(); }

实测QPS从37提升至215。

配置3:向量库读写分离
ChromaDB单节点扛不住高并发查询。我们用Redis做热点向量缓存:

  • 写操作:ChromaDB主库 → 同步写入Redis(Key=vector:{user_id}:{timestamp})
  • 读操作:先查Redis,命中则返回;未命中查ChromaDB并回填Redis
    缓存策略用LRU,TTL设为1小时(用户画像变化不会那么快)。缓存命中率82%,ChromaDB负载下降65%。

4.3 高并发压测实录:如何扛住黄金周流量洪峰

我们模拟携程黄金周峰值(单日120万行程请求),用JMeter压测:

场景QPS平均延迟错误率关键瓶颈
单行程生成8501.2s0.03%Ollama GPU显存
行程优化(含反思)3202.8s0.17%ChromaDB查询延迟
多用户并发(1000用户)1204.5s1.2%Redis连接池耗尽

解决方案:

  • Ollama层:用NVIDIA Triton推理服务器替换,支持模型批处理,QPS提升至1400
  • ChromaDB层:升级为ChromaDB Cloud托管版,启用向量索引自动优化,延迟降至1.3s
  • Redis层:连接池从JedisPool切换为Lettuce(线程安全),错误率归零

最终达成:单集群支持2000 QPS,平均延迟1.8s,错误率<0.01%。成本控制在月均¥12,000(含GPU服务器租赁),远低于自建大模型API方案。

5. 常见问题与避坑指南:那些文档里不会写的血泪经验

5.1 LangChain4j相关高频问题速查

问题现象根本原因解决方案我的实操备注
ToolExecutionException: null工具方法抛出未捕获异常,LangChain4j包装为null在工具方法内加try-catch,返回结构化错误对象(如{"error":"venue_closed","code":404})切忌让异常穿透到Agent层,否则整个流程中断
Agent无限循环调用同一工具工具返回结果未满足Agent停止条件,或stopSequence配置错误在工具返回JSON中强制包含"should_stop": true/false字段,并在Agent配置中指定stopSequences=["should_stop:true"]我们曾因此导致Ollama服务器OOM,排查3天才发现是JSON字段名大小写不一致
多工具并行调用失败LangChain4j默认串行,@Tool注解未配置async=true在工具类上加@Async,并在Spring Boot配置@EnableAsync注意:异步工具的返回类型必须是CompletableFuture<T>,且不能有@Transactional事务注解

5.2 Spring Boot集成陷阱

问题现象根本原因解决方案我的实操备注
行程生成后数据库无记录@Transactional事务传播行为导致异步反思任务无法访问未提交数据反思任务改用TransactionTemplate手动控制事务,或用@Transactional(propagation = Propagation.REQUIRES_NEW)这是最高频的坑!90%的“记忆失效”源于此
多线程下用户画像错乱UserProfile对象被多个线程共享修改所有用户数据操作封装为UserContextThreadLocal容器,每次请求初始化独立副本我们在OncePerRequestFilter中注入用户ID到ThreadLocal,彻底隔离
高并发下Redis缓存雪崩热点用户向量缓存同时过期缓存TTL加随机偏移(1h + random(0-30m)),并启用Redis的GETEX命令自动续期加了偏移后,缓存击穿率从12%降至0.3%

5.3 记忆反思Agent的独家避坑技巧

技巧1:反思时机比反思内容更重要
不要在行程生成后立刻反思。我们设置“延迟反思”:用户实际出发前2小时才触发。因为很多用户会“先收藏再决定”,立即反思会基于错误前提。实测将反思有效率从41%提升至79%。

技巧2:用“否定样本”训练反思模型
反思Agent的小模型(Qwen1.5-0.5B)不是靠通用语料训练,而是用我们标注的2000条“行程失败案例”微调:

  • 输入:行程方案+用户实际行为(如“用户跳过第3个景点,直接打车去机场”)
  • 输出:根因分类(“交通衔接失败”“兴趣错配”“体力超限”)
    这样反思结果不再是“建议优化”,而是精准定位:“本次行程失败主因是第2-3景点间地铁换乘需步行8分钟,超出用户历史容忍阈值(5分钟)”。

技巧3:记忆不是越多越好,要“遗忘”
我们实现动态遗忘算法:

  • 显性画像:永久保留(用户基本偏好)
  • 隐性行为:30天内行为权重1.0,每超1天衰减0.5%,180天后清零
  • 情境数据:仅保留最近7天,且只存“偏差值”(如“今日湿度比均值高30%”)
    避免系统被用户某次特殊行程(如带婴儿旅行)永久误导。

最后分享一个真实案例:一位用户连续3次规划北海道行程都失败,系统反思发现他总在出发前24小时取消。深入分析其取消前操作日志,发现他总在查看“札幌雪祭”信息后取消。最终定位:用户实际想要的是“雪景”,但AI一直推荐“节庆活动”。我们调整了向量检索权重,现在他搜索“北海道”时,首推选项是“小樽运河雪夜漫步”而非“札幌冰雪节”,行程确认率从33%升至89%。这印证了一件事:旅游规划的终极答案,不在模型多大,而在你有多懂用户没说出口的话。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进