ItineraryPlanAgent = "搜+算+审+修"闭环的行程规划专家——它从审批单或对话中获取需求,调 CLI/MCP 搜索候选交通酒店,让 LLM 对候选打偏好分后交给确定性计算引擎做组合排序,存 Redis 后委托审核子智能体五维检查,若有问题自动修复并重跑,最终给用户一份结构化可对比的方案。 它不做预订——预订由 BookingAgent 接手;它只管"出方案",并保证方案客观合规。
用户 ──→ MasterAgent(路由/意图识别)
│
├── ItineraryPlanAgent ←── 本文主角(搜索规划审核)
├── ItineraryManageAgent(差旅单/冲突检测)
└── BookingAgent(预订/退改)
MasterAgent 识别到用户意图为"规划行程"时,将任务路由到 ItineraryPlanAgent。该 agent 独立完成搜索 → 组合 → 审核 → 修复闭环,最终回传方案 Markdown 让 Master 呈现给用户。
核心能力概览
| 能力 | 实现方式 | 价值 |
|---|---|---|
| 多源候选搜索 | tuniu-cli Skill + weather/visa MCP | 一次拿齐机票/火车/酒店候选 |
| LLM 主观偏好评分 | Agent 在调工具前手动打分 → scores JSON | 把不可量化偏好变成可计算数值 |
| 客观组合排序 | plan_itinerary 工具(纯数学引擎) | 时间/价格/政策客观归一化,3 维加权排序 |
| 五维结构化审核 | itinerary_review_agent 子智能体 | 完整性/时间/出发地/预算/路线自动审查 |
| 审后修复闭环 | 最多 2 轮重跑 plan_itinerary | 去除违规候选、自动收敛到合规方案 |
| Plan-and-Execute + PlanNotebook | 框架级支持 | 降低 Token 浪费,带前端进度推送 |
| 长期记忆偏好召回 | LongTermMemory + retrieve/record | 跨会话复用用户差旅偏好 |
不管什么场景,ItineraryPlanAgent 干的事本质上是这条流水线,只是不同场景会跳过或重复其中某些步:
① 拿需求 → 查审批单 / 读对话 / 追问用户
② 拿约束 → 召回偏好(长期记忆) + 查差旅政策
③ 做计划 → create_plan(写进 PlanNotebook)
④ 搜候选 → 加载 tuniu-cli → 搜去程/返程/酒店
⑤ 打偏好分 → LLM 对每条候选打 0-100 分
⑥ 组合排序 → plan_itinerary(客观计算引擎,存 Redis)
⑦ 审核 → itinerary_review_agent(五维检查)
⑧ 判修复 → 有 FAIL/WARNING 就剔除违规候选,回到 ⑥(最多 2 轮)
⑨ 出方案 → 倒金字塔 Markdown,停下等用户确认
三个角色分工要记牢: - LLM(Agent 本体):负责主观——理解需求、判断偏好、打分、组织文案。 - plan_itinerary 工具:负责客观——组合、算钱、算时间、归一化、按权重排序。绝不解读偏好文本。 - 审核子 agent:负责把关——五维检查每套方案是否合规。
User Case

Agent 构建代码全解
// ItineraryPlanAgent.java —— @Configuration + @Scope("prototype")
// 关键模型与参数
private static final int MAX_ITERATIONS = 15;
@Bean(name = "itineraryPlanAgent")
@Scope("prototype") // 每次对话新建一个实例,避免状态串扰
public ReActAgent build() {
// ① 工具注册——并行执行开启
Toolkit toolkit = new Toolkit(ToolkitConfig.builder()
.parallel(true) // 允许同一轮推理中并发调多工具
.build());
toolkit.registration().tool(policyTools).apply(); // 差旅政策查询
toolkit.registration().tool(travelOrderReadTools).apply(); // 已审批差旅单
toolkit.registration().tool(bookingReadTools).apply(); // 外部预订记录
toolkit.registration().tool(queryUserInfoTools).apply(); // 用户信息+base城市
toolkit.registration().tool(apiKeyTools).apply(); // 第三方 API Key
toolkit.registration().tool(destinationLiveTools).apply(); // 天气+目的地新闻
toolkit.registration().tool(itineraryPlannerTool).apply(); // 核心组合排序引擎
toolkit.registration()
.mcpClient(weatherMcpClient)
.enableTools(weatherMcpEnabledTools).apply(); // 天气 MCP
toolkit.registration()
.mcpClient(oriznVisaMcpClient)
.enableTools(oriznMcpEnabledTools).apply(); // 签证 MCP
// ② 子智能体作为工具——委托审核
toolkit.registration()
.subAgent((SubAgentProvider<ReActAgent>) () ->
applicationContext.getBean("itineraryReviewAgent", ReActAgent.class),
SubAgentConfig.builder()
.toolName("itinerary_review_agent")
.description("委托行程审核子智能体对行程方案进行五维结构化审核")
.forwardEvents(false) // 不把子 agent 事件冒泡到父
.build())
.apply();
// ③ Skill(tuniu-cli 搜索)
SkillBox skillBox = buildPlanningSkillBox(toolkit);
// ④ PlanNotebook —— Plan-and-Execute 核心组件
PlanNotebook planNotebook = PlanNotebook.builder()
.needUserConfirm(false) // 自动执行,无需等用户逐步确认
.build();
// 注册变化钩子:步骤状态变更时实时推送前端
if (capturedSessionId != null) {
planNotebook.addChangeHook("plan_progress_notifier",
(nb, plan) -> progressNotifierHook.sendPlanUpdate(capturedSessionId, plan));
}
// ⑤ AutoContextMemory —— 超长对话自动压缩
AutoContextMemory memory = AgentMemoryFactory.create(stableModel);
// ⑥ 组装 ReActAgent
ReActAgent agent = ReActAgent.builder()
.name("ItineraryPlanAgent")
.model(strongModelWithThinking) // qwen3.7-max + enableThinking
.memory(memory)
.longTermMemory(longTermMemory) // 跨会话偏好记忆
.longTermMemoryMode(LongTermMemoryMode.AGENT_CONTROL) // Agent 自主决定何时读写
.toolkit(toolkit)
.toolExecutionContext(createToolCtx()) // 注入 sessionCtx(userId 等透明参数)
.skillBox(skillBox)
.hooks(List.of(
dynamicTimeInjectionHook, // 优先级 1:稳定前缀 + 动态时间
new AutoContextHook(), // 上下文超长时自动压缩
new PlanAwareThinkingHook(planNotebook, 2048), // 按阶段开关深度思考
cliResultCompressHook, // CLI 结果去重压缩
flightApiKeyHook, // 航班 API Key 注入
tuniuApiKeyHook, // 途牛 API Key 注入
rghUserIsolationHook, // 用户隔离
skillContentCollapseHook, // Skill 内容折叠
sessionPersistenceHook, // 会话持久化
executionLoggerHook, // 执行日志
toolCircuitBreakerHook, // 工具熔断器
progressNotifierHook, // 前端进度推送
activeAgentPersistenceHook, // 活跃 agent 记录
new PendingToolRecoveryHook() // 断点续跑恢复
))
.planNotebook(planNotebook)
.toolExecutionConfig(getToolConfig()) // 工具超时 1min/重试 3 次
.sysPrompt(PromptLoader.loadStatic("itinerary-plan-agent-system.md"))
.maxIters(MAX_ITERATIONS) // 最多 15 轮 ReAct
.generateOptions(getEnableThinkingGenerateOptions(2048)) // 缓存 + 思考预算
.build();
return agent;
}
为什么 MAX_ITERATIONS = 15?
行程规划是最复杂的任务,一次完整闭环的典型调用序列:
查审批单 → 查偏好(LTM) → 查政策 → 加载 Skill → 搜交通 → 搜酒店
→ 打分 → plan_itinerary → itinerary_review_agent → 判断修复
→ [可选: 重跑 plan_itinerary → 重审核] → 输出 Markdown
即使不修复也需 ~10 轮工具调用,留 15 轮可覆盖 2 次修复。
为什么 parallel(true)?
查审批单、查偏好、查政策之间无数据依赖,LLM 一次推理同时产出多个 tool_call 后可并发执行。Agent 搜去程和搜酒店也可以在同一轮并发,显著缩短端到端延时。
Plan-and-Execute
✅为什么行程规划智能体要用Plan-and-Exucute?
帮一个出差员工规划一趟"上海到杭州,周五去周日回"的行程,听起来简单。但拆开来看,Agent 至少要完成以下事情:查已审批的差旅单获取约束、召回用户历史偏好、查差旅政策拿到报销标准、搜索去程航班/高铁、搜索返程航班/高铁、搜索酒店、对候选做 LLMentor
核心工具链详解
plan_itinerary——客观组合排序引擎
✅为什么行程方案设计不用Skill要用工具?
整个行程规划流程分为两个阶段,恰好对应使用了2种能力: 数据收集天然适合 Skill:调用外部 API 本质就是按照文档拼接命令参数,LLM 擅长理解"搜索上海到杭州的航班"然后映射到正确的 CLI 命令。 但方案计算为什么不能也用 Sk LLMentor
✅行程方案设计工具设计细节
plan_itinerary,是行程规划的"计算大脑",承担了最核心的数学逻辑。 设计哲学 LLM 负责"主观"(偏好判断、打分) 工具负责"客观"(组合、归一化、排序、存储) 这种 LLM/Tool 职责分离是关键设计决策:如果让 LL LLMentor
为什么偏好权重 0.5 最高?
差旅场景中,用户偏好(靠窗/直飞/早班/免费退改)对满意度的影响往往大于几十块钱的差价或半小时的时间差,差旅场景对语言价格并不算敏感,只要在预算范围内即可。 0.5 的权重确保偏好强匹配的方案不会被纯价格/时间压下去,同时 0.3 + 0.2 保留了客观指标的话语权。
itinerary_review_agent——五维结构化审核
审核不是由一个工具完成的,而是委托给一个完整的子智能体:
✅为什么行程审核不用Skill要用子智能体?
我们的项目中,在行程规划后,我们需要对行程进行审核,检查时间是否正确,目的地是否正确, 是否符合差旅政策等等。 这个审核的节点,有很多实现方式,比如直接写在行程规划的智能体的prompt中,比如做成一个skill让行程规划智能体执行,比如做 LLMentor
Redis 跨工具中转
✅ReviewAgent如何获取到PlanAgent的规划结果?
ReviewAgent和PlanAgent之间需要传递方案,但是我们不是通过参数传递,而是通过 Redis 中转。PlanAgent 把规划结果写进 Redis,ReviewAgent 从同一个 key 读出来。(如果不考虑集群部署,可以考 LLMentor
系统提示词设计要点
系统提示词(itinerary-plan-agent-system.md)共 177 行,核心设计理念:
信息获取优先级
APPROVED 差旅单(最权威)> 当前对话上下文 > 追问用户 Agent 拿到用户请求后,第一件事是查有没有已审批的差旅单。有则直接用,省去多轮确认。
闭环流程强制约束
提示词用"必走/不可跳过/严禁"等强约束词确保流程不被 LLM 跳过:
多目标方案 + 审核 + 修复闭环(必走,不可跳过)
严禁跳过工具、直接在文案里虚构"修复版"方案
方案输出后必须停止,等待用户确认(最高优先级约束)
这些约束解决了 LLM 最常犯的三类错误:跳过审核直接输出、编造修复方案、自行替用户决策。
Skill 懒加载规则
搜索类 skill(如 tuniu-cli)必须在本步骤开始时才加载,
严禁在最初查审批单/常驻地/偏好/政策那一批调用里提前加载。
原因:技能正文加载后若长时间不用会被上下文折叠,
等到真正搜索时说明已丢失。
这是实际运行中发现的坑——过早加载 Skill 内容会被 AutoContextMemory 压缩折叠,等真正需要时 LLM 已"忘记"用法。
时间处理规则
详尽覆盖了"明天/后天/下周X/本周末/月底/下个月X号"等所有相对时间表述的转换规则,避免 LLM 推算星期出错。配合 DynamicTimeInjectionHook 注入的实时时间使用。
输出格式强约束
🏆 推荐方案(置顶,80% 用户只看这里) → 📋 方案对比表 → 🔍 方案明细 → 🧭 Agent 为你做了什么(评估足迹) → 👉 下一步 禁止暴露内部工具名、函数名、Redis key 等技术细节——面向用户的输出必须全中文自然语言。
长期记忆与偏好系统
我们给PlanAgent提供了长期记忆的工具:
.longTermMemory(longTermMemory)
.longTermMemoryMode(LongTermMemoryMode.AGENT_CONTROL)
AGENT_CONTROL 模式意味着 Agent 自主决定何时读写长期记忆,框架不自动触发。系统提示词定义了具体触发条件: 何时记录:用户说"以后都选高铁一等座""只住华住"等偏好表达时,调 record_to_memory 保存。 何时检索:规划前主动调 retrieve_from_memory 召回历史偏好,作为 plan_itinerary 的 preferences 参数和 LLM 打分依据。 何时不操作:闲聊、一次性事实查询、不确定是否为偏好的表述。 这确保了跨会话的偏好积累——用户说一次"我不坐红眼航班",以后每次规划都会自动避开。
Hook 链与执行顺序
ItineraryPlanAgent 注册了 14 个 Hook,按优先级排列: | 优先级 | Hook | 作用 | | --- | --- | --- | | 1 | DynamicTimeInjectionHook | 注入当前时间(保持 system prompt 前缀稳定以利用缓存) | | — | AutoContextHook | 上下文超 128K 时自动压缩旧消息 | | — | PlanAwareThinkingHook | 按 Plan/Execute/Summary 阶段切换 thinking | | 4 | CliResultCompressHook | CLI 返回结果去重+删冗余字段 | | — | FlightApiKeyHook | 航班 API Key 动态注入 | | — | TuniuApiKeyHook | 途牛 API Key 动态注入 | | — | RghUserIsolationHook | 用户隔离 | | — | SkillContentCollapseHook | 长时间未用的 Skill 内容折叠 | | — | SessionPersistenceHook | 会话状态持久化 | | — | ExecutionLoggerHook | 执行日志记录 | | 6 | ToolCircuitBreakerHook | Redis + Lua 熔断器 | | — | ProgressNotifierHook | WebSocket 前端进度推送 | | — | ActiveAgentPersistenceHook | 活跃 agent 记录 | | — | PendingToolRecoveryHook | 断点续跑恢复 |
其中 PlanAwareThinkingHook 和 ProgressNotifierHook 是行程规划独有的(行程管理 agent 没有),体现了规划任务的两个特殊需求:长推理任务需要降本、用户需要看到进度。